Skip to content

Commit fc216e2

Browse files
committed
defined_simple_relationship
1 parent 957595c commit fc216e2

6 files changed

Lines changed: 338 additions & 1 deletion

File tree

AGENTS.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ on [Ash Framework](https://www.ash-hq.org/) + [AshNeo4j](https://github.com/diff
2222
## Project structure
2323

2424
```
25+
lib/diffo/type/
26+
primitive.ex # Diffo.Type.Primitive — discriminated union of primitive Elixir types
27+
value.ex # Diffo.Type.Value — union of Primitive and Dynamic
28+
dynamic.ex # Diffo.Type.Dynamic — runtime-typed value (NewType with map storage)
29+
name_value_primitive.ex # Diffo.Type.NameValuePrimitive — name/Primitive pair TypedStruct
30+
name_value_array_primitive.ex # Diffo.Type.NameValueArrayPrimitive — name/[Primitive] pair TypedStruct
31+
2532
lib/diffo/provider/
2633
extension.ex # Unified Spark DSL extension (provider do)
2734
extension/
@@ -46,14 +53,19 @@ lib/diffo/provider/
4653
base_place.ex # Ash Fragment for Place resources
4754
components/
4855
base_characteristic.ex # Ash Fragment for typed characteristic resources
49-
base_relationship.ex # Ash Fragment for shared Relationship structure
56+
base_relationship.ex # Ash Fragment for shared Relationship structure
57+
defined_simple_relationship.ex # DefinedSimpleRelationship — relationship with one optional embedded characteristic, frozen at creation
58+
relationship.ex # Relationship — mutable TMF service/resource relationship with graph Characteristic nodes
5059
calculations/
5160
characteristic_value.ex # Calculation: builds .Value TypedStruct from record fields
5261
assigned_values.ex # Calculation: returns list of assigned integers for a pool+thing
5362
instance/extension.ex # Thin marker (sections: []) — kind identification
5463
party/extension.ex # Thin marker
5564
place/extension.ex # Thin marker
5665
66+
test/provider/
67+
defined_simple_relationship_test.exs # Integration: DefinedSimpleRelationship create/destroy + DefinedCharacteristic encoding
68+
5769
test/provider/extension/ # All provider extension tests
5870
instance_transformer_test.exs
5971
party_transformer_test.exs

lib/diffo/provider.ex

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,12 @@ defmodule Diffo.Provider do
7777
define :delete_relationship, action: :destroy
7878
end
7979

80+
resource Diffo.Provider.DefinedSimpleRelationship do
81+
define :create_defined_simple_relationship, action: :create
82+
define :get_defined_simple_relationship_by_id, action: :read, get_by: :id
83+
define :delete_defined_simple_relationship, action: :destroy
84+
end
85+
8086
resource Diffo.Provider.AssignedToRelationship do
8187
define :create_assigned_to_relationship, action: :create_assignment
8288
define :get_assigned_to_relationship_by_id, action: :read, get_by: :id
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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.DefinedSimpleRelationship do
6+
@moduledoc """
7+
Ash Resource for a relationship with an optional single embedded characteristic,
8+
set at creation and never changed.
9+
10+
Extends `BaseRelationship` (source, target, type, timestamps). Optionally carries
11+
one `DefinedCharacteristic` — a name/value pair stored directly on the Neo4j node.
12+
The value is a `Diffo.Type.Primitive`, covering string, integer, float, boolean,
13+
and temporal types.
14+
15+
Actions: **create** and **destroy** only. No update, no relate/unrelate. Once
16+
defined, the characteristic is closed — that is the commitment.
17+
18+
Contrast with `Provider.Relationship` which allows mutable graph-based `Characteristic`
19+
nodes to be added, removed, and updated over time.
20+
21+
`DefinedSimpleRelationship` is a general Provider primitive for any relationship
22+
whose characteristic is a commitment or promise made at creation time.
23+
"""
24+
use Ash.Resource,
25+
fragments: [Diffo.Provider.BaseRelationship],
26+
otp_app: :diffo,
27+
domain: Diffo.Provider
28+
29+
resource do
30+
description "An Ash Resource for a relationship with a single optional characteristic, defined at creation and closed thereafter"
31+
plural_name :defined_simple_relationships
32+
end
33+
34+
neo4j do
35+
relate [
36+
{:source, :RELATES, :incoming, :Instance},
37+
{:target, :RELATES, :outgoing, :Instance}
38+
]
39+
end
40+
41+
jason do
42+
pick [:alias, :type]
43+
44+
customize fn result, record ->
45+
target_type = Map.get(record, :target_type)
46+
47+
reference = %Diffo.Provider.Reference{
48+
id: record.target_id,
49+
href: Map.get(record, :target_href)
50+
}
51+
52+
list_name =
53+
Diffo.Provider.Relationship.derive_relationship_characteristic_list_name(target_type)
54+
55+
result
56+
|> Diffo.Util.set(target_type, reference)
57+
|> Diffo.Util.suppress(:alias)
58+
|> then(fn r ->
59+
case Map.get(record, :characteristic) do
60+
nil -> r
61+
char -> Diffo.Util.set(r, list_name, [char])
62+
end
63+
end)
64+
end
65+
66+
order [
67+
:alias,
68+
:type,
69+
:service,
70+
:resource,
71+
:serviceRelationshipCharacteristic,
72+
:resourceRelationshipCharacteristic
73+
]
74+
end
75+
76+
actions do
77+
create :create do
78+
description "creates a defined simple relationship between a source and target instance"
79+
accept [:alias, :type, :characteristic]
80+
81+
argument :source_id, :uuid
82+
argument :target_id, :string
83+
84+
change manage_relationship(:source_id, :source, type: :append)
85+
change manage_relationship(:target_id, :target, type: :append)
86+
change Diffo.Changes.DetailRelationship
87+
end
88+
end
89+
90+
attributes do
91+
attribute :alias, :atom do
92+
description "an optional alias for this relationship"
93+
allow_nil? true
94+
public? true
95+
end
96+
97+
attribute :characteristic, Diffo.Type.NameValuePrimitive do
98+
description "an optional single defining characteristic, set at creation and closed thereafter"
99+
allow_nil? true
100+
public? true
101+
end
102+
end
103+
104+
preparations do
105+
prepare build(sort: [created_at: :asc])
106+
end
107+
end
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# SPDX-FileCopyrightText: 2025 diffo contributors <https://github.com/diffo-dev/diffo/graphs.contributors>
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
defmodule Diffo.Type.NameValueArrayPrimitive do
6+
@moduledoc """
7+
Ash TypedStruct for a named array of primitive values.
8+
9+
A name/values pair where each value is a `Diffo.Type.Primitive` — covering string,
10+
integer, float, boolean, date, time, datetime, and duration.
11+
"""
12+
use Ash.TypedStruct, extensions: [AshJason.TypedStruct]
13+
14+
jason do
15+
order [:name, :values]
16+
end
17+
18+
typed_struct do
19+
field :name, :atom, allow_nil?: false, description: "the name"
20+
field :values, {:array, Diffo.Type.Primitive}, default: [], description: "the primitive values"
21+
end
22+
end
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# SPDX-FileCopyrightText: 2025 diffo contributors <https://github.com/diffo-dev/diffo/graphs.contributors>
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
defmodule Diffo.Type.NameValuePrimitive do
6+
@moduledoc """
7+
Ash TypedStruct for a named primitive value.
8+
9+
A name/value pair where the value is a `Diffo.Type.Primitive` — covering string,
10+
integer, float, boolean, date, time, datetime, and duration.
11+
"""
12+
use Ash.TypedStruct, extensions: [AshJason.TypedStruct]
13+
14+
jason do
15+
order [:name, :value]
16+
end
17+
18+
typed_struct do
19+
field :name, :atom, allow_nil?: false, description: "the name"
20+
field :value, Diffo.Type.Primitive, description: "the primitive value"
21+
end
22+
end
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
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.DefinedSimpleRelationshipTest do
6+
@moduledoc false
7+
use ExUnit.Case, async: true
8+
9+
alias Diffo.Type.NameValuePrimitive
10+
alias Diffo.Type.Primitive
11+
12+
setup do
13+
AshNeo4j.Sandbox.checkout()
14+
on_exit(&AshNeo4j.Sandbox.rollback/0)
15+
end
16+
17+
defp build_instances do
18+
spec_a = Diffo.Provider.create_specification!(%{name: "accessEvc"})
19+
spec_b = Diffo.Provider.create_specification!(%{name: "aggregationEvc"})
20+
source = Diffo.Provider.create_instance!(%{specified_by: spec_a.id, name: "access1"})
21+
target = Diffo.Provider.create_instance!(%{specified_by: spec_b.id, name: "agg1"})
22+
{source, target}
23+
end
24+
25+
describe "DefinedSimpleRelationship create" do
26+
test "creates a relationship with no characteristic" do
27+
{source, target} = build_instances()
28+
29+
rel =
30+
Diffo.Provider.create_defined_simple_relationship!(%{
31+
type: :assignedTo,
32+
source_id: source.id,
33+
target_id: target.id
34+
})
35+
36+
assert rel.type == :assignedTo
37+
assert rel.source_id == source.id
38+
assert rel.target_id == target.id
39+
assert rel.characteristic == nil
40+
end
41+
42+
test "creates a relationship with an integer characteristic" do
43+
{source, target} = build_instances()
44+
45+
rel =
46+
Diffo.Provider.create_defined_simple_relationship!(%{
47+
type: :assignedTo,
48+
source_id: source.id,
49+
target_id: target.id,
50+
characteristic: %NameValuePrimitive{
51+
name: :slot,
52+
value: Primitive.wrap("integer", 7)
53+
}
54+
})
55+
56+
assert rel.characteristic.name == :slot
57+
assert Diffo.Unwrap.unwrap(rel.characteristic.value) == 7
58+
end
59+
60+
test "creates a relationship with a string characteristic" do
61+
{source, target} = build_instances()
62+
63+
rel =
64+
Diffo.Provider.create_defined_simple_relationship!(%{
65+
type: :definedBy,
66+
source_id: source.id,
67+
target_id: target.id,
68+
characteristic: %NameValuePrimitive{
69+
name: :bandwidth,
70+
value: Primitive.wrap("string", "1000Mbps")
71+
}
72+
})
73+
74+
assert rel.characteristic.name == :bandwidth
75+
assert Diffo.Unwrap.unwrap(rel.characteristic.value) == "1000Mbps"
76+
end
77+
78+
test "characteristic is persisted and reloaded correctly" do
79+
{source, target} = build_instances()
80+
81+
created =
82+
Diffo.Provider.create_defined_simple_relationship!(%{
83+
type: :assignedTo,
84+
source_id: source.id,
85+
target_id: target.id,
86+
characteristic: %NameValuePrimitive{
87+
name: :slot,
88+
value: Primitive.wrap("integer", 42)
89+
}
90+
})
91+
92+
reloaded = Diffo.Provider.get_defined_simple_relationship_by_id!(created.id)
93+
94+
assert reloaded.characteristic.name == :slot
95+
assert Diffo.Unwrap.unwrap(reloaded.characteristic.value) == 42
96+
end
97+
98+
test "target_href and target_type are populated" do
99+
{source, target} = build_instances()
100+
101+
rel =
102+
Diffo.Provider.create_defined_simple_relationship!(%{
103+
type: :assignedTo,
104+
source_id: source.id,
105+
target_id: target.id
106+
})
107+
108+
assert rel.target_type == :service
109+
assert is_binary(rel.target_href)
110+
end
111+
end
112+
113+
describe "DefinedSimpleRelationship read" do
114+
test "get by id returns the relationship" do
115+
{source, target} = build_instances()
116+
117+
created =
118+
Diffo.Provider.create_defined_simple_relationship!(%{
119+
type: :assignedTo,
120+
source_id: source.id,
121+
target_id: target.id
122+
})
123+
124+
fetched = Diffo.Provider.get_defined_simple_relationship_by_id!(created.id)
125+
assert fetched.id == created.id
126+
assert fetched.type == :assignedTo
127+
end
128+
end
129+
130+
describe "DefinedSimpleRelationship destroy" do
131+
test "destroys the relationship" do
132+
{source, target} = build_instances()
133+
134+
rel =
135+
Diffo.Provider.create_defined_simple_relationship!(%{
136+
type: :assignedTo,
137+
source_id: source.id,
138+
target_id: target.id
139+
})
140+
141+
Diffo.Provider.delete_defined_simple_relationship!(rel)
142+
143+
assert_raise Ash.Error.Invalid, fn ->
144+
Diffo.Provider.get_defined_simple_relationship_by_id!(rel.id)
145+
end
146+
end
147+
end
148+
149+
describe "NameValuePrimitive TypedStruct" do
150+
test "new!/1 constructs with a Primitive value" do
151+
char = NameValuePrimitive.new!(name: :slot, value: Primitive.wrap("integer", 7))
152+
assert char.name == :slot
153+
assert Diffo.Unwrap.unwrap(char.value) == 7
154+
end
155+
156+
test "new!/1 raises when name is nil" do
157+
assert_raise Ash.Error.Invalid, fn ->
158+
NameValuePrimitive.new!(name: nil, value: Primitive.wrap("string", "x"))
159+
end
160+
end
161+
162+
test "Jason encoding produces name then unwrapped value" do
163+
char = NameValuePrimitive.new!(name: :slot, value: Primitive.wrap("integer", 7))
164+
json = Jason.encode!(char)
165+
assert json == ~s({"name":"slot","value":7})
166+
end
167+
end
168+
end

0 commit comments

Comments
 (0)