1.0.0 - This version may not be safe as it has not been updated for a long time. Find out if your coding project uses this component and get notified of any reported security vulnerabilities with Meterian-X Open Source Security Platform
Maintain your licence declarations and avoid unwanted licences to protect your IP the way you intended.
MIT - MIT LicenseProvides functionality for "deep merging" maps and keyword lists in elixir, which is if during merging both values at the same key are maps/keyword lists merge them recursively. This is done via a protocol so can be extended for your own structs/data types if needbe.
iex> DeepMerge.deep_merge(%{a: 1, b: [x: 10, y: 9]}, %{b: [y: 20, z: 30], c: 4})
%{a: 1, b: [x: 10, y: 20, z: 30], c: 4}
This functionality can be useful for instance when merging a default configuration with a user supplied custom configuration:
DeepMerge.deep_merge(default_config, custom_config) # => merged configuration
Further features include:
I wanted this to be a feature of Elixir itself, however the proposal was rejected hence this library exists :)
Add :deep_merge
to your list of dependencies in mix.exs
:
def deps do
[
{:deep_merge, "~> 1.0"}
]
end
Using this library is quite simple (and you might also want to look at the hexdocs) - just pass two structures to be deep merged into DeepMerge.deep_merge/2
:
iex> DeepMerge.deep_merge(%{a: 1, b: %{x: 10, y: 9}}, %{b: %{y: 20, z: 30}, c: 4})
%{a: 1, b: %{x: 10, y: 20, z: 30}, c: 4}
iex> DeepMerge.deep_merge([a: 1, b: [x: 10, y: 9]], [b: [y: 20, z: 30], c: 4])
[a: 1, b: [x: 10, y: 20, z: 30], c: 4]
It is worth noting that structs are not deeply merged - not with each other and not with normal maps. This is because structs, while internally a map, are more like their own data types and therefore should not be deeply merged... unless you implement the protocol provided by this library for them.
What is merged and how is defined by implementing the DeepMerge.Resolver
protocol. This library implements it for Map
, List
and falls back to Any
(where the right hand side value/override is taken).
If you want your own struct to be deeply merged you can simply @derive
the protocol:
defmodule Derived do
@derive [DeepMerge.Resolver]
defstruct [:attrs]
end
If you want to change the deep merge for a custom struct you can do so. An example implementation might look like this if you want to deeply merge your struct but only against non nil
values (because all keys are always there) if you merge against the same struct (but still merge with maps):
defimpl DeepMerge.Resolver, for: MyStruct do
def resolve(original, override = %MyStruct{}, resolver) do
cleaned_override =
override
|> Map.from_struct()
|> Enum.reject(fn {_key, value} -> is_nil(value) end)
|> Map.new()
Map.merge(original, cleaned_override, resolver)
end
def resolve(original, override, resolver) when is_map(override) do
Map.merge(original, override, resolver)
end
end
In this implementation, MyStruct
structs are merged with other MyStruct
structs, omitting nil values. The arguments passed to resolve
are the original value (left hand side) and the override value (right hand side, which would normally replace the original). The third parameter is a resolver
function which you can pass to Map.merge/3
/Keyword.merge/3
to continue the deep merge.
There is another deep merge variant that is a bit like Map.merge/3
as it takes an additional function which you can use to alter the deep merge behavior:
iex> resolver = fn
...> (_, original, override) when is_list(original) and is_list(override) ->
...> override
...> (_, _original, _override) ->
...> DeepMerge.continue_deep_merge
...> end
iex> DeepMerge.deep_merge(%{a: %{b: 1}, c: [d: 1]},
...> %{a: %{z: 5}, c: [x: 0]}, resolver)
%{a: %{b: 1, z: 5}, c: [x: 0]}
This function is called for a given merge conflict with the key where it occurred and the two conflicting values. Whatever value is returned in this function is inserted at that point in the structure - unless DeepMerge.continue_deep_merge
is returned in which case the deep merge continues as normal.
When would you want to use this versus a protocol? The best use case I can think of is when you want to alter behavior for which a protocol is already implemented or if you care about specific keys.
In the example above the behavior is changed so that keyword lists are not deep_merged (if they were the result would contain c: [d: 1, x:0]
), but maps still are if that's what you are looking for.
Well not necessarily, no. There are very simple implementations for maps that use Map.merge/3.
There are subtle things that can be missed there though (and I missed the first time around):
This library takes care of those problems and will take care of further problems/edge cases should they appear so you can focus on your business logic.
At the same time it offers extension mechanisms through protocols and a function in deep_merge/3
. So, it should be adjustable to your use case and if not please open an issue :)
You can check out a benchmark and its results.
The TLDR; is this: In the sample it is about 30 times slower than Map.merge/2
- however, less than twice as slow as calling Map.merge/3
with simple overriding behaviour (same behaviour as Map.merge/2
). This is because Map.merge/2
is highly optimized, but we need to do much more than the Map.merge/3
sample in the benchmark so I think it's a very passable result. We're still talking about a couple of μs.
Unless you come with great feature ideas of course ;) So if you come here and there are no recent commits, don't worry - there are no known bugs or whatever. It's a small little library that does its job.
Copyright (c) 2016 Tobias Pfeiffer
This library is MIT licensed. See the LICENSE for details.