From 743ad766c47967145921ee5d0b7f3e3b50e732f0 Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Fri, 24 Apr 2026 18:25:59 +0930 Subject: [PATCH 1/4] characteristic value may be array --- .../provider/components/characteristic.ex | 57 +++++++++++- lib/diffo/type/value.ex | 6 ++ test/provider/characteristic_test.exs | 88 ++++++++++++++++++- 3 files changed, 146 insertions(+), 5 deletions(-) diff --git a/lib/diffo/provider/components/characteristic.ex b/lib/diffo/provider/components/characteristic.ex index a9aba32..3f46f9d 100644 --- a/lib/diffo/provider/components/characteristic.ex +++ b/lib/diffo/provider/components/characteristic.ex @@ -34,11 +34,37 @@ defmodule Diffo.Provider.Characteristic do end jason do - pick [:name, :value] + pick [:name, :value, :values] + compact true end outstanding do - expect [:name, :value] + expect [:name] + + customize fn outstanding, expected, actual -> + key = if expected.is_array, do: :values, else: :value + expected_val = Map.get(expected, key) + + val_out = + if actual == nil do + expected_val + else + Outstanding.outstanding(expected_val, Map.get(actual, key)) + end + + cond do + val_out == nil -> + outstanding + + outstanding == nil -> + %{key => val_out} + |> Outstand.map_to_struct(Diffo.Provider.Characteristic) + |> Ash.Test.strip_metadata() + + true -> + Map.put(outstanding, key, val_out) + end + end end actions do @@ -46,7 +72,7 @@ defmodule Diffo.Provider.Characteristic do create :create do description "creates a characteristic" - accept [:name, :value, :type] + accept [:name, :value, :values, :is_array, :type] end read :read do @@ -71,7 +97,7 @@ defmodule Diffo.Provider.Characteristic do update :update do primary? true description "updates the characteristic value or instance, feature or relationship" - accept [:value] + accept [:value, :values, :is_array] argument :instance_id, :uuid argument :feature_id, :uuid argument :relationship_id, :uuid @@ -101,6 +127,20 @@ defmodule Diffo.Provider.Characteristic do public? true end + attribute :values, {:array, Diffo.Type.Value} do + description "the array of values of the characteristic" + constraints items: Diffo.Type.Value.subtype_constraints() + allow_nil? true + public? true + end + + attribute :is_array, :boolean do + description "true when this characteristic holds an array of values; defaults false" + default false + allow_nil? false + public? true + end + attribute :type, :atom do description "the type of the characteristic" allow_nil? false @@ -154,6 +194,10 @@ defmodule Diffo.Provider.Characteristic do validate present([:instance_id, :feature_id, :relationship_id], at_most: 1) do message "characteristic must be related to at most one of an instance, feature or relationship" end + + validate present([:value, :values], at_most: 1) do + message "characteristic must have at most one of value or values" + end end preparations do @@ -172,4 +216,9 @@ defmodule Diffo.Provider.Characteristic do """ def compare(%{name: name0}, %{name: name1}), do: Diffo.Util.compare(name0, name1) + + defimpl Diffo.Unwrap do + def unwrap(%{values: values}) when is_list(values), do: Diffo.Unwrap.unwrap(values) + def unwrap(%{value: value}), do: Diffo.Unwrap.unwrap(value) + end end diff --git a/lib/diffo/type/value.ex b/lib/diffo/type/value.ex index e3c8f5c..be97e86 100644 --- a/lib/diffo/type/value.ex +++ b/lib/diffo/type/value.ex @@ -92,6 +92,12 @@ defmodule Diffo.Type.Value do def handle_change(_old_value, nil, _constraints), do: {:ok, nil} def handle_change(old_value, new_value, constraints), do: super(old_value, new_value, constraints) + def handle_change_array(_old_values, nil, _constraints), do: {:ok, nil} + def handle_change_array(old_values, new_values, constraints), do: super(old_values, new_values, constraints) + + def prepare_change_array(_old_values, nil, _constraints), do: {:ok, nil} + def prepare_change_array(old_values, new_values, constraints), do: super(old_values, new_values, constraints) + def primitive(type, value), do: Diffo.Type.Primitive.wrap(type, value) def dynamic(%type{} = dynamic), do: dynamic(type, dynamic) diff --git a/test/provider/characteristic_test.exs b/test/provider/characteristic_test.exs index fcdc446..30f109e 100644 --- a/test/provider/characteristic_test.exs +++ b/test/provider/characteristic_test.exs @@ -206,8 +206,81 @@ defmodule Diffo.Provider.CharacteristicTest do end end + describe "Diffo.Provider create array Characteristics" do + test "create characteristic with values - success" do + characteristic = + Diffo.Provider.create_characteristic!(%{ + name: :ports, + values: [ + Value.primitive("integer", 1), + Value.primitive("integer", 2), + Value.primitive("integer", 3) + ], + is_array: true, + type: :instance + }) + + assert characteristic.is_array == true + assert Diffo.Unwrap.unwrap(characteristic) == [1, 2, 3] + end + + test "create characteristic with both value and values - failure" do + assert {:error, _} = + Diffo.Provider.create_characteristic(%{ + name: :bad, + value: Value.primitive("string", "x"), + values: [Value.primitive("string", "y")], + type: :instance + }) + end + end + + describe "Diffo.Provider update array Characteristics" do + test "update value characteristic to values (morphing) - success" do + characteristic = + Diffo.Provider.create_characteristic!(%{ + name: :ports, + value: Value.primitive("integer", 1), + type: :instance + }) + + updated = + Diffo.Provider.update_characteristic!(characteristic, %{ + value: nil, + values: [ + Value.primitive("integer", 1), + Value.primitive("integer", 2) + ], + is_array: true + }) + + assert updated.is_array == true + assert Diffo.Unwrap.unwrap(updated) == [1, 2] + end + + test "update values characteristic back to value (shrinking) - success" do + characteristic = + Diffo.Provider.create_characteristic!(%{ + name: :ports, + values: [Value.primitive("integer", 1), Value.primitive("integer", 2)], + is_array: true, + type: :instance + }) + + updated = + Diffo.Provider.update_characteristic!(characteristic, %{ + values: nil, + value: Value.primitive("integer", 1), + is_array: false + }) + + assert updated.is_array == false + assert Diffo.Unwrap.unwrap(updated) == 1 + end + end + describe "Diffo.Provider encode Characteristics" do - test "encode json - success" do + test "encode json value - success" do characteristic = Diffo.Provider.create_characteristic!(%{ name: :device, @@ -218,6 +291,19 @@ defmodule Diffo.Provider.CharacteristicTest do encoding = Jason.encode!(characteristic) assert encoding == "{\"name\":\"device\",\"value\":\"managed\"}" end + + test "encode json values - success" do + characteristic = + Diffo.Provider.create_characteristic!(%{ + name: :ports, + values: [Value.primitive("integer", 1), Value.primitive("integer", 2)], + is_array: true, + type: :instance + }) + + encoding = Jason.encode!(characteristic) + assert encoding == "{\"name\":\"ports\",\"values\":[1,2]}" + end end describe "Diffo.Provider outstanding Characteristics" do From 81a7d70126e692603a81792a8e83c460cb207c28 Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Fri, 24 Apr 2026 18:33:57 +0930 Subject: [PATCH 2/4] provider instance characteristic value may be array --- lib/diffo/provider/components/instance/extension.ex | 5 +++-- .../components/instance/extension/characteristic.ex | 11 +++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/lib/diffo/provider/components/instance/extension.ex b/lib/diffo/provider/components/instance/extension.ex index 7879295..18c6b73 100644 --- a/lib/diffo/provider/components/instance/extension.ex +++ b/lib/diffo/provider/components/instance/extension.ex @@ -81,9 +81,10 @@ defmodule Diffo.Provider.Instance.Extension do ], value_type: [ doc: """ - The optional type of the characteristic's value, an atom, may be a module name such as an Ash.TypedStruct + The type of the characteristic's value. An atom module name such as an Ash.TypedStruct for a scalar value, + or `{:array, module}` for an array of values of that type. """, - type: :atom + type: :any ] ] } diff --git a/lib/diffo/provider/components/instance/extension/characteristic.ex b/lib/diffo/provider/components/instance/extension/characteristic.ex index fac242d..ae0a6b8 100644 --- a/lib/diffo/provider/components/instance/extension/characteristic.ex +++ b/lib/diffo/provider/components/instance/extension/characteristic.ex @@ -48,9 +48,16 @@ defmodule Diffo.Provider.Instance.Characteristic do Enum.reduce_while(characteristics, [], fn %{name: name, value_type: value_type}, acc -> try do - value = Value.dynamic(struct(value_type)) + attrs = + case value_type do + {:array, _inner} -> + %{name: name, type: type, values: [], is_array: true} - case Provider.create_characteristic(%{name: name, type: type, value: value}) do + module -> + %{name: name, type: type, value: Value.dynamic(struct(module))} + end + + case Provider.create_characteristic(attrs) do {:ok, result} -> {:cont, [result | acc]} From 9d7b3a4e6a6a4da0816c563bc4634a8d9364ec50 Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Fri, 24 Apr 2026 18:39:59 +0930 Subject: [PATCH 3/4] provider feature characteristics --- .../provider/components/instance/extension/feature.ex | 11 +++++++++-- test/instance_extension/characteristic_test.exs | 9 +++++++++ test/instance_extension/feature_test.exs | 11 +++++++++++ test/support/resource/shelf.ex | 2 ++ 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/lib/diffo/provider/components/instance/extension/feature.ex b/lib/diffo/provider/components/instance/extension/feature.ex index 0a6e1e0..85de261 100644 --- a/lib/diffo/provider/components/instance/extension/feature.ex +++ b/lib/diffo/provider/components/instance/extension/feature.ex @@ -54,9 +54,16 @@ defmodule Diffo.Provider.Instance.Feature do characteristic_ids = Enum.reduce_while(characteristics, [], fn %{name: name, value_type: value_type}, acc -> try do - value = Value.dynamic(struct(value_type)) + attrs = + case value_type do + {:array, _inner} -> + %{name: name, type: :feature, values: [], is_array: true} - case Provider.create_characteristic(%{name: name, value: value, type: :feature}) do + module -> + %{name: name, type: :feature, value: Value.dynamic(struct(module))} + end + + case Provider.create_characteristic(attrs) do {:ok, result} -> {:cont, [result.id | acc]} diff --git a/test/instance_extension/characteristic_test.exs b/test/instance_extension/characteristic_test.exs index d563191..13f500b 100644 --- a/test/instance_extension/characteristic_test.exs +++ b/test/instance_extension/characteristic_test.exs @@ -25,5 +25,14 @@ defmodule Diffo.InstanceExtension.CharacteristicTest do assert hd(errors).message == "couldn't create characteristic with value of unknown type Elixir.InvalidValue" end + + test "create resource with array characteristic - success" do + {:ok, shelf} = Servo.build_shelf(%{}) + + shelves = Enum.find(shelf.characteristics, fn c -> c.name == :shelves end) + assert shelves.is_array == true + assert shelves.values == [] + assert Diffo.Unwrap.unwrap(shelves) == [] + end end end diff --git a/test/instance_extension/feature_test.exs b/test/instance_extension/feature_test.exs index 4b6cf81..4cf9825 100644 --- a/test/instance_extension/feature_test.exs +++ b/test/instance_extension/feature_test.exs @@ -25,5 +25,16 @@ defmodule Diffo.InstanceExtension.FeatureTest do assert hd(errors).message == "couldn't create feature characteristic with value of unknown type Elixir.InvalidValue" end + + test "create resource with array feature characteristic - success" do + {:ok, shelf} = Servo.build_shelf(%{}) + + spectral = Enum.find(shelf.features, fn f -> f.name == :spectralManagement end) + deployment_classes = Enum.find(spectral.characteristics, fn c -> c.name == :deploymentClasses end) + + assert deployment_classes.is_array == true + assert deployment_classes.values == [] + assert Diffo.Unwrap.unwrap(deployment_classes) == [] + end end end diff --git a/test/support/resource/shelf.ex b/test/support/resource/shelf.ex index 6740157..b95724e 100644 --- a/test/support/resource/shelf.ex +++ b/test/support/resource/shelf.ex @@ -42,12 +42,14 @@ defmodule Diffo.Test.Shelf do feature :spectralManagement do is_enabled? true characteristic :deploymentClass, DeploymentClassValue + characteristic :deploymentClasses, {:array, DeploymentClassValue} end end characteristics do characteristic :shelf, ShelfValue characteristic :slots, AssignableValue + characteristic :shelves, {:array, ShelfValue} end actions do From ba0970443ec91578d1537d319ce1c13c59f4d33d Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Fri, 24 Apr 2026 18:42:04 +0930 Subject: [PATCH 4/4] spark dsl doco --- documentation/dsls/DSL-Diffo.Provider.Instance.Extension.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/documentation/dsls/DSL-Diffo.Provider.Instance.Extension.md b/documentation/dsls/DSL-Diffo.Provider.Instance.Extension.md index 3a7a303..b1685af 100644 --- a/documentation/dsls/DSL-Diffo.Provider.Instance.Extension.md +++ b/documentation/dsls/DSL-Diffo.Provider.Instance.Extension.md @@ -110,7 +110,7 @@ Adds a Characteristic | Name | Type | Default | Docs | |------|------|---------|------| | [`name`](#features-feature-characteristic-name){: #features-feature-characteristic-name .spark-required} | `atom` | | The name of the characteristic, an atom | -| [`value_type`](#features-feature-characteristic-value_type){: #features-feature-characteristic-value_type } | `atom` | | The optional type of the characteristic's value, an atom, may be a module name such as an Ash.TypedStruct | +| [`value_type`](#features-feature-characteristic-value_type){: #features-feature-characteristic-value_type } | `any` | | The type of the characteristic's value. An atom module name such as an Ash.TypedStruct for a scalar value, or `{:array, module}` for an array of values of that type. | @@ -169,7 +169,7 @@ Adds a Characteristic | Name | Type | Default | Docs | |------|------|---------|------| | [`name`](#characteristics-characteristic-name){: #characteristics-characteristic-name .spark-required} | `atom` | | The name of the characteristic, an atom | -| [`value_type`](#characteristics-characteristic-value_type){: #characteristics-characteristic-value_type } | `atom` | | The optional type of the characteristic's value, an atom, may be a module name such as an Ash.TypedStruct | +| [`value_type`](#characteristics-characteristic-value_type){: #characteristics-characteristic-value_type } | `any` | | The type of the characteristic's value. An atom module name such as an Ash.TypedStruct for a scalar value, or `{:array, module}` for an array of values of that type. |