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/.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/CHANGELOG.md b/CHANGELOG.md index ed8b16d..3bb3573 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,56 +11,26 @@ 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) - -### Features: -* update for AshNeo4j DSL changes -* refactor specification relationships -* characteristic value schemas -* customise instance via specification -* improve relationships to avoid circular loads - -## [v0.1.2](https://github.com/diffo-dev/diffo/compare/v0.1.1...v0.1.2) (2025-10-20) - -### Features - -* REUSE compliant - -## [v0.1.3](https://github.com/diffo-dev/diffo/compare/v0.1.2...v0.1.3) (2025-12-01) - -### Features +## [v0.2.1](https://github.com/diffo-dev/diffo/compare/v0.2.0...v0.2.1) (2026-05-06) -* place_ref source party or place -* party_ref source place or party -* instance events - -### Maintenance - -* remove access domain - -## [v0.1.4](https://github.com/diffo-dev/diffo/compare/v0.1.3...v0.1.4) (2026-03-12) +## 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 -### Features +## 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 -* assigner unassign operation - -### Maintenance - -* updated ash_neo4j, uses bolty rather than boltx - -## [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) - ### 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 @@ -94,4 +64,53 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline ### Maintenance -* improved error handling \ No newline at end of file +* improved error handling + + [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.1.4](https://github.com/diffo-dev/diffo/compare/v0.1.3...v0.1.4) (2026-03-12) + +### Features + +* assigner unassign operation + +### Maintenance + +* updated ash_neo4j, uses bolty rather than boltx + +## [v0.1.3](https://github.com/diffo-dev/diffo/compare/v0.1.2...v0.1.3) (2025-12-01) + +### Features + +* place_ref source party or place +* party_ref source place or party +* instance events + +### Maintenance + +* remove access domain + +## [v0.1.2](https://github.com/diffo-dev/diffo/compare/v0.1.1...v0.1.2) (2025-10-20) + +### Features + +* REUSE compliant + +## [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 + 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..72111a9 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 two top-level sections: -## specification +## 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. +- `places do` — the place roles that instances of this kind relate to, mirroring `parties do` + in structure and 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 including generated functions. + + +## structure +Defines the structural shape of the Instance — its specification, characteristics, features, parties, and places + +### Nested DSLs + * [specification](#structure-specification) + * [characteristics](#structure-characteristics) + * characteristic + * [features](#structure-features) + * feature + * characteristic + * [parties](#structure-parties) + * party + * parties + * [places](#structure-places) + * place + * places + + +### 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 + + places do + place :installation_site, MyApp.GeographicSite + end +end + +``` + + + +### structure.specification Defines the Instance Specification @@ -31,23 +109,71 @@ 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. | +| [`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. | + + +### 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 + +``` -## features +### 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. | + + + + + + + + +### structure.features Configuration for Instance Features ### Nested DSLs - * [feature](#features-feature) + * [feature](#structure-features-feature) * characteristic @@ -67,7 +193,7 @@ end -### features.feature +### structure.features.feature ```elixir feature name ``` @@ -76,7 +202,7 @@ feature name Adds a Feature ### Nested DSLs - * [characteristic](#features-feature-characteristic) + * [characteristic](#structure-features-feature-characteristic) @@ -85,15 +211,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 ``` @@ -109,42 +235,123 @@ 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. | -### Introspection -Target: `Diffo.Provider.Instance.Characteristic` + + + + + +### structure.parties +List of Instance Party roles + +### Nested DSLs + * [party](#structure-parties-party) + * [parties](#structure-parties-parties) + + +### 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 +end + +``` + + + + +### structure.parties.party +```elixir +party role, party_type +``` + + +Declares a singular party role on this Instance + + + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`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.Feature` +Target: `Diffo.Provider.Instance.Extension.PartyDeclaration` +### structure.parties.parties +```elixir +parties role, party_type +``` +Declares a plural party role on this Instance -## characteristics -List of Instance Characteristics + + + + +### 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` + + +### structure.places +List of Instance Place roles ### Nested DSLs - * [characteristic](#characteristics-characteristic) + * [place](#structure-places-place) + * [places](#structure-places-places) ### Examples ``` -characteristics do - characteristic :dslam, Diffo.Access.Dslam - characteristic :aggregate_interface, Diffo.Access.AggregateInterface - characteristic :circuit, Diffo.Access.Circuit - characteristic :line, Diffo.Access.Line +places do + place :installation_site, MyApp.GeographicSite + places :coverage_areas, MyApp.GeographicLocation, constraints: [min: 1] + place :billing_address, MyApp.GeographicAddress, reference: true end ``` @@ -152,13 +359,13 @@ end -### characteristics.characteristic +### structure.places.place ```elixir -characteristic name, value_type +place role, place_type ``` -Adds a Characteristic +Declares a singular place role on this Instance @@ -168,9 +375,14 @@ 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-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. | @@ -178,7 +390,138 @@ Adds a Characteristic ### Introspection -Target: `Diffo.Provider.Instance.Characteristic` +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` + + + + + + +## behaviour +Defines the behavioural wiring for the Instance — actions, and in future triggers and tasks + +### Nested DSLs + * [actions](#behaviour-actions) + * create + * update + + +### Examples +``` +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 + +``` + + + + +### behaviour.actions.create +```elixir +create name +``` + + +Marks a create action for instance build wiring, injecting :specified_by, :features, and :characteristics arguments + + + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`name`](#behaviour-actions-create-name){: #behaviour-actions-create-name .spark-required} | `atom` | | The name of the create action to wire | + + + + + + + +### behaviour.actions.update +```elixir +update name +``` + + +Marks an update action for instance behaviour wiring + + + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`name`](#behaviour-actions-update-name){: #behaviour-actions-update-name .spark-required} | `atom` | | The name of the update action to wire | + + + + + + + + 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 new file mode 100644 index 0000000..643ce53 --- /dev/null +++ b/documentation/dsls/DSL-Diffo.Provider.Party.Extension.md @@ -0,0 +1,163 @@ + +# Diffo.Provider.Party.Extension + +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. + + +## instances +Declares the roles this Party kind plays with respect to Instances + +### Nested DSLs + * [role](#instances-role) + + +### Examples +``` +instances do + role :facilitates, MyApp.AccessService +end + +``` + + + + +### instances.role +```elixir +role role, party_type +``` + + +Declares a role this Party kind plays + + + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`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 | + + + + + + +### Introspection + +Target: `Diffo.Provider.Party.Extension.InstanceRole` + + + + +## parties +Declares the roles this Party kind plays with respect to other Parties + +### Nested DSLs + * [role](#parties-role) + + +### Examples +``` +parties do + role :managed_by, MyApp.Person +end + +``` + + + + +### parties.role +```elixir +role role, party_type +``` + + +Declares a role this Party kind plays with respect to other Parties + + + + + +### Arguments + +| Name | Type | Default | Docs | +|------|------|---------|------| +| [`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 | + + + + + + +### Introspection + +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_instance_extension.livemd b/documentation/how_to/use_diffo_provider_extension.livemd similarity index 63% rename from documentation/how_to/use_diffo_provider_instance_extension.livemd rename to documentation/how_to/use_diffo_provider_extension.livemd index 45348f3..78f7394 100644 --- a/documentation/how_to/use_diffo_provider_instance_extension.livemd +++ b/documentation/how_to/use_diffo_provider_extension.livemd @@ -9,7 +9,10 @@ SPDX-License-Identifier: MIT ```elixir Mix.install( [ - {:diffo, "~> 0.2.0"} + {:diffo, path: "/Users/Beanlanda/git/diffo"} + ], + config: [ + diffo: [ash_domains: [Diffo.Provider]] ], consolidate_protocols: false ) @@ -19,7 +22,7 @@ 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) @@ -28,9 +31,11 @@ In this 'Diffo Provider Instance 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 +* Declaring domain Places using the Place Extension ### Installing Neo4j and Configuring Bolty @@ -116,24 +121,14 @@ 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 +## Instance Extension -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). - -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 @@ -148,9 +143,14 @@ 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. + +The extension has two top-level sections: + +**`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. -Currently it has DSL to allow you to declare specification, features and characteristics. 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,72 +159,29 @@ 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 of the GPU and NPU Resource instances is created and managed by the Provider and is effectively a resource pool for individualy assignable cores. - -We'll define these Resources in a Compute domain which exposes an API. - -```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 - - 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 +Each instance of Cluster could be created on Consumer demand as a 'container' for the GPU and NPU core partial resources. - #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 +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. - 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 -``` +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. -## Declaring a Composite Resource +### 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 @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - Cluster - Cluster Resource Instance + Cluster Resource Instance """ 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 + alias Diffo.Compute.Engineer use Ash.Resource, fragments: [BaseInstance], @@ -235,37 +192,44 @@ 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" + 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 + + parties do + party :operator, Tenant + party :manager, Engineer + end + + places do + place :data_centre, Diffo.Compute.DataCentre + end end - characteristics do - characteristic :cluster, ClusterValue + 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 @@ -275,7 +239,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) @@ -300,19 +264,17 @@ 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] 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,65 +299,19 @@ 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. ```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 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 @@ -411,38 +327,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 @@ -488,9 +402,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 - - CardValue - AshTyped Struct for GPU Characteristic Value + AshTyped Struct for GPU Characteristic Value """ use Ash.TypedStruct, extensions: [AshJason.TypedStruct, AshOutstanding.TypedStruct] @@ -521,11 +433,257 @@ defmodule Diffo.Compute.GPUValue do end ``` -Now we'll create a couple of GPU instances: +## Party Extension + +`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. + +`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 — `Tenant` for the operating company, and `Engineer` for the individuals who manage resources. + +```elixir +defmodule Diffo.Compute.Tenant do + @moduledoc """ + Tenant in the Compute domain + """ + + alias Diffo.Provider.BaseParty + alias Diffo.Compute + + use Ash.Resource, + fragments: [BaseParty], + domain: Compute + + resource do + description "A Compute Tenant" + plural_name :tenants + end + + actions do + create :build do + accept [:id, :name] + change set_attribute(:type, :Organization) + end + end + + instances do + role :operator, Diffo.Compute.Cluster + role :operator, Diffo.Compute.GPU + end +end +``` + +```elixir +defmodule Diffo.Compute.Engineer do + @moduledoc """ + 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 + + actions do + create :build do + accept [:id, :name] + change set_attribute(:type, :Individual) + end + end + + instances do + role :manager, Diffo.Compute.Cluster + end + + parties do + role :employer, Diffo.Compute.Tenant + end +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: + +```elixir +defmodule Diffo.Compute do + @moduledoc """ + 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.Tenant + alias Diffo.Compute.Engineer + alias Diffo.Compute.DataCentre + + 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 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: :build + 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 +``` + +### Creating Party instances + +Clear any data from previous runs before starting (safe to re-evaluate): + +```elixir +AshNeo4j.Neo4jHelper.delete_all() +``` + +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 + +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 +alias Diffo.Provider.Instance.Place + +{:ok, dc} = Compute.create_data_centre(%{id: "NXTM2", name: "NextDC M2"}) +``` + +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} +] +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 gpu_1 = Compute.build_gpu!(%{name: "GPU 1"}) gpu_2 = Compute.build_gpu!(%{name: "GPU 2"}) ``` @@ -548,13 +706,12 @@ 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. ```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) @@ -581,8 +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 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, 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` 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. -The composite Cluster Resource is a fully fledged TMF Resource which can itself be related to consuming TMF Service and/or Resources. +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/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/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..74987a4 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 @@ -49,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/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..463856b 100644 --- a/lib/diffo/provider/components/base_instance.ex +++ b/lib/diffo/provider/components/base_instance.ex @@ -4,9 +4,166 @@ defmodule Diffo.Provider.BaseInstance do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference + Ash Resource Fragment which is 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 DSL has two top-level sections: `structure do` describes what the instance kind is; + `behaviour do` wires it to Ash actions. + + ### structure + + `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`. + + `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 means 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 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 + + `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`. + + ### 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 + - `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 + - `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 + + defmodule MyApp.Cluster do + use Ash.Resource, fragments: [BaseInstance], domain: MyApp.Domain + + resource do + description "A Cluster Resource Instance" + plural_name :clusters + end + + 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 + + places do + place :site, MyApp.GeographicSite + end + end + + 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 + + ## Rolling your own actions + + 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`. + + 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. + + ## 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, @@ -292,6 +449,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/base_party.ex b/lib/diffo/provider/components/base_party.ex new file mode 100644 index 0000000..5840ecc --- /dev/null +++ b/lib/diffo/provider/components/base_party.ex @@ -0,0 +1,262 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.BaseParty do + @moduledoc """ + Ash Resource Fragment which is a the point of extension for your 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. + + `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.RSP do + use Ash.Resource, fragments: [BaseParty], domain: MyApp.Domain + + resource do + description "A Retail Service Provider" + plural_name :rsps + 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(:referred_type, :Organization) + end + end + + instances do + role :provider, MyApp.AccessService + 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 + 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, + domain: Diffo.Provider, + data_layer: AshNeo4j.DataLayer, + extensions: [ + AshOutstanding.Resource, + AshJason.Resource, + Diffo.Provider.Party.Extension + ] + + neo4j do + relate [ + {:party_refs, :RELATES, :incoming, :PartyRef}, + {:external_identifiers, :OWNS, :outgoing, :ExternalIdentifier}, + {:notes, :AUTHORS, :outgoing, :Note} + ] + + guard [ + {:OWNS, :outgoing, :ExternalIdentifier} + ] + + label :Party + 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 :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 :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 :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 + + 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 + end + + actions do + defaults [:read, :destroy] + + create :create do + description "creates a party of this kind" + accept [:id, :href, :name, :type, :referred_type] + upsert? true + end + + update :update do + description "updates the party 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 + + argument :query, :ci_string do + description "Return only parties 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, :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/base_place.ex b/lib/diffo/provider/components/base_place.ex new file mode 100644 index 0000000..f29005c --- /dev/null +++ b/lib/diffo/provider/components/base_place.ex @@ -0,0 +1,217 @@ +# 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 + + ## 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 + 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} + ] + + label :Place + 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/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..8d09ff3 100644 --- a/lib/diffo/provider/components/entity_ref.ex +++ b/lib/diffo/provider/components/entity_ref.ex @@ -4,9 +4,7 @@ defmodule Diffo.Provider.EntityRef do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference - - EntityRef - Ash Resource for a TMF Entity Reference + Ash Resource for a TMF Entity Reference """ use Ash.Resource, otp_app: :diffo, @@ -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 18c6b73..0c8fd74 100644 --- a/lib/diffo/provider/components/instance/extension.ex +++ b/lib/diffo/provider/components/instance/extension.ex @@ -4,9 +4,45 @@ defmodule Diffo.Provider.Instance.Extension do @moduledoc """ - DSL Extension customising an Instance + DSL Extension customising an Instance. + + 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. + - `places do` — the place roles that instances of this kind relate to, mirroring `parties do` + in structure and 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 including generated functions. """ + # ── structure ────────────────────────────────────────────────────────────── + @specification %Spark.Dsl.Section{ name: :specification, describe: "Defines the Instance Specification", @@ -25,43 +61,43 @@ 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 ], + 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. - """ + 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." ] ] } @@ -73,17 +109,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 ] ] @@ -102,9 +133,7 @@ defmodule Diffo.Provider.Instance.Extension do end """ ], - entities: [ - @characteristic - ] + entities: [@characteristic] } @feature %Spark.Dsl.Entity{ @@ -114,16 +143,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 ] ], @@ -147,11 +172,235 @@ defmodule Diffo.Provider.Instance.Extension do end """ ], - entities: [ - @feature + entities: [@feature] + } + + @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 singular party role on this Instance", + target: Diffo.Provider.Instance.Extension.PartyDeclaration, + args: [:role, :party_type], + 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{ + name: :parties, + describe: "List of Instance Party roles", + 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 + end + """ + ], + 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, parties, and places", + 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 + + places do + place :installation_site, MyApp.GeographicSite + end + end + """ + ], + sections: [@specification, @characteristics, @features, @parties, @places] + } + + # ── 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] + 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.Persisters.PersistPlaces, + 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.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 39df01e..1d814d4 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 @@ -16,27 +11,17 @@ 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() - end + @doc false + def build_after(changeset, result) do + specified_by = Ash.Changeset.get_argument(changeset, :specified_by) + result = %{result | specification_id: specified_by} - @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} + 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/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 ae0a6b8..92226d3 100644 --- a/lib/diffo/provider/components/instance/extension/characteristic.ex +++ b/lib/diffo/provider/components/instance/extension/characteristic.ex @@ -3,17 +3,11 @@ # 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 alias Diffo.Provider.Instance - alias Diffo.Provider.Instance.Extension.Info alias Diffo.Type.Value @doc """ @@ -24,10 +18,9 @@ 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 + 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 @@ -40,13 +33,8 @@ 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 - characteristics = Info.characteristics(module) - - 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 @@ -78,8 +66,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 85de261..f0c8c75 100644 --- a/lib/diffo/provider/components/instance/extension/feature.ex +++ b/lib/diffo/provider/components/instance/extension/feature.ex @@ -3,17 +3,11 @@ # 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 alias Diffo.Provider.Instance - alias Diffo.Provider.Instance.Extension.Info alias Diffo.Type.Value @doc """ @@ -24,10 +18,9 @@ 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 + 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 @@ -40,14 +33,9 @@ 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 - features = Info.features(module) - + 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 -> @@ -106,8 +94,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/info.ex b/lib/diffo/provider/components/instance/extension/info.ex index 938a782..3356537 100644 --- a/lib/diffo/provider/components/instance/extension/info.ex +++ b/lib/diffo/provider/components/instance/extension/info.ex @@ -5,5 +5,12 @@ defmodule Diffo.Provider.Instance.Extension.Info do use Spark.InfoGenerator, extension: Diffo.Provider.Instance.Extension, - sections: [:specification, :features, :characteristics] + 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/party.ex b/lib/diffo/provider/components/instance/extension/party.ex index e8b808e..26bb9b4 100644 --- a/lib/diffo/provider/components/instance/extension/party.ex +++ b/lib/diffo/provider/components/instance/extension/party.ex @@ -3,12 +3,7 @@ # 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 @doc """ @@ -16,6 +11,60 @@ defmodule Diffo.Provider.Instance.Party do """ defstruct [:id, :role] + @doc false + def validate_parties(changeset, declarations) do + 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 new file mode 100644 index 0000000..e1cff0c --- /dev/null +++ b/lib/diffo/provider/components/instance/extension/party_declaration.ex @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Instance.Extension.PartyDeclaration do + @moduledoc """ + PartyDeclaration - DSL entity declaring a party role on an Instance + """ + defstruct [:role, :party_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/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_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/persisters/persist_specification.ex b/lib/diffo/provider/components/instance/extension/persisters/persist_specification.ex new file mode 100644 index 0000000..b5f6a1b --- /dev/null +++ b/lib/diffo/provider/components/instance/extension/persisters/persist_specification.ex @@ -0,0 +1,32 @@ +# 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), + 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) + ] + + 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/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/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/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..3691d09 100644 --- a/lib/diffo/provider/components/instance/extension/specification.ex +++ b/lib/diffo/provider/components/instance/extension/specification.ex @@ -3,50 +3,32 @@ # 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 alias Diffo.Provider.Instance - alias Diffo.Provider.Instance.Extension.Info @doc """ Struct for a Specification """ - defstruct [:id, :name, :type, :major_version, :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 """ - 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 - - @doc """ - Upserts the Specification from a Extended Instance's module - """ - def upsert_specification(module) when is_atom(module) do - options = Info.specification_options(module) + 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, _result} -> - {:ok, specification} + 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) {:error, error} -> - {:error, error} + Ash.Changeset.add_error(changeset, error) end end @@ -56,18 +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) - 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.respecify_instance(%Instance{id: result.id}, %{specified_by: specified_by}) end defimpl String.Chars do 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..aac21c2 --- /dev/null +++ b/lib/diffo/provider/components/instance/extension/transformers/transform_behaviour.ex @@ -0,0 +1,108 @@ +# 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)) + + @doc false + def place(role), do: Enum.find(places(), &(&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?(Diffo.Provider.Instance.Extension.Persisters.PersistPlaces), do: true + def after?(_), do: false +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 new file mode 100644 index 0000000..b81e133 --- /dev/null +++ b/lib/diffo/provider/components/instance/extension/verifiers/verify_characteristics.ex @@ -0,0 +1,62 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Instance.Extension.Verifiers.VerifyCharacteristics do + @moduledoc "Verifies that characteristic names are unique and 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, [:structure, :characteristics]) + + 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} -> + if Code.ensure_loaded?(module) do + acc + else + [ + DslError.exception( + module: resource, + path: [:structure, :characteristics, char.name], + message: "characteristics: value_type #{inspect(module)} does not exist" + ) + | acc + ] + end + + :error -> + acc + end + end) + + errors = duplicate_errors ++ type_errors + + 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..d85e65f --- /dev/null +++ b/lib/diffo/provider/components/instance/extension/verifiers/verify_features.ex @@ -0,0 +1,76 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Instance.Extension.Verifiers.VerifyFeatures do + @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 + alias Spark.Error.DslError + + @impl true + def verify(dsl_state) do + resource = Verifier.get_persisted(dsl_state, :module) + features = Verifier.get_entities(dsl_state, [:structure, :features]) + + 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 -> + 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 + inner_acc + else + [ + DslError.exception( + module: resource, + path: [:structure, :features, feature.name, :characteristics, char.name], + message: "features: characteristic value_type #{inspect(module)} does not exist" + ) + | inner_acc + ] + end + + :error -> + inner_acc + end + end) + end) + + errors = duplicate_feature_errors ++ type_errors + + 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..63bd593 --- /dev/null +++ b/lib/diffo/provider/components/instance/extension/verifiers/verify_parties.ex @@ -0,0 +1,68 @@ +# 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 + alias Diffo.Provider.Party.Extension.Info, as: PartyInfo + + @impl true + def verify(dsl_state) do + resource = Verifier.get_persisted(dsl_state, :module) + parties = Verifier.get_entities(dsl_state, [:structure, :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: [:structure, :parties], + message: "parties: role #{inspect(role)} is declared more than once" + ) + end) + + type_errors = + Enum.reduce(parties, [], fn party, 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) + + 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..c5d6621 --- /dev/null +++ b/lib/diffo/provider/components/instance/extension/verifiers/verify_specification.ex @@ -0,0 +1,82 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Instance.Extension.Verifiers.VerifySpecification do + @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) + + 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 + + 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/lib/diffo/provider/components/instance/info.ex b/lib/diffo/provider/components/instance/info.ex new file mode 100644 index 0000000..31c9c45 --- /dev/null +++ b/lib/diffo/provider/components/instance/info.ex @@ -0,0 +1,72 @@ +# 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 + + @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/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 new file mode 100644 index 0000000..a205402 --- /dev/null +++ b/lib/diffo/provider/components/party/extension.ex @@ -0,0 +1,118 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Party.Extension do + @moduledoc """ + 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", + 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" + ] + ] + } + + @instances %Spark.Dsl.Section{ + name: :instances, + describe: "Declares the roles this Party kind plays with respect to Instances", + examples: [ + """ + instances do + role :facilitates, MyApp.AccessService + end + """ + ], + entities: [@role] + } + + @parties %Spark.Dsl.Section{ + name: :parties, + describe: "Declares the roles this Party kind plays with respect to other Parties", + examples: [ + """ + parties do + role :managed_by, MyApp.Person + end + """ + ], + 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, @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/info.ex b/lib/diffo/provider/components/party/extension/info.ex new file mode 100644 index 0000000..2ca0532 --- /dev/null +++ b/lib/diffo/provider/components/party/extension/info.ex @@ -0,0 +1,16 @@ +# 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: [:instances, :parties, :places] + + @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/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..ed2d81e --- /dev/null +++ b/lib/diffo/provider/components/party/extension/instance_role.ex @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Party.Extension.InstanceRole do + @moduledoc """ + 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..fea890b --- /dev/null +++ b/lib/diffo/provider/components/party/extension/party_role.ex @@ -0,0 +1,14 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Diffo.Provider.Party.Extension.PartyRole do + @moduledoc """ + 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/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/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/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/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..d0b0234 100644 --- a/lib/diffo/provider/components/place.ex +++ b/lib/diffo/provider/components/place.ex @@ -4,158 +4,22 @@ 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, - 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, :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 - defaults [:read, :destroy] - - create :create do - description "creates a place" - accept [:id, :href, :name, :type, :referredType] - 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, :referredType] - 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 :referredType, :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(:referredType) - message "when referredType 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" - end - end - - preparations 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/extension.ex b/lib/diffo/provider/components/place/extension.ex new file mode 100644 index 0000000..74df77a --- /dev/null +++ b/lib/diffo/provider/components/place/extension.ex @@ -0,0 +1,119 @@ +# 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. + """ + @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], + 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/info.ex b/lib/diffo/provider/components/place/extension/info.ex new file mode 100644 index 0000000..4023595 --- /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: [:instances, :parties, :places] + + @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 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/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/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 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/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..dc90930 100644 --- a/lib/diffo/provider/components/specification.ex +++ b/lib/diffo/provider/components/specification.ex @@ -4,9 +4,53 @@ defmodule Diffo.Provider.Specification do @moduledoc """ - Diffo - TMF Service and Resource Management with a difference + Ash Resource for a TMF Service or Resource Specification. - 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 @@ -40,7 +84,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/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 62bea87..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 @@ -17,7 +11,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/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..5fbcd5e 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: [ @@ -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/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 cefb48b..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" @@ -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 @@ -95,7 +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.3.1")}, + {: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", @@ -113,9 +118,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/mix.lock b/mix.lock index a4888a5..0ba5589 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": {: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": {: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": {: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"}, "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/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..b626990 100644 --- a/test/instance_extension/assigner_test.exs +++ b/test/instance_extension/assigner_test.exs @@ -10,17 +10,13 @@ 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 - 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 @@ -64,7 +60,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 @@ -80,11 +76,11 @@ 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 - {:ok, assignee} = Servo.build_shelf() + {:ok, assignee} = Parties.build_shelf_with_installer() {:ok, card} = Servo.build_card(%{}) @@ -105,11 +101,11 @@ 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 - {:ok, assignee} = Servo.build_shelf() + {:ok, assignee} = Parties.build_shelf_with_installer() {:ok, card} = Servo.build_card(%{}) @@ -135,11 +131,11 @@ 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 - {:ok, assignee} = Servo.build_shelf() + {:ok, assignee} = Parties.build_shelf_with_installer() {:ok, card} = Servo.build_card(%{}) @@ -165,11 +161,11 @@ 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 - {:ok, assignee} = Servo.build_shelf() + {:ok, assignee} = Parties.build_shelf_with_installer() {:ok, card} = Servo.build_card(%{}) @@ -208,7 +204,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 diff --git a/test/instance_extension/characteristic_test.exs b/test/instance_extension/characteristic_test.exs index 13f500b..f3929e8 100644 --- a/test/instance_extension/characteristic_test.exs +++ b/test/instance_extension/characteristic_test.exs @@ -5,29 +5,16 @@ defmodule Diffo.InstanceExtension.CharacteristicTest do @moduledoc false use ExUnit.Case - alias Diffo.Test.Servo - - setup_all do - AshNeo4j.BoltyHelper.start() - end + alias Diffo.Test.Parties setup do - on_exit(fn -> - AshNeo4j.Neo4jHelper.delete_all() - end) + AshNeo4j.Sandbox.checkout() + on_exit(&AshNeo4j.Sandbox.rollback/0) 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} = 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..8756550 100644 --- a/test/instance_extension/feature_test.exs +++ b/test/instance_extension/feature_test.exs @@ -5,32 +5,21 @@ defmodule Diffo.InstanceExtension.FeatureTest do @moduledoc false use ExUnit.Case - alias Diffo.Test.Servo - - setup_all do - AshNeo4j.BoltyHelper.start() - end + alias Diffo.Test.Parties setup do - on_exit(fn -> - AshNeo4j.Neo4jHelper.delete_all() - end) + AshNeo4j.Sandbox.checkout() + on_exit(&AshNeo4j.Sandbox.rollback/0) 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} = 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) + + 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/party_test.exs b/test/instance_extension/party_test.exs new file mode 100644 index 0000000..6e6171a --- /dev/null +++ b/test/instance_extension/party_test.exs @@ -0,0 +1,210 @@ +# 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.Organization + alias Diffo.Test.Person + + alias Diffo.Test.Shelf + alias Diffo.Test.Nbn + alias Diffo.Test.Servo + alias Diffo.Provider.Instance.Party + + setup do + AshNeo4j.Sandbox.checkout() + on_exit(&AshNeo4j.Sandbox.rollback/0) + end + + describe "Party DSL — Organization" do + test "instance roles are declared" do + roles = PartyInfo.instances(Organization) + assert length(roles) == 1 + assert hd(roles).role == :facilitator + assert hd(roles).party_type == Diffo.Provider.Instance + end + + test "party roles are declared" do + roles = PartyInfo.parties(Organization) + assert length(roles) == 1 + assert hd(roles).role == :employer + end + end + + describe "Party DSL — Person" do + test "party roles are declared" do + roles = PartyInfo.parties(Person) + assert length(roles) == 1 + assert hd(roles).role == :manager + assert hd(roles).party_type == Diffo.Test.Person + end + + test "instance roles are declared" do + roles = PartyInfo.instances(Person) + assert length(roles) == 1 + assert hd(roles).role == :overseer + end + end + + describe "Instance DSL — Shelf parties" do + test "party declarations are accessible via info" do + 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.structure_parties(Shelf) + 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.structure_parties(Shelf) + facilitator = Enum.find(parties, &(&1.role == :facilitator)) + assert facilitator.multiple == false + end + + test "reference: true is declared" do + 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.structure_parties(Shelf) + facilitator = Enum.find(parties, &(&1.role == :facilitator)) + assert facilitator.reference == false + end + + test "calculate: is declared" do + 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.structure_parties(Shelf) + installer = Enum.find(parties, &(&1.role == :installer)) + assert installer.multiple == true + end + + test "parties (plural) constraints are declared" do + parties = InstanceInfo.structure_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 — 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 + + {:ok, loaded} = Nbn.get_organization_by_id(org.id) + assert loaded.name == "Acme Corp" + 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"}) + assert person.name == "Alice" + assert person.type == :Individual + + {:ok, loaded} = Nbn.get_person_by_id(person.id) + assert loaded.name == "Alice" + end + end +end diff --git a/test/instance_extension/place_test.exs b/test/instance_extension/place_test.exs new file mode 100644 index 0000000..6f5127b --- /dev/null +++ b/test/instance_extension/place_test.exs @@ -0,0 +1,128 @@ +# 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.Shelf + alias Diffo.Test.Nbn + + setup do + AshNeo4j.Sandbox.checkout() + on_exit(&AshNeo4j.Sandbox.rollback/0) + 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/instance_extension/specification_test.exs b/test/instance_extension/specification_test.exs index 9cba730..850680d 100644 --- a/test/instance_extension/specification_test.exs +++ b/test/instance_extension/specification_test.exs @@ -6,22 +6,43 @@ defmodule Diffo.InstanceExtension.SpecificationTest do @moduledoc false use ExUnit.Case alias Diffo.Test.Servo - - setup_all do - AshNeo4j.BoltyHelper.start() - end + alias Diffo.Test.Shelf setup do - on_exit(fn -> - AshNeo4j.Neo4jHelper.delete_all() - end) + AshNeo4j.Sandbox.checkout() + on_exit(&AshNeo4j.Sandbox.rollback/0) 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" + 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 + + 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/instance_extension/transformer_test.exs b/test/instance_extension/transformer_test.exs new file mode 100644 index 0000000..a32ca0e --- /dev/null +++ b/test/instance_extension/transformer_test.exs @@ -0,0 +1,291 @@ +# 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 + alias Diffo.Provider.Instance.Info + alias Diffo.Provider.Instance.Extension.PlaceDeclaration + + 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 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 "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) + assert :shelf in names + assert :slots in names + assert :shelves in names + end + + test "each characteristic is a Characteristic struct" do + [first | _] = Shelf.characteristics() + assert is_struct(first, Characteristic) + end + + 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 "PersistFeatures" do + test "bakes features/0 onto the resource" do + features = Shelf.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.features() + assert is_struct(feature, Feature) + end + + 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 "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 "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) + 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 + provider = Enum.find(Shelf.parties(), &(&1.role == :provider)) + assert provider.reference == true + end + + test "calculate party has calculate set" do + manager = Enum.find(Shelf.parties(), &(&1.role == :manager)) + assert manager.calculate == :manager_calc + end + + test "plural party has constraints" do + installer = Enum.find(Shelf.parties(), &(&1.role == :installer)) + assert installer.multiple == true + assert installer.constraints == [min: 1, max: 3] + end + + 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 "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) + 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 "feature_characteristic/2 returns nil for unknown characteristic" do + assert Shelf.feature_characteristic(:spectralManagement, :nonexistent) == nil + end + + 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 "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/instance_extension/verifier_test.exs b/test/instance_extension/verifier_test.exs new file mode 100644 index 0000000..c9d197e --- /dev/null +++ b/test/instance_extension/verifier_test.exs @@ -0,0 +1,466 @@ +# 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 + + structure do + specification do + id "ef016d85-9dbd-429c-04da-1df56cc7dda5" + name "invalid" + end + end + end + 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 + 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, + "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 + + structure do + specification do + id "cd29956f-6c68-44cc-bf54-705eb8d2f754" + name "invalid" + end + + characteristics do + characteristic :foo, NonExistent.CharValue + end + 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 + + structure do + specification do + id "cd29956f-6c68-44cc-bf54-705eb8d2f754" + name "invalid" + end + + characteristics do + characteristic :bar, {:array, NonExistent.ArrayValue} + end + end + end + end + ) + end + 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, + "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 + + structure do + 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 + 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 + + structure do + specification do + id "cd29956f-6c68-44cc-bf54-705eb8d2f754" + name "invalid" + end + + parties do + party :operator, Shelf + party :operator, Shelf + end + 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 + + structure do + specification do + id "cd29956f-6c68-44cc-bf54-705eb8d2f754" + name "invalid" + end + + parties do + party :operator, NonExistent.PartyModule + end + 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.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 + 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 + + 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 + ) + 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/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 1e83e17..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 @@ -29,7 +24,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 +32,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 +40,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 +79,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 +87,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 +95,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 +132,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 +140,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 +148,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 +188,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 +234,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 +257,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 +278,7 @@ defmodule Diffo.Provider.EntityRefTest do entity = Diffo.Provider.create_entity!(%{ id: "COR000000123456", - referredType: :cost, + referred_type: :cost, name: "2025-01" }) @@ -305,7 +300,7 @@ defmodule Diffo.Provider.EntityRefTest do entity = Diffo.Provider.create_entity!(%{ id: "COR000000123456", - referredType: :cost, + referred_type: :cost, name: "2025-01" }) @@ -327,7 +322,7 @@ defmodule Diffo.Provider.EntityRefTest do entity = Diffo.Provider.create_entity!(%{ id: "COR000000123456", - referredType: :cost, + referred_type: :cost, name: "2025-01" }) @@ -338,7 +333,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 +367,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 +401,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 +416,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 +426,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 +436,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 +486,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 +510,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..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 @@ -23,21 +18,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 +45,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 +53,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 +72,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 +80,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 +98,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 +130,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 +186,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 +204,7 @@ defmodule Diffo.Provider.EntityTest do entity = Diffo.Provider.create_entity!(%{ id: "COR000000123456", - referredType: :cost, + referred_type: :cost, name: "2025-01" }) @@ -225,47 +220,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 +268,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 +309,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 +332,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 +351,7 @@ defmodule Diffo.Provider.EntityTest do entity = Diffo.Provider.create_entity!(%{ id: "COR000000123456", - referredType: :cost, + referred_type: :cost, name: "2025-01" }) @@ -368,7 +363,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/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 105fcf8..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 @@ -29,7 +24,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 +32,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 +66,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 +74,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 +82,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 +137,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 +145,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 +153,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 +211,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 +219,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 +227,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 +293,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 +319,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 +341,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 +367,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 +394,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 +421,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 +429,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 +457,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 +483,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 +515,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 +560,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/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 1d92465..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 @@ -453,7 +449,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 +459,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 +467,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 @@ -648,7 +644,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 +664,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 +672,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 +680,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 +759,7 @@ defmodule Diffo.Provider.InstanceTest do entity = Diffo.Provider.create_entity!(%{ id: "COR000000123456", - referredType: :cost, + referred_type: :cost, name: "2025-01" }) @@ -860,7 +856,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 +876,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 +884,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 +892,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 +1324,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..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 @@ -28,7 +23,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 +31,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 +65,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 +73,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 +121,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 +129,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 +176,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 +184,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 +246,7 @@ defmodule Diffo.Provider.NoteTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) note = @@ -280,7 +275,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 +300,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 +329,7 @@ defmodule Diffo.Provider.NoteTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) note = @@ -358,7 +353,7 @@ defmodule Diffo.Provider.NoteTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) note = @@ -382,7 +377,7 @@ defmodule Diffo.Provider.NoteTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) note = @@ -409,7 +404,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 +412,7 @@ defmodule Diffo.Provider.NoteTest do id: "T3_CONNECTIVITY", name: :entityId, href: "entity/internal/T3_CONNECTIVITY", - referredType: :Entity + referred_type: :Entity }) note = @@ -442,7 +437,7 @@ defmodule Diffo.Provider.NoteTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) note = @@ -466,7 +461,7 @@ defmodule Diffo.Provider.NoteTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) note = @@ -491,7 +486,7 @@ defmodule Diffo.Provider.NoteTest do id: "T4_ACCESS", name: :entityId, href: "entity/internal/T4_ACCESS", - referredType: :Entity + referred_type: :Entity }) note = @@ -531,7 +526,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..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 @@ -65,21 +60,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 +119,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 +196,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 +255,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 +468,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 +477,7 @@ defmodule Diffo.Provider.PartyRefTest do id: "IND000000897353", name: :individualId, href: "party/internal/IND000000897353", - referredType: :Individual + referred_type: :Individual }) party_ref = @@ -509,7 +504,7 @@ defmodule Diffo.Provider.PartyRefTest do id: "IND000000897353", name: "individualId", href: "party/internal/IND000000897353", - referredType: :Individual + referred_type: :Individual }) party_ref = @@ -525,7 +520,7 @@ defmodule Diffo.Provider.PartyRefTest do id: ~r/IND\d{12}/, name: "individualId", type: :PartyRef, - referredType: :Individual + referred_type: :Individual } } @@ -543,7 +538,7 @@ defmodule Diffo.Provider.PartyRefTest do id: "IND000000897353", name: :individualId, href: "party/internal/IND000000897353", - referredType: :Individual + referred_type: :Individual }) party_ref = @@ -562,7 +557,7 @@ defmodule Diffo.Provider.PartyRefTest do Diffo.Provider.create_place!(%{ id: "LOC000000897353", name: :locationId, - referredType: :GeographicAddress + referred_type: :GeographicAddress }) party = @@ -570,7 +565,7 @@ defmodule Diffo.Provider.PartyRefTest do id: "IND000000897353", name: :individualId, href: "party/internal/IND000000897353", - referredType: :Individual + referred_type: :Individual }) party_ref = @@ -589,7 +584,7 @@ defmodule Diffo.Provider.PartyRefTest do Diffo.Provider.create_party!(%{ id: "ORG000163435034", name: :organizationId, - referredType: :Organization + referred_type: :Organization }) party = @@ -597,7 +592,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..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 @@ -24,13 +19,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 +39,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 +65,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 +89,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 +126,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 +137,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 +154,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 +225,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 +246,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 +279,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 +287,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 +330,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 +353,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 +389,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 +417,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..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 @@ -65,21 +60,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 +113,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 +185,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 +460,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 +469,7 @@ defmodule Diffo.Provider.PlaceRefTest do id: "LOC000000897353", name: :locationId, href: "place/nbnco/LOC000000897353", - referredType: :GeographicAddress + referred_type: :GeographicAddress }) place_ref = @@ -501,7 +496,7 @@ defmodule Diffo.Provider.PlaceRefTest do id: "LOC000000897353", name: "locationId", href: "place/nbnco/LOC000000897353", - referredType: :GeographicAddress + referred_type: :GeographicAddress }) place_ref = @@ -517,7 +512,7 @@ defmodule Diffo.Provider.PlaceRefTest do id: ~r/LOC\d{12}/, name: "locationId", type: :PlaceRef, - referredType: :GeographicAddress + referred_type: :GeographicAddress } } @@ -535,7 +530,7 @@ defmodule Diffo.Provider.PlaceRefTest do id: "LOC000000897353", name: :locationId, href: "place/nbnco/LOC000000897353", - referredType: :GeographicAddress + referred_type: :GeographicAddress }) place_ref = @@ -555,14 +550,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..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 @@ -24,13 +19,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 +39,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 +65,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 +89,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 +126,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 +137,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 +154,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 +221,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 +243,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 +276,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 +284,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 +327,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 +350,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 +371,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 +387,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/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/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/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 new file mode 100644 index 0000000..a0c41d3 --- /dev/null +++ b/test/provider/versioning_test.exs @@ -0,0 +1,147 @@ +# 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 do + AshNeo4j.Sandbox.checkout() + on_exit(&AshNeo4j.Sandbox.rollback/0) + 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/nbn.ex b/test/support/nbn.ex new file mode 100644 index 0000000..0c99bdc --- /dev/null +++ b/test/support/nbn.ex @@ -0,0 +1,53 @@ +# 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.Organization + alias Diffo.Test.Person + alias Diffo.Test.Carrier + alias Diffo.Test.GeographicSite + alias Diffo.Test.ExchangeBuilding + + domain do + description "NBN party and place domain" + end + + resources do + 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: :build + 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/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/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/resource/card.ex b/test/support/resource/card.ex index 4056a1b..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, Servo, :get_card_by_id) - end) - change load [:href] upsert? false 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/invalid/invalid_characteristic.ex b/test/support/resource/invalid/invalid_characteristic.ex deleted file mode 100644 index 52b0ef6..0000000 --- a/test/support/resource/invalid/invalid_characteristic.ex +++ /dev/null @@ -1,66 +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 - alias Diffo.Provider.Instance.ActionHelper - - alias Diffo.Test.Servo - - use Ash.Resource, - fragments: [BaseInstance], - domain: Servo - - resource 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 - - characteristics do - characteristic :invalid, InvalidValue - 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, - Servo, - :get_invalid_characteristic_by_id - ) - end) - - 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 2d8f791..0000000 --- a/test/support/resource/invalid/invalid_feature_characteristic.ex +++ /dev/null @@ -1,69 +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 - alias Diffo.Provider.Instance.ActionHelper - - alias Diffo.Test.Servo - - use Ash.Resource, - fragments: [BaseInstance], - domain: Servo - - resource 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 - - features do - feature :invalid_feature_characteristic do - is_enabled? true - characteristic :invalid, InvalidValue - end - end - - actions 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, - Servo, - :get_invalid_feature_characteristic_by_id - ) - end) - - 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 4591912..0000000 --- a/test/support/resource/invalid/invalid_specification.ex +++ /dev/null @@ -1,62 +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 - alias Diffo.Provider.Instance.ActionHelper - - alias Diffo.Test.Servo - - use Ash.Resource, - fragments: [BaseInstance], - domain: Servo - - resource do - description "Ash Resource with an invalid specification" - end - - specification do - id "ef016d85-9dbd-429c-04da-1df56cc7dda5" - name "invalidSpecification" - type :resourceSpecification - category "Network Resource" - 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, - Servo, - :get_invalid_specification_by_id - ) - end) - - change load [:href] - upsert? false - end - end -end diff --git a/test/support/resource/organization.ex b/test/support/resource/organization.ex new file mode 100644 index 0000000..0a7c7bc --- /dev/null +++ b/test/support/resource/organization.ex @@ -0,0 +1,51 @@ +# 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 + + 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 new file mode 100644 index 0000000..e260004 --- /dev/null +++ b/test/support/resource/person.ex @@ -0,0 +1,51 @@ +# 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 + + 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 + + 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 diff --git a/test/support/resource/shelf.ex b/test/support/resource/shelf.ex index b95724e..b2f87b9 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,49 +29,62 @@ 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 + major_version 1 + minor_version 2 + patch_version 3 + tmf_version 4 + 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} + end + end + + 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 - features do - feature :spectralManagement do - is_enabled? true - characteristic :deploymentClass, DeploymentClassValue - characteristic :deploymentClasses, {:array, DeploymentClassValue} + places do + place :installation_site, Diffo.Provider.Place + place :billing_address, Diffo.Provider.Place, reference: true end end - characteristics do - characteristic :shelf, ShelfValue - characteristic :slots, AssignableValue - characteristic :shelves, {:array, ShelfValue} + 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, Servo, :get_shelf_by_id) - end) - change load [:href] upsert? false end diff --git a/test/support/servo.ex b/test/support/servo.ex index 652e03b..0121a5b 100644 --- a/test/support/servo.ex +++ b/test/support/servo.ex @@ -14,9 +14,8 @@ defmodule Diffo.Test.Servo do alias Diffo.Test.Shelf alias Diffo.Test.Card - alias Diffo.Test.InvalidSpecification - alias Diffo.Test.InvalidCharacteristic - alias Diffo.Test.InvalidFeatureCharacteristic + alias Diffo.Test.Broadband + alias Diffo.Test.BroadbandV2 domain do description "service and resource management" @@ -39,19 +38,14 @@ defmodule Diffo.Test.Servo do define :assign_port, action: :assign_port end - resource InvalidSpecification do - define :get_invalid_specification_by_id, action: :read, get_by: :id - define :build_invalid_specification, action: :build + resource Broadband do + define :build_broadband, action: :build + define :get_broadband_by_id, action: :read, get_by: :id 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 + resource BroadbandV2 do + define :build_broadband_v2, action: :build + define :get_broadband_v2_by_id, action: :read, get_by: :id 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 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}}