Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ on [Ash Framework](https://www.ash-hq.org/) + [AshNeo4j](https://github.com/diff
## Project structure

```
lib/diffo/type/
primitive.ex # Diffo.Type.Primitive — discriminated union of primitive Elixir types
value.ex # Diffo.Type.Value — union of Primitive and Dynamic
dynamic.ex # Diffo.Type.Dynamic — runtime-typed value (NewType with map storage)
name_value_primitive.ex # Diffo.Type.NameValuePrimitive — name/Primitive pair TypedStruct
name_value_array_primitive.ex # Diffo.Type.NameValueArrayPrimitive — name/[Primitive] pair TypedStruct

lib/diffo/provider/
extension.ex # Unified Spark DSL extension (provider do)
extension/
Expand All @@ -46,14 +53,19 @@ lib/diffo/provider/
base_place.ex # Ash Fragment for Place resources
components/
base_characteristic.ex # Ash Fragment for typed characteristic resources
base_relationship.ex # Ash Fragment for shared Relationship structure
base_relationship.ex # Ash Fragment for shared Relationship structure
defined_simple_relationship.ex # DefinedSimpleRelationship — relationship with one optional embedded characteristic, frozen at creation
relationship.ex # Relationship — mutable TMF service/resource relationship with graph Characteristic nodes
calculations/
characteristic_value.ex # Calculation: builds .Value TypedStruct from record fields
assigned_values.ex # Calculation: returns list of assigned integers for a pool+thing
instance/extension.ex # Thin marker (sections: []) — kind identification
party/extension.ex # Thin marker
place/extension.ex # Thin marker

test/provider/
defined_simple_relationship_test.exs # Integration: DefinedSimpleRelationship create/destroy + DefinedCharacteristic encoding

test/provider/extension/ # All provider extension tests
instance_transformer_test.exs
party_transformer_test.exs
Expand Down
6 changes: 6 additions & 0 deletions lib/diffo/provider.ex
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,12 @@ defmodule Diffo.Provider do
define :delete_relationship, action: :destroy
end

resource Diffo.Provider.DefinedSimpleRelationship do
define :create_defined_simple_relationship, action: :create
define :get_defined_simple_relationship_by_id, action: :read, get_by: :id
define :delete_defined_simple_relationship, action: :destroy
end

resource Diffo.Provider.AssignedToRelationship do
define :create_assigned_to_relationship, action: :create_assignment
define :get_assigned_to_relationship_by_id, action: :read, get_by: :id
Expand Down
107 changes: 107 additions & 0 deletions lib/diffo/provider/components/defined_simple_relationship.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# SPDX-FileCopyrightText: 2025 diffo contributors <https://github.com/diffo-dev/diffo/graphs.contributors>
#
# SPDX-License-Identifier: MIT

defmodule Diffo.Provider.DefinedSimpleRelationship do
@moduledoc """
Ash Resource for a relationship with an optional single embedded characteristic,
set at creation and never changed.

Extends `BaseRelationship` (source, target, type, timestamps). Optionally carries
one `DefinedCharacteristic` — a name/value pair stored directly on the Neo4j node.
The value is a `Diffo.Type.Primitive`, covering string, integer, float, boolean,
and temporal types.

Actions: **create** and **destroy** only. No update, no relate/unrelate. Once
defined, the characteristic is closed — that is the commitment.

Contrast with `Provider.Relationship` which allows mutable graph-based `Characteristic`
nodes to be added, removed, and updated over time.

`DefinedSimpleRelationship` is a general Provider primitive for any relationship
whose characteristic is a commitment or promise made at creation time.
"""
use Ash.Resource,
fragments: [Diffo.Provider.BaseRelationship],
otp_app: :diffo,
domain: Diffo.Provider

resource do
description "An Ash Resource for a relationship with a single optional characteristic, defined at creation and closed thereafter"
plural_name :defined_simple_relationships
end

neo4j do
relate [
{:source, :RELATES, :incoming, :Instance},
{:target, :RELATES, :outgoing, :Instance}
]
end

jason do
pick [:alias, :type]

customize fn result, record ->
target_type = Map.get(record, :target_type)

reference = %Diffo.Provider.Reference{
id: record.target_id,
href: Map.get(record, :target_href)
}

list_name =
Diffo.Provider.Relationship.derive_relationship_characteristic_list_name(target_type)

result
|> Diffo.Util.set(target_type, reference)
|> Diffo.Util.suppress(:alias)
|> then(fn r ->
case Map.get(record, :characteristic) do
nil -> r
char -> Diffo.Util.set(r, list_name, [char])
end
end)
end

order [
:alias,
:type,
:service,
:resource,
:serviceRelationshipCharacteristic,
:resourceRelationshipCharacteristic
]
end

actions do
create :create do
description "creates a defined simple relationship between a source and target instance"
accept [:alias, :type, :characteristic]

argument :source_id, :uuid
argument :target_id, :string

change manage_relationship(:source_id, :source, type: :append)
change manage_relationship(:target_id, :target, type: :append)
change Diffo.Changes.DetailRelationship
end
end

attributes do
attribute :alias, :atom do
description "an optional alias for this relationship"
allow_nil? true
public? true
end

attribute :characteristic, Diffo.Type.NameValuePrimitive do
description "an optional single defining characteristic, set at creation and closed thereafter"
allow_nil? true
public? true
end
end

preparations do
prepare build(sort: [created_at: :asc])
end
end
22 changes: 22 additions & 0 deletions lib/diffo/type/name_value_array_primitive.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# SPDX-FileCopyrightText: 2025 diffo contributors <https://github.com/diffo-dev/diffo/graphs.contributors>
#
# SPDX-License-Identifier: MIT

defmodule Diffo.Type.NameValueArrayPrimitive do
@moduledoc """
Ash TypedStruct for a named array of primitive values.

A name/values pair where each value is a `Diffo.Type.Primitive` — covering string,
integer, float, boolean, date, time, datetime, and duration.
"""
use Ash.TypedStruct, extensions: [AshJason.TypedStruct]

jason do
order [:name, :values]
end

typed_struct do
field :name, :atom, allow_nil?: false, description: "the name"
field :values, {:array, Diffo.Type.Primitive}, default: [], description: "the primitive values"
end
end
22 changes: 22 additions & 0 deletions lib/diffo/type/name_value_primitive.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# SPDX-FileCopyrightText: 2025 diffo contributors <https://github.com/diffo-dev/diffo/graphs.contributors>
#
# SPDX-License-Identifier: MIT

defmodule Diffo.Type.NameValuePrimitive do
@moduledoc """
Ash TypedStruct for a named primitive value.

A name/value pair where the value is a `Diffo.Type.Primitive` — covering string,
integer, float, boolean, date, time, datetime, and duration.
"""
use Ash.TypedStruct, extensions: [AshJason.TypedStruct]

jason do
order [:name, :value]
end

typed_struct do
field :name, :atom, allow_nil?: false, description: "the name"
field :value, Diffo.Type.Primitive, description: "the primitive value"
end
end
168 changes: 168 additions & 0 deletions test/provider/defined_simple_relationship_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
# SPDX-FileCopyrightText: 2025 diffo contributors <https://github.com/diffo-dev/diffo/graphs.contributors>
#
# SPDX-License-Identifier: MIT

defmodule Diffo.Provider.DefinedSimpleRelationshipTest do
@moduledoc false
use ExUnit.Case, async: true

alias Diffo.Type.NameValuePrimitive
alias Diffo.Type.Primitive

setup do
AshNeo4j.Sandbox.checkout()
on_exit(&AshNeo4j.Sandbox.rollback/0)
end

defp build_instances do
spec_a = Diffo.Provider.create_specification!(%{name: "accessEvc"})
spec_b = Diffo.Provider.create_specification!(%{name: "aggregationEvc"})
source = Diffo.Provider.create_instance!(%{specified_by: spec_a.id, name: "access1"})
target = Diffo.Provider.create_instance!(%{specified_by: spec_b.id, name: "agg1"})
{source, target}
end

describe "DefinedSimpleRelationship create" do
test "creates a relationship with no characteristic" do
{source, target} = build_instances()

rel =
Diffo.Provider.create_defined_simple_relationship!(%{
type: :assignedTo,
source_id: source.id,
target_id: target.id
})

assert rel.type == :assignedTo
assert rel.source_id == source.id
assert rel.target_id == target.id
assert rel.characteristic == nil
end

test "creates a relationship with an integer characteristic" do
{source, target} = build_instances()

rel =
Diffo.Provider.create_defined_simple_relationship!(%{
type: :assignedTo,
source_id: source.id,
target_id: target.id,
characteristic: %NameValuePrimitive{
name: :slot,
value: Primitive.wrap("integer", 7)
}
})

assert rel.characteristic.name == :slot
assert Diffo.Unwrap.unwrap(rel.characteristic.value) == 7
end

test "creates a relationship with a string characteristic" do
{source, target} = build_instances()

rel =
Diffo.Provider.create_defined_simple_relationship!(%{
type: :definedBy,
source_id: source.id,
target_id: target.id,
characteristic: %NameValuePrimitive{
name: :bandwidth,
value: Primitive.wrap("string", "1000Mbps")
}
})

assert rel.characteristic.name == :bandwidth
assert Diffo.Unwrap.unwrap(rel.characteristic.value) == "1000Mbps"
end

test "characteristic is persisted and reloaded correctly" do
{source, target} = build_instances()

created =
Diffo.Provider.create_defined_simple_relationship!(%{
type: :assignedTo,
source_id: source.id,
target_id: target.id,
characteristic: %NameValuePrimitive{
name: :slot,
value: Primitive.wrap("integer", 42)
}
})

reloaded = Diffo.Provider.get_defined_simple_relationship_by_id!(created.id)

assert reloaded.characteristic.name == :slot
assert Diffo.Unwrap.unwrap(reloaded.characteristic.value) == 42
end

test "target_href and target_type are populated" do
{source, target} = build_instances()

rel =
Diffo.Provider.create_defined_simple_relationship!(%{
type: :assignedTo,
source_id: source.id,
target_id: target.id
})

assert rel.target_type == :service
assert is_binary(rel.target_href)
end
end

describe "DefinedSimpleRelationship read" do
test "get by id returns the relationship" do
{source, target} = build_instances()

created =
Diffo.Provider.create_defined_simple_relationship!(%{
type: :assignedTo,
source_id: source.id,
target_id: target.id
})

fetched = Diffo.Provider.get_defined_simple_relationship_by_id!(created.id)
assert fetched.id == created.id
assert fetched.type == :assignedTo
end
end

describe "DefinedSimpleRelationship destroy" do
test "destroys the relationship" do
{source, target} = build_instances()

rel =
Diffo.Provider.create_defined_simple_relationship!(%{
type: :assignedTo,
source_id: source.id,
target_id: target.id
})

Diffo.Provider.delete_defined_simple_relationship!(rel)

assert_raise Ash.Error.Invalid, fn ->
Diffo.Provider.get_defined_simple_relationship_by_id!(rel.id)
end
end
end

describe "NameValuePrimitive TypedStruct" do
test "new!/1 constructs with a Primitive value" do
char = NameValuePrimitive.new!(name: :slot, value: Primitive.wrap("integer", 7))
assert char.name == :slot
assert Diffo.Unwrap.unwrap(char.value) == 7
end

test "new!/1 raises when name is nil" do
assert_raise Ash.Error.Invalid, fn ->
NameValuePrimitive.new!(name: nil, value: Primitive.wrap("string", "x"))
end
end

test "Jason encoding produces name then unwrapped value" do
char = NameValuePrimitive.new!(name: :slot, value: Primitive.wrap("integer", 7))
json = Jason.encode!(char)
assert json == ~s({"name":"slot","value":7})
end
end
end
Loading