core_api/node/contracts/
camera_topology.rs1use serde::{Deserialize, Serialize};
3
4pub const CAMERA_TOPOLOGY_TARGET: &str = "/camera/device/topology/sources";
5
6#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
7#[serde(rename_all = "camelCase", deny_unknown_fields)]
8pub struct CameraTopologyRequest {}
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
11#[serde(rename_all = "camelCase", deny_unknown_fields)]
12pub struct CameraTopologySnapshot {
13 pub providers: Vec<CameraTopologyProvider>,
14 pub devices: Vec<CameraTopologyDevice>,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
18#[serde(rename_all = "camelCase", deny_unknown_fields)]
19pub struct CameraTopologyProvider {
20 pub id: String,
21 pub display_name: String,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
25#[serde(rename_all = "camelCase", deny_unknown_fields)]
26pub struct CameraTopologyDevice {
27 pub device_id: String,
28 pub provider_id: String,
29}
30
31impl CameraTopologySnapshot {
32 pub fn validate(&self) -> Result<(), String> {
33 let mut providers = std::collections::HashSet::new();
34 let mut devices = std::collections::HashSet::new();
35 for provider in &self.providers {
36 if provider.id.trim().is_empty()
37 || provider.display_name.trim().is_empty()
38 || !providers.insert(provider.id.as_str())
39 {
40 return Err("Invalid or duplicate Camera provider".into());
41 }
42 }
43 for device in &self.devices {
44 if device.device_id.trim().is_empty()
45 || !devices.insert(device.device_id.as_str())
46 || !providers.contains(device.provider_id.as_str())
47 {
48 return Err("Invalid Camera device or provider reference".into());
49 }
50 }
51 Ok(())
52 }
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58 use serde_json::json;
59 #[test]
60 fn topology_wire_contract_and_identity_constraints() {
61 let value = json!({"providers":[{"id":"rtsp","displayName":"RTSP"}],
62 "devices":[{"deviceId":"camera-1","providerId":"rtsp"}]});
63 let snapshot: CameraTopologySnapshot = serde_json::from_value(value.clone()).unwrap();
64 assert!(snapshot.validate().is_ok());
65 assert_eq!(serde_json::to_value(&snapshot).unwrap(), value);
66 let mut duplicate = snapshot.clone();
67 duplicate.devices.push(snapshot.devices[0].clone());
68 assert!(duplicate.validate().is_err());
69 let mut dangling = snapshot.clone();
70 dangling.devices[0].provider_id = "absent".into();
71 assert!(dangling.validate().is_err());
72 let mut leaked = value;
73 leaked["devices"][0]["password"] = json!("secret");
74 assert!(serde_json::from_value::<CameraTopologySnapshot>(leaked).is_err());
75 }
76 #[test]
77 fn topology_request_and_response_are_strict() {
78 assert!(serde_json::from_value::<CameraTopologyRequest>(json!({})).is_ok());
79 assert!(serde_json::from_value::<CameraTopologyRequest>(json!({"extra":true})).is_err());
80 assert!(serde_json::from_value::<CameraTopologySnapshot>(json!({"providers":[]})).is_err());
81 }
82}