From 86d7dfaea49173cf22c6041ffd997e1c78136bcc Mon Sep 17 00:00:00 2001 From: NexPB <1289189+NexPB@users.noreply.github.com> Date: Fri, 2 Apr 2021 10:48:46 +0900 Subject: [PATCH 1/2] feature: implement NiceMaps.merge_values/3 --- lib/nice_maps.ex | 72 +++++++++++++++++++++++++++++++++++++++++ test/nice_maps_test.exs | 49 ++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+) diff --git a/lib/nice_maps.ex b/lib/nice_maps.ex index 0128e02..b2ab25e 100644 --- a/lib/nice_maps.ex +++ b/lib/nice_maps.ex @@ -224,4 +224,76 @@ 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"]} + + """ + @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, acc -> + Map.put_new(acc, key, apply_merge_fun(map1[key], map2[key], opts)) + end) + end + + defmodule MergeError do + defexception message: "Can't merge given values because of incompatible types." + 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(MergeError) end diff --git a/test/nice_maps_test.exs b/test/nice_maps_test.exs index 4089759..f2bd62e 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.MergeError, fn -> + NiceMaps.merge_values(acc, new) + end + end + end end From 5df38b9f82a7ed92503b3d7417fdfc66abc7d1e3 Mon Sep 17 00:00:00 2001 From: NexPB <1289189+NexPB@users.noreply.github.com> Date: Mon, 12 Apr 2021 17:16:43 +0900 Subject: [PATCH 2/2] feature: ability to define merge fn per key --- lib/errors/merge_error.ex | 18 ++++++++++++++++++ lib/nice_maps.ex | 39 ++++++++++++++++++++++++++++++++------- test/nice_maps_test.exs | 2 +- 3 files changed, 51 insertions(+), 8 deletions(-) create mode 100644 lib/errors/merge_error.ex 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 b2ab25e..440532c 100644 --- a/lib/nice_maps.ex +++ b/lib/nice_maps.ex @@ -245,18 +245,43 @@ defmodule NiceMaps do 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, acc -> - Map.put_new(acc, key, apply_merge_fun(map1[key], map2[key], opts)) - end) - end + 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]}}) - defmodule MergeError do - defexception message: "Can't merge given values because of incompatible types." + e -> + e + end + end) end defp apply_merge_fun(val1, val2, opts) do @@ -295,5 +320,5 @@ defmodule NiceMaps do do: val2 defp apply_merge_fun(_val1, _val2), - do: raise(MergeError) + do: raise(NiceMaps.Errors.MergeError) end diff --git a/test/nice_maps_test.exs b/test/nice_maps_test.exs index f2bd62e..4decf79 100644 --- a/test/nice_maps_test.exs +++ b/test/nice_maps_test.exs @@ -111,7 +111,7 @@ defmodule NiceMapsTest do acc = %{count: 1} new = %{count: 2} - assert_raise NiceMaps.MergeError, fn -> + assert_raise NiceMaps.Errors.MergeError, fn -> NiceMaps.merge_values(acc, new) end end