Skip to content

Commit 5c4226e

Browse files
Merge pull request #92 from diffo-dev/86-transformers-persisters-verifiers
86 transformers persisters verifiers
2 parents 4a0d34c + 31a58e9 commit 5c4226e

36 files changed

Lines changed: 1857 additions & 440 deletions

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

Lines changed: 222 additions & 69 deletions
Large diffs are not rendered by default.

documentation/how_to/use_diffo_provider_extension.livemd

Lines changed: 45 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,12 @@ end
144144

145145
Diffo also has an inbuilt Spark DSL extension [Diffo.Provider.Instance.Extension](https://hexdocs.pm/diffo/Diffo.Provider.Instance.Extension.html) which provides DSL and functions for use in building and operating domain specific services and resources.
146146

147-
Currently it has DSL to allow you to declare specification, features, characteristics, and party roles. It can be used for services or resources.
147+
The extension has two top-level sections:
148+
149+
**`structure do`** — describes the static shape of the Instance kind: its TMF Specification, Characteristics, Features, and Party roles. All declarations are baked into the module at compile time and introspectable at runtime via generated functions (`specification/0`, `characteristics/0`, `features/0`, `parties/0`) and `Diffo.Provider.Instance.Info`.
150+
151+
**`behaviour do`** — declares which Ash actions should be wired for instance lifecycle management. Declaring `create :name` injects `:specified_by`, `:features`, and `:characteristics` arguments onto that action, and the `BuildBefore`/`BuildAfter` changes registered on `BaseInstance` automatically handle specification upsert, feature and characteristic creation, party validation, and graph relationship wiring for every create action. You write the action body for your domain-specific accepts and arguments; the structural wiring is handled for you.
152+
148153
Feature and Instance Characteristics can have payloads defined by [Ash.TypedStruct](https://hexdocs.pm/ash/Ash.TypedStruct.html). TypedStruct are DSL specified types which are effectively lightweight embedded resources. We've extended both [AshJason](https://hexdocs.pm/ash_jason/) and [AshOutstanding](https://hexdocs.pm/ash_outstanding/) to support Ash.TypedStruct.
149154

150155
For partial resource allocation and assignment we've created Diffo.Provider.Assigner. It is used by the host resource, which declares a characteristic with an Diffo.Provider.AssignableValue TypedStruct. Allocation is managed within the Provider domain using this characteristic. Assignment to Services or Resources is via 'reverse' type: "assignedTo" relationships enriched by relationship characteristics.
@@ -172,7 +177,6 @@ defmodule Diffo.Compute.Cluster do
172177
alias Diffo.Provider.BaseInstance
173178
alias Diffo.Provider.Instance.Relationship
174179
alias Diffo.Provider.Instance.Characteristic
175-
alias Diffo.Provider.Instance.ActionHelper
176180
alias Diffo.Compute
177181
alias Diffo.Compute.ClusterValue
178182
alias Diffo.Compute.Tenant
@@ -187,42 +191,40 @@ defmodule Diffo.Compute.Cluster do
187191
plural_name :Clusters
188192
end
189193

190-
specification do
191-
id "4bcfc4c9-e776-4878-a658-e8d81857bed7"
192-
name "cluster"
193-
type :resourceSpecification
194-
description "A Cluster Resource Instance"
195-
category "Network Resource"
196-
end
194+
structure do
195+
specification do
196+
id "4bcfc4c9-e776-4878-a658-e8d81857bed7"
197+
name "cluster"
198+
type :resourceSpecification
199+
description "A Cluster Resource Instance"
200+
category "Network Resource"
201+
end
202+
203+
characteristics do
204+
characteristic :cluster, ClusterValue
205+
end
197206

198-
characteristics do
199-
characteristic :cluster, ClusterValue
207+
parties do
208+
party :operator, Tenant
209+
party :manager, Engineer
210+
end
200211
end
201212

202-
parties do
203-
party :operator, Tenant
204-
party :manager, Engineer
213+
behaviour do
214+
actions do
215+
create :build
216+
end
205217
end
206218

207219
actions do
208220
create :build do
209221
description "creates a new Cluster resource instance for build"
210222
accept [:id, :name, :type, :which]
211-
argument :specified_by, :uuid, public?: false
212223
argument :relationships, {:array, :struct}
213-
argument :features, {:array, :uuid}, public?: false
214-
argument :characteristics, {:array, :uuid}, public?: false
215224
argument :places, {:array, :struct}
216225
argument :parties, {:array, :struct}
217226

218227
change set_attribute(:type, :resource)
219-
220-
change before_action(fn changeset, _context -> ActionHelper.build_before(changeset) end)
221-
222-
change after_action(fn changeset, result, _context ->
223-
ActionHelper.build_after(changeset, result, Compute, :get_cluster_by_id)
224-
end)
225-
226228
change load [:href]
227229
upsert? false
228230
end
@@ -232,7 +234,7 @@ defmodule Diffo.Compute.Cluster do
232234
argument :characteristic_value_updates, {:array, :term}
233235

234236
change after_action(fn changeset, result, _context ->
235-
with {:ok, _result} <- Characteristic.update_values(result, changeset),
237+
with {:ok, result} <- Characteristic.update_values(result, changeset),
236238
{:ok, cluster} <- Compute.get_cluster_by_id(result.id),
237239
do: {:ok, cluster}
238240
end)
@@ -305,7 +307,6 @@ defmodule Diffo.Compute.GPU do
305307
alias Diffo.Provider.BaseInstance
306308
alias Diffo.Provider.Instance.Relationship
307309
alias Diffo.Provider.Instance.Characteristic
308-
alias Diffo.Provider.Instance.ActionHelper
309310
alias Diffo.Provider.Assigner
310311
alias Diffo.Provider.Assignment
311312
alias Diffo.Provider.AssignableValue
@@ -321,38 +322,36 @@ defmodule Diffo.Compute.GPU do
321322
plural_name :gpus
322323
end
323324

324-
specification do
325-
id "ad50073f-17e0-45cb-b9b1-aa4296876156"
326-
name "gpu"
327-
type :resourceSpecification
328-
description "A GPU Resource Instance"
329-
category "Network Resource"
325+
structure do
326+
specification do
327+
id "ad50073f-17e0-45cb-b9b1-aa4296876156"
328+
name "gpu"
329+
type :resourceSpecification
330+
description "A GPU Resource Instance"
331+
category "Network Resource"
332+
end
333+
334+
characteristics do
335+
characteristic :gpu, GPUValue
336+
characteristic :cores, AssignableValue
337+
end
330338
end
331339

332-
characteristics do
333-
characteristic :gpu, GPUValue
334-
characteristic :cores, AssignableValue
340+
behaviour do
341+
actions do
342+
create :build
343+
end
335344
end
336345

337346
actions do
338347
create :build do
339348
description "creates a new GPU resource instance for build"
340349
accept [:id, :name, :type, :which]
341-
argument :specified_by, :uuid, public?: false
342350
argument :relationships, {:array, :struct}
343-
argument :features, {:array, :uuid}, public?: false
344-
argument :characteristics, {:array, :uuid}, public?: false
345351
argument :places, {:array, :struct}
346352
argument :parties, {:array, :struct}
347353

348354
change set_attribute(:type, :resource)
349-
350-
change before_action(fn changeset, _context -> ActionHelper.build_before(changeset) end)
351-
352-
change after_action(fn changeset, result, _context ->
353-
ActionHelper.build_after(changeset, result, Compute, :get_gpu_by_id)
354-
end)
355-
356355
change load [:href]
357356
upsert? false
358357
end
@@ -685,7 +684,7 @@ What happens when I request a specific assignment from an instance to which the
685684

686685
In this tutorial you've used Diffo's Provider Instance Extension to define a Compute domain with a composite Cluster resource comprised of assigned GPU cores, and the Provider Party Extension to define Tenant and Engineer party kinds that operate and manage those resources.
687686

688-
The BaseParty fragment follows the same pattern as BaseInstance — domain-specific resources use it as a fragment and finish their actions with a domain-scoped reload to pick up extended fields.
687+
`BaseParty` follows the same pattern as `BaseInstance` — domain-specific party resources use it as a fragment and write their own `build` action for domain-specific attributes. No manual wiring is needed.
689688

690689
A `BasePlace` extension for domain-specific Place kinds (such as a DataCentre with its own attributes) follows the same pattern and will be added in a future release.
691690

lib/diffo/provider/components/base_instance.ex

Lines changed: 89 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,30 @@
44

55
defmodule Diffo.Provider.BaseInstance do
66
@moduledoc """
7-
Ash Resource Fragment which is a the point of extension for your TMF Service or Resource Instance
7+
Ash Resource Fragment which is the point of extension for your TMF Service or Resource Instance.
88
99
`BaseInstance` is the foundation for domain-specific Service and Resource kinds.
1010
Include it as a fragment on an `Ash.Resource` to get common Instance attributes,
1111
Neo4j graph wiring, state machine, and the `Diffo.Provider.Instance.Extension` DSL.
1212
1313
## Instance Extension DSL
1414
15-
The `Diffo.Provider.Instance.Extension` DSL provides compile-time declaration blocks
16-
for describing the shape of a domain-specific Service or Resource.
15+
The DSL has two top-level sections: `structure do` describes what the instance kind is;
16+
`behaviour do` wires it to Ash actions.
1717
18-
`specification do` — declares the TMF Specification for this Instance kind.
18+
### structure
1919
20-
`features do` — declares the Features this Instance kind may have, each optionally
21-
carrying a typed characteristic payload.
20+
`specification do` — declares the TMF Specification for this Instance kind (id, name, type,
21+
major_version, description, category).
2222
23-
`characteristics do` — declares the top-level Characteristics of this Instance kind,
24-
each backed by an `Ash.TypedStruct`.
23+
`characteristics do` — declares the top-level Characteristics of this Instance kind, each
24+
backed by an `Ash.TypedStruct`.
25+
26+
`features do` — declares the Features this Instance kind may have, each optionally carrying
27+
its own typed characteristic payload.
2528
2629
`parties do` — declares the Party roles this Instance kind relates to. Role names are
27-
domain-specific nouns describing what the party is to the instance. Two forms:
30+
domain-specific nouns describing what the party means to the instance. Two forms:
2831
2932
parties do
3033
party :provider, MyApp.Provider, calculate: :provider_calculation
@@ -33,12 +36,44 @@ defmodule Diffo.Provider.BaseInstance do
3336
party :owner, MyApp.InfrastructureCo, reference: true
3437
end
3538
36-
- `party` — singular (at most one party in this role)
37-
- `parties` — plural (unbounded, or bounded with `constraints:`)
39+
- `party` — singular (at most one party in this role per instance)
40+
- `parties` — plural (unbounded, or bounded with `constraints: [min: n, max: m]`)
3841
- `reference: true` — no direct `PartyRef` edge; party is reachable by graph traversal
3942
- `calculate:` — names an Ash calculation on this resource that produces the party at build time
4043
41-
All declarations are introspectable via `Diffo.Provider.Instance.Extension.Info`.
44+
All declarations are introspectable at runtime via `Diffo.Provider.Instance.Info` and at
45+
compile time via `Diffo.Provider.Instance.Extension.Info`.
46+
47+
### behaviour
48+
49+
`behaviour do actions do create :name end end` — marks a named create action for build
50+
wiring. This injects `:specified_by`, `:features`, and `:characteristics` arguments onto
51+
that action so Ash accepts the values that `build_before/1` sets automatically.
52+
53+
You still write the action body yourself for domain-specific accepts, arguments, and changes.
54+
The build arguments are not public and do not need to appear in `accept`.
55+
56+
## Generated functions
57+
58+
Every resource using `BaseInstance` with a `specification do` gets the following functions
59+
generated at compile time:
60+
61+
- `specification/0` — the specification keyword list baked at compile time
62+
- `characteristics/0` — list of `Characteristic` structs
63+
- `features/0` — list of `Feature` structs
64+
- `parties/0` — list of `PartyDeclaration` structs
65+
- `characteristic/1` — returns the named `Characteristic` or `nil`
66+
- `feature/1` — returns the named `Feature` or `nil`
67+
- `feature_characteristic/2` — returns the named characteristic within a feature, or `nil`
68+
- `party/1` — returns the `PartyDeclaration` for the given role, or `nil`
69+
- `build_before/1` — called automatically before every create action; upserts the
70+
specification and creates features, characteristics, and parties, setting their ids
71+
as action arguments
72+
- `build_after/2` — called automatically after every create action; relates the created
73+
TMF entities to the new instance node
74+
75+
Resources without a `specification do id` get trivial passthroughs for `build_before/1`
76+
and `build_after/2`.
4277
4378
## Usage
4479
@@ -50,28 +85,50 @@ defmodule Diffo.Provider.BaseInstance do
5085
plural_name :clusters
5186
end
5287
53-
specification do
54-
id "4bcfc4c9-e776-4878-a658-e8d81857bed7"
55-
name "cluster"
56-
type :resourceSpecification
88+
structure do
89+
specification do
90+
id "4bcfc4c9-e776-4878-a658-e8d81857bed7"
91+
name "cluster"
92+
type :resourceSpecification
93+
end
94+
95+
parties do
96+
party :operator, MyApp.Organization
97+
parties :installer, MyApp.Engineer
98+
end
99+
end
100+
101+
behaviour do
102+
actions do
103+
create :build
104+
end
57105
end
58106
59-
parties do
60-
party :operator, MyApp.Organization
61-
parties :installer, MyApp.Engineer
107+
actions do
108+
create :build do
109+
description "creates a new Cluster resource instance"
110+
accept [:id, :name, :type, :which]
111+
argument :relationships, {:array, :struct}
112+
argument :parties, {:array, :struct}
113+
114+
change set_attribute(:type, :resource)
115+
change load [:href]
116+
upsert? false
117+
end
62118
end
63119
end
64120
65-
## Action pattern
121+
## Rolling your own actions
66122
67-
Domain-specific Instance resources should finish their `build` action with a reload via
68-
their own domain's `get_xxx_by_id` to pick up extended fields:
123+
The `behaviour do actions do create :name end end` declaration is optional. Omitting it
124+
means the `:specified_by`, `:features`, and `:characteristics` arguments are not declared
125+
on that action — but `build_before/1` and `build_after/2` are still called for every
126+
create via the global `BuildBefore` and `BuildAfter` changes registered on `BaseInstance`.
69127
70-
create :build do
71-
change after_action(fn changeset, result, _context ->
72-
ActionHelper.build_after(changeset, result, MyApp.Domain, :get_cluster_by_id)
73-
end)
74-
end
128+
If you have a create action that should NOT trigger the full build wiring (e.g. a
129+
lightweight admin create), you can override `build_before/1` or `build_after/2` on your
130+
resource, or use Ash's `skip_unknown_inputs` to absorb the injected arguments without
131+
declaring them.
75132
"""
76133
use Spark.Dsl.Fragment,
77134
of: Ash.Resource,
@@ -357,6 +414,11 @@ defmodule Diffo.Provider.BaseInstance do
357414
end
358415
end
359416

417+
changes do
418+
change Diffo.Provider.Instance.Extension.Changes.BuildBefore, on: [:create]
419+
change Diffo.Provider.Instance.Extension.Changes.BuildAfter, on: [:create]
420+
end
421+
360422
actions do
361423
defaults [:destroy]
362424

lib/diffo/provider/components/entity_ref.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
defmodule Diffo.Provider.EntityRef do
66
@moduledoc """
7-
EntityRef - Ash Resource for a TMF Entity Reference
7+
Ash Resource for a TMF Entity Reference
88
"""
99
use Ash.Resource,
1010
otp_app: :diffo,

0 commit comments

Comments
 (0)