diff --git a/config/config.exs b/config/config.exs index 053ae6b..ff4d1b4 100644 --- a/config/config.exs +++ b/config/config.exs @@ -37,5 +37,5 @@ config :spark, ] config :diffo, ash_domains: [Diffo.Provider] -config :diffo_example, ash_domains: [DiffoExample.Access] +config :diffo_example, ash_domains: [DiffoExample.Access, DiffoExample.Nbn] import_config "#{config_env()}.exs" diff --git a/lib/access/resources/characteristic_values/cable_value.ex b/lib/access/resources/characteristic_values/cable_value.ex index 1c72707..e5a315a 100644 --- a/lib/access/resources/characteristic_values/cable_value.ex +++ b/lib/access/resources/characteristic_values/cable_value.ex @@ -30,10 +30,4 @@ defmodule DiffoExample.Access.CableValue do field :technology, :atom, description: "the cable technology" end - - defimpl String.Chars do - def to_string(struct) do - inspect(struct) - end - end end diff --git a/lib/access/resources/characteristic_values/card_value.ex b/lib/access/resources/characteristic_values/card_value.ex index 7d59590..44441da 100644 --- a/lib/access/resources/characteristic_values/card_value.ex +++ b/lib/access/resources/characteristic_values/card_value.ex @@ -28,10 +28,4 @@ defmodule DiffoExample.Access.CardValue do field :technology, :atom, description: "the card technology" end - - defimpl String.Chars do - def to_string(struct) do - inspect(struct) - end - end end diff --git a/lib/access/resources/characteristic_values/float_unit.ex b/lib/access/resources/characteristic_values/float_unit.ex index 7e75101..ad05cdf 100644 --- a/lib/access/resources/characteristic_values/float_unit.ex +++ b/lib/access/resources/characteristic_values/float_unit.ex @@ -24,10 +24,4 @@ defmodule DiffoExample.Access.FloatUnit do field :unit, :atom, description: "the unit" end - - defimpl String.Chars do - def to_string(struct) do - inspect(struct) - end - end end diff --git a/lib/access/resources/characteristic_values/integer_unit.ex b/lib/access/resources/characteristic_values/integer_unit.ex index 4f9be8c..11d4c9b 100644 --- a/lib/access/resources/characteristic_values/integer_unit.ex +++ b/lib/access/resources/characteristic_values/integer_unit.ex @@ -24,10 +24,4 @@ defmodule DiffoExample.Access.IntegerUnit do field :unit, :atom, description: "the unit" end - - defimpl String.Chars do - def to_string(struct) do - inspect(struct) - end - end end diff --git a/lib/access/resources/characteristic_values/path_value.ex b/lib/access/resources/characteristic_values/path_value.ex index fba2f11..7ea2e0e 100644 --- a/lib/access/resources/characteristic_values/path_value.ex +++ b/lib/access/resources/characteristic_values/path_value.ex @@ -33,10 +33,4 @@ defmodule DiffoExample.Access.PathValue do field :technology, :atom, description: "the path technology" end - - defimpl String.Chars do - def to_string(struct) do - inspect(struct) - end - end end diff --git a/lib/access/resources/characteristic_values/shelf_value.ex b/lib/access/resources/characteristic_values/shelf_value.ex index e31da9f..80b1313 100644 --- a/lib/access/resources/characteristic_values/shelf_value.ex +++ b/lib/access/resources/characteristic_values/shelf_value.ex @@ -28,10 +28,4 @@ defmodule DiffoExample.Access.ShelfValue do field :technology, :atom, description: "the shelf technology" end - - defimpl String.Chars do - def to_string(struct) do - inspect(struct) - end - end end diff --git a/lib/access/services/characteristic_values/aggregate_interface.ex b/lib/access/services/characteristic_values/aggregate_interface.ex index b860c74..40d677c 100644 --- a/lib/access/services/characteristic_values/aggregate_interface.ex +++ b/lib/access/services/characteristic_values/aggregate_interface.ex @@ -13,6 +13,12 @@ defmodule DiffoExample.Access.AggregateInterface do jason do pick [:name, :physical_interface, :physical_layer, :link_layer, :svlan_id, :vpi] compact(true) + + rename physical_interface: "physicalInterface", + physical_layer: "physicalLayer", + link_layer: "linkLayer", + svlan_id: "svlanId", + vpi: "VPI" end outstanding do @@ -46,10 +52,4 @@ defmodule DiffoExample.Access.AggregateInterface do default: 0, description: "the aggregate interface vpi" end - - defimpl String.Chars do - def to_string(struct) do - inspect(struct) - end - end end diff --git a/lib/access/services/characteristic_values/bandwidth_profile.ex b/lib/access/services/characteristic_values/bandwidth_profile.ex index 5d49921..bd1c35b 100644 --- a/lib/access/services/characteristic_values/bandwidth_profile.ex +++ b/lib/access/services/characteristic_values/bandwidth_profile.ex @@ -33,10 +33,4 @@ defmodule DiffoExample.Access.BandwidthProfile do constraints: [one_of: [:kbps, :Mbps]], description: "the bandwidth profile units" end - - defimpl String.Chars do - def to_string(struct) do - inspect(struct) - end - end end diff --git a/lib/access/services/characteristic_values/circuit.ex b/lib/access/services/characteristic_values/circuit.ex index c56cacb..2aaddd4 100644 --- a/lib/access/services/characteristic_values/circuit.ex +++ b/lib/access/services/characteristic_values/circuit.ex @@ -15,6 +15,7 @@ defmodule DiffoExample.Access.Circuit do jason do pick [:circuit_id, :cvlan_id, :vci, :encapsulation, :bandwidth_profile] compact(true) + rename circuit_id: "circuitId", vci: "VCI", bandwidth_profile: "bandwidthProfile" end outstanding do @@ -41,13 +42,6 @@ defmodule DiffoExample.Access.Circuit do constraints: [one_of: [:PPPoA, :PPPoE, :IPoE]], description: "the circuit encapsulation" - field :bandwidth_profile, BandwidthProfile, - description: "the circuit bandwidth profile" - end - - defimpl String.Chars do - def to_string(struct) do - inspect(struct) - end + field :bandwidth_profile, BandwidthProfile, description: "the circuit bandwidth profile" end end diff --git a/lib/access/services/characteristic_values/constraints.ex b/lib/access/services/characteristic_values/constraints.ex index 092d6bc..720e45f 100644 --- a/lib/access/services/characteristic_values/constraints.ex +++ b/lib/access/services/characteristic_values/constraints.ex @@ -13,6 +13,7 @@ defmodule DiffoExample.Access.Constraints do jason do pick [:max_latency, :min_profile] compact(true) + rename max_latency: "maxLatency", min_profile: "minProfile" end outstanding do @@ -28,10 +29,4 @@ defmodule DiffoExample.Access.Constraints do constraints: [instance_of: BandwidthProfile], description: "the circuit bandwidth profile" end - - defimpl String.Chars do - def to_string(struct) do - inspect(struct) - end - end end diff --git a/lib/access/services/characteristic_values/dslam.ex b/lib/access/services/characteristic_values/dslam.ex index 5a2b92b..8042f43 100644 --- a/lib/access/services/characteristic_values/dslam.ex +++ b/lib/access/services/characteristic_values/dslam.ex @@ -36,10 +36,4 @@ defmodule DiffoExample.Access.Dslam do default: :eth, description: "the DSLAM technology" end - - defimpl String.Chars do - def to_string(struct) do - inspect(struct) - end - end end diff --git a/lib/access/services/characteristic_values/line.ex b/lib/access/services/characteristic_values/line.ex index 67e6a80..dbc5624 100644 --- a/lib/access/services/characteristic_values/line.ex +++ b/lib/access/services/characteristic_values/line.ex @@ -35,10 +35,4 @@ defmodule DiffoExample.Access.Line do field :profile, :string, description: "the line port profile" end - - defimpl String.Chars do - def to_string(struct) do - inspect(struct) - end - end end diff --git a/lib/access/util.ex b/lib/access/util.ex index 0b01f0a..bbb59bb 100644 --- a/lib/access/util.ex +++ b/lib/access/util.ex @@ -13,10 +13,11 @@ defmodule DiffoExample.Access.Util do alias Diffo.Provider.Assignment - def assignments(instance, type) do + @doc """ + Lists things that are assigned_to an Instance, as Assignments + """ + def assignments(instance, type) when is_struct(instance, Ash.Resource) and is_atom(type) do Enum.reduce(instance.reverse_relationships, [], fn reverse_relationship, acc -> - IO.inspect(reverse_relationship, label: :reverse_relationship) - case reverse_relationship.type do :assignedTo -> characteristic = diff --git a/lib/nbn/nbn.ex b/lib/nbn/nbn.ex new file mode 100644 index 0000000..3ae584e --- /dev/null +++ b/lib/nbn/nbn.ex @@ -0,0 +1,87 @@ +# SPDX-FileCopyrightText: 2025 diffo_example contributors +# +# SPDX-License-Identifier: MIT + +defmodule DiffoExample.Nbn do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + Nbn - example NBN domain + + Models NBN network resources including the Ethernet circuit (NbnEthernet) + and its constituent resources: UNI (dedicated), AVC (dedicated), NTD, + CVC (aggregates AVCs, terminates at NNI Group), NNI Group, and NNI. + """ + use Ash.Domain, + otp_app: :diffo + + alias DiffoExample.Nbn.NbnEthernet + alias DiffoExample.Nbn.Uni + alias DiffoExample.Nbn.Avc + alias DiffoExample.Nbn.Ntd + alias DiffoExample.Nbn.Cvc + alias DiffoExample.Nbn.NniGroup + alias DiffoExample.Nbn.Nni + + domain do + description "An example showing how TMF Resources for a fictional NBN domain can be extended from the Provider domain" + end + + resources do + resource NbnEthernet do + define :get_nbn_ethernet_by_id, action: :read, get_by: :id + define :build_nbn_ethernet, action: :build + define :define_nbn_ethernet, action: :define + define :relate_nbn_ethernet, action: :relate + define :mine_nbn_ethernet, action: :mine + end + + resource Uni do + define :get_uni_by_id, action: :read, get_by: :id + define :build_uni, action: :build + define :define_uni, action: :define + define :relate_uni, action: :relate + define :mine_uni, action: :mine + end + + resource Avc do + define :get_avc_by_id, action: :read, get_by: :id + define :build_avc, action: :build + define :define_avc, action: :define + define :relate_avc, action: :relate + define :mine_avc, action: :mine + end + + resource Ntd do + define :get_ntd_by_id, action: :read, get_by: :id + define :build_ntd, action: :build + define :define_ntd, action: :define + define :assign_port, action: :assign_port + define :relate_ntd, action: :relate + end + + resource Cvc do + define :get_cvc_by_id, action: :read, get_by: :id + define :build_cvc, action: :build + define :define_cvc, action: :define + define :assign_cvlan, action: :assign_cvlan + define :relate_cvc, action: :relate + define :mine_cvc, action: :mine + end + + resource NniGroup do + define :get_nni_group_by_id, action: :read, get_by: :id + define :build_nni_group, action: :build + define :define_nni_group, action: :define + define :assign_svlan, action: :assign_svlan + define :relate_nni_group, action: :relate + end + + resource Nni do + define :get_nni_by_id, action: :read, get_by: :id + define :build_nni, action: :build + define :define_nni, action: :define + define :relate_nni, action: :relate + end + end +end diff --git a/lib/nbn/resources/avc.ex b/lib/nbn/resources/avc.ex new file mode 100644 index 0000000..467d699 --- /dev/null +++ b/lib/nbn/resources/avc.ex @@ -0,0 +1,119 @@ +# SPDX-FileCopyrightText: 2025 diffo_example contributors +# +# SPDX-License-Identifier: MIT + +defmodule DiffoExample.Nbn.Avc do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + Avc - Access Virtual Circuit Resource Instance + + An AVC is the virtual circuit dedicated to an NBN Ethernet circuit, + carrying traffic between its related UNI and the CVC that aggregates it. + """ + + alias Diffo.Provider.BaseInstance + alias Diffo.Provider.Instance.Relationship + alias Diffo.Provider.Instance.Characteristic + alias Diffo.Provider.Instance.ActionHelper + + alias DiffoExample.Nbn + + use Ash.Resource, + fragments: [BaseInstance], + domain: Nbn + + resource do + description "An Ash Resource representing an Access Virtual Circuit (AVC)" + plural_name :Avcs + end + + specification do + id "b2c3d4e5-6f7a-4b8c-9d0e-1f2a3b4c5d6e" + name "avc" + type :resourceSpecification + description "An AVC Resource Instance dedicated to an NBN Ethernet circuit" + category "Network Resource" + end + + characteristics do + characteristic :avc, DiffoExample.Nbn.AvcValue + characteristic :cvc, DiffoExample.Nbn.CvcValue + end + + actions do + create :build do + description "creates a new AVC resource instance" + accept [:id, :which] + argument :specified_by, :uuid, public?: false + argument :relationships, {:array, :struct} + argument :features, {:array, :uuid}, public?: false + argument :characteristics, {:array, :uuid}, public?: false + argument :places, {:array, :struct} + argument :parties, {:array, :struct} + + change set_attribute(:name, &DiffoExample.Nbn.Avc.identifier/0) + + change set_attribute(:type, :resource) + + change before_action(fn changeset, _context -> ActionHelper.build_before(changeset) end) + + change after_action(fn changeset, result, _context -> + ActionHelper.build_after(changeset, result, Nbn, :get_avc_by_id) + end) + + change load [:href] + upsert? false + end + + update :define do + description "defines the AVC" + argument :characteristic_value_updates, {:array, :term} + + change after_action(fn changeset, result, _context -> + with {:ok, result} <- Characteristic.update_values(result, changeset), + {:ok, result} <- Nbn.get_avc_by_id(result.id), + do: {:ok, result} + end) + end + + update :relate do + description "relates the AVC with other instances" + argument :relationships, {:array, :struct} + + change after_action(fn changeset, result, _context -> + with {:ok, result} <- Relationship.relate_instance(result, changeset), + {:ok, result} <- Nbn.get_avc_by_id(result.id), + do: {:ok, result} + end) + end + + update :mine do + description "updates the AVC with data mined from related instances" + argument :characteristic_value_updates, {:array, :term} + + change before_action(fn changeset, context -> + DiffoExample.Nbn.Avc.mine_related(changeset, context) + end) + + change after_action(fn changeset, result, _context -> + with {:ok, result} <- Characteristic.update_values(result, changeset), + {:ok, result} <- Nbn.get_avc_by_id(result.id), + do: {:ok, result} + end) + end + end + + def identifier() do + DiffoExample.Nbn.Util.identifier("AVC") + end + + # mines related resource to characteristics + def mine_related(changeset, _context) when is_struct(changeset, Ash.Changeset) do + reverse_relationships = Ash.Changeset.get_attribute(changeset, :reverse_relationships) + + cvlan = {:cvlan, Diffo.Unwrap.unwrap(hd(hd(reverse_relationships).characteristics).value)} + + Ash.Changeset.force_set_argument(changeset, :characteristic_value_updates, avc: [cvlan]) + end +end diff --git a/lib/nbn/resources/characteristic_values/avc_value.ex b/lib/nbn/resources/characteristic_values/avc_value.ex new file mode 100644 index 0000000..e298968 --- /dev/null +++ b/lib/nbn/resources/characteristic_values/avc_value.ex @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: 2025 diffo_example contributors +# +# SPDX-License-Identifier: MIT + +defmodule DiffoExample.Nbn.AvcValue do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + AvcValue - AshTyped Struct for AVC Characteristic Value + """ + use Ash.TypedStruct, extensions: [AshJason.TypedStruct, AshOutstanding.TypedStruct] + + alias DiffoExample.Nbn.BandwidthProfile + + jason do + pick [:cvlan, :bandwidth_profile] + compact(true) + end + + outstanding do + expect [:cvlan, :bandwidth_profile] + end + + typed_struct do + field :cvlan, :integer, + constraints: [min: 0, max: 4000], + description: "the cvlan of the AVC, assigned by the related CVC" + + field :bandwidth_profile, BandwidthProfile, description: "the bandwidth profile of the AVC" + end +end diff --git a/lib/nbn/resources/characteristic_values/cvc_value.ex b/lib/nbn/resources/characteristic_values/cvc_value.ex new file mode 100644 index 0000000..c67f791 --- /dev/null +++ b/lib/nbn/resources/characteristic_values/cvc_value.ex @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: 2025 diffo_example contributors +# +# SPDX-License-Identifier: MIT + +defmodule DiffoExample.Nbn.CvcValue do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + CvcValue - AshTyped Struct for Connectivity Virtual Circuit Characteristic Value + """ + use Ash.TypedStruct, extensions: [AshJason.TypedStruct, AshOutstanding.TypedStruct] + + jason do + pick [:svlan, :bandwidth] + compact(true) + end + + outstanding do + expect [:svlan, :bandwidth] + end + + typed_struct do + field :svlan, :integer, + constraints: [min: 0, max: 4000], + description: "the svlan of the CVC, assigned by the related NNI Group" + + field :bandwidth, :integer, + constraints: [min: 0, max: 10000], + description: "total CVC bandwidth in Mbps" + end +end diff --git a/lib/nbn/resources/characteristic_values/nni_group_value.ex b/lib/nbn/resources/characteristic_values/nni_group_value.ex new file mode 100644 index 0000000..9b1b72c --- /dev/null +++ b/lib/nbn/resources/characteristic_values/nni_group_value.ex @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: 2025 diffo_example contributors +# +# SPDX-License-Identifier: MIT + +defmodule DiffoExample.Nbn.NniGroupValue do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + NniGroupValue - AshTyped Struct for NNI Group Characteristic Value + """ + use Ash.TypedStruct, extensions: [AshJason.TypedStruct, AshOutstanding.TypedStruct] + + jason do + pick [:name, :location] + compact(true) + end + + outstanding do + expect [:name, :location] + end + + typed_struct do + field :name, :string, description: "the NNI group name" + + field :location, :string, description: "the Point of Interconnect (PoI) location" + end +end diff --git a/lib/nbn/resources/characteristic_values/nni_value.ex b/lib/nbn/resources/characteristic_values/nni_value.ex new file mode 100644 index 0000000..a4d69b3 --- /dev/null +++ b/lib/nbn/resources/characteristic_values/nni_value.ex @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: 2025 diffo_example contributors +# +# SPDX-License-Identifier: MIT + +defmodule DiffoExample.Nbn.NniValue do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + NniValue - AshTyped Struct for NNI Characteristic Value + """ + use Ash.TypedStruct, extensions: [AshJason.TypedStruct, AshOutstanding.TypedStruct] + + jason do + pick [:port_id, :capacity, :technology] + compact(true) + rename port_id: "portId" + end + + outstanding do + expect [:port_id, :capacity] + end + + typed_struct do + field :port_id, :string, description: "the NNI port identifier" + + field :capacity, :integer, description: "the NNI port capacity in Gbps" + + field :technology, :atom, description: "the NNI technology (:Ethernet, :Fibre)" + end +end diff --git a/lib/nbn/resources/characteristic_values/ntd_value.ex b/lib/nbn/resources/characteristic_values/ntd_value.ex new file mode 100644 index 0000000..03f0be7 --- /dev/null +++ b/lib/nbn/resources/characteristic_values/ntd_value.ex @@ -0,0 +1,33 @@ +# SPDX-FileCopyrightText: 2025 diffo_example contributors +# +# SPDX-License-Identifier: MIT + +defmodule DiffoExample.Nbn.NtdValue do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + NtdValue - AshTyped Struct for NTD Characteristic Value + """ + use Ash.TypedStruct, extensions: [AshJason.TypedStruct, AshOutstanding.TypedStruct] + + alias DiffoExample.Nbn.Technology + + jason do + pick [:model, :serial_number, :technology] + compact(true) + end + + outstanding do + expect [:model, :serial_number] + end + + typed_struct do + field :model, :string, description: "the NTD device model" + + field :serial_number, :string, description: "the NTD serial number" + + field :technology, Technology, + description: "the access technology type", + default: Technology.default() + end +end diff --git a/lib/nbn/resources/characteristic_values/pri_value.ex b/lib/nbn/resources/characteristic_values/pri_value.ex new file mode 100644 index 0000000..a678aeb --- /dev/null +++ b/lib/nbn/resources/characteristic_values/pri_value.ex @@ -0,0 +1,38 @@ +# SPDX-FileCopyrightText: 2025 diffo_example contributors +# +# SPDX-License-Identifier: MIT + +defmodule DiffoExample.Nbn.PriValue do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + NbnEthernetValue - AshTyped Struct for NBN Ethernet Access Characteristic Value + """ + use Ash.TypedStruct, extensions: [AshJason.TypedStruct, AshOutstanding.TypedStruct] + + alias DiffoExample.Nbn.Technology + alias DiffoExample.Nbn.BandwidthProfile + alias DiffoExample.Nbn.Speeds + + jason do + pick [:avcid, :uniid, :technology, :bandwidth_profile, :speeds] + compact(true) + rename avcid: "AVCID", uniid: "UNIID", bandwidth_profile: "bandwidthProfile" + end + + outstanding do + expect [:avcid, :uniid, :technology, :bandwidth_profile, :speeds] + end + + typed_struct do + field :avcid, :string, description: "the avcid from the owned Avc Resource" + + field :uniid, :string, description: "the uniid from the owned Uni Resource" + + field :technology, Technology, description: "the technology type" + + field :bandwidth_profile, BandwidthProfile, description: "the bandwidth profile" + + field :speeds, Speeds, description: "the downstream and upstream speeds in Mbps" + end +end diff --git a/lib/nbn/resources/characteristic_values/uni_value.ex b/lib/nbn/resources/characteristic_values/uni_value.ex new file mode 100644 index 0000000..fcdd6a3 --- /dev/null +++ b/lib/nbn/resources/characteristic_values/uni_value.ex @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: 2025 diffo_example contributors +# +# SPDX-License-Identifier: MIT + +defmodule DiffoExample.Nbn.UniValue do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + UniValue - AshTyped Struct for UNI Characteristic Value + """ + use Ash.TypedStruct, extensions: [AshJason.TypedStruct, AshOutstanding.TypedStruct] + + alias DiffoExample.Nbn.Technology + + jason do + pick [:port, :encapsulation, :technology] + compact(true) + end + + outstanding do + expect [:port, :encapsulation, :technology] + end + + typed_struct do + field :port, :integer, description: "the port of the UNI, assigned by the related NTD" + + field :encapsulation, :string, description: "the encapsulation of the UNI" + + field :technology, Technology, description: "the access technology type" + end +end diff --git a/lib/nbn/resources/cvc.ex b/lib/nbn/resources/cvc.ex new file mode 100644 index 0000000..8e75dcf --- /dev/null +++ b/lib/nbn/resources/cvc.ex @@ -0,0 +1,134 @@ +# SPDX-FileCopyrightText: 2025 diffo_example contributors +# +# SPDX-License-Identifier: MIT + +defmodule DiffoExample.Nbn.Cvc do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + Cvc - Connectivity Virtual Circuit Resource Instance + + A CVC is the wholesale bandwidth product that supports AVC and terminates at an NNI Group. + The CVC assigns cvlan to AVC. + """ + + alias Diffo.Provider.BaseInstance + alias Diffo.Provider.Instance.Relationship + alias Diffo.Provider.Instance.Characteristic + alias Diffo.Provider.Instance.ActionHelper + alias Diffo.Provider.Assigner + alias Diffo.Provider.Assignment + + alias DiffoExample.Nbn + + use Ash.Resource, + fragments: [BaseInstance], + domain: Nbn + + resource do + description "An Ash Resource representing a Connectivity Virtual Circuit (CVC)" + plural_name :Cvcs + end + + specification do + id "d4e5f6a7-8b9c-4d0e-bf1a-3b4c5d6e7f8a" + name "cvc" + type :resourceSpecification + + description "A Connectivity Virtual Circuit Resource Instance that aggregates AVCs and terminates at an NNI Group" + + category "Network Resource" + end + + characteristics do + characteristic :cvc, DiffoExample.Nbn.CvcValue + characteristic :cvlans, Diffo.Provider.AssignableValue + end + + actions do + create :build do + description "creates a new CVC resource instance" + accept [:id, :which] + argument :specified_by, :uuid, public?: false + argument :relationships, {:array, :struct} + argument :features, {:array, :uuid}, public?: false + argument :characteristics, {:array, :uuid}, public?: false + argument :places, {:array, :struct} + argument :parties, {:array, :struct} + + change set_attribute(:name, &DiffoExample.Nbn.Cvc.identifier/0) + + change set_attribute(:type, :resource) + + change before_action(fn changeset, _context -> ActionHelper.build_before(changeset) end) + + change after_action(fn changeset, result, _context -> + ActionHelper.build_after(changeset, result, Nbn, :get_cvc_by_id) + end) + + change load [:href] + upsert? false + end + + update :define do + description "defines the CVC" + argument :characteristic_value_updates, {:array, :term} + + change after_action(fn changeset, result, _context -> + with {:ok, result} <- Characteristic.update_values(result, changeset), + {:ok, result} <- Nbn.get_cvc_by_id(result.id), + do: {:ok, result} + end) + end + + update :assign_cvlan do + description "assigns a C-VLAN ID from the CVC pool to an AVC" + argument :assignment, :struct, constraints: [instance_of: Assignment] + + change after_action(fn changeset, result, _context -> + with {:ok, result} <- Assigner.assign(result, changeset, :cvlans, :cvlan), + {:ok, result} <- Nbn.get_cvc_by_id(result.id), + do: {:ok, result} + end) + end + + update :relate do + description "relates the CVC with other instances (e.g. AVC aggregation, NNI Group termination)" + argument :relationships, {:array, :struct} + + change after_action(fn changeset, result, _context -> + with {:ok, result} <- Relationship.relate_instance(result, changeset), + {:ok, result} <- Nbn.get_cvc_by_id(result.id), + do: {:ok, result} + end) + end + + update :mine do + description "updates the CVC with data mined from related instances" + argument :characteristic_value_updates, {:array, :term} + + change before_action(fn changeset, context -> + DiffoExample.Nbn.Cvc.mine_related(changeset, context) + end) + + change after_action(fn changeset, result, _context -> + with {:ok, result} <- Characteristic.update_values(result, changeset), + {:ok, result} <- Nbn.get_cvc_by_id(result.id), + do: {:ok, result} + end) + end + end + + def identifier() do + DiffoExample.Nbn.Util.identifier("CVC") + end + + # mines related resource to characteristics + def mine_related(changeset, _context) when is_struct(changeset, Ash.Changeset) do + reverse_relationships = Ash.Changeset.get_attribute(changeset, :reverse_relationships) + + svlan = {:svlan, Diffo.Unwrap.unwrap(hd(hd(reverse_relationships).characteristics).value)} + + Ash.Changeset.force_set_argument(changeset, :characteristic_value_updates, cvc: [svlan]) + end +end diff --git a/lib/nbn/resources/nbn_ethernet.ex b/lib/nbn/resources/nbn_ethernet.ex new file mode 100644 index 0000000..d4594a5 --- /dev/null +++ b/lib/nbn/resources/nbn_ethernet.ex @@ -0,0 +1,162 @@ +# SPDX-FileCopyrightText: 2025 diffo_example contributors +# +# SPDX-License-Identifier: MIT + +defmodule DiffoExample.Nbn.NbnEthernet do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + NbnEthernet - NBN Ethernet access Resource Instance + + An NBN Ethernet access comprises of dedicated UNI and AVC resources. + """ + + alias Diffo.Provider.BaseInstance + alias Diffo.Provider.Instance.Relationship + alias Diffo.Provider.Instance.Characteristic + alias Diffo.Provider.Instance.ActionHelper + + alias DiffoExample.Nbn + alias DiffoExample.Nbn.Util + alias DiffoExample.Nbn.Speeds + + use Ash.Resource, + fragments: [BaseInstance], + domain: Nbn + + resource do + description "An Ash Resource representing an NBN Ethernet access" + plural_name :NbnEthernets + end + + specification do + id "f2a4c6e8-1b3d-4f5a-8c7e-9d0b2e4f6a8c" + name "nbnEthernet" + type :resourceSpecification + description "An NBN Ethernet access comprising a dedicated UNI and AVC" + category "Network Resource" + end + + characteristics do + characteristic :pri, DiffoExample.Nbn.PriValue + # values do + # value :uniid, DiffoExample.Nbn.Uni, :owns, :name + # value :avcid, DiffoExample.Nbn.Avc, :owns, :name + # end + end + + actions do + create :build do + description "creates a new NBN Ethernet access resource instance" + accept [:id, :which] + argument :specified_by, :uuid, public?: false + argument :relationships, {:array, :struct} + argument :features, {:array, :uuid}, public?: false + argument :characteristics, {:array, :uuid}, public?: false + argument :places, {:array, :struct} + argument :parties, {:array, :struct} + + change set_attribute(:name, &DiffoExample.Nbn.NbnEthernet.identifier/0) + + change set_attribute(:type, :resource) + + change before_action(fn changeset, _context -> ActionHelper.build_before(changeset) end) + + change after_action(fn changeset, result, _context -> + ActionHelper.build_after(changeset, result, Nbn, :get_nbn_ethernet_by_id) + end) + + change load [:href] + upsert? false + end + + update :define do + description "defines the NBN Ethernet access" + argument :characteristic_value_updates, {:array, :term} + + change after_action(fn changeset, result, _context -> + with {:ok, result} <- Characteristic.update_values(result, changeset), + {:ok, result} <- Nbn.get_nbn_ethernet_by_id(result.id), + do: {:ok, result} + end) + end + + update :relate do + description "relates the NBN Ethernet access with other instances (e.g. UNI)" + argument :relationships, {:array, :struct} + + change after_action(fn changeset, result, _context -> + with {:ok, result} <- Relationship.relate_instance(result, changeset), + {:ok, result} <- Nbn.get_nbn_ethernet_by_id(result.id), + do: {:ok, result} + end) + end + + update :mine do + description "updates the NBN Ethernet access with data mined from related instances" + argument :characteristic_value_updates, {:array, :term} + + change before_action(fn changeset, context -> + DiffoExample.Nbn.NbnEthernet.mine_related(changeset, context) + end) + + change after_action(fn changeset, result, _context -> + with {:ok, result} <- Characteristic.update_values(result, changeset), + {:ok, result} <- Nbn.get_nbn_ethernet_by_id(result.id), + do: {:ok, result} + end) + end + end + + def identifier() do + DiffoExample.Nbn.Util.identifier("PRI") + end + + # mines related resource to characteristics + def mine_related(changeset, _context) when is_struct(changeset, Ash.Changeset) do + forward_relationships = Ash.Changeset.get_attribute(changeset, :forward_relationships) + + pri_updates = + Enum.reduce(forward_relationships, [], fn forward_relationship, acc -> + {:ok, related} = Diffo.Provider.get_instance_by_id(forward_relationship.target_id) + related_name = {alias_to_id(forward_relationship.alias), related.name} + + case forward_relationship.alias do + :uni -> + # extract technology from uni characteristic + [ + {:technology, Util.extract(related.characteristics, :uni, :technology)} + | [related_name | acc] + ] + + :avc -> + # extract bandwidth_profile from avc characteristic + [ + {:bandwidth_profile, + Util.extract(related.characteristics, :avc, :bandwidth_profile)} + | [related_name | acc] + ] + + _ -> + [related_name | acc] + end + end) + + # calculate the speeds from the extracted technology and bandwidth_profile + speeds = + {:speeds, + Speeds.speeds( + Keyword.get(pri_updates, :bandwidth_profile), + Keyword.get(pri_updates, :technology) + )} + + Ash.Changeset.force_set_argument(changeset, :characteristic_value_updates, + pri: [speeds | pri_updates] + ) + end + + defp alias_to_id(alias) when is_atom(alias) do + (Atom.to_string(alias) <> "id") + |> String.to_atom() + end +end diff --git a/lib/nbn/resources/nni.ex b/lib/nbn/resources/nni.ex new file mode 100644 index 0000000..92785a5 --- /dev/null +++ b/lib/nbn/resources/nni.ex @@ -0,0 +1,95 @@ +# SPDX-FileCopyrightText: 2025 diffo_example contributors +# +# SPDX-License-Identifier: MIT + +defmodule DiffoExample.Nbn.Nni do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + Nni - Network-to-Network Interface Resource Instance + + An NNI is the physical handover port between the NBN network and the + Retail Service Provider (RSP). Multiple NNI resources are grouped within + an NNI Group resource. + """ + + alias Diffo.Provider.BaseInstance + alias Diffo.Provider.Instance.Relationship + alias Diffo.Provider.Instance.Characteristic + alias Diffo.Provider.Instance.ActionHelper + + alias DiffoExample.Nbn + + use Ash.Resource, + fragments: [BaseInstance], + domain: Nbn + + resource do + description "An Ash Resource representing a Network-to-Network Interface (NNI)" + plural_name :Nnis + end + + specification do + id "f6a7b8c9-0d1e-4f2a-9b3c-5d6e7f8a9b0c" + name "nni" + type :resourceSpecification + description "An NNI Resource Instance that is part of an NNI Group" + category "Network Resource" + end + + characteristics do + characteristic :nni, DiffoExample.Nbn.NniValue + end + + actions do + create :build do + description "creates a new NNI resource instance" + accept [:id, :which] + argument :specified_by, :uuid, public?: false + argument :relationships, {:array, :struct} + argument :features, {:array, :uuid}, public?: false + argument :characteristics, {:array, :uuid}, public?: false + argument :places, {:array, :struct} + argument :parties, {:array, :struct} + + change set_attribute(:type, :resource) + + change set_attribute(:name, &DiffoExample.Nbn.Nni.identifier/0) + + change before_action(fn changeset, _context -> ActionHelper.build_before(changeset) end) + + change after_action(fn changeset, result, _context -> + ActionHelper.build_after(changeset, result, Nbn, :get_nni_by_id) + end) + + change load [:href] + upsert? false + end + + update :define do + description "defines the NNI" + argument :characteristic_value_updates, {:array, :term} + + change after_action(fn changeset, result, _context -> + with {:ok, result} <- Characteristic.update_values(result, changeset), + {:ok, result} <- Nbn.get_nni_by_id(result.id), + do: {:ok, result} + end) + end + + update :relate do + description "relates the NNI with other instances (e.g. its parent NNI Group)" + argument :relationships, {:array, :struct} + + change after_action(fn changeset, result, _context -> + with {:ok, result} <- Relationship.relate_instance(result, changeset), + {:ok, result} <- Nbn.get_nni_by_id(result.id), + do: {:ok, result} + end) + end + + def identifier() do + DiffoExample.Nbn.Util.identifier("NNI") + end + end +end diff --git a/lib/nbn/resources/nni_group.ex b/lib/nbn/resources/nni_group.ex new file mode 100644 index 0000000..9629d5b --- /dev/null +++ b/lib/nbn/resources/nni_group.ex @@ -0,0 +1,103 @@ +# SPDX-FileCopyrightText: 2025 diffo_example contributors +# +# SPDX-License-Identifier: MIT + +defmodule DiffoExample.Nbn.NniGroup do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + NniGroup - NNI Group Resource Instance + + An NNI Group is the Point of Interconnect (PoI) grouping where a CVC + terminates. It comprises multiple NNI resources. + The NNI Group assigns svlan to CVC. + """ + + alias Diffo.Provider.BaseInstance + alias Diffo.Provider.Instance.Relationship + alias Diffo.Provider.Instance.Characteristic + alias Diffo.Provider.Instance.ActionHelper + alias Diffo.Provider.Assigner + alias Diffo.Provider.Assignment + + alias DiffoExample.Nbn + + use Ash.Resource, + fragments: [BaseInstance], + domain: Nbn + + resource do + description "An Ash Resource representing an NNI Group" + plural_name :NniGroups + end + + specification do + id "e5f6a7b8-9c0d-4e1f-8a2b-4c5d6e7f8a9b" + name "nniGroup" + type :resourceSpecification + description "An NNI Group Resource Instance comprising multiple NNI resources" + category "Network Resource" + end + + characteristics do + characteristic :nni_group, DiffoExample.Nbn.NniGroupValue + characteristic :svlans, Diffo.Provider.AssignableValue + end + + actions do + create :build do + description "creates a new NNI Group resource instance" + accept [:id, :name, :which] + argument :specified_by, :uuid, public?: false + argument :relationships, {:array, :struct} + argument :features, {:array, :uuid}, public?: false + argument :characteristics, {:array, :uuid}, public?: false + argument :places, {:array, :struct} + argument :parties, {:array, :struct} + + change set_attribute(:type, :resource) + + change before_action(fn changeset, _context -> ActionHelper.build_before(changeset) end) + + change after_action(fn changeset, result, _context -> + ActionHelper.build_after(changeset, result, Nbn, :get_nni_group_by_id) + end) + + change load [:href] + upsert? false + end + + update :define do + description "defines the NNI Group" + argument :characteristic_value_updates, {:array, :term} + + change after_action(fn changeset, result, _context -> + with {:ok, result} <- Characteristic.update_values(result, changeset), + {:ok, result} <- Nbn.get_nni_group_by_id(result.id), + do: {:ok, result} + end) + end + + update :assign_svlan do + description "assigns an S-VLAN ID from the NNI Group pool to a CVC" + argument :assignment, :struct, constraints: [instance_of: Assignment] + + change after_action(fn changeset, result, _context -> + with {:ok, result} <- Assigner.assign(result, changeset, :svlans, :svlan), + {:ok, result} <- Nbn.get_nni_group_by_id(result.id), + do: {:ok, result} + end) + end + + update :relate do + description "relates the NNI Group with other instances (e.g. NNI resources it comprises)" + argument :relationships, {:array, :struct} + + change after_action(fn changeset, result, _context -> + with {:ok, result} <- Relationship.relate_instance(result, changeset), + {:ok, result} <- Nbn.get_nni_group_by_id(result.id), + do: {:ok, result} + end) + end + end +end diff --git a/lib/nbn/resources/ntd.ex b/lib/nbn/resources/ntd.ex new file mode 100644 index 0000000..b9fa3ea --- /dev/null +++ b/lib/nbn/resources/ntd.ex @@ -0,0 +1,108 @@ +# SPDX-FileCopyrightText: 2025 diffo_example contributors +# +# SPDX-License-Identifier: MIT + +defmodule DiffoExample.Nbn.Ntd do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + Ntd - Network Termination Device Resource Instance + + An NTD is the device installed at the customer premises that connects + the premises to the NBN network. The NTD can assign ports to UNI. + """ + + alias Diffo.Provider.BaseInstance + alias Diffo.Provider.Instance.Relationship + alias Diffo.Provider.Instance.Characteristic + alias Diffo.Provider.Instance.ActionHelper + alias Diffo.Provider.Assigner + alias Diffo.Provider.Assignment + + alias DiffoExample.Nbn + + use Ash.Resource, + fragments: [BaseInstance], + domain: Nbn + + resource do + description "An Ash Resource representing a Network Termination Device (NTD)" + plural_name :Ntds + end + + specification do + id "c3d4e5f6-7a8b-4c9d-ae0f-2a3b4c5d6e7f" + name "ntd" + type :resourceSpecification + description "An NTD Resource Instance related to a UNI" + category "Network Resource" + end + + characteristics do + characteristic :ntd, DiffoExample.Nbn.NtdValue + characteristic :ports, Diffo.Provider.AssignableValue + end + + actions do + create :build do + description "creates a new NTD resource instance" + accept [:id, :which] + argument :specified_by, :uuid, public?: false + argument :relationships, {:array, :struct} + argument :features, {:array, :uuid}, public?: false + argument :characteristics, {:array, :uuid}, public?: false + argument :places, {:array, :struct} + argument :parties, {:array, :struct} + + change set_attribute(:type, :resource) + + change set_attribute(:name, &DiffoExample.Nbn.Ntd.identifier/0) + + change before_action(fn changeset, _context -> ActionHelper.build_before(changeset) end) + + change after_action(fn changeset, result, _context -> + ActionHelper.build_after(changeset, result, Nbn, :get_ntd_by_id) + end) + + change load [:href] + upsert? false + end + + update :define do + description "defines the NTD" + argument :characteristic_value_updates, {:array, :term} + + change after_action(fn changeset, result, _context -> + with {:ok, result} <- Characteristic.update_values(result, changeset), + {:ok, result} <- Nbn.get_ntd_by_id(result.id), + do: {:ok, result} + end) + end + + update :assign_port do + description "assigns a port from the NTD pool to a UNI" + argument :assignment, :struct, constraints: [instance_of: Assignment] + + change after_action(fn changeset, result, _context -> + with {:ok, result} <- Assigner.assign(result, changeset, :ports, :port), + {:ok, result} <- Nbn.get_ntd_by_id(result.id), + do: {:ok, result} + end) + end + + update :relate do + description "relates the NTD with other instances (e.g. UNI)" + argument :relationships, {:array, :struct} + + change after_action(fn changeset, result, _context -> + with {:ok, result} <- Relationship.relate_instance(result, changeset), + {:ok, result} <- Nbn.get_ntd_by_id(result.id), + do: {:ok, result} + end) + end + end + + def identifier() do + DiffoExample.Nbn.Util.identifier("NTD") + end +end diff --git a/lib/nbn/resources/types/bandwidth_profile.ex b/lib/nbn/resources/types/bandwidth_profile.ex new file mode 100644 index 0000000..0f30dea --- /dev/null +++ b/lib/nbn/resources/types/bandwidth_profile.ex @@ -0,0 +1,31 @@ +# SPDX-FileCopyrightText: 2025 diffo_example contributors +# +# SPDX-License-Identifier: MIT +defmodule DiffoExample.Nbn.BandwidthProfile do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + BandwidthProfile type for NBN domain + """ + + use Ash.Type.Enum, + values: [ + :D12_U1, + :D25_U5, + :D25_U10, + :D50_U20, + :D100_U40, + :D250_U100, + :D500_U200, + :D1000_U400, + :wireless_plus, + :wireless_fast, + :wireless_superfast, + :home_fast, + :home_superfast, + :home_ultrafast, + :home_hyperfast + ] + + def default, do: :home_fast +end diff --git a/lib/nbn/resources/types/speeds.ex b/lib/nbn/resources/types/speeds.ex new file mode 100644 index 0000000..ce3a931 --- /dev/null +++ b/lib/nbn/resources/types/speeds.ex @@ -0,0 +1,188 @@ +# SPDX-FileCopyrightText: 2025 diffo_example contributors +# +# SPDX-License-Identifier: MIT +defmodule DiffoExample.Nbn.Speeds do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + Speeds type for NBN domain + """ + + require Ash.Type.NewType + alias DiffoExample.Nbn.Technology + + use Ash.Type.NewType, + subtype_of: :tuple, + constraints: [ + fields: [downstream: [type: :integer], upstream: [type: :integer]] + ] + + def speeds do + [ + {12, 1}, + {25, 5}, + {25, 10}, + {50, 20}, + {100, 40}, + {250, 100}, + {500, 200}, + {1000, 400}, + # :home_fast + {500, 50}, + # :home_superfast + {750, 50}, + # :home_ultrafast + {1000, 100}, + # :home_hyperfast + {2000, 100}, + {2000, 200}, + # :wireless_plus, :wireless_fast, :wireless_superfast + {100, 20}, + {250, 20}, + {400, 40} + ] + end + + @impl true + def cast_input(nil, _constraints), do: {:ok, nil} + + def cast_input(value, _constraints) when is_tuple(value) do + if value in speeds() do + {:ok, value} + else + {:error, "invalid downstream and upstream speed combination"} + end + end + + def cast_input({_value, _constraints}), do: {:error, "value must be a tuple"} + + defimpl Jason.Encoder do + def encode(speeds, _opts) do + Jason.OrderedObject.new( + downstream: speeds.downstream, + upstream: speeds.upstream, + units: "Mbps" + ) + |> Jason.encode!() + end + end + + @doc """ + Returns a tuple of maximum downstream and upstream speeds in Mbps + given the bandwidth_profile and technology, or :error + + ## Examples + iex> DiffoExample.Nbn.Speeds.speeds(:D12_U1, :Satellite) + {12, 1} + iex> DiffoExample.Nbn.Speeds.speeds(:home_fast, :FTTP) + {500, 50} + iex> DiffoExample.Nbn.Speeds.speeds(:home_hyperfast, :HFC) + {2000, 100} + iex> DiffoExample.Nbn.Speeds.speeds(:home_fast, :FixedWireless) + :error + """ + def speeds(:D12_U1, technology) when is_atom(technology) do + if technology in Technology.values() do + {12, 1} + else + :error + end + end + + def speeds(:D25_U5, technology) when is_atom(technology) do + if technology in Technology.values() do + {25, 5} + else + :error + end + end + + def speeds(:D25_U10, technology) when is_atom(technology) do + if technology in [:FTTP, :HFC, :FTTC] do + {25, 10} + else + :error + end + end + + def speeds(:D50_U20, technology) when is_atom(technology) do + if technology in [:FTTP, :HFC, :FTTC] do + {50, 20} + else + :error + end + end + + def speeds(bandwidth_profile, :FixedWireless) do + case bandwidth_profile do + :wireless_plus -> + {100, 20} + + :wireless_fast -> + {250, 20} + + :wireless_superfast -> + {400, 40} + + _ -> + :error + end + end + + def speeds(bandwidth_profile, :HFC) do + case bandwidth_profile do + :home_fast -> + {500, 50} + + :home_superfast -> + {750, 50} + + :home_ultrafast -> + {1000, 100} + + :home_hyperfast -> + {2000, 100} + + :U100_D40 -> + {100, 40} + + _ -> + :error + end + end + + def speeds(bandwidth_profile, :FTTP) do + case bandwidth_profile do + :home_fast -> + {500, 50} + + :home_superfast -> + {750, 50} + + :home_ultrafast -> + {1000, 100} + + :home_hyperfast -> + {2000, 200} + + :D100_U40 -> + {100, 40} + + :D250_U100 -> + {250, 100} + + :D500_200 -> + {500, 200} + + :D1000_400 -> + {1000, 400} + + _ -> + :error + end + end + + def speeds(_bandwidth, _technology) do + :error + end +end diff --git a/lib/nbn/resources/types/technology.ex b/lib/nbn/resources/types/technology.ex new file mode 100644 index 0000000..74f7b6a --- /dev/null +++ b/lib/nbn/resources/types/technology.ex @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2025 diffo_example contributors +# +# SPDX-License-Identifier: MIT +defmodule DiffoExample.Nbn.Technology do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + Technology type for NBN domain + """ + + use Ash.Type.Enum, + values: [:FTTP, :FTTN, :FTTB, :FTTC, :HFC, :FixedWireless, :Satellite] + + def default do + :FTTP + end +end diff --git a/lib/nbn/resources/uni.ex b/lib/nbn/resources/uni.ex new file mode 100644 index 0000000..0abd419 --- /dev/null +++ b/lib/nbn/resources/uni.ex @@ -0,0 +1,126 @@ +# SPDX-FileCopyrightText: 2025 diffo_example contributors +# +# SPDX-License-Identifier: MIT + +defmodule DiffoExample.Nbn.Uni do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + Uni - User Network Interface Resource Instance + + A UNI is the physical/logical interface at the customer premises. It is + related to an NTD resource and to its parent NBN Ethernet access. + It is related to an AVC resource, which is in turn aggregated by a CVC. + """ + + alias Diffo.Provider.BaseInstance + alias Diffo.Provider.Instance.Relationship + alias Diffo.Provider.Instance.Characteristic + alias Diffo.Provider.Instance.ActionHelper + + alias DiffoExample.Nbn + alias DiffoExample.Nbn.Util + + use Ash.Resource, + fragments: [BaseInstance], + domain: Nbn + + resource do + description "An Ash Resource representing a User Network Interface (UNI)" + plural_name :Unis + end + + specification do + id "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d" + name "uni" + type :resourceSpecification + description "A UNI Resource Instance related to an NTD and an NBN Ethernet access" + category "Network Resource" + end + + characteristics do + characteristic :uni, DiffoExample.Nbn.UniValue + end + + actions do + create :build do + description "creates a new UNI resource instance" + accept [:id, :which] + argument :specified_by, :uuid, public?: false + argument :relationships, {:array, :struct} + argument :features, {:array, :uuid}, public?: false + argument :characteristics, {:array, :uuid}, public?: false + argument :places, {:array, :struct} + argument :parties, {:array, :struct} + + change set_attribute(:type, :resource) + + change set_attribute(:name, &DiffoExample.Nbn.Uni.identifier/0) + + change before_action(fn changeset, _context -> ActionHelper.build_before(changeset) end) + + change after_action(fn changeset, result, _context -> + ActionHelper.build_after(changeset, result, Nbn, :get_uni_by_id) + end) + + change load [:href] + upsert? false + end + + update :define do + description "defines the UNI" + argument :characteristic_value_updates, {:array, :term} + + change after_action(fn changeset, result, _context -> + with {:ok, result} <- Characteristic.update_values(result, changeset), + {:ok, result} <- Nbn.get_uni_by_id(result.id), + do: {:ok, result} + end) + end + + update :relate do + description "relates the UNI with other instances (e.g. NTD, NBN Ethernet access)" + argument :relationships, {:array, :struct} + + change after_action(fn changeset, result, _context -> + with {:ok, result} <- Relationship.relate_instance(result, changeset), + {:ok, result} <- Nbn.get_uni_by_id(result.id), + do: {:ok, result} + end) + end + + update :mine do + description "updates the UNI with data mined from related instances" + argument :characteristic_value_updates, {:array, :term} + + change before_action(fn changeset, context -> + DiffoExample.Nbn.Uni.mine_related(changeset, context) + end) + + change after_action(fn changeset, result, _context -> + with {:ok, result} <- Characteristic.update_values(result, changeset), + {:ok, result} <- Nbn.get_uni_by_id(result.id), + do: {:ok, result} + end) + end + end + + def identifier() do + DiffoExample.Nbn.Util.identifier("UNI") + end + + # mines related resource to characteristics + def mine_related(changeset, _context) when is_struct(changeset, Ash.Changeset) do + reverse_relationships = Ash.Changeset.get_attribute(changeset, :reverse_relationships) + + ntd_relationship = hd(reverse_relationships) + + port = {:port, Diffo.Unwrap.unwrap(hd(ntd_relationship.characteristics).value)} + {:ok, ntd} = Diffo.Provider.get_instance_by_id(ntd_relationship.source_id) + technology = {:technology, Util.extract(ntd.characteristics, :ntd, :technology)} + + Ash.Changeset.force_set_argument(changeset, :characteristic_value_updates, + uni: [port, technology] + ) + end +end diff --git a/lib/nbn/util.ex b/lib/nbn/util.ex new file mode 100644 index 0000000..453bcb7 --- /dev/null +++ b/lib/nbn/util.ex @@ -0,0 +1,55 @@ +# SPDX-FileCopyrightText: 2025 diffo_example contributors +# +# SPDX-License-Identifier: MIT + +defmodule DiffoExample.Nbn.Util do + @moduledoc """ + Diffo - TMF Service and Resource Management with a difference + + Util - various utilities for NBN domain + """ + + @doc """ + Generates a new random NBN identifier with the prefix + + ## Examples + iex> identifier = DiffoExample.Nbn.Util.identifier("AVC") + iex> DiffoExample.Nbn.Util.identifier?(identifier) + true + + """ + def identifier(prefix) when is_binary(prefix) and byte_size(prefix) == 3 do + prefix <> + (:rand.uniform(000_999_999_999) + |> Integer.to_string() + |> String.pad_leading(12, "0")) + end + + @doc """ + Returns whether the identifier is a valid NBN identifier + + ## Examples + iex> DiffoExample.Nbn.Util.identifier?("AVC120123456789") + true + iex> DiffoExample.Nbn.Util.identifier?("avc120123456789") + false + """ + def identifier?(identifier) when is_binary(identifier) do + Regex.match?(~r/[A-Z]{3}\d{12}/, identifier) + end + + @doc """ + Extracts a field value from a named item value map in a list, each value map is unwrapped with Diffo.Unwrap protocol + + ## Examples + iex> DiffoExample.Nbn.Util.extract([%{name: :avc, value: %{cvlan: 1}}], :avc, :cvlan) + 1 + """ + def extract(items, name, field) when is_list(items) and is_atom(name) and is_atom(field) do + case Enum.find(items, &(&1.name == name)) do + nil -> nil + %{value: nil} -> nil + %{value: value} -> value |> Diffo.Unwrap.unwrap() |> Map.get(field) + end + end +end diff --git a/mix.exs b/mix.exs index 2230e0c..496ce1d 100644 --- a/mix.exs +++ b/mix.exs @@ -78,7 +78,7 @@ defmodule DiffoExample.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - # {:diffo, diffo_version("~> 0.1.5")}, + # {:diffo, diffo_version("~> 0.1.6")}, {:diffo, github: "diffo-dev/diffo", branch: "dev"}, {:igniter, "~> 0.6", only: [:dev, :test]}, {:ex_doc, "~> 0.37", only: [:dev, :test], runtime: false} diff --git a/mix.lock b/mix.lock index 56ac9a9..3a8d2e0 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,5 @@ %{ - "ash": {:hex, :ash, "3.24.2", "38beca133e0dcab07e3c8a7c26e573287ada26e8ba8d4c90ac692b52b34b0309", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 1.0", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.6.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.3", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "3fd2a99504c1f58290efc3382501369ee9070098784925bdd7df9dbea8611d32"}, + "ash": {:hex, :ash, "3.24.3", "f7280a43c5e64f769a450f3dd59ace6dcd73edcdd0de7599815b1b31f59292fb", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 1.0", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.6.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.3", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c1022f8c549632137cbc8956f07bb4981405297f5abe7a752b4dffac175c3381"}, "ash_jason": {:hex, :ash_jason, "3.1.0", "84a88dfe5e25a20d55cf2d2664885cd086fa45871e8777aedc3ad96a282e2a6f", [:mix], [{:ash, ">= 3.6.2 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:spark, ">= 2.1.21 and < 3.0.0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "71e6bbc421fb2cf7079f8804814145cca458116c839fc798f9606b806e07eb2b"}, "ash_neo4j": {:git, "https://github.com/diffo-dev/ash_neo4j.git", "044d9d123af30719a9f1f377e2c24b5cc8e21ea8", [branch: "dev"]}, "ash_outstanding": {:hex, :ash_outstanding, "0.2.4", "c72b91f1b8e4859fb033eddf66d0ba36cfd8af0c2a9748c7ef9e6ccfdb5d093d", [:mix], [{:ash, ">= 3.6.2 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:outstanding, "~> 0.2.4", [hex: :outstanding, repo: "hexpm", optional: false]}], "hexpm", "64ba8f582ce69c9050352c75f0895db186c7a56f35039dab34c8e1ab7516f9ce"}, diff --git a/test/access/characteristic_value_test.exs b/test/access/characteristic_value_test.exs index 8620106..5ac058e 100644 --- a/test/access/characteristic_value_test.exs +++ b/test/access/characteristic_value_test.exs @@ -47,12 +47,14 @@ defmodule DiffoExample.Access.CharacteristicValueTest do assert encoding == ~s({\"name\":\"dslam\",\"value\":{\"name\":\"#{@dslam}\",\"family\":\"ISAM",\"model\":\"#{@model}\",\"technology\":\"eth\"}}) - aggregate_interface_value = Value.dynamic( - AggregateInterface.new!(%{ - name: "F DONC BOXH 010J", - physical_interface: "1000BASE-LX", - svlan_id: @svlan_id - })) + aggregate_interface_value = + Value.dynamic( + AggregateInterface.new!(%{ + name: "F DONC BOXH 010J", + physical_interface: "1000BASE-LX", + svlan_id: @svlan_id + }) + ) aggregate_interface = Diffo.Provider.create_characteristic!(%{ @@ -62,19 +64,21 @@ defmodule DiffoExample.Access.CharacteristicValueTest do }) assert Jason.encode!(aggregate_interface) == - ~s({\"name\":\"aggregate_interface\",\"value\":{\"name\":\"F DONC BOXH 010J\",\"physical_interface\":\"1000BASE-LX\",\"physical_layer\":\"GbE\",\"link_layer\":\"QinQ\",\"svlan_id\":3108,\"vpi\":0}}) + ~s({\"name\":\"aggregate_interface\",\"value\":{\"name\":\"F DONC BOXH 010J\",\"physicalInterface\":\"1000BASE-LX\",\"physicalLayer\":\"GbE\",\"linkLayer\":\"QinQ\",\"svlanId\":3108,\"VPI\":0}}) bandwidth_profile = BandwidthProfile.new!(%{downstream: 24, upstream: 1}) assert Jason.encode!(bandwidth_profile) == ~s({\"downstream\":24,\"upstream\":1,\"units\":\"Mbps\"}) - circuit_value = Value.dynamic( - Circuit.new!%{ - circuit_id: @circuit_id, - cvlan_id: @cvlan_id, - bandwidth_profile: bandwidth_profile - }) + circuit_value = + Value.dynamic( + Circuit.new!(%{ + circuit_id: @circuit_id, + cvlan_id: @cvlan_id, + bandwidth_profile: bandwidth_profile + }) + ) circuit = Diffo.Provider.create_characteristic!(%{ @@ -84,11 +88,12 @@ defmodule DiffoExample.Access.CharacteristicValueTest do }) assert Jason.encode!(circuit) == - ~s({\"name\":\"circuit\",\"value\":{\"circuit_id\":\"#{@circuit_id}\",\"cvlan_id\":82,\"vci\":0,\"encapsulation\":\"IPoE\",\"bandwidth_profile\":{\"downstream\":24,\"upstream\":1,\"units\":\"Mbps\"}}}) + ~s({\"name\":\"circuit\",\"value\":{\"circuitId\":\"#{@circuit_id}\",\"cvlan_id\":82,\"VCI\":0,\"encapsulation\":\"IPoE\",\"bandwidthProfile\":{\"downstream\":24,\"upstream\":1,\"units\":\"Mbps\"}}}) - line_value = Value.dynamic( - Line.new!(%{port: @port, slot: @slot, standard: :ADSL2plus, profile: @profile}) - ) + line_value = + Value.dynamic( + Line.new!(%{port: @port, slot: @slot, standard: :ADSL2plus, profile: @profile}) + ) line = Diffo.Provider.create_characteristic!(%{ diff --git a/test/access/dsl_access_test.exs b/test/access/dsl_access_test.exs index 41a7e41..58818f7 100644 --- a/test/access/dsl_access_test.exs +++ b/test/access/dsl_access_test.exs @@ -106,7 +106,7 @@ defmodule DiffoExample.Access.DslAccessTest do encoding = Jason.encode!(dsl_access) |> Diffo.Util.summarise_dates() assert encoding == - ~s({\"id\":\"#{dsl_access.id}",\"href\":\"serviceInventoryManagement/v4/service/#{dsl_access.id}\",\"category\":\"Network Service\",\"serviceSpecification\":{\"id\":\"da9b207a-26c3-451d-8abd-0640c6349979\",\"href\":\"serviceCatalogManagement/v4/serviceSpecification/da9b207a-26c3-451d-8abd-0640c6349979\",\"name\":\"dslAccess\",\"version\":\"v1.0.0\"},\"serviceDate\":\"now\",\"state\":\"initial\",\"feature\":[{\"name\":\"dynamic_line_management\",\"isEnabled\":true,\"featureCharacteristic\":[{\"name\":\"constraints\",\"value\":{}}]}],\"serviceCharacteristic\":[{\"name\":\"aggregate_interface\",\"value\":{\"physical_layer\":\"GbE\",\"link_layer\":\"QinQ\",\"svlan_id\":0,\"vpi\":0}},{\"name\":\"circuit\",\"value\":{\"cvlan_id\":0,\"vci\":0,\"encapsulation\":\"IPoE\"}},{\"name\":\"dslam\",\"value\":{\"family\":\"ISAM\",\"technology\":\"eth\"}},{\"name\":\"line\",\"value\":{\"standard\":\"ADSL2plus\"}}],\"place\":[{\"id\":\"1657363\",\"href\":\"place/telco/1657363\",\"name\":\"addressId\",\"role\":\"CustomerSite\",\"@referredType\":\"GeographicAddress\",\"@type\":\"PlaceRef\"}],\"relatedParty\":[{\"id\":\"IND000000897354\",\"name\":\"individualId\",\"role\":\"Customer\",\"@referredType\":\"Individual\",\"@type\":\"PartyRef\"},{\"id\":\"ORG000000123456\",\"name\":\"organizationId\",\"role\":\"Reseller\",\"@referredType\":\"Organization\",\"@type\":\"PartyRef\"}]}) + ~s({\"id\":\"#{dsl_access.id}",\"href\":\"serviceInventoryManagement/v4/service/#{dsl_access.id}\",\"category\":\"Network Service\",\"serviceSpecification\":{\"id\":\"da9b207a-26c3-451d-8abd-0640c6349979\",\"href\":\"serviceCatalogManagement/v4/serviceSpecification/da9b207a-26c3-451d-8abd-0640c6349979\",\"name\":\"dslAccess\",\"version\":\"v1.0.0\"},\"serviceDate\":\"now\",\"state\":\"initial\",\"feature\":[{\"name\":\"dynamic_line_management\",\"isEnabled\":true,\"featureCharacteristic\":[{\"name\":\"constraints\",\"value\":{}}]}],\"serviceCharacteristic\":[{\"name\":\"aggregate_interface\",\"value\":{\"physicalLayer\":\"GbE\",\"linkLayer\":\"QinQ\",\"svlanId\":0,\"VPI\":0}},{\"name\":\"circuit\",\"value\":{\"cvlan_id\":0,\"VCI\":0,\"encapsulation\":\"IPoE\"}},{\"name\":\"dslam\",\"value\":{\"family\":\"ISAM\",\"technology\":\"eth\"}},{\"name\":\"line\",\"value\":{\"standard\":\"ADSL2plus\"}}],\"place\":[{\"id\":\"1657363\",\"href\":\"place/telco/1657363\",\"name\":\"addressId\",\"role\":\"CustomerSite\",\"@referredType\":\"GeographicAddress\",\"@type\":\"PlaceRef\"}],\"relatedParty\":[{\"id\":\"IND000000897354\",\"name\":\"individualId\",\"role\":\"Customer\",\"@referredType\":\"Individual\",\"@type\":\"PartyRef\"},{\"id\":\"ORG000000123456\",\"name\":\"organizationId\",\"role\":\"Reseller\",\"@referredType\":\"Organization\",\"@type\":\"PartyRef\"}]}) end test "advance service to feasibilityChecked" do @@ -134,7 +134,7 @@ defmodule DiffoExample.Access.DslAccessTest do encoding = Jason.encode!(dsl_access) |> Diffo.Util.summarise_dates() assert encoding == - ~s({\"id\":\"#{dsl_access.id}",\"href\":\"serviceInventoryManagement/v4/service/#{dsl_access.id}\",\"category\":\"Network Service\",\"serviceSpecification\":{\"id\":\"da9b207a-26c3-451d-8abd-0640c6349979\",\"href\":\"serviceCatalogManagement/v4/serviceSpecification/da9b207a-26c3-451d-8abd-0640c6349979\",\"name\":\"dslAccess\",\"version\":\"v1.0.0\"},\"serviceDate\":\"now\",\"state\":\"feasibilityChecked\",\"operatingStatus\":\"feasible\",\"feature\":[{\"name\":\"dynamic_line_management\",\"isEnabled\":true,\"featureCharacteristic\":[{\"name\":\"constraints\",\"value\":{}}]}],\"serviceCharacteristic\":[{\"name\":\"aggregate_interface\",\"value\":{\"physical_layer\":\"GbE\",\"link_layer\":\"QinQ\",\"svlan_id\":0,\"vpi\":0}},{\"name\":\"circuit\",\"value\":{\"cvlan_id\":0,\"vci\":0,\"encapsulation\":\"IPoE\"}},{\"name\":\"dslam\",\"value\":{\"family\":\"ISAM\",\"technology\":\"eth\"}},{\"name\":\"line\",\"value\":{\"standard\":\"ADSL2plus\"}}],\"place\":[{\"id\":\"1657363\",\"href\":\"place/telco/1657363\",\"name\":\"addressId\",\"role\":\"CustomerSite\",\"@referredType\":\"GeographicAddress\",\"@type\":\"PlaceRef\"},{\"id\":\"DONC-0001\",\"href\":\"place/telco/DONC-0001\",\"name\":\"esaId\",\"role\":\"ServingArea\",\"@referredType\":\"GeographicLocation\",\"@type\":\"PlaceRef\"}],\"relatedParty\":[{\"id\":\"IND000000897354\",\"name\":\"individualId\",\"role\":\"Customer\",\"@referredType\":\"Individual\",\"@type\":\"PartyRef\"},{\"id\":\"ORG000000123456\",\"name\":\"organizationId\",\"role\":\"Reseller\",\"@referredType\":\"Organization\",\"@type\":\"PartyRef\"}]}) + ~s({\"id\":\"#{dsl_access.id}",\"href\":\"serviceInventoryManagement/v4/service/#{dsl_access.id}\",\"category\":\"Network Service\",\"serviceSpecification\":{\"id\":\"da9b207a-26c3-451d-8abd-0640c6349979\",\"href\":\"serviceCatalogManagement/v4/serviceSpecification/da9b207a-26c3-451d-8abd-0640c6349979\",\"name\":\"dslAccess\",\"version\":\"v1.0.0\"},\"serviceDate\":\"now\",\"state\":\"feasibilityChecked\",\"operatingStatus\":\"feasible\",\"feature\":[{\"name\":\"dynamic_line_management\",\"isEnabled\":true,\"featureCharacteristic\":[{\"name\":\"constraints\",\"value\":{}}]}],\"serviceCharacteristic\":[{\"name\":\"aggregate_interface\",\"value\":{\"physicalLayer\":\"GbE\",\"linkLayer\":\"QinQ\",\"svlanId\":0,\"VPI\":0}},{\"name\":\"circuit\",\"value\":{\"cvlan_id\":0,\"VCI\":0,\"encapsulation\":\"IPoE\"}},{\"name\":\"dslam\",\"value\":{\"family\":\"ISAM\",\"technology\":\"eth\"}},{\"name\":\"line\",\"value\":{\"standard\":\"ADSL2plus\"}}],\"place\":[{\"id\":\"1657363\",\"href\":\"place/telco/1657363\",\"name\":\"addressId\",\"role\":\"CustomerSite\",\"@referredType\":\"GeographicAddress\",\"@type\":\"PlaceRef\"},{\"id\":\"DONC-0001\",\"href\":\"place/telco/DONC-0001\",\"name\":\"esaId\",\"role\":\"ServingArea\",\"@referredType\":\"GeographicLocation\",\"@type\":\"PlaceRef\"}],\"relatedParty\":[{\"id\":\"IND000000897354\",\"name\":\"individualId\",\"role\":\"Customer\",\"@referredType\":\"Individual\",\"@type\":\"PartyRef\"},{\"id\":\"ORG000000123456\",\"name\":\"organizationId\",\"role\":\"Reseller\",\"@referredType\":\"Organization\",\"@type\":\"PartyRef\"}]}) end end @@ -175,7 +175,7 @@ defmodule DiffoExample.Access.DslAccessTest do encoding = Jason.encode!(dsl_access) |> Diffo.Util.summarise_dates() assert encoding == - ~s({\"id\":\"#{dsl_access.id}",\"href\":\"serviceInventoryManagement/v4/service/#{dsl_access.id}\",\"category\":\"Network Service\",\"serviceSpecification\":{\"id\":\"da9b207a-26c3-451d-8abd-0640c6349979\",\"href\":\"serviceCatalogManagement/v4/serviceSpecification/da9b207a-26c3-451d-8abd-0640c6349979\",\"name\":\"dslAccess\",\"version\":\"v1.0.0\"},\"serviceDate\":\"now\",\"state\":\"reserved\",\"operatingStatus\":\"feasible\",\"feature\":[{\"name\":\"dynamic_line_management\",\"isEnabled\":true,\"featureCharacteristic\":[{\"name\":\"constraints\",\"value\":{}}]}],\"serviceCharacteristic\":[{\"name\":\"aggregate_interface\",\"value\":{\"name\":\"eth0\",\"physical_layer\":\"GbE\",\"link_layer\":\"QinQ\",\"svlan_id\":3108,\"vpi\":0}},{\"name\":\"circuit\",\"value\":{\"cvlan_id\":82,\"vci\":0,\"encapsulation\":\"IPoE\"}},{\"name\":\"dslam\",\"value\":{\"name\":\"QDONC0001\",\"family\":\"ISAM\",\"model\":\"ISAM7330\",\"technology\":\"eth\"}},{\"name\":\"line\",\"value\":{\"port\":5,\"slot\":10,\"standard\":\"ADSL2plus\"}}],\"place\":[{\"id\":\"1657363\",\"href\":\"place/telco/1657363\",\"name\":\"addressId\",\"role\":\"CustomerSite\",\"@referredType\":\"GeographicAddress\",\"@type\":\"PlaceRef\"},{\"id\":\"DONC-0001\",\"href\":\"place/telco/DONC-0001\",\"name\":\"esaId\",\"role\":\"ServingArea\",\"@referredType\":\"GeographicLocation\",\"@type\":\"PlaceRef\"}],\"relatedParty\":[{\"id\":\"IND000000897354\",\"name\":\"individualId\",\"role\":\"Customer\",\"@referredType\":\"Individual\",\"@type\":\"PartyRef\"},{\"id\":\"ORG000000123456\",\"name\":\"organizationId\",\"role\":\"Reseller\",\"@referredType\":\"Organization\",\"@type\":\"PartyRef\"}]}) + ~s({\"id\":\"#{dsl_access.id}",\"href\":\"serviceInventoryManagement/v4/service/#{dsl_access.id}\",\"category\":\"Network Service\",\"serviceSpecification\":{\"id\":\"da9b207a-26c3-451d-8abd-0640c6349979\",\"href\":\"serviceCatalogManagement/v4/serviceSpecification/da9b207a-26c3-451d-8abd-0640c6349979\",\"name\":\"dslAccess\",\"version\":\"v1.0.0\"},\"serviceDate\":\"now\",\"state\":\"reserved\",\"operatingStatus\":\"feasible\",\"feature\":[{\"name\":\"dynamic_line_management\",\"isEnabled\":true,\"featureCharacteristic\":[{\"name\":\"constraints\",\"value\":{}}]}],\"serviceCharacteristic\":[{\"name\":\"aggregate_interface\",\"value\":{\"name\":\"eth0\",\"physicalLayer\":\"GbE\",\"linkLayer\":\"QinQ\",\"svlanId\":3108,\"VPI\":0}},{\"name\":\"circuit\",\"value\":{\"cvlan_id\":82,\"VCI\":0,\"encapsulation\":\"IPoE\"}},{\"name\":\"dslam\",\"value\":{\"name\":\"QDONC0001\",\"family\":\"ISAM\",\"model\":\"ISAM7330\",\"technology\":\"eth\"}},{\"name\":\"line\",\"value\":{\"port\":5,\"slot\":10,\"standard\":\"ADSL2plus\"}}],\"place\":[{\"id\":\"1657363\",\"href\":\"place/telco/1657363\",\"name\":\"addressId\",\"role\":\"CustomerSite\",\"@referredType\":\"GeographicAddress\",\"@type\":\"PlaceRef\"},{\"id\":\"DONC-0001\",\"href\":\"place/telco/DONC-0001\",\"name\":\"esaId\",\"role\":\"ServingArea\",\"@referredType\":\"GeographicLocation\",\"@type\":\"PlaceRef\"}],\"relatedParty\":[{\"id\":\"IND000000897354\",\"name\":\"individualId\",\"role\":\"Customer\",\"@referredType\":\"Individual\",\"@type\":\"PartyRef\"},{\"id\":\"ORG000000123456\",\"name\":\"organizationId\",\"role\":\"Reseller\",\"@referredType\":\"Organization\",\"@type\":\"PartyRef\"}]}) end end diff --git a/test/diffo_example_test.exs b/test/diffo_example_test.exs new file mode 100644 index 0000000..61a86e7 --- /dev/null +++ b/test/diffo_example_test.exs @@ -0,0 +1,11 @@ +# SPDX-FileCopyrightText: 2025 diffo_example contributors +# +# SPDX-License-Identifier: MIT + +defmodule DiffoExampleTest do + @moduledoc false + use ExUnit.Case + doctest DiffoExample.Access.Util + doctest DiffoExample.Nbn.Util + doctest DiffoExample.Nbn.Speeds +end diff --git a/test/nbn/nbn_ethernet_test.exs b/test/nbn/nbn_ethernet_test.exs new file mode 100644 index 0000000..86abbf8 --- /dev/null +++ b/test/nbn/nbn_ethernet_test.exs @@ -0,0 +1,421 @@ +# SPDX-FileCopyrightText: 2025 diffo_example contributors +# +# SPDX-License-Identifier: MIT + +defmodule DiffoExample.Nbn.NbnEthernetTest do + @moduledoc false + use ExUnit.Case + alias Diffo.Provider.Specification + alias Diffo.Provider.Characteristic + alias DiffoExample.Nbn + alias DiffoExample.Nbn.NbnEthernet + alias DiffoExample.Nbn.Uni + alias DiffoExample.Nbn.Avc + alias DiffoExample.Nbn.Ntd + alias DiffoExample.Nbn.Cvc + alias DiffoExample.Nbn.NniGroup + alias DiffoExample.Nbn.Nni + alias DiffoExample.Test.Characteristics + alias Diffo.Provider.Assignment + alias Diffo.Provider.Instance.Relationship + + setup_all do + AshNeo4j.BoltyHelper.start() + end + + setup do + on_exit(fn -> + AshNeo4j.Neo4jHelper.delete_all() + end) + end + + describe "build nbn_ethernet" do + test "create an nbn_ethernet access" do + {:ok, access} = Nbn.build_nbn_ethernet(%{}) + + # check the instance is an NbnEthernet + assert is_struct(access, NbnEthernet) + + # check specification resource enrichment and node relationship + refute is_nil(access.specification_id) + assert is_struct(access.specification, Specification) + + assert AshNeo4j.Neo4jHelper.nodes_relate_how?( + :Instance, + %{uuid: access.id}, + :Specification, + %{uuid: access.specification_id}, + :SPECIFIED_BY, + :outgoing + ) + + # check characteristic resource enrichment and node relationships + assert is_list(access.characteristics) + assert length(access.characteristics) == 1 + + Enum.each(access.characteristics, fn characteristic -> + assert is_struct(characteristic, Characteristic) + + assert AshNeo4j.Neo4jHelper.nodes_relate_how?( + :Instance, + %{uuid: access.id}, + :Characteristic, + %{uuid: characteristic.id}, + :HAS, + :outgoing + ) + end) + + encoding = Jason.encode!(access) |> Diffo.Util.summarise_dates() + + assert encoding == + ~s({"id":"#{access.id}","href":"resourceInventoryManagement/v4/resource/#{access.id}","category":"Network Resource",\"name\":\"#{access.name}","resourceSpecification":{"id":"f2a4c6e8-1b3d-4f5a-8c7e-9d0b2e4f6a8c","href":"resourceCatalogManagement/v4/resourceSpecification/f2a4c6e8-1b3d-4f5a-8c7e-9d0b2e4f6a8c","name":"nbnEthernet","version":"v1.0.0"},"resourceCharacteristic":[{"name":"pri","value":{}}]}) + end + + test "define nbn_ethernet access" do + {:ok, access} = Nbn.build_nbn_ethernet(%{}) + + updates = [ + pri: [ + avcid: "AVC000910202941", + uniid: "UNI000302814545", + speeds: {500, 50}, + technology: :FTTP + ] + ] + + {:ok, access} = Nbn.define_nbn_ethernet(access, %{characteristic_value_updates: updates}) + + Characteristics.check_values( + [ + pri: [ + avcid: "AVC000910202941", + uniid: "UNI000302814545", + speeds: {500, 50}, + technology: :FTTP + ] + ], + access + ) + end + + test "relate nbn_ethernet" do + {:ok, access} = Nbn.build_nbn_ethernet(%{}) + + {:ok, nni_group} = Nbn.build_nni_group(%{}) + {:ok, cvc} = Nbn.build_cvc(%{}) + + {:ok, _nni_group} = + Nbn.assign_svlan(nni_group, %{ + assignment: %Assignment{assignee_id: cvc.id, operation: :auto_assign} + }) + + {:ok, cvc} = Nbn.get_cvc_by_id(cvc.id, load: [:reverse_relationships]) + {:ok, cvc} = Nbn.mine_cvc(cvc) + + {:ok, avc} = Nbn.build_avc(%{}) + + {:ok, avc} = + Nbn.define_avc(avc, %{ + characteristic_value_updates: [avc: [bandwidth_profile: :home_fast]] + }) + + {:ok, _cvc} = + Nbn.assign_cvlan(cvc, %{ + assignment: %Assignment{assignee_id: avc.id, operation: :auto_assign} + }) + + {:ok, avc} = Nbn.get_avc_by_id(avc.id, load: [:reverse_relationships]) + {:ok, avc} = Nbn.mine_avc(avc) + + {:ok, ntd} = Nbn.build_ntd(%{}) + + {:ok, ntd} = + Nbn.define_ntd(ntd, %{characteristic_value_updates: [ntd: [technology: :FTTP]]}) + + {:ok, uni} = Nbn.build_uni(%{}) + + {:ok, _ntd} = + Nbn.assign_port(ntd, %{ + assignment: %Assignment{assignee_id: uni.id, operation: :auto_assign} + }) + + {:ok, uni} = Nbn.get_uni_by_id(uni.id, load: [:reverse_relationships]) + {:ok, uni} = Nbn.mine_uni(uni) + + relationships = [ + %Relationship{id: avc.id, direction: :forward, type: :owns, alias: :avc}, + %Relationship{id: uni.id, direction: :forward, type: :owns, alias: :uni} + ] + + {:ok, access} = Nbn.relate_nbn_ethernet(access, %{relationships: relationships}) + + {:ok, access} = Nbn.mine_nbn_ethernet(access) + + encoding = Jason.encode!(access) |> Diffo.Util.summarise_dates() + + assert encoding == + ~s({"id":"#{access.id}","href":"resourceInventoryManagement/v4/resource/#{access.id}","category":"Network Resource","name":"#{access.name}","resourceSpecification":{"id":"f2a4c6e8-1b3d-4f5a-8c7e-9d0b2e4f6a8c","href":"resourceCatalogManagement/v4/resourceSpecification/f2a4c6e8-1b3d-4f5a-8c7e-9d0b2e4f6a8c","name":"nbnEthernet","version":"v1.0.0"},"resourceRelationship":[{"alias":"avc","type":"owns","resource":{"id":"#{avc.id}","href":"resourceInventoryManagement/v4/resource/#{avc.id}"}},{"alias":"uni","type":"owns","resource":{"id\":"#{uni.id}","href":"resourceInventoryManagement/v4/resource/#{uni.id}"}}],"supportingResource":[{"id":"avc","href":"resourceInventoryManagement/v4/resource/#{avc.id}"},{"id\":"uni","href":"resourceInventoryManagement/v4/resource/#{uni.id}"}],"resourceCharacteristic":[{"name":"pri","value":{"AVCID":"#{avc.name}","UNIID":"#{uni.name}","technology":"FTTP","bandwidthProfile":"home_fast","speeds":[500,50]}}]}) + end + end + + describe "build uni" do + test "create a uni" do + {:ok, uni} = Nbn.build_uni(%{}) + + assert is_struct(uni, Uni) + refute is_nil(uni.specification_id) + assert is_struct(uni.specification, Specification) + assert is_list(uni.characteristics) + assert length(uni.characteristics) == 1 + end + + test "define uni" do + {:ok, uni} = Nbn.build_uni(%{}) + + updates = [ + uni: [port: 1, encapsulation: "DSCP Mapped", technology: :FTTP] + ] + + {:ok, uni} = Nbn.define_uni(uni, %{characteristic_value_updates: updates}) + + Characteristics.check_values( + [uni: [port: 1, encapsulation: "DSCP Mapped", technology: :FTTP]], + uni + ) + end + end + + describe "build avc" do + test "create an avc" do + {:ok, avc} = Nbn.build_avc(%{}) + + assert is_struct(avc, Avc) + refute is_nil(avc.specification_id) + assert is_struct(avc.specification, Specification) + assert is_list(avc.characteristics) + assert length(avc.characteristics) == 2 + end + + test "define avc" do + {:ok, avc} = Nbn.build_avc(%{}) + + updates = [ + avc: [cvlan: 1, bandwidth_profile: :home_fast] + ] + + {:ok, avc} = Nbn.define_avc(avc, %{characteristic_value_updates: updates}) + + Characteristics.check_values( + [avc: [cvlan: 1, bandwidth_profile: :home_fast]], + avc + ) + end + end + + describe "build ntd" do + test "create an ntd" do + {:ok, ntd} = Nbn.build_ntd(%{}) + + assert is_struct(ntd, Ntd) + refute is_nil(ntd.specification_id) + end + + test "define ntd and assign ports to unis" do + {:ok, ntd} = Nbn.build_ntd(%{}) + + updates = [ + ntd: [model: "Sercomm CG4000A", serial_number: "SCOMA1A057A2", technology: :FTTP], + ports: [first: 1, last: 4, free: 4, assignable_type: "port"] + ] + + {:ok, ntd} = Nbn.define_ntd(ntd, %{characteristic_value_updates: updates}) + + Characteristics.check_values( + [ + ntd: [model: "Sercomm CG4000A", serial_number: "SCOMA1A057A2", technology: :FTTP], + ports: [first: 1, last: 4, free: 4, assignable_type: "port"] + ], + ntd + ) + + {:ok, ntd} = Nbn.assign_port(ntd, %{assignment: create_uni()}) + {:ok, ntd} = Nbn.assign_port(ntd, %{assignment: create_uni()}) + + Characteristics.check_values( + [ + ntd: [model: "Sercomm CG4000A", serial_number: "SCOMA1A057A2", technology: :FTTP], + ports: [first: 1, last: 4, free: 2, assignable_type: "port"] + ], + ntd + ) + + # mine and check each uni + Enum.each(ntd.forward_relationships, fn relationship -> + {:ok, uni} = + Nbn.get_uni_by_id(relationship.target_id, load: [:reverse_relationships]) + + {:ok, uni} = Nbn.mine_uni(uni) + + # uni should have an uni characteristic with the port + Characteristics.check_values( + [ + uni: [port: &Outstand.any_integer/1] + ], + uni + ) + end) + end + end + + describe "build cvc" do + test "create a cvc" do + {:ok, cvc} = Nbn.build_cvc(%{}) + + assert is_struct(cvc, Cvc) + refute is_nil(cvc.specification_id) + end + + test "define cvc and assign cvlans to avcs" do + {:ok, cvc} = Nbn.build_cvc(%{}) + + updates = [ + cvc: [svlan: 1, bandwidth: 10000], + cvlans: [first: 1, last: 4000, free: 4000, assignable_type: "cvlan"] + ] + + {:ok, cvc} = Nbn.define_cvc(cvc, %{characteristic_value_updates: updates}) + + Characteristics.check_values( + [ + cvc: [svlan: 1, bandwidth: 10000], + cvlans: [first: 1, last: 4000, free: 4000, assignable_type: "cvlan"] + ], + cvc + ) + + {:ok, cvc} = Nbn.assign_cvlan(cvc, %{assignment: create_avc()}) + {:ok, cvc} = Nbn.assign_cvlan(cvc, %{assignment: create_avc()}) + + Characteristics.check_values( + [ + cvc: [svlan: 1, bandwidth: 10000], + cvlans: [first: 1, last: 4000, free: 3998, assignable_type: "cvlan"] + ], + cvc + ) + + # mine and check each avc + Enum.each(cvc.forward_relationships, fn relationship -> + {:ok, avc} = + Nbn.get_avc_by_id(relationship.target_id, load: [:reverse_relationships]) + + {:ok, avc} = Nbn.mine_avc(avc) + + # avc should have an avc characteristic with the cvlan + Characteristics.check_values( + [ + avc: [cvlan: &Outstand.any_integer/1], + cvc: [svlan: :no_value] + ], + avc + ) + end) + end + end + + describe "build nni_group" do + test "create an nni_group" do + {:ok, nni_group} = Nbn.build_nni_group(%{}) + + assert is_struct(nni_group, NniGroup) + refute is_nil(nni_group.specification_id) + end + + test "define nni_group and assign svlans to cvcs" do + {:ok, nni_group} = Nbn.build_nni_group(%{}) + + updates = [ + nni_group: [name: "SYD-POI-01", location: "Sydney Olympic Park"], + svlans: [first: 1, last: 4000, free: 4000, assignable_type: "svlan"] + ] + + {:ok, nni_group} = + Nbn.define_nni_group(nni_group, %{characteristic_value_updates: updates}) + + Characteristics.check_values( + [ + nni_group: [name: "SYD-POI-01", location: "Sydney Olympic Park"], + svlans: [first: 1, last: 4000, free: 4000, assignable_type: "svlan"] + ], + nni_group + ) + + {:ok, nni_group} = Nbn.assign_svlan(nni_group, %{assignment: create_cvc()}) + {:ok, nni_group} = Nbn.assign_svlan(nni_group, %{assignment: create_cvc()}) + + Characteristics.check_values( + [ + nni_group: [name: "SYD-POI-01", location: "Sydney Olympic Park"], + svlans: [first: 1, last: 4000, free: 3998, assignable_type: "svlan"] + ], + nni_group + ) + + # mine and check each cvc + Enum.each(nni_group.forward_relationships, fn relationship -> + {:ok, cvc} = + Nbn.get_cvc_by_id(relationship.target_id, load: [:reverse_relationships]) + + {:ok, avc} = Nbn.mine_cvc(cvc) + + # cvc should have an cvc characteristic with the svlan + Characteristics.check_values( + [ + cvc: [svlan: &Outstand.any_integer/1] + ], + avc + ) + end) + end + end + + describe "build nni" do + test "create an nni" do + {:ok, nni} = Nbn.build_nni(%{}) + + assert is_struct(nni, Nni) + refute is_nil(nni.specification_id) + end + + test "define nni" do + {:ok, nni} = Nbn.build_nni(%{}) + + updates = [ + nni: [port_id: "SYD-01-ETH-001", capacity: 10, technology: :Ethernet] + ] + + {:ok, nni} = Nbn.define_nni(nni, %{characteristic_value_updates: updates}) + + Characteristics.check_values( + [nni: [port_id: "SYD-01-ETH-001", capacity: 10, technology: :Ethernet]], + nni + ) + end + end + + defp create_uni() do + {:ok, uni} = Nbn.build_uni(%{}) + %Assignment{assignee_id: uni.id, operation: :auto_assign} + end + + defp create_cvc() do + {:ok, cvc} = Nbn.build_cvc(%{}) + %Assignment{assignee_id: cvc.id, operation: :auto_assign} + end + + defp create_avc() do + {:ok, avc} = Nbn.build_avc(%{}) + %Assignment{assignee_id: avc.id, operation: :auto_assign} + end +end diff --git a/test/support/characteristics.ex b/test/support/characteristics.ex index b1d81bd..00bed31 100644 --- a/test/support/characteristics.ex +++ b/test/support/characteristics.ex @@ -11,6 +11,9 @@ defmodule DiffoExample.Test.Characteristics do import Outstand import ExUnit.Assertions + @doc """ + uses Outstanding to check expected values within instance characteristics + """ def check_values(expected_values, instance) when is_list(expected_values) and is_struct(instance) do Enum.each( diff --git a/test/test_helper.exs b/test/test_helper.exs index 1caa23b..35444cb 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -6,4 +6,4 @@ Mix.Task.run("app.start") ExUnit.start() level = Application.get_env(:logger, :console) |> Keyword.get(:level) Logger.put_application_level(:diffo, level) -Logger.put_application_level(:ash_neo4j, level) +Logger.put_application_level(:ash_neo4j, :error)