ignition_core/client/gan.rs
1//! Gateway Area Network overview model (09-04, EXT-02) — `GET
2//! /data/api/v1/overview/gan`, the ZERO-CONNECTION body live-captured
3//! identically on both rigs (09-LIVE-CAPTURES §4: 8.3.6 rig A + 8.3.3
4//! rig B). A fresh non-GAN gateway's zero-connection shape IS the
5//! canonical capture — 5 flat scalars, no arrays, no nesting.
6//!
7//! **Number shapes (capture-locked):** the connection counts and
8//! `remoteGateways` serialize as JSON **ints** (`0`); the byte rates
9//! serialize as JSON **floats** (`0.0`) — so counts are integer-typed
10//! and rates are `f64` (which parses BOTH forms; a point release that
11//! switches a rate to whole-number JSON can never fail the parse).
12//!
13//! Decision 4 of the phase: `overview/gan` ONLY — no
14//! `/gateway-network/gateways` companion this phase; `ign api call`
15//! covers that list.
16
17use std::collections::BTreeMap;
18
19use serde::{Deserialize, Serialize};
20
21/// GET path of the GAN overview capability (83-api collection + live
22/// capture).
23pub const GAN_OVERVIEW_PATH: &str = "/data/api/v1/overview/gan";
24
25/// GET `/data/api/v1/overview/gan` — the 5-field GAN summary.
26/// Live-captured 200 on BOTH rigs; zero drift.
27#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28pub struct GanStatusWire {
29 /// Total GAN connections — `0` on both captures (JSON int); a
30 /// non-GAN gateway's steady state IS zero (the capture IS the
31 /// meaning: zeros are healthy data, not absence of feature).
32 #[serde(rename = "totalConnections", alias = "total_connections", default)]
33 pub total_connections: i64,
34 /// Currently-running connections — `0` on both captures (JSON int).
35 #[serde(rename = "runningConnections", alias = "running_connections", default)]
36 pub running_connections: i64,
37 /// Outbound byte rate — `0.0` on both captures (JSON FLOAT; f64
38 /// parses both the float and a hypothetical whole-number form).
39 #[serde(rename = "outgoingByteRate", alias = "outgoing_byte_rate", default)]
40 pub outgoing_byte_rate: f64,
41 /// Inbound byte rate — `0.0` on both captures (JSON float).
42 #[serde(rename = "incomingByteRate", alias = "incoming_byte_rate", default)]
43 pub incoming_byte_rate: f64,
44 /// Remote gateways known to this one — `0` on both captures (JSON
45 /// int).
46 #[serde(rename = "remoteGateways", alias = "remote_gateways", default)]
47 pub remote_gateways: i64,
48 /// Unknown keys round-trip (version tolerance — the passthrough
49 /// proof fixture appends a fake key and rides it here).
50 #[serde(flatten)]
51 pub extra: BTreeMap<String, serde_json::Value>,
52}
53
54#[cfg(test)]
55mod tests {
56 use super::GanStatusWire;
57
58 /// THE live capture (IDENTICAL on both rigs, 09-LIVE-CAPTURES §4)
59 /// — the zero-connection body, floats spelled `0.0`.
60 #[test]
61 fn gan_parses_the_live_zero_connection_capture() {
62 let wire: GanStatusWire = serde_json::from_value(serde_json::json!({
63 "totalConnections": 0,
64 "runningConnections": 0,
65 "outgoingByteRate": 0.0,
66 "incomingByteRate": 0.0,
67 "remoteGateways": 0
68 }))
69 .expect("the live zero-connection shape must parse");
70 assert_eq!(wire.total_connections, 0);
71 assert_eq!(wire.running_connections, 0);
72 assert_eq!(wire.outgoing_byte_rate, 0.0);
73 assert_eq!(wire.incoming_byte_rate, 0.0);
74 assert_eq!(wire.remote_gateways, 0);
75 }
76
77 /// Whole-number rate forms AND integer/float mixes still parse
78 /// (f64 tolerance), a populated shape carries real values, and
79 /// unknown keys ride `extra` (the round-trip proof).
80 #[test]
81 fn gan_tolerant_over_populated_and_unknown_shapes() {
82 let wire: GanStatusWire = serde_json::from_value(serde_json::json!({
83 "totalConnections": 4,
84 "runningConnections": 2,
85 "outgoingByteRate": 15360,
86 "incomingByteRate": 2048.5,
87 "remoteGateways": 1,
88 "futurePointReleaseField": {"rate": 1.25}
89 }))
90 .expect("whole-number + unknown-key forms must parse");
91 assert_eq!(wire.total_connections, 4);
92 assert_eq!(wire.running_connections, 2);
93 assert_eq!(wire.outgoing_byte_rate, 15360.0, "f64 parses ints too");
94 assert_eq!(wire.incoming_byte_rate, 2048.5);
95 assert_eq!(wire.remote_gateways, 1);
96 assert_eq!(
97 wire.extra.get("futurePointReleaseField"),
98 Some(&serde_json::json!({"rate": 1.25})),
99 "unknown keys ride flatten passthrough"
100 );
101 }
102}