Skip to content

Commit 2308991

Browse files
committed
Place DSL — BasePlace fragment, Place/Party/Instance Extension sections, persisters, verifiers, and tests
Extracts BasePlace as a Spark.Dsl.Fragment following the BaseParty pattern. Adds instances/parties/places DSL sections to Place.Extension and Party.Extension, with persisters baking role declarations onto resources at compile time and a VerifyRoles verifier checking for duplicates and correct base types across all sections. Adds places do to Instance.Extension structure do, with PersistPlaces and place/1 generated via TransformBehaviour. Four test fixtures illustrate the simple and complex patterns for both Party and Place. Moduledocs across BaseInstance, BaseParty, and BasePlace now document the domain-specific attributes contract explicitly. Livebook updated with a Place Extension section and simplified cluster creation using the domain API.
1 parent 115fbfd commit 2308991

24 files changed

Lines changed: 1310 additions & 9 deletions

lib/diffo/provider/components/base_party.ex

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,32 @@ defmodule Diffo.Provider.BaseParty do
7676
end
7777
end
7878
79+
## Domain-specific attributes
80+
81+
Add Ash `attribute` declarations directly to your derived resource for any fields beyond the
82+
base set. Those attributes can only be set via actions you declare on the derived resource —
83+
the base `create` action provided by `BaseParty` only accepts the base fields (`id`, `href`,
84+
`name`, `type`, `referred_type`). Use your domain API to call the derived resource's action:
85+
86+
defmodule MyApp.Carrier do
87+
use Ash.Resource, fragments: [BaseParty], domain: MyApp.Domain
88+
89+
attributes do
90+
attribute :abn, :string, public?: true
91+
attribute :carrier_code, :string, public?: true
92+
end
93+
94+
actions do
95+
create :build do
96+
accept [:id, :href, :name, :abn, :carrier_code]
97+
change set_attribute(:type, :Organization)
98+
end
99+
end
100+
end
101+
102+
# Use the domain API — Provider.create_party!/1 does not know about :abn
103+
MyApp.Domain.create_carrier!(%{name: "Acme", abn: "51824753556", carrier_code: "ACM"})
104+
79105
## TMF type and referred_type
80106
81107
The `type` and `referred_type` attributes map to the TMF `@type` and `@referredType` JSON

lib/diffo/provider/components/base_place.ex

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,32 @@ defmodule Diffo.Provider.BasePlace do
5353
end
5454
end
5555
56+
## Domain-specific attributes
57+
58+
Add Ash `attribute` declarations directly to your derived resource for any fields beyond the
59+
base set. Those attributes can only be set via actions you declare on the derived resource —
60+
the base `create` action provided by `BasePlace` only accepts the base fields (`id`, `href`,
61+
`name`, `type`, `referred_type`). Use your domain API to call the derived resource's action:
62+
63+
defmodule MyApp.DataCentre do
64+
use Ash.Resource, fragments: [BasePlace], domain: MyApp.Domain
65+
66+
attributes do
67+
attribute :tier, :integer, public?: true
68+
attribute :power_capacity_kw, :integer, public?: true
69+
end
70+
71+
actions do
72+
create :build do
73+
accept [:id, :href, :name, :tier, :power_capacity_kw]
74+
change set_attribute(:type, :GeographicSite)
75+
end
76+
end
77+
end
78+
79+
# Use the domain API — Provider.create_place!/1 does not know about :tier
80+
MyApp.Domain.create_data_centre!(%{name: "M2", tier: 3, power_capacity_kw: 40_000})
81+
5682
## TMF type and referred_type
5783
5884
The `type` and `referred_type` attributes map to the TMF `@type` and `@referredType` JSON

lib/diffo/provider/components/party/extension.ex

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,5 +106,13 @@ defmodule Diffo.Provider.Party.Extension do
106106
}
107107

108108
use Spark.Dsl.Extension,
109-
sections: [@instances, @parties, @places]
109+
sections: [@instances, @parties, @places],
110+
persisters: [
111+
Diffo.Provider.Party.Extension.Persisters.PersistInstances,
112+
Diffo.Provider.Party.Extension.Persisters.PersistParties,
113+
Diffo.Provider.Party.Extension.Persisters.PersistPlaces
114+
],
115+
verifiers: [
116+
Diffo.Provider.Party.Extension.Verifiers.VerifyRoles
117+
]
110118
end
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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.Party.Extension.Persisters.PersistInstances do
6+
@moduledoc "Persists instance role declarations and bakes instances/0"
7+
use Spark.Dsl.Transformer
8+
alias Spark.Dsl.Transformer
9+
10+
@impl true
11+
def transform(dsl_state) do
12+
declarations = Transformer.get_entities(dsl_state, [:instances])
13+
escaped = Macro.escape(declarations)
14+
dsl_state = Transformer.persist(dsl_state, :instances, declarations)
15+
16+
{:ok, Transformer.eval(dsl_state, [], quote do
17+
@doc false
18+
def instances, do: unquote(escaped)
19+
end)}
20+
end
21+
end
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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.Party.Extension.Persisters.PersistParties do
6+
@moduledoc "Persists party role declarations and bakes parties/0"
7+
use Spark.Dsl.Transformer
8+
alias Spark.Dsl.Transformer
9+
10+
@impl true
11+
def transform(dsl_state) do
12+
declarations = Transformer.get_entities(dsl_state, [:parties])
13+
escaped = Macro.escape(declarations)
14+
dsl_state = Transformer.persist(dsl_state, :parties, declarations)
15+
16+
{:ok, Transformer.eval(dsl_state, [], quote do
17+
@doc false
18+
def parties, do: unquote(escaped)
19+
end)}
20+
end
21+
end
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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.Party.Extension.Persisters.PersistPlaces do
6+
@moduledoc "Persists place role declarations and bakes places/0"
7+
use Spark.Dsl.Transformer
8+
alias Spark.Dsl.Transformer
9+
10+
@impl true
11+
def transform(dsl_state) do
12+
declarations = Transformer.get_entities(dsl_state, [:places])
13+
escaped = Macro.escape(declarations)
14+
dsl_state = Transformer.persist(dsl_state, :places, declarations)
15+
16+
{:ok, Transformer.eval(dsl_state, [], quote do
17+
@doc false
18+
def places, do: unquote(escaped)
19+
end)}
20+
end
21+
end
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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.Party.Extension.Verifiers.VerifyRoles do
6+
@moduledoc "Verifies role declarations across instances, parties, and places sections"
7+
use Spark.Dsl.Verifier
8+
9+
alias Spark.Dsl.Verifier
10+
alias Spark.Error.DslError
11+
alias Diffo.Provider.Instance.Extension.Info, as: InstanceInfo
12+
alias Diffo.Provider.Party.Extension.Info, as: PartyInfo
13+
alias Diffo.Provider.Place.Extension.Info, as: PlaceInfo
14+
15+
@impl true
16+
def verify(dsl_state) do
17+
resource = Verifier.get_persisted(dsl_state, :module)
18+
19+
errors =
20+
check_section(dsl_state, [:instances], :party_type, &InstanceInfo.instance?/1,
21+
"instances", "instance_type", "BaseInstance", resource) ++
22+
check_section(dsl_state, [:parties], :party_type, &PartyInfo.party?/1,
23+
"parties", "party_type", "BaseParty", resource) ++
24+
check_section(dsl_state, [:places], :place_type, &PlaceInfo.place?/1,
25+
"places", "place_type", "BasePlace", resource)
26+
27+
case errors do
28+
[] -> :ok
29+
_ -> {:error, errors}
30+
end
31+
end
32+
33+
defp check_section(dsl_state, path, type_field, type_check?, section, field, base, resource) do
34+
entities = Verifier.get_entities(dsl_state, path)
35+
duplicate_errors(entities, section, resource) ++
36+
type_errors(entities, type_field, type_check?, section, field, base, resource)
37+
end
38+
39+
defp duplicate_errors(entities, section, resource) do
40+
entities
41+
|> Enum.group_by(& &1.role)
42+
|> Enum.filter(fn {_role, list} -> length(list) > 1 end)
43+
|> Enum.map(fn {role, _} ->
44+
DslError.exception(
45+
module: resource,
46+
path: [String.to_atom(section)],
47+
message: "#{section}: role #{inspect(role)} is declared more than once"
48+
)
49+
end)
50+
end
51+
52+
defp type_errors(entities, type_field, type_check?, section, field, base, resource) do
53+
Enum.reduce(entities, [], fn entity, acc ->
54+
mod = Map.get(entity, type_field)
55+
56+
cond do
57+
is_nil(mod) ->
58+
acc
59+
60+
!Code.ensure_loaded?(mod) ->
61+
[DslError.exception(
62+
module: resource,
63+
path: [String.to_atom(section)],
64+
message: "#{section}: #{field} #{inspect(mod)} does not exist"
65+
) | acc]
66+
67+
!type_check?.(mod) ->
68+
[DslError.exception(
69+
module: resource,
70+
path: [String.to_atom(section)],
71+
message: "#{section}: #{field} #{inspect(mod)} does not extend #{base}"
72+
) | acc]
73+
74+
true ->
75+
acc
76+
end
77+
end)
78+
end
79+
end

lib/diffo/provider/components/place/extension.ex

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,5 +107,13 @@ defmodule Diffo.Provider.Place.Extension do
107107
}
108108

109109
use Spark.Dsl.Extension,
110-
sections: [@instances, @parties, @places]
110+
sections: [@instances, @parties, @places],
111+
persisters: [
112+
Diffo.Provider.Place.Extension.Persisters.PersistInstances,
113+
Diffo.Provider.Place.Extension.Persisters.PersistParties,
114+
Diffo.Provider.Place.Extension.Persisters.PersistPlaces
115+
],
116+
verifiers: [
117+
Diffo.Provider.Place.Extension.Verifiers.VerifyRoles
118+
]
111119
end
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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.Place.Extension.Persisters.PersistInstances do
6+
@moduledoc "Persists instance role declarations and bakes instances/0"
7+
use Spark.Dsl.Transformer
8+
alias Spark.Dsl.Transformer
9+
10+
@impl true
11+
def transform(dsl_state) do
12+
declarations = Transformer.get_entities(dsl_state, [:instances])
13+
escaped = Macro.escape(declarations)
14+
dsl_state = Transformer.persist(dsl_state, :instances, declarations)
15+
16+
{:ok, Transformer.eval(dsl_state, [], quote do
17+
@doc false
18+
def instances, do: unquote(escaped)
19+
end)}
20+
end
21+
end
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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.Place.Extension.Persisters.PersistParties do
6+
@moduledoc "Persists party role declarations and bakes parties/0"
7+
use Spark.Dsl.Transformer
8+
alias Spark.Dsl.Transformer
9+
10+
@impl true
11+
def transform(dsl_state) do
12+
declarations = Transformer.get_entities(dsl_state, [:parties])
13+
escaped = Macro.escape(declarations)
14+
dsl_state = Transformer.persist(dsl_state, :parties, declarations)
15+
16+
{:ok, Transformer.eval(dsl_state, [], quote do
17+
@doc false
18+
def parties, do: unquote(escaped)
19+
end)}
20+
end
21+
end

0 commit comments

Comments
 (0)