Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions lib/access/calculations/shelf_total_ports.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# SPDX-FileCopyrightText: 2025 diffo_example contributors <https://github.com/diffo-dev/diffo_example/graphs.contributors>
#
# SPDX-License-Identifier: MIT

defmodule DiffoExample.Access.Calculations.ShelfTotalPorts do
@moduledoc """
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.

Local-to-this-repo for now. Could in time become a more general
diffo-side primitive (`SumPoolCapacityOfAssignees` or similar) once the
pattern repeats; the cleanest path may also be an ash_neo4j aggregate
primitive that walks assignment edges natively. Worth its own yarn.
"""
use Ash.Resource.Calculation

@impl true
def load(_query, _opts, _context), do: []

@impl true
def calculate(records, _opts, _context) do
Enum.map(records, fn shelf ->
assignments =
Diffo.Provider.AssignmentRelationship
|> Ash.Query.filter_input(source_id: shelf.id, alias: :slot)
|> Ash.read!(domain: Diffo.Provider)

Enum.reduce(assignments, 0, fn assignment, acc ->
case Diffo.Provider.AssignableCharacteristic
|> Ash.Query.filter_input(instance_id: assignment.target_id, name: :ports)
|> Ash.read_one!(domain: Diffo.Provider) do
nil -> acc
ports -> acc + (ports.last - ports.first + 1)
end
end)
end)
end
end
19 changes: 19 additions & 0 deletions lib/access/resources/card.ex
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,23 @@ defmodule DiffoExample.Access.Card do
change {DiffoExample.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.
calculate :shelf,
{:array, :map},
{DiffoExample.Calculations.InheritedCharacteristic,
[via: [:slot], 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.
calculate :slot,
{:array, :integer},
{Diffo.Provider.Calculations.FieldFromAssignment, [alias: :slot, field: :value]} do
public? true
end
end
end
31 changes: 31 additions & 0 deletions lib/access/resources/path.ex
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,35 @@ defmodule DiffoExample.Access.Path do
change DiffoExample.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.
calculate :card,
{:array, :map},
{DiffoExample.Calculations.InheritedCharacteristic,
[via: [:port], 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.
calculate :port,
{:array, :integer},
{Diffo.Provider.Calculations.FieldFromAssignment, [alias: :port, 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].
calculate :shelf,
{:array, :map},
{DiffoExample.Calculations.InheritedCharacteristic,
[
via: [:port, :slot],
characteristic_module: DiffoExample.Access.ShelfCharacteristic
]} do
public? true
end
end
end
22 changes: 22 additions & 0 deletions lib/access/resources/shelf.ex
Original file line number Diff line number Diff line change
Expand Up @@ -87,4 +87,26 @@ defmodule DiffoExample.Access.Shelf do
change {DiffoExample.Changes.Assign, pool: :slots}
end
end

calculations do
# Brings up the card characteristic of every card this shelf has
# assigned a slot to, ordered by slot number. Cards-as-assignees name
# their slot :slot when requesting; the calc filters outgoing
# AssignmentRelationship records by that alias.
calculate :cards,
{:array, :map},
{DiffoExample.Calculations.ReverseInheritedCharacteristic,
[alias: :slot, characteristic_module: DiffoExample.Access.CardCharacteristic]} do
public? true
end

# Sum of port capacity across every card assigned to this shelf.
# Each card's :ports pool size is `(last - first + 1)`. Reaches across
# the slot-assignment chain to AssignableCharacteristic on each card.
calculate :total_ports,
:integer,
DiffoExample.Access.Calculations.ShelfTotalPorts do
public? true
end
end
end
74 changes: 74 additions & 0 deletions lib/diffo_example/calculations/inherited_characteristic.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# SPDX-FileCopyrightText: 2025 diffo_example contributors <https://github.com/diffo-dev/diffo_example/graphs.contributors>
#
# SPDX-License-Identifier: MIT

defmodule DiffoExample.Calculations.InheritedCharacteristic do
@moduledoc """
Brings up a typed characteristic value from an assignment-source instance.

Mirrors the shape of `Diffo.Provider.Calculations.InheritedPlace` and
`Diffo.Provider.Calculations.InheritedParty` — traverses
`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.

## Options

- `via:` *(required)* — list of alias atoms naming the assignment chain
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.
- `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`.

## 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,
[via: [:slot], characteristic_module: ShelfCharacteristic]}

# Path brings up the same via a two-hop chain — port-then-slot.
calculate :shelf, :map,
{DiffoExample.Calculations.InheritedCharacteristic,
[via: [:port, :slot], characteristic_module: ShelfCharacteristic]}
"""
use Ash.Resource.Calculation

@impl true
def load(_query, _opts, _context), do: []

@impl true
def calculate(records, opts, _context) do
via = Keyword.fetch!(opts, :via)
characteristic_module = Keyword.fetch!(opts, :characteristic_module)

Enum.map(records, fn record ->
final_ids =
Enum.reduce(via, [record.id], fn alias_step, 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)
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)
end)
end
end
75 changes: 75 additions & 0 deletions lib/diffo_example/calculations/reverse_inherited_characteristic.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# SPDX-FileCopyrightText: 2025 diffo_example contributors <https://github.com/diffo-dev/diffo_example/graphs.contributors>
#
# SPDX-License-Identifier: MIT

defmodule DiffoExample.Calculations.ReverseInheritedCharacteristic do
@moduledoc """
Brings up typed characteristic values from instances this one has
assigned to — the reverse of inheritance.

`InheritedCharacteristic` is the conventional direction: the assignee
inherits its characteristic from its assigner (you inherit from your
parents). This is the reverse: the assigner brings up the characteristic
of every assignee it's connected to (insanity is hereditary — you get
it from your kids).

Traverses OUTGOING `AssignmentRelationship` records (this instance as
source) optionally filtered by alias, then reads the typed characteristic
on each assignee.

Useful when the assigner wants to compose its assignees into its own
view — e.g. a shelf bringing up the cards it has assigned slots to,
ordered by slot number.

Worth yarning upstream alongside `inherited_characteristic` as a pair
of diffo-side DSL declarations.

## 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`).

## Example

# Shelf brings up the card characteristic from every card it's
# assigned a slot to, ordered by slot number.
calculate :cards, {:array, :map},
{DiffoExample.Calculations.ReverseInheritedCharacteristic,
[alias: :slot, characteristic_module: CardCharacteristic]}
"""
use Ash.Resource.Calculation

@impl true
def load(_query, _opts, _context), do: []

@impl true
def calculate(records, opts, _context) do
alias_filter = Keyword.get(opts, :alias)
characteristic_module = Keyword.fetch!(opts, :characteristic_module)

Enum.map(records, fn record ->
assignments =
Diffo.Provider.AssignmentRelationship
|> filter_outgoing(record.id, alias_filter)
|> Ash.Query.sort(value: :asc)
|> Ash.read!(domain: Diffo.Provider)

Enum.flat_map(assignments, fn assignment ->
characteristic_module
|> Ash.Query.filter_input(instance_id: assignment.target_id)
|> Ash.Query.load(:value)
|> Ash.read!()
|> Enum.map(& &1.value)
end)
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, alias_filter),
do: query |> Ash.Query.filter_input(source_id: source_id, alias: alias_filter)
end
32 changes: 28 additions & 4 deletions test/access/path_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,11 @@ 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).
Access.assign_port!(line_card, %{
assignment: %Assignment{assignee_id: path.id, operation: :auto_assign}
assignment: %Assignment{assignee_id: path.id, alias: :port, operation: :auto_assign}
})

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

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

# the path brings up its card and (transitively) its shelf via the
# port-then-slot assignment chain — each instance shows itself, the path
# also shows what's brought up from below.
{:ok, path_with_brought_up} =
Ash.load(path, [:card, :shelf, :port])

[%{family: :ISAM, model: "EBLT48", technology: :adsl2Plus}] = path_with_brought_up.card

[%{device_name: "QDONC-0001", family: :ISAM, model: "ISAM7330", technology: :DSLAM}] =
path_with_brought_up.shelf

assert path_with_brought_up.port == [1]

encoding =
Jason.encode!(path)
|> Diffo.Util.summarise_dates()
Expand Down Expand Up @@ -220,18 +236,26 @@ defmodule DiffoExample.Access.PathTest do

shelf =
Access.define_shelf!(shelf, %{
characteristic_value_updates: [slots: [first: 1, last: 10, assignable_type: "LineCard"]]
characteristic_value_updates: [
shelf: [device_name: name, family: :ISAM, model: "ISAM7330", technology: :DSLAM],
slots: [first: 1, last: 10, assignable_type: "LineCard"]
]
})

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

card =
Access.define_card!(card, %{
characteristic_value_updates: [ports: [first: 1, last: 48, assignable_type: "ADSL2+"]]
characteristic_value_updates: [
card: [family: :ISAM, model: "EBLT48", technology: :adsl2Plus],
ports: [first: 1, last: 48, assignable_type: "ADSL2+"]
]
})

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

[shelf, card]
Expand Down
Loading
Loading