Skip to main content

core_api/node/contracts/
camera_topology.rs

1//! Safe Camera provenance. Never carries source configuration or credentials.
2use 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}