From 84efbde9736c20ffde220867b9b7e94192c5d9ca Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Sun, 17 May 2026 00:00:12 +0930 Subject: [PATCH 1/2] base characteristic --- .../components/base_characteristic.ex | 133 ++++++++++++++ .../calculations/characteristic_value.ex | 20 ++ .../provider/components/characteristic.ex | 2 +- .../components/characteristic/extension.ex | 8 + .../provider/extension/characteristic.ex | 172 ++++++++++++------ lib/diffo/provider/extension/feature.ex | 35 +++- .../transformers/transform_behaviour.ex | 11 +- lib/diffo/type/characteristic_value.ex | 15 ++ test/provider/extension/assigner_test.exs | 29 ++- test/provider/extension/info_test.exs | 18 +- .../extension/instance_transformer_test.exs | 6 +- .../extension/instance_verifier_test.exs | 22 +-- test/provider/extension/party_test.exs | 8 +- .../extension/party_transformer_test.exs | 4 +- .../extension/party_verifier_test.exs | 16 +- test/provider/extension/place_test.exs | 6 +- .../extension/place_transformer_test.exs | 2 +- .../extension/place_verifier_test.exs | 16 +- .../provider/extension/specification_test.exs | 2 +- test/provider/versioning_test.exs | 4 +- test/support/nbn.ex | 10 +- test/support/resource/card_value.ex | 37 ---- test/support/resource/characteristic/card.ex | 50 +++++ .../resource/characteristic/card/value.ex | 19 ++ .../characteristic/deployment_class.ex | 49 +++++ .../characteristic/deployment_class/value.ex | 18 ++ test/support/resource/characteristic/shelf.ex | 50 +++++ .../resource/characteristic/shelf/value.ex | 19 ++ .../resource/deployment_class_value.ex | 32 ---- .../resource/{ => instance}/broadband.ex | 2 +- .../resource/{ => instance}/broadband_v2.ex | 2 +- test/support/resource/{ => instance}/card.ex | 9 +- test/support/resource/{ => instance}/shelf.ex | 29 +-- test/support/resource/{ => party}/carrier.ex | 2 +- .../resource/{ => party}/organization.ex | 4 +- test/support/resource/{ => party}/person.ex | 4 +- .../resource/{ => place}/exchange_building.ex | 4 +- .../resource/{ => place}/geographic_site.ex | 4 +- test/support/resource/shelf_value.ex | 37 ---- test/support/servo.ex | 15 +- test/type/dynamic_test.exs | 2 +- 41 files changed, 667 insertions(+), 260 deletions(-) create mode 100644 lib/diffo/provider/components/base_characteristic.ex create mode 100644 lib/diffo/provider/components/calculations/characteristic_value.ex create mode 100644 lib/diffo/provider/components/characteristic/extension.ex create mode 100644 lib/diffo/type/characteristic_value.ex delete mode 100644 test/support/resource/card_value.ex create mode 100644 test/support/resource/characteristic/card.ex create mode 100644 test/support/resource/characteristic/card/value.ex create mode 100644 test/support/resource/characteristic/deployment_class.ex create mode 100644 test/support/resource/characteristic/deployment_class/value.ex create mode 100644 test/support/resource/characteristic/shelf.ex create mode 100644 test/support/resource/characteristic/shelf/value.ex delete mode 100644 test/support/resource/deployment_class_value.ex rename test/support/resource/{ => instance}/broadband.ex (96%) rename test/support/resource/{ => instance}/broadband_v2.ex (96%) rename test/support/resource/{ => instance}/card.ex (90%) rename test/support/resource/{ => instance}/shelf.ex (80%) rename test/support/resource/{ => party}/carrier.ex (97%) rename test/support/resource/{ => party}/organization.ex (91%) rename test/support/resource/{ => party}/person.ex (91%) rename test/support/resource/{ => place}/exchange_building.ex (93%) rename test/support/resource/{ => place}/geographic_site.ex (90%) delete mode 100644 test/support/resource/shelf_value.ex diff --git a/lib/diffo/provider/components/base_characteristic.ex b/lib/diffo/provider/components/base_characteristic.ex new file mode 100644 index 0000000..6e4118b --- /dev/null +++ b/lib/diffo/provider/components/base_characteristic.ex @@ -0,0 +1,133 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.BaseCharacteristic do + @moduledoc """ + Ash Resource Fragment which is the point of extension for typed TMF Characteristics. + + `BaseCharacteristic` is the foundation for domain-specific Characteristic kinds. + Include it as a fragment on an `Ash.Resource` to get a typed characteristic node + in Neo4j with real Ash attributes — no `Ash.Type.Dynamic` required. + + `Diffo.Provider.Characteristic` remains available as the generic dynamic option + (storing values via `Diffo.Type.Value`); it includes `Characteristic.Extension` so + the DSL verifier accepts it alongside typed resources. + + ## Usage + + defmodule MyApp.CircuitCharacteristic do + use Ash.Resource, fragments: [BaseCharacteristic], domain: MyApp.Domain + + attributes do + attribute :bandwidth_mbps, :integer, public?: true + attribute :technology, :atom, public?: true + end + + actions do + create :create do + accept [:name, :bandwidth_mbps, :technology] + argument :instance_id, :uuid + argument :feature_id, :uuid + end + + update :update do + accept [:bandwidth_mbps, :technology] + end + end + + calculations do + calculate :value, Diffo.Type.CharacteristicValue, Diffo.Provider.Calculations.CharacteristicValue do + public? true + end + end + + preparations do + prepare build(load: [:value]) + end + + jason do + pick [:name, :value] + compact true + end + end + + ## DSL declaration + + provider do + characteristics do + characteristic :circuit, MyApp.CircuitCharacteristic + end + end + + At build time a `CircuitCharacteristic` node is created and connected to the + instance via an `:HAS` edge. The `name` attribute (e.g. `:circuit`) identifies + the characteristic's role on the instance. + + ## Typed vs dynamic + + | Style | DSL target | Neo4j node | Value storage | + |-------|-----------|------------|---------------| + | Typed | `BaseCharacteristic`-derived | per-type label (e.g. `:CircuitCharacteristic`) | direct Ash attributes | + | Dynamic | `Diffo.Provider.Characteristic` | `:Characteristic` | `Diffo.Type.Value` dynamic | + """ + use Spark.Dsl.Fragment, + of: Ash.Resource, + otp_app: :diffo, + domain: Diffo.Provider, + data_layer: AshNeo4j.DataLayer, + extensions: [ + AshJason.Resource, + Diffo.Provider.Characteristic.Extension + ] + + + neo4j do + relate [ + {:instance, :HAS, :incoming, :Instance}, + {:feature, :HAS, :incoming, :Feature} + ] + + guard [ + {:HAS, :incoming, :Instance}, + {:HAS, :incoming, :Feature} + ] + end + + attributes do + uuid_primary_key :id do + public? false + end + + attribute :name, :atom do + description "the role name of this characteristic on the owning instance or feature" + allow_nil? false + public? true + end + + create_timestamp :created_at + update_timestamp :updated_at + end + + relationships do + belongs_to :instance, Diffo.Provider.Instance do + allow_nil? true + public? true + end + + belongs_to :feature, Diffo.Provider.Feature do + allow_nil? true + public? true + end + end + + validations do + validate present([:instance_id, :feature_id], at_most: 1) do + message "characteristic must belong to at most one of an instance or feature" + end + end + + actions do + defaults [:read, :destroy] + end +end diff --git a/lib/diffo/provider/components/calculations/characteristic_value.ex b/lib/diffo/provider/components/calculations/characteristic_value.ex new file mode 100644 index 0000000..e870d88 --- /dev/null +++ b/lib/diffo/provider/components/calculations/characteristic_value.ex @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Calculations.CharacteristicValue do + @moduledoc false + use Ash.Resource.Calculation + + @impl true + def load(_query, _opts, _context), do: [] + + @impl true + def calculate(records, _opts, _context) do + Enum.map(records, fn record -> + value_module = Module.concat(record.__struct__, :Value) + field_names = value_module |> struct() |> Map.from_struct() |> Map.keys() + struct(value_module, Map.take(Map.from_struct(record), field_names)) + end) + end +end diff --git a/lib/diffo/provider/components/characteristic.ex b/lib/diffo/provider/components/characteristic.ex index 64f49dd..53b67e2 100644 --- a/lib/diffo/provider/components/characteristic.ex +++ b/lib/diffo/provider/components/characteristic.ex @@ -10,7 +10,7 @@ defmodule Diffo.Provider.Characteristic do otp_app: :diffo, domain: Diffo.Provider, data_layer: AshNeo4j.DataLayer, - extensions: [AshOutstanding.Resource, AshJason.Resource] + extensions: [AshOutstanding.Resource, AshJason.Resource, Diffo.Provider.Characteristic.Extension] resource do description "An Ash Resource for a TMF Characteristic" diff --git a/lib/diffo/provider/components/characteristic/extension.ex b/lib/diffo/provider/components/characteristic/extension.ex new file mode 100644 index 0000000..a5f4046 --- /dev/null +++ b/lib/diffo/provider/components/characteristic/extension.ex @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Characteristic.Extension do + @moduledoc "Marker extension identifying a module as a valid characteristic resource." + use Spark.Dsl.Extension, sections: [] +end diff --git a/lib/diffo/provider/extension/characteristic.ex b/lib/diffo/provider/extension/characteristic.ex index f931331..865fe68 100644 --- a/lib/diffo/provider/extension/characteristic.ex +++ b/lib/diffo/provider/extension/characteristic.ex @@ -12,9 +12,13 @@ defmodule Diffo.Provider.Extension.Characteristic do defstruct [:name, :value_type, __spark_metadata__: nil] + # ── build_before: dynamic characteristics only ───────────────────────────── + def set_characteristics_argument(changeset, declarations) when is_struct(changeset, Ash.Changeset) and is_list(declarations) do - case characteristics = create_characteristics_from_declarations(declarations, :instance) do + dynamic = Enum.reject(declarations, &typed?(&1.value_type)) + + case characteristics = create_characteristics_from_declarations(dynamic, :instance) do [] -> changeset @@ -54,6 +58,8 @@ defmodule Diffo.Provider.Extension.Characteristic do end) end + # ── build_after: relate dynamic, create typed ────────────────────────────── + def relate_instance(result, changeset) when is_struct(result) and is_struct(changeset, Ash.Changeset) do characteristics = Ash.Changeset.get_argument(changeset, :characteristics) @@ -63,72 +69,134 @@ defmodule Diffo.Provider.Extension.Characteristic do }) end + def create_typed(result, declarations) when is_struct(result) and is_list(declarations) do + typed = Enum.filter(declarations, &typed?(&1.value_type)) + + Enum.reduce_while(typed, {:ok, result}, fn %{name: name, value_type: module}, {:ok, acc} -> + case module + |> Ash.Changeset.for_create(:create, %{name: name, instance_id: acc.id}) + |> Ash.create() do + {:ok, _} -> {:cont, {:ok, acc}} + {:error, error} -> {:halt, {:error, error}} + end + end) + end + + # ── update: handle both typed and dynamic characteristics ────────────────── + def update_values(result, changeset) when is_struct(result) and is_struct(changeset, Ash.Changeset) do + update_all(result, changeset, []) + end + + def update_all(result, changeset, declarations) + when is_struct(result) and is_struct(changeset, Ash.Changeset) and is_list(declarations) do characteristic_value_updates = Ash.Changeset.get_argument(changeset, :characteristic_value_updates) case characteristic_value_updates do - nil -> - {:ok, result} + nil -> {:ok, result} + [] -> {:ok, result} + _ -> apply_updates(result, characteristic_value_updates, declarations) + end + end - [] -> - {:ok, result} + defp apply_updates(result, updates, declarations) do + Enum.reduce_while(updates, {:ok, result}, fn {name, update}, {:ok, acc} -> + decl = Enum.find(declarations, &(&1.name == name)) - _ -> - characteristic_updates = - Enum.reduce(characteristic_value_updates, [], fn {name, update}, acc -> - characteristic = - Enum.find(changeset.data.characteristics, fn %{name: n} -> n == name end) - - if characteristic do - cond do - is_list(update) -> - unwrapped = Diffo.Unwrap.unwrap(characteristic.value) - value_type = unwrapped.__struct__ - - updated = - Enum.reduce(update, unwrapped, fn {field, val}, acc -> - Map.put(acc, field, val) - end) - - new_value = Value.dynamic(struct(value_type, Map.from_struct(updated))) - [{characteristic, new_value} | acc] - - true -> - [{characteristic, update} | acc] - end - else - Logger.warning("couldn't find characteristic #{name}") - acc - end - end) - - characteristics = - Enum.reduce_while(characteristic_updates, [], fn {characteristic, value}, acc -> - case Provider.update_characteristic(characteristic, %{value: value}) do - {:ok, characteristic} -> - {:cont, [characteristic | acc]} - - {:error, error} -> - {:halt, {:error, error}} - end - end) - - case characteristics do - {:error, error} -> - {:error, error} + if decl && typed?(decl.value_type) do + apply_typed_update(acc, name, decl.value_type, update) + else + apply_dynamic_update(acc, name, update) + end + end) + end - [] -> - {:error, "couldn't update characteristics"} + defp apply_typed_update(result, name, module, field_updates) do + case module + |> Ash.Query.new() + |> Ash.Query.filter_input(instance_id: result.id, name: name) + |> Ash.read_one() do + {:ok, nil} -> + Logger.warning("couldn't find typed characteristic #{name}") + {:cont, {:ok, result}} + + {:ok, char} -> + attrs = if is_list(field_updates), do: Map.new(field_updates), else: field_updates + + case char + |> Ash.Changeset.for_update(:update, attrs) + |> Ash.update() do + {:ok, _} -> {:cont, {:ok, result}} + {:error, error} -> {:halt, {:error, error}} + end - _ -> - {:ok, Map.put(result, :characteristics, characteristics)} + {:error, error} -> + {:halt, {:error, error}} + end + end + + defp apply_dynamic_update(result, name, update) do + characteristic = Enum.find(result.characteristics, fn %{name: n} -> n == name end) + + if characteristic do + new_value = + cond do + is_list(update) -> + unwrapped = Diffo.Unwrap.unwrap(characteristic.value) + value_type = unwrapped.__struct__ + + updated = + Enum.reduce(update, unwrapped, fn {field, val}, acc -> + Map.put(acc, field, val) + end) + + Value.dynamic(struct(value_type, Map.from_struct(updated))) + + true -> + update end + + case Provider.update_characteristic(characteristic, %{value: new_value}) do + {:ok, updated_char} -> + updated_chars = + Enum.map(result.characteristics, fn c -> + if c.id == updated_char.id, do: updated_char, else: c + end) + + {:cont, {:ok, %{result | characteristics: updated_chars}}} + + {:error, error} -> + {:halt, {:error, error}} + end + else + Logger.warning("couldn't find characteristic #{name}") + {:cont, {:ok, result}} end end defimpl String.Chars do def to_string(struct), do: inspect(struct) end + + # ── helpers ──────────────────────────────────────────────────────────────── + + def typed?(module) when is_atom(module) and not is_nil(module) do + case Code.ensure_loaded(module) do + {:module, _} -> + try do + module != Diffo.Provider.Characteristic and + Diffo.Provider.Characteristic.Extension in Ash.Resource.Info.extensions(module) + rescue + _ -> false + end + + _ -> + false + end + end + + def typed?(_), do: false + end diff --git a/lib/diffo/provider/extension/feature.ex b/lib/diffo/provider/extension/feature.ex index 38b5578..d56ebaa 100644 --- a/lib/diffo/provider/extension/feature.ex +++ b/lib/diffo/provider/extension/feature.ex @@ -8,6 +8,7 @@ defmodule Diffo.Provider.Extension.Feature do alias Diffo.Provider alias Diffo.Provider.Instance + alias Diffo.Provider.Extension.Characteristic alias Diffo.Type.Value defstruct [:name, :is_enabled?, :characteristics, __spark_metadata__: nil] @@ -32,8 +33,10 @@ defmodule Diffo.Provider.Extension.Feature do declarations, [], fn %{name: name, is_enabled?: isEnabled, characteristics: characteristics}, acc -> + dynamic = Enum.reject(characteristics, &Characteristic.typed?(&1.value_type)) + characteristic_ids = - Enum.reduce_while(characteristics, [], fn %{name: name, value_type: value_type}, acc -> + Enum.reduce_while(dynamic, [], fn %{name: name, value_type: value_type}, acc -> try do attrs = case value_type do @@ -86,6 +89,36 @@ defmodule Diffo.Provider.Extension.Feature do Provider.relate_instance_features(%Instance{id: result.id}, %{features: features}) end + def create_typed_feature_chars(result, declarations) + when is_struct(result) and is_list(declarations) do + Enum.reduce_while(declarations, {:ok, result}, fn %{name: name, characteristics: char_decls}, + {:ok, acc} -> + feature = Enum.find(acc.features, fn f -> f.name == name end) + + if feature do + typed = Enum.filter(char_decls, &Characteristic.typed?(&1.value_type)) + + case create_typed_for_feature(feature, typed) do + :ok -> {:cont, {:ok, acc}} + {:error, error} -> {:halt, {:error, error}} + end + else + {:cont, {:ok, acc}} + end + end) + end + + defp create_typed_for_feature(feature, typed_decls) do + Enum.reduce_while(typed_decls, :ok, fn %{name: char_name, value_type: module}, :ok -> + case module + |> Ash.Changeset.for_create(:create, %{name: char_name, feature_id: feature.id}) + |> Ash.create() do + {:ok, _} -> {:cont, :ok} + {:error, error} -> {:halt, {:error, error}} + end + end) + end + defimpl String.Chars do def to_string(struct), do: inspect(struct) end diff --git a/lib/diffo/provider/extension/transformers/transform_behaviour.ex b/lib/diffo/provider/extension/transformers/transform_behaviour.ex index efb5f47..e456331 100644 --- a/lib/diffo/provider/extension/transformers/transform_behaviour.ex +++ b/lib/diffo/provider/extension/transformers/transform_behaviour.ex @@ -35,7 +35,16 @@ defmodule Diffo.Provider.Extension.Transformers.TransformBehaviour do after_body = quote do - Diffo.Provider.Instance.ActionHelper.build_after(changeset, result) + with {:ok, result} <- + Diffo.Provider.Instance.ActionHelper.build_after(changeset, result), + {:ok, result} <- + Diffo.Provider.Extension.Characteristic.create_typed(result, characteristics()), + {:ok, result} <- + Diffo.Provider.Extension.Feature.create_typed_feature_chars( + result, + features() + ), + do: {:ok, result} end {before_body, after_body} diff --git a/lib/diffo/type/characteristic_value.ex b/lib/diffo/type/characteristic_value.ex new file mode 100644 index 0000000..29edd07 --- /dev/null +++ b/lib/diffo/type/characteristic_value.ex @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Type.CharacteristicValue do + @moduledoc """ + Ash type for a typed characteristic value. + + Used as the return type for `:value` calculations on `BaseCharacteristic`-derived resources. + The actual value is a `TypedStruct` defined by the extender (e.g. `Card.Value`, `Shelf.Value`), + which controls field ordering and JSON encoding via `AshJason.TypedStruct`. + """ + use Ash.Type.NewType, + subtype_of: Ash.Type.Struct +end diff --git a/test/provider/extension/assigner_test.exs b/test/provider/extension/assigner_test.exs index c6e1632..3e8d8f2 100644 --- a/test/provider/extension/assigner_test.exs +++ b/test/provider/extension/assigner_test.exs @@ -12,7 +12,7 @@ defmodule Diffo.Provider.Extension.AssignerTest do alias Diffo.Test.Characteristics alias Diffo.Test.Parties alias Diffo.Test.Servo - alias Diffo.Test.Card + alias Diffo.Test.Instance.Card setup do AshNeo4j.Sandbox.checkout() @@ -40,9 +40,10 @@ defmodule Diffo.Provider.Extension.AssignerTest do :outgoing ) - # check characteristic resource enrichment and node relationships + # check dynamic characteristic resource enrichment and node relationships + # :card is now a typed CardValue node (not in characteristics); only :ports remains dynamic assert is_list(card.characteristics) - assert length(card.characteristics) == 2 + assert length(card.characteristics) == 1 Enum.each(card.characteristics, fn characteristic -> assert is_struct(characteristic, Characteristic) @@ -60,7 +61,7 @@ defmodule Diffo.Provider.Extension.AssignerTest do encoding = Jason.encode!(card) |> Diffo.Util.summarise_dates() assert encoding == - ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{card.id}",\"category\":\"Network Resource\",\"description\":\"A Card Resource Instance\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"},\"resourceCharacteristic\":[{\"name\":\"card\",\"value\":{}},{\"name\":\"ports\",\"value\":{\"first\":1,\"last\":1,\"free\":1,\"algorithm\":\"lowest\"}}]}) + ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{card.id}",\"category\":\"Network Resource\",\"description\":\"A Card Resource Instance\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"},\"resourceCharacteristic\":[{\"name\":\"ports\",\"value\":{\"first\":1,\"last\":1,\"free\":1,\"algorithm\":\"lowest\"}}]}) end test "define card" do @@ -73,10 +74,20 @@ defmodule Diffo.Provider.Extension.AssignerTest do {:ok, card} = Servo.define_card(card, %{characteristic_value_updates: updates}) + {:ok, card_value} = + Diffo.Test.Characteristic.Card + |> Ash.Query.new() + |> Ash.Query.filter_input(instance_id: card.id) + |> Ash.read_one() + + assert card_value.family == :ISAM + assert card_value.model == "EBLT48" + assert card_value.technology == :adsl2Plus + encoding = Jason.encode!(card) |> Diffo.Util.summarise_dates() assert encoding == - ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{card.id}",\"category\":\"Network Resource\",\"description\":\"A Card Resource Instance\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"},\"resourceCharacteristic\":[{\"name\":\"card\",\"value\":{\"family\":\"ISAM\",\"model\":\"EBLT48\",\"technology\":\"adsl2Plus\"}},{\"name\":\"ports\",\"value\":{\"first\":1,\"last\":48,\"free\":48,\"type\":\"ADSL2+\",\"algorithm\":\"lowest\"}}]}) + ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{card.id}",\"category\":\"Network Resource\",\"description\":\"A Card Resource Instance\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"},\"resourceCharacteristic\":[{\"name\":\"ports\",\"value\":{\"first\":1,\"last\":48,\"free\":48,\"type\":\"ADSL2+\",\"algorithm\":\"lowest\"}}]}) end test "auto assign port to resource" do @@ -101,7 +112,7 @@ defmodule Diffo.Provider.Extension.AssignerTest do encoding = Jason.encode!(card) |> Diffo.Util.summarise_dates() assert encoding == - ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{card.id}",\"category\":\"Network Resource\",\"description\":\"A Card Resource Instance\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"},\"resourceRelationship\":[{\"type\":\"assignedTo\",\"resource\":{\"id\":\"#{assignee.id}\",\"href\":\"resourceInventoryManagement/v4/resource/#{assignee.id}\"},\"resourceRelationshipCharacteristic\":[{\"name\":\"port\",\"value\":1}]}],\"resourceCharacteristic\":[{\"name\":\"card\",\"value\":{\"family\":\"ISAM\",\"model\":\"EBLT48\",\"technology\":\"adsl2Plus\"}},{\"name\":\"ports\",\"value\":{\"first\":1,\"last\":48,\"free\":47,\"type\":\"ADSL2+\",\"algorithm\":\"lowest\"}}]}) + ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{card.id}",\"category\":\"Network Resource\",\"description\":\"A Card Resource Instance\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"},\"resourceRelationship\":[{\"type\":\"assignedTo\",\"resource\":{\"id\":\"#{assignee.id}\",\"href\":\"resourceInventoryManagement/v4/resource/#{assignee.id}\"},\"resourceRelationshipCharacteristic\":[{\"name\":\"port\",\"value\":1}]}],\"resourceCharacteristic\":[{\"name\":\"ports\",\"value\":{\"first\":1,\"last\":48,\"free\":47,\"type\":\"ADSL2+\",\"algorithm\":\"lowest\"}}]}) end test "auto assign two ports to same resource" do @@ -131,7 +142,7 @@ defmodule Diffo.Provider.Extension.AssignerTest do encoding = Jason.encode!(card) |> Diffo.Util.summarise_dates() assert encoding == - ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{card.id}",\"category\":\"Network Resource\",\"description\":\"A Card Resource Instance\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"},\"resourceRelationship\":[{\"type\":\"assignedTo\",\"resource\":{\"id\":\"#{assignee.id}\",\"href\":\"resourceInventoryManagement/v4/resource/#{assignee.id}\"},\"resourceRelationshipCharacteristic\":[{\"name\":\"port\",\"value\":1}]},{\"type\":\"assignedTo\",\"resource\":{\"id\":\"#{assignee.id}\",\"href\":\"resourceInventoryManagement/v4/resource/#{assignee.id}\"},\"resourceRelationshipCharacteristic\":[{\"name\":\"port\",\"value\":2}]}],\"resourceCharacteristic\":[{\"name\":\"card\",\"value\":{\"family\":\"ISAM\",\"model\":\"EBLT48\",\"technology\":\"adsl2Plus\"}},{\"name\":\"ports\",\"value\":{\"first\":1,\"last\":48,\"free\":46,\"type\":\"ADSL2+\",\"algorithm\":\"lowest\"}}]}) + ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{card.id}",\"category\":\"Network Resource\",\"description\":\"A Card Resource Instance\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"},\"resourceRelationship\":[{\"type\":\"assignedTo\",\"resource\":{\"id\":\"#{assignee.id}\",\"href\":\"resourceInventoryManagement/v4/resource/#{assignee.id}\"},\"resourceRelationshipCharacteristic\":[{\"name\":\"port\",\"value\":1}]},{\"type\":\"assignedTo\",\"resource\":{\"id\":\"#{assignee.id}\",\"href\":\"resourceInventoryManagement/v4/resource/#{assignee.id}\"},\"resourceRelationshipCharacteristic\":[{\"name\":\"port\",\"value\":2}]}],\"resourceCharacteristic\":[{\"name\":\"ports\",\"value\":{\"first\":1,\"last\":48,\"free\":46,\"type\":\"ADSL2+\",\"algorithm\":\"lowest\"}}]}) end test "specific assignment rejects duplicate request" do @@ -161,7 +172,7 @@ defmodule Diffo.Provider.Extension.AssignerTest do encoding = Jason.encode!(card) |> Diffo.Util.summarise_dates() assert encoding == - ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{card.id}",\"category\":\"Network Resource\",\"description\":\"A Card Resource Instance\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"},\"resourceRelationship\":[{\"type\":\"assignedTo\",\"resource\":{\"id\":\"#{assignee.id}\",\"href\":\"resourceInventoryManagement/v4/resource/#{assignee.id}\"},\"resourceRelationshipCharacteristic\":[{\"name\":\"port\",\"value\":5}]}],\"resourceCharacteristic\":[{\"name\":\"card\",\"value\":{\"family\":\"ISAM\",\"model\":\"EBLT48\",\"technology\":\"adsl2Plus\"}},{\"name\":\"ports\",\"value\":{\"first\":1,\"last\":48,\"free\":47,\"type\":\"ADSL2+\",\"algorithm\":\"lowest\"}}]}) + ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{card.id}",\"category\":\"Network Resource\",\"description\":\"A Card Resource Instance\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"},\"resourceRelationship\":[{\"type\":\"assignedTo\",\"resource\":{\"id\":\"#{assignee.id}\",\"href\":\"resourceInventoryManagement/v4/resource/#{assignee.id}\"},\"resourceRelationshipCharacteristic\":[{\"name\":\"port\",\"value\":5}]}],\"resourceCharacteristic\":[{\"name\":\"ports\",\"value\":{\"first\":1,\"last\":48,\"free\":47,\"type\":\"ADSL2+\",\"algorithm\":\"lowest\"}}]}) end test "unassign an auto-assigned port from a resource" do @@ -204,7 +215,7 @@ defmodule Diffo.Provider.Extension.AssignerTest do encoding = Jason.encode!(card) |> Diffo.Util.summarise_dates() assert encoding == - ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{card.id}",\"category\":\"Network Resource\",\"description\":\"A Card Resource Instance\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"},\"resourceCharacteristic\":[{\"name\":\"card\",\"value\":{\"family\":\"ISAM\",\"model\":\"EBLT48\",\"technology\":\"adsl2Plus\"}},{\"name\":\"ports\",\"value\":{\"first\":1,\"last\":48,\"free\":48,\"type\":\"ADSL2+\",\"algorithm\":\"lowest\"}}]}) + ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{card.id}",\"category\":\"Network Resource\",\"description\":\"A Card Resource Instance\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"},\"resourceCharacteristic\":[{\"name\":\"ports\",\"value\":{\"first\":1,\"last\":48,\"free\":48,\"type\":\"ADSL2+\",\"algorithm\":\"lowest\"}}]}) end end end diff --git a/test/provider/extension/info_test.exs b/test/provider/extension/info_test.exs index d54e039..a26441f 100644 --- a/test/provider/extension/info_test.exs +++ b/test/provider/extension/info_test.exs @@ -10,7 +10,7 @@ defmodule Diffo.Provider.Extension.InfoTest do describe "instance?/1" do test "returns true for a BaseInstance-derived resource" do - assert Info.instance?(Diffo.Test.Shelf) == true + assert Info.instance?(Diffo.Test.Instance.Shelf) == true end test "returns true for the base Instance resource" do @@ -18,11 +18,11 @@ defmodule Diffo.Provider.Extension.InfoTest do end test "returns false for a BaseParty-derived resource" do - assert Info.instance?(Diffo.Test.Organization) == false + assert Info.instance?(Diffo.Test.Party.Organization) == false end test "returns false for a BasePlace-derived resource" do - assert Info.instance?(Diffo.Test.GeographicSite) == false + assert Info.instance?(Diffo.Test.Place.GeographicSite) == false end test "returns false for a non-existent module" do @@ -32,7 +32,7 @@ defmodule Diffo.Provider.Extension.InfoTest do describe "party?/1" do test "returns true for a BaseParty-derived resource" do - assert Info.party?(Diffo.Test.Organization) == true + assert Info.party?(Diffo.Test.Party.Organization) == true end test "returns true for the base Party resource" do @@ -40,11 +40,11 @@ defmodule Diffo.Provider.Extension.InfoTest do end test "returns false for a BaseInstance-derived resource" do - assert Info.party?(Diffo.Test.Shelf) == false + assert Info.party?(Diffo.Test.Instance.Shelf) == false end test "returns false for a BasePlace-derived resource" do - assert Info.party?(Diffo.Test.GeographicSite) == false + assert Info.party?(Diffo.Test.Place.GeographicSite) == false end test "returns false for a non-existent module" do @@ -54,7 +54,7 @@ defmodule Diffo.Provider.Extension.InfoTest do describe "place?/1" do test "returns true for a BasePlace-derived resource" do - assert Info.place?(Diffo.Test.GeographicSite) == true + assert Info.place?(Diffo.Test.Place.GeographicSite) == true end test "returns true for the base Place resource" do @@ -62,11 +62,11 @@ defmodule Diffo.Provider.Extension.InfoTest do end test "returns false for a BaseInstance-derived resource" do - assert Info.place?(Diffo.Test.Shelf) == false + assert Info.place?(Diffo.Test.Instance.Shelf) == false end test "returns false for a BaseParty-derived resource" do - assert Info.place?(Diffo.Test.Organization) == false + assert Info.place?(Diffo.Test.Party.Organization) == false end test "returns false for a non-existent module" do diff --git a/test/provider/extension/instance_transformer_test.exs b/test/provider/extension/instance_transformer_test.exs index c5b0395..b4d5b8a 100644 --- a/test/provider/extension/instance_transformer_test.exs +++ b/test/provider/extension/instance_transformer_test.exs @@ -6,8 +6,8 @@ defmodule Diffo.Provider.Extension.InstanceTransformerTest do @moduledoc false use ExUnit.Case, async: true, async: true - alias Diffo.Test.Shelf - alias Diffo.Test.Card + alias Diffo.Test.Instance.Shelf + alias Diffo.Test.Instance.Card alias Diffo.Provider.Extension.Characteristic alias Diffo.Provider.Extension.Feature alias Diffo.Provider.Extension.PlaceDeclaration @@ -241,7 +241,7 @@ defmodule Diffo.Provider.Extension.InstanceTransformerTest do test "characteristic/1 returns the named characteristic" do char = Shelf.characteristic(:shelves) assert char.name == :shelves - assert char.value_type == {:array, Diffo.Test.ShelfValue} + assert char.value_type == {:array, Diffo.Test.Characteristic.Shelf} end test "characteristic/1 returns nil for unknown name" do diff --git a/test/provider/extension/instance_verifier_test.exs b/test/provider/extension/instance_verifier_test.exs index 19a3d7f..cab2649 100644 --- a/test/provider/extension/instance_verifier_test.exs +++ b/test/provider/extension/instance_verifier_test.exs @@ -153,8 +153,8 @@ defmodule Diffo.Provider.Extension.InstanceVerifierTest do end characteristics do - characteristic :foo, Diffo.Test.ShelfValue - characteristic :foo, Diffo.Test.ShelfValue + characteristic :foo, Diffo.Test.Characteristic.Shelf + characteristic :foo, Diffo.Test.Characteristic.Shelf end end end @@ -273,8 +273,8 @@ defmodule Diffo.Provider.Extension.InstanceVerifierTest do features do feature :my_feature do - characteristic :baz, Diffo.Test.ShelfValue - characteristic :baz, Diffo.Test.ShelfValue + characteristic :baz, Diffo.Test.Characteristic.Shelf + characteristic :baz, Diffo.Test.Characteristic.Shelf end end end @@ -322,7 +322,7 @@ defmodule Diffo.Provider.Extension.InstanceVerifierTest do fn -> defmodule DuplicatePartyRole do alias Diffo.Provider.BaseInstance - alias Diffo.Test.Shelf + alias Diffo.Test.Instance.Shelf use Ash.Resource, fragments: [BaseInstance], domain: Diffo.Test.Servo resource do @@ -376,7 +376,7 @@ defmodule Diffo.Provider.Extension.InstanceVerifierTest do test "party_type not extending BaseParty warns DslError on compilation" do Util.assert_compile_time_warning( Spark.Error.DslError, - "parties: party_type Diffo.Test.Shelf does not extend BaseParty", + "parties: party_type Diffo.Test.Instance.Shelf does not extend BaseParty", fn -> defmodule InvalidPartyBaseType do alias Diffo.Provider.BaseInstance @@ -393,7 +393,7 @@ defmodule Diffo.Provider.Extension.InstanceVerifierTest do end parties do - party :operator, Diffo.Test.Shelf + party :operator, Diffo.Test.Instance.Shelf end end end @@ -532,7 +532,7 @@ defmodule Diffo.Provider.Extension.InstanceVerifierTest do test "party_ref with non-BaseParty type warns DslError on compilation" do Util.assert_compile_time_warning( Spark.Error.DslError, - "parties: party_type Diffo.Test.Shelf does not extend BaseParty", + "parties: party_type Diffo.Test.Instance.Shelf does not extend BaseParty", fn -> defmodule InvalidPartyRefBaseType do alias Diffo.Provider.BaseInstance @@ -549,7 +549,7 @@ defmodule Diffo.Provider.Extension.InstanceVerifierTest do end parties do - party_ref :owner, Diffo.Test.Shelf + party_ref :owner, Diffo.Test.Instance.Shelf end end end @@ -590,7 +590,7 @@ defmodule Diffo.Provider.Extension.InstanceVerifierTest do test "place_ref with non-BasePlace type warns DslError on compilation" do Util.assert_compile_time_warning( Spark.Error.DslError, - "places: place_type Diffo.Test.Shelf does not extend BasePlace", + "places: place_type Diffo.Test.Instance.Shelf does not extend BasePlace", fn -> defmodule InvalidPlaceRefBaseType do alias Diffo.Provider.BaseInstance @@ -607,7 +607,7 @@ defmodule Diffo.Provider.Extension.InstanceVerifierTest do end places do - place_ref :billing, Diffo.Test.Shelf + place_ref :billing, Diffo.Test.Instance.Shelf end end end diff --git a/test/provider/extension/party_test.exs b/test/provider/extension/party_test.exs index 530089f..ef8fd2e 100644 --- a/test/provider/extension/party_test.exs +++ b/test/provider/extension/party_test.exs @@ -8,10 +8,10 @@ defmodule Diffo.Provider.Extension.PartyTest do alias Diffo.Provider.Instance.Extension.Info, as: InstanceInfo alias Diffo.Provider.Party.Extension.Info, as: PartyInfo - alias Diffo.Test.Organization - alias Diffo.Test.Person + alias Diffo.Test.Party.Organization + alias Diffo.Test.Party.Person - alias Diffo.Test.Shelf + alias Diffo.Test.Instance.Shelf alias Diffo.Test.Nbn alias Diffo.Test.Servo alias Diffo.Provider.Instance.Party @@ -41,7 +41,7 @@ defmodule Diffo.Provider.Extension.PartyTest do roles = PartyInfo.parties(Person) assert length(roles) == 1 assert hd(roles).role == :manager - assert hd(roles).party_type == Diffo.Test.Person + assert hd(roles).party_type == Diffo.Test.Party.Person end test "instance roles are declared" do diff --git a/test/provider/extension/party_transformer_test.exs b/test/provider/extension/party_transformer_test.exs index ead64ec..4386133 100644 --- a/test/provider/extension/party_transformer_test.exs +++ b/test/provider/extension/party_transformer_test.exs @@ -6,8 +6,8 @@ defmodule Diffo.Provider.Extension.PartyTransformerTest do @moduledoc false use ExUnit.Case, async: true, async: true - alias Diffo.Test.Organization - alias Diffo.Test.Person + alias Diffo.Test.Party.Organization + alias Diffo.Test.Party.Person alias Diffo.Provider.Extension.InstanceRole alias Diffo.Provider.Extension.PartyRole alias Diffo.Provider.Extension.PlaceRole diff --git a/test/provider/extension/party_verifier_test.exs b/test/provider/extension/party_verifier_test.exs index ac4fd3b..a4bec88 100644 --- a/test/provider/extension/party_verifier_test.exs +++ b/test/provider/extension/party_verifier_test.exs @@ -58,7 +58,7 @@ defmodule Diffo.Provider.Extension.PartyVerifierTest do test "instance_type not extending BaseInstance warns DslError on compilation" do Util.assert_compile_time_warning( Spark.Error.DslError, - "instances: instance_type Diffo.Test.Organization does not extend BaseInstance", + "instances: instance_type Diffo.Test.Party.Organization does not extend BaseInstance", fn -> defmodule WrongInstanceType do alias Diffo.Provider.BaseParty @@ -70,7 +70,7 @@ defmodule Diffo.Provider.Extension.PartyVerifierTest do provider do instances do - role :operator, Diffo.Test.Organization + role :operator, Diffo.Test.Party.Organization end end end @@ -95,8 +95,8 @@ defmodule Diffo.Provider.Extension.PartyVerifierTest do provider do parties do - role :employer, Diffo.Test.Organization - role :employer, Diffo.Test.Organization + role :employer, Diffo.Test.Party.Organization + role :employer, Diffo.Test.Party.Organization end end end @@ -202,7 +202,7 @@ defmodule Diffo.Provider.Extension.PartyVerifierTest do test "place_type not extending BasePlace warns DslError on compilation" do Util.assert_compile_time_warning( Spark.Error.DslError, - "places: place_type Diffo.Test.Organization does not extend BasePlace", + "places: place_type Diffo.Test.Party.Organization does not extend BasePlace", fn -> defmodule WrongPartyPlaceRoleType do alias Diffo.Provider.BaseParty @@ -214,7 +214,7 @@ defmodule Diffo.Provider.Extension.PartyVerifierTest do provider do places do - role :headquarters, Diffo.Test.Organization + role :headquarters, Diffo.Test.Party.Organization end end end @@ -250,7 +250,7 @@ defmodule Diffo.Provider.Extension.PartyVerifierTest do test "instance_ref with non-BaseInstance type warns DslError on compilation" do Util.assert_compile_time_warning( Spark.Error.DslError, - "instances: instance_type Diffo.Test.Organization does not extend BaseInstance", + "instances: instance_type Diffo.Test.Party.Organization does not extend BaseInstance", fn -> defmodule InvalidPartyInstanceRefBaseType do alias Diffo.Provider.BaseParty @@ -262,7 +262,7 @@ defmodule Diffo.Provider.Extension.PartyVerifierTest do provider do instances do - instance_ref :manages, Diffo.Test.Organization + instance_ref :manages, Diffo.Test.Party.Organization end end end diff --git a/test/provider/extension/place_test.exs b/test/provider/extension/place_test.exs index 990733d..b159366 100644 --- a/test/provider/extension/place_test.exs +++ b/test/provider/extension/place_test.exs @@ -8,10 +8,10 @@ defmodule Diffo.Provider.Extension.PlaceTest do alias Diffo.Provider.Instance.Extension.Info, as: InstanceInfo alias Diffo.Provider.Place.Extension.Info, as: PlaceInfo - alias Diffo.Test.Organization - alias Diffo.Test.GeographicSite + alias Diffo.Test.Party.Organization + alias Diffo.Test.Place.GeographicSite - alias Diffo.Test.Shelf + alias Diffo.Test.Instance.Shelf alias Diffo.Test.Nbn setup do diff --git a/test/provider/extension/place_transformer_test.exs b/test/provider/extension/place_transformer_test.exs index 80871e1..e0137c6 100644 --- a/test/provider/extension/place_transformer_test.exs +++ b/test/provider/extension/place_transformer_test.exs @@ -6,7 +6,7 @@ defmodule Diffo.Provider.Extension.PlaceTransformerTest do @moduledoc false use ExUnit.Case, async: true, async: true - alias Diffo.Test.GeographicSite + alias Diffo.Test.Place.GeographicSite alias Diffo.Provider.Extension.InstanceRole alias Diffo.Provider.Extension.PartyRole alias Diffo.Provider.Extension.PlaceRole diff --git a/test/provider/extension/place_verifier_test.exs b/test/provider/extension/place_verifier_test.exs index d57bc05..3d10534 100644 --- a/test/provider/extension/place_verifier_test.exs +++ b/test/provider/extension/place_verifier_test.exs @@ -58,7 +58,7 @@ defmodule Diffo.Provider.Extension.PlaceVerifierTest do test "instance_type not extending BaseInstance warns DslError on compilation" do Util.assert_compile_time_warning( Spark.Error.DslError, - "instances: instance_type Diffo.Test.Organization does not extend BaseInstance", + "instances: instance_type Diffo.Test.Party.Organization does not extend BaseInstance", fn -> defmodule WrongPlaceInstanceType do alias Diffo.Provider.BasePlace @@ -70,7 +70,7 @@ defmodule Diffo.Provider.Extension.PlaceVerifierTest do provider do instances do - role :site_for, Diffo.Test.Organization + role :site_for, Diffo.Test.Party.Organization end end end @@ -95,8 +95,8 @@ defmodule Diffo.Provider.Extension.PlaceVerifierTest do provider do parties do - role :managed_by, Diffo.Test.Organization - role :managed_by, Diffo.Test.Organization + role :managed_by, Diffo.Test.Party.Organization + role :managed_by, Diffo.Test.Party.Organization end end end @@ -202,7 +202,7 @@ defmodule Diffo.Provider.Extension.PlaceVerifierTest do test "place_type not extending BasePlace warns DslError on compilation" do Util.assert_compile_time_warning( Spark.Error.DslError, - "places: place_type Diffo.Test.Organization does not extend BasePlace", + "places: place_type Diffo.Test.Party.Organization does not extend BasePlace", fn -> defmodule WrongPlacePlaceRoleType do alias Diffo.Provider.BasePlace @@ -214,7 +214,7 @@ defmodule Diffo.Provider.Extension.PlaceVerifierTest do provider do places do - role :contained_in, Diffo.Test.Organization + role :contained_in, Diffo.Test.Party.Organization end end end @@ -250,7 +250,7 @@ defmodule Diffo.Provider.Extension.PlaceVerifierTest do test "instance_ref with non-BaseInstance type warns DslError on compilation" do Util.assert_compile_time_warning( Spark.Error.DslError, - "instances: instance_type Diffo.Test.Organization does not extend BaseInstance", + "instances: instance_type Diffo.Test.Party.Organization does not extend BaseInstance", fn -> defmodule InvalidPlaceInstanceRefBaseType do alias Diffo.Provider.BasePlace @@ -262,7 +262,7 @@ defmodule Diffo.Provider.Extension.PlaceVerifierTest do provider do instances do - instance_ref :site_for, Diffo.Test.Organization + instance_ref :site_for, Diffo.Test.Party.Organization end end end diff --git a/test/provider/extension/specification_test.exs b/test/provider/extension/specification_test.exs index 952da58..c30016d 100644 --- a/test/provider/extension/specification_test.exs +++ b/test/provider/extension/specification_test.exs @@ -6,7 +6,7 @@ defmodule Diffo.Provider.Extension.SpecificationTest do @moduledoc false use ExUnit.Case, async: true alias Diffo.Test.Servo - alias Diffo.Test.Shelf + alias Diffo.Test.Instance.Shelf setup do AshNeo4j.Sandbox.checkout() diff --git a/test/provider/versioning_test.exs b/test/provider/versioning_test.exs index d8fc68b..555350d 100644 --- a/test/provider/versioning_test.exs +++ b/test/provider/versioning_test.exs @@ -7,8 +7,8 @@ defmodule Diffo.Provider.VersioningTest do use ExUnit.Case, async: true alias Diffo.Test.Servo - alias Diffo.Test.Broadband - alias Diffo.Test.BroadbandV2 + alias Diffo.Test.Instance.Broadband + alias Diffo.Test.Instance.BroadbandV2 setup do AshNeo4j.Sandbox.checkout() diff --git a/test/support/nbn.ex b/test/support/nbn.ex index 0c99bdc..337aac4 100644 --- a/test/support/nbn.ex +++ b/test/support/nbn.ex @@ -12,11 +12,11 @@ defmodule Diffo.Test.Nbn do otp_app: :diffo, validate_config_inclusion?: false - alias Diffo.Test.Organization - alias Diffo.Test.Person - alias Diffo.Test.Carrier - alias Diffo.Test.GeographicSite - alias Diffo.Test.ExchangeBuilding + alias Diffo.Test.Party.Organization + alias Diffo.Test.Party.Person + alias Diffo.Test.Party.Carrier + alias Diffo.Test.Place.GeographicSite + alias Diffo.Test.Place.ExchangeBuilding domain do description "NBN party and place domain" diff --git a/test/support/resource/card_value.ex b/test/support/resource/card_value.ex deleted file mode 100644 index f6dce53..0000000 --- a/test/support/resource/card_value.ex +++ /dev/null @@ -1,37 +0,0 @@ -# SPDX-FileCopyrightText: 2025 diffo contributors -# -# SPDX-License-Identifier: MIT - -defmodule Diffo.Test.CardValue do - @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - CardValue - AshTyped Struct for Card Characteristic Value - """ - use Ash.TypedStruct, extensions: [AshJason.TypedStruct, AshOutstanding.TypedStruct] - - jason do - pick [:name, :family, :model, :technology] - compact true - end - - outstanding do - expect [:name] - end - - typed_struct do - field :name, :string, description: "the card name" - - field :family, :atom, description: "the card family name" - - field :model, :string, description: "the card model name" - - field :technology, :atom, description: "the card technology" - end - - defimpl String.Chars do - def to_string(struct) do - inspect(struct) - end - end -end diff --git a/test/support/resource/characteristic/card.ex b/test/support/resource/characteristic/card.ex new file mode 100644 index 0000000..455a082 --- /dev/null +++ b/test/support/resource/characteristic/card.ex @@ -0,0 +1,50 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Test.Characteristic.Card do + @moduledoc "Typed characteristic for a Card's identity." + use Ash.Resource, + fragments: [Diffo.Provider.BaseCharacteristic], + domain: Diffo.Test.Servo + + resource do + description "Typed characteristic carrying card identity fields" + plural_name :card_values + end + + attributes do + attribute :family, :atom, public?: true, description: "the card family name" + attribute :model, :string, public?: true, description: "the card model name" + attribute :technology, :atom, public?: true, description: "the card technology" + end + + calculations do + calculate :value, Diffo.Type.CharacteristicValue, Diffo.Provider.Calculations.CharacteristicValue do + public? true + end + end + + actions do + create :create do + accept [:name, :family, :model, :technology] + argument :instance_id, :uuid + argument :feature_id, :uuid + change manage_relationship(:instance_id, :instance, type: :append) + change manage_relationship(:feature_id, :feature, type: :append) + end + + update :update do + accept [:family, :model, :technology] + end + end + + preparations do + prepare build(load: [:value]) + end + + jason do + pick [:name, :value] + compact true + end +end diff --git a/test/support/resource/characteristic/card/value.ex b/test/support/resource/characteristic/card/value.ex new file mode 100644 index 0000000..2e52661 --- /dev/null +++ b/test/support/resource/characteristic/card/value.ex @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Test.Characteristic.Card.Value do + @moduledoc "Typed value struct for a Card characteristic." + use Ash.TypedStruct, extensions: [AshJason.TypedStruct] + + typed_struct do + field :family, :atom, description: "the card family name" + field :model, :string, description: "the card model name" + field :technology, :atom, description: "the card technology" + end + + jason do + pick [:family, :model, :technology] + compact true + end +end diff --git a/test/support/resource/characteristic/deployment_class.ex b/test/support/resource/characteristic/deployment_class.ex new file mode 100644 index 0000000..d816f2c --- /dev/null +++ b/test/support/resource/characteristic/deployment_class.ex @@ -0,0 +1,49 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Test.Characteristic.DeploymentClass do + @moduledoc "Typed characteristic for a deployment class within a spectral management feature." + use Ash.Resource, + fragments: [Diffo.Provider.BaseCharacteristic], + domain: Diffo.Test.Servo + + resource do + description "Typed characteristic carrying deployment class fields" + plural_name :deployment_class_values + end + + attributes do + attribute :class, :string, public?: true, description: "the deployment class" + attribute :mask, :string, public?: true, description: "the mask name" + end + + calculations do + calculate :value, Diffo.Type.CharacteristicValue, Diffo.Provider.Calculations.CharacteristicValue do + public? true + end + end + + actions do + create :create do + accept [:name, :class, :mask] + argument :instance_id, :uuid + argument :feature_id, :uuid + change manage_relationship(:instance_id, :instance, type: :append) + change manage_relationship(:feature_id, :feature, type: :append) + end + + update :update do + accept [:class, :mask] + end + end + + preparations do + prepare build(load: [:value]) + end + + jason do + pick [:name, :value] + compact true + end +end diff --git a/test/support/resource/characteristic/deployment_class/value.ex b/test/support/resource/characteristic/deployment_class/value.ex new file mode 100644 index 0000000..53a6179 --- /dev/null +++ b/test/support/resource/characteristic/deployment_class/value.ex @@ -0,0 +1,18 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Test.Characteristic.DeploymentClass.Value do + @moduledoc "Typed value struct for a DeploymentClass characteristic." + use Ash.TypedStruct, extensions: [AshJason.TypedStruct] + + typed_struct do + field :class, :string, description: "the deployment class" + field :mask, :string, description: "the mask name" + end + + jason do + pick [:class, :mask] + compact true + end +end diff --git a/test/support/resource/characteristic/shelf.ex b/test/support/resource/characteristic/shelf.ex new file mode 100644 index 0000000..a400bf9 --- /dev/null +++ b/test/support/resource/characteristic/shelf.ex @@ -0,0 +1,50 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Test.Characteristic.Shelf do + @moduledoc "Typed characteristic for a Shelf's identity." + use Ash.Resource, + fragments: [Diffo.Provider.BaseCharacteristic], + domain: Diffo.Test.Servo + + resource do + description "Typed characteristic carrying shelf identity fields" + plural_name :shelf_values + end + + attributes do + attribute :family, :atom, public?: true, description: "the shelf family name" + attribute :model, :string, public?: true, description: "the shelf model name" + attribute :technology, :atom, public?: true, description: "the shelf technology" + end + + calculations do + calculate :value, Diffo.Type.CharacteristicValue, Diffo.Provider.Calculations.CharacteristicValue do + public? true + end + end + + actions do + create :create do + accept [:name, :family, :model, :technology] + argument :instance_id, :uuid + argument :feature_id, :uuid + change manage_relationship(:instance_id, :instance, type: :append) + change manage_relationship(:feature_id, :feature, type: :append) + end + + update :update do + accept [:family, :model, :technology] + end + end + + preparations do + prepare build(load: [:value]) + end + + jason do + pick [:name, :value] + compact true + end +end diff --git a/test/support/resource/characteristic/shelf/value.ex b/test/support/resource/characteristic/shelf/value.ex new file mode 100644 index 0000000..11cb450 --- /dev/null +++ b/test/support/resource/characteristic/shelf/value.ex @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Test.Characteristic.Shelf.Value do + @moduledoc "Typed value struct for a Shelf characteristic." + use Ash.TypedStruct, extensions: [AshJason.TypedStruct] + + typed_struct do + field :family, :atom, description: "the shelf family name" + field :model, :string, description: "the shelf model name" + field :technology, :atom, description: "the shelf technology" + end + + jason do + pick [:family, :model, :technology] + compact true + end +end diff --git a/test/support/resource/deployment_class_value.ex b/test/support/resource/deployment_class_value.ex deleted file mode 100644 index c82933e..0000000 --- a/test/support/resource/deployment_class_value.ex +++ /dev/null @@ -1,32 +0,0 @@ -# SPDX-FileCopyrightText: 2025 diffo contributors -# -# SPDX-License-Identifier: MIT - -defmodule Diffo.Test.DeploymentClassValue do - @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - DeploymentClassValue - AshTyped Struct for DeploymentClass Feature Characteristic Value - """ - use Ash.TypedStruct, extensions: [AshJason.TypedStruct, AshOutstanding.TypedStruct] - - jason do - pick [:class, :mask] - compact true - end - - outstanding do - expect [:name] - end - - typed_struct do - field :class, :string, description: "the deployment class" - field :mask, :string, description: "the mask name" - end - - defimpl String.Chars do - def to_string(struct) do - inspect(struct) - end - end -end diff --git a/test/support/resource/broadband.ex b/test/support/resource/instance/broadband.ex similarity index 96% rename from test/support/resource/broadband.ex rename to test/support/resource/instance/broadband.ex index 7eac2d6..946967a 100644 --- a/test/support/resource/broadband.ex +++ b/test/support/resource/instance/broadband.ex @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: MIT -defmodule Diffo.Test.Broadband do +defmodule Diffo.Test.Instance.Broadband do @moduledoc """ Diffo - TMF Service and Resource Management with a difference diff --git a/test/support/resource/broadband_v2.ex b/test/support/resource/instance/broadband_v2.ex similarity index 96% rename from test/support/resource/broadband_v2.ex rename to test/support/resource/instance/broadband_v2.ex index 3dc4b1b..ceea732 100644 --- a/test/support/resource/broadband_v2.ex +++ b/test/support/resource/instance/broadband_v2.ex @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: MIT -defmodule Diffo.Test.BroadbandV2 do +defmodule Diffo.Test.Instance.BroadbandV2 do @moduledoc """ Diffo - TMF Service and Resource Management with a difference diff --git a/test/support/resource/card.ex b/test/support/resource/instance/card.ex similarity index 90% rename from test/support/resource/card.ex rename to test/support/resource/instance/card.ex index 6ec064b..bad90e8 100644 --- a/test/support/resource/card.ex +++ b/test/support/resource/instance/card.ex @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: MIT -defmodule Diffo.Test.Card do +defmodule Diffo.Test.Instance.Card do @moduledoc """ Diffo - TMF Service and Resource Management with a difference @@ -15,7 +15,7 @@ defmodule Diffo.Test.Card do alias Diffo.Provider.Assignment alias Diffo.Provider.AssignableValue alias Diffo.Test.Servo - alias Diffo.Test.CardValue + alias Diffo.Test.Characteristic.Card, as: CardCharacteristic use Ash.Resource, fragments: [BaseInstance], @@ -36,7 +36,7 @@ defmodule Diffo.Test.Card do end characteristics do - characteristic :card, CardValue + characteristic :card, CardCharacteristic characteristic :ports, AssignableValue end @@ -65,7 +65,8 @@ defmodule Diffo.Test.Card do argument :characteristic_value_updates, {:array, :term} change after_action(fn changeset, result, _context -> - with {:ok, result} <- Characteristic.update_values(result, changeset), + with {:ok, result} <- + Characteristic.update_all(result, changeset, characteristics()), {:ok, result} <- Servo.get_card_by_id(result.id), do: {:ok, result} end) diff --git a/test/support/resource/shelf.ex b/test/support/resource/instance/shelf.ex similarity index 80% rename from test/support/resource/shelf.ex rename to test/support/resource/instance/shelf.ex index 4b24237..1643880 100644 --- a/test/support/resource/shelf.ex +++ b/test/support/resource/instance/shelf.ex @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: MIT -defmodule Diffo.Test.Shelf do +defmodule Diffo.Test.Instance.Shelf do @moduledoc """ Diffo - TMF Service and Resource Management with a difference @@ -17,8 +17,10 @@ defmodule Diffo.Test.Shelf do alias Diffo.Provider.AssignableValue alias Diffo.Test.Servo - alias Diffo.Test.ShelfValue - alias Diffo.Test.DeploymentClassValue + alias Diffo.Test.Characteristic.Shelf, as: ShelfCharacteristic + alias Diffo.Test.Characteristic.DeploymentClass + alias Diffo.Test.Party.Organization + alias Diffo.Test.Party.Person use Ash.Resource, fragments: [BaseInstance], @@ -45,23 +47,23 @@ defmodule Diffo.Test.Shelf do features do feature :spectralManagement do is_enabled? true - characteristic :deploymentClass, DeploymentClassValue - characteristic :deploymentClasses, {:array, DeploymentClassValue} + characteristic :deploymentClass, DeploymentClass + characteristic :deploymentClasses, {:array, DeploymentClass} end end characteristics do - characteristic :shelf, ShelfValue + characteristic :shelf, ShelfCharacteristic characteristic :slots, AssignableValue - characteristic :shelves, {:array, ShelfValue} + characteristic :shelves, {:array, ShelfCharacteristic} end parties do - party :facilitator, Diffo.Test.Organization - party :overseer, Diffo.Test.Person - party_ref :provider, Diffo.Test.Organization - party :manager, Diffo.Test.Organization, calculate: :manager_calc - parties :installer, Diffo.Test.Person, constraints: [min: 1, max: 3] + party :facilitator, Organization + party :overseer, Person + party_ref :provider, Organization + party :manager, Organization, calculate: :manager_calc + parties :installer, Person, constraints: [min: 1, max: 3] end places do @@ -94,7 +96,8 @@ defmodule Diffo.Test.Shelf do argument :characteristic_value_updates, {:array, :term} change after_action(fn changeset, result, _context -> - with {:ok, result} <- Characteristic.update_values(result, changeset), + with {:ok, result} <- + Characteristic.update_all(result, changeset, characteristics()), {:ok, result} <- Servo.get_shelf_by_id(result.id), do: {:ok, result} end) diff --git a/test/support/resource/carrier.ex b/test/support/resource/party/carrier.ex similarity index 97% rename from test/support/resource/carrier.ex rename to test/support/resource/party/carrier.ex index 3614c5c..f41c71e 100644 --- a/test/support/resource/carrier.ex +++ b/test/support/resource/party/carrier.ex @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: MIT -defmodule Diffo.Test.Carrier do +defmodule Diffo.Test.Party.Carrier do @moduledoc """ Diffo - TMF Service and Resource Management with a difference diff --git a/test/support/resource/organization.ex b/test/support/resource/party/organization.ex similarity index 91% rename from test/support/resource/organization.ex rename to test/support/resource/party/organization.ex index bbe81c4..b49d08d 100644 --- a/test/support/resource/organization.ex +++ b/test/support/resource/party/organization.ex @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: MIT -defmodule Diffo.Test.Organization do +defmodule Diffo.Test.Party.Organization do @moduledoc """ Diffo - TMF Service and Resource Management with a difference @@ -43,7 +43,7 @@ defmodule Diffo.Test.Organization do end parties do - role :employer, Diffo.Test.Person + role :employer, Diffo.Test.Party.Person end places do diff --git a/test/support/resource/person.ex b/test/support/resource/party/person.ex similarity index 91% rename from test/support/resource/person.ex rename to test/support/resource/party/person.ex index 1e559aa..25d82f4 100644 --- a/test/support/resource/person.ex +++ b/test/support/resource/party/person.ex @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: MIT -defmodule Diffo.Test.Person do +defmodule Diffo.Test.Party.Person do @moduledoc """ Diffo - TMF Service and Resource Management with a difference @@ -43,7 +43,7 @@ defmodule Diffo.Test.Person do end parties do - role :manager, Diffo.Test.Person + role :manager, Diffo.Test.Party.Person end places do diff --git a/test/support/resource/exchange_building.ex b/test/support/resource/place/exchange_building.ex similarity index 93% rename from test/support/resource/exchange_building.ex rename to test/support/resource/place/exchange_building.ex index 83e9f4b..ae5281a 100644 --- a/test/support/resource/exchange_building.ex +++ b/test/support/resource/place/exchange_building.ex @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: MIT -defmodule Diffo.Test.ExchangeBuilding do +defmodule Diffo.Test.Place.ExchangeBuilding do @moduledoc """ Diffo - TMF Service and Resource Management with a difference @@ -60,7 +60,7 @@ defmodule Diffo.Test.ExchangeBuilding do end parties do - role :operator, Diffo.Test.Carrier + role :operator, Diffo.Test.Party.Carrier end end end diff --git a/test/support/resource/geographic_site.ex b/test/support/resource/place/geographic_site.ex similarity index 90% rename from test/support/resource/geographic_site.ex rename to test/support/resource/place/geographic_site.ex index 614964e..41e5be2 100644 --- a/test/support/resource/geographic_site.ex +++ b/test/support/resource/place/geographic_site.ex @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: MIT -defmodule Diffo.Test.GeographicSite do +defmodule Diffo.Test.Place.GeographicSite do @moduledoc """ Diffo - TMF Service and Resource Management with a difference @@ -44,7 +44,7 @@ defmodule Diffo.Test.GeographicSite do end parties do - role :managed_by, Diffo.Test.Organization + role :managed_by, Diffo.Test.Party.Organization end places do diff --git a/test/support/resource/shelf_value.ex b/test/support/resource/shelf_value.ex deleted file mode 100644 index 0cdbc0e..0000000 --- a/test/support/resource/shelf_value.ex +++ /dev/null @@ -1,37 +0,0 @@ -# SPDX-FileCopyrightText: 2025 diffo contributors -# -# SPDX-License-Identifier: MIT - -defmodule Diffo.Test.ShelfValue do - @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - ShelfValue - AshTyped Struct for Shelf Characteristic Value - """ - use Ash.TypedStruct, extensions: [AshJason.TypedStruct, AshOutstanding.TypedStruct] - - jason do - pick [:name, :family, :model, :technology] - compact true - end - - outstanding do - expect [:name] - end - - typed_struct do - field :name, :string, description: "the shelf name" - - field :family, :atom, description: "the shelf family name" - - field :model, :string, description: "the shelf model name" - - field :technology, :atom, description: "the shelf technology" - end - - defimpl String.Chars do - def to_string(struct) do - inspect(struct) - end - end -end diff --git a/test/support/servo.ex b/test/support/servo.ex index 0121a5b..20acdf6 100644 --- a/test/support/servo.ex +++ b/test/support/servo.ex @@ -12,10 +12,13 @@ defmodule Diffo.Test.Servo do otp_app: :diffo, validate_config_inclusion?: false - alias Diffo.Test.Shelf - alias Diffo.Test.Card - alias Diffo.Test.Broadband - alias Diffo.Test.BroadbandV2 + alias Diffo.Test.Instance.Shelf + alias Diffo.Test.Instance.Card + alias Diffo.Test.Instance.Broadband + alias Diffo.Test.Instance.BroadbandV2 + alias Diffo.Test.Characteristic.Shelf, as: ShelfCharacteristic + alias Diffo.Test.Characteristic.Card, as: CardCharacteristic + alias Diffo.Test.Characteristic.DeploymentClass domain do description "service and resource management" @@ -47,5 +50,9 @@ defmodule Diffo.Test.Servo do define :build_broadband_v2, action: :build define :get_broadband_v2_by_id, action: :read, get_by: :id end + + resource ShelfCharacteristic + resource CardCharacteristic + resource DeploymentClass end end diff --git a/test/type/dynamic_test.exs b/test/type/dynamic_test.exs index 2778b52..f30dfec 100644 --- a/test/type/dynamic_test.exs +++ b/test/type/dynamic_test.exs @@ -8,7 +8,7 @@ defmodule Diffo.Type.DynamicTest do use Outstand alias Diffo.Type.Dynamic alias Diffo.Test.Patch - alias Diffo.Test.CardValue + alias Diffo.Test.Characteristic.Card, as: CardValue describe "dynamic type validation" do test "cast_input rejects non-NewType scalar Ash type" do From a8a04c5f281d92d4a74f5adcf25ec60c4bc1ff34 Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Sun, 17 May 2026 00:07:14 +0930 Subject: [PATCH 2/2] docs and guidance --- AGENTS.md | 8 +- .../use_diffo_provider_extension.livemd | 248 +++++++++++------- usage-rules.md | 70 ++++- 3 files changed, 229 insertions(+), 97 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 130e77b..37da489 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,6 +40,9 @@ lib/diffo/provider/ base_party.ex # Ash Fragment for Party resources base_place.ex # Ash Fragment for Place resources components/ + base_characteristic.ex # Ash Fragment for typed characteristic resources + calculations/ + characteristic_value.ex # Calculation: builds .Value TypedStruct from record fields instance/extension.ex # Thin marker (sections: []) — kind identification party/extension.ex # Thin marker place/extension.ex # Thin marker @@ -82,8 +85,8 @@ provider do end characteristics do - characteristic :slot_value, MyApp.SlotValue - characteristic :ports, {:array, MyApp.Port} + characteristic :slot_value, MyApp.SlotCharacteristic + characteristic :ports, {:array, MyApp.PortCharacteristic} end features do @@ -144,6 +147,7 @@ mix test --max-failures 5 # stop early - Using old `structure do` / top-level `instances do` — use `provider do` only. - Using `party :role, Type, reference: true` — use `party_ref :role, Type`. +- Using a plain `Ash.TypedStruct` as a `characteristic` DSL target — use a `BaseCharacteristic`-derived resource instead; the TypedStruct belongs in `.Value`. - Calling `build_before/1` or `build_after/2` in actions — these run automatically. - Declaring `:specified_by`, `:features`, `:characteristics` as action arguments. - Editing `documentation/dsls/DSL-Diffo.Provider.Extension.md` — it is Spark-generated; diff --git a/documentation/how_to/use_diffo_provider_extension.livemd b/documentation/how_to/use_diffo_provider_extension.livemd index 7870bde..954e651 100644 --- a/documentation/how_to/use_diffo_provider_extension.livemd +++ b/documentation/how_to/use_diffo_provider_extension.livemd @@ -137,7 +137,7 @@ The `provider do` section contains: **`specification do`** — the TMF Specification (id, name, type, version, description, category). The id is a stable UUID4, the same in every environment for this Instance kind. -**`characteristics do`** — typed value slots backed by `Ash.TypedStruct`. +**`characteristics do`** — typed value slots backed by `Diffo.Provider.BaseCharacteristic`-derived resources. **`features do`** — optional capabilities with their own typed characteristic payload. @@ -149,7 +149,7 @@ The id is a stable UUID4, the same in every environment for this Instance kind. Declaring `create :name` injects `:specified_by`, `:features`, and `:characteristics` arguments automatically onto that action. -Feature and Instance Characteristics can have payloads defined by [Ash.TypedStruct](https://hexdocs.pm/ash/Ash.TypedStruct.html). TypedStruct are DSL specified types which are effectively lightweight embedded resources. We've extended both [AshJason](https://hexdocs.pm/ash_jason/) and [AshOutstanding](https://hexdocs.pm/ash_outstanding/) to support Ash.TypedStruct. +Each characteristic is a dedicated Ash resource using the `Diffo.Provider.BaseCharacteristic` fragment. It carries direct typed attributes and a `:value` calculation that builds a companion `.Value` TypedStruct for ordered JSON encoding. The TypedStruct uses [AshJason.TypedStruct](https://hexdocs.pm/ash_jason/) to control field order in the JSON output. For partial resource allocation and assignment we've created Diffo.Provider.Assigner. It is used by the host resource, which declares a characteristic with a `Diffo.Provider.AssignableValue` TypedStruct. Allocation is managed within the Provider domain using this characteristic. Assignment to Services or Resources is via 'reverse' type: "assignedTo" relationships enriched by relationship characteristics. @@ -165,6 +165,73 @@ We'll define all the resources first, then declare the `Diffo.Compute` domain on We will start by declaring the Cluster Resource. It is going to be a composite resource, where it can be assigned individual GPU and NPU cores via resource relationships. It is an Ash.Resource incorporating the `Diffo.Provider.BaseInstance` fragment. +First we define the `ClusterCharacteristic` typed resource and its companion `Value` TypedStruct: + +```elixir +defmodule Diffo.Compute.ClusterCharacteristic do + @moduledoc "Typed characteristic carrying cluster composition fields." + use Ash.Resource, + fragments: [Diffo.Provider.BaseCharacteristic], + domain: Diffo.Compute + + resource do + plural_name :cluster_characteristics + end + + attributes do + attribute :gpu_cores, :integer, public?: true, default: 0, constraints: [min: 0] + attribute :npu_cores, :integer, public?: true, default: 0, constraints: [min: 0] + end + + calculations do + calculate :value, Diffo.Type.CharacteristicValue, + Diffo.Provider.Calculations.CharacteristicValue do + public? true + end + end + + actions do + create :create do + accept [:name, :gpu_cores, :npu_cores] + argument :instance_id, :uuid + argument :feature_id, :uuid + change manage_relationship(:instance_id, :instance, type: :append) + change manage_relationship(:feature_id, :feature, type: :append) + end + + update :update do + accept [:gpu_cores, :npu_cores] + end + end + + preparations do + prepare build(load: [:value]) + end + + jason do + pick [:name, :value] + compact true + end +end + +defmodule Diffo.Compute.ClusterCharacteristic.Value do + @moduledoc "Value struct for ClusterCharacteristic — controls JSON field order." + use Ash.TypedStruct, extensions: [AshJason.TypedStruct] + + typed_struct do + field :gpu_cores, :integer + field :npu_cores, :integer + end + + jason do + pick [:gpu_cores, :npu_cores] + compact true + end +end +``` + +Now the Cluster resource itself. It declares `ClusterCharacteristic` as the `:cluster` characteristic — updates to it are made directly on the characteristic resource, so no `update :define` is needed here: + ```elixir defmodule Diffo.Compute.Cluster do @moduledoc """ @@ -173,9 +240,8 @@ defmodule Diffo.Compute.Cluster do alias Diffo.Provider.BaseInstance alias Diffo.Provider.Instance.Relationship - alias Diffo.Provider.Instance.Characteristic alias Diffo.Compute - alias Diffo.Compute.ClusterValue + alias Diffo.Compute.ClusterCharacteristic alias Diffo.Compute.Tenant alias Diffo.Compute.Engineer @@ -198,7 +264,7 @@ defmodule Diffo.Compute.Cluster do end characteristics do - characteristic :cluster, ClusterValue + characteristic :cluster, ClusterCharacteristic end parties do @@ -230,17 +296,6 @@ defmodule Diffo.Compute.Cluster do upsert? false end - update :define do - description "defines the cluster" - argument :characteristic_value_updates, {:array, :term} - - change after_action(fn changeset, result, _context -> - with {:ok, result} <- Characteristic.update_values(result, changeset), - {:ok, cluster} <- Compute.get_cluster_by_id(result.id), - do: {:ok, cluster} - end) - end - update :relate do description "relates the cluster with other instances" argument :relationships, {:array, :struct} @@ -255,49 +310,78 @@ defmodule Diffo.Compute.Cluster do end ``` -And of course we'll need a ClusterValue TypedStruct for the Cluster Resource's cluster characteristic: +### Using the Assigner + +We'll now define a GPU Resource which uses the `Diffo.Provider.Assigner` functionality. + +First define the `GpuCharacteristic` typed resource and its `Value` TypedStruct: ```elixir -defmodule Diffo.Compute.ClusterValue do - @moduledoc """ - AshTyped Struct for Cluster Characteristic Value - """ - use Ash.TypedStruct, extensions: [AshJason.TypedStruct, AshOutstanding.TypedStruct] +defmodule Diffo.Compute.GpuCharacteristic do + @moduledoc "Typed characteristic carrying GPU identity fields." + use Ash.Resource, + fragments: [Diffo.Provider.BaseCharacteristic], + domain: Diffo.Compute - jason do - pick [:name, :gpu_cores, :npu_cores] - compact true + resource do + plural_name :gpu_characteristics end - outstanding do - expect [:gpu_cores] + attributes do + attribute :family, :atom, public?: true, description: "the GPU family name" + attribute :model, :string, public?: true, description: "the GPU model name" + attribute :technology, :atom, public?: true, description: "the GPU technology" end - typed_struct do - field :name, :string, description: "the cluster name" + calculations do + calculate :value, Diffo.Type.CharacteristicValue, + Diffo.Provider.Calculations.CharacteristicValue do + public? true + end + end + + actions do + create :create do + accept [:name, :family, :model, :technology] + argument :instance_id, :uuid + argument :feature_id, :uuid + change manage_relationship(:instance_id, :instance, type: :append) + change manage_relationship(:feature_id, :feature, type: :append) + end - field :gpu_cores, :integer, - default: 0, - constraints: [min: 0], - description: "the number of GPU cores in the cluster" + update :update do + accept [:family, :model, :technology] + end + end - field :npu_cores, :integer, - default: 0, - constraints: [min: 0], - description: "the number of NPU cores in the cluster" + preparations do + prepare build(load: [:value]) end - defimpl String.Chars do - def to_string(struct) do - inspect(struct) - end + jason do + pick [:name, :value] + compact true end end -``` -### Using the Assigner +defmodule Diffo.Compute.GpuCharacteristic.Value do + @moduledoc "Value struct for GpuCharacteristic — controls JSON field order." + use Ash.TypedStruct, extensions: [AshJason.TypedStruct] -We'll now define a GPU Resource which uses the `Diffo.Provider.Assigner` functionality. + typed_struct do + field :family, :atom + field :model, :string + field :technology, :atom + end + + jason do + pick [:family, :model, :technology] + compact true + end +end +``` + +The GPU resource declares `GpuCharacteristic` for the typed `:gpu` slot and keeps `AssignableValue` for the `:cores` allocation pool (the assigner still uses the dynamic characteristic pattern). The `update :define` action now only needs to handle the dynamic `:cores` update — the typed `:gpu` characteristic is updated directly on the characteristic resource: ```elixir defmodule Diffo.Compute.GPU do @@ -312,7 +396,7 @@ defmodule Diffo.Compute.GPU do alias Diffo.Provider.Assignment alias Diffo.Provider.AssignableValue alias Diffo.Compute - alias Diffo.Compute.GPUValue + alias Diffo.Compute.GpuCharacteristic use Ash.Resource, fragments: [BaseInstance], @@ -333,7 +417,7 @@ defmodule Diffo.Compute.GPU do end characteristics do - characteristic :gpu, GPUValue + characteristic :gpu, GpuCharacteristic characteristic :cores, AssignableValue end @@ -358,7 +442,7 @@ defmodule Diffo.Compute.GPU do end update :define do - description "defines the GPU" + description "allocates the GPU cores (AssignableValue)" argument :characteristic_value_updates, {:array, :term} change after_action(fn changeset, result, _context -> @@ -392,41 +476,6 @@ defmodule Diffo.Compute.GPU do end end ``` - -And we must define the GPUValue TypedStruct, used in the GPU's gpu characteristic: - -```elixir -defmodule Diffo.Compute.GPUValue do - @moduledoc """ - AshTyped Struct for GPU Characteristic Value - """ - use Ash.TypedStruct, extensions: [AshJason.TypedStruct, AshOutstanding.TypedStruct] - - jason do - pick [:name, :family, :model, :technology] - compact true - end - - outstanding do - expect [:name] - end - - typed_struct do - field :name, :string, description: "the GPU name" - - field :family, :atom, description: "the GPU family name" - - field :model, :string, description: "the GPU model name" - - field :technology, :atom, description: "the GPU technology" - end - - defimpl String.Chars do - def to_string(struct) do - inspect(struct) - end - end -end ``` ## Party Extension @@ -579,8 +628,10 @@ defmodule Diffo.Compute do validate_config_inclusion?: false alias Diffo.Compute.GPU + alias Diffo.Compute.GpuCharacteristic #alias Diffo.Compute.NPU alias Diffo.Compute.Cluster + alias Diffo.Compute.ClusterCharacteristic alias Diffo.Compute.Tenant alias Diffo.Compute.Engineer alias Diffo.Compute.DataCentre @@ -594,6 +645,10 @@ defmodule Diffo.Compute do define :assign_gpu_core, action: :assign_core end + resource GpuCharacteristic do + define :update_gpu_characteristic, action: :update + end + #resource NPU do #define :get_npu_by_id, action: :read, get_by: :id #define :build_npu, action: :build @@ -605,10 +660,13 @@ defmodule Diffo.Compute do resource Cluster do define :get_cluster_by_id, action: :read, get_by: :id define :build_cluster, action: :build - define :define_cluster, action: :define define :relate_cluster, action: :relate end + resource ClusterCharacteristic do + define :update_cluster_characteristic, action: :update + end + resource Tenant do define :create_tenant, action: :build define :get_tenant_by_id, action: :read, get_by: :id @@ -687,19 +745,27 @@ gpu_1 = Compute.build_gpu!(%{name: "GPU 1"}) gpu_2 = Compute.build_gpu!(%{name: "GPU 2"}) ``` -We need to define each GPU instance, in this case defining the gpu Characteristic AssignableValue performs the allocation - in this case setting how many GPU cores are available. +We set the typed `:gpu` characteristic directly on the characteristic resource, then allocate the `:cores` AssignableValue via `update :define`: ```elixir -updates = [ - gpu: [family: :nvidia, model: "GeForce RTX5090", technology: :blackwell], - cores: [first: 1, last: 680, free: 680, assignable_type: "tensor"] -] +# Update the typed GpuCharacteristic on each GPU +[gpu_char_1] = Enum.filter(gpu_1.characteristics, fn c -> c.name == :gpu end) +[gpu_char_2] = Enum.filter(gpu_2.characteristics, fn c -> c.name == :gpu end) + +gpu_attrs = %{family: :nvidia, model: "GeForce RTX5090", technology: :blackwell} +Compute.update_gpu_characteristic!(gpu_char_1, gpu_attrs) +Compute.update_gpu_characteristic!(gpu_char_2, gpu_attrs) +``` + +```elixir +# Allocate the cores pool (AssignableValue — dynamic characteristic) +core_updates = [cores: [first: 1, last: 680, free: 680, assignable_type: "tensor"]] -gpu_1 = Compute.define_gpu!(gpu_1, %{characteristic_value_updates: updates}) -gpu_2 = Compute.define_gpu!(gpu_2, %{characteristic_value_updates: updates}) +gpu_1 = Compute.define_gpu!(gpu_1, %{characteristic_value_updates: core_updates}) +gpu_2 = Compute.define_gpu!(gpu_2, %{characteristic_value_updates: core_updates}) ``` -The GPU's core characteristic is an AssignableValue, now we've allocated it we can use it to keep track of how many cores are free (unassigned). We can render one as json: +The GPU's `:cores` characteristic is an AssignableValue that tracks how many cores are free (unassigned). We can render one as json: ```elixir Jason.encode!(gpu_1, pretty: true) |> IO.puts diff --git a/usage-rules.md b/usage-rules.md index 2a9e034..beac8e4 100644 --- a/usage-rules.md +++ b/usage-rules.md @@ -67,15 +67,77 @@ end ### `characteristics do` — Instance only -Declares typed value slots. Each characteristic is backed by an `Ash.TypedStruct`. Do **not** +Declares typed value slots. Each characteristic is a `Diffo.Provider.BaseCharacteristic`-derived +Ash resource with direct typed attributes. A companion `.Value` TypedStruct (using +`AshJason.TypedStruct`) drives ordered JSON encoding via a `:value` calculation. Do **not** add plain Ash attributes for data that belongs in a characteristic. ```elixir provider do characteristics do - characteristic :downstream_speed, MyApp.Speed - characteristic :access_technology, MyApp.AccessTechnology - characteristic :ports, {:array, MyApp.Port} + characteristic :downstream_speed, MyApp.SpeedCharacteristic + characteristic :access_technology, MyApp.AccessTechnologyCharacteristic + characteristic :ports, {:array, MyApp.PortCharacteristic} + end +end +``` + +Each characteristic module uses `Diffo.Provider.BaseCharacteristic` as a fragment and declares +its own attributes, a `:value` calculation, and create/update actions: + +```elixir +defmodule MyApp.SpeedCharacteristic do + use Ash.Resource, + fragments: [Diffo.Provider.BaseCharacteristic], + domain: MyApp.Domain + + attributes do + attribute :downstream_mbps, :integer, public?: true + attribute :upstream_mbps, :integer, public?: true + end + + calculations do + calculate :value, Diffo.Type.CharacteristicValue, + Diffo.Provider.Calculations.CharacteristicValue do + public? true + end + end + + actions do + create :create do + accept [:name, :downstream_mbps, :upstream_mbps] + argument :instance_id, :uuid + argument :feature_id, :uuid + change manage_relationship(:instance_id, :instance, type: :append) + change manage_relationship(:feature_id, :feature, type: :append) + end + + update :update do + accept [:downstream_mbps, :upstream_mbps] + end + end + + preparations do + prepare build(load: [:value]) + end + + jason do + pick [:name, :value] + compact true + end +end + +defmodule MyApp.SpeedCharacteristic.Value do + use Ash.TypedStruct, extensions: [AshJason.TypedStruct] + + typed_struct do + field :downstream_mbps, :integer + field :upstream_mbps, :integer + end + + jason do + pick [:downstream_mbps, :upstream_mbps] + compact true end end ```