Skip to content

Commit c5dd990

Browse files
committed
inheritied party and place via instance dsl
1 parent e3ea3c4 commit c5dd990

15 files changed

Lines changed: 433 additions & 14 deletions

File tree

AGENTS.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ lib/diffo/provider/
6666
base_characteristic.ex # Ash Fragment for typed characteristic resources
6767
base_relationship.ex # Ash Fragment for shared Relationship structure
6868
defined_simple_relationship.ex # DefinedSimpleRelationship — relationship with one optional embedded characteristic, frozen at creation
69-
assignment_relationship.ex # AssignmentRelationship — pool assignment relationship with top-level pool/thing/value scalar attributes
69+
assignment_relationship.ex # AssignmentRelationship — pool assignment relationship with top-level pool/thing/value/alias scalar attributes
7070
relationship.ex # Relationship — mutable TMF service/resource relationship with graph Characteristic nodes
7171
calculations/
7272
characteristic_value.ex # Calculation: builds .Value TypedStruct from record fields
@@ -300,6 +300,7 @@ not. Add any useful hypotheses as a follow-up comment on the issue, then leave i
300300
- Calling `Assigner.assign/4` when a `pools do` declaration exists — prefer `Assigner.assign/3` which looks up the thing automatically.
301301
- Forgetting to call `Pool.update_pools/3` in `:define` actions when the resource has `pools do` — pool bounds (`first`, `last`, `algorithm`) are set here.
302302
- Calling `Assigner.assign/3` on an instance that is not in the correct lifecycle state — the assigner enforces: resource instances must have `resource_state: :operating`; service instances must have `service_state: :active` or `:inactive`. Lifecycle state transitions are an internal domain concern managed by the provider; assignment actions are external-facing. Future: consumer reads may filter out non-`:operating` resources entirely.
303+
- Wondering why `Relationship` and `AssignmentRelationship` both have an `alias` attribute with a `[:source_id, :alias]` / `[:target_id, :alias]` identity — alias is a "baby name" given to a relationship slot before (or when) the target is bound. Its full purpose becomes clear alongside the first-order expectation system (see issue #122): the expectation declares the alias for a slot it expects to be filled, and the actual relationship carries the same alias so the two can be matched. Without expectations in place, aliases look like optional metadata; with them, they are the join key between intent and fulfilment.
303304
- Using `characteristic :pool_name, Diffo.Provider.AssignedToRelationship``AssignedToRelationship` no longer exists; use `pools do / pool :name, :thing / end` instead.
304305
- Querying `Diffo.Provider.Relationship` for assignment records — assignments are stored as `Diffo.Provider.DefinedSimpleRelationship`; access them via `instance.assignments`.
305306
- Filtering `instance.forward_relationships` for `type == :assignedTo` — those records no longer exist there; use `instance.assignments` directly.

lib/diffo/provider/assigner/assigner.ex

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ defmodule Diffo.Provider.Assigner do
3535
is_atom(thing) do
3636
assignment = Map.get(changeset.arguments, :assignment, %{})
3737
assignee_id = Map.get(assignment, :assignee_id)
38+
alias_name = Map.get(assignment, :alias)
3839

3940
case assignee_id do
4041
nil ->
@@ -44,12 +45,12 @@ defmodule Diffo.Provider.Assigner do
4445
case Map.get(assignment, :operation, :auto_assign) do
4546
:auto_assign ->
4647
with {:ok, value} <- next(result, pool, thing) do
47-
create_assignment(result, pool, thing, value, assignee_id)
48+
create_assignment(result, pool, thing, value, assignee_id, alias_name)
4849
end
4950

5051
:assign ->
5152
if assignable?(result, pool, thing, assignment.id) do
52-
create_assignment(result, pool, thing, assignment.id, assignee_id)
53+
create_assignment(result, pool, thing, assignment.id, assignee_id, alias_name)
5354
else
5455
{:error, "#{thing} #{assignment.id} is not assignable"}
5556
end
@@ -69,11 +70,12 @@ defmodule Diffo.Provider.Assigner do
6970

7071
defp check_lifecycle(_), do: :ok
7172

72-
defp create_assignment(result, pool, thing, value, assignee_id)
73+
defp create_assignment(result, pool, thing, value, assignee_id, alias_name)
7374
when is_struct(result) and is_atom(pool) and is_atom(thing) and is_integer(value) and
7475
is_bitstring(assignee_id) do
7576
with {:ok, _} <-
7677
Diffo.Provider.create_assignment_relationship(%{
78+
alias: alias_name,
7779
pool: pool,
7880
thing: thing,
7981
value: value,

lib/diffo/provider/assigner/assignment.ex

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ defmodule Diffo.Provider.Assignment do
1919
constraints: [min: 0],
2020
description: "the id of the assigned thing"
2121

22+
field :alias, :atom, description: "the consumer's stable name for this assignment slot"
23+
2224
field :assignable_type, :string, description: "the type of the assigned thing"
2325

2426
field :assignee_id, :uuid, description: "the id of the assignee Ash resource"

lib/diffo/provider/components/assignment_relationship.ex

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,18 @@ defmodule Diffo.Provider.AssignmentRelationship do
4747
list_name =
4848
Diffo.Provider.Relationship.derive_relationship_characteristic_list_name(record.target_type)
4949

50-
characteristic = %{name: record.thing, value: record.value}
50+
characteristics =
51+
[%{name: record.thing, value: record.value}]
52+
|> then(fn chars ->
53+
case record.alias do
54+
nil -> chars
55+
a -> chars ++ [%{name: :alias, value: a}]
56+
end
57+
end)
5158

5259
result
5360
|> Diffo.Util.set(record.target_type, reference)
54-
|> Diffo.Util.set(list_name, [characteristic])
61+
|> Diffo.Util.set(list_name, characteristics)
5562
end
5663

5764
order [:type, :resource, :service, :resourceRelationshipCharacteristic,
@@ -61,7 +68,7 @@ defmodule Diffo.Provider.AssignmentRelationship do
6168
actions do
6269
create :create do
6370
description "creates a pool assignment relationship between a source and target instance"
64-
accept [:pool, :thing, :value]
71+
accept [:alias, :pool, :thing, :value]
6572

6673
argument :source_id, :uuid
6774
argument :target_id, :string
@@ -74,6 +81,12 @@ defmodule Diffo.Provider.AssignmentRelationship do
7481
end
7582

7683
attributes do
84+
attribute :alias, :atom do
85+
description "the alias of this assignment, used by the consuming instance to name the slot"
86+
allow_nil? true
87+
public? true
88+
end
89+
7790
attribute :pool, :atom do
7891
description "the pool name this assignment belongs to (e.g. :ports)"
7992
allow_nil? false
@@ -98,6 +111,10 @@ defmodule Diffo.Provider.AssignmentRelationship do
98111
identity :unique_assignment, [:source_id, :pool, :thing, :value] do
99112
pre_check? true
100113
end
114+
115+
identity :unique_alias, [:target_id, :alias] do
116+
pre_check? true
117+
end
101118
end
102119

103120
preparations do
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# SPDX-FileCopyrightText: 2025 diffo contributors <https://github.com/diffo-dev/diffo/graphs.contributors>
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
defmodule Diffo.Provider.Calculations.InheritedParty do
6+
@moduledoc false
7+
use Ash.Resource.Calculation
8+
9+
@impl true
10+
def load(_query, _opts, _context), do: []
11+
12+
@impl true
13+
def calculate(records, opts, _context) do
14+
via = opts[:via]
15+
source_role = opts[:source_role]
16+
17+
Enum.map(records, fn record ->
18+
final_ids =
19+
Enum.reduce(via, [record.id], fn alias_step, ids ->
20+
Enum.flat_map(ids, fn id ->
21+
Diffo.Provider.AssignmentRelationship
22+
|> Ash.Query.filter_input(target_id: id, alias: alias_step)
23+
|> Ash.read!(domain: Diffo.Provider)
24+
|> Enum.map(& &1.source_id)
25+
end)
26+
end)
27+
28+
Enum.flat_map(final_ids, fn id ->
29+
Diffo.Provider.PartyRef
30+
|> Ash.Query.filter_input(instance_id: id, role: source_role)
31+
|> Ash.Query.load(:party)
32+
|> Ash.read!(domain: Diffo.Provider)
33+
|> Enum.map(& &1.party)
34+
end)
35+
end)
36+
end
37+
end
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# SPDX-FileCopyrightText: 2025 diffo contributors <https://github.com/diffo-dev/diffo/graphs.contributors>
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
defmodule Diffo.Provider.Calculations.InheritedPlace do
6+
@moduledoc false
7+
use Ash.Resource.Calculation
8+
9+
@impl true
10+
def load(_query, _opts, _context), do: []
11+
12+
@impl true
13+
def calculate(records, opts, _context) do
14+
via = opts[:via]
15+
source_role = opts[:source_role]
16+
17+
Enum.map(records, fn record ->
18+
final_ids =
19+
Enum.reduce(via, [record.id], fn alias_step, ids ->
20+
Enum.flat_map(ids, fn id ->
21+
Diffo.Provider.AssignmentRelationship
22+
|> Ash.Query.filter_input(target_id: id, alias: alias_step)
23+
|> Ash.read!(domain: Diffo.Provider)
24+
|> Enum.map(& &1.source_id)
25+
end)
26+
end)
27+
28+
Enum.flat_map(final_ids, fn id ->
29+
Diffo.Provider.PlaceRef
30+
|> Ash.Query.filter_input(instance_id: id, role: source_role)
31+
|> Ash.Query.load(:place)
32+
|> Ash.read!(domain: Diffo.Provider)
33+
|> Enum.map(& &1.place)
34+
end)
35+
end)
36+
end
37+
end

lib/diffo/provider/components/relationship.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,9 @@ defmodule Diffo.Provider.Relationship do
130130
end
131131

132132
identities do
133-
identity :unique_source_and_target, [:source_id, :target_id]
133+
identity :unique_source_alias, [:source_id, :alias] do
134+
pre_check? true
135+
end
134136
end
135137

136138
preparations do

lib/diffo/provider/extension.ex

Lines changed: 61 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,8 @@ defmodule Diffo.Provider.Extension do
9393
Characteristic,
9494
Feature,
9595
InstanceRole,
96+
InheritedPartyDeclaration,
97+
InheritedPlaceDeclaration,
9698
PartyDeclaration,
9799
PartyRole,
98100
PlaceDeclaration,
@@ -292,17 +294,43 @@ defmodule Diffo.Provider.Extension do
292294
]
293295
}
294296

297+
@inherited_party_entity %Spark.Dsl.Entity{
298+
name: :inherited_party,
299+
describe:
300+
"Declares a party derived by traversing the assignment graph — generates a calculation, no PartyRef node created",
301+
target: InheritedPartyDeclaration,
302+
args: [:role],
303+
schema: [
304+
role: [
305+
type: :atom,
306+
doc: "The role name — also the default alias to follow on AssignmentRelationship.",
307+
required: true
308+
],
309+
via: [
310+
type: {:list, :atom},
311+
doc:
312+
"Sequence of assignment aliases to traverse. Defaults to [role] for single-hop. Use a list for multi-level."
313+
],
314+
source_role: [
315+
type: :atom,
316+
doc: "The PartyRef role to pick up on the arrived-at instance.",
317+
required: true
318+
]
319+
]
320+
}
321+
295322
@parties %Spark.Dsl.Section{
296323
name: :parties,
297324
describe:
298-
"Party roles on this resource — `party`/`parties`/`party_ref` for Instance kinds; `role` for Party and Place kinds",
325+
"Party roles on this resource — `party`/`parties`/`party_ref`/`inherited_party` for Instance kinds; `role` for Party and Place kinds",
299326
examples: [
300327
"""
301328
# Instance
302329
parties do
303330
party :provider, MyApp.Provider
304331
party_ref :owner, MyApp.InfrastructureCo
305332
parties :technicians, MyApp.Technician, constraints: [min: 1]
333+
inherited_party :customer, source_role: :owner
306334
end
307335
308336
# Party or Place
@@ -311,7 +339,7 @@ defmodule Diffo.Provider.Extension do
311339
end
312340
"""
313341
],
314-
entities: [@party_entity, @parties_entity, @party_ref_entity, @party_role_entity]
342+
entities: [@party_entity, @parties_entity, @party_ref_entity, @party_role_entity, @inherited_party_entity]
315343
}
316344

317345
# ── places ─────────────────────────────────────────────────────────────────
@@ -368,16 +396,43 @@ defmodule Diffo.Provider.Extension do
368396
]
369397
}
370398

399+
@inherited_place_entity %Spark.Dsl.Entity{
400+
name: :inherited_place,
401+
describe:
402+
"Declares a place derived by traversing the assignment graph — generates a calculation, no PlaceRef node created",
403+
target: InheritedPlaceDeclaration,
404+
args: [:role],
405+
schema: [
406+
role: [
407+
type: :atom,
408+
doc: "The role name — also the default alias to follow on AssignmentRelationship.",
409+
required: true
410+
],
411+
via: [
412+
type: {:list, :atom},
413+
doc:
414+
"Sequence of assignment aliases to traverse. Defaults to [role] for single-hop. Use a list for multi-level."
415+
],
416+
source_role: [
417+
type: :atom,
418+
doc: "The PlaceRef role to pick up on the arrived-at instance.",
419+
required: true
420+
]
421+
]
422+
}
423+
371424
@places %Spark.Dsl.Section{
372425
name: :places,
373426
describe:
374-
"Place roles on this resource — `place`/`places`/`place_ref` for Instance kinds; `role` for Party and Place kinds",
427+
"Place roles on this resource — `place`/`places`/`place_ref`/`inherited_place` for Instance kinds; `role` for Party and Place kinds",
375428
examples: [
376429
"""
377430
# Instance
378431
places do
379432
place :installation_site, MyApp.GeographicSite
380433
place_ref :billing_address, MyApp.GeographicAddress
434+
inherited_place :a_end, source_role: :location
435+
inherited_place :poi, via: [:cvc_link, :nni_link], source_role: :poi
381436
end
382437
383438
# Party or Place
@@ -386,7 +441,7 @@ defmodule Diffo.Provider.Extension do
386441
end
387442
"""
388443
],
389-
entities: [@place_entity, @places_entity, @place_ref_entity, @place_role_entity]
444+
entities: [@place_entity, @places_entity, @place_ref_entity, @place_role_entity, @inherited_place_entity]
390445
}
391446

392447
# ── instances ──────────────────────────────────────────────────────────────
@@ -586,7 +641,8 @@ defmodule Diffo.Provider.Extension do
586641
sections: [@provider],
587642
transformers: [
588643
Diffo.Provider.Extension.Transformers.TransformRelationships,
589-
Diffo.Provider.Extension.Transformers.TransformBehaviour
644+
Diffo.Provider.Extension.Transformers.TransformBehaviour,
645+
Diffo.Provider.Extension.Transformers.TransformInheritedRefs
590646
],
591647
persisters: [
592648
Diffo.Provider.Extension.Persisters.PersistSpecification,
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# SPDX-FileCopyrightText: 2025 diffo contributors <https://github.com/diffo-dev/diffo/graphs.contributors>
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
defmodule Diffo.Provider.Extension.InheritedPartyDeclaration do
6+
@moduledoc "DSL entity declaring an inherited party role — derived by traversing the assignment graph"
7+
defstruct [:role, :via, :source_role, __spark_metadata__: nil]
8+
9+
defimpl String.Chars do
10+
def to_string(struct), do: inspect(struct)
11+
end
12+
end
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# SPDX-FileCopyrightText: 2025 diffo contributors <https://github.com/diffo-dev/diffo/graphs.contributors>
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
defmodule Diffo.Provider.Extension.InheritedPlaceDeclaration do
6+
@moduledoc "DSL entity declaring an inherited place role — derived by traversing the assignment graph"
7+
defstruct [:role, :via, :source_role, __spark_metadata__: nil]
8+
9+
defimpl String.Chars do
10+
def to_string(struct), do: inspect(struct)
11+
end
12+
end

0 commit comments

Comments
 (0)