Skip to main content

core_api/node/contracts/
matter_device_sources.rs

1//! Read-only Matter provenance projected by the Node, without Fabric internals.
2use serde::{Deserialize, Serialize};
3pub const MATTER_DEVICE_SOURCES_TARGET: &str = "/matter/device/sources";
4#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
5#[serde(rename_all = "camelCase", deny_unknown_fields)]
6pub struct MatterDeviceSourcesRequest {}
7#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
8#[serde(rename_all = "camelCase")]
9pub enum MatterSourceAvailability {
10    Available,
11    Unavailable,
12}
13#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
14#[serde(rename_all = "camelCase", deny_unknown_fields)]
15pub struct MatterDeviceSourcesSnapshot {
16    pub availability: MatterSourceAvailability,
17    pub bridges: Vec<MatterSourceBridge>,
18    pub devices: Vec<MatterDeviceSource>,
19}
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
21#[serde(rename_all = "camelCase", deny_unknown_fields)]
22pub struct MatterSourceBridge {
23    pub node_id: String,
24    pub display_name: String,
25    pub reachable: bool,
26}
27#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
28#[serde(rename_all = "camelCase", deny_unknown_fields)]
29pub struct MatterDeviceSource {
30    pub node_id: String,
31    pub availability: MatterSourceAvailability,
32    pub bridge_node_id: Option<String>,
33}
34impl MatterDeviceSourcesSnapshot {
35    pub fn validate(&self) -> Result<(), String> {
36        use MatterSourceAvailability::*;
37        if self.availability == Unavailable
38            && (!self.bridges.is_empty() || !self.devices.is_empty())
39        {
40            return Err("Unavailable Matter inventory must be empty".into());
41        }
42        let mut bridges = std::collections::HashSet::new();
43        for bridge in &self.bridges {
44            if bridge.node_id.trim().is_empty()
45                || bridge.display_name.trim().is_empty()
46                || !bridges.insert(bridge.node_id.as_str())
47            {
48                return Err("Invalid or duplicate Matter bridge".into());
49            }
50        }
51        let mut devices = std::collections::HashSet::new();
52        for device in &self.devices {
53            if device.node_id.trim().is_empty() || !devices.insert(device.node_id.as_str()) {
54                return Err("Invalid or duplicate Matter source".into());
55            }
56            if let Some(bridge) = &device.bridge_node_id {
57                if device.availability != Available
58                    || !bridges.contains(bridge.as_str())
59                    || bridge != &device.node_id
60                {
61                    return Err("Invalid Matter bridge reference".into());
62                }
63            }
64        }
65        for bridge in &self.bridges {
66            if !self.devices.iter().any(|d| {
67                d.node_id == bridge.node_id
68                    && d.bridge_node_id.as_deref() == Some(bridge.node_id.as_str())
69            }) {
70                return Err("Matter bridge has no source mapping".into());
71            }
72        }
73        Ok(())
74    }
75}
76#[cfg(test)]
77mod tests {
78    use super::*;
79    use serde_json::json;
80    #[test]
81    fn validates_direct_bridged_and_unknown_sources() {
82        let value = json!({"availability":"available","bridges":[{"nodeId":"1","displayName":"Bridge","reachable":false}],"devices":[{"nodeId":"1","availability":"available","bridgeNodeId":"1"},{"nodeId":"2","availability":"available","bridgeNodeId":null},{"nodeId":"3","availability":"unavailable","bridgeNodeId":null}]});
83        let snapshot: MatterDeviceSourcesSnapshot = serde_json::from_value(value.clone()).unwrap();
84        assert!(snapshot.validate().is_ok());
85        assert_eq!(serde_json::to_value(&snapshot).unwrap(), value);
86        let mut invalid = snapshot.clone();
87        invalid.devices[0].bridge_node_id = Some("missing".into());
88        assert!(invalid.validate().is_err());
89        let mut invalid = snapshot.clone();
90        invalid.availability = MatterSourceAvailability::Unavailable;
91        assert!(invalid.validate().is_err());
92        let mut invalid = snapshot.clone();
93        invalid.devices[0].availability = MatterSourceAvailability::Unavailable;
94        assert!(invalid.validate().is_err());
95        let mut invalid = snapshot.clone();
96        invalid.devices.push(invalid.devices[0].clone());
97        assert!(invalid.validate().is_err());
98        let mut invalid = value;
99        invalid["bridges"][0]["endpoints"] = json!([]);
100        assert!(serde_json::from_value::<MatterDeviceSourcesSnapshot>(invalid).is_err());
101        assert!(
102            serde_json::from_value::<MatterDeviceSourcesRequest>(json!({"discover":true})).is_err()
103        );
104    }
105}