Skip to content

Commit 8401928

Browse files
committed
forward and reverse inherited characteristics
1 parent bac8da5 commit 8401928

8 files changed

Lines changed: 349 additions & 4 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# SPDX-FileCopyrightText: 2025 diffo_example contributors <https://github.com/diffo-dev/diffo_example/graphs.contributors>
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
defmodule DiffoExample.Access.Calculations.ShelfTotalPorts do
6+
@moduledoc """
7+
Sums the `:ports` pool capacity across every card a shelf has assigned
8+
a slot to.
9+
10+
For each outgoing slot-assignment (alias `:slot`, source = shelf), looks
11+
up the assigned card's `AssignableCharacteristic` for the `:ports` pool
12+
and sums `(last - first + 1)` across all of them.
13+
14+
Local-to-this-repo for now. Could in time become a more general
15+
diffo-side primitive (`SumPoolCapacityOfAssignees` or similar) once the
16+
pattern repeats; the cleanest path may also be an ash_neo4j aggregate
17+
primitive that walks assignment edges natively. Worth its own yarn.
18+
"""
19+
use Ash.Resource.Calculation
20+
21+
@impl true
22+
def load(_query, _opts, _context), do: []
23+
24+
@impl true
25+
def calculate(records, _opts, _context) do
26+
Enum.map(records, fn shelf ->
27+
assignments =
28+
Diffo.Provider.AssignmentRelationship
29+
|> Ash.Query.filter_input(source_id: shelf.id, alias: :slot)
30+
|> Ash.read!(domain: Diffo.Provider)
31+
32+
Enum.reduce(assignments, 0, fn assignment, acc ->
33+
case Diffo.Provider.AssignableCharacteristic
34+
|> Ash.Query.filter_input(instance_id: assignment.target_id, name: :ports)
35+
|> Ash.read_one!(domain: Diffo.Provider) do
36+
nil -> acc
37+
ports -> acc + (ports.last - ports.first + 1)
38+
end
39+
end)
40+
end)
41+
end
42+
end

lib/access/resources/card.ex

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,23 @@ defmodule DiffoExample.Access.Card do
8787
change {DiffoExample.Changes.Assign, pool: :ports}
8888
end
8989
end
90+
91+
calculations do
92+
# The shelf characteristic value brought up from the shelf this card is
93+
# in — derived live via the :slot assignment.
94+
calculate :shelf,
95+
{:array, :map},
96+
{DiffoExample.Calculations.InheritedCharacteristic,
97+
[via: [:slot], characteristic_module: DiffoExample.Access.ShelfCharacteristic]} do
98+
public? true
99+
end
100+
101+
# The slot number this card occupies on its shelf — the value of the
102+
# shelf's :slots-pool assignment to this card.
103+
calculate :slot,
104+
{:array, :integer},
105+
{Diffo.Provider.Calculations.FieldFromAssignment, [alias: :slot, field: :value]} do
106+
public? true
107+
end
108+
end
90109
end

lib/access/resources/path.ex

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,35 @@ defmodule DiffoExample.Access.Path do
7474
change DiffoExample.Changes.Relate
7575
end
7676
end
77+
78+
calculations do
79+
# The card characteristic value brought up from the card this path is
80+
# assigned a port on — via the :port assignment.
81+
calculate :card,
82+
{:array, :map},
83+
{DiffoExample.Calculations.InheritedCharacteristic,
84+
[via: [:port], characteristic_module: DiffoExample.Access.CardCharacteristic]} do
85+
public? true
86+
end
87+
88+
# The port number this path occupies on its card — the value of the
89+
# card's :ports-pool assignment to this path.
90+
calculate :port,
91+
{:array, :integer},
92+
{Diffo.Provider.Calculations.FieldFromAssignment, [alias: :port, field: :value]} do
93+
public? true
94+
end
95+
96+
# The shelf characteristic value brought up transitively — port to the
97+
# card, then the card's slot to its shelf. Two-hop via [:port, :slot].
98+
calculate :shelf,
99+
{:array, :map},
100+
{DiffoExample.Calculations.InheritedCharacteristic,
101+
[
102+
via: [:port, :slot],
103+
characteristic_module: DiffoExample.Access.ShelfCharacteristic
104+
]} do
105+
public? true
106+
end
107+
end
77108
end

lib/access/resources/shelf.ex

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,26 @@ defmodule DiffoExample.Access.Shelf do
8787
change {DiffoExample.Changes.Assign, pool: :slots}
8888
end
8989
end
90+
91+
calculations do
92+
# Brings up the card characteristic of every card this shelf has
93+
# assigned a slot to, ordered by slot number. Cards-as-assignees name
94+
# their slot :slot when requesting; the calc filters outgoing
95+
# AssignmentRelationship records by that alias.
96+
calculate :cards,
97+
{:array, :map},
98+
{DiffoExample.Calculations.ReverseInheritedCharacteristic,
99+
[alias: :slot, characteristic_module: DiffoExample.Access.CardCharacteristic]} do
100+
public? true
101+
end
102+
103+
# Sum of port capacity across every card assigned to this shelf.
104+
# Each card's :ports pool size is `(last - first + 1)`. Reaches across
105+
# the slot-assignment chain to AssignableCharacteristic on each card.
106+
calculate :total_ports,
107+
:integer,
108+
DiffoExample.Access.Calculations.ShelfTotalPorts do
109+
public? true
110+
end
111+
end
90112
end
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# SPDX-FileCopyrightText: 2025 diffo_example contributors <https://github.com/diffo-dev/diffo_example/graphs.contributors>
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
defmodule DiffoExample.Calculations.InheritedCharacteristic do
6+
@moduledoc """
7+
Brings up a typed characteristic value from an assignment-source instance.
8+
9+
Mirrors the shape of `Diffo.Provider.Calculations.InheritedPlace` and
10+
`Diffo.Provider.Calculations.InheritedParty` — traverses
11+
`AssignmentRelationship` by alias to reach source instances, then queries
12+
the typed characteristic resource on each source and returns its `.value`.
13+
14+
Local-to-this-repo for now. Worth yarning upstream as a diffo-side
15+
`inherited_characteristic` DSL declaration backed by a
16+
`Diffo.Provider.Calculations.InheritedCharacteristic` calc, sitting
17+
alongside the existing inherited-place and inherited-party machinery.
18+
19+
## Options
20+
21+
- `via:` *(required)* — list of alias atoms naming the assignment chain
22+
from this instance back to the source whose characteristic we want.
23+
Each step filters `AssignmentRelationship` by `target_id` and `alias`,
24+
then follows `source_id` to the next set of instances. The aliases are
25+
the assignee's slot names, supplied when the assignment is made.
26+
- `characteristic_module:` *(required)* — the typed characteristic Ash
27+
resource on the final source (e.g. `ShelfCharacteristic`). The calc
28+
queries this resource by `instance_id` and returns the `.value`.
29+
30+
## Example
31+
32+
# Card brings up its shelf's typed characteristic via the slot
33+
# assignment the shelf made to it (alias :slot on the incoming
34+
# AssignmentRelationship).
35+
calculate :shelf, :map,
36+
{DiffoExample.Calculations.InheritedCharacteristic,
37+
[via: [:slot], characteristic_module: ShelfCharacteristic]}
38+
39+
# Path brings up the same via a two-hop chain — port-then-slot.
40+
calculate :shelf, :map,
41+
{DiffoExample.Calculations.InheritedCharacteristic,
42+
[via: [:port, :slot], characteristic_module: ShelfCharacteristic]}
43+
"""
44+
use Ash.Resource.Calculation
45+
46+
@impl true
47+
def load(_query, _opts, _context), do: []
48+
49+
@impl true
50+
def calculate(records, opts, _context) do
51+
via = Keyword.fetch!(opts, :via)
52+
characteristic_module = Keyword.fetch!(opts, :characteristic_module)
53+
54+
Enum.map(records, fn record ->
55+
final_ids =
56+
Enum.reduce(via, [record.id], fn alias_step, ids ->
57+
Enum.flat_map(ids, fn id ->
58+
Diffo.Provider.AssignmentRelationship
59+
|> Ash.Query.filter_input(target_id: id, alias: alias_step)
60+
|> Ash.read!(domain: Diffo.Provider)
61+
|> Enum.map(& &1.source_id)
62+
end)
63+
end)
64+
65+
Enum.flat_map(final_ids, fn id ->
66+
characteristic_module
67+
|> Ash.Query.filter_input(instance_id: id)
68+
|> Ash.Query.load(:value)
69+
|> Ash.read!()
70+
|> Enum.map(& &1.value)
71+
end)
72+
end)
73+
end
74+
end
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# SPDX-FileCopyrightText: 2025 diffo_example contributors <https://github.com/diffo-dev/diffo_example/graphs.contributors>
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
defmodule DiffoExample.Calculations.ReverseInheritedCharacteristic do
6+
@moduledoc """
7+
Brings up typed characteristic values from instances this one has
8+
assigned to — the reverse of inheritance.
9+
10+
`InheritedCharacteristic` is the conventional direction: the assignee
11+
inherits its characteristic from its assigner (you inherit from your
12+
parents). This is the reverse: the assigner brings up the characteristic
13+
of every assignee it's connected to (insanity is hereditary — you get
14+
it from your kids).
15+
16+
Traverses OUTGOING `AssignmentRelationship` records (this instance as
17+
source) optionally filtered by alias, then reads the typed characteristic
18+
on each assignee.
19+
20+
Useful when the assigner wants to compose its assignees into its own
21+
view — e.g. a shelf bringing up the cards it has assigned slots to,
22+
ordered by slot number.
23+
24+
Worth yarning upstream alongside `inherited_characteristic` as a pair
25+
of diffo-side DSL declarations.
26+
27+
## Options
28+
29+
- `alias:` *(optional)* — filter outgoing assignments by alias (the
30+
assignee's slot name). When omitted, all outgoing assignments are
31+
included.
32+
- `characteristic_module:` *(required)* — the typed characteristic Ash
33+
resource on each assignee (e.g. `CardCharacteristic`).
34+
35+
## Example
36+
37+
# Shelf brings up the card characteristic from every card it's
38+
# assigned a slot to, ordered by slot number.
39+
calculate :cards, {:array, :map},
40+
{DiffoExample.Calculations.ReverseInheritedCharacteristic,
41+
[alias: :slot, characteristic_module: CardCharacteristic]}
42+
"""
43+
use Ash.Resource.Calculation
44+
45+
@impl true
46+
def load(_query, _opts, _context), do: []
47+
48+
@impl true
49+
def calculate(records, opts, _context) do
50+
alias_filter = Keyword.get(opts, :alias)
51+
characteristic_module = Keyword.fetch!(opts, :characteristic_module)
52+
53+
Enum.map(records, fn record ->
54+
assignments =
55+
Diffo.Provider.AssignmentRelationship
56+
|> filter_outgoing(record.id, alias_filter)
57+
|> Ash.Query.sort(value: :asc)
58+
|> Ash.read!(domain: Diffo.Provider)
59+
60+
Enum.flat_map(assignments, fn assignment ->
61+
characteristic_module
62+
|> Ash.Query.filter_input(instance_id: assignment.target_id)
63+
|> Ash.Query.load(:value)
64+
|> Ash.read!()
65+
|> Enum.map(& &1.value)
66+
end)
67+
end)
68+
end
69+
70+
defp filter_outgoing(query, source_id, nil),
71+
do: query |> Ash.Query.filter_input(source_id: source_id)
72+
73+
defp filter_outgoing(query, source_id, alias_filter),
74+
do: query |> Ash.Query.filter_input(source_id: source_id, alias: alias_filter)
75+
end

test/access/path_test.exs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,11 @@ defmodule DiffoExample.Access.PathTest do
109109
# now assign a port from a line card
110110
[_dslam, line_card] = create_dslam_with_line_card("QDONC-0001", tl(places), parties)
111111

112+
# path-as-assignee names its slot :port when requesting the port-assignment
113+
# from the line card. This alias lets the InheritedCharacteristic calc
114+
# traverse path → port → card (and transitively card → slot → shelf).
112115
Access.assign_port!(line_card, %{
113-
assignment: %Assignment{assignee_id: path.id, operation: :auto_assign}
116+
assignment: %Assignment{assignee_id: path.id, alias: :port, operation: :auto_assign}
114117
})
115118

116119
# 5 cables each assigned a pair to the path, plus 1 line card assigned a port
@@ -124,6 +127,19 @@ defmodule DiffoExample.Access.PathTest do
124127

125128
{:ok, path} = Access.get_path_by_id(path.id)
126129

130+
# the path brings up its card and (transitively) its shelf via the
131+
# port-then-slot assignment chain — each instance shows itself, the path
132+
# also shows what's brought up from below.
133+
{:ok, path_with_brought_up} =
134+
Ash.load(path, [:card, :shelf, :port])
135+
136+
[%{family: :ISAM, model: "EBLT48", technology: :adsl2Plus}] = path_with_brought_up.card
137+
138+
[%{device_name: "QDONC-0001", family: :ISAM, model: "ISAM7330", technology: :DSLAM}] =
139+
path_with_brought_up.shelf
140+
141+
assert path_with_brought_up.port == [1]
142+
127143
encoding =
128144
Jason.encode!(path)
129145
|> Diffo.Util.summarise_dates()
@@ -220,18 +236,26 @@ defmodule DiffoExample.Access.PathTest do
220236

221237
shelf =
222238
Access.define_shelf!(shelf, %{
223-
characteristic_value_updates: [slots: [first: 1, last: 10, assignable_type: "LineCard"]]
239+
characteristic_value_updates: [
240+
shelf: [device_name: name, family: :ISAM, model: "ISAM7330", technology: :DSLAM],
241+
slots: [first: 1, last: 10, assignable_type: "LineCard"]
242+
]
224243
})
225244

226245
card = Access.build_card!(%{name: "dslam line card #{name} 1"})
227246

228247
card =
229248
Access.define_card!(card, %{
230-
characteristic_value_updates: [ports: [first: 1, last: 48, assignable_type: "ADSL2+"]]
249+
characteristic_value_updates: [
250+
card: [family: :ISAM, model: "EBLT48", technology: :adsl2Plus],
251+
ports: [first: 1, last: 48, assignable_type: "ADSL2+"]
252+
]
231253
})
232254

255+
# card-as-assignee names its slot :slot when requesting; alias lets
256+
# downstream calculations traverse the assignment by name.
233257
Access.assign_slot!(shelf, %{
234-
assignment: %Assignment{assignee_id: card.id, operation: :auto_assign}
258+
assignment: %Assignment{assignee_id: card.id, alias: :slot, operation: :auto_assign}
235259
})
236260

237261
[shelf, card]

0 commit comments

Comments
 (0)