From 855de936b26976989a519257473461190bee41e9 Mon Sep 17 00:00:00 2001 From: Alex McLain Date: Fri, 31 Oct 2025 14:09:03 -0700 Subject: [PATCH 1/2] Merge creates maps for nested keys --- lib/speck/validation_metadata/attribute.ex | 5 +++++ test/validation_metadata/attribute_test.exs | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/lib/speck/validation_metadata/attribute.ex b/lib/speck/validation_metadata/attribute.ex index 3877931..f8d5e08 100644 --- a/lib/speck/validation_metadata/attribute.ex +++ b/lib/speck/validation_metadata/attribute.ex @@ -31,6 +31,11 @@ defmodule Speck.ValidationMetadata.Attribute do %{path => value} end + defp merge(nil = _params, [attribute | path], value) + when not is_integer(attribute) do + %{attribute => merge(%{}, path, value)} + end + defp merge(params, [path], value) when is_map(params) do params |> to_key_strings() diff --git a/test/validation_metadata/attribute_test.exs b/test/validation_metadata/attribute_test.exs index ac4a17d..6e173a8 100644 --- a/test/validation_metadata/attribute_test.exs +++ b/test/validation_metadata/attribute_test.exs @@ -3,6 +3,15 @@ defmodule Speck.ValidationMetadata.Attribute.Test do alias Speck.ValidationMetadata.Attribute + test "merge adds nested maps that don't exist" do + attributes = [ + {["state", "reported", "serial"], :present, "sn1234"} + ] + + assert Attribute.merge(attributes, %{}) == + %{"state" => %{"reported" => %{"serial" => "sn1234"}}} + end + describe "use case" do test "can merge unknown attributes back into a device shadow" do shadow_reported = %{ From 64b1abaaa747de9bb6e5bfee40f5342d0166e6a6 Mon Sep 17 00:00:00 2001 From: Alex McLain Date: Fri, 31 Oct 2025 14:58:00 -0700 Subject: [PATCH 2/2] Add attribute merge strategies --- lib/speck/validation_metadata/attribute.ex | 45 ++++++++++++++------- test/validation_metadata/attribute_test.exs | 42 +++++++++++++++++++ 2 files changed, 73 insertions(+), 14 deletions(-) diff --git a/lib/speck/validation_metadata/attribute.ex b/lib/speck/validation_metadata/attribute.ex index f8d5e08..8de4783 100644 --- a/lib/speck/validation_metadata/attribute.ex +++ b/lib/speck/validation_metadata/attribute.ex @@ -19,33 +19,50 @@ defmodule Speck.ValidationMetadata.Attribute do @doc """ Merge the values from metadata attributes into a given map. + + ## Opts + - `:merge_strategy` - Determines whether a value in the params or attributes + takes priority when both are present. Defaults to `:param_priority`. """ - @spec merge(attributes :: [t], params :: map) :: map - def merge(attributes, params) do + @spec merge( + attributes :: [t], + params :: map, + opts :: [merge_strategy: :param_priority | :attribute_priority] + ) :: map + def merge(attributes, params, opts \\ []) do + merge_strategy = Keyword.get(opts, :merge_strategy, :param_priority) + Enum.reduce(attributes, params, fn {path, _status, value}, acc -> - merge(acc, path, value) + merge(acc, path, value, merge_strategy) end) end - defp merge(nil = _params, [path], value) do + defp merge(nil = _params, [path], value, _strategy) do %{path => value} end - defp merge(nil = _params, [attribute | path], value) + defp merge(nil = _params, [attribute | path], value, strategy) when not is_integer(attribute) do - %{attribute => merge(%{}, path, value)} + %{attribute => merge(%{}, path, value, strategy)} end - defp merge(params, [path], value) when is_map(params) do - params - |> to_key_strings() - |> Map.put(path, value) + defp merge(params, [path], value, strategy) when is_map(params) do + params_with_key_strings = to_key_strings(params) + + skip_put = + strategy == :param_priority + && Map.has_key?(params_with_key_strings, path) + + case skip_put do + true -> params_with_key_strings + _ -> Map.put(params_with_key_strings, path, value) + end end - defp merge(params, [index | path], value) when is_integer(index) do + defp merge(params, [index | path], value, strategy) when is_integer(index) do params2 = params || [] item = Enum.at(params2, index) - new_item = merge(item, path, value) + new_item = merge(item, path, value, strategy) case item do nil -> List.insert_at(params2, -1, new_item) @@ -53,9 +70,9 @@ defmodule Speck.ValidationMetadata.Attribute do end end - defp merge(params, [attribute | path], value) do + defp merge(params, [attribute | path], value, strategy) do params2 = to_key_strings(params) - Map.put(params2, attribute, merge(params2[attribute], path, value)) + Map.put(params2, attribute, merge(params2[attribute], path, value, strategy)) end defp to_key_strings(map) do diff --git a/test/validation_metadata/attribute_test.exs b/test/validation_metadata/attribute_test.exs index 6e173a8..e652577 100644 --- a/test/validation_metadata/attribute_test.exs +++ b/test/validation_metadata/attribute_test.exs @@ -12,6 +12,48 @@ defmodule Speck.ValidationMetadata.Attribute.Test do %{"state" => %{"reported" => %{"serial" => "sn1234"}}} end + describe "merge strategy" do + test "attribute priority" do + params = %{"name" => "Test Device"} + + attributes = [ + {["name"], :present, "My Device"} + ] + + assert Attribute.merge(attributes, params, merge_strategy: :attribute_priority) == + %{"name" => "My Device"} + + params = %{"state" => %{"reported" => %{"name" => "Test Device"}}} + + attributes = [ + {["state", "reported", "name"], :present, "My Device"} + ] + + assert Attribute.merge(attributes, params, merge_strategy: :attribute_priority) == + %{"state" => %{"reported" => %{"name" => "My Device"}}} + end + + test "param priority" do + params = %{"name" => "Test Device"} + + attributes = [ + {["name"], :present, "My Device"} + ] + + assert Attribute.merge(attributes, params, merge_strategy: :param_priority) == + %{"name" => "Test Device"} + + params = %{"state" => %{"reported" => %{"name" => "Test Device"}}} + + attributes = [ + {["state", "reported", "name"], :present, "My Device"} + ] + + assert Attribute.merge(attributes, params, merge_strategy: :param_priority) == + %{"state" => %{"reported" => %{"name" => "Test Device"}}} + end + end + describe "use case" do test "can merge unknown attributes back into a device shadow" do shadow_reported = %{