diff --git a/AGENTS.md b/AGENTS.md index 3a359a0..615dc1b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -331,8 +331,9 @@ not. Add any useful hypotheses as a follow-up comment on the issue, then leave i - Using `characteristic :name, Diffo.Provider.AssignableCharacteristic` for pools — use `pools do / pool :name, :thing / end` instead. - Using the removed `AssignableValue` TypedStruct — it no longer exists; use `pools do`. - Calling `Assigner.assign/4` when a `pools do` declaration exists — prefer `Assigner.assign/3` which looks up the thing automatically. -- Forgetting to call `Pool.update_pools/3` in `:define` actions when the resource has `pools do` — pool bounds (`first`, `last`, `algorithm`) are set here. -- Calling `Assigner.assign/3` on an instance that is not in the correct lifecycle state — the assigner enforces: resource instances must have `resource_state: :operating`; service instances must have `service_state: :active` or `:inactive`. Lifecycle state transitions are an internal domain concern managed by the provider; assignment actions are external-facing. Future: consumer reads may filter out non-`:operating` resources entirely. +- Hand-writing the `:define` / `:relate` / `:assign_*` after-action plumbing — use `Diffo.Provider.Changes.Define`, `Diffo.Provider.Changes.Relate`, and `{Diffo.Provider.Changes.Assign, pool: :name}` (since 0.4.1). The change modules thread `Characteristic.update_all/3`, `Pool.update_pools/3`, `Relationship.relate_instance/2` and `Assigner.assign/3` together and reload via the resource's primary `:read` action. +- Hand-writing the `:create` / `:update` accept lists on a `BaseCharacteristic`-derived resource — they are synthesised from the resource's public attributes (since 0.4.1). Declare your own only when you need a narrower accept list. +- Calling `Assigner.assign/3` on an instance that is not in the correct lifecycle state — the assigner enforces: resource instances must have `resource_state` of `:installing` or `:operating`; service instances must have `service_state` of `:feasibilityChecked`, `:reserved`, `:inactive`, `:active`, or `:suspended` (since 0.4.1). The full lists are exposed via `Assigner.assignable_resource_states/0` and `Assigner.assignable_service_states/0`. Lifecycle state transitions are an internal domain concern managed by the provider; assignment actions are external-facing. - Wondering why `Relationship` and `AssignmentRelationship` both have an `alias` attribute with a `[:source_id, :alias]` / `[:target_id, :alias]` identity — alias is a "baby name" given to a relationship slot before (or when) the target is bound. Its full purpose becomes clear alongside the first-order expectation system (see issue #122): the expectation declares the alias for a slot it expects to be filled, and the actual relationship carries the same alias so the two can be matched. Without expectations in place, aliases look like optional metadata; with them, they are the join key between intent and fulfilment. - Using `characteristic :pool_name, Diffo.Provider.AssignedToRelationship` — `AssignedToRelationship` no longer exists; use `pools do / pool :name, :thing / end` instead. - Querying `Diffo.Provider.Relationship` for assignment records — assignments are stored as `Diffo.Provider.DefinedSimpleRelationship`; access them via `instance.assignments`. diff --git a/CHANGELOG.md b/CHANGELOG.md index c438506..1760d8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,42 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline +## [v0.4.1](https://github.com/diffo-dev/diffo/compare/v0.4.0...v0.4.1) (2026-05-22) + +### Bug Fixes + +* **Assigner lifecycle** (#168) — broadened the lifecycle states permitted to make assignments. Services may now assign from `:feasibilityChecked`, `:reserved`, `:inactive`, `:active`, or `:suspended` (was `:active` / `:inactive` only). Resources may now assign from `:installing` or `:operating` (was `:operating` only). `Assigner.assignable_state?/1` exposes the policy directly. + +### Features + +* **`Diffo.Provider.Changes.Define` / `Relate` / `Assign`** (#170) — change modules that wrap the standard after-action patterns every Instance consumer writes. Replace the hand-written `after_action` body threading `Characteristic.update_all` / `Pool.update_pools` / `Relationship.relate_instance` / `Assigner.assign` together with a one-liner: + + ```elixir + update :define do + argument :characteristic_value_updates, {:array, :term} + change Diffo.Provider.Changes.Define + end + + update :relate do + argument :relationships, {:array, :struct} + change Diffo.Provider.Changes.Relate + end + + update :assign_port do + argument :assignment, :struct, constraints: [instance_of: Assignment] + change {Diffo.Provider.Changes.Assign, pool: :ports} + end + ``` + + Reload happens via the resource's primary `:read` action, so no consumer-specific reader is needed. +* **BaseCharacteristic auto-generated `:create` / `:update` actions** (#171) — `BaseCharacteristic`-derived resources now get default `:create` and `:update` actions synthesised from their public attributes. `:create` accepts `[:name | ]` with `:instance_id` / `:feature_id` arguments and `manage_relationship` changes; `:update` accepts ``. Consumers may still declare their own actions to override the defaults. +* **Typed characteristics and pools in Instance JSON** (#169) — `BaseInstance` now loads two new calculations (`:typed_characteristics`, `:pool_characteristics`) by default and the jason customize merges their values into the `serviceCharacteristic` / `resourceCharacteristic` array. Typed `BaseCharacteristic` records and `AssignableCharacteristic` pool records that were already present in the graph are now visible at the TMF JSON surface. + +### Notable Changes + +* `Diffo.Provider.Calculations.TypedCharacteristics` and `Diffo.Provider.Calculations.PoolCharacteristics` — new calc modules backing the JSON surfacing for #169. +* Regression test added for #62 (characteristic update validation) — typed `BaseCharacteristic` updates now reject unknown fields and invalid types through Ash's standard changeset machinery. + ## [v0.4.0](https://github.com/diffo-dev/diffo/compare/v0.3.0...v0.4.0) (2026-05-20) ### Breaking Changes diff --git a/lib/diffo/provider/changes/assign.ex b/lib/diffo/provider/changes/assign.ex new file mode 100644 index 0000000..1c55222 --- /dev/null +++ b/lib/diffo/provider/changes/assign.ex @@ -0,0 +1,56 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Changes.Assign do + @moduledoc """ + After-action change for the standard `:assign_*` pattern. + + Performs an assignment for the named pool via `Assigner.assign/3`, then + reloads the result via the resource's primary `:read` action. + + ## Usage + + update :assign_port do + argument :assignment, :struct, constraints: [instance_of: Assignment] + change {Diffo.Provider.Changes.Assign, pool: :ports} + end + + ## Options + + - `:pool` (required) — the pool name (atom) declared via `pools do` on the + consuming instance resource. + """ + use Ash.Resource.Change + + require Ash.Query + + alias Diffo.Provider.Assigner + + @impl true + def init(opts) do + case Keyword.fetch(opts, :pool) do + {:ok, pool} when is_atom(pool) and not is_nil(pool) -> + {:ok, opts} + + _ -> + {:error, "Diffo.Provider.Changes.Assign requires a :pool atom option"} + end + end + + @impl true + def change(changeset, opts, _context) do + pool = Keyword.fetch!(opts, :pool) + + Ash.Changeset.after_action(changeset, fn changeset, result -> + with {:ok, result} <- Assigner.assign(result, changeset, pool) do + id = result.id + + changeset.resource + |> Ash.Query.for_read(:read) + |> Ash.Query.filter(id == ^id) + |> Ash.read_one() + end + end) + end +end diff --git a/lib/diffo/provider/changes/define.ex b/lib/diffo/provider/changes/define.ex new file mode 100644 index 0000000..c33c314 --- /dev/null +++ b/lib/diffo/provider/changes/define.ex @@ -0,0 +1,49 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Changes.Define do + @moduledoc """ + After-action change for the standard `:define` pattern. + + Applies `characteristic_value_updates` to the instance's declared typed and + dynamic characteristics (`Characteristic.update_all/3`) and to its declared + pools (`Pool.update_pools/3`), then reloads the result via the resource's + primary `:read` action. + + ## Usage + + update :define do + argument :characteristic_value_updates, {:array, :term} + change Diffo.Provider.Changes.Define + end + + This replaces the hand-written `after_action` block that threads + `characteristics()`, `pools()`, `Characteristic.update_all/3` and + `Pool.update_pools/3` together on every consumer. + """ + use Ash.Resource.Change + + require Ash.Query + + alias Diffo.Provider.Extension.Characteristic + alias Diffo.Provider.Extension.Pool + + @impl true + def change(changeset, _opts, _context) do + Ash.Changeset.after_action(changeset, fn changeset, result -> + module = changeset.resource + + with {:ok, result} <- Ash.load(result, [:characteristics]), + {:ok, result} <- Characteristic.update_all(result, changeset, module.characteristics()), + {:ok, result} <- Pool.update_pools(result, changeset, module.pools()) do + id = result.id + + module + |> Ash.Query.for_read(:read) + |> Ash.Query.filter(id == ^id) + |> Ash.read_one() + end + end) + end +end diff --git a/lib/diffo/provider/changes/relate.ex b/lib/diffo/provider/changes/relate.ex new file mode 100644 index 0000000..a3dd61f --- /dev/null +++ b/lib/diffo/provider/changes/relate.ex @@ -0,0 +1,39 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Changes.Relate do + @moduledoc """ + After-action change for the standard `:relate` pattern. + + Creates relationships from the `:relationships` argument on the changeset via + `Relationship.relate_instance/2`, then reloads the result via the resource's + primary `:read` action. + + ## Usage + + update :relate do + argument :relationships, {:array, :struct} + change Diffo.Provider.Changes.Relate + end + """ + use Ash.Resource.Change + + require Ash.Query + + alias Diffo.Provider.Instance.Relationship + + @impl true + def change(changeset, _opts, _context) do + Ash.Changeset.after_action(changeset, fn changeset, result -> + with {:ok, result} <- Relationship.relate_instance(result, changeset) do + id = result.id + + changeset.resource + |> Ash.Query.for_read(:read) + |> Ash.Query.filter(id == ^id) + |> Ash.read_one() + end + end) + end +end diff --git a/lib/diffo/provider/components/base_instance.ex b/lib/diffo/provider/components/base_instance.ex index b58f8bc..99eb0bb 100644 --- a/lib/diffo/provider/components/base_instance.ex +++ b/lib/diffo/provider/components/base_instance.ex @@ -235,6 +235,7 @@ defmodule Diffo.Provider.BaseInstance do :features, Instance.derive_feature_list_name(record.type) ) + |> Instance.merge_typed_and_pool_characteristics(record) |> Util.suppress_rename( :characteristics, Instance.derive_characteristic_list_name(record.type) @@ -688,6 +689,8 @@ defmodule Diffo.Provider.BaseInstance do :notes, :features, :characteristics, + :typed_characteristics, + :pool_characteristics, :places, :parties ], @@ -699,5 +702,17 @@ defmodule Diffo.Provider.BaseInstance do calculate :href, :string, Diffo.Provider.Calculations.InstanceHref do description "the inventory href of the service or resource instance" end + + calculate :typed_characteristics, + {:array, :struct}, + Diffo.Provider.Calculations.TypedCharacteristics do + description "typed BaseCharacteristic records declared via the characteristics DSL" + end + + calculate :pool_characteristics, + {:array, :struct}, + Diffo.Provider.Calculations.PoolCharacteristics do + description "AssignableCharacteristic records declared via the pools DSL" + end end end diff --git a/lib/diffo/provider/components/calculations/pool_characteristics.ex b/lib/diffo/provider/components/calculations/pool_characteristics.ex new file mode 100644 index 0000000..796735b --- /dev/null +++ b/lib/diffo/provider/components/calculations/pool_characteristics.ex @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Calculations.PoolCharacteristics do + @moduledoc """ + Loads the `AssignableCharacteristic` pool records associated with an + instance, one per `pool :name, :thing` declaration on the resource module. + + Used by `BaseInstance` to surface pool characteristics alongside the + dynamic `Diffo.Provider.Characteristic` records in the + `serviceCharacteristic` / `resourceCharacteristic` JSON view (#169). + """ + 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 -> + if function_exported?(record.__struct__, :pools, 0) and record.__struct__.pools() != [] do + Diffo.Provider.AssignableCharacteristic + |> Ash.Query.filter_input(instance_id: record.id) + |> Ash.read!(domain: Diffo.Provider) + else + [] + end + end) + end +end diff --git a/lib/diffo/provider/components/calculations/typed_characteristics.ex b/lib/diffo/provider/components/calculations/typed_characteristics.ex new file mode 100644 index 0000000..36ea0ad --- /dev/null +++ b/lib/diffo/provider/components/calculations/typed_characteristics.ex @@ -0,0 +1,56 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Calculations.TypedCharacteristics do + @moduledoc """ + Loads all typed `BaseCharacteristic`-derived records associated with an + instance. + + For each `characteristic :role, ValueModule` declaration on the instance's + resource module — including both singular and `{:array, ValueModule}` forms + — this calculation queries `ValueModule` by `instance_id == record.id` and + returns the collected records as a flat list. + + Used by `BaseInstance` to surface typed characteristics alongside the + dynamic `Diffo.Provider.Characteristic` records in the + `serviceCharacteristic` / `resourceCharacteristic` JSON view (#169). + """ + use Ash.Resource.Calculation + + alias Diffo.Provider.Extension.Characteristic, as: CharacteristicHelper + + @impl true + def load(_query, _opts, _context), do: [] + + @impl true + def calculate(records, _opts, _context) do + Enum.map(records, fn record -> + record + |> typed_value_modules() + |> Enum.flat_map(fn module -> + module + |> Ash.Query.filter_input(instance_id: record.id) + |> Ash.read!(domain: Diffo.Provider) + end) + end) + end + + defp typed_value_modules(record) do + module = record.__struct__ + + if function_exported?(module, :characteristics, 0) do + module.characteristics() + |> Enum.map(fn %{value_type: value_type} -> + case value_type do + {:array, mod} -> mod + mod when is_atom(mod) -> mod + end + end) + |> Enum.uniq() + |> Enum.filter(&CharacteristicHelper.typed?/1) + else + [] + end + end +end diff --git a/lib/diffo/provider/components/characteristic/extension.ex b/lib/diffo/provider/components/characteristic/extension.ex index a5f4046..16561f8 100644 --- a/lib/diffo/provider/components/characteristic/extension.ex +++ b/lib/diffo/provider/components/characteristic/extension.ex @@ -3,6 +3,18 @@ # 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: [] + @moduledoc """ + Marker extension identifying a module as a valid characteristic resource. + + Also synthesises default `:create` and `:update` actions on + `BaseCharacteristic`-derived resources from the resource's declared public + attributes — `:create` accepts `[:name | ]` with + `:instance_id` / `:feature_id` arguments and corresponding + `manage_relationship` changes; `:update` accepts `` only. + Consumers may declare their own `:create` / `:update` actions to override + the synthesised ones. + """ + use Spark.Dsl.Extension, + sections: [], + transformers: [Diffo.Provider.Characteristic.Extension.Transformers.GenerateActions] end diff --git a/lib/diffo/provider/components/characteristic/extension/transformers/generate_actions.ex b/lib/diffo/provider/components/characteristic/extension/transformers/generate_actions.ex new file mode 100644 index 0000000..755b9d5 --- /dev/null +++ b/lib/diffo/provider/components/characteristic/extension/transformers/generate_actions.ex @@ -0,0 +1,71 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Characteristic.Extension.Transformers.GenerateActions do + @moduledoc """ + Synthesises default `:create` and `:update` actions on + `BaseCharacteristic`-derived resources from the resource's declared public + attributes. + + `:create` accepts `[:name | ]` with `:instance_id` and + `:feature_id` `:uuid` arguments and corresponding `manage_relationship` + changes. `:update` accepts `` only. + + Consumers that declare their own `:create` or `:update` action take + precedence — `add_new_action/4` will not replace an existing one. + + System attributes (`:id`, timestamps, the BaseCharacteristic `:name` slot) + are excluded from the synthesised lists; `:name` is included on `:create` + only. + """ + use Spark.Dsl.Transformer + + alias Ash.Resource.Builder + alias Spark.Dsl.Transformer + + # Attributes generated by Ash internals / BaseCharacteristic that must not + # appear in the synthesised accept lists. + @reserved_attrs [:id, :name, :created_at, :updated_at] + + @impl true + def transform(dsl_state) do + public_attrs = consumer_public_attributes(dsl_state) + + with {:ok, dsl_state} <- maybe_add_create(dsl_state, public_attrs) do + maybe_add_update(dsl_state, public_attrs) + end + end + + defp consumer_public_attributes(dsl_state) do + dsl_state + |> Transformer.get_entities([:attributes]) + |> Enum.filter(&(Map.get(&1, :public?) && &1.name not in @reserved_attrs)) + |> Enum.map(& &1.name) + end + + defp maybe_add_create(dsl_state, public_attrs) do + with {:ok, instance_arg} <- Builder.build_action_argument(:instance_id, :uuid), + {:ok, feature_arg} <- Builder.build_action_argument(:feature_id, :uuid), + {:ok, instance_change} <- + Builder.build_action_change( + {Ash.Resource.Change.ManageRelationship, + [argument: :instance_id, relationship: :instance, opts: [type: :append]]} + ), + {:ok, feature_change} <- + Builder.build_action_change( + {Ash.Resource.Change.ManageRelationship, + [argument: :feature_id, relationship: :feature, opts: [type: :append]]} + ) do + Builder.add_new_action(dsl_state, :create, :create, + accept: [:name | public_attrs], + arguments: [instance_arg, feature_arg], + changes: [instance_change, feature_change] + ) + end + end + + defp maybe_add_update(dsl_state, public_attrs) do + Builder.add_new_action(dsl_state, :update, :update, accept: public_attrs) + end +end diff --git a/lib/diffo/provider/components/instance/util.ex b/lib/diffo/provider/components/instance/util.ex index 0e20761..09902ed 100644 --- a/lib/diffo/provider/components/instance/util.ex +++ b/lib/diffo/provider/components/instance/util.ex @@ -124,6 +124,22 @@ defmodule Diffo.Provider.Instance.Util do end end + @doc false + def merge_typed_and_pool_characteristics(result, record) do + typed = Map.get(record, :typed_characteristics) || [] + pool = Map.get(record, :pool_characteristics) || [] + extras = typed ++ pool + + case extras do + [] -> + result + + _ -> + existing = Diffo.Util.get(result, :characteristics) || [] + Diffo.Util.set(result, :characteristics, existing ++ extras) + end + end + @doc false def derive_type(specification_type) do case specification_type do diff --git a/mix.exs b/mix.exs index 5bc13cd..be6dc83 100644 --- a/mix.exs +++ b/mix.exs @@ -6,7 +6,7 @@ defmodule Diffo.MixProject do @moduledoc false use Mix.Project - @version "0.4.0" + @version "0.4.1" @name "Diffo" @description "TMF Service and Resource Manager with a difference" @github_url "https://github.com/diffo-dev/diffo" diff --git a/test/provider/extension/assigner_test.exs b/test/provider/extension/assigner_test.exs index 5e662a2..8969fc8 100644 --- a/test/provider/extension/assigner_test.exs +++ b/test/provider/extension/assigner_test.exs @@ -105,7 +105,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\"}}) + ~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,\"algorithm\":\"lowest\"}}]}) end test "define card" do @@ -131,7 +131,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\"}}) + ~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,\"type\":\"ADSL2+\",\"algorithm\":\"lowest\"}}]}) end test "auto assign port to resource" do @@ -157,7 +157,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\"},\"lifecycleState\":\"operating\",\"resourceRelationship\":[{\"type\":\"assignedTo\",\"resource\":{\"id\":\"#{assignee.id}\",\"href\":\"resourceInventoryManagement/v4/resource/#{assignee.id}\"},\"resourceRelationshipCharacteristic\":[{\"name\":\"port\",\"value\":1}]}]}) + ~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\"},\"lifecycleState\":\"operating\",\"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,\"type\":\"ADSL2+\",\"algorithm\":\"lowest\"}}]}) end test "auto assign two ports to same resource" do @@ -188,7 +188,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\"},\"lifecycleState\":\"operating\",\"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}]}]}) + ~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\"},\"lifecycleState\":\"operating\",\"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,\"type\":\"ADSL2+\",\"algorithm\":\"lowest\"}}]}) end test "specific assignment rejects duplicate request" do @@ -219,7 +219,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\"},\"lifecycleState\":\"operating\",\"resourceRelationship\":[{\"type\":\"assignedTo\",\"resource\":{\"id\":\"#{assignee.id}\",\"href\":\"resourceInventoryManagement/v4/resource/#{assignee.id}\"},\"resourceRelationshipCharacteristic\":[{\"name\":\"port\",\"value\":5}]}]}) + ~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\"},\"lifecycleState\":\"operating\",\"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,\"type\":\"ADSL2+\",\"algorithm\":\"lowest\"}}]}) end test "unassign an auto-assigned port from a resource" do @@ -258,7 +258,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\"},\"lifecycleState\":\"operating\"}) + ~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\"},\"lifecycleState\":\"operating\",\"resourceCharacteristic\":[{\"name\":\"card\",\"value\":{\"family\":\"ISAM\",\"model\":\"EBLT48\",\"technology\":\"adsl2Plus\"}},{\"name\":\"ports\",\"value\":{\"first\":1,\"last\":48,\"type\":\"ADSL2+\",\"algorithm\":\"lowest\"}}]}) end test "auto assign port to resource in :installing state (#168)" do diff --git a/test/support/resource/characteristic/card_characteristic.ex b/test/support/resource/characteristic/card_characteristic.ex index ad9ea36..bb8ef8a 100644 --- a/test/support/resource/characteristic/card_characteristic.ex +++ b/test/support/resource/characteristic/card_characteristic.ex @@ -13,20 +13,6 @@ defmodule Diffo.Test.Characteristic.CardCharacteristic do plural_name :card_values 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 - attributes do attribute :family, :atom, public?: true, description: "the card family name" attribute :model, :string, public?: true, description: "the card model name" diff --git a/test/support/resource/characteristic/deployment_class.ex b/test/support/resource/characteristic/deployment_class.ex index 59f7b6b..7610b5b 100644 --- a/test/support/resource/characteristic/deployment_class.ex +++ b/test/support/resource/characteristic/deployment_class.ex @@ -13,20 +13,6 @@ defmodule Diffo.Test.Characteristic.DeploymentClass do plural_name :deployment_class_values 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 - attributes do attribute :class, :string, public?: true, description: "the deployment class" attribute :mask, :string, public?: true, description: "the mask name" diff --git a/test/support/resource/characteristic/shelf_characteristic.ex b/test/support/resource/characteristic/shelf_characteristic.ex index 3cdfc47..6a3e3e5 100644 --- a/test/support/resource/characteristic/shelf_characteristic.ex +++ b/test/support/resource/characteristic/shelf_characteristic.ex @@ -13,20 +13,6 @@ defmodule Diffo.Test.Characteristic.ShelfCharacteristic do plural_name :shelf_values 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 - attributes do attribute :family, :atom, public?: true, description: "the shelf family name" attribute :model, :string, public?: true, description: "the shelf model name" diff --git a/test/support/resource/instance/access_service.ex b/test/support/resource/instance/access_service.ex index 42f702d..acee2c5 100644 --- a/test/support/resource/instance/access_service.ex +++ b/test/support/resource/instance/access_service.ex @@ -39,20 +39,6 @@ defmodule Diffo.Test.Instance.AccessService do end end - calculations do - calculate :assigner_name, {:array, :string}, - {Diffo.Provider.Calculations.FieldViaAssignedRelationship, [via: [:primary], field: :name]} - - calculate :assigner_names, {:array, :string}, - {Diffo.Provider.Calculations.FieldViaAssignedRelationship, [field: :name]} - - calculate :assigned_port, {:array, :integer}, - {Diffo.Provider.Calculations.FieldFromAssignment, [alias: :primary, field: :value]} - - calculate :all_assignment_values, {:array, :integer}, - {Diffo.Provider.Calculations.FieldFromAssignment, [field: :value]} - end - actions do create :build do accept [:id, :name, :type] @@ -61,4 +47,23 @@ defmodule Diffo.Test.Instance.AccessService do upsert? false end end + + calculations do + calculate :assigner_name, + {:array, :string}, + {Diffo.Provider.Calculations.FieldViaAssignedRelationship, + [via: [:primary], field: :name]} + + calculate :assigner_names, + {:array, :string}, + {Diffo.Provider.Calculations.FieldViaAssignedRelationship, [field: :name]} + + calculate :assigned_port, + {:array, :integer}, + {Diffo.Provider.Calculations.FieldFromAssignment, [alias: :primary, field: :value]} + + calculate :all_assignment_values, + {:array, :integer}, + {Diffo.Provider.Calculations.FieldFromAssignment, [field: :value]} + end end diff --git a/test/support/resource/instance/card_instance.ex b/test/support/resource/instance/card_instance.ex index b0e27c3..eb01721 100644 --- a/test/support/resource/instance/card_instance.ex +++ b/test/support/resource/instance/card_instance.ex @@ -9,11 +9,8 @@ defmodule Diffo.Test.Instance.CardInstance do Card - Card Resource Instance """ alias Diffo.Provider.BaseInstance - alias Diffo.Provider.Instance.Relationship - alias Diffo.Provider.Extension.Characteristic - alias Diffo.Provider.Extension.Pool - alias Diffo.Provider.Assigner alias Diffo.Provider.Assignment + alias Diffo.Provider.Changes alias Diffo.Test.Servo alias Diffo.Test.Characteristic.CardCharacteristic @@ -70,36 +67,19 @@ defmodule Diffo.Test.Instance.CardInstance do update :define do description "defines the card" argument :characteristic_value_updates, {:array, :term} - - change after_action(fn changeset, result, _context -> - with {:ok, result} <- - Characteristic.update_all(result, changeset, characteristics()), - {:ok, result} <- Pool.update_pools(result, changeset, pools()), - {:ok, result} <- Servo.get_card_by_id(result.id), - do: {:ok, result} - end) + change Changes.Define end update :relate do description "relates the card with other instances" argument :relationships, {:array, :struct} - - change after_action(fn changeset, result, _context -> - with {:ok, result} <- Relationship.relate_instance(result, changeset), - {:ok, result} <- Servo.get_card_by_id(result.id), - do: {:ok, result} - end) + change Changes.Relate end update :assign_port do description "relates the card with an instance by assigning a port" argument :assignment, :struct, constraints: [instance_of: Assignment] - - change after_action(fn changeset, result, _context -> - with {:ok, result} <- Assigner.assign(result, changeset, :ports), - {:ok, result} <- Servo.get_card_by_id(result.id), - do: {:ok, result} - end) + change {Changes.Assign, pool: :ports} end end end diff --git a/test/support/resource/instance/shelf_instance.ex b/test/support/resource/instance/shelf_instance.ex index fb8f2ee..4c2af1d 100644 --- a/test/support/resource/instance/shelf_instance.ex +++ b/test/support/resource/instance/shelf_instance.ex @@ -10,11 +10,8 @@ defmodule Diffo.Test.Instance.ShelfInstance do """ alias Diffo.Provider.BaseInstance - alias Diffo.Provider.Instance.Relationship - alias Diffo.Provider.Extension.Characteristic - alias Diffo.Provider.Extension.Pool - alias Diffo.Provider.Assigner alias Diffo.Provider.Assignment + alias Diffo.Provider.Changes alias Diffo.Test.Servo alias Diffo.Test.Characteristic.ShelfCharacteristic alias Diffo.Test.Characteristic.DeploymentClass @@ -84,15 +81,6 @@ defmodule Diffo.Test.Instance.ShelfInstance do end end - calculations do - calculate :linked_target_name, {:array, :string}, - {Diffo.Provider.Calculations.FieldViaRelationship, [alias: :link, field: :name]} - - calculate :assigned_linked_name, {:array, :string}, - {Diffo.Provider.Calculations.FieldViaRelationship, - [type: :assignedTo, alias: :link, field: :name]} - end - actions do create :build do description "creates a new Shelf resource instance for build" @@ -109,36 +97,30 @@ defmodule Diffo.Test.Instance.ShelfInstance do update :define do description "defines the shelf" argument :characteristic_value_updates, {:array, :term} - - change after_action(fn changeset, result, _context -> - with {:ok, result} <- - Characteristic.update_all(result, changeset, characteristics()), - {:ok, result} <- Pool.update_pools(result, changeset, pools()), - {:ok, result} <- Servo.get_shelf_by_id(result.id), - do: {:ok, result} - end) + change Changes.Define end update :relate do description "relates the shelf with cards" argument :relationships, {:array, :struct} - - change after_action(fn changeset, result, _context -> - with {:ok, result} <- Relationship.relate_instance(result, changeset), - {:ok, result} <- Servo.get_shelf_by_id(result.id), - do: {:ok, result} - end) + change Changes.Relate end update :assign_slot do description "relates the shelf with an instance by assigning a slot" argument :assignment, :struct, constraints: [instance_of: Assignment] - - change after_action(fn changeset, result, _context -> - with {:ok, result} <- Assigner.assign(result, changeset, :slots), - {:ok, result} <- Servo.get_shelf_by_id(result.id), - do: {:ok, result} - end) + change {Changes.Assign, pool: :slots} end end + + calculations do + calculate :linked_target_name, + {:array, :string}, + {Diffo.Provider.Calculations.FieldViaRelationship, [alias: :link, field: :name]} + + calculate :assigned_linked_name, + {:array, :string}, + {Diffo.Provider.Calculations.FieldViaRelationship, + [type: :assignedTo, alias: :link, field: :name]} + end end diff --git a/usage-rules.md b/usage-rules.md index 218ae5c..287ddb0 100644 --- a/usage-rules.md +++ b/usage-rules.md @@ -124,7 +124,10 @@ end ``` Each characteristic module uses `Diffo.Provider.BaseCharacteristic` as a fragment and declares -its own attributes, a `:value` calculation, and create/update actions: +its own attributes and a `:value` calculation. Default `:create` and `:update` actions +covering all public attributes (with `:name` on `:create` only and `:instance_id` / +`:feature_id` arguments wired to `manage_relationship`) are synthesised automatically — +declare your own only when you need a narrower accept list: ```elixir defmodule MyApp.SpeedCharacteristic do @@ -144,20 +147,6 @@ defmodule MyApp.SpeedCharacteristic do 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 @@ -296,35 +285,31 @@ end - Each Instance module gets `pools/0` (list of declarations) and `pool/1` (lookup by name) generated at compile time. -In the `:define` action, apply updates for both characteristics and pools: +For the `:define`, `:relate`, and `:assign_*` action patterns use the bundled change +modules. They wrap the standard after-action plumbing and reload via the resource's +primary `:read` action — no per-domain reader is required: ```elixir update :define do argument :characteristic_value_updates, {:array, :term} - - change after_action(fn changeset, result, _context -> - with {:ok, result} <- Characteristic.update_all(result, changeset, characteristics()), - {:ok, result} <- Pool.update_pools(result, changeset, pools()), - {:ok, result} <- MyDomain.get_by_id(result.id), - do: {:ok, result} - end) + change Diffo.Provider.Changes.Define end -``` -In assignment actions, use `Assigner.assign/3` (thing is looked up from the pool declaration): +update :relate do + argument :relationships, {:array, :struct} + change Diffo.Provider.Changes.Relate +end -```elixir update :assign_core do argument :assignment, :struct, constraints: [instance_of: Assignment] - - change after_action(fn changeset, result, _context -> - with {:ok, result} <- Assigner.assign(result, changeset, :cores), - {:ok, result} <- MyDomain.get_by_id(result.id), - do: {:ok, result} - end) + change {Diffo.Provider.Changes.Assign, pool: :cores} end ``` +If you need to do more than the standard pattern, the underlying helpers +(`Characteristic.update_all/3`, `Pool.update_pools/3`, `Relationship.relate_instance/2`, +`Assigner.assign/3`) remain available for a hand-written `after_action`. + ### `relationships do` — Instance only Declares which relationship roles this Instance kind may participate in as a **source** or