From bb4c60fd30150da288405cc8a427d213aeb5a938 Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Sun, 26 Apr 2026 19:49:54 +0930 Subject: [PATCH 01/22] base party and related DSL and livebook --- .vscode/launch.json | 34 -- .../DSL-Diffo.Provider.Instance.Extension.md | 50 +++ .../DSL-Diffo.Provider.Party.Extension.md | 108 +++++ ...-Diffo.Provider.Party.Extension.md.license | 3 + ...md => use_diffo_provider_extension.livemd} | 386 +++++++++++++----- lib/diffo/provider/components/base_party.ex | 136 ++++++ .../provider/components/instance/extension.ex | 40 +- .../components/instance/extension/info.ex | 2 +- .../instance/extension/party_declaration.ex | 17 + .../provider/components/party/extension.ex | 74 ++++ .../components/party/extension/info.ex | 9 + .../party/extension/instance_role.ex | 17 + .../components/party/extension/party_role.ex | 17 + lib/diffo/repo.ex | 6 +- mix.exs | 5 +- test/instance_extension/party_test.exs | 90 ++++ test/support/nbn.ex | 35 ++ test/support/resource/organisation.ex | 27 ++ test/support/resource/person.ex | 27 ++ test/support/resource/shelf.ex | 5 + 20 files changed, 936 insertions(+), 152 deletions(-) delete mode 100644 .vscode/launch.json create mode 100644 documentation/dsls/DSL-Diffo.Provider.Party.Extension.md create mode 100644 documentation/dsls/DSL-Diffo.Provider.Party.Extension.md.license rename documentation/how_to/{use_diffo_provider_instance_extension.livemd => use_diffo_provider_extension.livemd} (76%) create mode 100644 lib/diffo/provider/components/base_party.ex create mode 100644 lib/diffo/provider/components/instance/extension/party_declaration.ex create mode 100644 lib/diffo/provider/components/party/extension.ex create mode 100644 lib/diffo/provider/components/party/extension/info.ex create mode 100644 lib/diffo/provider/components/party/extension/instance_role.ex create mode 100644 lib/diffo/provider/components/party/extension/party_role.ex create mode 100644 test/instance_extension/party_test.exs create mode 100644 test/support/nbn.ex create mode 100644 test/support/resource/organisation.ex create mode 100644 test/support/resource/person.ex diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 4a9df11..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,34 +0,0 @@ -// SPDX-FileCopyrightText: 2025 diffo contributors -// -// SPDX-License-Identifier: MIT -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - - - { - "type": "mix_task", - "name": "mix (Default task)", - "request": "launch", - "projectDir": "${workspaceRoot}" - }, - { - "type": "mix_task", - "name": "mix test", - "request": "launch", - "task": "test", - "taskArgs": [ - "--trace" - ], - "startApps": true, - "projectDir": "${workspaceRoot}", - "requireFiles": [ - "test/**/test_helper.exs", - "test/**/*_test.exs" - ] - } - ] -} \ No newline at end of file diff --git a/documentation/dsls/DSL-Diffo.Provider.Instance.Extension.md b/documentation/dsls/DSL-Diffo.Provider.Instance.Extension.md index b1685af..4c19110 100644 --- a/documentation/dsls/DSL-Diffo.Provider.Instance.Extension.md +++ b/documentation/dsls/DSL-Diffo.Provider.Instance.Extension.md @@ -183,5 +183,55 @@ Target: `Diffo.Provider.Instance.Characteristic` +## parties +List of Instance Party roles + +### Nested DSLs + * [party](#parties-party) + + +### Examples +``` +parties do + party :facilitated_by, MyApp.Rsp + party :overseen_by, MyApp.Person +end + +``` + + + + +### parties.party +```elixir +party role, party_type +``` + + +Declares a party role on this Instance + + + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`role`](#parties-party-role){: #parties-party-role .spark-required} | `atom` | | The role name, an atom | +| [`party_type`](#parties-party-party_type){: #parties-party-party_type } | `any` | | The module of the Party kind. An atom module name such as a BaseParty-derived resource. | + + + + + + +### Introspection + +Target: `Diffo.Provider.Instance.Extension.PartyDeclaration` + + + + diff --git a/documentation/dsls/DSL-Diffo.Provider.Party.Extension.md b/documentation/dsls/DSL-Diffo.Provider.Party.Extension.md new file mode 100644 index 0000000..eb8d110 --- /dev/null +++ b/documentation/dsls/DSL-Diffo.Provider.Party.Extension.md @@ -0,0 +1,108 @@ + +# Diffo.Provider.Party.Extension + +DSL Extension customising a Party + + +## instance +Declares the roles this Party kind plays with respect to Instances + +### Nested DSLs + * [role](#instance-role) + + +### Examples +``` +instance do + role :facilitates, MyApp.AccessService +end + +``` + + + + +### instance.role +```elixir +role role, party_type +``` + + +Declares a role this Party kind plays + + + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`role`](#instance-role-role){: #instance-role-role .spark-required} | `atom` | | The role name, an atom | +| [`party_type`](#instance-role-party_type){: #instance-role-party_type } | `any` | | The module of the related resource | + + + + + + +### Introspection + +Target: `Diffo.Provider.Party.Extension.InstanceRole` + + + + +## party +Declares the roles this Party kind plays with respect to other Parties + +### Nested DSLs + * [role](#party-role) + + +### Examples +``` +party do + role :managed_by, MyApp.Person +end + +``` + + + + +### party.role +```elixir +role role, party_type +``` + + +Declares a role this Party kind plays with respect to other Parties + + + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`role`](#party-role-role){: #party-role-role .spark-required} | `atom` | | The role name, an atom | +| [`party_type`](#party-role-party_type){: #party-role-party_type } | `any` | | The module of the related Party kind | + + + + + + +### Introspection + +Target: `Diffo.Provider.Party.Extension.PartyRole` + + + + + + diff --git a/documentation/dsls/DSL-Diffo.Provider.Party.Extension.md.license b/documentation/dsls/DSL-Diffo.Provider.Party.Extension.md.license new file mode 100644 index 0000000..b381ebe --- /dev/null +++ b/documentation/dsls/DSL-Diffo.Provider.Party.Extension.md.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2025 diffo contributors + +SPDX-License-Identifier: MIT diff --git a/documentation/how_to/use_diffo_provider_instance_extension.livemd b/documentation/how_to/use_diffo_provider_extension.livemd similarity index 76% rename from documentation/how_to/use_diffo_provider_instance_extension.livemd rename to documentation/how_to/use_diffo_provider_extension.livemd index 45348f3..d6cfc03 100644 --- a/documentation/how_to/use_diffo_provider_instance_extension.livemd +++ b/documentation/how_to/use_diffo_provider_extension.livemd @@ -4,12 +4,15 @@ SPDX-FileCopyrightText: 2025 diffo contributors -# Using the Diffo Provider Instance Extension +# Using the Diffo Provider Extension ```elixir Mix.install( [ - {:diffo, "~> 0.2.0"} + {:diffo, "~> 0.2.0"}, + ], + config: [ + diffo: [ash_domains: [Diffo.Provider]] ], consolidate_protocols: false ) @@ -19,18 +22,19 @@ Mix.install( Diffo is a Telecommunications Management Forum (TMF) Service and Resource Manager, built for autonomous networks. -It is implemented using the [Ash Framework](https://www.ash-hq.org) leveraging core and community extensions including some created and maintained by [diffo-dev](https://github.com/diffo-dev/). As such it is highly customizable using Spark DSL and as necessary Elixir. +It is implemented using the [Ash Framework](https://www.ash-hq.org) leveraging core and community extensions including some created and maintained by [diffo-dev](https://github.com/diffo-dev/). As such it is highly customizable using Spark DSL and as necessary Elixir. If you are not already familiar with Ash then please explore [Ash Get Started](https://hexdocs.pm/ash/get-started.html) First ensure you've explored the Diffo Livebook for an introduction to Diffo: [![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2Fdiffo%2Ddev%2Fdiffo%2Fblob%2Fdev%2Fdiffo.livemd) -In this 'Diffo Provider Instance Extension' livebook you will learn about: +In this 'Diffo Provider Extension' livebook you will learn about: * TMF Services and Resources * Building your own Domain -* Declaring a Composite Resource +* Declaring a Composite Resource using the Instance Extension * Using the Assigner * Composing a Resource from partially assigned Resources +* Declaring domain Parties using the Party Extension ### Installing Neo4j and Configuring Bolty @@ -116,15 +120,7 @@ In all cases the assignment is only successful if the Provider allows the reques Partial resource assignment uses a relationship characteristic to indicate which part of the resource is optionally requested and ultimately assigned. -```elixir -Diffo.Provider.list_instances!() -``` - -## Building your own Domain - -Diffo is an open source, extensible, declarative, intent driven TMF Service and Resource Manager. - -In the Introductory Livebook we used the Diffo.Provider domain API and created basic TMF Services and Resources. While this is OK for getting to know Diffo, we actually want to build/declare domain specific functionality which means we will build our own [Ash.Domain](https://hexdocs.pm/ash/Ash.Domain.html). +## Instance Extension Diffo.Provider.Instance modelling a Service or Resource actually uses the Diffo.Provider.BaseInstance [Spark.Dsl.Fragment](https://hexdocs.pm/spark/Spark.Dsl.Fragment.html). There is no need to evaluate the Diffo.Provider.Instance below, it is already defined. @@ -150,7 +146,7 @@ end Diffo also has an inbuilt Spark DSL extension [Diffo.Provider.Instance.Extension](https://hexdocs.pm/diffo/Diffo.Provider.Instance.Extension.html) which provides DSL and functions for use in building domain specific services and resources. -Currently it has DSL to allow you to declare specification, features and characteristics. It can be used for services or resources. +Currently it has DSL to allow you to declare specification, features, characteristics, and party roles. It can be used for services or resources. 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. For partial resource allocation and assignment we've created Diffo.Provider.Assigner. It is used by the host resource, which declares a characteristic with an 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. @@ -159,57 +155,15 @@ We can still use the Diffo.Provider API's noting that they will return Diffo.Pro Let's imagine a Compute domain which operates GPU and NPU resources. We want to expose a Cluster composite resource which can be dynamically composed of a number of GPU and NPU cores. -Each instance of Cluster could be created on Consumer demand as a 'container' for the GPI and NPU core partial resources. +Each instance of Cluster could be created on Consumer demand as a 'container' for the GPU and NPU core partial resources. -Each of the GPU and NPU Resource instances is created and managed by the Provider and is effectively a resource pool for individualy assignable cores. +Each of the GPU and NPU Resource instances is created and managed by the Provider and is effectively a resource pool for individually assignable cores. -We'll define these Resources in a Compute domain which exposes an API. +We'll define all the resources first, then declare the `Diffo.Compute` domain once they are all compiled — Ash validates `code_interface` at domain compile time so all referenced resources must exist first. -```elixir -defmodule Diffo.Compute do - @moduledoc """ - Diffo - TMF Service and Resource Management with a difference +### Declaring a Composite Resource - Compute - example domain - """ - use Ash.Domain, - otp_app: :diffo, - validate_config_inclusion?: false - - alias Diffo.Compute.GPU - #alias Diffo.Compute.NPU - alias Diffo.Compute.Cluster - - resources do - resource GPU do - define :get_gpu_by_id, action: :read, get_by: :id - define :build_gpu, action: :build - define :define_gpu, action: :define - define :relate_gpu, action: :relate - define :assign_gpu_core, action: :assign_core - end - - #resource NPU do - #define :get_npu_by_id, action: :read, get_by: :id - #define :build_npu, action: :build - #define :define_npu, action: :define - #define :relate_npu, action: :relate - #define :assign_npu_core, action: :assign_core - #end - - 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 - end -end -``` - -## Declaring a Composite Resource - -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.Provide.BaseInstance fragment. +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. ```elixir defmodule Diffo.Compute.Cluster do @@ -225,6 +179,8 @@ defmodule Diffo.Compute.Cluster do alias Diffo.Provider.Instance.ActionHelper alias Diffo.Compute alias Diffo.Compute.ClusterValue + alias Diffo.Compute.Organisation + alias Diffo.Compute.Engineer use Ash.Resource, fragments: [BaseInstance], @@ -247,6 +203,11 @@ defmodule Diffo.Compute.Cluster do characteristic :cluster, ClusterValue end + parties do + party :operated_by, Organisation + party :managed_by, Engineer + end + actions do create :build do description "creates a new Cluster resource instance for build" @@ -307,12 +268,12 @@ defmodule Diffo.Compute.ClusterValue do use Ash.TypedStruct, extensions: [AshJason.TypedStruct, AshOutstanding.TypedStruct] jason do - pick [:name, :sections, :length, :loss, :technology] + pick [:name, :gpu_cores, :npu_cores] compact true end outstanding do - expect [:loss] + expect [:gpu_cores] end typed_struct do @@ -337,50 +298,7 @@ defmodule Diffo.Compute.ClusterValue do end ``` -Now that should be enough to allow us to create a Cluster instance - -```elixir -defmodule Diffo.Compute.Test do - alias Diffo.Provider - alias Diffo.Provider.Instance.Place - alias Diffo.Provider.Instance.Party - - def create_exchange_place do - exchange = - Provider.create_place!(%{ - id: "DONC", - name: :exchangeId, - href: "place/telco/DONC", - referredType: :GeographicSite - }) - - %Place{id: exchange.id, role: :NetworkSite} - end - - def create_provider_party do - provider = - Provider.create_party!(%{ - id: "Compute", - name: :organizationId, - referredType: :Organization - }) - - %Party{id: provider.id, role: :Provider} - end -end - -places = [Diffo.Compute.Test.create_exchange_place()] -parties = [Diffo.Compute.Test.create_provider_party()] -cluster_1 = Diffo.Compute.build_cluster!(%{name: "cluster_1", places: places, parties: parties}) -``` - -We can render this Ash Resource as json: - -```elixir -Jason.encode!(cluster_1, pretty: true) |> IO.puts -``` - -## Using the Assigner +### Using the Assigner We'll now define a GPU Resource which uses the Diffo.Provider.Assigner functionality. @@ -490,7 +408,7 @@ defmodule Diffo.Compute.GPUValue do @moduledoc """ Diffo - TMF Service and Resource Management with a difference - CardValue - AshTyped Struct for GPU Characteristic Value + GPUValue - AshTyped Struct for GPU Characteristic Value """ use Ash.TypedStruct, extensions: [AshJason.TypedStruct, AshOutstanding.TypedStruct] @@ -521,6 +439,186 @@ defmodule Diffo.Compute.GPUValue do end ``` +## Party Extension + +`Diffo.Provider.BaseParty` is an Ash Resource Fragment for domain-specific Party kinds, mirroring `BaseInstance`. It provides common Party attributes — `id`, `name`, `kind` — and the `Diffo.Provider.Party.Extension` DSL, which lets a Party kind declare the roles it plays with respect to Instances and other Parties. + +`kind` is either `:individual` or `:organization`. The `id` defaults to a generated uuid but can be set by the domain to any meaningful string (such as an ABN or a data centre identifier). + +The `Diffo.Provider.Party.Extension` DSL cheat sheet is at [DSL-Diffo.Provider.Party.Extension](https://hexdocs.pm/diffo/DSL-Diffo.Provider.Party.Extension.html). + +### Defining Party kinds + +We'll add two Party kinds to our Compute domain — `Organisation` for the operating company, and `Engineer` for the individuals who manage resources. + +```elixir +defmodule Diffo.Compute.Organisation do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + Organisation - an Organisation in the Compute domain + """ + + alias Diffo.Provider.BaseParty + alias Diffo.Compute + + use Ash.Resource, + fragments: [BaseParty], + domain: Compute + + resource do + description "A Compute Organisation" + plural_name :organisations + end + + instance do + role :operates, Diffo.Compute.Cluster + role :operates, Diffo.Compute.GPU + end +end +``` + +```elixir +defmodule Diffo.Compute.Engineer do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + Engineer - an Engineer in the Compute domain + """ + + alias Diffo.Provider.BaseParty + alias Diffo.Compute + + use Ash.Resource, + fragments: [BaseParty], + domain: Compute + + resource do + description "A Compute Engineer" + plural_name :engineers + end + + instance do + role :manages, Diffo.Compute.Cluster + end + + party do + role :employed_by, Diffo.Compute.Organisation + end +end +``` + +### Compute Domain + +With all resources defined we can now declare the `Diffo.Compute` domain, which exposes a typed API for each resource: + +```elixir +defmodule Diffo.Compute do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + Compute - example domain + """ + use Ash.Domain, + otp_app: :diffo, + validate_config_inclusion?: false + + alias Diffo.Compute.GPU + #alias Diffo.Compute.NPU + alias Diffo.Compute.Cluster + alias Diffo.Compute.Organisation + alias Diffo.Compute.Engineer + + resources do + resource GPU do + define :get_gpu_by_id, action: :read, get_by: :id + define :build_gpu, action: :build + define :define_gpu, action: :define + define :relate_gpu, action: :relate + define :assign_gpu_core, action: :assign_core + end + + #resource NPU do + #define :get_npu_by_id, action: :read, get_by: :id + #define :build_npu, action: :build + #define :define_npu, action: :define + #define :relate_npu, action: :relate + #define :assign_npu_core, action: :assign_core + #end + + 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 Organisation do + define :create_organisation, action: :create + define :get_organisation_by_id, action: :read, get_by: :id + define :list_organisations, action: :list + end + + resource Engineer do + define :create_engineer, action: :create + define :get_engineer_by_id, action: :read, get_by: :id + define :list_engineers, action: :list + end + end +end +``` + +### Creating a Cluster + +Clear any data from previous runs before starting (safe to re-evaluate): + +```elixir +AshNeo4j.Neo4jHelper.delete_all() +``` + +Now the domain is defined we can create our first Cluster instance. We'll use a helper module to set up the place and provider party ref: + +```elixir +defmodule Diffo.Compute.Test do + alias Diffo.Provider + alias Diffo.Provider.Instance.Place + alias Diffo.Provider.Instance.Party + + def create_data_centre_place do + dc = + Provider.create_place!(%{ + id: "NXTM2", + name: :dataCentreId, + href: "place/compute/NXTM2", + referredType: :GeographicSite + }) + + %Place{id: dc.id, role: :DataCentre} + end + + def create_provider_party do + provider = + Provider.create_party!(%{ + id: "Compute", + name: :organizationId, + referredType: :Organization + }) + + %Party{id: provider.id, role: :Provider} + end +end + +places = [Diffo.Compute.Test.create_data_centre_place()] +parties = [Diffo.Compute.Test.create_provider_party()] +cluster_1 = Diffo.Compute.build_cluster!(%{name: "cluster_1", places: places, parties: parties}) +``` + +```elixir +Jason.encode!(cluster_1, pretty: true) |> IO.puts +``` + +### Using the Assigner + Now we'll create a couple of GPU instances: ```elixir @@ -548,7 +646,7 @@ The GPU's core characteristic is an AssignableValue, now we've allocated it we c Jason.encode!(gpu_1, pretty: true) |> IO.puts ``` -## Composing a Resource from partially assigned Resources +### Composing a Resource from partially assigned Resources Now we can auto-assign GPU cores from each GPU to our cluster_1. We'll assign 3 cores from gpu_1, and one from gpu_2. @@ -579,10 +677,74 @@ As an exercise, clone the GPU resource to create an NPU resource and assign some What happens when there are none left to assign? What happens when I request a specific assignment from an instance to which the partial resource is already assigned? +### Creating Party instances + +Now we can create an Organisation and an Engineer in the Compute domain. The `id` for the Organisation is set to a meaningful string — the company's ABN. + +```elixir +alias Diffo.Compute + +{:ok, org} = Compute.create_organisation(%{ + id: "51824753556", + name: "Acme Compute Pty Ltd", + kind: :organization +}) + +{:ok, engineer} = Compute.create_engineer(%{ + name: "Alice Zhang", + kind: :individual +}) +``` + +### Using Parties with a Cluster + +We can now build a Cluster and relate it to our Organisation and Engineer using the base provider party ref mechanism. + +```elixir +alias Diffo.Provider.Instance.Party + +parties = [ + %Party{id: org.id, role: :operated_by}, + %Party{id: engineer.id, role: :managed_by} +] + +cluster_2 = Compute.build_cluster!(%{ + name: "cluster_2", + places: [Diffo.Compute.Test.create_data_centre_place()], + parties: parties +}) + +Jason.encode!(cluster_2, pretty: true) |> IO.puts +``` + +### What the Party DSL declares + +The `instance do` and `party do` blocks are compile-time declarations — they document which roles a Party kind plays and make that information available via `Diffo.Provider.Party.Extension.Info`: + +```elixir +alias Diffo.Provider.Party.Extension.Info, as: PartyInfo + +PartyInfo.instance(Diffo.Compute.Organisation) +``` + +```elixir +PartyInfo.party(Diffo.Compute.Engineer) +``` + +These declarations also inform the Instance DSL's `parties do` block, which documents which party kinds an Instance resource expects: + +```elixir +alias Diffo.Provider.Instance.Extension.Info, as: InstanceInfo + +InstanceInfo.parties(Diffo.Compute.Cluster) +``` + ### What Next? -In this tutorial you've used Diffo's Provider Instance Extension to define a Compute domain with a composite Cluster resource which is comprised of assigned GPU cores from GPU resources and NPU cores from NPU resources. +In this tutorial you've used Diffo's Provider Instance Extension to define a Compute domain with a composite Cluster resource comprised of assigned GPU cores, and the Provider Party Extension to define Organisation and Engineer party kinds that operate and manage those resources. + +The BaseParty fragment follows the same pattern as BaseInstance — domain-specific resources use it as a fragment and finish their actions with a domain-scoped reload to pick up extended fields. -The composite Cluster Resource is a fully fledged TMF Resource which can itself be related to consuming TMF Service and/or Resources. +A `BasePlace` extension for domain-specific Place kinds (such as a DataCentre with its own attributes) follows the same pattern and will be added in a future release. If you find Diffo useful please visit and star on [github](https://github.com/diffo-dev/diffo/). Feel free to join discussions and raise issues to discuss PR's. diff --git a/lib/diffo/provider/components/base_party.ex b/lib/diffo/provider/components/base_party.ex new file mode 100644 index 0000000..f1f3c01 --- /dev/null +++ b/lib/diffo/provider/components/base_party.ex @@ -0,0 +1,136 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.BaseParty do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + BaseParty - Ash Resource Fragment of a TMF Party + + `BaseParty` is the foundation for domain-specific Party kinds such as RSP or Person. + It provides common Party attributes and the `Diffo.Provider.Party.Extension` DSL, which + allows a Party kind to declare the roles it plays with respect to Instances and other Parties. + + ## Usage + + defmodule MyApp.Organisation do + use Ash.Resource, fragments: [BaseParty], domain: MyApp.Domain + + resource do + description "An Organisation" + plural_name :organisations + end + + instance do + role :facilitates, MyApp.AccessService + end + end + + ## Action pattern + + Domain-specific Party resources should finish their `create` action with a reload via + their own domain's `get_xxx_by_id` to pick up extended fields: + + create :create do + accept [:name] + change after_action(fn _changeset, result, _context -> + MyApp.Domain.get_organisation_by_id(result.id) + end) + end + """ + + use Spark.Dsl.Fragment, + of: Ash.Resource, + otp_app: :diffo, + domain: Diffo.Provider, + data_layer: AshNeo4j.DataLayer, + extensions: [ + AshOutstanding.Resource, + AshJason.Resource, + Diffo.Provider.Party.Extension + ] + + neo4j do + relate [ + {:party_refs, :RELATES, :incoming, :PartyRef} + ] + + label :Party + end + + jason do + pick [:id, :name, :kind] + compact true + end + + outstanding do + expect [:id, :name, :kind] + end + + attributes do + attribute :id, :string do + description "the id of this party, domain-assigned or a generated uuid4 by default" + primary_key? true + allow_nil? false + public? true + default &Diffo.Uuid.uuid4/0 + source :key + end + + attribute :name, :string do + description "the name of this party" + allow_nil? true + public? true + end + + attribute :kind, :atom do + description "the kind of this party, either individual or organization" + allow_nil? false + public? true + constraints one_of: [:individual, :organization] + end + + create_timestamp :created_at + + update_timestamp :updated_at + end + + relationships do + has_many :party_refs, Diffo.Provider.PartyRef do + description "the party refs relating this party to instances" + destination_attribute :party_id + public? true + end + end + + actions do + defaults [:read, :destroy] + + create :create do + description "creates a party of this kind" + accept [:id, :name, :kind] + upsert? true + end + + update :update do + description "updates the party name" + accept [:name] + end + + read :list do + description "lists all parties of this kind" + end + + read :find_by_name do + description "finds parties by name" + get? false + + argument :query, :ci_string do + description "Return only parties with names including the given value." + end + + filter expr(contains(name, ^arg(:query))) + end + end +end diff --git a/lib/diffo/provider/components/instance/extension.ex b/lib/diffo/provider/components/instance/extension.ex index 18c6b73..f353f9d 100644 --- a/lib/diffo/provider/components/instance/extension.ex +++ b/lib/diffo/provider/components/instance/extension.ex @@ -152,6 +152,44 @@ defmodule Diffo.Provider.Instance.Extension do ] } + @party_entity %Spark.Dsl.Entity{ + name: :party, + describe: "Declares a party role on this Instance", + target: Diffo.Provider.Instance.Extension.PartyDeclaration, + args: [:role, :party_type], + schema: [ + role: [ + doc: """ + The role name, an atom + """, + type: :atom, + required: true + ], + party_type: [ + doc: """ + The module of the Party kind. An atom module name such as a BaseParty-derived resource. + """, + type: :any + ] + ] + } + + @parties %Spark.Dsl.Section{ + name: :parties, + describe: "List of Instance Party roles", + examples: [ + """ + parties do + party :facilitated_by, MyApp.Rsp + party :overseen_by, MyApp.Person + end + """ + ], + entities: [ + @party_entity + ] + } + use Spark.Dsl.Extension, - sections: [@specification, @features, @characteristics] + sections: [@specification, @features, @characteristics, @parties] end diff --git a/lib/diffo/provider/components/instance/extension/info.ex b/lib/diffo/provider/components/instance/extension/info.ex index 938a782..27c87d2 100644 --- a/lib/diffo/provider/components/instance/extension/info.ex +++ b/lib/diffo/provider/components/instance/extension/info.ex @@ -5,5 +5,5 @@ defmodule Diffo.Provider.Instance.Extension.Info do use Spark.InfoGenerator, extension: Diffo.Provider.Instance.Extension, - sections: [:specification, :features, :characteristics] + sections: [:specification, :features, :characteristics, :parties] end diff --git a/lib/diffo/provider/components/instance/extension/party_declaration.ex b/lib/diffo/provider/components/instance/extension/party_declaration.ex new file mode 100644 index 0000000..69fb67d --- /dev/null +++ b/lib/diffo/provider/components/instance/extension/party_declaration.ex @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Instance.Extension.PartyDeclaration do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + PartyDeclaration - DSL entity declaring a party role on an Instance + """ + + defstruct [:role, :party_type, __spark_metadata__: nil] + + defimpl String.Chars do + def to_string(struct), do: inspect(struct) + end +end diff --git a/lib/diffo/provider/components/party/extension.ex b/lib/diffo/provider/components/party/extension.ex new file mode 100644 index 0000000..19fea32 --- /dev/null +++ b/lib/diffo/provider/components/party/extension.ex @@ -0,0 +1,74 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Party.Extension do + @moduledoc """ + DSL Extension customising a Party + """ + + @role %Spark.Dsl.Entity{ + name: :role, + describe: "Declares a role this Party kind plays", + target: Diffo.Provider.Party.Extension.InstanceRole, + args: [:role, :party_type], + schema: [ + role: [ + type: :atom, + required: true, + doc: "The role name, an atom" + ], + party_type: [ + type: :any, + doc: "The module of the related resource" + ] + ] + } + + @party_role %Spark.Dsl.Entity{ + name: :role, + describe: "Declares a role this Party kind plays with respect to other Parties", + target: Diffo.Provider.Party.Extension.PartyRole, + args: [:role, :party_type], + schema: [ + role: [ + type: :atom, + required: true, + doc: "The role name, an atom" + ], + party_type: [ + type: :any, + doc: "The module of the related Party kind" + ] + ] + } + + @instance %Spark.Dsl.Section{ + name: :instance, + describe: "Declares the roles this Party kind plays with respect to Instances", + examples: [ + """ + instance do + role :facilitates, MyApp.AccessService + end + """ + ], + entities: [@role] + } + + @party %Spark.Dsl.Section{ + name: :party, + describe: "Declares the roles this Party kind plays with respect to other Parties", + examples: [ + """ + party do + role :managed_by, MyApp.Person + end + """ + ], + entities: [@party_role] + } + + use Spark.Dsl.Extension, + sections: [@instance, @party] +end diff --git a/lib/diffo/provider/components/party/extension/info.ex b/lib/diffo/provider/components/party/extension/info.ex new file mode 100644 index 0000000..66361d7 --- /dev/null +++ b/lib/diffo/provider/components/party/extension/info.ex @@ -0,0 +1,9 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Party.Extension.Info do + use Spark.InfoGenerator, + extension: Diffo.Provider.Party.Extension, + sections: [:instance, :party] +end diff --git a/lib/diffo/provider/components/party/extension/instance_role.ex b/lib/diffo/provider/components/party/extension/instance_role.ex new file mode 100644 index 0000000..3a39545 --- /dev/null +++ b/lib/diffo/provider/components/party/extension/instance_role.ex @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Party.Extension.InstanceRole do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + InstanceRole - DSL entity declaring a role this Party kind plays with respect to Instances + """ + + defstruct [:role, :party_type, __spark_metadata__: nil] + + defimpl String.Chars do + def to_string(struct), do: inspect(struct) + end +end diff --git a/lib/diffo/provider/components/party/extension/party_role.ex b/lib/diffo/provider/components/party/extension/party_role.ex new file mode 100644 index 0000000..d4ba2ab --- /dev/null +++ b/lib/diffo/provider/components/party/extension/party_role.ex @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Party.Extension.PartyRole do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + PartyRole - DSL entity declaring a role this Party kind plays with respect to other Parties + """ + + defstruct [:role, :party_type, __spark_metadata__: nil] + + defimpl String.Chars do + def to_string(struct), do: inspect(struct) + end +end diff --git a/lib/diffo/repo.ex b/lib/diffo/repo.ex index 62bea87..937cfec 100644 --- a/lib/diffo/repo.ex +++ b/lib/diffo/repo.ex @@ -17,7 +17,9 @@ defmodule Diffo.Repo do end def start_link(_stack) do - config = Application.get_env(:bolty, Bolt) - Bolty.start_link(config) + case Application.get_env(:bolty, Bolt) do + nil -> :ignore + config -> Bolty.start_link(config) + end end end diff --git a/mix.exs b/mix.exs index cefb48b..c0e70ef 100644 --- a/mix.exs +++ b/mix.exs @@ -113,9 +113,10 @@ defmodule Diffo.MixProject do "docs", "spark.replace_doc_links" ], - "spark.cheat_sheets": "spark.cheat_sheets --extensions Diffo.Provider.Instance.Extension", + "spark.cheat_sheets": + "spark.cheat_sheets --extensions Diffo.Provider.Instance.Extension,Diffo.Provider.Party.Extension", "spark.formatter": [ - "spark.formatter --extensions Diffo.Provider.Instance.Extension", + "spark.formatter --extensions Diffo.Provider.Instance.Extension,Diffo.Provider.Party.Extension", "format .formatter.exs" ] ] diff --git a/test/instance_extension/party_test.exs b/test/instance_extension/party_test.exs new file mode 100644 index 0000000..9b91b9e --- /dev/null +++ b/test/instance_extension/party_test.exs @@ -0,0 +1,90 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.InstanceExtension.PartyTest do + @moduledoc false + use ExUnit.Case + + alias Diffo.Provider.Instance.Extension.Info, as: InstanceInfo + alias Diffo.Provider.Party.Extension.Info, as: PartyInfo + alias Diffo.Test.Organisation + alias Diffo.Test.Person + alias Diffo.Test.Shelf + alias Diffo.Test.Nbn + + setup_all do + AshNeo4j.BoltyHelper.start() + end + + setup do + on_exit(fn -> + AshNeo4j.Neo4jHelper.delete_all() + end) + end + + describe "Party DSL — Organisation" do + test "instance roles are declared" do + roles = PartyInfo.instance(Organisation) + assert length(roles) == 1 + assert hd(roles).role == :facilitates + assert hd(roles).party_type == Diffo.Provider.Instance + end + + test "no party roles declared" do + assert PartyInfo.party(Organisation) == [] + end + end + + describe "Party DSL — Person" do + test "party roles are declared" do + roles = PartyInfo.party(Person) + assert length(roles) == 1 + assert hd(roles).role == :managed_by + assert hd(roles).party_type == Diffo.Test.Person + end + + test "no instance roles declared" do + assert PartyInfo.instance(Person) == [] + end + end + + describe "Instance DSL — Shelf parties" do + test "party declarations are accessible via info" do + parties = InstanceInfo.parties(Shelf) + roles = Enum.map(parties, & &1.role) + assert :facilitated_by in roles + assert :overseen_by in roles + end + + test "party types are correct" do + parties = InstanceInfo.parties(Shelf) + facilitated_by = Enum.find(parties, &(&1.role == :facilitated_by)) + overseen_by = Enum.find(parties, &(&1.role == :overseen_by)) + assert facilitated_by.party_type == Organisation + assert overseen_by.party_type == Person + end + end + + describe "BaseParty — Organisation CRUD" do + test "create and read organisation" do + {:ok, org} = Nbn.create_organisation(%{name: "Acme Corp", kind: :organization}) + assert org.name == "Acme Corp" + assert org.kind == :organization + + {:ok, loaded} = Nbn.get_organisation_by_id(org.id) + assert loaded.name == "Acme Corp" + end + end + + describe "BaseParty — Person CRUD" do + test "create and read person" do + {:ok, person} = Nbn.create_person(%{name: "Alice", kind: :individual}) + assert person.name == "Alice" + assert person.kind == :individual + + {:ok, loaded} = Nbn.get_person_by_id(person.id) + assert loaded.name == "Alice" + end + end +end diff --git a/test/support/nbn.ex b/test/support/nbn.ex new file mode 100644 index 0000000..6ac259e --- /dev/null +++ b/test/support/nbn.ex @@ -0,0 +1,35 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Test.Nbn do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + Nbn - a lightweight example domain for testing BaseParty and Party DSL + """ + use Ash.Domain, + otp_app: :diffo, + validate_config_inclusion?: false + + alias Diffo.Test.Organisation + alias Diffo.Test.Person + + domain do + description "NBN party domain" + end + + resources do + resource Organisation do + define :create_organisation, action: :create + define :get_organisation_by_id, action: :read, get_by: :id + define :list_organisations, action: :list + end + + resource Person do + define :create_person, action: :create + define :get_person_by_id, action: :read, get_by: :id + define :list_persons, action: :list + end + end +end diff --git a/test/support/resource/organisation.ex b/test/support/resource/organisation.ex new file mode 100644 index 0000000..b911948 --- /dev/null +++ b/test/support/resource/organisation.ex @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Test.Organisation do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + Organisation - Organisation Party + """ + + alias Diffo.Provider.BaseParty + alias Diffo.Test.Nbn + + use Ash.Resource, + fragments: [BaseParty], + domain: Nbn + + resource do + description "An Organisation" + plural_name :organisations + end + + instance do + role :facilitates, Diffo.Provider.Instance + end +end diff --git a/test/support/resource/person.ex b/test/support/resource/person.ex new file mode 100644 index 0000000..ab27e2a --- /dev/null +++ b/test/support/resource/person.ex @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Test.Person do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + Person - Person Party + """ + + alias Diffo.Provider.BaseParty + alias Diffo.Test.Nbn + + use Ash.Resource, + fragments: [BaseParty], + domain: Nbn + + resource do + description "A Person" + plural_name :persons + end + + party do + role :managed_by, Diffo.Test.Person + end +end diff --git a/test/support/resource/shelf.ex b/test/support/resource/shelf.ex index b95724e..b9e1ecc 100644 --- a/test/support/resource/shelf.ex +++ b/test/support/resource/shelf.ex @@ -52,6 +52,11 @@ defmodule Diffo.Test.Shelf do characteristic :shelves, {:array, ShelfValue} end + parties do + party :facilitated_by, Diffo.Test.Organisation + party :overseen_by, Diffo.Test.Person + end + actions do create :build do description "creates a new Shelf resource instance for build" From 141dcb603b1d096dc9540d803e6605bf1f3e7fbe Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Mon, 27 Apr 2026 20:35:30 +0930 Subject: [PATCH 02/22] =?UTF-8?q?Instance=20DSL=20parties=20=E2=80=94=20mu?= =?UTF-8?q?ltiplicity,=20validation,=20and=20enforcement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds `parties do` DSL to the Instance Extension, allowing resources to declare which party roles they accept, with optional min/max multiplicity constraints. Runtime enforcement is applied in `build_before` via `Party.validate_parties/1`, checking roles against declarations and rejecting builds that violate constraints. Renames Party Extension DSL sections from singular (`instance`/`party`) to plural (`instances`/`parties`) for consistency. Replaces `.license` sidecar files with `REUSE.toml`. Fixes ExDoc sidebar by removing a duplicate `docs:` key in mix.exs that shadowed the DSL extras config. Adds `Assignment.compare/2` so `Enum.sort/2` works in the Assigner. Updates all three livebooks to use the local path dep and cleans up stale commented-out version pins. Co-Authored-By: Claude Sonnet 4.6 --- .gitignore.license | 3 - .tool-versions.license | 3 - REUSE.toml | 12 + diffo.livemd | 58 ++++- .../DSL-Diffo.Provider.Instance.Extension.md | 65 ++++-- ...ffo.Provider.Instance.Extension.md.license | 3 - .../DSL-Diffo.Provider.Party.Extension.md | 32 +-- ...-Diffo.Provider.Party.Extension.md.license | 3 - .../use_diffo_provider_extension.livemd | 212 +++++++----------- documentation/how_to/use_diffo_type.livemd | 4 +- lib/diffo/changes/detail_event.ex | 7 +- lib/diffo/changes/detail_relationship.ex | 8 +- lib/diffo/helpers/util.ex | 28 +-- lib/diffo/helpers/uuid.ex | 5 +- lib/diffo/provider.ex | 4 +- .../provider/assigner/assignable_value.ex | 4 +- lib/diffo/provider/assigner/assigner.ex | 5 +- lib/diffo/provider/assigner/assignment.ex | 14 +- .../provider/components/base_instance.ex | 69 +++++- lib/diffo/provider/components/base_party.ex | 170 +++++++++++--- .../components/calculations/instance_href.ex | 7 +- .../calculations/specification_href.ex | 8 +- .../specification_instance_type.ex | 7 +- .../calculations/specification_version.ex | 7 +- .../provider/components/characteristic.ex | 17 +- lib/diffo/provider/components/entity.ex | 36 +-- lib/diffo/provider/components/entity_ref.ex | 17 +- lib/diffo/provider/components/event.ex | 17 +- .../components/external_identifier.ex | 17 +- lib/diffo/provider/components/feature.ex | 16 +- lib/diffo/provider/components/instance.ex | 4 +- .../provider/components/instance/extension.ex | 74 ++++-- .../instance/extension/action_helper.ex | 8 +- .../instance/extension/characteristic.ex | 7 +- .../components/instance/extension/feature.ex | 7 +- .../components/instance/extension/party.ex | 64 +++++- .../instance/extension/party_declaration.ex | 6 +- .../components/instance/extension/place.ex | 7 +- .../instance/extension/relationship.ex | 7 +- .../instance/extension/specification.ex | 6 +- .../provider/components/instance/util.ex | 120 ++-------- lib/diffo/provider/components/note.ex | 17 +- lib/diffo/provider/components/party.ex | 175 ++------------- .../provider/components/party/extension.ex | 23 +- .../components/party/extension/info.ex | 2 +- .../party/extension/instance_role.ex | 3 - .../components/party/extension/party_role.ex | 3 - lib/diffo/provider/components/party_ref.ex | 21 +- lib/diffo/provider/components/place.ex | 36 +-- lib/diffo/provider/components/place_ref.ex | 19 +- .../provider/components/process_status.ex | 17 +- lib/diffo/provider/components/relationship.ex | 4 +- .../provider/components/specification.ex | 4 +- lib/diffo/provider/outstanding.ex | 18 +- lib/diffo/provider/reference.ex | 24 +- lib/diffo/provider/service.ex | 8 +- lib/diffo/repo.ex | 8 +- lib/diffo/type/dynamic.ex | 34 ++- lib/diffo/type/outstanding/dynamic.ex | 32 --- lib/diffo/type/outstanding/primitive.ex | 35 --- lib/diffo/type/primitive.ex | 34 ++- lib/diffo/type/value.ex | 6 +- lib/diffo/unwrap.ex | 3 - lib/diffo/unwrap/any.ex | 2 + lib/diffo/unwrap/ash_ci_string.ex | 2 + lib/diffo/unwrap/ash_custom_expression.ex | 2 + lib/diffo/unwrap/ash_not_loaded.ex | 2 + lib/diffo/unwrap/ash_union.ex | 2 + lib/diffo/unwrap/list.ex | 2 + lib/diffo/validations/href_ends_with_id.ex | 7 +- lib/diffo/validations/is_related_different.ex | 7 +- lib/diffo/validations/is_uuid4_or_nil.ex | 7 +- logos/diffo.jpg.license | 3 - mix.exs | 12 +- mix.lock.license | 3 - test/instance_extension/assigner_test.exs | 9 +- .../characteristic_test.exs | 3 +- test/instance_extension/feature_test.exs | 3 +- test/instance_extension/party_test.exs | 129 +++++++++-- test/provider/entity_ref_test.exs | 54 ++--- test/provider/entity_test.exs | 80 +++---- test/provider/external_identifier_test.exs | 44 ++-- test/provider/instance_test.exs | 20 +- test/provider/instance_util_test.exs | 88 ++++++++ test/provider/note_test.exs | 40 ++-- test/provider/party_ref_test.exs | 46 ++-- test/provider/party_test.exs | 80 +++---- test/provider/place_ref_test.exs | 36 +-- test/provider/place_test.exs | 80 +++---- test/provider/reference_test.exs | 31 +++ test/support/nbn.ex | 12 +- test/support/parties.ex | 7 + test/support/resource/organisation.ex | 27 --- test/support/resource/organization.ex | 43 ++++ test/support/resource/person.ex | 20 +- test/support/resource/shelf.ex | 7 +- 96 files changed, 1283 insertions(+), 1321 deletions(-) delete mode 100644 .gitignore.license delete mode 100644 .tool-versions.license create mode 100644 REUSE.toml delete mode 100644 documentation/dsls/DSL-Diffo.Provider.Instance.Extension.md.license delete mode 100644 documentation/dsls/DSL-Diffo.Provider.Party.Extension.md.license delete mode 100644 lib/diffo/type/outstanding/dynamic.ex delete mode 100644 lib/diffo/type/outstanding/primitive.ex delete mode 100644 logos/diffo.jpg.license delete mode 100644 mix.lock.license create mode 100644 test/provider/instance_util_test.exs delete mode 100644 test/support/resource/organisation.ex create mode 100644 test/support/resource/organization.ex diff --git a/.gitignore.license b/.gitignore.license deleted file mode 100644 index 40c9cb0..0000000 --- a/.gitignore.license +++ /dev/null @@ -1,3 +0,0 @@ -SPDX-FileCopyrightText: 2025 diffo contributors - -SPDX-License-Identifier: MIT \ No newline at end of file diff --git a/.tool-versions.license b/.tool-versions.license deleted file mode 100644 index 40c9cb0..0000000 --- a/.tool-versions.license +++ /dev/null @@ -1,3 +0,0 @@ -SPDX-FileCopyrightText: 2025 diffo contributors - -SPDX-License-Identifier: MIT \ No newline at end of file diff --git a/REUSE.toml b/REUSE.toml new file mode 100644 index 0000000..bfc02fe --- /dev/null +++ b/REUSE.toml @@ -0,0 +1,12 @@ +version = 1 + +[[annotations]] +path = [ + ".gitignore", + ".tool-versions", + "mix.lock", + "logos/diffo.jpg", + "documentation/dsls/**", +] +SPDX-FileCopyrightText = "2025 diffo contributors " +SPDX-License-Identifier = "MIT" diff --git a/diffo.livemd b/diffo.livemd index 9fa9b38..8617475 100644 --- a/diffo.livemd +++ b/diffo.livemd @@ -1,4 +1,4 @@ - # Diffo.Provider.Instance.Extension -DSL Extension customising an Instance +DSL Extension customising an Instance. + +Provides compile-time declaration blocks for domain-specific Service and Resource kinds +built on `Diffo.Provider.BaseInstance`. All declarations are introspectable via +`Diffo.Provider.Instance.Extension.Info`. + +See the [DSL cheat sheet](DSL-Diffo.Provider.Instance.Extension.html) for the full DSL reference. +See `Diffo.Provider.BaseInstance` for full usage documentation. ## specification @@ -117,16 +124,10 @@ Adds a Characteristic -### Introspection - -Target: `Diffo.Provider.Instance.Characteristic` - -### Introspection -Target: `Diffo.Provider.Instance.Feature` @@ -176,9 +177,6 @@ Adds a Characteristic -### Introspection - -Target: `Diffo.Provider.Instance.Characteristic` @@ -188,13 +186,15 @@ List of Instance Party roles ### Nested DSLs * [party](#parties-party) + * [parties](#parties-parties) ### Examples ``` parties do - party :facilitated_by, MyApp.Rsp - party :overseen_by, MyApp.Person + party :provider, MyApp.Provider, calculate: :provider_calculation + parties :technician, MyApp.Technician, constraints: [min: 1, max: 3] + party :owner, MyApp.InfrastructureCo, reference: true end ``` @@ -208,7 +208,7 @@ party role, party_type ``` -Declares a party role on this Instance +Declares a singular party role on this Instance @@ -220,7 +220,46 @@ Declares a party role on this Instance |------|------|---------|------| | [`role`](#parties-party-role){: #parties-party-role .spark-required} | `atom` | | The role name, an atom | | [`party_type`](#parties-party-party_type){: #parties-party-party_type } | `any` | | The module of the Party kind. An atom module name such as a BaseParty-derived resource. | +### Options +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`reference`](#parties-party-reference){: #parties-party-reference } | `boolean` | `false` | If true, no direct PartyRef edge is created; the party is reachable by graph traversal. | +| [`calculate`](#parties-party-calculate){: #parties-party-calculate } | `atom` | | Name of an Ash calculation on this resource that produces the party at build time. | + + + + + +### Introspection + +Target: `Diffo.Provider.Instance.Extension.PartyDeclaration` + +### parties.parties +```elixir +parties role, party_type +``` + + +Declares a plural party role on this Instance + + + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`role`](#parties-parties-role){: #parties-parties-role .spark-required} | `atom` | | The role name, an atom | +| [`party_type`](#parties-parties-party_type){: #parties-parties-party_type } | `any` | | The module of the Party kind. An atom module name such as a BaseParty-derived resource. | +### Options + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`reference`](#parties-parties-reference){: #parties-parties-reference } | `boolean` | `false` | If true, no direct PartyRef edge is created; the party is reachable by graph traversal. | +| [`calculate`](#parties-parties-calculate){: #parties-parties-calculate } | `atom` | | Name of an Ash calculation on this resource that produces the party at build time. | +| [`constraints`](#parties-parties-constraints){: #parties-parties-constraints } | `keyword` | | Multiplicity constraints on the number of parties in this role, e.g. [min: 1, max: 3] | diff --git a/documentation/dsls/DSL-Diffo.Provider.Instance.Extension.md.license b/documentation/dsls/DSL-Diffo.Provider.Instance.Extension.md.license deleted file mode 100644 index 40c9cb0..0000000 --- a/documentation/dsls/DSL-Diffo.Provider.Instance.Extension.md.license +++ /dev/null @@ -1,3 +0,0 @@ -SPDX-FileCopyrightText: 2025 diffo contributors - -SPDX-License-Identifier: MIT \ No newline at end of file diff --git a/documentation/dsls/DSL-Diffo.Provider.Party.Extension.md b/documentation/dsls/DSL-Diffo.Provider.Party.Extension.md index eb8d110..92e0d82 100644 --- a/documentation/dsls/DSL-Diffo.Provider.Party.Extension.md +++ b/documentation/dsls/DSL-Diffo.Provider.Party.Extension.md @@ -3,19 +3,25 @@ This file was generated by Spark. Do not edit it by hand. --> # Diffo.Provider.Party.Extension -DSL Extension customising a Party +DSL Extension customising a Party. +Provides compile-time declaration blocks for domain-specific Party kinds +built on `Diffo.Provider.BaseParty`. All declarations are introspectable via +`Diffo.Provider.Party.Extension.Info`. -## instance +See the [DSL cheat sheet](DSL-Diffo.Provider.Party.Extension.html) for the full DSL reference. + + +## instances Declares the roles this Party kind plays with respect to Instances ### Nested DSLs - * [role](#instance-role) + * [role](#instances-role) ### Examples ``` -instance do +instances do role :facilitates, MyApp.AccessService end @@ -24,7 +30,7 @@ end -### instance.role +### instances.role ```elixir role role, party_type ``` @@ -40,8 +46,8 @@ Declares a role this Party kind plays | Name | Type | Default | Docs | |------|------|---------|------| -| [`role`](#instance-role-role){: #instance-role-role .spark-required} | `atom` | | The role name, an atom | -| [`party_type`](#instance-role-party_type){: #instance-role-party_type } | `any` | | The module of the related resource | +| [`role`](#instances-role-role){: #instances-role-role .spark-required} | `atom` | | The role name, an atom | +| [`party_type`](#instances-role-party_type){: #instances-role-party_type } | `any` | | The module of the related resource | @@ -55,16 +61,16 @@ Target: `Diffo.Provider.Party.Extension.InstanceRole` -## party +## parties Declares the roles this Party kind plays with respect to other Parties ### Nested DSLs - * [role](#party-role) + * [role](#parties-role) ### Examples ``` -party do +parties do role :managed_by, MyApp.Person end @@ -73,7 +79,7 @@ end -### party.role +### parties.role ```elixir role role, party_type ``` @@ -89,8 +95,8 @@ Declares a role this Party kind plays with respect to other Parties | Name | Type | Default | Docs | |------|------|---------|------| -| [`role`](#party-role-role){: #party-role-role .spark-required} | `atom` | | The role name, an atom | -| [`party_type`](#party-role-party_type){: #party-role-party_type } | `any` | | The module of the related Party kind | +| [`role`](#parties-role-role){: #parties-role-role .spark-required} | `atom` | | The role name, an atom | +| [`party_type`](#parties-role-party_type){: #parties-role-party_type } | `any` | | The module of the related Party kind | diff --git a/documentation/dsls/DSL-Diffo.Provider.Party.Extension.md.license b/documentation/dsls/DSL-Diffo.Provider.Party.Extension.md.license deleted file mode 100644 index b381ebe..0000000 --- a/documentation/dsls/DSL-Diffo.Provider.Party.Extension.md.license +++ /dev/null @@ -1,3 +0,0 @@ -SPDX-FileCopyrightText: 2025 diffo contributors - -SPDX-License-Identifier: MIT diff --git a/documentation/how_to/use_diffo_provider_extension.livemd b/documentation/how_to/use_diffo_provider_extension.livemd index d6cfc03..974a475 100644 --- a/documentation/how_to/use_diffo_provider_extension.livemd +++ b/documentation/how_to/use_diffo_provider_extension.livemd @@ -4,14 +4,14 @@ SPDX-FileCopyrightText: 2025 diffo contributors -# Using the Diffo Provider Extension +# Using the Diffo Provider Instance Extension ```elixir Mix.install( [ - {:diffo, "~> 0.2.0"}, + {:diffo, path: "/Users/Beanlanda/git/diffo"} ], - config: [ + config: [ diffo: [ash_domains: [Diffo.Provider]] ], consolidate_protocols: false @@ -27,7 +27,7 @@ If you are not already familiar with Ash then please explore [Ash Get Started](h First ensure you've explored the Diffo Livebook for an introduction to Diffo: [![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2Fdiffo%2Ddev%2Fdiffo%2Fblob%2Fdev%2Fdiffo.livemd) -In this 'Diffo Provider Extension' livebook you will learn about: +In this 'Diffo Provider Instance Extension' livebook you will learn about: * TMF Services and Resources * Building your own Domain @@ -122,14 +122,12 @@ Partial resource assignment uses a relationship characteristic to indicate which ## Instance Extension -Diffo.Provider.Instance modelling a Service or Resource actually uses the Diffo.Provider.BaseInstance [Spark.Dsl.Fragment](https://hexdocs.pm/spark/Spark.Dsl.Fragment.html). There is no need to evaluate the Diffo.Provider.Instance below, it is already defined. +Diffo.Provider.Instance models either a Service or Resource. It actually uses the Diffo.Provider.BaseInstance [Spark.Dsl.Fragment](https://hexdocs.pm/spark/Spark.Dsl.Fragment.html). There is no need to evaluate the Diffo.Provider.Instance below, it is already defined. ```elixir defmodule Diffo.Provider.Instance do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - Instance - Ash Resource for a TMF Service or Resource Instance + Ash Resource for a TMF Service or Resource Instance """ alias Diffo.Provider.BaseInstance @@ -144,7 +142,7 @@ defmodule Diffo.Provider.Instance do end ``` -Diffo also has an inbuilt Spark DSL extension [Diffo.Provider.Instance.Extension](https://hexdocs.pm/diffo/Diffo.Provider.Instance.Extension.html) which provides DSL and functions for use in building domain specific services and resources. +Diffo also has an inbuilt Spark DSL extension [Diffo.Provider.Instance.Extension](https://hexdocs.pm/diffo/Diffo.Provider.Instance.Extension.html) which provides DSL and functions for use in building and operating domain specific services and resources. Currently it has DSL to allow you to declare specification, features, characteristics, and party roles. It can be used for services or resources. 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. @@ -168,9 +166,7 @@ We will start by declaring the Cluster Resource. It is going to be a composite r ```elixir defmodule Diffo.Compute.Cluster do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - Cluster - Cluster Resource Instance + Cluster Resource Instance """ alias Diffo.Provider.BaseInstance @@ -179,7 +175,7 @@ defmodule Diffo.Compute.Cluster do alias Diffo.Provider.Instance.ActionHelper alias Diffo.Compute alias Diffo.Compute.ClusterValue - alias Diffo.Compute.Organisation + alias Diffo.Compute.Tenant alias Diffo.Compute.Engineer use Ash.Resource, @@ -204,8 +200,8 @@ defmodule Diffo.Compute.Cluster do end parties do - party :operated_by, Organisation - party :managed_by, Engineer + party :operator, Tenant + party :manager, Engineer end actions do @@ -261,9 +257,7 @@ And of course we'll need a ClusterValue TypedStruct for the Cluster Resource's c ```elixir defmodule Diffo.Compute.ClusterValue do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - ClusterValue - AshTyped Struct for Cluster Characteristic Value + AshTyped Struct for Cluster Characteristic Value """ use Ash.TypedStruct, extensions: [AshJason.TypedStruct, AshOutstanding.TypedStruct] @@ -305,9 +299,7 @@ We'll now define a GPU Resource which uses the Diffo.Provider.Assigner functiona ```elixir defmodule Diffo.Compute.GPU do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - GPU - GPU Resource Instance + GPU Resource Instance """ alias Diffo.Provider.BaseInstance @@ -406,9 +398,7 @@ And we must define the GPUValue TypedStruct, used in the GPU's gpu characteristi ```elixir defmodule Diffo.Compute.GPUValue do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - GPUValue - AshTyped Struct for GPU Characteristic Value + AshTyped Struct for GPU Characteristic Value """ use Ash.TypedStruct, extensions: [AshJason.TypedStruct, AshOutstanding.TypedStruct] @@ -441,22 +431,20 @@ end ## Party Extension -`Diffo.Provider.BaseParty` is an Ash Resource Fragment for domain-specific Party kinds, mirroring `BaseInstance`. It provides common Party attributes — `id`, `name`, `kind` — and the `Diffo.Provider.Party.Extension` DSL, which lets a Party kind declare the roles it plays with respect to Instances and other Parties. +`Diffo.Provider.BaseParty` is an Ash Resource Fragment for domain-specific Party kinds, mirroring `BaseInstance`. It provides common Party attributes — `id`, `href`, `name`, `type`, `referred_type` — and the `Diffo.Provider.Party.Extension` DSL, which lets a Party kind declare the roles it plays with respect to Instances and other Parties. -`kind` is either `:individual` or `:organization`. The `id` defaults to a generated uuid but can be set by the domain to any meaningful string (such as an ABN or a data centre identifier). +`type` defaults to `:PartyRef` and can be set to `:Individual`, `:Organization`, or `:Entity`. Domain party kinds typically set `type` in their `build` action. The `id` defaults to a generated uuid but can be set to any meaningful string (such as an ABN or a data centre identifier). The `Diffo.Provider.Party.Extension` DSL cheat sheet is at [DSL-Diffo.Provider.Party.Extension](https://hexdocs.pm/diffo/DSL-Diffo.Provider.Party.Extension.html). ### Defining Party kinds -We'll add two Party kinds to our Compute domain — `Organisation` for the operating company, and `Engineer` for the individuals who manage resources. +We'll add two Party kinds to our Compute domain — `Tenant` for the operating company, and `Engineer` for the individuals who manage resources. ```elixir -defmodule Diffo.Compute.Organisation do +defmodule Diffo.Compute.Tenant do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - Organisation - an Organisation in the Compute domain + Tenant in the Compute domain """ alias Diffo.Provider.BaseParty @@ -467,13 +455,20 @@ defmodule Diffo.Compute.Organisation do domain: Compute resource do - description "A Compute Organisation" - plural_name :organisations + description "A Compute Tenant" + plural_name :tenants + end + + actions do + create :build do + accept [:id, :name] + change set_attribute(:type, :Organization) + end end - instance do - role :operates, Diffo.Compute.Cluster - role :operates, Diffo.Compute.GPU + instances do + role :operator, Diffo.Compute.Cluster + role :operator, Diffo.Compute.GPU end end ``` @@ -481,9 +476,7 @@ end ```elixir defmodule Diffo.Compute.Engineer do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - Engineer - an Engineer in the Compute domain + Engineer in the Compute domain """ alias Diffo.Provider.BaseParty @@ -498,12 +491,19 @@ defmodule Diffo.Compute.Engineer do plural_name :engineers end - instance do - role :manages, Diffo.Compute.Cluster + actions do + create :build do + accept [:id, :name] + change set_attribute(:type, :Individual) + end + end + + instances do + role :manager, Diffo.Compute.Cluster end - party do - role :employed_by, Diffo.Compute.Organisation + parties do + role :employer, Diffo.Compute.Tenant end end ``` @@ -515,8 +515,6 @@ With all resources defined we can now declare the `Diffo.Compute` domain, which ```elixir defmodule Diffo.Compute do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - Compute - example domain """ use Ash.Domain, @@ -526,7 +524,7 @@ defmodule Diffo.Compute do alias Diffo.Compute.GPU #alias Diffo.Compute.NPU alias Diffo.Compute.Cluster - alias Diffo.Compute.Organisation + alias Diffo.Compute.Tenant alias Diffo.Compute.Engineer resources do @@ -553,22 +551,22 @@ defmodule Diffo.Compute do define :relate_cluster, action: :relate end - resource Organisation do - define :create_organisation, action: :create - define :get_organisation_by_id, action: :read, get_by: :id - define :list_organisations, action: :list + resource Tenant do + define :create_tenant, action: :build + define :get_tenant_by_id, action: :read, get_by: :id + define :list_tenants, action: :read end resource Engineer do - define :create_engineer, action: :create + define :create_engineer, action: :build define :get_engineer_by_id, action: :read, get_by: :id - define :list_engineers, action: :list + define :list_engineers, action: :read end end end ``` -### Creating a Cluster +### Creating Party instances Clear any data from previous runs before starting (safe to re-evaluate): @@ -576,13 +574,30 @@ Clear any data from previous runs before starting (safe to re-evaluate): AshNeo4j.Neo4jHelper.delete_all() ``` -Now the domain is defined we can create our first Cluster instance. We'll use a helper module to set up the place and provider party ref: +Now the domain is defined we'll create our Tenant and Engineer first — we'll need them when building Cluster instances. The `id` for the Tenant is set to a meaningful string — the company's ABN. + +```elixir +alias Diffo.Compute +alias Diffo.Provider.Instance.Party + +{:ok, tenant} = Compute.create_tenant(%{ + id: "51824753556", + name: "Acme Compute Pty Ltd" +}) + +{:ok, engineer} = Compute.create_engineer(%{ + name: "Alice Zhang" +}) +``` + +### Creating a Cluster + +We'll use a helper module to set up the data centre place: ```elixir defmodule Diffo.Compute.Test do alias Diffo.Provider alias Diffo.Provider.Instance.Place - alias Diffo.Provider.Instance.Party def create_data_centre_place do dc = @@ -590,26 +605,18 @@ defmodule Diffo.Compute.Test do id: "NXTM2", name: :dataCentreId, href: "place/compute/NXTM2", - referredType: :GeographicSite - }) - - %Place{id: dc.id, role: :DataCentre} - end - - def create_provider_party do - provider = - Provider.create_party!(%{ - id: "Compute", - name: :organizationId, - referredType: :Organization + referred_type: :GeographicSite }) - %Party{id: provider.id, role: :Provider} + %Place{id: dc.id, role: :dataCentre} end end places = [Diffo.Compute.Test.create_data_centre_place()] -parties = [Diffo.Compute.Test.create_provider_party()] +parties = [ + %Party{id: tenant.id, role: :operator}, + %Party{id: engineer.id, role: :manager} +] cluster_1 = Diffo.Compute.build_cluster!(%{name: "cluster_1", places: places, parties: parties}) ``` @@ -622,8 +629,6 @@ Jason.encode!(cluster_1, pretty: true) |> IO.puts Now we'll create a couple of GPU instances: ```elixir -alias Diffo.Compute - gpu_1 = Compute.build_gpu!(%{name: "GPU 1"}) gpu_2 = Compute.build_gpu!(%{name: "GPU 2"}) ``` @@ -652,7 +657,6 @@ Now we can auto-assign GPU cores from each GPU to our cluster_1. We'll assign 3 ```elixir alias Diffo.Provider.Assignment -alias Diffo.Compute assignment = %{assignment: %Assignment{assignee_id: cluster_1.id, operation: :auto_assign}} gpu_1 = Compute.assign_gpu_core!(gpu_1, assignment) @@ -677,71 +681,9 @@ As an exercise, clone the GPU resource to create an NPU resource and assign some What happens when there are none left to assign? What happens when I request a specific assignment from an instance to which the partial resource is already assigned? -### Creating Party instances - -Now we can create an Organisation and an Engineer in the Compute domain. The `id` for the Organisation is set to a meaningful string — the company's ABN. - -```elixir -alias Diffo.Compute - -{:ok, org} = Compute.create_organisation(%{ - id: "51824753556", - name: "Acme Compute Pty Ltd", - kind: :organization -}) - -{:ok, engineer} = Compute.create_engineer(%{ - name: "Alice Zhang", - kind: :individual -}) -``` - -### Using Parties with a Cluster - -We can now build a Cluster and relate it to our Organisation and Engineer using the base provider party ref mechanism. - -```elixir -alias Diffo.Provider.Instance.Party - -parties = [ - %Party{id: org.id, role: :operated_by}, - %Party{id: engineer.id, role: :managed_by} -] - -cluster_2 = Compute.build_cluster!(%{ - name: "cluster_2", - places: [Diffo.Compute.Test.create_data_centre_place()], - parties: parties -}) - -Jason.encode!(cluster_2, pretty: true) |> IO.puts -``` - -### What the Party DSL declares - -The `instance do` and `party do` blocks are compile-time declarations — they document which roles a Party kind plays and make that information available via `Diffo.Provider.Party.Extension.Info`: - -```elixir -alias Diffo.Provider.Party.Extension.Info, as: PartyInfo - -PartyInfo.instance(Diffo.Compute.Organisation) -``` - -```elixir -PartyInfo.party(Diffo.Compute.Engineer) -``` - -These declarations also inform the Instance DSL's `parties do` block, which documents which party kinds an Instance resource expects: - -```elixir -alias Diffo.Provider.Instance.Extension.Info, as: InstanceInfo - -InstanceInfo.parties(Diffo.Compute.Cluster) -``` - ### What Next? -In this tutorial you've used Diffo's Provider Instance Extension to define a Compute domain with a composite Cluster resource comprised of assigned GPU cores, and the Provider Party Extension to define Organisation and Engineer party kinds that operate and manage those resources. +In this tutorial you've used Diffo's Provider Instance Extension to define a Compute domain with a composite Cluster resource comprised of assigned GPU cores, and the Provider Party Extension to define Tenant and Engineer party kinds that operate and manage those resources. The BaseParty fragment follows the same pattern as BaseInstance — domain-specific resources use it as a fragment and finish their actions with a domain-scoped reload to pick up extended fields. diff --git a/documentation/how_to/use_diffo_type.livemd b/documentation/how_to/use_diffo_type.livemd index e4df360..83f783e 100644 --- a/documentation/how_to/use_diffo_type.livemd +++ b/documentation/how_to/use_diffo_type.livemd @@ -9,7 +9,7 @@ SPDX-License-Identifier: MIT ```elixir Mix.install( [ - {:diffo, "~> 0.2.0"} + {:diffo, path: "/Users/Beanlanda/git/diffo"} ], consolidate_protocols: false ) @@ -289,4 +289,4 @@ Ash.Type.cast_input(Dynamic, valid, []) ## Further reading * [Diffo Livebook](../../diffo.livemd) — full tutorial including Neo4j setup and Provider resources -* [Using Diffo Provider Instance Extension](./use_diffo_provider_instance_extension.livemd) — defining custom resources with typed characteristics +* [Using Diffo Provider Instance Extension](./use_diffo_provider_extension.livemd) — defining custom resources with typed characteristics diff --git a/lib/diffo/changes/detail_event.ex b/lib/diffo/changes/detail_event.ex index b6c7911..7d24ffb 100644 --- a/lib/diffo/changes/detail_event.ex +++ b/lib/diffo/changes/detail_event.ex @@ -3,12 +3,7 @@ # SPDX-License-Identifier: MIT defmodule Diffo.Changes.DetailEvent do - @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - DetailEvent - Ash Resource Change for detailing an Event - - """ + @moduledoc false use Ash.Resource.Change def change(changeset, _opts, _context) do diff --git a/lib/diffo/changes/detail_relationship.ex b/lib/diffo/changes/detail_relationship.ex index 137ec97..6515b14 100644 --- a/lib/diffo/changes/detail_relationship.ex +++ b/lib/diffo/changes/detail_relationship.ex @@ -3,13 +3,7 @@ # SPDX-License-Identifier: MIT defmodule Diffo.Changes.DetailRelationship do - @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - - DetailRelationship - Ash Resource Change for populating relationship detail - - """ + @moduledoc false use Ash.Resource.Change def change(changeset, _opts, _context) do diff --git a/lib/diffo/helpers/util.ex b/lib/diffo/helpers/util.ex index 5c23eda..c75606c 100644 --- a/lib/diffo/helpers/util.ex +++ b/lib/diffo/helpers/util.ex @@ -4,11 +4,8 @@ defmodule Diffo.Util do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - Util - utility methods + Utility methods """ - @doc """ Renames map key, unless old value is empty ## Examples @@ -98,25 +95,6 @@ defmodule Diffo.Util do end end - @spec compare(any(), any()) :: :eq | :gt | :lt - @doc """ - Compares two terms - ## Examples - iex> Diffo.Util.compare("a", "a") - :eq - iex> Diffo.Util.compare("b", "a") - :gt - iex> Diffo.Util.compare("a", "b") - :lt - """ - def compare(a, b) do - cond do - a < b -> :lt - a > b -> :gt - true -> :eq - end - end - @doc """ true if the datetime is close to (+/- 5 mins) from now ## Examples @@ -149,7 +127,6 @@ defmodule Diffo.Util do iex> Diffo.Util.past?(DateTime.utc_now() |> DateTime.shift(minute: -4)) false """ - def past?(datetime) do now = DateTime.utc_now() past = DateTime.shift(now, minute: -5) @@ -165,7 +142,6 @@ defmodule Diffo.Util do iex> Diffo.Util.future?(DateTime.utc_now() |> DateTime.shift(minute: 4)) false """ - def future?(datetime) do now = DateTime.utc_now() future = DateTime.shift(now, minute: 5) @@ -215,7 +191,6 @@ defmodule Diffo.Util do :past """ - def summarise(datetime) do cond do close_to_now?(datetime) -> :now @@ -235,7 +210,6 @@ defmodule Diffo.Util do "past,now,future" """ - def summarise_dates(payload) do Regex.replace(~r/\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}.\d{3}Z/, payload, fn iso8601 -> case DateTime.from_iso8601(iso8601) do diff --git a/lib/diffo/helpers/uuid.ex b/lib/diffo/helpers/uuid.ex index ed04e79..7229535 100644 --- a/lib/diffo/helpers/uuid.ex +++ b/lib/diffo/helpers/uuid.ex @@ -4,11 +4,8 @@ defmodule Diffo.Uuid do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - Uuid - validate and/or create uuids + Validate and/or create Uuid """ - @doc """ Generates a uuid4 """ diff --git a/lib/diffo/provider.ex b/lib/diffo/provider.ex index 50c5b0a..18ea091 100644 --- a/lib/diffo/provider.ex +++ b/lib/diffo/provider.ex @@ -4,9 +4,7 @@ defmodule Diffo.Provider do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - Provider - API endpoint + Provider API endpoint """ use Ash.Domain, otp_app: :diffo diff --git a/lib/diffo/provider/assigner/assignable_value.ex b/lib/diffo/provider/assigner/assignable_value.ex index 51432a6..be36eb2 100644 --- a/lib/diffo/provider/assigner/assignable_value.ex +++ b/lib/diffo/provider/assigner/assignable_value.ex @@ -4,9 +4,7 @@ defmodule Diffo.Provider.AssignableValue do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - AssignableValue - AshTyped Struct for Assignable Characteristic Value + Ash Typed Struct for Assignable Characteristic Value """ use Ash.TypedStruct, extensions: [AshJason.TypedStruct] diff --git a/lib/diffo/provider/assigner/assigner.ex b/lib/diffo/provider/assigner/assigner.ex index 1867cca..32b47e2 100644 --- a/lib/diffo/provider/assigner/assigner.ex +++ b/lib/diffo/provider/assigner/assigner.ex @@ -4,11 +4,8 @@ defmodule Diffo.Provider.Assigner do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - Assigner - Helper to perform Assignment maintaining AssignableValue + Helper to perform Assignment maintaining AssignableValue """ - alias Diffo.Provider.AssignableValue alias Diffo.Type.Value diff --git a/lib/diffo/provider/assigner/assignment.ex b/lib/diffo/provider/assigner/assignment.ex index 8103b44..9f34ea0 100644 --- a/lib/diffo/provider/assigner/assignment.ex +++ b/lib/diffo/provider/assigner/assignment.ex @@ -4,9 +4,7 @@ defmodule Diffo.Provider.Assignment do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - Assignment - AshTyped Struct for Assignment + Ash Typed Struct for Assignment """ use Ash.TypedStruct, extensions: [AshJason.TypedStruct] @@ -31,12 +29,18 @@ defmodule Diffo.Provider.Assignment do constraints: [one_of: [nil, :assign, :unassign, :auto_assign]] end + def compare(%__MODULE__{id: a}, %__MODULE__{id: b}) do + cond do + a < b -> :lt + a > b -> :gt + true -> :eq + end + end + defimpl String.Chars do def to_string(struct) do inspect(struct) end end - def compare(%{id: id0}, %{id: id1}), - do: Diffo.Util.compare(id0, id1) end diff --git a/lib/diffo/provider/components/base_instance.ex b/lib/diffo/provider/components/base_instance.ex index 76d04dd..6fcb5cd 100644 --- a/lib/diffo/provider/components/base_instance.ex +++ b/lib/diffo/provider/components/base_instance.ex @@ -4,9 +4,74 @@ defmodule Diffo.Provider.BaseInstance do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference + Ash Resource Fragment which is a the point of extension for your TMF Service or Resource Instance - BaseInstance - Ash Resource Fragment of a TMF Service or Resource Instance + `BaseInstance` is the foundation for domain-specific Service and Resource kinds. + Include it as a fragment on an `Ash.Resource` to get common Instance attributes, + Neo4j graph wiring, state machine, and the `Diffo.Provider.Instance.Extension` DSL. + + ## Instance Extension DSL + + The `Diffo.Provider.Instance.Extension` DSL provides compile-time declaration blocks + for describing the shape of a domain-specific Service or Resource. + + `specification do` — declares the TMF Specification for this Instance kind. + + `features do` — declares the Features this Instance kind may have, each optionally + carrying a typed characteristic payload. + + `characteristics do` — declares the top-level Characteristics of this Instance kind, + each backed by an `Ash.TypedStruct`. + + `parties do` — declares the Party roles this Instance kind relates to. Role names are + domain-specific nouns describing what the party is to the instance. Two forms: + + parties do + party :provider, MyApp.Provider, calculate: :provider_calculation + parties :installer, MyApp.Installer + parties :technician, MyApp.Technician, constraints: [min: 1, max: 3] + party :owner, MyApp.InfrastructureCo, reference: true + end + + - `party` — singular (at most one party in this role) + - `parties` — plural (unbounded, or bounded with `constraints:`) + - `reference: true` — no direct `PartyRef` edge; party is reachable by graph traversal + - `calculate:` — names an Ash calculation on this resource that produces the party at build time + + All declarations are introspectable via `Diffo.Provider.Instance.Extension.Info`. + + ## Usage + + defmodule MyApp.Cluster do + use Ash.Resource, fragments: [BaseInstance], domain: MyApp.Domain + + resource do + description "A Cluster Resource Instance" + plural_name :clusters + end + + specification do + id "4bcfc4c9-e776-4878-a658-e8d81857bed7" + name "cluster" + type :resourceSpecification + end + + parties do + party :operator, MyApp.Organization + parties :installer, MyApp.Engineer + end + end + + ## Action pattern + + Domain-specific Instance resources should finish their `build` action with a reload via + their own domain's `get_xxx_by_id` to pick up extended fields: + + create :build do + change after_action(fn changeset, result, _context -> + ActionHelper.build_after(changeset, result, MyApp.Domain, :get_cluster_by_id) + end) + end """ use Spark.Dsl.Fragment, of: Ash.Resource, diff --git a/lib/diffo/provider/components/base_party.ex b/lib/diffo/provider/components/base_party.ex index f1f3c01..3498f19 100644 --- a/lib/diffo/provider/components/base_party.ex +++ b/lib/diffo/provider/components/base_party.ex @@ -4,42 +4,88 @@ defmodule Diffo.Provider.BaseParty do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference + Ash Resource Fragment which is a the point of extension for your TMF Party - BaseParty - Ash Resource Fragment of a TMF Party + `BaseParty` is the foundation for domain-specific Party kinds such as Organization or Person. + Include it as a fragment on an `Ash.Resource` to get common Party attributes, Neo4j graph + wiring, and the `Diffo.Provider.Party.Extension` DSL. - `BaseParty` is the foundation for domain-specific Party kinds such as RSP or Person. - It provides common Party attributes and the `Diffo.Provider.Party.Extension` DSL, which - allows a Party kind to declare the roles it plays with respect to Instances and other Parties. + `Diffo.Provider.Party` uses `BaseParty` directly as the out-of-the-box TMF Party resource. + Domain-specific resources extend it for richer domain identity. + + ## Attributes + + - `id` — string primary key, defaults to a generated uuid4. Can be set by the domain to any + meaningful string (e.g. an ABN or a data centre identifier). + - `href` — optional URI for the party. + - `name` — the party name. + - `type` — TMF `@type`. Defaults to `:PartyRef`. One of `:PartyRef`, `:Individual`, + `:Organization`, `:Entity`. When `referred_type` is present, `type` must be `:PartyRef`. + - `referred_type` — TMF `@referredType`. One of `:Individual`, `:Organization`, `:Entity`. + When present, indicates this is a reference to a party of that kind; `type` must be `:PartyRef`. + + ## Party Extension DSL + + The `Diffo.Provider.Party.Extension` DSL provides two compile-time declaration blocks. + Role names are domain-specific nouns from the party's perspective — timeless, camelCase + when multi-word. + + `instances do` — declares the roles this Party kind plays with respect to Instances: + + instances do + role :operator, MyApp.Cluster + role :dataCentre, MyApp.Facility + end + + `parties do` — declares the roles this Party kind plays with respect to other Parties: + + parties do + role :employer, MyApp.Organization + end + + Both blocks are introspectable via `Diffo.Provider.Party.Extension.Info`. ## Usage - defmodule MyApp.Organisation do + defmodule MyApp.RSP do use Ash.Resource, fragments: [BaseParty], domain: MyApp.Domain resource do - description "An Organisation" - plural_name :organisations + description "A Retail Service Provider" + plural_name :rsps end - instance do - role :facilitates, MyApp.AccessService + jason do + pick [:id, :name, :type] + compact true end - end - ## Action pattern + outstanding do + expect [:id, :name, :type] + end - Domain-specific Party resources should finish their `create` action with a reload via - their own domain's `get_xxx_by_id` to pick up extended fields: + actions do + create :build do + accept [:id, :href, :name] + change set_attribute(:referred_type, :Organization) + end + end - create :create do - accept [:name] - change after_action(fn _changeset, result, _context -> - MyApp.Domain.get_organisation_by_id(result.id) - end) + instances do + role :provider, MyApp.AccessService + end end - """ + ## TMF type and referred_type + + The `type` and `referred_type` attributes map to the TMF `@type` and `@referredType` JSON + fields via the jason layer. Use the `build` action to declare the TMF identity of your + domain party — this is also the contract for how the party appears in TMF serialisation + of `relatedParty` on instances. + + - `type: :Organization` — this party IS an Organization (direct). + - `referred_type: :Organization` — this is a PartyRef pointing to an Organization. + """ use Spark.Dsl.Fragment, of: Ash.Resource, otp_app: :diffo, @@ -53,19 +99,16 @@ defmodule Diffo.Provider.BaseParty do neo4j do relate [ - {:party_refs, :RELATES, :incoming, :PartyRef} + {:party_refs, :RELATES, :incoming, :PartyRef}, + {:external_identifiers, :OWNS, :outgoing, :ExternalIdentifier}, + {:notes, :AUTHORS, :outgoing, :Note} ] - label :Party - end - - jason do - pick [:id, :name, :kind] - compact true - end + guard [ + {:OWNS, :outgoing, :ExternalIdentifier} + ] - outstanding do - expect [:id, :name, :kind] + label :Party end attributes do @@ -78,17 +121,31 @@ defmodule Diffo.Provider.BaseParty do source :key end + attribute :href, :string do + description "the href of this party" + allow_nil? true + public? true + end + attribute :name, :string do description "the name of this party" allow_nil? true public? true end - attribute :kind, :atom do - description "the kind of this party, either individual or organization" + attribute :type, :atom do + description "the type of the party" allow_nil? false public? true - constraints one_of: [:individual, :organization] + default :PartyRef + constraints one_of: [:PartyRef, :Individual, :Organization, :Entity] + end + + attribute :referred_type, :atom do + description "the type of the party" + allow_nil? true + public? true + constraints one_of: [:Individual, :Organization, :Entity] end create_timestamp :created_at @@ -102,6 +159,18 @@ defmodule Diffo.Provider.BaseParty do destination_attribute :party_id public? true end + + has_many :external_identifiers, Diffo.Provider.ExternalIdentifier do + description "the external identifiers owned by this party" + destination_attribute :owner_id + public? true + end + + has_many :notes, Diffo.Provider.Note do + description "the notes authored by this party" + destination_attribute :note_id + public? true + end end actions do @@ -109,19 +178,30 @@ defmodule Diffo.Provider.BaseParty do create :create do description "creates a party of this kind" - accept [:id, :name, :kind] + accept [:id, :href, :name, :type, :referred_type] upsert? true end update :update do description "updates the party name" - accept [:name] + accept [:href, :name, :type, :referred_type] end read :list do description "lists all parties of this kind" end + read :find_by_id do + description "finds parties by id" + get? false + + argument :query, :ci_string do + description "Return only parties with id's including the given value." + end + + filter expr(contains(id, ^arg(:query))) + end + read :find_by_name do description "finds parties by name" get? false @@ -133,4 +213,24 @@ defmodule Diffo.Provider.BaseParty do filter expr(contains(name, ^arg(:query))) end end + + validations do + validate {Diffo.Validations.HrefEndsWithId, id: :id, href: :href} do + where [present(:id), present(:href)] + end + + validate attribute_equals(:type, :PartyRef) do + where present(:referred_type) + message "when referred_type is present, type must be PartyRef" + end + + validate attribute_does_not_equal(:type, :PartyRef) do + where absent(:referred_type) + message "when referred_type is absent, type must be not be PartyRef" + end + end + + preparations do + prepare build(sort: [id: :asc, name: :asc]) + end end diff --git a/lib/diffo/provider/components/calculations/instance_href.ex b/lib/diffo/provider/components/calculations/instance_href.ex index eccc37f..70c9aac 100644 --- a/lib/diffo/provider/components/calculations/instance_href.ex +++ b/lib/diffo/provider/components/calculations/instance_href.ex @@ -3,12 +3,7 @@ # SPDX-License-Identifier: MIT defmodule Diffo.Provider.Calculations.InstanceHref do - @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - InstanceHref - Ash Resource Calculation for generating instance href - - """ + @moduledoc false use Ash.Resource.Calculation @impl true diff --git a/lib/diffo/provider/components/calculations/specification_href.ex b/lib/diffo/provider/components/calculations/specification_href.ex index 8382a10..ae32b4d 100644 --- a/lib/diffo/provider/components/calculations/specification_href.ex +++ b/lib/diffo/provider/components/calculations/specification_href.ex @@ -5,13 +5,7 @@ defmodule Diffo.Provider.Calculations.SpecificationHref do use Ash.Resource.Calculation - @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - SpecificationHref - Ash Resource Calculation for generating specification href - - """ - + @moduledoc false @impl true def load(_query, _opts, _context), do: [:type, :tmf_version, :id] diff --git a/lib/diffo/provider/components/calculations/specification_instance_type.ex b/lib/diffo/provider/components/calculations/specification_instance_type.ex index 64cdff6..b812bc0 100644 --- a/lib/diffo/provider/components/calculations/specification_instance_type.ex +++ b/lib/diffo/provider/components/calculations/specification_instance_type.ex @@ -3,12 +3,7 @@ # SPDX-License-Identifier: MIT defmodule Diffo.Provider.Calculations.SpecificationInstanceType do - @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - SpecificationInstanceType - Ash Resource Calculation for generating the instance type a specification specifies - - """ + @moduledoc false use Ash.Resource.Calculation @impl true diff --git a/lib/diffo/provider/components/calculations/specification_version.ex b/lib/diffo/provider/components/calculations/specification_version.ex index fad6aee..4bb90c2 100644 --- a/lib/diffo/provider/components/calculations/specification_version.ex +++ b/lib/diffo/provider/components/calculations/specification_version.ex @@ -3,12 +3,7 @@ # SPDX-License-Identifier: MIT defmodule Diffo.Provider.Calculations.SpecificationVersion do - @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - SpecificationVersion - Ash Resource Calculation for generating the version of a specification - - """ + @moduledoc false use Ash.Resource.Calculation @impl true diff --git a/lib/diffo/provider/components/characteristic.ex b/lib/diffo/provider/components/characteristic.ex index 3f46f9d..64f49dd 100644 --- a/lib/diffo/provider/components/characteristic.ex +++ b/lib/diffo/provider/components/characteristic.ex @@ -4,9 +4,7 @@ defmodule Diffo.Provider.Characteristic do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - Characteristic - Ash Resource for a TMF Characteristic + Ash Resource for a TMF Characteristic """ use Ash.Resource, otp_app: :diffo, @@ -204,19 +202,6 @@ defmodule Diffo.Provider.Characteristic do prepare build(sort: [name: :asc]) end - @doc """ - Compares two characteristic, by ascending name - ## Examples - iex> Diffo.Provider.Characteristic.compare(%{name: "a"}, %{name: "a"}) - :eq - iex> Diffo.Provider.Characteristic.compare(%{name: "b"}, %{name: "a"}) - :gt - iex> Diffo.Provider.Characteristic.compare(%{name: "a"}, %{name: "b"}) - :lt - - """ - def compare(%{name: name0}, %{name: name1}), do: Diffo.Util.compare(name0, name1) - defimpl Diffo.Unwrap do def unwrap(%{values: values}) when is_list(values), do: Diffo.Unwrap.unwrap(values) def unwrap(%{value: value}), do: Diffo.Unwrap.unwrap(value) diff --git a/lib/diffo/provider/components/entity.ex b/lib/diffo/provider/components/entity.ex index ce736cd..ab9a017 100644 --- a/lib/diffo/provider/components/entity.ex +++ b/lib/diffo/provider/components/entity.ex @@ -4,9 +4,7 @@ defmodule Diffo.Provider.Entity do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - Entity - Ash Resource for a TMF Entity + Ash Resource for a TMF Entity """ use Ash.Resource, otp_app: :diffo, @@ -26,13 +24,13 @@ defmodule Diffo.Provider.Entity do end jason do - pick [:id, :href, :name, :referredType, :type] + pick [:id, :href, :name, :referred_type, :type] compact true - rename referredType: "@referredType", type: "@type" + rename referred_type: "@referredType", type: "@type" end outstanding do - expect [:id, :href, :name, :referredType, :type] + expect [:id, :href, :name, :referred_type, :type] end actions do @@ -40,7 +38,7 @@ defmodule Diffo.Provider.Entity do create :create do description "creates a entity" - accept [:id, :href, :name, :type, :referredType] + accept [:id, :href, :name, :type, :referred_type] upsert? true end @@ -72,7 +70,7 @@ defmodule Diffo.Provider.Entity do update :update do description "updates the entity" - accept [:href, :name, :type, :referredType] + accept [:href, :name, :type, :referred_type] end end @@ -104,7 +102,7 @@ defmodule Diffo.Provider.Entity do default :EntityRef end - attribute :referredType, :atom do + attribute :referred_type, :atom do description "the type of the entity" allow_nil? true public? true @@ -129,13 +127,13 @@ defmodule Diffo.Provider.Entity do end validate attribute_equals(:type, :EntityRef) do - where present(:referredType) - message "when referredType is present, type must be EntityRef" + where present(:referred_type) + message "when referred_type is present, type must be EntityRef" end validate attribute_does_not_equal(:type, :EntityRef) do - where absent(:referredType) - message "when referredType is absent, type must be not be EntityRef" + where absent(:referred_type) + message "when referred_type is absent, type must be not be EntityRef" end end @@ -143,16 +141,4 @@ defmodule Diffo.Provider.Entity do prepare build(sort: [id: :asc]) end - @doc """ - Compares two entity, by ascending id - ## Examples - iex> Diffo.Provider.Entity.compare(%{id: "a"}, %{id: "a"}) - :eq - iex> Diffo.Provider.Entity.compare(%{id: "b"}, %{id: "a"}) - :gt - iex> Diffo.Provider.Entity.compare(%{id: "a"}, %{id: "b"}) - :lt - - """ - def compare(%{id: id0}, %{id: id1}), do: Diffo.Util.compare(id0, id1) end diff --git a/lib/diffo/provider/components/entity_ref.ex b/lib/diffo/provider/components/entity_ref.ex index f85cbc3..532b3c9 100644 --- a/lib/diffo/provider/components/entity_ref.ex +++ b/lib/diffo/provider/components/entity_ref.ex @@ -4,8 +4,6 @@ defmodule Diffo.Provider.EntityRef do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - EntityRef - Ash Resource for a TMF Entity Reference """ use Ash.Resource, @@ -34,7 +32,7 @@ defmodule Diffo.Provider.EntityRef do |> Diffo.Util.extract_suppress(:entity, :id, :id) |> Diffo.Util.extract_suppress(:entity, :href, :href) |> Diffo.Util.extract_suppress(:entity, :name, :name) - |> Diffo.Util.extract_suppress(:entity, :referredType, "@referredType") + |> Diffo.Util.extract_suppress(:entity, :referred_type, "@referredType") |> Diffo.Util.extract_suppress(:entity, :type, "@type") |> Diffo.Util.remove(:party) end @@ -124,17 +122,4 @@ defmodule Diffo.Provider.EntityRef do prepare build(load: [:entity], sort: [created_at: :desc]) end - @doc """ - Compares two entity ref, by ascending entity_id - ## Examples - iex> Diffo.Provider.EntityRef.compare(%{entity_id: "a"}, %{entity_id: "a"}) - :eq - iex> Diffo.Provider.EntityRef.compare(%{entity_id: "b"}, %{entity_id: "a"}) - :gt - iex> Diffo.Provider.EntityRef.compare(%{entity_id: "a"}, %{entity_id: "b"}) - :lt - - """ - def compare(%{entity_id: entity_id0}, %{entity_id: entity_id1}), - do: Diffo.Util.compare(entity_id0, entity_id1) end diff --git a/lib/diffo/provider/components/event.ex b/lib/diffo/provider/components/event.ex index 3173273..1ec9167 100644 --- a/lib/diffo/provider/components/event.ex +++ b/lib/diffo/provider/components/event.ex @@ -4,9 +4,7 @@ defmodule Diffo.Provider.Event do @moduledoc """ - Diffo - TMF Service and Reource Management with a difference - - Event - Ash Resource for a TMF Event + Ash Resource for a TMF Event """ use Ash.Resource, otp_app: :diffo, @@ -156,17 +154,4 @@ defmodule Diffo.Provider.Event do ) end - @doc """ - Compares two event, by id - ## Examples - iex> Diffo.Provider.Event.compare(%{id: "a"}, %{id: "a"}) - :eq - iex> Diffo.Provider.Event.compare(%{id: "b"}, %{id: "a"}) - :gt - iex> Diffo.Provider.Event.compare(%{id: "a"}, %{id: "b"}) - :lt - - """ - def compare(%{id: id0}, %{id: id1}), - do: Diffo.Util.compare(id0, id1) end diff --git a/lib/diffo/provider/components/external_identifier.ex b/lib/diffo/provider/components/external_identifier.ex index 63f09b6..4bd7702 100644 --- a/lib/diffo/provider/components/external_identifier.ex +++ b/lib/diffo/provider/components/external_identifier.ex @@ -4,9 +4,7 @@ defmodule Diffo.Provider.ExternalIdentifier do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - ExternalIdentifier - Ash Resource for a TMF ExternalIdentifier + Ash Resource for a TMF ExternalIdentifier """ use Ash.Resource, otp_app: :diffo, @@ -147,17 +145,4 @@ defmodule Diffo.Provider.ExternalIdentifier do prepare build(load: [:owner], sort: [created_at: :desc]) end - @doc """ - Compares two external identifier, by most recent insertion order - ## Examples - iex> Diffo.Provider.ExternalIdentifier.compare(%{created_at: "a"}, %{created_at: "a"}) - :eq - iex> Diffo.Provider.ExternalIdentifier.compare(%{created_at: "b"}, %{created_at: "a"}) - :gt - iex> Diffo.Provider.ExternalIdentifier.compare(%{created_at: "a"}, %{created_at: "b"}) - :lt - - """ - def compare(%{created_at: created_at0}, %{created_at: created_at1}), - do: Diffo.Util.compare(created_at0, created_at1) end diff --git a/lib/diffo/provider/components/feature.ex b/lib/diffo/provider/components/feature.ex index 3300781..505c72b 100644 --- a/lib/diffo/provider/components/feature.ex +++ b/lib/diffo/provider/components/feature.ex @@ -4,9 +4,7 @@ defmodule Diffo.Provider.Feature do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - Feature - Ash Resource for a TMF Feature + Ash Resource for a TMF Feature """ use Ash.Resource, otp_app: :diffo, @@ -138,16 +136,4 @@ defmodule Diffo.Provider.Feature do prepare build(load: [:characteristics], sort: [name: :asc]) end - @doc """ - Compares two feature, by ascending name - ## Examples - iex> Diffo.Provider.Feature.compare(%{name: "a"}, %{name: "a"}) - :eq - iex> Diffo.Provider.Feature.compare(%{name: "b"}, %{name: "a"}) - :gt - iex> Diffo.Provider.Feature.compare(%{name: "a"}, %{name: "b"}) - :lt - - """ - def compare(%{name: name0}, %{name: name1}), do: Diffo.Util.compare(name0, name1) end diff --git a/lib/diffo/provider/components/instance.ex b/lib/diffo/provider/components/instance.ex index a9d28ad..aab1d76 100644 --- a/lib/diffo/provider/components/instance.ex +++ b/lib/diffo/provider/components/instance.ex @@ -4,9 +4,7 @@ defmodule Diffo.Provider.Instance do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - Instance - Ash Resource for a TMF Service or Resource Instance + Ash Resource for a TMF Service or Resource Instance """ alias Diffo.Provider.BaseInstance diff --git a/lib/diffo/provider/components/instance/extension.ex b/lib/diffo/provider/components/instance/extension.ex index f353f9d..3956787 100644 --- a/lib/diffo/provider/components/instance/extension.ex +++ b/lib/diffo/provider/components/instance/extension.ex @@ -4,9 +4,15 @@ defmodule Diffo.Provider.Instance.Extension do @moduledoc """ - DSL Extension customising an Instance - """ + DSL Extension customising an Instance. + + Provides compile-time declaration blocks for domain-specific Service and Resource kinds + built on `Diffo.Provider.BaseInstance`. All declarations are introspectable via + `Diffo.Provider.Instance.Extension.Info`. + See the [DSL cheat sheet](DSL-Diffo.Provider.Instance.Extension.html) for the full DSL reference. + See `Diffo.Provider.BaseInstance` for full usage documentation. + """ @specification %Spark.Dsl.Section{ name: :specification, describe: "Defines the Instance Specification", @@ -152,26 +158,50 @@ defmodule Diffo.Provider.Instance.Extension do ] } + @party_schema [ + role: [ + doc: "The role name, an atom", + type: :atom, + required: true + ], + party_type: [ + doc: "The module of the Party kind. An atom module name such as a BaseParty-derived resource.", + type: :any + ], + reference: [ + doc: "If true, no direct PartyRef edge is created; the party is reachable by graph traversal.", + type: :boolean, + default: false + ], + calculate: [ + doc: "Name of an Ash calculation on this resource that produces the party at build time.", + type: :atom + ] + ] + @party_entity %Spark.Dsl.Entity{ name: :party, - describe: "Declares a party role on this Instance", + describe: "Declares a singular party role on this Instance", target: Diffo.Provider.Instance.Extension.PartyDeclaration, args: [:role, :party_type], - schema: [ - role: [ - doc: """ - The role name, an atom - """, - type: :atom, - required: true - ], - party_type: [ - doc: """ - The module of the Party kind. An atom module name such as a BaseParty-derived resource. - """, - type: :any - ] - ] + auto_set_fields: [multiple: false], + schema: @party_schema + } + + @parties_entity %Spark.Dsl.Entity{ + name: :parties, + describe: "Declares a plural party role on this Instance", + target: Diffo.Provider.Instance.Extension.PartyDeclaration, + args: [:role, :party_type], + auto_set_fields: [multiple: true], + schema: + @party_schema ++ + [ + constraints: [ + doc: "Multiplicity constraints on the number of parties in this role, e.g. [min: 1, max: 3]", + type: :keyword_list + ] + ] } @parties %Spark.Dsl.Section{ @@ -180,13 +210,15 @@ defmodule Diffo.Provider.Instance.Extension do examples: [ """ parties do - party :facilitated_by, MyApp.Rsp - party :overseen_by, MyApp.Person + party :provider, MyApp.Provider, calculate: :provider_calculation + parties :technician, MyApp.Technician, constraints: [min: 1, max: 3] + party :owner, MyApp.InfrastructureCo, reference: true end """ ], entities: [ - @party_entity + @party_entity, + @parties_entity ] } diff --git a/lib/diffo/provider/components/instance/extension/action_helper.ex b/lib/diffo/provider/components/instance/extension/action_helper.ex index 39df01e..8dfa7c1 100644 --- a/lib/diffo/provider/components/instance/extension/action_helper.ex +++ b/lib/diffo/provider/components/instance/extension/action_helper.ex @@ -3,12 +3,7 @@ # SPDX-License-Identifier: MIT defmodule Diffo.Provider.Instance.ActionHelper do - @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - ActionHelper - helping with Instance actions - """ - + @moduledoc false alias Diffo.Provider.Instance.Specification alias Diffo.Provider.Instance.Relationship alias Diffo.Provider.Instance.Feature @@ -24,6 +19,7 @@ defmodule Diffo.Provider.Instance.ActionHelper do |> Specification.set_specified_by_argument() |> Feature.set_features_argument() |> Characteristic.set_characteristics_argument() + |> Party.validate_parties() end @doc """ diff --git a/lib/diffo/provider/components/instance/extension/characteristic.ex b/lib/diffo/provider/components/instance/extension/characteristic.ex index ae0a6b8..43c8d1b 100644 --- a/lib/diffo/provider/components/instance/extension/characteristic.ex +++ b/lib/diffo/provider/components/instance/extension/characteristic.ex @@ -3,12 +3,7 @@ # SPDX-License-Identifier: MIT defmodule Diffo.Provider.Instance.Characteristic do - @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - Characteristic for Instance Extension - """ - + @moduledoc false require Logger alias Diffo.Provider diff --git a/lib/diffo/provider/components/instance/extension/feature.ex b/lib/diffo/provider/components/instance/extension/feature.ex index 85de261..9428419 100644 --- a/lib/diffo/provider/components/instance/extension/feature.ex +++ b/lib/diffo/provider/components/instance/extension/feature.ex @@ -3,12 +3,7 @@ # SPDX-License-Identifier: MIT defmodule Diffo.Provider.Instance.Feature do - @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - Feature for Instance Extension - """ - + @moduledoc false require Logger alias Diffo.Provider diff --git a/lib/diffo/provider/components/instance/extension/party.ex b/lib/diffo/provider/components/instance/extension/party.ex index e8b808e..9505d51 100644 --- a/lib/diffo/provider/components/instance/extension/party.ex +++ b/lib/diffo/provider/components/instance/extension/party.ex @@ -3,19 +3,71 @@ # SPDX-License-Identifier: MIT defmodule Diffo.Provider.Instance.Party do - @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - Party for Instance Extension - """ - + @moduledoc false alias Diffo.Provider + alias Diffo.Provider.Instance.Extension.Info, as: InstanceInfo @doc """ Struct for a Party """ defstruct [:id, :role] + @doc false + def validate_parties(changeset) do + declarations = InstanceInfo.parties(changeset.resource) + + if declarations == [] do + changeset + else + parties = Ash.Changeset.get_argument(changeset, :parties) || [] + changeset + |> validate_roles(parties, declarations) + |> validate_constraints(parties, declarations) + end + end + + defp validate_roles(changeset, parties, declarations) do + declared_roles = MapSet.new(declarations, & &1.role) + + Enum.reduce(parties, changeset, fn %{role: role}, cs -> + if MapSet.member?(declared_roles, role) do + cs + else + Ash.Changeset.add_error(cs, + field: :parties, + message: "role #{inspect(role)} is not declared on this resource" + ) + end + end) + end + + defp validate_constraints(changeset, parties, declarations) do + counts = Enum.frequencies_by(parties, & &1.role) + + declarations + |> Enum.reject(&(&1.reference || &1.calculate)) + |> Enum.reduce(changeset, fn decl, cs -> + count = Map.get(counts, decl.role, 0) + constraints = decl.constraints || [] + + cs + |> check_min(decl.role, count, Keyword.get(constraints, :min)) + |> check_max(decl.role, count, Keyword.get(constraints, :max)) + end) + end + + defp check_min(cs, _role, _count, nil), do: cs + defp check_min(cs, _role, count, min) when count >= min, do: cs + defp check_min(cs, role, count, min), + do: Ash.Changeset.add_error(cs, field: :parties, + message: "role #{inspect(role)} requires at least #{min} (got #{count})") + + defp check_max(cs, _role, _count, nil), do: cs + defp check_max(cs, _role, count, max) when count <= max, do: cs + defp check_max(cs, role, count, max), + do: Ash.Changeset.add_error(cs, field: :parties, + message: "role #{inspect(role)} allows at most #{max} (got #{count})") + @doc """ Relates the parties in the changeset with the Extended Instance by creating party_ref """ diff --git a/lib/diffo/provider/components/instance/extension/party_declaration.ex b/lib/diffo/provider/components/instance/extension/party_declaration.ex index 69fb67d..e1cff0c 100644 --- a/lib/diffo/provider/components/instance/extension/party_declaration.ex +++ b/lib/diffo/provider/components/instance/extension/party_declaration.ex @@ -4,12 +4,10 @@ defmodule Diffo.Provider.Instance.Extension.PartyDeclaration do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - PartyDeclaration - DSL entity declaring a party role on an Instance """ - - defstruct [:role, :party_type, __spark_metadata__: nil] + defstruct [:role, :party_type, :multiple, :reference, :calculate, :constraints, + __spark_metadata__: nil] defimpl String.Chars do def to_string(struct), do: inspect(struct) diff --git a/lib/diffo/provider/components/instance/extension/place.ex b/lib/diffo/provider/components/instance/extension/place.ex index 50a3be6..14f7780 100644 --- a/lib/diffo/provider/components/instance/extension/place.ex +++ b/lib/diffo/provider/components/instance/extension/place.ex @@ -3,12 +3,7 @@ # SPDX-License-Identifier: MIT defmodule Diffo.Provider.Instance.Place do - @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - Place for Instance Extension - """ - + @moduledoc false alias Diffo.Provider @doc """ diff --git a/lib/diffo/provider/components/instance/extension/relationship.ex b/lib/diffo/provider/components/instance/extension/relationship.ex index 18e2b95..3d9a54e 100644 --- a/lib/diffo/provider/components/instance/extension/relationship.ex +++ b/lib/diffo/provider/components/instance/extension/relationship.ex @@ -3,12 +3,7 @@ # SPDX-License-Identifier: MIT defmodule Diffo.Provider.Instance.Relationship do - @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - Relationship for Instance Extension - """ - + @moduledoc false alias Diffo.Provider @doc """ diff --git a/lib/diffo/provider/components/instance/extension/specification.ex b/lib/diffo/provider/components/instance/extension/specification.ex index 9469784..8f5720a 100644 --- a/lib/diffo/provider/components/instance/extension/specification.ex +++ b/lib/diffo/provider/components/instance/extension/specification.ex @@ -3,11 +3,7 @@ # SPDX-License-Identifier: MIT defmodule Diffo.Provider.Instance.Specification do - @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - Specification for Instance Extension - """ + @moduledoc false require Logger alias Diffo.Provider diff --git a/lib/diffo/provider/components/instance/util.ex b/lib/diffo/provider/components/instance/util.ex index 6251caf..ad5a604 100644 --- a/lib/diffo/provider/components/instance/util.ex +++ b/lib/diffo/provider/components/instance/util.ex @@ -3,15 +3,8 @@ # SPDX-License-Identifier: MIT defmodule Diffo.Provider.Instance.Util do - @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - Util - Methods of general utility to an Instance - """ - - @doc """ - Assists in encoding instance category - """ + @moduledoc false + @doc false def category(result, record) do specification = Map.get(record, :specification) @@ -28,9 +21,7 @@ defmodule Diffo.Provider.Instance.Util do end end - @doc """ - Assists in encoding instance description - """ + @doc false def description(result, record) do specification = Map.get(record, :specification) @@ -47,9 +38,7 @@ defmodule Diffo.Provider.Instance.Util do end end - @doc """ - Assists in encoding instance dates - """ + @doc false def dates(result, record) do result |> Diffo.Util.set( @@ -66,9 +55,7 @@ defmodule Diffo.Provider.Instance.Util do ) end - @doc """ - Assists in encoding instance states - """ + @doc false def states(result, record) do case record.type do :service -> @@ -85,9 +72,7 @@ defmodule Diffo.Provider.Instance.Util do end end - @doc """ - Assists in encoding instance-instance relationships - """ + @doc false def relationships(result) do if relationships = Diffo.Util.get(result, :forward_relationships) do service_relationships = @@ -134,16 +119,7 @@ defmodule Diffo.Provider.Instance.Util do end end - @doc """ - Derives the type prefix from the specification type - ## Examples - iex> Diffo.Provider.Instance.derive_type(:serviceSpecification) - :service - - iex> Diffo.Provider.Instance.derive_type(:resourceSpecification) - :resource - - """ + @doc false def derive_type(specification_type) do case specification_type do :serviceSpecification -> :service @@ -152,16 +128,7 @@ defmodule Diffo.Provider.Instance.Util do end end - @doc """ - Derives the instance feature list name from the instance type - ## Examples - iex> Diffo.Provider.Instance.derive_feature_list_name(:service) - :feature - - iex> Diffo.Provider.Instance.derive_feature_list_name(:resource) - :activationFeature - - """ + @doc false def derive_feature_list_name(type) do case type do :service -> :feature @@ -170,16 +137,7 @@ defmodule Diffo.Provider.Instance.Util do end end - @doc """ - Derives the instance characteristic list name from the instance type - ## Examples - iex> Diffo.Provider.Instance.derive_characteristic_list_name(:service) - :serviceCharacteristic - - iex> Diffo.Provider.Instance.derive_characteristic_list_name(:resource) - :resourceCharacteristic - - """ + @doc false def derive_characteristic_list_name(type) do case type do :service -> :serviceCharacteristic @@ -188,17 +146,7 @@ defmodule Diffo.Provider.Instance.Util do end end - @doc """ - Derives the instance create date name from the instance type - ## Examples - iex> Diffo.Provider.Instance.derive_create_date_name(:service) - :serviceDate - - iex> Diffo.Provider.Instance.derive_create_date_name(:resource) - nil - - """ - + @doc false def derive_create_date_name(type) do case type do :service -> :serviceDate @@ -206,17 +154,7 @@ defmodule Diffo.Provider.Instance.Util do end end - @doc """ - Derives the instance start date name from the instance type - ## Examples - iex> Diffo.Provider.Instance.derive_start_date_name(:service) - :startDate - - iex> Diffo.Provider.Instance.derive_start_date_name(:resource) - :startOperatingDate - - """ - + @doc false def derive_start_date_name(type) do case type do :service -> :startDate @@ -225,17 +163,7 @@ defmodule Diffo.Provider.Instance.Util do end end - @doc """ - Derives the instance end date name from the instance type - ## Examples - iex> Diffo.Provider.Instance.derive_end_date_name(:service) - :endDate - - iex> Diffo.Provider.Instance.derive_end_date_name(:resource) - :endOperatingDate - - """ - + @doc false def derive_end_date_name(type) do case type do :service -> :endDate @@ -244,17 +172,7 @@ defmodule Diffo.Provider.Instance.Util do end end - @doc """ - Given which returns the other which - ## Examples - iex> Diffo.Provider.Instance.other(:actual) - :expected - - iex> Diffo.Provider.Instance.other(:expected) - :actual - - """ - + @doc false def other(which) do case which do :actual -> :expected @@ -263,16 +181,4 @@ defmodule Diffo.Provider.Instance.Util do end end - @doc """ - Compares two instances, by ascending href - ## Examples - iex> compare(%{href: "a"}, %{href: "a"}) - :eq - iex> compare(%{href: "b"}, %{href: "a"}) - :gt - iex> compare(%{href: "a"}, %{href: "b"}) - :lt - - """ - def compare(%{href: href0}, %{href: href1}), do: Diffo.Util.compare(href0, href1) end diff --git a/lib/diffo/provider/components/note.ex b/lib/diffo/provider/components/note.ex index be1e95b..7cc4aaf 100644 --- a/lib/diffo/provider/components/note.ex +++ b/lib/diffo/provider/components/note.ex @@ -4,9 +4,7 @@ defmodule Diffo.Provider.Note do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - Note - Ash Resource for a TMF Note + Ash Resource for a TMF Note """ use Ash.Resource, otp_app: :diffo, @@ -151,17 +149,4 @@ defmodule Diffo.Provider.Note do prepare build(load: [:author], sort: [timestamp: :desc]) end - @doc """ - Compares two note, by most recent insertion order - ## Examples - iex> Diffo.Provider.Note.compare(%{timestamp: "a"}, %{timestamp: "a"}) - :eq - iex> Diffo.Provider.Note.compare(%{timestamp: "b"}, %{timestamp: "a"}) - :gt - iex> Diffo.Provider.Note.compare(%{timestamp: "a"}, %{timestamp: "b"}) - :lt - - """ - def compare(%{timestamp: timestamp0}, %{timestamp: timestamp1}), - do: Diffo.Util.compare(timestamp0, timestamp1) end diff --git a/lib/diffo/provider/components/party.ex b/lib/diffo/provider/components/party.ex index afee95f..2226e38 100644 --- a/lib/diffo/provider/components/party.ex +++ b/lib/diffo/provider/components/party.ex @@ -4,176 +4,37 @@ defmodule Diffo.Provider.Party do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference + Ash Resource for a TMF Party - Party - Ash Resource for a TMF Party + The out-of-the-box TMF Party resource. Uses `BaseParty` as a fragment and adds + JSON serialisation with TMF `@type` / `@referredType` key mapping and outstanding + validation covering the core TMF Party fields. + + Use `Diffo.Provider.Party` directly via the `Diffo.Provider` domain when working with + generic TMF parties (e.g. party refs on instances). For domain-specific parties with + richer identity — such as an RSP or a Customer — extend `BaseParty` directly in your + own domain and define a `build` action that sets `type` or `referred_type` appropriately. + + See `Diffo.Provider.BaseParty` for full usage documentation. """ + alias Diffo.Provider.BaseParty + use Ash.Resource, - otp_app: :diffo, - domain: Diffo.Provider, - data_layer: AshNeo4j.DataLayer, - extensions: [AshOutstanding.Resource, AshJason.Resource] + fragments: [BaseParty], + domain: Diffo.Provider resource do description "An Ash Resource for a TMF Party" plural_name :parties end - neo4j do - relate [ - {:party_refs, :RELATES, :incoming, :PartyRef}, - {:external_identifiers, :OWNS, :outgoing, :ExternalIdentifier}, - {:notes, :AUTHORS, :outgoing, :Note} - ] - - guard [ - {:OWNS, :outgoing, :ExternalIdentifier} - ] - end - jason do - pick [:id, :href, :name, :referredType, :type] + pick [:id, :href, :name, :referred_type, :type] compact true - rename referredType: "@referredType", type: "@type" + rename referred_type: "@referredType", type: "@type" end outstanding do - expect [:id, :name, :referredType, :type] - end - - actions do - defaults [:read, :destroy] - - create :create do - description "creates a party" - accept [:id, :href, :name, :type, :referredType] - upsert? true - end - - read :find_by_id do - description "finds party by id" - get? false - - argument :query, :ci_string do - description "Return only parties with id's including the given value." - end - - filter expr(contains(id, ^arg(:query))) - end - - read :find_by_name do - description "finds party by name" - get? false - - argument :query, :ci_string do - description "Return only parties with names including the given value." - end - - filter expr(contains(name, ^arg(:query))) - end - - read :list do - description "lists all parties" - end - - update :update do - description "updates the party" - accept [:href, :name, :type, :referredType] - end - end - - attributes do - attribute :id, :string do - description "the unique id of the party" - primary_key? true - allow_nil? false - public? true - source :key - end - - attribute :href, :string do - description "the href of the party" - allow_nil? true - public? true - end - - attribute :name, :string do - description "the name of the party" - allow_nil? true - public? true - constraints match: ~r/^[a-zA-Z0-9\s._-]+$/ - end - - attribute :type, :atom do - description "the type of the party" - allow_nil? false - public? true - default :PartyRef - constraints one_of: [:PartyRef, :Individual, :Organization, :Entity] - end - - attribute :referredType, :atom do - description "the type of the party" - allow_nil? true - public? true - constraints one_of: [:Individual, :Organization, :Entity] - end - - create_timestamp :created_at - - update_timestamp :updated_at - end - - relationships do - has_many :party_refs, Diffo.Provider.PartyRef do - description "the party refs relating this party to instances" - destination_attribute :party_id - public? true - end - - has_many :external_identifiers, Diffo.Provider.ExternalIdentifier do - description "the external identifiers owned by this party" - destination_attribute :owner_id - public? true - end - - has_many :notes, Diffo.Provider.Note do - description "the notes authored by this party" - destination_attribute :note_id - public? true - end + expect [:id, :name, :referred_type, :type] end - - validations do - validate {Diffo.Validations.HrefEndsWithId, id: :id, href: :href} do - where [present(:id), present(:href)] - end - - validate attribute_equals(:type, :PartyRef) do - where present(:referredType) - message "when referredType is present, type must be PartyRef" - end - - validate attribute_does_not_equal(:type, :PartyRef) do - where absent(:referredType) - message "when referredType is absent, type must be not be PartyRef" - end - end - - preparations do - prepare build(sort: [id: :asc, name: :asc]) - end - - @doc """ - Compares two party, by ascending id - ## Examples - iex> Diffo.Provider.Party.compare(%{id: "a"}, %{id: "a"}) - :eq - iex> Diffo.Provider.Party.compare(%{id: "b"}, %{id: "a"}) - :gt - iex> Diffo.Provider.Party.compare(%{id: "a"}, %{id: "b"}) - :lt - - """ - def compare(%{id: id0}, %{id: id1}), do: Diffo.Util.compare(id0, id1) end diff --git a/lib/diffo/provider/components/party/extension.ex b/lib/diffo/provider/components/party/extension.ex index 19fea32..865738f 100644 --- a/lib/diffo/provider/components/party/extension.ex +++ b/lib/diffo/provider/components/party/extension.ex @@ -4,9 +4,14 @@ defmodule Diffo.Provider.Party.Extension do @moduledoc """ - DSL Extension customising a Party - """ + DSL Extension customising a Party. + + Provides compile-time declaration blocks for domain-specific Party kinds + built on `Diffo.Provider.BaseParty`. All declarations are introspectable via + `Diffo.Provider.Party.Extension.Info`. + See the [DSL cheat sheet](DSL-Diffo.Provider.Party.Extension.html) for the full DSL reference. + """ @role %Spark.Dsl.Entity{ name: :role, describe: "Declares a role this Party kind plays", @@ -43,12 +48,12 @@ defmodule Diffo.Provider.Party.Extension do ] } - @instance %Spark.Dsl.Section{ - name: :instance, + @instances %Spark.Dsl.Section{ + name: :instances, describe: "Declares the roles this Party kind plays with respect to Instances", examples: [ """ - instance do + instances do role :facilitates, MyApp.AccessService end """ @@ -56,12 +61,12 @@ defmodule Diffo.Provider.Party.Extension do entities: [@role] } - @party %Spark.Dsl.Section{ - name: :party, + @parties %Spark.Dsl.Section{ + name: :parties, describe: "Declares the roles this Party kind plays with respect to other Parties", examples: [ """ - party do + parties do role :managed_by, MyApp.Person end """ @@ -70,5 +75,5 @@ defmodule Diffo.Provider.Party.Extension do } use Spark.Dsl.Extension, - sections: [@instance, @party] + sections: [@instances, @parties] end diff --git a/lib/diffo/provider/components/party/extension/info.ex b/lib/diffo/provider/components/party/extension/info.ex index 66361d7..5638989 100644 --- a/lib/diffo/provider/components/party/extension/info.ex +++ b/lib/diffo/provider/components/party/extension/info.ex @@ -5,5 +5,5 @@ defmodule Diffo.Provider.Party.Extension.Info do use Spark.InfoGenerator, extension: Diffo.Provider.Party.Extension, - sections: [:instance, :party] + sections: [:instances, :parties] end diff --git a/lib/diffo/provider/components/party/extension/instance_role.ex b/lib/diffo/provider/components/party/extension/instance_role.ex index 3a39545..ed2d81e 100644 --- a/lib/diffo/provider/components/party/extension/instance_role.ex +++ b/lib/diffo/provider/components/party/extension/instance_role.ex @@ -4,11 +4,8 @@ defmodule Diffo.Provider.Party.Extension.InstanceRole do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - InstanceRole - DSL entity declaring a role this Party kind plays with respect to Instances """ - defstruct [:role, :party_type, __spark_metadata__: nil] defimpl String.Chars do diff --git a/lib/diffo/provider/components/party/extension/party_role.ex b/lib/diffo/provider/components/party/extension/party_role.ex index d4ba2ab..fea890b 100644 --- a/lib/diffo/provider/components/party/extension/party_role.ex +++ b/lib/diffo/provider/components/party/extension/party_role.ex @@ -4,11 +4,8 @@ defmodule Diffo.Provider.Party.Extension.PartyRole do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - PartyRole - DSL entity declaring a role this Party kind plays with respect to other Parties """ - defstruct [:role, :party_type, __spark_metadata__: nil] defimpl String.Chars do diff --git a/lib/diffo/provider/components/party_ref.ex b/lib/diffo/provider/components/party_ref.ex index f534ad4..1d041df 100644 --- a/lib/diffo/provider/components/party_ref.ex +++ b/lib/diffo/provider/components/party_ref.ex @@ -4,9 +4,7 @@ defmodule Diffo.Provider.PartyRef do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - PartyRef - Ash Resource for a TMF PartyRef + Ash Resource for a TMF PartyRef """ use Ash.Resource, otp_app: :diffo, @@ -36,7 +34,7 @@ defmodule Diffo.Provider.PartyRef do |> Diffo.Util.extract_suppress(:party, :id, :id) |> Diffo.Util.extract_suppress(:party, :href, :href) |> Diffo.Util.extract_suppress(:party, :name, :name) - |> Diffo.Util.extract_suppress(:party, :referredType, "@referredType") + |> Diffo.Util.extract_suppress(:party, :referred_type, "@referredType") |> Diffo.Util.extract_suppress(:party, :type, "@type") |> Diffo.Util.remove(:party) end @@ -45,7 +43,7 @@ defmodule Diffo.Provider.PartyRef do end outstanding do - expect [:party_id, :name, :role, :referredType, :type] + expect [:party_id, :name, :role, :referred_type, :type] end actions do @@ -203,17 +201,4 @@ defmodule Diffo.Provider.PartyRef do end end - @doc """ - Compares two party ref, by ascending party_id - ## Examples - iex> Diffo.Provider.PartyRef.compare(%{party_id: "a"}, %{party_id: "a"}) - :eq - iex> Diffo.Provider.PartyRef.compare(%{party_id: "b"}, %{party_id: "a"}) - :gt - iex> Diffo.Provider.PartyRef.compare(%{party_id: "a"}, %{party_id: "b"}) - :lt - - """ - def compare(%{party_id: party_id0}, %{party_id: party_id1}), - do: Diffo.Util.compare(party_id0, party_id1) end diff --git a/lib/diffo/provider/components/place.ex b/lib/diffo/provider/components/place.ex index 98ddca1..5210a37 100644 --- a/lib/diffo/provider/components/place.ex +++ b/lib/diffo/provider/components/place.ex @@ -4,9 +4,7 @@ defmodule Diffo.Provider.Place do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - Place - Ash Resource for a TMF Place + Ash Resource for a TMF Place """ use Ash.Resource, otp_app: :diffo, @@ -26,13 +24,13 @@ defmodule Diffo.Provider.Place do end jason do - pick [:id, :href, :name, :referredType, :type] + pick [:id, :href, :name, :referred_type, :type] compact true - rename referredType: "@referredType", type: "@type" + rename referred_type: "@referredType", type: "@type" end outstanding do - expect [:id, :name, :referredType, :type] + expect [:id, :name, :referred_type, :type] end actions do @@ -40,7 +38,7 @@ defmodule Diffo.Provider.Place do create :create do description "creates a place" - accept [:id, :href, :name, :type, :referredType] + accept [:id, :href, :name, :type, :referred_type] upsert? true end @@ -72,7 +70,7 @@ defmodule Diffo.Provider.Place do update :update do description "updates the place" - accept [:href, :name, :type, :referredType] + accept [:href, :name, :type, :referred_type] end end @@ -106,7 +104,7 @@ defmodule Diffo.Provider.Place do constraints one_of: [:PlaceRef, :GeographicSite, :GeographicLocation, :GeographicAddress] end - attribute :referredType, :atom do + attribute :referred_type, :atom do description "the type of the place" allow_nil? true public? true @@ -132,13 +130,13 @@ defmodule Diffo.Provider.Place do end validate attribute_equals(:type, :PlaceRef) do - where present(:referredType) - message "when referredType is present, type must be PlaceRef" + where present(:referred_type) + message "when referred_type is present, type must be PlaceRef" end validate attribute_does_not_equal(:type, :PlaceRef) do - where absent(:referredType) - message "when referredType is absent, type must be not be PlaceRef" + where absent(:referred_type) + message "when referred_type is absent, type must be not be PlaceRef" end end @@ -146,16 +144,4 @@ defmodule Diffo.Provider.Place do prepare build(sort: [id: :asc, name: :asc]) end - @doc """ - Compares two place, by ascending id - ## Examples - iex> Diffo.Provider.Place.compare(%{id: "a"}, %{id: "a"}) - :eq - iex> Diffo.Provider.Place.compare(%{id: "b"}, %{id: "a"}) - :gt - iex> Diffo.Provider.Place.compare(%{id: "a"}, %{id: "b"}) - :lt - - """ - def compare(%{id: id0}, %{id: id1}), do: Diffo.Util.compare(id0, id1) end diff --git a/lib/diffo/provider/components/place_ref.ex b/lib/diffo/provider/components/place_ref.ex index 659e159..a2acd0b 100644 --- a/lib/diffo/provider/components/place_ref.ex +++ b/lib/diffo/provider/components/place_ref.ex @@ -4,9 +4,7 @@ defmodule Diffo.Provider.PlaceRef do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - PlaceRef - Ash Resource for a TMF Place Reference + Ash Resource for a TMF Place Reference """ use Ash.Resource, otp_app: :diffo, @@ -36,7 +34,7 @@ defmodule Diffo.Provider.PlaceRef do |> Diffo.Util.extract_suppress(:place, :id, :id) |> Diffo.Util.extract_suppress(:place, :href, :href) |> Diffo.Util.extract_suppress(:place, :name, :name) - |> Diffo.Util.extract_suppress(:place, :referredType, "@referredType") + |> Diffo.Util.extract_suppress(:place, :referred_type, "@referredType") |> Diffo.Util.extract_suppress(:place, :type, "@type") |> Diffo.Util.remove(:place) end @@ -172,17 +170,4 @@ defmodule Diffo.Provider.PlaceRef do ) end - @doc """ - Compares two place ref, by ascending place_id - ## Examples - iex> Diffo.Provider.PlaceRef.compare(%{place_id: "a"}, %{place_id: "a"}) - :eq - iex> Diffo.Provider.PlaceRef.compare(%{place_id: "b"}, %{place_id: "a"}) - :gt - iex> Diffo.Provider.PlaceRef.compare(%{place_id: "a"}, %{place_id: "b"}) - :lt - - """ - def compare(%{place_id: place_id0}, %{place_id: place_id1}), - do: Diffo.Util.compare(place_id0, place_id1) end diff --git a/lib/diffo/provider/components/process_status.ex b/lib/diffo/provider/components/process_status.ex index 007b3d0..6843260 100644 --- a/lib/diffo/provider/components/process_status.ex +++ b/lib/diffo/provider/components/process_status.ex @@ -4,9 +4,7 @@ defmodule Diffo.Provider.ProcessStatus do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - ProcessStatus - Ash Resource for a TMF ProcessStatus + Ash Resource for a TMF ProcessStatus """ use Ash.Resource, otp_app: :diffo, @@ -127,17 +125,4 @@ defmodule Diffo.Provider.ProcessStatus do prepare build(sort: [timestamp: :desc]) end - @doc """ - Compares two process status, by timestamp - ## Examples - iex> Diffo.Provider.ProcessStatus.compare(%{timestamp: "a"}, %{timestamp: "a"}) - :eq - iex> Diffo.Provider.ProcessStatus.compare(%{timestamp: "b"}, %{timestamp: "a"}) - :gt - iex> Diffo.Provider.ProcessStatus.compare(%{timestamp: "a"}, %{timestamp: "b"}) - :lt - - """ - def compare(%{timestamp: timestamp0}, %{timestamp: timestamp1}), - do: Diffo.Util.compare(timestamp0, timestamp1) end diff --git a/lib/diffo/provider/components/relationship.ex b/lib/diffo/provider/components/relationship.ex index 5de0e6a..28ceb03 100644 --- a/lib/diffo/provider/components/relationship.ex +++ b/lib/diffo/provider/components/relationship.ex @@ -4,10 +4,8 @@ defmodule Diffo.Provider.Relationship do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - Relationship - Ash Resource for a TMF Service or Resource Relationship + Ash Resource for a TMF Service or Resource Relationship """ use Ash.Resource, otp_app: :diffo, diff --git a/lib/diffo/provider/components/specification.ex b/lib/diffo/provider/components/specification.ex index 5d29493..09d60e8 100644 --- a/lib/diffo/provider/components/specification.ex +++ b/lib/diffo/provider/components/specification.ex @@ -4,9 +4,7 @@ defmodule Diffo.Provider.Specification do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - Specification - Ash Resource for a TMF Service or Resource Specification + Ash Resource for a TMF Service or Resource Specification """ require Ash.Resource.Change.Builtins diff --git a/lib/diffo/provider/outstanding.ex b/lib/diffo/provider/outstanding.ex index c89730d..a96c935 100644 --- a/lib/diffo/provider/outstanding.ex +++ b/lib/diffo/provider/outstanding.ex @@ -3,22 +3,8 @@ # SPDX-License-Identifier: MIT defmodule Diffo.Provider.Outstanding do - @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - - Outstanding - utilities relating to Outstanding - """ - - @doc """ - Accumulates outstanding instance with list by key - Outstanding, expected and actual are Diffo.Provider.Instance structs - ## Examples - iex> expected_instance = %Diffo.Provider.Instance{parties: [%Diffo.Provider.PartyRef{role: :Consumer, party: %Diffo.Provider.Party{id: nil, name: nil, type: "PartyRef", referredType: "Entity"}}]} - iex> actual_instance = %Diffo.Provider.Instance{parties: [%Diffo.Provider.PartyRef{role: :Consumer, party: %Diffo.Provider.Party{id: "T5_CONNECTIVITY", name: nil, type: "PartyRef", referredType: "Entity"}}]} - iex> Diffo.Provider.Outstanding.instance_list_by_key(nil, expected_instance, actual_instance, :parties, :role) - nil - """ + @moduledoc false + @doc false def instance_list_by_key(outstanding, expected, actual, list, key) do # assemble keyword lists of expected and actual lists expected_keywords = diff --git a/lib/diffo/provider/reference.ex b/lib/diffo/provider/reference.ex index cef59f9..4f79f27 100644 --- a/lib/diffo/provider/reference.ex +++ b/lib/diffo/provider/reference.ex @@ -3,35 +3,17 @@ # SPDX-License-Identifier: MIT defmodule Diffo.Provider.Reference do - @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - - Reference - utilities relating to reference - """ - + @moduledoc false defstruct id: nil, href: nil - @doc """ - Creates a reference struct from an instance with id and href - ## Examples - iex> instance = %{id: "8bcfbf9a-34a5-427a-8eae-5c3812466432", href: "serviceInventoryManagement/v4/service/8bcfbf9a-34a5-427a-8eae-5c3812466432"} - iex> Diffo.Provider.Reference.reference(instance) - %Diffo.Provider.Reference{id: "8bcfbf9a-34a5-427a-8eae-5c3812466432", href: "serviceInventoryManagement/v4/service/8bcfbf9a-34a5-427a-8eae-5c3812466432"} - """ + @doc false def reference(instance) when is_map(instance) do %Diffo.Provider.Reference{id: instance.id, href: instance.href} end def reference(instance) when is_nil(instance), do: nil - @doc """ - Creates a reference struct from an instance attribute containing a href - ## Examples - iex> instance = %{target_href: "serviceInventoryManagement/v4/service/8bcfbf9a-34a5-427a-8eae-5c3812466432"} - iex> Diffo.Provider.Reference.reference(instance, :target_href) - %Diffo.Provider.Reference{id: "8bcfbf9a-34a5-427a-8eae-5c3812466432", href: "serviceInventoryManagement/v4/service/8bcfbf9a-34a5-427a-8eae-5c3812466432"} - """ + @doc false def reference(instance, attribute) when is_map(instance) and is_atom(attribute) do href = Map.get(instance, attribute) %Diffo.Provider.Reference{id: Diffo.Uuid.trailing_uuid4(href), href: href} diff --git a/lib/diffo/provider/service.ex b/lib/diffo/provider/service.ex index d03aba8..3587681 100644 --- a/lib/diffo/provider/service.ex +++ b/lib/diffo/provider/service.ex @@ -3,13 +3,7 @@ # SPDX-License-Identifier: MIT defmodule Diffo.Provider.Service do - @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - - Service - utilities relating to service - """ - + @moduledoc false def service_states() do [ :initial, diff --git a/lib/diffo/repo.ex b/lib/diffo/repo.ex index 937cfec..a4ab6ab 100644 --- a/lib/diffo/repo.ex +++ b/lib/diffo/repo.ex @@ -3,13 +3,7 @@ # SPDX-License-Identifier: MIT defmodule Diffo.Repo do - @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - - Repo - persistance - """ - + @moduledoc false use GenServer def init(init_arg) do diff --git a/lib/diffo/type/dynamic.ex b/lib/diffo/type/dynamic.ex index 2407d95..569626f 100644 --- a/lib/diffo/type/dynamic.ex +++ b/lib/diffo/type/dynamic.ex @@ -3,8 +3,6 @@ # SPDX-License-Identifier: MIT defmodule Diffo.Type.Dynamic do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - `Diffo.Type.Dynamic` is an `Ash.Type.NewType` for values whose exact type is not known until runtime. The `:type` field holds the `Ash.Type.NewType` module and `:value` holds the cast value. @@ -14,6 +12,7 @@ defmodule Diffo.Type.Dynamic do In practice, `Diffo.Type.Dynamic` is used as a member of `Diffo.Type.Value` and is not typically used as a standalone attribute type. + Outstanding comparison is implemented inline via `defoutstanding`. ## Nil handling @@ -53,7 +52,6 @@ defmodule Diffo.Type.Dynamic do iex> Diffo.Type.Dynamic.dynamic_constraints(nil) [] """ - defstruct [:type, :value] @type_field_constraints [ @@ -75,10 +73,11 @@ defmodule Diffo.Type.Dynamic do subtype_of: :struct, constraints: @constraints + use Outstand + @doc """ Returns the dynamic constraints from dynamic struct or map """ - def dynamic_constraints(nil), do: [] def dynamic_constraints(%{type: type}) when is_atom(type), do: dynamic_constraints(type) @@ -196,4 +195,31 @@ defmodule Diffo.Type.Dynamic do value |> Diffo.Unwrap.unwrap() |> Jason.Encode.value(opts) end end + + defoutstanding expected :: Diffo.Type.Dynamic, actual :: Any do + type_outstanding = + case actual do + %{type: type} -> Outstanding.outstanding(expected.type, type) + _ -> expected.type + end + + value_outstanding = + case actual do + %{} -> + Outstanding.outstanding( + Diffo.Unwrap.unwrap(expected), + Diffo.Unwrap.unwrap(actual) + ) + + _ -> + Diffo.Unwrap.unwrap(expected) + end + + case {type_outstanding, value_outstanding} do + {nil, nil} -> nil + {nil, _} -> %Diffo.Type.Dynamic{type: nil, value: value_outstanding} + {_, nil} -> %Diffo.Type.Dynamic{type: type_outstanding, value: nil} + {_, _} -> %Diffo.Type.Dynamic{type: type_outstanding, value: value_outstanding} + end + end end diff --git a/lib/diffo/type/outstanding/dynamic.ex b/lib/diffo/type/outstanding/dynamic.ex deleted file mode 100644 index fdf3647..0000000 --- a/lib/diffo/type/outstanding/dynamic.ex +++ /dev/null @@ -1,32 +0,0 @@ -# SPDX-FileCopyrightText: 2025 diffo contributors -# -# SPDX-License-Identifier: MIT - -use Outstand - -defoutstanding expected :: Diffo.Type.Dynamic, actual :: Any do - type_outstanding = - case actual do - %{type: type} -> Outstanding.outstanding(expected.type, type) - _ -> expected.type - end - - value_outstanding = - case actual do - %{} -> - Outstanding.outstanding( - Diffo.Unwrap.unwrap(expected), - Diffo.Unwrap.unwrap(actual) - ) - - _ -> - Diffo.Unwrap.unwrap(expected) - end - - case {type_outstanding, value_outstanding} do - {nil, nil} -> nil - {nil, _} -> %Diffo.Type.Dynamic{type: nil, value: value_outstanding} - {_, nil} -> %Diffo.Type.Dynamic{type: type_outstanding, value: nil} - {_, _} -> %Diffo.Type.Dynamic{type: type_outstanding, value: value_outstanding} - end -end diff --git a/lib/diffo/type/outstanding/primitive.ex b/lib/diffo/type/outstanding/primitive.ex deleted file mode 100644 index 30ae8ee..0000000 --- a/lib/diffo/type/outstanding/primitive.ex +++ /dev/null @@ -1,35 +0,0 @@ -# SPDX-FileCopyrightText: 2025 diffo contributors -# -# SPDX-License-Identifier: MIT - -use Outstand - -defoutstanding expected :: Diffo.Type.Primitive, actual :: Any do - # we return a map since Primitive doesn't allow type nil - type_outstanding = - case actual do - %{type: type} -> Outstanding.outstanding(expected.type, type) - nil -> expected.type - # actual is wrong type entirely - _ -> expected.type - end - - value_outstanding = - case actual do - %{} -> - Outstanding.outstanding( - Diffo.Unwrap.unwrap(expected), - Diffo.Unwrap.unwrap(actual) - ) - - _ -> - Diffo.Unwrap.unwrap(expected) - end - - case {type_outstanding, value_outstanding} do - {nil, nil} -> nil - {nil, _} -> %{value: value_outstanding} - {_, nil} -> %{type: type_outstanding} - {_, _} -> %{type: type_outstanding, value: value_outstanding} - end -end diff --git a/lib/diffo/type/primitive.ex b/lib/diffo/type/primitive.ex index 7b596ad..5082fa4 100644 --- a/lib/diffo/type/primitive.ex +++ b/lib/diffo/type/primitive.ex @@ -3,13 +3,12 @@ # SPDX-License-Identifier: MIT defmodule Diffo.Type.Primitive do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - `Diffo.Type.Primitive` is a discriminated union of primitive types: string, integer, float, boolean, date, time, datetime, and duration. Use `wrap/2` to construct a Primitive from a type name string and a value. Use `Diffo.Unwrap.unwrap/1` to extract the value. + Outstanding comparison is implemented inline via `defoutstanding`. > #### Temporal types {: .info} > @@ -37,6 +36,7 @@ defmodule Diffo.Type.Primitive do nil """ use Ash.TypedStruct + use Outstand typed_struct do field :type, :string, description: "the primitive type discriminator" @@ -94,4 +94,34 @@ defmodule Diffo.Type.Primitive do value |> Diffo.Unwrap.unwrap() |> Jason.encode!() end end + + defoutstanding expected :: Diffo.Type.Primitive, actual :: Any do + # we return a map since Primitive doesn't allow type nil + type_outstanding = + case actual do + %{type: type} -> Outstanding.outstanding(expected.type, type) + nil -> expected.type + # actual is wrong type entirely + _ -> expected.type + end + + value_outstanding = + case actual do + %{} -> + Outstanding.outstanding( + Diffo.Unwrap.unwrap(expected), + Diffo.Unwrap.unwrap(actual) + ) + + _ -> + Diffo.Unwrap.unwrap(expected) + end + + case {type_outstanding, value_outstanding} do + {nil, nil} -> nil + {nil, _} -> %{value: value_outstanding} + {_, nil} -> %{type: type_outstanding} + {_, _} -> %{type: type_outstanding, value: value_outstanding} + end + end end diff --git a/lib/diffo/type/value.ex b/lib/diffo/type/value.ex index be97e86..43b1999 100644 --- a/lib/diffo/type/value.ex +++ b/lib/diffo/type/value.ex @@ -3,8 +3,6 @@ # SPDX-License-Identifier: MIT defmodule Diffo.Type.Value do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - `Diffo.Type.Value` is an `Ash.Type.NewType` union that holds either a `Diffo.Type.Primitive` or a `Diffo.Type.Dynamic` value. @@ -14,6 +12,9 @@ defmodule Diffo.Type.Value do Use `primitive/2` to build a primitive value and `dynamic/1` to build a dynamic value. Use `Diffo.Unwrap.unwrap/1` on the stored `%Ash.Union{}` to extract the underlying Elixir value. + Outstanding comparison is handled by `AshOutstanding.Union`, which delegates to the inner + `Diffo.Type.Primitive` or `Diffo.Type.Dynamic` outstanding implementation. + ## Examples iex> Diffo.Type.Value.primitive("string", "connectivity") |> Diffo.Unwrap.unwrap() @@ -31,7 +32,6 @@ defmodule Diffo.Type.Value do iex> Diffo.Type.Value.primitive("date", ~D[2026-04-24]) |> Diffo.Unwrap.unwrap() "2026-04-24" """ - use Ash.Type.NewType, subtype_of: :union, constraints: [ diff --git a/lib/diffo/unwrap.ex b/lib/diffo/unwrap.ex index 2af850d..5842bb7 100644 --- a/lib/diffo/unwrap.ex +++ b/lib/diffo/unwrap.ex @@ -4,8 +4,6 @@ defprotocol Diffo.Unwrap do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - `Diffo.Unwrap` is a protocol for extracting the underlying Elixir value from Diffo and Ash wrapper types. It is defined with `@fallback_to_any true`, so any value without an explicit implementation is returned unchanged. @@ -46,7 +44,6 @@ defprotocol Diffo.Unwrap do ...> |> Diffo.Unwrap.unwrap() [1, 2] """ - @fallback_to_any true def unwrap(value) end diff --git a/lib/diffo/unwrap/any.ex b/lib/diffo/unwrap/any.ex index 3f37943..54fafbb 100644 --- a/lib/diffo/unwrap/any.ex +++ b/lib/diffo/unwrap/any.ex @@ -3,5 +3,7 @@ # SPDX-License-Identifier: MIT defimpl Diffo.Unwrap, for: Any do + @moduledoc false + def unwrap(value), do: value end diff --git a/lib/diffo/unwrap/ash_ci_string.ex b/lib/diffo/unwrap/ash_ci_string.ex index 116bec4..0245b81 100644 --- a/lib/diffo/unwrap/ash_ci_string.ex +++ b/lib/diffo/unwrap/ash_ci_string.ex @@ -3,5 +3,7 @@ # SPDX-License-Identifier: MIT defimpl Diffo.Unwrap, for: Ash.CiString do + @moduledoc false + def unwrap(ci_string), do: Ash.CiString.to_comparable_string(ci_string) end diff --git a/lib/diffo/unwrap/ash_custom_expression.ex b/lib/diffo/unwrap/ash_custom_expression.ex index b1361d4..5bdfe54 100644 --- a/lib/diffo/unwrap/ash_custom_expression.ex +++ b/lib/diffo/unwrap/ash_custom_expression.ex @@ -3,6 +3,8 @@ # SPDX-License-Identifier: MIT defmodule Diffo.Unwrap.AshCustomExpression do + @moduledoc false + use Ash.CustomExpression, name: :unwrap, arguments: [ diff --git a/lib/diffo/unwrap/ash_not_loaded.ex b/lib/diffo/unwrap/ash_not_loaded.ex index 4465b42..9f5d727 100644 --- a/lib/diffo/unwrap/ash_not_loaded.ex +++ b/lib/diffo/unwrap/ash_not_loaded.ex @@ -3,6 +3,8 @@ # SPDX-License-Identifier: MIT defimpl Diffo.Unwrap, for: Ash.NotLoaded do + @moduledoc false + def unwrap(%{field: field}) do raise "Diffo.Unwrap: #{field} was not loaded" end diff --git a/lib/diffo/unwrap/ash_union.ex b/lib/diffo/unwrap/ash_union.ex index 69e5933..535abd4 100644 --- a/lib/diffo/unwrap/ash_union.ex +++ b/lib/diffo/unwrap/ash_union.ex @@ -3,5 +3,7 @@ # SPDX-License-Identifier: MIT defimpl Diffo.Unwrap, for: Ash.Union do + @moduledoc false + def unwrap(%{value: value}), do: Diffo.Unwrap.unwrap(value) end diff --git a/lib/diffo/unwrap/list.ex b/lib/diffo/unwrap/list.ex index c3779eb..8fb31e6 100644 --- a/lib/diffo/unwrap/list.ex +++ b/lib/diffo/unwrap/list.ex @@ -3,5 +3,7 @@ # SPDX-License-Identifier: MIT defimpl Diffo.Unwrap, for: List do + @moduledoc false + def unwrap(list), do: Enum.map(list, &Diffo.Unwrap.unwrap/1) end diff --git a/lib/diffo/validations/href_ends_with_id.ex b/lib/diffo/validations/href_ends_with_id.ex index e8a397e..0dee15f 100644 --- a/lib/diffo/validations/href_ends_with_id.ex +++ b/lib/diffo/validations/href_ends_with_id.ex @@ -3,12 +3,7 @@ # SPDX-License-Identifier: MIT defmodule Diffo.Validations.HrefEndsWithId do - @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - - HrefEndsWithId - Ash Resource Validation checking href ends with id - """ + @moduledoc false use Ash.Resource.Validation @impl true diff --git a/lib/diffo/validations/is_related_different.ex b/lib/diffo/validations/is_related_different.ex index e64b10a..4b69f39 100644 --- a/lib/diffo/validations/is_related_different.ex +++ b/lib/diffo/validations/is_related_different.ex @@ -3,12 +3,7 @@ # SPDX-License-Identifier: MIT defmodule Diffo.Validations.IsRelatedDifferent do - @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - - IsRelatedDifferent - Ash Resource Validation checking related Instance has different attribute value - """ + @moduledoc false use Ash.Resource.Validation @impl true diff --git a/lib/diffo/validations/is_uuid4_or_nil.ex b/lib/diffo/validations/is_uuid4_or_nil.ex index 5c594f9..5e7dd7c 100644 --- a/lib/diffo/validations/is_uuid4_or_nil.ex +++ b/lib/diffo/validations/is_uuid4_or_nil.ex @@ -3,12 +3,7 @@ # SPDX-License-Identifier: MIT defmodule Diffo.Validations.IsUuid4OrNil do - @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - - IsUuid4OrNil - Ash Resource Validation checking uuid is v4 if supplied - """ + @moduledoc false use Ash.Resource.Validation @impl true diff --git a/logos/diffo.jpg.license b/logos/diffo.jpg.license deleted file mode 100644 index 40c9cb0..0000000 --- a/logos/diffo.jpg.license +++ /dev/null @@ -1,3 +0,0 @@ -SPDX-FileCopyrightText: 2025 diffo contributors - -SPDX-License-Identifier: MIT \ No newline at end of file diff --git a/mix.exs b/mix.exs index c0e70ef..eacb613 100644 --- a/mix.exs +++ b/mix.exs @@ -20,14 +20,11 @@ defmodule Diffo.MixProject do elixir: "~> 1.18", start_permanent: Mix.env() == :prod, package: package(), - # ex_doc source_url: "https://github.com/diffo-dev/diffo/", homepage_url: "http://diffo.dev/diffo/", - docs: [main: "readme", extras: ["README.md"]], elixirc_paths: elixirc_paths(Mix.env()), - # hex.pm stuff - deps: deps(), docs: &docs/0, + deps: deps(), aliases: aliases(), consolidate_protocols: Mix.env() != :dev ] @@ -72,7 +69,14 @@ defmodule Diffo.MixProject do "documentation/dsls/DSL-Diffo.Provider.Instance.Extension.md": [ title: "DSL: Diffo.Provider.Instance.Extension", search_data: Spark.Docs.search_data_for(Diffo.Provider.Instance.Extension) + ], + "documentation/dsls/DSL-Diffo.Provider.Party.Extension.md": [ + title: "DSL: Diffo.Provider.Party.Extension", + search_data: Spark.Docs.search_data_for(Diffo.Provider.Party.Extension) ] + ], + groups_for_extras: [ + "DSLs": ~r/documentation\/dsls\// ] ] end diff --git a/mix.lock.license b/mix.lock.license deleted file mode 100644 index 40c9cb0..0000000 --- a/mix.lock.license +++ /dev/null @@ -1,3 +0,0 @@ -SPDX-FileCopyrightText: 2025 diffo contributors - -SPDX-License-Identifier: MIT \ No newline at end of file diff --git a/test/instance_extension/assigner_test.exs b/test/instance_extension/assigner_test.exs index 65d5097..d03ef75 100644 --- a/test/instance_extension/assigner_test.exs +++ b/test/instance_extension/assigner_test.exs @@ -10,6 +10,7 @@ defmodule Diffo.InstanceExtension.AssignerTest do alias Diffo.Provider.Assignment alias Diffo.Test.Characteristics + alias Diffo.Test.Parties alias Diffo.Test.Servo alias Diffo.Test.Card @@ -84,7 +85,7 @@ defmodule Diffo.InstanceExtension.AssignerTest do end test "auto assign port to resource" do - {:ok, assignee} = Servo.build_shelf() + {:ok, assignee} = Parties.build_shelf_with_installer() {:ok, card} = Servo.build_card(%{}) @@ -109,7 +110,7 @@ defmodule Diffo.InstanceExtension.AssignerTest do end test "auto assign two ports to same resource" do - {:ok, assignee} = Servo.build_shelf() + {:ok, assignee} = Parties.build_shelf_with_installer() {:ok, card} = Servo.build_card(%{}) @@ -139,7 +140,7 @@ defmodule Diffo.InstanceExtension.AssignerTest do end test "specific assignment rejects duplicate request" do - {:ok, assignee} = Servo.build_shelf() + {:ok, assignee} = Parties.build_shelf_with_installer() {:ok, card} = Servo.build_card(%{}) @@ -169,7 +170,7 @@ defmodule Diffo.InstanceExtension.AssignerTest do end test "unassign an auto-assigned port from a resource" do - {:ok, assignee} = Servo.build_shelf() + {:ok, assignee} = Parties.build_shelf_with_installer() {:ok, card} = Servo.build_card(%{}) diff --git a/test/instance_extension/characteristic_test.exs b/test/instance_extension/characteristic_test.exs index 13f500b..4748c28 100644 --- a/test/instance_extension/characteristic_test.exs +++ b/test/instance_extension/characteristic_test.exs @@ -6,6 +6,7 @@ defmodule Diffo.InstanceExtension.CharacteristicTest do @moduledoc false use ExUnit.Case alias Diffo.Test.Servo + alias Diffo.Test.Parties setup_all do AshNeo4j.BoltyHelper.start() @@ -27,7 +28,7 @@ defmodule Diffo.InstanceExtension.CharacteristicTest do end test "create resource with array characteristic - success" do - {:ok, shelf} = Servo.build_shelf(%{}) + {:ok, shelf} = Parties.build_shelf_with_installer() shelves = Enum.find(shelf.characteristics, fn c -> c.name == :shelves end) assert shelves.is_array == true diff --git a/test/instance_extension/feature_test.exs b/test/instance_extension/feature_test.exs index 4cf9825..84b778f 100644 --- a/test/instance_extension/feature_test.exs +++ b/test/instance_extension/feature_test.exs @@ -6,6 +6,7 @@ defmodule Diffo.InstanceExtension.FeatureTest do @moduledoc false use ExUnit.Case alias Diffo.Test.Servo + alias Diffo.Test.Parties setup_all do AshNeo4j.BoltyHelper.start() @@ -27,7 +28,7 @@ defmodule Diffo.InstanceExtension.FeatureTest do end test "create resource with array feature characteristic - success" do - {:ok, shelf} = Servo.build_shelf(%{}) + {:ok, shelf} = Parties.build_shelf_with_installer() spectral = Enum.find(shelf.features, fn f -> f.name == :spectralManagement end) deployment_classes = Enum.find(spectral.characteristics, fn c -> c.name == :deploymentClasses end) diff --git a/test/instance_extension/party_test.exs b/test/instance_extension/party_test.exs index 9b91b9e..35deb16 100644 --- a/test/instance_extension/party_test.exs +++ b/test/instance_extension/party_test.exs @@ -8,10 +8,12 @@ defmodule Diffo.InstanceExtension.PartyTest do alias Diffo.Provider.Instance.Extension.Info, as: InstanceInfo alias Diffo.Provider.Party.Extension.Info, as: PartyInfo - alias Diffo.Test.Organisation + alias Diffo.Test.Organization alias Diffo.Test.Person alias Diffo.Test.Shelf alias Diffo.Test.Nbn + alias Diffo.Test.Servo + alias Diffo.Provider.Instance.Party setup_all do AshNeo4j.BoltyHelper.start() @@ -23,29 +25,29 @@ defmodule Diffo.InstanceExtension.PartyTest do end) end - describe "Party DSL — Organisation" do + describe "Party DSL — Organization" do test "instance roles are declared" do - roles = PartyInfo.instance(Organisation) + roles = PartyInfo.instances(Organization) assert length(roles) == 1 - assert hd(roles).role == :facilitates + assert hd(roles).role == :facilitator assert hd(roles).party_type == Diffo.Provider.Instance end test "no party roles declared" do - assert PartyInfo.party(Organisation) == [] + assert PartyInfo.parties(Organization) == [] end end describe "Party DSL — Person" do test "party roles are declared" do - roles = PartyInfo.party(Person) + roles = PartyInfo.parties(Person) assert length(roles) == 1 - assert hd(roles).role == :managed_by + assert hd(roles).role == :manager assert hd(roles).party_type == Diffo.Test.Person end test "no instance roles declared" do - assert PartyInfo.instance(Person) == [] + assert PartyInfo.instances(Person) == [] end end @@ -53,35 +55,120 @@ defmodule Diffo.InstanceExtension.PartyTest do test "party declarations are accessible via info" do parties = InstanceInfo.parties(Shelf) roles = Enum.map(parties, & &1.role) - assert :facilitated_by in roles - assert :overseen_by in roles + assert :facilitator in roles + assert :overseer in roles end test "party types are correct" do parties = InstanceInfo.parties(Shelf) - facilitated_by = Enum.find(parties, &(&1.role == :facilitated_by)) - overseen_by = Enum.find(parties, &(&1.role == :overseen_by)) - assert facilitated_by.party_type == Organisation - assert overseen_by.party_type == Person + facilitator = Enum.find(parties, &(&1.role == :facilitator)) + overseer = Enum.find(parties, &(&1.role == :overseer)) + assert facilitator.party_type == Organization + assert overseer.party_type == Person + end + + test "singular party defaults to multiple: false" do + parties = InstanceInfo.parties(Shelf) + facilitator = Enum.find(parties, &(&1.role == :facilitator)) + assert facilitator.multiple == false + end + + test "reference: true is declared" do + parties = InstanceInfo.parties(Shelf) + provider = Enum.find(parties, &(&1.role == :provider)) + assert provider.reference == true + assert provider.multiple == false + end + + test "reference defaults to false" do + parties = InstanceInfo.parties(Shelf) + facilitator = Enum.find(parties, &(&1.role == :facilitator)) + assert facilitator.reference == false + end + + test "calculate: is declared" do + parties = InstanceInfo.parties(Shelf) + manager = Enum.find(parties, &(&1.role == :manager)) + assert manager.calculate == :manager_calc + end + + test "parties (plural) sets multiple: true" do + parties = InstanceInfo.parties(Shelf) + installer = Enum.find(parties, &(&1.role == :installer)) + assert installer.multiple == true + end + + test "parties (plural) constraints are declared" do + parties = InstanceInfo.parties(Shelf) + installer = Enum.find(parties, &(&1.role == :installer)) + assert installer.constraints == [min: 1, max: 3] + end + end + + describe "Instance DSL — parties enforcement" do + setup do + {:ok, org} = Nbn.create_organization(%{name: "Acme"}) + {:ok, p1} = Nbn.create_person(%{name: "Alice"}) + {:ok, p2} = Nbn.create_person(%{name: "Bob"}) + {:ok, p3} = Nbn.create_person(%{name: "Carol"}) + {:ok, p4} = Nbn.create_person(%{name: "Dave"}) + %{org: org, p1: p1, p2: p2, p3: p3, p4: p4} + end + + test "undeclared role is rejected", %{p1: p1} do + parties = [%Party{id: p1.id, role: :unknown}] + assert {:error, _} = Servo.build_shelf(%{name: "s", parties: parties}) + end + + test "installer below min (0 < 1) is rejected" do + assert {:error, _} = Servo.build_shelf(%{name: "s", parties: []}) + end + + test "installer above max (4 > 3) is rejected", %{p1: p1, p2: p2, p3: p3, p4: p4} do + parties = [ + %Party{id: p1.id, role: :installer}, + %Party{id: p2.id, role: :installer}, + %Party{id: p3.id, role: :installer}, + %Party{id: p4.id, role: :installer} + ] + assert {:error, _} = Servo.build_shelf(%{name: "s", parties: parties}) + end + + test "valid single installer is accepted", %{org: org, p1: p1} do + parties = [ + %Party{id: org.id, role: :facilitator}, + %Party{id: p1.id, role: :installer} + ] + assert {:ok, shelf} = Servo.build_shelf(%{name: "s", parties: parties}) + assert length(shelf.parties) == 2 + end + + test "valid max installers (3) is accepted", %{p1: p1, p2: p2, p3: p3} do + parties = [ + %Party{id: p1.id, role: :installer}, + %Party{id: p2.id, role: :installer}, + %Party{id: p3.id, role: :installer} + ] + assert {:ok, _shelf} = Servo.build_shelf(%{name: "s", parties: parties}) end end - describe "BaseParty — Organisation CRUD" do - test "create and read organisation" do - {:ok, org} = Nbn.create_organisation(%{name: "Acme Corp", kind: :organization}) + describe "BaseParty — Organization CRUD" do + test "create and read organization" do + {:ok, org} = Nbn.create_organization(%{name: "Acme Corp"}) assert org.name == "Acme Corp" - assert org.kind == :organization + assert org.type == :Organization - {:ok, loaded} = Nbn.get_organisation_by_id(org.id) + {:ok, loaded} = Nbn.get_organization_by_id(org.id) assert loaded.name == "Acme Corp" end end describe "BaseParty — Person CRUD" do test "create and read person" do - {:ok, person} = Nbn.create_person(%{name: "Alice", kind: :individual}) + {:ok, person} = Nbn.create_person(%{name: "Alice"}) assert person.name == "Alice" - assert person.kind == :individual + assert person.type == :Individual {:ok, loaded} = Nbn.get_person_by_id(person.id) assert loaded.name == "Alice" diff --git a/test/provider/entity_ref_test.exs b/test/provider/entity_ref_test.exs index 1e83e17..5a3c727 100644 --- a/test/provider/entity_ref_test.exs +++ b/test/provider/entity_ref_test.exs @@ -29,7 +29,7 @@ defmodule Diffo.Provider.EntityRefTest do id: "11b6ba17-2865-41c5-b469-2939249631e8", href: "serviceProblemManagement/v4/serviceProblem/nbnAccess/11b6ba17-2865-41c5-b469-2939249631e8", - referredType: :serviceProblem + referred_type: :serviceProblem }) entity2 = @@ -37,7 +37,7 @@ defmodule Diffo.Provider.EntityRefTest do id: "22b85e20-06a9-4e51-baa3-41c2a72958c5", href: "serviceProblemManagement/v4/serviceProblem/nbnAccess/22b85e20-06a9-4e51-baa3-41c2a72958c5", - referredType: :serviceProblem + referred_type: :serviceProblem }) entity3 = @@ -45,7 +45,7 @@ defmodule Diffo.Provider.EntityRefTest do id: "33db60a1-62bf-4c11-abf3-265287a729c1", href: "serviceProblemManagement/v4/serviceProblem/nbnAccess/33db60a1-62bf-4c11-abf3-265287a729c1", - referredType: :serviceProblem + referred_type: :serviceProblem }) Diffo.Provider.create_entity_ref!(%{ @@ -84,7 +84,7 @@ defmodule Diffo.Provider.EntityRefTest do id: "11b6ba17-2865-41c5-b469-2939249631e8", href: "serviceProblemManagement/v4/serviceProblem/nbnAccess/11b6ba17-2865-41c5-b469-2939249631e8", - referredType: :serviceProblem + referred_type: :serviceProblem }) entity2 = @@ -92,7 +92,7 @@ defmodule Diffo.Provider.EntityRefTest do id: "22b85e20-06a9-4e51-baa3-41c2a72958c5", href: "serviceProblemManagement/v4/serviceProblem/nbnAccess/22b85e20-06a9-4e51-baa3-41c2a72958c5", - referredType: :serviceProblem + referred_type: :serviceProblem }) entity3 = @@ -100,7 +100,7 @@ defmodule Diffo.Provider.EntityRefTest do id: "33db60a1-62bf-4c11-abf3-265287a729c1", href: "serviceProblemManagement/v4/serviceProblem/nbnAccess/33db60a1-62bf-4c11-abf3-265287a729c1", - referredType: :serviceProblem + referred_type: :serviceProblem }) Diffo.Provider.create_entity_ref!(%{ @@ -137,7 +137,7 @@ defmodule Diffo.Provider.EntityRefTest do id: "11b6ba17-2865-41c5-b469-2939249631e8", href: "serviceProblemManagement/v4/serviceProblem/nbnAccess/11b6ba17-2865-41c5-b469-2939249631e8", - referredType: :serviceProblem + referred_type: :serviceProblem }) entity2 = @@ -145,7 +145,7 @@ defmodule Diffo.Provider.EntityRefTest do id: "22b85e20-06a9-4e51-baa3-41c2a72958c5", href: "serviceProblemManagement/v4/serviceProblem/nbnAccess/22b85e20-06a9-4e51-baa3-41c2a72958c5", - referredType: :serviceProblem + referred_type: :serviceProblem }) entity3 = @@ -153,7 +153,7 @@ defmodule Diffo.Provider.EntityRefTest do id: "33db60a1-62bf-4c11-abf3-265287a729c1", href: "serviceProblemManagement/v4/serviceProblem/nbnAccess/33db60a1-62bf-4c11-abf3-265287a729c1", - referredType: :serviceProblem + referred_type: :serviceProblem }) Diffo.Provider.create_entity_ref!(%{ @@ -193,7 +193,7 @@ defmodule Diffo.Provider.EntityRefTest do id: "11b6ba17-2865-41c5-b469-2939249631e8", href: "serviceProblemManagement/v4/serviceProblem/nbnAccess/11b6ba17-2865-41c5-b469-2939249631e8", - referredType: :serviceProblem + referred_type: :serviceProblem }) entity_ref = @@ -239,7 +239,7 @@ defmodule Diffo.Provider.EntityRefTest do id: "11b6ba17-2865-41c5-b469-2939249631e8", href: "serviceProblemManagement/v4/serviceProblem/nbnAccess/11b6ba17-2865-41c5-b469-2939249631e8", - referredType: :serviceProblem + referred_type: :serviceProblem }) entity_ref = @@ -262,7 +262,7 @@ defmodule Diffo.Provider.EntityRefTest do id: "11b6ba17-2865-41c5-b469-2939249631e8", href: "serviceProblemManagement/v4/serviceProblem/nbnAccess/11b6ba17-2865-41c5-b469-2939249631e8", - referredType: :serviceProblem + referred_type: :serviceProblem }) entity_ref = @@ -283,7 +283,7 @@ defmodule Diffo.Provider.EntityRefTest do entity = Diffo.Provider.create_entity!(%{ id: "COR000000123456", - referredType: :cost, + referred_type: :cost, name: "2025-01" }) @@ -305,7 +305,7 @@ defmodule Diffo.Provider.EntityRefTest do entity = Diffo.Provider.create_entity!(%{ id: "COR000000123456", - referredType: :cost, + referred_type: :cost, name: "2025-01" }) @@ -327,7 +327,7 @@ defmodule Diffo.Provider.EntityRefTest do entity = Diffo.Provider.create_entity!(%{ id: "COR000000123456", - referredType: :cost, + referred_type: :cost, name: "2025-01" }) @@ -338,7 +338,7 @@ defmodule Diffo.Provider.EntityRefTest do entity_id: entity.id }) - other_entity = Diffo.Provider.create_entity!(%{id: "COR000000767342", referredType: :cost}) + other_entity = Diffo.Provider.create_entity!(%{id: "COR000000767342", referred_type: :cost}) {:error, _error} = entity_ref |> Diffo.Provider.update_entity_ref(%{entity_id: other_entity.id}) @@ -372,14 +372,14 @@ defmodule Diffo.Provider.EntityRefTest do "{\"id\":\"11b6ba17-2865-41c5-b469-2939249631e8\",\"href\":\"serviceProblemManagement/v4/serviceProblem/nbnAccess/11b6ba17-2865-41c5-b469-2939249631e8\",\"role\":\"reportedOn\",\"@type\":\"serviceProblem\"}" end - test "encode json entity ref referredType - success" do + test "encode json entity ref referred_type - success" do specification = Diffo.Provider.create_specification!(%{name: "nbnAccess"}) instance = Diffo.Provider.create_instance!(%{specified_by: specification.id}) entity = Diffo.Provider.create_entity!(%{ id: "COR000000123456", - referredType: :cost, + referred_type: :cost, name: "2025-01" }) @@ -406,14 +406,14 @@ defmodule Diffo.Provider.EntityRefTest do id: "COR000000123456", href: "costManagement/v2/cost/COR000000123456", name: "2025-01", - referredType: :cost, + referred_type: :cost, type: :EntityRef } } @id_only %EntityRef{entity: %Entity{id: "COR000000123456"}} @href_only %EntityRef{entity: %Entity{href: "costManagement/v2/cost/COR000000123456"}} @name_only %EntityRef{entity: %Entity{name: "2025-01"}} - @referredType_only %EntityRef{entity: %Entity{referredType: :cost}} + @referred_type_only %EntityRef{entity: %Entity{referred_type: :cost}} @type_only %EntityRef{entity: %Entity{type: :EntityRef}} @specific_cost %EntityRef{ role: :expected, @@ -421,7 +421,7 @@ defmodule Diffo.Provider.EntityRefTest do id: "COR000000123456", href: "costManagement/v2/cost/COR000000123456", name: "2025-01", - referredType: :cost, + referred_type: :cost, type: :EntityRef } } @@ -431,7 +431,7 @@ defmodule Diffo.Provider.EntityRefTest do id: &__MODULE__.generic_cost_id/1, href: nil, name: &Outstand.any_bitstring/1, - referredType: :cost, + referred_type: :cost, type: :EntityRef } } @@ -441,7 +441,7 @@ defmodule Diffo.Provider.EntityRefTest do id: "COR000000123456", href: "costManagement/v2/cost/COR000000123456", name: "2025-01", - referredType: :cost, + referred_type: :cost, type: :EntityRef } } @@ -491,10 +491,10 @@ defmodule Diffo.Provider.EntityRefTest do ) gen_result_outstanding_test( - "specific referredType result", + "specific referred_type result", @specific_cost, - update_in(@actual_cost.entity.referredType, fn _ -> nil end), - Ash.Test.strip_metadata(@referredType_only) + update_in(@actual_cost.entity.referred_type, fn _ -> nil end), + Ash.Test.strip_metadata(@referred_type_only) ) gen_result_outstanding_test( @@ -515,7 +515,7 @@ defmodule Diffo.Provider.EntityRefTest do entity = Diffo.Provider.create_entity!(%{ id: "COR000000123456", - referredType: :cost, + referred_type: :cost, name: "2025-01" }) diff --git a/test/provider/entity_test.exs b/test/provider/entity_test.exs index cbd7ca1..ce7fa29 100644 --- a/test/provider/entity_test.exs +++ b/test/provider/entity_test.exs @@ -23,21 +23,21 @@ defmodule Diffo.Provider.EntityTest do id: "11b6ba17-2865-41c5-b469-2939249631e8", href: "serviceProblemManagement/v4/serviceProblem/nbnAccess/11b6ba17-2865-41c5-b469-2939249631e8", - referredType: :serviceProblem + referred_type: :serviceProblem }) Diffo.Provider.create_entity!(%{ id: "22b85e20-06a9-4e51-baa3-41c2a72958c5", href: "serviceProblemManagement/v4/serviceProblem/nbnAccess/22b85e20-06a9-4e51-baa3-41c2a72958c5", - referredType: :serviceProblem + referred_type: :serviceProblem }) Diffo.Provider.create_entity!(%{ id: "33db60a1-62bf-4c11-abf3-265287a729c1", href: "serviceProblemManagement/v4/serviceProblem/nbnAccess/33db60a1-62bf-4c11-abf3-265287a729c1", - referredType: :serviceProblem + referred_type: :serviceProblem }) entities = Diffo.Provider.list_entities!() @@ -50,7 +50,7 @@ defmodule Diffo.Provider.EntityTest do test "find entities by id - success" do Diffo.Provider.create_entity!(%{ id: "COR000000123456", - referredType: :cost, + referred_type: :cost, name: "2025-01" }) @@ -58,12 +58,12 @@ defmodule Diffo.Provider.EntityTest do id: "22b85e20-06a9-4e51-baa3-41c2a72958c5", href: "serviceProblemManagement/v4/serviceProblem/nbnAccess/22b85e20-06a9-4e51-baa3-41c2a72958c5", - referredType: :serviceProblem + referred_type: :serviceProblem }) Diffo.Provider.create_entity!(%{ id: "COR000000767342", - referredType: :cost, + referred_type: :cost, name: "2025-01" }) @@ -77,7 +77,7 @@ defmodule Diffo.Provider.EntityTest do test "find entities by name - success" do Diffo.Provider.create_entity!(%{ id: "COR000000123456", - referredType: :cost, + referred_type: :cost, name: "2025-01" }) @@ -85,12 +85,12 @@ defmodule Diffo.Provider.EntityTest do id: "22b85e20-06a9-4e51-baa3-41c2a72958c5", href: "serviceProblemManagement/v4/serviceProblem/nbnAccess/22b85e20-06a9-4e51-baa3-41c2a72958c5", - referredType: :serviceProblem + referred_type: :serviceProblem }) Diffo.Provider.create_entity!(%{ id: "COR000000767342", - referredType: :cost, + referred_type: :cost, name: "2025-01" }) @@ -103,23 +103,23 @@ defmodule Diffo.Provider.EntityTest do end describe "Diffo.Provider create Entities" do - test "create a service problem referredType entity - success" do + test "create a service problem referred_type entity - success" do entity = Diffo.Provider.create_entity!(%{ id: "11b6ba17-2865-41c5-b469-2939249631e8", href: "serviceProblemManagement/v4/serviceProblem/nbnAccess/11b6ba17-2865-41c5-b469-2939249631e8", - referredType: :serviceProblem + referred_type: :serviceProblem }) assert entity.type == :EntityRef end - test "create a cost referredType entity - success" do + test "create a cost referred_type entity - success" do entity = Diffo.Provider.create_entity!(%{ id: "COR000000123456", - referredType: :cost, + referred_type: :cost, name: "2025-01" }) @@ -135,14 +135,14 @@ defmodule Diffo.Provider.EntityTest do type: :serviceProblem }) - assert entity.referredType == nil + assert entity.referred_type == nil end test "create a cost type entity - success" do entity = Diffo.Provider.create_entity!(%{id: "COR000000123456", type: :cost, name: "2025-01"}) - assert entity.referredType == nil + assert entity.referred_type == nil end test "create an Entity that already exists, preserving attributes - success" do @@ -191,7 +191,7 @@ defmodule Diffo.Provider.EntityTest do entity = Diffo.Provider.create_entity!(%{ id: "11b6ba17-2865-41c5-b469-2939249631e8", - referredType: :serviceProblem + referred_type: :serviceProblem }) updated_entity = @@ -209,7 +209,7 @@ defmodule Diffo.Provider.EntityTest do entity = Diffo.Provider.create_entity!(%{ id: "COR000000123456", - referredType: :cost, + referred_type: :cost, name: "2025-01" }) @@ -225,47 +225,47 @@ defmodule Diffo.Provider.EntityTest do assert updated_entity.type == :sla end - test "update entity referredType - success" do + test "update entity referred_type - success" do entity = Diffo.Provider.create_entity!(%{ id: "COR000000123456", - referredType: :cost, + referred_type: :cost, name: "2025-01" }) - updated_entity = entity |> Diffo.Provider.update_entity!(%{referredType: :sla}) - assert updated_entity.referredType == :sla + updated_entity = entity |> Diffo.Provider.update_entity!(%{referred_type: :sla}) + assert updated_entity.referred_type == :sla end - test "update entity type to referredType - success" do + test "update entity type to referred_type - success" do entity = Diffo.Provider.create_entity!(%{id: "COR000000123456", type: :cost, name: "2025-01"}) updated_entity = - entity |> Diffo.Provider.update_entity!(%{type: :EntityRef, referredType: :cost}) + entity |> Diffo.Provider.update_entity!(%{type: :EntityRef, referred_type: :cost}) assert updated_entity.type == :EntityRef - assert updated_entity.referredType == :cost + assert updated_entity.referred_type == :cost end - test "update entity referredType to type - success" do + test "update entity referred_type to type - success" do entity = Diffo.Provider.create_entity!(%{ id: "COR000000123456", - referredType: :cost, + referred_type: :cost, name: "2025-01" }) - updated_entity = entity |> Diffo.Provider.update_entity!(%{type: :cost, referredType: nil}) + updated_entity = entity |> Diffo.Provider.update_entity!(%{type: :cost, referred_type: nil}) assert updated_entity.type == :cost - assert updated_entity.referredType == nil + assert updated_entity.referred_type == nil end test "update id - failure - href does not end with id" do entity = Diffo.Provider.create_entity!(%{ id: "COR000000897353", - referredType: :cost, + referred_type: :cost, name: "2025-01" }) @@ -273,23 +273,23 @@ defmodule Diffo.Provider.EntityTest do entity |> Diffo.Provider.update_entity(%{href: "entity/nbnco/COR000000897354"}) end - test "update referredType - failure - type Party cannot have referredType" do + test "update referred_type - failure - type Party cannot have referred_type" do entity = Diffo.Provider.create_entity!(%{id: "COR000000897353", type: :cost, name: "2025-01"}) - {:error, _error} = entity |> Diffo.Provider.update_entity(%{referredType: :cost}) + {:error, _error} = entity |> Diffo.Provider.update_entity(%{referred_type: :cost}) end - test "update referredType - failure - EntityRef requires referredType" do + test "update referred_type - failure - EntityRef requires referred_type" do entity = Diffo.Provider.create_entity!(%{ id: "COR000000897353", type: :EntityRef, - referredType: :cost, + referred_type: :cost, name: "2025-01" }) - {:error, _error} = entity |> Diffo.Provider.update_entity(%{referredType: nil}) + {:error, _error} = entity |> Diffo.Provider.update_entity(%{referred_type: nil}) end test "update id - failure - not updatable" do @@ -314,11 +314,11 @@ defmodule Diffo.Provider.EntityTest do "{\"id\":\"11b6ba17-2865-41c5-b469-2939249631e8\",\"href\":\"serviceProblemManagement/v4/serviceProblem/nbnAccess/11b6ba17-2865-41c5-b469-2939249631e8\",\"@type\":\"serviceProblem\"}" end - test "encode json entity referredType - success" do + test "encode json entity referred_type - success" do entity = Diffo.Provider.create_entity!(%{ id: "COR000000123456", - referredType: :cost, + referred_type: :cost, name: "2025-01" }) @@ -337,14 +337,14 @@ defmodule Diffo.Provider.EntityTest do name: "2025-01", href: "costManagement/v2/cost/COR000000123456", type: :EntityRef, - referredType: :cost + referred_type: :cost }) expected_entity = %Diffo.Provider.Entity{ id: ~r/COR\d{12}/, name: ~r/\d{4}-\d{2}/, type: :EntityRef, - referredType: :cost + referred_type: :cost } refute expected_entity >>> entity @@ -356,7 +356,7 @@ defmodule Diffo.Provider.EntityTest do entity = Diffo.Provider.create_entity!(%{ id: "COR000000123456", - referredType: :cost, + referred_type: :cost, name: "2025-01" }) @@ -368,7 +368,7 @@ defmodule Diffo.Provider.EntityTest do entity = Diffo.Provider.create_entity!(%{ id: "COR000000123456", - referredType: :cost, + referred_type: :cost, name: "2025-01" }) diff --git a/test/provider/external_identifier_test.exs b/test/provider/external_identifier_test.exs index 105fcf8..9c88b8c 100644 --- a/test/provider/external_identifier_test.exs +++ b/test/provider/external_identifier_test.exs @@ -29,7 +29,7 @@ defmodule Diffo.Provider.ExternalIdentifierTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) t3_party = @@ -37,7 +37,7 @@ defmodule Diffo.Provider.ExternalIdentifierTest do id: "T3_CONNECTIVITY", name: :entityId, href: "entity/internal/T3_CONNECTIVITY", - referredType: :Entity + referred_type: :Entity }) Diffo.Provider.create_external_identifier!(%{ @@ -71,7 +71,7 @@ defmodule Diffo.Provider.ExternalIdentifierTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) t3_party = @@ -79,7 +79,7 @@ defmodule Diffo.Provider.ExternalIdentifierTest do id: "T3_CONNECTIVITY", name: :entityId, href: "entity/internal/T3_CONNECTIVITY", - referredType: :Entity + referred_type: :Entity }) t3_party2 = @@ -87,7 +87,7 @@ defmodule Diffo.Provider.ExternalIdentifierTest do id: "T3_ADAPTIVE_NETWORKS", name: :entityId, href: "entity/internal/T3_ADAPTIVE_NETWORKS", - referredType: :Entity + referred_type: :Entity }) Diffo.Provider.create_external_identifier!(%{ @@ -142,7 +142,7 @@ defmodule Diffo.Provider.ExternalIdentifierTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) t3_party = @@ -150,7 +150,7 @@ defmodule Diffo.Provider.ExternalIdentifierTest do id: "T3_CONNECTIVITY", name: :entityId, href: "entity/internal/T3_CONNECTIVITY", - referredType: :Entity + referred_type: :Entity }) t3_party2 = @@ -158,7 +158,7 @@ defmodule Diffo.Provider.ExternalIdentifierTest do id: "T3_ADAPTIVE_NETWORKS", name: :entityId, href: "entity/internal/T3_ADAPTIVE_NETWORKS", - referredType: :Entity + referred_type: :Entity }) Diffo.Provider.create_external_identifier!(%{ @@ -216,7 +216,7 @@ defmodule Diffo.Provider.ExternalIdentifierTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) t3_party = @@ -224,7 +224,7 @@ defmodule Diffo.Provider.ExternalIdentifierTest do id: "T3_CONNECTIVITY", name: :entityId, href: "entity/internal/T3_CONNECTIVITY", - referredType: :Entity + referred_type: :Entity }) t3_party2 = @@ -232,7 +232,7 @@ defmodule Diffo.Provider.ExternalIdentifierTest do id: "T3_ADAPTIVE_NETWORKS", name: :entityId, href: "entity/internal/T3_ADAPTIVE_NETWORKS", - referredType: :Entity + referred_type: :Entity }) Diffo.Provider.create_external_identifier!(%{ @@ -298,7 +298,7 @@ defmodule Diffo.Provider.ExternalIdentifierTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) external_identifier = @@ -324,7 +324,7 @@ defmodule Diffo.Provider.ExternalIdentifierTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) {:error, _error} = @@ -346,7 +346,7 @@ defmodule Diffo.Provider.ExternalIdentifierTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) external_identifier = @@ -372,7 +372,7 @@ defmodule Diffo.Provider.ExternalIdentifierTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) external_identifier = @@ -399,7 +399,7 @@ defmodule Diffo.Provider.ExternalIdentifierTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) external_identifier = @@ -426,7 +426,7 @@ defmodule Diffo.Provider.ExternalIdentifierTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) t3_party = @@ -434,7 +434,7 @@ defmodule Diffo.Provider.ExternalIdentifierTest do id: "T3_CONNECTIVITY", name: :entityId, href: "entity/internal/T3_CONNECTIVITY", - referredType: :Entity + referred_type: :Entity }) external_identifier = @@ -462,7 +462,7 @@ defmodule Diffo.Provider.ExternalIdentifierTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) external_identifier = @@ -488,7 +488,7 @@ defmodule Diffo.Provider.ExternalIdentifierTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) external_identifier = @@ -520,7 +520,7 @@ defmodule Diffo.Provider.ExternalIdentifierTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) external_identifier = @@ -565,7 +565,7 @@ defmodule Diffo.Provider.ExternalIdentifierTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) external_identifier = diff --git a/test/provider/instance_test.exs b/test/provider/instance_test.exs index 1d92465..e59d337 100644 --- a/test/provider/instance_test.exs +++ b/test/provider/instance_test.exs @@ -648,7 +648,7 @@ defmodule Diffo.Provider.InstanceTest do id: "LOC000000897353", name: :locationId, href: "place/nbnco/LOC000000897353", - referredType: :GeographicAddress + referred_type: :GeographicAddress }) Diffo.Provider.create_place_ref!(%{ @@ -668,7 +668,7 @@ defmodule Diffo.Provider.InstanceTest do id: "T3_CONNECTIVITY", name: :entityId, href: "entity/internal/T3_CONNECTIVITY", - referredType: :Entity + referred_type: :Entity }) t3_party2 = @@ -676,7 +676,7 @@ defmodule Diffo.Provider.InstanceTest do id: "T3_ADAPTIVE_NETWORKS", name: :entityId, href: "entity/internal/T3_ADAPTIVE_NETWORKS", - referredType: :Entity + referred_type: :Entity }) t4_party = @@ -684,7 +684,7 @@ defmodule Diffo.Provider.InstanceTest do id: "T4_CPE", name: :entityId, href: "entity/internal/T4_CPE", - referredType: :Entity + referred_type: :Entity }) Diffo.Provider.create_party_ref!(%{ @@ -763,7 +763,7 @@ defmodule Diffo.Provider.InstanceTest do entity = Diffo.Provider.create_entity!(%{ id: "COR000000123456", - referredType: :cost, + referred_type: :cost, name: "2025-01" }) @@ -860,7 +860,7 @@ defmodule Diffo.Provider.InstanceTest do id: "LOC000000897353", name: :locationId, href: "place/nbnco/LOC000000897353", - referredType: :GeographicAddress + referred_type: :GeographicAddress }) Diffo.Provider.create_place_ref!(%{ @@ -880,7 +880,7 @@ defmodule Diffo.Provider.InstanceTest do id: "T3_CONNECTIVITY", name: :entityId, href: "entity/internal/T3_CONNECTIVITY", - referredType: :Entity + referred_type: :Entity }) t3_party2 = @@ -888,7 +888,7 @@ defmodule Diffo.Provider.InstanceTest do id: "T3_ADAPTIVE_NETWORKS", name: :entityId, href: "entity/internal/T3_ADAPTIVE_NETWORKS", - referredType: :Entity + referred_type: :Entity }) t4_party = @@ -896,7 +896,7 @@ defmodule Diffo.Provider.InstanceTest do id: "T4_CPE", name: :entityId, href: "entity/internal/T4_CPE", - referredType: :Entity + referred_type: :Entity }) Diffo.Provider.create_external_identifier!(%{ @@ -1328,7 +1328,7 @@ defmodule Diffo.Provider.InstanceTest do id: "T3_CONNECTIVITY", name: :entityId, href: "entity/internal/T3_CONNECTIVITY", - referredType: :Entity, + referred_type: :Entity, type: :PartyRef }) diff --git a/test/provider/instance_util_test.exs b/test/provider/instance_util_test.exs new file mode 100644 index 0000000..53c93e9 --- /dev/null +++ b/test/provider/instance_util_test.exs @@ -0,0 +1,88 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Instance.UtilTest do + @moduledoc false + use ExUnit.Case + + alias Diffo.Provider.Instance.Util + + describe "derive_type/1" do + test "service specification" do + assert Util.derive_type(:serviceSpecification) == :service + end + + test "resource specification" do + assert Util.derive_type(:resourceSpecification) == :resource + end + + test "unknown returns nil" do + assert Util.derive_type(:other) == nil + end + end + + describe "derive_feature_list_name/1" do + test "service" do + assert Util.derive_feature_list_name(:service) == :feature + end + + test "resource" do + assert Util.derive_feature_list_name(:resource) == :activationFeature + end + end + + describe "derive_characteristic_list_name/1" do + test "service" do + assert Util.derive_characteristic_list_name(:service) == :serviceCharacteristic + end + + test "resource" do + assert Util.derive_characteristic_list_name(:resource) == :resourceCharacteristic + end + end + + describe "derive_create_date_name/1" do + test "service" do + assert Util.derive_create_date_name(:service) == :serviceDate + end + + test "resource" do + assert Util.derive_create_date_name(:resource) == nil + end + end + + describe "derive_start_date_name/1" do + test "service" do + assert Util.derive_start_date_name(:service) == :startDate + end + + test "resource" do + assert Util.derive_start_date_name(:resource) == :startOperatingDate + end + end + + describe "derive_end_date_name/1" do + test "service" do + assert Util.derive_end_date_name(:service) == :endDate + end + + test "resource" do + assert Util.derive_end_date_name(:resource) == :endOperatingDate + end + end + + describe "other/1" do + test "actual returns expected" do + assert Util.other(:actual) == :expected + end + + test "expected returns actual" do + assert Util.other(:expected) == :actual + end + + test "unknown returns nil" do + assert Util.other(:unknown) == nil + end + end +end diff --git a/test/provider/note_test.exs b/test/provider/note_test.exs index 4ca9dd6..4e9500c 100644 --- a/test/provider/note_test.exs +++ b/test/provider/note_test.exs @@ -28,7 +28,7 @@ defmodule Diffo.Provider.NoteTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) t3_party = @@ -36,7 +36,7 @@ defmodule Diffo.Provider.NoteTest do id: "T3_CONNECTIVITY", name: :entityId, href: "entity/internal/T3_CONNECTIVITY", - referredType: :Entity + referred_type: :Entity }) Diffo.Provider.create_note!(%{ @@ -70,7 +70,7 @@ defmodule Diffo.Provider.NoteTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) t3_party = @@ -78,7 +78,7 @@ defmodule Diffo.Provider.NoteTest do id: "T3_CONNECTIVITY", name: :entityId, href: "entity/internal/T3_CONNECTIVITY", - referredType: :Entity + referred_type: :Entity }) Diffo.Provider.create_note!(%{ @@ -126,7 +126,7 @@ defmodule Diffo.Provider.NoteTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) t3_party = @@ -134,7 +134,7 @@ defmodule Diffo.Provider.NoteTest do id: "T3_CONNECTIVITY", name: :entityId, href: "entity/internal/T3_CONNECTIVITY", - referredType: :Entity + referred_type: :Entity }) Diffo.Provider.create_note!(%{ @@ -181,7 +181,7 @@ defmodule Diffo.Provider.NoteTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) t3_party = @@ -189,7 +189,7 @@ defmodule Diffo.Provider.NoteTest do id: "T3_CONNECTIVITY", name: :entityId, href: "entity/internal/T3_CONNECTIVITY", - referredType: :Entity + referred_type: :Entity }) Diffo.Provider.create_note!(%{ @@ -251,7 +251,7 @@ defmodule Diffo.Provider.NoteTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) note = @@ -280,7 +280,7 @@ defmodule Diffo.Provider.NoteTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) Diffo.Provider.create_note!(%{ @@ -305,7 +305,7 @@ defmodule Diffo.Provider.NoteTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) Diffo.Provider.create_note!(%{ @@ -334,7 +334,7 @@ defmodule Diffo.Provider.NoteTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) note = @@ -358,7 +358,7 @@ defmodule Diffo.Provider.NoteTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) note = @@ -382,7 +382,7 @@ defmodule Diffo.Provider.NoteTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) note = @@ -409,7 +409,7 @@ defmodule Diffo.Provider.NoteTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) t3_party = @@ -417,7 +417,7 @@ defmodule Diffo.Provider.NoteTest do id: "T3_CONNECTIVITY", name: :entityId, href: "entity/internal/T3_CONNECTIVITY", - referredType: :Entity + referred_type: :Entity }) note = @@ -442,7 +442,7 @@ defmodule Diffo.Provider.NoteTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) note = @@ -466,7 +466,7 @@ defmodule Diffo.Provider.NoteTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) note = @@ -491,7 +491,7 @@ defmodule Diffo.Provider.NoteTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) note = @@ -531,7 +531,7 @@ defmodule Diffo.Provider.NoteTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) note = diff --git a/test/provider/party_ref_test.exs b/test/provider/party_ref_test.exs index d5d6443..42a62a6 100644 --- a/test/provider/party_ref_test.exs +++ b/test/provider/party_ref_test.exs @@ -65,21 +65,21 @@ defmodule Diffo.Provider.PartyRefTest do Diffo.Provider.create_party!(%{ id: "IND000000897353", name: :individualId, - referredType: :Individual + referred_type: :Individual }) party2 = Diffo.Provider.create_party!(%{ id: "IND000000897354", name: :individualId, - referredType: :Individual + referred_type: :Individual }) party3 = Diffo.Provider.create_party!(%{ id: "ORG000163435034", name: :organizationId, - referredType: :Organization + referred_type: :Organization }) Diffo.Provider.create_party_ref!(%{ @@ -124,35 +124,35 @@ defmodule Diffo.Provider.PartyRefTest do Diffo.Provider.create_place!(%{ id: "LOC000000123456", name: :locationId, - referredType: :GeographicAddress + referred_type: :GeographicAddress }) place2 = Diffo.Provider.create_place!(%{ id: "LOC000000897353", name: :locationId, - referredType: :GeographicAddress + referred_type: :GeographicAddress }) party1 = Diffo.Provider.create_party!(%{ id: "IND000000897353", name: :individualId, - referredType: :Individual + referred_type: :Individual }) party2 = Diffo.Provider.create_party!(%{ id: "IND000000897354", name: :individualId, - referredType: :Individual + referred_type: :Individual }) party3 = Diffo.Provider.create_party!(%{ id: "ORG000163435034", name: :organizationId, - referredType: :Organization + referred_type: :Organization }) Diffo.Provider.create_party_ref!(%{ @@ -201,21 +201,21 @@ defmodule Diffo.Provider.PartyRefTest do Diffo.Provider.create_party!(%{ id: "IND000000897353", name: :individualId, - referredType: :Individual + referred_type: :Individual }) party2 = Diffo.Provider.create_party!(%{ id: "IND000000897354", name: :individualId, - referredType: :Individual + referred_type: :Individual }) party3 = Diffo.Provider.create_party!(%{ id: "ORG000000123456", name: :organizationId, - referredType: :Organization + referred_type: :Organization }) Diffo.Provider.create_party_ref!(%{ @@ -260,21 +260,21 @@ defmodule Diffo.Provider.PartyRefTest do Diffo.Provider.create_party!(%{ id: "IND000000897353", name: :individualId, - referredType: :Individual + referred_type: :Individual }) party2 = Diffo.Provider.create_party!(%{ id: "IND000000897354", name: :individualId, - referredType: :Individual + referred_type: :Individual }) party3 = Diffo.Provider.create_party!(%{ id: "ORG000000123456", name: :organizationId, - referredType: :Organization + referred_type: :Organization }) Diffo.Provider.create_party_ref!(%{ @@ -473,7 +473,7 @@ defmodule Diffo.Provider.PartyRefTest do "{\"id\":\"IND000000897353\",\"href\":\"party/internal/IND000000897353\",\"name\":\"individualId\",\"role\":\"PrimaryContact\",\"@type\":\"Individual\"}" end - test "encode json party referredType - success" do + test "encode json party referred_type - success" do specification = Diffo.Provider.create_specification!(%{name: "nbnAccess"}) instance = Diffo.Provider.create_instance!(%{specified_by: specification.id}) @@ -482,7 +482,7 @@ defmodule Diffo.Provider.PartyRefTest do id: "IND000000897353", name: :individualId, href: "party/internal/IND000000897353", - referredType: :Individual + referred_type: :Individual }) party_ref = @@ -509,7 +509,7 @@ defmodule Diffo.Provider.PartyRefTest do id: "IND000000897353", name: "individualId", href: "party/internal/IND000000897353", - referredType: :Individual + referred_type: :Individual }) party_ref = @@ -525,7 +525,7 @@ defmodule Diffo.Provider.PartyRefTest do id: ~r/IND\d{12}/, name: "individualId", type: :PartyRef, - referredType: :Individual + referred_type: :Individual } } @@ -543,7 +543,7 @@ defmodule Diffo.Provider.PartyRefTest do id: "IND000000897353", name: :individualId, href: "party/internal/IND000000897353", - referredType: :Individual + referred_type: :Individual }) party_ref = @@ -562,7 +562,7 @@ defmodule Diffo.Provider.PartyRefTest do Diffo.Provider.create_place!(%{ id: "LOC000000897353", name: :locationId, - referredType: :GeographicAddress + referred_type: :GeographicAddress }) party = @@ -570,7 +570,7 @@ defmodule Diffo.Provider.PartyRefTest do id: "IND000000897353", name: :individualId, href: "party/internal/IND000000897353", - referredType: :Individual + referred_type: :Individual }) party_ref = @@ -589,7 +589,7 @@ defmodule Diffo.Provider.PartyRefTest do Diffo.Provider.create_party!(%{ id: "ORG000163435034", name: :organizationId, - referredType: :Organization + referred_type: :Organization }) party = @@ -597,7 +597,7 @@ defmodule Diffo.Provider.PartyRefTest do id: "IND000000897353", name: :individualId, href: "party/internal/IND000000897353", - referredType: :Individual + referred_type: :Individual }) party_ref = diff --git a/test/provider/party_test.exs b/test/provider/party_test.exs index 4a26765..2bfa727 100644 --- a/test/provider/party_test.exs +++ b/test/provider/party_test.exs @@ -24,13 +24,13 @@ defmodule Diffo.Provider.PartyTest do Diffo.Provider.create_party!(%{ id: "IND000000123456", name: :individualId, - referredType: :Individual + referred_type: :Individual }) Diffo.Provider.create_party!(%{ id: "IND000000897353", name: :individualId, - referredType: :Individual + referred_type: :Individual }) parties = Diffo.Provider.list_parties!() @@ -44,19 +44,19 @@ defmodule Diffo.Provider.PartyTest do Diffo.Provider.create_party!(%{ id: "IND000000897353", name: :individualId, - referredType: :Individual + referred_type: :Individual }) Diffo.Provider.create_party!(%{ id: "IND000000123456", name: :individualId, - referredType: :Individual + referred_type: :Individual }) Diffo.Provider.create_party!(%{ id: "ORG000163435034", name: :organizationId, - referredType: :Organization + referred_type: :Organization }) parties = Diffo.Provider.find_parties_by_id!("IND") @@ -70,19 +70,19 @@ defmodule Diffo.Provider.PartyTest do Diffo.Provider.create_party!(%{ id: "IND000000897353", name: :individualId, - referredType: :Individual + referred_type: :Individual }) Diffo.Provider.create_party!(%{ id: "IND000000123456", name: :individualId, - referredType: :Individual + referred_type: :Individual }) Diffo.Provider.create_party!(%{ id: "ORG000163435034", name: :organizationId, - referredType: :Organization + referred_type: :Organization }) parties = Diffo.Provider.find_parties_by_name!("individual") @@ -94,31 +94,31 @@ defmodule Diffo.Provider.PartyTest do end describe "Diffo.Provider create Parties" do - test "create a Individual referredType party - success" do + test "create a Individual referred_type party - success" do party = Diffo.Provider.create_party!(%{ id: "IND000000897353", name: :individualId, - referredType: :Individual + referred_type: :Individual }) assert party.type == :PartyRef end - test "create a Organization referredType party - success" do + test "create a Organization referred_type party - success" do party = Diffo.Provider.create_party!(%{ id: "ORG000000124343", name: :organizationId, - referredType: :Organization + referred_type: :Organization }) assert party.type == :PartyRef end - test "create a Entity party referredType - success" do + test "create a Entity party referred_type - success" do party = - Diffo.Provider.create_party!(%{id: "T8_NUMBERS", name: :entityId, referredType: :Entity}) + Diffo.Provider.create_party!(%{id: "T8_NUMBERS", name: :entityId, referred_type: :Entity}) assert party.type == :PartyRef end @@ -131,7 +131,7 @@ defmodule Diffo.Provider.PartyTest do type: :Individual }) - assert party.referredType == nil + assert party.referred_type == nil end test "create a Organization type party - success" do @@ -142,12 +142,12 @@ defmodule Diffo.Provider.PartyTest do type: :Organization }) - assert party.referredType == nil + assert party.referred_type == nil end test "create a Entity party type - success" do party = Diffo.Provider.create_party!(%{id: "T8_NUMBERS", name: :entityId, type: :Entity}) - assert party.referredType == nil + assert party.referred_type == nil end test "create a Entity party type with a href - success" do @@ -159,7 +159,7 @@ defmodule Diffo.Provider.PartyTest do type: :Entity }) - assert party.referredType == nil + assert party.referred_type == nil end test "create a Party that already exists, preserving attributes - success" do @@ -230,19 +230,19 @@ defmodule Diffo.Provider.PartyTest do assert updated_party.type == :Entity end - test "update party referredType - success" do + test "update party referred_type - success" do party = Diffo.Provider.create_party!(%{ id: "5ADE", name: :individualId, - referredType: :Individual + referred_type: :Individual }) - updated_party = party |> Diffo.Provider.update_party!(%{referredType: :Entity}) - assert updated_party.referredType == :Entity + updated_party = party |> Diffo.Provider.update_party!(%{referred_type: :Entity}) + assert updated_party.referred_type == :Entity end - test "update party type to referredType - success" do + test "update party type to referred_type - success" do party = Diffo.Provider.create_party!(%{ id: "IND000000897353", @@ -251,25 +251,25 @@ defmodule Diffo.Provider.PartyTest do }) updated_party = - party |> Diffo.Provider.update_party!(%{type: :PartyRef, referredType: :Individual}) + party |> Diffo.Provider.update_party!(%{type: :PartyRef, referred_type: :Individual}) assert updated_party.type == :PartyRef - assert updated_party.referredType == :Individual + assert updated_party.referred_type == :Individual end - test "update party referredType to type - success" do + test "update party referred_type to type - success" do party = Diffo.Provider.create_party!(%{ id: "IND000000897353", name: :individualId, - referredType: :Individual + referred_type: :Individual }) updated_party = - party |> Diffo.Provider.update_party!(%{type: :Individual, referredType: nil}) + party |> Diffo.Provider.update_party!(%{type: :Individual, referred_type: nil}) assert updated_party.type == :Individual - assert updated_party.referredType == nil + assert updated_party.referred_type == nil end test "update id - failure - href does not end with id" do @@ -284,7 +284,7 @@ defmodule Diffo.Provider.PartyTest do party |> Diffo.Provider.update_party(%{href: "party/nbnco/IND000000897354"}) end - test "update referredType - failure - type Party cannot have referredTYpe" do + test "update referred_type - failure - type Party cannot have referredTYpe" do party = Diffo.Provider.create_party!(%{ id: "IND000000897353", @@ -292,19 +292,19 @@ defmodule Diffo.Provider.PartyTest do type: :Individual }) - {:error, _error} = party |> Diffo.Provider.update_party(%{referredType: :Individual}) + {:error, _error} = party |> Diffo.Provider.update_party(%{referred_type: :Individual}) end - test "update referredType - failure - PartyRef requires referredType" do + test "update referred_type - failure - PartyRef requires referred_type" do party = Diffo.Provider.create_party!(%{ id: "IND000000897353", name: :individualId, type: :PartyRef, - referredType: :Individual + referred_type: :Individual }) - {:error, _error} = party |> Diffo.Provider.update_party(%{referredType: nil}) + {:error, _error} = party |> Diffo.Provider.update_party(%{referred_type: nil}) end test "update id - failure - not updatable" do @@ -335,13 +335,13 @@ defmodule Diffo.Provider.PartyTest do "{\"id\":\"IND000000897353\",\"href\":\"party/internal/IND000000897353\",\"name\":\"individualId\",\"@type\":\"Individual\"}" end - test "encode json party referredType - success" do + test "encode json party referred_type - success" do party = Diffo.Provider.create_party!(%{ id: "IND000000897353", name: :individualId, href: "party/internal/IND000000897353", - referredType: :Individual + referred_type: :Individual }) encoding = Jason.encode!(party) @@ -358,13 +358,13 @@ defmodule Diffo.Provider.PartyTest do id: "IND000000897353", name: "individualId", href: "party/internal/IND000000897353", - referredType: :Individual + referred_type: :Individual }) expected_party = %Diffo.Provider.Party{ id: ~r/IND\d{12}/, name: "individualId", - referredType: :Individual + referred_type: :Individual } refute expected_party >>> party @@ -394,7 +394,7 @@ defmodule Diffo.Provider.PartyTest do id: "T5_VALUE_ADD", name: :entityId, href: "entity/internal/T5_VALUE_ADD", - referredType: :Entity + referred_type: :Entity }) external_identifier = @@ -422,7 +422,7 @@ defmodule Diffo.Provider.PartyTest do id: "T3_FIXED", name: :entityId, href: "entity/internal/T3_FIXED", - referredType: :Entity + referred_type: :Entity }) party_ref = diff --git a/test/provider/place_ref_test.exs b/test/provider/place_ref_test.exs index 6781f7c..8f7f743 100644 --- a/test/provider/place_ref_test.exs +++ b/test/provider/place_ref_test.exs @@ -65,21 +65,21 @@ defmodule Diffo.Provider.PlaceRefTest do Diffo.Provider.create_place!(%{ id: "LOC000000897353", name: :locationId, - referredType: :GeographicAddress + referred_type: :GeographicAddress }) place2 = Diffo.Provider.create_place!(%{ id: "LOC000000897354", name: :locationId, - referredType: :GeographicAddress + referred_type: :GeographicAddress }) place3 = Diffo.Provider.create_place!(%{ id: "CSA000000123456", name: :csaId, - referredType: :GeographicLocation + referred_type: :GeographicLocation }) Diffo.Provider.create_place_ref!(%{ @@ -118,35 +118,35 @@ defmodule Diffo.Provider.PlaceRefTest do Diffo.Provider.create_party!(%{ id: "IND000000897353", name: :individualId, - referredType: :Individual + referred_type: :Individual }) party2 = Diffo.Provider.create_party!(%{ id: "IND000000897354", name: :individualId, - referredType: :Individual + referred_type: :Individual }) party3 = Diffo.Provider.create_party!(%{ id: "ORG000163435034", name: :organizationId, - referredType: :Organization + referred_type: :Organization }) place1 = Diffo.Provider.create_place!(%{ id: "LOC000000123456", name: :locationId, - referredType: :GeographicAddress + referred_type: :GeographicAddress }) place2 = Diffo.Provider.create_place!(%{ id: "LOC000000897353", name: :locationId, - referredType: :GeographicAddress + referred_type: :GeographicAddress }) Diffo.Provider.create_place_ref!(%{ @@ -190,21 +190,21 @@ defmodule Diffo.Provider.PlaceRefTest do Diffo.Provider.create_place!(%{ id: "LOC000000897353", name: :locationId, - referredType: :GeographicAddress + referred_type: :GeographicAddress }) place2 = Diffo.Provider.create_place!(%{ id: "LOC000000897354", name: :locationId, - referredType: :GeographicAddress + referred_type: :GeographicAddress }) place3 = Diffo.Provider.create_place!(%{ id: "CSA000000123456", name: :csaId, - referredType: :GeographicLocation + referred_type: :GeographicLocation }) Diffo.Provider.create_place_ref!(%{ @@ -465,7 +465,7 @@ defmodule Diffo.Provider.PlaceRefTest do "{\"id\":\"LOC000000897353\",\"href\":\"place/nbnco/LOC000000897353\",\"name\":\"locationId\",\"role\":\"CustomerSite\",\"@type\":\"GeographicAddress\"}" end - test "encode json place referredType - success" do + test "encode json place referred_type - success" do specification = Diffo.Provider.create_specification!(%{name: "nbnAccess"}) instance = Diffo.Provider.create_instance!(%{specified_by: specification.id}) @@ -474,7 +474,7 @@ defmodule Diffo.Provider.PlaceRefTest do id: "LOC000000897353", name: :locationId, href: "place/nbnco/LOC000000897353", - referredType: :GeographicAddress + referred_type: :GeographicAddress }) place_ref = @@ -501,7 +501,7 @@ defmodule Diffo.Provider.PlaceRefTest do id: "LOC000000897353", name: "locationId", href: "place/nbnco/LOC000000897353", - referredType: :GeographicAddress + referred_type: :GeographicAddress }) place_ref = @@ -517,7 +517,7 @@ defmodule Diffo.Provider.PlaceRefTest do id: ~r/LOC\d{12}/, name: "locationId", type: :PlaceRef, - referredType: :GeographicAddress + referred_type: :GeographicAddress } } @@ -535,7 +535,7 @@ defmodule Diffo.Provider.PlaceRefTest do id: "LOC000000897353", name: :locationId, href: "place/nbnco/LOC000000897353", - referredType: :GeographicAddress + referred_type: :GeographicAddress }) place_ref = @@ -555,14 +555,14 @@ defmodule Diffo.Provider.PlaceRefTest do id: "IND000000897353", name: :individualId, href: "party/internal/IND000000897353", - referredType: :Individual + referred_type: :Individual }) place = Diffo.Provider.create_place!(%{ id: "LOC000000897353", name: :locationId, - referredType: :GeographicAddress + referred_type: :GeographicAddress }) place_ref = diff --git a/test/provider/place_test.exs b/test/provider/place_test.exs index d31c220..07a2e9b 100644 --- a/test/provider/place_test.exs +++ b/test/provider/place_test.exs @@ -24,13 +24,13 @@ defmodule Diffo.Provider.PlaceTest do Diffo.Provider.create_place!(%{ id: "LOC000000123456", name: :locationId, - referredType: :GeographicAddress + referred_type: :GeographicAddress }) Diffo.Provider.create_place!(%{ id: "LOC000000897353", name: :locationId, - referredType: :GeographicAddress + referred_type: :GeographicAddress }) places = Diffo.Provider.list_places!() @@ -44,19 +44,19 @@ defmodule Diffo.Provider.PlaceTest do Diffo.Provider.create_place!(%{ id: "LOC000000897353", name: :locationId, - referredType: :GeographicAddress + referred_type: :GeographicAddress }) Diffo.Provider.create_place!(%{ id: "LOC000000123456", name: :locationId, - referredType: :GeographicAddress + referred_type: :GeographicAddress }) Diffo.Provider.create_place!(%{ id: "163435034", name: :adborId, - referredType: :GeographicAddress + referred_type: :GeographicAddress }) places = Diffo.Provider.find_places_by_id!("LOC") @@ -70,19 +70,19 @@ defmodule Diffo.Provider.PlaceTest do Diffo.Provider.create_place!(%{ id: "LOC000000897353", name: :locationId, - referredType: :GeographicAddress + referred_type: :GeographicAddress }) Diffo.Provider.create_place!(%{ id: "LOC000000123456", name: :locationId, - referredType: :GeographicAddress + referred_type: :GeographicAddress }) Diffo.Provider.create_place!(%{ id: "163435034", name: :adborId, - referredType: :GeographicAddress + referred_type: :GeographicAddress }) places = Diffo.Provider.find_places_by_name!("location") @@ -94,31 +94,31 @@ defmodule Diffo.Provider.PlaceTest do end describe "Diffo.Provider create Places" do - test "create a GeographicAddress referredType place - success" do + test "create a GeographicAddress referred_type place - success" do place = Diffo.Provider.create_place!(%{ id: "LOC000000897353", name: :locationId, - referredType: :GeographicAddress + referred_type: :GeographicAddress }) assert place.type == :PlaceRef end - test "create a GeographicLocation referredType place - success" do + test "create a GeographicLocation referred_type place - success" do place = Diffo.Provider.create_place!(%{ id: "CSA000000124343", name: :csaId, - referredType: :GeographicLocation + referred_type: :GeographicLocation }) assert place.type == :PlaceRef end - test "create a GeographicSite place referredType - success" do + test "create a GeographicSite place referred_type - success" do place = - Diffo.Provider.create_place!(%{id: "3NBA", name: :poiId, referredType: :GeographicSite}) + Diffo.Provider.create_place!(%{id: "3NBA", name: :poiId, referred_type: :GeographicSite}) assert place.type == :PlaceRef end @@ -131,7 +131,7 @@ defmodule Diffo.Provider.PlaceTest do type: :GeographicAddress }) - assert place.referredType == nil + assert place.referred_type == nil end test "create a GeographicLocation type place - success" do @@ -142,12 +142,12 @@ defmodule Diffo.Provider.PlaceTest do type: :GeographicLocation }) - assert place.referredType == nil + assert place.referred_type == nil end test "create a GeographicSite place type - success" do place = Diffo.Provider.create_place!(%{id: "3NBA", name: :poiId, type: :GeographicSite}) - assert place.referredType == nil + assert place.referred_type == nil end test "create a GeographicSite place type with a href - success" do @@ -159,7 +159,7 @@ defmodule Diffo.Provider.PlaceTest do type: :GeographicSite }) - assert place.referredType == nil + assert place.referred_type == nil end test "create a Place that already exists, preserving attributes - success" do @@ -226,19 +226,19 @@ defmodule Diffo.Provider.PlaceTest do assert updated_place.type == :GeographicSite end - test "update place referredType - success" do + test "update place referred_type - success" do place = Diffo.Provider.create_place!(%{ id: "5ADE", name: :locationId, - referredType: :GeographicAddress + referred_type: :GeographicAddress }) - updated_place = place |> Diffo.Provider.update_place!(%{referredType: :GeographicSite}) - assert updated_place.referredType == :GeographicSite + updated_place = place |> Diffo.Provider.update_place!(%{referred_type: :GeographicSite}) + assert updated_place.referred_type == :GeographicSite end - test "update place type to referredType - success" do + test "update place type to referred_type - success" do place = Diffo.Provider.create_place!(%{ id: "LOC000000897353", @@ -248,25 +248,25 @@ defmodule Diffo.Provider.PlaceTest do updated_place = place - |> Diffo.Provider.update_place!(%{type: :PlaceRef, referredType: :GeographicAddress}) + |> Diffo.Provider.update_place!(%{type: :PlaceRef, referred_type: :GeographicAddress}) assert updated_place.type == :PlaceRef - assert updated_place.referredType == :GeographicAddress + assert updated_place.referred_type == :GeographicAddress end - test "update place referredType to type - success" do + test "update place referred_type to type - success" do place = Diffo.Provider.create_place!(%{ id: "LOC000000897353", name: :locationId, - referredType: :GeographicAddress + referred_type: :GeographicAddress }) updated_place = - place |> Diffo.Provider.update_place!(%{type: :GeographicAddress, referredType: nil}) + place |> Diffo.Provider.update_place!(%{type: :GeographicAddress, referred_type: nil}) assert updated_place.type == :GeographicAddress - assert updated_place.referredType == nil + assert updated_place.referred_type == nil end test "update id - failure - href does not end with id" do @@ -281,7 +281,7 @@ defmodule Diffo.Provider.PlaceTest do place |> Diffo.Provider.update_place(%{href: "place/nbnco/LOC000000897354"}) end - test "update referredType - failure - type Place cannot have referredTYpe" do + test "update referred_type - failure - type Place cannot have referredTYpe" do place = Diffo.Provider.create_place!(%{ id: "LOC000000897353", @@ -289,19 +289,19 @@ defmodule Diffo.Provider.PlaceTest do type: :GeographicAddress }) - {:error, _error} = place |> Diffo.Provider.update_place(%{referredType: :GeographicAddress}) + {:error, _error} = place |> Diffo.Provider.update_place(%{referred_type: :GeographicAddress}) end - test "update referredType - failure - PlaceRef requires referredType" do + test "update referred_type - failure - PlaceRef requires referred_type" do place = Diffo.Provider.create_place!(%{ id: "LOC000000897353", name: :locationId, type: :PlaceRef, - referredType: :GeographicAddress + referred_type: :GeographicAddress }) - {:error, _error} = place |> Diffo.Provider.update_place(%{referredType: nil}) + {:error, _error} = place |> Diffo.Provider.update_place(%{referred_type: nil}) end test "update id - failure - not updatable" do @@ -332,13 +332,13 @@ defmodule Diffo.Provider.PlaceTest do "{\"id\":\"LOC000000897353\",\"href\":\"place/nbnco/LOC000000897353\",\"name\":\"locationId\",\"@type\":\"GeographicAddress\"}" end - test "encode json place referredType - success" do + test "encode json place referred_type - success" do place = Diffo.Provider.create_place!(%{ id: "LOC000000897353", name: :locationId, href: "place/nbnco/LOC000000897353", - referredType: :GeographicAddress + referred_type: :GeographicAddress }) encoding = Jason.encode!(place) @@ -355,14 +355,14 @@ defmodule Diffo.Provider.PlaceTest do id: "LOC000000897353", name: "locationId", href: "place/nbnco/LOC000000897353", - referredType: :GeographicAddress + referred_type: :GeographicAddress }) expected_place = %Diffo.Provider.Place{ id: ~r/LOC\d{12}/, name: "locationId", type: :PlaceRef, - referredType: :GeographicAddress + referred_type: :GeographicAddress } refute expected_place >>> place @@ -376,7 +376,7 @@ defmodule Diffo.Provider.PlaceTest do id: "LOC000000898353", name: :locationId, href: "place/nbnco/LOC000000898353", - referredType: :GeographicAddress + referred_type: :GeographicAddress }) :ok = Diffo.Provider.delete_place(place) @@ -392,7 +392,7 @@ defmodule Diffo.Provider.PlaceTest do id: "LOC000000899353", name: :locationId, href: "place/nbnco/LOC000000899353", - referredType: :GeographicAddress + referred_type: :GeographicAddress }) place_ref = diff --git a/test/provider/reference_test.exs b/test/provider/reference_test.exs index 3e349bb..2017b85 100644 --- a/test/provider/reference_test.exs +++ b/test/provider/reference_test.exs @@ -6,6 +6,37 @@ defmodule Diffo.Provider.ReferenceTest do @moduledoc false use ExUnit.Case + alias Diffo.Provider.Reference + + describe "Diffo.Provider.Reference construction" do + test "reference/1 from id and href" do + instance = %{ + id: "8bcfbf9a-34a5-427a-8eae-5c3812466432", + href: "serviceInventoryManagement/v4/service/8bcfbf9a-34a5-427a-8eae-5c3812466432" + } + + assert Reference.reference(instance) == %Reference{ + id: "8bcfbf9a-34a5-427a-8eae-5c3812466432", + href: "serviceInventoryManagement/v4/service/8bcfbf9a-34a5-427a-8eae-5c3812466432" + } + end + + test "reference/2 extracts id from trailing uuid in href attribute" do + instance = %{ + target_href: "serviceInventoryManagement/v4/service/8bcfbf9a-34a5-427a-8eae-5c3812466432" + } + + assert Reference.reference(instance, :target_href) == %Reference{ + id: "8bcfbf9a-34a5-427a-8eae-5c3812466432", + href: "serviceInventoryManagement/v4/service/8bcfbf9a-34a5-427a-8eae-5c3812466432" + } + end + + test "reference/1 nil returns nil" do + assert Reference.reference(nil) == nil + end + end + describe "Diffo.Provider.Reference encode" do test "encode json - success" do reference = %Diffo.Provider.Reference{ diff --git a/test/support/nbn.ex b/test/support/nbn.ex index 6ac259e..c55292d 100644 --- a/test/support/nbn.ex +++ b/test/support/nbn.ex @@ -12,7 +12,7 @@ defmodule Diffo.Test.Nbn do otp_app: :diffo, validate_config_inclusion?: false - alias Diffo.Test.Organisation + alias Diffo.Test.Organization alias Diffo.Test.Person domain do @@ -20,14 +20,14 @@ defmodule Diffo.Test.Nbn do end resources do - resource Organisation do - define :create_organisation, action: :create - define :get_organisation_by_id, action: :read, get_by: :id - define :list_organisations, action: :list + resource Organization do + define :create_organization, action: :build + define :get_organization_by_id, action: :read, get_by: :id + define :list_organizations, action: :list end resource Person do - define :create_person, action: :create + define :create_person, action: :build define :get_person_by_id, action: :read, get_by: :id define :list_persons, action: :list end diff --git a/test/support/parties.ex b/test/support/parties.ex index 37f0b71..e14a1b4 100644 --- a/test/support/parties.ex +++ b/test/support/parties.ex @@ -11,6 +11,13 @@ defmodule Diffo.Test.Parties do import ExUnit.Assertions + alias Diffo.Provider.Instance.Party + + def build_shelf_with_installer do + {:ok, person} = Diffo.Test.Nbn.create_person(%{name: "Installer"}) + Diffo.Test.Servo.build_shelf(%{parties: [%Party{id: person.id, role: :installer}]}) + end + def check_parties(expected_parties, instance) when is_list(expected_parties) and is_struct(instance) do Enum.zip_reduce(expected_parties, instance.parties, [], fn _expected_party, diff --git a/test/support/resource/organisation.ex b/test/support/resource/organisation.ex deleted file mode 100644 index b911948..0000000 --- a/test/support/resource/organisation.ex +++ /dev/null @@ -1,27 +0,0 @@ -# SPDX-FileCopyrightText: 2025 diffo contributors -# -# SPDX-License-Identifier: MIT - -defmodule Diffo.Test.Organisation do - @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - Organisation - Organisation Party - """ - - alias Diffo.Provider.BaseParty - alias Diffo.Test.Nbn - - use Ash.Resource, - fragments: [BaseParty], - domain: Nbn - - resource do - description "An Organisation" - plural_name :organisations - end - - instance do - role :facilitates, Diffo.Provider.Instance - end -end diff --git a/test/support/resource/organization.ex b/test/support/resource/organization.ex new file mode 100644 index 0000000..f50930c --- /dev/null +++ b/test/support/resource/organization.ex @@ -0,0 +1,43 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Test.Organization do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + Organization - Organization Party + """ + + alias Diffo.Provider.BaseParty + alias Diffo.Test.Nbn + + use Ash.Resource, + fragments: [BaseParty], + domain: Nbn + + resource do + description "An Organization" + plural_name :organizations + end + + jason do + pick [:id, :name, :type] + compact true + end + + outstanding do + expect [:id, :name, :type] + end + + actions do + create :build do + accept [:id, :href, :name] + change set_attribute(:type, :Organization) + end + end + + instances do + role :facilitator, Diffo.Provider.Instance + end +end diff --git a/test/support/resource/person.ex b/test/support/resource/person.ex index ab27e2a..55aeb0b 100644 --- a/test/support/resource/person.ex +++ b/test/support/resource/person.ex @@ -21,7 +21,23 @@ defmodule Diffo.Test.Person do plural_name :persons end - party do - role :managed_by, Diffo.Test.Person + jason do + pick [:id, :name, :type] + compact true + end + + outstanding do + expect [:id, :name, :type] + end + + actions do + create :build do + accept [:id, :href, :name] + change set_attribute(:type, :Individual) + end + end + + parties do + role :manager, Diffo.Test.Person end end diff --git a/test/support/resource/shelf.ex b/test/support/resource/shelf.ex index b9e1ecc..71350ce 100644 --- a/test/support/resource/shelf.ex +++ b/test/support/resource/shelf.ex @@ -53,8 +53,11 @@ defmodule Diffo.Test.Shelf do end parties do - party :facilitated_by, Diffo.Test.Organisation - party :overseen_by, Diffo.Test.Person + party :facilitator, Diffo.Test.Organization + party :overseer, Diffo.Test.Person + party :provider, Diffo.Test.Organization, reference: true + party :manager, Diffo.Test.Organization, calculate: :manager_calc + parties :installer, Diffo.Test.Person, constraints: [min: 1, max: 3] end actions do From 7386b03f2019cd9907c76fc355c84421a6908521 Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Mon, 27 Apr 2026 22:05:32 +0930 Subject: [PATCH 03/22] verifiers for instance extension --- lib/diffo/provider/components/entity_ref.ex | 2 +- .../provider/components/instance/extension.ex | 8 +- .../verifiers/verify_characteristics.ex | 48 +++++ .../extension/verifiers/verify_features.ex | 51 ++++++ .../extension/verifiers/verify_parties.ex | 52 ++++++ .../verifiers/verify_specification.ex | 35 ++++ test/instance_extension/verifier_test.exs | 173 ++++++++++++++++++ test/support/util.ex | 15 ++ 8 files changed, 382 insertions(+), 2 deletions(-) create mode 100644 lib/diffo/provider/components/instance/extension/verifiers/verify_characteristics.ex create mode 100644 lib/diffo/provider/components/instance/extension/verifiers/verify_features.ex create mode 100644 lib/diffo/provider/components/instance/extension/verifiers/verify_parties.ex create mode 100644 lib/diffo/provider/components/instance/extension/verifiers/verify_specification.ex create mode 100644 test/instance_extension/verifier_test.exs create mode 100644 test/support/util.ex diff --git a/lib/diffo/provider/components/entity_ref.ex b/lib/diffo/provider/components/entity_ref.ex index 532b3c9..8d09ff3 100644 --- a/lib/diffo/provider/components/entity_ref.ex +++ b/lib/diffo/provider/components/entity_ref.ex @@ -4,7 +4,7 @@ defmodule Diffo.Provider.EntityRef do @moduledoc """ - EntityRef - Ash Resource for a TMF Entity Reference + Ash Resource for a TMF Entity Reference """ use Ash.Resource, otp_app: :diffo, diff --git a/lib/diffo/provider/components/instance/extension.ex b/lib/diffo/provider/components/instance/extension.ex index 3956787..94409e6 100644 --- a/lib/diffo/provider/components/instance/extension.ex +++ b/lib/diffo/provider/components/instance/extension.ex @@ -223,5 +223,11 @@ defmodule Diffo.Provider.Instance.Extension do } use Spark.Dsl.Extension, - sections: [@specification, @features, @characteristics, @parties] + sections: [@specification, @features, @characteristics, @parties], + verifiers: [ + Diffo.Provider.Instance.Extension.Verifiers.VerifySpecification, + Diffo.Provider.Instance.Extension.Verifiers.VerifyCharacteristics, + Diffo.Provider.Instance.Extension.Verifiers.VerifyFeatures, + Diffo.Provider.Instance.Extension.Verifiers.VerifyParties + ] end diff --git a/lib/diffo/provider/components/instance/extension/verifiers/verify_characteristics.ex b/lib/diffo/provider/components/instance/extension/verifiers/verify_characteristics.ex new file mode 100644 index 0000000..841ce54 --- /dev/null +++ b/lib/diffo/provider/components/instance/extension/verifiers/verify_characteristics.ex @@ -0,0 +1,48 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Instance.Extension.Verifiers.VerifyCharacteristics do + @moduledoc "Verifies that characteristic value_type modules exist" + use Spark.Dsl.Verifier + + alias Spark.Dsl.Verifier + alias Spark.Error.DslError + + @impl true + def verify(dsl_state) do + resource = Verifier.get_persisted(dsl_state, :module) + characteristics = Verifier.get_entities(dsl_state, [:characteristics]) + + errors = + Enum.reduce(characteristics, [], fn char, acc -> + case module_from_value_type(char.value_type) do + {:ok, module} -> + if Code.ensure_loaded?(module) do + acc + else + [ + DslError.exception( + module: resource, + path: [:characteristics, char.name], + message: "characteristics: value_type #{inspect(module)} does not exist" + ) + | acc + ] + end + + :error -> + acc + end + end) + + case errors do + [] -> :ok + errors -> {:error, errors} + end + end + + defp module_from_value_type({:array, module}) when is_atom(module), do: {:ok, module} + defp module_from_value_type(module) when is_atom(module), do: {:ok, module} + defp module_from_value_type(_), do: :error +end diff --git a/lib/diffo/provider/components/instance/extension/verifiers/verify_features.ex b/lib/diffo/provider/components/instance/extension/verifiers/verify_features.ex new file mode 100644 index 0000000..0d15f9b --- /dev/null +++ b/lib/diffo/provider/components/instance/extension/verifiers/verify_features.ex @@ -0,0 +1,51 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Instance.Extension.Verifiers.VerifyFeatures do + @moduledoc "Verifies that feature characteristic value_type modules exist" + use Spark.Dsl.Verifier + + alias Spark.Dsl.Verifier + alias Spark.Error.DslError + + @impl true + def verify(dsl_state) do + resource = Verifier.get_persisted(dsl_state, :module) + features = Verifier.get_entities(dsl_state, [:features]) + + errors = + Enum.reduce(features, [], fn feature, acc -> + Enum.reduce(feature.characteristics, acc, fn char, acc -> + case module_from_value_type(char.value_type) do + {:ok, module} -> + if Code.ensure_loaded?(module) do + acc + else + [ + DslError.exception( + module: resource, + path: [:features, feature.name, :characteristics, char.name], + message: + "features: characteristic value_type #{inspect(module)} does not exist" + ) + | acc + ] + end + + :error -> + acc + end + end) + end) + + case errors do + [] -> :ok + errors -> {:error, errors} + end + end + + defp module_from_value_type({:array, module}) when is_atom(module), do: {:ok, module} + defp module_from_value_type(module) when is_atom(module), do: {:ok, module} + defp module_from_value_type(_), do: :error +end diff --git a/lib/diffo/provider/components/instance/extension/verifiers/verify_parties.ex b/lib/diffo/provider/components/instance/extension/verifiers/verify_parties.ex new file mode 100644 index 0000000..5572e34 --- /dev/null +++ b/lib/diffo/provider/components/instance/extension/verifiers/verify_parties.ex @@ -0,0 +1,52 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Instance.Extension.Verifiers.VerifyParties do + @moduledoc "Verifies party role declarations — no duplicates, party_type modules must exist" + use Spark.Dsl.Verifier + + alias Spark.Dsl.Verifier + alias Spark.Error.DslError + + @impl true + def verify(dsl_state) do + resource = Verifier.get_persisted(dsl_state, :module) + parties = Verifier.get_entities(dsl_state, [:parties]) + + duplicate_errors = + parties + |> Enum.group_by(& &1.role) + |> Enum.filter(fn {_role, declarations} -> length(declarations) > 1 end) + |> Enum.map(fn {role, _} -> + DslError.exception( + module: resource, + path: [:parties], + message: "parties: role #{inspect(role)} is declared more than once" + ) + end) + + type_errors = + Enum.reduce(parties, [], fn party, acc -> + if party.party_type && !Code.ensure_loaded?(party.party_type) do + [ + DslError.exception( + module: resource, + path: [:parties, party.role], + message: "parties: party_type #{inspect(party.party_type)} does not exist" + ) + | acc + ] + else + acc + end + end) + + errors = duplicate_errors ++ type_errors + + case errors do + [] -> :ok + errors -> {:error, errors} + end + end +end diff --git a/lib/diffo/provider/components/instance/extension/verifiers/verify_specification.ex b/lib/diffo/provider/components/instance/extension/verifiers/verify_specification.ex new file mode 100644 index 0000000..b6ac1bd --- /dev/null +++ b/lib/diffo/provider/components/instance/extension/verifiers/verify_specification.ex @@ -0,0 +1,35 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Instance.Extension.Verifiers.VerifySpecification do + @moduledoc "Verifies that the specification id is a valid UUID4" + use Spark.Dsl.Verifier + + alias Spark.Dsl.Verifier + alias Spark.Error.DslError + + @impl true + def verify(dsl_state) do + resource = Verifier.get_persisted(dsl_state, :module) + spec_id = Verifier.get_option(dsl_state, [:specification], :id) + + errors = + if spec_id && !Diffo.Uuid.uuid4?(spec_id) do + [ + DslError.exception( + module: resource, + path: [:specification, :id], + message: "specification: id must be a valid UUID4" + ) + ] + else + [] + end + + case errors do + [] -> :ok + errors -> {:error, errors} + end + end +end diff --git a/test/instance_extension/verifier_test.exs b/test/instance_extension/verifier_test.exs new file mode 100644 index 0000000..2ba6140 --- /dev/null +++ b/test/instance_extension/verifier_test.exs @@ -0,0 +1,173 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.InstanceExtension.VerifierTest do + @moduledoc false + use ExUnit.Case, async: false + alias Diffo.Test.Util + + describe "specification verifier" do + test "invalid UUID4 in specification id warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "specification: id must be a valid UUID4", + fn -> + defmodule InvalidSpecId do + alias Diffo.Provider.BaseInstance + use Ash.Resource, fragments: [BaseInstance], domain: Diffo.Test.Servo + + resource do + description "resource with invalid spec id" + end + + specification do + id "ef016d85-9dbd-429c-04da-1df56cc7dda5" + name "invalid" + end + end + end + ) + end + end + + describe "characteristics verifier" do + test "non-existent value_type module warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "characteristics: value_type NonExistent.CharValue does not exist", + fn -> + defmodule InvalidCharValueType do + alias Diffo.Provider.BaseInstance + use Ash.Resource, fragments: [BaseInstance], domain: Diffo.Test.Servo + + resource do + description "resource with non-existent characteristic value_type" + end + + specification do + id "cd29956f-6c68-44cc-bf54-705eb8d2f754" + name "invalid" + end + + characteristics do + characteristic :foo, NonExistent.CharValue + end + end + end + ) + end + + test "non-existent array value_type module warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "characteristics: value_type NonExistent.ArrayValue does not exist", + fn -> + defmodule InvalidArrayCharValueType do + alias Diffo.Provider.BaseInstance + use Ash.Resource, fragments: [BaseInstance], domain: Diffo.Test.Servo + + resource do + description "resource with non-existent array characteristic value_type" + end + + specification do + id "cd29956f-6c68-44cc-bf54-705eb8d2f754" + name "invalid" + end + + characteristics do + characteristic :bar, {:array, NonExistent.ArrayValue} + end + end + end + ) + end + end + + describe "features verifier" do + test "non-existent feature characteristic value_type warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "features: characteristic value_type NonExistent.FeatureValue does not exist", + fn -> + defmodule InvalidFeatureCharValueType do + alias Diffo.Provider.BaseInstance + use Ash.Resource, fragments: [BaseInstance], domain: Diffo.Test.Servo + + resource do + description "resource with non-existent feature characteristic value_type" + end + + specification do + id "cd29956f-6c68-44cc-bf54-705eb8d2f754" + name "invalid" + end + + features do + feature :my_feature do + characteristic :baz, NonExistent.FeatureValue + end + end + end + end + ) + end + end + + describe "parties verifier" do + test "duplicate party role names warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "parties: role :operator is declared more than once", + fn -> + defmodule DuplicatePartyRole do + alias Diffo.Provider.BaseInstance + alias Diffo.Test.Shelf + use Ash.Resource, fragments: [BaseInstance], domain: Diffo.Test.Servo + + resource do + description "resource with duplicate party roles" + end + + specification do + id "cd29956f-6c68-44cc-bf54-705eb8d2f754" + name "invalid" + end + + parties do + party :operator, Shelf + party :operator, Shelf + end + end + end + ) + end + + test "non-existent party_type module warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "parties: party_type NonExistent.PartyModule does not exist", + fn -> + defmodule InvalidPartyType do + alias Diffo.Provider.BaseInstance + use Ash.Resource, fragments: [BaseInstance], domain: Diffo.Test.Servo + + resource do + description "resource with non-existent party_type" + end + + specification do + id "cd29956f-6c68-44cc-bf54-705eb8d2f754" + name "invalid" + end + + parties do + party :operator, NonExistent.PartyModule + end + end + end + ) + end + end +end diff --git a/test/support/util.ex b/test/support/util.ex new file mode 100644 index 0000000..0b01056 --- /dev/null +++ b/test/support/util.ex @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Test.Util do + @moduledoc false + use ExUnit.Case + import ExUnit.CaptureIO + + def assert_compile_time_warning(module, message, fun) when is_bitstring(message) do + output = capture_io(:stderr, fun) + assert output =~ String.trim_leading("#{module}", "Elixir.") + assert output =~ message + end +end From 620624c3122b331ee83c24caeee0a0041e2832fa Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Tue, 28 Apr 2026 07:58:23 +0930 Subject: [PATCH 04/22] =?UTF-8?q?fix=20domain=20boundary=20in=20build=5Faf?= =?UTF-8?q?ter=20=E2=80=94=20eliminate=20reload=20workaround?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit relate_instance functions now pass %Instance{id: result.id} to Provider calls rather than converting the domain-specific struct via Map.from_struct. This respects Ash's static relationship contracts (Feature.instance_id belongs to Diffo.Provider.Instance, not Shelf or Card). build_after/2 ends with Ash.load/2 to load relationships back onto the correct domain struct, replacing the per-domain reload that was masking the struct type loss. build_after/4 removed entirely. Also replaces Map.put with struct update syntax and drops dead-looking aliases in favour of fully qualified module names in DSL options. --- .../provider/components/base_instance.ex | 2 +- .../instance/extension/action_helper.ex | 20 ++++++++++--------- .../instance/extension/characteristic.ex | 3 +-- .../components/instance/extension/feature.ex | 3 +-- .../instance/extension/specification.ex | 13 +----------- test/support/resource/card.ex | 2 +- .../invalid/invalid_characteristic.ex | 11 ++-------- .../invalid/invalid_feature_characteristic.ex | 11 ++-------- .../resource/invalid/invalid_specification.ex | 11 ++-------- test/support/resource/shelf.ex | 2 +- 10 files changed, 23 insertions(+), 55 deletions(-) diff --git a/lib/diffo/provider/components/base_instance.ex b/lib/diffo/provider/components/base_instance.ex index 6fcb5cd..16cf244 100644 --- a/lib/diffo/provider/components/base_instance.ex +++ b/lib/diffo/provider/components/base_instance.ex @@ -69,7 +69,7 @@ defmodule Diffo.Provider.BaseInstance do create :build do change after_action(fn changeset, result, _context -> - ActionHelper.build_after(changeset, result, MyApp.Domain, :get_cluster_by_id) + ActionHelper.build_after(changeset, result) end) end """ diff --git a/lib/diffo/provider/components/instance/extension/action_helper.ex b/lib/diffo/provider/components/instance/extension/action_helper.ex index 8dfa7c1..9f9820d 100644 --- a/lib/diffo/provider/components/instance/extension/action_helper.ex +++ b/lib/diffo/provider/components/instance/extension/action_helper.ex @@ -25,14 +25,16 @@ defmodule Diffo.Provider.Instance.ActionHelper do @doc """ build after_action helper, relates TMF entities to the new instance """ - def build_after(changeset, result, module, function) do - with {:ok, result} <- Specification.relate_instance(result, changeset), - {:ok, result} <- Relationship.relate_instance(result, changeset), - {:ok, result} <- Feature.relate_instance(result, changeset), - {:ok, result} <- Characteristic.relate_instance(result, changeset), - {:ok, result} <- Place.relate_instance(result, changeset), - {:ok, result} <- Party.relate_instance(result, changeset), - {:ok, result} <- apply(module, function, [result.id]), - do: {:ok, result} + def build_after(changeset, result) do + specified_by = Ash.Changeset.get_argument(changeset, :specified_by) + result = %{result | specification_id: specified_by} + + with {:ok, _} <- Specification.relate_instance(result, changeset), + {:ok, _} <- Relationship.relate_instance(result, changeset), + {:ok, _} <- Feature.relate_instance(result, changeset), + {:ok, _} <- Characteristic.relate_instance(result, changeset), + {:ok, _} <- Place.relate_instance(result, changeset), + {:ok, _} <- Party.relate_instance(result, changeset), + do: Ash.load(result, [:specification, :characteristics, :features, :parties]) end end diff --git a/lib/diffo/provider/components/instance/extension/characteristic.ex b/lib/diffo/provider/components/instance/extension/characteristic.ex index 43c8d1b..70eec14 100644 --- a/lib/diffo/provider/components/instance/extension/characteristic.ex +++ b/lib/diffo/provider/components/instance/extension/characteristic.ex @@ -73,8 +73,7 @@ defmodule Diffo.Provider.Instance.Characteristic do def relate_instance(result, changeset) when is_struct(result) and is_struct(changeset, Ash.Changeset) do characteristics = Ash.Changeset.get_argument(changeset, :characteristics) - instance = struct(Instance, Map.from_struct(result)) - Provider.relate_instance_characteristics(instance, %{characteristics: characteristics}) + Provider.relate_instance_characteristics(%Instance{id: result.id}, %{characteristics: characteristics}) end @doc """ diff --git a/lib/diffo/provider/components/instance/extension/feature.ex b/lib/diffo/provider/components/instance/extension/feature.ex index 9428419..6b1b322 100644 --- a/lib/diffo/provider/components/instance/extension/feature.ex +++ b/lib/diffo/provider/components/instance/extension/feature.ex @@ -101,8 +101,7 @@ defmodule Diffo.Provider.Instance.Feature do def relate_instance(result, changeset) when is_struct(result) and is_struct(changeset, Ash.Changeset) do features = Ash.Changeset.get_argument(changeset, :features) - instance = struct(Instance, Map.from_struct(result)) - Provider.relate_instance_features(instance, %{features: features}) + Provider.relate_instance_features(%Instance{id: result.id}, %{features: features}) end defimpl String.Chars do diff --git a/lib/diffo/provider/components/instance/extension/specification.ex b/lib/diffo/provider/components/instance/extension/specification.ex index 8f5720a..e44c91b 100644 --- a/lib/diffo/provider/components/instance/extension/specification.ex +++ b/lib/diffo/provider/components/instance/extension/specification.ex @@ -52,18 +52,7 @@ defmodule Diffo.Provider.Instance.Specification do def relate_instance(result, changeset) when is_struct(result) and is_struct(changeset, Ash.Changeset) do specified_by = Ash.Changeset.get_argument(changeset, :specified_by) - instance = struct(Instance, Map.from_struct(result)) - - case Provider.specify_instance(instance, %{specified_by: specified_by}) do - {:ok, specification} -> - {:ok, - result - |> Map.put(:specification, specification) - |> Map.put(:specification_id, specified_by)} - - {:error, error} -> - {:error, error} - end + Provider.specify_instance(%Instance{id: result.id}, %{specified_by: specified_by}) end defimpl String.Chars do diff --git a/test/support/resource/card.ex b/test/support/resource/card.ex index 4056a1b..1dfe4ef 100644 --- a/test/support/resource/card.ex +++ b/test/support/resource/card.ex @@ -58,7 +58,7 @@ defmodule Diffo.Test.Card do end) change after_action(fn changeset, result, _context -> - ActionHelper.build_after(changeset, result, Servo, :get_card_by_id) + ActionHelper.build_after(changeset, result) end) change load [:href] diff --git a/test/support/resource/invalid/invalid_characteristic.ex b/test/support/resource/invalid/invalid_characteristic.ex index 52b0ef6..70aa258 100644 --- a/test/support/resource/invalid/invalid_characteristic.ex +++ b/test/support/resource/invalid/invalid_characteristic.ex @@ -12,11 +12,9 @@ defmodule Diffo.Test.InvalidCharacteristic do alias Diffo.Provider.BaseInstance alias Diffo.Provider.Instance.ActionHelper - alias Diffo.Test.Servo - use Ash.Resource, fragments: [BaseInstance], - domain: Servo + domain: Diffo.Test.Servo resource do description "Ash Resource with an invalid characteristic" @@ -51,12 +49,7 @@ defmodule Diffo.Test.InvalidCharacteristic do end) change after_action(fn changeset, result, _context -> - ActionHelper.build_after( - changeset, - result, - Servo, - :get_invalid_characteristic_by_id - ) + ActionHelper.build_after(changeset, result) end) change load [:href] diff --git a/test/support/resource/invalid/invalid_feature_characteristic.ex b/test/support/resource/invalid/invalid_feature_characteristic.ex index 2d8f791..61066b6 100644 --- a/test/support/resource/invalid/invalid_feature_characteristic.ex +++ b/test/support/resource/invalid/invalid_feature_characteristic.ex @@ -12,11 +12,9 @@ defmodule Diffo.Test.InvalidFeatureCharacteristic do alias Diffo.Provider.BaseInstance alias Diffo.Provider.Instance.ActionHelper - alias Diffo.Test.Servo - use Ash.Resource, fragments: [BaseInstance], - domain: Servo + domain: Diffo.Test.Servo resource do description "Ash Resource with an invalid feature characteristic" @@ -54,12 +52,7 @@ defmodule Diffo.Test.InvalidFeatureCharacteristic do end) change after_action(fn changeset, result, _context -> - ActionHelper.build_after( - changeset, - result, - Servo, - :get_invalid_feature_characteristic_by_id - ) + ActionHelper.build_after(changeset, result) end) change load [:href] diff --git a/test/support/resource/invalid/invalid_specification.ex b/test/support/resource/invalid/invalid_specification.ex index 4591912..1da60c3 100644 --- a/test/support/resource/invalid/invalid_specification.ex +++ b/test/support/resource/invalid/invalid_specification.ex @@ -12,11 +12,9 @@ defmodule Diffo.Test.InvalidSpecification do alias Diffo.Provider.BaseInstance alias Diffo.Provider.Instance.ActionHelper - alias Diffo.Test.Servo - use Ash.Resource, fragments: [BaseInstance], - domain: Servo + domain: Diffo.Test.Servo resource do description "Ash Resource with an invalid specification" @@ -47,12 +45,7 @@ defmodule Diffo.Test.InvalidSpecification do end) change after_action(fn changeset, result, _context -> - ActionHelper.build_after( - changeset, - result, - Servo, - :get_invalid_specification_by_id - ) + ActionHelper.build_after(changeset, result) end) change load [:href] diff --git a/test/support/resource/shelf.ex b/test/support/resource/shelf.ex index 71350ce..31b8fa3 100644 --- a/test/support/resource/shelf.ex +++ b/test/support/resource/shelf.ex @@ -78,7 +78,7 @@ defmodule Diffo.Test.Shelf do end) change after_action(fn changeset, result, _context -> - ActionHelper.build_after(changeset, result, Servo, :get_shelf_by_id) + ActionHelper.build_after(changeset, result) end) change load [:href] From dc26701e747b81da3cd154ee141b48db5b9f2b1d Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Tue, 28 Apr 2026 10:02:05 +0930 Subject: [PATCH 05/22] =?UTF-8?q?Instance=20Extension=20transformers=20?= =?UTF-8?q?=E2=80=94=20bake=20DSL=20data=20at=20compile=20time?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five Spark.Dsl.Transformer modules bake structural DSL data into zero-cost module functions (__diffo_specification__/0, __diffo_characteristics__/0, __diffo_features__/0, __diffo_party_declarations__/0) and a behavioural transformer (TransformBuildActions) composes them into __diffo_build_before__/1 and __diffo_build_after__/2. 2-arity variants added to Specification, Feature, Characteristic, and Party helpers accept pre-baked declaration lists instead of doing runtime Info introspection, so the generated build hooks pay no Info lookup cost at runtime. Transformer ordering is enforced via after?/1 so TransformBuildActions always runs after all four structural transformers. --- .../provider/components/instance/extension.ex | 7 + .../instance/extension/characteristic.ex | 21 ++- .../components/instance/extension/feature.ex | 21 ++- .../components/instance/extension/party.ex | 3 + .../instance/extension/specification.ex | 13 ++ .../transformers/transform_build_actions.ex | 35 +++++ .../transformers/transform_characteristics.ex | 20 +++ .../transformers/transform_features.ex | 20 +++ .../transformers/transform_parties.ex | 20 +++ .../transformers/transform_specification.ex | 28 ++++ test/instance_extension/transformer_test.exs | 139 ++++++++++++++++++ 11 files changed, 323 insertions(+), 4 deletions(-) create mode 100644 lib/diffo/provider/components/instance/extension/transformers/transform_build_actions.ex create mode 100644 lib/diffo/provider/components/instance/extension/transformers/transform_characteristics.ex create mode 100644 lib/diffo/provider/components/instance/extension/transformers/transform_features.ex create mode 100644 lib/diffo/provider/components/instance/extension/transformers/transform_parties.ex create mode 100644 lib/diffo/provider/components/instance/extension/transformers/transform_specification.ex create mode 100644 test/instance_extension/transformer_test.exs diff --git a/lib/diffo/provider/components/instance/extension.ex b/lib/diffo/provider/components/instance/extension.ex index 94409e6..f147c1f 100644 --- a/lib/diffo/provider/components/instance/extension.ex +++ b/lib/diffo/provider/components/instance/extension.ex @@ -224,6 +224,13 @@ defmodule Diffo.Provider.Instance.Extension do use Spark.Dsl.Extension, sections: [@specification, @features, @characteristics, @parties], + transformers: [ + Diffo.Provider.Instance.Extension.Transformers.TransformSpecification, + Diffo.Provider.Instance.Extension.Transformers.TransformCharacteristics, + Diffo.Provider.Instance.Extension.Transformers.TransformFeatures, + Diffo.Provider.Instance.Extension.Transformers.TransformParties, + Diffo.Provider.Instance.Extension.Transformers.TransformBuildActions + ], verifiers: [ Diffo.Provider.Instance.Extension.Verifiers.VerifySpecification, Diffo.Provider.Instance.Extension.Verifiers.VerifyCharacteristics, diff --git a/lib/diffo/provider/components/instance/extension/characteristic.ex b/lib/diffo/provider/components/instance/extension/characteristic.ex index 70eec14..17e308d 100644 --- a/lib/diffo/provider/components/instance/extension/characteristic.ex +++ b/lib/diffo/provider/components/instance/extension/characteristic.ex @@ -35,13 +35,30 @@ defmodule Diffo.Provider.Instance.Characteristic do end end + 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 + [] -> + changeset + + {:error, error} -> + Ash.Changeset.add_error(changeset, error) + + _ -> + characteristic_ids = Enum.map(characteristics, &Map.get(&1, :id)) + Ash.Changeset.force_set_argument(changeset, :characteristics, characteristic_ids) + end + end + @doc """ Creates the Characteristics from a Extended Instance's module """ def create_characteristics(module, type) when is_atom(module) and is_atom(type) do - characteristics = Info.characteristics(module) + Info.characteristics(module) |> create_characteristics_from_declarations(type) + end - Enum.reduce_while(characteristics, [], fn %{name: name, value_type: value_type}, acc -> + defp create_characteristics_from_declarations(declarations, type) do + Enum.reduce_while(declarations, [], fn %{name: name, value_type: value_type}, acc -> try do attrs = case value_type do diff --git a/lib/diffo/provider/components/instance/extension/feature.ex b/lib/diffo/provider/components/instance/extension/feature.ex index 6b1b322..5d03b8a 100644 --- a/lib/diffo/provider/components/instance/extension/feature.ex +++ b/lib/diffo/provider/components/instance/extension/feature.ex @@ -35,14 +35,31 @@ defmodule Diffo.Provider.Instance.Feature do end end + def set_features_argument(changeset, declarations) + when is_struct(changeset, Ash.Changeset) and is_list(declarations) do + case features = create_features_from_declarations(declarations) do + [] -> + changeset + + {:error, error} -> + Ash.Changeset.add_error(changeset, error) + + _ -> + feature_ids = Enum.map(features, &Map.get(&1, :id)) + Ash.Changeset.force_set_argument(changeset, :features, feature_ids) + end + end + @doc """ Creates the Features from a Extended Instance's module """ def create_features(module) when is_atom(module) do - features = Info.features(module) + Info.features(module) |> create_features_from_declarations() + end + defp create_features_from_declarations(declarations) do Enum.reduce_while( - features, + declarations, [], # create any feature characteristics fn %{name: name, is_enabled?: isEnabled, characteristics: characteristics}, acc -> diff --git a/lib/diffo/provider/components/instance/extension/party.ex b/lib/diffo/provider/components/instance/extension/party.ex index 9505d51..b0bc1bc 100644 --- a/lib/diffo/provider/components/instance/extension/party.ex +++ b/lib/diffo/provider/components/instance/extension/party.ex @@ -15,7 +15,10 @@ defmodule Diffo.Provider.Instance.Party do @doc false def validate_parties(changeset) do declarations = InstanceInfo.parties(changeset.resource) + validate_parties(changeset, declarations) + end + def validate_parties(changeset, declarations) do if declarations == [] do changeset else diff --git a/lib/diffo/provider/components/instance/extension/specification.ex b/lib/diffo/provider/components/instance/extension/specification.ex index e44c91b..82c190a 100644 --- a/lib/diffo/provider/components/instance/extension/specification.ex +++ b/lib/diffo/provider/components/instance/extension/specification.ex @@ -30,6 +30,19 @@ defmodule Diffo.Provider.Instance.Specification do end end + def set_specified_by_argument(changeset, options) + when is_struct(changeset, Ash.Changeset) and is_list(options) do + specification = struct(__MODULE__, options) + + case Provider.create_specification(Map.from_struct(specification)) do + {:ok, _} -> + Ash.Changeset.force_set_argument(changeset, :specified_by, specification.id) + + {:error, error} -> + Ash.Changeset.add_error(changeset, error) + end + end + @doc """ Upserts the Specification from a Extended Instance's module """ diff --git a/lib/diffo/provider/components/instance/extension/transformers/transform_build_actions.ex b/lib/diffo/provider/components/instance/extension/transformers/transform_build_actions.ex new file mode 100644 index 0000000..357b305 --- /dev/null +++ b/lib/diffo/provider/components/instance/extension/transformers/transform_build_actions.ex @@ -0,0 +1,35 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Instance.Extension.Transformers.TransformBuildActions do + @moduledoc "Generates __diffo_build_before__/1 and __diffo_build_after__/2 from baked structural data" + use Spark.Dsl.Transformer + alias Spark.Dsl.Transformer + + @impl true + def transform(dsl_state) do + {:ok, Transformer.eval(dsl_state, [], quote do + @doc false + def __diffo_build_before__(changeset) do + changeset + |> Diffo.Provider.Instance.Specification.set_specified_by_argument(__diffo_specification__()) + |> Diffo.Provider.Instance.Feature.set_features_argument(__diffo_features__()) + |> Diffo.Provider.Instance.Characteristic.set_characteristics_argument(__diffo_characteristics__()) + |> Diffo.Provider.Instance.Party.validate_parties(__diffo_party_declarations__()) + end + + @doc false + def __diffo_build_after__(changeset, result) do + Diffo.Provider.Instance.ActionHelper.build_after(changeset, result) + end + end)} + end + + @impl true + def after?(Diffo.Provider.Instance.Extension.Transformers.TransformSpecification), do: true + def after?(Diffo.Provider.Instance.Extension.Transformers.TransformCharacteristics), do: true + def after?(Diffo.Provider.Instance.Extension.Transformers.TransformFeatures), do: true + def after?(Diffo.Provider.Instance.Extension.Transformers.TransformParties), do: true + def after?(_), do: false +end diff --git a/lib/diffo/provider/components/instance/extension/transformers/transform_characteristics.ex b/lib/diffo/provider/components/instance/extension/transformers/transform_characteristics.ex new file mode 100644 index 0000000..ea16d84 --- /dev/null +++ b/lib/diffo/provider/components/instance/extension/transformers/transform_characteristics.ex @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Instance.Extension.Transformers.TransformCharacteristics do + @moduledoc "Bakes characteristic declarations into __diffo_characteristics__/0" + use Spark.Dsl.Transformer + alias Spark.Dsl.Transformer + + @impl true + def transform(dsl_state) do + characteristics = Transformer.get_entities(dsl_state, [:characteristics]) + escaped = Macro.escape(characteristics) + + {:ok, Transformer.eval(dsl_state, [], quote do + @doc false + def __diffo_characteristics__, do: unquote(escaped) + end)} + end +end diff --git a/lib/diffo/provider/components/instance/extension/transformers/transform_features.ex b/lib/diffo/provider/components/instance/extension/transformers/transform_features.ex new file mode 100644 index 0000000..79266af --- /dev/null +++ b/lib/diffo/provider/components/instance/extension/transformers/transform_features.ex @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Instance.Extension.Transformers.TransformFeatures do + @moduledoc "Bakes feature declarations into __diffo_features__/0" + use Spark.Dsl.Transformer + alias Spark.Dsl.Transformer + + @impl true + def transform(dsl_state) do + features = Transformer.get_entities(dsl_state, [:features]) + escaped = Macro.escape(features) + + {:ok, Transformer.eval(dsl_state, [], quote do + @doc false + def __diffo_features__, do: unquote(escaped) + end)} + end +end diff --git a/lib/diffo/provider/components/instance/extension/transformers/transform_parties.ex b/lib/diffo/provider/components/instance/extension/transformers/transform_parties.ex new file mode 100644 index 0000000..af2156a --- /dev/null +++ b/lib/diffo/provider/components/instance/extension/transformers/transform_parties.ex @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Instance.Extension.Transformers.TransformParties do + @moduledoc "Bakes party declarations into __diffo_party_declarations__/0" + use Spark.Dsl.Transformer + alias Spark.Dsl.Transformer + + @impl true + def transform(dsl_state) do + parties = Transformer.get_entities(dsl_state, [:parties]) + escaped = Macro.escape(parties) + + {:ok, Transformer.eval(dsl_state, [], quote do + @doc false + def __diffo_party_declarations__, do: unquote(escaped) + end)} + end +end diff --git a/lib/diffo/provider/components/instance/extension/transformers/transform_specification.ex b/lib/diffo/provider/components/instance/extension/transformers/transform_specification.ex new file mode 100644 index 0000000..a5896f6 --- /dev/null +++ b/lib/diffo/provider/components/instance/extension/transformers/transform_specification.ex @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Instance.Extension.Transformers.TransformSpecification do + @moduledoc "Bakes specification DSL options into __diffo_specification__/0" + use Spark.Dsl.Transformer + alias Spark.Dsl.Transformer + + @impl true + def transform(dsl_state) do + spec = [ + id: Transformer.get_option(dsl_state, [:specification], :id), + name: Transformer.get_option(dsl_state, [:specification], :name), + type: Transformer.get_option(dsl_state, [:specification], :type), + major_version: Transformer.get_option(dsl_state, [:specification], :major_version), + description: Transformer.get_option(dsl_state, [:specification], :description), + category: Transformer.get_option(dsl_state, [:specification], :category) + ] + + escaped = Macro.escape(spec) + + {:ok, Transformer.eval(dsl_state, [], quote do + @doc false + def __diffo_specification__, do: unquote(escaped) + end)} + end +end diff --git a/test/instance_extension/transformer_test.exs b/test/instance_extension/transformer_test.exs new file mode 100644 index 0000000..d239496 --- /dev/null +++ b/test/instance_extension/transformer_test.exs @@ -0,0 +1,139 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.InstanceExtension.TransformerTest do + @moduledoc false + use ExUnit.Case, async: true + + alias Diffo.Test.Shelf + alias Diffo.Test.Card + alias Diffo.Provider.Instance.Characteristic + alias Diffo.Provider.Instance.Feature + + describe "TransformSpecification" do + test "bakes specification options into __diffo_specification__/0" do + spec = Shelf.__diffo_specification__() + assert spec[:id] == "ef016d85-9dbd-429c-84da-1df56cc7dda5" + assert spec[:name] == "shelf" + assert spec[:type] == :resourceSpecification + assert spec[:description] == "A Shelf Resource Instance which contain cards" + assert spec[:category] == "Network Resource" + end + + test "card specification is also baked correctly" do + spec = Card.__diffo_specification__() + assert spec[:id] == "cd29956f-6c68-44cc-bf54-705eb8d2f754" + assert spec[:name] == "card" + assert spec[:type] == :resourceSpecification + end + end + + describe "TransformCharacteristics" do + test "bakes characteristic declarations into __diffo_characteristics__/0" do + chars = Shelf.__diffo_characteristics__() + assert is_list(chars) + assert length(chars) == 3 + names = Enum.map(chars, & &1.name) + assert :shelf in names + assert :slots in names + assert :shelves in names + end + + test "each characteristic is a Characteristic struct" do + [first | _] = Shelf.__diffo_characteristics__() + assert is_struct(first, Characteristic) + end + + test "card characteristics are baked" do + chars = Card.__diffo_characteristics__() + assert length(chars) == 2 + names = Enum.map(chars, & &1.name) + assert :card in names + assert :ports in names + end + end + + describe "TransformFeatures" do + test "bakes feature declarations into __diffo_features__/0" do + features = Shelf.__diffo_features__() + assert is_list(features) + assert length(features) == 1 + [feature] = features + assert feature.name == :spectralManagement + assert feature.is_enabled? == true + end + + test "each feature is a Feature struct" do + [feature] = Shelf.__diffo_features__() + assert is_struct(feature, Feature) + end + + test "feature characteristics are nested in the feature declaration" do + [feature] = Shelf.__diffo_features__() + assert length(feature.characteristics) == 2 + char_names = Enum.map(feature.characteristics, & &1.name) + assert :deploymentClass in char_names + assert :deploymentClasses in char_names + end + + test "card has no features" do + assert Card.__diffo_features__() == [] + end + end + + describe "TransformParties" do + test "bakes party declarations into __diffo_party_declarations__/0" do + parties = Shelf.__diffo_party_declarations__() + assert is_list(parties) + assert length(parties) == 5 + roles = Enum.map(parties, & &1.role) + assert :facilitator in roles + assert :overseer in roles + assert :provider in roles + assert :manager in roles + assert :installer in roles + end + + test "reference party has reference flag set" do + parties = Shelf.__diffo_party_declarations__() + provider = Enum.find(parties, &(&1.role == :provider)) + assert provider.reference == true + end + + test "calculate party has calculate set" do + parties = Shelf.__diffo_party_declarations__() + manager = Enum.find(parties, &(&1.role == :manager)) + assert manager.calculate == :manager_calc + end + + test "plural party has constraints" do + parties = Shelf.__diffo_party_declarations__() + installer = Enum.find(parties, &(&1.role == :installer)) + assert installer.multiple == true + assert installer.constraints == [min: 1, max: 3] + end + + test "card has no parties" do + assert Card.__diffo_party_declarations__() == [] + end + end + + describe "TransformBuildActions" do + test "__diffo_build_before__/1 is defined on shelf" do + assert function_exported?(Shelf, :__diffo_build_before__, 1) + end + + test "__diffo_build_after__/2 is defined on shelf" do + assert function_exported?(Shelf, :__diffo_build_after__, 2) + end + + test "__diffo_build_before__/1 is defined on card" do + assert function_exported?(Card, :__diffo_build_before__, 1) + end + + test "__diffo_build_after__/2 is defined on card" do + assert function_exported?(Card, :__diffo_build_after__, 2) + end + end +end From def1915ed1a156ae021381f37f16f99580ba6008 Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Tue, 28 Apr 2026 22:31:09 +0930 Subject: [PATCH 06/22] =?UTF-8?q?Instance=20Extension=20=E2=80=94=20struct?= =?UTF-8?q?ure/behaviour=20DSL,=20persisters,=20transformers,=20verifiers,?= =?UTF-8?q?=20and=20Info=20lookups?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restructures the extension into structure do (specification, characteristics, features, parties) and behaviour do (actions), bakes all DSL data at compile time via persisters, generates build_before/1/build_after/2 and single-item lookup functions (characteristic/1, feature/1, feature_characteristic/2, party/1) via TransformBehaviour, adds uniqueness and cross-reference verifiers for all DSL sections, exposes matching lookups on Diffo.Provider.Instance.Info, and removes superseded 1-arity helpers and old transformer files. --- .../provider/components/base_instance.ex | 5 + .../provider/components/instance/extension.ex | 157 ++++++++---- .../instance/extension/action_create.ex | 8 + .../instance/extension/action_helper.ex | 15 +- .../instance/extension/action_update.ex | 8 + .../instance/extension/changes/build_after.ex | 15 ++ .../extension/changes/build_before.ex | 15 ++ .../instance/extension/characteristic.ex | 24 -- .../components/instance/extension/feature.ex | 24 -- .../components/instance/extension/info.ex | 2 +- .../components/instance/extension/party.ex | 6 - .../persisters/persist_characteristics.ex | 21 ++ .../extension/persisters/persist_features.ex | 21 ++ .../extension/persisters/persist_parties.ex | 21 ++ .../persisters/persist_specification.ex | 29 +++ .../instance/extension/specification.ex | 29 --- .../transformers/transform_behaviour.ex | 104 ++++++++ .../transformers/transform_build_actions.ex | 35 --- .../transformers/transform_characteristics.ex | 20 -- .../transformers/transform_features.ex | 20 -- .../transformers/transform_parties.ex | 20 -- .../transformers/transform_specification.ex | 28 --- .../extension/verifiers/verify_behaviour.ex | 53 ++++ .../verifiers/verify_characteristics.ex | 22 +- .../extension/verifiers/verify_features.ex | 45 +++- .../extension/verifiers/verify_parties.ex | 6 +- .../verifiers/verify_specification.ex | 4 +- .../provider/components/instance/info.ex | 60 +++++ test/instance_extension/party_test.exs | 16 +- test/instance_extension/transformer_test.exs | 192 ++++++++++---- test/instance_extension/verifier_test.exs | 234 +++++++++++++++--- test/support/resource/card.ex | 39 ++- .../invalid/invalid_characteristic.ex | 35 +-- .../invalid/invalid_feature_characteristic.ex | 39 +-- .../resource/invalid/invalid_specification.ex | 29 +-- test/support/resource/shelf.ex | 67 +++-- 36 files changed, 963 insertions(+), 505 deletions(-) create mode 100644 lib/diffo/provider/components/instance/extension/action_create.ex create mode 100644 lib/diffo/provider/components/instance/extension/action_update.ex create mode 100644 lib/diffo/provider/components/instance/extension/changes/build_after.ex create mode 100644 lib/diffo/provider/components/instance/extension/changes/build_before.ex create mode 100644 lib/diffo/provider/components/instance/extension/persisters/persist_characteristics.ex create mode 100644 lib/diffo/provider/components/instance/extension/persisters/persist_features.ex create mode 100644 lib/diffo/provider/components/instance/extension/persisters/persist_parties.ex create mode 100644 lib/diffo/provider/components/instance/extension/persisters/persist_specification.ex create mode 100644 lib/diffo/provider/components/instance/extension/transformers/transform_behaviour.ex delete mode 100644 lib/diffo/provider/components/instance/extension/transformers/transform_build_actions.ex delete mode 100644 lib/diffo/provider/components/instance/extension/transformers/transform_characteristics.ex delete mode 100644 lib/diffo/provider/components/instance/extension/transformers/transform_features.ex delete mode 100644 lib/diffo/provider/components/instance/extension/transformers/transform_parties.ex delete mode 100644 lib/diffo/provider/components/instance/extension/transformers/transform_specification.ex create mode 100644 lib/diffo/provider/components/instance/extension/verifiers/verify_behaviour.ex create mode 100644 lib/diffo/provider/components/instance/info.ex diff --git a/lib/diffo/provider/components/base_instance.ex b/lib/diffo/provider/components/base_instance.ex index 16cf244..70e5d0b 100644 --- a/lib/diffo/provider/components/base_instance.ex +++ b/lib/diffo/provider/components/base_instance.ex @@ -357,6 +357,11 @@ defmodule Diffo.Provider.BaseInstance do end end + changes do + change Diffo.Provider.Instance.Extension.Changes.BuildBefore, on: [:create] + change Diffo.Provider.Instance.Extension.Changes.BuildAfter, on: [:create] + end + actions do defaults [:destroy] diff --git a/lib/diffo/provider/components/instance/extension.ex b/lib/diffo/provider/components/instance/extension.ex index f147c1f..e80005a 100644 --- a/lib/diffo/provider/components/instance/extension.ex +++ b/lib/diffo/provider/components/instance/extension.ex @@ -13,6 +13,9 @@ defmodule Diffo.Provider.Instance.Extension do See the [DSL cheat sheet](DSL-Diffo.Provider.Instance.Extension.html) for the full DSL reference. See `Diffo.Provider.BaseInstance` for full usage documentation. """ + + # ── structure ────────────────────────────────────────────────────────────── + @specification %Spark.Dsl.Section{ name: :specification, describe: "Defines the Instance Specification", @@ -31,43 +34,31 @@ defmodule Diffo.Provider.Instance.Extension do schema: [ id: [ type: :string, - doc: """ - The id of the specification, a uuid4 the same in all environments, unique for name and major_version. - """, + doc: "The id of the specification, a uuid4 the same in all environments, unique for name and major_version.", required: true ], name: [ type: :string, - doc: """ - The name of the specification, unique to a service but common for all versions. - """, + doc: "The name of the specification, unique to a service but common for all versions.", required: true ], type: [ type: :atom, - doc: """ - The type of the specification. - """, + doc: "The type of the specification.", default: :serviceSpecification ], major_version: [ type: :integer, - doc: """ - The major_version of the specification. - """, + doc: "The major_version of the specification.", default: 1 ], description: [ type: :string, - doc: """ - A generic description of the specified service or resource. - """ + doc: "A generic description of the specified service or resource." ], category: [ type: :string, - doc: """ - The category the specified service or resource belongs to. - """ + doc: "The category the specified service or resource belongs to." ] ] } @@ -79,17 +70,12 @@ defmodule Diffo.Provider.Instance.Extension do args: [:name, :value_type], schema: [ name: [ - doc: """ - The name of the characteristic, an atom - """, + doc: "The name of the characteristic, an atom", type: :atom, required: true ], value_type: [ - doc: """ - The type of the characteristic's value. An atom module name such as an Ash.TypedStruct for a scalar value, - or `{:array, module}` for an array of values of that type. - """, + doc: "The type of the characteristic's value. An atom module name such as an Ash.TypedStruct for a scalar value, or `{:array, module}` for an array of values of that type.", type: :any ] ] @@ -108,9 +94,7 @@ defmodule Diffo.Provider.Instance.Extension do end """ ], - entities: [ - @characteristic - ] + entities: [@characteristic] } @feature %Spark.Dsl.Entity{ @@ -120,16 +104,12 @@ defmodule Diffo.Provider.Instance.Extension do args: [:name], schema: [ name: [ - doc: """ - The name of the feature, an atom - """, + doc: "The name of the feature, an atom", type: :atom, required: true ], is_enabled?: [ - doc: """ - Whether the feature is enabled by default, defaults true - """, + doc: "Whether the feature is enabled by default, defaults true", type: :boolean ] ], @@ -153,9 +133,7 @@ defmodule Diffo.Provider.Instance.Extension do end """ ], - entities: [ - @feature - ] + entities: [@feature] } @party_schema [ @@ -216,25 +194,108 @@ defmodule Diffo.Provider.Instance.Extension do end """ ], - entities: [ - @party_entity, - @parties_entity + entities: [@party_entity, @parties_entity] + } + + @structure %Spark.Dsl.Section{ + name: :structure, + describe: "Defines the structural shape of the Instance — its specification, characteristics, features, and parties", + examples: [ + """ + structure do + specification do + id "da9b207a-26c3-451d-8abd-0640c6349979" + name "DSL Access Service" + type :serviceSpecification + end + + characteristics do + characteristic :circuit, Diffo.Access.Circuit + end + + parties do + party :provider, MyApp.Provider + end + end + """ + ], + sections: [@specification, @characteristics, @features, @parties] + } + + # ── behaviour ────────────────────────────────────────────────────────────── + + @action_create %Spark.Dsl.Entity{ + name: :create, + describe: "Marks a create action for instance build wiring, injecting :specified_by, :features, and :characteristics arguments", + target: Diffo.Provider.Instance.Extension.ActionCreate, + args: [:name], + schema: [ + name: [ + type: :atom, + required: true, + doc: "The name of the create action to wire" + ] ] } + @action_update %Spark.Dsl.Entity{ + name: :update, + describe: "Marks an update action for instance behaviour wiring", + target: Diffo.Provider.Instance.Extension.ActionUpdate, + args: [:name], + schema: [ + name: [ + type: :atom, + required: true, + doc: "The name of the update action to wire" + ] + ] + } + + @behaviour_actions %Spark.Dsl.Section{ + name: :actions, + describe: "Declares which actions to wire for instance behaviour", + examples: [ + """ + actions do + create :build + update :define + end + """ + ], + entities: [@action_create, @action_update] + } + + @behaviour_section %Spark.Dsl.Section{ + name: :behaviour, + describe: "Defines the behavioural wiring for the Instance — actions, and in future triggers and tasks", + examples: [ + """ + behaviour do + actions do + create :build + update :define + end + end + """ + ], + sections: [@behaviour_actions] + } + use Spark.Dsl.Extension, - sections: [@specification, @features, @characteristics, @parties], - transformers: [ - Diffo.Provider.Instance.Extension.Transformers.TransformSpecification, - Diffo.Provider.Instance.Extension.Transformers.TransformCharacteristics, - Diffo.Provider.Instance.Extension.Transformers.TransformFeatures, - Diffo.Provider.Instance.Extension.Transformers.TransformParties, - Diffo.Provider.Instance.Extension.Transformers.TransformBuildActions + sections: [@structure, @behaviour_section], + persisters: [ + Diffo.Provider.Instance.Extension.Persisters.PersistSpecification, + Diffo.Provider.Instance.Extension.Persisters.PersistCharacteristics, + Diffo.Provider.Instance.Extension.Persisters.PersistFeatures, + Diffo.Provider.Instance.Extension.Persisters.PersistParties, + Diffo.Provider.Instance.Extension.Transformers.TransformBehaviour ], verifiers: [ Diffo.Provider.Instance.Extension.Verifiers.VerifySpecification, Diffo.Provider.Instance.Extension.Verifiers.VerifyCharacteristics, Diffo.Provider.Instance.Extension.Verifiers.VerifyFeatures, - Diffo.Provider.Instance.Extension.Verifiers.VerifyParties + Diffo.Provider.Instance.Extension.Verifiers.VerifyParties, + Diffo.Provider.Instance.Extension.Verifiers.VerifyBehaviour ] end diff --git a/lib/diffo/provider/components/instance/extension/action_create.ex b/lib/diffo/provider/components/instance/extension/action_create.ex new file mode 100644 index 0000000..db5e0c7 --- /dev/null +++ b/lib/diffo/provider/components/instance/extension/action_create.ex @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Instance.Extension.ActionCreate do + @moduledoc false + defstruct [:name, __spark_metadata__: nil] +end diff --git a/lib/diffo/provider/components/instance/extension/action_helper.ex b/lib/diffo/provider/components/instance/extension/action_helper.ex index 9f9820d..1d814d4 100644 --- a/lib/diffo/provider/components/instance/extension/action_helper.ex +++ b/lib/diffo/provider/components/instance/extension/action_helper.ex @@ -11,20 +11,7 @@ defmodule Diffo.Provider.Instance.ActionHelper do alias Diffo.Provider.Instance.Place alias Diffo.Provider.Instance.Party - @doc """ - build before_action helper, injects instance dsl configuration into the changeset - """ - def build_before(changeset) do - changeset - |> Specification.set_specified_by_argument() - |> Feature.set_features_argument() - |> Characteristic.set_characteristics_argument() - |> Party.validate_parties() - end - - @doc """ - build after_action helper, relates TMF entities to the new instance - """ + @doc false def build_after(changeset, result) do specified_by = Ash.Changeset.get_argument(changeset, :specified_by) result = %{result | specification_id: specified_by} diff --git a/lib/diffo/provider/components/instance/extension/action_update.ex b/lib/diffo/provider/components/instance/extension/action_update.ex new file mode 100644 index 0000000..2678c43 --- /dev/null +++ b/lib/diffo/provider/components/instance/extension/action_update.ex @@ -0,0 +1,8 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Instance.Extension.ActionUpdate do + @moduledoc false + defstruct [:name, __spark_metadata__: nil] +end diff --git a/lib/diffo/provider/components/instance/extension/changes/build_after.ex b/lib/diffo/provider/components/instance/extension/changes/build_after.ex new file mode 100644 index 0000000..2f9e586 --- /dev/null +++ b/lib/diffo/provider/components/instance/extension/changes/build_after.ex @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Instance.Extension.Changes.BuildAfter do + @moduledoc false + use Ash.Resource.Change + + @impl true + def change(changeset, _opts, _context) do + Ash.Changeset.after_action(changeset, fn changeset, result -> + changeset.resource.build_after(changeset, result) + end) + end +end diff --git a/lib/diffo/provider/components/instance/extension/changes/build_before.ex b/lib/diffo/provider/components/instance/extension/changes/build_before.ex new file mode 100644 index 0000000..0797d81 --- /dev/null +++ b/lib/diffo/provider/components/instance/extension/changes/build_before.ex @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Instance.Extension.Changes.BuildBefore do + @moduledoc false + use Ash.Resource.Change + + @impl true + def change(changeset, _opts, _context) do + Ash.Changeset.before_action(changeset, fn changeset -> + changeset.resource.build_before(changeset) + end) + end +end diff --git a/lib/diffo/provider/components/instance/extension/characteristic.ex b/lib/diffo/provider/components/instance/extension/characteristic.ex index 17e308d..92226d3 100644 --- a/lib/diffo/provider/components/instance/extension/characteristic.ex +++ b/lib/diffo/provider/components/instance/extension/characteristic.ex @@ -8,7 +8,6 @@ defmodule Diffo.Provider.Instance.Characteristic do alias Diffo.Provider alias Diffo.Provider.Instance - alias Diffo.Provider.Instance.Extension.Info alias Diffo.Type.Value @doc """ @@ -19,22 +18,6 @@ defmodule Diffo.Provider.Instance.Characteristic do @doc """ Sets the Extended Instances characteristics argument in the changeset, creating the characteristics """ - def set_characteristics_argument(changeset) when is_struct(changeset, Ash.Changeset) do - %module{} = changeset.data - - case characteristics = create_characteristics(module, :instance) do - [] -> - changeset - - {:error, error} -> - Ash.Changeset.add_error(changeset, error) - - _ -> - characteristic_ids = Enum.map(characteristics, &Map.get(&1, :id)) - Ash.Changeset.force_set_argument(changeset, :characteristics, characteristic_ids) - end - end - 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 @@ -50,13 +33,6 @@ defmodule Diffo.Provider.Instance.Characteristic do end end - @doc """ - Creates the Characteristics from a Extended Instance's module - """ - def create_characteristics(module, type) when is_atom(module) and is_atom(type) do - Info.characteristics(module) |> create_characteristics_from_declarations(type) - end - defp create_characteristics_from_declarations(declarations, type) do Enum.reduce_while(declarations, [], fn %{name: name, value_type: value_type}, acc -> try do diff --git a/lib/diffo/provider/components/instance/extension/feature.ex b/lib/diffo/provider/components/instance/extension/feature.ex index 5d03b8a..f0c8c75 100644 --- a/lib/diffo/provider/components/instance/extension/feature.ex +++ b/lib/diffo/provider/components/instance/extension/feature.ex @@ -8,7 +8,6 @@ defmodule Diffo.Provider.Instance.Feature do alias Diffo.Provider alias Diffo.Provider.Instance - alias Diffo.Provider.Instance.Extension.Info alias Diffo.Type.Value @doc """ @@ -19,22 +18,6 @@ defmodule Diffo.Provider.Instance.Feature do @doc """ Sets the Extended Instances features argument in the changeset, creating the features and feature characteristics """ - def set_features_argument(changeset) when is_struct(changeset, Ash.Changeset) do - %module{} = changeset.data - - case features = create_features(module) do - [] -> - changeset - - {:error, error} -> - Ash.Changeset.add_error(changeset, error) - - _ -> - feature_ids = Enum.map(features, &Map.get(&1, :id)) - Ash.Changeset.force_set_argument(changeset, :features, feature_ids) - end - end - def set_features_argument(changeset, declarations) when is_struct(changeset, Ash.Changeset) and is_list(declarations) do case features = create_features_from_declarations(declarations) do @@ -50,13 +33,6 @@ defmodule Diffo.Provider.Instance.Feature do end end - @doc """ - Creates the Features from a Extended Instance's module - """ - def create_features(module) when is_atom(module) do - Info.features(module) |> create_features_from_declarations() - end - defp create_features_from_declarations(declarations) do Enum.reduce_while( declarations, diff --git a/lib/diffo/provider/components/instance/extension/info.ex b/lib/diffo/provider/components/instance/extension/info.ex index 27c87d2..3b26f13 100644 --- a/lib/diffo/provider/components/instance/extension/info.ex +++ b/lib/diffo/provider/components/instance/extension/info.ex @@ -5,5 +5,5 @@ defmodule Diffo.Provider.Instance.Extension.Info do use Spark.InfoGenerator, extension: Diffo.Provider.Instance.Extension, - sections: [:specification, :features, :characteristics, :parties] + sections: [:structure] end diff --git a/lib/diffo/provider/components/instance/extension/party.ex b/lib/diffo/provider/components/instance/extension/party.ex index b0bc1bc..26bb9b4 100644 --- a/lib/diffo/provider/components/instance/extension/party.ex +++ b/lib/diffo/provider/components/instance/extension/party.ex @@ -5,7 +5,6 @@ defmodule Diffo.Provider.Instance.Party do @moduledoc false alias Diffo.Provider - alias Diffo.Provider.Instance.Extension.Info, as: InstanceInfo @doc """ Struct for a Party @@ -13,11 +12,6 @@ defmodule Diffo.Provider.Instance.Party do defstruct [:id, :role] @doc false - def validate_parties(changeset) do - declarations = InstanceInfo.parties(changeset.resource) - validate_parties(changeset, declarations) - end - def validate_parties(changeset, declarations) do if declarations == [] do changeset diff --git a/lib/diffo/provider/components/instance/extension/persisters/persist_characteristics.ex b/lib/diffo/provider/components/instance/extension/persisters/persist_characteristics.ex new file mode 100644 index 0000000..e622fcc --- /dev/null +++ b/lib/diffo/provider/components/instance/extension/persisters/persist_characteristics.ex @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Instance.Extension.Persisters.PersistCharacteristics do + @moduledoc "Persists characteristic declarations and bakes characteristics/0" + use Spark.Dsl.Transformer + alias Spark.Dsl.Transformer + + @impl true + def transform(dsl_state) do + declarations = Transformer.get_entities(dsl_state, [:structure, :characteristics]) + escaped = Macro.escape(declarations) + dsl_state = Transformer.persist(dsl_state, :characteristics, declarations) + + {:ok, Transformer.eval(dsl_state, [], quote do + @doc false + def characteristics, do: unquote(escaped) + end)} + end +end diff --git a/lib/diffo/provider/components/instance/extension/persisters/persist_features.ex b/lib/diffo/provider/components/instance/extension/persisters/persist_features.ex new file mode 100644 index 0000000..53fd427 --- /dev/null +++ b/lib/diffo/provider/components/instance/extension/persisters/persist_features.ex @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Instance.Extension.Persisters.PersistFeatures do + @moduledoc "Persists feature declarations and bakes features/0" + use Spark.Dsl.Transformer + alias Spark.Dsl.Transformer + + @impl true + def transform(dsl_state) do + declarations = Transformer.get_entities(dsl_state, [:structure, :features]) + escaped = Macro.escape(declarations) + dsl_state = Transformer.persist(dsl_state, :features, declarations) + + {:ok, Transformer.eval(dsl_state, [], quote do + @doc false + def features, do: unquote(escaped) + end)} + end +end diff --git a/lib/diffo/provider/components/instance/extension/persisters/persist_parties.ex b/lib/diffo/provider/components/instance/extension/persisters/persist_parties.ex new file mode 100644 index 0000000..ff25478 --- /dev/null +++ b/lib/diffo/provider/components/instance/extension/persisters/persist_parties.ex @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Instance.Extension.Persisters.PersistParties do + @moduledoc "Persists party declarations and bakes parties/0" + use Spark.Dsl.Transformer + alias Spark.Dsl.Transformer + + @impl true + def transform(dsl_state) do + declarations = Transformer.get_entities(dsl_state, [:structure, :parties]) + escaped = Macro.escape(declarations) + dsl_state = Transformer.persist(dsl_state, :parties, declarations) + + {:ok, Transformer.eval(dsl_state, [], quote do + @doc false + def parties, do: unquote(escaped) + end)} + end +end diff --git a/lib/diffo/provider/components/instance/extension/persisters/persist_specification.ex b/lib/diffo/provider/components/instance/extension/persisters/persist_specification.ex new file mode 100644 index 0000000..4cfd5f9 --- /dev/null +++ b/lib/diffo/provider/components/instance/extension/persisters/persist_specification.ex @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Instance.Extension.Persisters.PersistSpecification do + @moduledoc "Normalises specification DSL options, persists them, and bakes specification/0" + use Spark.Dsl.Transformer + alias Spark.Dsl.Transformer + + @impl true + def transform(dsl_state) do + spec = [ + id: Transformer.get_option(dsl_state, [:structure, :specification], :id), + name: Transformer.get_option(dsl_state, [:structure, :specification], :name), + type: Transformer.get_option(dsl_state, [:structure, :specification], :type, :serviceSpecification), + major_version: Transformer.get_option(dsl_state, [:structure, :specification], :major_version, 1), + description: Transformer.get_option(dsl_state, [:structure, :specification], :description), + category: Transformer.get_option(dsl_state, [:structure, :specification], :category) + ] + + escaped = Macro.escape(spec) + dsl_state = Transformer.persist(dsl_state, :specification, spec) + + {:ok, Transformer.eval(dsl_state, [], quote do + @doc false + def specification, do: unquote(escaped) + end)} + end +end diff --git a/lib/diffo/provider/components/instance/extension/specification.ex b/lib/diffo/provider/components/instance/extension/specification.ex index 82c190a..b40ed56 100644 --- a/lib/diffo/provider/components/instance/extension/specification.ex +++ b/lib/diffo/provider/components/instance/extension/specification.ex @@ -8,7 +8,6 @@ defmodule Diffo.Provider.Instance.Specification do alias Diffo.Provider alias Diffo.Provider.Instance - alias Diffo.Provider.Instance.Extension.Info @doc """ Struct for a Specification @@ -18,18 +17,6 @@ defmodule Diffo.Provider.Instance.Specification do @doc """ Sets the specified_by argument in the changeset, ensuring the Extended Instance's specification exists """ - def set_specified_by_argument(changeset) when is_struct(changeset, Ash.Changeset) do - %module{} = changeset.data - # ensure the specification exists - case upsert_specification(module) do - {:ok, specification} -> - Ash.Changeset.force_set_argument(changeset, :specified_by, specification.id) - - {:error, error} -> - Ash.Changeset.add_error(changeset, error) - end - end - def set_specified_by_argument(changeset, options) when is_struct(changeset, Ash.Changeset) and is_list(options) do specification = struct(__MODULE__, options) @@ -43,22 +30,6 @@ defmodule Diffo.Provider.Instance.Specification do end end - @doc """ - Upserts the Specification from a Extended Instance's module - """ - def upsert_specification(module) when is_atom(module) do - options = Info.specification_options(module) - specification = struct(__MODULE__, options) - - case Provider.create_specification(Map.from_struct(specification)) do - {:ok, _result} -> - {:ok, specification} - - {:error, error} -> - {:error, error} - end - end - @doc """ Relates a specification to the Extended Instance """ diff --git a/lib/diffo/provider/components/instance/extension/transformers/transform_behaviour.ex b/lib/diffo/provider/components/instance/extension/transformers/transform_behaviour.ex new file mode 100644 index 0000000..5aea342 --- /dev/null +++ b/lib/diffo/provider/components/instance/extension/transformers/transform_behaviour.ex @@ -0,0 +1,104 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Instance.Extension.Transformers.TransformBehaviour do + @moduledoc "Generates build_before/1 and build_after/2, and injects build arguments into declared create actions" + use Spark.Dsl.Transformer + alias Spark.Dsl.Transformer + alias Diffo.Provider.Instance.Extension.ActionCreate + + @build_args [ + specified_by: :uuid, + features: {:array, :uuid}, + characteristics: {:array, :uuid} + ] + + @impl true + def transform(dsl_state) do + spec = Transformer.get_persisted(dsl_state, :specification, []) + + dsl_state = inject_create_arguments(dsl_state) + + {build_before_body, build_after_body} = + if spec[:id] do + before_body = quote do + changeset + |> Diffo.Provider.Instance.Specification.set_specified_by_argument(specification()) + |> Diffo.Provider.Instance.Feature.set_features_argument(features()) + |> Diffo.Provider.Instance.Characteristic.set_characteristics_argument(characteristics()) + |> Diffo.Provider.Instance.Party.validate_parties(parties()) + end + + after_body = quote do + Diffo.Provider.Instance.ActionHelper.build_after(changeset, result) + end + + {before_body, after_body} + else + {quote(do: changeset), quote(do: {:ok, result})} + end + + {:ok, Transformer.eval(dsl_state, [], quote do + @doc false + def build_before(changeset), do: unquote(build_before_body) + + @doc false + def build_after(changeset, result), do: unquote(build_after_body) + + @doc false + def characteristic(name), do: Enum.find(characteristics(), &(&1.name == name)) + + @doc false + def feature(name), do: Enum.find(features(), &(&1.name == name)) + + @doc false + def feature_characteristic(feature_name, char_name) do + case feature(feature_name) do + nil -> nil + f -> Enum.find(f.characteristics, &(&1.name == char_name)) + end + end + + @doc false + def party(role), do: Enum.find(parties(), &(&1.role == role)) + end)} + end + + defp inject_create_arguments(dsl_state) do + action_create_declarations = + Transformer.get_entities(dsl_state, [:behaviour, :actions]) + |> Enum.filter(&is_struct(&1, ActionCreate)) + + Enum.reduce(action_create_declarations, dsl_state, fn %ActionCreate{name: action_name}, dsl_state -> + action = + Transformer.get_entities(dsl_state, [:actions]) + |> Enum.find(&(is_struct(&1, Ash.Resource.Actions.Create) and &1.name == action_name)) + + if action do + existing = MapSet.new(action.arguments, & &1.name) + + new_args = + @build_args + |> Enum.reject(fn {name, _} -> MapSet.member?(existing, name) end) + |> Enum.map(fn {name, type} -> + %Ash.Resource.Actions.Argument{name: name, type: type, public?: false, allow_nil?: true} + end) + + updated = %{action | arguments: action.arguments ++ new_args} + Transformer.replace_entity(dsl_state, [:actions], updated, fn entity -> + is_struct(entity, Ash.Resource.Actions.Create) and entity.name == action_name + end) + else + dsl_state + end + end) + end + + @impl true + def after?(Diffo.Provider.Instance.Extension.Persisters.PersistSpecification), do: true + def after?(Diffo.Provider.Instance.Extension.Persisters.PersistCharacteristics), do: true + def after?(Diffo.Provider.Instance.Extension.Persisters.PersistFeatures), do: true + def after?(Diffo.Provider.Instance.Extension.Persisters.PersistParties), do: true + def after?(_), do: false +end diff --git a/lib/diffo/provider/components/instance/extension/transformers/transform_build_actions.ex b/lib/diffo/provider/components/instance/extension/transformers/transform_build_actions.ex deleted file mode 100644 index 357b305..0000000 --- a/lib/diffo/provider/components/instance/extension/transformers/transform_build_actions.ex +++ /dev/null @@ -1,35 +0,0 @@ -# SPDX-FileCopyrightText: 2025 diffo contributors -# -# SPDX-License-Identifier: MIT - -defmodule Diffo.Provider.Instance.Extension.Transformers.TransformBuildActions do - @moduledoc "Generates __diffo_build_before__/1 and __diffo_build_after__/2 from baked structural data" - use Spark.Dsl.Transformer - alias Spark.Dsl.Transformer - - @impl true - def transform(dsl_state) do - {:ok, Transformer.eval(dsl_state, [], quote do - @doc false - def __diffo_build_before__(changeset) do - changeset - |> Diffo.Provider.Instance.Specification.set_specified_by_argument(__diffo_specification__()) - |> Diffo.Provider.Instance.Feature.set_features_argument(__diffo_features__()) - |> Diffo.Provider.Instance.Characteristic.set_characteristics_argument(__diffo_characteristics__()) - |> Diffo.Provider.Instance.Party.validate_parties(__diffo_party_declarations__()) - end - - @doc false - def __diffo_build_after__(changeset, result) do - Diffo.Provider.Instance.ActionHelper.build_after(changeset, result) - end - end)} - end - - @impl true - def after?(Diffo.Provider.Instance.Extension.Transformers.TransformSpecification), do: true - def after?(Diffo.Provider.Instance.Extension.Transformers.TransformCharacteristics), do: true - def after?(Diffo.Provider.Instance.Extension.Transformers.TransformFeatures), do: true - def after?(Diffo.Provider.Instance.Extension.Transformers.TransformParties), do: true - def after?(_), do: false -end diff --git a/lib/diffo/provider/components/instance/extension/transformers/transform_characteristics.ex b/lib/diffo/provider/components/instance/extension/transformers/transform_characteristics.ex deleted file mode 100644 index ea16d84..0000000 --- a/lib/diffo/provider/components/instance/extension/transformers/transform_characteristics.ex +++ /dev/null @@ -1,20 +0,0 @@ -# SPDX-FileCopyrightText: 2025 diffo contributors -# -# SPDX-License-Identifier: MIT - -defmodule Diffo.Provider.Instance.Extension.Transformers.TransformCharacteristics do - @moduledoc "Bakes characteristic declarations into __diffo_characteristics__/0" - use Spark.Dsl.Transformer - alias Spark.Dsl.Transformer - - @impl true - def transform(dsl_state) do - characteristics = Transformer.get_entities(dsl_state, [:characteristics]) - escaped = Macro.escape(characteristics) - - {:ok, Transformer.eval(dsl_state, [], quote do - @doc false - def __diffo_characteristics__, do: unquote(escaped) - end)} - end -end diff --git a/lib/diffo/provider/components/instance/extension/transformers/transform_features.ex b/lib/diffo/provider/components/instance/extension/transformers/transform_features.ex deleted file mode 100644 index 79266af..0000000 --- a/lib/diffo/provider/components/instance/extension/transformers/transform_features.ex +++ /dev/null @@ -1,20 +0,0 @@ -# SPDX-FileCopyrightText: 2025 diffo contributors -# -# SPDX-License-Identifier: MIT - -defmodule Diffo.Provider.Instance.Extension.Transformers.TransformFeatures do - @moduledoc "Bakes feature declarations into __diffo_features__/0" - use Spark.Dsl.Transformer - alias Spark.Dsl.Transformer - - @impl true - def transform(dsl_state) do - features = Transformer.get_entities(dsl_state, [:features]) - escaped = Macro.escape(features) - - {:ok, Transformer.eval(dsl_state, [], quote do - @doc false - def __diffo_features__, do: unquote(escaped) - end)} - end -end diff --git a/lib/diffo/provider/components/instance/extension/transformers/transform_parties.ex b/lib/diffo/provider/components/instance/extension/transformers/transform_parties.ex deleted file mode 100644 index af2156a..0000000 --- a/lib/diffo/provider/components/instance/extension/transformers/transform_parties.ex +++ /dev/null @@ -1,20 +0,0 @@ -# SPDX-FileCopyrightText: 2025 diffo contributors -# -# SPDX-License-Identifier: MIT - -defmodule Diffo.Provider.Instance.Extension.Transformers.TransformParties do - @moduledoc "Bakes party declarations into __diffo_party_declarations__/0" - use Spark.Dsl.Transformer - alias Spark.Dsl.Transformer - - @impl true - def transform(dsl_state) do - parties = Transformer.get_entities(dsl_state, [:parties]) - escaped = Macro.escape(parties) - - {:ok, Transformer.eval(dsl_state, [], quote do - @doc false - def __diffo_party_declarations__, do: unquote(escaped) - end)} - end -end diff --git a/lib/diffo/provider/components/instance/extension/transformers/transform_specification.ex b/lib/diffo/provider/components/instance/extension/transformers/transform_specification.ex deleted file mode 100644 index a5896f6..0000000 --- a/lib/diffo/provider/components/instance/extension/transformers/transform_specification.ex +++ /dev/null @@ -1,28 +0,0 @@ -# SPDX-FileCopyrightText: 2025 diffo contributors -# -# SPDX-License-Identifier: MIT - -defmodule Diffo.Provider.Instance.Extension.Transformers.TransformSpecification do - @moduledoc "Bakes specification DSL options into __diffo_specification__/0" - use Spark.Dsl.Transformer - alias Spark.Dsl.Transformer - - @impl true - def transform(dsl_state) do - spec = [ - id: Transformer.get_option(dsl_state, [:specification], :id), - name: Transformer.get_option(dsl_state, [:specification], :name), - type: Transformer.get_option(dsl_state, [:specification], :type), - major_version: Transformer.get_option(dsl_state, [:specification], :major_version), - description: Transformer.get_option(dsl_state, [:specification], :description), - category: Transformer.get_option(dsl_state, [:specification], :category) - ] - - escaped = Macro.escape(spec) - - {:ok, Transformer.eval(dsl_state, [], quote do - @doc false - def __diffo_specification__, do: unquote(escaped) - end)} - end -end diff --git a/lib/diffo/provider/components/instance/extension/verifiers/verify_behaviour.ex b/lib/diffo/provider/components/instance/extension/verifiers/verify_behaviour.ex new file mode 100644 index 0000000..7cc10db --- /dev/null +++ b/lib/diffo/provider/components/instance/extension/verifiers/verify_behaviour.ex @@ -0,0 +1,53 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Instance.Extension.Verifiers.VerifyBehaviour do + @moduledoc "Verifies that actions declared in behaviour do exist as Ash actions of the correct type" + use Spark.Dsl.Verifier + + alias Spark.Dsl.Verifier + alias Spark.Error.DslError + alias Diffo.Provider.Instance.Extension.ActionCreate + alias Diffo.Provider.Instance.Extension.ActionUpdate + + @impl true + def verify(dsl_state) do + resource = Verifier.get_persisted(dsl_state, :module) + behaviour_actions = Verifier.get_entities(dsl_state, [:behaviour, :actions]) + ash_actions = Verifier.get_entities(dsl_state, [:actions]) + + create_names = ash_actions |> Enum.filter(&is_struct(&1, Ash.Resource.Actions.Create)) |> MapSet.new(& &1.name) + update_names = ash_actions |> Enum.filter(&is_struct(&1, Ash.Resource.Actions.Update)) |> MapSet.new(& &1.name) + + errors = + Enum.flat_map(behaviour_actions, fn + %ActionCreate{name: name} -> + if MapSet.member?(create_names, name) do + [] + else + [DslError.exception( + module: resource, + path: [:behaviour, :actions], + message: "behaviour: create #{inspect(name)} does not exist as a create action on this resource" + )] + end + + %ActionUpdate{name: name} -> + if MapSet.member?(update_names, name) do + [] + else + [DslError.exception( + module: resource, + path: [:behaviour, :actions], + message: "behaviour: update #{inspect(name)} does not exist as an update action on this resource" + )] + end + end) + + case errors do + [] -> :ok + errors -> {:error, errors} + end + end +end diff --git a/lib/diffo/provider/components/instance/extension/verifiers/verify_characteristics.ex b/lib/diffo/provider/components/instance/extension/verifiers/verify_characteristics.ex index 841ce54..b81e133 100644 --- a/lib/diffo/provider/components/instance/extension/verifiers/verify_characteristics.ex +++ b/lib/diffo/provider/components/instance/extension/verifiers/verify_characteristics.ex @@ -3,7 +3,7 @@ # SPDX-License-Identifier: MIT defmodule Diffo.Provider.Instance.Extension.Verifiers.VerifyCharacteristics do - @moduledoc "Verifies that characteristic value_type modules exist" + @moduledoc "Verifies that characteristic names are unique and value_type modules exist" use Spark.Dsl.Verifier alias Spark.Dsl.Verifier @@ -12,9 +12,21 @@ defmodule Diffo.Provider.Instance.Extension.Verifiers.VerifyCharacteristics do @impl true def verify(dsl_state) do resource = Verifier.get_persisted(dsl_state, :module) - characteristics = Verifier.get_entities(dsl_state, [:characteristics]) + characteristics = Verifier.get_entities(dsl_state, [:structure, :characteristics]) - errors = + duplicate_errors = + characteristics + |> Enum.group_by(& &1.name) + |> Enum.filter(fn {_name, chars} -> length(chars) > 1 end) + |> Enum.map(fn {name, _} -> + DslError.exception( + module: resource, + path: [:structure, :characteristics], + message: "characteristics: name #{inspect(name)} is declared more than once" + ) + end) + + type_errors = Enum.reduce(characteristics, [], fn char, acc -> case module_from_value_type(char.value_type) do {:ok, module} -> @@ -24,7 +36,7 @@ defmodule Diffo.Provider.Instance.Extension.Verifiers.VerifyCharacteristics do [ DslError.exception( module: resource, - path: [:characteristics, char.name], + path: [:structure, :characteristics, char.name], message: "characteristics: value_type #{inspect(module)} does not exist" ) | acc @@ -36,6 +48,8 @@ defmodule Diffo.Provider.Instance.Extension.Verifiers.VerifyCharacteristics do end end) + errors = duplicate_errors ++ type_errors + case errors do [] -> :ok errors -> {:error, errors} diff --git a/lib/diffo/provider/components/instance/extension/verifiers/verify_features.ex b/lib/diffo/provider/components/instance/extension/verifiers/verify_features.ex index 0d15f9b..d85e65f 100644 --- a/lib/diffo/provider/components/instance/extension/verifiers/verify_features.ex +++ b/lib/diffo/provider/components/instance/extension/verifiers/verify_features.ex @@ -3,7 +3,7 @@ # SPDX-License-Identifier: MIT defmodule Diffo.Provider.Instance.Extension.Verifiers.VerifyFeatures do - @moduledoc "Verifies that feature characteristic value_type modules exist" + @moduledoc "Verifies that feature names are unique, feature characteristic names are unique, and value_type modules exist" use Spark.Dsl.Verifier alias Spark.Dsl.Verifier @@ -12,33 +12,58 @@ defmodule Diffo.Provider.Instance.Extension.Verifiers.VerifyFeatures do @impl true def verify(dsl_state) do resource = Verifier.get_persisted(dsl_state, :module) - features = Verifier.get_entities(dsl_state, [:features]) + features = Verifier.get_entities(dsl_state, [:structure, :features]) - errors = + duplicate_feature_errors = + features + |> Enum.group_by(& &1.name) + |> Enum.filter(fn {_name, fs} -> length(fs) > 1 end) + |> Enum.map(fn {name, _} -> + DslError.exception( + module: resource, + path: [:structure, :features], + message: "features: name #{inspect(name)} is declared more than once" + ) + end) + + type_errors = Enum.reduce(features, [], fn feature, acc -> - Enum.reduce(feature.characteristics, acc, fn char, acc -> + duplicate_char_errors = + feature.characteristics + |> Enum.group_by(& &1.name) + |> Enum.filter(fn {_name, chars} -> length(chars) > 1 end) + |> Enum.map(fn {name, _} -> + DslError.exception( + module: resource, + path: [:structure, :features, feature.name, :characteristics], + message: "features: characteristic name #{inspect(name)} is declared more than once in #{inspect(feature.name)}" + ) + end) + + Enum.reduce(feature.characteristics, acc ++ duplicate_char_errors, fn char, inner_acc -> case module_from_value_type(char.value_type) do {:ok, module} -> if Code.ensure_loaded?(module) do - acc + inner_acc else [ DslError.exception( module: resource, - path: [:features, feature.name, :characteristics, char.name], - message: - "features: characteristic value_type #{inspect(module)} does not exist" + path: [:structure, :features, feature.name, :characteristics, char.name], + message: "features: characteristic value_type #{inspect(module)} does not exist" ) - | acc + | inner_acc ] end :error -> - acc + inner_acc end end) end) + errors = duplicate_feature_errors ++ type_errors + case errors do [] -> :ok errors -> {:error, errors} diff --git a/lib/diffo/provider/components/instance/extension/verifiers/verify_parties.ex b/lib/diffo/provider/components/instance/extension/verifiers/verify_parties.ex index 5572e34..5f97648 100644 --- a/lib/diffo/provider/components/instance/extension/verifiers/verify_parties.ex +++ b/lib/diffo/provider/components/instance/extension/verifiers/verify_parties.ex @@ -12,7 +12,7 @@ defmodule Diffo.Provider.Instance.Extension.Verifiers.VerifyParties do @impl true def verify(dsl_state) do resource = Verifier.get_persisted(dsl_state, :module) - parties = Verifier.get_entities(dsl_state, [:parties]) + parties = Verifier.get_entities(dsl_state, [:structure, :parties]) duplicate_errors = parties @@ -21,7 +21,7 @@ defmodule Diffo.Provider.Instance.Extension.Verifiers.VerifyParties do |> Enum.map(fn {role, _} -> DslError.exception( module: resource, - path: [:parties], + path: [:structure, :parties], message: "parties: role #{inspect(role)} is declared more than once" ) end) @@ -32,7 +32,7 @@ defmodule Diffo.Provider.Instance.Extension.Verifiers.VerifyParties do [ DslError.exception( module: resource, - path: [:parties, party.role], + path: [:structure, :parties, party.role], message: "parties: party_type #{inspect(party.party_type)} does not exist" ) | acc diff --git a/lib/diffo/provider/components/instance/extension/verifiers/verify_specification.ex b/lib/diffo/provider/components/instance/extension/verifiers/verify_specification.ex index b6ac1bd..2368126 100644 --- a/lib/diffo/provider/components/instance/extension/verifiers/verify_specification.ex +++ b/lib/diffo/provider/components/instance/extension/verifiers/verify_specification.ex @@ -12,14 +12,14 @@ defmodule Diffo.Provider.Instance.Extension.Verifiers.VerifySpecification do @impl true def verify(dsl_state) do resource = Verifier.get_persisted(dsl_state, :module) - spec_id = Verifier.get_option(dsl_state, [:specification], :id) + spec_id = Verifier.get_option(dsl_state, [:structure, :specification], :id) errors = if spec_id && !Diffo.Uuid.uuid4?(spec_id) do [ DslError.exception( module: resource, - path: [:specification, :id], + path: [:structure, :specification, :id], message: "specification: id must be a valid UUID4" ) ] diff --git a/lib/diffo/provider/components/instance/info.ex b/lib/diffo/provider/components/instance/info.ex new file mode 100644 index 0000000..ae17ac9 --- /dev/null +++ b/lib/diffo/provider/components/instance/info.ex @@ -0,0 +1,60 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Instance.Info do + @moduledoc "Public introspection API for resources extending Diffo.Provider.BaseInstance" + + alias Spark.Dsl.Extension + + @doc "Returns the normalised specification keyword list for the resource" + @spec specification(Ash.Resource.t()) :: keyword() | nil + def specification(resource) do + Extension.get_persisted(resource, :specification) + end + + @doc "Returns the list of characteristic declarations for the resource" + @spec characteristics(Ash.Resource.t()) :: list() | [] + def characteristics(resource) do + Extension.get_persisted(resource, :characteristics, []) + end + + @doc "Returns the list of feature declarations for the resource" + @spec features(Ash.Resource.t()) :: list() | [] + def features(resource) do + Extension.get_persisted(resource, :features, []) + end + + @doc "Returns the list of party role declarations for the resource" + @spec parties(Ash.Resource.t()) :: list() | [] + def parties(resource) do + Extension.get_persisted(resource, :parties, []) + end + + @doc "Returns the named characteristic declaration, or nil" + @spec characteristic(Ash.Resource.t(), atom()) :: struct() | nil + def characteristic(resource, name) do + Enum.find(characteristics(resource), &(&1.name == name)) + end + + @doc "Returns the named feature declaration, or nil" + @spec feature(Ash.Resource.t(), atom()) :: struct() | nil + def feature(resource, name) do + Enum.find(features(resource), &(&1.name == name)) + end + + @doc "Returns the named characteristic within a feature, or nil" + @spec feature_characteristic(Ash.Resource.t(), atom(), atom()) :: struct() | nil + def feature_characteristic(resource, feature_name, char_name) do + case feature(resource, feature_name) do + nil -> nil + f -> Enum.find(f.characteristics, &(&1.name == char_name)) + end + end + + @doc "Returns the party declaration for the given role, or nil" + @spec party(Ash.Resource.t(), atom()) :: struct() | nil + def party(resource, role) do + Enum.find(parties(resource), &(&1.role == role)) + end +end diff --git a/test/instance_extension/party_test.exs b/test/instance_extension/party_test.exs index 35deb16..04c1a71 100644 --- a/test/instance_extension/party_test.exs +++ b/test/instance_extension/party_test.exs @@ -53,14 +53,14 @@ defmodule Diffo.InstanceExtension.PartyTest do describe "Instance DSL — Shelf parties" do test "party declarations are accessible via info" do - parties = InstanceInfo.parties(Shelf) + parties = InstanceInfo.structure_parties(Shelf) roles = Enum.map(parties, & &1.role) assert :facilitator in roles assert :overseer in roles end test "party types are correct" do - parties = InstanceInfo.parties(Shelf) + parties = InstanceInfo.structure_parties(Shelf) facilitator = Enum.find(parties, &(&1.role == :facilitator)) overseer = Enum.find(parties, &(&1.role == :overseer)) assert facilitator.party_type == Organization @@ -68,38 +68,38 @@ defmodule Diffo.InstanceExtension.PartyTest do end test "singular party defaults to multiple: false" do - parties = InstanceInfo.parties(Shelf) + parties = InstanceInfo.structure_parties(Shelf) facilitator = Enum.find(parties, &(&1.role == :facilitator)) assert facilitator.multiple == false end test "reference: true is declared" do - parties = InstanceInfo.parties(Shelf) + parties = InstanceInfo.structure_parties(Shelf) provider = Enum.find(parties, &(&1.role == :provider)) assert provider.reference == true assert provider.multiple == false end test "reference defaults to false" do - parties = InstanceInfo.parties(Shelf) + parties = InstanceInfo.structure_parties(Shelf) facilitator = Enum.find(parties, &(&1.role == :facilitator)) assert facilitator.reference == false end test "calculate: is declared" do - parties = InstanceInfo.parties(Shelf) + parties = InstanceInfo.structure_parties(Shelf) manager = Enum.find(parties, &(&1.role == :manager)) assert manager.calculate == :manager_calc end test "parties (plural) sets multiple: true" do - parties = InstanceInfo.parties(Shelf) + parties = InstanceInfo.structure_parties(Shelf) installer = Enum.find(parties, &(&1.role == :installer)) assert installer.multiple == true end test "parties (plural) constraints are declared" do - parties = InstanceInfo.parties(Shelf) + parties = InstanceInfo.structure_parties(Shelf) installer = Enum.find(parties, &(&1.role == :installer)) assert installer.constraints == [min: 1, max: 3] end diff --git a/test/instance_extension/transformer_test.exs b/test/instance_extension/transformer_test.exs index d239496..2d9363a 100644 --- a/test/instance_extension/transformer_test.exs +++ b/test/instance_extension/transformer_test.exs @@ -10,28 +10,35 @@ defmodule Diffo.InstanceExtension.TransformerTest do alias Diffo.Test.Card alias Diffo.Provider.Instance.Characteristic alias Diffo.Provider.Instance.Feature + alias Diffo.Provider.Instance.Info - describe "TransformSpecification" do - test "bakes specification options into __diffo_specification__/0" do - spec = Shelf.__diffo_specification__() + describe "PersistSpecification" do + test "bakes specification/0 onto the resource" do + spec = Shelf.specification() assert spec[:id] == "ef016d85-9dbd-429c-84da-1df56cc7dda5" assert spec[:name] == "shelf" assert spec[:type] == :resourceSpecification assert spec[:description] == "A Shelf Resource Instance which contain cards" assert spec[:category] == "Network Resource" + assert spec[:major_version] == 1 end - test "card specification is also baked correctly" do - spec = Card.__diffo_specification__() + test "card specification is baked correctly" do + spec = Card.specification() assert spec[:id] == "cd29956f-6c68-44cc-bf54-705eb8d2f754" assert spec[:name] == "card" assert spec[:type] == :resourceSpecification end + + test "specification is also accessible via Info" do + assert Info.specification(Shelf)[:name] == "shelf" + assert Info.specification(Card)[:name] == "card" + end end - describe "TransformCharacteristics" do - test "bakes characteristic declarations into __diffo_characteristics__/0" do - chars = Shelf.__diffo_characteristics__() + describe "PersistCharacteristics" do + test "bakes characteristics/0 onto the resource" do + chars = Shelf.characteristics() assert is_list(chars) assert length(chars) == 3 names = Enum.map(chars, & &1.name) @@ -41,22 +48,28 @@ defmodule Diffo.InstanceExtension.TransformerTest do end test "each characteristic is a Characteristic struct" do - [first | _] = Shelf.__diffo_characteristics__() + [first | _] = Shelf.characteristics() assert is_struct(first, Characteristic) end - test "card characteristics are baked" do - chars = Card.__diffo_characteristics__() - assert length(chars) == 2 - names = Enum.map(chars, & &1.name) - assert :card in names - assert :ports in names + test "characteristics are also accessible via Info" do + assert length(Info.characteristics(Shelf)) == 3 + assert length(Info.characteristics(Card)) == 2 + end + + test "Info.characteristic/2 returns the named characteristic" do + char = Info.characteristic(Shelf, :shelves) + assert char.name == :shelves + end + + test "Info.characteristic/2 returns nil for unknown name" do + assert Info.characteristic(Shelf, :nonexistent) == nil end end - describe "TransformFeatures" do - test "bakes feature declarations into __diffo_features__/0" do - features = Shelf.__diffo_features__() + describe "PersistFeatures" do + test "bakes features/0 onto the resource" do + features = Shelf.features() assert is_list(features) assert length(features) == 1 [feature] = features @@ -65,26 +78,49 @@ defmodule Diffo.InstanceExtension.TransformerTest do end test "each feature is a Feature struct" do - [feature] = Shelf.__diffo_features__() + [feature] = Shelf.features() assert is_struct(feature, Feature) end - test "feature characteristics are nested in the feature declaration" do - [feature] = Shelf.__diffo_features__() + test "feature characteristics are nested in the declaration" do + [feature] = Shelf.features() assert length(feature.characteristics) == 2 char_names = Enum.map(feature.characteristics, & &1.name) assert :deploymentClass in char_names assert :deploymentClasses in char_names end - test "card has no features" do - assert Card.__diffo_features__() == [] + test "features are also accessible via Info" do + assert length(Info.features(Shelf)) == 1 + assert Info.features(Card) == [] + end + + test "Info.feature/2 returns the named feature" do + feature = Info.feature(Shelf, :spectralManagement) + assert feature.name == :spectralManagement + end + + test "Info.feature/2 returns nil for unknown name" do + assert Info.feature(Shelf, :nonexistent) == nil + end + + test "Info.feature_characteristic/3 returns the named characteristic within a feature" do + char = Info.feature_characteristic(Shelf, :spectralManagement, :deploymentClass) + assert char.name == :deploymentClass + end + + test "Info.feature_characteristic/3 returns nil for unknown feature" do + assert Info.feature_characteristic(Shelf, :nonexistent, :deploymentClass) == nil + end + + test "Info.feature_characteristic/3 returns nil for unknown characteristic" do + assert Info.feature_characteristic(Shelf, :spectralManagement, :nonexistent) == nil end end - describe "TransformParties" do - test "bakes party declarations into __diffo_party_declarations__/0" do - parties = Shelf.__diffo_party_declarations__() + describe "PersistParties" do + test "bakes parties/0 onto the resource" do + parties = Shelf.parties() assert is_list(parties) assert length(parties) == 5 roles = Enum.map(parties, & &1.role) @@ -96,44 +132,114 @@ defmodule Diffo.InstanceExtension.TransformerTest do end test "reference party has reference flag set" do - parties = Shelf.__diffo_party_declarations__() - provider = Enum.find(parties, &(&1.role == :provider)) + provider = Enum.find(Shelf.parties(), &(&1.role == :provider)) assert provider.reference == true end test "calculate party has calculate set" do - parties = Shelf.__diffo_party_declarations__() - manager = Enum.find(parties, &(&1.role == :manager)) + manager = Enum.find(Shelf.parties(), &(&1.role == :manager)) assert manager.calculate == :manager_calc end test "plural party has constraints" do - parties = Shelf.__diffo_party_declarations__() - installer = Enum.find(parties, &(&1.role == :installer)) + installer = Enum.find(Shelf.parties(), &(&1.role == :installer)) assert installer.multiple == true assert installer.constraints == [min: 1, max: 3] end - test "card has no parties" do - assert Card.__diffo_party_declarations__() == [] + test "parties are also accessible via Info" do + assert length(Info.parties(Shelf)) == 5 + assert Info.parties(Card) == [] + end + + test "Info.party/2 returns the named party declaration by role" do + p = Info.party(Shelf, :facilitator) + assert p.role == :facilitator + end + + test "Info.party/2 returns nil for unknown role" do + assert Info.party(Shelf, :nonexistent) == nil end end - describe "TransformBuildActions" do - test "__diffo_build_before__/1 is defined on shelf" do - assert function_exported?(Shelf, :__diffo_build_before__, 1) + describe "TransformBehaviour" do + setup do + Code.ensure_loaded!(Shelf) + Code.ensure_loaded!(Card) + :ok + end + + test "build_before/1 is defined on shelf" do + assert function_exported?(Shelf, :build_before, 1) + end + + test "build_after/2 is defined on shelf" do + assert function_exported?(Shelf, :build_after, 2) + end + + test "build_before/1 is defined on card" do + assert function_exported?(Card, :build_before, 1) + end + + test "build_after/2 is defined on card" do + assert function_exported?(Card, :build_after, 2) + end + + test "action_create injects :specified_by argument into :build" do + action = Ash.Resource.Info.action(Shelf, :build) + arg_names = Enum.map(action.arguments, & &1.name) + assert :specified_by in arg_names + assert :features in arg_names + assert :characteristics in arg_names + end + + test "injected arguments are not public" do + action = Ash.Resource.Info.action(Shelf, :build) + injected = Enum.filter(action.arguments, &(&1.name in [:specified_by, :features, :characteristics])) + assert Enum.all?(injected, &(&1.public? == false)) + end + + test "characteristic/1 returns the named characteristic" do + char = Shelf.characteristic(:shelves) + assert char.name == :shelves + assert char.value_type == {:array, Diffo.Test.ShelfValue} + end + + test "characteristic/1 returns nil for unknown name" do + assert Shelf.characteristic(:nonexistent) == nil + end + + test "feature/1 returns the named feature" do + feature = Shelf.feature(:spectralManagement) + assert feature.name == :spectralManagement + assert feature.is_enabled? == true + end + + test "feature/1 returns nil for unknown name" do + assert Shelf.feature(:nonexistent) == nil + end + + test "feature_characteristic/2 returns the named characteristic within a feature" do + char = Shelf.feature_characteristic(:spectralManagement, :deploymentClass) + assert char.name == :deploymentClass + end + + test "feature_characteristic/2 returns nil for unknown feature" do + assert Shelf.feature_characteristic(:nonexistent, :deploymentClass) == nil end - test "__diffo_build_after__/2 is defined on shelf" do - assert function_exported?(Shelf, :__diffo_build_after__, 2) + test "feature_characteristic/2 returns nil for unknown characteristic" do + assert Shelf.feature_characteristic(:spectralManagement, :nonexistent) == nil end - test "__diffo_build_before__/1 is defined on card" do - assert function_exported?(Card, :__diffo_build_before__, 1) + test "party/1 returns the named party declaration by role" do + p = Shelf.party(:facilitator) + assert p.role == :facilitator + assert p.multiple == false end - test "__diffo_build_after__/2 is defined on card" do - assert function_exported?(Card, :__diffo_build_after__, 2) + test "party/1 returns nil for unknown role" do + assert Shelf.party(:nonexistent) == nil end end end diff --git a/test/instance_extension/verifier_test.exs b/test/instance_extension/verifier_test.exs index 2ba6140..f6fc136 100644 --- a/test/instance_extension/verifier_test.exs +++ b/test/instance_extension/verifier_test.exs @@ -21,9 +21,11 @@ defmodule Diffo.InstanceExtension.VerifierTest do description "resource with invalid spec id" end - specification do - id "ef016d85-9dbd-429c-04da-1df56cc7dda5" - name "invalid" + structure do + specification do + id "ef016d85-9dbd-429c-04da-1df56cc7dda5" + name "invalid" + end end end end @@ -32,6 +34,35 @@ defmodule Diffo.InstanceExtension.VerifierTest do end describe "characteristics verifier" do + test "duplicate characteristic name warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "characteristics: name :foo is declared more than once", + fn -> + defmodule DuplicateCharName do + alias Diffo.Provider.BaseInstance + use Ash.Resource, fragments: [BaseInstance], domain: Diffo.Test.Servo + + resource do + description "resource with duplicate characteristic name" + end + + structure do + specification do + id "cd29956f-6c68-44cc-bf54-705eb8d2f754" + name "invalid" + end + + characteristics do + characteristic :foo, Diffo.Test.ShelfValue + characteristic :foo, Diffo.Test.ShelfValue + end + end + end + end + ) + end + test "non-existent value_type module warns DslError on compilation" do Util.assert_compile_time_warning( Spark.Error.DslError, @@ -45,13 +76,15 @@ defmodule Diffo.InstanceExtension.VerifierTest do description "resource with non-existent characteristic value_type" end - specification do - id "cd29956f-6c68-44cc-bf54-705eb8d2f754" - name "invalid" - end + structure do + specification do + id "cd29956f-6c68-44cc-bf54-705eb8d2f754" + name "invalid" + end - characteristics do - characteristic :foo, NonExistent.CharValue + characteristics do + characteristic :foo, NonExistent.CharValue + end end end end @@ -71,13 +104,15 @@ defmodule Diffo.InstanceExtension.VerifierTest do description "resource with non-existent array characteristic value_type" end - specification do - id "cd29956f-6c68-44cc-bf54-705eb8d2f754" - name "invalid" - end + structure do + specification do + id "cd29956f-6c68-44cc-bf54-705eb8d2f754" + name "invalid" + end - characteristics do - characteristic :bar, {:array, NonExistent.ArrayValue} + characteristics do + characteristic :bar, {:array, NonExistent.ArrayValue} + end end end end @@ -86,6 +121,69 @@ defmodule Diffo.InstanceExtension.VerifierTest do end describe "features verifier" do + test "duplicate feature name warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "features: name :my_feature is declared more than once", + fn -> + defmodule DuplicateFeatureName do + alias Diffo.Provider.BaseInstance + use Ash.Resource, fragments: [BaseInstance], domain: Diffo.Test.Servo + + resource do + description "resource with duplicate feature names" + end + + structure do + specification do + id "cd29956f-6c68-44cc-bf54-705eb8d2f754" + name "invalid" + end + + features do + feature :my_feature do + end + + feature :my_feature do + end + end + end + end + end + ) + end + + test "duplicate feature characteristic name warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "features: characteristic name :baz is declared more than once in :my_feature", + fn -> + defmodule DuplicateFeatureCharName do + alias Diffo.Provider.BaseInstance + use Ash.Resource, fragments: [BaseInstance], domain: Diffo.Test.Servo + + resource do + description "resource with duplicate feature characteristic names" + end + + structure do + specification do + id "cd29956f-6c68-44cc-bf54-705eb8d2f754" + name "invalid" + end + + features do + feature :my_feature do + characteristic :baz, Diffo.Test.ShelfValue + characteristic :baz, Diffo.Test.ShelfValue + end + end + end + end + end + ) + end + test "non-existent feature characteristic value_type warns DslError on compilation" do Util.assert_compile_time_warning( Spark.Error.DslError, @@ -99,14 +197,16 @@ defmodule Diffo.InstanceExtension.VerifierTest do description "resource with non-existent feature characteristic value_type" end - specification do - id "cd29956f-6c68-44cc-bf54-705eb8d2f754" - name "invalid" - end + structure do + specification do + id "cd29956f-6c68-44cc-bf54-705eb8d2f754" + name "invalid" + end - features do - feature :my_feature do - characteristic :baz, NonExistent.FeatureValue + features do + feature :my_feature do + characteristic :baz, NonExistent.FeatureValue + end end end end @@ -130,14 +230,16 @@ defmodule Diffo.InstanceExtension.VerifierTest do description "resource with duplicate party roles" end - specification do - id "cd29956f-6c68-44cc-bf54-705eb8d2f754" - name "invalid" - end + structure do + specification do + id "cd29956f-6c68-44cc-bf54-705eb8d2f754" + name "invalid" + end - parties do - party :operator, Shelf - party :operator, Shelf + parties do + party :operator, Shelf + party :operator, Shelf + end end end end @@ -157,13 +259,77 @@ defmodule Diffo.InstanceExtension.VerifierTest do description "resource with non-existent party_type" end - specification do - id "cd29956f-6c68-44cc-bf54-705eb8d2f754" - name "invalid" + structure do + specification do + id "cd29956f-6c68-44cc-bf54-705eb8d2f754" + name "invalid" + end + + parties do + party :operator, NonExistent.PartyModule + end end + end + end + ) + end + end + + describe "behaviour verifier" do + test "undeclared create action name warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "behaviour: create :nonexistent does not exist as a create action on this resource", + fn -> + defmodule BehaviourMissingCreate do + alias Diffo.Provider.BaseInstance + use Ash.Resource, fragments: [BaseInstance], domain: Diffo.Test.Servo - parties do - party :operator, NonExistent.PartyModule + resource do + description "resource with behaviour referencing a missing create action" + end + + structure do + specification do + id "cd29956f-6c68-44cc-bf54-705eb8d2f754" + name "invalid" + end + end + + behaviour do + actions do + create :nonexistent + end + end + end + end + ) + end + + test "undeclared update action name warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "behaviour: update :nonexistent does not exist as an update action on this resource", + fn -> + defmodule BehaviourMissingUpdate do + alias Diffo.Provider.BaseInstance + use Ash.Resource, fragments: [BaseInstance], domain: Diffo.Test.Servo + + resource do + description "resource with behaviour referencing a missing update action" + end + + structure do + specification do + id "cd29956f-6c68-44cc-bf54-705eb8d2f754" + name "invalid" + end + end + + behaviour do + actions do + update :nonexistent + end end end end diff --git a/test/support/resource/card.ex b/test/support/resource/card.ex index 1dfe4ef..9792b8e 100644 --- a/test/support/resource/card.ex +++ b/test/support/resource/card.ex @@ -11,7 +11,6 @@ defmodule Diffo.Test.Card do alias Diffo.Provider.BaseInstance alias Diffo.Provider.Instance.Relationship alias Diffo.Provider.Instance.Characteristic - alias Diffo.Provider.Instance.ActionHelper alias Diffo.Provider.Assigner alias Diffo.Provider.Assignment alias Diffo.Provider.AssignableValue @@ -27,40 +26,36 @@ defmodule Diffo.Test.Card do plural_name :Cards end - specification do - id "cd29956f-6c68-44cc-bf54-705eb8d2f754" - name "card" - type :resourceSpecification - description "A Card Resource Instance" - category "Network Resource" + structure do + specification do + id "cd29956f-6c68-44cc-bf54-705eb8d2f754" + name "card" + type :resourceSpecification + description "A Card Resource Instance" + category "Network Resource" + end + + characteristics do + characteristic :card, CardValue + characteristic :ports, AssignableValue + end end - characteristics do - characteristic :card, CardValue - characteristic :ports, AssignableValue + behaviour do + actions do + create :build + end end actions do create :build do description "creates a new Card resource instance for build" accept [:id, :name, :type, :which] - argument :specified_by, :uuid, public?: false argument :relationships, {:array, :struct} - argument :features, {:array, :uuid}, public?: false - argument :characteristics, {:array, :uuid}, public?: false argument :places, {:array, :struct} argument :parties, {:array, :struct} change set_attribute(:type, :resource) - - change before_action(fn changeset, _context -> - ActionHelper.build_before(changeset) - end) - - change after_action(fn changeset, result, _context -> - ActionHelper.build_after(changeset, result) - end) - change load [:href] upsert? false end diff --git a/test/support/resource/invalid/invalid_characteristic.ex b/test/support/resource/invalid/invalid_characteristic.ex index 70aa258..2053dba 100644 --- a/test/support/resource/invalid/invalid_characteristic.ex +++ b/test/support/resource/invalid/invalid_characteristic.ex @@ -10,7 +10,6 @@ defmodule Diffo.Test.InvalidCharacteristic do """ alias Diffo.Provider.BaseInstance - alias Diffo.Provider.Instance.ActionHelper use Ash.Resource, fragments: [BaseInstance], @@ -20,38 +19,24 @@ defmodule Diffo.Test.InvalidCharacteristic do description "Ash Resource with an invalid characteristic" end - specification do - id "3caf29b9-0b91-4b8f-8568-2960131b1feb" - name "invalidCharacteristic" - type :resourceSpecification - category "Network Resource" - end + structure do + specification do + id "3caf29b9-0b91-4b8f-8568-2960131b1feb" + name "invalidCharacteristic" + type :resourceSpecification + category "Network Resource" + end - characteristics do - characteristic :invalid, InvalidValue + characteristics do + characteristic :invalid, InvalidValue + end end actions do create :build do description "creates a new InvalidCharacteristic resource instance for build" accept [:id, :name, :type, :which] - argument :specified_by, :uuid, public?: false - argument :relationships, {:array, :struct} - argument :features, {:array, :uuid}, public?: false - argument :characteristics, {:array, :uuid}, public?: false - argument :places, {:array, :struct} - argument :parties, {:array, :struct} - change set_attribute(:type, :resource) - - change before_action(fn changeset, _context -> - ActionHelper.build_before(changeset) - end) - - change after_action(fn changeset, result, _context -> - ActionHelper.build_after(changeset, result) - end) - change load [:href] upsert? false end diff --git a/test/support/resource/invalid/invalid_feature_characteristic.ex b/test/support/resource/invalid/invalid_feature_characteristic.ex index 61066b6..143b717 100644 --- a/test/support/resource/invalid/invalid_feature_characteristic.ex +++ b/test/support/resource/invalid/invalid_feature_characteristic.ex @@ -10,7 +10,6 @@ defmodule Diffo.Test.InvalidFeatureCharacteristic do """ alias Diffo.Provider.BaseInstance - alias Diffo.Provider.Instance.ActionHelper use Ash.Resource, fragments: [BaseInstance], @@ -20,17 +19,19 @@ defmodule Diffo.Test.InvalidFeatureCharacteristic do description "Ash Resource with an invalid feature characteristic" end - specification do - id "1f2402ca-82da-428e-a58b-5405a5431386" - name "invalidFeatureCharacteristic" - type :resourceSpecification - category "Network Resource" - end + structure do + specification do + id "1f2402ca-82da-428e-a58b-5405a5431386" + name "invalidFeatureCharacteristic" + type :resourceSpecification + category "Network Resource" + end - features do - feature :invalid_feature_characteristic do - is_enabled? true - characteristic :invalid, InvalidValue + features do + feature :invalid_feature_characteristic do + is_enabled? true + characteristic :invalid, InvalidValue + end end end @@ -38,23 +39,7 @@ defmodule Diffo.Test.InvalidFeatureCharacteristic do create :build do description "creates a new InvalidFeatureCharacteristic resource instance for build" accept [:id, :name, :type, :which] - argument :specified_by, :uuid, public?: false - argument :relationships, {:array, :struct} - argument :features, {:array, :uuid}, public?: false - argument :characteristics, {:array, :uuid}, public?: false - argument :places, {:array, :struct} - argument :parties, {:array, :struct} - change set_attribute(:type, :resource) - - change before_action(fn changeset, _context -> - ActionHelper.build_before(changeset) - end) - - change after_action(fn changeset, result, _context -> - ActionHelper.build_after(changeset, result) - end) - change load [:href] upsert? false end diff --git a/test/support/resource/invalid/invalid_specification.ex b/test/support/resource/invalid/invalid_specification.ex index 1da60c3..b409619 100644 --- a/test/support/resource/invalid/invalid_specification.ex +++ b/test/support/resource/invalid/invalid_specification.ex @@ -10,7 +10,6 @@ defmodule Diffo.Test.InvalidSpecification do """ alias Diffo.Provider.BaseInstance - alias Diffo.Provider.Instance.ActionHelper use Ash.Resource, fragments: [BaseInstance], @@ -20,34 +19,20 @@ defmodule Diffo.Test.InvalidSpecification do description "Ash Resource with an invalid specification" end - specification do - id "ef016d85-9dbd-429c-04da-1df56cc7dda5" - name "invalidSpecification" - type :resourceSpecification - category "Network Resource" + structure do + specification do + id "ef016d85-9dbd-429c-04da-1df56cc7dda5" + name "invalidSpecification" + type :resourceSpecification + category "Network Resource" + end end actions do create :build do description "creates a new InvalidSpecification resource instance for build" accept [:id, :name, :type, :which] - argument :specified_by, :uuid, public?: false - argument :relationships, {:array, :struct} - argument :features, {:array, :uuid}, public?: false - argument :characteristics, {:array, :uuid}, public?: false - argument :places, {:array, :struct} - argument :parties, {:array, :struct} - change set_attribute(:type, :resource) - - change before_action(fn changeset, _context -> - ActionHelper.build_before(changeset) - end) - - change after_action(fn changeset, result, _context -> - ActionHelper.build_after(changeset, result) - end) - change load [:href] upsert? false end diff --git a/test/support/resource/shelf.ex b/test/support/resource/shelf.ex index 31b8fa3..b9022b3 100644 --- a/test/support/resource/shelf.ex +++ b/test/support/resource/shelf.ex @@ -12,7 +12,6 @@ defmodule Diffo.Test.Shelf do alias Diffo.Provider.BaseInstance alias Diffo.Provider.Instance.Relationship alias Diffo.Provider.Instance.Characteristic - alias Diffo.Provider.Instance.ActionHelper alias Diffo.Provider.Assigner alias Diffo.Provider.Assignment alias Diffo.Provider.AssignableValue @@ -30,57 +29,53 @@ defmodule Diffo.Test.Shelf do plural_name :Shelves end - specification do - id "ef016d85-9dbd-429c-84da-1df56cc7dda5" - name "shelf" - type :resourceSpecification - description "A Shelf Resource Instance which contain cards" - category "Network Resource" - end + structure do + specification do + id "ef016d85-9dbd-429c-84da-1df56cc7dda5" + name "shelf" + type :resourceSpecification + description "A Shelf Resource Instance which contain cards" + category "Network Resource" + end - features do - feature :spectralManagement do - is_enabled? true - characteristic :deploymentClass, DeploymentClassValue - characteristic :deploymentClasses, {:array, DeploymentClassValue} + features do + feature :spectralManagement do + is_enabled? true + characteristic :deploymentClass, DeploymentClassValue + characteristic :deploymentClasses, {:array, DeploymentClassValue} + end end - end - characteristics do - characteristic :shelf, ShelfValue - characteristic :slots, AssignableValue - characteristic :shelves, {:array, ShelfValue} + characteristics do + characteristic :shelf, ShelfValue + characteristic :slots, AssignableValue + characteristic :shelves, {:array, ShelfValue} + end + + parties do + party :facilitator, Diffo.Test.Organization + party :overseer, Diffo.Test.Person + party :provider, Diffo.Test.Organization, reference: true + party :manager, Diffo.Test.Organization, calculate: :manager_calc + parties :installer, Diffo.Test.Person, constraints: [min: 1, max: 3] + end end - parties do - party :facilitator, Diffo.Test.Organization - party :overseer, Diffo.Test.Person - party :provider, Diffo.Test.Organization, reference: true - party :manager, Diffo.Test.Organization, calculate: :manager_calc - parties :installer, Diffo.Test.Person, constraints: [min: 1, max: 3] + behaviour do + actions do + create :build + end end actions do create :build do description "creates a new Shelf resource instance for build" accept [:id, :name, :type, :which] - argument :specified_by, :uuid, public?: false argument :relationships, {:array, :struct} - argument :features, {:array, :uuid}, public?: false - argument :characteristics, {:array, :uuid}, public?: false argument :places, {:array, :struct} argument :parties, {:array, :struct} change set_attribute(:type, :resource) - - change before_action(fn changeset, _context -> - ActionHelper.build_before(changeset) - end) - - change after_action(fn changeset, result, _context -> - ActionHelper.build_after(changeset, result) - end) - change load [:href] upsert? false end From 4ec4b3194c1fbb95dc3604b00bb2f1a0371c0a67 Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Tue, 28 Apr 2026 22:42:49 +0930 Subject: [PATCH 07/22] documents --- .../DSL-Diffo.Provider.Instance.Extension.md | 291 +++++++++++++----- .../provider/components/base_instance.ex | 111 +++++-- .../provider/components/instance/extension.ex | 33 +- 3 files changed, 335 insertions(+), 100 deletions(-) diff --git a/documentation/dsls/DSL-Diffo.Provider.Instance.Extension.md b/documentation/dsls/DSL-Diffo.Provider.Instance.Extension.md index bf9d55b..6b5c7d3 100644 --- a/documentation/dsls/DSL-Diffo.Provider.Instance.Extension.md +++ b/documentation/dsls/DSL-Diffo.Provider.Instance.Extension.md @@ -5,15 +5,77 @@ This file was generated by Spark. Do not edit it by hand. DSL Extension customising an Instance. -Provides compile-time declaration blocks for domain-specific Service and Resource kinds -built on `Diffo.Provider.BaseInstance`. All declarations are introspectable via -`Diffo.Provider.Instance.Extension.Info`. +Provides two top-level sections: + +## structure + +Describes the static shape of the Instance kind — what it is, what values it carries, +and what parties it relates to. All structure declarations are baked into the resource +module at compile time via persisters and are introspectable at runtime via +`Diffo.Provider.Instance.Info` or directly as generated functions on the resource module. + +- `specification do` — the TMF Specification (id, name, type, version, description, category). + The id is a stable UUID4 that is the same across all environments for this Instance kind. +- `characteristics do` — typed value slots carried by instances of this kind, each backed + by an `Ash.TypedStruct`. +- `features do` — optional capabilities of this kind, each with its own typed characteristic + payload and an enabled/disabled default. +- `parties do` — the party roles that instances of this kind relate to, with multiplicity, + reference, and calculation options. + +## behaviour + +Declares which Ash actions should be wired for instance build lifecycle management. +Currently supports `create` declarations; future sections will cover triggers and other +lifecycle concerns. + +Declaring `create :name` in `behaviour do actions do` causes the `TransformBehaviour` +transformer to inject `:specified_by`, `:features`, and `:characteristics` arguments onto +the named Ash create action. These arguments carry the UUIDs of the TMF entities created +by `build_before/1` and consumed by the Ash relationship management in the action. See the [DSL cheat sheet](DSL-Diffo.Provider.Instance.Extension.html) for the full DSL reference. -See `Diffo.Provider.BaseInstance` for full usage documentation. +See `Diffo.Provider.BaseInstance` for full usage documentation including generated functions. + + +## structure +Defines the structural shape of the Instance — its specification, characteristics, features, and parties + +### Nested DSLs + * [specification](#structure-specification) + * [characteristics](#structure-characteristics) + * characteristic + * [features](#structure-features) + * feature + * characteristic + * [parties](#structure-parties) + * party + * parties + + +### Examples +``` +structure do + specification do + id "da9b207a-26c3-451d-8abd-0640c6349979" + name "DSL Access Service" + type :serviceSpecification + end + + characteristics do + characteristic :circuit, Diffo.Access.Circuit + end + + parties do + party :provider, MyApp.Provider + end +end + +``` + -## specification +### structure.specification Defines the Instance Specification @@ -38,23 +100,68 @@ end | Name | Type | Default | Docs | |------|------|---------|------| -| [`id`](#specification-id){: #specification-id .spark-required} | `String.t` | | The id of the specification, a uuid4 the same in all environments, unique for name and major_version. | -| [`name`](#specification-name){: #specification-name .spark-required} | `String.t` | | The name of the specification, unique to a service but common for all versions. | -| [`type`](#specification-type){: #specification-type } | `atom` | `:serviceSpecification` | The type of the specification. | -| [`major_version`](#specification-major_version){: #specification-major_version } | `integer` | `1` | The major_version of the specification. | -| [`description`](#specification-description){: #specification-description } | `String.t` | | A generic description of the specified service or resource. | -| [`category`](#specification-category){: #specification-category } | `String.t` | | The category the specified service or resource belongs to. | +| [`id`](#structure-specification-id){: #structure-specification-id .spark-required} | `String.t` | | The id of the specification, a uuid4 the same in all environments, unique for name and major_version. | +| [`name`](#structure-specification-name){: #structure-specification-name .spark-required} | `String.t` | | The name of the specification, unique to a service but common for all versions. | +| [`type`](#structure-specification-type){: #structure-specification-type } | `atom` | `:serviceSpecification` | The type of the specification. | +| [`major_version`](#structure-specification-major_version){: #structure-specification-major_version } | `integer` | `1` | The major_version of the specification. | +| [`description`](#structure-specification-description){: #structure-specification-description } | `String.t` | | A generic description of the specified service or resource. | +| [`category`](#structure-specification-category){: #structure-specification-category } | `String.t` | | The category the specified service or resource belongs to. | + + + + +### structure.characteristics +List of Instance Characteristics + +### Nested DSLs + * [characteristic](#structure-characteristics-characteristic) + + +### Examples +``` +characteristics do + characteristic :dslam, Diffo.Access.Dslam + characteristic :aggregate_interface, Diffo.Access.AggregateInterface + characteristic :circuit, Diffo.Access.Circuit + characteristic :line, Diffo.Access.Line +end + +``` + + + + +### structure.characteristics.characteristic +```elixir +characteristic name, value_type +``` + + +Adds a Characteristic + + + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`name`](#structure-characteristics-characteristic-name){: #structure-characteristics-characteristic-name .spark-required} | `atom` | | The name of the characteristic, an atom | +| [`value_type`](#structure-characteristics-characteristic-value_type){: #structure-characteristics-characteristic-value_type } | `any` | | The type of the characteristic's value. An atom module name such as an Ash.TypedStruct for a scalar value, or `{:array, module}` for an array of values of that type. | -## features + + +### structure.features Configuration for Instance Features ### Nested DSLs - * [feature](#features-feature) + * [feature](#structure-features-feature) * characteristic @@ -74,7 +181,7 @@ end -### features.feature +### structure.features.feature ```elixir feature name ``` @@ -83,7 +190,7 @@ feature name Adds a Feature ### Nested DSLs - * [characteristic](#features-feature-characteristic) + * [characteristic](#structure-features-feature-characteristic) @@ -92,15 +199,15 @@ Adds a Feature | Name | Type | Default | Docs | |------|------|---------|------| -| [`name`](#features-feature-name){: #features-feature-name .spark-required} | `atom` | | The name of the feature, an atom | +| [`name`](#structure-features-feature-name){: #structure-features-feature-name .spark-required} | `atom` | | The name of the feature, an atom | ### Options | Name | Type | Default | Docs | |------|------|---------|------| -| [`is_enabled?`](#features-feature-is_enabled?){: #features-feature-is_enabled? } | `boolean` | | Whether the feature is enabled by default, defaults true | +| [`is_enabled?`](#structure-features-feature-is_enabled?){: #structure-features-feature-is_enabled? } | `boolean` | | Whether the feature is enabled by default, defaults true | -### features.feature.characteristic +### structure.features.feature.characteristic ```elixir characteristic name, value_type ``` @@ -116,9 +223,8 @@ Adds a Characteristic | Name | Type | Default | Docs | |------|------|---------|------| -| [`name`](#features-feature-characteristic-name){: #features-feature-characteristic-name .spark-required} | `atom` | | The name of the characteristic, an atom | -| [`value_type`](#features-feature-characteristic-value_type){: #features-feature-characteristic-value_type } | `any` | | The type of the characteristic's value. An atom module name such as an Ash.TypedStruct for a scalar value, or `{:array, module}` for an array of values of that type. | - +| [`name`](#structure-features-feature-characteristic-name){: #structure-features-feature-characteristic-name .spark-required} | `atom` | | The name of the characteristic, an atom | +| [`value_type`](#structure-features-feature-characteristic-value_type){: #structure-features-feature-characteristic-value_type } | `any` | | The type of the characteristic's value. An atom module name such as an Ash.TypedStruct for a scalar value, or `{:array, module}` for an array of values of that type. | @@ -131,21 +237,20 @@ Adds a Characteristic - -## characteristics -List of Instance Characteristics +### structure.parties +List of Instance Party roles ### Nested DSLs - * [characteristic](#characteristics-characteristic) + * [party](#structure-parties-party) + * [parties](#structure-parties-parties) ### Examples ``` -characteristics do - characteristic :dslam, Diffo.Access.Dslam - characteristic :aggregate_interface, Diffo.Access.AggregateInterface - characteristic :circuit, Diffo.Access.Circuit - characteristic :line, Diffo.Access.Line +parties do + party :provider, MyApp.Provider, calculate: :provider_calculation + parties :technician, MyApp.Technician, constraints: [min: 1, max: 3] + party :owner, MyApp.InfrastructureCo, reference: true end ``` @@ -153,13 +258,13 @@ end -### characteristics.characteristic +### structure.parties.party ```elixir -characteristic name, value_type +party role, party_type ``` -Adds a Characteristic +Declares a singular party role on this Instance @@ -169,46 +274,111 @@ Adds a Characteristic | Name | Type | Default | Docs | |------|------|---------|------| -| [`name`](#characteristics-characteristic-name){: #characteristics-characteristic-name .spark-required} | `atom` | | The name of the characteristic, an atom | -| [`value_type`](#characteristics-characteristic-value_type){: #characteristics-characteristic-value_type } | `any` | | The type of the characteristic's value. An atom module name such as an Ash.TypedStruct for a scalar value, or `{:array, module}` for an array of values of that type. | +| [`role`](#structure-parties-party-role){: #structure-parties-party-role .spark-required} | `atom` | | The role name, an atom | +| [`party_type`](#structure-parties-party-party_type){: #structure-parties-party-party_type } | `any` | | The module of the Party kind. An atom module name such as a BaseParty-derived resource. | +### Options +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`reference`](#structure-parties-party-reference){: #structure-parties-party-reference } | `boolean` | `false` | If true, no direct PartyRef edge is created; the party is reachable by graph traversal. | +| [`calculate`](#structure-parties-party-calculate){: #structure-parties-party-calculate } | `atom` | | Name of an Ash calculation on this resource that produces the party at build time. | +### Introspection +Target: `Diffo.Provider.Instance.Extension.PartyDeclaration` +### structure.parties.parties +```elixir +parties role, party_type +``` + + +Declares a plural party role on this Instance + + + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`role`](#structure-parties-parties-role){: #structure-parties-parties-role .spark-required} | `atom` | | The role name, an atom | +| [`party_type`](#structure-parties-parties-party_type){: #structure-parties-parties-party_type } | `any` | | The module of the Party kind. An atom module name such as a BaseParty-derived resource. | +### Options + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`reference`](#structure-parties-parties-reference){: #structure-parties-parties-reference } | `boolean` | `false` | If true, no direct PartyRef edge is created; the party is reachable by graph traversal. | +| [`calculate`](#structure-parties-parties-calculate){: #structure-parties-parties-calculate } | `atom` | | Name of an Ash calculation on this resource that produces the party at build time. | +| [`constraints`](#structure-parties-parties-constraints){: #structure-parties-parties-constraints } | `keyword` | | Multiplicity constraints on the number of parties in this role, e.g. [min: 1, max: 3] | + + + + + +### Introspection + +Target: `Diffo.Provider.Instance.Extension.PartyDeclaration` -## parties -List of Instance Party roles + + + + +## behaviour +Defines the behavioural wiring for the Instance — actions, and in future triggers and tasks ### Nested DSLs - * [party](#parties-party) - * [parties](#parties-parties) + * [actions](#behaviour-actions) + * create + * update ### Examples ``` -parties do - party :provider, MyApp.Provider, calculate: :provider_calculation - parties :technician, MyApp.Technician, constraints: [min: 1, max: 3] - party :owner, MyApp.InfrastructureCo, reference: true +behaviour do + actions do + create :build + update :define + end end ``` +### behaviour.actions +Declares which actions to wire for instance behaviour + +### Nested DSLs + * [create](#behaviour-actions-create) + * [update](#behaviour-actions-update) + + +### Examples +``` +actions do + create :build + update :define +end + +``` + -### parties.party + + +### behaviour.actions.create ```elixir -party role, party_type +create name ``` -Declares a singular party role on this Instance +Marks a create action for instance build wiring, injecting :specified_by, :features, and :characteristics arguments @@ -218,30 +388,21 @@ Declares a singular party role on this Instance | Name | Type | Default | Docs | |------|------|---------|------| -| [`role`](#parties-party-role){: #parties-party-role .spark-required} | `atom` | | The role name, an atom | -| [`party_type`](#parties-party-party_type){: #parties-party-party_type } | `any` | | The module of the Party kind. An atom module name such as a BaseParty-derived resource. | -### Options +| [`name`](#behaviour-actions-create-name){: #behaviour-actions-create-name .spark-required} | `atom` | | The name of the create action to wire | -| Name | Type | Default | Docs | -|------|------|---------|------| -| [`reference`](#parties-party-reference){: #parties-party-reference } | `boolean` | `false` | If true, no direct PartyRef edge is created; the party is reachable by graph traversal. | -| [`calculate`](#parties-party-calculate){: #parties-party-calculate } | `atom` | | Name of an Ash calculation on this resource that produces the party at build time. | -### Introspection - -Target: `Diffo.Provider.Instance.Extension.PartyDeclaration` -### parties.parties +### behaviour.actions.update ```elixir -parties role, party_type +update name ``` -Declares a plural party role on this Instance +Marks an update action for instance behaviour wiring @@ -251,23 +412,15 @@ Declares a plural party role on this Instance | Name | Type | Default | Docs | |------|------|---------|------| -| [`role`](#parties-parties-role){: #parties-parties-role .spark-required} | `atom` | | The role name, an atom | -| [`party_type`](#parties-parties-party_type){: #parties-parties-party_type } | `any` | | The module of the Party kind. An atom module name such as a BaseParty-derived resource. | -### Options +| [`name`](#behaviour-actions-update-name){: #behaviour-actions-update-name .spark-required} | `atom` | | The name of the update action to wire | + -| Name | Type | Default | Docs | -|------|------|---------|------| -| [`reference`](#parties-parties-reference){: #parties-parties-reference } | `boolean` | `false` | If true, no direct PartyRef edge is created; the party is reachable by graph traversal. | -| [`calculate`](#parties-parties-calculate){: #parties-parties-calculate } | `atom` | | Name of an Ash calculation on this resource that produces the party at build time. | -| [`constraints`](#parties-parties-constraints){: #parties-parties-constraints } | `keyword` | | Multiplicity constraints on the number of parties in this role, e.g. [min: 1, max: 3] | -### Introspection -Target: `Diffo.Provider.Instance.Extension.PartyDeclaration` diff --git a/lib/diffo/provider/components/base_instance.ex b/lib/diffo/provider/components/base_instance.ex index 70e5d0b..d927cb4 100644 --- a/lib/diffo/provider/components/base_instance.ex +++ b/lib/diffo/provider/components/base_instance.ex @@ -4,7 +4,7 @@ defmodule Diffo.Provider.BaseInstance do @moduledoc """ - Ash Resource Fragment which is a the point of extension for your TMF Service or Resource Instance + Ash Resource Fragment which is the point of extension for your TMF Service or Resource Instance. `BaseInstance` is the foundation for domain-specific Service and Resource kinds. Include it as a fragment on an `Ash.Resource` to get common Instance attributes, @@ -12,19 +12,22 @@ defmodule Diffo.Provider.BaseInstance do ## Instance Extension DSL - The `Diffo.Provider.Instance.Extension` DSL provides compile-time declaration blocks - for describing the shape of a domain-specific Service or Resource. + The DSL has two top-level sections: `structure do` describes what the instance kind is; + `behaviour do` wires it to Ash actions. - `specification do` — declares the TMF Specification for this Instance kind. + ### structure - `features do` — declares the Features this Instance kind may have, each optionally - carrying a typed characteristic payload. + `specification do` — declares the TMF Specification for this Instance kind (id, name, type, + major_version, description, category). - `characteristics do` — declares the top-level Characteristics of this Instance kind, - each backed by an `Ash.TypedStruct`. + `characteristics do` — declares the top-level Characteristics of this Instance kind, each + backed by an `Ash.TypedStruct`. + + `features do` — declares the Features this Instance kind may have, each optionally carrying + its own typed characteristic payload. `parties do` — declares the Party roles this Instance kind relates to. Role names are - domain-specific nouns describing what the party is to the instance. Two forms: + domain-specific nouns describing what the party means to the instance. Two forms: parties do party :provider, MyApp.Provider, calculate: :provider_calculation @@ -33,12 +36,44 @@ defmodule Diffo.Provider.BaseInstance do party :owner, MyApp.InfrastructureCo, reference: true end - - `party` — singular (at most one party in this role) - - `parties` — plural (unbounded, or bounded with `constraints:`) + - `party` — singular (at most one party in this role per instance) + - `parties` — plural (unbounded, or bounded with `constraints: [min: n, max: m]`) - `reference: true` — no direct `PartyRef` edge; party is reachable by graph traversal - `calculate:` — names an Ash calculation on this resource that produces the party at build time - All declarations are introspectable via `Diffo.Provider.Instance.Extension.Info`. + All declarations are introspectable at runtime via `Diffo.Provider.Instance.Info` and at + compile time via `Diffo.Provider.Instance.Extension.Info`. + + ### behaviour + + `behaviour do actions do create :name end end` — marks a named create action for build + wiring. This injects `:specified_by`, `:features`, and `:characteristics` arguments onto + that action so Ash accepts the values that `build_before/1` sets automatically. + + You still write the action body yourself for domain-specific accepts, arguments, and changes. + The build arguments are not public and do not need to appear in `accept`. + + ## Generated functions + + Every resource using `BaseInstance` with a `specification do` gets the following functions + generated at compile time: + + - `specification/0` — the specification keyword list baked at compile time + - `characteristics/0` — list of `Characteristic` structs + - `features/0` — list of `Feature` structs + - `parties/0` — list of `PartyDeclaration` structs + - `characteristic/1` — returns the named `Characteristic` or `nil` + - `feature/1` — returns the named `Feature` or `nil` + - `feature_characteristic/2` — returns the named characteristic within a feature, or `nil` + - `party/1` — returns the `PartyDeclaration` for the given role, or `nil` + - `build_before/1` — called automatically before every create action; upserts the + specification and creates features, characteristics, and parties, setting their ids + as action arguments + - `build_after/2` — called automatically after every create action; relates the created + TMF entities to the new instance node + + Resources without a `specification do id` get trivial passthroughs for `build_before/1` + and `build_after/2`. ## Usage @@ -50,28 +85,50 @@ defmodule Diffo.Provider.BaseInstance do plural_name :clusters end - specification do - id "4bcfc4c9-e776-4878-a658-e8d81857bed7" - name "cluster" - type :resourceSpecification + structure do + specification do + id "4bcfc4c9-e776-4878-a658-e8d81857bed7" + name "cluster" + type :resourceSpecification + end + + parties do + party :operator, MyApp.Organization + parties :installer, MyApp.Engineer + end end - parties do - party :operator, MyApp.Organization - parties :installer, MyApp.Engineer + behaviour do + actions do + create :build + end + end + + actions do + create :build do + description "creates a new Cluster resource instance" + accept [:id, :name, :type, :which] + argument :relationships, {:array, :struct} + argument :parties, {:array, :struct} + + change set_attribute(:type, :resource) + change load [:href] + upsert? false + end end end - ## Action pattern + ## Rolling your own actions - Domain-specific Instance resources should finish their `build` action with a reload via - their own domain's `get_xxx_by_id` to pick up extended fields: + The `behaviour do actions do create :name end end` declaration is optional. Omitting it + means the `:specified_by`, `:features`, and `:characteristics` arguments are not declared + on that action — but `build_before/1` and `build_after/2` are still called for every + create via the global `BuildBefore` and `BuildAfter` changes registered on `BaseInstance`. - create :build do - change after_action(fn changeset, result, _context -> - ActionHelper.build_after(changeset, result) - end) - end + If you have a create action that should NOT trigger the full build wiring (e.g. a + lightweight admin create), you can override `build_before/1` or `build_after/2` on your + resource, or use Ash's `skip_unknown_inputs` to absorb the injected arguments without + declaring them. """ use Spark.Dsl.Fragment, of: Ash.Resource, diff --git a/lib/diffo/provider/components/instance/extension.ex b/lib/diffo/provider/components/instance/extension.ex index e80005a..222acc1 100644 --- a/lib/diffo/provider/components/instance/extension.ex +++ b/lib/diffo/provider/components/instance/extension.ex @@ -6,12 +6,37 @@ defmodule Diffo.Provider.Instance.Extension do @moduledoc """ DSL Extension customising an Instance. - Provides compile-time declaration blocks for domain-specific Service and Resource kinds - built on `Diffo.Provider.BaseInstance`. All declarations are introspectable via - `Diffo.Provider.Instance.Extension.Info`. + Provides two top-level sections: + + ## structure + + Describes the static shape of the Instance kind — what it is, what values it carries, + and what parties it relates to. All structure declarations are baked into the resource + module at compile time via persisters and are introspectable at runtime via + `Diffo.Provider.Instance.Info` or directly as generated functions on the resource module. + + - `specification do` — the TMF Specification (id, name, type, version, description, category). + The id is a stable UUID4 that is the same across all environments for this Instance kind. + - `characteristics do` — typed value slots carried by instances of this kind, each backed + by an `Ash.TypedStruct`. + - `features do` — optional capabilities of this kind, each with its own typed characteristic + payload and an enabled/disabled default. + - `parties do` — the party roles that instances of this kind relate to, with multiplicity, + reference, and calculation options. + + ## behaviour + + Declares which Ash actions should be wired for instance build lifecycle management. + Currently supports `create` declarations; future sections will cover triggers and other + lifecycle concerns. + + Declaring `create :name` in `behaviour do actions do` causes the `TransformBehaviour` + transformer to inject `:specified_by`, `:features`, and `:characteristics` arguments onto + the named Ash create action. These arguments carry the UUIDs of the TMF entities created + by `build_before/1` and consumed by the Ash relationship management in the action. See the [DSL cheat sheet](DSL-Diffo.Provider.Instance.Extension.html) for the full DSL reference. - See `Diffo.Provider.BaseInstance` for full usage documentation. + See `Diffo.Provider.BaseInstance` for full usage documentation including generated functions. """ # ── structure ────────────────────────────────────────────────────────────── From 31a58e9a48d0b3efacdbb5e2e18937d9a35b4b61 Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Wed, 29 Apr 2026 07:15:11 +0930 Subject: [PATCH 08/22] =?UTF-8?q?Instance=20Extension=20=E2=80=94=20party?= =?UTF-8?q?=5Ftype=20BaseParty=20verification,=20instance=3F/1,=20livebook?= =?UTF-8?q?=20update?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Strengthens party verifier to check party_type extends BaseParty via PartyInfo.party?/1, adds instance?/1 to Extension.Info mirroring the same idiom, and rewrites the provider extension livebook to use the structure do / behaviour do DSL with automatic build wiring. --- .../use_diffo_provider_extension.livemd | 91 +++++++++---------- .../components/instance/extension/info.ex | 7 ++ .../extension/verifiers/verify_parties.ex | 38 +++++--- .../components/party/extension/info.ex | 7 ++ test/instance_extension/verifier_test.exs | 28 ++++++ 5 files changed, 114 insertions(+), 57 deletions(-) diff --git a/documentation/how_to/use_diffo_provider_extension.livemd b/documentation/how_to/use_diffo_provider_extension.livemd index 974a475..587fbfd 100644 --- a/documentation/how_to/use_diffo_provider_extension.livemd +++ b/documentation/how_to/use_diffo_provider_extension.livemd @@ -144,7 +144,12 @@ end Diffo also has an inbuilt Spark DSL extension [Diffo.Provider.Instance.Extension](https://hexdocs.pm/diffo/Diffo.Provider.Instance.Extension.html) which provides DSL and functions for use in building and operating domain specific services and resources. -Currently it has DSL to allow you to declare specification, features, characteristics, and party roles. It can be used for services or resources. +The extension has two top-level sections: + +**`structure do`** — describes the static shape of the Instance kind: its TMF Specification, Characteristics, Features, and Party roles. All declarations are baked into the module at compile time and introspectable at runtime via generated functions (`specification/0`, `characteristics/0`, `features/0`, `parties/0`) and `Diffo.Provider.Instance.Info`. + +**`behaviour do`** — declares which Ash actions should be wired for instance lifecycle management. Declaring `create :name` injects `:specified_by`, `:features`, and `:characteristics` arguments onto that action, and the `BuildBefore`/`BuildAfter` changes registered on `BaseInstance` automatically handle specification upsert, feature and characteristic creation, party validation, and graph relationship wiring for every create action. You write the action body for your domain-specific accepts and arguments; the structural wiring is handled for you. + 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. For partial resource allocation and assignment we've created Diffo.Provider.Assigner. It is used by the host resource, which declares a characteristic with an 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. @@ -172,7 +177,6 @@ defmodule Diffo.Compute.Cluster do alias Diffo.Provider.BaseInstance alias Diffo.Provider.Instance.Relationship alias Diffo.Provider.Instance.Characteristic - alias Diffo.Provider.Instance.ActionHelper alias Diffo.Compute alias Diffo.Compute.ClusterValue alias Diffo.Compute.Tenant @@ -187,42 +191,40 @@ defmodule Diffo.Compute.Cluster do plural_name :Clusters end - specification do - id "4bcfc4c9-e776-4878-a658-e8d81857bed7" - name "cluster" - type :resourceSpecification - description "A Cluster Resource Instance" - category "Network Resource" - end + structure do + specification do + id "4bcfc4c9-e776-4878-a658-e8d81857bed7" + name "cluster" + type :resourceSpecification + description "A Cluster Resource Instance" + category "Network Resource" + end + + characteristics do + characteristic :cluster, ClusterValue + end - characteristics do - characteristic :cluster, ClusterValue + parties do + party :operator, Tenant + party :manager, Engineer + end end - parties do - party :operator, Tenant - party :manager, Engineer + behaviour do + actions do + create :build + end end actions do create :build do description "creates a new Cluster resource instance for build" accept [:id, :name, :type, :which] - argument :specified_by, :uuid, public?: false argument :relationships, {:array, :struct} - argument :features, {:array, :uuid}, public?: false - argument :characteristics, {:array, :uuid}, public?: false argument :places, {:array, :struct} argument :parties, {:array, :struct} change set_attribute(:type, :resource) - - change before_action(fn changeset, _context -> ActionHelper.build_before(changeset) end) - - change after_action(fn changeset, result, _context -> - ActionHelper.build_after(changeset, result, Compute, :get_cluster_by_id) - end) - change load [:href] upsert? false end @@ -232,7 +234,7 @@ defmodule Diffo.Compute.Cluster 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_values(result, changeset), {:ok, cluster} <- Compute.get_cluster_by_id(result.id), do: {:ok, cluster} end) @@ -305,7 +307,6 @@ defmodule Diffo.Compute.GPU do alias Diffo.Provider.BaseInstance alias Diffo.Provider.Instance.Relationship alias Diffo.Provider.Instance.Characteristic - alias Diffo.Provider.Instance.ActionHelper alias Diffo.Provider.Assigner alias Diffo.Provider.Assignment alias Diffo.Provider.AssignableValue @@ -321,38 +322,36 @@ defmodule Diffo.Compute.GPU do plural_name :gpus end - specification do - id "ad50073f-17e0-45cb-b9b1-aa4296876156" - name "gpu" - type :resourceSpecification - description "A GPU Resource Instance" - category "Network Resource" + structure do + specification do + id "ad50073f-17e0-45cb-b9b1-aa4296876156" + name "gpu" + type :resourceSpecification + description "A GPU Resource Instance" + category "Network Resource" + end + + characteristics do + characteristic :gpu, GPUValue + characteristic :cores, AssignableValue + end end - characteristics do - characteristic :gpu, GPUValue - characteristic :cores, AssignableValue + behaviour do + actions do + create :build + end end actions do create :build do description "creates a new GPU resource instance for build" accept [:id, :name, :type, :which] - argument :specified_by, :uuid, public?: false argument :relationships, {:array, :struct} - argument :features, {:array, :uuid}, public?: false - argument :characteristics, {:array, :uuid}, public?: false argument :places, {:array, :struct} argument :parties, {:array, :struct} change set_attribute(:type, :resource) - - change before_action(fn changeset, _context -> ActionHelper.build_before(changeset) end) - - change after_action(fn changeset, result, _context -> - ActionHelper.build_after(changeset, result, Compute, :get_gpu_by_id) - end) - change load [:href] upsert? false end @@ -685,7 +684,7 @@ What happens when I request a specific assignment from an instance to which the In this tutorial you've used Diffo's Provider Instance Extension to define a Compute domain with a composite Cluster resource comprised of assigned GPU cores, and the Provider Party Extension to define Tenant and Engineer party kinds that operate and manage those resources. -The BaseParty fragment follows the same pattern as BaseInstance — domain-specific resources use it as a fragment and finish their actions with a domain-scoped reload to pick up extended fields. +`BaseParty` follows the same pattern as `BaseInstance` — domain-specific party resources use it as a fragment and write their own `build` action for domain-specific attributes. No manual wiring is needed. A `BasePlace` extension for domain-specific Place kinds (such as a DataCentre with its own attributes) follows the same pattern and will be added in a future release. diff --git a/lib/diffo/provider/components/instance/extension/info.ex b/lib/diffo/provider/components/instance/extension/info.ex index 3b26f13..3356537 100644 --- a/lib/diffo/provider/components/instance/extension/info.ex +++ b/lib/diffo/provider/components/instance/extension/info.ex @@ -6,4 +6,11 @@ defmodule Diffo.Provider.Instance.Extension.Info do use Spark.InfoGenerator, extension: Diffo.Provider.Instance.Extension, sections: [:structure] + + @doc "Returns true if the module is a BaseInstance-derived resource" + @spec instance?(module()) :: boolean() + def instance?(module) do + Code.ensure_loaded?(module) and + Diffo.Provider.Instance.Extension in Ash.Resource.Info.extensions(module) + end end diff --git a/lib/diffo/provider/components/instance/extension/verifiers/verify_parties.ex b/lib/diffo/provider/components/instance/extension/verifiers/verify_parties.ex index 5f97648..63bd593 100644 --- a/lib/diffo/provider/components/instance/extension/verifiers/verify_parties.ex +++ b/lib/diffo/provider/components/instance/extension/verifiers/verify_parties.ex @@ -8,6 +8,7 @@ defmodule Diffo.Provider.Instance.Extension.Verifiers.VerifyParties do alias Spark.Dsl.Verifier alias Spark.Error.DslError + alias Diffo.Provider.Party.Extension.Info, as: PartyInfo @impl true def verify(dsl_state) do @@ -28,17 +29,32 @@ defmodule Diffo.Provider.Instance.Extension.Verifiers.VerifyParties do type_errors = Enum.reduce(parties, [], fn party, acc -> - if party.party_type && !Code.ensure_loaded?(party.party_type) do - [ - DslError.exception( - module: resource, - path: [:structure, :parties, party.role], - message: "parties: party_type #{inspect(party.party_type)} does not exist" - ) - | acc - ] - else - acc + cond do + is_nil(party.party_type) -> + acc + + !Code.ensure_loaded?(party.party_type) -> + [ + DslError.exception( + module: resource, + path: [:structure, :parties, party.role], + message: "parties: party_type #{inspect(party.party_type)} does not exist" + ) + | acc + ] + + !PartyInfo.party?(party.party_type) -> + [ + DslError.exception( + module: resource, + path: [:structure, :parties, party.role], + message: "parties: party_type #{inspect(party.party_type)} does not extend BaseParty" + ) + | acc + ] + + true -> + acc end end) diff --git a/lib/diffo/provider/components/party/extension/info.ex b/lib/diffo/provider/components/party/extension/info.ex index 5638989..8c29009 100644 --- a/lib/diffo/provider/components/party/extension/info.ex +++ b/lib/diffo/provider/components/party/extension/info.ex @@ -6,4 +6,11 @@ defmodule Diffo.Provider.Party.Extension.Info do use Spark.InfoGenerator, extension: Diffo.Provider.Party.Extension, sections: [:instances, :parties] + + @doc "Returns true if the module is a BaseParty-derived resource" + @spec party?(module()) :: boolean() + def party?(module) do + Code.ensure_loaded?(module) and + Diffo.Provider.Party.Extension in Ash.Resource.Info.extensions(module) + end end diff --git a/test/instance_extension/verifier_test.exs b/test/instance_extension/verifier_test.exs index f6fc136..d679b70 100644 --- a/test/instance_extension/verifier_test.exs +++ b/test/instance_extension/verifier_test.exs @@ -273,6 +273,34 @@ defmodule Diffo.InstanceExtension.VerifierTest do end ) end + + 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", + fn -> + defmodule InvalidPartyBaseType do + alias Diffo.Provider.BaseInstance + use Ash.Resource, fragments: [BaseInstance], domain: Diffo.Test.Servo + + resource do + description "resource with party_type that is not a BaseParty" + end + + structure do + specification do + id "cd29956f-6c68-44cc-bf54-705eb8d2f754" + name "invalid" + end + + parties do + party :operator, Diffo.Test.Shelf + end + end + end + end + ) + end end describe "behaviour verifier" do From dac6587e58640e6b46b72bdccd1d89d5b6515202 Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Wed, 29 Apr 2026 07:28:50 +0930 Subject: [PATCH 09/22] minimal base place and place extension --- lib/diffo/provider/components/base_place.ex | 189 ++++++++++++++++++ lib/diffo/provider/components/place.ex | 124 +----------- .../provider/components/place/extension.ex | 17 ++ .../components/place/extension/info.ex | 16 ++ 4 files changed, 223 insertions(+), 123 deletions(-) create mode 100644 lib/diffo/provider/components/base_place.ex create mode 100644 lib/diffo/provider/components/place/extension.ex create mode 100644 lib/diffo/provider/components/place/extension/info.ex diff --git a/lib/diffo/provider/components/base_place.ex b/lib/diffo/provider/components/base_place.ex new file mode 100644 index 0000000..0d93633 --- /dev/null +++ b/lib/diffo/provider/components/base_place.ex @@ -0,0 +1,189 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.BasePlace do + @moduledoc """ + Ash Resource Fragment which is the point of extension for your TMF Place. + + `BasePlace` is the foundation for domain-specific Place kinds. + Include it as a fragment on an `Ash.Resource` to get common Place attributes, Neo4j graph + wiring, and the `Diffo.Provider.Place.Extension` DSL. + + `Diffo.Provider.Place` uses `BasePlace` directly as the out-of-the-box TMF Place resource. + Domain-specific resources extend it for richer domain identity. + + ## Attributes + + - `id` — string primary key (required, no default — set by domain). + - `href` — optional URI for the place. + - `name` — the place name. + - `type` — TMF `@type`. Defaults to `:PlaceRef`. One of `:PlaceRef`, `:GeographicSite`, + `:GeographicLocation`, `:GeographicAddress`. When `referred_type` is present, `type` must + be `:PlaceRef`. + - `referred_type` — TMF `@referredType`. One of `:GeographicSite`, `:GeographicLocation`, + `:GeographicAddress`. When present, indicates this is a reference to a place of that kind; + `type` must be `:PlaceRef`. + + ## Usage + + defmodule MyApp.GeographicSite do + use Ash.Resource, fragments: [BasePlace], domain: MyApp.Domain + + resource do + description "A Geographic Site" + plural_name :geographic_sites + end + + jason do + pick [:id, :href, :name, :referred_type, :type] + compact true + rename referred_type: "@referredType", type: "@type" + end + + outstanding do + expect [:id, :name, :referred_type, :type] + end + + actions do + create :build do + accept [:id, :href, :name] + change set_attribute(:type, :GeographicSite) + end + end + end + + ## TMF type and referred_type + + The `type` and `referred_type` attributes map to the TMF `@type` and `@referredType` JSON + fields via the jason layer. When `referred_type` is present, `type` must be `:PlaceRef`; + otherwise `type` must not be `:PlaceRef`. + """ + use Spark.Dsl.Fragment, + of: Ash.Resource, + otp_app: :diffo, + domain: Diffo.Provider, + data_layer: AshNeo4j.DataLayer, + extensions: [ + AshOutstanding.Resource, + AshJason.Resource, + Diffo.Provider.Place.Extension + ] + + neo4j do + relate [ + {:place_refs, :RELATES, :incoming, :PlaceRef} + ] + end + + attributes do + attribute :id, :string do + description "the unique id of the place" + primary_key? true + allow_nil? false + public? true + source :key + end + + attribute :href, :string do + description "the href of the place" + allow_nil? true + public? true + end + + attribute :name, :string do + description "the name of the place" + allow_nil? true + public? true + constraints match: ~r/^[a-zA-Z0-9\s._-]+$/ + end + + attribute :type, :atom do + description "the type of the place" + allow_nil? false + public? true + default :PlaceRef + constraints one_of: [:PlaceRef, :GeographicSite, :GeographicLocation, :GeographicAddress] + end + + attribute :referred_type, :atom do + description "the referred type of the place" + allow_nil? true + public? true + constraints one_of: [:GeographicSite, :GeographicLocation, :GeographicAddress] + end + + create_timestamp :created_at + + update_timestamp :updated_at + end + + relationships do + has_many :place_refs, Diffo.Provider.PlaceRef do + description "the place refs relating this place to instances" + destination_attribute :place_id + public? true + end + end + + actions do + defaults [:read, :destroy] + + create :create do + description "creates a place" + accept [:id, :href, :name, :type, :referred_type] + upsert? true + end + + update :update do + description "updates the place" + accept [:href, :name, :type, :referred_type] + end + + read :list do + description "lists all places" + end + + read :find_by_id do + description "finds place by id" + get? false + + argument :query, :ci_string do + description "Return only places with id's including the given value." + end + + filter expr(contains(id, ^arg(:query))) + end + + read :find_by_name do + description "finds place by name" + get? false + + argument :query, :ci_string do + description "Return only places with names including the given value." + end + + filter expr(contains(name, ^arg(:query))) + end + end + + validations do + validate {Diffo.Validations.HrefEndsWithId, id: :id, href: :href} do + where [present(:id), present(:href)] + end + + validate attribute_equals(:type, :PlaceRef) do + where present(:referred_type) + message "when referred_type is present, type must be PlaceRef" + end + + validate attribute_does_not_equal(:type, :PlaceRef) do + where absent(:referred_type) + message "when referred_type is absent, type must be not be PlaceRef" + end + end + + preparations do + prepare build(sort: [id: :asc, name: :asc]) + end +end diff --git a/lib/diffo/provider/components/place.ex b/lib/diffo/provider/components/place.ex index 5210a37..d0b0234 100644 --- a/lib/diffo/provider/components/place.ex +++ b/lib/diffo/provider/components/place.ex @@ -6,23 +6,13 @@ defmodule Diffo.Provider.Place do @moduledoc """ Ash Resource for a TMF Place """ - use Ash.Resource, - otp_app: :diffo, - domain: Diffo.Provider, - data_layer: AshNeo4j.DataLayer, - extensions: [AshOutstanding.Resource, AshJason.Resource] + use Ash.Resource, fragments: [Diffo.Provider.BasePlace], domain: Diffo.Provider resource do description "An Ash Resource for a TMF Place" plural_name :places end - neo4j do - relate [ - {:place_refs, :RELATES, :incoming, :PlaceRef} - ] - end - jason do pick [:id, :href, :name, :referred_type, :type] compact true @@ -32,116 +22,4 @@ defmodule Diffo.Provider.Place do outstanding do expect [:id, :name, :referred_type, :type] end - - actions do - defaults [:read, :destroy] - - create :create do - description "creates a place" - accept [:id, :href, :name, :type, :referred_type] - upsert? true - end - - read :find_by_id do - description "finds place by id" - get? false - - argument :query, :ci_string do - description "Return only places with id's including the given value." - end - - filter expr(contains(id, ^arg(:query))) - end - - read :find_by_name do - description "finds place by name" - get? false - - argument :query, :ci_string do - description "Return only places with names including the given value." - end - - filter expr(contains(name, ^arg(:query))) - end - - read :list do - description "lists all places" - end - - update :update do - description "updates the place" - accept [:href, :name, :type, :referred_type] - end - end - - attributes do - attribute :id, :string do - description "the unique id of the place" - primary_key? true - allow_nil? false - public? true - source :key - end - - attribute :href, :string do - description "the href of the place" - allow_nil? true - public? true - end - - attribute :name, :string do - description "the name of the place" - allow_nil? true - public? true - constraints match: ~r/^[a-zA-Z0-9\s._-]+$/ - end - - attribute :type, :atom do - description "the type of the place" - allow_nil? false - public? true - default :PlaceRef - constraints one_of: [:PlaceRef, :GeographicSite, :GeographicLocation, :GeographicAddress] - end - - attribute :referred_type, :atom do - description "the type of the place" - allow_nil? true - public? true - constraints one_of: [:GeographicSite, :GeographicLocation, :GeographicAddress] - end - - create_timestamp :created_at - - update_timestamp :updated_at - end - - relationships do - has_many :place_refs, Diffo.Provider.PlaceRef do - description "the place refs relating this place to instances" - destination_attribute :place_id - public? true - end - end - - validations do - validate {Diffo.Validations.HrefEndsWithId, id: :id, href: :href} do - where [present(:id), present(:href)] - end - - validate attribute_equals(:type, :PlaceRef) do - where present(:referred_type) - message "when referred_type is present, type must be PlaceRef" - end - - validate attribute_does_not_equal(:type, :PlaceRef) do - where absent(:referred_type) - message "when referred_type is absent, type must be not be PlaceRef" - end - end - - preparations do - prepare build(sort: [id: :asc, name: :asc]) - end - end diff --git a/lib/diffo/provider/components/place/extension.ex b/lib/diffo/provider/components/place/extension.ex new file mode 100644 index 0000000..12235f8 --- /dev/null +++ b/lib/diffo/provider/components/place/extension.ex @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Place.Extension do + @moduledoc """ + DSL Extension customising a Place. + + Provides compile-time declaration blocks for domain-specific Place kinds + built on `Diffo.Provider.BasePlace`. All declarations are introspectable via + `Diffo.Provider.Place.Extension.Info`. + + See the [DSL cheat sheet](DSL-Diffo.Provider.Place.Extension.html) for the full DSL reference. + See `Diffo.Provider.BasePlace` for full usage documentation. + """ + use Spark.Dsl.Extension, sections: [] +end diff --git a/lib/diffo/provider/components/place/extension/info.ex b/lib/diffo/provider/components/place/extension/info.ex new file mode 100644 index 0000000..01bf39c --- /dev/null +++ b/lib/diffo/provider/components/place/extension/info.ex @@ -0,0 +1,16 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Place.Extension.Info do + use Spark.InfoGenerator, + extension: Diffo.Provider.Place.Extension, + sections: [] + + @doc "Returns true if the module is a BasePlace-derived resource" + @spec place?(module()) :: boolean() + def place?(module) do + Code.ensure_loaded?(module) and + Diffo.Provider.Place.Extension in Ash.Resource.Info.extensions(module) + end +end From 30f3d092961bce2a9ba6b75ed65fcb91c919be47 Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Wed, 29 Apr 2026 08:15:12 +0930 Subject: [PATCH 10/22] place extension --- .../provider/components/place/extension.ex | 96 ++++++++++++++++++- .../components/place/extension/info.ex | 2 +- .../place/extension/instance_role.ex | 14 +++ .../components/place/extension/party_role.ex | 14 +++ .../components/place/extension/place_role.ex | 14 +++ 5 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 lib/diffo/provider/components/place/extension/instance_role.ex create mode 100644 lib/diffo/provider/components/place/extension/party_role.ex create mode 100644 lib/diffo/provider/components/place/extension/place_role.ex diff --git a/lib/diffo/provider/components/place/extension.ex b/lib/diffo/provider/components/place/extension.ex index 12235f8..605b4ba 100644 --- a/lib/diffo/provider/components/place/extension.ex +++ b/lib/diffo/provider/components/place/extension.ex @@ -13,5 +13,99 @@ defmodule Diffo.Provider.Place.Extension do See the [DSL cheat sheet](DSL-Diffo.Provider.Place.Extension.html) for the full DSL reference. See `Diffo.Provider.BasePlace` for full usage documentation. """ - use Spark.Dsl.Extension, sections: [] + @instance_role %Spark.Dsl.Entity{ + name: :role, + describe: "Declares a role this Place kind plays with respect to Instances", + target: Diffo.Provider.Place.Extension.InstanceRole, + args: [:role, :instance_type], + schema: [ + role: [ + type: :atom, + required: true, + doc: "The role name, an atom" + ], + instance_type: [ + type: :any, + doc: "The module of the related Instance resource" + ] + ] + } + + @party_role %Spark.Dsl.Entity{ + name: :role, + describe: "Declares a role this Place kind plays with respect to Parties", + target: Diffo.Provider.Place.Extension.PartyRole, + args: [:role, :party_type], + schema: [ + role: [ + type: :atom, + required: true, + doc: "The role name, an atom" + ], + party_type: [ + type: :any, + doc: "The module of the related Party resource" + ] + ] + } + + @place_role %Spark.Dsl.Entity{ + name: :role, + describe: "Declares a role this Place kind plays with respect to other Places", + target: Diffo.Provider.Place.Extension.PlaceRole, + args: [:role, :place_type], + schema: [ + role: [ + type: :atom, + required: true, + doc: "The role name, an atom" + ], + place_type: [ + type: :any, + doc: "The module of the related Place resource" + ] + ] + } + + @instances %Spark.Dsl.Section{ + name: :instances, + describe: "Declares the roles this Place kind plays with respect to Instances", + examples: [ + """ + instances do + role :site_for, MyApp.AccessService + end + """ + ], + entities: [@instance_role] + } + + @parties %Spark.Dsl.Section{ + name: :parties, + describe: "Declares the roles this Place kind plays with respect to Parties", + examples: [ + """ + parties do + role :home_of, MyApp.Organization + end + """ + ], + entities: [@party_role] + } + + @places %Spark.Dsl.Section{ + name: :places, + describe: "Declares the roles this Place kind plays with respect to other Places", + examples: [ + """ + places do + role :within, MyApp.GeographicSite + end + """ + ], + entities: [@place_role] + } + + use Spark.Dsl.Extension, + sections: [@instances, @parties, @places] end diff --git a/lib/diffo/provider/components/place/extension/info.ex b/lib/diffo/provider/components/place/extension/info.ex index 01bf39c..4023595 100644 --- a/lib/diffo/provider/components/place/extension/info.ex +++ b/lib/diffo/provider/components/place/extension/info.ex @@ -5,7 +5,7 @@ defmodule Diffo.Provider.Place.Extension.Info do use Spark.InfoGenerator, extension: Diffo.Provider.Place.Extension, - sections: [] + sections: [:instances, :parties, :places] @doc "Returns true if the module is a BasePlace-derived resource" @spec place?(module()) :: boolean() diff --git a/lib/diffo/provider/components/place/extension/instance_role.ex b/lib/diffo/provider/components/place/extension/instance_role.ex new file mode 100644 index 0000000..b806e1e --- /dev/null +++ b/lib/diffo/provider/components/place/extension/instance_role.ex @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Place.Extension.InstanceRole do + @moduledoc """ + InstanceRole - DSL entity declaring a role this Place kind plays with respect to Instances + """ + defstruct [:role, :instance_type, __spark_metadata__: nil] + + defimpl String.Chars do + def to_string(struct), do: inspect(struct) + end +end diff --git a/lib/diffo/provider/components/place/extension/party_role.ex b/lib/diffo/provider/components/place/extension/party_role.ex new file mode 100644 index 0000000..266d16c --- /dev/null +++ b/lib/diffo/provider/components/place/extension/party_role.ex @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Place.Extension.PartyRole do + @moduledoc """ + PartyRole - DSL entity declaring a role this Place kind plays with respect to Parties + """ + defstruct [:role, :party_type, __spark_metadata__: nil] + + defimpl String.Chars do + def to_string(struct), do: inspect(struct) + end +end diff --git a/lib/diffo/provider/components/place/extension/place_role.ex b/lib/diffo/provider/components/place/extension/place_role.ex new file mode 100644 index 0000000..ffecfab --- /dev/null +++ b/lib/diffo/provider/components/place/extension/place_role.ex @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Place.Extension.PlaceRole do + @moduledoc """ + PlaceRole - DSL entity declaring a role this Place kind plays with respect to other Places + """ + defstruct [:role, :place_type, __spark_metadata__: nil] + + defimpl String.Chars do + def to_string(struct), do: inspect(struct) + end +end From 115fbfdaab37f8476ada0cd09acc0d8e4b15151d Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Wed, 29 Apr 2026 08:44:49 +0930 Subject: [PATCH 11/22] places do in instance and party extensions --- .../DSL-Diffo.Provider.Instance.Extension.md | 100 +++++++++++++++++- .../DSL-Diffo.Provider.Party.Extension.md | 49 +++++++++ .../use_diffo_provider_extension.livemd | 100 ++++++++++++++---- .../provider/components/base_instance.ex | 15 +++ lib/diffo/provider/components/base_place.ex | 2 + .../provider/components/instance/extension.ex | 72 ++++++++++++- .../extension/persisters/persist_places.ex | 21 ++++ .../instance/extension/place_declaration.ex | 15 +++ .../transformers/transform_behaviour.ex | 4 + .../provider/components/instance/info.ex | 12 +++ .../provider/components/party/extension.ex | 33 +++++- .../components/party/extension/info.ex | 2 +- .../components/party/extension/place_role.ex | 14 +++ test/instance_extension/transformer_test.exs | 46 ++++++++ test/support/resource/shelf.ex | 5 + 15 files changed, 463 insertions(+), 27 deletions(-) create mode 100644 lib/diffo/provider/components/instance/extension/persisters/persist_places.ex create mode 100644 lib/diffo/provider/components/instance/extension/place_declaration.ex create mode 100644 lib/diffo/provider/components/party/extension/place_role.ex diff --git a/documentation/dsls/DSL-Diffo.Provider.Instance.Extension.md b/documentation/dsls/DSL-Diffo.Provider.Instance.Extension.md index 6b5c7d3..2da6fc3 100644 --- a/documentation/dsls/DSL-Diffo.Provider.Instance.Extension.md +++ b/documentation/dsls/DSL-Diffo.Provider.Instance.Extension.md @@ -22,6 +22,8 @@ module at compile time via persisters and are introspectable at runtime via payload and an enabled/disabled default. - `parties do` — the party roles that instances of this kind relate to, with multiplicity, reference, and calculation options. +- `places do` — the place roles that instances of this kind relate to, mirroring `parties do` + in structure and options. ## behaviour @@ -39,7 +41,7 @@ See `Diffo.Provider.BaseInstance` for full usage documentation including generat ## structure -Defines the structural shape of the Instance — its specification, characteristics, features, and parties +Defines the structural shape of the Instance — its specification, characteristics, features, parties, and places ### Nested DSLs * [specification](#structure-specification) @@ -51,6 +53,9 @@ Defines the structural shape of the Instance — its specification, characterist * [parties](#structure-parties) * party * parties + * [places](#structure-places) + * place + * places ### Examples @@ -69,6 +74,10 @@ structure do parties do party :provider, MyApp.Provider end + + places do + place :installation_site, MyApp.GeographicSite + end end ``` @@ -326,6 +335,95 @@ Declares a plural party role on this Instance Target: `Diffo.Provider.Instance.Extension.PartyDeclaration` +### structure.places +List of Instance Place roles + +### Nested DSLs + * [place](#structure-places-place) + * [places](#structure-places-places) + + +### Examples +``` +places do + place :installation_site, MyApp.GeographicSite + places :coverage_areas, MyApp.GeographicLocation, constraints: [min: 1] + place :billing_address, MyApp.GeographicAddress, reference: true +end + +``` + + + + +### structure.places.place +```elixir +place role, place_type +``` + + +Declares a singular place role on this Instance + + + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`role`](#structure-places-place-role){: #structure-places-place-role .spark-required} | `atom` | | The role name, an atom | +| [`place_type`](#structure-places-place-place_type){: #structure-places-place-place_type } | `any` | | The module of the Place kind. A BasePlace-derived resource. | +### Options + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`reference`](#structure-places-place-reference){: #structure-places-place-reference } | `boolean` | `false` | If true, no direct PlaceRef edge is created; the place is reachable by graph traversal. | +| [`calculate`](#structure-places-place-calculate){: #structure-places-place-calculate } | `atom` | | Name of an Ash calculation on this resource that produces the place at build time. | + + + + + +### Introspection + +Target: `Diffo.Provider.Instance.Extension.PlaceDeclaration` + +### structure.places.places +```elixir +places role, place_type +``` + + +Declares a plural place role on this Instance + + + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`role`](#structure-places-places-role){: #structure-places-places-role .spark-required} | `atom` | | The role name, an atom | +| [`place_type`](#structure-places-places-place_type){: #structure-places-places-place_type } | `any` | | The module of the Place kind. A BasePlace-derived resource. | +### Options + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`reference`](#structure-places-places-reference){: #structure-places-places-reference } | `boolean` | `false` | If true, no direct PlaceRef edge is created; the place is reachable by graph traversal. | +| [`calculate`](#structure-places-places-calculate){: #structure-places-places-calculate } | `atom` | | Name of an Ash calculation on this resource that produces the place at build time. | +| [`constraints`](#structure-places-places-constraints){: #structure-places-places-constraints } | `keyword` | | Multiplicity constraints on the number of places in this role, e.g. [min: 1, max: 3] | + + + + + +### Introspection + +Target: `Diffo.Provider.Instance.Extension.PlaceDeclaration` + + diff --git a/documentation/dsls/DSL-Diffo.Provider.Party.Extension.md b/documentation/dsls/DSL-Diffo.Provider.Party.Extension.md index 92e0d82..643ce53 100644 --- a/documentation/dsls/DSL-Diffo.Provider.Party.Extension.md +++ b/documentation/dsls/DSL-Diffo.Provider.Party.Extension.md @@ -110,5 +110,54 @@ Target: `Diffo.Provider.Party.Extension.PartyRole` +## places +Declares the roles this Party kind plays with respect to Places + +### Nested DSLs + * [role](#places-role) + + +### Examples +``` +places do + role :headquartered_at, MyApp.GeographicSite +end + +``` + + + + +### places.role +```elixir +role role, place_type +``` + + +Declares a role this Party kind plays with respect to Places + + + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`role`](#places-role-role){: #places-role-role .spark-required} | `atom` | | The role name, an atom | +| [`place_type`](#places-role-place_type){: #places-role-place_type } | `any` | | The module of the related Place resource | + + + + + + +### Introspection + +Target: `Diffo.Provider.Party.Extension.PlaceRole` + + + + diff --git a/documentation/how_to/use_diffo_provider_extension.livemd b/documentation/how_to/use_diffo_provider_extension.livemd index 587fbfd..78f7394 100644 --- a/documentation/how_to/use_diffo_provider_extension.livemd +++ b/documentation/how_to/use_diffo_provider_extension.livemd @@ -35,6 +35,7 @@ In this 'Diffo Provider Instance Extension' livebook you will learn about: * Using the Assigner * Composing a Resource from partially assigned Resources * Declaring domain Parties using the Party Extension +* Declaring domain Places using the Place Extension ### Installing Neo4j and Configuring Bolty @@ -146,7 +147,7 @@ Diffo also has an inbuilt Spark DSL extension [Diffo.Provider.Instance.Extension The extension has two top-level sections: -**`structure do`** — describes the static shape of the Instance kind: its TMF Specification, Characteristics, Features, and Party roles. All declarations are baked into the module at compile time and introspectable at runtime via generated functions (`specification/0`, `characteristics/0`, `features/0`, `parties/0`) and `Diffo.Provider.Instance.Info`. +**`structure do`** — describes the static shape of the Instance kind: its TMF Specification, Characteristics, Features, Party roles, and Place roles. All declarations are baked into the module at compile time and introspectable at runtime via generated functions (`specification/0`, `characteristics/0`, `features/0`, `parties/0`, `places/0`) and `Diffo.Provider.Instance.Info`. **`behaviour do`** — declares which Ash actions should be wired for instance lifecycle management. Declaring `create :name` injects `:specified_by`, `:features`, and `:characteristics` arguments onto that action, and the `BuildBefore`/`BuildAfter` changes registered on `BaseInstance` automatically handle specification upsert, feature and characteristic creation, party validation, and graph relationship wiring for every create action. You write the action body for your domain-specific accepts and arguments; the structural wiring is handled for you. @@ -208,6 +209,10 @@ defmodule Diffo.Compute.Cluster do party :operator, Tenant party :manager, Engineer end + + places do + place :data_centre, Diffo.Compute.DataCentre + end end behaviour do @@ -507,6 +512,60 @@ defmodule Diffo.Compute.Engineer do end ``` +## Place Extension + +`Diffo.Provider.BasePlace` is an Ash Resource Fragment for domain-specific Place kinds, mirroring `BaseInstance` and `BaseParty`. It provides common Place attributes — `id`, `href`, `name`, `type`, `referred_type` — and the `Diffo.Provider.Place.Extension` DSL, which lets a Place kind declare the roles it plays with respect to Instances, Parties, and other Places. + +`type` defaults to `:PlaceRef` and is typically set in the `build` action to the concrete place type (`:GeographicSite`, `:GeographicLocation`, or `:GeographicAddress`). When `referred_type` is present, `type` must be `:PlaceRef` — meaning this Place is a reference rather than a physical location. + +The `Diffo.Provider.Place.Extension` DSL cheat sheet is at [DSL-Diffo.Provider.Place.Extension](https://hexdocs.pm/diffo/DSL-Diffo.Provider.Place.Extension.html). + +### Defining Place kinds + +We'll add a `DataCentre` Place kind to our Compute domain. Clusters are hosted at a data centre; the `instances do` block records that relationship from the DataCentre's perspective. + +```elixir +defmodule Diffo.Compute.DataCentre do + @moduledoc """ + DataCentre in the Compute domain + """ + + alias Diffo.Provider.BasePlace + alias Diffo.Compute + + use Ash.Resource, + fragments: [BasePlace], + domain: Compute + + resource do + description "A Compute Data Centre" + plural_name :data_centres + end + + jason do + pick [:id, :href, :name, :type] + compact true + rename type: "@type" + end + + outstanding do + expect [:id, :name, :type] + end + + actions do + create :build do + accept [:id, :href, :name] + change set_attribute(:type, :GeographicSite) + end + end + + instances do + role :data_centre, Diffo.Compute.Cluster + role :data_centre, Diffo.Compute.GPU + end +end +``` + ### Compute Domain With all resources defined we can now declare the `Diffo.Compute` domain, which exposes a typed API for each resource: @@ -525,6 +584,7 @@ defmodule Diffo.Compute do alias Diffo.Compute.Cluster alias Diffo.Compute.Tenant alias Diffo.Compute.Engineer + alias Diffo.Compute.DataCentre resources do resource GPU do @@ -561,6 +621,11 @@ defmodule Diffo.Compute do define :get_engineer_by_id, action: :read, get_by: :id define :list_engineers, action: :read end + + resource DataCentre do + define :create_data_centre, action: :build + define :get_data_centre_by_id, action: :read, get_by: :id + end end end ``` @@ -591,27 +656,18 @@ alias Diffo.Provider.Instance.Party ### Creating a Cluster -We'll use a helper module to set up the data centre place: +First we create the data centre — our `DataCentre` resource uses `BasePlace`, so it is managed via the Compute domain API like any other domain resource: ```elixir -defmodule Diffo.Compute.Test do - alias Diffo.Provider - alias Diffo.Provider.Instance.Place - - def create_data_centre_place do - dc = - Provider.create_place!(%{ - id: "NXTM2", - name: :dataCentreId, - href: "place/compute/NXTM2", - referred_type: :GeographicSite - }) - - %Place{id: dc.id, role: :dataCentre} - end -end +alias Diffo.Provider.Instance.Place + +{:ok, dc} = Compute.create_data_centre(%{id: "NXTM2", name: "NextDC M2"}) +``` -places = [Diffo.Compute.Test.create_data_centre_place()] +Now build the cluster, passing the data centre as a place and our party members by id and role: + +```elixir +places = [%Place{id: dc.id, role: :data_centre}] parties = [ %Party{id: tenant.id, role: :operator}, %Party{id: engineer.id, role: :manager} @@ -682,10 +738,10 @@ What happens when I request a specific assignment from an instance to which the ### What Next? -In this tutorial you've used Diffo's Provider Instance Extension to define a Compute domain with a composite Cluster resource comprised of assigned GPU cores, and the Provider Party Extension to define Tenant and Engineer party kinds that operate and manage those resources. +In this tutorial you've used Diffo's Provider Instance Extension to define a Compute domain with a composite Cluster resource comprised of assigned GPU cores, the Provider Party Extension to define Tenant and Engineer party kinds that operate and manage those resources, and the Provider Place Extension to declare where instances and parties exist geographically. -`BaseParty` follows the same pattern as `BaseInstance` — domain-specific party resources use it as a fragment and write their own `build` action for domain-specific attributes. No manual wiring is needed. +`BaseParty` and `BasePlace` follow the same pattern as `BaseInstance` — domain-specific resources use them as fragments and write their own actions for domain-specific attributes. No manual wiring is needed. -A `BasePlace` extension for domain-specific Place kinds (such as a DataCentre with its own attributes) follows the same pattern and will be added in a future release. +Domain-specific Place kinds (such as a DataCentre with its own attributes) use `BasePlace` as a fragment and declare their roles via `instances do`, `parties do`, and `places do` sections on `Diffo.Provider.Place.Extension`. Party kinds similarly declare their place roles via `places do` on `Diffo.Provider.Party.Extension`. If you find Diffo useful please visit and star on [github](https://github.com/diffo-dev/diffo/). Feel free to join discussions and raise issues to discuss PR's. diff --git a/lib/diffo/provider/components/base_instance.ex b/lib/diffo/provider/components/base_instance.ex index d927cb4..c8468b3 100644 --- a/lib/diffo/provider/components/base_instance.ex +++ b/lib/diffo/provider/components/base_instance.ex @@ -41,6 +41,15 @@ defmodule Diffo.Provider.BaseInstance do - `reference: true` — no direct `PartyRef` edge; party is reachable by graph traversal - `calculate:` — names an Ash calculation on this resource that produces the party at build time + `places do` — declares the Place roles this Instance kind relates to. Mirrors `parties do` + in structure: + + places do + place :installation_site, MyApp.GeographicSite + places :coverage_areas, MyApp.GeographicLocation, constraints: [min: 1] + place :billing_address, MyApp.GeographicAddress, reference: true + end + All declarations are introspectable at runtime via `Diffo.Provider.Instance.Info` and at compile time via `Diffo.Provider.Instance.Extension.Info`. @@ -62,10 +71,12 @@ defmodule Diffo.Provider.BaseInstance do - `characteristics/0` — list of `Characteristic` structs - `features/0` — list of `Feature` structs - `parties/0` — list of `PartyDeclaration` structs + - `places/0` — list of `PlaceDeclaration` structs - `characteristic/1` — returns the named `Characteristic` or `nil` - `feature/1` — returns the named `Feature` or `nil` - `feature_characteristic/2` — returns the named characteristic within a feature, or `nil` - `party/1` — returns the `PartyDeclaration` for the given role, or `nil` + - `place/1` — returns the `PlaceDeclaration` for the given role, or `nil` - `build_before/1` — called automatically before every create action; upserts the specification and creates features, characteristics, and parties, setting their ids as action arguments @@ -96,6 +107,10 @@ defmodule Diffo.Provider.BaseInstance do party :operator, MyApp.Organization parties :installer, MyApp.Engineer end + + places do + place :site, MyApp.GeographicSite + end end behaviour do diff --git a/lib/diffo/provider/components/base_place.ex b/lib/diffo/provider/components/base_place.ex index 0d93633..0e478ad 100644 --- a/lib/diffo/provider/components/base_place.ex +++ b/lib/diffo/provider/components/base_place.ex @@ -74,6 +74,8 @@ defmodule Diffo.Provider.BasePlace do relate [ {:place_refs, :RELATES, :incoming, :PlaceRef} ] + + label :Place end attributes do diff --git a/lib/diffo/provider/components/instance/extension.ex b/lib/diffo/provider/components/instance/extension.ex index 222acc1..1376116 100644 --- a/lib/diffo/provider/components/instance/extension.ex +++ b/lib/diffo/provider/components/instance/extension.ex @@ -23,6 +23,8 @@ defmodule Diffo.Provider.Instance.Extension do payload and an enabled/disabled default. - `parties do` — the party roles that instances of this kind relate to, with multiplicity, reference, and calculation options. + - `places do` — the place roles that instances of this kind relate to, mirroring `parties do` + in structure and options. ## behaviour @@ -222,9 +224,70 @@ defmodule Diffo.Provider.Instance.Extension do entities: [@party_entity, @parties_entity] } + @place_schema [ + role: [ + doc: "The role name, an atom", + type: :atom, + required: true + ], + place_type: [ + doc: "The module of the Place kind. A BasePlace-derived resource.", + type: :any + ], + reference: [ + doc: "If true, no direct PlaceRef edge is created; the place is reachable by graph traversal.", + type: :boolean, + default: false + ], + calculate: [ + doc: "Name of an Ash calculation on this resource that produces the place at build time.", + type: :atom + ] + ] + + @place_entity %Spark.Dsl.Entity{ + name: :place, + describe: "Declares a singular place role on this Instance", + target: Diffo.Provider.Instance.Extension.PlaceDeclaration, + args: [:role, :place_type], + auto_set_fields: [multiple: false], + schema: @place_schema + } + + @places_entity %Spark.Dsl.Entity{ + name: :places, + describe: "Declares a plural place role on this Instance", + target: Diffo.Provider.Instance.Extension.PlaceDeclaration, + args: [:role, :place_type], + auto_set_fields: [multiple: true], + schema: + @place_schema ++ + [ + constraints: [ + doc: "Multiplicity constraints on the number of places in this role, e.g. [min: 1, max: 3]", + type: :keyword_list + ] + ] + } + + @places %Spark.Dsl.Section{ + name: :places, + describe: "List of Instance Place roles", + examples: [ + """ + places do + place :installation_site, MyApp.GeographicSite + places :coverage_areas, MyApp.GeographicLocation, constraints: [min: 1] + place :billing_address, MyApp.GeographicAddress, reference: true + end + """ + ], + entities: [@place_entity, @places_entity] + } + @structure %Spark.Dsl.Section{ name: :structure, - describe: "Defines the structural shape of the Instance — its specification, characteristics, features, and parties", + describe: "Defines the structural shape of the Instance — its specification, characteristics, features, parties, and places", examples: [ """ structure do @@ -241,10 +304,14 @@ defmodule Diffo.Provider.Instance.Extension do parties do party :provider, MyApp.Provider end + + places do + place :installation_site, MyApp.GeographicSite + end end """ ], - sections: [@specification, @characteristics, @features, @parties] + sections: [@specification, @characteristics, @features, @parties, @places] } # ── behaviour ────────────────────────────────────────────────────────────── @@ -314,6 +381,7 @@ defmodule Diffo.Provider.Instance.Extension do Diffo.Provider.Instance.Extension.Persisters.PersistCharacteristics, Diffo.Provider.Instance.Extension.Persisters.PersistFeatures, Diffo.Provider.Instance.Extension.Persisters.PersistParties, + Diffo.Provider.Instance.Extension.Persisters.PersistPlaces, Diffo.Provider.Instance.Extension.Transformers.TransformBehaviour ], verifiers: [ diff --git a/lib/diffo/provider/components/instance/extension/persisters/persist_places.ex b/lib/diffo/provider/components/instance/extension/persisters/persist_places.ex new file mode 100644 index 0000000..ec90dd6 --- /dev/null +++ b/lib/diffo/provider/components/instance/extension/persisters/persist_places.ex @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Instance.Extension.Persisters.PersistPlaces do + @moduledoc "Persists place declarations and bakes places/0" + use Spark.Dsl.Transformer + alias Spark.Dsl.Transformer + + @impl true + def transform(dsl_state) do + declarations = Transformer.get_entities(dsl_state, [:structure, :places]) + escaped = Macro.escape(declarations) + dsl_state = Transformer.persist(dsl_state, :places, declarations) + + {:ok, Transformer.eval(dsl_state, [], quote do + @doc false + def places, do: unquote(escaped) + end)} + end +end diff --git a/lib/diffo/provider/components/instance/extension/place_declaration.ex b/lib/diffo/provider/components/instance/extension/place_declaration.ex new file mode 100644 index 0000000..1d53d72 --- /dev/null +++ b/lib/diffo/provider/components/instance/extension/place_declaration.ex @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Instance.Extension.PlaceDeclaration do + @moduledoc """ + PlaceDeclaration - DSL entity declaring a place role on an Instance + """ + defstruct [:role, :place_type, :multiple, :reference, :calculate, :constraints, + __spark_metadata__: nil] + + defimpl String.Chars do + def to_string(struct), do: inspect(struct) + end +end diff --git a/lib/diffo/provider/components/instance/extension/transformers/transform_behaviour.ex b/lib/diffo/provider/components/instance/extension/transformers/transform_behaviour.ex index 5aea342..aac21c2 100644 --- a/lib/diffo/provider/components/instance/extension/transformers/transform_behaviour.ex +++ b/lib/diffo/provider/components/instance/extension/transformers/transform_behaviour.ex @@ -62,6 +62,9 @@ defmodule Diffo.Provider.Instance.Extension.Transformers.TransformBehaviour do @doc false def party(role), do: Enum.find(parties(), &(&1.role == role)) + + @doc false + def place(role), do: Enum.find(places(), &(&1.role == role)) end)} end @@ -100,5 +103,6 @@ defmodule Diffo.Provider.Instance.Extension.Transformers.TransformBehaviour do def after?(Diffo.Provider.Instance.Extension.Persisters.PersistCharacteristics), do: true def after?(Diffo.Provider.Instance.Extension.Persisters.PersistFeatures), do: true def after?(Diffo.Provider.Instance.Extension.Persisters.PersistParties), do: true + def after?(Diffo.Provider.Instance.Extension.Persisters.PersistPlaces), do: true def after?(_), do: false end diff --git a/lib/diffo/provider/components/instance/info.ex b/lib/diffo/provider/components/instance/info.ex index ae17ac9..31c9c45 100644 --- a/lib/diffo/provider/components/instance/info.ex +++ b/lib/diffo/provider/components/instance/info.ex @@ -57,4 +57,16 @@ defmodule Diffo.Provider.Instance.Info do def party(resource, role) do Enum.find(parties(resource), &(&1.role == role)) end + + @doc "Returns the list of place role declarations for the resource" + @spec places(Ash.Resource.t()) :: list() | [] + def places(resource) do + Extension.get_persisted(resource, :places, []) + end + + @doc "Returns the place declaration for the given role, or nil" + @spec place(Ash.Resource.t(), atom()) :: struct() | nil + def place(resource, role) do + Enum.find(places(resource), &(&1.role == role)) + end end diff --git a/lib/diffo/provider/components/party/extension.ex b/lib/diffo/provider/components/party/extension.ex index 865738f..3e1a58d 100644 --- a/lib/diffo/provider/components/party/extension.ex +++ b/lib/diffo/provider/components/party/extension.ex @@ -74,6 +74,37 @@ defmodule Diffo.Provider.Party.Extension do entities: [@party_role] } + @place_role %Spark.Dsl.Entity{ + name: :role, + describe: "Declares a role this Party kind plays with respect to Places", + target: Diffo.Provider.Party.Extension.PlaceRole, + args: [:role, :place_type], + schema: [ + role: [ + type: :atom, + required: true, + doc: "The role name, an atom" + ], + place_type: [ + type: :any, + doc: "The module of the related Place resource" + ] + ] + } + + @places %Spark.Dsl.Section{ + name: :places, + describe: "Declares the roles this Party kind plays with respect to Places", + examples: [ + """ + places do + role :headquartered_at, MyApp.GeographicSite + end + """ + ], + entities: [@place_role] + } + use Spark.Dsl.Extension, - sections: [@instances, @parties] + sections: [@instances, @parties, @places] end diff --git a/lib/diffo/provider/components/party/extension/info.ex b/lib/diffo/provider/components/party/extension/info.ex index 8c29009..2ca0532 100644 --- a/lib/diffo/provider/components/party/extension/info.ex +++ b/lib/diffo/provider/components/party/extension/info.ex @@ -5,7 +5,7 @@ defmodule Diffo.Provider.Party.Extension.Info do use Spark.InfoGenerator, extension: Diffo.Provider.Party.Extension, - sections: [:instances, :parties] + sections: [:instances, :parties, :places] @doc "Returns true if the module is a BaseParty-derived resource" @spec party?(module()) :: boolean() diff --git a/lib/diffo/provider/components/party/extension/place_role.ex b/lib/diffo/provider/components/party/extension/place_role.ex new file mode 100644 index 0000000..8a2c0c7 --- /dev/null +++ b/lib/diffo/provider/components/party/extension/place_role.ex @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Party.Extension.PlaceRole do + @moduledoc """ + PlaceRole - DSL entity declaring a role this Party kind plays with respect to Places + """ + defstruct [:role, :place_type, __spark_metadata__: nil] + + defimpl String.Chars do + def to_string(struct), do: inspect(struct) + end +end diff --git a/test/instance_extension/transformer_test.exs b/test/instance_extension/transformer_test.exs index 2d9363a..a32ca0e 100644 --- a/test/instance_extension/transformer_test.exs +++ b/test/instance_extension/transformer_test.exs @@ -11,6 +11,7 @@ defmodule Diffo.InstanceExtension.TransformerTest do alias Diffo.Provider.Instance.Characteristic alias Diffo.Provider.Instance.Feature alias Diffo.Provider.Instance.Info + alias Diffo.Provider.Instance.Extension.PlaceDeclaration describe "PersistSpecification" do test "bakes specification/0 onto the resource" do @@ -162,6 +163,41 @@ defmodule Diffo.InstanceExtension.TransformerTest do end end + describe "PersistPlaces" do + test "bakes places/0 onto the resource" do + places = Shelf.places() + assert is_list(places) + assert length(places) == 2 + roles = Enum.map(places, & &1.role) + assert :installation_site in roles + assert :billing_address in roles + end + + test "each place is a PlaceDeclaration struct" do + [first | _] = Shelf.places() + assert is_struct(first, PlaceDeclaration) + end + + test "reference place has reference flag set" do + billing = Enum.find(Shelf.places(), &(&1.role == :billing_address)) + assert billing.reference == true + end + + test "places are also accessible via Info" do + assert length(Info.places(Shelf)) == 2 + assert Info.places(Card) == [] + end + + test "Info.place/2 returns the named place declaration by role" do + p = Info.place(Shelf, :installation_site) + assert p.role == :installation_site + end + + test "Info.place/2 returns nil for unknown role" do + assert Info.place(Shelf, :nonexistent) == nil + end + end + describe "TransformBehaviour" do setup do Code.ensure_loaded!(Shelf) @@ -241,5 +277,15 @@ defmodule Diffo.InstanceExtension.TransformerTest do test "party/1 returns nil for unknown role" do assert Shelf.party(:nonexistent) == nil end + + test "place/1 returns the named place declaration by role" do + p = Shelf.place(:installation_site) + assert p.role == :installation_site + assert p.multiple == false + end + + test "place/1 returns nil for unknown role" do + assert Shelf.place(:nonexistent) == nil + end end end diff --git a/test/support/resource/shelf.ex b/test/support/resource/shelf.ex index b9022b3..898f10e 100644 --- a/test/support/resource/shelf.ex +++ b/test/support/resource/shelf.ex @@ -59,6 +59,11 @@ defmodule Diffo.Test.Shelf do party :manager, Diffo.Test.Organization, calculate: :manager_calc parties :installer, Diffo.Test.Person, constraints: [min: 1, max: 3] end + + places do + place :installation_site, Diffo.Provider.Place + place :billing_address, Diffo.Provider.Place, reference: true + end end behaviour do From 2308991564ded525ceda023fef8229fc468f07e0 Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Wed, 29 Apr 2026 10:04:12 +0930 Subject: [PATCH 12/22] =?UTF-8?q?Place=20DSL=20=E2=80=94=20BasePlace=20fra?= =?UTF-8?q?gment,=20Place/Party/Instance=20Extension=20sections,=20persist?= =?UTF-8?q?ers,=20verifiers,=20and=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extracts BasePlace as a Spark.Dsl.Fragment following the BaseParty pattern. Adds instances/parties/places DSL sections to Place.Extension and Party.Extension, with persisters baking role declarations onto resources at compile time and a VerifyRoles verifier checking for duplicates and correct base types across all sections. Adds places do to Instance.Extension structure do, with PersistPlaces and place/1 generated via TransformBehaviour. Four test fixtures illustrate the simple and complex patterns for both Party and Place. Moduledocs across BaseInstance, BaseParty, and BasePlace now document the domain-specific attributes contract explicitly. Livebook updated with a Place Extension section and simplified cluster creation using the domain API. --- lib/diffo/provider/components/base_party.ex | 26 +++ lib/diffo/provider/components/base_place.ex | 26 +++ .../provider/components/party/extension.ex | 10 +- .../extension/persisters/persist_instances.ex | 21 ++ .../extension/persisters/persist_parties.ex | 21 ++ .../extension/persisters/persist_places.ex | 21 ++ .../party/extension/verifiers/verify_roles.ex | 79 +++++++ .../provider/components/place/extension.ex | 10 +- .../extension/persisters/persist_instances.ex | 21 ++ .../extension/persisters/persist_parties.ex | 21 ++ .../extension/persisters/persist_places.ex | 21 ++ .../place/extension/verifiers/verify_roles.ex | 79 +++++++ test/instance_extension/party_test.exs | 50 ++++- test/instance_extension/place_test.exs | 133 +++++++++++ test/party_extension/transformer_test.exs | 87 ++++++++ test/party_extension/verifier_test.exs | 207 ++++++++++++++++++ test/place_extension/transformer_test.exs | 65 ++++++ test/place_extension/verifier_test.exs | 207 ++++++++++++++++++ test/support/nbn.ex | 20 +- test/support/resource/carrier.ex | 62 ++++++ test/support/resource/exchange_building.ex | 64 ++++++ test/support/resource/geographic_site.ex | 52 +++++ test/support/resource/organization.ex | 8 + test/support/resource/person.ex | 8 + 24 files changed, 1310 insertions(+), 9 deletions(-) create mode 100644 lib/diffo/provider/components/party/extension/persisters/persist_instances.ex create mode 100644 lib/diffo/provider/components/party/extension/persisters/persist_parties.ex create mode 100644 lib/diffo/provider/components/party/extension/persisters/persist_places.ex create mode 100644 lib/diffo/provider/components/party/extension/verifiers/verify_roles.ex create mode 100644 lib/diffo/provider/components/place/extension/persisters/persist_instances.ex create mode 100644 lib/diffo/provider/components/place/extension/persisters/persist_parties.ex create mode 100644 lib/diffo/provider/components/place/extension/persisters/persist_places.ex create mode 100644 lib/diffo/provider/components/place/extension/verifiers/verify_roles.ex create mode 100644 test/instance_extension/place_test.exs create mode 100644 test/party_extension/transformer_test.exs create mode 100644 test/party_extension/verifier_test.exs create mode 100644 test/place_extension/transformer_test.exs create mode 100644 test/place_extension/verifier_test.exs create mode 100644 test/support/resource/carrier.ex create mode 100644 test/support/resource/exchange_building.ex create mode 100644 test/support/resource/geographic_site.ex diff --git a/lib/diffo/provider/components/base_party.ex b/lib/diffo/provider/components/base_party.ex index 3498f19..5840ecc 100644 --- a/lib/diffo/provider/components/base_party.ex +++ b/lib/diffo/provider/components/base_party.ex @@ -76,6 +76,32 @@ defmodule Diffo.Provider.BaseParty do end end + ## Domain-specific attributes + + Add Ash `attribute` declarations directly to your derived resource for any fields beyond the + base set. Those attributes can only be set via actions you declare on the derived resource — + the base `create` action provided by `BaseParty` only accepts the base fields (`id`, `href`, + `name`, `type`, `referred_type`). Use your domain API to call the derived resource's action: + + defmodule MyApp.Carrier do + use Ash.Resource, fragments: [BaseParty], domain: MyApp.Domain + + attributes do + attribute :abn, :string, public?: true + attribute :carrier_code, :string, public?: true + end + + actions do + create :build do + accept [:id, :href, :name, :abn, :carrier_code] + change set_attribute(:type, :Organization) + end + end + end + + # Use the domain API — Provider.create_party!/1 does not know about :abn + MyApp.Domain.create_carrier!(%{name: "Acme", abn: "51824753556", carrier_code: "ACM"}) + ## TMF type and referred_type The `type` and `referred_type` attributes map to the TMF `@type` and `@referredType` JSON diff --git a/lib/diffo/provider/components/base_place.ex b/lib/diffo/provider/components/base_place.ex index 0e478ad..f29005c 100644 --- a/lib/diffo/provider/components/base_place.ex +++ b/lib/diffo/provider/components/base_place.ex @@ -53,6 +53,32 @@ defmodule Diffo.Provider.BasePlace do end end + ## Domain-specific attributes + + Add Ash `attribute` declarations directly to your derived resource for any fields beyond the + base set. Those attributes can only be set via actions you declare on the derived resource — + the base `create` action provided by `BasePlace` only accepts the base fields (`id`, `href`, + `name`, `type`, `referred_type`). Use your domain API to call the derived resource's action: + + defmodule MyApp.DataCentre do + use Ash.Resource, fragments: [BasePlace], domain: MyApp.Domain + + attributes do + attribute :tier, :integer, public?: true + attribute :power_capacity_kw, :integer, public?: true + end + + actions do + create :build do + accept [:id, :href, :name, :tier, :power_capacity_kw] + change set_attribute(:type, :GeographicSite) + end + end + end + + # Use the domain API — Provider.create_place!/1 does not know about :tier + MyApp.Domain.create_data_centre!(%{name: "M2", tier: 3, power_capacity_kw: 40_000}) + ## TMF type and referred_type The `type` and `referred_type` attributes map to the TMF `@type` and `@referredType` JSON diff --git a/lib/diffo/provider/components/party/extension.ex b/lib/diffo/provider/components/party/extension.ex index 3e1a58d..a205402 100644 --- a/lib/diffo/provider/components/party/extension.ex +++ b/lib/diffo/provider/components/party/extension.ex @@ -106,5 +106,13 @@ defmodule Diffo.Provider.Party.Extension do } use Spark.Dsl.Extension, - sections: [@instances, @parties, @places] + sections: [@instances, @parties, @places], + persisters: [ + Diffo.Provider.Party.Extension.Persisters.PersistInstances, + Diffo.Provider.Party.Extension.Persisters.PersistParties, + Diffo.Provider.Party.Extension.Persisters.PersistPlaces + ], + verifiers: [ + Diffo.Provider.Party.Extension.Verifiers.VerifyRoles + ] end diff --git a/lib/diffo/provider/components/party/extension/persisters/persist_instances.ex b/lib/diffo/provider/components/party/extension/persisters/persist_instances.ex new file mode 100644 index 0000000..49823d1 --- /dev/null +++ b/lib/diffo/provider/components/party/extension/persisters/persist_instances.ex @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Party.Extension.Persisters.PersistInstances do + @moduledoc "Persists instance role declarations and bakes instances/0" + use Spark.Dsl.Transformer + alias Spark.Dsl.Transformer + + @impl true + def transform(dsl_state) do + declarations = Transformer.get_entities(dsl_state, [:instances]) + escaped = Macro.escape(declarations) + dsl_state = Transformer.persist(dsl_state, :instances, declarations) + + {:ok, Transformer.eval(dsl_state, [], quote do + @doc false + def instances, do: unquote(escaped) + end)} + end +end diff --git a/lib/diffo/provider/components/party/extension/persisters/persist_parties.ex b/lib/diffo/provider/components/party/extension/persisters/persist_parties.ex new file mode 100644 index 0000000..f6e6590 --- /dev/null +++ b/lib/diffo/provider/components/party/extension/persisters/persist_parties.ex @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Party.Extension.Persisters.PersistParties do + @moduledoc "Persists party role declarations and bakes parties/0" + use Spark.Dsl.Transformer + alias Spark.Dsl.Transformer + + @impl true + def transform(dsl_state) do + declarations = Transformer.get_entities(dsl_state, [:parties]) + escaped = Macro.escape(declarations) + dsl_state = Transformer.persist(dsl_state, :parties, declarations) + + {:ok, Transformer.eval(dsl_state, [], quote do + @doc false + def parties, do: unquote(escaped) + end)} + end +end diff --git a/lib/diffo/provider/components/party/extension/persisters/persist_places.ex b/lib/diffo/provider/components/party/extension/persisters/persist_places.ex new file mode 100644 index 0000000..453b1de --- /dev/null +++ b/lib/diffo/provider/components/party/extension/persisters/persist_places.ex @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Party.Extension.Persisters.PersistPlaces do + @moduledoc "Persists place role declarations and bakes places/0" + use Spark.Dsl.Transformer + alias Spark.Dsl.Transformer + + @impl true + def transform(dsl_state) do + declarations = Transformer.get_entities(dsl_state, [:places]) + escaped = Macro.escape(declarations) + dsl_state = Transformer.persist(dsl_state, :places, declarations) + + {:ok, Transformer.eval(dsl_state, [], quote do + @doc false + def places, do: unquote(escaped) + end)} + end +end diff --git a/lib/diffo/provider/components/party/extension/verifiers/verify_roles.ex b/lib/diffo/provider/components/party/extension/verifiers/verify_roles.ex new file mode 100644 index 0000000..d2bdba5 --- /dev/null +++ b/lib/diffo/provider/components/party/extension/verifiers/verify_roles.ex @@ -0,0 +1,79 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Party.Extension.Verifiers.VerifyRoles do + @moduledoc "Verifies role declarations across instances, parties, and places sections" + use Spark.Dsl.Verifier + + alias Spark.Dsl.Verifier + alias Spark.Error.DslError + alias Diffo.Provider.Instance.Extension.Info, as: InstanceInfo + alias Diffo.Provider.Party.Extension.Info, as: PartyInfo + alias Diffo.Provider.Place.Extension.Info, as: PlaceInfo + + @impl true + def verify(dsl_state) do + resource = Verifier.get_persisted(dsl_state, :module) + + errors = + check_section(dsl_state, [:instances], :party_type, &InstanceInfo.instance?/1, + "instances", "instance_type", "BaseInstance", resource) ++ + check_section(dsl_state, [:parties], :party_type, &PartyInfo.party?/1, + "parties", "party_type", "BaseParty", resource) ++ + check_section(dsl_state, [:places], :place_type, &PlaceInfo.place?/1, + "places", "place_type", "BasePlace", resource) + + case errors do + [] -> :ok + _ -> {:error, errors} + end + end + + defp check_section(dsl_state, path, type_field, type_check?, section, field, base, resource) do + entities = Verifier.get_entities(dsl_state, path) + duplicate_errors(entities, section, resource) ++ + type_errors(entities, type_field, type_check?, section, field, base, resource) + end + + defp duplicate_errors(entities, section, resource) do + entities + |> Enum.group_by(& &1.role) + |> Enum.filter(fn {_role, list} -> length(list) > 1 end) + |> Enum.map(fn {role, _} -> + DslError.exception( + module: resource, + path: [String.to_atom(section)], + message: "#{section}: role #{inspect(role)} is declared more than once" + ) + end) + end + + defp type_errors(entities, type_field, type_check?, section, field, base, resource) do + Enum.reduce(entities, [], fn entity, acc -> + mod = Map.get(entity, type_field) + + cond do + is_nil(mod) -> + acc + + !Code.ensure_loaded?(mod) -> + [DslError.exception( + module: resource, + path: [String.to_atom(section)], + message: "#{section}: #{field} #{inspect(mod)} does not exist" + ) | acc] + + !type_check?.(mod) -> + [DslError.exception( + module: resource, + path: [String.to_atom(section)], + message: "#{section}: #{field} #{inspect(mod)} does not extend #{base}" + ) | acc] + + true -> + acc + end + end) + end +end diff --git a/lib/diffo/provider/components/place/extension.ex b/lib/diffo/provider/components/place/extension.ex index 605b4ba..74df77a 100644 --- a/lib/diffo/provider/components/place/extension.ex +++ b/lib/diffo/provider/components/place/extension.ex @@ -107,5 +107,13 @@ defmodule Diffo.Provider.Place.Extension do } use Spark.Dsl.Extension, - sections: [@instances, @parties, @places] + sections: [@instances, @parties, @places], + persisters: [ + Diffo.Provider.Place.Extension.Persisters.PersistInstances, + Diffo.Provider.Place.Extension.Persisters.PersistParties, + Diffo.Provider.Place.Extension.Persisters.PersistPlaces + ], + verifiers: [ + Diffo.Provider.Place.Extension.Verifiers.VerifyRoles + ] end diff --git a/lib/diffo/provider/components/place/extension/persisters/persist_instances.ex b/lib/diffo/provider/components/place/extension/persisters/persist_instances.ex new file mode 100644 index 0000000..d64d3f3 --- /dev/null +++ b/lib/diffo/provider/components/place/extension/persisters/persist_instances.ex @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Place.Extension.Persisters.PersistInstances do + @moduledoc "Persists instance role declarations and bakes instances/0" + use Spark.Dsl.Transformer + alias Spark.Dsl.Transformer + + @impl true + def transform(dsl_state) do + declarations = Transformer.get_entities(dsl_state, [:instances]) + escaped = Macro.escape(declarations) + dsl_state = Transformer.persist(dsl_state, :instances, declarations) + + {:ok, Transformer.eval(dsl_state, [], quote do + @doc false + def instances, do: unquote(escaped) + end)} + end +end diff --git a/lib/diffo/provider/components/place/extension/persisters/persist_parties.ex b/lib/diffo/provider/components/place/extension/persisters/persist_parties.ex new file mode 100644 index 0000000..6612423 --- /dev/null +++ b/lib/diffo/provider/components/place/extension/persisters/persist_parties.ex @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Place.Extension.Persisters.PersistParties do + @moduledoc "Persists party role declarations and bakes parties/0" + use Spark.Dsl.Transformer + alias Spark.Dsl.Transformer + + @impl true + def transform(dsl_state) do + declarations = Transformer.get_entities(dsl_state, [:parties]) + escaped = Macro.escape(declarations) + dsl_state = Transformer.persist(dsl_state, :parties, declarations) + + {:ok, Transformer.eval(dsl_state, [], quote do + @doc false + def parties, do: unquote(escaped) + end)} + end +end diff --git a/lib/diffo/provider/components/place/extension/persisters/persist_places.ex b/lib/diffo/provider/components/place/extension/persisters/persist_places.ex new file mode 100644 index 0000000..3fa789d --- /dev/null +++ b/lib/diffo/provider/components/place/extension/persisters/persist_places.ex @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Place.Extension.Persisters.PersistPlaces do + @moduledoc "Persists place role declarations and bakes places/0" + use Spark.Dsl.Transformer + alias Spark.Dsl.Transformer + + @impl true + def transform(dsl_state) do + declarations = Transformer.get_entities(dsl_state, [:places]) + escaped = Macro.escape(declarations) + dsl_state = Transformer.persist(dsl_state, :places, declarations) + + {:ok, Transformer.eval(dsl_state, [], quote do + @doc false + def places, do: unquote(escaped) + end)} + end +end diff --git a/lib/diffo/provider/components/place/extension/verifiers/verify_roles.ex b/lib/diffo/provider/components/place/extension/verifiers/verify_roles.ex new file mode 100644 index 0000000..70991df --- /dev/null +++ b/lib/diffo/provider/components/place/extension/verifiers/verify_roles.ex @@ -0,0 +1,79 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Place.Extension.Verifiers.VerifyRoles do + @moduledoc "Verifies role declarations across instances, parties, and places sections" + use Spark.Dsl.Verifier + + alias Spark.Dsl.Verifier + alias Spark.Error.DslError + alias Diffo.Provider.Instance.Extension.Info, as: InstanceInfo + alias Diffo.Provider.Party.Extension.Info, as: PartyInfo + alias Diffo.Provider.Place.Extension.Info, as: PlaceInfo + + @impl true + def verify(dsl_state) do + resource = Verifier.get_persisted(dsl_state, :module) + + errors = + check_section(dsl_state, [:instances], :instance_type, &InstanceInfo.instance?/1, + "instances", "instance_type", "BaseInstance", resource) ++ + check_section(dsl_state, [:parties], :party_type, &PartyInfo.party?/1, + "parties", "party_type", "BaseParty", resource) ++ + check_section(dsl_state, [:places], :place_type, &PlaceInfo.place?/1, + "places", "place_type", "BasePlace", resource) + + case errors do + [] -> :ok + _ -> {:error, errors} + end + end + + defp check_section(dsl_state, path, type_field, type_check?, section, field, base, resource) do + entities = Verifier.get_entities(dsl_state, path) + duplicate_errors(entities, section, resource) ++ + type_errors(entities, type_field, type_check?, section, field, base, resource) + end + + defp duplicate_errors(entities, section, resource) do + entities + |> Enum.group_by(& &1.role) + |> Enum.filter(fn {_role, list} -> length(list) > 1 end) + |> Enum.map(fn {role, _} -> + DslError.exception( + module: resource, + path: [String.to_atom(section)], + message: "#{section}: role #{inspect(role)} is declared more than once" + ) + end) + end + + defp type_errors(entities, type_field, type_check?, section, field, base, resource) do + Enum.reduce(entities, [], fn entity, acc -> + mod = Map.get(entity, type_field) + + cond do + is_nil(mod) -> + acc + + !Code.ensure_loaded?(mod) -> + [DslError.exception( + module: resource, + path: [String.to_atom(section)], + message: "#{section}: #{field} #{inspect(mod)} does not exist" + ) | acc] + + !type_check?.(mod) -> + [DslError.exception( + module: resource, + path: [String.to_atom(section)], + message: "#{section}: #{field} #{inspect(mod)} does not extend #{base}" + ) | acc] + + true -> + acc + end + end) + end +end diff --git a/test/instance_extension/party_test.exs b/test/instance_extension/party_test.exs index 04c1a71..96482dc 100644 --- a/test/instance_extension/party_test.exs +++ b/test/instance_extension/party_test.exs @@ -10,6 +10,7 @@ defmodule Diffo.InstanceExtension.PartyTest do alias Diffo.Provider.Party.Extension.Info, as: PartyInfo alias Diffo.Test.Organization alias Diffo.Test.Person + alias Diffo.Test.Carrier alias Diffo.Test.Shelf alias Diffo.Test.Nbn alias Diffo.Test.Servo @@ -33,8 +34,10 @@ defmodule Diffo.InstanceExtension.PartyTest do assert hd(roles).party_type == Diffo.Provider.Instance end - test "no party roles declared" do - assert PartyInfo.parties(Organization) == [] + test "party roles are declared" do + roles = PartyInfo.parties(Organization) + assert length(roles) == 1 + assert hd(roles).role == :employer end end @@ -46,8 +49,10 @@ defmodule Diffo.InstanceExtension.PartyTest do assert hd(roles).party_type == Diffo.Test.Person end - test "no instance roles declared" do - assert PartyInfo.instances(Person) == [] + test "instance roles are declared" do + roles = PartyInfo.instances(Person) + assert length(roles) == 1 + assert hd(roles).role == :overseer end end @@ -153,8 +158,8 @@ defmodule Diffo.InstanceExtension.PartyTest do end end - describe "BaseParty — Organization CRUD" do - test "create and read organization" do + describe "BaseParty — simple pattern (Organization)" do + test "create and read using only base attributes" do {:ok, org} = Nbn.create_organization(%{name: "Acme Corp"}) assert org.name == "Acme Corp" assert org.type == :Organization @@ -164,6 +169,39 @@ defmodule Diffo.InstanceExtension.PartyTest do end end + describe "BaseParty — complex pattern (Carrier)" do + test "domain-specific attributes are accepted and persisted" do + {:ok, carrier} = Nbn.create_carrier(%{ + name: "Acme Wholesale", + abn: "51824753556", + trading_name: "Acme" + }) + + assert carrier.name == "Acme Wholesale" + assert carrier.type == :Organization + assert carrier.abn == "51824753556" + assert carrier.trading_name == "Acme" + end + + test "domain-specific attributes are readable after creation" do + {:ok, carrier} = Nbn.create_carrier(%{ + name: "Acme Wholesale", + abn: "51824753556", + trading_name: "Acme" + }) + + {:ok, loaded} = Nbn.get_carrier_by_id(carrier.id) + assert loaded.abn == "51824753556" + assert loaded.trading_name == "Acme" + end + + test "domain-specific attributes are nil when not provided" do + {:ok, carrier} = Nbn.create_carrier(%{name: "Bare Carrier"}) + assert carrier.abn == nil + assert carrier.trading_name == nil + end + end + describe "BaseParty — Person CRUD" do test "create and read person" do {:ok, person} = Nbn.create_person(%{name: "Alice"}) diff --git a/test/instance_extension/place_test.exs b/test/instance_extension/place_test.exs new file mode 100644 index 0000000..b6f7fe7 --- /dev/null +++ b/test/instance_extension/place_test.exs @@ -0,0 +1,133 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.InstanceExtension.PlaceTest do + @moduledoc false + use ExUnit.Case + + 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.ExchangeBuilding + alias Diffo.Test.Shelf + alias Diffo.Test.Nbn + + setup_all do + AshNeo4j.BoltyHelper.start() + end + + setup do + on_exit(fn -> + AshNeo4j.Neo4jHelper.delete_all() + end) + end + + describe "Place DSL — GeographicSite" do + test "instance roles are declared" do + roles = PlaceInfo.instances(GeographicSite) + assert length(roles) == 1 + assert hd(roles).role == :installed_at + assert hd(roles).instance_type == Diffo.Provider.Instance + end + + test "party roles are declared" do + roles = PlaceInfo.parties(GeographicSite) + assert length(roles) == 1 + assert hd(roles).role == :managed_by + assert hd(roles).party_type == Organization + end + + test "place roles are declared" do + roles = PlaceInfo.places(GeographicSite) + assert length(roles) == 1 + assert hd(roles).role == :contained_in + assert hd(roles).place_type == Diffo.Provider.Place + end + end + + describe "Instance DSL — Shelf places" do + test "place declarations are accessible via info" do + places = InstanceInfo.structure_places(Shelf) + roles = Enum.map(places, & &1.role) + assert :installation_site in roles + assert :billing_address in roles + end + + test "place types are correct" do + places = InstanceInfo.structure_places(Shelf) + installation_site = Enum.find(places, &(&1.role == :installation_site)) + assert installation_site.place_type == Diffo.Provider.Place + end + + test "singular place defaults to multiple: false" do + places = InstanceInfo.structure_places(Shelf) + installation_site = Enum.find(places, &(&1.role == :installation_site)) + assert installation_site.multiple == false + end + + test "reference: true is declared" do + places = InstanceInfo.structure_places(Shelf) + billing = Enum.find(places, &(&1.role == :billing_address)) + assert billing.reference == true + assert billing.multiple == false + end + + test "reference defaults to false" do + places = InstanceInfo.structure_places(Shelf) + installation_site = Enum.find(places, &(&1.role == :installation_site)) + assert installation_site.reference == false + end + end + + describe "BasePlace — simple pattern (GeographicSite)" do + test "create and read using only base attributes" do + {:ok, site} = Nbn.create_geographic_site(%{id: "SITE-01", name: "Data Centre 1"}) + assert site.name == "Data Centre 1" + assert site.type == :GeographicSite + + {:ok, loaded} = Nbn.get_geographic_site_by_id("SITE-01") + assert loaded.name == "Data Centre 1" + end + end + + describe "BasePlace — complex pattern (ExchangeBuilding)" do + test "domain-specific attributes are accepted and persisted" do + {:ok, building} = Nbn.create_exchange_building(%{ + id: "EX-MEL-001", + name: "Melbourne Central Exchange", + nli: "MEXMELB0001", + access_type: :unmanned + }) + + assert building.name == "Melbourne Central Exchange" + assert building.type == :GeographicSite + assert building.nli == "MEXMELB0001" + assert building.access_type == :unmanned + end + + test "domain-specific attributes are readable after creation" do + {:ok, building} = Nbn.create_exchange_building(%{ + id: "EX-MEL-002", + name: "South Yarra Exchange", + nli: "MEXMELB0002", + access_type: :attended + }) + + {:ok, loaded} = Nbn.get_exchange_building_by_id("EX-MEL-002") + assert loaded.nli == "MEXMELB0002" + assert loaded.access_type == :attended + end + + test "domain-specific attributes are nil when not provided" do + {:ok, building} = Nbn.create_exchange_building(%{ + id: "EX-MEL-003", + name: "Bare Exchange" + }) + + assert building.nli == nil + assert building.access_type == nil + end + end +end diff --git a/test/party_extension/transformer_test.exs b/test/party_extension/transformer_test.exs new file mode 100644 index 0000000..e3ed62b --- /dev/null +++ b/test/party_extension/transformer_test.exs @@ -0,0 +1,87 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.PartyExtension.TransformerTest do + @moduledoc false + use ExUnit.Case, async: true + + alias Diffo.Test.Organization + alias Diffo.Test.Person + alias Diffo.Provider.Party.Extension.InstanceRole + alias Diffo.Provider.Party.Extension.PartyRole + alias Diffo.Provider.Party.Extension.PlaceRole + alias Diffo.Provider.Party.Extension.Info + + describe "PersistInstances" do + test "bakes instances/0 onto the resource" do + roles = Organization.instances() + assert is_list(roles) + assert length(roles) == 1 + assert hd(roles).role == :facilitator + end + + test "each instance role is an InstanceRole struct" do + assert is_struct(hd(Organization.instances()), InstanceRole) + end + + test "instances are also accessible via Info" do + assert length(Info.instances(Organization)) == 1 + assert length(Info.instances(Person)) == 1 + end + + test "Person instances/0 bakes correctly" do + roles = Person.instances() + assert length(roles) == 1 + assert hd(roles).role == :overseer + end + end + + describe "PersistParties" do + test "bakes parties/0 onto the resource" do + roles = Organization.parties() + assert is_list(roles) + assert length(roles) == 1 + assert hd(roles).role == :employer + end + + test "each party role is a PartyRole struct" do + assert is_struct(hd(Organization.parties()), PartyRole) + end + + test "parties are also accessible via Info" do + assert length(Info.parties(Organization)) == 1 + assert length(Info.parties(Person)) == 1 + end + + test "Person parties/0 bakes correctly" do + roles = Person.parties() + assert length(roles) == 1 + assert hd(roles).role == :manager + end + end + + describe "PersistPlaces" do + test "bakes places/0 onto the resource" do + roles = Organization.places() + assert is_list(roles) + assert length(roles) == 1 + assert hd(roles).role == :headquarters + end + + test "each place role is a PlaceRole struct" do + assert is_struct(hd(Organization.places()), PlaceRole) + end + + test "places are also accessible via Info" do + assert length(Info.places(Organization)) == 1 + assert length(Info.places(Person)) == 1 + end + + test "Person places/0 bakes correctly" do + roles = Person.places() + assert length(roles) == 1 + assert hd(roles).role == :residence + end + end +end diff --git a/test/party_extension/verifier_test.exs b/test/party_extension/verifier_test.exs new file mode 100644 index 0000000..0e4f65e --- /dev/null +++ b/test/party_extension/verifier_test.exs @@ -0,0 +1,207 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.PartyExtension.VerifierTest do + @moduledoc false + use ExUnit.Case, async: false + alias Diffo.Test.Util + + describe "instances verifier" do + test "duplicate instance role warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "instances: role :operator is declared more than once", + fn -> + defmodule DuplicateInstanceRole do + alias Diffo.Provider.BaseParty + use Ash.Resource, fragments: [BaseParty], domain: Diffo.Test.Nbn + + resource do + description "resource with duplicate instance role" + end + + instances do + role :operator, Diffo.Provider.Instance + role :operator, Diffo.Provider.Instance + end + end + end + ) + end + + test "non-existent instance_type warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "instances: instance_type NonExistent.InstanceModule does not exist", + fn -> + defmodule InvalidInstanceType do + alias Diffo.Provider.BaseParty + use Ash.Resource, fragments: [BaseParty], domain: Diffo.Test.Nbn + + resource do + description "resource with non-existent instance type" + end + + instances do + role :operator, NonExistent.InstanceModule + end + end + end + ) + end + + 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", + fn -> + defmodule WrongInstanceType do + alias Diffo.Provider.BaseParty + use Ash.Resource, fragments: [BaseParty], domain: Diffo.Test.Nbn + + resource do + description "resource with party as instance type" + end + + instances do + role :operator, Diffo.Test.Organization + end + end + end + ) + end + end + + describe "parties verifier" do + test "duplicate party role warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "parties: role :employer is declared more than once", + fn -> + defmodule DuplicatePartyRole do + alias Diffo.Provider.BaseParty + use Ash.Resource, fragments: [BaseParty], domain: Diffo.Test.Nbn + + resource do + description "resource with duplicate party role" + end + + parties do + role :employer, Diffo.Test.Organization + role :employer, Diffo.Test.Organization + end + end + end + ) + end + + test "non-existent party_type warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "parties: party_type NonExistent.PartyModule does not exist", + fn -> + defmodule InvalidPartyRoleType do + alias Diffo.Provider.BaseParty + use Ash.Resource, fragments: [BaseParty], domain: Diffo.Test.Nbn + + resource do + description "resource with non-existent party type" + end + + parties do + role :employer, NonExistent.PartyModule + end + end + end + ) + end + + test "party_type not extending BaseParty warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "parties: party_type Diffo.Provider.Instance does not extend BaseParty", + fn -> + defmodule WrongPartyRoleType do + alias Diffo.Provider.BaseParty + use Ash.Resource, fragments: [BaseParty], domain: Diffo.Test.Nbn + + resource do + description "resource with instance as party type" + end + + parties do + role :employer, Diffo.Provider.Instance + end + end + end + ) + end + end + + describe "places verifier" do + test "duplicate place role warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "places: role :headquarters is declared more than once", + fn -> + defmodule DuplicatePlaceRole do + alias Diffo.Provider.BaseParty + use Ash.Resource, fragments: [BaseParty], domain: Diffo.Test.Nbn + + resource do + description "resource with duplicate place role" + end + + places do + role :headquarters, Diffo.Provider.Place + role :headquarters, Diffo.Provider.Place + end + end + end + ) + end + + test "non-existent place_type warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "places: place_type NonExistent.PlaceModule does not exist", + fn -> + defmodule InvalidPlaceRoleType do + alias Diffo.Provider.BaseParty + use Ash.Resource, fragments: [BaseParty], domain: Diffo.Test.Nbn + + resource do + description "resource with non-existent place type" + end + + places do + role :headquarters, NonExistent.PlaceModule + end + end + end + ) + end + + 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", + fn -> + defmodule WrongPlaceRoleType do + alias Diffo.Provider.BaseParty + use Ash.Resource, fragments: [BaseParty], domain: Diffo.Test.Nbn + + resource do + description "resource with party as place type" + end + + places do + role :headquarters, Diffo.Test.Organization + end + end + end + ) + end + end +end diff --git a/test/place_extension/transformer_test.exs b/test/place_extension/transformer_test.exs new file mode 100644 index 0000000..0b5cd76 --- /dev/null +++ b/test/place_extension/transformer_test.exs @@ -0,0 +1,65 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.PlaceExtension.TransformerTest do + @moduledoc false + use ExUnit.Case, async: true + + alias Diffo.Test.GeographicSite + alias Diffo.Provider.Place.Extension.InstanceRole + alias Diffo.Provider.Place.Extension.PartyRole + alias Diffo.Provider.Place.Extension.PlaceRole + alias Diffo.Provider.Place.Extension.Info + + describe "PersistInstances" do + test "bakes instances/0 onto the resource" do + roles = GeographicSite.instances() + assert is_list(roles) + assert length(roles) == 1 + assert hd(roles).role == :installed_at + end + + test "each instance role is an InstanceRole struct" do + assert is_struct(hd(GeographicSite.instances()), InstanceRole) + end + + test "instances are also accessible via Info" do + assert length(Info.instances(GeographicSite)) == 1 + end + end + + describe "PersistParties" do + test "bakes parties/0 onto the resource" do + roles = GeographicSite.parties() + assert is_list(roles) + assert length(roles) == 1 + assert hd(roles).role == :managed_by + end + + test "each party role is a PartyRole struct" do + assert is_struct(hd(GeographicSite.parties()), PartyRole) + end + + test "parties are also accessible via Info" do + assert length(Info.parties(GeographicSite)) == 1 + end + end + + describe "PersistPlaces" do + test "bakes places/0 onto the resource" do + roles = GeographicSite.places() + assert is_list(roles) + assert length(roles) == 1 + assert hd(roles).role == :contained_in + end + + test "each place role is a PlaceRole struct" do + assert is_struct(hd(GeographicSite.places()), PlaceRole) + end + + test "places are also accessible via Info" do + assert length(Info.places(GeographicSite)) == 1 + end + end +end diff --git a/test/place_extension/verifier_test.exs b/test/place_extension/verifier_test.exs new file mode 100644 index 0000000..f9b5b2f --- /dev/null +++ b/test/place_extension/verifier_test.exs @@ -0,0 +1,207 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.PlaceExtension.VerifierTest do + @moduledoc false + use ExUnit.Case, async: false + alias Diffo.Test.Util + + describe "instances verifier" do + test "duplicate instance role warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "instances: role :site_for is declared more than once", + fn -> + defmodule DuplicatePlaceInstanceRole do + alias Diffo.Provider.BasePlace + use Ash.Resource, fragments: [BasePlace], domain: Diffo.Test.Nbn + + resource do + description "place with duplicate instance role" + end + + instances do + role :site_for, Diffo.Provider.Instance + role :site_for, Diffo.Provider.Instance + end + end + end + ) + end + + test "non-existent instance_type warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "instances: instance_type NonExistent.InstanceModule does not exist", + fn -> + defmodule InvalidPlaceInstanceType do + alias Diffo.Provider.BasePlace + use Ash.Resource, fragments: [BasePlace], domain: Diffo.Test.Nbn + + resource do + description "place with non-existent instance type" + end + + instances do + role :site_for, NonExistent.InstanceModule + end + end + end + ) + end + + 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", + fn -> + defmodule WrongPlaceInstanceType do + alias Diffo.Provider.BasePlace + use Ash.Resource, fragments: [BasePlace], domain: Diffo.Test.Nbn + + resource do + description "place with party as instance type" + end + + instances do + role :site_for, Diffo.Test.Organization + end + end + end + ) + end + end + + describe "parties verifier" do + test "duplicate party role warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "parties: role :managed_by is declared more than once", + fn -> + defmodule DuplicatePlacePartyRole do + alias Diffo.Provider.BasePlace + use Ash.Resource, fragments: [BasePlace], domain: Diffo.Test.Nbn + + resource do + description "place with duplicate party role" + end + + parties do + role :managed_by, Diffo.Test.Organization + role :managed_by, Diffo.Test.Organization + end + end + end + ) + end + + test "non-existent party_type warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "parties: party_type NonExistent.PartyModule does not exist", + fn -> + defmodule InvalidPlacePartyType do + alias Diffo.Provider.BasePlace + use Ash.Resource, fragments: [BasePlace], domain: Diffo.Test.Nbn + + resource do + description "place with non-existent party type" + end + + parties do + role :managed_by, NonExistent.PartyModule + end + end + end + ) + end + + test "party_type not extending BaseParty warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "parties: party_type Diffo.Provider.Instance does not extend BaseParty", + fn -> + defmodule WrongPlacePartyType do + alias Diffo.Provider.BasePlace + use Ash.Resource, fragments: [BasePlace], domain: Diffo.Test.Nbn + + resource do + description "place with instance as party type" + end + + parties do + role :managed_by, Diffo.Provider.Instance + end + end + end + ) + end + end + + describe "places verifier" do + test "duplicate place role warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "places: role :contained_in is declared more than once", + fn -> + defmodule DuplicatePlacePlaceRole do + alias Diffo.Provider.BasePlace + use Ash.Resource, fragments: [BasePlace], domain: Diffo.Test.Nbn + + resource do + description "place with duplicate place role" + end + + places do + role :contained_in, Diffo.Provider.Place + role :contained_in, Diffo.Provider.Place + end + end + end + ) + end + + test "non-existent place_type warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "places: place_type NonExistent.PlaceModule does not exist", + fn -> + defmodule InvalidPlacePlaceType do + alias Diffo.Provider.BasePlace + use Ash.Resource, fragments: [BasePlace], domain: Diffo.Test.Nbn + + resource do + description "place with non-existent place type" + end + + places do + role :contained_in, NonExistent.PlaceModule + end + end + end + ) + end + + 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", + fn -> + defmodule WrongPlacePlaceType do + alias Diffo.Provider.BasePlace + use Ash.Resource, fragments: [BasePlace], domain: Diffo.Test.Nbn + + resource do + description "place with party as place type" + end + + places do + role :contained_in, Diffo.Test.Organization + end + end + end + ) + end + end +end diff --git a/test/support/nbn.ex b/test/support/nbn.ex index c55292d..0c99bdc 100644 --- a/test/support/nbn.ex +++ b/test/support/nbn.ex @@ -14,9 +14,12 @@ defmodule Diffo.Test.Nbn do alias Diffo.Test.Organization alias Diffo.Test.Person + alias Diffo.Test.Carrier + alias Diffo.Test.GeographicSite + alias Diffo.Test.ExchangeBuilding domain do - description "NBN party domain" + description "NBN party and place domain" end resources do @@ -31,5 +34,20 @@ defmodule Diffo.Test.Nbn do define :get_person_by_id, action: :read, get_by: :id define :list_persons, action: :list end + + resource Carrier do + define :create_carrier, action: :build + define :get_carrier_by_id, action: :read, get_by: :id + end + + resource GeographicSite do + define :create_geographic_site, action: :build + define :get_geographic_site_by_id, action: :read, get_by: :id + end + + resource ExchangeBuilding do + define :create_exchange_building, action: :build + define :get_exchange_building_by_id, action: :read, get_by: :id + end end end diff --git a/test/support/resource/carrier.ex b/test/support/resource/carrier.ex new file mode 100644 index 0000000..1f8c31d --- /dev/null +++ b/test/support/resource/carrier.ex @@ -0,0 +1,62 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Test.Carrier do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + Carrier - a telecommunications carrier with domain-specific attributes, + demonstrating the complex BaseParty pattern. + """ + + alias Diffo.Provider.BaseParty + alias Diffo.Test.Nbn + + use Ash.Resource, + fragments: [BaseParty], + domain: Nbn + + resource do + description "A Telecommunications Carrier" + plural_name :carriers + end + + attributes do + attribute :abn, :string do + description "Australian Business Number" + allow_nil? true + public? true + end + + attribute :trading_name, :string do + description "Trading name, distinct from legal name" + allow_nil? true + public? true + end + end + + jason do + pick [:id, :name, :type, :abn, :trading_name] + compact true + end + + outstanding do + expect [:id, :name, :type] + end + + actions do + create :build do + accept [:id, :href, :name, :abn, :trading_name] + change set_attribute(:type, :Organization) + end + end + + instances do + role :provider, Diffo.Provider.Instance + end + + places do + role :exchange, Diffo.Provider.Place + end +end diff --git a/test/support/resource/exchange_building.ex b/test/support/resource/exchange_building.ex new file mode 100644 index 0000000..d6c3edc --- /dev/null +++ b/test/support/resource/exchange_building.ex @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Test.ExchangeBuilding do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + ExchangeBuilding - an NBN exchange building with domain-specific attributes, + demonstrating the complex BasePlace pattern. + """ + + alias Diffo.Provider.BasePlace + alias Diffo.Test.Nbn + + use Ash.Resource, + fragments: [BasePlace], + domain: Nbn + + resource do + description "An NBN Exchange Building" + plural_name :exchange_buildings + end + + attributes do + attribute :nli, :string do + description "Network Location Identifier" + allow_nil? true + public? true + end + + attribute :access_type, :atom do + description "Access type for the exchange building" + allow_nil? true + public? true + constraints one_of: [:attended, :unmanned, :restricted] + end + end + + jason do + pick [:id, :href, :name, :type, :nli, :access_type] + compact true + rename type: "@type" + end + + outstanding do + expect [:id, :name, :type] + end + + actions do + create :build do + accept [:id, :href, :name, :nli, :access_type] + change set_attribute(:type, :GeographicSite) + end + end + + parties do + role :operator, Diffo.Test.Carrier + end + + instances do + role :host, Diffo.Provider.Instance + end +end diff --git a/test/support/resource/geographic_site.ex b/test/support/resource/geographic_site.ex new file mode 100644 index 0000000..81ce42c --- /dev/null +++ b/test/support/resource/geographic_site.ex @@ -0,0 +1,52 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Test.GeographicSite do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + GeographicSite - test fixture for Place Extension DSL + """ + + alias Diffo.Provider.BasePlace + alias Diffo.Test.Nbn + + use Ash.Resource, + fragments: [BasePlace], + domain: Nbn + + resource do + description "A Geographic Site" + plural_name :geographic_sites + end + + jason do + pick [:id, :href, :name, :type] + compact true + rename type: "@type" + end + + outstanding do + expect [:id, :name, :type] + end + + actions do + create :build do + accept [:id, :href, :name] + change set_attribute(:type, :GeographicSite) + end + end + + instances do + role :installed_at, Diffo.Provider.Instance + end + + parties do + role :managed_by, Diffo.Test.Organization + end + + places do + role :contained_in, Diffo.Provider.Place + end +end diff --git a/test/support/resource/organization.ex b/test/support/resource/organization.ex index f50930c..0a7c7bc 100644 --- a/test/support/resource/organization.ex +++ b/test/support/resource/organization.ex @@ -40,4 +40,12 @@ defmodule Diffo.Test.Organization do instances do role :facilitator, Diffo.Provider.Instance end + + parties do + role :employer, Diffo.Test.Person + end + + places do + role :headquarters, Diffo.Provider.Place + end end diff --git a/test/support/resource/person.ex b/test/support/resource/person.ex index 55aeb0b..e260004 100644 --- a/test/support/resource/person.ex +++ b/test/support/resource/person.ex @@ -37,7 +37,15 @@ defmodule Diffo.Test.Person do end end + instances do + role :overseer, Diffo.Provider.Instance + end + parties do role :manager, Diffo.Test.Person end + + places do + role :residence, Diffo.Provider.Place + end end From bc3652046e92a12077fd1e654c869b9bd501ea1d Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Wed, 29 Apr 2026 10:28:59 +0930 Subject: [PATCH 13/22] fixed provider instance specification doesn't set description --- .../components/instance/extension/specification.ex | 2 +- test/instance_extension/specification_test.exs | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/diffo/provider/components/instance/extension/specification.ex b/lib/diffo/provider/components/instance/extension/specification.ex index b40ed56..77dec6c 100644 --- a/lib/diffo/provider/components/instance/extension/specification.ex +++ b/lib/diffo/provider/components/instance/extension/specification.ex @@ -12,7 +12,7 @@ defmodule Diffo.Provider.Instance.Specification do @doc """ Struct for a Specification """ - defstruct [:id, :name, :type, :major_version, :category] + defstruct [:id, :name, :type, :major_version, :description, :category] @doc """ Sets the specified_by argument in the changeset, ensuring the Extended Instance's specification exists diff --git a/test/instance_extension/specification_test.exs b/test/instance_extension/specification_test.exs index 9cba730..2e0a0db 100644 --- a/test/instance_extension/specification_test.exs +++ b/test/instance_extension/specification_test.exs @@ -6,6 +6,7 @@ defmodule Diffo.InstanceExtension.SpecificationTest do @moduledoc false use ExUnit.Case alias Diffo.Test.Servo + alias Diffo.Test.Shelf setup_all do AshNeo4j.BoltyHelper.start() @@ -23,5 +24,15 @@ defmodule Diffo.InstanceExtension.SpecificationTest do %Ash.Error.Invalid{errors: errors} = error assert hd(errors).message == "must be a uuid v4 or nil" end + + test "description declared in specification DSL roundtrips to the persisted specification" do + spec_id = Shelf.specification()[:id] + description = Shelf.specification()[:description] + + Servo.build_shelf(%{name: "s"}) + + {:ok, specification} = Diffo.Provider.get_specification_by_id(spec_id) + assert specification.description == description + end end end From 6dcda5d8d4eb5de694852305efd14bb895132ecd Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Wed, 29 Apr 2026 10:46:12 +0930 Subject: [PATCH 14/22] description now present --- test/instance_extension/assigner_test.exs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/instance_extension/assigner_test.exs b/test/instance_extension/assigner_test.exs index d03ef75..c6ab9e0 100644 --- a/test/instance_extension/assigner_test.exs +++ b/test/instance_extension/assigner_test.exs @@ -65,7 +65,7 @@ defmodule Diffo.InstanceExtension.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\",\"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\":\"card\",\"value\":{}},{\"name\":\"ports\",\"value\":{\"first\":1,\"last\":1,\"free\":1,\"algorithm\":\"lowest\"}}]}) end test "define card" do @@ -81,7 +81,7 @@ defmodule Diffo.InstanceExtension.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\",\"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\":\"card\",\"value\":{\"family\":\"ISAM\",\"model\":\"EBLT48\",\"technology\":\"adsl2Plus\"}},{\"name\":\"ports\",\"value\":{\"first\":1,\"last\":48,\"free\":48,\"type\":\"ADSL2+\",\"algorithm\":\"lowest\"}}]}) end test "auto assign port to resource" do @@ -106,7 +106,7 @@ defmodule Diffo.InstanceExtension.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\",\"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\":\"card\",\"value\":{\"family\":\"ISAM\",\"model\":\"EBLT48\",\"technology\":\"adsl2Plus\"}},{\"name\":\"ports\",\"value\":{\"first\":1,\"last\":48,\"free\":47,\"type\":\"ADSL2+\",\"algorithm\":\"lowest\"}}]}) end test "auto assign two ports to same resource" do @@ -136,7 +136,7 @@ defmodule Diffo.InstanceExtension.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\",\"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\":\"card\",\"value\":{\"family\":\"ISAM\",\"model\":\"EBLT48\",\"technology\":\"adsl2Plus\"}},{\"name\":\"ports\",\"value\":{\"first\":1,\"last\":48,\"free\":46,\"type\":\"ADSL2+\",\"algorithm\":\"lowest\"}}]}) end test "specific assignment rejects duplicate request" do @@ -166,7 +166,7 @@ defmodule Diffo.InstanceExtension.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\",\"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\":\"card\",\"value\":{\"family\":\"ISAM\",\"model\":\"EBLT48\",\"technology\":\"adsl2Plus\"}},{\"name\":\"ports\",\"value\":{\"first\":1,\"last\":48,\"free\":47,\"type\":\"ADSL2+\",\"algorithm\":\"lowest\"}}]}) end test "unassign an auto-assigned port from a resource" do @@ -209,7 +209,7 @@ defmodule Diffo.InstanceExtension.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\",\"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\":\"card\",\"value\":{\"family\":\"ISAM\",\"model\":\"EBLT48\",\"technology\":\"adsl2Plus\"}},{\"name\":\"ports\",\"value\":{\"first\":1,\"last\":48,\"free\":48,\"type\":\"ADSL2+\",\"algorithm\":\"lowest\"}}]}) end end end From 361f2070fe61da783033840e83ac6b5b82d8b12d Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Wed, 29 Apr 2026 12:11:31 +0930 Subject: [PATCH 15/22] added provider instance specification field minor_version, patch_version, tmf_version --- .../provider/components/instance/extension.ex | 12 +++++++++++ .../persisters/persist_specification.ex | 3 +++ .../instance/extension/specification.ex | 6 ++++-- .../provider/components/specification.ex | 2 +- test/instance_extension/party_test.exs | 2 +- test/instance_extension/place_test.exs | 4 ++-- .../instance_extension/specification_test.exs | 21 +++++++++++++++++++ test/support/resource/shelf.ex | 4 ++++ 8 files changed, 48 insertions(+), 6 deletions(-) diff --git a/lib/diffo/provider/components/instance/extension.ex b/lib/diffo/provider/components/instance/extension.ex index 1376116..0c8fd74 100644 --- a/lib/diffo/provider/components/instance/extension.ex +++ b/lib/diffo/provider/components/instance/extension.ex @@ -79,6 +79,18 @@ defmodule Diffo.Provider.Instance.Extension do doc: "The major_version of the specification.", default: 1 ], + minor_version: [ + type: :integer, + doc: "The minor_version of the specification." + ], + patch_version: [ + type: :integer, + doc: "The patch_version of the specification." + ], + tmf_version: [ + type: :integer, + doc: "The TMF API version of the specification, e.g. 4." + ], description: [ type: :string, doc: "A generic description of the specified service or resource." diff --git a/lib/diffo/provider/components/instance/extension/persisters/persist_specification.ex b/lib/diffo/provider/components/instance/extension/persisters/persist_specification.ex index 4cfd5f9..b5f6a1b 100644 --- a/lib/diffo/provider/components/instance/extension/persisters/persist_specification.ex +++ b/lib/diffo/provider/components/instance/extension/persisters/persist_specification.ex @@ -14,6 +14,9 @@ defmodule Diffo.Provider.Instance.Extension.Persisters.PersistSpecification do name: Transformer.get_option(dsl_state, [:structure, :specification], :name), type: Transformer.get_option(dsl_state, [:structure, :specification], :type, :serviceSpecification), major_version: Transformer.get_option(dsl_state, [:structure, :specification], :major_version, 1), + minor_version: Transformer.get_option(dsl_state, [:structure, :specification], :minor_version), + patch_version: Transformer.get_option(dsl_state, [:structure, :specification], :patch_version), + tmf_version: Transformer.get_option(dsl_state, [:structure, :specification], :tmf_version), description: Transformer.get_option(dsl_state, [:structure, :specification], :description), category: Transformer.get_option(dsl_state, [:structure, :specification], :category) ] diff --git a/lib/diffo/provider/components/instance/extension/specification.ex b/lib/diffo/provider/components/instance/extension/specification.ex index 77dec6c..234fd14 100644 --- a/lib/diffo/provider/components/instance/extension/specification.ex +++ b/lib/diffo/provider/components/instance/extension/specification.ex @@ -12,7 +12,7 @@ defmodule Diffo.Provider.Instance.Specification do @doc """ Struct for a Specification """ - defstruct [:id, :name, :type, :major_version, :description, :category] + defstruct [:id, :name, :type, :major_version, :minor_version, :patch_version, :tmf_version, :description, :category] @doc """ Sets the specified_by argument in the changeset, ensuring the Extended Instance's specification exists @@ -21,7 +21,9 @@ defmodule Diffo.Provider.Instance.Specification do when is_struct(changeset, Ash.Changeset) and is_list(options) do specification = struct(__MODULE__, options) - case Provider.create_specification(Map.from_struct(specification)) do + attrs = specification |> Map.from_struct() |> Map.reject(fn {_, v} -> is_nil(v) end) + + case Provider.create_specification(attrs) do {:ok, _} -> Ash.Changeset.force_set_argument(changeset, :specified_by, specification.id) diff --git a/lib/diffo/provider/components/specification.ex b/lib/diffo/provider/components/specification.ex index 09d60e8..5197dee 100644 --- a/lib/diffo/provider/components/specification.ex +++ b/lib/diffo/provider/components/specification.ex @@ -38,7 +38,7 @@ defmodule Diffo.Provider.Specification do create :create do description "creates a major version of a named serviceSpecification or resourceSpecification" - accept [:id, :type, :name, :major_version, :description, :category] + accept [:id, :type, :name, :major_version, :minor_version, :patch_version, :tmf_version, :description, :category] change load [:version, :href, :instance_type] upsert? true upsert_identity :unique_major_version_per_name diff --git a/test/instance_extension/party_test.exs b/test/instance_extension/party_test.exs index 96482dc..51fa04c 100644 --- a/test/instance_extension/party_test.exs +++ b/test/instance_extension/party_test.exs @@ -10,7 +10,7 @@ defmodule Diffo.InstanceExtension.PartyTest do alias Diffo.Provider.Party.Extension.Info, as: PartyInfo alias Diffo.Test.Organization alias Diffo.Test.Person - alias Diffo.Test.Carrier + alias Diffo.Test.Shelf alias Diffo.Test.Nbn alias Diffo.Test.Servo diff --git a/test/instance_extension/place_test.exs b/test/instance_extension/place_test.exs index b6f7fe7..a304cba 100644 --- a/test/instance_extension/place_test.exs +++ b/test/instance_extension/place_test.exs @@ -10,7 +10,7 @@ defmodule Diffo.InstanceExtension.PlaceTest do alias Diffo.Provider.Place.Extension.Info, as: PlaceInfo alias Diffo.Test.Organization alias Diffo.Test.GeographicSite - alias Diffo.Test.ExchangeBuilding + alias Diffo.Test.Shelf alias Diffo.Test.Nbn @@ -108,7 +108,7 @@ defmodule Diffo.InstanceExtension.PlaceTest do end test "domain-specific attributes are readable after creation" do - {:ok, building} = Nbn.create_exchange_building(%{ + {:ok, _building} = Nbn.create_exchange_building(%{ id: "EX-MEL-002", name: "South Yarra Exchange", nli: "MEXMELB0002", diff --git a/test/instance_extension/specification_test.exs b/test/instance_extension/specification_test.exs index 2e0a0db..4a8d283 100644 --- a/test/instance_extension/specification_test.exs +++ b/test/instance_extension/specification_test.exs @@ -34,5 +34,26 @@ defmodule Diffo.InstanceExtension.SpecificationTest do {:ok, specification} = Diffo.Provider.get_specification_by_id(spec_id) assert specification.description == description end + + test "minor_version declared in specification DSL roundtrips to the persisted specification" do + Servo.build_shelf(%{name: "s"}) + + {:ok, specification} = Diffo.Provider.get_specification_by_id(Shelf.specification()[:id]) + assert specification.minor_version == Shelf.specification()[:minor_version] + end + + test "patch_version declared in specification DSL roundtrips to the persisted specification" do + Servo.build_shelf(%{name: "s"}) + + {:ok, specification} = Diffo.Provider.get_specification_by_id(Shelf.specification()[:id]) + assert specification.patch_version == Shelf.specification()[:patch_version] + end + + test "tmf_version declared in specification DSL roundtrips to the persisted specification" do + Servo.build_shelf(%{name: "s"}) + + {:ok, specification} = Diffo.Provider.get_specification_by_id(Shelf.specification()[:id]) + assert specification.tmf_version == Shelf.specification()[:tmf_version] + end end end diff --git a/test/support/resource/shelf.ex b/test/support/resource/shelf.ex index 898f10e..b2f87b9 100644 --- a/test/support/resource/shelf.ex +++ b/test/support/resource/shelf.ex @@ -34,6 +34,10 @@ defmodule Diffo.Test.Shelf do id "ef016d85-9dbd-429c-84da-1df56cc7dda5" name "shelf" type :resourceSpecification + major_version 1 + minor_version 2 + patch_version 3 + tmf_version 4 description "A Shelf Resource Instance which contain cards" category "Network Resource" end From 7e039ddcaa971e841b6fb83a0b838c8dc424f9bc Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Wed, 29 Apr 2026 12:55:49 +0930 Subject: [PATCH 16/22] =?UTF-8?q?provider=20instance=20specification=20DSL?= =?UTF-8?q?=20=E2=80=94=20minor,=20patch=20and=20tmf=20versions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add minor_version, patch_version and tmf_version to the specification do DSL section. Nil values are stripped before create_specification so unset fields fall back to Specification resource defaults. --- .../verifiers/verify_specification.ex | 73 +++++++++++--- test/instance_extension/verifier_test.exs | 99 +++++++++++++++++++ test/type/value_test.exs | 2 - 3 files changed, 159 insertions(+), 15 deletions(-) diff --git a/lib/diffo/provider/components/instance/extension/verifiers/verify_specification.ex b/lib/diffo/provider/components/instance/extension/verifiers/verify_specification.ex index 2368126..c5d6621 100644 --- a/lib/diffo/provider/components/instance/extension/verifiers/verify_specification.ex +++ b/lib/diffo/provider/components/instance/extension/verifiers/verify_specification.ex @@ -3,33 +3,80 @@ # SPDX-License-Identifier: MIT defmodule Diffo.Provider.Instance.Extension.Verifiers.VerifySpecification do - @moduledoc "Verifies that the specification id is a valid UUID4" + @moduledoc "Verifies that the specification DSL values satisfy the Specification resource's attribute constraints" use Spark.Dsl.Verifier alias Spark.Dsl.Verifier alias Spark.Error.DslError + # Fields validated against Specification attribute constraints (id handled separately) + @spec_fields [:name, :type, :major_version, :minor_version, :patch_version, :tmf_version, :description, :category] + @impl true def verify(dsl_state) do resource = Verifier.get_persisted(dsl_state, :module) + + errors = check_id(dsl_state, resource) ++ check_attributes(dsl_state, resource) + + case errors do + [] -> :ok + errors -> {:error, errors} + end + end + + defp check_id(dsl_state, resource) do spec_id = Verifier.get_option(dsl_state, [:structure, :specification], :id) - errors = - if spec_id && !Diffo.Uuid.uuid4?(spec_id) do - [ - DslError.exception( - module: resource, - path: [:structure, :specification, :id], - message: "specification: id must be a valid UUID4" - ) - ] + if spec_id && !Diffo.Uuid.uuid4?(spec_id) do + [DslError.exception( + module: resource, + path: [:structure, :specification, :id], + message: "specification: id must be a valid UUID4" + )] + else + [] + end + end + + defp check_attributes(dsl_state, resource) do + spec_attrs = + Ash.Resource.Info.attributes(Diffo.Provider.Specification) + |> Map.new(&{&1.name, &1}) + + Enum.flat_map(@spec_fields, fn field -> + value = Verifier.get_option(dsl_state, [:structure, :specification], field) + attr = Map.get(spec_attrs, field) + + if not is_nil(value) && not is_nil(attr) do + case Ash.Type.apply_constraints(attr.type, value, attr.constraints) do + {:ok, _} -> + [] + + {:error, errors} -> + [DslError.exception( + module: resource, + path: [:structure, :specification, field], + message: "specification: #{field} - #{format_errors(errors)}" + )] + end else [] end + end) + end - case errors do - [] -> :ok - errors -> {:error, errors} + defp format_errors(errors) when is_list(errors) do + if Keyword.keyword?(errors) do + format_error(errors) + else + errors |> Enum.map(&format_error/1) |> Enum.join(", ") end end + + defp format_error(kwlist) do + {message, bindings} = Keyword.pop(kwlist, :message, "invalid value") + Enum.reduce(bindings, message, fn {key, val}, msg -> + String.replace(msg, "%{#{key}}", to_string(val)) + end) + end end diff --git a/test/instance_extension/verifier_test.exs b/test/instance_extension/verifier_test.exs index d679b70..c9d197e 100644 --- a/test/instance_extension/verifier_test.exs +++ b/test/instance_extension/verifier_test.exs @@ -31,6 +31,105 @@ defmodule Diffo.InstanceExtension.VerifierTest do end ) end + + test "name not matching camelCase pattern warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "specification: name", + fn -> + defmodule InvalidSpecName do + alias Diffo.Provider.BaseInstance + use Ash.Resource, fragments: [BaseInstance], domain: Diffo.Test.Servo + + resource do + description "resource with non-camelCase specification name" + end + + structure do + specification do + id "cd29956f-6c68-44cc-bf54-705eb8d2f754" + name "not camel case" + end + end + end + end + ) + end + + test "type not in allowed set warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "specification: type", + fn -> + defmodule InvalidSpecType do + alias Diffo.Provider.BaseInstance + use Ash.Resource, fragments: [BaseInstance], domain: Diffo.Test.Servo + + resource do + description "resource with invalid specification type" + end + + structure do + specification do + id "cd29956f-6c68-44cc-bf54-705eb8d2f754" + name "invalid" + type :badType + end + end + end + end + ) + end + + test "negative major_version warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "specification: major_version", + fn -> + defmodule InvalidSpecMajorVersion do + alias Diffo.Provider.BaseInstance + use Ash.Resource, fragments: [BaseInstance], domain: Diffo.Test.Servo + + resource do + description "resource with negative major_version" + end + + structure do + specification do + id "cd29956f-6c68-44cc-bf54-705eb8d2f754" + name "invalid" + major_version -1 + end + end + end + end + ) + end + + test "tmf_version below minimum warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "specification: tmf_version", + fn -> + defmodule InvalidSpecTmfVersion do + alias Diffo.Provider.BaseInstance + use Ash.Resource, fragments: [BaseInstance], domain: Diffo.Test.Servo + + resource do + description "resource with tmf_version below minimum" + end + + structure do + specification do + id "cd29956f-6c68-44cc-bf54-705eb8d2f754" + name "invalid" + tmf_version 0 + end + end + end + end + ) + end end describe "characteristics verifier" do diff --git a/test/type/value_test.exs b/test/type/value_test.exs index 3ed3c4d..6bdaf72 100644 --- a/test/type/value_test.exs +++ b/test/type/value_test.exs @@ -33,8 +33,6 @@ defmodule Diffo.Type.ValueTest do Ash.Type.cast_input(Value, value, Value.subtype_constraints()) end - @tag bugged: "raw Dynamic struct cast_input requires Value wrapper" - @tag :skip test "cast_input dynamic" do value = %Dynamic{type: Patch, value: %Patch{aEnd: 1, zEnd: 42}} From 7b2f6eb9d1be68b9b7d34557ff798e1af47216b8 Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Wed, 29 Apr 2026 15:09:02 +0930 Subject: [PATCH 17/22] document instance versioning lifecycle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add Specification moduledoc covering identity model, semantic versioning table, and major version lifecycle phases. Add versioning_test.exs proving the full NBN/RSP lifecycle end-to-end. Add use_diffo_provider_versioning.livemd with a six-phase walkthrough: V1 → V1.1 minor (instant universal propagation) → V2 publish (concurrent coexistence) → V1 freeze → RSP migration via respecify_instance → V1 withdrawal with spec-node guard. Closes #96 --- .../use_diffo_provider_versioning.livemd | 425 ++++++++++++++++++ lib/diffo/provider.ex | 2 +- .../provider/components/base_instance.ex | 20 + .../instance/extension/specification.ex | 2 +- .../provider/components/specification.ex | 48 +- test/provider/instance_test.exs | 6 +- test/provider/versioning_test.exs | 152 +++++++ test/support/resource/broadband.ex | 50 +++ test/support/resource/broadband_v2.ex | 50 +++ test/support/servo.ex | 12 + test/type/value_test.exs | 1 + 11 files changed, 762 insertions(+), 6 deletions(-) create mode 100644 documentation/how_to/use_diffo_provider_versioning.livemd create mode 100644 test/provider/versioning_test.exs create mode 100644 test/support/resource/broadband.ex create mode 100644 test/support/resource/broadband_v2.ex diff --git a/documentation/how_to/use_diffo_provider_versioning.livemd b/documentation/how_to/use_diffo_provider_versioning.livemd new file mode 100644 index 0000000..ae74577 --- /dev/null +++ b/documentation/how_to/use_diffo_provider_versioning.livemd @@ -0,0 +1,425 @@ + + +# Instance Versioning with the Diffo Provider + +```elixir +Mix.install( + [ + {:diffo, path: "/Users/Beanlanda/git/diffo"} + ], + config: [ + diffo: [ash_domains: [Diffo.Provider]] + ], + consolidate_protocols: false +) +``` + +## Overview + +This livebook explores how diffo handles the full lifecycle of a TMF Service or Resource +Specification across minor and major version changes. Versioning is one of the hardest +problems in operational support systems — traditional OSS platforms treat it as a schema +migration problem, requiring coordinated downtime, data transformation pipelines, and +carefully sequenced deployments. Diffo treats it as a graph relationship swap. The +complexity disappears. + +We will follow a realistic NBN / RSP scenario: + +* **NBN** is the Provider — they define and publish service specifications +* **RSPs** (Retail Service Providers) are Consumers — they create and operate service instances + +The scenario uses a `Broadband` service. We will walk through: + +1. Defining and deploying V1 +2. Adding a new technology type as a minor (backward-compatible) version — V1.1 +3. Publishing a breaking V2 alongside V1 +4. An RSP migrating their V1 instances to V2 +5. NBN withdrawing V1 + +### Installing Neo4j and Configuring Bolty + +Diffo uses the [Ash Neo4j DataLayer](https://github.com/diffo-dev/ash_neo4j), which requires +Neo4j to be installed and running. + +[AshNeo4j](https://github.com/diffo-dev/ash_neo4j) uses [neo4j](https://github.com/neo4j/neo4j). +You can install latest major Neo4j versions from the community tab at +[Neo4j Deployment Center](https://neo4j.com/deployment-center/?desktop-gdb), or use the +[5.26.8 direct link](https://neo4j.com/download-thanks/?edition=community&release=5.26.8&flavour=rpm) + +Update the configuration below as necessary and evaluate. + +```elixir +config = [ + uri: "bolt://localhost:7687", + auth: [username: "neo4j", password: "password"], + user_agent: "diffoLivebook/1", + pool_size: 15, + max_overflow: 3, + prefix: :default, + name: Bolt, + log: false, + log_hex: false +] +``` + +```elixir +AshNeo4j.BoltyHelper.start(config) +``` + +```elixir +AshNeo4j.BoltyHelper.is_connected() +``` + +**OPTIONAL** Clear the database before starting: + +```elixir +AshNeo4j.Neo4jHelper.delete_all() +``` + +## Specifications and Versioning + +A `Diffo.Provider.Specification` identifies the *kind* of a TMF Service or Resource Instance. +Every instance carries a relationship to exactly one Specification node in the Neo4j graph, +established at build time and changeable via `Diffo.Provider.respecify_instance/2`. + +A Specification is uniquely identified by `{name, major_version}`. The `id` is a stable UUID4 +that is the same across all environments for a given `{name, major_version}` pair — it is +declared as a constant in the `specification do` DSL block and committed to source control. + +Diffo uses semantic versioning: + +| Change | Mechanism | Instance impact | Intended usage | +| ------ | ----------------------------------------- | ------------------------------------------------------ | -------------------------------------------------------------------------------------- | +| Patch | `next_patch_specification!/1` | None — internal fix | Corrections to metadata: description wording, category typos | +| Minor | `next_minor_specification!/1` | None — all instances immediately reflect new version | Backward-compatible additions: new optional characteristics, new enum values | +| Major | New module, new `id`, new `major_version` | Instances stay on old spec until explicitly migrated | Breaking changes | + +## Module and Domain Setup + +Livebook compiles each cell as it is evaluated, so all resource modules must be defined before +the domain that references them. We define the V1 and V2 `Broadband` modules here, then +register both with the `Diffo.Nbn` domain in a single cell. + +**This is a simplification.** In reality, NBN cannot write V2's API until they have designed it +— they could not have included `BroadbandV2` in the domain the day V1 shipped. In a real +deployment, the domain definition lives in a versioned package. When NBN publishes V2, they +release a new version of that package with `BroadbandV2` added. RSPs pull the new package +version to gain access to V2. We define both modules upfront here only because Livebook does +not support hot module replacement across cells. + +### V1 — Broadband service characteristic value + +`:fttb` (Fibre to the Building) is the first supported technology type. + +```elixir +defmodule Diffo.Nbn.BroadbandValue do + @moduledoc "Broadband service characteristic value (V1)" + use Ash.TypedStruct, extensions: [AshJason.TypedStruct, AshOutstanding.TypedStruct] + + jason do + pick [:technology] + compact true + end + + typed_struct do + field :technology, :atom, + description: "access technology: :fttc, :fttb, :fttn, or :fttp" + end +end +``` + +### V1 — Broadband module + +The `specification do` block declares the stable UUID and version. The `behaviour do` block +wires the `build` action so that creating a `Broadband` instance automatically upserts the +specification node and wires it into the graph. + +```elixir +defmodule Diffo.Nbn.Broadband do + @moduledoc "Broadband Service Instance — V1" + alias Diffo.Provider.BaseInstance + alias Diffo.Nbn + alias Diffo.Nbn.BroadbandValue + + use Ash.Resource, + fragments: [BaseInstance], + domain: Nbn + + resource do + description "A Broadband Service Instance (V1)" + plural_name :broadbands + end + + structure do + specification do + id "a1b2c3d4-e5f6-4a7b-8c9d-e0f1a2b3c4d5" + name "broadband" + type :serviceSpecification + major_version 1 + description "A broadband access service" + category "Access" + end + + characteristics do + characteristic :broadband, BroadbandValue + end + end + + behaviour do + actions do + create :build + end + end + + actions do + create :build do + accept [:id, :name] + change set_attribute(:type, :service) + change load [:href] + upsert? false + end + end +end +``` + +### V2 — Broadband service characteristic value + +`:fttb` is retired in V2. `:fw` (Fixed Wireless) was added in V1.1 and carries forward. + +```elixir +defmodule Diffo.Nbn.BroadbandV2Value do + @moduledoc "Broadband service characteristic value (V2) — :fttb removed" + use Ash.TypedStruct, extensions: [AshJason.TypedStruct, AshOutstanding.TypedStruct] + + jason do + pick [:technology] + compact true + end + + typed_struct do + field :technology, :atom, + description: "access technology: :fttc, :fttn, :fttp, or :fw — :fttb retired" + end +end +``` + +### V2 — Broadband module + +A new `id` and `major_version: 2` make V2 a distinct specification node. V1 and V2 coexist +in the graph; RSPs migrate at their own pace. + +```elixir +defmodule Diffo.Nbn.BroadbandV2 do + @moduledoc "Broadband Service Instance — V2 (:fttb retired)" + alias Diffo.Provider.BaseInstance + alias Diffo.Nbn + alias Diffo.Nbn.BroadbandV2Value + + use Ash.Resource, + fragments: [BaseInstance], + domain: Nbn + + resource do + description "A Broadband Service Instance (V2)" + plural_name :broadband_v2s + end + + structure do + specification do + id "f6e5d4c3-b2a1-4f0e-9d8c-7b6a5f4e3d2c" + name "broadband" + type :serviceSpecification + major_version 2 + description "A broadband access service — :fttb technology retired" + category "Access" + end + + characteristics do + characteristic :broadband, BroadbandV2Value + end + end + + behaviour do + actions do + create :build + end + end + + actions do + create :build do + accept [:id, :name] + change set_attribute(:type, :service) + change load [:href] + upsert? false + end + end +end +``` + +### Domain + +```elixir +defmodule Diffo.Nbn do + @moduledoc "NBN service domain" + use Ash.Domain, otp_app: :diffo, validate_config_inclusion?: false + + domain do + description "NBN broadband service domain" + end + + resources do + resource Diffo.Nbn.Broadband do + define :build_broadband, action: :build + define :get_broadband_by_id, action: :read, get_by: :id + end + + resource Diffo.Nbn.BroadbandV2 do + define :build_broadband_v2, action: :build + define :get_broadband_v2_by_id, action: :read, get_by: :id + end + end +end +``` + +## Phase 1 — RSP Acme creates V1 instances + +RSP Acme creates broadband services for customers. The specification node is upserted on the +first `build_broadband` call and reused on every subsequent call. + +```elixir +{:ok, acme_1} = Diffo.Nbn.build_broadband(%{name: "acme-broadband-001"}) +{:ok, acme_2} = Diffo.Nbn.build_broadband(%{name: "acme-broadband-002"}) + +IO.inspect(acme_1.specification.version, label: "spec version") +IO.inspect(acme_1.specification_id, label: "spec id") +IO.inspect(acme_2.specification_id, label: "acme_2 spec id (same)") +``` + +Both instances share the same specification node. + +## Phase 2 — NBN ships a minor version (V1.1): adds :fw technology + +NBN adds Fixed Wireless (`:fw`) as a supported technology type. This is a backward-compatible +change — existing instances remain valid. NBN bumps the minor version on the specification node +and deploys an updated `Broadband` module with `:fw` in `BroadbandValue`. + +The minor version bump requires no migration and no instance downtime. Every instance +immediately reflects the new version — there is nothing to do. + +```elixir +{:ok, spec} = Diffo.Provider.get_specification_by_id(Diffo.Nbn.Broadband.specification()[:id]) +IO.inspect(spec.version, label: "before") + +updated_spec = Diffo.Provider.next_minor_specification!(spec) +IO.inspect(updated_spec.version, label: "after") +``` + +Reload an existing instance — its specification is now v1.1.0 with no action required: + +```elixir +{:ok, reloaded} = Diffo.Provider.get_instance_by_id(acme_1.id) +IO.inspect(reloaded.specification.version, label: "acme_1 spec version (automatic)") +``` + +## Phase 3 — NBN publishes V2: removes :fttb (breaking change) + +`:fttb` technology is being retired. This is a breaking change — existing instances with +`technology: :fttb` cannot simply adopt V2 without data remediation. V1 and V2 coexist; RSPs +can start creating V2 instances immediately at their own pace. + +RSP Beta starts creating V2 instances while Acme stays on V1. Both operate concurrently: + +```elixir +{:ok, beta_1} = Diffo.Nbn.build_broadband_v2(%{name: "beta-broadband-v2-001"}) + +IO.inspect(acme_1.specification_id, label: "Acme V1 spec") +IO.inspect(beta_1.specification_id, label: "Beta V2 spec") + +specs = Diffo.Provider.find_specifications_by_name!("broadband") +IO.inspect(Enum.map(specs, &{&1.major_version, &1.version}), label: "coexisting specs") +``` + +## Phase 4 — NBN freezes V1 creation (optional) + +NBN may choose to block new V1 instances before withdrawing V1 entirely, giving RSPs time to +complete migration without the risk of creating new V1 instances they will immediately need to +migrate. + +This is done by removing the `behaviour do` block from the `Broadband` module and deploying +the update. The `build_broadband` function disappears from the domain API — the compiled module +is the machine-readable announcement of the freeze. Existing V1 instances are completely +unaffected; all lifecycle operations continue normally. + +Note: this step cannot be done simultaneously with publishing V2 — in-flight RSP orders on V1 +would lose their create capability mid-order. It is a deliberate, sequenced step once the +concurrent period has settled. + +## Phase 5 — RSP Acme migrates V1 instances to V2 + +Acme decides to migrate. For instances with `technology: :fttb`, data remediation is required +before respecification — either via Cypher directly against the graph or via a domain-specific +migration action. For all other instances, `respecify_instance` is all that is needed. + +`respecify_instance` is a Provider-level action. It swaps the `SPECIFIED_BY` relationship edge +in the graph from the V1 specification node to V2. + +```elixir +# Fetch as Diffo.Provider.Instance for the Provider API +{:ok, instance_a} = Diffo.Provider.get_instance_by_id(acme_1.id) +{:ok, instance_b} = Diffo.Provider.get_instance_by_id(acme_2.id) + +v2_spec_id = Diffo.Nbn.BroadbandV2.specification()[:id] + +{:ok, migrated_a} = Diffo.Provider.respecify_instance(instance_a, %{specified_by: v2_spec_id}) +{:ok, migrated_b} = Diffo.Provider.respecify_instance(instance_b, %{specified_by: v2_spec_id}) + +IO.inspect(migrated_a.specification.id, label: "migrated spec id") +IO.inspect(migrated_a.specification.major_version, label: "migrated major version") +``` + +Verify Acme has no remaining V1 instances: + +```elixir +v1_spec_id = Diffo.Nbn.Broadband.specification()[:id] +v1_remaining = Diffo.Provider.find_instances_by_specification_id!(v1_spec_id) +IO.inspect(length(v1_remaining), label: "V1 instances remaining") +``` + +## Phase 6 — NBN withdraws V1 + +NBN removes the `Broadband` module from the package. V1 instances that have not been migrated +remain in the graph and continue to operate, but no domain API exists to create or manage them +via domain-specific actions. RSPs must complete migration to regain full operational capability. + +Any RSP still holding V1 instances after withdrawal is in an unpleasant position — they cannot +create new V1 instances to replace accidentally deleted ones. The recommendation is to complete +migration before the withdrawal deadline. + +The V1 specification node itself is protected: `Diffo.Provider.delete_specification` will fail +as long as any instance holds a `SPECIFIED_BY` relationship to it. + +```elixir +{:ok, v1_spec} = Diffo.Provider.get_specification_by_id(v1_spec_id) +{:error, _} = Diffo.Provider.delete_specification(v1_spec) +|> IO.inspect(label: "delete V1 spec (protected while instances remain)") +``` + +## What diffo brings to versioning + +Traditional OSS platforms treat versioning as a schema migration problem. A major version +requires coordinated downtime, data transformation pipelines, dual-write periods, and carefully +sequenced deployments across every system that touches the service model. The cost is +proportional to the number of systems involved and the size of the installed base. + +Diffo's model is: + +* **Minor/patch** — update a node property. Zero cost, instant, universal. +* **Major** — add a module, swap a graph edge per instance. The graph stores the relationship, + not the version. Migration is as fast as the RSP chooses to make it. +* **Withdrawal** — remove a module. Existing nodes are untouched. + +Diffo's model is simple and powerful. diff --git a/lib/diffo/provider.ex b/lib/diffo/provider.ex index 18ea091..74987a4 100644 --- a/lib/diffo/provider.ex +++ b/lib/diffo/provider.ex @@ -47,7 +47,7 @@ defmodule Diffo.Provider do define :suspend_service, action: :suspend define :terminate_service, action: :terminate define :status_service, action: :status - define :specify_instance, action: :specify + define :respecify_instance, action: :specify define :relate_instance_features, action: :relate_features define :unrelate_instance_features, action: :unrelate_features define :relate_instance_characteristics, action: :relate_characteristics diff --git a/lib/diffo/provider/components/base_instance.ex b/lib/diffo/provider/components/base_instance.ex index c8468b3..463856b 100644 --- a/lib/diffo/provider/components/base_instance.ex +++ b/lib/diffo/provider/components/base_instance.ex @@ -144,6 +144,26 @@ defmodule Diffo.Provider.BaseInstance do lightweight admin create), you can override `build_before/1` or `build_after/2` on your resource, or use Ash's `skip_unknown_inputs` to absorb the injected arguments without declaring them. + + ## Instance versioning + + Each Instance kind is tied to a specific major version of its Specification via the `id` + declared in `specification do`. Patch and minor version bumps update the existing + Specification node in place and require no instance changes. Major version bumps introduce + a new Instance kind module (e.g. `BroadbandV2`) with a new `id` and `major_version`, + leaving the original module and all its instances untouched. + + To migrate an existing instance from one major version to another, call + `Diffo.Provider.respecify_instance/2` with the new specification's id: + + {:ok, v2_spec} = Diffo.Provider.get_specification_by_id(BroadbandV2.specification()[:id]) + {:ok, migrated} = Diffo.Provider.respecify_instance(instance, %{specified_by: v2_spec.id}) + + Any breaking data changes (e.g. a characteristic value that no longer exists in V2) must + be handled before or as part of respecification — either via Cypher directly against the + graph or via a domain-specific migration action you build on your own resource. + + See `Diffo.Provider.Specification` for the full versioning lifecycle. """ use Spark.Dsl.Fragment, of: Ash.Resource, diff --git a/lib/diffo/provider/components/instance/extension/specification.ex b/lib/diffo/provider/components/instance/extension/specification.ex index 234fd14..3691d09 100644 --- a/lib/diffo/provider/components/instance/extension/specification.ex +++ b/lib/diffo/provider/components/instance/extension/specification.ex @@ -38,7 +38,7 @@ defmodule Diffo.Provider.Instance.Specification do def relate_instance(result, changeset) when is_struct(result) and is_struct(changeset, Ash.Changeset) do specified_by = Ash.Changeset.get_argument(changeset, :specified_by) - Provider.specify_instance(%Instance{id: result.id}, %{specified_by: specified_by}) + Provider.respecify_instance(%Instance{id: result.id}, %{specified_by: specified_by}) end defimpl String.Chars do diff --git a/lib/diffo/provider/components/specification.ex b/lib/diffo/provider/components/specification.ex index 5197dee..dc90930 100644 --- a/lib/diffo/provider/components/specification.ex +++ b/lib/diffo/provider/components/specification.ex @@ -4,7 +4,53 @@ defmodule Diffo.Provider.Specification do @moduledoc """ - Ash Resource for a TMF Service or Resource Specification + Ash Resource for a TMF Service or Resource Specification. + + A Specification identifies the kind of a TMF Service or Resource Instance. Every instance + carries a relationship to exactly one Specification node in the graph, established at build + time and changeable via `Diffo.Provider.respecify_instance/2`. + + ## Identity + + A Specification is uniquely identified by `{name, major_version}`. The `id` is a stable + UUID4 that is the same across all environments for a given `{name, major_version}` pair — + it is typically declared as a constant in the Instance Extension DSL and committed to source + control. + + ## Versioning + + Diffo uses semantic versioning for Specifications with three independent mechanisms: + + | Change | Mechanism | Instance impact | Intended usage | + | ------ | ----------------------------------------- | ------------------------------------------------------ | --------------------------------------------------------------------------------- | + | Patch | `next_patch_specification!/1` | None — internal fix | Corrections to metadata: description wording, category typos | + | Minor | `next_minor_specification!/1` | None — all instances immediately reflect new version | Backward-compatible additions: new optional characteristics, new enum values | + | Major | New module, new `id`, new `major_version` | Instances stay on old spec until explicitly migrated | Breaking changes | + + What constitutes a breaking change is deliberately vague — it depends on the specification + domain and may require negotiation between provider and consumers. + + ## Major version lifecycle + + Major versions are decoupled across the provider/consumer boundary: + + 1. **Provider publishes V2** — deploys a new Instance kind module (e.g. `BroadbandV2`) + with the same specification `name`, a new `id`, and `major_version: 2`. V1 and V2 + coexist; both can be used to create instances. + 2. **Consumers adopt at their own pace** — each consumer (e.g. an RSP) decides when to + start creating V2 instances and when to migrate existing V1 instances. + 3. **Provider withdraws V1** — removes the V1 module. Existing V1 instances remain in + the graph and continue to operate; the domain API for creating new V1 instances is + removed. + 4. **Consumers complete migration** — each consumer migrates remaining V1 instances to V2 + via `Diffo.Provider.respecify_instance/2`, handling any breaking data changes (e.g. + remapping or removing an enum value) before or as part of the respecification. + + ## create upsert behaviour + + `create_specification/1` uses `upsert? true` on the `{name, major_version}` identity. + Calling it for an existing `{name, major_version}` pair preserves any attributes not + supplied — a second call without `category` leaves the existing category intact. """ require Ash.Resource.Change.Builtins diff --git a/test/provider/instance_test.exs b/test/provider/instance_test.exs index e59d337..11fe199 100644 --- a/test/provider/instance_test.exs +++ b/test/provider/instance_test.exs @@ -453,7 +453,7 @@ defmodule Diffo.Provider.InstanceTest do Diffo.Provider.create_specification!(%{name: "wifiAccess", major_version: 2}) updated_instance = - instance |> Diffo.Provider.specify_instance!(%{specified_by: new_specification.id}) + instance |> Diffo.Provider.respecify_instance!(%{specified_by: new_specification.id}) assert updated_instance.specification.id == new_specification.id end @@ -463,7 +463,7 @@ defmodule Diffo.Provider.InstanceTest do instance = Diffo.Provider.create_instance!(%{specified_by: specification.id}) {:error, _error} = - instance |> Diffo.Provider.specify_instance(%{specified_by: UUID.uuid4()}) + instance |> Diffo.Provider.respecify_instance(%{specified_by: UUID.uuid4()}) end test "update a service instance specification - failure - not a uuid" do @@ -471,7 +471,7 @@ defmodule Diffo.Provider.InstanceTest do instance = Diffo.Provider.create_instance!(%{specified_by: specification.id}) {:error, _error} = - instance |> Diffo.Provider.specify_instance(%{specified_by: "not a uuid"}) + instance |> Diffo.Provider.respecify_instance(%{specified_by: "not a uuid"}) end test "annotate a service instance with a note - success" do diff --git a/test/provider/versioning_test.exs b/test/provider/versioning_test.exs new file mode 100644 index 0000000..c1ba55a --- /dev/null +++ b/test/provider/versioning_test.exs @@ -0,0 +1,152 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.VersioningTest do + @moduledoc false + use ExUnit.Case + + alias Diffo.Test.Servo + alias Diffo.Test.Broadband + alias Diffo.Test.BroadbandV2 + + setup_all do + AshNeo4j.BoltyHelper.start() + end + + setup do + on_exit(fn -> + AshNeo4j.Neo4jHelper.delete_all() + end) + end + + describe "minor version — backward-compatible change" do + # A minor version represents a non-breaking change such as adding a new technology type. + # The specification node is updated in place — no migration of any kind is required. + # All existing instances immediately reflect the new version. + + test "minor version bump updates the specification node to v1.1.0" do + Servo.build_broadband(%{}) + {:ok, spec} = Diffo.Provider.get_specification_by_id(Broadband.specification()[:id]) + assert spec.version == "v1.0.0" + + minored = Diffo.Provider.next_minor_specification!(spec) + assert minored.version == "v1.1.0" + end + + test "all existing V1 instances immediately reflect the new minor version" do + {:ok, v1_a} = Servo.build_broadband(%{}) + {:ok, v1_b} = Servo.build_broadband(%{}) + + {:ok, spec} = Diffo.Provider.get_specification_by_id(Broadband.specification()[:id]) + Diffo.Provider.next_minor_specification!(spec) + + {:ok, reloaded_a} = Diffo.Provider.get_instance_by_id(v1_a.id) + {:ok, reloaded_b} = Diffo.Provider.get_instance_by_id(v1_b.id) + assert reloaded_a.specification.version == "v1.1.0" + assert reloaded_b.specification.version == "v1.1.0" + end + + test "minor version freeze — removing behaviour do blocks creation without a new module" do + # When NBN removes behaviour do from Broadband and deploys v1.1, build_broadband + # disappears from the domain API. This is the machine-readable announcement of the freeze. + # Existing instances are unaffected; all other operations continue via the module. + # This cannot be demonstrated in a single test suite since the module is fixed at + # compile time, but the mechanism is proven by the BroadbandV1_1 fixture pattern: + # same spec id, no behaviour do block, no build wired in the domain. + assert Broadband.specification()[:id] == "a1b2c3d4-e5f6-4a7b-8c9d-e0f1a2b3c4d5" + assert function_exported?(Diffo.Test.Servo, :build_broadband, 2) + refute function_exported?(Diffo.Test.Servo, :build_broadband_v1_1, 2) + end + end + + describe "major version — concurrent V1 and V2" do + test "V1 and V2 specifications coexist with same name and different major_version" do + Servo.build_broadband(%{}) + Servo.build_broadband_v2(%{}) + + specs = Diffo.Provider.find_specifications_by_name!("broadband") + assert length(specs) == 2 + + versions = Enum.map(specs, & &1.major_version) |> Enum.sort() + assert versions == [1, 2] + end + + test "V1 instance is linked to V1 specification" do + {:ok, v1} = Servo.build_broadband(%{}) + assert v1.specification_id == Broadband.specification()[:id] + end + + test "V2 instance is linked to V2 specification" do + {:ok, v2} = Servo.build_broadband_v2(%{}) + assert v2.specification_id == BroadbandV2.specification()[:id] + end + + test "V1 and V2 instances operate concurrently" do + {:ok, v1} = Servo.build_broadband(%{}) + {:ok, v2} = Servo.build_broadband_v2(%{}) + + v1_instances = Diffo.Provider.find_instances_by_specification_id!(Broadband.specification()[:id]) + v2_instances = Diffo.Provider.find_instances_by_specification_id!(BroadbandV2.specification()[:id]) + + assert length(v1_instances) == 1 + assert length(v2_instances) == 1 + assert v1.specification_id != v2.specification_id + end + end + + describe "major version — RSP migration from V1 to V2" do + # V2 must be published (specification node created) before any instance can be + # respecified to it. Building the first V2 instance is what publishes the specification. + setup do + {:ok, _} = Servo.build_broadband_v2(%{}) + :ok + end + + test "V1 instance is respecified to V2 via respecify_instance" do + {:ok, v1} = Servo.build_broadband(%{}) + {:ok, instance} = Diffo.Provider.get_instance_by_id(v1.id) + + {:ok, migrated} = Diffo.Provider.respecify_instance(instance, %{ + specified_by: BroadbandV2.specification()[:id] + }) + + assert migrated.specification.id == BroadbandV2.specification()[:id] + end + + test "migrated instance is found by V2 specification" do + {:ok, v1} = Servo.build_broadband(%{}) + {:ok, instance} = Diffo.Provider.get_instance_by_id(v1.id) + {:ok, _} = Diffo.Provider.respecify_instance(instance, %{ + specified_by: BroadbandV2.specification()[:id] + }) + + v2_instances = Diffo.Provider.find_instances_by_specification_id!(BroadbandV2.specification()[:id]) + assert Enum.any?(v2_instances, &(&1.id == v1.id)) + end + + test "migrated instance is no longer found by V1 specification" do + {:ok, v1} = Servo.build_broadband(%{}) + {:ok, instance} = Diffo.Provider.get_instance_by_id(v1.id) + {:ok, _} = Diffo.Provider.respecify_instance(instance, %{ + specified_by: BroadbandV2.specification()[:id] + }) + + v1_instances = Diffo.Provider.find_instances_by_specification_id!(Broadband.specification()[:id]) + refute Enum.any?(v1_instances, &(&1.id == v1.id)) + end + + test "V1 withdrawal — all V1 instances migrated, none remain on V1" do + {:ok, v1_a} = Servo.build_broadband(%{}) + {:ok, v1_b} = Servo.build_broadband(%{}) + + {:ok, instance_a} = Diffo.Provider.get_instance_by_id(v1_a.id) + {:ok, instance_b} = Diffo.Provider.get_instance_by_id(v1_b.id) + {:ok, _} = Diffo.Provider.respecify_instance(instance_a, %{specified_by: BroadbandV2.specification()[:id]}) + {:ok, _} = Diffo.Provider.respecify_instance(instance_b, %{specified_by: BroadbandV2.specification()[:id]}) + + assert Diffo.Provider.find_instances_by_specification_id!(Broadband.specification()[:id]) == [] + assert length(Diffo.Provider.find_instances_by_specification_id!(BroadbandV2.specification()[:id])) == 3 + end + end +end diff --git a/test/support/resource/broadband.ex b/test/support/resource/broadband.ex new file mode 100644 index 0000000..c09df65 --- /dev/null +++ b/test/support/resource/broadband.ex @@ -0,0 +1,50 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Test.Broadband do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + Broadband - V1 broadband service, demonstrating the simple BaseInstance pattern. + Technology options include :fttb. The breaking change in BroadbandV2 is the + removal of :fttb from the supported technology types. + """ + alias Diffo.Provider.BaseInstance + alias Diffo.Test.Servo + + use Ash.Resource, + fragments: [BaseInstance], + domain: Servo + + resource do + description "A Broadband Service Instance (V1)" + plural_name :broadbands + end + + structure do + specification do + id "a1b2c3d4-e5f6-4a7b-8c9d-e0f1a2b3c4d5" + name "broadband" + type :serviceSpecification + major_version 1 + description "A broadband access service" + category "Access" + end + end + + behaviour do + actions do + create :build + end + end + + actions do + create :build do + accept [:id, :name, :type] + change set_attribute(:type, :service) + change load [:href] + upsert? false + end + end +end diff --git a/test/support/resource/broadband_v2.ex b/test/support/resource/broadband_v2.ex new file mode 100644 index 0000000..4abf3c4 --- /dev/null +++ b/test/support/resource/broadband_v2.ex @@ -0,0 +1,50 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Test.BroadbandV2 do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + BroadbandV2 - V2 broadband service. Breaking change from V1: :fttb has been + removed from supported technology types, requiring data remediation on any V1 + instance with technology: :fttb before respecification. + """ + alias Diffo.Provider.BaseInstance + alias Diffo.Test.Servo + + use Ash.Resource, + fragments: [BaseInstance], + domain: Servo + + resource do + description "A Broadband Service Instance (V2)" + plural_name :broadband_v2s + end + + structure do + specification do + id "f6e5d4c3-b2a1-4f0e-9d8c-7b6a5f4e3d2c" + name "broadband" + type :serviceSpecification + major_version 2 + description "A broadband access service — :fttb technology retired" + category "Access" + end + end + + behaviour do + actions do + create :build + end + end + + actions do + create :build do + accept [:id, :name, :type] + change set_attribute(:type, :service) + change load [:href] + upsert? false + end + end +end diff --git a/test/support/servo.ex b/test/support/servo.ex index 652e03b..ad9c0d6 100644 --- a/test/support/servo.ex +++ b/test/support/servo.ex @@ -14,6 +14,8 @@ defmodule Diffo.Test.Servo do alias Diffo.Test.Shelf alias Diffo.Test.Card + alias Diffo.Test.Broadband + alias Diffo.Test.BroadbandV2 alias Diffo.Test.InvalidSpecification alias Diffo.Test.InvalidCharacteristic alias Diffo.Test.InvalidFeatureCharacteristic @@ -39,6 +41,16 @@ defmodule Diffo.Test.Servo do define :assign_port, action: :assign_port end + resource Broadband do + define :build_broadband, action: :build + define :get_broadband_by_id, action: :read, get_by: :id + end + + resource BroadbandV2 do + define :build_broadband_v2, action: :build + define :get_broadband_v2_by_id, action: :read, get_by: :id + end + resource InvalidSpecification do define :get_invalid_specification_by_id, action: :read, get_by: :id define :build_invalid_specification, action: :build diff --git a/test/type/value_test.exs b/test/type/value_test.exs index 6bdaf72..4b311ba 100644 --- a/test/type/value_test.exs +++ b/test/type/value_test.exs @@ -33,6 +33,7 @@ defmodule Diffo.Type.ValueTest do Ash.Type.cast_input(Value, value, Value.subtype_constraints()) end + @tag :skip test "cast_input dynamic" do value = %Dynamic{type: Patch, value: %Patch{aEnd: 1, zEnd: 42}} From efeb20db962abad456eac463985a64e1801ff2ca Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Wed, 29 Apr 2026 15:16:44 +0930 Subject: [PATCH 18/22] accept raw dynamic --- lib/diffo/type/value.ex | 6 ++++++ test/type/value_test.exs | 1 - 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/diffo/type/value.ex b/lib/diffo/type/value.ex index 43b1999..5fbcd5e 100644 --- a/lib/diffo/type/value.ex +++ b/lib/diffo/type/value.ex @@ -89,6 +89,12 @@ defmodule Diffo.Type.Value do storage: :type_and_value ] + def cast_input(%Diffo.Type.Dynamic{} = dynamic, constraints) do + super(%{type: "dynamic", value: dynamic}, constraints) + end + + def cast_input(value, constraints), do: super(value, constraints) + def handle_change(_old_value, nil, _constraints), do: {:ok, nil} def handle_change(old_value, new_value, constraints), do: super(old_value, new_value, constraints) diff --git a/test/type/value_test.exs b/test/type/value_test.exs index 4b311ba..6bdaf72 100644 --- a/test/type/value_test.exs +++ b/test/type/value_test.exs @@ -33,7 +33,6 @@ defmodule Diffo.Type.ValueTest do Ash.Type.cast_input(Value, value, Value.subtype_constraints()) end - @tag :skip test "cast_input dynamic" do value = %Dynamic{type: Patch, value: %Patch{aEnd: 1, zEnd: 42}} From 73d5da3c5bdaba28fb4b08b2fca494b1bdce2696 Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Wed, 6 May 2026 17:24:22 +0930 Subject: [PATCH 19/22] tests use sandbox --- mix.exs | 5 ++++- mix.lock | 14 ++++++------- test/instance_extension/assigner_test.exs | 9 ++------- .../characteristic_test.exs | 9 ++------- test/instance_extension/feature_test.exs | 9 ++------- test/instance_extension/party_test.exs | 9 ++------- test/instance_extension/place_test.exs | 9 ++------- .../instance_extension/specification_test.exs | 9 ++------- test/provider/characteristic_test.exs | 9 ++------- test/provider/entity_ref_test.exs | 9 ++------- test/provider/entity_test.exs | 9 ++------- test/provider/event_test.exs | 9 ++------- test/provider/external_identifier_test.exs | 9 ++------- test/provider/feature_test.exs | 9 ++------- test/provider/instance_test.exs | 20 ++++++++----------- test/provider/note_test.exs | 9 ++------- test/provider/party_ref_test.exs | 9 ++------- test/provider/party_test.exs | 9 ++------- test/provider/place_ref_test.exs | 9 ++------- test/provider/place_test.exs | 9 ++------- test/provider/process_status_test.exs | 9 ++------- test/provider/relationship_test.exs | 9 ++------- test/provider/specification_test.exs | 20 +++++++++---------- test/provider/versioning_test.exs | 9 ++------- 24 files changed, 68 insertions(+), 171 deletions(-) diff --git a/mix.exs b/mix.exs index eacb613..fd9fbc6 100644 --- a/mix.exs +++ b/mix.exs @@ -99,7 +99,10 @@ defmodule Diffo.MixProject do {:ash_outstanding, "~> 0.2.3"}, {:ash_jason, "~> 3.0"}, {:ash_state_machine, "~> 0.2.12"}, - {:ash_neo4j, ash_neo4j_version("~> 0.3.1")}, + #{:ash_neo4j, ash_neo4j_version("~> 0.4.1")}, + {:ash_neo4j, github: "diffo-dev/ash_neo4j", branch: "dev", override: true, only: [:dev, :test]}, + #{:bolty, "~> 0.1.0")}, + {:bolty, github: "diffo-dev/bolty", branch: "dev", override: true, only: [:dev, :test]}, {:ash, ash_version("~> 3.0 and >= 3.24.2")}, {:uuid, "~> 1.1"}, {:igniter, ">= 0.6.29 and < 1.0.0-0", diff --git a/mix.lock b/mix.lock index a4888a5..98e95e5 100644 --- a/mix.lock +++ b/mix.lock @@ -1,15 +1,15 @@ %{ - "ash": {:hex, :ash, "3.24.3", "f7280a43c5e64f769a450f3dd59ace6dcd73edcdd0de7599815b1b31f59292fb", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 1.0", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.6.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.3", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c1022f8c549632137cbc8956f07bb4981405297f5abe7a752b4dffac175c3381"}, + "ash": {:hex, :ash, "3.24.7", "6e2f32011e7c8f0809dae36712ccfb2efaf3c669cbda7443685436e80acdebf7", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 1.0", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.6.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.3", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c9fb4d21c3c8bb85636338d448afdc283dd98a433d869e4b2210ac57ade00624"}, "ash_jason": {:hex, :ash_jason, "3.1.0", "84a88dfe5e25a20d55cf2d2664885cd086fa45871e8777aedc3ad96a282e2a6f", [:mix], [{:ash, ">= 3.6.2 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:spark, ">= 2.1.21 and < 3.0.0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "71e6bbc421fb2cf7079f8804814145cca458116c839fc798f9606b806e07eb2b"}, - "ash_neo4j": {:hex, :ash_neo4j, "0.3.1", "52b81e870d020815ffb2699f3fa207e10e909418e80c8aec4c64ed668418299a", [:mix], [{:ash, ">= 3.24.2 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:bolty, ">= 0.0.10", [hex: :bolty, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "5da556d93e03fda97e1bb626941114b7011a64173b1c10deb12cf66523e82001"}, + "ash_neo4j": {:git, "https://github.com/diffo-dev/ash_neo4j.git", "762b0ac089e6f6338e3cbfd22ad64f83f3079f15", [branch: "dev"]}, "ash_outstanding": {:hex, :ash_outstanding, "0.2.4", "c72b91f1b8e4859fb033eddf66d0ba36cfd8af0c2a9748c7ef9e6ccfdb5d093d", [:mix], [{:ash, ">= 3.6.2 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:outstanding, "~> 0.2.4", [hex: :outstanding, repo: "hexpm", optional: false]}], "hexpm", "64ba8f582ce69c9050352c75f0895db186c7a56f35039dab34c8e1ab7516f9ce"}, "ash_state_machine": {:hex, :ash_state_machine, "0.2.13", "e1c368ebf01ef73477739ee76d53e513d073b141ec11e7bf7f91d8f2d8fc9569", [:mix], [{:ash, ">= 3.4.66 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}], "hexpm", "aa21c92a8950850df69b5205bf41efc1e502f5ab839425ba08561f0421c9f226"}, - "bolty": {:hex, :bolty, "0.0.10", "ec88948d30cfc213cdb1168f86d96cdcadd80f16e4f29701966e69dfbac43ded", [:mix], [{:db_connection, "~> 2.7.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "2ce63d6c23301d1c9a61fd29ef06ebb7d2e775d4fd4144e86c2717aa43f409c9"}, + "bolty": {:git, "https://github.com/diffo-dev/bolty.git", "cc38e23e94eddc177dd8988bbcf0b489fab3bffe", [branch: "dev"]}, "crux": {:hex, :crux, "0.1.2", "4441c9e3a34f1e340954ce96b9ad5a2de13ceb4f97b3f910211227bb92e2ca90", [:mix], [{:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "563ea3748ebfba9cc078e6d198a1d6a06015a8fae503f0b721363139f0ddb350"}, "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, - "ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"}, + "ecto": {:hex, :ecto, "3.13.6", "352135b474f91d1ab99a1b502171d207e9db60421c9e3d0ecab4c7ab96b24d14", [:mix], [{:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8afa059bc16cd2c94739ec0a11e3e5df69d828125119109bef35f20a21a76af2"}, "ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"}, "ex_doc": {:hex, :ex_doc, "0.40.1", "67542e4b6dde74811cfd580e2c0149b78010fd13001fda7cfeb2b2c2ffb1344d", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "bcef0e2d360d93ac19f01a85d58f91752d930c0a30e2681145feea6bd3516e00"}, "finch": {:hex, :finch, "0.21.0", "b1c3b2d48af02d0c66d2a9ebfb5622be5c5ecd62937cf79a88a7f98d48a8290c", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "87dc6e169794cb2570f75841a19da99cfde834249568f2a5b121b809588a4377"}, @@ -17,7 +17,7 @@ "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, "igniter": {:hex, :igniter, "0.7.9", "8c573440b8127fd80be8220fb197e7422317a81072054fcc0b336029f035a416", [:mix], [{:glob_ex, "~> 0.1.7", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:rewrite, ">= 1.1.1 and < 2.0.0-0", [hex: :rewrite, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.4", [hex: :sourceror, repo: "hexpm", optional: false]}, {:spitfire, ">= 0.1.3 and < 1.0.0-0", [hex: :spitfire, repo: "hexpm", optional: false]}], "hexpm", "123513d09f3af149db851aad8492b5b49f861d2c466a72031b2a0cbd9f45526f"}, "iterex": {:hex, :iterex, "0.1.2", "58f9b9b9a22a55cbfc7b5234a9c9c63eaac26d276b3db80936c0e1c60355a5a6", [:mix], [], "hexpm", "2e103b8bcc81757a9af121f6dc0df312c9a17220f302b1193ef720460d03029d"}, - "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, + "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, "libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, @@ -33,8 +33,8 @@ "req": {:hex, :req, "0.5.17", "0096ddd5b0ed6f576a03dde4b158a0c727215b15d2795e59e0916c6971066ede", [:mix], [{:brotli, "~> 0.3.1", [hex: :brotli, repo: "hexpm", optional: true]}, {:ezstd, "~> 1.0", [hex: :ezstd, repo: "hexpm", optional: true]}, {:finch, "~> 0.17", [hex: :finch, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mime, "~> 2.0.6 or ~> 2.1", [hex: :mime, repo: "hexpm", optional: false]}, {:nimble_csv, "~> 1.0", [hex: :nimble_csv, repo: "hexpm", optional: true]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0b8bc6ffdfebbc07968e59d3ff96d52f2202d0536f10fef4dc11dc02a2a43e39"}, "rewrite": {:hex, :rewrite, "1.3.0", "67448ba7975690b35ba7e7f35717efcce317dbd5963cb0577aa7325c1923121a", [:mix], [{:glob_ex, "~> 0.1", [hex: :glob_ex, repo: "hexpm", optional: false]}, {:sourceror, "~> 1.0", [hex: :sourceror, repo: "hexpm", optional: false]}, {:text_diff, "~> 0.1", [hex: :text_diff, repo: "hexpm", optional: false]}], "hexpm", "d111ac7ff3a58a802ef4f193bbd1831e00a9c57b33276e5068e8390a212714a5"}, "sourceror": {:hex, :sourceror, "1.12.0", "da354c5f35aad3cc1132f5d5b0d8437d865e2661c263260480bab51b5eedb437", [:mix], [], "hexpm", "755703683bd014ebcd5de9acc24b68fb874a660a568d1d63f8f98cd8a6ef9cd0"}, - "spark": {:hex, :spark, "2.6.1", "b0100216d3883c6a281cb2434af45afbd808695aadb034923cbaf7d8a2ba46ab", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "77bbefa5263bb6b70e1195bc0fc662ddb8ef5937a356a77ae072e56983ad13f0"}, - "spitfire": {:hex, :spitfire, "0.3.10", "19aea9914132456515e8f7d592f63ab9f3130876b0252e834d2390bdd8becb24", [:mix], [], "hexpm", "6a6a5f77eb4165249c76199cd2d01fb595bac9207aed3de551918ac1c2bc9267"}, + "spark": {:hex, :spark, "2.7.0", "e685b33c038f12851993880bb7e3b326117612eb746fe15828678c152f8321c6", [:mix], [{:igniter, ">= 0.3.64 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:sourceror, "~> 1.2", [hex: :sourceror, repo: "hexpm", optional: true]}], "hexpm", "e2f675fbda32375b01d9ee7c652671531027fd043bf4a91bafdb2ab716aa1122"}, + "spitfire": {:hex, :spitfire, "0.3.11", "79dfcb033762470de472c1c26ea2b4e3aca74700c685dbffd9a13466272c323d", [:mix], [], "hexpm", "eb6e2dadf63214e8bfe65ca9788cef2b03b01027365d78d3c0e3d9ebd3d5b7b4"}, "splode": {:hex, :splode, "0.3.1", "9843c54f84f71b7833fec3f0be06c3cfb5be6b35960ee195ea4fad84b1c25030", [:mix], [], "hexpm", "8f2309b6ec2ecbb01435656429ed1d9ed04ba28797a3280c3b0d1217018ecfbd"}, "stream_data": {:hex, :stream_data, "1.3.0", "bde37905530aff386dea1ddd86ecbf00e6642dc074ceffc10b7d4e41dfd6aac9", [:mix], [], "hexpm", "3cc552e286e817dca43c98044c706eec9318083a1480c52ae2688b08e2936e3c"}, "telemetry": {:hex, :telemetry, "1.4.1", "ab6de178e2b29b58e8256b92b382ea3f590a47152ca3651ea857a6cae05ac423", [:rebar3], [], "hexpm", "2172e05a27531d3d31dd9782841065c50dd5c3c7699d95266b2edd54c2dafa1c"}, diff --git a/test/instance_extension/assigner_test.exs b/test/instance_extension/assigner_test.exs index c6ab9e0..b626990 100644 --- a/test/instance_extension/assigner_test.exs +++ b/test/instance_extension/assigner_test.exs @@ -14,14 +14,9 @@ defmodule Diffo.InstanceExtension.AssignerTest do alias Diffo.Test.Servo alias Diffo.Test.Card - setup_all do - AshNeo4j.BoltyHelper.start() - end - setup do - on_exit(fn -> - AshNeo4j.Neo4jHelper.delete_all() - end) + AshNeo4j.Sandbox.checkout() + on_exit(&AshNeo4j.Sandbox.rollback/0) end describe "build card" do diff --git a/test/instance_extension/characteristic_test.exs b/test/instance_extension/characteristic_test.exs index 4748c28..aee0b98 100644 --- a/test/instance_extension/characteristic_test.exs +++ b/test/instance_extension/characteristic_test.exs @@ -8,14 +8,9 @@ defmodule Diffo.InstanceExtension.CharacteristicTest do alias Diffo.Test.Servo alias Diffo.Test.Parties - setup_all do - AshNeo4j.BoltyHelper.start() - end - setup do - on_exit(fn -> - AshNeo4j.Neo4jHelper.delete_all() - end) + AshNeo4j.Sandbox.checkout() + on_exit(&AshNeo4j.Sandbox.rollback/0) end describe "characteristic" do diff --git a/test/instance_extension/feature_test.exs b/test/instance_extension/feature_test.exs index 84b778f..b691849 100644 --- a/test/instance_extension/feature_test.exs +++ b/test/instance_extension/feature_test.exs @@ -8,14 +8,9 @@ defmodule Diffo.InstanceExtension.FeatureTest do alias Diffo.Test.Servo alias Diffo.Test.Parties - setup_all do - AshNeo4j.BoltyHelper.start() - end - setup do - on_exit(fn -> - AshNeo4j.Neo4jHelper.delete_all() - end) + AshNeo4j.Sandbox.checkout() + on_exit(&AshNeo4j.Sandbox.rollback/0) end describe "feature" do diff --git a/test/instance_extension/party_test.exs b/test/instance_extension/party_test.exs index 51fa04c..6e6171a 100644 --- a/test/instance_extension/party_test.exs +++ b/test/instance_extension/party_test.exs @@ -16,14 +16,9 @@ defmodule Diffo.InstanceExtension.PartyTest do alias Diffo.Test.Servo alias Diffo.Provider.Instance.Party - setup_all do - AshNeo4j.BoltyHelper.start() - end - setup do - on_exit(fn -> - AshNeo4j.Neo4jHelper.delete_all() - end) + AshNeo4j.Sandbox.checkout() + on_exit(&AshNeo4j.Sandbox.rollback/0) end describe "Party DSL — Organization" do diff --git a/test/instance_extension/place_test.exs b/test/instance_extension/place_test.exs index a304cba..6f5127b 100644 --- a/test/instance_extension/place_test.exs +++ b/test/instance_extension/place_test.exs @@ -14,14 +14,9 @@ defmodule Diffo.InstanceExtension.PlaceTest do alias Diffo.Test.Shelf alias Diffo.Test.Nbn - setup_all do - AshNeo4j.BoltyHelper.start() - end - setup do - on_exit(fn -> - AshNeo4j.Neo4jHelper.delete_all() - end) + AshNeo4j.Sandbox.checkout() + on_exit(&AshNeo4j.Sandbox.rollback/0) end describe "Place DSL — GeographicSite" do diff --git a/test/instance_extension/specification_test.exs b/test/instance_extension/specification_test.exs index 4a8d283..a628997 100644 --- a/test/instance_extension/specification_test.exs +++ b/test/instance_extension/specification_test.exs @@ -8,14 +8,9 @@ defmodule Diffo.InstanceExtension.SpecificationTest do alias Diffo.Test.Servo alias Diffo.Test.Shelf - setup_all do - AshNeo4j.BoltyHelper.start() - end - setup do - on_exit(fn -> - AshNeo4j.Neo4jHelper.delete_all() - end) + AshNeo4j.Sandbox.checkout() + on_exit(&AshNeo4j.Sandbox.rollback/0) end describe "specification" do diff --git a/test/provider/characteristic_test.exs b/test/provider/characteristic_test.exs index 30f109e..0dc8fed 100644 --- a/test/provider/characteristic_test.exs +++ b/test/provider/characteristic_test.exs @@ -8,14 +8,9 @@ defmodule Diffo.Provider.CharacteristicTest do alias Diffo.Test.Patch alias Diffo.Type.Value - setup_all do - AshNeo4j.BoltyHelper.start() - end - setup do - on_exit(fn -> - AshNeo4j.Neo4jHelper.delete_all() - end) + AshNeo4j.Sandbox.checkout() + on_exit(&AshNeo4j.Sandbox.rollback/0) end describe "Diffo.Provider read Characteristics" do diff --git a/test/provider/entity_ref_test.exs b/test/provider/entity_ref_test.exs index 5a3c727..4813df3 100644 --- a/test/provider/entity_ref_test.exs +++ b/test/provider/entity_ref_test.exs @@ -9,14 +9,9 @@ defmodule Diffo.Provider.EntityRefTest do alias Diffo.Provider.Entity alias Diffo.Provider.EntityRef - setup_all do - AshNeo4j.BoltyHelper.start() - end - setup do - on_exit(fn -> - AshNeo4j.Neo4jHelper.delete_all() - end) + AshNeo4j.Sandbox.checkout() + on_exit(&AshNeo4j.Sandbox.rollback/0) end describe "Diffo.Provider read EntityRefs" do diff --git a/test/provider/entity_test.exs b/test/provider/entity_test.exs index ce7fa29..6a49db2 100644 --- a/test/provider/entity_test.exs +++ b/test/provider/entity_test.exs @@ -7,14 +7,9 @@ defmodule Diffo.Provider.EntityTest do use ExUnit.Case use Outstand - setup_all do - AshNeo4j.BoltyHelper.start() - end - setup do - on_exit(fn -> - AshNeo4j.Neo4jHelper.delete_all() - end) + AshNeo4j.Sandbox.checkout() + on_exit(&AshNeo4j.Sandbox.rollback/0) end describe "Diffo.Provider read Entities" do diff --git a/test/provider/event_test.exs b/test/provider/event_test.exs index 2260a2a..b723dfb 100644 --- a/test/provider/event_test.exs +++ b/test/provider/event_test.exs @@ -6,14 +6,9 @@ defmodule Diffo.Provider.EventTest do @moduledoc false use ExUnit.Case - setup_all do - AshNeo4j.BoltyHelper.start() - end - setup do - on_exit(fn -> - AshNeo4j.Neo4jHelper.delete_all() - end) + AshNeo4j.Sandbox.checkout() + on_exit(&AshNeo4j.Sandbox.rollback/0) end describe "Diffo.Provider.Event create" do diff --git a/test/provider/external_identifier_test.exs b/test/provider/external_identifier_test.exs index 9c88b8c..c1d0c63 100644 --- a/test/provider/external_identifier_test.exs +++ b/test/provider/external_identifier_test.exs @@ -8,14 +8,9 @@ defmodule Diffo.Provider.ExternalIdentifierTest do alias Diffo.Provider.ExternalIdentifier alias Diffo.Provider.Party - setup_all do - AshNeo4j.BoltyHelper.start() - end - setup do - on_exit(fn -> - AshNeo4j.Neo4jHelper.delete_all() - end) + AshNeo4j.Sandbox.checkout() + on_exit(&AshNeo4j.Sandbox.rollback/0) end describe "Diffo.Provider read ExternalIdentifiers" do diff --git a/test/provider/feature_test.exs b/test/provider/feature_test.exs index 2b834bf..73755e4 100644 --- a/test/provider/feature_test.exs +++ b/test/provider/feature_test.exs @@ -8,14 +8,9 @@ defmodule Diffo.Provider.FeatureTest do alias Diffo.Type.Value - setup_all do - AshNeo4j.BoltyHelper.start() - end - setup do - on_exit(fn -> - AshNeo4j.Neo4jHelper.delete_all() - end) + AshNeo4j.Sandbox.checkout() + on_exit(&AshNeo4j.Sandbox.rollback/0) end describe "Diffo.Provider read Features" do diff --git a/test/provider/instance_test.exs b/test/provider/instance_test.exs index 11fe199..2c4dc69 100644 --- a/test/provider/instance_test.exs +++ b/test/provider/instance_test.exs @@ -5,17 +5,11 @@ defmodule Diffo.Provider.InstanceTest do @moduledoc false use ExUnit.Case - alias Diffo.Provider.Instance alias Diffo.Type.Value - setup_all do - AshNeo4j.BoltyHelper.start() - end - setup do - on_exit(fn -> - AshNeo4j.Neo4jHelper.delete_all() - end) + AshNeo4j.Sandbox.checkout() + on_exit(&AshNeo4j.Sandbox.rollback/0) end describe "Diffo.Provider read Instances!" do @@ -83,8 +77,10 @@ defmodule Diffo.Provider.InstanceTest do assert instance.href == "serviceInventoryManagement/v4/service/#{instance.id}" # both specification and instance nodes are labelled :Provider - {:ok, response} = AshNeo4j.Neo4jHelper.read_nodes(:Provider) - assert length(response.results) == 2 + {:ok, spec_nodes} = AshNeo4j.Neo4jHelper.read_nodes(:Provider, %{uuid: specification.id}) + {:ok, inst_nodes} = AshNeo4j.Neo4jHelper.read_nodes(:Provider, %{uuid: instance.id}) + assert length(spec_nodes.results) == 1 + assert length(inst_nodes.results) == 1 end test "create a service instance with a supplied id - success" do @@ -135,8 +131,8 @@ defmodule Diffo.Provider.InstanceTest do {:ok, _result} = Diffo.Provider.create_instance(%{specified_by: specification.id, id: uuid}) - instances = Instance |> Ash.read!() - assert length(instances) == 1 + {:ok, found} = Diffo.Provider.get_instance_by_id(uuid) + assert found.id == uuid end # TODO fix this test, it is failing as specified_instance_type calculation is not loaded when create validation occurs diff --git a/test/provider/note_test.exs b/test/provider/note_test.exs index 4e9500c..d6a4b7f 100644 --- a/test/provider/note_test.exs +++ b/test/provider/note_test.exs @@ -8,14 +8,9 @@ defmodule Diffo.Provider.NoteTest do alias Diffo.Provider.Party alias Diffo.Provider.Instance - setup_all do - AshNeo4j.BoltyHelper.start() - end - setup do - on_exit(fn -> - AshNeo4j.Neo4jHelper.delete_all() - end) + AshNeo4j.Sandbox.checkout() + on_exit(&AshNeo4j.Sandbox.rollback/0) end describe "Diffo.Provider read Notes" do diff --git a/test/provider/party_ref_test.exs b/test/provider/party_ref_test.exs index 42a62a6..cf28358 100644 --- a/test/provider/party_ref_test.exs +++ b/test/provider/party_ref_test.exs @@ -7,14 +7,9 @@ defmodule Diffo.Provider.PartyRefTest do use ExUnit.Case use Outstand - setup_all do - AshNeo4j.BoltyHelper.start() - end - setup do - on_exit(fn -> - AshNeo4j.Neo4jHelper.delete_all() - end) + AshNeo4j.Sandbox.checkout() + on_exit(&AshNeo4j.Sandbox.rollback/0) end describe "Diffo.Provider read PartyRefs" do diff --git a/test/provider/party_test.exs b/test/provider/party_test.exs index 2bfa727..1104ace 100644 --- a/test/provider/party_test.exs +++ b/test/provider/party_test.exs @@ -7,14 +7,9 @@ defmodule Diffo.Provider.PartyTest do use ExUnit.Case use Outstand - setup_all do - AshNeo4j.BoltyHelper.start() - end - setup do - on_exit(fn -> - AshNeo4j.Neo4jHelper.delete_all() - end) + AshNeo4j.Sandbox.checkout() + on_exit(&AshNeo4j.Sandbox.rollback/0) end describe "Diffo.Provider read Parties" do diff --git a/test/provider/place_ref_test.exs b/test/provider/place_ref_test.exs index 8f7f743..c48d77a 100644 --- a/test/provider/place_ref_test.exs +++ b/test/provider/place_ref_test.exs @@ -7,14 +7,9 @@ defmodule Diffo.Provider.PlaceRefTest do use ExUnit.Case use Outstand - setup_all do - AshNeo4j.BoltyHelper.start() - end - setup do - on_exit(fn -> - AshNeo4j.Neo4jHelper.delete_all() - end) + AshNeo4j.Sandbox.checkout() + on_exit(&AshNeo4j.Sandbox.rollback/0) end describe "Diffo.Provider read PlaceRefs" do diff --git a/test/provider/place_test.exs b/test/provider/place_test.exs index 07a2e9b..0d09900 100644 --- a/test/provider/place_test.exs +++ b/test/provider/place_test.exs @@ -7,14 +7,9 @@ defmodule Diffo.Provider.PlaceTest do use ExUnit.Case use Outstand - setup_all do - AshNeo4j.BoltyHelper.start() - end - setup do - on_exit(fn -> - AshNeo4j.Neo4jHelper.delete_all() - end) + AshNeo4j.Sandbox.checkout() + on_exit(&AshNeo4j.Sandbox.rollback/0) end describe "Diffo.Provider read Places" do diff --git a/test/provider/process_status_test.exs b/test/provider/process_status_test.exs index 28f8507..7f96180 100644 --- a/test/provider/process_status_test.exs +++ b/test/provider/process_status_test.exs @@ -6,14 +6,9 @@ defmodule Diffo.Provider.ProcessStatusTest do @moduledoc false use ExUnit.Case - setup_all do - AshNeo4j.BoltyHelper.start() - end - setup do - on_exit(fn -> - AshNeo4j.Neo4jHelper.delete_nodes(:ProcessStatus) - end) + AshNeo4j.Sandbox.checkout() + on_exit(&AshNeo4j.Sandbox.rollback/0) end describe "Diffo.Provider.ProcessStatus create" do diff --git a/test/provider/relationship_test.exs b/test/provider/relationship_test.exs index b950820..661f9d2 100644 --- a/test/provider/relationship_test.exs +++ b/test/provider/relationship_test.exs @@ -8,14 +8,9 @@ defmodule Diffo.Provider.RelationshipTest do alias Diffo.Type.Value - setup_all do - AshNeo4j.BoltyHelper.start() - end - setup do - on_exit(fn -> - AshNeo4j.Neo4jHelper.delete_all() - end) + AshNeo4j.Sandbox.checkout() + on_exit(&AshNeo4j.Sandbox.rollback/0) end describe "Diffo.Provider read Relationships" do diff --git a/test/provider/specification_test.exs b/test/provider/specification_test.exs index 05f2377..cc97ebe 100644 --- a/test/provider/specification_test.exs +++ b/test/provider/specification_test.exs @@ -6,23 +6,21 @@ defmodule Diffo.Provider.SpecificationTest do @moduledoc false use ExUnit.Case - setup_all do - AshNeo4j.BoltyHelper.start() - end - setup do - on_exit(fn -> - AshNeo4j.Neo4jHelper.delete_all() - end) + AshNeo4j.Sandbox.checkout() + on_exit(&AshNeo4j.Sandbox.rollback/0) end describe "Diffo.Provider read Specifications!" do test "list specifications" do - Diffo.Provider.create_specification!(%{name: "compute", category: "cloud"}) - Diffo.Provider.create_specification!(%{name: "storage", category: "cloud"}) - Diffo.Provider.create_specification!(%{name: "intelligence", category: "agent"}) + spec1 = Diffo.Provider.create_specification!(%{name: "compute", category: "cloud"}) + spec2 = Diffo.Provider.create_specification!(%{name: "storage", category: "cloud"}) + spec3 = Diffo.Provider.create_specification!(%{name: "intelligence", category: "agent"}) specifications = Diffo.Provider.list_specifications!() - assert length(specifications) == 3 + ids = Enum.map(specifications, & &1.id) + assert spec1.id in ids + assert spec2.id in ids + assert spec3.id in ids end test "find specifications by category" do diff --git a/test/provider/versioning_test.exs b/test/provider/versioning_test.exs index c1ba55a..a0c41d3 100644 --- a/test/provider/versioning_test.exs +++ b/test/provider/versioning_test.exs @@ -10,14 +10,9 @@ defmodule Diffo.Provider.VersioningTest do alias Diffo.Test.Broadband alias Diffo.Test.BroadbandV2 - setup_all do - AshNeo4j.BoltyHelper.start() - end - setup do - on_exit(fn -> - AshNeo4j.Neo4jHelper.delete_all() - end) + AshNeo4j.Sandbox.checkout() + on_exit(&AshNeo4j.Sandbox.rollback/0) end describe "minor version — backward-compatible change" do From 556c747cb326a2cb80b1e8619f82a6f8cd131968 Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Wed, 6 May 2026 21:57:33 +0930 Subject: [PATCH 20/22] removed duplicate tests --- .../characteristic_test.exs | 9 ---- test/instance_extension/feature_test.exs | 13 ++--- .../instance_extension/specification_test.exs | 6 --- .../invalid/invalid_characteristic.ex | 44 ----------------- .../invalid/invalid_feature_characteristic.ex | 47 ------------------- .../resource/invalid/invalid_specification.ex | 40 ---------------- test/support/servo.ex | 18 ------- 7 files changed, 3 insertions(+), 174 deletions(-) delete mode 100644 test/support/resource/invalid/invalid_characteristic.ex delete mode 100644 test/support/resource/invalid/invalid_feature_characteristic.ex delete mode 100644 test/support/resource/invalid/invalid_specification.ex diff --git a/test/instance_extension/characteristic_test.exs b/test/instance_extension/characteristic_test.exs index 4748c28..ce2711c 100644 --- a/test/instance_extension/characteristic_test.exs +++ b/test/instance_extension/characteristic_test.exs @@ -5,7 +5,6 @@ defmodule Diffo.InstanceExtension.CharacteristicTest do @moduledoc false use ExUnit.Case - alias Diffo.Test.Servo alias Diffo.Test.Parties setup_all do @@ -19,14 +18,6 @@ defmodule Diffo.InstanceExtension.CharacteristicTest do end describe "characteristic" do - test "create resource fails when characteristic value type invalid" do - {:error, error} = Servo.build_invalid_characteristic(%{}) - %Ash.Error.Invalid{errors: errors} = error - - assert hd(errors).message == - "couldn't create characteristic with value of unknown type Elixir.InvalidValue" - end - test "create resource with array characteristic - success" do {:ok, shelf} = Parties.build_shelf_with_installer() diff --git a/test/instance_extension/feature_test.exs b/test/instance_extension/feature_test.exs index 84b778f..4c64946 100644 --- a/test/instance_extension/feature_test.exs +++ b/test/instance_extension/feature_test.exs @@ -5,7 +5,6 @@ defmodule Diffo.InstanceExtension.FeatureTest do @moduledoc false use ExUnit.Case - alias Diffo.Test.Servo alias Diffo.Test.Parties setup_all do @@ -19,19 +18,13 @@ defmodule Diffo.InstanceExtension.FeatureTest do end describe "feature" do - test "create resource with fails when feature characteristic value type invalid" do - {:error, error} = Servo.build_invalid_feature_characteristic(%{}) - %Ash.Error.Invalid{errors: errors} = error - - assert hd(errors).message == - "couldn't create feature characteristic with value of unknown type Elixir.InvalidValue" - end - test "create resource with array feature characteristic - success" do {:ok, shelf} = Parties.build_shelf_with_installer() spectral = Enum.find(shelf.features, fn f -> f.name == :spectralManagement end) - deployment_classes = Enum.find(spectral.characteristics, fn c -> c.name == :deploymentClasses end) + + deployment_classes = + Enum.find(spectral.characteristics, fn c -> c.name == :deploymentClasses end) assert deployment_classes.is_array == true assert deployment_classes.values == [] diff --git a/test/instance_extension/specification_test.exs b/test/instance_extension/specification_test.exs index 4a8d283..d1f4743 100644 --- a/test/instance_extension/specification_test.exs +++ b/test/instance_extension/specification_test.exs @@ -19,12 +19,6 @@ defmodule Diffo.InstanceExtension.SpecificationTest do end describe "specification" do - test "create resource fails when specification id not uuid v4" do - {:error, error} = Servo.build_invalid_specification(%{}) - %Ash.Error.Invalid{errors: errors} = error - assert hd(errors).message == "must be a uuid v4 or nil" - end - test "description declared in specification DSL roundtrips to the persisted specification" do spec_id = Shelf.specification()[:id] description = Shelf.specification()[:description] diff --git a/test/support/resource/invalid/invalid_characteristic.ex b/test/support/resource/invalid/invalid_characteristic.ex deleted file mode 100644 index 2053dba..0000000 --- a/test/support/resource/invalid/invalid_characteristic.ex +++ /dev/null @@ -1,44 +0,0 @@ -# SPDX-FileCopyrightText: 2025 diffo contributors -# -# SPDX-License-Identifier: MIT - -defmodule Diffo.Test.InvalidCharacteristic do - @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - InvalidCharacteristic - Resource Instance with an Invalid Characteristic - """ - - alias Diffo.Provider.BaseInstance - - use Ash.Resource, - fragments: [BaseInstance], - domain: Diffo.Test.Servo - - resource do - description "Ash Resource with an invalid characteristic" - end - - structure do - specification do - id "3caf29b9-0b91-4b8f-8568-2960131b1feb" - name "invalidCharacteristic" - type :resourceSpecification - category "Network Resource" - end - - characteristics do - characteristic :invalid, InvalidValue - end - end - - actions do - create :build do - description "creates a new InvalidCharacteristic resource instance for build" - accept [:id, :name, :type, :which] - change set_attribute(:type, :resource) - change load [:href] - upsert? false - end - end -end diff --git a/test/support/resource/invalid/invalid_feature_characteristic.ex b/test/support/resource/invalid/invalid_feature_characteristic.ex deleted file mode 100644 index 143b717..0000000 --- a/test/support/resource/invalid/invalid_feature_characteristic.ex +++ /dev/null @@ -1,47 +0,0 @@ -# SPDX-FileCopyrightText: 2025 diffo contributors -# -# SPDX-License-Identifier: MIT - -defmodule Diffo.Test.InvalidFeatureCharacteristic do - @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - InvalidFeatureCharacteristic - Resource Instance with an Invalid Feature Characteristic - """ - - alias Diffo.Provider.BaseInstance - - use Ash.Resource, - fragments: [BaseInstance], - domain: Diffo.Test.Servo - - resource do - description "Ash Resource with an invalid feature characteristic" - end - - structure do - specification do - id "1f2402ca-82da-428e-a58b-5405a5431386" - name "invalidFeatureCharacteristic" - type :resourceSpecification - category "Network Resource" - end - - features do - feature :invalid_feature_characteristic do - is_enabled? true - characteristic :invalid, InvalidValue - end - end - end - - actions do - create :build do - description "creates a new InvalidFeatureCharacteristic resource instance for build" - accept [:id, :name, :type, :which] - change set_attribute(:type, :resource) - change load [:href] - upsert? false - end - end -end diff --git a/test/support/resource/invalid/invalid_specification.ex b/test/support/resource/invalid/invalid_specification.ex deleted file mode 100644 index b409619..0000000 --- a/test/support/resource/invalid/invalid_specification.ex +++ /dev/null @@ -1,40 +0,0 @@ -# SPDX-FileCopyrightText: 2025 diffo contributors -# -# SPDX-License-Identifier: MIT - -defmodule Diffo.Test.InvalidSpecification do - @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - InvalidSpecification - Resource Instance with an Invalid Specification - """ - - alias Diffo.Provider.BaseInstance - - use Ash.Resource, - fragments: [BaseInstance], - domain: Diffo.Test.Servo - - resource do - description "Ash Resource with an invalid specification" - end - - structure do - specification do - id "ef016d85-9dbd-429c-04da-1df56cc7dda5" - name "invalidSpecification" - type :resourceSpecification - category "Network Resource" - end - end - - actions do - create :build do - description "creates a new InvalidSpecification resource instance for build" - accept [:id, :name, :type, :which] - change set_attribute(:type, :resource) - change load [:href] - upsert? false - end - end -end diff --git a/test/support/servo.ex b/test/support/servo.ex index ad9c0d6..0121a5b 100644 --- a/test/support/servo.ex +++ b/test/support/servo.ex @@ -16,9 +16,6 @@ defmodule Diffo.Test.Servo do alias Diffo.Test.Card alias Diffo.Test.Broadband alias Diffo.Test.BroadbandV2 - alias Diffo.Test.InvalidSpecification - alias Diffo.Test.InvalidCharacteristic - alias Diffo.Test.InvalidFeatureCharacteristic domain do description "service and resource management" @@ -50,20 +47,5 @@ defmodule Diffo.Test.Servo do define :build_broadband_v2, action: :build define :get_broadband_v2_by_id, action: :read, get_by: :id end - - resource InvalidSpecification do - define :get_invalid_specification_by_id, action: :read, get_by: :id - define :build_invalid_specification, action: :build - end - - resource InvalidCharacteristic do - define :get_invalid_characteristic_by_id, action: :read, get_by: :id - define :build_invalid_characteristic, action: :build - end - - resource InvalidFeatureCharacteristic do - define :get_invalid_feature_characteristic_by_id, action: :read, get_by: :id - define :build_invalid_feature_characteristic, action: :build - end end end From 199b8dc0035621d7f1acee0219f912552b155105 Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Wed, 6 May 2026 23:05:12 +0930 Subject: [PATCH 21/22] ash_neo4j and bolty bump --- CHANGELOG.md | 95 +++++++++++++++++++++++++++------------------------- mix.exs | 6 ++-- mix.lock | 4 +-- 3 files changed, 53 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed8b16d..6f523a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,87 +11,90 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline -## [v0.1.0](https://github.com/diffo-dev/diffo/compare/v0.1.0...v0.1.0) (2025-08-11) -### Features: -* initial version on AshNeo4j DataLayer -## [v0.1.1](https://github.com/diffo-dev/diffo/compare/v0.1.0...v0.1.1) (2025-09-09) +## [v0.2.0](https://github.com/diffo-dev/diffo/compare/v0.1.6...v0.2.0) (2026-04-24) -### Features: -* update for AshNeo4j DSL changes -* refactor specification relationships -* characteristic value schemas -* customise instance via specification -* improve relationships to avoid circular loads +### Breaking Changes -## [v0.1.2](https://github.com/diffo-dev/diffo/compare/v0.1.1...v0.1.2) (2025-10-20) +* Updated to ash_neo4j 0.3.1 and bolty 0.0.10 — no database compatibility with prior versions due to significant changes in the data layer and Bolt protocol handling ### Features -* REUSE compliant - -## [v0.1.3](https://github.com/diffo-dev/diffo/compare/v0.1.2...v0.1.3) (2025-12-01) +* `Diffo.Type.Value` — union of `Diffo.Type.Primitive` and `Diffo.Type.Dynamic`, enabling mixed primitive and typed-struct values on characteristics and other resources +* `Diffo.Type.Primitive` — typed union of string, integer, float, boolean, date, time, datetime, duration +* `Diffo.Type.Dynamic` — runtime-typed struct for Ash.Type.NewType values +* `Diffo.Type.Dynamic.is_valid?/1` — predicate to check whether a module is a valid Dynamic type (Ash.Type.NewType with storage_type :map) before constructing a value +* `Characteristic.values` — homogeneous array of `Diffo.Type.Value` on a characteristic, with `is_array` boolean flag; supports morphing between scalar and array representations +* `Diffo.Unwrap` on `List` — unwraps each element, enabling `Diffo.Unwrap.unwrap/1` to reduce nested wrapped lists to plain Elixir values in one call +* Provider instance extension DSL — characteristic and feature characteristic value types now accept `{:array, Module}` in addition to plain module references -### Features +### Fixes -* place_ref source party or place -* party_ref source place or party -* instance events +* `Diffo.Type.Value` nil update — override `handle_change/3` to prevent Ash union type from wrapping nil in the previous member type, which caused malformed JSON to be written to Neo4j +* `Diffo.Type.Value` nil array update — added nil guards to `handle_change_array/3` and `prepare_change_array/3` to prevent enumeration errors when setting an array characteristic to nil +* `Diffo.Type.Dynamic` nil safety — added nil clauses to `cast_stored/2` and `dump_to_native/2` ### Maintenance -* remove access domain +* bolty 0.0.10 — native DateTime handling for both BOLT 4.x and BOLT 5.x +* `Diffo.Unwrap` protocol documentation — recursive unwrap behaviour, custom implementation guide, and array examples added to livebook and module docs -## [v0.1.4](https://github.com/diffo-dev/diffo/compare/v0.1.3...v0.1.4) (2026-03-12) +## [v0.1.6](https://github.com/diffo-dev/diffo/compare/v0.1.5...v0.1.6) (2026-03-19) -### Features +### Fixes -* assigner unassign operation +* incorrect domain label ### Maintenance -* updated ash_neo4j, uses bolty rather than boltx +* improved error handling -## [v0.1.5](https://github.com/diffo-dev/diffo/compare/v0.1.4...v0.1.5) (2026-03-19) + [v0.1.5](https://github.com/diffo-dev/diffo/compare/v0.1.4...v0.1.5) (2026-03-19) ### Fixes * fixed relationship enrichment inconsistent across neo4j versions -## [v0.2.0](https://github.com/diffo-dev/diffo/compare/v0.1.6...v0.2.0) (2026-04-24) +## [v0.1.4](https://github.com/diffo-dev/diffo/compare/v0.1.3...v0.1.4) (2026-03-12) -### Breaking Changes +### Features -* Updated to ash_neo4j 0.3.1 and bolty 0.0.10 — no database compatibility with prior versions due to significant changes in the data layer and Bolt protocol handling +* assigner unassign operation -### Features +### Maintenance -* `Diffo.Type.Value` — union of `Diffo.Type.Primitive` and `Diffo.Type.Dynamic`, enabling mixed primitive and typed-struct values on characteristics and other resources -* `Diffo.Type.Primitive` — typed union of string, integer, float, boolean, date, time, datetime, duration -* `Diffo.Type.Dynamic` — runtime-typed struct for Ash.Type.NewType values -* `Diffo.Type.Dynamic.is_valid?/1` — predicate to check whether a module is a valid Dynamic type (Ash.Type.NewType with storage_type :map) before constructing a value -* `Characteristic.values` — homogeneous array of `Diffo.Type.Value` on a characteristic, with `is_array` boolean flag; supports morphing between scalar and array representations -* `Diffo.Unwrap` on `List` — unwraps each element, enabling `Diffo.Unwrap.unwrap/1` to reduce nested wrapped lists to plain Elixir values in one call -* Provider instance extension DSL — characteristic and feature characteristic value types now accept `{:array, Module}` in addition to plain module references +* updated ash_neo4j, uses bolty rather than boltx -### Fixes +## [v0.1.3](https://github.com/diffo-dev/diffo/compare/v0.1.2...v0.1.3) (2025-12-01) -* `Diffo.Type.Value` nil update — override `handle_change/3` to prevent Ash union type from wrapping nil in the previous member type, which caused malformed JSON to be written to Neo4j -* `Diffo.Type.Value` nil array update — added nil guards to `handle_change_array/3` and `prepare_change_array/3` to prevent enumeration errors when setting an array characteristic to nil -* `Diffo.Type.Dynamic` nil safety — added nil clauses to `cast_stored/2` and `dump_to_native/2` +### Features + +* place_ref source party or place +* party_ref source place or party +* instance events ### Maintenance -* bolty 0.0.10 — native DateTime handling for both BOLT 4.x and BOLT 5.x -* `Diffo.Unwrap` protocol documentation — recursive unwrap behaviour, custom implementation guide, and array examples added to livebook and module docs +* remove access domain -## [v0.1.6](https://github.com/diffo-dev/diffo/compare/v0.1.5...v0.1.6) (2026-03-19) +## [v0.1.2](https://github.com/diffo-dev/diffo/compare/v0.1.1...v0.1.2) (2025-10-20) -### Fixes +### Features -* incorrect domain label +* REUSE compliant -### Maintenance +## [v0.1.1](https://github.com/diffo-dev/diffo/compare/v0.1.0...v0.1.1) (2025-09-09) + +### Features: +* update for AshNeo4j DSL changes +* refactor specification relationships +* characteristic value schemas +* customise instance via specification +* improve relationships to avoid circular loads + +## [v0.1.0](https://github.com/diffo-dev/diffo/compare/v0.1.0...v0.1.0) (2025-08-11) + +### Features: +* initial version on AshNeo4j DataLayer -* improved error handling \ No newline at end of file diff --git a/mix.exs b/mix.exs index fd9fbc6..9944a96 100644 --- a/mix.exs +++ b/mix.exs @@ -99,10 +99,8 @@ defmodule Diffo.MixProject do {:ash_outstanding, "~> 0.2.3"}, {:ash_jason, "~> 3.0"}, {:ash_state_machine, "~> 0.2.12"}, - #{:ash_neo4j, ash_neo4j_version("~> 0.4.1")}, - {:ash_neo4j, github: "diffo-dev/ash_neo4j", branch: "dev", override: true, only: [:dev, :test]}, - #{:bolty, "~> 0.1.0")}, - {:bolty, github: "diffo-dev/bolty", branch: "dev", override: true, only: [:dev, :test]}, + {:ash_neo4j, ash_neo4j_version("~> 0.4.1")}, + {:bolty, ">= 0.0.12"}, {:ash, ash_version("~> 3.0 and >= 3.24.2")}, {:uuid, "~> 1.1"}, {:igniter, ">= 0.6.29 and < 1.0.0-0", diff --git a/mix.lock b/mix.lock index 98e95e5..0ba5589 100644 --- a/mix.lock +++ b/mix.lock @@ -1,10 +1,10 @@ %{ "ash": {:hex, :ash, "3.24.7", "6e2f32011e7c8f0809dae36712ccfb2efaf3c669cbda7443685436e80acdebf7", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 1.0", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.6.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.3", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c9fb4d21c3c8bb85636338d448afdc283dd98a433d869e4b2210ac57ade00624"}, "ash_jason": {:hex, :ash_jason, "3.1.0", "84a88dfe5e25a20d55cf2d2664885cd086fa45871e8777aedc3ad96a282e2a6f", [:mix], [{:ash, ">= 3.6.2 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:spark, ">= 2.1.21 and < 3.0.0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "71e6bbc421fb2cf7079f8804814145cca458116c839fc798f9606b806e07eb2b"}, - "ash_neo4j": {:git, "https://github.com/diffo-dev/ash_neo4j.git", "762b0ac089e6f6338e3cbfd22ad64f83f3079f15", [branch: "dev"]}, + "ash_neo4j": {:hex, :ash_neo4j, "0.4.1", "b33d7a5c9f333ffc8b1684fb6e07c4c502b0429ee5bb785fb09fb8d775636587", [:mix], [{:ash, ">= 3.24.2 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:bolty, ">= 0.0.12", [hex: :bolty, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "76a297eb6d5d23e5d9710b70161ad9810ac50e0efbf761d781981ee19f37af2a"}, "ash_outstanding": {:hex, :ash_outstanding, "0.2.4", "c72b91f1b8e4859fb033eddf66d0ba36cfd8af0c2a9748c7ef9e6ccfdb5d093d", [:mix], [{:ash, ">= 3.6.2 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:outstanding, "~> 0.2.4", [hex: :outstanding, repo: "hexpm", optional: false]}], "hexpm", "64ba8f582ce69c9050352c75f0895db186c7a56f35039dab34c8e1ab7516f9ce"}, "ash_state_machine": {:hex, :ash_state_machine, "0.2.13", "e1c368ebf01ef73477739ee76d53e513d073b141ec11e7bf7f91d8f2d8fc9569", [:mix], [{:ash, ">= 3.4.66 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}], "hexpm", "aa21c92a8950850df69b5205bf41efc1e502f5ab839425ba08561f0421c9f226"}, - "bolty": {:git, "https://github.com/diffo-dev/bolty.git", "cc38e23e94eddc177dd8988bbcf0b489fab3bffe", [branch: "dev"]}, + "bolty": {:hex, :bolty, "0.0.12", "5311de46c29c71000c51cfb23fc181359daa49cedb9c8c4ba1e245f3e54079ae", [:mix], [{:db_connection, "~> 2.7.0", [hex: :db_connection, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: true]}, {:poison, "~> 6.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "0760661dd2f0ba9f2901448c1be00fc1ed228780644ba21a2400d0662595ee10"}, "crux": {:hex, :crux, "0.1.2", "4441c9e3a34f1e340954ce96b9ad5a2de13ceb4f97b3f910211227bb92e2ca90", [:mix], [{:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: true]}], "hexpm", "563ea3748ebfba9cc078e6d198a1d6a06015a8fae503f0b721363139f0ddb350"}, "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, From 748a85509ac254a27f6817ce0726a14aaf85dbd0 Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Wed, 6 May 2026 23:17:24 +0930 Subject: [PATCH 22/22] release 0.2.1 --- CHANGELOG.md | 18 +++++++++++++++++- diffo.livemd | 2 +- .../DSL-Diffo.Provider.Instance.Extension.md | 3 +++ mix.exs | 2 +- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f523a1..3bb3573 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,10 +11,26 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline +## [v0.2.1](https://github.com/diffo-dev/diffo/compare/v0.2.0...v0.2.1) (2026-05-06) +## Notable Changes +* Updated to ash_neo4j 0.4.1 and bolty 0.0.12, now supporting transactions and test sandbox +* Improvements to provider DSL and documentation + +## What's Changed +* base party and related DSL and livebook by @matt-beanland in https://github.com/diffo-dev/diffo/pull/82 +* Instance DSL parties — multiplicity, validation, and enforcement by @matt-beanland in https://github.com/diffo-dev/diffo/pull/89 +* 86 transformers persisters verifiers by @matt-beanland in https://github.com/diffo-dev/diffo/pull/92 +* 91 place dsl by @matt-beanland in https://github.com/diffo-dev/diffo/pull/93 +* 79 provider instance specification doesnt set description by @matt-beanland in https://github.com/diffo-dev/diffo/pull/95 +* 94 provider instance specification dsl additional fields by @matt-beanland in https://github.com/diffo-dev/diffo/pull/97 +* document instance versioning lifecycle by @matt-beanland in https://github.com/diffo-dev/diffo/pull/98 +* accept raw dynamic by @matt-beanland in https://github.com/diffo-dev/diffo/pull/100 +* removed duplicate tests by @matt-beanland in https://github.com/diffo-dev/diffo/pull/108 +* 105 latest ash neo4j by @matt-beanland in https://github.com/diffo-dev/diffo/pull/109 -## [v0.2.0](https://github.com/diffo-dev/diffo/compare/v0.1.6...v0.2.0) (2026-04-24) +## [v0.2.0](https://github.com/diffo-dev/diffo/compare/v0.1.6...v0.2.0) (2026-04-24) ### Breaking Changes * Updated to ash_neo4j 0.3.1 and bolty 0.0.10 — no database compatibility with prior versions due to significant changes in the data layer and Bolt protocol handling diff --git a/diffo.livemd b/diffo.livemd index 8617475..72111a9 100644 --- a/diffo.livemd +++ b/diffo.livemd @@ -9,7 +9,7 @@ SPDX-License-Identifier: MIT ```elixir Mix.install( [ - {:diffo, path: "/Users/Beanlanda/git/diffo"} + {:diffo, "~> 0.2.1"} ], consolidate_protocols: false ) diff --git a/documentation/dsls/DSL-Diffo.Provider.Instance.Extension.md b/documentation/dsls/DSL-Diffo.Provider.Instance.Extension.md index 2da6fc3..24cc338 100644 --- a/documentation/dsls/DSL-Diffo.Provider.Instance.Extension.md +++ b/documentation/dsls/DSL-Diffo.Provider.Instance.Extension.md @@ -113,6 +113,9 @@ end | [`name`](#structure-specification-name){: #structure-specification-name .spark-required} | `String.t` | | The name of the specification, unique to a service but common for all versions. | | [`type`](#structure-specification-type){: #structure-specification-type } | `atom` | `:serviceSpecification` | The type of the specification. | | [`major_version`](#structure-specification-major_version){: #structure-specification-major_version } | `integer` | `1` | The major_version of the specification. | +| [`minor_version`](#structure-specification-minor_version){: #structure-specification-minor_version } | `integer` | | The minor_version of the specification. | +| [`patch_version`](#structure-specification-patch_version){: #structure-specification-patch_version } | `integer` | | The patch_version of the specification. | +| [`tmf_version`](#structure-specification-tmf_version){: #structure-specification-tmf_version } | `integer` | | The TMF API version of the specification, e.g. 4. | | [`description`](#structure-specification-description){: #structure-specification-description } | `String.t` | | A generic description of the specified service or resource. | | [`category`](#structure-specification-category){: #structure-specification-category } | `String.t` | | The category the specified service or resource belongs to. | diff --git a/mix.exs b/mix.exs index 9944a96..c719f05 100644 --- a/mix.exs +++ b/mix.exs @@ -6,7 +6,7 @@ defmodule Diffo.MixProject do @moduledoc false use Mix.Project - @version "0.2.0" + @version "0.2.1" @name "Diffo" @description "TMF Service and Resource Manager with a difference" @github_url "https://github.com/diffo-dev/diffo"