Skip to content

Commit 069ed1f

Browse files
committed
#49 part 2 — NTD brings up assigned UNIs as unis[]
* NTD :unis calc — reverse-inherits each assigned UNI's typed value via :port * ReverseInheritedCharacteristic extended with thing: filter (source-side principle) * New test/nbn/show_neo4j_test.exs — sync, non-sandboxed exploration module that builds the NBN graph in real Neo4j for browser inspection. Both prior :show_json tests moved here and re-tagged :show_neo4j * DataCase reverted to its original shape — sandbox isolates without needing per-module wipes * 1 new sandboxed test, 53/53 pass
1 parent 2548f63 commit 069ed1f

6 files changed

Lines changed: 313 additions & 158 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,17 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline
2828

2929
### Features:
3030
* NBN — AVC, CVC, NniGroup characteristic inheritance and metrics (issue #49):
31-
* 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).
32-
* CVC inherits the upstream NniGroup's `nni_group` characteristic via the `:svlan` assignment.
31+
* 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.
32+
* CVC inherits the upstream NniGroup's `nni_group` characteristic via the `:svlan` assignment (singular).
33+
* NniGroup brings up the typed value of every comprised NNI as `nnis[]` via the `:contains` relationship.
3334
* New `cvc_metrics` characteristic on CVC carries `avcs_count` and `avcs_total_bandwidth` aggregated live over assigned AVCs.
3435
* 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`.
36+
* NBN — NTD brings up assigned UNIs as `unis[]` via the `:port` assignment (issue #49 part 2).
3537
* `BandwidthProfile.downstream/1` — atom-to-Mbps mapping used by the metrics aggregation. CVCs are treated as symmetric capacity in this model (satellite asymmetry ignored).
3638

3739
### Refactors (continued):
38-
* `DiffoExample.Calculations.InheritedCharacteristic` renamed to `InheritedCharacteristicViaAssignment` to make room for a future `InheritedCharacteristicViaRelationship` sibling (the latter lands in the PRI bring-up work).
40+
* `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.
41+
* `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.
3942

4043
## [v0.2.2](https://github.com/diffo-dev/diffo/compare/v0.2.1..v0.2.2) (2026-05-21)
4144

lib/diffo_example/calculations/reverse_inherited_characteristic.ex

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,19 +26,34 @@ defmodule DiffoExample.Calculations.ReverseInheritedCharacteristic do
2626
2727
## Options
2828
29-
- `alias:` *(optional)* — filter outgoing assignments by alias (the
30-
assignee's slot name). When omitted, all outgoing assignments are
31-
included.
3229
- `characteristic_module:` *(required)* — the typed characteristic Ash
3330
resource on each assignee (e.g. `CardCharacteristic`).
31+
- `alias:` *(optional)* — filter outgoing assignments by alias (the
32+
assignee's slot name). Use when every consumer follows the same
33+
aliasing convention.
34+
- `thing:` *(optional)* — filter outgoing assignments by `thing` (the
35+
pool's thing name, e.g. `:slot`, `:port`). Always set from the pool
36+
DSL declaration, so this is more robust than `alias:` when consumers
37+
don't reliably name their slots. See `[[assignment-direction-asymmetry]]`.
3438
35-
## Example
39+
Specify either `alias:` or `thing:` (or both); omitting both includes
40+
every outgoing assignment from this instance.
41+
42+
## Examples
3643
3744
# Shelf brings up the card characteristic from every card it's
38-
# assigned a slot to, ordered by slot number.
45+
# assigned a slot to, ordered by slot number. Cards-as-assignees
46+
# name their slot :slot when requesting.
3947
calculate :cards, {:array, :map},
4048
{DiffoExample.Calculations.ReverseInheritedCharacteristic,
4149
[alias: :slot, characteristic_module: CardCharacteristic]}
50+
51+
# NTD brings up the UNI characteristic from every UNI it's assigned
52+
# a port to. The UNIs may not have set an alias on their request,
53+
# so filter by `thing` from the :ports pool declaration.
54+
calculate :unis, {:array, :map},
55+
{DiffoExample.Calculations.ReverseInheritedCharacteristic,
56+
[thing: :port, characteristic_module: UniCharacteristic]}
4257
"""
4358
use Ash.Resource.Calculation
4459

@@ -48,12 +63,13 @@ defmodule DiffoExample.Calculations.ReverseInheritedCharacteristic do
4863
@impl true
4964
def calculate(records, opts, _context) do
5065
alias_filter = Keyword.get(opts, :alias)
66+
thing_filter = Keyword.get(opts, :thing)
5167
characteristic_module = Keyword.fetch!(opts, :characteristic_module)
5268

5369
Enum.map(records, fn record ->
5470
assignments =
5571
Diffo.Provider.AssignmentRelationship
56-
|> filter_outgoing(record.id, alias_filter)
72+
|> filter_outgoing(record.id, alias_filter, thing_filter)
5773
|> Ash.Query.sort(value: :asc)
5874
|> Ash.read!(domain: Diffo.Provider)
5975

@@ -67,9 +83,20 @@ defmodule DiffoExample.Calculations.ReverseInheritedCharacteristic do
6783
end)
6884
end
6985

70-
defp filter_outgoing(query, source_id, nil),
71-
do: query |> Ash.Query.filter_input(source_id: source_id)
86+
defp filter_outgoing(query, source_id, nil, nil),
87+
do: Ash.Query.filter_input(query, source_id: source_id)
88+
89+
defp filter_outgoing(query, source_id, alias_filter, nil),
90+
do: Ash.Query.filter_input(query, source_id: source_id, alias: alias_filter)
91+
92+
defp filter_outgoing(query, source_id, nil, thing_filter),
93+
do: Ash.Query.filter_input(query, source_id: source_id, thing: thing_filter)
7294

73-
defp filter_outgoing(query, source_id, alias_filter),
74-
do: query |> Ash.Query.filter_input(source_id: source_id, alias: alias_filter)
95+
defp filter_outgoing(query, source_id, alias_filter, thing_filter),
96+
do:
97+
Ash.Query.filter_input(query,
98+
source_id: source_id,
99+
alias: alias_filter,
100+
thing: thing_filter
101+
)
75102
end

lib/nbn/resources/ntd.ex

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,4 +115,18 @@ defmodule DiffoExample.Nbn.Ntd do
115115
change Diffo.Provider.Changes.Relate
116116
end
117117
end
118+
119+
calculations do
120+
# The UNI characteristic value of every UNI this NTD has assigned a
121+
# port to — reverse traversal of outgoing :port AssignmentRelationships
122+
# sourced from this NTD. Low cardinality (typical NTD has a handful of
123+
# ports). Filter by `thing` (the pool's thing name) rather than `alias`
124+
# — see assignment-direction-asymmetry memory.
125+
calculate :unis,
126+
{:array, :map},
127+
{DiffoExample.Calculations.ReverseInheritedCharacteristic,
128+
[thing: :port, characteristic_module: DiffoExample.Nbn.UniCharacteristic]} do
129+
public? true
130+
end
131+
end
118132
end

test/nbn/nbn_ethernet_test.exs

Lines changed: 37 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -19,150 +19,6 @@ defmodule DiffoExample.Nbn.NbnEthernetTest do
1919
alias Diffo.Provider.Assignment
2020
alias Diffo.Provider.Instance.Relationship
2121

22-
@tag :show_json
23-
test "show AVC, CVC, NniGroup as JSON (run with `mix test --only show_json`)" do
24-
{:ok, nni_group} = Nbn.build_nni_group(%{})
25-
26-
{:ok, nni_group} =
27-
Nbn.define_nni_group(nni_group, %{
28-
characteristic_value_updates: [
29-
nni_group: [group_name: "SYD-POI-01", location: "Sydney Olympic Park"],
30-
svlans: [first: 1, last: 4000, assignable_type: "svlan"]
31-
]
32-
})
33-
34-
# Two CVCs assigned svlans from this NniGroup
35-
cvc_ids =
36-
for bandwidth <- [400, 600] do
37-
{:ok, cvc} = Nbn.build_cvc(%{})
38-
39-
{:ok, _} =
40-
Nbn.define_cvc(cvc, %{
41-
characteristic_value_updates: [
42-
cvc: [bandwidth: bandwidth],
43-
cvlans: [first: 1, last: 4000, assignable_type: "cvlan"]
44-
]
45-
})
46-
47-
{:ok, _} =
48-
Nbn.assign_svlan(nni_group, %{
49-
assignment: %Assignment{
50-
assignee_id: cvc.id,
51-
alias: :svlan,
52-
operation: :auto_assign
53-
}
54-
})
55-
56-
cvc.id
57-
end
58-
59-
# Two NNIs comprised by this NniGroup — realistic capacities so
60-
# utilization comes out in the 0–1 range we expect operationally.
61-
nni_ids =
62-
for {port_id, capacity} <- [{"SYD-01-ETH-1", 10000}, {"SYD-01-ETH-2", 10000}] do
63-
{:ok, nni} = Nbn.build_nni(%{})
64-
65-
{:ok, _} =
66-
Nbn.define_nni(nni, %{
67-
characteristic_value_updates: [
68-
nni: [port_id: port_id, capacity: capacity]
69-
]
70-
})
71-
72-
nni.id
73-
end
74-
75-
{:ok, _} =
76-
Nbn.relate_nni_group(nni_group, %{
77-
relationships:
78-
Enum.map(nni_ids, fn id ->
79-
%Relationship{id: id, direction: :forward, type: :contains}
80-
end)
81-
})
82-
83-
# One AVC assigned a cvlan from the first CVC
84-
cvc1_id = hd(cvc_ids)
85-
{:ok, cvc1} = Nbn.get_cvc_by_id(cvc1_id)
86-
87-
{:ok, avc} = Nbn.build_avc(%{})
88-
89-
{:ok, _} =
90-
Nbn.define_avc(avc, %{
91-
characteristic_value_updates: [avc: [bandwidth_profile: :home_fast]]
92-
})
93-
94-
{:ok, _} =
95-
Nbn.assign_cvlan(cvc1, %{
96-
assignment: %Assignment{
97-
assignee_id: avc.id,
98-
alias: :cvlan,
99-
operation: :auto_assign
100-
}
101-
})
102-
103-
# Reload all three with their inheritance calcs / metrics loaded
104-
{:ok, avc} = Nbn.get_avc_by_id(avc.id, load: [:cvc, :nni_group])
105-
{:ok, cvc} = Nbn.get_cvc_by_id(cvc1_id, load: [:nni_group])
106-
{:ok, nni_group} = Nbn.get_nni_group_by_id(nni_group.id, load: [:nnis])
107-
108-
cvc_metrics =
109-
DiffoExample.Nbn.CvcMetrics
110-
|> Ash.Query.filter_input(instance_id: cvc.id)
111-
|> Ash.Query.load(:value)
112-
|> Ash.read_one!()
113-
114-
nni_group_metrics =
115-
DiffoExample.Nbn.NniGroupMetrics
116-
|> Ash.Query.filter_input(instance_id: nni_group.id)
117-
|> Ash.Query.load(:value)
118-
|> Ash.read_one!()
119-
120-
IO.puts("\n========== AVC.cvc (single-hop :cvlan) ==========")
121-
IO.inspect(avc.cvc, label: "avc.cvc")
122-
123-
IO.puts("\n========== AVC.nni_group (two-hop [:cvlan, :svlan]) ==========")
124-
IO.inspect(avc.nni_group, label: "avc.nni_group")
125-
126-
IO.puts("\n========== CVC.nni_group (single-hop :svlan) ==========")
127-
IO.inspect(cvc.nni_group, label: "cvc.nni_group")
128-
129-
IO.puts("\n========== CvcMetrics record (live aggregate over assigned AVCs) ==========")
130-
IO.inspect(cvc_metrics.value, label: "cvc_metrics.value")
131-
132-
IO.puts("\n========== NniGroupMetrics record (cvcs/nnis aggregates + utilization) ==========")
133-
IO.inspect(nni_group_metrics.value, label: "nni_group_metrics.value")
134-
135-
IO.puts("\n========== NniGroup.nnis (brought-up via :contains Relationship) ==========")
136-
IO.inspect(nni_group.nnis, label: "nni_group.nnis")
137-
138-
IO.puts("\n========== AVC (TMF JSON, current state) ==========")
139-
140-
avc
141-
|> Jason.encode!()
142-
|> Diffo.Util.summarise_dates()
143-
|> Jason.decode!()
144-
|> Jason.encode!(pretty: true)
145-
|> IO.puts()
146-
147-
IO.puts("\n========== CVC (TMF JSON, current state) ==========")
148-
149-
cvc
150-
|> Jason.encode!()
151-
|> Diffo.Util.summarise_dates()
152-
|> Jason.decode!()
153-
|> Jason.encode!(pretty: true)
154-
|> IO.puts()
155-
156-
IO.puts("\n========== NniGroup (TMF JSON, current state) ==========")
157-
158-
nni_group
159-
|> Jason.encode!()
160-
|> Diffo.Util.summarise_dates()
161-
|> Jason.decode!()
162-
|> Jason.encode!(pretty: true)
163-
|> IO.puts()
164-
end
165-
16622
describe "build nbn_ethernet" do
16723
test "create an nbn_ethernet access" do
16824
{:ok, access} = Nbn.build_nbn_ethernet(%{})
@@ -453,6 +309,43 @@ defmodule DiffoExample.Nbn.NbnEthernetTest do
453309
)
454310
end)
455311
end
312+
313+
test "ntd brings up assigned UNIs as unis[] via :port assignment" do
314+
{:ok, ntd} = Nbn.build_ntd(%{})
315+
316+
{:ok, ntd} =
317+
Nbn.define_ntd(ntd, %{
318+
characteristic_value_updates: [
319+
ntd: [model: "Sercomm CG4000A", technology: :FTTP],
320+
ports: [first: 1, last: 4, assignable_type: "port"]
321+
]
322+
})
323+
324+
# Two UNIs defined and assigned ports from the NTD
325+
for {port_num, encap} <- [{1, "DSCP Mapped"}, {2, "untagged"}] do
326+
{:ok, uni} = Nbn.build_uni(%{})
327+
328+
{:ok, _} =
329+
Nbn.define_uni(uni, %{
330+
characteristic_value_updates: [
331+
uni: [port: port_num, encapsulation: encap, technology: :FTTP]
332+
]
333+
})
334+
335+
{:ok, _} =
336+
Nbn.assign_port(ntd, %{
337+
assignment: %Assignment{assignee_id: uni.id, operation: :auto_assign}
338+
})
339+
end
340+
341+
{:ok, ntd} = Nbn.get_ntd_by_id(ntd.id, load: [:unis])
342+
343+
assert is_list(ntd.unis)
344+
assert length(ntd.unis) == 2
345+
346+
ports = Enum.map(ntd.unis, & &1.port) |> Enum.sort()
347+
assert ports == [1, 2]
348+
end
456349
end
457350

458351
describe "build cvc" do

0 commit comments

Comments
 (0)