diff --git a/CHANGELOG.md b/CHANGELOG.md index 75fa993..186bbcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,37 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline +## [v0.2.3](https://github.com/diffo-dev/diffo/compare/v0.2.2..v0.2.3) (2026-05-22) + +### Maintenance: +* updated to diffo 0.4.1 (issue #48) +* refreshed agent guidance via `mix usage_rules.sync` + +### Refactors: +* adopted upstream `Diffo.Provider.Changes.{Define,Relate,Assign}` across 11 instance resources; deleted the local `DiffoExample.Changes.*` trio now that the same modules ship in diffo proper ([diffo#170](https://github.com/diffo-dev/diffo/pull/170)) +* slimmed 16 `BaseCharacteristic`-derived resources by relying on the auto-generated `:create`/`:update` actions ([diffo#171](https://github.com/diffo-dev/diffo/pull/171)) — ~360 lines removed; custom `:update` retained on `cable`, `path`, `circuit`, `constraints` where unit/bandwidth-profile composition is needed +* `Cable.relate` inline `after_action` (which referenced an unaliased `Relationship` module) folded into `change Diffo.Provider.Changes.Relate` + +### Resolved workarounds: +* `DslAccess.qualify_result` now transitions to `:feasibilityChecked` — restores correct TMF form following [diffo#168](https://github.com/diffo-dev/diffo/pull/168) broadening the Assigner lifecycle gate to include `:feasibilityChecked` +* `Util.summarise_characteristics/2` no longer called from tests — typed characteristic + pool records now surface in TMF JSON by default ([diffo#169](https://github.com/diffo-dev/diffo/pull/169)). The projection function is retained in [lib/diffo_example/util.ex](lib/diffo_example/util.ex) for future projection demonstrations. Expected JSON strings updated across `cable`, `card`, `cable`, `path`, `shelf`, `dsl_access`, `nbn_ethernet` tests to reflect the real typed/pool surfacing + +### Features: +* NBN — AVC, CVC, NniGroup characteristic inheritance and metrics (issue #49): + * AVC inherits the upstream CVC's `cvc` characteristic via the `:cvlan` assignment (single-hop), and the NniGroup's `nni_group` characteristic transitively via `[:cvlan, :svlan]` (two-hop). Both singular. + * CVC inherits the upstream NniGroup's `nni_group` characteristic via the `:svlan` assignment (singular). + * NniGroup brings up the typed value of every comprised NNI as `nnis[]` via the `:contains` relationship. + * New `cvc_metrics` characteristic on CVC carries `avcs_count` and `avcs_total_bandwidth` aggregated live over assigned AVCs. + * New `nni_group_metrics` characteristic on NniGroup carries `cvcs_count`/`cvcs_total_bandwidth` (demand), `nnis_count`/`nnis_total_bandwidth` (capacity), and `utilization = cvcs_total_bandwidth / nnis_total_bandwidth`. +* NBN — NTD brings up assigned UNIs as `unis[]` via the `:port` assignment (issue #49 part 2). +* NBN — NbnEthernet (PRI) brings up four characteristics surfacing the full delivery chain (issue #49 part 3): `avc` single-hop via the `:circuit` owns relationship, `uni` single-hop via the `:port` owns relationship, `cvc` two-hop via `:circuit` then `:cvc`, and `ntd` two-hop via `:port` then `:ntd`. All singular. +* NBN and Access — consumer-side aliases on assignments and relationships now name the **upstream related resource** the consumer is part of (its domain role), not the slot/thing being received. NBN: AVC sets `:cvc` on its cvlan assignment, CVC sets `:nni_group` on its svlan assignment, UNI sets `:ntd` on its port assignment; PRI's two `:owns` relationships are aliased `:circuit` (AVC) and `:port` (UNI). Access: Card sets `:shelf` on its slot assignment, Path sets `:card` on its port assignment, and `Shelf.cards` filters on `alias: :shelf`. Inheritance walks use these consumer-aliases. Pool/metric aggregations are unaffected — they still filter by `thing`. +* `BandwidthProfile.downstream/1` — atom-to-Mbps mapping used by the metrics aggregation. CVCs are treated as symmetric capacity in this model (satellite asymmetry ignored). + +### Refactors (continued): +* `DiffoExample.Calculations.InheritedCharacteristic` renamed to `InheritedCharacteristicViaAssignment`; new sibling `InheritedCharacteristicViaRelationship` traverses `Provider.Relationship` edges (forward source → target). Both calcs accept `singular?:` to unwrap to a single value where graph identity guarantees ≤1 result. `InheritedCharacteristicViaRelationship` also accepts a `then_via:` list of assignment aliases to continue the walk via `AssignmentRelationship` after the relationship hop — covers mixed paths like PRI's `cvc` (relationship + assignment). +* `ReverseInheritedCharacteristic` extended with a `thing:` filter option, complementing the existing `alias:` filter. Source-side aggregations should prefer `thing:` since it's always set from the pool DSL — see the assignment-direction-asymmetry rationale. + ## [v0.2.2](https://github.com/diffo-dev/diffo/compare/v0.2.1..v0.2.2) (2026-05-21) ### Maintenance: diff --git a/config/config.exs b/config/config.exs index 9920208..ff4d1b4 100644 --- a/config/config.exs +++ b/config/config.exs @@ -16,7 +16,6 @@ config :spark, :characteristics, :neo4j, :jason, - :json_api, :outstanding, :actions, :state_machine, diff --git a/documentation/domains/_access_api.md b/documentation/domains/_access_api.md new file mode 100644 index 0000000..ae61946 --- /dev/null +++ b/documentation/domains/_access_api.md @@ -0,0 +1,60 @@ + + +# Access Domain API + +The Elixir function-call surface for each resource in the `DiffoExample.Access` domain. Generated from the `define` declarations in the domain's `resources do` block. + +## Cable + +| Function | Action | Arguments | Purpose | +|---|---|---|---| +| `assign_pair` | `:assign_pair` | `assignment` (struct) | relates the cable with an instance by assigning a pair | +| `build_cable` | `:build` | `id`, `name`, `type`, `which`, `relationships` (list of struct), `places` (list of struct), `parties` (list of struct) | creates a new Cable resource instance for build | +| `define_cable` | `:define` | `characteristic_value_updates` (list of term) | defines the cable | +| `get_cable_by_id` | `:read` | `id` | read a service or resource instance | +| `relate_cable` | `:relate` | `relationships` (list of struct) | relates the cable with other instances | + +## Card + +| Function | Action | Arguments | Purpose | +|---|---|---|---| +| `assign_port` | `:assign_port` | `assignment` (struct) | relates the card with an instance by assigning a port | +| `build_card` | `:build` | `id`, `name`, `type`, `which`, `relationships` (list of struct), `places` (list of struct), `parties` (list of struct) | creates a new Card resource instance for build | +| `define_card` | `:define` | `characteristic_value_updates` (list of term) | defines the card | +| `get_card_by_id` | `:read` | `id` | read a service or resource instance | +| `relate_card` | `:relate` | `relationships` (list of struct) | relates the card with other instances | + +## DslAccess + +| Function | Action | Arguments | Purpose | +|---|---|---|---| +| `design_dsl_result` | `:design_result` | `characteristic_value_updates` (list of term) | updates the DSL Access service with the design | +| `get_dsl_by_id` | `:read` | `id` | read a service or resource instance | +| `qualify_dsl` | `:qualify` | `id`, `name`, `type`, `which`, `places` (list of struct), `parties` (list of struct) | creates a new DSL Access service instance for qualification | +| `qualify_dsl_result` | `:qualify_result` | `service_operating_status`, `places` (list of struct) | updates the DSL Access service with qualification result | + +## Path + +| Function | Action | Arguments | Purpose | +|---|---|---|---| +| `build_path` | `:build` | `id`, `name`, `type`, `which`, `relationships` (list of struct), `places` (list of struct), `parties` (list of struct) | creates a new Path resource instance for build | +| `define_path` | `:define` | `characteristic_value_updates` (list of term) | defines the path | +| `get_path_by_id` | `:read` | `id` | read a service or resource instance | +| `relate_path` | `:relate` | `relationships` (list of struct) | relates the path with other instances | + +## Shelf + +| Function | Action | Arguments | Purpose | +|---|---|---|---| +| `assign_slot` | `:assign_slot` | `assignment` (struct) | relates the shelf with an instance by assigning a slot | +| `build_shelf` | `:build` | `id`, `name`, `type`, `which`, `relationships` (list of struct), `places` (list of struct), `parties` (list of struct) | creates a new Shelf resource instance for build | +| `define_shelf` | `:define` | `characteristic_value_updates` (list of term) | defines the shelf | +| `get_shelf_by_id` | `:read` | `id` | read a service or resource instance | +| `relate_shelf` | `:relate` | `relationships` (list of struct) | relates the shelf with cards | diff --git a/documentation/domains/_nbn_api.md b/documentation/domains/_nbn_api.md new file mode 100644 index 0000000..aba0362 --- /dev/null +++ b/documentation/domains/_nbn_api.md @@ -0,0 +1,90 @@ + + +# NBN Domain API + +The Elixir function-call surface for each resource in the `DiffoExample.Nbn` domain. Generated from the `define` declarations in the domain's `resources do` block. + +## Avc + +| Function | Action | Arguments | Purpose | +|---|---|---|---| +| `build_avc` | `:build` | `id`, `which`, `relationships` (list of struct), `places` (list of struct), `parties` (list of struct) | creates a new AVC resource instance | +| `define_avc` | `:define` | `characteristic_value_updates` (list of term) | defines the AVC | +| `get_avc_by_id` | `:read` | `id` | read a service or resource instance | +| `relate_avc` | `:relate` | `relationships` (list of struct) | relates the AVC with other instances | + +## Cvc + +| Function | Action | Arguments | Purpose | +|---|---|---|---| +| `assign_cvlan` | `:assign_cvlan` | `assignment` (struct) | assigns a C-VLAN ID from the CVC pool to an AVC | +| `build_cvc` | `:build` | `id`, `which`, `relationships` (list of struct), `places` (list of struct), `parties` (list of struct) | creates a new CVC resource instance | +| `define_cvc` | `:define` | `characteristic_value_updates` (list of term) | defines the CVC | +| `get_cvc_by_id` | `:read` | `id` | read a service or resource instance | +| `relate_cvc` | `:relate` | `relationships` (list of struct) | relates the CVC with other instances (e.g. AVC aggregation, NNI Group termination) | + +## NbnEthernet + +| Function | Action | Arguments | Purpose | +|---|---|---|---| +| `build_nbn_ethernet` | `:build` | `id`, `which`, `relationships` (list of struct), `places` (list of struct), `parties` (list of struct) | creates a new NBN Ethernet access resource instance | +| `define_nbn_ethernet` | `:define` | `characteristic_value_updates` (list of term) | defines the NBN Ethernet access | +| `get_nbn_ethernet_by_id` | `:read` | `id` | read a service or resource instance | +| `relate_nbn_ethernet` | `:relate` | `relationships` (list of struct) | relates the NBN Ethernet access with other instances (e.g. UNI) | + +## Nni + +| Function | Action | Arguments | Purpose | +|---|---|---|---| +| `build_nni` | `:build` | `id`, `which`, `relationships` (list of struct), `places` (list of struct), `parties` (list of struct) | creates a new NNI resource instance | +| `define_nni` | `:define` | `characteristic_value_updates` (list of term) | defines the NNI | +| `get_nni_by_id` | `:read` | `id` | read a service or resource instance | +| `relate_nni` | `:relate` | `relationships` (list of struct) | relates the NNI with other instances (e.g. its parent NNI Group) | + +## NniGroup + +| Function | Action | Arguments | Purpose | +|---|---|---|---| +| `assign_svlan` | `:assign_svlan` | `assignment` (struct) | assigns an S-VLAN ID from the NNI Group pool to a CVC | +| `build_nni_group` | `:build` | `id`, `name`, `which`, `relationships` (list of struct), `places` (list of struct), `parties` (list of struct) | creates a new NNI Group resource instance | +| `define_nni_group` | `:define` | `characteristic_value_updates` (list of term) | defines the NNI Group | +| `get_nni_group_by_id` | `:read` | `id` | read a service or resource instance | +| `relate_nni_group` | `:relate` | `relationships` (list of struct) | relates the NNI Group with other instances (e.g. NNI resources it comprises) | + +## Ntd + +| Function | Action | Arguments | Purpose | +|---|---|---|---| +| `assign_port` | `:assign_port` | `assignment` (struct) | assigns a port from the NTD pool to a UNI | +| `build_ntd` | `:build` | `id`, `which`, `relationships` (list of struct), `places` (list of struct), `parties` (list of struct) | creates a new NTD resource instance | +| `define_ntd` | `:define` | `characteristic_value_updates` (list of term) | defines the NTD | +| `get_ntd_by_id` | `:read` | `id` | read a service or resource instance | +| `relate_ntd` | `:relate` | `relationships` (list of struct) | relates the NTD with other instances (e.g. UNI) | + +## Rsp + +| Function | Action | Arguments | Purpose | +|---|---|---|---| +| `activate_rsp` | `:activate` | — | — | +| `create_rsp` | `:build` | `name`, `short_name`, `id` | — | +| `deactivate_rsp` | `:deactivate` | — | — | +| `get_rsp_by_epid` | `:read` | `id` | — | +| `get_rsp_by_short_name` | `:read` | `short_name` | — | +| `list_rsps` | `:inventory` | — | — | +| `suspend_rsp` | `:suspend` | — | — | + +## Uni + +| Function | Action | Arguments | Purpose | +|---|---|---|---| +| `build_uni` | `:build` | `id`, `which`, `relationships` (list of struct), `places` (list of struct), `parties` (list of struct) | creates a new UNI resource instance | +| `define_uni` | `:define` | `characteristic_value_updates` (list of term) | defines the UNI | +| `get_uni_by_id` | `:read` | `id` | read a service or resource instance | +| `relate_uni` | `:relate` | `relationships` (list of struct) | relates the UNI with other instances (e.g. NTD, NBN Ethernet access) | diff --git a/documentation/domains/access.md b/documentation/domains/access.md new file mode 100644 index 0000000..bda39f5 --- /dev/null +++ b/documentation/domains/access.md @@ -0,0 +1,140 @@ + + +# The Access Domain + +Access is a small **DSL service** domain — a single fictional telco delivering broadband over copper to its own customers. It models the service the telco sells (`DslAccess`) and the physical infrastructure it runs on (`Shelf`, `Card`, `Path`, `Cable`). + +Use it as the warm-up. The pattern is small enough to hold in your head and rich enough to show every diffo modelling primitive you'll need. NBN (the second example) revisits the same primitives with multi-tenancy and a much longer delivery chain. + +## What's in here + +| Kind | Resource | Plays the role of | +|---|---|---| +| Service | `DslAccess` | the broadband product the telco sells to a subscriber | +| Resource | `Shelf` | a DSLAM frame at the exchange — slots for line cards | +| Resource | `Card` | a line card in a shelf slot — ports for customer paths | +| Resource | `Path` | the physical/logical access path from the exchange to the customer | +| Resource | `Cable` | a copper cable — carries pairs assigned to paths | + +`Shelf`, `Card`, and `Cable` each declare a **pool**: `:slots`, `:ports`, `:pairs`. Other resources consume those pools by being assigned a value — a card takes a slot from a shelf, a path takes a port from a card and a pair from a cable. + +## Topology + +The downstream view (the assignment direction) is a stack: + +``` +Shelf (slots pool) ─── Card (ports pool) ─── Path + │ + Cable (pairs pool) +``` + +A `Card` is **assigned a slot** from its `Shelf`; a `Path` is **assigned a port** from its `Card` and **assigned pairs** from `Cable`s along the route. The consuming resource names its upstream by the role it plays — `:shelf`, `:card`, `:cable` — and that name (`alias`) sits on the assignment record so the relationship can be walked from either side. + +The `DslAccess` service stands in front of the resources. It's the *what we sell*; the resources are the *what makes it work*. + +## Inheritance — bringing upstream context up + +Every consumer can read characteristics from what it's part of, without copying: + +- `Card.shelf` brings up the **`ShelfCharacteristic`** value of the shelf this card sits in (single-hop via `:shelf`). +- `Path.card` brings up the card it's plugged into (single-hop via `:card`). +- `Path.shelf` brings up the shelf — two-hop via `[:card, :shelf]`. +- `Shelf.cards` brings up every card sitting in a slot (reverse direction). + +These are derived live from the assignment graph; nothing is duplicated. + +## Service lifecycle + +`DslAccess` is a TMF service with a small state machine: + +``` +:initial ── qualify_dsl_result ──▶ :feasibilityChecked ── design_dsl_result ──▶ :reserved +``` + +Each transition is an Ash action. Each action shapes the JSON output — the service's `state` field reflects where you are. + +## Scenario walk-through + +The standard provisioning flow — set up the infrastructure, then qualify and design a service for a subscriber: + +```elixir +# 1. Exchange has a shelf with a slots pool +{:ok, shelf} = Access.build_shelf!(%{name: "QDONC-0001", places: [exchange]}) +Access.define_shelf!(shelf, %{ + characteristic_value_updates: [ + shelf: [device_name: "QDONC-0001", family: :ISAM, model: "ISAM7330", technology: :DSLAM], + slots: [first: 1, last: 10, assignable_type: "LineCard"] + ] +}) + +# 2. A line card consumes a slot from the shelf — it names its upstream Shelf :shelf +{:ok, card} = Access.build_card!(%{name: "line card 1"}) +Access.define_card!(card, %{ + characteristic_value_updates: [ + card: [family: :ISAM, model: "EBLT48", technology: :adsl2Plus], + ports: [first: 1, last: 48, assignable_type: "ADSL2+"] + ] +}) +Access.assign_slot!(shelf, %{ + assignment: %Assignment{assignee_id: card.id, alias: :shelf, operation: :auto_assign} +}) + +# 3. The subscriber's path through copper to the exchange +{:ok, path} = Access.build_path!(%{name: "82 Rathmullen", places: [customer_site, exchange]}) +Access.define_path!(path, %{ + characteristic_value_updates: [path: [technology: :copper, sections: 5]] +}) + +# 4. Cables along the route — each one assigns a pair to the path +Enum.each(cables, fn cable -> + Access.assign_pair!(cable, %{ + assignment: %Assignment{assignee_id: path.id, alias: :cable, operation: :auto_assign} + }) +end) + +# 5. The card assigns a port to the path — the path names its upstream Card :card +Access.assign_port!(card, %{ + assignment: %Assignment{assignee_id: path.id, alias: :card, operation: :auto_assign} +}) + +# 6. Now sell the service. Qualify first (do we have feasibility at this address?) +{:ok, dsl} = Access.qualify_dsl(%{parties: [customer, reseller], places: [customer_site]}) +{:ok, dsl} = Access.qualify_dsl_result(dsl, %{ + service_operating_status: :feasible, + places: [esa] +}) + +# 7. Design — set the service's characteristics. State goes to :reserved. +{:ok, dsl} = Access.design_dsl_result(dsl, %{ + characteristic_value_updates: [ + dslam: [device_name: "QDONC0001", model: "ISAM7330"], + aggregate_interface: [interface_name: "eth0", svlan_id: 3108], + circuit: [cvlan_id: 82], + line: [slot: 10, port: 5] + ] +}) +``` + +Read the path's brought-up context to see inheritance working live: + +```elixir +{:ok, path} = Access.get_path_by_id(path.id, load: [:card, :shelf, :port]) + +path.card # ⇒ [%CardCharacteristic.Value{family: :ISAM, model: "EBLT48", ...}] +path.shelf # ⇒ [%ShelfCharacteristic.Value{device_name: "QDONC-0001", ...}] (two-hop) +path.port # ⇒ [1] (the port number this path was assigned) +``` + +## Domain API reference + +See [_access_api.md](_access_api.md) for the auto-generated table of every `code_interface` function on `DiffoExample.Access` — function name, action, arguments, purpose. Regenerated with `mix gen.api_docs`. + +## What's underneath? + +You've been using diffo's Provider primitives the whole time — **specifications**, **typed characteristics**, **pools**, **assignments**, **relationships**, **state machines**, and the **TMF JSON encoding** that surfaces them. None of those are bespoke to Access; they all come from the [Provider domain](provider.md), which is where to look next once you've internalised this scenario. + +For a runnable walk-through of the scenario, open [diffo_example_access.livemd](diffo_example_access.livemd) in Livebook. diff --git a/documentation/domains/diffo_example_access.livemd b/documentation/domains/diffo_example_access.livemd new file mode 100644 index 0000000..587448d --- /dev/null +++ b/documentation/domains/diffo_example_access.livemd @@ -0,0 +1,345 @@ + + +# Diffo Example — Access Domain + +```elixir +Mix.install( + [ + {:diffo_example, "~> 0.2.3"}, + {:diffo, "~> 0.4.1"}, + {:kino, "~> 0.14"} + ], + config: [ + bolty: [ + {Bolt, + [ + uri: "bolt://localhost:7687", + auth: [username: "neo4j", password: "password"], + user_agent: "diffoExampleAccessLivebook/1", + pool_size: 15, + max_overflow: 3, + prefix: :default, + name: Bolt, + log: false, + log_hex: false + ]} + ] + ], + consolidate_protocols: false +) +``` + +## Overview + +Access is a small **DSL service domain** — a single fictional telco delivering broadband over copper to its own customers. It's the warm-up example: small enough to hold in your head, rich enough to show every diffo modelling primitive you'll need. + +This notebook walks the standard provisioning flow end-to-end: + +1. Set up exchange infrastructure (a shelf with line cards, a customer access path, cables). +2. Qualify a subscriber for the service. +3. Design the service against the infrastructure. +4. Read the inheritance chain that brings upstream context up to every consumer. + +See [access.md](access.md) for the narrative version. Once you've done both, [provider.md](provider.md) lifts the lid on the primitives you've been using the whole time. + +## Setting up + +Connect to Neo4j (running locally on the default port). It is helpful to keep the Neo4j browser open at as you go through the cells. + +```elixir +AshNeo4j.BoltyHelper.is_connected() +``` + +**Optional** — clear the database so the scenario builds from a clean slate: + +```elixir +AshNeo4j.Neo4jHelper.delete_all() +``` + +```elixir +alias Diffo.Provider +alias Diffo.Provider.Assignment +alias Diffo.Provider.Instance.{Place, Party, Relationship} +alias DiffoExample.Access +``` + +## The resources at a glance + +| Kind | Resource | Plays the role of | +| --- | --- | --- | +| Service | `DslAccess` | the broadband product the telco sells | +| Resource | `Shelf` | a DSLAM frame at the exchange — slots for line cards | +| Resource | `Card` | a line card — ports for customer paths | +| Resource | `Path` | the access path from the exchange to the customer | +| Resource | `Cable` | a copper cable — pairs assigned to paths | + +`Shelf`, `Card`, and `Cable` each declare a **pool** (`:slots`, `:ports`, `:pairs`). Each consumer takes a value from its upstream's pool and names the upstream by the role it plays — `:shelf`, `:card`, `:cable`. That name (the assignment's `alias`) lets the relationship be walked from either side. + +## Places and parties + +Real services exist somewhere and for someone. Set up the **places** (where) and **parties** (who) the scenario refers to: + +```elixir +customer_site = + Provider.create_place!(%{ + id: "1657363", + name: :addressId, + href: "place/telco/1657363", + referred_type: :GeographicAddress + }) + +exchange = + Provider.create_place!(%{ + id: "DONC", + name: :exchangeId, + href: "place/telco/DONC", + referred_type: :GeographicSite + }) + +esa = + Provider.create_place!(%{ + id: "DONC-0001", + name: :esaId, + href: "place/telco/DONC-0001", + referred_type: :GeographicLocation + }) + +individual = + Provider.create_party!(%{ + id: "IND000000897354", + name: :individualId, + referred_type: :Individual + }) + +reseller = + Provider.create_party!(%{ + id: "ORG000000123456", + name: :organizationId, + referred_type: :Organization + }) + +provider = + Provider.create_party!(%{ + id: "Access", + name: :organizationId, + referred_type: :Organization + }) + +customer_site_ref = %Place{id: customer_site.id, role: :CustomerSite} +exchange_ref = %Place{id: exchange.id, role: :NetworkSite} +esa_ref = %Place{id: esa.id, role: :ServingArea} + +customer_ref = %Party{id: individual.id, role: :Customer} +reseller_ref = %Party{id: reseller.id, role: :Reseller} +provider_ref = %Party{id: provider.id, role: :Provider} +``` + +## 1. The exchange has a shelf + +`Shelf` declares a `:slots` pool. We build it, then `:define` it with its identity and the bounds of the slots pool. + +```elixir +{:ok, shelf} = Access.build_shelf(%{ + name: "QDONC-0001", + places: [esa_ref], + parties: [provider_ref] +}) + +{:ok, shelf} = + Access.define_shelf(shelf, %{ + characteristic_value_updates: [ + shelf: [device_name: "QDONC-0001", family: :ISAM, model: "ISAM7330", technology: :DSLAM], + slots: [first: 1, last: 10, assignable_type: "LineCard"] + ] + }) +``` + +## 2. A line card consumes a slot + +The card has its own identity and a `:ports` pool. When it takes a slot from the shelf, it names its upstream `:shelf` — the alias names the **related resource the card is part of**, not the slot value. + +```elixir +{:ok, card} = Access.build_card(%{name: "line card 1"}) + +{:ok, card} = + Access.define_card(card, %{ + characteristic_value_updates: [ + card: [family: :ISAM, model: "EBLT48", technology: :adsl2Plus], + ports: [first: 1, last: 48, assignable_type: "ADSL2+"] + ] + }) + +{:ok, shelf} = + Access.assign_slot(shelf, %{ + assignment: %Assignment{ + assignee_id: card.id, + alias: :shelf, + operation: :auto_assign + } + }) +``` + +## 3. A path through copper to the exchange + +```elixir +{:ok, path} = + Access.build_path(%{ + name: "82 Rathmullen - DONC", + places: [customer_site_ref, exchange_ref, esa_ref], + parties: [provider_ref] + }) + +{:ok, path} = + Access.define_path(path, %{ + characteristic_value_updates: [ + path: [device_name: "82 Rathmullen - DONC", technology: :copper, sections: 5] + ] + }) +``` + +## 4. Cables along the route + +One cable for brevity (you can multiply this for a longer cable run). The path takes a pair from each cable and names its upstream `:cable`. + +```elixir +{:ok, cable} = + Access.build_cable(%{name: "lead in cable"}) + +{:ok, cable} = + Access.define_cable(cable, %{ + characteristic_value_updates: [ + cable: [pairs: 60, technology: :PIUT], + pairs: [first: 1, last: 60, assignable_type: "copper"] + ] + }) + +{:ok, _cable} = + Access.assign_pair(cable, %{ + assignment: %Assignment{ + assignee_id: path.id, + alias: :cable, + operation: :auto_assign + } + }) +``` + +## 5. The card assigns a port to the path + +```elixir +{:ok, _card} = + Access.assign_port(card, %{ + assignment: %Assignment{ + assignee_id: path.id, + alias: :card, + operation: :auto_assign + } + }) +``` + +The infrastructure is now in place. The path has a port on the card, the card has a slot on the shelf, the path has a pair from the cable. + +## 6. Qualify the subscriber + +Now sell the service. `qualify_dsl` creates a `DslAccess` in `:initial` state — checking we can serve this address at all: + +```elixir +{:ok, dsl} = + Access.qualify_dsl(%{ + parties: [customer_ref, reseller_ref], + places: [customer_site_ref] + }) + +dsl.service_state +``` + +`qualify_dsl_result` records the outcome — `:feasible` means we have copper in reach. The state moves to `:feasibilityChecked`. + +```elixir +{:ok, dsl} = + Access.qualify_dsl_result(dsl, %{ + service_operating_status: :feasible, + places: [esa_ref] + }) + +dsl.service_state +``` + +## 7. Design the service + +Set the service's typed characteristics — the actual configuration that gets provisioned to the exchange. The state moves to `:reserved`. + +```elixir +{:ok, dsl} = + Access.design_dsl_result(dsl, %{ + characteristic_value_updates: [ + dslam: [device_name: "QDONC0001", model: "ISAM7330"], + aggregate_interface: [interface_name: "eth0", svlan_id: 3108], + circuit: [cvlan_id: 82], + line: [slot: 10, port: 5] + ] + }) + +dsl.service_state +``` + +## 8. Inheritance — bringing upstream context up + +The path was assigned a port from the card; the card was assigned a slot from the shelf. Without copying anything, the path can read both: + +```elixir +{:ok, path} = Access.get_path_by_id(path.id, load: [:card, :shelf, :port]) + +%{ + card: path.card, + shelf: path.shelf, + port: path.port +} +``` + +`path.card` brings up the `CardCharacteristic` value via the `:card` alias on the port assignment. `path.shelf` brings it up two-hop via `[:card, :shelf]`. `path.port` is the port number itself. + +## TMF JSON + +The service and resources serialise to TMF-shaped JSON. Encode the path: + +```elixir +path +|> Jason.encode!() +|> Jason.decode!() +|> Jason.encode!(pretty: true) +|> IO.puts() +``` + +And the service: + +```elixir +{:ok, dsl} = Access.get_dsl_by_id(dsl.id) + +dsl +|> Jason.encode!() +|> Jason.decode!() +|> Jason.encode!(pretty: true) +|> IO.puts() +``` + +Notice the typed characteristics surfacing inline in `serviceCharacteristic` / `resourceCharacteristic`, the pool records (`slots`, `ports`, `pairs`) with their state, and the `serviceRelationship` / `resourceRelationship` arrays linking the assignment graph together. The whole TMF surface comes from the modelling — no encoder code anywhere in this notebook. + +## Exploring the graph + +In the Neo4j browser () try: + +```cypher +MATCH (n) RETURN n LIMIT 100; +``` + +You'll see Specification nodes, Instance nodes (Shelf, Card, Path, Cable, DslAccess), Characteristic nodes (one per declared typed characteristic), and the assignment and relationship edges between them. This is what the JSON above is materialised from. + +## What next? + +You've used every diffo modelling primitive — specifications, typed characteristics, pools, assignments, relationships, state machines. None of them are bespoke to Access. They all come from diffo's [Provider](provider.md) domain — open that next to see what's underneath. + +When you're ready for a richer example with multi-tenancy and a longer delivery chain, the [NBN domain](nbn.md) revisits the same primitives at scale. diff --git a/documentation/domains/diffo_example_nbn.livemd b/documentation/domains/diffo_example_nbn.livemd index 73b3dbe..f819a2c 100644 --- a/documentation/domains/diffo_example_nbn.livemd +++ b/documentation/domains/diffo_example_nbn.livemd @@ -4,15 +4,14 @@ SPDX-FileCopyrightText: 2025 diffo_example contributors -# Diffo Example - NBN Domain +# Diffo Example — NBN Domain ```elixir Mix.install( [ - {:diffo_example, "~> 0.2.1"}, + {:diffo_example, "~> 0.2.3"}, {:diffo, "~> 0.4.1"}, - {:kino, "~> 0.14"}, - {:req, "~> 0.5"} + {:kino, "~> 0.14"} ], config: [ bolty: [ @@ -20,7 +19,7 @@ Mix.install( [ uri: "bolt://localhost:7687", auth: [username: "neo4j", password: "password"], - user_agent: "diffoExampleLivebook/1", + user_agent: "diffoExampleNbnLivebook/1", pool_size: 15, max_overflow: 3, prefix: :default, @@ -36,272 +35,330 @@ Mix.install( ## Overview -[Diffo](https://github.com/diffo-dev/diffo) is a Telecommunications Management Forum (TMF) Service and Resource Manager, built for autonomous networks. It is implemented using the [Ash Framework](https://www.ash-hq.org) and stores data in Neo4j via [AshNeo4j](https://github.com/diffo-dev/ash_neo4j). +NBN is a fictional, simplified take on Australia's wholesale broadband network. Where [Access](access.md) is a single telco with its own DSL service, **NBN is wholesale** — one physical network shared by many Retail Service Providers (RSPs). -If you are new to Diffo, start with the [Diffo livebook](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2Fdiffo-dev%2Fdiffo%2Fblob%2Fdev%2Fdiffo.livemd) which introduces the core Provider concepts — Specification, Instance, Feature, Characteristic, Party, Place, and Relationship. +This notebook walks the provisioning flow when an RSP sells an NBN Ethernet access to a subscriber. Two things make NBN richer than Access: -Diffo includes a Provider Instance extension that lets you declare specialised TMF Services and Resources using a Spark DSL with very little Elixir code. The [Provider Instance Extension livebook](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2Fdiffo-dev%2Fdiffo%2Fblob%2Fdev%2Fdocumentation%2Fhow_to%2Fuse_diffo_provider_instance_extension.livemd) covers this in detail. +- **Multi-tenancy** — every resource is owned by an RSP and policy-scoped to its owner. +- **Longer delivery chain** — `NbnEthernet` (PRI) owns an `Avc` and a `Uni`. The `Avc` consumes a `:cvlan` from a `Cvc`. The `Cvc` consumes an `:svlan` from an `NniGroup`. The `Uni` consumes a `:port` from an `Ntd`. Every step is a chance to bring upstream context up. -The NBN domain in this example was built entirely with that DSL — a declarative model of a realistic NBN Ethernet access hierarchy with minimal custom Elixir, derived from a short domain description. It demonstrates how much can be expressed through the Provider extension alone. +See [nbn.md](nbn.md) for the narrative version including the named-vs-metrics characteristic pattern, the alias convention, and what each consumer inherits. -The NBN domain models a fictional NBN Ethernet access circuit and its constituent resources: +## Setting up -* **NbnEthernet** — the parent circuit resource (identified by a PRI) -* **UNI** — User Network Interface at the customer premises -* **AVC** — Access Virtual Circuit (dedicated, carries traffic between UNI and CVC) -* **NTD** — Network Termination Device (installed at customer premises, assigns ports to UNI) -* **CVC** — Connectivity Virtual Circuit (aggregates AVCs, terminates at NNI Group) -* **NNI Group** — group of NNIs at the point of interconnect -* **NNI** — Network-to-Network Interface - -## Installing Neo4j and Configuring Bolty - -Bolty is configured in the `Mix.install` block above — update the Neo4j credentials there if needed before evaluating. - -You need [Neo4j](https://neo4j.com/deployment-center/) installed and running. Verify the connection: +Connect to Neo4j (running locally on the default port). It is helpful to keep the Neo4j browser open at as you go through the cells. ```elixir AshNeo4j.BoltyHelper.is_connected() ``` -It is helpful to have a Neo4j browser open locally, typically at http://localhost:7474/browser/ - -**OPTIONAL** Clear the database before starting: +**Optional** — clear the database so the scenario builds from a clean slate: ```elixir AshNeo4j.Neo4jHelper.delete_all() ``` -## About NBN Co - -NBN (National Broadband Network) is Australia's wholesale fixed-line access network, operated by NBN Co. It provides standardised access products to Retail Service Providers (RSPs), who in turn deliver internet and other services to end customers. - -For the purpose of this example we are going to refer to a simplified, and re-imagined NBN Co as NBN. - -An RSP typically combines: - -* An **NBN Ethernet** access circuit (UNI + AVC) at the customer premises — the access and aggregation layer modelled in this domain -* A **home gateway** device installed at the UNI, which provides the customer's LAN, Wi-Fi, and sometimes voice -* Transport, aggregation, and edge infrastructure connecting the NNI to the RSP's network and on to the internet - -NBN connects the customer premises to the RSP's network via a Point of Interconnect (POI). The NNI sits at the POI, grouped into NNI Groups. AVCs carrying customer traffic are aggregated onto a CVC, which terminates at the NNI Group. The RSP purchases CVC capacity to carry the aggregate traffic of its customers at that POI. - -NBN delivers over several access technologies — FTTP, FTTN, FTTB, FTTC, HFC, Fixed Wireless, and Satellite — which determine which bandwidth profiles and speeds are available to a given premises. - -## NBN Ethernet Technology and Speeds - -The NBN domain defines Technology as an Ash Enum covering all NBN access types: - ```elixir -alias DiffoExample.Nbn.{Technology,Speeds} -Technology.values() -``` - -Speeds are derived from a bandwidth_profile and technology combination. For example: - -```elixir -Speeds.speeds(:home_fast, :FTTP) +alias Diffo.Provider.Assignment +alias Diffo.Provider.Instance.Relationship +alias DiffoExample.Nbn +alias DiffoExample.Nbn.{CvcMetrics, NniGroupMetrics} ``` -```elixir -Speeds.speeds(:home_hyperfast, :HFC) -``` +## The Retail Service Providers -```elixir -Speeds.speeds(:wireless_superfast, :FixedWireless) -``` +Seed the RSPs and pick one to operate as. Every resource we build will be owned by that RSP and isolated from resources owned by others. ```elixir -# returns :error for invalid combinations -Speeds.speeds(:home_fast, :FixedWireless) -``` - -## Multi-tenancy - -Each RSP operates in isolation — they can only see and manage the resources they own. This multi-tenancy is enforced at the Ash policy layer: every NBN resource is stamped with the owning RSP's EPID at creation, and subsequent reads, updates, and destroys are scoped to the record owner. - -RSP is modelled as a Party (using the `Diffo.Provider.BaseParty` fragment), with its EPID as the Party id. This means the `rsp_id` stamped on owned resources is a human-readable four-digit identifier rather than a UUID. - -Select the RSP you want to operate as for the rest of this livebook. All resources you build will be owned by that RSP and isolated from resources owned by others. - -```elixir -alias DiffoExample.Nbn -alias DiffoExample.Nbn.Rsp -import Jason, only: [encode: 2] DiffoExample.Nbn.Initializer.init() rsps = Nbn.list_rsps!() -Kino.DataTable.new(rsps, keys: [:id, :name, :short_name, :state]) +Kino.DataTable.new(rsps, keys: [:id, :short_name, :name, :state]) ``` ```elixir -rsp_input = Kino.Input.select( - "Operate as RSP", - Enum.map(rsps, fn rsp -> {rsp.name, Atom.to_string(rsp.short_name)} end) -) +rsp_input = + Kino.Input.select( + "Operate as RSP", + Enum.map(rsps, fn rsp -> {rsp.name, Atom.to_string(rsp.short_name)} end) + ) ``` ```elixir -actor = Enum.find(rsps, fn rsp -> rsp.name == Kino.Input.read(rsp_input) end) +actor = Enum.find(rsps, fn rsp -> Atom.to_string(rsp.short_name) == Kino.Input.read(rsp_input) end) actor ``` -## Maintaining Shareable Resources - -As an RSP we need maintain some shareable network resources: NNI, NNI Group, and CVC. +## 1. Shareable infrastructure — NNI Group + NNIs -We'll need these everywhere we operate, in advance of and sufficient for all the NBN Ethernet Accesses we have. We'll just build one of each right now. - -Build an NNI — the physical interconnect between the RSP and NBN: +An RSP builds (or has built for it) shareable infrastructure at each Point of Interconnect: an `NniGroup` containing `Nni`s, and a `Cvc` that terminates at the NNI Group. This gets done once per POI per RSP and serves many customers. ```elixir -alias DiffoExample.Nbn.{Nni, NniGroup, CVC} -nni = Nbn.build_nni!(%{}, actor: actor) -nni |> Jason.encode!(pretty: true) |> IO.puts +{:ok, nni_group} = Nbn.build_nni_group(%{}, actor: actor) + +{:ok, nni_group} = + Nbn.define_nni_group( + nni_group, + %{ + characteristic_value_updates: [ + nni_group: [group_name: "SYD-POI-01", location: "Sydney Olympic Park"], + svlans: [first: 1, last: 4000, assignable_type: "svlan"] + ] + }, + actor: actor + ) ``` -Build an NNI Group — a logical grouping of NNIs at a point of interconnect: +Add two `Nni`s (the physical interconnect ports) and relate them as `:contains`: ```elixir -nni_group = Nbn.build_nni_group!(%{}, actor: actor) -nni_group |> Jason.encode!(pretty: true) |> IO.puts +nni_ids = + for {port_id, capacity} <- [{"SYD-01-ETH-1", 10_000}, {"SYD-01-ETH-2", 10_000}] do + {:ok, nni} = Nbn.build_nni(%{}, actor: actor) + + {:ok, _} = + Nbn.define_nni( + nni, + %{ + characteristic_value_updates: [ + nni: [port_id: port_id, capacity: capacity] + ] + }, + actor: actor + ) + + nni.id + end + +{:ok, nni_group} = + Nbn.relate_nni_group( + nni_group, + %{ + relationships: + Enum.map(nni_ids, fn id -> + %Relationship{id: id, direction: :forward, type: :contains} + end) + }, + actor: actor + ) ``` -Define the NNI Group with an SVLAN assignment and relate the NNI: +## 2. Shareable infrastructure — CVC + +A `Cvc` takes an `:svlan` from the `NniGroup` and names its upstream `:nni_group` (the consumer-alias names the related resource): ```elixir -nni_group = Nbn.define_nni_group!(nni_group, %{ - characteristic_value_updates: [ - nni_group: [name: "SYD-POI-01", location: "Sydney Olympic Park"], - svlans: [first: 1, last: 4000, free: 4000, assignable_type: "svlan"] - ] -}, actor: actor) -nni_group = Nbn.relate_nni_group!(nni_group, %{ - relationships: [%Diffo.Provider.Instance.Relationship{id: nni.id, alias: :nni, type: :isAssigned}] -}, actor: actor) -nni_group |> Jason.encode!(pretty: true) |> IO.puts +{:ok, cvc} = Nbn.build_cvc(%{}, actor: actor) + +{:ok, cvc} = + Nbn.define_cvc( + cvc, + %{ + characteristic_value_updates: [ + cvc: [bandwidth: 1000], + cvlans: [first: 1, last: 4000, assignable_type: "cvlan"] + ] + }, + actor: actor + ) + +{:ok, _nni_group} = + Nbn.assign_svlan( + nni_group, + %{ + assignment: %Assignment{ + assignee_id: cvc.id, + alias: :nni_group, + operation: :auto_assign + } + }, + actor: actor + ) ``` -Build a CVC — the aggregation virtual circuit that terminates at the NNI Group: +## 3. Per-customer infrastructure — NTD + UNI + +The `Ntd` is the device installed at the customer premises — NBN-managed, no RSP `actor:` needed. The `Uni` consumes a `:port` from it and names its upstream `:ntd`. ```elixir -cvc = Nbn.build_cvc!(%{}, actor: actor) -cvc = Nbn.relate_cvc!(cvc, %{ - relationships: [%Diffo.Provider.Instance.Relationship{id: nni_group.id, alias: :nni_group, type: :isAssigned}] -}, actor: actor) -cvc |> Jason.encode!(pretty: true) |> IO.puts -``` +{:ok, ntd} = Nbn.build_ntd(%{}) -## Provisioning NBN Ethernet +{:ok, ntd} = + Nbn.define_ntd(ntd, %{ + characteristic_value_updates: [ + ntd: [model: "Sercomm CG4000A", serial_number: "SCOMA1A057A2", technology: :FTTP], + ports: [first: 1, last: 4, assignable_type: "port"] + ] + }) -For each customer site we want to provide service to, we need an NBN Ethernet composite resource, involving an NTD, UNI, AVC and CVC. +{:ok, uni} = Nbn.build_uni(%{}) -The NTD is NBN infrastructure — built and managed by NBN, visible to any RSP. It may not exist at a new or existing customer site, so may be built on demand by NBN. +{:ok, uni} = + Nbn.define_uni(uni, %{ + characteristic_value_updates: [ + uni: [port: 1, encapsulation: "DSCP Mapped", technology: :FTTP] + ] + }) + +{:ok, _ntd} = + Nbn.assign_port(ntd, %{ + assignment: %Assignment{ + assignee_id: uni.id, + alias: :ntd, + operation: :auto_assign + } + }) +``` -Build an NTD — the device installed at the customer premises: +## 4. The AVC + +An `Avc` belongs to one RSP. It consumes a `:cvlan` from the RSP's `Cvc` and names its upstream `:cvc`: ```elixir -alias DiffoExample.Nbn.{Ntd, Uni, Avc, NbnEthernet} -ntd = Nbn.build_ntd!(%{}) -ntd = Nbn.define_ntd!(ntd, %{ - characteristic_value_updates: [ - ntd: [technology: :FTTP], - ports: [first: 1, last: 4, free: 4, assignable_type: "port"] - ] -}) -ntd |> Jason.encode!(pretty: true) |> IO.puts +{:ok, avc} = Nbn.build_avc(%{}, actor: actor) + +{:ok, avc} = + Nbn.define_avc( + avc, + %{ + characteristic_value_updates: [avc: [bandwidth_profile: :home_fast]] + }, + actor: actor + ) + +{:ok, _cvc} = + Nbn.assign_cvlan( + cvc, + %{ + assignment: %Assignment{ + assignee_id: avc.id, + alias: :cvc, + operation: :auto_assign + } + }, + actor: actor + ) ``` -Build a UNI — the interface at the customer premises — and assign a port from the NTD: +## 5. The NBN Ethernet access (PRI) + +The PRI is the service the RSP sells. It owns the `Avc` and the `Uni` via two `:owns` relationships — aliased `:circuit` (the role the AVC plays — the access virtual circuit) and `:port` (the role the UNI plays — the customer's port): ```elixir -uni = Nbn.build_uni!(%{}) -alias Diffo.Provider.Assignment -ntd = Nbn.assign_port!(ntd, %{ - assignment: %Assignment{assignee_id: uni.id, operation: :auto_assign} -}) -ntd |> Jason.encode!(pretty: true) |> IO.puts +{:ok, pri} = Nbn.build_nbn_ethernet(%{}, actor: actor) + +{:ok, _pri} = + Nbn.relate_nbn_ethernet( + pri, + %{ + relationships: [ + %Relationship{id: avc.id, direction: :forward, type: :owns, alias: :circuit}, + %Relationship{id: uni.id, direction: :forward, type: :owns, alias: :port} + ] + }, + actor: actor + ) ``` -Relate the UNI back to the NTD so it can mine technology and port from it: +## 6. Inheritance — the brought-up delivery chain + +Load the PRI with all four inherited characteristics. Each one resolves through the assignment and relationship graph live: ```elixir -uni = Nbn.relate_uni!(uni, %{ - relationships: [%Diffo.Provider.Instance.Relationship{id: ntd.id, alias: :ntd, type: :isAssigned}] -}) -uni = Nbn.mine_uni!(uni, %{}) -uni |> Jason.encode!(pretty: true) |> IO.puts +{:ok, pri} = + Nbn.get_nbn_ethernet_by_id( + pri.id, + load: [:avc, :uni, :cvc, :ntd], + actor: actor + ) + +%{ + avc: pri.avc, + uni: pri.uni, + cvc: pri.cvc, + ntd: pri.ntd +} ``` -Build an AVC and assign it a CVLAN from the CVC: +Single-hop (`:avc`, `:uni`) goes via the `:circuit` and `:port` owns relationships. Two-hop (`:cvc`, `:ntd`) walks the relationship hop then back through the `:cvc` and `:ntd` assignment aliases. All singular — the AssignmentRelationship's `[target_id, alias]` identity guarantees at most one upstream per hop. + +The AVC and CVC can also bring up their own context: ```elixir -avc = Nbn.build_avc!(%{}, actor: actor) -avc = Nbn.define_avc!(avc, %{ - characteristic_value_updates: [avc: [bandwidth_profile: :home_ultrafast]] -}, actor: actor) -cvc = Nbn.assign_cvlan!(cvc, %{ - assignment: %Assignment{assignee_id: avc.id, operation: :auto_assign} -}, actor: actor) -avc = Nbn.mine_avc!(avc, %{}, actor: actor) -avc |> Jason.encode!(pretty: true) |> IO.puts +{:ok, avc} = Nbn.get_avc_by_id(avc.id, load: [:cvc, :nni_group], actor: actor) + +%{ + cvc: avc.cvc, # single-hop via :cvc + nni_group: avc.nni_group # two-hop via [:cvc, :nni_group] +} ``` -Now build the top-level NBN Ethernet access and relate it to both the UNI and AVC: +## 7. Metrics — the local KPIs -```elixir -pri = Nbn.build_nbn_ethernet!(%{}, actor: actor) -pri = Nbn.relate_nbn_ethernet!(pri, %{ - relationships: [ - %Diffo.Provider.Instance.Relationship{id: uni.id, alias: :uni, type: :isAssigned}, - %Diffo.Provider.Instance.Relationship{id: avc.id, alias: :avc, type: :isAssigned} - ] -}, actor: actor) -pri = Nbn.mine_nbn_ethernet!(pri, %{}, actor: actor) -pri |> Jason.encode!(pretty: true) |> IO.puts -``` +Metrics are local-only characteristics that don't propagate. Read the CVC's metrics and the NNI Group's metrics directly: -The `mine` action on NbnEthernet extracts technology from the UNI and bandwidth_profile from the AVC and derives the speeds automatically. +```elixir +cvc_metrics = + CvcMetrics + |> Ash.Query.filter_input(instance_id: cvc.id) + |> Ash.Query.load(:value) + |> Ash.read_one!(actor: actor) -## Exploring the Graph +cvc_metrics.value +``` -You can query all nodes and relationships in Neo4j browser with: +```elixir +nni_group_metrics = + NniGroupMetrics + |> Ash.Query.filter_input(instance_id: nni_group.id) + |> Ash.Query.load(:value) + |> Ash.read_one!(actor: actor) -```cypher -MATCH (n1)-[r]->(n2) RETURN r, n1, n2 LIMIT 50 +nni_group_metrics.value ``` -Or from Elixir: +`utilization = cvcs_total_bandwidth / nnis_total_bandwidth` — demand over capacity at the NNI Group. Expected 0–1 under normal provisioning; >1 under deliberate oversubscription. + +`NniGroup.nnis` brings up the typed values of each contained NNI (low-cardinality, so the list is fine): ```elixir -AshNeo4j.Cypher.run("MATCH (n1)-[r]->(n2) RETURN r, n1, n2 LIMIT 50") +{:ok, nni_group} = Nbn.get_nni_group_by_id(nni_group.id, load: [:nnis], actor: actor) +nni_group.nnis ``` -## JSON API - -The NBN domain exposes a JSON API via `Plug.Cowboy` on port 4000. Start the server in your application before evaluating these cells. +## 8. TMF JSON -First check the catalog — all NBN specifications are initialised on startup: +The PRI serialises to TMF-shaped JSON. The metrics characteristic is inline; the `:owns` relationships surface in `resourceRelationship`; the typed characteristics are surfaced post-#169. ```elixir -Req.get!("http://localhost:4000/catalog", decode_body: false).body |> IO.puts() +pri +|> Jason.encode!() +|> Jason.decode!() +|> Jason.encode!(pretty: true) +|> IO.puts() ``` -Now retrieve all NBN Ethernet instances: +The NNI Group's JSON shows the `:contains` relationships to NNIs alongside the `:assignedTo` relationships to CVCs, the `svlans` pool, and the `metrics` characteristic: ```elixir -Req.get!("http://localhost:4000/nbnEthernet", decode_body: false).body |> IO.puts() +nni_group +|> Jason.encode!() +|> Jason.decode!() +|> Jason.encode!(pretty: true) +|> IO.puts() ``` -Or fetch the one we provisioned above by id: +## Exploring the graph -```elixir -Req.get!("http://localhost:4000/nbnEthernet/#{pri.id}", decode_body: false).body |> IO.puts() +```cypher +MATCH (n)-[r]->(m) RETURN n, r, m LIMIT 100; ``` -## What Next? +You'll see Specification nodes (one per resource type), Instance nodes (the things we just built), Characteristic nodes (typed and pool), and the assignment / relationship edges between them. Filter by the RSP's stamp to see one tenant's slice. + +## What next? -You've provisioned a complete NBN Ethernet access — NTD, UNI, AVC, CVC, NNI Group, and NNI — and seen how the `mine` actions propagate technology, speeds, CVLAN and port assignments up the resource hierarchy automatically. +You've used multi-tenancy, the long delivery chain, the named-vs-metrics pattern, the alias convention, and the cross-resource inheritance — every pattern from the [#49 design](https://github.com/diffo-dev/diffo_example/issues/49). -The Access domain in `diffo_example` shows a similar pattern for DSL access services. Explore `lib/access/` for copper-network equivalents (Cable, Card, Path, Shelf). +Open [provider.md](provider.md) if you want to revisit the primitives now that you've seen them at scale, or [access.md](access.md) for the simpler single-tenant warm-up. -If you find Diffo useful please visit and star on [GitHub](https://github.com/diffo-dev/diffo/). +When you're ready to model your own domain, start with one specification, declare its characteristics, decide whether anything pools, and watch the JSON come out the other side. The structure carries you a long way. diff --git a/documentation/domains/nbn.md b/documentation/domains/nbn.md index eb3bcc0..1d591a1 100644 --- a/documentation/domains/nbn.md +++ b/documentation/domains/nbn.md @@ -6,18 +6,207 @@ SPDX-License-Identifier: MIT # The NBN Domain -## The Perentie Ecosystem +NBN is a fictional, simplified take on Australia's wholesale broadband network. Where [Access](access.md) was a single telco running its own DSL service, **NBN is wholesale** — a single physical network shared by many Retail Service Providers (RSPs) who in turn sell to end customers. That changes the modelling job in two ways: -NBN Co operates as **Perentie** — Australia's largest monitor lizard, ancient and continent-wide. Perentie owns the territory. It does not compete with the animals moving through its country; it simply defines the ground they all walk on. +- **Multi-tenancy** — every resource is owned by an RSP and policy-scoped to its owner. +- **Longer delivery chain** — the customer's broadband signal hops through more layers, which is what the inheritance and metrics patterns from issue [#49](https://github.com/diffo-dev/diffo_example/issues/49) are designed for. -The RSPs are the spirit animals of the ecosystem, each finding their niche in Perentie's range: +NBN is the second example for that reason. Do [Access](access.md) first — same primitives, simpler stage. + +## The Retail Service Providers + +The example ships with seven fictional RSPs as their spirit animals — the cast of Australian wildlife competing for niches in the network: | RSP | Spirit Animal | Inspiration | | ------------------ | ------------------ | ------------------------------------------------------------------------------------------------------------------------ | -| Wedge-tail Telecom | Wedge-tailed Eagle | Australia's apex aerial predator — dominant, territorial, commands every landscape it surveys | -| Quokka Connect | Quokka | Famously friendly, genuinely Australian, radiates good energy — operates in WA under bilateral agreement with Perentie | +| Wedge-tail Telecom | Wedge-tailed Eagle | Australia's apex aerial predator — dominant, territorial, commands every landscape it surveys | +| Quokka Connect | Quokka | Famously friendly, genuinely Australian, radiates good energy — operates in WA under bilateral agreement | | Ibis Telecom | White Ibis | Beloved in spite of its reputation, scrappy, surprisingly capable | | Taipan Group | Taipan | Carries the TPG initials; fast, precise, not to be underestimated | | Echidna Networks | Echidna | Prickly on the surface, uniquely capable beneath it | | Dugong Digital | Dugong | Slow and steady, but still very much alive | | Lyrebird | Lyrebird | Mimics everything, loops back on itself, endlessly clever | + +`Rsp` is modelled as a Party (using diffo's `BaseParty` fragment), with a four-digit EPID as its id. Every NBN resource is stamped with the owning RSP's EPID at creation, and policies scope reads, updates, and destroys to the owner. + +## What's in here + +### Service + +| Resource | Plays the role of | +| --- | --- | +| `NbnEthernet` (PRI) | the wholesale broadband product an RSP buys from NBN for one customer site | + +### Resources + +| Resource | Plays the role of | Pool | +| --- | --- | --- | +| `Uni` | the customer-side network interface | — | +| `Avc` | dedicated Access Virtual Circuit (the customer's traffic) | — | +| `Ntd` | Network Termination Device installed at the premises | `:ports` | +| `Cvc` | Connectivity Virtual Circuit (aggregates many AVCs on a pipe) | `:cvlans` | +| `NniGroup` | grouping of NNIs at a single Point of Interconnect | `:svlans` | +| `Nni` | physical Network-to-Network Interface at the POI | — | + +## Topology + +Two views, both useful. + +### The provisioning view — what's assigned to what + +``` +NniGroup (svlans) ──── CVC (cvlans) ──── AVC + │ + NbnEthernet (PRI) ──── UNI ──── NTD (ports) + ▲ │ + └───────┘ +``` + +`NniGroup` contains many `Nni`s (low-cardinality `:contains` relationship) and assigns one `:svlan` to each `Cvc`. Each `Cvc` assigns ~4000 `:cvlans` to `Avc`s. Each `Ntd` assigns one of its few `:ports` to a `Uni`. The `NbnEthernet` (PRI) owns one `Avc` and one `Uni` per customer site via two `:owns` relationships aliased `:circuit` (for the AVC) and `:port` (for the UNI). + +### The consumer's view — what each thing is part of + +Each consumer names its upstream by the role it plays: + +| Consumer | Upstream | Consumer's `alias` for it | +| --- | --- | --- | +| `Avc` | `Cvc` | `:cvc` | +| `Cvc` | `NniGroup` | `:nni_group` | +| `Uni` | `Ntd` | `:ntd` | +| `NbnEthernet` | `Avc` | `:circuit` | +| `NbnEthernet` | `Uni` | `:port` | + +These aliases sit on the assignment / relationship records. They're what the inheritance walks follow. See [provider.md](provider.md) for what alias means and why it lands on the consumer's side. + +## Two characteristics per resource + +A pattern that emerges at NBN's cardinality: each resource carries **two** typed characteristics. + +- A **named characteristic** (`cvc`, `nni_group`, `avc`, …) — identity and context that *can* be inherited downstream. +- A **`metrics` characteristic** — local KPIs (counts, totals, utilization) that **must not** be inherited. Downstream consumers want context, not their parent's sibling-count. + +| Resource | Named | Metrics | +| --- | --- | --- | +| `Avc` | `avc` | — (leaf) | +| `Cvc` | `cvc` | `metrics` — `avcs_count`, `avcs_total_bandwidth` | +| `NniGroup` | `nni_group` | `metrics` — `cvcs_count`/`cvcs_total_bandwidth`, `nnis_count`/`nnis_total_bandwidth`, `utilization` | +| `Ntd` | `ntd` | — (low N) | +| `Uni` | `uni` | — (leaf) | +| `NbnEthernet` | `pri` | — (the service is a leaf) | + +The cardinality on the inverse direction is what drives this. A CVC has ~4000 AVCs — listing them all as `avcs[]` would explode the JSON. A summary keeps the KPIs without the explosion. Where cardinality is low (an `NniGroup` has a handful of NNIs, an `Ntd` has a handful of UNIs) you can have both — the NNIs surface as a `:contains` relationship and as an aggregate. + +## Inheritance — what each consumer can bring up + +Forward, via assignment (singular — each consumer has one upstream): + +- `Avc.cvc` — single-hop via `:cvc` +- `Avc.nni_group` — two-hop via `[:cvc, :nni_group]` +- `Cvc.nni_group` — single-hop via `:nni_group` + +Forward, via relationship (singular): + +- `NbnEthernet.avc` — single-hop via the `:circuit` owns relationship +- `NbnEthernet.uni` — single-hop via the `:port` owns relationship +- `NbnEthernet.cvc` — two-hop: `:circuit` owns relationship, then `:cvc` assignment back to the CVC +- `NbnEthernet.ntd` — two-hop: `:port` owns relationship, then `:ntd` assignment back to the NTD + +Reverse, low-N (returns a list): + +- `NniGroup.nnis` — every NNI this group contains, via `:contains` + +All of these return the typed `.Value{}` struct (the inner payload), not the wrapping record — same shape that surfaces in TMF JSON's `value` field. + +## Scenario walk-through + +The provisioning flow when an RSP sells an NBN Ethernet access to a subscriber: + +```elixir +# Acting as one RSP (after Initializer.init seeded them) +actor = Nbn.list_rsps!() |> Enum.find(&(&1.short_name == :quokka)) + +# 1. Shareable infrastructure (built once per RSP per POI) +{:ok, nni_group} = Nbn.build_nni_group(%{}, actor: actor) +{:ok, nni_group} = Nbn.define_nni_group(nni_group, %{ + characteristic_value_updates: [ + nni_group: [group_name: "SYD-POI-01", location: "Sydney Olympic Park"], + svlans: [first: 1, last: 4000, assignable_type: "svlan"] + ] +}, actor: actor) + +# 2. CVC takes an svlan from the NNI Group, naming the upstream :nni_group +{:ok, cvc} = Nbn.build_cvc(%{}, actor: actor) +{:ok, cvc} = Nbn.define_cvc(cvc, %{ + characteristic_value_updates: [ + cvc: [bandwidth: 1000], + cvlans: [first: 1, last: 4000, assignable_type: "cvlan"] + ] +}, actor: actor) +{:ok, _nni_group} = Nbn.assign_svlan(nni_group, %{ + assignment: %Assignment{assignee_id: cvc.id, alias: :nni_group, operation: :auto_assign} +}, actor: actor) + +# 3. Per-customer infrastructure — NTD at the premises, UNI on the NTD +{:ok, ntd} = Nbn.build_ntd(%{}) # NBN-managed, no RSP actor +{:ok, ntd} = Nbn.define_ntd(ntd, %{ + characteristic_value_updates: [ + ntd: [model: "Sercomm CG4000A", technology: :FTTP], + ports: [first: 1, last: 4, assignable_type: "port"] + ] +}) +{:ok, uni} = Nbn.build_uni(%{}) +{:ok, _ntd} = Nbn.assign_port(ntd, %{ + assignment: %Assignment{assignee_id: uni.id, alias: :ntd, operation: :auto_assign} +}) + +# 4. AVC takes a cvlan from the CVC, naming the upstream :cvc +{:ok, avc} = Nbn.build_avc(%{}, actor: actor) +{:ok, _avc} = Nbn.define_avc(avc, %{ + characteristic_value_updates: [avc: [bandwidth_profile: :home_fast]] +}, actor: actor) +{:ok, _cvc} = Nbn.assign_cvlan(cvc, %{ + assignment: %Assignment{assignee_id: avc.id, alias: :cvc, operation: :auto_assign} +}, actor: actor) + +# 5. The PRI (NbnEthernet) owns the AVC :circuit and the UNI :port +{:ok, pri} = Nbn.build_nbn_ethernet(%{}, actor: actor) +{:ok, _pri} = Nbn.relate_nbn_ethernet(pri, %{ + relationships: [ + %Relationship{id: avc.id, direction: :forward, type: :owns, alias: :circuit}, + %Relationship{id: uni.id, direction: :forward, type: :owns, alias: :port} + ] +}, actor: actor) +``` + +Then read the full chain — every brought-up characteristic resolves through the assignment and relationship graph: + +```elixir +{:ok, pri} = Nbn.get_nbn_ethernet_by_id(pri.id, load: [:avc, :uni, :cvc, :ntd], actor: actor) + +%{ + avc: pri.avc, # %AvcCharacteristic.Value{bandwidth_profile: :home_fast} + uni: pri.uni, # %UniCharacteristic.Value{...} + cvc: pri.cvc, # %CvcCharacteristic.Value{bandwidth: 1000} (two-hop) + ntd: pri.ntd # %NtdCharacteristic.Value{technology: :FTTP} (two-hop) +} +``` + +And read the metrics — the CVC's view of its AVCs, the NNI Group's view of its CVCs and NNIs: + +```elixir +DiffoExample.Nbn.CvcMetrics +|> Ash.Query.filter_input(instance_id: cvc.id) +|> Ash.Query.load(:value) +|> Ash.read_one!() +# %CvcMetrics.Value{avcs_count: 1, avcs_total_bandwidth: 500} +``` + +## Domain API reference + +See [_nbn_api.md](_nbn_api.md) for the auto-generated table of every `code_interface` function on `DiffoExample.Nbn` — function name, action, arguments, purpose. Regenerated with `mix gen.api_docs`. + +## What next? + +You've seen the wholesale story — multi-tenant ownership, longer delivery chain, named-vs-metrics characteristics, full inheritance with the alias convention. NBN is a deliberately small slice of the real wholesale problem; the next layer down (NBN's own internal `fibreAccess`, `aggregation`, switching) lives in separate domains, modelled or not by their respective owners. The contract between them is the same shape diffo uses internally — expectations and action APIs — which at organisational boundaries aligns with **NaaS** (Network as a Service, the TM Forum standard for inter-provider interfaces). + +For a runnable walk-through of the scenario, open [diffo_example_nbn.livemd](diffo_example_nbn.livemd) in Livebook. diff --git a/documentation/domains/provider.md b/documentation/domains/provider.md new file mode 100644 index 0000000..d8e0cee --- /dev/null +++ b/documentation/domains/provider.md @@ -0,0 +1,76 @@ + + +# The Provider Domain + +The Provider domain is **diffo's own domain** — the base resources and DSL on top of which Access, NBN, and any other domain you build are written. You've used every Provider primitive in the [Access scenario](access.md) without naming them; this page names what you've been using. + +Provider isn't yours to extend. You don't `define` actions on it or relate to its resources directly. You build *with* it. + +## What Provider gives you + +Every `Instance` resource in your domain (`Shelf`, `Card`, `DslAccess`, `NbnEthernet` …) is a `BaseInstance`-derived Ash resource. `BaseInstance` is a Provider fragment that brings in a complete TMF Service/Resource surface — id, href, lifecycle state, places, parties, characteristics, features, relationships, the JSON encoder, the graph layer. Mix it in and a small DSL on top is enough to model your domain. + +The primitives, in the order you met them in the Access scenario: + +### Specification + +Every `Instance` declares a `Specification` — the *type* of thing it is (`shelf`, `dslAccess`, `nbnEthernet`). Specifications carry a stable id, a name, a category, a description, and the TMF kind (`:serviceSpecification` or `:resourceSpecification`). They show up in every TMF JSON payload as `serviceSpecification` / `resourceSpecification`. You declared one with `specification do … end` inside your `provider do` block. + +### Instance + +A concrete thing of a specification — a particular `shelf` named "QDONC-0001" with a unique id. You created instances with `:build` actions (e.g. `Access.build_shelf/1`). Each instance is a node in the graph. + +### Characteristic + +Typed value slots on an instance — the actual data the consumer cares about. You declared them with `characteristics do characteristic :foo, FooCharacteristic end`. Each is its own little Ash resource (a `BaseCharacteristic`-derived module with attributes and a `:value` calculation), and the Provider encoder lifts it inline as `{name: foo, value: {…}}` in the instance's `serviceCharacteristic` / `resourceCharacteristic` array. + +There's a sister kind — **metrics characteristics** — for local, non-inheritable aggregates (e.g. `CvcMetrics.avcs_count`). Same shape; the `:value` calc just reads the graph instead of stored attributes. + +### Pool + +A range of allocatable values declared on an instance — `:slots`, `:ports`, `:cvlans`, `:pairs`. You declared one with `pools do pool :slots, :slot end`: the first atom names the pool (the AssignableCharacteristic), the second names the *thing* being allocated. Pools surface in JSON as their own characteristic record showing first/last/free/algorithm. + +### Assignment + +A consumer takes a value from another instance's pool — Card takes a `:slot` from Shelf, Path takes a `:port` from Card, Path takes a `:pair` from Cable. Each assignment is an `AssignmentRelationship` edge with the value and an alias. The **alias is the consumer's name for the upstream related resource it's part of** — Card sets `alias: :shelf` because it's part of a Shelf. That alias is the key for inheritance walks. + +### Relationship + +Arbitrary edges between instances — `:contains`, `:owns`, `:isPartOf`. You created them with `:relate` actions taking `Relationship` structs. Provider stores these as either `Provider.Relationship` (mutable characteristics) or `DefinedSimpleRelationship` (one frozen characteristic at creation, used by the Assigner). They surface as `resourceRelationship` / `serviceRelationship` entries in JSON. + +### Feature + +Optional capabilities on an instance (`:dynamic_line_management` on `DslAccess`). Declared with `features do feature :name, is_enabled?: bool end`, optionally carrying their own characteristics. + +### Place and Party + +Where and who. Declared with `places do … end` and `parties do … end` blocks on an instance, populated by passing `%Place{id, role}` and `%Party{id, role}` structs to build/qualify actions. Surface as TMF `place` and `relatedParty` references. + +### State machine + +Services and resources both carry lifecycle state. You declared transitions with `state_machine do transitions do … end end`; each action that should transition uses `change transition_state(:new_state)` or `change set_attribute(:resource_state, …)`. The current state surfaces in JSON as `state` (service) or `lifecycleState` (resource). + +## What the encoder does + +You never wrote serialiser code. Once your instance has its specification, characteristics, features, places, parties, relationships and lifecycle state, the Provider encoder maps the whole graph into TMF-compliant JSON every time you call `Jason.encode!(instance)`. The order, naming, and nesting conventions all come from the Provider's `jason do` configurations. Customise per resource if you need to; otherwise just declare the model and the JSON shape follows. + +## Bringing context up — the inheritance calcs + +Provider doesn't (yet) ship the inheritance calculations you used in Access — `Card.shelf`, `Path.card`, `Path.shelf`. Those live in this example codebase as [InheritedCharacteristicViaAssignment](../../lib/diffo_example/calculations/inherited_characteristic_via_assignment.ex) and [InheritedCharacteristicViaRelationship](../../lib/diffo_example/calculations/inherited_characteristic_via_relationship.ex). They're small (one Ash calc each) and worth yarning upstream as Provider primitives — sister calcs to the existing `InheritedPlace` and `InheritedParty` that Provider already ships. + +## Going deeper into Provider + +This page is an orientation. For a deeper look at Provider itself: + +- The [Diffo livebook](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2Fdiffo-dev%2Fdiffo%2Fblob%2Fdev%2Fdiffo.livemd) — walks the Provider concepts directly (Specification, Instance, Feature, Characteristic, Party, Place, Relationship). +- The [Provider Instance Extension livebook](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2Fdiffo-dev%2Fdiffo%2Fblob%2Fdev%2Fdocumentation%2Fhow_to%2Fuse_diffo_provider_instance_extension.livemd) — walks the DSL you used to declare the Access resources. + +## What next? + +You've seen the primitives in action ([Access](access.md)) and you've seen what they are (here). The next move is to bring your own domain — even a sketch is enough — and model it the way Access does. Start with one specification, declare its characteristics, decide whether anything pools, sketch a build action, and watch the JSON come out the other side. + +When you're ready for a richer example, the [NBN domain](nbn.md) revisits the same primitives at scale: multi-tenancy, a longer delivery chain, and the cross-resource inheritance that Access only hinted at. diff --git a/lib/access/calculations/shelf_total_ports.ex b/lib/access/calculations/shelf_total_ports.ex index 08b12a6..106805d 100644 --- a/lib/access/calculations/shelf_total_ports.ex +++ b/lib/access/calculations/shelf_total_ports.ex @@ -7,9 +7,10 @@ defmodule DiffoExample.Access.Calculations.ShelfTotalPorts do Sums the `:ports` pool capacity across every card a shelf has assigned a slot to. - For each outgoing slot-assignment (alias `:slot`, source = shelf), looks - up the assigned card's `AssignableCharacteristic` for the `:ports` pool - and sums `(last - first + 1)` across all of them. + For each outgoing slot-assignment (cards consumer-alias their upstream + Shelf relationship as `:shelf`), looks up the assigned card's + `AssignableCharacteristic` for the `:ports` pool and sums + `(last - first + 1)` across all of them. Local-to-this-repo for now. Could in time become a more general diffo-side primitive (`SumPoolCapacityOfAssignees` or similar) once the @@ -26,7 +27,7 @@ defmodule DiffoExample.Access.Calculations.ShelfTotalPorts do Enum.map(records, fn shelf -> assignments = Diffo.Provider.AssignmentRelationship - |> Ash.Query.filter_input(source_id: shelf.id, alias: :slot) + |> Ash.Query.filter_input(source_id: shelf.id, alias: :shelf) |> Ash.read!(domain: Diffo.Provider) Enum.reduce(assignments, 0, fn assignment, acc -> diff --git a/lib/access/resources/cable.ex b/lib/access/resources/cable.ex index 73ca03c..c481d9f 100644 --- a/lib/access/resources/cable.ex +++ b/lib/access/resources/cable.ex @@ -70,25 +70,21 @@ defmodule DiffoExample.Access.Cable do argument :characteristic_value_updates, {:array, :term} change set_attribute(:resource_state, :operating) - change DiffoExample.Changes.Define + change Diffo.Provider.Changes.Define end update :relate do description "relates the cable with other instances" argument :relationships, {:array, :struct} - change after_action(fn changeset, result, _context -> - with {:ok, result} <- Relationship.relate_instance(result, changeset), - {:ok, result} <- Access.get_cable_by_id(result.id), - do: {:ok, result} - end) + change Diffo.Provider.Changes.Relate end update :assign_pair do description "relates the cable with an instance by assigning a pair" argument :assignment, :struct, constraints: [instance_of: Assignment] - change {DiffoExample.Changes.Assign, pool: :pairs} + change {Diffo.Provider.Changes.Assign, pool: :pairs} end end end diff --git a/lib/access/resources/card.ex b/lib/access/resources/card.ex index cc160ad..4172981 100644 --- a/lib/access/resources/card.ex +++ b/lib/access/resources/card.ex @@ -70,39 +70,40 @@ defmodule DiffoExample.Access.Card do argument :characteristic_value_updates, {:array, :term} change set_attribute(:resource_state, :operating) - change DiffoExample.Changes.Define + change Diffo.Provider.Changes.Define end update :relate do description "relates the card with other instances" argument :relationships, {:array, :struct} - change DiffoExample.Changes.Relate + change Diffo.Provider.Changes.Relate end update :assign_port do description "relates the card with an instance by assigning a port" argument :assignment, :struct, constraints: [instance_of: Assignment] - change {DiffoExample.Changes.Assign, pool: :ports} + change {Diffo.Provider.Changes.Assign, pool: :ports} end end calculations do - # The shelf characteristic value brought up from the shelf this card is - # in — derived live via the :slot assignment. + # The shelf characteristic value brought up from the shelf this card + # is part of — Card's :shelf consumer-alias on its slot assignment + # from the Shelf. calculate :shelf, {:array, :map}, - {DiffoExample.Calculations.InheritedCharacteristic, - [via: [:slot], characteristic_module: DiffoExample.Access.ShelfCharacteristic]} do + {DiffoExample.Calculations.InheritedCharacteristicViaAssignment, + [via: [:shelf], characteristic_module: DiffoExample.Access.ShelfCharacteristic]} do public? true end - # The slot number this card occupies on its shelf — the value of the - # shelf's :slots-pool assignment to this card. + # The slot number this card occupies on its shelf — the :value of + # the assignment Card aliases :shelf (its upstream Shelf). calculate :slot, {:array, :integer}, - {Diffo.Provider.Calculations.FieldFromAssignment, [alias: :slot, field: :value]} do + {Diffo.Provider.Calculations.FieldFromAssignment, [alias: :shelf, field: :value]} do public? true end end diff --git a/lib/access/resources/characteristic_values/cable_characteristic.ex b/lib/access/resources/characteristic_values/cable_characteristic.ex index 856447a..ff3b040 100644 --- a/lib/access/resources/characteristic_values/cable_characteristic.ex +++ b/lib/access/resources/characteristic_values/cable_characteristic.ex @@ -16,14 +16,6 @@ defmodule DiffoExample.Access.CableCharacteristic do end actions do - create :create do - accept [:name, :pairs, :length_amount, :length_unit, :loss_amount, :loss_unit, :technology] - argument :instance_id, :uuid - argument :feature_id, :uuid - change manage_relationship(:instance_id, :instance, type: :append) - change manage_relationship(:feature_id, :feature, type: :append) - end - update :update do accept [:pairs, :technology, :length_amount, :length_unit, :loss_amount, :loss_unit] argument :length, :term, allow_nil?: true diff --git a/lib/access/resources/characteristic_values/card_characteristic.ex b/lib/access/resources/characteristic_values/card_characteristic.ex index f3fb0be..f308463 100644 --- a/lib/access/resources/characteristic_values/card_characteristic.ex +++ b/lib/access/resources/characteristic_values/card_characteristic.ex @@ -13,20 +13,6 @@ defmodule DiffoExample.Access.CardCharacteristic do plural_name :card_characteristics end - actions do - create :create do - accept [:name, :family, :model, :technology] - argument :instance_id, :uuid - argument :feature_id, :uuid - change manage_relationship(:instance_id, :instance, type: :append) - change manage_relationship(:feature_id, :feature, type: :append) - end - - update :update do - accept [:family, :model, :technology] - end - end - attributes do attribute :family, :atom, public?: true attribute :model, :string, public?: true diff --git a/lib/access/resources/characteristic_values/path_characteristic.ex b/lib/access/resources/characteristic_values/path_characteristic.ex index 731e24d..76e08c7 100644 --- a/lib/access/resources/characteristic_values/path_characteristic.ex +++ b/lib/access/resources/characteristic_values/path_characteristic.ex @@ -16,24 +16,6 @@ defmodule DiffoExample.Access.PathCharacteristic do end actions do - create :create do - accept [ - :name, - :device_name, - :sections, - :length_amount, - :length_unit, - :loss_amount, - :loss_unit, - :technology - ] - - argument :instance_id, :uuid - argument :feature_id, :uuid - change manage_relationship(:instance_id, :instance, type: :append) - change manage_relationship(:feature_id, :feature, type: :append) - end - update :update do accept [ :device_name, diff --git a/lib/access/resources/characteristic_values/shelf_characteristic.ex b/lib/access/resources/characteristic_values/shelf_characteristic.ex index 6a915cd..fdafc29 100644 --- a/lib/access/resources/characteristic_values/shelf_characteristic.ex +++ b/lib/access/resources/characteristic_values/shelf_characteristic.ex @@ -13,20 +13,6 @@ defmodule DiffoExample.Access.ShelfCharacteristic do plural_name :shelf_characteristics end - actions do - create :create do - accept [:name, :device_name, :family, :model, :technology] - argument :instance_id, :uuid - argument :feature_id, :uuid - change manage_relationship(:instance_id, :instance, type: :append) - change manage_relationship(:feature_id, :feature, type: :append) - end - - update :update do - accept [:device_name, :family, :model, :technology] - end - end - attributes do attribute :device_name, :string, public?: true attribute :family, :atom, public?: true diff --git a/lib/access/resources/path.ex b/lib/access/resources/path.ex index 816e511..28642fe 100644 --- a/lib/access/resources/path.ex +++ b/lib/access/resources/path.ex @@ -64,42 +64,44 @@ defmodule DiffoExample.Access.Path do description "defines the path" argument :characteristic_value_updates, {:array, :term} - change DiffoExample.Changes.Define + change Diffo.Provider.Changes.Define end update :relate do description "relates the path with other instances" argument :relationships, {:array, :struct} - change DiffoExample.Changes.Relate + change Diffo.Provider.Changes.Relate end end calculations do # The card characteristic value brought up from the card this path is - # assigned a port on — via the :port assignment. + # part of — Path's :card consumer-alias on its port assignment from + # the Card. calculate :card, {:array, :map}, - {DiffoExample.Calculations.InheritedCharacteristic, - [via: [:port], characteristic_module: DiffoExample.Access.CardCharacteristic]} do + {DiffoExample.Calculations.InheritedCharacteristicViaAssignment, + [via: [:card], characteristic_module: DiffoExample.Access.CardCharacteristic]} do public? true end - # The port number this path occupies on its card — the value of the - # card's :ports-pool assignment to this path. + # The port number this path occupies on its card — the :value of the + # assignment Path aliases :card (its upstream Card). calculate :port, {:array, :integer}, - {Diffo.Provider.Calculations.FieldFromAssignment, [alias: :port, field: :value]} do + {Diffo.Provider.Calculations.FieldFromAssignment, [alias: :card, field: :value]} do public? true end - # The shelf characteristic value brought up transitively — port to the - # card, then the card's slot to its shelf. Two-hop via [:port, :slot]. + # The shelf characteristic value brought up transitively — Path's + # :card alias to the Card, then the Card's :shelf alias to its Shelf. + # Two-hop via [:card, :shelf]. calculate :shelf, {:array, :map}, - {DiffoExample.Calculations.InheritedCharacteristic, + {DiffoExample.Calculations.InheritedCharacteristicViaAssignment, [ - via: [:port, :slot], + via: [:card, :shelf], characteristic_module: DiffoExample.Access.ShelfCharacteristic ]} do public? true diff --git a/lib/access/resources/shelf.ex b/lib/access/resources/shelf.ex index 243de66..5f06a45 100644 --- a/lib/access/resources/shelf.ex +++ b/lib/access/resources/shelf.ex @@ -70,21 +70,21 @@ defmodule DiffoExample.Access.Shelf do argument :characteristic_value_updates, {:array, :term} change set_attribute(:resource_state, :operating) - change DiffoExample.Changes.Define + change Diffo.Provider.Changes.Define end update :relate do description "relates the shelf with cards" argument :relationships, {:array, :struct} - change DiffoExample.Changes.Relate + change Diffo.Provider.Changes.Relate end update :assign_slot do description "relates the shelf with an instance by assigning a slot" argument :assignment, :struct, constraints: [instance_of: Assignment] - change {DiffoExample.Changes.Assign, pool: :slots} + change {Diffo.Provider.Changes.Assign, pool: :slots} end end @@ -96,7 +96,7 @@ defmodule DiffoExample.Access.Shelf do calculate :cards, {:array, :map}, {DiffoExample.Calculations.ReverseInheritedCharacteristic, - [alias: :slot, characteristic_module: DiffoExample.Access.CardCharacteristic]} do + [alias: :shelf, characteristic_module: DiffoExample.Access.CardCharacteristic]} do public? true end diff --git a/lib/access/services/characteristic_values/aggregate_characteristic.ex b/lib/access/services/characteristic_values/aggregate_characteristic.ex index 0c5c7cf..986a753 100644 --- a/lib/access/services/characteristic_values/aggregate_characteristic.ex +++ b/lib/access/services/characteristic_values/aggregate_characteristic.ex @@ -13,29 +13,6 @@ defmodule DiffoExample.Access.AggregateCharacteristic do plural_name :aggregate_characteristics end - actions do - create :create do - accept [ - :name, - :interface_name, - :physical_interface, - :physical_layer, - :link_layer, - :svlan_id, - :vpi - ] - - argument :instance_id, :uuid - argument :feature_id, :uuid - change manage_relationship(:instance_id, :instance, type: :append) - change manage_relationship(:feature_id, :feature, type: :append) - end - - update :update do - accept [:interface_name, :physical_interface, :physical_layer, :link_layer, :svlan_id, :vpi] - end - end - attributes do attribute :interface_name, :string, public?: true attribute :physical_interface, :string, public?: true diff --git a/lib/access/services/characteristic_values/circuit_characteristic.ex b/lib/access/services/characteristic_values/circuit_characteristic.ex index 87dcea5..1829997 100644 --- a/lib/access/services/characteristic_values/circuit_characteristic.ex +++ b/lib/access/services/characteristic_values/circuit_characteristic.ex @@ -16,24 +16,6 @@ defmodule DiffoExample.Access.CircuitCharacteristic do end actions do - create :create do - accept [ - :name, - :circuit_id, - :cvlan_id, - :vci, - :encapsulation, - :bp_downstream, - :bp_upstream, - :bp_units - ] - - argument :instance_id, :uuid - argument :feature_id, :uuid - change manage_relationship(:instance_id, :instance, type: :append) - change manage_relationship(:feature_id, :feature, type: :append) - end - update :update do accept [:circuit_id, :cvlan_id, :vci, :encapsulation] argument :bandwidth_profile, :term, allow_nil?: true diff --git a/lib/access/services/characteristic_values/constraints_characteristic.ex b/lib/access/services/characteristic_values/constraints_characteristic.ex index 4cfcade..d3f3177 100644 --- a/lib/access/services/characteristic_values/constraints_characteristic.ex +++ b/lib/access/services/characteristic_values/constraints_characteristic.ex @@ -16,14 +16,6 @@ defmodule DiffoExample.Access.ConstraintsCharacteristic do end actions do - create :create do - accept [:name, :max_latency, :mp_downstream, :mp_upstream, :mp_units] - argument :instance_id, :uuid - argument :feature_id, :uuid - change manage_relationship(:instance_id, :instance, type: :append) - change manage_relationship(:feature_id, :feature, type: :append) - end - update :update do accept [:max_latency] argument :min_profile, :term, allow_nil?: true diff --git a/lib/access/services/characteristic_values/dslam_characteristic.ex b/lib/access/services/characteristic_values/dslam_characteristic.ex index 06d61ef..fbbc010 100644 --- a/lib/access/services/characteristic_values/dslam_characteristic.ex +++ b/lib/access/services/characteristic_values/dslam_characteristic.ex @@ -13,20 +13,6 @@ defmodule DiffoExample.Access.DslamCharacteristic do plural_name :dslam_characteristics end - actions do - create :create do - accept [:name, :device_name, :family, :model, :technology] - argument :instance_id, :uuid - argument :feature_id, :uuid - change manage_relationship(:instance_id, :instance, type: :append) - change manage_relationship(:feature_id, :feature, type: :append) - end - - update :update do - accept [:device_name, :family, :model, :technology] - end - end - attributes do attribute :device_name, :string, public?: true attribute :family, :atom, public?: true diff --git a/lib/access/services/characteristic_values/line_characteristic.ex b/lib/access/services/characteristic_values/line_characteristic.ex index 33506b5..e0b5fe7 100644 --- a/lib/access/services/characteristic_values/line_characteristic.ex +++ b/lib/access/services/characteristic_values/line_characteristic.ex @@ -13,20 +13,6 @@ defmodule DiffoExample.Access.LineCharacteristic do plural_name :line_characteristics end - actions do - create :create do - accept [:name, :port, :slot, :standard, :profile] - argument :instance_id, :uuid - argument :feature_id, :uuid - change manage_relationship(:instance_id, :instance, type: :append) - change manage_relationship(:feature_id, :feature, type: :append) - end - - update :update do - accept [:port, :slot, :standard, :profile] - end - end - attributes do attribute :port, :integer, public?: true attribute :slot, :integer, public?: true diff --git a/lib/access/services/dsl_access.ex b/lib/access/services/dsl_access.ex index 7983f8f..a059900 100644 --- a/lib/access/services/dsl_access.ex +++ b/lib/access/services/dsl_access.ex @@ -54,8 +54,8 @@ defmodule DiffoExample.Access.DslAccess do state_machine do transitions do - transition action: :qualify_result, from: :initial, to: :inactive - transition action: :design_result, from: [:initial, :inactive], to: :reserved + transition action: :qualify_result, from: :initial, to: :feasibilityChecked + transition action: :design_result, from: [:initial, :feasibilityChecked], to: :reserved end end @@ -76,7 +76,7 @@ defmodule DiffoExample.Access.DslAccess do argument :places, {:array, :struct} require_atomic? false - change transition_state(:inactive) + change transition_state(:feasibilityChecked) validate argument_in(:service_operating_status, [ nil, @@ -99,7 +99,7 @@ defmodule DiffoExample.Access.DslAccess do argument :characteristic_value_updates, {:array, :term} change transition_state(:reserved) - change DiffoExample.Changes.Define + change Diffo.Provider.Changes.Define end end end diff --git a/lib/diffo_example/calculations/inherited_characteristic.ex b/lib/diffo_example/calculations/inherited_characteristic_via_assignment.ex similarity index 52% rename from lib/diffo_example/calculations/inherited_characteristic.ex rename to lib/diffo_example/calculations/inherited_characteristic_via_assignment.ex index 1495028..22768d4 100644 --- a/lib/diffo_example/calculations/inherited_characteristic.ex +++ b/lib/diffo_example/calculations/inherited_characteristic_via_assignment.ex @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: MIT -defmodule DiffoExample.Calculations.InheritedCharacteristic do +defmodule DiffoExample.Calculations.InheritedCharacteristicViaAssignment do @moduledoc """ Brings up a typed characteristic value from an assignment-source instance. @@ -11,10 +11,15 @@ defmodule DiffoExample.Calculations.InheritedCharacteristic do `AssignmentRelationship` by alias to reach source instances, then queries the typed characteristic resource on each source and returns its `.value`. - Local-to-this-repo for now. Worth yarning upstream as a diffo-side - `inherited_characteristic` DSL declaration backed by a - `Diffo.Provider.Calculations.InheritedCharacteristic` calc, sitting - alongside the existing inherited-place and inherited-party machinery. + Sibling to `InheritedCharacteristicViaRelationship`, which performs the + analogous traversal over `DefinedSimpleRelationship` edges (forward, + source → target). Pick the right calc by the kind of edge being + traversed — assignment vs. relationship. + + Local-to-this-repo for now. Worth yarning upstream as a pair of + diffo-side DSL declarations backed by analogous calcs in the provider + extension, sitting alongside the existing inherited-place and + inherited-party machinery. ## Options @@ -22,24 +27,39 @@ defmodule DiffoExample.Calculations.InheritedCharacteristic do from this instance back to the source whose characteristic we want. Each step filters `AssignmentRelationship` by `target_id` and `alias`, then follows `source_id` to the next set of instances. The aliases are - the assignee's slot names, supplied when the assignment is made. + the **consumer's name for the upstream related resource** at each hop + (e.g. AVC names its CVC slot `:cvc`, CVC names its NniGroup slot + `:nni_group`) — set when the assignment is made. - `characteristic_module:` *(required)* — the typed characteristic Ash resource on the final source (e.g. `ShelfCharacteristic`). The calc queries this resource by `instance_id` and returns the `.value`. + - `singular?:` *(optional, default `false`)* — when `true`, unwraps the + result to a single value (or `nil`) rather than a list. Safe whenever + every hop in `via:` traverses an `AssignmentRelationship` with identity + `[:target_id, :alias]` — that guarantees ≤1 source per step, so the + walk yields at most one value. Declare the calc's return type as `:map` + (rather than `{:array, :map}`) when using this option. ## Example # Card brings up its shelf's typed characteristic via the slot # assignment the shelf made to it (alias :slot on the incoming # AssignmentRelationship). - calculate :shelf, :map, - {DiffoExample.Calculations.InheritedCharacteristic, + calculate :shelf, {:array, :map}, + {DiffoExample.Calculations.InheritedCharacteristicViaAssignment, [via: [:slot], characteristic_module: ShelfCharacteristic]} # Path brings up the same via a two-hop chain — port-then-slot. - calculate :shelf, :map, - {DiffoExample.Calculations.InheritedCharacteristic, + calculate :shelf, {:array, :map}, + {DiffoExample.Calculations.InheritedCharacteristicViaAssignment, [via: [:port, :slot], characteristic_module: ShelfCharacteristic]} + + # AVC brings up its singular CVC via its :cvc consumer-alias on the + # cvlan assignment from the CVC. AssignmentRelationship identity + # guarantees ≤1 source, so we declare :map and ask the calc to unwrap. + calculate :cvc, :map, + {DiffoExample.Calculations.InheritedCharacteristicViaAssignment, + [via: [:cvc], characteristic_module: CvcCharacteristic, singular?: true]} """ use Ash.Resource.Calculation @@ -50,6 +70,7 @@ defmodule DiffoExample.Calculations.InheritedCharacteristic do def calculate(records, opts, _context) do via = Keyword.fetch!(opts, :via) characteristic_module = Keyword.fetch!(opts, :characteristic_module) + singular? = Keyword.get(opts, :singular?, false) Enum.map(records, fn record -> final_ids = @@ -62,13 +83,16 @@ defmodule DiffoExample.Calculations.InheritedCharacteristic do end) end) - Enum.flat_map(final_ids, fn id -> - characteristic_module - |> Ash.Query.filter_input(instance_id: id) - |> Ash.Query.load(:value) - |> Ash.read!() - |> Enum.map(& &1.value) - end) + values = + Enum.flat_map(final_ids, fn id -> + characteristic_module + |> Ash.Query.filter_input(instance_id: id) + |> Ash.Query.load(:value) + |> Ash.read!() + |> Enum.map(& &1.value) + end) + + if singular?, do: List.first(values), else: values end) end end diff --git a/lib/diffo_example/calculations/inherited_characteristic_via_relationship.ex b/lib/diffo_example/calculations/inherited_characteristic_via_relationship.ex new file mode 100644 index 0000000..9cf438e --- /dev/null +++ b/lib/diffo_example/calculations/inherited_characteristic_via_relationship.ex @@ -0,0 +1,130 @@ +# SPDX-FileCopyrightText: 2025 diffo_example contributors +# +# SPDX-License-Identifier: MIT + +defmodule DiffoExample.Calculations.InheritedCharacteristicViaRelationship do + @moduledoc """ + Brings up typed characteristic values from target instances reached via + forward `Diffo.Provider.Relationship` edges (source → target), optionally + filtered by `type:` and/or `alias:`. + + Sibling to `InheritedCharacteristicViaAssignment`, which performs the + analogous traversal over `AssignmentRelationship` edges. Pick the right + calc by the kind of edge being traversed — relationship vs. assignment. + + Use this when the edge between the consuming instance and the target was + created by a `:relate` action (a `Provider.Relationship` record). Use + `InheritedCharacteristicViaAssignment` when the edge was created by the + Assigner (an `AssignmentRelationship` record). + + Local-to-this-repo for now. Worth yarning upstream alongside the + assignment variant as a pair of provider-side calcs. + + ## Options + + - `characteristic_module:` *(required)* — the typed characteristic Ash + resource on the final source (e.g. `NniCharacteristic`). The calc + queries this resource by `instance_id` and returns the `.value`. + - `type:` *(optional)* — filter relationships by type atom (e.g. `:contains`). + - `alias:` *(optional)* — filter relationships by alias atom (e.g. `:avc`). + - `then_via:` *(optional)* — list of consumer-alias atoms to walk back + via `AssignmentRelationship` **after** the relationship hop. Each step + walks back through the target's incoming assignments (`target_id + + alias` identity, so each step has cardinality ≤1). Aliases name the + upstream related resource each consumer is part of. Use this for mixed + paths — one relationship hop followed by one or more assignment hops + — e.g. PRI's `:cvc` bring-up: `:circuit` owns relationship, then `:cvc` + assignment back to the CVC. + - `singular?:` *(optional, default `false`)* — unwrap to a single value + when the consumer expects a 1-cardinality result (e.g. PRI's `:avc` or + `:uni` aliased owns-relationship). Declare the calc's return type as + `:map` (rather than `{:array, :map}`) when using this option. + + ## Examples + + # NniGroup brings up the typed characteristic of every NNI it + # comprises — forward traversal of :contains relationships, returns + # a list of NniCharacteristic values. + calculate :nnis, {:array, :map}, + {DiffoExample.Calculations.InheritedCharacteristicViaRelationship, + [type: :contains, characteristic_module: NniCharacteristic]} + + # PRI brings up the singular AVC it owns — PRI calls this related + # resource :circuit (its domain role), set as the alias on PRI's + # owns relationship. + calculate :avc, :map, + {DiffoExample.Calculations.InheritedCharacteristicViaRelationship, + [alias: :circuit, characteristic_module: AvcCharacteristic, singular?: true]} + + # PRI brings up the singular CVC two-hop — :circuit owns relationship + # from PRI to AVC, then back via the AVC's :cvc consumer-alias + # assignment from CVC. + calculate :cvc, :map, + {DiffoExample.Calculations.InheritedCharacteristicViaRelationship, + [alias: :circuit, then_via: [:cvc], + characteristic_module: CvcCharacteristic, singular?: true]} + """ + use Ash.Resource.Calculation + require Ash.Query + + @impl true + def load(_query, _opts, _context), do: [] + + @impl true + def calculate(records, opts, _context) do + characteristic_module = Keyword.fetch!(opts, :characteristic_module) + type_filter = Keyword.get(opts, :type) + alias_filter = Keyword.get(opts, :alias) + then_via = Keyword.get(opts, :then_via, []) + singular? = Keyword.get(opts, :singular?, false) + + Enum.map(records, fn record -> + target_ids = + Diffo.Provider.Relationship + |> filter_relationships(record.id, type_filter, alias_filter) + |> Ash.read!(domain: Diffo.Provider) + |> Enum.map(& &1.target_id) + + final_ids = walk_assignments(target_ids, then_via) + + values = + Enum.flat_map(final_ids, fn id -> + characteristic_module + |> Ash.Query.filter_input(instance_id: id) + |> Ash.Query.load(:value) + |> Ash.read!() + |> Enum.map(& &1.value) + end) + + if singular?, do: List.first(values), else: values + end) + end + + # Walks back through incoming `AssignmentRelationship` records for each + # id, following `target_id + alias` (identity, ≤1 source per step). + defp walk_assignments(ids, []), do: ids + + defp walk_assignments(ids, [alias_step | rest]) do + next_ids = + Enum.flat_map(ids, fn id -> + Diffo.Provider.AssignmentRelationship + |> Ash.Query.filter_input(target_id: id, alias: alias_step) + |> Ash.read!(domain: Diffo.Provider) + |> Enum.map(& &1.source_id) + end) + + walk_assignments(next_ids, rest) + end + + defp filter_relationships(query, source_id, nil, nil), + do: Ash.Query.filter_input(query, source_id: source_id) + + defp filter_relationships(query, source_id, type, nil), + do: Ash.Query.filter_input(query, source_id: source_id, type: type) + + defp filter_relationships(query, source_id, nil, alias_name), + do: Ash.Query.filter_input(query, source_id: source_id, alias: alias_name) + + defp filter_relationships(query, source_id, type, alias_name), + do: Ash.Query.filter_input(query, source_id: source_id, type: type, alias: alias_name) +end diff --git a/lib/diffo_example/calculations/reverse_inherited_characteristic.ex b/lib/diffo_example/calculations/reverse_inherited_characteristic.ex index af146f9..66629d9 100644 --- a/lib/diffo_example/calculations/reverse_inherited_characteristic.ex +++ b/lib/diffo_example/calculations/reverse_inherited_characteristic.ex @@ -26,19 +26,34 @@ defmodule DiffoExample.Calculations.ReverseInheritedCharacteristic do ## Options - - `alias:` *(optional)* — filter outgoing assignments by alias (the - assignee's slot name). When omitted, all outgoing assignments are - included. - `characteristic_module:` *(required)* — the typed characteristic Ash resource on each assignee (e.g. `CardCharacteristic`). + - `alias:` *(optional)* — filter outgoing assignments by alias (the + assignee's slot name). Use when every consumer follows the same + aliasing convention. + - `thing:` *(optional)* — filter outgoing assignments by `thing` (the + pool's thing name, e.g. `:slot`, `:port`). Always set from the pool + DSL declaration, so this is more robust than `alias:` when consumers + don't reliably name their slots. See `[[assignment-direction-asymmetry]]`. - ## Example + Specify either `alias:` or `thing:` (or both); omitting both includes + every outgoing assignment from this instance. + + ## Examples # Shelf brings up the card characteristic from every card it's - # assigned a slot to, ordered by slot number. + # assigned a slot to, ordered by slot number. Cards-as-assignees + # name their slot :slot when requesting. calculate :cards, {:array, :map}, {DiffoExample.Calculations.ReverseInheritedCharacteristic, [alias: :slot, characteristic_module: CardCharacteristic]} + + # NTD brings up the UNI characteristic from every UNI it's assigned + # a port to. The UNIs may not have set an alias on their request, + # so filter by `thing` from the :ports pool declaration. + calculate :unis, {:array, :map}, + {DiffoExample.Calculations.ReverseInheritedCharacteristic, + [thing: :port, characteristic_module: UniCharacteristic]} """ use Ash.Resource.Calculation @@ -48,12 +63,13 @@ defmodule DiffoExample.Calculations.ReverseInheritedCharacteristic do @impl true def calculate(records, opts, _context) do alias_filter = Keyword.get(opts, :alias) + thing_filter = Keyword.get(opts, :thing) characteristic_module = Keyword.fetch!(opts, :characteristic_module) Enum.map(records, fn record -> assignments = Diffo.Provider.AssignmentRelationship - |> filter_outgoing(record.id, alias_filter) + |> filter_outgoing(record.id, alias_filter, thing_filter) |> Ash.Query.sort(value: :asc) |> Ash.read!(domain: Diffo.Provider) @@ -67,9 +83,20 @@ defmodule DiffoExample.Calculations.ReverseInheritedCharacteristic do end) end - defp filter_outgoing(query, source_id, nil), - do: query |> Ash.Query.filter_input(source_id: source_id) + defp filter_outgoing(query, source_id, nil, nil), + do: Ash.Query.filter_input(query, source_id: source_id) + + defp filter_outgoing(query, source_id, alias_filter, nil), + do: Ash.Query.filter_input(query, source_id: source_id, alias: alias_filter) + + defp filter_outgoing(query, source_id, nil, thing_filter), + do: Ash.Query.filter_input(query, source_id: source_id, thing: thing_filter) - defp filter_outgoing(query, source_id, alias_filter), - do: query |> Ash.Query.filter_input(source_id: source_id, alias: alias_filter) + defp filter_outgoing(query, source_id, alias_filter, thing_filter), + do: + Ash.Query.filter_input(query, + source_id: source_id, + alias: alias_filter, + thing: thing_filter + ) end diff --git a/lib/diffo_example/changes/assign.ex b/lib/diffo_example/changes/assign.ex deleted file mode 100644 index 0887940..0000000 --- a/lib/diffo_example/changes/assign.ex +++ /dev/null @@ -1,43 +0,0 @@ -# SPDX-FileCopyrightText: 2025 diffo_example contributors -# -# SPDX-License-Identifier: MIT - -defmodule DiffoExample.Changes.Assign do - @moduledoc """ - After-action for `:assign_*`-style update actions on Diffo Instance resources - that declare pools. - - Applies the `:assignment` argument via `Assigner.assign/3` against the named - pool, then reloads the instance through its primary read so preparations - re-run. - - Pass the pool name as an option: - - update :assign_pair do - argument :assignment, :struct, constraints: [instance_of: Assignment] - change {DiffoExample.Changes.Assign, pool: :pairs} - end - """ - use Ash.Resource.Change - require Ash.Query - - alias Diffo.Provider.Assigner - - @impl true - def change(changeset, opts, _context) do - pool = Keyword.fetch!(opts, :pool) - - Ash.Changeset.after_action(changeset, fn changeset, result -> - mod = result.__struct__ - - with {:ok, result} <- Assigner.assign(result, changeset, pool), - {:ok, result} <- - mod - |> Ash.Query.for_read(:read) - |> Ash.Query.filter(id == ^result.id) - |> Ash.read_one() do - {:ok, result} - end - end) - end -end diff --git a/lib/diffo_example/changes/define.ex b/lib/diffo_example/changes/define.ex deleted file mode 100644 index 5af026c..0000000 --- a/lib/diffo_example/changes/define.ex +++ /dev/null @@ -1,49 +0,0 @@ -# SPDX-FileCopyrightText: 2025 diffo_example contributors -# -# SPDX-License-Identifier: MIT - -defmodule DiffoExample.Changes.Define do - @moduledoc """ - After-action for `:define`-style update actions on Diffo Instance resources. - - Applies the `:characteristic_value_updates` argument against the resource's - compile-time `characteristics/0` and `pools/0` declarations, then reloads - the instance through its primary read so preparations re-run. - - Use it on any instance update action that carries - `argument :characteristic_value_updates, {:array, :term}`: - - update :define do - argument :characteristic_value_updates, {:array, :term} - - change set_attribute(:resource_state, :operating) - change DiffoExample.Changes.Define - end - - Lifecycle transitions (resource_state for resources, transition_state for - services) remain on the action — they are intent-specific. - """ - use Ash.Resource.Change - require Ash.Query - - alias Diffo.Provider.Extension.Characteristic - alias Diffo.Provider.Extension.Pool - - @impl true - def change(changeset, _opts, _context) do - Ash.Changeset.after_action(changeset, fn changeset, result -> - mod = result.__struct__ - - with {:ok, result} <- Ash.load(result, [:characteristics]), - {:ok, result} <- Characteristic.update_all(result, changeset, mod.characteristics()), - {:ok, result} <- Pool.update_pools(result, changeset, mod.pools()), - {:ok, result} <- - mod - |> Ash.Query.for_read(:read) - |> Ash.Query.filter(id == ^result.id) - |> Ash.read_one() do - {:ok, result} - end - end) - end -end diff --git a/lib/diffo_example/changes/relate.ex b/lib/diffo_example/changes/relate.ex deleted file mode 100644 index 5ddd24c..0000000 --- a/lib/diffo_example/changes/relate.ex +++ /dev/null @@ -1,40 +0,0 @@ -# SPDX-FileCopyrightText: 2025 diffo_example contributors -# -# SPDX-License-Identifier: MIT - -defmodule DiffoExample.Changes.Relate do - @moduledoc """ - After-action for `:relate`-style update actions on Diffo Instance resources. - - Applies the `:relationships` argument via `Relationship.relate_instance`, - then reloads the instance through its primary read so preparations re-run. - - Use it on any instance update action that carries - `argument :relationships, {:array, :struct}`: - - update :relate do - argument :relationships, {:array, :struct} - change DiffoExample.Changes.Relate - end - """ - use Ash.Resource.Change - require Ash.Query - - alias Diffo.Provider.Instance.Relationship - - @impl true - def change(changeset, _opts, _context) do - Ash.Changeset.after_action(changeset, fn changeset, result -> - mod = result.__struct__ - - with {:ok, result} <- Relationship.relate_instance(result, changeset), - {:ok, result} <- - mod - |> Ash.Query.for_read(:read) - |> Ash.Query.filter(id == ^result.id) - |> Ash.read_one() do - {:ok, result} - end - end) - end -end diff --git a/lib/diffo_example/util.ex b/lib/diffo_example/util.ex index e0047ad..6aedea5 100644 --- a/lib/diffo_example/util.ex +++ b/lib/diffo_example/util.ex @@ -12,26 +12,25 @@ defmodule DiffoExample.Util do projection to both sides of a comparison. """ - @absent_characteristic "absent_diffo_169" + @absent_characteristic "absent_characteristic" @doc """ Project the `*Characteristic` arrays in a JSON payload to a coarser form derived from `instance`'s declarations. - While [diffo#169](https://github.com/diffo-dev/diffo/issues/169) is - open, typed characteristic records and pool records do not collapse - into the TMF `serviceCharacteristic` / `resourceCharacteristic` / - `featureCharacteristic` arrays. Without this projection the actual - JSON has no entries at all and the expected has rich entries. - - This projection reads the declared characteristic, pool and feature - characteristic names from the instance's module and replaces each - named characteristic array with a sorted list of - `%{"name" => name, "value" => "absent_diffo_169"}` entries. Applied - to both sides, names align; the rich value collapses to the same - placeholder. When #169 lands and the collapse arrives, remove the - `|> summarise_characteristics(instance)` wraps from each call site - (or delete this function) — every previously masked test surfaces. + General-purpose projection helper for comparing TMF payloads at the + *names-only* level, ignoring the specific values carried by typed + characteristic records and pool records. Reads the declared + characteristic, pool and feature characteristic names from the + instance's module and replaces each named characteristic array with a + sorted list of `%{"name" => name, "value" => "absent_characteristic"}` + entries. Applied to both sides of a comparison, names align and rich + values collapse to the same placeholder. + + Useful for demonstrating projections — coarsening test assertions to + the structural shape (which characteristics are declared and surface) + without coupling them to the values those characteristics happen to + carry on a given run. Modelled after `Diffo.Util.summarise_dates/1`. """ diff --git a/lib/mix/tasks/gen.api_docs.ex b/lib/mix/tasks/gen.api_docs.ex new file mode 100644 index 0000000..cbd18fb --- /dev/null +++ b/lib/mix/tasks/gen.api_docs.ex @@ -0,0 +1,174 @@ +# SPDX-FileCopyrightText: 2025 diffo_example contributors +# +# SPDX-License-Identifier: MIT + +defmodule Mix.Tasks.Gen.ApiDocs do + @shortdoc "Generates per-domain API markdown fragments from code_interface defines" + @moduledoc """ + Walks each configured domain's `code_interface` defines and writes a + markdown table fragment for inclusion in the domain doc pages. + + Each fragment lists the resource sections, with one row per `define` + showing the generated Elixir function, the underlying action, the + meaningful arguments, and the action's purpose (its `description:`). + + ## Usage + + mix gen.api_docs + + Writes to: + + - `documentation/domains/_access_api.md` + - `documentation/domains/_nbn_api.md` + + These fragments are intended to be referenced (e.g. via `!include` + pseudo-markers or just kept open in the editor alongside the narrative + page). They're regenerated on demand so they don't drift from the + code-interface declarations. + """ + + use Mix.Task + + @doc_root "documentation/domains" + + @domains [ + {DiffoExample.Access, "_access_api.md", "Access"}, + {DiffoExample.Nbn, "_nbn_api.md", "NBN"} + ] + + @autogen_banner """ + + """ + + @impl Mix.Task + def run(_args) do + Mix.Task.run("compile", []) + + File.mkdir_p!(@doc_root) + + Enum.each(@domains, fn {domain, file, title} -> + path = Path.join(@doc_root, file) + content = render_domain(domain, title) + File.write!(path, content) + Mix.shell().info("wrote #{path}") + end) + end + + defp render_domain(domain, title) do + refs = + domain + |> Ash.Domain.Info.resource_references() + |> Enum.sort_by(&short_name(&1.resource)) + + body = + refs + |> Enum.map(&render_resource/1) + |> Enum.reject(&(&1 == :empty)) + |> Enum.join("\n\n") + + [ + @autogen_banner, + "\n", + "# #{title} Domain API\n", + "\n", + "The Elixir function-call surface for each resource in the `#{inspect(domain)}` domain. ", + "Generated from the `define` declarations in the domain's `resources do` block.\n", + "\n", + body, + "\n" + ] + |> IO.iodata_to_binary() + end + + defp render_resource(ref) do + case ref.definitions do + [] -> + :empty + + defs -> + sorted = Enum.sort_by(defs, & &1.name) + + rows = + sorted + |> Enum.map(&render_row(&1, ref.resource)) + |> Enum.join("\n") + + """ + ## #{short_name(ref.resource)} + + | Function | Action | Arguments | Purpose | + |---|---|---|---| + #{rows} + """ + |> String.trim_trailing() + end + end + + defp render_row(interface, resource) do + action_name = interface.action || interface.name + action = Ash.Resource.Info.action(resource, action_name) + + fn_cell = "`#{interface.name}`" + action_cell = "`:#{action_name}`" + args_cell = render_args(interface, action) + purpose_cell = render_purpose(action) + + "| #{fn_cell} | #{action_cell} | #{args_cell} | #{purpose_cell} |" + end + + # Auto-injected by Diffo's `behaviour do create :build end` fragment — + # not part of the user-facing API surface. + @injected_build_args [:specified_by, :features, :characteristics] + + defp render_args(interface, action) do + get_by_arg = + cond do + interface.get_by -> Enum.map(List.wrap(interface.get_by), &"`#{&1}`") + interface.get_by_identity -> ["`#{interface.get_by_identity}`"] + true -> [] + end + + accept = Enum.map(Map.get(action, :accept) || [], &"`#{&1}`") + + arguments = + (Map.get(action, :arguments) || []) + |> Enum.reject(&(&1.name in @injected_build_args)) + |> Enum.map(fn arg -> "`#{arg.name}` (#{type_label(arg.type)})" end) + + case get_by_arg ++ accept ++ arguments do + [] -> "—" + list -> Enum.join(list, ", ") + end + end + + defp render_purpose(action) do + case action.description do + nil -> "—" + "" -> "—" + desc -> desc |> String.replace("\n", " ") |> String.trim() + end + end + + defp type_label({:array, type}), do: "list of #{type_label(type)}" + + defp type_label(type) when is_atom(type) do + type + |> to_string() + |> String.replace_prefix("Elixir.Ash.Type.", "") + |> String.replace_prefix("Elixir.", "") + |> String.downcase() + end + + defp type_label(other), do: inspect(other) + + defp short_name(module) do + module |> Module.split() |> List.last() + end +end diff --git a/lib/nbn/api_router.ex b/lib/nbn/api_router.ex deleted file mode 100644 index ffa0828..0000000 --- a/lib/nbn/api_router.ex +++ /dev/null @@ -1,10 +0,0 @@ -# SPDX-FileCopyrightText: 2025 diffo_example contributors -# -# SPDX-License-Identifier: MIT - -defmodule DiffoExample.Nbn.ApiRouter do - @moduledoc false - use AshJsonApi.Router, - domains: [DiffoExample.Nbn], - open_api: "/open_api" -end diff --git a/lib/nbn/nbn.ex b/lib/nbn/nbn.ex index b21cafc..522d63c 100644 --- a/lib/nbn/nbn.ex +++ b/lib/nbn/nbn.ex @@ -15,7 +15,7 @@ defmodule DiffoExample.Nbn do use Ash.Domain, otp_app: :diffo, fragments: [Diffo.Provider.DomainFragment], - extensions: [AshAi, AshJsonApi.Domain] + extensions: [AshAi] alias DiffoExample.Nbn.NbnEthernet alias DiffoExample.Nbn.Uni @@ -27,7 +27,9 @@ defmodule DiffoExample.Nbn do alias DiffoExample.Nbn.Rsp alias DiffoExample.Nbn.AvcCharacteristic alias DiffoExample.Nbn.CvcCharacteristic + alias DiffoExample.Nbn.CvcMetrics alias DiffoExample.Nbn.NniGroupCharacteristic + alias DiffoExample.Nbn.NniGroupMetrics alias DiffoExample.Nbn.NniCharacteristic alias DiffoExample.Nbn.NtdCharacteristic alias DiffoExample.Nbn.UniCharacteristic @@ -84,77 +86,6 @@ defmodule DiffoExample.Nbn do tool :deactivate_rsp, Rsp, :deactivate end - json_api do - routes do - base_route "/nbnEthernet", NbnEthernet do - index :read - get :read - post :build - patch :define - patch :relate, route: "/:id/relate" - delete :destroy - end - - base_route "/uni", Uni do - index :read - get :read - post :build - patch :define - patch :relate, route: "/:id/relate" - delete :destroy - end - - base_route "/avc", Avc do - index :read - get :read - post :build - patch :define - patch :relate, route: "/:id/relate" - delete :destroy - end - - base_route "/ntd", Ntd do - index :read - get :read - post :build - patch :define - patch :relate, route: "/:id/relate" - delete :destroy - end - - base_route "/cvc", Cvc do - index :read - get :read - post :build - patch :define - patch :relate, route: "/:id/relate" - delete :destroy - end - - base_route "/nniGroup", NniGroup do - index :read - get :read - post :build - patch :define - patch :relate, route: "/:id/relate" - delete :destroy - end - - base_route "/nni", Nni do - index :read - get :read - post :build - patch :define - patch :relate, route: "/:id/relate" - delete :destroy - end - - base_route "/rsp", Rsp do - get :read - end - end - end - resources do resource NbnEthernet do define :get_nbn_ethernet_by_id, action: :read, get_by: :id @@ -220,7 +151,9 @@ defmodule DiffoExample.Nbn do resource AvcCharacteristic resource CvcCharacteristic + resource CvcMetrics resource NniGroupCharacteristic + resource NniGroupMetrics resource NniCharacteristic resource NtdCharacteristic resource UniCharacteristic diff --git a/lib/nbn/resources/avc.ex b/lib/nbn/resources/avc.ex index 609cfeb..8aa66ed 100644 --- a/lib/nbn/resources/avc.ex +++ b/lib/nbn/resources/avc.ex @@ -19,7 +19,6 @@ defmodule DiffoExample.Nbn.Avc do use Ash.Resource, fragments: [BaseInstance], domain: Nbn, - extensions: [AshJsonApi.Resource], authorizers: [Ash.Policy.Authorizer] resource do @@ -27,10 +26,6 @@ defmodule DiffoExample.Nbn.Avc do plural_name :Avcs end - json_api do - type "avc" - end - provider do specification do id "b2c3d4e5-6f7a-4b8c-9d0e-1f2a3b4c5d6e" @@ -77,14 +72,14 @@ defmodule DiffoExample.Nbn.Avc do argument :characteristic_value_updates, {:array, :term} change set_attribute(:resource_state, :operating) - change DiffoExample.Changes.Define + change Diffo.Provider.Changes.Define end update :relate do description "relates the AVC with other instances" argument :relationships, {:array, :struct} - change DiffoExample.Changes.Relate + change Diffo.Provider.Changes.Relate end end @@ -96,6 +91,36 @@ defmodule DiffoExample.Nbn.Avc do end end + calculations do + # The CVC characteristic value brought up from the singular CVC this + # AVC is part of — single-hop via the AVC's :cvc consumer-alias on its + # cvlan assignment from the CVC. + calculate :cvc, + :map, + {DiffoExample.Calculations.InheritedCharacteristicViaAssignment, + [ + via: [:cvc], + characteristic_module: DiffoExample.Nbn.CvcCharacteristic, + singular?: true + ]} do + public? true + end + + # The singular NniGroup characteristic value brought up transitively — + # AVC's :cvc alias to the CVC, then the CVC's :nni_group alias to its + # NniGroup. Two-hop via [:cvc, :nni_group]. + calculate :nni_group, + :map, + {DiffoExample.Calculations.InheritedCharacteristicViaAssignment, + [ + via: [:cvc, :nni_group], + characteristic_module: DiffoExample.Nbn.NniGroupCharacteristic, + singular?: true + ]} do + public? true + end + end + def identifier() do DiffoExample.Nbn.Util.identifier("AVC") end diff --git a/lib/nbn/resources/characteristic_values/avc_characteristic.ex b/lib/nbn/resources/characteristic_values/avc_characteristic.ex index 94df0fa..eed80e1 100644 --- a/lib/nbn/resources/characteristic_values/avc_characteristic.ex +++ b/lib/nbn/resources/characteristic_values/avc_characteristic.ex @@ -13,20 +13,6 @@ defmodule DiffoExample.Nbn.AvcCharacteristic do plural_name :avc_characteristics end - actions do - create :create do - accept [:name, :cvlan, :bandwidth_profile] - argument :instance_id, :uuid - argument :feature_id, :uuid - change manage_relationship(:instance_id, :instance, type: :append) - change manage_relationship(:feature_id, :feature, type: :append) - end - - update :update do - accept [:cvlan, :bandwidth_profile] - end - end - attributes do attribute :cvlan, :integer, public?: true attribute :bandwidth_profile, DiffoExample.Nbn.BandwidthProfile, public?: true diff --git a/lib/nbn/resources/characteristic_values/cvc_characteristic.ex b/lib/nbn/resources/characteristic_values/cvc_characteristic.ex index 0a5fb96..3587794 100644 --- a/lib/nbn/resources/characteristic_values/cvc_characteristic.ex +++ b/lib/nbn/resources/characteristic_values/cvc_characteristic.ex @@ -13,20 +13,6 @@ defmodule DiffoExample.Nbn.CvcCharacteristic do plural_name :cvc_characteristics end - actions do - create :create do - accept [:name, :svlan, :bandwidth] - argument :instance_id, :uuid - argument :feature_id, :uuid - change manage_relationship(:instance_id, :instance, type: :append) - change manage_relationship(:feature_id, :feature, type: :append) - end - - update :update do - accept [:svlan, :bandwidth] - end - end - attributes do attribute :svlan, :integer, public?: true attribute :bandwidth, :integer, public?: true diff --git a/lib/nbn/resources/characteristic_values/cvc_metrics.ex b/lib/nbn/resources/characteristic_values/cvc_metrics.ex new file mode 100644 index 0000000..7f6f5f1 --- /dev/null +++ b/lib/nbn/resources/characteristic_values/cvc_metrics.ex @@ -0,0 +1,96 @@ +# SPDX-FileCopyrightText: 2025 diffo_example contributors +# +# SPDX-License-Identifier: MIT + +defmodule DiffoExample.Nbn.CvcMetrics do + @moduledoc """ + Local metrics characteristic for a CVC — `avcs_count` and + `avcs_total_bandwidth` aggregated live across the AVCs the CVC has + assigned a cvlan to. Not inheritable. + """ + use Ash.Resource, + fragments: [Diffo.Provider.BaseCharacteristic], + domain: DiffoExample.Nbn + + resource do + description "Live metrics for a CVC — count and total downstream bandwidth across its assigned AVCs" + plural_name :cvc_metrics + end + + calculations do + calculate :value, + Diffo.Type.CharacteristicValue, + DiffoExample.Nbn.CvcMetrics.ValueCalculation do + public? true + end + end + + preparations do + prepare build(load: [:value]) + end + + jason do + pick [:name, :value] + compact true + end +end + +defmodule DiffoExample.Nbn.CvcMetrics.Value do + @moduledoc false + use Ash.TypedStruct, extensions: [AshJason.TypedStruct] + + jason do + pick [:avcs_count, :avcs_total_bandwidth] + compact true + rename avcs_count: "avcsCount", avcs_total_bandwidth: "avcsTotalBandwidth" + end + + typed_struct do + field :avcs_count, :integer + field :avcs_total_bandwidth, :integer + end +end + +defmodule DiffoExample.Nbn.CvcMetrics.ValueCalculation do + @moduledoc false + use Ash.Resource.Calculation + + require Ash.Query + + alias DiffoExample.Nbn.AvcCharacteristic + alias DiffoExample.Nbn.BandwidthProfile + alias DiffoExample.Nbn.CvcMetrics + + @impl true + def load(_, _, _), do: [] + + @impl true + def calculate(records, _, _) do + Enum.map(records, fn r -> + # AVCs the CVC has assigned a cvlan to live on the target side of + # outgoing AssignmentRelationships sourced from this CVC's :cvlan + # pool. Filter by `thing` (the pool's thing name) rather than `alias` + # — alias is the consumer's slot name and may be unset. + avc_ids = + Diffo.Provider.AssignmentRelationship + |> Ash.Query.filter_input(source_id: r.instance_id, thing: :cvlan) + |> Ash.read!(domain: Diffo.Provider) + |> Enum.map(& &1.target_id) + + avcs = + Enum.flat_map(avc_ids, fn id -> + AvcCharacteristic + |> Ash.Query.filter_input(instance_id: id) + |> Ash.read!() + end) + + %CvcMetrics.Value{ + avcs_count: length(avcs), + avcs_total_bandwidth: + Enum.reduce(avcs, 0, fn avc, acc -> + acc + BandwidthProfile.downstream(avc.bandwidth_profile) + end) + } + end) + end +end diff --git a/lib/nbn/resources/characteristic_values/nni_characteristic.ex b/lib/nbn/resources/characteristic_values/nni_characteristic.ex index 8ca3a7a..14df69f 100644 --- a/lib/nbn/resources/characteristic_values/nni_characteristic.ex +++ b/lib/nbn/resources/characteristic_values/nni_characteristic.ex @@ -13,20 +13,6 @@ defmodule DiffoExample.Nbn.NniCharacteristic do plural_name :nni_characteristics end - actions do - create :create do - accept [:name, :port_id, :capacity, :technology] - argument :instance_id, :uuid - argument :feature_id, :uuid - change manage_relationship(:instance_id, :instance, type: :append) - change manage_relationship(:feature_id, :feature, type: :append) - end - - update :update do - accept [:port_id, :capacity, :technology] - end - end - attributes do attribute :port_id, :string, public?: true attribute :capacity, :integer, public?: true diff --git a/lib/nbn/resources/characteristic_values/nni_group_characteristic.ex b/lib/nbn/resources/characteristic_values/nni_group_characteristic.ex index 18a09a6..2974594 100644 --- a/lib/nbn/resources/characteristic_values/nni_group_characteristic.ex +++ b/lib/nbn/resources/characteristic_values/nni_group_characteristic.ex @@ -13,20 +13,6 @@ defmodule DiffoExample.Nbn.NniGroupCharacteristic do plural_name :nni_group_characteristics end - actions do - create :create do - accept [:name, :group_name, :location] - argument :instance_id, :uuid - argument :feature_id, :uuid - change manage_relationship(:instance_id, :instance, type: :append) - change manage_relationship(:feature_id, :feature, type: :append) - end - - update :update do - accept [:group_name, :location] - end - end - attributes do attribute :group_name, :string, public?: true attribute :location, :string, public?: true diff --git a/lib/nbn/resources/characteristic_values/nni_group_metrics.ex b/lib/nbn/resources/characteristic_values/nni_group_metrics.ex new file mode 100644 index 0000000..c1b8c87 --- /dev/null +++ b/lib/nbn/resources/characteristic_values/nni_group_metrics.ex @@ -0,0 +1,145 @@ +# SPDX-FileCopyrightText: 2025 diffo_example contributors +# +# SPDX-License-Identifier: MIT + +defmodule DiffoExample.Nbn.NniGroupMetrics do + @moduledoc """ + Local metrics characteristic for an NNI Group — demand-side aggregates + across assigned CVCs (`cvcs_count`, `cvcs_total_bandwidth`), capacity-side + aggregates across comprised NNIs (`nnis_count`, `nnis_total_bandwidth`), + and the derived `utilization = cvcs_total_bandwidth / nnis_total_bandwidth`. + + Expected `utilization` range 0–1 under normal provisioning; >1 indicates + deliberate oversubscription. Not inheritable. + """ + use Ash.Resource, + fragments: [Diffo.Provider.BaseCharacteristic], + domain: DiffoExample.Nbn + + resource do + description "Live metrics for an NNI Group — demand from CVCs, capacity from NNIs, and utilization" + plural_name :nni_group_metrics + end + + calculations do + calculate :value, + Diffo.Type.CharacteristicValue, + DiffoExample.Nbn.NniGroupMetrics.ValueCalculation do + public? true + end + end + + preparations do + prepare build(load: [:value]) + end + + jason do + pick [:name, :value] + compact true + end +end + +defmodule DiffoExample.Nbn.NniGroupMetrics.Value do + @moduledoc false + use Ash.TypedStruct, extensions: [AshJason.TypedStruct] + + jason do + pick [ + :cvcs_count, + :cvcs_total_bandwidth, + :nnis_count, + :nnis_total_bandwidth, + :utilization + ] + + compact true + + rename cvcs_count: "cvcsCount", + cvcs_total_bandwidth: "cvcsTotalBandwidth", + nnis_count: "nnisCount", + nnis_total_bandwidth: "nnisTotalBandwidth" + end + + typed_struct do + field :cvcs_count, :integer + field :cvcs_total_bandwidth, :integer + field :nnis_count, :integer + field :nnis_total_bandwidth, :integer + field :utilization, :float + end +end + +defmodule DiffoExample.Nbn.NniGroupMetrics.ValueCalculation do + @moduledoc false + use Ash.Resource.Calculation + + require Ash.Query + + alias DiffoExample.Nbn.CvcCharacteristic + alias DiffoExample.Nbn.NniCharacteristic + alias DiffoExample.Nbn.NniGroupMetrics + + @impl true + def load(_, _, _), do: [] + + @impl true + def calculate(records, _, _) do + Enum.map(records, fn r -> + {cvcs_count, cvcs_total_bandwidth} = cvc_aggregates(r.instance_id) + {nnis_count, nnis_total_bandwidth} = nni_aggregates(r.instance_id) + + %NniGroupMetrics.Value{ + cvcs_count: cvcs_count, + cvcs_total_bandwidth: cvcs_total_bandwidth, + nnis_count: nnis_count, + nnis_total_bandwidth: nnis_total_bandwidth, + utilization: utilization(cvcs_total_bandwidth, nnis_total_bandwidth) + } + end) + end + + # Demand: CVCs the NNI Group has assigned an svlan to live on the target + # side of outgoing AssignmentRelationships sourced from the group's :svlan + # pool. Filter by `thing` (the pool's thing name) rather than `alias` — + # alias is the consumer's slot name and may be unset. + defp cvc_aggregates(nni_group_id) do + cvc_ids = + Diffo.Provider.AssignmentRelationship + |> Ash.Query.filter_input(source_id: nni_group_id, thing: :svlan) + |> Ash.read!(domain: Diffo.Provider) + |> Enum.map(& &1.target_id) + + cvcs = + Enum.flat_map(cvc_ids, fn id -> + CvcCharacteristic + |> Ash.Query.filter_input(instance_id: id) + |> Ash.read!() + end) + + {length(cvcs), Enum.reduce(cvcs, 0, fn cvc, acc -> acc + (cvc.bandwidth || 0) end)} + end + + # Capacity: NNIs the NNI Group comprises live on the target side of + # outgoing :contains Relationships sourced from the group. Relationships + # (not DefinedSimpleRelationships) carry the relate-action's TMF + # source/target/type/alias surface. + defp nni_aggregates(nni_group_id) do + nni_ids = + Diffo.Provider.Relationship + |> Ash.Query.filter_input(source_id: nni_group_id, type: :contains) + |> Ash.read!(domain: Diffo.Provider) + |> Enum.map(& &1.target_id) + + nnis = + Enum.flat_map(nni_ids, fn id -> + NniCharacteristic + |> Ash.Query.filter_input(instance_id: id) + |> Ash.read!() + end) + + {length(nnis), Enum.reduce(nnis, 0, fn nni, acc -> acc + (nni.capacity || 0) end)} + end + + defp utilization(_demand, capacity) when capacity in [nil, 0], do: 0.0 + defp utilization(demand, capacity), do: demand / capacity +end diff --git a/lib/nbn/resources/characteristic_values/ntd_characteristic.ex b/lib/nbn/resources/characteristic_values/ntd_characteristic.ex index 0ddf67e..4c7ab5c 100644 --- a/lib/nbn/resources/characteristic_values/ntd_characteristic.ex +++ b/lib/nbn/resources/characteristic_values/ntd_characteristic.ex @@ -13,20 +13,6 @@ defmodule DiffoExample.Nbn.NtdCharacteristic do plural_name :ntd_characteristics end - actions do - create :create do - accept [:name, :model, :serial_number, :technology] - argument :instance_id, :uuid - argument :feature_id, :uuid - change manage_relationship(:instance_id, :instance, type: :append) - change manage_relationship(:feature_id, :feature, type: :append) - end - - update :update do - accept [:model, :serial_number, :technology] - end - end - attributes do attribute :model, :string, public?: true attribute :serial_number, :string, public?: true diff --git a/lib/nbn/resources/characteristic_values/pri_characteristic.ex b/lib/nbn/resources/characteristic_values/pri_characteristic.ex index 283ddc6..ba9b785 100644 --- a/lib/nbn/resources/characteristic_values/pri_characteristic.ex +++ b/lib/nbn/resources/characteristic_values/pri_characteristic.ex @@ -13,36 +13,6 @@ defmodule DiffoExample.Nbn.PriCharacteristic do plural_name :pri_characteristics end - actions do - create :create do - accept [ - :name, - :avcid, - :uniid, - :technology, - :bandwidth_profile, - :speeds_downstream, - :speeds_upstream - ] - - argument :instance_id, :uuid - argument :feature_id, :uuid - change manage_relationship(:instance_id, :instance, type: :append) - change manage_relationship(:feature_id, :feature, type: :append) - end - - update :update do - accept [ - :avcid, - :uniid, - :technology, - :bandwidth_profile, - :speeds_downstream, - :speeds_upstream - ] - end - end - attributes do attribute :avcid, :string, public?: true attribute :uniid, :string, public?: true diff --git a/lib/nbn/resources/characteristic_values/uni_characteristic.ex b/lib/nbn/resources/characteristic_values/uni_characteristic.ex index 3a805f3..28dc640 100644 --- a/lib/nbn/resources/characteristic_values/uni_characteristic.ex +++ b/lib/nbn/resources/characteristic_values/uni_characteristic.ex @@ -13,20 +13,6 @@ defmodule DiffoExample.Nbn.UniCharacteristic do plural_name :uni_characteristics end - actions do - create :create do - accept [:name, :port, :encapsulation, :technology] - argument :instance_id, :uuid - argument :feature_id, :uuid - change manage_relationship(:instance_id, :instance, type: :append) - change manage_relationship(:feature_id, :feature, type: :append) - end - - update :update do - accept [:port, :encapsulation, :technology] - end - end - attributes do attribute :port, :integer, public?: true attribute :encapsulation, :string, public?: true diff --git a/lib/nbn/resources/cvc.ex b/lib/nbn/resources/cvc.ex index b2011ca..d2243f3 100644 --- a/lib/nbn/resources/cvc.ex +++ b/lib/nbn/resources/cvc.ex @@ -20,7 +20,6 @@ defmodule DiffoExample.Nbn.Cvc do use Ash.Resource, fragments: [BaseInstance], domain: Nbn, - extensions: [AshJsonApi.Resource], authorizers: [Ash.Policy.Authorizer] resource do @@ -28,10 +27,6 @@ defmodule DiffoExample.Nbn.Cvc do plural_name :Cvcs end - json_api do - type "cvc" - end - provider do specification do id "d4e5f6a7-8b9c-4d0e-bf1a-3b4c5d6e7f8a" @@ -45,6 +40,7 @@ defmodule DiffoExample.Nbn.Cvc do characteristics do characteristic :cvc, DiffoExample.Nbn.CvcCharacteristic + characteristic :metrics, DiffoExample.Nbn.CvcMetrics end pools do @@ -83,21 +79,21 @@ defmodule DiffoExample.Nbn.Cvc do argument :characteristic_value_updates, {:array, :term} change set_attribute(:resource_state, :operating) - change DiffoExample.Changes.Define + change Diffo.Provider.Changes.Define 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 {DiffoExample.Changes.Assign, pool: :cvlans} + change {Diffo.Provider.Changes.Assign, pool: :cvlans} end update :relate do description "relates the CVC with other instances (e.g. AVC aggregation, NNI Group termination)" argument :relationships, {:array, :struct} - change DiffoExample.Changes.Relate + change Diffo.Provider.Changes.Relate end end @@ -109,6 +105,22 @@ defmodule DiffoExample.Nbn.Cvc do end end + calculations do + # The NniGroup characteristic value brought up from the singular + # NniGroup this CVC is part of — single-hop via the CVC's :nni_group + # consumer-alias on its svlan assignment from the NniGroup. + calculate :nni_group, + :map, + {DiffoExample.Calculations.InheritedCharacteristicViaAssignment, + [ + via: [:nni_group], + characteristic_module: DiffoExample.Nbn.NniGroupCharacteristic, + singular?: true + ]} do + public? true + end + end + def identifier() do DiffoExample.Nbn.Util.identifier("CVC") end diff --git a/lib/nbn/resources/nbn_ethernet.ex b/lib/nbn/resources/nbn_ethernet.ex index 5ddc50c..254002a 100644 --- a/lib/nbn/resources/nbn_ethernet.ex +++ b/lib/nbn/resources/nbn_ethernet.ex @@ -18,7 +18,6 @@ defmodule DiffoExample.Nbn.NbnEthernet do use Ash.Resource, fragments: [BaseInstance], domain: Nbn, - extensions: [AshJsonApi.Resource], authorizers: [Ash.Policy.Authorizer] resource do @@ -26,10 +25,6 @@ defmodule DiffoExample.Nbn.NbnEthernet do plural_name :NbnEthernets end - json_api do - type "nbnEthernet" - end - provider do specification do id "f2a4c6e8-1b3d-4f5a-8c7e-9d0b2e4f6a8c" @@ -75,14 +70,14 @@ defmodule DiffoExample.Nbn.NbnEthernet do argument :characteristic_value_updates, {:array, :term} change set_attribute(:resource_state, :operating) - change DiffoExample.Changes.Define + change Diffo.Provider.Changes.Define end update :relate do description "relates the NBN Ethernet access with other instances (e.g. UNI)" argument :relationships, {:array, :struct} - change DiffoExample.Changes.Relate + change Diffo.Provider.Changes.Relate end end @@ -94,6 +89,67 @@ defmodule DiffoExample.Nbn.NbnEthernet do end end + calculations do + # PRI names its two owns relationships by the domain role each plays — + # `:circuit` for the AVC (Access Virtual Circuit) and `:port` for the + # UNI (the customer's port). Both are consumer-aliases on PRI's owns + # relationships, set at relate time. + + # The singular AVC this access owns — single-hop via the :circuit owns relationship. + calculate :avc, + :map, + {DiffoExample.Calculations.InheritedCharacteristicViaRelationship, + [ + alias: :circuit, + characteristic_module: DiffoExample.Nbn.AvcCharacteristic, + singular?: true + ]} do + public? true + end + + # The singular UNI this access owns — single-hop via the :port owns relationship. + calculate :uni, + :map, + {DiffoExample.Calculations.InheritedCharacteristicViaRelationship, + [ + alias: :port, + characteristic_module: DiffoExample.Nbn.UniCharacteristic, + singular?: true + ]} do + public? true + end + + # The singular CVC backing this access's circuit — two-hop via the + # :circuit owns relationship, then back via the AVC's :cvc consumer-alias + # assignment to its CVC. + calculate :cvc, + :map, + {DiffoExample.Calculations.InheritedCharacteristicViaRelationship, + [ + alias: :circuit, + then_via: [:cvc], + characteristic_module: DiffoExample.Nbn.CvcCharacteristic, + singular?: true + ]} do + public? true + end + + # The singular NTD this access's port plugs into — two-hop via the + # :port owns relationship, then back via the UNI's :ntd consumer-alias + # assignment to its NTD. + calculate :ntd, + :map, + {DiffoExample.Calculations.InheritedCharacteristicViaRelationship, + [ + alias: :port, + then_via: [:ntd], + characteristic_module: DiffoExample.Nbn.NtdCharacteristic, + singular?: true + ]} do + public? true + end + end + def identifier() do DiffoExample.Nbn.Util.identifier("PRI") end diff --git a/lib/nbn/resources/nni.ex b/lib/nbn/resources/nni.ex index db4886c..dcf239b 100644 --- a/lib/nbn/resources/nni.ex +++ b/lib/nbn/resources/nni.ex @@ -20,7 +20,6 @@ defmodule DiffoExample.Nbn.Nni do use Ash.Resource, fragments: [BaseInstance], domain: Nbn, - extensions: [AshJsonApi.Resource], authorizers: [Ash.Policy.Authorizer] resource do @@ -28,10 +27,6 @@ defmodule DiffoExample.Nbn.Nni do plural_name :Nnis end - json_api do - type "nni" - end - provider do specification do id "f6a7b8c9-0d1e-4f2a-9b3c-5d6e7f8a9b0c" @@ -77,14 +72,14 @@ defmodule DiffoExample.Nbn.Nni do argument :characteristic_value_updates, {:array, :term} change set_attribute(:resource_state, :operating) - change DiffoExample.Changes.Define + change Diffo.Provider.Changes.Define end update :relate do description "relates the NNI with other instances (e.g. its parent NNI Group)" argument :relationships, {:array, :struct} - change DiffoExample.Changes.Relate + change Diffo.Provider.Changes.Relate end end diff --git a/lib/nbn/resources/nni_group.ex b/lib/nbn/resources/nni_group.ex index 5ae78e6..45c8127 100644 --- a/lib/nbn/resources/nni_group.ex +++ b/lib/nbn/resources/nni_group.ex @@ -21,7 +21,6 @@ defmodule DiffoExample.Nbn.NniGroup do use Ash.Resource, fragments: [BaseInstance], domain: Nbn, - extensions: [AshJsonApi.Resource], authorizers: [Ash.Policy.Authorizer] resource do @@ -29,10 +28,6 @@ defmodule DiffoExample.Nbn.NniGroup do plural_name :NniGroups end - json_api do - type "nniGroup" - end - provider do specification do id "e5f6a7b8-9c0d-4e1f-8a2b-4c5d6e7f8a9b" @@ -44,6 +39,7 @@ defmodule DiffoExample.Nbn.NniGroup do characteristics do characteristic :nni_group, DiffoExample.Nbn.NniGroupCharacteristic + characteristic :metrics, DiffoExample.Nbn.NniGroupMetrics end pools do @@ -81,21 +77,21 @@ defmodule DiffoExample.Nbn.NniGroup do argument :characteristic_value_updates, {:array, :term} change set_attribute(:resource_state, :operating) - change DiffoExample.Changes.Define + change Diffo.Provider.Changes.Define 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 {DiffoExample.Changes.Assign, pool: :svlans} + change {Diffo.Provider.Changes.Assign, pool: :svlans} end update :relate do description "relates the NNI Group with other instances (e.g. NNI resources it comprises)" argument :relationships, {:array, :struct} - change DiffoExample.Changes.Relate + change Diffo.Provider.Changes.Relate end end @@ -107,5 +103,16 @@ defmodule DiffoExample.Nbn.NniGroup do end end + calculations do + # The NNI characteristic value of every NNI this NniGroup comprises — + # forward traversal of :contains Relationships (low cardinality). + calculate :nnis, + {:array, :map}, + {DiffoExample.Calculations.InheritedCharacteristicViaRelationship, + [type: :contains, characteristic_module: DiffoExample.Nbn.NniCharacteristic]} do + public? true + end + end + use DiffoExample.Nbn.RspOwnership end diff --git a/lib/nbn/resources/ntd.ex b/lib/nbn/resources/ntd.ex index 3e728af..151b88d 100644 --- a/lib/nbn/resources/ntd.ex +++ b/lib/nbn/resources/ntd.ex @@ -20,14 +20,8 @@ defmodule DiffoExample.Nbn.Ntd do use Ash.Resource, fragments: [BaseInstance], domain: Nbn, - extensions: [AshJsonApi.Resource], authorizers: [Ash.Policy.Authorizer] - resource do - description "An Ash Resource representing a Network Termination Device (NTD)" - plural_name :Ntds - end - policies do bypass DiffoExample.Nbn.Checks.NoActor do authorize_if always() @@ -42,6 +36,11 @@ defmodule DiffoExample.Nbn.Ntd do end end + resource do + description "An Ash Resource representing a Network Termination Device (NTD)" + plural_name :Ntds + end + provider do specification do id "c3d4e5f6-7a8b-4c9d-ae0f-2a3b4c5d6e7f" @@ -71,10 +70,6 @@ defmodule DiffoExample.Nbn.Ntd do end end - json_api do - type "ntd" - end - def identifier() do DiffoExample.Nbn.Util.identifier("NTD") end @@ -98,21 +93,35 @@ defmodule DiffoExample.Nbn.Ntd do argument :characteristic_value_updates, {:array, :term} change set_attribute(:resource_state, :operating) - change DiffoExample.Changes.Define + change Diffo.Provider.Changes.Define end update :assign_port do description "assigns a port from the NTD pool to a UNI" argument :assignment, :struct, constraints: [instance_of: Assignment] - change {DiffoExample.Changes.Assign, pool: :ports} + change {Diffo.Provider.Changes.Assign, pool: :ports} end update :relate do description "relates the NTD with other instances (e.g. UNI)" argument :relationships, {:array, :struct} - change DiffoExample.Changes.Relate + change Diffo.Provider.Changes.Relate + end + end + + calculations do + # The UNI characteristic value of every UNI this NTD has assigned a + # port to — reverse traversal of outgoing :port AssignmentRelationships + # sourced from this NTD. Low cardinality (typical NTD has a handful of + # ports). Filter by `thing` (the pool's thing name) rather than `alias` + # — see assignment-direction-asymmetry memory. + calculate :unis, + {:array, :map}, + {DiffoExample.Calculations.ReverseInheritedCharacteristic, + [thing: :port, characteristic_module: DiffoExample.Nbn.UniCharacteristic]} do + public? true end end end diff --git a/lib/nbn/resources/rsp.ex b/lib/nbn/resources/rsp.ex index eeaa628..12fde23 100644 --- a/lib/nbn/resources/rsp.ex +++ b/lib/nbn/resources/rsp.ex @@ -20,7 +20,7 @@ defmodule DiffoExample.Nbn.Rsp do use Ash.Resource, domain: Nbn, authorizers: [Ash.Policy.Authorizer], - extensions: [AshStateMachine, AshJsonApi.Resource], + extensions: [AshStateMachine], fragments: [Diffo.Provider.BaseParty] policies do @@ -58,10 +58,6 @@ defmodule DiffoExample.Nbn.Rsp do # actions: :read (primary), :destroy, :create (accept [:id,:name,:type,:referred_type]), # :update (name), :list (unsorted), :find_by_name - json_api do - type "rsp" - end - provider do instances do role :owns_avc, DiffoExample.Nbn.Avc diff --git a/lib/nbn/resources/types/bandwidth_profile.ex b/lib/nbn/resources/types/bandwidth_profile.ex index 0f30dea..d7ac57a 100644 --- a/lib/nbn/resources/types/bandwidth_profile.ex +++ b/lib/nbn/resources/types/bandwidth_profile.ex @@ -28,4 +28,36 @@ defmodule DiffoExample.Nbn.BandwidthProfile do ] def default, do: :home_fast + + @doc """ + Returns the representative downstream rate (Mbps) for a bandwidth profile. + + CVCs are sold as symmetric capacity in this model — we ignore the asymmetry + of satellite/wireless tiers and use the headline downstream figure as the + bandwidth contribution when aggregating. Used by `CvcMetrics` to compute + `avcs_total_bandwidth` across assigned AVCs. + + ## Examples + + iex> DiffoExample.Nbn.BandwidthProfile.downstream(:D100_U40) + 100 + iex> DiffoExample.Nbn.BandwidthProfile.downstream(:home_fast) + 500 + """ + def downstream(:D12_U1), do: 12 + def downstream(:D25_U5), do: 25 + def downstream(:D25_U10), do: 25 + def downstream(:D50_U20), do: 50 + def downstream(:D100_U40), do: 100 + def downstream(:D250_U100), do: 250 + def downstream(:D500_U200), do: 500 + def downstream(:D1000_U400), do: 1000 + def downstream(:wireless_plus), do: 100 + def downstream(:wireless_fast), do: 250 + def downstream(:wireless_superfast), do: 400 + def downstream(:home_fast), do: 500 + def downstream(:home_superfast), do: 750 + def downstream(:home_ultrafast), do: 1000 + def downstream(:home_hyperfast), do: 2000 + def downstream(nil), do: 0 end diff --git a/lib/nbn/resources/uni.ex b/lib/nbn/resources/uni.ex index 86a6f40..1ea714d 100644 --- a/lib/nbn/resources/uni.ex +++ b/lib/nbn/resources/uni.ex @@ -20,14 +20,8 @@ defmodule DiffoExample.Nbn.Uni do use Ash.Resource, fragments: [BaseInstance], domain: Nbn, - extensions: [AshJsonApi.Resource], authorizers: [Ash.Policy.Authorizer] - resource do - description "An Ash Resource representing a User Network Interface (UNI)" - plural_name :Unis - end - policies do bypass DiffoExample.Nbn.Checks.NoActor do authorize_if always() @@ -42,6 +36,11 @@ defmodule DiffoExample.Nbn.Uni do end end + resource do + description "An Ash Resource representing a User Network Interface (UNI)" + plural_name :Unis + end + provider do specification do id "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d" @@ -67,10 +66,6 @@ defmodule DiffoExample.Nbn.Uni do end end - json_api do - type "uni" - end - def identifier() do DiffoExample.Nbn.Util.identifier("UNI") end @@ -94,14 +89,14 @@ defmodule DiffoExample.Nbn.Uni do argument :characteristic_value_updates, {:array, :term} change set_attribute(:resource_state, :operating) - change DiffoExample.Changes.Define + change Diffo.Provider.Changes.Define end update :relate do description "relates the UNI with other instances (e.g. NTD, NBN Ethernet access)" argument :relationships, {:array, :struct} - change DiffoExample.Changes.Relate + change Diffo.Provider.Changes.Relate end end end diff --git a/lib/nbn/router.ex b/lib/nbn/router.ex index d72af52..5c214ab 100644 --- a/lib/nbn/router.ex +++ b/lib/nbn/router.ex @@ -6,8 +6,8 @@ defmodule DiffoExample.Nbn.Router do @moduledoc """ Diffo - TMF Service and Resource Management with a difference - NBN HTTP router. Handles the catalog endpoint directly and forwards - all JSON API traffic to the AshJsonApi router. + NBN HTTP router. Serves the catalog endpoint and forwards `/mcp` to the + AshAi MCP router (the externally-callable surface for the domain). Start with: @@ -17,7 +17,7 @@ defmodule DiffoExample.Nbn.Router do plug Plug.Parsers, parsers: [:json], - pass: ["application/vnd.api+json", "application/json"], + pass: ["application/json"], json_decoder: Jason plug :match @@ -39,5 +39,7 @@ defmodule DiffoExample.Nbn.Router do protocol_version_statement: "2024-11-05" ] - forward "/", to: DiffoExample.Nbn.ApiRouter + match _ do + send_resp(conn, 404, "not found") + end end diff --git a/mix.exs b/mix.exs index a9f4bd7..c458e96 100644 --- a/mix.exs +++ b/mix.exs @@ -6,7 +6,7 @@ defmodule DiffoExample.MixProject do @moduledoc false use Mix.Project - @version "0.2.2" + @version "0.2.3" @name "DiffoExample" @description "Examples for Diffo TMF Service and Resource Manager" @github_url "https://github.com/diffo-dev/diffo-example" @@ -61,8 +61,11 @@ defmodule DiffoExample.MixProject do logo: "logos/diffo.jpg", extras: [ "README.md": [title: "Guide"], - "documentation/domains/diffo_example_nbn.livemd": [title: "NBN Livebook"], + "documentation/domains/access.md": [title: "The Access Domain"], + "documentation/domains/diffo_example_access.livemd": [title: "Access Livebook"], + "documentation/domains/provider.md": [title: "The Provider Domain"], "documentation/domains/nbn.md": [title: "The NBN Domain"], + "documentation/domains/diffo_example_nbn.livemd": [title: "NBN Livebook"], "documentation/how_to/setup_mcp.md": [title: "Setup the MCP server"], "LICENSES/MIT.md": [title: "License"] ], @@ -89,9 +92,8 @@ defmodule DiffoExample.MixProject do # Run "mix help deps" to learn about dependencies. defp deps do [ - {:diffo, diffo_version("~> 0.4.0")}, + {:diffo, diffo_version("~> 0.4.1")}, {:ash_ai, "~> 0.6"}, - {:ash_json_api, "~> 1.6"}, {:plug_cowboy, "~> 2.7"}, {:picosat_elixir, "~> 0.2.0"}, {:simple_sat, ">= 0.0.0"}, diff --git a/mix.lock b/mix.lock index 2643834..91ce530 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,6 @@ %{ "abnf_parsec": {:hex, :abnf_parsec, "2.1.0", "c4e88d5d089f1698297c0daced12be1fb404e6e577ecf261313ebba5477941f9", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "e0ed6290c7cc7e5020c006d1003520390c9bdd20f7c3f776bd49bfe3c5cd362a"}, - "ash": {:hex, :ash, "3.25.2", "d23c52a9f823e98895d0cf1dc8bbf5d22943ffa45ba087e583d94bb05d205b2e", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 1.0", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.6.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.3", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c4e3fb9252719dd3fec84610a5a19e309f298265076da23c0bef21de237e98bb"}, + "ash": {:hex, :ash, "3.26.0", "29cc66579ac8b8efc30f6d3c2c408c5b7d27fc4b878a6a2a9971f2dd36c09d56", [:mix], [{:crux, ">= 0.1.2 and < 1.0.0-0", [hex: :crux, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ecto, "~> 3.7", [hex: :ecto, repo: "hexpm", optional: false]}, {:ets, "~> 0.8", [hex: :ets, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:picosat_elixir, "~> 0.2", [hex: :picosat_elixir, repo: "hexpm", optional: true]}, {:plug, ">= 0.0.0", [hex: :plug, repo: "hexpm", optional: true]}, {:reactor, "~> 1.0", [hex: :reactor, repo: "hexpm", optional: false]}, {:simple_sat, ">= 0.1.1 and < 1.0.0-0", [hex: :simple_sat, repo: "hexpm", optional: true]}, {:spark, ">= 2.6.0", [hex: :spark, repo: "hexpm", optional: false]}, {:splode, "~> 0.3", [hex: :splode, repo: "hexpm", optional: false]}, {:stream_data, "~> 1.0", [hex: :stream_data, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.1", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "cec7b582dcbf89810cefa641d28fa996ae2c48e5c0d6b40183f3e526e0a85d20"}, "ash_ai": {:hex, :ash_ai, "0.6.1", "d30bda859f17bed34302cd3d0c16acf56841c43c2fa3b9271c681122b8297d01", [:mix], [{:ash, ">= 3.7.1 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_authentication, "~> 4.8", [hex: :ash_authentication, repo: "hexpm", optional: true]}, {:ash_json_api, ">= 1.4.27 and < 2.0.0-0", [hex: :ash_json_api, repo: "hexpm", optional: false]}, {:ash_oban, "~> 0.5", [hex: :ash_oban, repo: "hexpm", optional: true]}, {:ash_phoenix, "~> 2.0", [hex: :ash_phoenix, repo: "hexpm", optional: true]}, {:ash_postgres, "~> 2.5", [hex: :ash_postgres, repo: "hexpm", optional: true]}, {:igniter, "~> 0.5", [hex: :igniter, repo: "hexpm", optional: true]}, {:open_api_spex, "~> 3.0", [hex: :open_api_spex, repo: "hexpm", optional: false]}, {:phx_new, "~> 1.7", [hex: :phx_new, repo: "hexpm", optional: true]}, {:plug, "~> 1.17", [hex: :plug, repo: "hexpm", optional: true]}, {:req_llm, "~> 1.7", [hex: :req_llm, repo: "hexpm", optional: false]}], "hexpm", "e950e5743ef093fdd2561cf608a1719d46dcc4125bee6f50d43fd6f8a9526c05"}, "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_json_api": {:hex, :ash_json_api, "1.6.5", "ff925107ebdced10407a6045dc3ff9e8335fe3485ce042f899817a2b47f49b5f", [:mix], [{:ash, ">= 3.19.1 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:igniter, ">= 0.3.58 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:json_xema, "~> 0.4", [hex: :json_xema, repo: "hexpm", optional: false]}, {:open_api_spex, "~> 3.16", [hex: :open_api_spex, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6", [hex: :phoenix, repo: "hexpm", optional: false]}, {:plug, "~> 1.11", [hex: :plug, repo: "hexpm", optional: false]}, {:spark, ">= 2.2.10", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "ab2f413d977a560843bbf7a7f6bc486b74e944ef51d9adf93c355a4bf984b0df"}, @@ -16,7 +16,7 @@ "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, "decimal": {:hex, :decimal, "2.4.1", "6c0fbede12fb122ba685e9ab41c6a40c129e322b3aa192f9e072e61f3a6ffaf2", [:mix], [], "hexpm", "7e618897933a8455f19a727d7c5e50a2c071a544b700e5e724298ecb4340187f"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, - "diffo": {:hex, :diffo, "0.4.0", "919101d104f3c3c8fbe61ee38f94da84a9a0f107dac94875b00b6cca30b5c04e", [:mix], [{:ash, ">= 3.24.2 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_jason, "~> 3.0", [hex: :ash_jason, repo: "hexpm", optional: false]}, {:ash_neo4j, "~> 0.6", [hex: :ash_neo4j, repo: "hexpm", optional: false]}, {:ash_outstanding, "~> 0.2.3", [hex: :ash_outstanding, repo: "hexpm", optional: false]}, {:ash_state_machine, "~> 0.2.12", [hex: :ash_state_machine, repo: "hexpm", optional: false]}, {:bolty, ">= 0.0.12", [hex: :bolty, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:uuid, "~> 1.1", [hex: :uuid, repo: "hexpm", optional: false]}], "hexpm", "6e3b37d523ee1e19c92f21956b9c3f710dc3ed87d5be813d0ed120f331bc630d"}, + "diffo": {:hex, :diffo, "0.4.1", "34d7030428b34463fb0b248a720979656ef523904ddc7382d0d7a3f85d067928", [:mix], [{:ash, ">= 3.24.2 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_jason, "~> 3.0", [hex: :ash_jason, repo: "hexpm", optional: false]}, {:ash_neo4j, "~> 0.6", [hex: :ash_neo4j, repo: "hexpm", optional: false]}, {:ash_outstanding, "~> 0.2.3", [hex: :ash_outstanding, repo: "hexpm", optional: false]}, {:ash_state_machine, "~> 0.2.12", [hex: :ash_state_machine, repo: "hexpm", optional: false]}, {:bolty, ">= 0.0.12", [hex: :bolty, repo: "hexpm", optional: false]}, {:igniter, ">= 0.6.29 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: true]}, {:uuid, "~> 1.1", [hex: :uuid, repo: "hexpm", optional: false]}], "hexpm", "bab49f959a2123cc31a4434ea7ba367f632edac30ad1808bf22a5d353a1460a3"}, "dotenvy": {:hex, :dotenvy, "1.1.1", "00e318f3c51de9fafc4b48598447e386f19204dc18ca69886905bb8f8b08b667", [:mix], [], "hexpm", "c8269471b5701e9e56dc86509c1199ded2b33dce088c3471afcfef7839766d8e"}, "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, "ecto": {:hex, :ecto, "3.13.6", "352135b474f91d1ab99a1b502171d207e9db60421c9e3d0ecab4c7ab96b24d14", [:mix], [{:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "8afa059bc16cd2c94739ec0a11e3e5df69d828125119109bef35f20a21a76af2"}, @@ -34,7 +34,6 @@ "jason": {:hex, :jason, "1.4.5", "2e3a008590b0b8d7388c20293e9dcc9cf3e5d642fd2a114e4cbbb52e595d940a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "b0c823996102bcd0239b3c2444eb00409b72f6a140c1950bc8b457d836b30684"}, "json_xema": {:hex, :json_xema, "0.6.5", "060459c9c9152650edb4427b1acbc61fa43a23bcea0301d200cafa76e0880f37", [:mix], [{:conv_case, "~> 0.2", [hex: :conv_case, repo: "hexpm", optional: false]}, {:xema, "~> 0.16", [hex: :xema, repo: "hexpm", optional: false]}], "hexpm", "b8ffdbc2f67aa8b91b44e1ba0ab77eb5c0b0142116f8fbb804977fb939d470ef"}, "jsv": {:hex, :jsv, "0.19.1", "9dd02fb0a7beee58917a1a364cdd125c2df86ff99177d1b0bdd6b896c25d05cf", [:mix], [{:abnf_parsec, "~> 2.0", [hex: :abnf_parsec, repo: "hexpm", optional: false]}, {:decimal, "~> 2.0 or ~> 3.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:idna, "~> 6.0 or ~> 7.0", [hex: :idna, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:nimble_options, "~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:texture, "~> 1.0", [hex: :texture, repo: "hexpm", optional: false]}], "hexpm", "ccdd8eb4a7953a0bd939951b0924e4a41aaa6b3934b0875b64f3dbcae97b09be"}, - "libgraph": {:hex, :libgraph, "0.16.0", "3936f3eca6ef826e08880230f806bfea13193e49bf153f93edcf0239d4fd1d07", [:mix], [], "hexpm", "41ca92240e8a4138c30a7e06466acc709b0cbb795c643e9e17174a178982d6bf"}, "llm_db": {:hex, :llm_db, "2026.4.8", "402ba69f4281e964e885aa465eebbb72b2f74aadc88c631ffe4a6ddd6a5d62b6", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:dotenvy, "~> 1.1", [hex: :dotenvy, repo: "hexpm", optional: false]}, {:igniter, "~> 0.7", [hex: :igniter, repo: "hexpm", optional: true]}, {:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:req, "~> 0.5", [hex: :req, repo: "hexpm", optional: false]}, {:toml, "~> 0.7", [hex: :toml, repo: "hexpm", optional: false]}, {:zoi, "~> 0.10", [hex: :zoi, repo: "hexpm", optional: false]}], "hexpm", "d34d1ff9931572e74873a3d7e6334efed1f0fe8e9a85e2c283c02b8970cee525"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, diff --git a/test/access/cable_test.exs b/test/access/cable_test.exs index 2365ba0..61a7560 100644 --- a/test/access/cable_test.exs +++ b/test/access/cable_test.exs @@ -12,7 +12,6 @@ defmodule DiffoExample.Access.CableTest do alias DiffoExample.Access.Cable alias DiffoExample.Access.IntegerUnit alias DiffoExample.Test.Characteristics - alias DiffoExample.Util describe "build cable" do test "create a cable" do @@ -39,11 +38,9 @@ defmodule DiffoExample.Access.CableTest do encoding = Jason.encode!(cable) |> Diffo.Util.summarise_dates() - |> Util.summarise_characteristics(cable) assert encoding == - ~s({\"id\":\"#{cable.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{cable.id}",\"category\":\"Network Resource\",\"description\":\"A Cable Resource Instance\",\"resourceSpecification\":{\"id\":\"ce0a567a-6abb-4862-9e33-851fd79fa595\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/ce0a567a-6abb-4862-9e33-851fd79fa595\",\"name\":\"cable\",\"version\":\"v1.0.0\"}}) - |> Util.summarise_characteristics(cable) + ~s({\"id\":\"#{cable.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{cable.id}",\"category\":\"Network Resource\",\"description\":\"A Cable Resource Instance\",\"resourceSpecification\":{\"id\":\"ce0a567a-6abb-4862-9e33-851fd79fa595\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/ce0a567a-6abb-4862-9e33-851fd79fa595\",\"name\":\"cable\",\"version\":\"v1.0.0\"},\"resourceCharacteristic\":[{\"name\":\"cable\",\"value\":{}},{\"name\":\"pairs\",\"value\":{\"first\":1,\"last\":1,\"algorithm\":\"lowest\"}}]}) end test "define cable" do @@ -67,11 +64,9 @@ defmodule DiffoExample.Access.CableTest do encoding = Jason.encode!(cable) |> Diffo.Util.summarise_dates() - |> Util.summarise_characteristics(cable) assert encoding == - ~s({\"id\":\"#{cable.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{cable.id}",\"category\":\"Network Resource\",\"description\":\"A Cable Resource Instance\",\"resourceSpecification\":{\"id\":\"ce0a567a-6abb-4862-9e33-851fd79fa595\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/ce0a567a-6abb-4862-9e33-851fd79fa595\",\"name\":\"cable\",\"version\":\"v1.0.0\"},\"lifecycleState\":\"operating\"}) - |> Util.summarise_characteristics(cable) + ~s({\"id\":\"#{cable.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{cable.id}",\"category\":\"Network Resource\",\"description\":\"A Cable Resource Instance\",\"resourceSpecification\":{\"id\":\"ce0a567a-6abb-4862-9e33-851fd79fa595\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/ce0a567a-6abb-4862-9e33-851fd79fa595\",\"name\":\"cable\",\"version\":\"v1.0.0\"},\"lifecycleState\":\"operating\",\"resourceCharacteristic\":[{\"name\":\"cable\",\"value\":{\"pairs\":60,\"length\":{\"amount\":600,\"unit\":\"m\"},\"technology\":\"PIUT\"}},{\"name\":\"pairs\",\"value\":{\"first\":1,\"last\":60,\"type\":\"copper\",\"algorithm\":\"lowest\"}}]}) end test "auto assign pair to service" do @@ -96,11 +91,9 @@ defmodule DiffoExample.Access.CableTest do encoding = Jason.encode!(cable) |> Diffo.Util.summarise_dates() - |> Util.summarise_characteristics(cable) assert encoding == - ~s({\"id\":\"#{cable.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{cable.id}",\"category\":\"Network Resource\",\"description\":\"A Cable Resource Instance\",\"resourceSpecification\":{\"id\":\"ce0a567a-6abb-4862-9e33-851fd79fa595\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/ce0a567a-6abb-4862-9e33-851fd79fa595\",\"name\":\"cable\",\"version\":\"v1.0.0\"},\"lifecycleState\":\"operating\",\"serviceRelationship\":[{\"type\":\"assignedTo\",\"service\":{\"id\":\"#{assignee.id}\",\"href\":\"serviceInventoryManagement/v4/service/#{assignee.id}\"},\"serviceRelationshipCharacteristic\":[{\"name\":\"pair\",\"value\":1}]}]}) - |> Util.summarise_characteristics(cable) + ~s({\"id\":\"#{cable.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{cable.id}",\"category\":\"Network Resource\",\"description\":\"A Cable Resource Instance\",\"resourceSpecification\":{\"id\":\"ce0a567a-6abb-4862-9e33-851fd79fa595\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/ce0a567a-6abb-4862-9e33-851fd79fa595\",\"name\":\"cable\",\"version\":\"v1.0.0\"},\"lifecycleState\":\"operating\",\"serviceRelationship\":[{\"type\":\"assignedTo\",\"service\":{\"id\":\"#{assignee.id}\",\"href\":\"serviceInventoryManagement/v4/service/#{assignee.id}\"},\"serviceRelationshipCharacteristic\":[{\"name\":\"pair\",\"value\":1}]}],\"resourceCharacteristic\":[{\"name\":\"cable\",\"value\":{\"pairs\":60,\"length\":{\"amount\":600,\"unit\":\"m\"},\"technology\":\"PIUT\"}},{\"name\":\"pairs\",\"value\":{\"first\":1,\"last\":60,\"type\":\"copper\",\"algorithm\":\"lowest\"}}]}) end test "auto assign two pairs to same service" do @@ -130,11 +123,9 @@ defmodule DiffoExample.Access.CableTest do encoding = Jason.encode!(cable) |> Diffo.Util.summarise_dates() - |> Util.summarise_characteristics(cable) assert encoding == - ~s({\"id\":\"#{cable.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{cable.id}",\"category\":\"Network Resource\",\"description\":\"A Cable Resource Instance\",\"resourceSpecification\":{\"id\":\"ce0a567a-6abb-4862-9e33-851fd79fa595\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/ce0a567a-6abb-4862-9e33-851fd79fa595\",\"name\":\"cable\",\"version\":\"v1.0.0\"},\"lifecycleState\":\"operating\",\"serviceRelationship\":[{\"type\":\"assignedTo\",\"service\":{\"id\":\"#{assignee.id}\",\"href\":\"serviceInventoryManagement/v4/service/#{assignee.id}\"},\"serviceRelationshipCharacteristic\":[{\"name\":\"pair\",\"value\":1}]},{\"type\":\"assignedTo\",\"service\":{\"id\":\"#{assignee.id}\",\"href\":\"serviceInventoryManagement/v4/service/#{assignee.id}\"},\"serviceRelationshipCharacteristic\":[{\"name\":\"pair\",\"value\":2}]}]}) - |> Util.summarise_characteristics(cable) + ~s({\"id\":\"#{cable.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{cable.id}",\"category\":\"Network Resource\",\"description\":\"A Cable Resource Instance\",\"resourceSpecification\":{\"id\":\"ce0a567a-6abb-4862-9e33-851fd79fa595\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/ce0a567a-6abb-4862-9e33-851fd79fa595\",\"name\":\"cable\",\"version\":\"v1.0.0\"},\"lifecycleState\":\"operating\",\"serviceRelationship\":[{\"type\":\"assignedTo\",\"service\":{\"id\":\"#{assignee.id}\",\"href\":\"serviceInventoryManagement/v4/service/#{assignee.id}\"},\"serviceRelationshipCharacteristic\":[{\"name\":\"pair\",\"value\":1}]},{\"type\":\"assignedTo\",\"service\":{\"id\":\"#{assignee.id}\",\"href\":\"serviceInventoryManagement/v4/service/#{assignee.id}\"},\"serviceRelationshipCharacteristic\":[{\"name\":\"pair\",\"value\":2}]}],\"resourceCharacteristic\":[{\"name\":\"cable\",\"value\":{\"pairs\":60,\"length\":{\"amount\":600,\"unit\":\"m\"},\"technology\":\"PIUT\"}},{\"name\":\"pairs\",\"value\":{\"first\":1,\"last\":60,\"type\":\"copper\",\"algorithm\":\"lowest\"}}]}) end test "specific assignment rejects duplicate request" do @@ -164,11 +155,9 @@ defmodule DiffoExample.Access.CableTest do encoding = Jason.encode!(cable) |> Diffo.Util.summarise_dates() - |> Util.summarise_characteristics(cable) assert encoding == - ~s({\"id\":\"#{cable.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{cable.id}",\"category\":\"Network Resource\",\"description\":\"A Cable Resource Instance\",\"resourceSpecification\":{\"id\":\"ce0a567a-6abb-4862-9e33-851fd79fa595\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/ce0a567a-6abb-4862-9e33-851fd79fa595\",\"name\":\"cable\",\"version\":\"v1.0.0\"},\"lifecycleState\":\"operating\",\"serviceRelationship\":[{\"type\":\"assignedTo\",\"service\":{\"id\":\"#{assignee.id}\",\"href\":\"serviceInventoryManagement/v4/service/#{assignee.id}\"},\"serviceRelationshipCharacteristic\":[{\"name\":\"pair\",\"value\":5}]}]}) - |> Util.summarise_characteristics(cable) + ~s({\"id\":\"#{cable.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{cable.id}",\"category\":\"Network Resource\",\"description\":\"A Cable Resource Instance\",\"resourceSpecification\":{\"id\":\"ce0a567a-6abb-4862-9e33-851fd79fa595\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/ce0a567a-6abb-4862-9e33-851fd79fa595\",\"name\":\"cable\",\"version\":\"v1.0.0\"},\"lifecycleState\":\"operating\",\"serviceRelationship\":[{\"type\":\"assignedTo\",\"service\":{\"id\":\"#{assignee.id}\",\"href\":\"serviceInventoryManagement/v4/service/#{assignee.id}\"},\"serviceRelationshipCharacteristic\":[{\"name\":\"pair\",\"value\":5}]}],\"resourceCharacteristic\":[{\"name\":\"cable\",\"value\":{\"pairs\":60,\"length\":{\"amount\":600,\"unit\":\"m\"},\"technology\":\"PIUT\"}},{\"name\":\"pairs\",\"value\":{\"first\":1,\"last\":60,\"type\":\"copper\",\"algorithm\":\"lowest\"}}]}) end end end diff --git a/test/access/card_test.exs b/test/access/card_test.exs index cca2682..d27afdd 100644 --- a/test/access/card_test.exs +++ b/test/access/card_test.exs @@ -11,7 +11,6 @@ defmodule DiffoExample.Access.CardTest do alias DiffoExample.Access alias DiffoExample.Access.Card alias DiffoExample.Test.Characteristics - alias DiffoExample.Util describe "build card" do test "create a card" do @@ -38,11 +37,9 @@ defmodule DiffoExample.Access.CardTest do encoding = Jason.encode!(card) |> Diffo.Util.summarise_dates() - |> Util.summarise_characteristics(card) assert encoding == - ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{card.id}",\"category\":\"Network Resource\",\"description\":\"A Card Resource Instance\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"}}) - |> Util.summarise_characteristics(card) + ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{card.id}",\"category\":\"Network Resource\",\"description\":\"A Card Resource Instance\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"},\"resourceCharacteristic\":[{\"name\":\"card\",\"value\":{}},{\"name\":\"ports\",\"value\":{\"first\":1,\"last\":1,\"algorithm\":\"lowest\"}}]}) end test "define card" do @@ -66,11 +63,9 @@ defmodule DiffoExample.Access.CardTest do encoding = Jason.encode!(card) |> Diffo.Util.summarise_dates() - |> Util.summarise_characteristics(card) assert encoding == - ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{card.id}",\"category\":\"Network Resource\",\"description\":\"A Card Resource Instance\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"},\"lifecycleState\":\"operating\"}) - |> Util.summarise_characteristics(card) + ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{card.id}",\"category\":\"Network Resource\",\"description\":\"A Card Resource Instance\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"},\"lifecycleState\":\"operating\",\"resourceCharacteristic\":[{\"name\":\"card\",\"value\":{\"family\":\"ISAM\",\"model\":\"EBLT48\",\"technology\":\"adsl2Plus\"}},{\"name\":\"ports\",\"value\":{\"first\":1,\"last\":48,\"type\":\"ADSL2+\",\"algorithm\":\"lowest\"}}]}) end test "auto assign port to service" do @@ -95,11 +90,9 @@ defmodule DiffoExample.Access.CardTest do encoding = Jason.encode!(card) |> Diffo.Util.summarise_dates() - |> Util.summarise_characteristics(card) assert encoding == - ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{card.id}",\"category\":\"Network Resource\",\"description\":\"A Card Resource Instance\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"},\"lifecycleState\":\"operating\",\"serviceRelationship\":[{\"type\":\"assignedTo\",\"service\":{\"id\":\"#{assignee.id}\",\"href\":\"serviceInventoryManagement/v4/service/#{assignee.id}\"},\"serviceRelationshipCharacteristic\":[{\"name\":\"port\",\"value\":1}]}]}) - |> Util.summarise_characteristics(card) + ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{card.id}",\"category\":\"Network Resource\",\"description\":\"A Card Resource Instance\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"},\"lifecycleState\":\"operating\",\"serviceRelationship\":[{\"type\":\"assignedTo\",\"service\":{\"id\":\"#{assignee.id}\",\"href\":\"serviceInventoryManagement/v4/service/#{assignee.id}\"},\"serviceRelationshipCharacteristic\":[{\"name\":\"port\",\"value\":1}]}],\"resourceCharacteristic\":[{\"name\":\"card\",\"value\":{\"family\":\"ISAM\",\"model\":\"EBLT48\",\"technology\":\"adsl2Plus\"}},{\"name\":\"ports\",\"value\":{\"first\":1,\"last\":48,\"type\":\"ADSL2+\",\"algorithm\":\"lowest\"}}]}) end test "auto assign two ports to same service" do @@ -129,11 +122,9 @@ defmodule DiffoExample.Access.CardTest do encoding = Jason.encode!(card) |> Diffo.Util.summarise_dates() - |> Util.summarise_characteristics(card) assert encoding == - ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{card.id}",\"category\":\"Network Resource\",\"description\":\"A Card Resource Instance\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"},\"lifecycleState\":\"operating\",\"serviceRelationship\":[{\"type\":\"assignedTo\",\"service\":{\"id\":\"#{assignee.id}\",\"href\":\"serviceInventoryManagement/v4/service/#{assignee.id}\"},\"serviceRelationshipCharacteristic\":[{\"name\":\"port\",\"value\":1}]},{\"type\":\"assignedTo\",\"service\":{\"id\":\"#{assignee.id}\",\"href\":\"serviceInventoryManagement/v4/service/#{assignee.id}\"},\"serviceRelationshipCharacteristic\":[{\"name\":\"port\",\"value\":2}]}]}) - |> Util.summarise_characteristics(card) + ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{card.id}",\"category\":\"Network Resource\",\"description\":\"A Card Resource Instance\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"},\"lifecycleState\":\"operating\",\"serviceRelationship\":[{\"type\":\"assignedTo\",\"service\":{\"id\":\"#{assignee.id}\",\"href\":\"serviceInventoryManagement/v4/service/#{assignee.id}\"},\"serviceRelationshipCharacteristic\":[{\"name\":\"port\",\"value\":1}]},{\"type\":\"assignedTo\",\"service\":{\"id\":\"#{assignee.id}\",\"href\":\"serviceInventoryManagement/v4/service/#{assignee.id}\"},\"serviceRelationshipCharacteristic\":[{\"name\":\"port\",\"value\":2}]}],\"resourceCharacteristic\":[{\"name\":\"card\",\"value\":{\"family\":\"ISAM\",\"model\":\"EBLT48\",\"technology\":\"adsl2Plus\"}},{\"name\":\"ports\",\"value\":{\"first\":1,\"last\":48,\"type\":\"ADSL2+\",\"algorithm\":\"lowest\"}}]}) end test "specific assignment rejects duplicate request" do @@ -163,11 +154,9 @@ defmodule DiffoExample.Access.CardTest do encoding = Jason.encode!(card) |> Diffo.Util.summarise_dates() - |> Util.summarise_characteristics(card) assert encoding == - ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{card.id}",\"category\":\"Network Resource\",\"description\":\"A Card Resource Instance\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"},\"lifecycleState\":\"operating\",\"serviceRelationship\":[{\"type\":\"assignedTo\",\"service\":{\"id\":\"#{assignee.id}\",\"href\":\"serviceInventoryManagement/v4/service/#{assignee.id}\"},\"serviceRelationshipCharacteristic\":[{\"name\":\"port\",\"value\":5}]}]}) - |> Util.summarise_characteristics(card) + ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{card.id}",\"category\":\"Network Resource\",\"description\":\"A Card Resource Instance\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"},\"lifecycleState\":\"operating\",\"serviceRelationship\":[{\"type\":\"assignedTo\",\"service\":{\"id\":\"#{assignee.id}\",\"href\":\"serviceInventoryManagement/v4/service/#{assignee.id}\"},\"serviceRelationshipCharacteristic\":[{\"name\":\"port\",\"value\":5}]}],\"resourceCharacteristic\":[{\"name\":\"card\",\"value\":{\"family\":\"ISAM\",\"model\":\"EBLT48\",\"technology\":\"adsl2Plus\"}},{\"name\":\"ports\",\"value\":{\"first\":1,\"last\":48,\"type\":\"ADSL2+\",\"algorithm\":\"lowest\"}}]}) end end end diff --git a/test/access/dsl_access_test.exs b/test/access/dsl_access_test.exs index 8478e74..9815384 100644 --- a/test/access/dsl_access_test.exs +++ b/test/access/dsl_access_test.exs @@ -15,7 +15,6 @@ defmodule DiffoExample.Access.DslAccessTest do alias DiffoExample.Access.DslAccess alias DiffoExample.Test.Parties alias DiffoExample.Test.Places - alias DiffoExample.Util describe "service qualification" do test "create an initial service for service qualification" do @@ -71,14 +70,12 @@ defmodule DiffoExample.Access.DslAccessTest do encoding = Jason.encode!(dsl_access) |> Diffo.Util.summarise_dates() - |> Util.summarise_characteristics(dsl_access) assert encoding == - ~s({\"id\":\"#{dsl_access.id}",\"href\":\"serviceInventoryManagement/v4/service/#{dsl_access.id}\",\"category\":\"Network Service\",\"description\":\"A DSL Access Network Service connecting a subscriber premises to an NNI\",\"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\"}]}) - |> Util.summarise_characteristics(dsl_access) + ~s({\"id\":\"#{dsl_access.id}",\"href\":\"serviceInventoryManagement/v4/service/#{dsl_access.id}\",\"category\":\"Network Service\",\"description\":\"A DSL Access Network Service connecting a subscriber premises to an NNI\",\"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}],\"serviceCharacteristic\":[{\"name\":\"dslam\",\"value\":{}},{\"name\":\"aggregate_interface\",\"value\":{}},{\"name\":\"circuit\",\"value\":{}},{\"name\":\"line\",\"value\":{}}],\"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 inactive" do + test "advance service to feasibilityChecked" do initial_parties = create_initial_parties() initial_place = create_initial_place() @@ -95,7 +92,7 @@ defmodule DiffoExample.Access.DslAccessTest do # check the instance is a DslAccess assert is_struct(dsl_access, DslAccess) - assert dsl_access.service_state == :inactive + assert dsl_access.service_state == :feasibilityChecked assert dsl_access.service_operating_status == :feasible Places.check_places([initial_place | [esa_place]], dsl_access) @@ -103,11 +100,9 @@ defmodule DiffoExample.Access.DslAccessTest do encoding = Jason.encode!(dsl_access) |> Diffo.Util.summarise_dates() - |> Util.summarise_characteristics(dsl_access) assert encoding == - ~s({\"id\":\"#{dsl_access.id}",\"href\":\"serviceInventoryManagement/v4/service/#{dsl_access.id}\",\"category\":\"Network Service\",\"description\":\"A DSL Access Network Service connecting a subscriber premises to an NNI\",\"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\":\"inactive\",\"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\"}]}) - |> Util.summarise_characteristics(dsl_access) + ~s({\"id\":\"#{dsl_access.id}",\"href\":\"serviceInventoryManagement/v4/service/#{dsl_access.id}\",\"category\":\"Network Service\",\"description\":\"A DSL Access Network Service connecting a subscriber premises to an NNI\",\"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}],\"serviceCharacteristic\":[{\"name\":\"dslam\",\"value\":{}},{\"name\":\"aggregate_interface\",\"value\":{}},{\"name\":\"circuit\",\"value\":{}},{\"name\":\"line\",\"value\":{}}],\"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 @@ -148,11 +143,9 @@ defmodule DiffoExample.Access.DslAccessTest do encoding = Jason.encode!(dsl_access) |> Diffo.Util.summarise_dates() - |> Util.summarise_characteristics(dsl_access) assert encoding == - ~s({\"id\":\"#{dsl_access.id}",\"href\":\"serviceInventoryManagement/v4/service/#{dsl_access.id}\",\"category\":\"Network Service\",\"description\":\"A DSL Access Network Service connecting a subscriber premises to an NNI\",\"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\"}]}) - |> Util.summarise_characteristics(dsl_access) + ~s({\"id\":\"#{dsl_access.id}",\"href\":\"serviceInventoryManagement/v4/service/#{dsl_access.id}\",\"category\":\"Network Service\",\"description\":\"A DSL Access Network Service connecting a subscriber premises to an NNI\",\"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}],\"serviceCharacteristic\":[{\"name\":\"dslam\",\"value\":{\"name\":\"QDONC0001\",\"model\":\"ISAM7330\"}},{\"name\":\"aggregate_interface\",\"value\":{\"name\":\"eth0\",\"svlanId\":3108}},{\"name\":\"circuit\",\"value\":{\"cvlan_id\":82}},{\"name\":\"line\",\"value\":{\"port\":5,\"slot\":10}}],\"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/access/path_test.exs b/test/access/path_test.exs index 1a135c7..94debff 100644 --- a/test/access/path_test.exs +++ b/test/access/path_test.exs @@ -16,7 +16,6 @@ defmodule DiffoExample.Access.PathTest do alias DiffoExample.Access.Path alias DiffoExample.Test.Parties alias DiffoExample.Test.Places - alias DiffoExample.Util describe "build path" do test "create a path" do @@ -52,11 +51,9 @@ defmodule DiffoExample.Access.PathTest do encoding = Jason.encode!(path) |> Diffo.Util.summarise_dates() - |> Util.summarise_characteristics(path) assert encoding == - ~s({\"id\":\"#{path.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{path.id}",\"category\":\"Network Resource\",\"description\":\"A Path Resource Instance\",\"name\":\"82 Rathmullen - DONC\",\"resourceSpecification\":{\"id\":\"1d507914-8f76-48cb-aa0e-3a8f92951ab0\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/1d507914-8f76-48cb-aa0e-3a8f92951ab0\",\"name\":\"path\",\"version\":\"v1.0.0\"},\"resourceCharacteristic\":[{\"name\":\"path\",\"value\":{\"sections\":0}}],\"place\":[{\"id\":\"1657363\",\"href\":\"place/telco/1657363\",\"name\":\"addressId\",\"role\":\"CustomerSite\",\"@referredType\":\"GeographicAddress\",\"@type\":\"PlaceRef\"},{\"id\":\"DONC\",\"href\":\"place/telco/DONC\",\"name\":\"exchangeId\",\"role\":\"NetworkSite\",\"@referredType\":\"GeographicSite\",\"@type\":\"PlaceRef\"},{\"id\":\"DONC-0001\",\"href\":\"place/telco/DONC-0001\",\"name\":\"esaId\",\"role\":\"ServingArea\",\"@referredType\":\"GeographicLocation\",\"@type\":\"PlaceRef\"}],\"relatedParty\":[{\"id\":\"Access\",\"name\":\"organizationId\",\"role\":\"Provider\",\"@referredType\":\"Organization\",\"@type\":\"PartyRef\"}]}) - |> Util.summarise_characteristics(path) + ~s({\"id\":\"#{path.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{path.id}",\"category\":\"Network Resource\",\"description\":\"A Path Resource Instance\",\"name\":\"82 Rathmullen - DONC\",\"resourceSpecification\":{\"id\":\"1d507914-8f76-48cb-aa0e-3a8f92951ab0\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/1d507914-8f76-48cb-aa0e-3a8f92951ab0\",\"name\":\"path\",\"version\":\"v1.0.0\"},\"resourceCharacteristic\":[{\"name\":\"path\",\"value\":{}}],\"place\":[{\"id\":\"1657363\",\"href\":\"place/telco/1657363\",\"name\":\"addressId\",\"role\":\"CustomerSite\",\"@referredType\":\"GeographicAddress\",\"@type\":\"PlaceRef\"},{\"id\":\"DONC\",\"href\":\"place/telco/DONC\",\"name\":\"exchangeId\",\"role\":\"NetworkSite\",\"@referredType\":\"GeographicSite\",\"@type\":\"PlaceRef\"},{\"id\":\"DONC-0001\",\"href\":\"place/telco/DONC-0001\",\"name\":\"esaId\",\"role\":\"ServingArea\",\"@referredType\":\"GeographicLocation\",\"@type\":\"PlaceRef\"}],\"relatedParty\":[{\"id\":\"Access\",\"name\":\"organizationId\",\"role\":\"Provider\",\"@referredType\":\"Organization\",\"@type\":\"PartyRef\"}]}) end end @@ -76,11 +73,9 @@ defmodule DiffoExample.Access.PathTest do encoding = Jason.encode!(path) |> Diffo.Util.summarise_dates() - |> Util.summarise_characteristics(path) assert encoding == - ~s({\"id\":\"#{path.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{path.id}",\"category\":\"Network Resource\",\"description\":\"A Path Resource Instance\",\"name\":\"82 Rathmullen - DONC\",\"resourceSpecification\":{\"id\":\"1d507914-8f76-48cb-aa0e-3a8f92951ab0\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/1d507914-8f76-48cb-aa0e-3a8f92951ab0\",\"name\":\"path\",\"version\":\"v1.0.0\"},\"resourceCharacteristic\":[{\"name\":\"path\",\"value\":{\"name\":\"82 Rathmullen - DONC\",\"sections\":0,\"technology\":\"copper\"}}],\"place\":[{\"id\":\"1657363\",\"href\":\"place/telco/1657363\",\"name\":\"addressId\",\"role\":\"CustomerSite\",\"@referredType\":\"GeographicAddress\",\"@type\":\"PlaceRef\"},{\"id\":\"DONC\",\"href\":\"place/telco/DONC\",\"name\":\"exchangeId\",\"role\":\"NetworkSite\",\"@referredType\":\"GeographicSite\",\"@type\":\"PlaceRef\"},{\"id\":\"DONC-0001\",\"href\":\"place/telco/DONC-0001\",\"name\":\"esaId\",\"role\":\"ServingArea\",\"@referredType\":\"GeographicLocation\",\"@type\":\"PlaceRef\"}],\"relatedParty\":[{\"id\":\"Access\",\"name\":\"organizationId\",\"role\":\"Provider\",\"@referredType\":\"Organization\",\"@type\":\"PartyRef\"}]}) - |> Util.summarise_characteristics(path) + ~s({\"id\":\"#{path.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{path.id}",\"category\":\"Network Resource\",\"description\":\"A Path Resource Instance\",\"name\":\"82 Rathmullen - DONC\",\"resourceSpecification\":{\"id\":\"1d507914-8f76-48cb-aa0e-3a8f92951ab0\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/1d507914-8f76-48cb-aa0e-3a8f92951ab0\",\"name\":\"path\",\"version\":\"v1.0.0\"},\"resourceCharacteristic\":[{\"name\":\"path\",\"value\":{\"name\":\"82 Rathmullen - DONC\",\"technology\":\"copper\"}}],\"place\":[{\"id\":\"1657363\",\"href\":\"place/telco/1657363\",\"name\":\"addressId\",\"role\":\"CustomerSite\",\"@referredType\":\"GeographicAddress\",\"@type\":\"PlaceRef\"},{\"id\":\"DONC\",\"href\":\"place/telco/DONC\",\"name\":\"exchangeId\",\"role\":\"NetworkSite\",\"@referredType\":\"GeographicSite\",\"@type\":\"PlaceRef\"},{\"id\":\"DONC-0001\",\"href\":\"place/telco/DONC-0001\",\"name\":\"esaId\",\"role\":\"ServingArea\",\"@referredType\":\"GeographicLocation\",\"@type\":\"PlaceRef\"}],\"relatedParty\":[{\"id\":\"Access\",\"name\":\"organizationId\",\"role\":\"Provider\",\"@referredType\":\"Organization\",\"@type\":\"PartyRef\"}]}) end test "relate cables and dslam" do @@ -109,11 +104,12 @@ defmodule DiffoExample.Access.PathTest do # now assign a port from a line card [_dslam, line_card] = create_dslam_with_line_card("QDONC-0001", tl(places), parties) - # path-as-assignee names its slot :port when requesting the port-assignment - # from the line card. This alias lets the InheritedCharacteristic calc - # traverse path → port → card (and transitively card → slot → shelf). + # path-as-assignee names its upstream Card relationship :card when + # requesting the port-assignment. The consumer-alias names the + # related resource, letting the inheritance calc traverse path → :card + # → card (and transitively card → :shelf → shelf). Access.assign_port!(line_card, %{ - assignment: %Assignment{assignee_id: path.id, alias: :port, operation: :auto_assign} + assignment: %Assignment{assignee_id: path.id, alias: :card, operation: :auto_assign} }) # 5 cables each assigned a pair to the path, plus 1 line card assigned a port @@ -143,12 +139,10 @@ defmodule DiffoExample.Access.PathTest do encoding = Jason.encode!(path) |> Diffo.Util.summarise_dates() - |> Util.summarise_characteristics(path) # the reverse relationships are not encoded to json assert encoding == - ~s({\"id\":\"#{path.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{path.id}",\"category\":\"Network Resource\",\"description\":\"A Path Resource Instance\",\"name\":\"82 Rathmullen - DONC\",\"resourceSpecification\":{\"id\":\"1d507914-8f76-48cb-aa0e-3a8f92951ab0\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/1d507914-8f76-48cb-aa0e-3a8f92951ab0\",\"name\":\"path\",\"version\":\"v1.0.0\"},\"resourceCharacteristic\":[{\"name\":\"path\",\"value\":{\"name\":\"82 Rathmullen - DONC\",\"sections\":0,\"technology\":\"copper\"}}],\"place\":[{\"id\":\"1657363\",\"href\":\"place/telco/1657363\",\"name\":\"addressId\",\"role\":\"CustomerSite\",\"@referredType\":\"GeographicAddress\",\"@type\":\"PlaceRef\"},{\"id\":\"DONC\",\"href\":\"place/telco/DONC\",\"name\":\"exchangeId\",\"role\":\"NetworkSite\",\"@referredType\":\"GeographicSite\",\"@type\":\"PlaceRef\"},{\"id\":\"DONC-0001\",\"href\":\"place/telco/DONC-0001\",\"name\":\"esaId\",\"role\":\"ServingArea\",\"@referredType\":\"GeographicLocation\",\"@type\":\"PlaceRef\"}],\"relatedParty\":[{\"id\":\"Access\",\"name\":\"organizationId\",\"role\":\"Provider\",\"@referredType\":\"Organization\",\"@type\":\"PartyRef\"}]}) - |> Util.summarise_characteristics(path) + ~s({\"id\":\"#{path.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{path.id}",\"category\":\"Network Resource\",\"description\":\"A Path Resource Instance\",\"name\":\"82 Rathmullen - DONC\",\"resourceSpecification\":{\"id\":\"1d507914-8f76-48cb-aa0e-3a8f92951ab0\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/1d507914-8f76-48cb-aa0e-3a8f92951ab0\",\"name\":\"path\",\"version\":\"v1.0.0\"},\"resourceCharacteristic\":[{\"name\":\"path\",\"value\":{\"name\":\"82 Rathmullen - DONC\",\"technology\":\"copper\"}}],\"place\":[{\"id\":\"1657363\",\"href\":\"place/telco/1657363\",\"name\":\"addressId\",\"role\":\"CustomerSite\",\"@referredType\":\"GeographicAddress\",\"@type\":\"PlaceRef\"},{\"id\":\"DONC\",\"href\":\"place/telco/DONC\",\"name\":\"exchangeId\",\"role\":\"NetworkSite\",\"@referredType\":\"GeographicSite\",\"@type\":\"PlaceRef\"},{\"id\":\"DONC-0001\",\"href\":\"place/telco/DONC-0001\",\"name\":\"esaId\",\"role\":\"ServingArea\",\"@referredType\":\"GeographicLocation\",\"@type\":\"PlaceRef\"}],\"relatedParty\":[{\"id\":\"Access\",\"name\":\"organizationId\",\"role\":\"Provider\",\"@referredType\":\"Organization\",\"@type\":\"PartyRef\"}]}) end defp create_customer_place do @@ -252,10 +246,11 @@ defmodule DiffoExample.Access.PathTest do ] }) - # card-as-assignee names its slot :slot when requesting; alias lets - # downstream calculations traverse the assignment by name. + # card-as-assignee names its upstream Shelf relationship :shelf when + # requesting the slot-assignment; consumer-aliases let downstream calcs + # traverse by relationship. Access.assign_slot!(shelf, %{ - assignment: %Assignment{assignee_id: card.id, alias: :slot, operation: :auto_assign} + assignment: %Assignment{assignee_id: card.id, alias: :shelf, operation: :auto_assign} }) [shelf, card] diff --git a/test/access/shelf_test.exs b/test/access/shelf_test.exs index 0f31cce..82a5e9a 100644 --- a/test/access/shelf_test.exs +++ b/test/access/shelf_test.exs @@ -17,7 +17,6 @@ defmodule DiffoExample.Access.ShelfTest do alias DiffoExample.Test.Characteristics alias DiffoExample.Test.Parties alias DiffoExample.Test.Places - alias DiffoExample.Util describe "build shelf" do test "create a shelf" do @@ -52,11 +51,9 @@ defmodule DiffoExample.Access.ShelfTest do encoding = Jason.encode!(shelf) |> Diffo.Util.summarise_dates() - |> Util.summarise_characteristics(shelf) assert encoding == - ~s({\"id\":\"#{shelf.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{shelf.id}",\"category\":\"Network Resource\",\"description\":\"A Shelf Resource Instance which contain cards\",\"name\":\"QDONC-0001\",\"resourceSpecification\":{\"id\":\"ef016d85-9dbd-429c-84da-1df56cc7dda5\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/ef016d85-9dbd-429c-84da-1df56cc7dda5\",\"name\":\"shelf\",\"version\":\"v1.0.0\"},\"resourceCharacteristic\":[{\"name\":\"shelf\",\"value\":{}},{\"name\":\"slots\",\"value\":{\"first\":1,\"last\":1,\"free\":1,\"algorithm\":\"lowest\"}}],\"place\":[{\"id\":\"DONC-0001\",\"href\":\"place/telco/DONC-0001\",\"name\":\"esaId\",\"role\":\"ServingArea\",\"@referredType\":\"GeographicLocation\",\"@type\":\"PlaceRef\"}],\"relatedParty\":[{\"id\":\"Access\",\"name\":\"organizationId\",\"role\":\"Provider\",\"@referredType\":\"Organization\",\"@type\":\"PartyRef\"}]}) - |> Util.summarise_characteristics(shelf) + ~s({\"id\":\"#{shelf.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{shelf.id}",\"category\":\"Network Resource\",\"description\":\"A Shelf Resource Instance which contain cards\",\"name\":\"QDONC-0001\",\"resourceSpecification\":{\"id\":\"ef016d85-9dbd-429c-84da-1df56cc7dda5\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/ef016d85-9dbd-429c-84da-1df56cc7dda5\",\"name\":\"shelf\",\"version\":\"v1.0.0\"},\"resourceCharacteristic\":[{\"name\":\"shelf\",\"value\":{}},{\"name\":\"slots\",\"value\":{\"first\":1,\"last\":1,\"algorithm\":\"lowest\"}}],\"place\":[{\"id\":\"DONC-0001\",\"href\":\"place/telco/DONC-0001\",\"name\":\"esaId\",\"role\":\"ServingArea\",\"@referredType\":\"GeographicLocation\",\"@type\":\"PlaceRef\"}],\"relatedParty\":[{\"id\":\"Access\",\"name\":\"organizationId\",\"role\":\"Provider\",\"@referredType\":\"Organization\",\"@type\":\"PartyRef\"}]}) end end @@ -75,11 +72,9 @@ defmodule DiffoExample.Access.ShelfTest do encoding = Jason.encode!(shelf) |> Diffo.Util.summarise_dates() - |> Util.summarise_characteristics(shelf) assert encoding == - ~s({\"id\":\"#{shelf.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{shelf.id}",\"category\":\"Network Resource\",\"description\":\"A Shelf Resource Instance which contain cards\",\"name\":\"QDONC-0001\",\"resourceSpecification\":{\"id\":\"ef016d85-9dbd-429c-84da-1df56cc7dda5\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/ef016d85-9dbd-429c-84da-1df56cc7dda5\",\"name\":\"shelf\",\"version\":\"v1.0.0\"},\"lifecycleState\":\"operating\",\"resourceCharacteristic\":[{\"name\":\"shelf\",\"value\":{\"name\":\"QDONC-1001\",\"family\":\"ISAM\",\"model\":\"ISAM7330\",\"technology\":\"DSLAM\"}},{\"name\":\"slots\",\"value\":{\"first\":1,\"last\":10,\"free\":10,\"type\":\"LineCard\",\"algorithm\":\"lowest\"}}],\"place\":[{\"id\":\"DONC-0001\",\"href\":\"place/telco/DONC-0001\",\"name\":\"esaId\",\"role\":\"ServingArea\",\"@referredType\":\"GeographicLocation\",\"@type\":\"PlaceRef\"}],\"relatedParty\":[{\"id\":\"Access\",\"name\":\"organizationId\",\"role\":\"Provider\",\"@referredType\":\"Organization\",\"@type\":\"PartyRef\"}]}) - |> Util.summarise_characteristics(shelf) + ~s({\"id\":\"#{shelf.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{shelf.id}",\"category\":\"Network Resource\",\"description\":\"A Shelf Resource Instance which contain cards\",\"name\":\"QDONC-0001\",\"resourceSpecification\":{\"id\":\"ef016d85-9dbd-429c-84da-1df56cc7dda5\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/ef016d85-9dbd-429c-84da-1df56cc7dda5\",\"name\":\"shelf\",\"version\":\"v1.0.0\"},\"lifecycleState\":\"operating\",\"resourceCharacteristic\":[{\"name\":\"shelf\",\"value\":{\"name\":\"QDONC-1001\",\"family\":\"ISAM\",\"model\":\"ISAM7330\",\"technology\":\"DSLAM\"}},{\"name\":\"slots\",\"value\":{\"first\":1,\"last\":10,\"type\":\"LineCard\",\"algorithm\":\"lowest\"}}],\"place\":[{\"id\":\"DONC-0001\",\"href\":\"place/telco/DONC-0001\",\"name\":\"esaId\",\"role\":\"ServingArea\",\"@referredType\":\"GeographicLocation\",\"@type\":\"PlaceRef\"}],\"relatedParty\":[{\"id\":\"Access\",\"name\":\"organizationId\",\"role\":\"Provider\",\"@referredType\":\"Organization\",\"@type\":\"PartyRef\"}]}) end test "relate common cards" do @@ -102,14 +97,12 @@ defmodule DiffoExample.Access.ShelfTest do encoding = Jason.encode!(shelf) |> Diffo.Util.summarise_dates() - |> Util.summarise_characteristics(shelf) [card0, card1, card2, card3] = cards # resource relationships are sorted in the create order of the relationships assert encoding == - ~s({\"id\":\"#{shelf.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{shelf.id}",\"category\":\"Network Resource\",\"description\":\"A Shelf Resource Instance which contain cards\",\"resourceSpecification\":{\"id\":\"ef016d85-9dbd-429c-84da-1df56cc7dda5\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/ef016d85-9dbd-429c-84da-1df56cc7dda5\",\"name\":\"shelf\",\"version\":\"v1.0.0\"},\"lifecycleState\":\"operating\",\"resourceRelationship\":[{\"type\":\"contains\",\"resource\":{\"id\":\"#{card0.id}\",\"href\":\"resourceInventoryManagement/v4/resource/#{card0.id}\"}},{\"type\":\"contains\",\"resource\":{\"id\":\"#{card1.id}\",\"href\":\"resourceInventoryManagement/v4/resource/#{card1.id}\"}},{\"type\":\"contains\",\"resource\":{\"id\":\"#{card2.id}\",\"href\":\"resourceInventoryManagement/v4/resource/#{card2.id}\"}},{\"type\":\"contains\",\"resource\":{\"id\":\"#{card3.id}\",\"href\":\"resourceInventoryManagement/v4/resource/#{card3.id}\"}}],\"resourceCharacteristic\":[{\"name\":\"shelf\",\"value\":{\"name\":\"QDONC-1001\",\"family\":\"ISAM\",\"model\":\"ISAM7330\",\"technology\":\"DSLAM\"}},{\"name\":\"slots\",\"value\":{\"first\":1,\"last\":10,\"free\":10,\"type\":\"LineCard\",\"algorithm\":\"lowest\"}}],\"place\":[{\"id\":\"DONC-0001\",\"href\":\"place/telco/DONC-0001\",\"name\":\"esaId\",\"role\":\"ServingArea\",\"@referredType\":\"GeographicLocation\",\"@type\":\"PlaceRef\"}],\"relatedParty\":[{\"id\":\"Access\",\"name\":\"organizationId\",\"role\":\"Provider\",\"@referredType\":\"Organization\",\"@type\":\"PartyRef\"}]}) - |> Util.summarise_characteristics(shelf) + ~s({\"id\":\"#{shelf.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{shelf.id}",\"category\":\"Network Resource\",\"description\":\"A Shelf Resource Instance which contain cards\",\"resourceSpecification\":{\"id\":\"ef016d85-9dbd-429c-84da-1df56cc7dda5\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/ef016d85-9dbd-429c-84da-1df56cc7dda5\",\"name\":\"shelf\",\"version\":\"v1.0.0\"},\"lifecycleState\":\"operating\",\"resourceRelationship\":[{\"type\":\"contains\",\"resource\":{\"id\":\"#{card0.id}\",\"href\":\"resourceInventoryManagement/v4/resource/#{card0.id}\"}},{\"type\":\"contains\",\"resource\":{\"id\":\"#{card1.id}\",\"href\":\"resourceInventoryManagement/v4/resource/#{card1.id}\"}},{\"type\":\"contains\",\"resource\":{\"id\":\"#{card2.id}\",\"href\":\"resourceInventoryManagement/v4/resource/#{card2.id}\"}},{\"type\":\"contains\",\"resource\":{\"id\":\"#{card3.id}\",\"href\":\"resourceInventoryManagement/v4/resource/#{card3.id}\"}}],\"resourceCharacteristic\":[{\"name\":\"shelf\",\"value\":{\"name\":\"QDONC-1001\",\"family\":\"ISAM\",\"model\":\"ISAM7330\",\"technology\":\"DSLAM\"}},{\"name\":\"slots\",\"value\":{\"first\":1,\"last\":10,\"type\":\"LineCard\",\"algorithm\":\"lowest\"}}],\"place\":[{\"id\":\"DONC-0001\",\"href\":\"place/telco/DONC-0001\",\"name\":\"esaId\",\"role\":\"ServingArea\",\"@referredType\":\"GeographicLocation\",\"@type\":\"PlaceRef\"}],\"relatedParty\":[{\"id\":\"Access\",\"name\":\"organizationId\",\"role\":\"Provider\",\"@referredType\":\"Organization\",\"@type\":\"PartyRef\"}]}) end test "auto assign line cards" do @@ -135,14 +128,12 @@ defmodule DiffoExample.Access.ShelfTest do encoding = Jason.encode!(shelf) |> Diffo.Util.summarise_dates() - |> Util.summarise_characteristics(shelf) lc1 = line_card1.assignee_id lc2 = line_card2.assignee_id assert encoding == - ~s({\"id\":\"#{shelf.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{shelf.id}",\"category\":\"Network Resource\",\"description\":\"A Shelf Resource Instance which contain cards\",\"name\":\"QDONC-0001\",\"resourceSpecification\":{\"id\":\"ef016d85-9dbd-429c-84da-1df56cc7dda5\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/ef016d85-9dbd-429c-84da-1df56cc7dda5\",\"name\":\"shelf\",\"version\":\"v1.0.0\"},\"lifecycleState\":\"operating\",\"resourceRelationship\":[{\"type\":\"assignedTo\",\"resource\":{\"id\":\"#{lc1}\",\"href\":\"resourceInventoryManagement/v4/resource/#{lc1}\"},\"resourceRelationshipCharacteristic\":[{\"name\":\"slot\",\"value\":1}]},{\"type\":\"assignedTo\",\"resource\":{\"id\":\"#{lc2}\",\"href\":\"resourceInventoryManagement/v4/resource/#{lc2}\"},\"resourceRelationshipCharacteristic\":[{\"name\":\"slot\",\"value\":2}]}],\"resourceCharacteristic\":[{\"name\":\"shelf\",\"value\":{\"name\":\"QDONC-1001\",\"family\":\"ISAM\",\"model\":\"ISAM7330\",\"technology\":\"DSLAM\"}},{\"name\":\"slots\",\"value\":{\"first\":1,\"last\":10,\"free\":8,\"type\":\"LineCard\",\"algorithm\":\"lowest\"}}],\"place\":[{\"id\":\"DONC-0001\",\"href\":\"place/telco/DONC-0001\",\"name\":\"esaId\",\"role\":\"ServingArea\",\"@referredType\":\"GeographicLocation\",\"@type\":\"PlaceRef\"}],\"relatedParty\":[{\"id\":\"Access\",\"name\":\"organizationId\",\"role\":\"Provider\",\"@referredType\":\"Organization\",\"@type\":\"PartyRef\"}]}) - |> Util.summarise_characteristics(shelf) + ~s({\"id\":\"#{shelf.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{shelf.id}",\"category\":\"Network Resource\",\"description\":\"A Shelf Resource Instance which contain cards\",\"name\":\"QDONC-0001\",\"resourceSpecification\":{\"id\":\"ef016d85-9dbd-429c-84da-1df56cc7dda5\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/ef016d85-9dbd-429c-84da-1df56cc7dda5\",\"name\":\"shelf\",\"version\":\"v1.0.0\"},\"lifecycleState\":\"operating\",\"resourceRelationship\":[{\"type\":\"assignedTo\",\"resource\":{\"id\":\"#{lc1}\",\"href\":\"resourceInventoryManagement/v4/resource/#{lc1}\"},\"resourceRelationshipCharacteristic\":[{\"name\":\"slot\",\"value\":1}]},{\"type\":\"assignedTo\",\"resource\":{\"id\":\"#{lc2}\",\"href\":\"resourceInventoryManagement/v4/resource/#{lc2}\"},\"resourceRelationshipCharacteristic\":[{\"name\":\"slot\",\"value\":2}]}],\"resourceCharacteristic\":[{\"name\":\"shelf\",\"value\":{\"name\":\"QDONC-1001\",\"family\":\"ISAM\",\"model\":\"ISAM7330\",\"technology\":\"DSLAM\"}},{\"name\":\"slots\",\"value\":{\"first\":1,\"last\":10,\"type\":\"LineCard\",\"algorithm\":\"lowest\"}}],\"place\":[{\"id\":\"DONC-0001\",\"href\":\"place/telco/DONC-0001\",\"name\":\"esaId\",\"role\":\"ServingArea\",\"@referredType\":\"GeographicLocation\",\"@type\":\"PlaceRef\"}],\"relatedParty\":[{\"id\":\"Access\",\"name\":\"organizationId\",\"role\":\"Provider\",\"@referredType\":\"Organization\",\"@type\":\"PartyRef\"}]}) end defp create_common_cards() do @@ -189,15 +180,16 @@ defmodule DiffoExample.Access.ShelfTest do ] }) - # Each card-as-assignee names its slot :slot when requesting. + # Each card-as-assignee names its upstream Shelf relationship :shelf + # when requesting. {:ok, _shelf} = Access.assign_slot(shelf, %{ - assignment: %Assignment{assignee_id: card_a.id, alias: :slot, operation: :auto_assign} + assignment: %Assignment{assignee_id: card_a.id, alias: :shelf, operation: :auto_assign} }) {:ok, shelf} = Access.assign_slot(shelf, %{ - assignment: %Assignment{assignee_id: card_b.id, alias: :slot, operation: :auto_assign} + assignment: %Assignment{assignee_id: card_b.id, alias: :shelf, operation: :auto_assign} }) # Shelf brings up its cards (in slot order) and aggregates total ports. diff --git a/test/nbn/nbn_ethernet_test.exs b/test/nbn/nbn_ethernet_test.exs index fb1677a..a0aa2d7 100644 --- a/test/nbn/nbn_ethernet_test.exs +++ b/test/nbn/nbn_ethernet_test.exs @@ -16,7 +16,6 @@ defmodule DiffoExample.Nbn.NbnEthernetTest do alias DiffoExample.Nbn.NniGroup alias DiffoExample.Nbn.Nni alias DiffoExample.Test.Characteristics - alias DiffoExample.Util alias Diffo.Provider.Assignment alias Diffo.Provider.Instance.Relationship @@ -47,11 +46,9 @@ defmodule DiffoExample.Nbn.NbnEthernetTest do encoding = Jason.encode!(access) |> Diffo.Util.summarise_dates() - |> Util.summarise_characteristics(access) assert encoding == ~s({"id":"#{access.id}","href":"resourceInventoryManagement/v4/resource/#{access.id}","category":"Network Resource","description":"An NBN Ethernet access comprising a dedicated UNI and AVC",\"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":{}}]}) - |> Util.summarise_characteristics(access) end test "define nbn_ethernet access" do @@ -144,11 +141,94 @@ defmodule DiffoExample.Nbn.NbnEthernetTest do encoding = Jason.encode!(access) |> Diffo.Util.summarise_dates() - |> Util.summarise_characteristics(access) assert encoding == - ~s({"id":"#{access.id}","href":"resourceInventoryManagement/v4/resource/#{access.id}","category":"Network Resource","description":"An NBN Ethernet access comprising a dedicated UNI and AVC","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]}}]}) - |> Util.summarise_characteristics(access) + ~s({"id":"#{access.id}","href":"resourceInventoryManagement/v4/resource/#{access.id}","category":"Network Resource","description":"An NBN Ethernet access comprising a dedicated UNI and AVC","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":{}}]}) + end + + test "pri brings up avc, uni, cvc, ntd via relationship + assignment chains" do + # CVC with cvlan pool, assigned to an AVC + {:ok, cvc} = Nbn.build_cvc(%{}) + + {:ok, cvc} = + Nbn.define_cvc(cvc, %{ + characteristic_value_updates: [ + cvc: [svlan: 1, bandwidth: 1000], + cvlans: [first: 1, last: 100, assignable_type: "cvlan"] + ] + }) + + # NTD with port pool, assigned to a UNI + {:ok, ntd} = Nbn.build_ntd(%{}) + + {:ok, ntd} = + Nbn.define_ntd(ntd, %{ + characteristic_value_updates: [ + ntd: [model: "Sercomm CG4000A", technology: :FTTP], + ports: [first: 1, last: 4, assignable_type: "port"] + ] + }) + + # AVC + UNI + {:ok, avc} = Nbn.build_avc(%{}) + + {:ok, _} = + Nbn.define_avc(avc, %{ + characteristic_value_updates: [avc: [bandwidth_profile: :home_fast]] + }) + + {:ok, uni} = Nbn.build_uni(%{}) + + {:ok, _} = + Nbn.define_uni(uni, %{ + characteristic_value_updates: [ + uni: [port: 1, encapsulation: "DSCP Mapped", technology: :FTTP] + ] + }) + + # AVC takes a cvlan from CVC; UNI takes a port from NTD. Consumer + # aliases name the upstream resource each is part of, so the + # inheritance walks (target_id + alias identity) resolve cleanly. + {:ok, _} = + Nbn.assign_cvlan(cvc, %{ + assignment: %Assignment{ + assignee_id: avc.id, + alias: :cvc, + operation: :auto_assign + } + }) + + {:ok, _} = + Nbn.assign_port(ntd, %{ + assignment: %Assignment{ + assignee_id: uni.id, + alias: :ntd, + operation: :auto_assign + } + }) + + # PRI owns the AVC and UNI. Aliases name the role each related + # resource plays from PRI's perspective — the AVC is the :circuit, + # the UNI is the :port. + {:ok, pri} = Nbn.build_nbn_ethernet(%{}) + + {:ok, _} = + Nbn.relate_nbn_ethernet(pri, %{ + relationships: [ + %Relationship{id: avc.id, direction: :forward, type: :owns, alias: :circuit}, + %Relationship{id: uni.id, direction: :forward, type: :owns, alias: :port} + ] + }) + + {:ok, pri} = Nbn.get_nbn_ethernet_by_id(pri.id, load: [:avc, :uni, :cvc, :ntd]) + + # Single-hop via :owns relationship + assert %{bandwidth_profile: :home_fast} = pri.avc + assert %{port: 1, encapsulation: "DSCP Mapped", technology: :FTTP} = pri.uni + + # Two-hop: :owns relationship then :cvlan / :port assignment + assert %{svlan: 1, bandwidth: 1000} = pri.cvc + assert %{model: "Sercomm CG4000A", technology: :FTTP} = pri.ntd end end @@ -206,6 +286,61 @@ defmodule DiffoExample.Nbn.NbnEthernetTest do avc ) end + + test "avc inherits cvc (single-hop) and nni_group (two-hop) via assignment chain" do + # NniGroup with svlan pool, then a CVC that takes an svlan from it, + # then an AVC that takes a cvlan from the CVC. AVC's inherited calcs + # should bring up cvc and nni_group characteristics. + {:ok, nni_group} = Nbn.build_nni_group(%{}) + + {:ok, nni_group} = + Nbn.define_nni_group(nni_group, %{ + characteristic_value_updates: [ + nni_group: [group_name: "SYD-POI-01", location: "Sydney"], + svlans: [first: 1, last: 4000, assignable_type: "svlan"] + ] + }) + + {:ok, cvc} = Nbn.build_cvc(%{}) + + {:ok, cvc} = + Nbn.define_cvc(cvc, %{ + characteristic_value_updates: [ + cvc: [bandwidth: 1000], + cvlans: [first: 1, last: 4000, assignable_type: "cvlan"] + ] + }) + + {:ok, _nni_group} = + Nbn.assign_svlan(nni_group, %{ + assignment: %Assignment{ + assignee_id: cvc.id, + alias: :nni_group, + operation: :auto_assign + } + }) + + {: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, + alias: :cvc, + operation: :auto_assign + } + }) + + {:ok, avc} = Nbn.get_avc_by_id(avc.id, load: [:cvc, :nni_group]) + + assert %{bandwidth: 1000} = avc.cvc + assert %{group_name: "SYD-POI-01", location: "Sydney"} = avc.nni_group + end end describe "build ntd" do @@ -259,6 +394,43 @@ defmodule DiffoExample.Nbn.NbnEthernetTest do ) end) end + + test "ntd brings up assigned UNIs as unis[] via :port assignment" do + {:ok, ntd} = Nbn.build_ntd(%{}) + + {:ok, ntd} = + Nbn.define_ntd(ntd, %{ + characteristic_value_updates: [ + ntd: [model: "Sercomm CG4000A", technology: :FTTP], + ports: [first: 1, last: 4, assignable_type: "port"] + ] + }) + + # Two UNIs defined and assigned ports from the NTD + for {port_num, encap} <- [{1, "DSCP Mapped"}, {2, "untagged"}] do + {:ok, uni} = Nbn.build_uni(%{}) + + {:ok, _} = + Nbn.define_uni(uni, %{ + characteristic_value_updates: [ + uni: [port: port_num, encapsulation: encap, technology: :FTTP] + ] + }) + + {:ok, _} = + Nbn.assign_port(ntd, %{ + assignment: %Assignment{assignee_id: uni.id, operation: :auto_assign} + }) + end + + {:ok, ntd} = Nbn.get_ntd_by_id(ntd.id, load: [:unis]) + + assert is_list(ntd.unis) + assert length(ntd.unis) == 2 + + ports = Enum.map(ntd.unis, & &1.port) |> Enum.sort() + assert ports == [1, 2] + end end describe "build cvc" do @@ -313,6 +485,43 @@ defmodule DiffoExample.Nbn.NbnEthernetTest do ) end) end + + test "cvc metrics aggregates avcs_count and avcs_total_bandwidth across assigned avcs" do + {:ok, cvc} = Nbn.build_cvc(%{}) + + {:ok, cvc} = + Nbn.define_cvc(cvc, %{ + characteristic_value_updates: [ + cvc: [svlan: 1, bandwidth: 10000], + cvlans: [first: 1, last: 4000, assignable_type: "cvlan"] + ] + }) + + # Two AVCs with distinct bandwidth_profiles — :home_fast (500 Mbps + # downstream) and :D100_U40 (100 Mbps downstream). + for profile <- [:home_fast, :D100_U40] do + {:ok, avc} = Nbn.build_avc(%{}) + + {:ok, _} = + Nbn.define_avc(avc, %{ + characteristic_value_updates: [avc: [bandwidth_profile: profile]] + }) + + {:ok, _} = + Nbn.assign_cvlan(cvc, %{ + assignment: %Assignment{assignee_id: avc.id, operation: :auto_assign} + }) + end + + metrics = + DiffoExample.Nbn.CvcMetrics + |> Ash.Query.filter_input(instance_id: cvc.id) + |> Ash.Query.load(:value) + |> Ash.read_one!() + + assert metrics.value.avcs_count == 2 + assert metrics.value.avcs_total_bandwidth == 600 + end end describe "build nni_group" do @@ -366,6 +575,78 @@ defmodule DiffoExample.Nbn.NbnEthernetTest do ) end) end + + test "nni_group metrics — cvcs and nnis aggregates plus utilization" do + {:ok, nni_group} = Nbn.build_nni_group(%{}) + + {:ok, nni_group} = + Nbn.define_nni_group(nni_group, %{ + characteristic_value_updates: [ + nni_group: [group_name: "SYD-POI-01", location: "Sydney"], + svlans: [first: 1, last: 4000, assignable_type: "svlan"] + ] + }) + + # Demand side: two CVCs assigned svlans from this NniGroup. + for bandwidth <- [400, 600] do + {:ok, cvc} = Nbn.build_cvc(%{}) + + {:ok, _} = + Nbn.define_cvc(cvc, %{ + characteristic_value_updates: [cvc: [bandwidth: bandwidth]] + }) + + {:ok, _} = + Nbn.assign_svlan(nni_group, %{ + assignment: %Assignment{assignee_id: cvc.id, operation: :auto_assign} + }) + end + + # Capacity side: two NNIs comprised by this NniGroup, related via + # DefinedSimpleRelationship type :contains. + nni_ids = + for capacity <- [10, 10] do + {:ok, nni} = Nbn.build_nni(%{}) + + {:ok, _} = + Nbn.define_nni(nni, %{ + characteristic_value_updates: [ + nni: [port_id: "SYD-01-ETH-#{capacity}", capacity: capacity] + ] + }) + + nni.id + end + + {:ok, _nni_group} = + Nbn.relate_nni_group(nni_group, %{ + relationships: + Enum.map(nni_ids, fn nni_id -> + %Relationship{id: nni_id, direction: :forward, type: :contains} + end) + }) + + metrics = + DiffoExample.Nbn.NniGroupMetrics + |> Ash.Query.filter_input(instance_id: nni_group.id) + |> Ash.Query.load(:value) + |> Ash.read_one!() + + assert metrics.value.cvcs_count == 2 + assert metrics.value.cvcs_total_bandwidth == 1000 + assert metrics.value.nnis_count == 2 + assert metrics.value.nnis_total_bandwidth == 20 + assert_in_delta metrics.value.utilization, 50.0, 0.001 + + # nnis[] brings up the NNI characteristic of every comprised NNI via + # the same :contains relationships. + {:ok, nni_group} = Nbn.get_nni_group_by_id(nni_group.id, load: [:nnis]) + + assert is_list(nni_group.nnis) + assert length(nni_group.nnis) == 2 + + assert Enum.all?(nni_group.nnis, &match?(%{capacity: 10}, &1)) + end end describe "build nni" do diff --git a/test/nbn/show_neo4j_test.exs b/test/nbn/show_neo4j_test.exs new file mode 100644 index 0000000..77a8dc5 --- /dev/null +++ b/test/nbn/show_neo4j_test.exs @@ -0,0 +1,315 @@ +# SPDX-FileCopyrightText: 2025 diffo_example contributors +# +# SPDX-License-Identifier: MIT + +defmodule DiffoExample.Nbn.ShowNeo4jTest do + @moduledoc """ + Tests tagged `:show_neo4j` build a coherent NBN graph in the **real** + Neo4j database (no sandbox, no rollback) so the result can be inspected + via the Neo4j browser afterwards. + + Each test prints the Ash-side records and TMF JSON to stdout while it + runs, and the persisted nodes/relationships remain in Neo4j for further + exploration. The two tests together build the picture — NniGroup → CVCs + → AVCs chain (with metrics), and an NTD with assigned UNIs. + + Run with: + + mix test --only show_neo4j + + Excluded from default test runs. + """ + use ExUnit.Case, async: false + + alias Diffo.Provider.Assignment + alias Diffo.Provider.Instance.Relationship + alias DiffoExample.Nbn + + @moduletag :show_neo4j + + test "NniGroup with NNIs, CVCs and an AVC — inheritance + metrics" do + {:ok, nni_group} = Nbn.build_nni_group(%{}) + + {:ok, nni_group} = + Nbn.define_nni_group(nni_group, %{ + characteristic_value_updates: [ + nni_group: [group_name: "SYD-POI-01", location: "Sydney Olympic Park"], + svlans: [first: 1, last: 4000, assignable_type: "svlan"] + ] + }) + + # Two CVCs assigned svlans from this NniGroup + cvc_ids = + for bandwidth <- [400, 600] do + {:ok, cvc} = Nbn.build_cvc(%{}) + + {:ok, _} = + Nbn.define_cvc(cvc, %{ + characteristic_value_updates: [ + cvc: [bandwidth: bandwidth], + cvlans: [first: 1, last: 4000, assignable_type: "cvlan"] + ] + }) + + {:ok, _} = + Nbn.assign_svlan(nni_group, %{ + assignment: %Assignment{ + assignee_id: cvc.id, + alias: :nni_group, + operation: :auto_assign + } + }) + + cvc.id + end + + # Two NNIs comprised by this NniGroup — realistic capacities so + # utilization comes out in the 0–1 range + nni_ids = + for {port_id, capacity} <- [{"SYD-01-ETH-1", 10000}, {"SYD-01-ETH-2", 10000}] do + {:ok, nni} = Nbn.build_nni(%{}) + + {:ok, _} = + Nbn.define_nni(nni, %{ + characteristic_value_updates: [ + nni: [port_id: port_id, capacity: capacity] + ] + }) + + nni.id + end + + {:ok, _} = + Nbn.relate_nni_group(nni_group, %{ + relationships: + Enum.map(nni_ids, fn id -> + %Relationship{id: id, direction: :forward, type: :contains} + end) + }) + + # One AVC assigned a cvlan from the first CVC + cvc1_id = hd(cvc_ids) + {:ok, cvc1} = Nbn.get_cvc_by_id(cvc1_id) + + {:ok, avc} = Nbn.build_avc(%{}) + + {:ok, _} = + Nbn.define_avc(avc, %{ + characteristic_value_updates: [avc: [bandwidth_profile: :home_fast]] + }) + + {:ok, _} = + Nbn.assign_cvlan(cvc1, %{ + assignment: %Assignment{ + assignee_id: avc.id, + alias: :cvc, + operation: :auto_assign + } + }) + + # Reload all three with their inheritance calcs / metrics loaded + {:ok, avc} = Nbn.get_avc_by_id(avc.id, load: [:cvc, :nni_group]) + {:ok, cvc} = Nbn.get_cvc_by_id(cvc1_id, load: [:nni_group]) + {:ok, nni_group} = Nbn.get_nni_group_by_id(nni_group.id, load: [:nnis]) + + cvc_metrics = + DiffoExample.Nbn.CvcMetrics + |> Ash.Query.filter_input(instance_id: cvc.id) + |> Ash.Query.load(:value) + |> Ash.read_one!() + + nni_group_metrics = + DiffoExample.Nbn.NniGroupMetrics + |> Ash.Query.filter_input(instance_id: nni_group.id) + |> Ash.Query.load(:value) + |> Ash.read_one!() + + IO.puts("\n========== AVC.cvc (single-hop :cvlan) ==========") + IO.inspect(avc.cvc, label: "avc.cvc") + + IO.puts("\n========== AVC.nni_group (two-hop [:cvlan, :svlan]) ==========") + IO.inspect(avc.nni_group, label: "avc.nni_group") + + IO.puts("\n========== CVC.nni_group (single-hop :svlan) ==========") + IO.inspect(cvc.nni_group, label: "cvc.nni_group") + + IO.puts("\n========== CvcMetrics record ==========") + IO.inspect(cvc_metrics.value, label: "cvc_metrics.value") + + IO.puts("\n========== NniGroupMetrics record ==========") + IO.inspect(nni_group_metrics.value, label: "nni_group_metrics.value") + + IO.puts("\n========== NniGroup.nnis (brought-up via :contains) ==========") + IO.inspect(nni_group.nnis, label: "nni_group.nnis") + + IO.puts("\n========== AVC (TMF JSON) ==========") + + avc + |> Jason.encode!() + |> Diffo.Util.summarise_dates() + |> Jason.decode!() + |> Jason.encode!(pretty: true) + |> IO.puts() + + IO.puts("\n========== CVC (TMF JSON) ==========") + + cvc + |> Jason.encode!() + |> Diffo.Util.summarise_dates() + |> Jason.decode!() + |> Jason.encode!(pretty: true) + |> IO.puts() + + IO.puts("\n========== NniGroup (TMF JSON) ==========") + + nni_group + |> Jason.encode!() + |> Diffo.Util.summarise_dates() + |> Jason.decode!() + |> Jason.encode!(pretty: true) + |> IO.puts() + end + + test "PRI (NbnEthernet access) with full delivery chain — AVC + UNI + CVC + NTD" do + # CVC + cvlan pool + {:ok, cvc} = Nbn.build_cvc(%{}) + + {:ok, cvc} = + Nbn.define_cvc(cvc, %{ + characteristic_value_updates: [ + cvc: [svlan: 1, bandwidth: 1000], + cvlans: [first: 1, last: 100, assignable_type: "cvlan"] + ] + }) + + # NTD + port pool + {:ok, ntd} = Nbn.build_ntd(%{}) + + {:ok, ntd} = + Nbn.define_ntd(ntd, %{ + characteristic_value_updates: [ + ntd: [model: "Sercomm CG4000A", technology: :FTTP], + ports: [first: 1, last: 4, assignable_type: "port"] + ] + }) + + # AVC + UNI + {:ok, avc} = Nbn.build_avc(%{}) + + {:ok, _} = + Nbn.define_avc(avc, %{ + characteristic_value_updates: [avc: [bandwidth_profile: :home_fast]] + }) + + {:ok, uni} = Nbn.build_uni(%{}) + + {:ok, _} = + Nbn.define_uni(uni, %{ + characteristic_value_updates: [ + uni: [port: 1, encapsulation: "DSCP Mapped", technology: :FTTP] + ] + }) + + {:ok, _} = + Nbn.assign_cvlan(cvc, %{ + assignment: %Assignment{ + assignee_id: avc.id, + alias: :cvc, + operation: :auto_assign + } + }) + + {:ok, _} = + Nbn.assign_port(ntd, %{ + assignment: %Assignment{ + assignee_id: uni.id, + alias: :ntd, + operation: :auto_assign + } + }) + + # PRI owns AVC (named :circuit from PRI's view) and UNI (named :port). + {:ok, pri} = Nbn.build_nbn_ethernet(%{}) + + {:ok, _} = + Nbn.relate_nbn_ethernet(pri, %{ + relationships: [ + %Relationship{id: avc.id, direction: :forward, type: :owns, alias: :circuit}, + %Relationship{id: uni.id, direction: :forward, type: :owns, alias: :port} + ] + }) + + {:ok, pri} = Nbn.get_nbn_ethernet_by_id(pri.id, load: [:avc, :uni, :cvc, :ntd]) + + IO.puts("\n========== PRI.avc (single-hop via :avc owns) ==========") + IO.inspect(pri.avc, label: "pri.avc") + + IO.puts("\n========== PRI.uni (single-hop via :uni owns) ==========") + IO.inspect(pri.uni, label: "pri.uni") + + IO.puts("\n========== PRI.cvc (two-hop via :avc owns + :cvlan assignment) ==========") + IO.inspect(pri.cvc, label: "pri.cvc") + + IO.puts("\n========== PRI.ntd (two-hop via :uni owns + :port assignment) ==========") + IO.inspect(pri.ntd, label: "pri.ntd") + + IO.puts("\n========== PRI (TMF JSON) ==========") + + pri + |> Jason.encode!() + |> Diffo.Util.summarise_dates() + |> Jason.decode!() + |> Jason.encode!(pretty: true) + |> IO.puts() + end + + test "NTD with assigned UNIs" do + {:ok, ntd} = Nbn.build_ntd(%{}) + + {:ok, ntd} = + Nbn.define_ntd(ntd, %{ + characteristic_value_updates: [ + ntd: [ + model: "Sercomm CG4000A", + serial_number: "SCOMA1A057A2", + technology: :FTTP + ], + ports: [first: 1, last: 4, assignable_type: "port"] + ] + }) + + for {port_num, encap} <- [{1, "DSCP Mapped"}, {2, "untagged"}] do + {:ok, uni} = Nbn.build_uni(%{}) + + {:ok, _} = + Nbn.define_uni(uni, %{ + characteristic_value_updates: [ + uni: [port: port_num, encapsulation: encap, technology: :FTTP] + ] + }) + + {:ok, _} = + Nbn.assign_port(ntd, %{ + assignment: %Assignment{ + assignee_id: uni.id, + alias: :ntd, + operation: :auto_assign + } + }) + end + + {:ok, ntd} = Nbn.get_ntd_by_id(ntd.id, load: [:unis]) + + IO.puts("\n========== NTD.unis (brought-up via :port assignment) ==========") + IO.inspect(ntd.unis, label: "ntd.unis") + + IO.puts("\n========== NTD (TMF JSON) ==========") + + ntd + |> Jason.encode!() + |> Diffo.Util.summarise_dates() + |> Jason.decode!() + |> Jason.encode!(pretty: true) + |> IO.puts() + end +end diff --git a/test/test_helper.exs b/test/test_helper.exs index 7b3a5b5..1b7a8ee 100644 --- a/test/test_helper.exs +++ b/test/test_helper.exs @@ -7,4 +7,4 @@ level = Application.get_env(:logger, :console) |> Keyword.get(:level) Logger.put_application_level(:diffo, level) Logger.put_application_level(:ash_neo4j, :error) AshNeo4j.Neo4jHelper.delete_all() -ExUnit.start() +ExUnit.start(exclude: [:show_neo4j])