From f5c6b74bb1cfcd361a3724a1f6ed5866075e46f7 Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Thu, 11 Dec 2025 14:44:27 +1030 Subject: [PATCH] assigner unassign operation --- .../provider/assigner/assignable_value.ex | 2 +- lib/diffo/provider/assigner/assigner.ex | 76 +++++++++++++++++-- lib/diffo/provider/assigner/assignment.ex | 8 +- mix.lock | 4 +- test/assigner/assigner_test.exs | 42 ++++++++++ test/provider/characteristic_test.exs | 2 +- test/provider/external_identifier_test.exs | 2 - test/provider/feature_test.exs | 26 ++++++- test/provider/relationship_test.exs | 42 +++++++++- 9 files changed, 185 insertions(+), 19 deletions(-) diff --git a/lib/diffo/provider/assigner/assignable_value.ex b/lib/diffo/provider/assigner/assignable_value.ex index e049eb8..7ee059b 100644 --- a/lib/diffo/provider/assigner/assignable_value.ex +++ b/lib/diffo/provider/assigner/assignable_value.ex @@ -31,7 +31,7 @@ defmodule Diffo.Provider.AssignableValue do default: 1, constraints: [min: 0] - field :type, :string, description: "the type of the thing" + field :type, :atom, description: "the type of the assignable thing" field :algorithm, :atom, description: "the assignment algorithm", diff --git a/lib/diffo/provider/assigner/assigner.ex b/lib/diffo/provider/assigner/assigner.ex index ed4d415..f0b348c 100644 --- a/lib/diffo/provider/assigner/assigner.ex +++ b/lib/diffo/provider/assigner/assigner.ex @@ -45,7 +45,7 @@ defmodule Diffo.Provider.Assigner do end :unassign -> - unrelate_is_assigned(result, things, assignment.id, assignee_id) + unrelate_is_assigned(result, things, thing, assignment.id, assignee_id) end end end @@ -80,12 +80,57 @@ defmodule Diffo.Provider.Assigner do end end - defp unrelate_is_assigned(result, things, value, assignee_id) - when is_struct(result) and is_atom(things) and is_integer(value) and + defp unrelate_is_assigned(result, things, thing, value, assignee_id) + when is_struct(result) and is_atom(things) and is_atom(thing) and is_integer(value) and is_bitstring(assignee_id) do - # destroy characteristic - # destroy relationship - {:error, "not implemented"} + relationships = + Enum.filter(result.forward_relationships, fn %{ + type: type, + target_id: target_id, + characteristics: characteristics + } -> + type == :assignedTo and target_id == assignee_id and + Enum.any?(characteristics, fn %{name: name, value: v} -> + name == thing and v == value + end) + end) + + case length(relationships) do + 0 -> + {:error, "#{thing} #{value} is not assigned to assignee #{assignee_id}"} + + 1 -> + relationship = hd(relationships) + + characteristic = + Enum.find(relationship.characteristics, fn %{name: n} -> n == thing end) + + # unrelate the relationship characterisitic + relationship = Diffo.Provider.unrelate_relationship_characteristics!(relationship, %{ + characteristics: [characteristic.id] + }) + + # delete the relationship characteristic + Diffo.Provider.delete_characteristic(characteristic.id) + + # delete the relationship + case Diffo.Provider.delete_relationship(relationship.id) do + :ok -> + case increment_free(result, things) do + :ok -> + {:ok, result} + + {:error, error} -> + {:error, error} + end + + {:error, error} -> + {:error, error} + end + + _ -> + {:error, "multiple relationships found for #{thing} #{value} and assignee #{assignee_id}"} + end end defp assignments(instance, thing) when is_struct(instance) and is_atom(thing) do @@ -103,7 +148,8 @@ defmodule Diffo.Provider.Assigner do assignment = struct(Diffo.Provider.Assignment, %{ id: characteristic.value, - instance_id: target_id + type: thing, + assignee_id: target_id }) [assignment | acc] @@ -165,6 +211,22 @@ defmodule Diffo.Provider.Assigner do end end + defp increment_free(instance, things) when is_struct(instance) and is_atom(things) do + characteristic = + Enum.find(instance.characteristics, fn %{name: name} -> name == things end) + + {_free, assignable_value} = + Map.get_and_update(characteristic.value, :free, fn free -> {free + 1, free + 1} end) + + case Diffo.Provider.update_characteristic(characteristic, %{value: assignable_value}) do + {:ok, _characteristic} -> + :ok + + {:error, error} -> + {:error, error} + end + end + defp free(instance, thing, assignable_value) when is_struct(instance) and is_atom(thing) and is_struct(assignable_value, AssignableValue) do diff --git a/lib/diffo/provider/assigner/assignment.ex b/lib/diffo/provider/assigner/assignment.ex index 79370b5..108f8f5 100644 --- a/lib/diffo/provider/assigner/assignment.ex +++ b/lib/diffo/provider/assigner/assignment.ex @@ -18,14 +18,16 @@ defmodule Diffo.Provider.Assignment do typed_struct do field :id, :integer, constraints: [min: 0], - description: "the id of the partial resource" + description: "the id of the assigned thing" + + field :type, :atom, description: "the type of the assigned thing" field :assignee_id, :uuid, description: "the id of the assignee Ash resource" field :operation, :atom, description: "the operation the assignee is requesting", - default: :assign, - constraints: [one_of: [:assign, :unassign, :auto_assign]] + default: nil, + constraints: [one_of: [nil, :assign, :unassign, :auto_assign]] end defimpl String.Chars do diff --git a/mix.lock b/mix.lock index bbb3611..a387395 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,5 @@ %{ - "ash": {:hex, :ash, "3.10.0", "839d696ef8a4d1f5b980a469fb19ef1383f21ddfb0e602ef91fc9811b2be529a", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.11", [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.3.14 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, ">= 0.2.6 and < 1.0.0-0", [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", "04b722edb6f8674fbe6ee7833e7e7ca43c404635e748bc4d17a6a1dba288dfc7"}, + "ash": {:hex, :ash, "3.11.1", "9794620bffeb83d1803d92a64e7803f70b57372eb4addba5c12a24343cd04e1a", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 0.11", [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.3.14 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, ">= 0.2.6 and < 1.0.0-0", [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", "e0074302bb88d667635fcbfdacbf8a641c53973a3902d0e744f567a49ec808fc"}, "ash_jason": {:hex, :ash_jason, "3.0.4", "50478b7a654584c9fcdb39dfca15a20f832f9183c393eb6aed7c9755e645550f", [: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", "f6d33382fce7032d2f177663cf30c2d3eb887fcd1c5bb2c22bc65d72e0b95095"}, "ash_neo4j": {:hex, :ash_neo4j, "0.2.12", "cc662833f3d4eb10ff36b217a1693c6deff3a878a9aea3baaf66090d987bb54a", [:mix], [{:ash, ">= 3.6.2 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:boltx, ">= 0.0.6", [hex: :boltx, repo: "hexpm", optional: false]}], "hexpm", "fda719bfc002ce0840d62eabbf0502ab79dd1e249d852aa3eb0bee457bf96352"}, "ash_outstanding": {:hex, :ash_outstanding, "0.2.3", "dc8ec13028ea7bd1d74b46569b9db08f0d275d63700e2418d9e33fe4b21af2eb", [: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", "05e2718b59937d9f7e77b7bc90f70e8f28c3f328de7cabf3ea55ca04a1abed52"}, @@ -11,7 +11,7 @@ "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, "ecto": {:hex, :ecto, "3.13.5", "9d4a69700183f33bf97208294768e561f5c7f1ecf417e0fa1006e4a91713a834", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "df9efebf70cf94142739ba357499661ef5dbb559ef902b68ea1f3c1fabce36de"}, "ets": {:hex, :ets, "0.9.0", "79c6a6c205436780486f72d84230c6cba2f8a9920456750ddd1e47389107d5fd", [:mix], [], "hexpm", "2861fdfb04bcaeff370f1a5904eec864f0a56dcfebe5921ea9aadf2a481c822b"}, - "ex_doc": {:hex, :ex_doc, "0.39.1", "e19d356a1ba1e8f8cfc79ce1c3f83884b6abfcb79329d435d4bbb3e97ccc286e", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "8abf0ed3e3ca87c0847dfc4168ceab5bedfe881692f1b7c45f4a11b232806865"}, + "ex_doc": {:hex, :ex_doc, "0.39.3", "519c6bc7e84a2918b737aec7ef48b96aa4698342927d080437f61395d361dcee", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "0590955cf7ad3b625780ee1c1ea627c28a78948c6c0a9b0322bd976a079996e1"}, "finch": {:hex, :finch, "0.20.0", "5330aefb6b010f424dcbbc4615d914e9e3deae40095e73ab0c1bb0968933cadf", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.6.2 or ~> 1.7", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 1.1", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "2658131a74d051aabfcba936093c903b8e89da9a1b63e430bee62045fa9b2ee2"}, "glob_ex": {:hex, :glob_ex, "0.1.11", "cb50d3f1ef53f6ca04d6252c7fde09fd7a1cf63387714fe96f340a1349e62c93", [:mix], [], "hexpm", "342729363056e3145e61766b416769984c329e4378f1d558b63e341020525de4"}, "hpax": {:hex, :hpax, "1.0.3", "ed67ef51ad4df91e75cc6a1494f851850c0bd98ebc0be6e81b026e765ee535aa", [:mix], [], "hexpm", "8eab6e1cfa8d5918c2ce4ba43588e894af35dbd8e91e6e55c817bca5847df34a"}, diff --git a/test/assigner/assigner_test.exs b/test/assigner/assigner_test.exs index 34c56e7..981b3bb 100644 --- a/test/assigner/assigner_test.exs +++ b/test/assigner/assigner_test.exs @@ -166,5 +166,47 @@ defmodule Diffo.Test.AssignerTest do assert encoding == ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/card/#{card.id}",\"category\":\"Network Resource\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"},\"resourceRelationship\":[{\"type\":\"assignedTo\",\"resource\":{\"id\":\"#{assignee.id}\",\"href\":\"resourceInventoryManagement/v4/resource/shelf/#{assignee.id}\"},\"resourceRelationshipCharacteristic\":[{\"name\":\"port\",\"value\":5}]}],\"resourceCharacteristic\":[{\"name\":\"card\",\"value\":{\"family\":\"ISAM\",\"model\":\"EBLT48\",\"technology\":\"adsl2Plus\"}},{\"name\":\"ports\",\"value\":{\"first\":1,\"last\":48,\"free\":47,\"type\":\"ADSL2+\",\"algorithm\":\"lowest\"}}]}) end + + test "unassign an auto-assigned port from a resource" do + {:ok, assignee} = Domain.build_shelf() + + {:ok, card} = Domain.build_card(%{}) + + updates = [ + card: [family: :ISAM, model: "EBLT48", technology: :adsl2Plus], + ports: [first: 1, last: 48, free: 48, type: "ADSL2+"] + ] + + {:ok, card} = Domain.define_card(card, %{characteristic_value_updates: updates}) + + {:ok, card} = + Domain.assign_port(card, %{ + assignment: %Assignment{assignee_id: assignee.id, operation: :auto_assign} + }) + + Characteristics.check_values([ports: [free: 47]], card) + + assigned_port = + Enum.find(card.forward_relationships, fn rel -> rel.type == :assignedTo end) + |> Map.get(:characteristics) + |> Enum.find(fn char -> char.name == :port end) + |> Map.get(:value) + + {:ok, card} = + Domain.assign_port(card, %{ + assignment: %Assignment{ + id: assigned_port, + assignee_id: assignee.id, + operation: :unassign + } + }) + + Characteristics.check_values([ports: [free: 48]], card) + + encoding = Jason.encode!(card) |> Diffo.Util.summarise_dates() + + assert encoding == + ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/card/#{card.id}",\"category\":\"Network Resource\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"},\"resourceCharacteristic\":[{\"name\":\"card\",\"value\":{\"family\":\"ISAM\",\"model\":\"EBLT48\",\"technology\":\"adsl2Plus\"}},{\"name\":\"ports\",\"value\":{\"first\":1,\"last\":48,\"free\":48,\"type\":\"ADSL2+\",\"algorithm\":\"lowest\"}}]}) + end end end diff --git a/test/provider/characteristic_test.exs b/test/provider/characteristic_test.exs index 5c15345..424ebdf 100644 --- a/test/provider/characteristic_test.exs +++ b/test/provider/characteristic_test.exs @@ -126,7 +126,7 @@ defmodule Diffo.Provider.CharacteristicTest do end end - describe "Diffo.Provider updated Characteristics" do + describe "Diffo.Provider update Characteristics" do test "update characteristic value - success" do parent_specification = Diffo.Provider.create_specification!(%{name: "can", type: :resourceSpecification}) diff --git a/test/provider/external_identifier_test.exs b/test/provider/external_identifier_test.exs index f6f6d0d..5327804 100644 --- a/test/provider/external_identifier_test.exs +++ b/test/provider/external_identifier_test.exs @@ -477,8 +477,6 @@ defmodule Diffo.Provider.ExternalIdentifierTest do |> Diffo.Provider.update_external_identifier(%{owner_id: "T4_VIRTUAL"}) end - @tag bugged: true - # update bug? test "update instance_id - success" do specification = Diffo.Provider.create_specification!(%{name: "nbnAccess"}) instance1 = Diffo.Provider.create_instance!(%{specified_by: specification.id}) diff --git a/test/provider/feature_test.exs b/test/provider/feature_test.exs index 6806e64..74000e4 100644 --- a/test/provider/feature_test.exs +++ b/test/provider/feature_test.exs @@ -84,7 +84,7 @@ defmodule Diffo.Provider.FeatureTest do end end - describe "Diffo.Provider updated Features" do + describe "Diffo.Provider update Features" do test "update feature isEnabled - success" do feature = Diffo.Provider.create_feature!(%{ @@ -125,6 +125,29 @@ defmodule Diffo.Provider.FeatureTest do }) end + test "update feature remove characteristic - success" do + device_characteristic = + Diffo.Provider.create_characteristic!(%{ + name: :device, + value: :epic1000a, + type: :feature + }) + + feature = + Diffo.Provider.create_feature!(%{ + name: :management, + characteristics: [device_characteristic.id] + }) + + updated_feature = + feature + |> Diffo.Provider.unrelate_feature_characteristics!(%{ + characteristics: [device_characteristic.id] + }) + + assert updated_feature.characteristics == [] + end + test "update feature with duplicate characteristic - failure" do first_characteristic = Diffo.Provider.create_characteristic!(%{ @@ -267,7 +290,6 @@ defmodule Diffo.Provider.FeatureTest do :ok = Diffo.Provider.delete_feature(feature) {:error, _error} = Diffo.Provider.get_feature_by_id(feature.id) - Diffo.Provider.get_characteristic_by_id!(characteristic.id) end test "delete feature with related instance - failure, related instance" do diff --git a/test/provider/relationship_test.exs b/test/provider/relationship_test.exs index b4de649..6c63f37 100644 --- a/test/provider/relationship_test.exs +++ b/test/provider/relationship_test.exs @@ -339,7 +339,47 @@ defmodule Diffo.Provider.RelationshipTest do end end - describe "Diffo.Provider updated Relationships" do + describe "Diffo.Provider update Relationships" do + test "update relationship remove characteristics - success" do + specification = Diffo.Provider.create_specification!(%{name: "evc1"}) + + first_instance = + Diffo.Provider.create_instance!(%{specified_by: specification.id, name: "first"}) + + second_instance = + Diffo.Provider.create_instance!(%{specified_by: specification.id, name: "second"}) + + first_characteristic = + Diffo.Provider.create_characteristic!(%{ + name: :role, + value: "worker", + type: :relationship + }) + + second_characteristic = + Diffo.Provider.create_characteristic!(%{ + name: :type, + value: :evpl, + type: :relationship + }) + + relationship = + Diffo.Provider.create_relationship!(%{ + type: :uses, + source_id: first_instance.id, + target_id: second_instance.id, + characteristics: [first_characteristic.id, second_characteristic.id] + }) + + updated_relationship = + relationship + |> Diffo.Provider.unrelate_relationship_characteristics!(%{ + characteristics: [first_characteristic.id, second_characteristic.id] + }) + + assert updated_relationship.characteristics == [] + end + test "add alias - success" do parent_specification = Diffo.Provider.create_specification!(%{name: "can", type: :resourceSpecification})