Skip to content

Commit 3273a61

Browse files
Merge pull request #23 from diffo-dev/dev
release 0.2.0
2 parents 0ebdc3d + 3d3863f commit 3273a61

61 files changed

Lines changed: 3084 additions & 167 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.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: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,4 +29,18 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline
2929
## [v0.0.4](https://github.com/diffo-dev/diffo/compare/v0.0.3..v0.0.4) (2026-03-19)
3030

3131
### Fixes:
32-
* fixed relationship enrichment inconsistent across neo4j versions
32+
* fixed relationship enrichment inconsistent across neo4j versions
33+
34+
## [v0.2.0](https://github.com/diffo-dev/diffo/compare/v0.0.4..v0.2.0) (2026-04-26)
35+
36+
### Maintenance:
37+
* updated to diffo 0.2.0
38+
39+
### Features:
40+
* new NBN domain modelling NBN Ethernet access and constituent resources (UNI, AVC, NTD, CVC, NNI Group, NNI)
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 & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,21 @@ SPDX-License-Identifier: MIT
1111
[![License](https://img.shields.io/hexpm/l/diffo)](https://github.com/diffo-dev/diffo_example/blob/master/LICENSES/MIT.md)
1212
[![REUSE status](https://api.reuse.software/badge/github.com/diffo-dev/diffo_example)](https://api.reuse.software/info/github.com/diffo-dev/diffo_example)
1313

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

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

1930
## Installation
2031

@@ -31,11 +42,6 @@ end
3142

3243
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/).
3344

34-
## Tutorial
35-
36-
Diffo has a livebook and you should use this as an introduction.
37-
38-
3945
## Contributions
4046

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

config/config.exs

Lines changed: 2 additions & 1 deletion
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,
@@ -37,5 +38,5 @@ config :spark,
3738
]
3839

3940
config :diffo, ash_domains: [Diffo.Provider]
40-
config :diffo_example, ash_domains: [DiffoExample.Access]
41+
config :diffo_example, ash_domains: [DiffoExample.Access, DiffoExample.Nbn]
4142
import_config "#{config_env()}.exs"
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
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+
# Diffo Example - NBN Domain
8+
9+
```elixir
10+
Mix.install(
11+
[
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+
]
31+
],
32+
consolidate_protocols: false
33+
)
34+
```
35+
36+
## Overview
37+
38+
[Diffo](https://github.com/diffo-dev/diffo) is a Telecommunications Management Forum (TMF) Service and Resource Manager, built for autonomous networks. It is implemented using the [Ash Framework](https://www.ash-hq.org) and stores data in Neo4j via [AshNeo4j](https://github.com/diffo-dev/ash_neo4j).
39+
40+
If you are new to Diffo, start with the [Diffo livebook](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2Fdiffo-dev%2Fdiffo%2Fblob%2Fdev%2Fdiffo.livemd) which introduces the core Provider concepts — Specification, Instance, Feature, Characteristic, Party, Place, and Relationship.
41+
42+
Diffo includes a Provider Instance extension that lets you declare specialised TMF Services and Resources using a Spark DSL with very little Elixir code. The [Provider Instance Extension livebook](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2Fdiffo-dev%2Fdiffo%2Fblob%2Fdev%2Fdocumentation%2Fhow_to%2Fuse_diffo_provider_instance_extension.livemd) covers this in detail.
43+
44+
The NBN domain in this example was built entirely with that DSL — a declarative model of a realistic NBN Ethernet access hierarchy with minimal custom Elixir, derived from a short domain description. It demonstrates how much can be expressed through the Provider extension alone.
45+
46+
The NBN domain models a fictional NBN Ethernet access circuit and its constituent resources:
47+
48+
* **NbnEthernet** — the parent circuit resource (identified by a PRI)
49+
* **UNI** — User Network Interface at the customer premises
50+
* **AVC** — Access Virtual Circuit (dedicated, carries traffic between UNI and CVC)
51+
* **NTD** — Network Termination Device (installed at customer premises, assigns ports to UNI)
52+
* **CVC** — Connectivity Virtual Circuit (aggregates AVCs, terminates at NNI Group)
53+
* **NNI Group** — group of NNIs at the point of interconnect
54+
* **NNI** — Network-to-Network Interface
55+
56+
## Installing Neo4j and Configuring Bolty
57+
58+
Bolty is configured in the `Mix.install` block above — update the Neo4j credentials there if needed before evaluating.
59+
60+
You need [Neo4j](https://neo4j.com/deployment-center/) installed and running. Verify the connection:
61+
62+
```elixir
63+
AshNeo4j.BoltyHelper.is_connected()
64+
```
65+
66+
It is helpful to have a Neo4j browser open locally, typically at http://localhost:7474/browser/
67+
68+
**OPTIONAL** Clear the database before starting:
69+
70+
```elixir
71+
AshNeo4j.Neo4jHelper.delete_all()
72+
```
73+
74+
## About NBN Co
75+
76+
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.
77+
78+
For the purpose of this example we are going to refer to a simplified, and re-imagined NBN Co as NBN.
79+
80+
An RSP typically combines:
81+
82+
* An **NBN Ethernet** access circuit (UNI + AVC) at the customer premises — the access and aggregation layer modelled in this domain
83+
* A **home gateway** device installed at the UNI, which provides the customer's LAN, Wi-Fi, and sometimes voice
84+
* Transport, aggregation, and edge infrastructure connecting the NNI to the RSP's network and on to the internet
85+
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.
87+
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.
89+
90+
## NBN Ethernet Technology and Speeds
91+
92+
The NBN domain defines Technology as an Ash Enum covering all NBN access types:
93+
94+
```elixir
95+
alias DiffoExample.Nbn.{Technology,Speeds}
96+
Technology.values()
97+
```
98+
99+
Speeds are derived from a bandwidth_profile and technology combination. For example:
100+
101+
```elixir
102+
Speeds.speeds(:home_fast, :FTTP)
103+
```
104+
105+
```elixir
106+
Speeds.speeds(:home_hyperfast, :HFC)
107+
```
108+
109+
```elixir
110+
Speeds.speeds(:wireless_superfast, :FixedWireless)
111+
```
112+
113+
```elixir
114+
# returns :error for invalid combinations
115+
Speeds.speeds(:home_fast, :FixedWireless)
116+
```
117+
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.
148+
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.
150+
151+
Build an NNI — the physical interconnect between the RSP and NBN:
152+
153+
```elixir
154+
alias DiffoExample.Nbn.{Nni, NniGroup, CVC}
155+
nni = Nbn.build_nni!(%{}, actor: actor)
156+
nni |> Jason.encode!(pretty: true) |> IO.puts
157+
```
158+
159+
Build an NNI Group — a logical grouping of NNIs at a point of interconnect:
160+
161+
```elixir
162+
nni_group = Nbn.build_nni_group!(%{}, actor: actor)
163+
nni_group |> Jason.encode!(pretty: true) |> IO.puts
164+
```
165+
166+
Define the NNI Group with an SVLAN assignment and relate the NNI:
167+
168+
```elixir
169+
nni_group = Nbn.define_nni_group!(nni_group, %{
170+
characteristic_value_updates: [nni_group: [svlan: 100]]
171+
}, actor: actor)
172+
nni_group = Nbn.relate_nni_group!(nni_group, %{
173+
relationships: [%Diffo.Provider.Instance.Relationship{id: nni.id, alias: :nni, type: :isAssigned}]
174+
}, actor: actor)
175+
nni_group |> Jason.encode!(pretty: true) |> IO.puts
176+
```
177+
178+
Build a CVC — the aggregation virtual circuit that terminates at the NNI Group:
179+
180+
```elixir
181+
cvc = Nbn.build_cvc!(%{}, actor: actor)
182+
cvc = Nbn.relate_cvc!(cvc, %{
183+
relationships: [%Diffo.Provider.Instance.Relationship{id: nni_group.id, alias: :nni_group, type: :isAssigned}]
184+
}, actor: actor)
185+
cvc |> Jason.encode!(pretty: true) |> IO.puts
186+
```
187+
188+
## Provisioning NBN Ethernet
189+
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.
193+
194+
Build an NTD — the device installed at the customer premises:
195+
196+
```elixir
197+
alias DiffoExample.Nbn.{Ntd, Uni, Avc, NbnEthernet}
198+
ntd = Nbn.build_ntd!(%{})
199+
ntd = Nbn.define_ntd!(ntd, %{
200+
characteristic_value_updates: [ntd: [technology: :FTTP, ports: [1, 2, 3, 4]]]
201+
})
202+
ntd |> Jason.encode!(pretty: true) |> IO.puts
203+
```
204+
205+
Build a UNI — the interface at the customer premises — and assign a port from the NTD:
206+
207+
```elixir
208+
uni = Nbn.build_uni!(%{})
209+
alias Diffo.Provider.Assignment
210+
ntd = Nbn.assign_port!(ntd, %{
211+
assignment: %Assignment{assignee_id: uni.id, operation: :auto_assign}
212+
})
213+
ntd |> Jason.encode!(pretty: true) |> IO.puts
214+
```
215+
216+
Relate the UNI back to the NTD so it can mine technology and port from it:
217+
218+
```elixir
219+
uni = Nbn.relate_uni!(uni, %{
220+
relationships: [%Diffo.Provider.Instance.Relationship{id: ntd.id, alias: :ntd, type: :isAssigned}]
221+
})
222+
uni = Nbn.mine_uni!(uni, %{})
223+
uni |> Jason.encode!(pretty: true) |> IO.puts
224+
```
225+
226+
Build an AVC and assign it a CVLAN from the CVC:
227+
228+
```elixir
229+
avc = Nbn.build_avc!(%{}, actor: actor)
230+
avc = Nbn.define_avc!(avc, %{
231+
characteristic_value_updates: [avc: [bandwidth_profile: :home_ultrafast]]
232+
}, actor: actor)
233+
cvc = Nbn.assign_cvlan!(cvc, %{
234+
assignment: %Assignment{assignee_id: avc.id, operation: :auto_assign}
235+
}, actor: actor)
236+
avc = Nbn.mine_avc!(avc, %{}, actor: actor)
237+
avc |> Jason.encode!(pretty: true) |> IO.puts
238+
```
239+
240+
Now build the top-level NBN Ethernet access and relate it to both the UNI and AVC:
241+
242+
```elixir
243+
pri = Nbn.build_nbn_ethernet!(%{}, actor: actor)
244+
pri = Nbn.relate_nbn_ethernet!(pri, %{
245+
relationships: [
246+
%Diffo.Provider.Instance.Relationship{id: uni.id, alias: :uni, type: :isAssigned},
247+
%Diffo.Provider.Instance.Relationship{id: avc.id, alias: :avc, type: :isAssigned}
248+
]
249+
}, actor: actor)
250+
pri = Nbn.mine_nbn_ethernet!(pri, %{}, actor: actor)
251+
pri |> Jason.encode!(pretty: true) |> IO.puts
252+
```
253+
254+
The `mine` action on NbnEthernet extracts technology from the UNI and bandwidth_profile from the AVC and derives the speeds automatically.
255+
256+
## Exploring the Graph
257+
258+
You can query all nodes and relationships in Neo4j browser with:
259+
260+
```cypher
261+
MATCH (n1)-[r]->(n2) RETURN r, n1, n2 LIMIT 50
262+
```
263+
264+
Or from Elixir:
265+
266+
```elixir
267+
AshNeo4j.Cypher.run("MATCH (n1)-[r]->(n2) RETURN r, n1, n2 LIMIT 50")
268+
```
269+
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+
292+
## What Next?
293+
294+
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.
295+
296+
The Access domain in `diffo_example` shows a similar pattern for DSL access services. Explore `lib/access/` for copper-network equivalents (Cable, Card, Path, Shelf).
297+
298+
If you find Diffo useful please visit and star on [GitHub](https://github.com/diffo-dev/diffo/).

0 commit comments

Comments
 (0)