diff --git a/.formatter.exs b/.formatter.exs index a6a9c7d..c0ee74e 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -3,33 +3,46 @@ # SPDX-License-Identifier: MIT # Used by "mix format" -locals_without_parens = [ - id: 1, +spark_locals_without_parens = [ + calculate: 1, category: 1, - is_enabled?: 1, characteristic: 2, - pick: 1, - rename: 1, - field: 3, - expect: 1, - relate: 1, - guard: 1, - customize: 1, - order: 1, - initial_states: 1, - default_initial_state: 1, - state_attribute: 1, - transition: 1, - compact: 1, - label: 1 + characteristic: 3, + constraints: 1, + create: 1, + create: 2, + description: 1, + feature: 1, + feature: 2, + id: 1, + is_enabled?: 1, + major_version: 1, + minor_version: 1, + name: 1, + parties: 2, + parties: 3, + party: 2, + party: 3, + patch_version: 1, + place: 2, + place: 3, + places: 2, + places: 3, + reference: 1, + role: 2, + role: 3, + tmf_version: 1, + type: 1, + update: 1, + update: 2 ] [ plugins: [Spark.Formatter], inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"], - import_deps: [:ash], - locals_without_parens: locals_without_parens, + import_deps: [:ash, :ash_jason, :ash_neo4j, :ash_outstanding, :ash_state_machine], + locals_without_parens: spark_locals_without_parens, export: [ - locals_without_parens: locals_without_parens + locals_without_parens: spark_locals_without_parens ] ] diff --git a/.gitignore b/.gitignore index 9481f4e..3fdc82c 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,5 @@ diffo-*.tar .DS_Store # Agent related -.claude/* \ No newline at end of file +.claude/* +CLAUDE.md \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bb3573..613e20a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,20 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline +## [v0.2.2](https://github.com/diffo-dev/diffo/compare/v0.2.1...v0.2.2) (2026-05-08) + +## Notable Changes +* Updated to ash_neo4j 0.5.0 with async test support +* Igniter installer — `mix igniter.install diffo` now sets up Neo4j config, custom expressions, and Spark DSL formatter +* Spark DSL formatter configured for all provider extensions; `mix format` enforced across the codebase +* `usage-rules.md` added for AI coding assistant guidance when working with Diffo + +## What's Changed +* async tests by @matt-beanland in https://github.com/diffo-dev/diffo/pull/114 +* igniter by @matt-beanlanda in https://github.com/diffo-dev/diffo/pull/116 +* spark formatter by @matt-beanlanda in https://github.com/diffo-dev/diffo/pull/117 +* usage_rules by @matt-beanlanda in https://github.com/diffo-dev/diffo/pull/118 + ## [v0.2.1](https://github.com/diffo-dev/diffo/compare/v0.2.0...v0.2.1) (2026-05-06) ## Notable Changes diff --git a/README.md b/README.md index 0b19036..95dd7da 100644 --- a/README.md +++ b/README.md @@ -34,18 +34,25 @@ Diffo is especially suited for use in organisations with loosely coupled 'entity ## Installation -If [available in Hex](https://hex.pm/docs/publish), the package can be installed -by adding `diffo` to your list of dependencies in `mix.exs`: +The recommended way to install Diffo is with [Igniter](https://hexdocs.pm/igniter): + +```bash +mix igniter.install diffo +``` + +This will add the dependency, configure Neo4j (via `ash_neo4j`), register the custom expression, and set up the Spark formatter. + +Alternatively, add `diffo` to your list of dependencies in `mix.exs` manually: ```elixir def deps do [ - {:diffo, "~> 0.1.6"} + {:diffo, "~> 0.2.1"} ] end ``` -You should need [Neo4j](https://github.com/neo4j/neo4j) available. We recommend the Neo4j Community 5 latest, available at [Neo4j Deploymnent Centre](https://neo4j.com/deployment-center/) which can be installed locally. You can also configure connection to a cloud based database service such as [Neo4j AuraDB](https://neo4j.com/product/auradb/). +You will need [Neo4j](https://github.com/neo4j/neo4j) available. We recommend the Neo4j Community 5 latest, available at [Neo4j Deploymnent Centre](https://neo4j.com/deployment-center/) which can be installed locally. You can also configure connection to a cloud based database service such as [Neo4j AuraDB](https://neo4j.com/product/auradb/). ## Tutorial diff --git a/diffo.livemd b/diffo.livemd index 72111a9..5b3fb54 100644 --- a/diffo.livemd +++ b/diffo.livemd @@ -9,7 +9,7 @@ SPDX-License-Identifier: MIT ```elixir Mix.install( [ - {:diffo, "~> 0.2.1"} + {:diffo, "~> 0.2.2"} ], consolidate_protocols: false ) diff --git a/lib/diffo/provider/assigner/assignment.ex b/lib/diffo/provider/assigner/assignment.ex index 9f34ea0..a2cb1d1 100644 --- a/lib/diffo/provider/assigner/assignment.ex +++ b/lib/diffo/provider/assigner/assignment.ex @@ -42,5 +42,4 @@ defmodule Diffo.Provider.Assignment do inspect(struct) end end - end diff --git a/lib/diffo/provider/components/entity.ex b/lib/diffo/provider/components/entity.ex index ab9a017..35477c2 100644 --- a/lib/diffo/provider/components/entity.ex +++ b/lib/diffo/provider/components/entity.ex @@ -140,5 +140,4 @@ defmodule Diffo.Provider.Entity do preparations do prepare build(sort: [id: :asc]) end - end diff --git a/lib/diffo/provider/components/entity_ref.ex b/lib/diffo/provider/components/entity_ref.ex index 8d09ff3..f2ec7af 100644 --- a/lib/diffo/provider/components/entity_ref.ex +++ b/lib/diffo/provider/components/entity_ref.ex @@ -121,5 +121,4 @@ defmodule Diffo.Provider.EntityRef do preparations do prepare build(load: [:entity], sort: [created_at: :desc]) end - end diff --git a/lib/diffo/provider/components/event.ex b/lib/diffo/provider/components/event.ex index 1ec9167..11edcd5 100644 --- a/lib/diffo/provider/components/event.ex +++ b/lib/diffo/provider/components/event.ex @@ -153,5 +153,4 @@ defmodule Diffo.Provider.Event do Diffo.Util.to_iso8601(record.created_at) ) end - end diff --git a/lib/diffo/provider/components/external_identifier.ex b/lib/diffo/provider/components/external_identifier.ex index 4bd7702..5ee72d3 100644 --- a/lib/diffo/provider/components/external_identifier.ex +++ b/lib/diffo/provider/components/external_identifier.ex @@ -144,5 +144,4 @@ defmodule Diffo.Provider.ExternalIdentifier do preparations do prepare build(load: [:owner], sort: [created_at: :desc]) end - end diff --git a/lib/diffo/provider/components/feature.ex b/lib/diffo/provider/components/feature.ex index 505c72b..5fc2cc4 100644 --- a/lib/diffo/provider/components/feature.ex +++ b/lib/diffo/provider/components/feature.ex @@ -135,5 +135,4 @@ defmodule Diffo.Provider.Feature do preparations do prepare build(load: [:characteristics], sort: [name: :asc]) end - end diff --git a/lib/diffo/provider/components/instance/extension.ex b/lib/diffo/provider/components/instance/extension.ex index 0c8fd74..9fc716c 100644 --- a/lib/diffo/provider/components/instance/extension.ex +++ b/lib/diffo/provider/components/instance/extension.ex @@ -61,7 +61,8 @@ 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: [ @@ -114,7 +115,8 @@ defmodule Diffo.Provider.Instance.Extension do 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 ] ] @@ -182,11 +184,13 @@ defmodule Diffo.Provider.Instance.Extension do required: true ], party_type: [ - doc: "The module of the Party kind. An atom module name such as a BaseParty-derived resource.", + 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.", + doc: + "If true, no direct PartyRef edge is created; the party is reachable by graph traversal.", type: :boolean, default: false ], @@ -215,7 +219,8 @@ defmodule Diffo.Provider.Instance.Extension do @party_schema ++ [ constraints: [ - doc: "Multiplicity constraints on the number of parties in this role, e.g. [min: 1, max: 3]", + doc: + "Multiplicity constraints on the number of parties in this role, e.g. [min: 1, max: 3]", type: :keyword_list ] ] @@ -247,7 +252,8 @@ defmodule Diffo.Provider.Instance.Extension do type: :any ], reference: [ - doc: "If true, no direct PlaceRef edge is created; the place is reachable by graph traversal.", + doc: + "If true, no direct PlaceRef edge is created; the place is reachable by graph traversal.", type: :boolean, default: false ], @@ -276,7 +282,8 @@ defmodule Diffo.Provider.Instance.Extension do @place_schema ++ [ constraints: [ - doc: "Multiplicity constraints on the number of places in this role, e.g. [min: 1, max: 3]", + doc: + "Multiplicity constraints on the number of places in this role, e.g. [min: 1, max: 3]", type: :keyword_list ] ] @@ -299,7 +306,8 @@ defmodule Diffo.Provider.Instance.Extension do @structure %Spark.Dsl.Section{ name: :structure, - describe: "Defines the structural shape of the Instance — its specification, characteristics, features, parties, and places", + describe: + "Defines the structural shape of the Instance — its specification, characteristics, features, parties, and places", examples: [ """ structure do @@ -330,7 +338,8 @@ defmodule Diffo.Provider.Instance.Extension do @action_create %Spark.Dsl.Entity{ name: :create, - describe: "Marks a create action for instance build wiring, injecting :specified_by, :features, and :characteristics arguments", + 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: [ @@ -372,7 +381,8 @@ defmodule Diffo.Provider.Instance.Extension do @behaviour_section %Spark.Dsl.Section{ name: :behaviour, - describe: "Defines the behavioural wiring for the Instance — actions, and in future triggers and tasks", + describe: + "Defines the behavioural wiring for the Instance — actions, and in future triggers and tasks", examples: [ """ behaviour do diff --git a/lib/diffo/provider/components/instance/extension/characteristic.ex b/lib/diffo/provider/components/instance/extension/characteristic.ex index 92226d3..b746dfc 100644 --- a/lib/diffo/provider/components/instance/extension/characteristic.ex +++ b/lib/diffo/provider/components/instance/extension/characteristic.ex @@ -66,7 +66,10 @@ 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) - Provider.relate_instance_characteristics(%Instance{id: result.id}, %{characteristics: characteristics}) + + Provider.relate_instance_characteristics(%Instance{id: result.id}, %{ + characteristics: characteristics + }) end @doc """ diff --git a/lib/diffo/provider/components/instance/extension/party.ex b/lib/diffo/provider/components/instance/extension/party.ex index 26bb9b4..7a9b800 100644 --- a/lib/diffo/provider/components/instance/extension/party.ex +++ b/lib/diffo/provider/components/instance/extension/party.ex @@ -17,6 +17,7 @@ defmodule Diffo.Provider.Instance.Party do changeset else parties = Ash.Changeset.get_argument(changeset, :parties) || [] + changeset |> validate_roles(parties, declarations) |> validate_constraints(parties, declarations) @@ -55,15 +56,23 @@ defmodule Diffo.Provider.Instance.Party do 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})") + 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})") + do: + Ash.Changeset.add_error(cs, + field: :parties, + message: "role #{inspect(role)} allows at most #{max} (got #{count})" + ) @doc """ Relates the parties in the changeset with the Extended Instance by creating party_ref diff --git a/lib/diffo/provider/components/instance/extension/party_declaration.ex b/lib/diffo/provider/components/instance/extension/party_declaration.ex index e1cff0c..45194e2 100644 --- a/lib/diffo/provider/components/instance/extension/party_declaration.ex +++ b/lib/diffo/provider/components/instance/extension/party_declaration.ex @@ -6,8 +6,15 @@ 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] + defstruct [ + :role, + :party_type, + :multiple, + :reference, + :calculate, + :constraints, + __spark_metadata__: nil + ] defimpl String.Chars do def to_string(struct), do: inspect(struct) diff --git a/lib/diffo/provider/components/instance/extension/persisters/persist_characteristics.ex b/lib/diffo/provider/components/instance/extension/persisters/persist_characteristics.ex index e622fcc..2266fa1 100644 --- a/lib/diffo/provider/components/instance/extension/persisters/persist_characteristics.ex +++ b/lib/diffo/provider/components/instance/extension/persisters/persist_characteristics.ex @@ -13,9 +13,14 @@ defmodule Diffo.Provider.Instance.Extension.Persisters.PersistCharacteristics do 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)} + {: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 index 53fd427..3cf3414 100644 --- a/lib/diffo/provider/components/instance/extension/persisters/persist_features.ex +++ b/lib/diffo/provider/components/instance/extension/persisters/persist_features.ex @@ -13,9 +13,14 @@ defmodule Diffo.Provider.Instance.Extension.Persisters.PersistFeatures do 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)} + {: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 index ff25478..3278630 100644 --- a/lib/diffo/provider/components/instance/extension/persisters/persist_parties.ex +++ b/lib/diffo/provider/components/instance/extension/persisters/persist_parties.ex @@ -13,9 +13,14 @@ defmodule Diffo.Provider.Instance.Extension.Persisters.PersistParties do 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)} + {: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 index ec90dd6..4295061 100644 --- a/lib/diffo/provider/components/instance/extension/persisters/persist_places.ex +++ b/lib/diffo/provider/components/instance/extension/persisters/persist_places.ex @@ -13,9 +13,14 @@ defmodule Diffo.Provider.Instance.Extension.Persisters.PersistPlaces do 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)} + {: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 index b5f6a1b..db87d31 100644 --- a/lib/diffo/provider/components/instance/extension/persisters/persist_specification.ex +++ b/lib/diffo/provider/components/instance/extension/persisters/persist_specification.ex @@ -12,10 +12,19 @@ defmodule Diffo.Provider.Instance.Extension.Persisters.PersistSpecification 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), + 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) @@ -24,9 +33,14 @@ defmodule Diffo.Provider.Instance.Extension.Persisters.PersistSpecification do 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)} + {: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_declaration.ex b/lib/diffo/provider/components/instance/extension/place_declaration.ex index 1d53d72..3469946 100644 --- a/lib/diffo/provider/components/instance/extension/place_declaration.ex +++ b/lib/diffo/provider/components/instance/extension/place_declaration.ex @@ -6,8 +6,15 @@ 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] + defstruct [ + :role, + :place_type, + :multiple, + :reference, + :calculate, + :constraints, + __spark_metadata__: nil + ] defimpl String.Chars do def to_string(struct), do: inspect(struct) diff --git a/lib/diffo/provider/components/instance/extension/specification.ex b/lib/diffo/provider/components/instance/extension/specification.ex index 3691d09..25f892b 100644 --- a/lib/diffo/provider/components/instance/extension/specification.ex +++ b/lib/diffo/provider/components/instance/extension/specification.ex @@ -12,7 +12,17 @@ defmodule Diffo.Provider.Instance.Specification do @doc """ Struct for a Specification """ - defstruct [:id, :name, :type, :major_version, :minor_version, :patch_version, :tmf_version, :description, :category] + defstruct [ + :id, + :name, + :type, + :major_version, + :minor_version, + :patch_version, + :tmf_version, + :description, + :category + ] @doc """ Sets the specified_by argument in the changeset, ensuring the Extended Instance's specification exists diff --git a/lib/diffo/provider/components/instance/extension/transformers/transform_behaviour.ex b/lib/diffo/provider/components/instance/extension/transformers/transform_behaviour.ex index aac21c2..0064f9f 100644 --- a/lib/diffo/provider/components/instance/extension/transformers/transform_behaviour.ex +++ b/lib/diffo/provider/components/instance/extension/transformers/transform_behaviour.ex @@ -22,50 +22,59 @@ defmodule Diffo.Provider.Instance.Extension.Transformers.TransformBehaviour do {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 = + 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)} + {: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 @@ -73,7 +82,8 @@ defmodule Diffo.Provider.Instance.Extension.Transformers.TransformBehaviour do 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 -> + 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)) @@ -85,10 +95,16 @@ defmodule Diffo.Provider.Instance.Extension.Transformers.TransformBehaviour do @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} + %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) diff --git a/lib/diffo/provider/components/instance/extension/verifiers/verify_behaviour.ex b/lib/diffo/provider/components/instance/extension/verifiers/verify_behaviour.ex index 7cc10db..4196362 100644 --- a/lib/diffo/provider/components/instance/extension/verifiers/verify_behaviour.ex +++ b/lib/diffo/provider/components/instance/extension/verifiers/verify_behaviour.ex @@ -17,8 +17,15 @@ defmodule Diffo.Provider.Instance.Extension.Verifiers.VerifyBehaviour do 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) + 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 @@ -26,22 +33,28 @@ defmodule Diffo.Provider.Instance.Extension.Verifiers.VerifyBehaviour do 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" - )] + [ + 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" - )] + [ + DslError.exception( + module: resource, + path: [:behaviour, :actions], + message: + "behaviour: update #{inspect(name)} does not exist as an update action on this resource" + ) + ] end 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 index d85e65f..424731b 100644 --- a/lib/diffo/provider/components/instance/extension/verifiers/verify_features.ex +++ b/lib/diffo/provider/components/instance/extension/verifiers/verify_features.ex @@ -36,7 +36,8 @@ defmodule Diffo.Provider.Instance.Extension.Verifiers.VerifyFeatures do 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)}" + message: + "features: characteristic name #{inspect(name)} is declared more than once in #{inspect(feature.name)}" ) end) @@ -50,7 +51,8 @@ defmodule Diffo.Provider.Instance.Extension.Verifiers.VerifyFeatures do DslError.exception( module: resource, path: [:structure, :features, feature.name, :characteristics, char.name], - message: "features: characteristic value_type #{inspect(module)} does not exist" + message: + "features: characteristic value_type #{inspect(module)} does not exist" ) | inner_acc ] diff --git a/lib/diffo/provider/components/instance/extension/verifiers/verify_parties.ex b/lib/diffo/provider/components/instance/extension/verifiers/verify_parties.ex index 63bd593..67de82b 100644 --- a/lib/diffo/provider/components/instance/extension/verifiers/verify_parties.ex +++ b/lib/diffo/provider/components/instance/extension/verifiers/verify_parties.ex @@ -48,7 +48,8 @@ defmodule Diffo.Provider.Instance.Extension.Verifiers.VerifyParties do DslError.exception( module: resource, path: [:structure, :parties, party.role], - message: "parties: party_type #{inspect(party.party_type)} does not extend BaseParty" + message: + "parties: party_type #{inspect(party.party_type)} does not extend BaseParty" ) | acc ] diff --git a/lib/diffo/provider/components/instance/extension/verifiers/verify_specification.ex b/lib/diffo/provider/components/instance/extension/verifiers/verify_specification.ex index c5d6621..4902596 100644 --- a/lib/diffo/provider/components/instance/extension/verifiers/verify_specification.ex +++ b/lib/diffo/provider/components/instance/extension/verifiers/verify_specification.ex @@ -10,7 +10,16 @@ defmodule Diffo.Provider.Instance.Extension.Verifiers.VerifySpecification do 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] + @spec_fields [ + :name, + :type, + :major_version, + :minor_version, + :patch_version, + :tmf_version, + :description, + :category + ] @impl true def verify(dsl_state) do @@ -28,11 +37,13 @@ defmodule Diffo.Provider.Instance.Extension.Verifiers.VerifySpecification 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" - )] + [ + DslError.exception( + module: resource, + path: [:structure, :specification, :id], + message: "specification: id must be a valid UUID4" + ) + ] else [] end @@ -53,11 +64,13 @@ defmodule Diffo.Provider.Instance.Extension.Verifiers.VerifySpecification do [] {:error, errors} -> - [DslError.exception( - module: resource, - path: [:structure, :specification, field], - message: "specification: #{field} - #{format_errors(errors)}" - )] + [ + DslError.exception( + module: resource, + path: [:structure, :specification, field], + message: "specification: #{field} - #{format_errors(errors)}" + ) + ] end else [] @@ -75,6 +88,7 @@ defmodule Diffo.Provider.Instance.Extension.Verifiers.VerifySpecification do 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) diff --git a/lib/diffo/provider/components/instance/util.ex b/lib/diffo/provider/components/instance/util.ex index ad5a604..3092cf2 100644 --- a/lib/diffo/provider/components/instance/util.ex +++ b/lib/diffo/provider/components/instance/util.ex @@ -180,5 +180,4 @@ defmodule Diffo.Provider.Instance.Util do _ -> nil end end - end diff --git a/lib/diffo/provider/components/note.ex b/lib/diffo/provider/components/note.ex index 7cc4aaf..16747bd 100644 --- a/lib/diffo/provider/components/note.ex +++ b/lib/diffo/provider/components/note.ex @@ -148,5 +148,4 @@ defmodule Diffo.Provider.Note do preparations do prepare build(load: [:author], sort: [timestamp: :desc]) 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 index 49823d1..364b19c 100644 --- a/lib/diffo/provider/components/party/extension/persisters/persist_instances.ex +++ b/lib/diffo/provider/components/party/extension/persisters/persist_instances.ex @@ -13,9 +13,14 @@ defmodule Diffo.Provider.Party.Extension.Persisters.PersistInstances do 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)} + {: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 index f6e6590..943c332 100644 --- a/lib/diffo/provider/components/party/extension/persisters/persist_parties.ex +++ b/lib/diffo/provider/components/party/extension/persisters/persist_parties.ex @@ -13,9 +13,14 @@ defmodule Diffo.Provider.Party.Extension.Persisters.PersistParties do 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)} + {: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 index 453b1de..6a83be1 100644 --- a/lib/diffo/provider/components/party/extension/persisters/persist_places.ex +++ b/lib/diffo/provider/components/party/extension/persisters/persist_places.ex @@ -13,9 +13,14 @@ defmodule Diffo.Provider.Party.Extension.Persisters.PersistPlaces do 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)} + {:ok, + Transformer.eval( + dsl_state, + [], + quote do + @doc false + def places, do: unquote(escaped) + end + )} end end diff --git a/lib/diffo/provider/components/party/extension/verifiers/verify_roles.ex b/lib/diffo/provider/components/party/extension/verifiers/verify_roles.ex index d2bdba5..050c4c1 100644 --- a/lib/diffo/provider/components/party/extension/verifiers/verify_roles.ex +++ b/lib/diffo/provider/components/party/extension/verifiers/verify_roles.ex @@ -17,12 +17,36 @@ defmodule Diffo.Provider.Party.Extension.Verifiers.VerifyRoles 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) + 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 @@ -32,6 +56,7 @@ defmodule Diffo.Provider.Party.Extension.Verifiers.VerifyRoles do 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 @@ -58,18 +83,24 @@ defmodule Diffo.Provider.Party.Extension.Verifiers.VerifyRoles do acc !Code.ensure_loaded?(mod) -> - [DslError.exception( - module: resource, - path: [String.to_atom(section)], - message: "#{section}: #{field} #{inspect(mod)} does not exist" - ) | acc] + [ + 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] + [ + DslError.exception( + module: resource, + path: [String.to_atom(section)], + message: "#{section}: #{field} #{inspect(mod)} does not extend #{base}" + ) + | acc + ] true -> acc diff --git a/lib/diffo/provider/components/party_ref.ex b/lib/diffo/provider/components/party_ref.ex index 1d041df..b5569c9 100644 --- a/lib/diffo/provider/components/party_ref.ex +++ b/lib/diffo/provider/components/party_ref.ex @@ -200,5 +200,4 @@ defmodule Diffo.Provider.PartyRef do result end 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 index d64d3f3..2ef8471 100644 --- a/lib/diffo/provider/components/place/extension/persisters/persist_instances.ex +++ b/lib/diffo/provider/components/place/extension/persisters/persist_instances.ex @@ -13,9 +13,14 @@ defmodule Diffo.Provider.Place.Extension.Persisters.PersistInstances do 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)} + {: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 index 6612423..bdc46f3 100644 --- a/lib/diffo/provider/components/place/extension/persisters/persist_parties.ex +++ b/lib/diffo/provider/components/place/extension/persisters/persist_parties.ex @@ -13,9 +13,14 @@ defmodule Diffo.Provider.Place.Extension.Persisters.PersistParties do 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)} + {: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 index 3fa789d..93549a0 100644 --- a/lib/diffo/provider/components/place/extension/persisters/persist_places.ex +++ b/lib/diffo/provider/components/place/extension/persisters/persist_places.ex @@ -13,9 +13,14 @@ defmodule Diffo.Provider.Place.Extension.Persisters.PersistPlaces do 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)} + {:ok, + Transformer.eval( + dsl_state, + [], + quote do + @doc false + def places, do: unquote(escaped) + end + )} end end diff --git a/lib/diffo/provider/components/place/extension/verifiers/verify_roles.ex b/lib/diffo/provider/components/place/extension/verifiers/verify_roles.ex index 70991df..756d041 100644 --- a/lib/diffo/provider/components/place/extension/verifiers/verify_roles.ex +++ b/lib/diffo/provider/components/place/extension/verifiers/verify_roles.ex @@ -17,12 +17,36 @@ defmodule Diffo.Provider.Place.Extension.Verifiers.VerifyRoles 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) + 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 @@ -32,6 +56,7 @@ defmodule Diffo.Provider.Place.Extension.Verifiers.VerifyRoles do 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 @@ -58,18 +83,24 @@ defmodule Diffo.Provider.Place.Extension.Verifiers.VerifyRoles do acc !Code.ensure_loaded?(mod) -> - [DslError.exception( - module: resource, - path: [String.to_atom(section)], - message: "#{section}: #{field} #{inspect(mod)} does not exist" - ) | acc] + [ + 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] + [ + DslError.exception( + module: resource, + path: [String.to_atom(section)], + message: "#{section}: #{field} #{inspect(mod)} does not extend #{base}" + ) + | acc + ] true -> acc diff --git a/lib/diffo/provider/components/place_ref.ex b/lib/diffo/provider/components/place_ref.ex index a2acd0b..d412dd9 100644 --- a/lib/diffo/provider/components/place_ref.ex +++ b/lib/diffo/provider/components/place_ref.ex @@ -169,5 +169,4 @@ defmodule Diffo.Provider.PlaceRef do sort: [role: :asc, created_at: :desc] ) end - end diff --git a/lib/diffo/provider/components/process_status.ex b/lib/diffo/provider/components/process_status.ex index 6843260..4f83696 100644 --- a/lib/diffo/provider/components/process_status.ex +++ b/lib/diffo/provider/components/process_status.ex @@ -124,5 +124,4 @@ defmodule Diffo.Provider.ProcessStatus do preparations do prepare build(sort: [timestamp: :desc]) end - end diff --git a/lib/diffo/provider/components/specification.ex b/lib/diffo/provider/components/specification.ex index dc90930..066f97f 100644 --- a/lib/diffo/provider/components/specification.ex +++ b/lib/diffo/provider/components/specification.ex @@ -84,7 +84,19 @@ 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, :minor_version, :patch_version, :tmf_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/type/value.ex b/lib/diffo/type/value.ex index 5fbcd5e..99b3789 100644 --- a/lib/diffo/type/value.ex +++ b/lib/diffo/type/value.ex @@ -96,13 +96,19 @@ defmodule Diffo.Type.Value do 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) + + def handle_change(old_value, new_value, constraints), + do: super(old_value, new_value, constraints) def handle_change_array(_old_values, nil, _constraints), do: {:ok, nil} - def handle_change_array(old_values, new_values, constraints), do: super(old_values, new_values, constraints) + + def handle_change_array(old_values, new_values, constraints), + do: super(old_values, new_values, constraints) def prepare_change_array(_old_values, nil, _constraints), do: {:ok, nil} - def prepare_change_array(old_values, new_values, constraints), do: super(old_values, new_values, constraints) + + def prepare_change_array(old_values, new_values, constraints), + do: super(old_values, new_values, constraints) def primitive(type, value), do: Diffo.Type.Primitive.wrap(type, value) diff --git a/lib/mix/tasks/diffo.install.ex b/lib/mix/tasks/diffo.install.ex new file mode 100644 index 0000000..11d45a0 --- /dev/null +++ b/lib/mix/tasks/diffo.install.ex @@ -0,0 +1,79 @@ +# SPDX-FileCopyrightText: 2025 diffo contributors +# +# SPDX-License-Identifier: MIT + +defmodule Mix.Tasks.Diffo.Install.Docs do + @moduledoc false + + def short_doc, do: "Installs Diffo" + def example, do: "mix igniter.install diffo" + + def long_doc do + """ + #{short_doc()} + + ## Example + + ```bash + #{example()} + ``` + """ + end +end + +if Code.ensure_loaded?(Igniter) do + defmodule Mix.Tasks.Diffo.Install do + @shortdoc "#{__MODULE__.Docs.short_doc()}" + @moduledoc __MODULE__.Docs.long_doc() + + use Igniter.Mix.Task + + @impl Igniter.Mix.Task + def info(_argv, _composing_task) do + %Igniter.Mix.Task.Info{ + group: :ash, + installs: [{:ash_neo4j, "~> 0.5"}], + example: __MODULE__.Docs.example() + } + end + + @impl Igniter.Mix.Task + def igniter(igniter) do + igniter + |> Igniter.Project.Formatter.import_dep(:diffo) + |> Spark.Igniter.prepend_to_section_order( + :"Ash.Resource", + [:specification, :features, :characteristics] + ) + |> Igniter.Project.Config.configure( + "config.exs", + :ash, + [:custom_expressions], + [Diffo.Unwrap.AshCustomExpression], + updater: fn zipper -> + Igniter.Code.List.prepend_new_to_list( + zipper, + quote(do: Diffo.Unwrap.AshCustomExpression) + ) + end + ) + end + end +else + defmodule Mix.Tasks.Diffo.Install do + @shortdoc "#{__MODULE__.Docs.short_doc()} | Install `igniter` to use" + @moduledoc __MODULE__.Docs.long_doc() + + use Mix.Task + + def run(_argv) do + Mix.shell().error(""" + The task 'diffo.install' requires igniter. Please install igniter and try again. + + For more information, see: https://hexdocs.pm/igniter/readme.html#installation + """) + + exit({:shutdown, 1}) + end + end +end diff --git a/mix.exs b/mix.exs index c719f05..a5bfda4 100644 --- a/mix.exs +++ b/mix.exs @@ -6,7 +6,7 @@ defmodule Diffo.MixProject do @moduledoc false use Mix.Project - @version "0.2.1" + @version "0.2.2" @name "Diffo" @description "TMF Service and Resource Manager with a difference" @github_url "https://github.com/diffo-dev/diffo" @@ -26,7 +26,8 @@ defmodule Diffo.MixProject do docs: &docs/0, deps: deps(), aliases: aliases(), - consolidate_protocols: Mix.env() != :dev + consolidate_protocols: Mix.env() != :dev, + usage_rules: usage_rules() ] end @@ -76,7 +77,7 @@ defmodule Diffo.MixProject do ] ], groups_for_extras: [ - "DSLs": ~r/documentation\/dsls\// + DSLs: ~r/documentation\/dsls\// ] ] end @@ -93,13 +94,31 @@ defmodule Diffo.MixProject do ] end + defp usage_rules do + [ + file: "CLAUDE.md", + usage_rules: ["usage_rules:all"], + skills: [ + location: ".claude/skills", + build: [ + "diffo-framework": [ + description: + "Use when working with Diffo or its underlying Ash ecosystem. Consult when making any domain, resource, or provider changes.", + usage_rules: [:ash, :ash_neo4j, :spark, :reactor, :igniter] + ] + ] + ] + ] + end + # Run "mix help deps" to learn about dependencies. defp deps do [ + {:usage_rules, "~> 1.2", only: [:dev]}, {:ash_outstanding, "~> 0.2.3"}, {:ash_jason, "~> 3.0"}, {:ash_state_machine, "~> 0.2.12"}, - {:ash_neo4j, ash_neo4j_version("~> 0.4.1")}, + {:ash_neo4j, ash_neo4j_version("~> 0.5")}, {:bolty, ">= 0.0.12"}, {:ash, ash_version("~> 3.0 and >= 3.24.2")}, {:uuid, "~> 1.1"}, diff --git a/mix.lock b/mix.lock index 0ba5589..a8c2edd 100644 --- a/mix.lock +++ b/mix.lock @@ -1,13 +1,13 @@ %{ "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.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_neo4j": {:hex, :ash_neo4j, "0.5.0", "7e19abf973cd86fb67fa8b3544daef68be1ad3f912a2c4b3c6c3ddd7244d7e52", [: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]}, {:usage_rules, "~> 1.2", [hex: :usage_rules, repo: "hexpm", optional: true]}], "hexpm", "76de0829dddfce12b53869e4e129a19a14b4474178f3189bfd97a5aae6b096ae"}, "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.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"}, + "decimal": {:hex, :decimal, "3.0.0", "ce2befbd7218427e4a57d1c6efa6bf50cfc7d0c480c422e70f4fb533074a5f33", [:mix], [], "hexpm", "7a6ab3f806f09738991fc951b2fd2390b3377113feec605a540121aaf772a87b"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, "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"}, @@ -39,6 +39,7 @@ "stream_data": {:hex, :stream_data, "1.3.0", "bde37905530aff386dea1ddd86ecbf00e6642dc074ceffc10b7d4e41dfd6aac9", [:mix], [], "hexpm", "3cc552e286e817dca43c98044c706eec9318083a1480c52ae2688b08e2936e3c"}, "telemetry": {:hex, :telemetry, "1.4.1", "ab6de178e2b29b58e8256b92b382ea3f590a47152ca3651ea857a6cae05ac423", [:rebar3], [], "hexpm", "2172e05a27531d3d31dd9782841065c50dd5c3c7699d95266b2edd54c2dafa1c"}, "text_diff": {:hex, :text_diff, "0.1.0", "1caf3175e11a53a9a139bc9339bd607c47b9e376b073d4571c031913317fecaa", [:mix], [], "hexpm", "d1ffaaecab338e49357b6daa82e435f877e0649041ace7755583a0ea3362dbd7"}, + "usage_rules": {:hex, :usage_rules, "1.2.6", "a7b3f8d6e5d265701139d5714749c37c54bb82230a4c51ec54a12a1e4769b9d1", [:mix], [{:igniter, ">= 0.6.6 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}], "hexpm", "608411b9876a16a9d62a427dbaf42faf458e4cd0a508b3bd7e5ee71502073582"}, "uuid": {:hex, :uuid, "1.1.8", "e22fc04499de0de3ed1116b770c7737779f226ceefa0badb3592e64d5cfb4eb9", [:mix], [], "hexpm", "c790593b4c3b601f5dc2378baae7efaf5b3d73c4c6456ba85759905be792f2ac"}, "yamerl": {:hex, :yamerl, "0.10.0", "4ff81fee2f1f6a46f1700c0d880b24d193ddb74bd14ef42cb0bcf46e81ef2f8e", [:rebar3], [], "hexpm", "346adb2963f1051dc837a2364e4acf6eb7d80097c0f53cbdc3046ec8ec4b4e6e"}, "yaml_elixir": {:hex, :yaml_elixir, "2.12.1", "d74f2d82294651b58dac849c45a82aaea639766797359baff834b64439f6b3f4", [:mix], [{:yamerl, "~> 0.10", [hex: :yamerl, repo: "hexpm", optional: false]}], "hexpm", "d9ac16563c737d55f9bfeed7627489156b91268a3a21cd55c54eb2e335207fed"}, diff --git a/test/diffo_test.exs b/test/diffo_test.exs index b893a49..7df32d7 100644 --- a/test/diffo_test.exs +++ b/test/diffo_test.exs @@ -4,7 +4,7 @@ defmodule DiffoTest do @moduledoc false - use ExUnit.Case + use ExUnit.Case, async: true doctest Diffo doctest Diffo.Unwrap doctest Diffo.Type.Primitive diff --git a/test/instance_extension/assigner_test.exs b/test/instance_extension/assigner_test.exs index b626990..9c5ca9e 100644 --- a/test/instance_extension/assigner_test.exs +++ b/test/instance_extension/assigner_test.exs @@ -4,7 +4,7 @@ defmodule Diffo.InstanceExtension.AssignerTest do @moduledoc false - use ExUnit.Case + use ExUnit.Case, async: true alias Diffo.Provider.Specification alias Diffo.Provider.Characteristic alias Diffo.Provider.Assignment diff --git a/test/instance_extension/characteristic_test.exs b/test/instance_extension/characteristic_test.exs index f3929e8..bdb6234 100644 --- a/test/instance_extension/characteristic_test.exs +++ b/test/instance_extension/characteristic_test.exs @@ -4,7 +4,7 @@ defmodule Diffo.InstanceExtension.CharacteristicTest do @moduledoc false - use ExUnit.Case + use ExUnit.Case, async: true alias Diffo.Test.Parties setup do diff --git a/test/instance_extension/feature_test.exs b/test/instance_extension/feature_test.exs index 8756550..f572d34 100644 --- a/test/instance_extension/feature_test.exs +++ b/test/instance_extension/feature_test.exs @@ -4,7 +4,7 @@ defmodule Diffo.InstanceExtension.FeatureTest do @moduledoc false - use ExUnit.Case + use ExUnit.Case, async: true alias Diffo.Test.Parties setup do diff --git a/test/instance_extension/party_test.exs b/test/instance_extension/party_test.exs index 6e6171a..56f717a 100644 --- a/test/instance_extension/party_test.exs +++ b/test/instance_extension/party_test.exs @@ -4,7 +4,7 @@ defmodule Diffo.InstanceExtension.PartyTest do @moduledoc false - use ExUnit.Case + use ExUnit.Case, async: true alias Diffo.Provider.Instance.Extension.Info, as: InstanceInfo alias Diffo.Provider.Party.Extension.Info, as: PartyInfo @@ -131,6 +131,7 @@ defmodule Diffo.InstanceExtension.PartyTest do %Party{id: p3.id, role: :installer}, %Party{id: p4.id, role: :installer} ] + assert {:error, _} = Servo.build_shelf(%{name: "s", parties: parties}) end @@ -139,6 +140,7 @@ defmodule Diffo.InstanceExtension.PartyTest do %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 @@ -149,6 +151,7 @@ defmodule Diffo.InstanceExtension.PartyTest do %Party{id: p2.id, role: :installer}, %Party{id: p3.id, role: :installer} ] + assert {:ok, _shelf} = Servo.build_shelf(%{name: "s", parties: parties}) end end @@ -166,11 +169,12 @@ defmodule Diffo.InstanceExtension.PartyTest do 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" - }) + {:ok, carrier} = + Nbn.create_carrier(%{ + name: "Acme Wholesale", + abn: "51824753556", + trading_name: "Acme" + }) assert carrier.name == "Acme Wholesale" assert carrier.type == :Organization @@ -179,11 +183,12 @@ defmodule Diffo.InstanceExtension.PartyTest do end test "domain-specific attributes are readable after creation" do - {:ok, carrier} = Nbn.create_carrier(%{ - name: "Acme Wholesale", - abn: "51824753556", - trading_name: "Acme" - }) + {: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" diff --git a/test/instance_extension/place_test.exs b/test/instance_extension/place_test.exs index 6f5127b..87e5cf5 100644 --- a/test/instance_extension/place_test.exs +++ b/test/instance_extension/place_test.exs @@ -4,7 +4,7 @@ defmodule Diffo.InstanceExtension.PlaceTest do @moduledoc false - use ExUnit.Case + use ExUnit.Case, async: true alias Diffo.Provider.Instance.Extension.Info, as: InstanceInfo alias Diffo.Provider.Place.Extension.Info, as: PlaceInfo @@ -89,12 +89,13 @@ defmodule Diffo.InstanceExtension.PlaceTest do 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 - }) + {: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 @@ -103,12 +104,13 @@ defmodule Diffo.InstanceExtension.PlaceTest do 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, _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" @@ -116,10 +118,11 @@ defmodule Diffo.InstanceExtension.PlaceTest do end test "domain-specific attributes are nil when not provided" do - {:ok, building} = Nbn.create_exchange_building(%{ - id: "EX-MEL-003", - name: "Bare Exchange" - }) + {:ok, building} = + Nbn.create_exchange_building(%{ + id: "EX-MEL-003", + name: "Bare Exchange" + }) assert building.nli == nil assert building.access_type == nil diff --git a/test/instance_extension/specification_test.exs b/test/instance_extension/specification_test.exs index 850680d..87349c4 100644 --- a/test/instance_extension/specification_test.exs +++ b/test/instance_extension/specification_test.exs @@ -4,7 +4,7 @@ defmodule Diffo.InstanceExtension.SpecificationTest do @moduledoc false - use ExUnit.Case + use ExUnit.Case, async: true alias Diffo.Test.Servo alias Diffo.Test.Shelf diff --git a/test/instance_extension/transformer_test.exs b/test/instance_extension/transformer_test.exs index a32ca0e..1e1ed66 100644 --- a/test/instance_extension/transformer_test.exs +++ b/test/instance_extension/transformer_test.exs @@ -4,7 +4,7 @@ defmodule Diffo.InstanceExtension.TransformerTest do @moduledoc false - use ExUnit.Case, async: true + use ExUnit.Case, async: true, async: true alias Diffo.Test.Shelf alias Diffo.Test.Card @@ -231,7 +231,10 @@ defmodule Diffo.InstanceExtension.TransformerTest do 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])) + + injected = + Enum.filter(action.arguments, &(&1.name in [:specified_by, :features, :characteristics])) + assert Enum.all?(injected, &(&1.public? == false)) end diff --git a/test/instance_extension/verifier_test.exs b/test/instance_extension/verifier_test.exs index c9d197e..0273c97 100644 --- a/test/instance_extension/verifier_test.exs +++ b/test/instance_extension/verifier_test.exs @@ -4,7 +4,7 @@ defmodule Diffo.InstanceExtension.VerifierTest do @moduledoc false - use ExUnit.Case, async: false + use ExUnit.Case, async: true, async: false alias Diffo.Test.Util describe "specification verifier" do diff --git a/test/party_extension/transformer_test.exs b/test/party_extension/transformer_test.exs index e3ed62b..3614d8b 100644 --- a/test/party_extension/transformer_test.exs +++ b/test/party_extension/transformer_test.exs @@ -4,7 +4,7 @@ defmodule Diffo.PartyExtension.TransformerTest do @moduledoc false - use ExUnit.Case, async: true + use ExUnit.Case, async: true, async: true alias Diffo.Test.Organization alias Diffo.Test.Person diff --git a/test/party_extension/verifier_test.exs b/test/party_extension/verifier_test.exs index 0e4f65e..d4d64ea 100644 --- a/test/party_extension/verifier_test.exs +++ b/test/party_extension/verifier_test.exs @@ -4,7 +4,7 @@ defmodule Diffo.PartyExtension.VerifierTest do @moduledoc false - use ExUnit.Case, async: false + use ExUnit.Case, async: true, async: false alias Diffo.Test.Util describe "instances verifier" do diff --git a/test/place_extension/transformer_test.exs b/test/place_extension/transformer_test.exs index 0b5cd76..98c5158 100644 --- a/test/place_extension/transformer_test.exs +++ b/test/place_extension/transformer_test.exs @@ -4,7 +4,7 @@ defmodule Diffo.PlaceExtension.TransformerTest do @moduledoc false - use ExUnit.Case, async: true + use ExUnit.Case, async: true, async: true alias Diffo.Test.GeographicSite alias Diffo.Provider.Place.Extension.InstanceRole diff --git a/test/place_extension/verifier_test.exs b/test/place_extension/verifier_test.exs index f9b5b2f..48c4eb1 100644 --- a/test/place_extension/verifier_test.exs +++ b/test/place_extension/verifier_test.exs @@ -4,7 +4,7 @@ defmodule Diffo.PlaceExtension.VerifierTest do @moduledoc false - use ExUnit.Case, async: false + use ExUnit.Case, async: true, async: false alias Diffo.Test.Util describe "instances verifier" do diff --git a/test/provider/characteristic_test.exs b/test/provider/characteristic_test.exs index 0dc8fed..c719c84 100644 --- a/test/provider/characteristic_test.exs +++ b/test/provider/characteristic_test.exs @@ -4,7 +4,7 @@ defmodule Diffo.Provider.CharacteristicTest do @moduledoc false - use ExUnit.Case + use ExUnit.Case, async: true alias Diffo.Test.Patch alias Diffo.Type.Value diff --git a/test/provider/entity_ref_test.exs b/test/provider/entity_ref_test.exs index 4813df3..aafacff 100644 --- a/test/provider/entity_ref_test.exs +++ b/test/provider/entity_ref_test.exs @@ -4,7 +4,7 @@ defmodule Diffo.Provider.EntityRefTest do @moduledoc false - use ExUnit.Case + use ExUnit.Case, async: true use Outstand alias Diffo.Provider.Entity alias Diffo.Provider.EntityRef diff --git a/test/provider/entity_test.exs b/test/provider/entity_test.exs index 6a49db2..07d14bc 100644 --- a/test/provider/entity_test.exs +++ b/test/provider/entity_test.exs @@ -4,7 +4,7 @@ defmodule Diffo.Provider.EntityTest do @moduledoc false - use ExUnit.Case + use ExUnit.Case, async: true use Outstand setup do diff --git a/test/provider/event_test.exs b/test/provider/event_test.exs index b723dfb..f560585 100644 --- a/test/provider/event_test.exs +++ b/test/provider/event_test.exs @@ -4,7 +4,7 @@ defmodule Diffo.Provider.EventTest do @moduledoc false - use ExUnit.Case + use ExUnit.Case, async: true setup do AshNeo4j.Sandbox.checkout() diff --git a/test/provider/external_identifier_test.exs b/test/provider/external_identifier_test.exs index c1d0c63..bb7487c 100644 --- a/test/provider/external_identifier_test.exs +++ b/test/provider/external_identifier_test.exs @@ -4,7 +4,7 @@ defmodule Diffo.Provider.ExternalIdentifierTest do @moduledoc false - use ExUnit.Case + use ExUnit.Case, async: true alias Diffo.Provider.ExternalIdentifier alias Diffo.Provider.Party diff --git a/test/provider/feature_test.exs b/test/provider/feature_test.exs index 73755e4..ad70b66 100644 --- a/test/provider/feature_test.exs +++ b/test/provider/feature_test.exs @@ -4,7 +4,7 @@ defmodule Diffo.Provider.FeatureTest do @moduledoc false - use ExUnit.Case + use ExUnit.Case, async: true alias Diffo.Type.Value diff --git a/test/provider/instance_test.exs b/test/provider/instance_test.exs index 2c4dc69..cbd1c75 100644 --- a/test/provider/instance_test.exs +++ b/test/provider/instance_test.exs @@ -4,7 +4,7 @@ defmodule Diffo.Provider.InstanceTest do @moduledoc false - use ExUnit.Case + use ExUnit.Case, async: true alias Diffo.Type.Value setup do diff --git a/test/provider/instance_util_test.exs b/test/provider/instance_util_test.exs index 53c93e9..b8344b8 100644 --- a/test/provider/instance_util_test.exs +++ b/test/provider/instance_util_test.exs @@ -4,7 +4,7 @@ defmodule Diffo.Provider.Instance.UtilTest do @moduledoc false - use ExUnit.Case + use ExUnit.Case, async: true alias Diffo.Provider.Instance.Util diff --git a/test/provider/note_test.exs b/test/provider/note_test.exs index d6a4b7f..4473475 100644 --- a/test/provider/note_test.exs +++ b/test/provider/note_test.exs @@ -4,7 +4,7 @@ defmodule Diffo.Provider.NoteTest do @moduledoc false - use ExUnit.Case + use ExUnit.Case, async: true alias Diffo.Provider.Party alias Diffo.Provider.Instance diff --git a/test/provider/party_ref_test.exs b/test/provider/party_ref_test.exs index cf28358..67f9bb6 100644 --- a/test/provider/party_ref_test.exs +++ b/test/provider/party_ref_test.exs @@ -4,7 +4,7 @@ defmodule Diffo.Provider.PartyRefTest do @moduledoc false - use ExUnit.Case + use ExUnit.Case, async: true use Outstand setup do diff --git a/test/provider/party_test.exs b/test/provider/party_test.exs index 1104ace..a0e7569 100644 --- a/test/provider/party_test.exs +++ b/test/provider/party_test.exs @@ -4,7 +4,7 @@ defmodule Diffo.Provider.PartyTest do @moduledoc false - use ExUnit.Case + use ExUnit.Case, async: true use Outstand setup do diff --git a/test/provider/place_ref_test.exs b/test/provider/place_ref_test.exs index c48d77a..47ea2fe 100644 --- a/test/provider/place_ref_test.exs +++ b/test/provider/place_ref_test.exs @@ -4,7 +4,7 @@ defmodule Diffo.Provider.PlaceRefTest do @moduledoc false - use ExUnit.Case + use ExUnit.Case, async: true use Outstand setup do diff --git a/test/provider/place_test.exs b/test/provider/place_test.exs index 0d09900..5980cfe 100644 --- a/test/provider/place_test.exs +++ b/test/provider/place_test.exs @@ -4,7 +4,7 @@ defmodule Diffo.Provider.PlaceTest do @moduledoc false - use ExUnit.Case + use ExUnit.Case, async: true use Outstand setup do @@ -284,7 +284,8 @@ defmodule Diffo.Provider.PlaceTest do type: :GeographicAddress }) - {:error, _error} = place |> Diffo.Provider.update_place(%{referred_type: :GeographicAddress}) + {:error, _error} = + place |> Diffo.Provider.update_place(%{referred_type: :GeographicAddress}) end test "update referred_type - failure - PlaceRef requires referred_type" do diff --git a/test/provider/process_status_test.exs b/test/provider/process_status_test.exs index 7f96180..5bf7ef1 100644 --- a/test/provider/process_status_test.exs +++ b/test/provider/process_status_test.exs @@ -4,7 +4,7 @@ defmodule Diffo.Provider.ProcessStatusTest do @moduledoc false - use ExUnit.Case + use ExUnit.Case, async: true setup do AshNeo4j.Sandbox.checkout() diff --git a/test/provider/reference_test.exs b/test/provider/reference_test.exs index 2017b85..578b224 100644 --- a/test/provider/reference_test.exs +++ b/test/provider/reference_test.exs @@ -4,7 +4,7 @@ defmodule Diffo.Provider.ReferenceTest do @moduledoc false - use ExUnit.Case + use ExUnit.Case, async: true alias Diffo.Provider.Reference diff --git a/test/provider/relationship_test.exs b/test/provider/relationship_test.exs index 661f9d2..fbb4733 100644 --- a/test/provider/relationship_test.exs +++ b/test/provider/relationship_test.exs @@ -4,7 +4,7 @@ defmodule Diffo.Provider.RelationshipTest do @moduledoc false - use ExUnit.Case + use ExUnit.Case, async: true alias Diffo.Type.Value diff --git a/test/provider/specification_test.exs b/test/provider/specification_test.exs index cc97ebe..be5d808 100644 --- a/test/provider/specification_test.exs +++ b/test/provider/specification_test.exs @@ -4,7 +4,7 @@ defmodule Diffo.Provider.SpecificationTest do @moduledoc false - use ExUnit.Case + use ExUnit.Case, async: true setup do AshNeo4j.Sandbox.checkout() diff --git a/test/provider/versioning_test.exs b/test/provider/versioning_test.exs index a0c41d3..d8fc68b 100644 --- a/test/provider/versioning_test.exs +++ b/test/provider/versioning_test.exs @@ -4,7 +4,7 @@ defmodule Diffo.Provider.VersioningTest do @moduledoc false - use ExUnit.Case + use ExUnit.Case, async: true alias Diffo.Test.Servo alias Diffo.Test.Broadband @@ -81,8 +81,11 @@ defmodule Diffo.Provider.VersioningTest 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]) + 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 @@ -102,9 +105,10 @@ defmodule Diffo.Provider.VersioningTest 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] - }) + {:ok, migrated} = + Diffo.Provider.respecify_instance(instance, %{ + specified_by: BroadbandV2.specification()[:id] + }) assert migrated.specification.id == BroadbandV2.specification()[:id] end @@ -112,22 +116,30 @@ defmodule Diffo.Provider.VersioningTest do 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]) + {: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]) + {: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 @@ -137,11 +149,25 @@ defmodule Diffo.Provider.VersioningTest do {: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 + {:ok, _} = + Diffo.Provider.respecify_instance(instance_a, %{ + specified_by: BroadbandV2.specification()[:id] + }) + + {:ok, _} = + Diffo.Provider.respecify_instance(instance_b, %{ + specified_by: BroadbandV2.specification()[:id] + }) + + assert Diffo.Provider.find_instances_by_specification_id!(Broadband.specification()[:id]) == + [] + + assert length( + Diffo.Provider.find_instances_by_specification_id!( + BroadbandV2.specification()[:id] + ) + ) == 3 end end end diff --git a/test/support/resource/carrier.ex b/test/support/resource/carrier.ex index 1f8c31d..c99abf6 100644 --- a/test/support/resource/carrier.ex +++ b/test/support/resource/carrier.ex @@ -22,17 +22,10 @@ defmodule Diffo.Test.Carrier do 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 + actions do + create :build do + accept [:id, :href, :name, :abn, :trading_name] + change set_attribute(:type, :Organization) end end @@ -45,10 +38,17 @@ defmodule Diffo.Test.Carrier do expect [:id, :name, :type] end - actions do - create :build do - accept [:id, :href, :name, :abn, :trading_name] - change set_attribute(:type, :Organization) + 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 diff --git a/test/support/resource/exchange_building.ex b/test/support/resource/exchange_building.ex index d6c3edc..d12d96c 100644 --- a/test/support/resource/exchange_building.ex +++ b/test/support/resource/exchange_building.ex @@ -22,18 +22,10 @@ defmodule Diffo.Test.ExchangeBuilding do 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] + actions do + create :build do + accept [:id, :href, :name, :nli, :access_type] + change set_attribute(:type, :GeographicSite) end end @@ -47,10 +39,18 @@ defmodule Diffo.Test.ExchangeBuilding do expect [:id, :name, :type] end - actions do - create :build do - accept [:id, :href, :name, :nli, :access_type] - change set_attribute(:type, :GeographicSite) + 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 diff --git a/test/support/util.ex b/test/support/util.ex index 0b01056..f4deebf 100644 --- a/test/support/util.ex +++ b/test/support/util.ex @@ -4,7 +4,7 @@ defmodule Diffo.Test.Util do @moduledoc false - use ExUnit.Case + use ExUnit.Case, async: true import ExUnit.CaptureIO def assert_compile_time_warning(module, message, fun) when is_bitstring(message) do diff --git a/test/type/dynamic_test.exs b/test/type/dynamic_test.exs index 9e07a24..2778b52 100644 --- a/test/type/dynamic_test.exs +++ b/test/type/dynamic_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: MIT defmodule Diffo.Type.DynamicTest do - use ExUnit.Case + use ExUnit.Case, async: true use Outstand alias Diffo.Type.Dynamic diff --git a/test/type/primitive_test.exs b/test/type/primitive_test.exs index c1437b7..d3ea968 100644 --- a/test/type/primitive_test.exs +++ b/test/type/primitive_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: MIT defmodule Diffo.Type.PrimitiveTest do - use ExUnit.Case + use ExUnit.Case, async: true use Outstand alias Diffo.Type.Primitive diff --git a/test/type/unwrap_test.exs b/test/type/unwrap_test.exs index 9c0c9db..e89c5f7 100644 --- a/test/type/unwrap_test.exs +++ b/test/type/unwrap_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: MIT defmodule Diffo.UnwrapTest do - use ExUnit.Case + use ExUnit.Case, async: true alias Diffo.Type.Primitive alias Diffo.Type.Value diff --git a/test/type/value_test.exs b/test/type/value_test.exs index 6bdaf72..eee3408 100644 --- a/test/type/value_test.exs +++ b/test/type/value_test.exs @@ -3,7 +3,7 @@ # SPDX-License-Identifier: MIT defmodule Diffo.Type.ValueTest do - use ExUnit.Case + use ExUnit.Case, async: true use Outstand alias Diffo.Type.Value diff --git a/usage-rules.md b/usage-rules.md new file mode 100644 index 0000000..0afb07d --- /dev/null +++ b/usage-rules.md @@ -0,0 +1,181 @@ + + +# Rules for working with Diffo + +## What Diffo is + +Diffo is an Ash Framework layer that models [TM Forum](https://www.tmforum.org/) (TMF) Service +and Resource Management domains on top of a Neo4j graph database. It provides three base +fragments — `BaseInstance`, `BaseParty`, `BasePlace` — plus the `Diffo.Provider.Instance.Extension` +and `Diffo.Provider.Party.Extension` DSLs. Read these rules and the Ash/AshNeo4j usage rules +**before** writing any domain code. + +## The three kinds of domain resource + +| Kind | Base fragment | DSL extension | +|---|---|---| +| Instance (service or resource) | `Diffo.Provider.BaseInstance` | `Diffo.Provider.Instance.Extension` | +| Party (organisation, person, entity) | `Diffo.Provider.BaseParty` | `Diffo.Provider.Party.Extension` | +| Place (site, address, location) | `Diffo.Provider.BasePlace` | `Diffo.Provider.Party.Extension` | + +Do **not** use plain `Ash.Resource` + `AshNeo4j.DataLayer` directly for domain resources. +Always start from the appropriate base fragment: + +```elixir +defmodule MyApp.BroadbandService do + use Ash.Resource, fragments: [Diffo.Provider.BaseInstance], domain: MyApp.Domain + ... +end +``` + +## Instance Extension DSL + +Every resource using `BaseInstance` gains two top-level DSL sections: `structure do` and +`behaviour do`. + +### structure + +`specification do` — declares the TMF Specification for this Instance kind. The `id` is a +**stable UUID4 that must be the same in every environment** — generate it once and never +change it. A new major version requires a new module with a new `id`. + +```elixir +structure do + specification do + id "da9b207a-26c3-451d-8abd-0640c6349979" + name "DSL Access Service" + type :serviceSpecification + major_version 1 + description "An access network service connecting a subscriber premises to an NNI via DSL" + category "Network Service" + end +end +``` + +`characteristics do` — declares typed value slots. Each characteristic is backed by an +`Ash.TypedStruct`. Do **not** add plain Ash attributes for data that belongs in a characteristic. + +```elixir +characteristics do + characteristic :downstream_speed, MyApp.Speed + characteristic :access_technology, MyApp.AccessTechnology +end +``` + +`features do` — declares optional capabilities, each with an enabled/disabled default and +optionally its own typed characteristic payload: + +```elixir +features do + feature :voice, is_enabled?: false + feature :static_ip, is_enabled?: false do + characteristic :ip_address, MyApp.IpAddress + end +end +``` + +`parties do` — declares party roles. Use `party` for singular (at most one) and `parties` +for plural relationships: + +```elixir +parties do + party :provider, MyApp.RSP + parties :installer, MyApp.Engineer, constraints: [min: 1, max: 3] + party :owner, MyApp.Organization, reference: true + party :operator, MyApp.RSP, calculate: :derive_operator +end +``` + +- `reference: true` — no direct `PartyRef` edge is created; the party is reachable by graph + traversal. Do not add a `PartyRef` relationship manually when `reference: true` is set. +- `calculate:` — names an Ash calculation on this resource that produces the party struct at + build time. The calculation runs inside `build_before/1`; do not call it manually. + +`places do` — mirrors `parties do` in structure and options: + +```elixir +places do + place :installation_site, MyApp.GeographicSite + places :coverage_areas, MyApp.GeographicLocation, constraints: [min: 1] +end +``` + +### behaviour + +`behaviour do actions do create :name end end` — marks a named create action for build +wiring. This injects the `:specified_by`, `:features`, and `:characteristics` Ash action +arguments automatically. Do **not** declare these arguments in the action body. + +```elixir +behaviour do + actions do + create :build + end +end +``` + +## Generated functions on Instance resources + +Every resource with a complete `specification do` block gets these compile-time generated +functions: + +- `specification/0`, `characteristics/0`, `features/0`, `parties/0`, `places/0` +- `characteristic/1`, `feature/1`, `feature_characteristic/2`, `party/1`, `place/1` +- `build_before/1` — upserts the Specification node; creates Feature, Characteristic, and + Party nodes; sets action argument ids. Called automatically before every create action. +- `build_after/2` — relates the created TMF entities to the new instance node. Called + automatically after every create action. + +**Never call `build_before/1` or `build_after/2` manually** in action bodies or changesets. +They are wired to every create action via global `BuildBefore` and `BuildAfter` changes on +`BaseInstance`. + +## Instance versioning + +- **Minor/patch version bumps** — update `minor_version` or `patch_version` in `specification do`. + The existing Specification node is updated in place. No instance changes required. +- **Major version bump** — create a new module (e.g. `BroadbandServiceV2`) with a new `id` + and `major_version 2`. The original module and all its instances remain untouched. +- **Never change the `id`** of an existing specification. It is a stable cross-environment + identity; changing it orphans existing instances. + +## Party and Place resources + +Party and Place resources use `BaseParty`/`BasePlace` and the Party Extension DSL to declare +the Instance and Party roles they participate in: + +```elixir +defmodule MyApp.RSP do + use Ash.Resource, fragments: [Diffo.Provider.BaseParty], domain: MyApp.Domain + + instances do + role :provider, MyApp.BroadbandService + role :provider, MyApp.VoiceService + end + + parties do + role :employer, MyApp.Organization + end +end +``` + +Role names are domain nouns from the party's perspective — timeless, `camelCase` when +multi-word (e.g. `:dataCentre`, not `:data_centre`). + +## Common mistakes + +- **Do not add raw Ash attributes for TMF-modelled data** — use `characteristics`, `features`, + `parties`, and `places` in the DSL instead. +- **Do not declare `:specified_by`, `:features`, or `:characteristics` Ash action arguments** + — the `behaviour do` block injects them automatically. +- **Do not call `build_before/1` / `build_after/2` yourself** — they run automatically. +- **Do not create a separate Specification resource manually** — the Specification node is + managed entirely by the `build_before/1` generated function. +- **Do not use `party/1` in place of `parties/3`** (and vice versa) — `party` declares a + singular role; `parties` declares a plural role. Mismatching causes compile-time errors. +- **Do not set a `referred_type` without also setting `type: :PartyRef`** — TMF requires + both fields when using a party reference.