Skip to content

Commit b9c4ffb

Browse files
Merge pull request #166 from diffo-dev/160-docs-pass-inherited-dsl-aliases-and-field-calculations
update docs and guidance
2 parents f8460c4 + dd8e5c5 commit b9c4ffb

10 files changed

Lines changed: 434 additions & 10 deletions

AGENTS.md

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,10 @@ lib/diffo/provider/
5151
relationship_step.ex # RelationshipStep struct — pipeline step for relationships do
5252
persisters/ # Terminal bakers — run after all transformers; only read DSL state and bake module functions
5353
transformers/
54-
transform_relationships.ex # TransformRelationships — resolves relationships pipeline, bakes permitted_source_roles/0 and permitted_target_roles/0
54+
transform_relationships.ex # TransformRelationships — resolves relationships pipeline, bakes permitted_source_roles/0 and permitted_target_roles/0
55+
transform_inherited_refs.ex # TransformInheritedRefs — injects calculations for inherited_place/inherited_party declarations
56+
inherited_place_declaration.ex # DSL entity struct for inherited_place
57+
inherited_party_declaration.ex # DSL entity struct for inherited_party
5558
verifiers/
5659
verify_relationships.ex # Verifies relationship role declarations are atoms
5760
validations/
@@ -69,8 +72,13 @@ lib/diffo/provider/
6972
assignment_relationship.ex # AssignmentRelationship — pool assignment relationship with top-level pool/thing/value/alias scalar attributes
7073
relationship.ex # Relationship — mutable TMF service/resource relationship with graph Characteristic nodes
7174
calculations/
72-
characteristic_value.ex # Calculation: builds .Value TypedStruct from record fields
73-
assigned_values.ex # Calculation: returns list of assigned integers for a pool+thing
75+
characteristic_value.ex # Calculation: builds .Value TypedStruct from record fields
76+
assigned_values.ex # Calculation: returns list of assigned integers for a pool+thing
77+
inherited_place.ex # Calculation: backing impl for inherited_place DSL
78+
inherited_party.ex # Calculation: backing impl for inherited_party DSL
79+
field_from_assignment.ex # Calculation: field from AssignmentRelationship record
80+
field_via_assigned_relationship.ex # Calculation: field from source instance via assignment traversal
81+
field_via_relationship.ex # Calculation: field from target instance via DefinedSimpleRelationship
7482
instance/extension.ex # Thin marker (sections: []) — kind identification
7583
party/extension.ex # Thin marker
7684
place/extension.ex # Thin marker
@@ -147,6 +155,10 @@ provider do
147155
places do
148156
place :installation_site, MyApp.GeographicSite
149157
place_ref :billing_address, MyApp.GeographicAddress
158+
# Inherited — generates a calculation that traverses AssignmentRelationship
159+
# by alias and reads PlaceRef from the source instance. No PlaceRef edge is created.
160+
inherited_place :exchange, source_role: :location # alias = role name (single-hop default)
161+
inherited_place :nni_site, via: [:uplink], source_role: :location # explicit alias
150162
end
151163

152164
behaviour do
@@ -350,3 +362,17 @@ not. Add any useful hypotheses as a follow-up comment on the issue, then leave i
350362
- Using `Ash.Resource.Change` for pure permission or constraint checks — anything that only
351363
decides valid/invalid with no side effects belongs in `Ash.Resource.Validation`, not a
352364
change. Changes are for mutations.
365+
- Using `inherited_place` or `inherited_party` without an assignment alias in place — the
366+
traversal filters by alias; if the assignment was created without an alias (or with a
367+
different alias), the calculation returns an empty list. Ensure the `alias:` field on
368+
`Assignment` matches the declared role (or the `via:` step) before expecting results.
369+
- Referencing `Diffo.Provider.Calculations.InheritedPlace` or `InheritedParty` directly in
370+
`calculations do` — these are internal modules injected by the transformer. Use the
371+
`inherited_place` / `inherited_party` DSL entities in `places do` / `parties do` instead.
372+
- Reaching for `FieldViaRelationship` to traverse an `AssignmentRelationship` — that module
373+
traverses `DefinedSimpleRelationship` (forward, source → target). For assignments
374+
(reverse, target → source) use `FieldViaAssignedRelationship` or `FieldFromAssignment`.
375+
- Querying `FieldViaRelationship` without supplying `alias:` or `type:` — a source instance
376+
typically has many forward `DefinedSimpleRelationship` records pointing to unrelated things.
377+
Without at least one filter the result is a noisy mix. Always supply `alias:`, `type:`, or
378+
both.

documentation/how_to/use_diffo_provider_extension.livemd

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,6 +494,114 @@ defmodule Diffo.Compute.GPU do
494494
end
495495
```
496496

497+
## Aliases, Inherited DSL, and Field Calculations
498+
499+
### Aliases on assignment slots
500+
501+
Every `AssignmentRelationship` carries an optional `:alias` — an atom given to a slot by
502+
the consuming (target) side before or when the assignment is bound. Think of it as a stable
503+
name for the slot: the consumer says "I have a slot called `:primary_gpu`", and the producer
504+
assigns into it carrying `alias: :primary_gpu`. The alias never changes, even if the
505+
assignment is recreated.
506+
507+
Pass the alias via `Assignment.alias` when assigning:
508+
509+
```elixir
510+
# Assign a core from gpu_1 into cluster_1's :primary_gpu slot
511+
assignment = %{
512+
assignment: %Assignment{
513+
assignee_id: cluster_1.id,
514+
operation: :auto_assign,
515+
alias: :primary_gpu
516+
}
517+
}
518+
gpu_1 = Compute.assign_gpu_core!(gpu_1, assignment)
519+
```
520+
521+
The identity constraint `[:target_id, :alias]` on `AssignmentRelationship` guarantees at
522+
most one assignment per (cluster, alias) pair — the `:primary_gpu` slot can only hold one
523+
assignment at a time.
524+
525+
### Inheriting a place from an assigned resource
526+
527+
A service or resource can declare that it inherits a place from the instance that assigned
528+
something to it — without creating its own `PlaceRef` edge. The `inherited_place` DSL entity
529+
in `places do` generates an Ash calculation that traverses the assignment graph at read time.
530+
531+
In our Compute example: if a `GPU` instance has a `:data_centre` place, and a `Cluster`
532+
wants to surface the data centre of its primary GPU, it can declare:
533+
534+
```elixir
535+
provider do
536+
places do
537+
# Traverses AssignmentRelationship where alias = :primary_gpu,
538+
# reads PlaceRef with role :data_centre from the source GPU instance.
539+
inherited_place :primary_data_centre, via: [:primary_gpu], source_role: :data_centre
540+
end
541+
end
542+
```
543+
544+
Load it like any other calculation:
545+
546+
```elixir
547+
cluster = Ash.load!(cluster_1, [:primary_data_centre], domain: Compute)
548+
# cluster.primary_data_centre => [%DataCentre{...}]
549+
```
550+
551+
`inherited_party` works identically for party inheritance:
552+
553+
```elixir
554+
# Cluster inherits the operating Tenant from the GPU it was assigned from
555+
provider do
556+
parties do
557+
inherited_party :operator, via: [:primary_gpu], source_role: :operator
558+
end
559+
end
560+
```
561+
562+
### Reading fields from the assignment graph
563+
564+
Three calculation modules handle common traversal patterns. All return lists.
565+
566+
**`FieldFromAssignment`** — reads a field directly from the `AssignmentRelationship`
567+
record. Use it for values that live on the relationship itself: `:value`, `:pool`,
568+
`:thing`, `:alias`.
569+
570+
```elixir
571+
# Core number assigned to this cluster under the :primary_gpu slot
572+
calculate :primary_core, {:array, :integer},
573+
{Diffo.Provider.Calculations.FieldFromAssignment, [alias: :primary_gpu, field: :value]}
574+
```
575+
576+
**`FieldViaAssignedRelationship`** — traverses assignment in reverse (cluster → GPU)
577+
and reads a field from the source instance. Use it for fields that live on the assigning
578+
resource, not the relationship.
579+
580+
```elixir
581+
# Name of the GPU holding the :primary_gpu slot on this cluster
582+
calculate :primary_gpu_name, {:array, :string},
583+
{Diffo.Provider.Calculations.FieldViaAssignedRelationship,
584+
[via: [:primary_gpu], field: :name]}
585+
```
586+
587+
**`FieldViaRelationship`** — traverses `DefinedSimpleRelationship` in the forward
588+
direction (source → target) filtered by `alias:` and/or `type:`. Use it when this
589+
instance is the *source* of a named forward relationship.
590+
591+
```elixir
592+
# Name of the downstream node this GPU provides to
593+
calculate :downstream_name, {:array, :string},
594+
{Diffo.Provider.Calculations.FieldViaRelationship,
595+
[type: :assignedTo, alias: :downstream, field: :name]}
596+
```
597+
598+
| I want… | Use |
599+
|---------|-----|
600+
| Value on the assignment record (`:value`, `:pool`) | `FieldFromAssignment` |
601+
| Field from the instance that assigned to me | `FieldViaAssignedRelationship` |
602+
| Field from an instance I have a forward relationship to | `FieldViaRelationship` |
603+
| Place/party inherited via assignment | `inherited_place` / `inherited_party` |
604+
497605
## Party Extension
498606

499607
`Diffo.Provider.BaseParty` is an Ash Resource Fragment for domain-specific Party kinds, mirroring `BaseInstance`. It provides common Party attributes — `id`, `href`, `name`, `type`, `referred_type` — and the unified `Diffo.Provider.Extension` DSL. Within `provider do`, a Party kind uses `instances do`, `parties do`, and `places do` sections to declare the roles it plays.

lib/diffo/provider/components/calculations/field_from_assignment.ex

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,34 @@
33
# SPDX-License-Identifier: MIT
44

55
defmodule Diffo.Provider.Calculations.FieldFromAssignment do
6-
@moduledoc false
6+
@moduledoc """
7+
Reads a field directly from an `AssignmentRelationship` record.
8+
9+
Filters `AssignmentRelationship` by `target_id = current.id` and returns the named
10+
field from each matching record — no second hop to the source instance. This is the
11+
right choice when you want a value that lives on the relationship itself (`:value`,
12+
`:thing`, `:pool`, `:alias`) rather than on the assigning instance.
13+
14+
Use `FieldViaAssignedRelationship` instead when you need a field from the source
15+
instance (e.g. `:name`).
16+
17+
## Options
18+
19+
- `field:` *(required)* — atom naming the field to read from the relationship record
20+
(e.g. `:value`, `:thing`, `:pool`, `:alias`).
21+
- `alias:` *(optional)* — atom matching the `alias` attribute on the relationship.
22+
When omitted, all assignments where `target_id = current.id` are included.
23+
24+
## Examples
25+
26+
# Port number assigned to this service under the :primary slot
27+
calculate :assigned_port, {:array, :integer},
28+
{Diffo.Provider.Calculations.FieldFromAssignment, [alias: :primary, field: :value]}
29+
30+
# Pool name for every assignment on this instance
31+
calculate :assignment_pools, {:array, :atom},
32+
{Diffo.Provider.Calculations.FieldFromAssignment, [field: :pool]}
33+
"""
734
use Ash.Resource.Calculation
835

936
@impl true

lib/diffo/provider/components/calculations/field_via_assigned_relationship.ex

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,32 @@
33
# SPDX-License-Identifier: MIT
44

55
defmodule Diffo.Provider.Calculations.FieldViaAssignedRelationship do
6-
@moduledoc false
6+
@moduledoc """
7+
Reads a field from the source instance of an `AssignmentRelationship`.
8+
9+
Traverses `AssignmentRelationship` in reverse — filtering by `target_id = current.id`
10+
— to reach the source instances (pool owners) that assigned something to this instance,
11+
then returns the named field from each.
12+
13+
## Options
14+
15+
- `field:` *(required)* — atom naming the field to read from the source instance
16+
(e.g. `:name`, `:type`).
17+
- `via:` *(optional)* — list of alias atoms to step through. Each step filters
18+
`AssignmentRelationship` by the alias and follows `source_id` to the next set of
19+
instances. Multi-hop is supported by chaining steps. When omitted, all assignments
20+
where `target_id = current.id` are traversed without alias filtering.
21+
22+
## Examples
23+
24+
# Name of the CVC that holds the :svlan assignment slot on this AVC
25+
calculate :cvc_id, {:array, :string},
26+
{Diffo.Provider.Calculations.FieldViaAssignedRelationship, [via: [:svlan], field: :name]}
27+
28+
# Name of every instance that has ever assigned anything to this one
29+
calculate :assigner_names, {:array, :string},
30+
{Diffo.Provider.Calculations.FieldViaAssignedRelationship, [field: :name]}
31+
"""
732
use Ash.Resource.Calculation
833

934
@impl true

lib/diffo/provider/components/calculations/field_via_relationship.ex

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,39 @@
33
# SPDX-License-Identifier: MIT
44

55
defmodule Diffo.Provider.Calculations.FieldViaRelationship do
6-
@moduledoc false
6+
@moduledoc """
7+
Reads a field from target instances reached via `DefinedSimpleRelationship`.
8+
9+
Traverses `DefinedSimpleRelationship` in the forward direction — filtering by
10+
`source_id = current.id` — and returns the named field from each resolved target
11+
instance. Both `type:` and `alias:` are optional filters; when omitted they match
12+
any value on that dimension.
13+
14+
## Options
15+
16+
- `field:` *(required)* — atom naming the field to read from the target instance
17+
(e.g. `:name`, `:type`).
18+
- `alias:` *(optional)* — atom matching the `alias` attribute on the relationship.
19+
When omitted, relationships with any alias (including nil) are included.
20+
- `type:` *(optional)* — atom matching the `type` attribute on the relationship
21+
(e.g. `:assignedTo`, `:reliesOn`). When omitted, all types are included.
22+
23+
Providing neither filter returns fields from every forward `DefinedSimpleRelationship`
24+
on this instance. In practice at least one of `alias:` or `type:` should be supplied,
25+
since a source instance typically has many forward relationships pointing to unrelated
26+
things.
27+
28+
## Examples
29+
30+
# Name of the target reached via the :provides alias
31+
calculate :provider_name, {:array, :string},
32+
{Diffo.Provider.Calculations.FieldViaRelationship, [alias: :provides, field: :name]}
33+
34+
# Name of the target reached via the :link alias, restricted to :assignedTo type
35+
calculate :assigned_linked_name, {:array, :string},
36+
{Diffo.Provider.Calculations.FieldViaRelationship,
37+
[type: :assignedTo, alias: :link, field: :name]}
38+
"""
739
use Ash.Resource.Calculation
840

941
@impl true

lib/diffo/provider/components/calculations/inherited_party.ex

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,16 @@
33
# SPDX-License-Identifier: MIT
44

55
defmodule Diffo.Provider.Calculations.InheritedParty do
6-
@moduledoc false
6+
@moduledoc """
7+
Backing calculation for `inherited_party` DSL declarations.
8+
9+
Traverses `AssignmentRelationship` by alias to reach source instances, then reads
10+
their `PartyRef` records for the declared `source_role`. Injected automatically by
11+
`TransformInheritedRefs` — do not reference this module directly; use the
12+
`inherited_party` DSL entity instead.
13+
14+
See `Diffo.Provider.Extension.InheritedPartyDeclaration` for the DSL options.
15+
"""
716
use Ash.Resource.Calculation
817

918
@impl true

lib/diffo/provider/components/calculations/inherited_place.ex

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,16 @@
33
# SPDX-License-Identifier: MIT
44

55
defmodule Diffo.Provider.Calculations.InheritedPlace do
6-
@moduledoc false
6+
@moduledoc """
7+
Backing calculation for `inherited_place` DSL declarations.
8+
9+
Traverses `AssignmentRelationship` by alias to reach source instances, then reads
10+
their `PlaceRef` records for the declared `source_role`. Injected automatically by
11+
`TransformInheritedRefs` — do not reference this module directly; use the
12+
`inherited_place` DSL entity instead.
13+
14+
See `Diffo.Provider.Extension.InheritedPlaceDeclaration` for the DSL options.
15+
"""
716
use Ash.Resource.Calculation
817

918
@impl true

lib/diffo/provider/extension/inherited_party_declaration.ex

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,32 @@
33
# SPDX-License-Identifier: MIT
44

55
defmodule Diffo.Provider.Extension.InheritedPartyDeclaration do
6-
@moduledoc "DSL entity declaring an inherited party role — derived by traversing the assignment graph"
6+
@moduledoc """
7+
DSL entity for an `inherited_party` declaration inside `parties do` on an Instance resource.
8+
9+
Generates an Ash calculation of the same name as `role` that traverses the assignment
10+
graph to inherit a party from a related source instance. The calculation is injected
11+
by `TransformInheritedRefs` at compile time — no `PartyRef` edge is created on the
12+
consuming instance itself.
13+
14+
## Fields
15+
16+
- `role` — atom; the name of the generated calculation (and the party slot name from
17+
the consumer's perspective).
18+
- `source_role` — atom; the `PartyRef` role to read from the resolved source instance
19+
(e.g. `:provider`). Required.
20+
- `via` — optional list of alias atoms for multi-hop traversal. When nil the role name
21+
is used as the single alias step (single-hop default). When provided, each step
22+
filters `AssignmentRelationship` by that alias atom before following `source_id` to
23+
the next set of instances.
24+
25+
## Example
26+
27+
parties do
28+
inherited_party :provider, source_role: :provider
29+
inherited_party :nni_owner, via: [:uplink], source_role: :owner
30+
end
31+
"""
732
defstruct [:role, :via, :source_role, __spark_metadata__: nil]
833

934
defimpl String.Chars do

lib/diffo/provider/extension/inherited_place_declaration.ex

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,32 @@
33
# SPDX-License-Identifier: MIT
44

55
defmodule Diffo.Provider.Extension.InheritedPlaceDeclaration do
6-
@moduledoc "DSL entity declaring an inherited place role — derived by traversing the assignment graph"
6+
@moduledoc """
7+
DSL entity for an `inherited_place` declaration inside `places do` on an Instance resource.
8+
9+
Generates an Ash calculation of the same name as `role` that traverses the assignment
10+
graph to inherit a place from a related source instance. The calculation is injected
11+
by `TransformInheritedRefs` at compile time — no `PlaceRef` edge is created on the
12+
consuming instance itself.
13+
14+
## Fields
15+
16+
- `role` — atom; the name of the generated calculation (and the place slot name from
17+
the consumer's perspective).
18+
- `source_role` — atom; the `PlaceRef` role to read from the resolved source instance
19+
(e.g. `:location`). Required.
20+
- `via` — optional list of alias atoms for multi-hop traversal. When nil the role name
21+
is used as the single alias step (single-hop default). When provided, each step
22+
filters `AssignmentRelationship` by that alias atom before following `source_id` to
23+
the next set of instances.
24+
25+
## Example
26+
27+
places do
28+
inherited_place :installation_site, source_role: :location
29+
inherited_place :exchange, via: [:primary, :uplink], source_role: :location
30+
end
31+
"""
732
defstruct [:role, :via, :source_role, __spark_metadata__: nil]
833

934
defimpl String.Chars do

0 commit comments

Comments
 (0)