1use std::collections::BTreeMap;
22
23use serde::{Deserialize, Serialize};
24
25pub(crate) const DESIGNERS_PATH: &str = "/data/api/v1/designers";
27
28pub(crate) const PERSPECTIVE_SESSIONS_LIST_PATH: &str = "/data/perspective/api/v1/sessions/";
31
32pub(crate) const PERSPECTIVE_SESSIONS_TERMINATE_PATH: &str = "/data/perspective/api/v1/sessions";
35
36pub(crate) const VISION_CLIENTS_PATH: &str = "/data/vision/api/v1/clients";
38
39pub(crate) fn designer_prune_path(id: &str) -> String {
41 format!("/data/api/v1/designer/{id}")
42}
43
44pub(crate) fn vision_client_terminate_path(id: &str) -> String {
46 format!("/data/vision/api/v1/client/{id}")
47}
48
49#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
53pub struct DesignerInfo {
54 pub id: String,
56 #[serde(default)]
58 pub address: String,
59 #[serde(default)]
61 pub user: String,
62 #[serde(default)]
64 pub project: String,
65 #[serde(default)]
68 pub memory: serde_json::Value,
69 #[serde(default)]
72 pub uptime: i64,
73 #[serde(default)]
75 pub lastcomm: i64,
76 #[serde(default)]
78 pub timeout: i64,
79 #[serde(default)]
81 pub timezone: String,
82 #[serde(flatten)]
84 pub extra: BTreeMap<String, serde_json::Value>,
85}
86
87#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
91pub struct PerspectiveSession {
92 pub id: String,
94 #[serde(default)]
96 pub username: String,
97 #[serde(default)]
99 pub authorized: bool,
100 #[serde(default)]
102 pub project: String,
103 #[serde(
105 rename = "clientAddress",
106 alias = "client_address",
107 default,
108 skip_serializing_if = "String::is_empty"
109 )]
110 pub client_address: String,
111 #[serde(rename = "lastComm", alias = "last_comm", default)]
114 pub last_comm: i64,
115 #[serde(rename = "activePages", alias = "active_pages", default)]
117 pub active_pages: i64,
118 #[serde(
120 rename = "userAgent",
121 alias = "user_agent",
122 default,
123 skip_serializing_if = "String::is_empty"
124 )]
125 pub user_agent: String,
126 #[serde(flatten)]
129 pub extra: BTreeMap<String, serde_json::Value>,
130}
131
132#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
135pub struct VisionClient {
136 pub id: String,
138 #[serde(default)]
140 pub address: String,
141 #[serde(default)]
143 pub user: String,
144 #[serde(default)]
146 pub project: String,
147 #[serde(default)]
149 pub memory: serde_json::Value,
150 #[serde(default)]
152 pub uptime: i64,
153 #[serde(default)]
155 pub lastcomm: i64,
156 #[serde(default)]
158 pub timeout: i64,
159 #[serde(default)]
161 pub timezone: String,
162 #[serde(rename = "tagCount", alias = "tag_count", default)]
164 pub tag_count: i64,
165 #[serde(flatten)]
167 pub extra: BTreeMap<String, serde_json::Value>,
168}
169
170#[cfg(test)]
171mod tests {
172 use super::{DesignerInfo, PerspectiveSession, VisionClient};
173
174 #[test]
177 fn designer_info_parses_the_openapi_shape() {
178 let designer: DesignerInfo = serde_json::from_value(serde_json::json!({
179 "id": "d-1",
180 "user": "admin",
181 "uptime": 600000,
182 "lastcomm": 1787346747022i64,
183 "timeout": 3600000,
184 "memory": {"used": 268435456i64, "max": 1073741824i64},
185 "project": "MyProject",
186 "address": "192.168.1.50:52526",
187 "timezone": "America/New_York"
188 }))
189 .expect("openapi designer shape must parse");
190 assert_eq!(designer.id, "d-1");
191 assert_eq!(designer.user, "admin");
192 assert_eq!(designer.uptime, 600000, "epoch ms");
193 assert_eq!(
194 designer.memory["used"], 268435456i64,
195 "memory is an OBJECT (openapi) — passthrough"
196 );
197
198 let round = serde_json::to_value(&designer).expect("serialize");
200 assert_eq!(round["memory"]["max"], 1073741824i64);
201 assert_eq!(round["timezone"], "America/New_York");
202 }
203
204 #[test]
207 fn perspective_session_parses_the_camel_case_shape() {
208 let session: PerspectiveSession = serde_json::from_value(serde_json::json!({
209 "id": "psess-1",
210 "username": "admin",
211 "authorized": true,
212 "project": "MyProject",
213 "clientAddress": "10.0.0.5",
214 "lastComm": 1787346747022i64,
215 "sessionScope": "G",
216 "activePages": 2,
217 "pageIds": ["viewA", "viewB"],
218 "recentBytesSent": 1024,
219 "totalBytesSent": 4096,
220 "userAgent": "Mozilla/5.0"
221 }))
222 .expect("perspective capture shape must parse");
223 assert_eq!(session.id, "psess-1");
224 assert_eq!(session.client_address, "10.0.0.5", "clientAddress rename");
225 assert_eq!(session.last_comm, 1787346747022, "lastComm rename");
226 assert_eq!(session.active_pages, 2, "activePages rename");
227 assert_eq!(session.user_agent, "Mozilla/5.0", "userAgent rename");
228 assert_eq!(
229 session.extra.get("sessionScope"),
230 Some(&serde_json::json!("G")),
231 "unmodeled known keys round-trip"
232 );
233
234 let round = serde_json::to_value(&session).expect("serialize");
235 assert_eq!(round["clientAddress"], "10.0.0.5");
236 assert_eq!(round["userAgent"], "Mozilla/5.0");
237 }
238
239 #[test]
241 fn vision_client_parses_designer_shape_plus_tag_count() {
242 let client: VisionClient = serde_json::from_value(serde_json::json!({
243 "id": "v-1",
244 "user": "operator",
245 "uptime": 120000,
246 "lastcomm": 1787346747022i64,
247 "timeout": 3600000,
248 "memory": {"used": 134217728i64, "max": 536870912i64},
249 "project": "PlantFloor",
250 "address": "10.0.0.9:443",
251 "timezone": "UTC",
252 "tagCount": 1523
253 }))
254 .expect("vision capture shape must parse");
255 assert_eq!(client.id, "v-1");
256 assert_eq!(client.tag_count, 1523, "tagCount rename");
257
258 let round = serde_json::to_value(&client).expect("serialize");
259 assert_eq!(round["tagCount"], 1523, "gateway-native key on the way out");
260 }
261}