Skip to content

Commit 141dcb6

Browse files
matt-beanlandclaude
andcommitted
Instance DSL parties — multiplicity, validation, and enforcement
Adds `parties do` DSL to the Instance Extension, allowing resources to declare which party roles they accept, with optional min/max multiplicity constraints. Runtime enforcement is applied in `build_before` via `Party.validate_parties/1`, checking roles against declarations and rejecting builds that violate constraints. Renames Party Extension DSL sections from singular (`instance`/`party`) to plural (`instances`/`parties`) for consistency. Replaces `.license` sidecar files with `REUSE.toml`. Fixes ExDoc sidebar by removing a duplicate `docs:` key in mix.exs that shadowed the DSL extras config. Adds `Assignment.compare/2` so `Enum.sort/2` works in the Assigner. Updates all three livebooks to use the local path dep and cleans up stale commented-out version pins. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5197a37 commit 141dcb6

96 files changed

Lines changed: 1283 additions & 1321 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore.license

Lines changed: 0 additions & 3 deletions
This file was deleted.

.tool-versions.license

Lines changed: 0 additions & 3 deletions
This file was deleted.

REUSE.toml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
version = 1
2+
3+
[[annotations]]
4+
path = [
5+
".gitignore",
6+
".tool-versions",
7+
"mix.lock",
8+
"logos/diffo.jpg",
9+
"documentation/dsls/**",
10+
]
11+
SPDX-FileCopyrightText = "2025 diffo contributors <https://github.com/diffo-dev/diffo/graphs.contributors>"
12+
SPDX-License-Identifier = "MIT"

diffo.livemd

Lines changed: 46 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<!--
1+
<!--
22
SPDX-FileCopyrightText: 2025 diffo contributors <https://github.com/diffo-dev/diffo/graphs.contributors>
33
44
SPDX-License-Identifier: MIT
@@ -9,7 +9,7 @@ SPDX-License-Identifier: MIT
99
```elixir
1010
Mix.install(
1111
[
12-
{:diffo, "~> 0.2.0"}
12+
{:diffo, path: "/Users/Beanlanda/git/diffo"}
1313
],
1414
consolidate_protocols: false
1515
)
@@ -113,10 +113,41 @@ alias Diffo.Provider.PartyRef
113113
alias Diffo.Provider.Place
114114
alias Diffo.Provider.PartyRef
115115
alias Diffo.Uuid
116+
alias Diffo.Type.Value
116117
import Jason, only: [encode: 2]
117118
use Outstand
118119
```
119120

121+
The value types used in this livebook are defined without the Provider Extension DSL — each requires a dedicated `Ash.TypedStruct` so that `Diffo.Type.Dynamic` can round-trip values through Neo4j storage.
122+
123+
```elixir
124+
defmodule Diffo.Livebook.E2eValue do
125+
@derive Jason.Encoder
126+
use Ash.TypedStruct
127+
typed_struct do
128+
field :downstream, :integer
129+
field :upstream, :integer
130+
field :units, :atom
131+
end
132+
end
133+
134+
defmodule Diffo.Livebook.OptionsValue do
135+
@derive Jason.Encoder
136+
use Ash.TypedStruct
137+
typed_struct do
138+
field :options, {:array, :atom}
139+
end
140+
end
141+
142+
defmodule Diffo.Livebook.TechnologyValue do
143+
@derive Jason.Encoder
144+
use Ash.TypedStruct
145+
typed_struct do
146+
field :access, :atom
147+
end
148+
end
149+
```
150+
120151
We can either create specification instances with Ash directly, or use the Diffo.Provider code interface.
121152

122153
```elixir
@@ -173,13 +204,13 @@ individual =
173204
Diffo.Provider.create_party!(%{
174205
id: "IND000000897354",
175206
name: :individualId,
176-
referredType: :Individual
207+
referred_type: :Individual
177208
})
178209
org =
179210
Diffo.Provider.create_party!(%{
180211
id: "ORG000000123456",
181212
name: :organizationId,
182-
referredType: :Organization
213+
referred_type: :Organization
183214
})
184215
parties = [individual, org]
185216
Jason.encode!(parties, pretty: true) |> IO.puts
@@ -197,7 +228,7 @@ Jason.encode!(parties, pretty: true) |> IO.puts
197228
We'll also add a CustomerSite place where the service is to be delivered to the customer. Historically this is the Z-end.
198229

199230
```elixir
200-
z_end = Provider.create_place!(%{id: "1657363", name: :addressId, href: "place/telco/1657363", referredType: :GeographicAddress})
231+
z_end = Provider.create_place!(%{id: "1657363", name: :addressId, href: "place/telco/1657363", referred_type: :GeographicAddress})
201232
Jason.encode!(z_end, pretty: true) |> IO.puts
202233
```
203234

@@ -230,7 +261,7 @@ First we'll create a backup feature to indicate that we want mobile backup, and
230261
We'll create an e2e 'instance' characteristic to detail our bandwidth and latency requirements:
231262

232263
```elixir
233-
e2e = Provider.create_characteristic!(%{type: :instance, name: :e2e, value: %{bandwidth: %{downstream: 250, upstream: 25, units: :Mbps}}})
264+
e2e = Provider.create_characteristic!(%{type: :instance, name: :e2e, value: Value.dynamic(%Diffo.Livebook.E2eValue{downstream: 250, upstream: 25, units: :Mbps})})
234265
broadband_0001 = broadband_0001
235266
|> Provider.relate_instance_characteristics!(%{characteristics: [e2e.id]})
236267
broadband_0001 |> Jason.encode!(pretty: true) |> IO.puts
@@ -240,7 +271,7 @@ We'll create mobile backup and device management features. The device management
240271

241272
```elixir
242273
backup = Provider.create_feature!(%{name: :backup, isEnabled: true})
243-
options = Provider.create_characteristic!(%{type: :feature, name: :options, value: [:updates, :monitoring]})
274+
options = Provider.create_characteristic!(%{type: :feature, name: :options, value: Value.dynamic(%Diffo.Livebook.OptionsValue{options: [:updates, :monitoring]})})
244275
device_management = Provider.create_feature!(%{name: :deviceManagement, isEnabled: true, characteristics: [options.id]})
245276
broadband_0001 = broadband_0001
246277
|> Provider.relate_instance_features!(%{features: [backup.id, device_management.id]})
@@ -305,13 +336,13 @@ We'll have a few things outstanding which we would normally find out during serv
305336
We recommend using outstanding to drive next task logic, so that the orchestration is directed by the difference engine. This could look like an address lookup (where we learn the provider) followed by a provider service qualification (where we learn the technology) and the related network places.
306337

307338
```elixir
308-
nbn = Provider.create_party!(%{id: :nbn, name: "NBNCo", referredType: :Organization})
339+
nbn = Provider.create_party!(%{id: "nbn", name: "NBNCo", referred_type: :Organization})
309340
nbn_party_ref = Provider.create_party_ref!(%{instance_id: broadband_0001.id, party_id: nbn.id, role: :Provider})
310-
provider_z_end = Provider.create_place!(%{id: "LOC000000899353", name: "locationId", href: "place/nbnco/LOC000000899353",referredType: :GeographicAddress})
341+
provider_z_end = Provider.create_place!(%{id: "LOC000000899353", name: "locationId", href: "place/nbnco/LOC000000899353", referred_type: :GeographicAddress})
311342
provider_z_end_place_ref = Provider.create_place_ref!(%{instance_id: broadband_0001.id, role: :CustomerSite, place_id: provider_z_end.id})
312-
csa = Provider.create_place!(%{id: "CSA200000000685", name: "csaId", href: "place/nbnco/CSA200000000685", referredType: :GeographicLocation})
343+
csa = Provider.create_place!(%{id: "CSA200000000685", name: "csaId", href: "place/nbnco/CSA200000000685", referred_type: :GeographicLocation})
313344
csa_place_ref = Provider.create_place_ref!(%{instance_id: broadband_0001.id, role: :ServingArea, place_id: csa.id})
314-
poi = Provider.create_place!(%{id: "2CAR", name: "poiId", href: "place/nbnco/2CAR",referredType: :GeographicSite})
345+
poi = Provider.create_place!(%{id: "2CAR", name: "poiId", href: "place/nbnco/2CAR", referred_type: :GeographicSite})
315346
poi_place_ref = Provider.create_place_ref!(%{instance_id: broadband_0001.id, role: :AccessNNI, place_id: poi.id})
316347

317348
places = Diffo.Provider.list_place_refs!()
@@ -331,7 +362,7 @@ outstanding = expected_instance --- broadband_0001
331362
We create and add the technology characteristic to the actual service. This should resolve the characteristic expectation.
332363

333364
```elixir
334-
technology = Provider.create_characteristic!(%{type: :instance, name: :technology, value: %{access: :nbnEthernet}})
365+
technology = Provider.create_characteristic!(%{type: :instance, name: :technology, value: Value.dynamic(%Diffo.Livebook.TechnologyValue{access: :nbnEthernet})})
335366
broadband_0001 |> Provider.relate_instance_characteristics!(%{characteristics: [technology.id]})
336367

337368

@@ -358,6 +389,7 @@ Given that our feasibilityCheck above was complete we want to set the :feasibili
358389

359390
```elixir
360391
broadband_0001 = broadband_0001 |> Provider.feasibilityCheck_service!(%{service_operating_status: :feasible})
392+
broadband_0001 = Provider.get_instance_by_id!(broadband_0001.id)
361393
broadband_0001 |> Jason.encode!(pretty: true) |> IO.puts
362394
```
363395

@@ -450,4 +482,6 @@ broadband_0001 |> Jason.encode!(pretty: true) |> IO.puts
450482
In this tutorial you've used Diffo to create, relate and update some TMF Service and Resources, simulating activities over the service and resource lifecycle, and you've learned how this functionality is underpinned by open source Neo4j, Ash Framework and Elixir.
451483
Diffo contains an example Access domain showing how to specialise the structure and behaviour of TMF Services and Resources.
452484

485+
But there is a lot of friction here and we still only have structure and not much behaviour. In practice, you'd use the Provider Extension DSLs to do the heavy lifting. But what you've learned here is foundational to help you move on to the other livebooks.
486+
453487
If you find Diffo useful please visit and star on [github](https://github.com/diffo-dev/diffo/). Feel free to join discussions and raise issues to discuss PR's.

documentation/dsls/DSL-Diffo.Provider.Instance.Extension.md

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@ This file was generated by Spark. Do not edit it by hand.
33
-->
44
# Diffo.Provider.Instance.Extension
55

6-
DSL Extension customising an Instance
6+
DSL Extension customising an Instance.
7+
8+
Provides compile-time declaration blocks for domain-specific Service and Resource kinds
9+
built on `Diffo.Provider.BaseInstance`. All declarations are introspectable via
10+
`Diffo.Provider.Instance.Extension.Info`.
11+
12+
See the [DSL cheat sheet](DSL-Diffo.Provider.Instance.Extension.html) for the full DSL reference.
13+
See `Diffo.Provider.BaseInstance` for full usage documentation.
714

815

916
## specification
@@ -117,16 +124,10 @@ Adds a Characteristic
117124

118125

119126

120-
### Introspection
121-
122-
Target: `Diffo.Provider.Instance.Characteristic`
123-
124127

125128

126129

127-
### Introspection
128130

129-
Target: `Diffo.Provider.Instance.Feature`
130131

131132

132133

@@ -176,9 +177,6 @@ Adds a Characteristic
176177

177178

178179

179-
### Introspection
180-
181-
Target: `Diffo.Provider.Instance.Characteristic`
182180

183181

184182

@@ -188,13 +186,15 @@ List of Instance Party roles
188186

189187
### Nested DSLs
190188
* [party](#parties-party)
189+
* [parties](#parties-parties)
191190

192191

193192
### Examples
194193
```
195194
parties do
196-
party :facilitated_by, MyApp.Rsp
197-
party :overseen_by, MyApp.Person
195+
party :provider, MyApp.Provider, calculate: :provider_calculation
196+
parties :technician, MyApp.Technician, constraints: [min: 1, max: 3]
197+
party :owner, MyApp.InfrastructureCo, reference: true
198198
end
199199
200200
```
@@ -208,7 +208,7 @@ party role, party_type
208208
```
209209

210210

211-
Declares a party role on this Instance
211+
Declares a singular party role on this Instance
212212

213213

214214

@@ -220,7 +220,46 @@ Declares a party role on this Instance
220220
|------|------|---------|------|
221221
| [`role`](#parties-party-role){: #parties-party-role .spark-required} | `atom` | | The role name, an atom |
222222
| [`party_type`](#parties-party-party_type){: #parties-party-party_type } | `any` | | The module of the Party kind. An atom module name such as a BaseParty-derived resource. |
223+
### Options
223224

225+
| Name | Type | Default | Docs |
226+
|------|------|---------|------|
227+
| [`reference`](#parties-party-reference){: #parties-party-reference } | `boolean` | `false` | If true, no direct PartyRef edge is created; the party is reachable by graph traversal. |
228+
| [`calculate`](#parties-party-calculate){: #parties-party-calculate } | `atom` | | Name of an Ash calculation on this resource that produces the party at build time. |
229+
230+
231+
232+
233+
234+
### Introspection
235+
236+
Target: `Diffo.Provider.Instance.Extension.PartyDeclaration`
237+
238+
### parties.parties
239+
```elixir
240+
parties role, party_type
241+
```
242+
243+
244+
Declares a plural party role on this Instance
245+
246+
247+
248+
249+
250+
### Arguments
251+
252+
| Name | Type | Default | Docs |
253+
|------|------|---------|------|
254+
| [`role`](#parties-parties-role){: #parties-parties-role .spark-required} | `atom` | | The role name, an atom |
255+
| [`party_type`](#parties-parties-party_type){: #parties-parties-party_type } | `any` | | The module of the Party kind. An atom module name such as a BaseParty-derived resource. |
256+
### Options
257+
258+
| Name | Type | Default | Docs |
259+
|------|------|---------|------|
260+
| [`reference`](#parties-parties-reference){: #parties-parties-reference } | `boolean` | `false` | If true, no direct PartyRef edge is created; the party is reachable by graph traversal. |
261+
| [`calculate`](#parties-parties-calculate){: #parties-parties-calculate } | `atom` | | Name of an Ash calculation on this resource that produces the party at build time. |
262+
| [`constraints`](#parties-parties-constraints){: #parties-parties-constraints } | `keyword` | | Multiplicity constraints on the number of parties in this role, e.g. [min: 1, max: 3] |
224263

225264

226265

documentation/dsls/DSL-Diffo.Provider.Instance.Extension.md.license

Lines changed: 0 additions & 3 deletions
This file was deleted.

documentation/dsls/DSL-Diffo.Provider.Party.Extension.md

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,25 @@ This file was generated by Spark. Do not edit it by hand.
33
-->
44
# Diffo.Provider.Party.Extension
55

6-
DSL Extension customising a Party
6+
DSL Extension customising a Party.
77

8+
Provides compile-time declaration blocks for domain-specific Party kinds
9+
built on `Diffo.Provider.BaseParty`. All declarations are introspectable via
10+
`Diffo.Provider.Party.Extension.Info`.
811

9-
## instance
12+
See the [DSL cheat sheet](DSL-Diffo.Provider.Party.Extension.html) for the full DSL reference.
13+
14+
15+
## instances
1016
Declares the roles this Party kind plays with respect to Instances
1117

1218
### Nested DSLs
13-
* [role](#instance-role)
19+
* [role](#instances-role)
1420

1521

1622
### Examples
1723
```
18-
instance do
24+
instances do
1925
role :facilitates, MyApp.AccessService
2026
end
2127
@@ -24,7 +30,7 @@ end
2430

2531

2632

27-
### instance.role
33+
### instances.role
2834
```elixir
2935
role role, party_type
3036
```
@@ -40,8 +46,8 @@ Declares a role this Party kind plays
4046

4147
| Name | Type | Default | Docs |
4248
|------|------|---------|------|
43-
| [`role`](#instance-role-role){: #instance-role-role .spark-required} | `atom` | | The role name, an atom |
44-
| [`party_type`](#instance-role-party_type){: #instance-role-party_type } | `any` | | The module of the related resource |
49+
| [`role`](#instances-role-role){: #instances-role-role .spark-required} | `atom` | | The role name, an atom |
50+
| [`party_type`](#instances-role-party_type){: #instances-role-party_type } | `any` | | The module of the related resource |
4551

4652

4753

@@ -55,16 +61,16 @@ Target: `Diffo.Provider.Party.Extension.InstanceRole`
5561

5662

5763

58-
## party
64+
## parties
5965
Declares the roles this Party kind plays with respect to other Parties
6066

6167
### Nested DSLs
62-
* [role](#party-role)
68+
* [role](#parties-role)
6369

6470

6571
### Examples
6672
```
67-
party do
73+
parties do
6874
role :managed_by, MyApp.Person
6975
end
7076
@@ -73,7 +79,7 @@ end
7379

7480

7581

76-
### party.role
82+
### parties.role
7783
```elixir
7884
role role, party_type
7985
```
@@ -89,8 +95,8 @@ Declares a role this Party kind plays with respect to other Parties
8995

9096
| Name | Type | Default | Docs |
9197
|------|------|---------|------|
92-
| [`role`](#party-role-role){: #party-role-role .spark-required} | `atom` | | The role name, an atom |
93-
| [`party_type`](#party-role-party_type){: #party-role-party_type } | `any` | | The module of the related Party kind |
98+
| [`role`](#parties-role-role){: #parties-role-role .spark-required} | `atom` | | The role name, an atom |
99+
| [`party_type`](#parties-role-party_type){: #parties-role-party_type } | `any` | | The module of the related Party kind |
94100

95101

96102

documentation/dsls/DSL-Diffo.Provider.Party.Extension.md.license

Lines changed: 0 additions & 3 deletions
This file was deleted.

0 commit comments

Comments
 (0)