diff --git a/lib/errors/merge_error.ex b/lib/errors/merge_error.ex new file mode 100644 index 0000000..33bbc28 --- /dev/null +++ b/lib/errors/merge_error.ex @@ -0,0 +1,18 @@ +defmodule NiceMaps.Errors.MergeError do + defexception [:message] + + @impl true + def exception(%{key: key, values: {value1, value2}}) do + msg = """ + Can't merge given values for key `#{inspect(key)}`, values: + #{inspect(value1)}, + #{inspect(value2)} + """ + + %__MODULE__{message: msg} + end + + def exception(_) do + %__MODULE__{message: "Can't merge given values because of incompatible types."} + end +end diff --git a/lib/nice_maps.ex b/lib/nice_maps.ex index 0128e02..440532c 100644 --- a/lib/nice_maps.ex +++ b/lib/nice_maps.ex @@ -224,4 +224,101 @@ defmodule NiceMaps do key_type = Keyword.get(opts, :key_type) parse_key_type(key, key_type) end + + @doc """ + Merges the values of two given maps. + + ## Options + + - `:keys` (optional) only merge the given keys + - `:fun` (optional) merge handler annonymous function that accepts two arguments + + ## Examples + + iex> acc_requests = %{success: ["200", "200", "201"], failed: []} + iex> new_requests = %{success: ["200", "200"], failed: ["404"]} + iex> NiceMaps.merge_values(acc_requests, new_requests) + %{success: ["200", "200", "201", "200", "200"], failed: ["404"]} + + iex> acc_requests = %{success: ["200", "200", "201"], failed: []} + iex> new_requests = %{success: ["200", "200"], failed: ["404"]} + iex> NiceMaps.merge_values(acc_requests, new_requests, keys: [:success]) + %{success: ["200", "200", "201", "200", "200"]} + + iex> joiner_fn = fn v1, v2 -> (v1 ++ v2) |> Enum.join(",") end + iex> acc_requests = %{success: ["200", "200", "201"], failed: []} + iex> new_requests = %{success: ["200", "200"], failed: ["404"]} + iex> NiceMaps.merge_values(acc_requests, new_requests, fun: joiner_fn) + %{success: "200,200,201,200,200", failed: "404"} + + iex> acc_requests = %{success: ["200", "200", "201"], failed: ["404"]} + iex> new_requests = %{success: ["200", "200"], failed: ["404"]} + iex> NiceMaps.merge_values(acc_requests, new_requests, + ...> keys: [ + ...> :success, + ...> failed: fn v1, v2 -> (v1 ++ v2) |> Enum.join(",") end + ...> ] + ...> ) + %{success: ["200", "200", "201", "200", "200"], failed: "404,404" } + + """ + @spec merge_values(map(), map(), keyword()) :: map() + def merge_values(%{} = map1, %{} = map2, opts \\ []) do + keys = Keyword.get(opts, :keys, Map.keys(map1)) + + Enum.reduce(keys, %{}, fn + {key, fun}, acc when is_function(fun) -> + Map.put_new(acc, key, fun.(map1[key], map2[key])) + + key, acc when is_atom(key) -> + try do + Map.put_new(acc, key, apply_merge_fun(map1[key], map2[key], opts)) + rescue + # we catch the base error to provide more information afterwards + _ in NiceMaps.Errors.MergeError -> + raise(NiceMaps.Errors.MergeError, %{key: key, values: {map1[key], map2[key]}}) + + e -> + e + end + end) + end + + defp apply_merge_fun(val1, val2, opts) do + if fun = Keyword.get(opts, :fun) do + fun.(val1, val2) + else + apply_merge_fun(val1, val2) + end + end + + defp apply_merge_fun(val1, val2) when is_list(val1) and is_list(val2), + do: val1 ++ val2 + + defp apply_merge_fun(val1, val2) when is_list(val1) and is_nil(val2), + do: val1 + + defp apply_merge_fun(val1, val2) when is_nil(val1) and is_list(val2), + do: val2 + + defp apply_merge_fun(val1, val2) when is_map(val1) and is_map(val2), + do: Map.merge(val1, val2) + + defp apply_merge_fun(val1, val2) when is_map(val1) and is_nil(val2), + do: val1 + + defp apply_merge_fun(val1, val2) when is_nil(val1) and is_map(val2), + do: val2 + + defp apply_merge_fun(val1, val2) when is_bitstring(val1) and is_bitstring(val2), + do: val1 <> val2 + + defp apply_merge_fun(val1, val2) when is_bitstring(val1) and is_nil(val2), + do: val1 + + defp apply_merge_fun(val1, val2) when is_nil(val1) and is_bitstring(val2), + do: val2 + + defp apply_merge_fun(_val1, _val2), + do: raise(NiceMaps.Errors.MergeError) end diff --git a/test/nice_maps_test.exs b/test/nice_maps_test.exs index 4089759..4decf79 100644 --- a/test/nice_maps_test.exs +++ b/test/nice_maps_test.exs @@ -67,4 +67,53 @@ defmodule NiceMapsTest do refute Map.has_key?(map2, :__struct__) end end + + describe "NiceMaps.merge_values" do + test "[] values" do + acc = %{ + order_lines: [%{fulfillment_status: "partial"}] + } + + new = %{ + order_lines: [%{fulfillment_status: "fulfilled"}] + } + + assert %{ + order_lines: [%{fulfillment_status: "partial"}, %{fulfillment_status: "fulfilled"}] + } = NiceMaps.merge_values(acc, new) + end + + test "%{} values" do + acc = %{ + order: %{id: 12} + } + + new = %{ + order: %{id: 15, title: "new"} + } + + assert %{order: %{id: 15, title: "new"}} = NiceMaps.merge_values(acc, new) + end + + test "string values" do + acc = %{ + title: "old" + } + + new = %{ + title: "new" + } + + assert %{title: "oldnew"} = NiceMaps.merge_values(acc, new) + end + + test "number value" do + acc = %{count: 1} + new = %{count: 2} + + assert_raise NiceMaps.Errors.MergeError, fn -> + NiceMaps.merge_values(acc, new) + end + end + end end