use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
pub(crate) const DESIGNERS_PATH: &str = "/data/api/v1/designers";
pub(crate) const PERSPECTIVE_SESSIONS_LIST_PATH: &str = "/data/perspective/api/v1/sessions/";
pub(crate) const PERSPECTIVE_SESSIONS_TERMINATE_PATH: &str = "/data/perspective/api/v1/sessions";
pub(crate) const VISION_CLIENTS_PATH: &str = "/data/vision/api/v1/clients";
pub(crate) fn designer_prune_path(id: &str) -> String {
format!("/data/api/v1/designer/{id}")
}
pub(crate) fn vision_client_terminate_path(id: &str) -> String {
format!("/data/vision/api/v1/client/{id}")
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DesignerInfo {
pub id: String,
#[serde(default)]
pub address: String,
#[serde(default)]
pub user: String,
#[serde(default)]
pub project: String,
#[serde(default)]
pub memory: serde_json::Value,
#[serde(default)]
pub uptime: i64,
#[serde(default)]
pub lastcomm: i64,
#[serde(default)]
pub timeout: i64,
#[serde(default)]
pub timezone: String,
#[serde(flatten)]
pub extra: BTreeMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PerspectiveSession {
pub id: String,
#[serde(default)]
pub username: String,
#[serde(default)]
pub authorized: bool,
#[serde(default)]
pub project: String,
#[serde(
rename = "clientAddress",
alias = "client_address",
default,
skip_serializing_if = "String::is_empty"
)]
pub client_address: String,
#[serde(rename = "lastComm", alias = "last_comm", default)]
pub last_comm: i64,
#[serde(rename = "activePages", alias = "active_pages", default)]
pub active_pages: i64,
#[serde(
rename = "userAgent",
alias = "user_agent",
default,
skip_serializing_if = "String::is_empty"
)]
pub user_agent: String,
#[serde(flatten)]
pub extra: BTreeMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct VisionClient {
pub id: String,
#[serde(default)]
pub address: String,
#[serde(default)]
pub user: String,
#[serde(default)]
pub project: String,
#[serde(default)]
pub memory: serde_json::Value,
#[serde(default)]
pub uptime: i64,
#[serde(default)]
pub lastcomm: i64,
#[serde(default)]
pub timeout: i64,
#[serde(default)]
pub timezone: String,
#[serde(rename = "tagCount", alias = "tag_count", default)]
pub tag_count: i64,
#[serde(flatten)]
pub extra: BTreeMap<String, serde_json::Value>,
}
#[cfg(test)]
mod tests {
use super::{DesignerInfo, PerspectiveSession, VisionClient};
#[test]
fn designer_info_parses_the_openapi_shape() {
let designer: DesignerInfo = serde_json::from_value(serde_json::json!({
"id": "d-1",
"user": "admin",
"uptime": 600000,
"lastcomm": 1787346747022i64,
"timeout": 3600000,
"memory": {"used": 268435456i64, "max": 1073741824i64},
"project": "MyProject",
"address": "192.168.1.50:52526",
"timezone": "America/New_York"
}))
.expect("openapi designer shape must parse");
assert_eq!(designer.id, "d-1");
assert_eq!(designer.user, "admin");
assert_eq!(designer.uptime, 600000, "epoch ms");
assert_eq!(
designer.memory["used"], 268435456i64,
"memory is an OBJECT (openapi) — passthrough"
);
let round = serde_json::to_value(&designer).expect("serialize");
assert_eq!(round["memory"]["max"], 1073741824i64);
assert_eq!(round["timezone"], "America/New_York");
}
#[test]
fn perspective_session_parses_the_camel_case_shape() {
let session: PerspectiveSession = serde_json::from_value(serde_json::json!({
"id": "psess-1",
"username": "admin",
"authorized": true,
"project": "MyProject",
"clientAddress": "10.0.0.5",
"lastComm": 1787346747022i64,
"sessionScope": "G",
"activePages": 2,
"pageIds": ["viewA", "viewB"],
"recentBytesSent": 1024,
"totalBytesSent": 4096,
"userAgent": "Mozilla/5.0"
}))
.expect("perspective capture shape must parse");
assert_eq!(session.id, "psess-1");
assert_eq!(session.client_address, "10.0.0.5", "clientAddress rename");
assert_eq!(session.last_comm, 1787346747022, "lastComm rename");
assert_eq!(session.active_pages, 2, "activePages rename");
assert_eq!(session.user_agent, "Mozilla/5.0", "userAgent rename");
assert_eq!(
session.extra.get("sessionScope"),
Some(&serde_json::json!("G")),
"unmodeled known keys round-trip"
);
let round = serde_json::to_value(&session).expect("serialize");
assert_eq!(round["clientAddress"], "10.0.0.5");
assert_eq!(round["userAgent"], "Mozilla/5.0");
}
#[test]
fn vision_client_parses_designer_shape_plus_tag_count() {
let client: VisionClient = serde_json::from_value(serde_json::json!({
"id": "v-1",
"user": "operator",
"uptime": 120000,
"lastcomm": 1787346747022i64,
"timeout": 3600000,
"memory": {"used": 134217728i64, "max": 536870912i64},
"project": "PlantFloor",
"address": "10.0.0.9:443",
"timezone": "UTC",
"tagCount": 1523
}))
.expect("vision capture shape must parse");
assert_eq!(client.id, "v-1");
assert_eq!(client.tag_count, 1523, "tagCount rename");
let round = serde_json::to_value(&client).expect("serialize");
assert_eq!(round["tagCount"], 1523, "gateway-native key on the way out");
}
}