Skip to content

Commit c3fb128

Browse files
committed
improved via assignment_relationship, added identity
1 parent 7a4d917 commit c3fb128

8 files changed

Lines changed: 180 additions & 79 deletions

File tree

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ lib/diffo/provider/
6666
base_characteristic.ex # Ash Fragment for typed characteristic resources
6767
base_relationship.ex # Ash Fragment for shared Relationship structure
6868
defined_simple_relationship.ex # DefinedSimpleRelationship — relationship with one optional embedded characteristic, frozen at creation
69+
assignment_relationship.ex # AssignmentRelationship — pool assignment relationship with top-level pool/thing/value scalar attributes
6970
relationship.ex # Relationship — mutable TMF service/resource relationship with graph Characteristic nodes
7071
calculations/
7172
characteristic_value.ex # Calculation: builds .Value TypedStruct from record fields

lib/diffo/provider.ex

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ defmodule Diffo.Provider do
8383
define :delete_defined_simple_relationship, action: :destroy
8484
end
8585

86+
resource Diffo.Provider.AssignmentRelationship do
87+
define :create_assignment_relationship, action: :create
88+
define :get_assignment_relationship_by_id, action: :read, get_by: :id
89+
define :delete_assignment_relationship, action: :destroy
90+
end
91+
8692
resource Diffo.Provider.AssignableCharacteristic do
8793
define :create_assignable_characteristic, action: :create
8894
define :get_assignable_characteristic_by_id, action: :read, get_by: :id

lib/diffo/provider/assigner/assigner.ex

Lines changed: 58 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,14 @@
44

55
defmodule Diffo.Provider.Assigner do
66
@moduledoc """
7-
Helper to perform Assignment using `Diffo.Provider.DefinedSimpleRelationship`.
7+
Helper to perform Assignment using `Diffo.Provider.AssignmentRelationship`.
88
9-
Each assignment is stored as a `DefinedSimpleRelationship` with `type: :assignedTo`
10-
and a single `NameValuePrimitive` characteristic carrying the thing name and assigned value.
9+
Each assignment is stored as an `AssignmentRelationship` with top-level `pool`,
10+
`thing`, and `value` attributes. This makes them filterable at the Cypher level
11+
and usable in aggregate expressions.
1112
"""
1213
alias Diffo.Provider.AssignableCharacteristic
13-
alias Diffo.Provider.DefinedSimpleRelationship
14-
alias Diffo.Type.NameValuePrimitive
15-
alias Diffo.Type.Primitive
14+
alias Diffo.Provider.AssignmentRelationship
1615

1716
@doc """
1817
Assign a thing using the pool declared via `pools do` on the instance module.
@@ -42,96 +41,76 @@ defmodule Diffo.Provider.Assigner do
4241
_ ->
4342
case Map.get(assignment, :operation, :auto_assign) do
4443
:auto_assign ->
45-
case next(result, pool, thing) do
46-
{:ok, assigned} ->
47-
relate_is_assigned(result, pool, thing, assigned, assignee_id)
48-
49-
{:error, error} ->
50-
{:error, error}
44+
with {:ok, value} <- next(result, pool, thing) do
45+
create_assignment(result, pool, thing, value, assignee_id)
5146
end
5247

5348
:assign ->
54-
case assignable?(result, pool, thing, assignment.id) do
55-
true ->
56-
relate_is_assigned(result, pool, thing, assignment.id, assignee_id)
57-
58-
false ->
59-
{:error, "#{thing} #{assignment.id} is not assignable"}
49+
if assignable?(result, pool, thing, assignment.id) do
50+
create_assignment(result, pool, thing, assignment.id, assignee_id)
51+
else
52+
{:error, "#{thing} #{assignment.id} is not assignable"}
6053
end
6154

6255
:unassign ->
63-
unrelate_is_assigned(result, pool, thing, assignment.id, assignee_id)
56+
destroy_assignment(result, pool, thing, assignment.id, assignee_id)
6457
end
6558
end
6659
end
6760

68-
defp relate_is_assigned(result, _pool, thing, value, assignee_id)
69-
when is_struct(result) and is_atom(thing) and is_integer(value) and
61+
defp create_assignment(result, pool, thing, value, assignee_id)
62+
when is_struct(result) and is_atom(pool) and is_atom(thing) and is_integer(value) and
7063
is_bitstring(assignee_id) do
71-
case Diffo.Provider.create_defined_simple_relationship(%{
72-
type: :assignedTo,
73-
characteristic: %NameValuePrimitive{
74-
name: thing,
75-
value: Primitive.wrap("integer", value)
76-
},
77-
source_id: result.id,
78-
target_id: assignee_id
79-
}) do
80-
{:ok, _relationship} ->
81-
{:ok, result}
82-
83-
{:error, error} ->
84-
{:error, error}
64+
with {:ok, _} <-
65+
Diffo.Provider.create_assignment_relationship(%{
66+
pool: pool,
67+
thing: thing,
68+
value: value,
69+
source_id: result.id,
70+
target_id: assignee_id
71+
}) do
72+
{:ok, result}
8573
end
8674
end
8775

88-
defp unrelate_is_assigned(result, pool, thing, value, assignee_id)
76+
defp destroy_assignment(result, pool, thing, value, assignee_id)
8977
when is_struct(result) and is_atom(pool) and is_atom(thing) and is_integer(value) and
9078
is_bitstring(assignee_id) do
9179
case find_assignment(result.id, assignee_id, pool, thing, value) do
9280
{:ok, nil} ->
9381
{:error, "#{thing} #{value} is not assigned to assignee #{assignee_id}"}
9482

95-
{:ok, relationship} ->
96-
case Ash.destroy(relationship, domain: Diffo.Provider) do
97-
:ok ->
98-
{:ok, result}
99-
100-
{:error, error} ->
101-
{:error, error}
83+
{:ok, assignment} ->
84+
with :ok <- Ash.destroy(assignment, domain: Diffo.Provider) do
85+
{:ok, result}
10286
end
10387

10488
{:error, error} ->
10589
{:error, error}
10690
end
10791
end
10892

109-
defp find_assignment(source_id, target_id, _pool, thing, value) do
110-
case DefinedSimpleRelationship
111-
|> Ash.Query.new()
112-
|> Ash.Query.filter_input(source_id: source_id, target_id: target_id, type: :assignedTo)
113-
|> Ash.read(domain: Diffo.Provider) do
114-
{:ok, rels} ->
115-
{:ok,
116-
Enum.find(rels, fn rel ->
117-
rel.characteristic &&
118-
rel.characteristic.name == thing &&
119-
Diffo.Unwrap.unwrap(rel.characteristic.value) == value
120-
end)}
121-
122-
{:error, error} ->
123-
{:error, error}
124-
end
93+
defp find_assignment(source_id, target_id, pool, thing, value) do
94+
AssignmentRelationship
95+
|> Ash.Query.filter_input(
96+
source_id: source_id,
97+
target_id: target_id,
98+
pool: pool,
99+
thing: thing,
100+
value: value
101+
)
102+
|> Ash.read_one(domain: Diffo.Provider)
125103
end
126104

127105
defp next(instance, pool, thing)
128106
when is_struct(instance) and is_atom(pool) and is_atom(thing) do
129-
case pool_characteristic(instance.id, pool, thing) do
107+
case pool_characteristic(instance.id, pool) do
130108
{:ok, nil} ->
131109
{:error, "pool #{pool} not found on instance #{instance.id}"}
132110

133111
{:ok, char} ->
134-
free = Enum.to_list(char.first..char.last) -- char.assigned_values
112+
assigned = assigned_values_for(instance.id, thing)
113+
free = Enum.to_list(char.first..char.last) -- assigned
135114

136115
case free do
137116
[] ->
@@ -152,18 +131,30 @@ defmodule Diffo.Provider.Assigner do
152131

153132
defp assignable?(instance, pool, thing, value)
154133
when is_struct(instance) and is_atom(pool) and is_atom(thing) and is_integer(value) do
155-
case pool_characteristic(instance.id, pool, thing) do
156-
{:ok, nil} -> false
157-
{:ok, char} -> value in (Enum.to_list(char.first..char.last) -- char.assigned_values)
158-
{:error, _} -> false
134+
case pool_characteristic(instance.id, pool) do
135+
{:ok, nil} ->
136+
false
137+
138+
{:ok, char} ->
139+
assigned = assigned_values_for(instance.id, thing)
140+
value in (Enum.to_list(char.first..char.last) -- assigned)
141+
142+
{:error, _} ->
143+
false
159144
end
160145
end
161146

162-
defp pool_characteristic(instance_id, pool, thing) do
147+
defp assigned_values_for(instance_id, thing) do
148+
AssignmentRelationship
149+
|> Ash.Query.filter_input(source_id: instance_id, thing: thing)
150+
|> Ash.read!(domain: Diffo.Provider)
151+
|> Enum.map(& &1.value)
152+
end
153+
154+
defp pool_characteristic(instance_id, pool) do
163155
AssignableCharacteristic
164156
|> Ash.Query.new()
165157
|> Ash.Query.filter_input(instance_id: instance_id, name: pool)
166-
|> Ash.Query.load(assigned_values: [thing: thing])
167158
|> Ash.read_one(domain: Diffo.Provider)
168159
end
169160
end
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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.AssignmentRelationship do
6+
@moduledoc """
7+
Ash Resource for a pool assignment relationship.
8+
9+
Stores a single pool assignment as a direct Neo4j relationship between a source
10+
(the pool-owning instance) and a target (the assignee instance). `pool`, `thing`,
11+
and `value` are top-level scalar attributes, making them filterable at the Cypher
12+
level and usable in aggregate filters via AshNeo4j #253.
13+
14+
Contrast with `DefinedSimpleRelationship`, which stores its characteristic as an
15+
embedded `NameValuePrimitive` — suitable as a general primitive but opaque to the
16+
data layer for filtering purposes.
17+
18+
Actions: **create** and **destroy** only. Assignments are commitments; to change
19+
an assignment, destroy and recreate.
20+
"""
21+
use Ash.Resource,
22+
fragments: [Diffo.Provider.BaseRelationship],
23+
otp_app: :diffo,
24+
domain: Diffo.Provider
25+
26+
resource do
27+
description "A pool assignment relationship between a source and target instance"
28+
plural_name :assignment_relationships
29+
end
30+
31+
neo4j do
32+
relate [
33+
{:source, :RELATES, :incoming, :Instance},
34+
{:target, :RELATES, :outgoing, :Instance}
35+
]
36+
end
37+
38+
jason do
39+
pick [:type]
40+
41+
customize fn result, record ->
42+
reference = %Diffo.Provider.Reference{
43+
id: record.target_id,
44+
href: record.target_href
45+
}
46+
47+
list_name =
48+
Diffo.Provider.Relationship.derive_relationship_characteristic_list_name(record.target_type)
49+
50+
characteristic = %{name: record.thing, value: record.value}
51+
52+
result
53+
|> Diffo.Util.set(record.target_type, reference)
54+
|> Diffo.Util.set(list_name, [characteristic])
55+
end
56+
57+
order [:type, :resource, :service, :resourceRelationshipCharacteristic,
58+
:serviceRelationshipCharacteristic]
59+
end
60+
61+
actions do
62+
create :create do
63+
description "creates a pool assignment relationship between a source and target instance"
64+
accept [:pool, :thing, :value]
65+
66+
argument :source_id, :uuid
67+
argument :target_id, :string
68+
69+
change set_attribute(:type, :assignedTo)
70+
change manage_relationship(:source_id, :source, type: :append)
71+
change manage_relationship(:target_id, :target, type: :append)
72+
change Diffo.Changes.DetailRelationship
73+
end
74+
end
75+
76+
attributes do
77+
attribute :pool, :atom do
78+
description "the pool name this assignment belongs to (e.g. :ports)"
79+
allow_nil? false
80+
public? true
81+
end
82+
83+
attribute :thing, :atom do
84+
description "the kind of thing being assigned (e.g. :port)"
85+
allow_nil? false
86+
public? true
87+
end
88+
89+
attribute :value, :integer do
90+
description "the assigned integer value"
91+
allow_nil? false
92+
public? true
93+
constraints min: 0
94+
end
95+
end
96+
97+
identities do
98+
identity :unique_assignment, [:source_id, :pool, :thing, :value] do
99+
pre_check? true
100+
end
101+
end
102+
103+
preparations do
104+
prepare build(sort: [created_at: :asc])
105+
end
106+
end

lib/diffo/provider/components/base_instance.ex

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ defmodule Diffo.Provider.BaseInstance do
188188
{:process_statuses, :STATUSES, :incoming, :ProcessStatus},
189189
{:forward_relationships, :RELATES, :outgoing, :Relationship},
190190
{:reverse_relationships, :RELATES, :incoming, :Relationship},
191-
{:assignments, :RELATES, :outgoing, :DefinedSimpleRelationship},
191+
{:assignments, :RELATES, :outgoing, :AssignmentRelationship},
192192
{:features, :HAS, :outgoing, :Feature},
193193
{:characteristics, :HAS, :outgoing, :Characteristic},
194194
{:entities, :RELATES, :outgoing, :EntityRef},
@@ -409,7 +409,7 @@ defmodule Diffo.Provider.BaseInstance do
409409
public? true
410410
end
411411

412-
has_many :assignments, Diffo.Provider.DefinedSimpleRelationship do
412+
has_many :assignments, Diffo.Provider.AssignmentRelationship do
413413
description "the instance's outgoing pool assignment relationships"
414414
destination_attribute :source_id
415415
public? true

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

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,10 @@ defmodule Diffo.Provider.Calculations.AssignedValues do
1414
thing = context.arguments[:thing]
1515

1616
Enum.map(records, fn record ->
17-
Diffo.Provider.DefinedSimpleRelationship
18-
|> Ash.Query.filter_input(source_id: record.instance_id, type: :assignedTo)
17+
Diffo.Provider.AssignmentRelationship
18+
|> Ash.Query.filter_input(source_id: record.instance_id, thing: thing)
1919
|> Ash.read!(domain: Diffo.Provider)
20-
|> Enum.filter(fn rel -> rel.characteristic && rel.characteristic.name == thing end)
21-
|> Enum.map(fn rel -> Diffo.Unwrap.unwrap(rel.characteristic.value) end)
20+
|> Enum.map(& &1.value)
2221
end)
2322
end
2423
end

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

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,10 @@ defmodule Diffo.Provider.Calculations.FreeValues do
1717

1818
record ->
1919
count =
20-
Diffo.Provider.DefinedSimpleRelationship
21-
|> Ash.Query.filter_input(source_id: record.instance_id, type: :assignedTo)
20+
Diffo.Provider.AssignmentRelationship
21+
|> Ash.Query.filter_input(source_id: record.instance_id, thing: record.thing)
2222
|> Ash.read!(domain: Diffo.Provider)
23-
|> Enum.count(fn rel ->
24-
rel.characteristic && rel.characteristic.name == record.thing
25-
end)
23+
|> length()
2624

2725
record.last - record.first + 1 - count
2826
end)

test/provider/extension/assigner_test.exs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ defmodule Diffo.Provider.Extension.AssignerTest do
191191

192192
assert length(card.assignments) == 1
193193

194-
assigned_port = Diffo.Unwrap.unwrap(hd(card.assignments).characteristic.value)
194+
assigned_port = hd(card.assignments).value
195195

196196
{:ok, card} =
197197
Servo.assign_port(card, %{

0 commit comments

Comments
 (0)