Skip to content

Commit 3d3863f

Browse files
Merge pull request #22 from diffo-dev/21-20-json-api-and-rsp-multitenancy
21 20 json api and rsp multitenancy
2 parents 7c7c846 + 439c5eb commit 3d3863f

27 files changed

Lines changed: 927 additions & 95 deletions

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,7 @@ diffo-*.tar
2424

2525
/.elixir_ls
2626

27-
.DS_Store
27+
.DS_Store
28+
29+
# Agent related
30+
.claude/*

CHANGELOG.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,16 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline
3131
### Fixes:
3232
* fixed relationship enrichment inconsistent across neo4j versions
3333

34-
## [v0.2.0](https://github.com/diffo-dev/diffo/compare/v0.0.4..v0.2.0) (2026-04-24)
34+
## [v0.2.0](https://github.com/diffo-dev/diffo/compare/v0.0.4..v0.2.0) (2026-04-26)
3535

3636
### Maintenance:
3737
* updated to diffo 0.2.0
3838

3939
### Features:
4040
* 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
41+
* JSON API via AshJsonApi and Plug.Cowboy
42+
* RSP resource with AshStateMachine lifecycle (inactive/active/suspended) and Ash Policy authorisation
43+
* RSP multi-tenancy: SetRspId change, OwnedByActor and NoActor policy checks, RspOwnership macro shared across RSP-owned resources
44+
* NTD and UNI modelled as NBN-owned infrastructure — readable by any RSP, mutable only by internal calls
45+
* Interactive NBN livebook with Kino RSP selector and actor-scoped provisioning flow
46+
* NBN domain documentation including Perentie ecosystem narrative

README.md

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,24 @@ SPDX-License-Identifier: MIT
88

99
[![Module Version](https://img.shields.io/hexpm/v/diffo)](https://hex.pm/packages/diffo_example)
1010
[![Hex Docs](https://img.shields.io/badge/hex-docs-lightgreen)](https://hexdocs.pm/diffo_example/)
11-
[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2Fdiffo-dev%2Fdiffo_example%2Fblob%2Fmain%2Fdiffo_example.livemd)
1211
[![License](https://img.shields.io/hexpm/l/diffo)](https://github.com/diffo-dev/diffo_example/blob/master/LICENSES/MIT.md)
1312
[![REUSE status](https://api.reuse.software/badge/github.com/diffo-dev/diffo_example)](https://api.reuse.software/info/github.com/diffo-dev/diffo_example)
1413

15-
This repo contains Diffo Examples.
16-
1714
[Diffo](https://github.com/diffo-dev/diffo) is a Telecommunications Management Forum (TMF) Service and Resource Manager, built for autonomous networks.
1815

16+
This repo contains two independent example domains, each modelling a different slice of a telco network.
17+
18+
## NBN Domain
19+
20+
A declarative model of a fictional NBN Ethernet access hierarchy — NbnEthernet, UNI, AVC, NTD, CVC, NNI Group, and NNI — built entirely with the Diffo Provider Instance DSL. Includes multi-tenancy via Ash Policy: each RSP can only see and manage the resources they own.
21+
22+
[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2Fdiffo-dev%2Fdiffo_example%2Fblob%2Fdev%2Fdocumentation%2Fdomains%2Fdiffo_example_nbn.livemd)
23+
24+
The livebook walks through provisioning a complete NBN Ethernet access circuit, selecting an RSP to operate as, and demonstrating how the `mine` actions propagate technology, speeds, CVLAN, and port assignments up the resource hierarchy.
25+
26+
## Access Domain
27+
28+
A copper-network equivalent covering DSL access services — Cable, Card, Path, and Shelf. Explore `lib/access/` for the domain model.
1929

2030
## Installation
2131

@@ -32,13 +42,6 @@ end
3242

3343
You need [Neo4j](https://github.com/neo4j/neo4j) available. We recommend the Neo4j Community 5 latest, available at [Neo4j Deploymnent Centre](https://neo4j.com/deployment-center/) which can be installed locally. You can also configure connection to a cloud based database service such as [Neo4j AuraDB](https://neo4j.com/product/auradb/).
3444

35-
## Tutorial
36-
37-
Click the **Run in Livebook** badge above to open the interactive tutorial, or find it at [diffo_example.livemd](diffo_example.livemd).
38-
39-
The diffo_example livebook walks through provisioning a complete NBN Ethernet access circuit — NTD, UNI, AVC, CVC, NNI Group, and NNI — showing how the `mine` actions propagate technology, speeds, CVLAN, and port assignments up the resource hierarchy.
40-
41-
4245
## Contributions
4346

4447
Contributions are welcome, please start with an [issue](https://github.com/diffo-dev/diffo_example/issues)

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 renamed to documentation/domains/diffo_example_nbn.livemd

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

3856
## Installing Neo4j and Configuring Bolty
3957

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

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-
```
60+
You need [Neo4j](https://neo4j.com/deployment-center/) installed and running. Verify the connection:
5961

6062
```elixir
6163
AshNeo4j.BoltyHelper.is_connected()
@@ -69,42 +71,28 @@ It is helpful to have a Neo4j browser open locally, typically at http://localhos
6971
AshNeo4j.Neo4jHelper.delete_all()
7072
```
7173

72-
## Setup Aliases
73-
74-
```elixir
75-
require Ash.Query
76-
alias DiffoExample.Nbn
77-
alias DiffoExample.Nbn.NbnEthernet
78-
alias DiffoExample.Nbn.Uni
79-
alias DiffoExample.Nbn.Avc
80-
alias DiffoExample.Nbn.Ntd
81-
alias DiffoExample.Nbn.Cvc
82-
alias DiffoExample.Nbn.NniGroup
83-
alias DiffoExample.Nbn.Nni
84-
alias DiffoExample.Nbn.Technology
85-
alias DiffoExample.Nbn.Speeds
86-
import Jason, only: [encode: 2]
87-
```
88-
89-
## About NBN
74+
## About NBN Co
9075

9176
NBN (National Broadband Network) is Australia's wholesale fixed-line access network, operated by NBN Co. It provides standardised access products to Retail Service Providers (RSPs), who in turn deliver internet and other services to end customers.
9277

78+
For the purpose of this example we are going to refer to a simplified, and re-imagined NBN Co as NBN.
79+
9380
An RSP typically combines:
9481

9582
* An **NBN Ethernet** access circuit (UNI + AVC) at the customer premises — the access and aggregation layer modelled in this domain
9683
* A **home gateway** device installed at the UNI, which provides the customer's LAN, Wi-Fi, and sometimes voice
9784
* Transport, aggregation, and edge infrastructure connecting the NNI to the RSP's network and on to the internet
9885

99-
NBN Co connects the customer premises to the RSP's network via a Point of Interconnect (POI). The NNI sits at the POI, grouped into NNI Groups. AVCs carrying customer traffic are aggregated onto a CVC, which terminates at the NNI Group. The RSP purchases CVC capacity to carry the aggregate traffic of its customers at that POI.
86+
NBN connects the customer premises to the RSP's network via a Point of Interconnect (POI). The NNI sits at the POI, grouped into NNI Groups. AVCs carrying customer traffic are aggregated onto a CVC, which terminates at the NNI Group. The RSP purchases CVC capacity to carry the aggregate traffic of its customers at that POI.
10087

101-
NBN is delivered over several access technologies — FTTP, FTTN, FTTB, FTTC, HFC, Fixed Wireless, and Satellite — which determine which bandwidth profiles and speeds are available to a given premises.
88+
NBN delivers over several access technologies — FTTP, FTTN, FTTB, FTTC, HFC, Fixed Wireless, and Satellite — which determine which bandwidth profiles and speeds are available to a given premises.
10289

103-
## Technology and Speeds
90+
## NBN Ethernet Technology and Speeds
10491

10592
The NBN domain defines Technology as an Ash Enum covering all NBN access types:
10693

10794
```elixir
95+
alias DiffoExample.Nbn.{Technology,Speeds}
10896
Technology.values()
10997
```
11098

@@ -127,53 +115,86 @@ Speeds.speeds(:wireless_superfast, :FixedWireless)
127115
Speeds.speeds(:home_fast, :FixedWireless)
128116
```
129117

130-
## Building the Network Hinterland
118+
## Multi-tenancy
119+
120+
Each RSP operates in isolation — they can only see and manage the resources they own. This multi-tenancy is enforced at the Ash policy layer: every NBN resource is stamped with the owning RSP's id at creation, and subsequent reads, updates, and destroys are scoped to the record owner.
121+
122+
Select the RSP you want to operate as for the rest of this livebook. All resources you build will be owned by that RSP and isolated from resources owned by others.
123+
124+
```elixir
125+
alias DiffoExample.Nbn
126+
alias DiffoExample.Nbn.Rsp
127+
import Jason, only: [encode: 2]
128+
DiffoExample.Nbn.Initializer.init()
129+
rsps = Nbn.list_rsps!()
130+
Kino.DataTable.new(rsps, keys: [:epid, :name, :short_name, :state])
131+
```
132+
133+
```elixir
134+
rsp_input = Kino.Input.select(
135+
"Operate as RSP",
136+
Enum.map(rsps, fn rsp -> {rsp.name, Atom.to_string(rsp.short_name)} end)
137+
)
138+
```
139+
140+
```elixir
141+
actor = Enum.find(rsps, fn rsp -> rsp.name == Kino.Input.read(rsp_input) end)
142+
actor
143+
```
144+
145+
## Maintaining Shareable Resources
146+
147+
As an RSP we need maintain some shareable network resources: NNI, NNI Group, and CVC.
131148

132-
Before we can provision an NBN Ethernet access we need the shared network resources: NNI, NNI Group, and CVC.
149+
We'll need these everywhere we operate, in advance of and sufficient for all the NBN Ethernet Accesses we have. We'll just build one of each right now.
133150

134-
Build an NNI — the physical interconnect between the RSP and NBN Co:
151+
Build an NNI — the physical interconnect between the RSP and NBN:
135152

136153
```elixir
137-
nni = Nbn.build_nni!(%{})
154+
alias DiffoExample.Nbn.{Nni, NniGroup, CVC}
155+
nni = Nbn.build_nni!(%{}, actor: actor)
138156
nni |> Jason.encode!(pretty: true) |> IO.puts
139157
```
140158

141159
Build an NNI Group — a logical grouping of NNIs at a point of interconnect:
142160

143161
```elixir
144-
nni_group = Nbn.build_nni_group!(%{})
162+
nni_group = Nbn.build_nni_group!(%{}, actor: actor)
145163
nni_group |> Jason.encode!(pretty: true) |> IO.puts
146164
```
147165

148166
Define the NNI Group with an SVLAN assignment and relate the NNI:
149167

150168
```elixir
151-
nni_group = Nbn.define_nni_group!(%{
169+
nni_group = Nbn.define_nni_group!(nni_group, %{
152170
characteristic_value_updates: [nni_group: [svlan: 100]]
153-
})
171+
}, actor: actor)
154172
nni_group = Nbn.relate_nni_group!(nni_group, %{
155-
relationships: [%{alias: :nni, target_id: nni.id, type: :isAssigned}]
156-
})
173+
relationships: [%Diffo.Provider.Instance.Relationship{id: nni.id, alias: :nni, type: :isAssigned}]
174+
}, actor: actor)
157175
nni_group |> Jason.encode!(pretty: true) |> IO.puts
158176
```
159177

160178
Build a CVC — the aggregation virtual circuit that terminates at the NNI Group:
161179

162180
```elixir
163-
cvc = Nbn.build_cvc!(%{})
181+
cvc = Nbn.build_cvc!(%{}, actor: actor)
164182
cvc = Nbn.relate_cvc!(cvc, %{
165-
relationships: [%{alias: :nni_group, target_id: nni_group.id, type: :isAssigned}]
166-
})
183+
relationships: [%Diffo.Provider.Instance.Relationship{id: nni_group.id, alias: :nni_group, type: :isAssigned}]
184+
}, actor: actor)
167185
cvc |> Jason.encode!(pretty: true) |> IO.puts
168186
```
169187

170-
## Provisioning an NBN Ethernet Access
188+
## Provisioning NBN Ethernet
171189

172-
With the hinterland in place we can provision a customer-facing NBN Ethernet access.
190+
For each customer site we want to provide service to, we need an NBN Ethernet composite resource, involving an NTD, UNI, AVC and CVC.
191+
192+
The NTD is NBN infrastructure — built and managed by NBN, visible to any RSP. It may not exist at a new or existing customer site, so may be built on demand by NBN.
173193

174194
Build an NTD — the device installed at the customer premises:
175195

176196
```elixir
197+
alias DiffoExample.Nbn.{Ntd, Uni, Avc, NbnEthernet}
177198
ntd = Nbn.build_ntd!(%{})
178199
ntd = Nbn.define_ntd!(ntd, %{
179200
characteristic_value_updates: [ntd: [technology: :FTTP, ports: [1, 2, 3, 4]]]
@@ -187,7 +208,7 @@ Build a UNI — the interface at the customer premises — and assign a port fro
187208
uni = Nbn.build_uni!(%{})
188209
alias Diffo.Provider.Assignment
189210
ntd = Nbn.assign_port!(ntd, %{
190-
assignment: %Assignment{assignee_id: uni.id, value: 1}
211+
assignment: %Assignment{assignee_id: uni.id, operation: :auto_assign}
191212
})
192213
ntd |> Jason.encode!(pretty: true) |> IO.puts
193214
```
@@ -196,7 +217,7 @@ Relate the UNI back to the NTD so it can mine technology and port from it:
196217

197218
```elixir
198219
uni = Nbn.relate_uni!(uni, %{
199-
relationships: [%{alias: :ntd, target_id: ntd.id, type: :isAssigned}]
220+
relationships: [%Diffo.Provider.Instance.Relationship{id: ntd.id, alias: :ntd, type: :isAssigned}]
200221
})
201222
uni = Nbn.mine_uni!(uni, %{})
202223
uni |> Jason.encode!(pretty: true) |> IO.puts
@@ -205,28 +226,28 @@ uni |> Jason.encode!(pretty: true) |> IO.puts
205226
Build an AVC and assign it a CVLAN from the CVC:
206227

207228
```elixir
208-
avc = Nbn.build_avc!(%{})
229+
avc = Nbn.build_avc!(%{}, actor: actor)
209230
avc = Nbn.define_avc!(avc, %{
210231
characteristic_value_updates: [avc: [bandwidth_profile: :home_ultrafast]]
211-
})
232+
}, actor: actor)
212233
cvc = Nbn.assign_cvlan!(cvc, %{
213-
assignment: %Assignment{assignee_id: avc.id, value: 200}
214-
})
215-
avc = Nbn.mine_avc!(avc, %{})
234+
assignment: %Assignment{assignee_id: avc.id, operation: :auto_assign}
235+
}, actor: actor)
236+
avc = Nbn.mine_avc!(avc, %{}, actor: actor)
216237
avc |> Jason.encode!(pretty: true) |> IO.puts
217238
```
218239

219240
Now build the top-level NBN Ethernet access and relate it to both the UNI and AVC:
220241

221242
```elixir
222-
pri = Nbn.build_nbn_ethernet!(%{})
243+
pri = Nbn.build_nbn_ethernet!(%{}, actor: actor)
223244
pri = Nbn.relate_nbn_ethernet!(pri, %{
224245
relationships: [
225-
%{alias: :uni, target_id: uni.id, type: :isAssigned},
226-
%{alias: :avc, target_id: avc.id, type: :isAssigned}
246+
%Diffo.Provider.Instance.Relationship{id: uni.id, alias: :uni, type: :isAssigned},
247+
%Diffo.Provider.Instance.Relationship{id: avc.id, alias: :avc, type: :isAssigned}
227248
]
228-
})
229-
pri = Nbn.mine_nbn_ethernet!(pri, %{})
249+
}, actor: actor)
250+
pri = Nbn.mine_nbn_ethernet!(pri, %{}, actor: actor)
230251
pri |> Jason.encode!(pretty: true) |> IO.puts
231252
```
232253

@@ -246,6 +267,28 @@ Or from Elixir:
246267
AshNeo4j.Cypher.run("MATCH (n1)-[r]->(n2) RETURN r, n1, n2 LIMIT 50")
247268
```
248269

270+
## JSON API
271+
272+
The NBN domain exposes a JSON API via `Plug.Cowboy` on port 4000. Start the server in your application before evaluating these cells.
273+
274+
First check the catalog — all NBN specifications are initialised on startup:
275+
276+
```elixir
277+
Req.get!("http://localhost:4000/catalog", decode_body: false).body |> IO.puts()
278+
```
279+
280+
Now retrieve all NBN Ethernet instances:
281+
282+
```elixir
283+
Req.get!("http://localhost:4000/nbnEthernet", decode_body: false).body |> IO.puts()
284+
```
285+
286+
Or fetch the one we provisioned above by id:
287+
288+
```elixir
289+
Req.get!("http://localhost:4000/nbnEthernet/#{pri.id}", decode_body: false).body |> IO.puts()
290+
```
291+
249292
## What Next?
250293

251294
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.

documentation/domains/nbn.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<!--
2+
SPDX-FileCopyrightText: 2025 diffo_example contributors <https://github.com/diffo-dev/diffo_example/graphs.contributors>
3+
4+
SPDX-License-Identifier: MIT
5+
-->
6+
7+
# The NBN Domain
8+
9+
## The Perentie Ecosystem
10+
11+
NBN Co operates as **Perentie** — Australia's largest monitor lizard, ancient and continent-wide. Perentie owns the territory. It does not compete with the animals moving through its country; it simply defines the ground they all walk on.
12+
13+
The RSPs are the spirit animals of the ecosystem, each finding their niche in Perentie's range:
14+
15+
| RSP | Spirit Animal | Inspiration |
16+
| ------------------ | ------------------ | ------------------------------------------------------------------------------------------------------------------------ |
17+
| Wedge-tail Telecom | Wedge-tailed Eagle | Australia's apex aerial predator — dominant, territorial, commands every landscape it surveys |
18+
| Quokka Connect | Quokka | Famously friendly, genuinely Australian, radiates good energy — operates in WA under bilateral agreement with Perentie |
19+
| Ibis Telecom | White Ibis | Beloved in spite of its reputation, scrappy, surprisingly capable |
20+
| Taipan Group | Taipan | Carries the TPG initials; fast, precise, not to be underestimated |
21+
| Echidna Networks | Echidna | Prickly on the surface, uniquely capable beneath it |
22+
| Dugong Digital | Dugong | Slow and steady, but still very much alive |
23+
| Lyrebird | Lyrebird | Mimics everything, loops back on itself, endlessly clever |

0 commit comments

Comments
 (0)