Skip to content

Commit eb76a93

Browse files
committed
json api
1 parent 7c7c846 commit eb76a93

19 files changed

Lines changed: 303 additions & 44 deletions

.claude/settings.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Bash(mix deps.get)",
5+
"Bash(git -C /Users/beanlanda/git/diffo_example status)",
6+
"Read(//Users/beanlanda/git/**)",
7+
"Read(//Users/beanlanda/.mix/**)"
8+
]
9+
}
10+
}

CHANGELOG.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,4 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline
3737
* updated to diffo 0.2.0
3838

3939
### Features:
40-
* new NBN domain modelling NBN Ethernet access and constituent resources (UNI, AVC, NTD, CVC, NNI Group, NNI)
41-
* NBN Technology and Speeds as Ash Enum types
42-
* speeds derived from NTD technology and AVC bandwidth_profile via mine action
40+
* new NBN domain modelling NBN Ethernet access and constituent resources (UNI, AVC, NTD, CVC, NNI Group, NNI), JSON API and livebook

config/config.exs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ config :spark,
1616
:characteristics,
1717
:neo4j,
1818
:jason,
19+
:json_api,
1920
:outstanding,
2021
:actions,
2122
:state_machine,

diffo_example.livemd

Lines changed: 47 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,21 @@ SPDX-License-Identifier: MIT
99
```elixir
1010
Mix.install(
1111
[
12-
{:diffo_example, "~> 0.2.0"}
12+
{:diffo_example, "~> 0.2.0"},
13+
{:req, "~> 0.5"}
14+
],
15+
config: [
16+
bolty: [{Bolt, [
17+
uri: "bolt://localhost:7687",
18+
auth: [username: "neo4j", password: "password"],
19+
user_agent: "diffoExampleLivebook/1",
20+
pool_size: 15,
21+
max_overflow: 3,
22+
prefix: :default,
23+
name: Bolt,
24+
log: false,
25+
log_hex: false
26+
]}]
1327
],
1428
consolidate_protocols: false
1529
)
@@ -37,25 +51,9 @@ The NBN domain models a fictional NBN Ethernet access circuit and its constituen
3751

3852
## Installing Neo4j and Configuring Bolty
3953

40-
Update the configuration below to match your Neo4j installation and evaluate.
54+
Bolty is configured in the `Mix.install` block above — update the Neo4j credentials there if needed before evaluating.
4155

42-
```elixir
43-
config = [
44-
uri: "bolt://localhost:7687",
45-
auth: [username: "neo4j", password: "password"],
46-
user_agent: "diffoExampleLivebook/1",
47-
pool_size: 15,
48-
max_overflow: 3,
49-
prefix: :default,
50-
name: Bolt,
51-
log: false,
52-
log_hex: false
53-
]
54-
```
55-
56-
```elixir
57-
AshNeo4j.BoltyHelper.start(config)
58-
```
56+
You need [Neo4j](https://neo4j.com/deployment-center/) installed and running. Verify the connection:
5957

6058
```elixir
6159
AshNeo4j.BoltyHelper.is_connected()
@@ -148,11 +146,11 @@ nni_group |> Jason.encode!(pretty: true) |> IO.puts
148146
Define the NNI Group with an SVLAN assignment and relate the NNI:
149147

150148
```elixir
151-
nni_group = Nbn.define_nni_group!(%{
149+
nni_group = Nbn.define_nni_group!(nni_group, %{
152150
characteristic_value_updates: [nni_group: [svlan: 100]]
153151
})
154152
nni_group = Nbn.relate_nni_group!(nni_group, %{
155-
relationships: [%{alias: :nni, target_id: nni.id, type: :isAssigned}]
153+
relationships: [%Diffo.Provider.Instance.Relationship{id: nni.id, alias: :nni, type: :isAssigned}]
156154
})
157155
nni_group |> Jason.encode!(pretty: true) |> IO.puts
158156
```
@@ -162,7 +160,7 @@ Build a CVC — the aggregation virtual circuit that terminates at the NNI Group
162160
```elixir
163161
cvc = Nbn.build_cvc!(%{})
164162
cvc = Nbn.relate_cvc!(cvc, %{
165-
relationships: [%{alias: :nni_group, target_id: nni_group.id, type: :isAssigned}]
163+
relationships: [%Diffo.Provider.Instance.Relationship{id: nni_group.id, alias: :nni_group, type: :isAssigned}]
166164
})
167165
cvc |> Jason.encode!(pretty: true) |> IO.puts
168166
```
@@ -187,7 +185,7 @@ Build a UNI — the interface at the customer premises — and assign a port fro
187185
uni = Nbn.build_uni!(%{})
188186
alias Diffo.Provider.Assignment
189187
ntd = Nbn.assign_port!(ntd, %{
190-
assignment: %Assignment{assignee_id: uni.id, value: 1}
188+
assignment: %Assignment{assignee_id: uni.id, operation: :auto_assign}
191189
})
192190
ntd |> Jason.encode!(pretty: true) |> IO.puts
193191
```
@@ -196,7 +194,7 @@ Relate the UNI back to the NTD so it can mine technology and port from it:
196194

197195
```elixir
198196
uni = Nbn.relate_uni!(uni, %{
199-
relationships: [%{alias: :ntd, target_id: ntd.id, type: :isAssigned}]
197+
relationships: [%Diffo.Provider.Instance.Relationship{id: ntd.id, alias: :ntd, type: :isAssigned}]
200198
})
201199
uni = Nbn.mine_uni!(uni, %{})
202200
uni |> Jason.encode!(pretty: true) |> IO.puts
@@ -210,7 +208,7 @@ avc = Nbn.define_avc!(avc, %{
210208
characteristic_value_updates: [avc: [bandwidth_profile: :home_ultrafast]]
211209
})
212210
cvc = Nbn.assign_cvlan!(cvc, %{
213-
assignment: %Assignment{assignee_id: avc.id, value: 200}
211+
assignment: %Assignment{assignee_id: avc.id, operation: :auto_assign}
214212
})
215213
avc = Nbn.mine_avc!(avc, %{})
216214
avc |> Jason.encode!(pretty: true) |> IO.puts
@@ -222,8 +220,8 @@ Now build the top-level NBN Ethernet access and relate it to both the UNI and AV
222220
pri = Nbn.build_nbn_ethernet!(%{})
223221
pri = Nbn.relate_nbn_ethernet!(pri, %{
224222
relationships: [
225-
%{alias: :uni, target_id: uni.id, type: :isAssigned},
226-
%{alias: :avc, target_id: avc.id, type: :isAssigned}
223+
%Diffo.Provider.Instance.Relationship{id: uni.id, alias: :uni, type: :isAssigned},
224+
%Diffo.Provider.Instance.Relationship{id: avc.id, alias: :avc, type: :isAssigned}
227225
]
228226
})
229227
pri = Nbn.mine_nbn_ethernet!(pri, %{})
@@ -246,6 +244,28 @@ Or from Elixir:
246244
AshNeo4j.Cypher.run("MATCH (n1)-[r]->(n2) RETURN r, n1, n2 LIMIT 50")
247245
```
248246

247+
## JSON API
248+
249+
The NBN domain exposes a JSON API via `Plug.Cowboy` on port 4000. Start the server in your application before evaluating these cells.
250+
251+
First check the catalog — all NBN specifications are initialised on startup:
252+
253+
```elixir
254+
Req.get!("http://localhost:4000/catalog").body |> Jason.encode!(pretty: true) |> IO.puts()
255+
```
256+
257+
Now retrieve all NBN Ethernet instances:
258+
259+
```elixir
260+
Req.get!("http://localhost:4000/nbnEthernet").body |> Jason.encode!(pretty: true) |> IO.puts()
261+
```
262+
263+
Or fetch the one we provisioned above by id:
264+
265+
```elixir
266+
Req.get!("http://localhost:4000/nbnEthernet/#{pri.id}").body |> Jason.encode!(pretty: true) |> IO.puts()
267+
```
268+
249269
## What Next?
250270

251271
You've provisioned a complete NBN Ethernet access — NTD, UNI, AVC, CVC, NNI Group, and NNI — and seen how the `mine` actions propagate technology, speeds, CVLAN and port assignments up the resource hierarchy automatically.

lib/diffo_example/application.ex

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ defmodule DiffoExample.Application do
99

1010
@impl true
1111
def start(_type, _args) do
12-
Supervisor.start_link([], strategy: :one_for_one)
12+
children = [
13+
{Plug.Cowboy, scheme: :http, plug: DiffoExample.Nbn.Router, options: [port: 4000]},
14+
{Task, &DiffoExample.Nbn.Initializer.init/0}
15+
]
16+
17+
Supervisor.start_link(children, strategy: :one_for_one, name: DiffoExample.Supervisor)
1318
end
1419
end

lib/nbn/api_router.ex

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# SPDX-FileCopyrightText: 2025 diffo_example contributors <https://github.com/diffo-dev/diffo_example/graphs.contributors>
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
defmodule DiffoExample.Nbn.ApiRouter do
6+
@moduledoc false
7+
use AshJsonApi.Router,
8+
domains: [DiffoExample.Nbn],
9+
open_api: "/open_api"
10+
end

lib/nbn/catalog.ex

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# SPDX-FileCopyrightText: 2025 diffo_example contributors <https://github.com/diffo-dev/diffo_example/graphs.contributors>
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
defmodule DiffoExample.Nbn.Catalog do
6+
@moduledoc """
7+
Diffo - TMF Service and Resource Management with a difference
8+
9+
Catalog - the NBN resource and service catalog.
10+
"""
11+
12+
def list do
13+
Diffo.Provider.list_specifications!()
14+
|> Enum.map(fn spec ->
15+
Jason.OrderedObject.new(
16+
id: spec.id,
17+
href: spec.href,
18+
name: spec.name,
19+
version: spec.version,
20+
description: spec.description,
21+
category: spec.category
22+
)
23+
end)
24+
end
25+
end

lib/nbn/initializer.ex

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# SPDX-FileCopyrightText: 2025 diffo_example contributors <https://github.com/diffo-dev/diffo_example/graphs.contributors>
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
defmodule DiffoExample.Nbn.Initializer do
6+
@moduledoc """
7+
Diffo - TMF Service and Resource Management with a difference
8+
9+
Initializes the NBN domain's specifications in the catalog on application startup,
10+
so the catalog is populated before any instances are built.
11+
"""
12+
13+
alias Diffo.Provider.Instance.Specification
14+
15+
def init do
16+
DiffoExample.Nbn
17+
|> Ash.Domain.Info.resources()
18+
|> Enum.each(fn module ->
19+
try do
20+
Specification.upsert_specification(module)
21+
rescue
22+
_ -> :ok
23+
end
24+
end)
25+
end
26+
end

lib/nbn/nbn.ex

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ defmodule DiffoExample.Nbn do
1313
CVC (aggregates AVCs, terminates at NNI Group), NNI Group, and NNI.
1414
"""
1515
use Ash.Domain,
16-
otp_app: :diffo
16+
otp_app: :diffo,
17+
extensions: [AshJsonApi.Domain]
1718

1819
alias DiffoExample.Nbn.NbnEthernet
1920
alias DiffoExample.Nbn.Uni
@@ -27,6 +28,78 @@ defmodule DiffoExample.Nbn do
2728
description "An example showing how TMF Resources for a fictional NBN domain can be extended from the Provider domain"
2829
end
2930

31+
json_api do
32+
routes do
33+
base_route "/nbnEthernet", NbnEthernet do
34+
index :read
35+
get :read
36+
post :build
37+
patch :define
38+
patch :relate, route: "/:id/relate"
39+
patch :mine, route: "/:id/mine"
40+
delete :destroy
41+
end
42+
43+
base_route "/uni", Uni do
44+
index :read
45+
get :read
46+
post :build
47+
patch :define
48+
patch :relate, route: "/:id/relate"
49+
patch :mine, route: "/:id/mine"
50+
delete :destroy
51+
end
52+
53+
base_route "/avc", Avc do
54+
index :read
55+
get :read
56+
post :build
57+
patch :define
58+
patch :relate, route: "/:id/relate"
59+
patch :mine, route: "/:id/mine"
60+
delete :destroy
61+
end
62+
63+
base_route "/ntd", Ntd do
64+
index :read
65+
get :read
66+
post :build
67+
patch :define
68+
patch :relate, route: "/:id/relate"
69+
delete :destroy
70+
end
71+
72+
base_route "/cvc", Cvc do
73+
index :read
74+
get :read
75+
post :build
76+
patch :define
77+
patch :relate, route: "/:id/relate"
78+
patch :mine, route: "/:id/mine"
79+
delete :destroy
80+
end
81+
82+
base_route "/nniGroup", NniGroup do
83+
index :read
84+
get :read
85+
post :build
86+
patch :define
87+
patch :relate, route: "/:id/relate"
88+
delete :destroy
89+
end
90+
91+
base_route "/nni", Nni do
92+
index :read
93+
get :read
94+
post :build
95+
patch :define
96+
patch :relate, route: "/:id/relate"
97+
delete :destroy
98+
end
99+
100+
end
101+
end
102+
30103
resources do
31104
resource NbnEthernet do
32105
define :get_nbn_ethernet_by_id, action: :read, get_by: :id
@@ -83,5 +156,6 @@ defmodule DiffoExample.Nbn do
83156
define :define_nni, action: :define
84157
define :relate_nni, action: :relate
85158
end
159+
86160
end
87161
end

lib/nbn/resources/avc.ex

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@ defmodule DiffoExample.Nbn.Avc do
2121

2222
use Ash.Resource,
2323
fragments: [BaseInstance],
24-
domain: Nbn
24+
domain: Nbn,
25+
extensions: [AshJsonApi.Resource]
26+
27+
json_api do
28+
type "avc"
29+
end
2530

2631
resource do
2732
description "An Ash Resource representing an Access Virtual Circuit (AVC)"
@@ -110,9 +115,9 @@ defmodule DiffoExample.Nbn.Avc do
110115

111116
# mines related resource to characteristics
112117
def mine_related(changeset, _context) when is_struct(changeset, Ash.Changeset) do
113-
reverse_relationships = Ash.Changeset.get_attribute(changeset, :reverse_relationships)
118+
avc = Ash.load!(changeset.data, [reverse_relationships: [:characteristics]])
114119

115-
cvlan = {:cvlan, Diffo.Unwrap.unwrap(hd(hd(reverse_relationships).characteristics).value)}
120+
cvlan = {:cvlan, Diffo.Unwrap.unwrap(hd(hd(avc.reverse_relationships).characteristics).value)}
116121

117122
Ash.Changeset.force_set_argument(changeset, :characteristic_value_updates, avc: [cvlan])
118123
end

0 commit comments

Comments
 (0)