core_api/
node_commissioning.rs1use serde::{Deserialize, Serialize};
3use serde_json::{Map, Value};
4
5pub const NODE_PREFLIGHT_SCHEMA_VERSION: &str = "mhome.node.preflight.v1";
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8#[serde(rename_all = "camelCase")]
9pub struct NodeCommissioningAcceptRequest {
10 pub token: String,
11 pub node_id: String,
12 pub hub_id: String,
13 pub hub_url: String,
14}
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(rename_all = "camelCase")]
17pub struct NodeCommissioningAcceptResponse {
18 pub ok: bool,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22#[serde(rename_all = "camelCase")]
23pub struct NodeCommissioningErrorResponse {
24 pub ok: bool,
25 pub reason: String,
26 pub error: String,
27 #[serde(skip_serializing_if = "Option::is_none")]
28 pub detail: Option<String>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32#[serde(rename_all = "camelCase")]
33pub struct NodeCommissioningPrepareRequest {
34 #[serde(flatten)]
35 pub commissioning: NodeCommissioningAcceptRequest,
36}
37#[derive(Debug, Clone, Serialize, Deserialize)]
38#[serde(rename_all = "camelCase")]
39pub struct NodeCommissioningStatusRequest {
40 #[serde(flatten)]
41 pub commissioning: NodeCommissioningAcceptRequest,
42}
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(rename_all = "snake_case")]
45pub enum NodePreflightState {
46 Preparing,
47 Ready,
48 Failed,
49}
50#[derive(Debug, Clone, Serialize, Deserialize)]
51#[serde(rename_all = "camelCase")]
52pub struct NodePreflightError {
53 pub code: String,
54 pub detail: String,
55 pub retryable: bool,
56}
57#[derive(Debug, Clone, Serialize, Deserialize)]
58#[serde(rename_all = "camelCase")]
59pub struct NodePreflightReadiness {
60 pub schema_version: String,
61 pub state: NodePreflightState,
62 pub revision: u64,
63 pub commissionable: bool,
64 pub runtime_usable: bool,
65 #[serde(deserialize_with = "required_nullable_error")]
66 pub error: Option<NodePreflightError>,
67 pub details: Map<String, Value>,
68}
69fn required_nullable_error<'de, D: serde::Deserializer<'de>>(
70 deserializer: D,
71) -> Result<Option<NodePreflightError>, D::Error> {
72 Option::<NodePreflightError>::deserialize(deserializer)
73}
74impl NodePreflightReadiness {
75 pub fn validate(&self) -> Result<(), &'static str> {
76 if self.schema_version != NODE_PREFLIGHT_SCHEMA_VERSION {
77 return Err("unsupported preflight schema");
78 }
79 if self.commissionable && (!self.runtime_usable || self.state != NodePreflightState::Ready)
80 {
81 return Err("commissionable requires a ready, usable runtime");
82 }
83 if (self.state == NodePreflightState::Failed) != self.error.is_some() {
84 return Err("failed preflight requires an error, other states must not contain one");
85 }
86 if self
87 .error
88 .as_ref()
89 .is_some_and(|e| e.code.trim().is_empty() || e.detail.trim().is_empty())
90 {
91 return Err("preflight error must include code and detail");
92 }
93 Ok(())
94 }
95}
96#[derive(Debug, Clone, Serialize, Deserialize)]
97#[serde(rename_all = "camelCase")]
98pub struct NodeCommissioningPrepareResponse {
99 pub ok: bool,
100 pub readiness: NodePreflightReadiness,
101}
102#[derive(Debug, Clone, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct NodeCommissioningStatusResponse {
105 pub ok: bool,
106 pub readiness: NodePreflightReadiness,
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 #[test]
113 fn requires_nullable_error_field() {
114 let mut value = serde_json::json!({"schemaVersion":NODE_PREFLIGHT_SCHEMA_VERSION,"state":"ready",
115 "revision":1,"commissionable":true,"runtimeUsable":true,"details":{}});
116 assert!(serde_json::from_value::<NodePreflightReadiness>(value.clone()).is_err());
117 value["error"] = Value::Null;
118 assert!(serde_json::from_value::<NodePreflightReadiness>(value)
119 .unwrap()
120 .validate()
121 .is_ok());
122 }
123 #[test]
124 fn rejects_private_readiness_and_inconsistent_success() {
125 assert!(serde_json::from_value::<NodePreflightReadiness>(
126 serde_json::json!({"commissionable":true})
127 )
128 .is_err());
129 let mut r = NodePreflightReadiness {
130 schema_version: NODE_PREFLIGHT_SCHEMA_VERSION.into(),
131 state: NodePreflightState::Ready,
132 revision: 1,
133 commissionable: true,
134 runtime_usable: true,
135 error: None,
136 details: Map::new(),
137 };
138 assert!(r.validate().is_ok());
139 r.runtime_usable = false;
140 assert!(r.validate().is_err());
141 r.runtime_usable = true;
142 r.schema_version = "matter.readiness.v1".into();
143 assert!(r.validate().is_err());
144 }
145}