From 361f2070fe61da783033840e83ac6b5b82d8b12d Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Wed, 29 Apr 2026 12:11:31 +0930 Subject: [PATCH 1/2] added provider instance specification field minor_version, patch_version, tmf_version --- .../provider/components/instance/extension.ex | 12 +++++++++++ .../persisters/persist_specification.ex | 3 +++ .../instance/extension/specification.ex | 6 ++++-- .../provider/components/specification.ex | 2 +- test/instance_extension/party_test.exs | 2 +- test/instance_extension/place_test.exs | 4 ++-- .../instance_extension/specification_test.exs | 21 +++++++++++++++++++ test/support/resource/shelf.ex | 4 ++++ 8 files changed, 48 insertions(+), 6 deletions(-) diff --git a/lib/diffo/provider/components/instance/extension.ex b/lib/diffo/provider/components/instance/extension.ex index 1376116..0c8fd74 100644 --- a/lib/diffo/provider/components/instance/extension.ex +++ b/lib/diffo/provider/components/instance/extension.ex @@ -79,6 +79,18 @@ defmodule Diffo.Provider.Instance.Extension do doc: "The major_version of the specification.", default: 1 ], + minor_version: [ + type: :integer, + doc: "The minor_version of the specification." + ], + patch_version: [ + type: :integer, + doc: "The patch_version of the specification." + ], + tmf_version: [ + type: :integer, + doc: "The TMF API version of the specification, e.g. 4." + ], description: [ type: :string, doc: "A generic description of the specified service or resource." diff --git a/lib/diffo/provider/components/instance/extension/persisters/persist_specification.ex b/lib/diffo/provider/components/instance/extension/persisters/persist_specification.ex index 4cfd5f9..b5f6a1b 100644 --- a/lib/diffo/provider/components/instance/extension/persisters/persist_specification.ex +++ b/lib/diffo/provider/components/instance/extension/persisters/persist_specification.ex @@ -14,6 +14,9 @@ defmodule Diffo.Provider.Instance.Extension.Persisters.PersistSpecification do name: Transformer.get_option(dsl_state, [:structure, :specification], :name), type: Transformer.get_option(dsl_state, [:structure, :specification], :type, :serviceSpecification), major_version: Transformer.get_option(dsl_state, [:structure, :specification], :major_version, 1), + minor_version: Transformer.get_option(dsl_state, [:structure, :specification], :minor_version), + patch_version: Transformer.get_option(dsl_state, [:structure, :specification], :patch_version), + tmf_version: Transformer.get_option(dsl_state, [:structure, :specification], :tmf_version), description: Transformer.get_option(dsl_state, [:structure, :specification], :description), category: Transformer.get_option(dsl_state, [:structure, :specification], :category) ] diff --git a/lib/diffo/provider/components/instance/extension/specification.ex b/lib/diffo/provider/components/instance/extension/specification.ex index 77dec6c..234fd14 100644 --- a/lib/diffo/provider/components/instance/extension/specification.ex +++ b/lib/diffo/provider/components/instance/extension/specification.ex @@ -12,7 +12,7 @@ defmodule Diffo.Provider.Instance.Specification do @doc """ Struct for a Specification """ - defstruct [:id, :name, :type, :major_version, :description, :category] + defstruct [:id, :name, :type, :major_version, :minor_version, :patch_version, :tmf_version, :description, :category] @doc """ Sets the specified_by argument in the changeset, ensuring the Extended Instance's specification exists @@ -21,7 +21,9 @@ defmodule Diffo.Provider.Instance.Specification do when is_struct(changeset, Ash.Changeset) and is_list(options) do specification = struct(__MODULE__, options) - case Provider.create_specification(Map.from_struct(specification)) do + attrs = specification |> Map.from_struct() |> Map.reject(fn {_, v} -> is_nil(v) end) + + case Provider.create_specification(attrs) do {:ok, _} -> Ash.Changeset.force_set_argument(changeset, :specified_by, specification.id) diff --git a/lib/diffo/provider/components/specification.ex b/lib/diffo/provider/components/specification.ex index 09d60e8..5197dee 100644 --- a/lib/diffo/provider/components/specification.ex +++ b/lib/diffo/provider/components/specification.ex @@ -38,7 +38,7 @@ defmodule Diffo.Provider.Specification do create :create do description "creates a major version of a named serviceSpecification or resourceSpecification" - accept [:id, :type, :name, :major_version, :description, :category] + accept [:id, :type, :name, :major_version, :minor_version, :patch_version, :tmf_version, :description, :category] change load [:version, :href, :instance_type] upsert? true upsert_identity :unique_major_version_per_name diff --git a/test/instance_extension/party_test.exs b/test/instance_extension/party_test.exs index 96482dc..51fa04c 100644 --- a/test/instance_extension/party_test.exs +++ b/test/instance_extension/party_test.exs @@ -10,7 +10,7 @@ defmodule Diffo.InstanceExtension.PartyTest do alias Diffo.Provider.Party.Extension.Info, as: PartyInfo alias Diffo.Test.Organization alias Diffo.Test.Person - alias Diffo.Test.Carrier + alias Diffo.Test.Shelf alias Diffo.Test.Nbn alias Diffo.Test.Servo diff --git a/test/instance_extension/place_test.exs b/test/instance_extension/place_test.exs index b6f7fe7..a304cba 100644 --- a/test/instance_extension/place_test.exs +++ b/test/instance_extension/place_test.exs @@ -10,7 +10,7 @@ defmodule Diffo.InstanceExtension.PlaceTest do alias Diffo.Provider.Place.Extension.Info, as: PlaceInfo alias Diffo.Test.Organization alias Diffo.Test.GeographicSite - alias Diffo.Test.ExchangeBuilding + alias Diffo.Test.Shelf alias Diffo.Test.Nbn @@ -108,7 +108,7 @@ defmodule Diffo.InstanceExtension.PlaceTest do end test "domain-specific attributes are readable after creation" do - {:ok, building} = Nbn.create_exchange_building(%{ + {:ok, _building} = Nbn.create_exchange_building(%{ id: "EX-MEL-002", name: "South Yarra Exchange", nli: "MEXMELB0002", diff --git a/test/instance_extension/specification_test.exs b/test/instance_extension/specification_test.exs index 2e0a0db..4a8d283 100644 --- a/test/instance_extension/specification_test.exs +++ b/test/instance_extension/specification_test.exs @@ -34,5 +34,26 @@ defmodule Diffo.InstanceExtension.SpecificationTest do {:ok, specification} = Diffo.Provider.get_specification_by_id(spec_id) assert specification.description == description end + + test "minor_version declared in specification DSL roundtrips to the persisted specification" do + Servo.build_shelf(%{name: "s"}) + + {:ok, specification} = Diffo.Provider.get_specification_by_id(Shelf.specification()[:id]) + assert specification.minor_version == Shelf.specification()[:minor_version] + end + + test "patch_version declared in specification DSL roundtrips to the persisted specification" do + Servo.build_shelf(%{name: "s"}) + + {:ok, specification} = Diffo.Provider.get_specification_by_id(Shelf.specification()[:id]) + assert specification.patch_version == Shelf.specification()[:patch_version] + end + + test "tmf_version declared in specification DSL roundtrips to the persisted specification" do + Servo.build_shelf(%{name: "s"}) + + {:ok, specification} = Diffo.Provider.get_specification_by_id(Shelf.specification()[:id]) + assert specification.tmf_version == Shelf.specification()[:tmf_version] + end end end diff --git a/test/support/resource/shelf.ex b/test/support/resource/shelf.ex index 898f10e..b2f87b9 100644 --- a/test/support/resource/shelf.ex +++ b/test/support/resource/shelf.ex @@ -34,6 +34,10 @@ defmodule Diffo.Test.Shelf do id "ef016d85-9dbd-429c-84da-1df56cc7dda5" name "shelf" type :resourceSpecification + major_version 1 + minor_version 2 + patch_version 3 + tmf_version 4 description "A Shelf Resource Instance which contain cards" category "Network Resource" end From 7e039ddcaa971e841b6fb83a0b838c8dc424f9bc Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Wed, 29 Apr 2026 12:55:49 +0930 Subject: [PATCH 2/2] =?UTF-8?q?provider=20instance=20specification=20DSL?= =?UTF-8?q?=20=E2=80=94=20minor,=20patch=20and=20tmf=20versions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add minor_version, patch_version and tmf_version to the specification do DSL section. Nil values are stripped before create_specification so unset fields fall back to Specification resource defaults. --- .../verifiers/verify_specification.ex | 73 +++++++++++--- test/instance_extension/verifier_test.exs | 99 +++++++++++++++++++ test/type/value_test.exs | 2 - 3 files changed, 159 insertions(+), 15 deletions(-) diff --git a/lib/diffo/provider/components/instance/extension/verifiers/verify_specification.ex b/lib/diffo/provider/components/instance/extension/verifiers/verify_specification.ex index 2368126..c5d6621 100644 --- a/lib/diffo/provider/components/instance/extension/verifiers/verify_specification.ex +++ b/lib/diffo/provider/components/instance/extension/verifiers/verify_specification.ex @@ -3,33 +3,80 @@ # SPDX-License-Identifier: MIT defmodule Diffo.Provider.Instance.Extension.Verifiers.VerifySpecification do - @moduledoc "Verifies that the specification id is a valid UUID4" + @moduledoc "Verifies that the specification DSL values satisfy the Specification resource's attribute constraints" use Spark.Dsl.Verifier alias Spark.Dsl.Verifier alias Spark.Error.DslError + # Fields validated against Specification attribute constraints (id handled separately) + @spec_fields [:name, :type, :major_version, :minor_version, :patch_version, :tmf_version, :description, :category] + @impl true def verify(dsl_state) do resource = Verifier.get_persisted(dsl_state, :module) + + errors = check_id(dsl_state, resource) ++ check_attributes(dsl_state, resource) + + case errors do + [] -> :ok + errors -> {:error, errors} + end + end + + defp check_id(dsl_state, resource) do spec_id = Verifier.get_option(dsl_state, [:structure, :specification], :id) - errors = - if spec_id && !Diffo.Uuid.uuid4?(spec_id) do - [ - DslError.exception( - module: resource, - path: [:structure, :specification, :id], - message: "specification: id must be a valid UUID4" - ) - ] + if spec_id && !Diffo.Uuid.uuid4?(spec_id) do + [DslError.exception( + module: resource, + path: [:structure, :specification, :id], + message: "specification: id must be a valid UUID4" + )] + else + [] + end + end + + defp check_attributes(dsl_state, resource) do + spec_attrs = + Ash.Resource.Info.attributes(Diffo.Provider.Specification) + |> Map.new(&{&1.name, &1}) + + Enum.flat_map(@spec_fields, fn field -> + value = Verifier.get_option(dsl_state, [:structure, :specification], field) + attr = Map.get(spec_attrs, field) + + if not is_nil(value) && not is_nil(attr) do + case Ash.Type.apply_constraints(attr.type, value, attr.constraints) do + {:ok, _} -> + [] + + {:error, errors} -> + [DslError.exception( + module: resource, + path: [:structure, :specification, field], + message: "specification: #{field} - #{format_errors(errors)}" + )] + end else [] end + end) + end - case errors do - [] -> :ok - errors -> {:error, errors} + defp format_errors(errors) when is_list(errors) do + if Keyword.keyword?(errors) do + format_error(errors) + else + errors |> Enum.map(&format_error/1) |> Enum.join(", ") end end + + defp format_error(kwlist) do + {message, bindings} = Keyword.pop(kwlist, :message, "invalid value") + Enum.reduce(bindings, message, fn {key, val}, msg -> + String.replace(msg, "%{#{key}}", to_string(val)) + end) + end end diff --git a/test/instance_extension/verifier_test.exs b/test/instance_extension/verifier_test.exs index d679b70..c9d197e 100644 --- a/test/instance_extension/verifier_test.exs +++ b/test/instance_extension/verifier_test.exs @@ -31,6 +31,105 @@ defmodule Diffo.InstanceExtension.VerifierTest do end ) end + + test "name not matching camelCase pattern warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "specification: name", + fn -> + defmodule InvalidSpecName do + alias Diffo.Provider.BaseInstance + use Ash.Resource, fragments: [BaseInstance], domain: Diffo.Test.Servo + + resource do + description "resource with non-camelCase specification name" + end + + structure do + specification do + id "cd29956f-6c68-44cc-bf54-705eb8d2f754" + name "not camel case" + end + end + end + end + ) + end + + test "type not in allowed set warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "specification: type", + fn -> + defmodule InvalidSpecType do + alias Diffo.Provider.BaseInstance + use Ash.Resource, fragments: [BaseInstance], domain: Diffo.Test.Servo + + resource do + description "resource with invalid specification type" + end + + structure do + specification do + id "cd29956f-6c68-44cc-bf54-705eb8d2f754" + name "invalid" + type :badType + end + end + end + end + ) + end + + test "negative major_version warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "specification: major_version", + fn -> + defmodule InvalidSpecMajorVersion do + alias Diffo.Provider.BaseInstance + use Ash.Resource, fragments: [BaseInstance], domain: Diffo.Test.Servo + + resource do + description "resource with negative major_version" + end + + structure do + specification do + id "cd29956f-6c68-44cc-bf54-705eb8d2f754" + name "invalid" + major_version -1 + end + end + end + end + ) + end + + test "tmf_version below minimum warns DslError on compilation" do + Util.assert_compile_time_warning( + Spark.Error.DslError, + "specification: tmf_version", + fn -> + defmodule InvalidSpecTmfVersion do + alias Diffo.Provider.BaseInstance + use Ash.Resource, fragments: [BaseInstance], domain: Diffo.Test.Servo + + resource do + description "resource with tmf_version below minimum" + end + + structure do + specification do + id "cd29956f-6c68-44cc-bf54-705eb8d2f754" + name "invalid" + tmf_version 0 + end + end + end + end + ) + end end describe "characteristics verifier" do diff --git a/test/type/value_test.exs b/test/type/value_test.exs index 3ed3c4d..6bdaf72 100644 --- a/test/type/value_test.exs +++ b/test/type/value_test.exs @@ -33,8 +33,6 @@ defmodule Diffo.Type.ValueTest do Ash.Type.cast_input(Value, value, Value.subtype_constraints()) end - @tag bugged: "raw Dynamic struct cast_input requires Value wrapper" - @tag :skip test "cast_input dynamic" do value = %Dynamic{type: Patch, value: %Patch{aEnd: 1, zEnd: 42}}