1use serde::{Deserialize, Serialize};
2use serde_json::{Map, Value};
3
4use crate::NodeInstancePolicy;
5
6pub const NODE_DESCRIBE_SCHEMA_VERSION: &str = "mhome.node.describe.v1";
7pub const NODE_READINESS_SCHEMA_VERSION: &str = "mhome.node.readiness.v1";
8pub const NODE_SERVICE_PROTOCOL_V1_SCHEMA: &str =
9 include_str!("../contract/node-service-protocol-v1.json");
10
11pub fn node_describe_target(node_type: &str) -> String {
12 format!("/{node_type}/describe")
13}
14
15pub fn node_readiness_target(node_type: &str) -> String {
16 format!("/{node_type}/readiness")
17}
18
19#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(deny_unknown_fields)]
21pub struct NodeDescribeRequest {}
22
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24#[serde(rename_all = "camelCase")]
25pub struct NodeDescription {
26 pub schema_version: String,
27 pub node_type: String,
28 pub service_id: String,
29 pub service_version: String,
30 pub instance_policy: NodeInstancePolicy,
31 pub routes: Vec<String>,
32 pub capabilities: Vec<String>,
33 pub details: Map<String, Value>,
34}
35
36#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(deny_unknown_fields)]
38pub struct NodeReadinessRequest {}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
41#[serde(rename_all = "snake_case")]
42pub enum NodeReadinessState {
43 Starting,
44 Ready,
45 Degraded,
46 Failed,
47 Stopping,
48 Stopped,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52#[serde(rename_all = "camelCase")]
53pub struct NodeReadinessReason {
54 pub code: String,
55 pub message: String,
56 pub retryable: bool,
57}
58
59#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
60#[serde(rename_all = "camelCase")]
61pub struct NodeReadinessSnapshot {
62 pub schema_version: String,
63 pub node_type: String,
64 pub service_id: String,
65 pub service_version: String,
66 pub process_generation: String,
67 pub state: NodeReadinessState,
68 pub ready: bool,
69 pub revision: u64,
70 pub updated_at_ms: i64,
71 pub reason: Option<NodeReadinessReason>,
72 pub details: Map<String, Value>,
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78 use serde_json::json;
79
80 #[test]
81 fn empty_requests_reject_unknown_fields() {
82 assert!(serde_json::from_value::<NodeDescribeRequest>(json!({})).is_ok());
83 assert!(serde_json::from_value::<NodeReadinessRequest>(json!({})).is_ok());
84 assert!(
85 serde_json::from_value::<NodeDescribeRequest>(json!({"nodeId": "legacy"})).is_err()
86 );
87 assert!(serde_json::from_value::<NodeReadinessRequest>(json!({"refresh": true})).is_err());
88 }
89
90 #[test]
91 fn response_contracts_are_additive_for_consumers() {
92 let description: NodeDescription = serde_json::from_value(json!({
93 "schemaVersion": NODE_DESCRIBE_SCHEMA_VERSION,
94 "nodeType": "camera",
95 "serviceId": "camera",
96 "serviceVersion": "1.0.0",
97 "instancePolicy": "multiple",
98 "routes": ["/camera/describe", "/camera/readiness"],
99 "capabilities": [],
100 "details": {},
101 "futureField": true
102 }))
103 .expect("additive description");
104 assert_eq!(description.node_type, "camera");
105
106 let readiness: NodeReadinessSnapshot = serde_json::from_value(json!({
107 "schemaVersion": NODE_READINESS_SCHEMA_VERSION,
108 "nodeType": "camera",
109 "serviceId": "camera",
110 "serviceVersion": "1.0.0",
111 "processGeneration": "generation-1",
112 "state": "degraded",
113 "ready": true,
114 "revision": 2,
115 "updatedAtMs": 3,
116 "reason": null,
117 "details": {},
118 "futureField": true
119 }))
120 .expect("additive readiness");
121 assert_eq!(readiness.state, NodeReadinessState::Degraded);
122 }
123
124 #[test]
125 fn canonical_schema_declares_every_wire_field_and_policy() {
126 let schema: Value = serde_json::from_str(NODE_SERVICE_PROTOCOL_V1_SCHEMA).unwrap();
127 assert_eq!(
128 schema["$defs"]["emptyRequest"]["additionalProperties"],
129 false
130 );
131 assert_eq!(schema["$defs"]["describe"]["additionalProperties"], true);
132 assert_eq!(
133 schema["$defs"]["readinessReason"]["additionalProperties"],
134 true
135 );
136 assert_eq!(schema["$defs"]["readiness"]["additionalProperties"], true);
137 assert_eq!(
138 schema["$defs"]["describe"]["properties"]["schemaVersion"]["const"],
139 NODE_DESCRIBE_SCHEMA_VERSION
140 );
141 assert_eq!(
142 schema["$defs"]["readiness"]["properties"]["schemaVersion"]["const"],
143 NODE_READINESS_SCHEMA_VERSION
144 );
145 assert_eq!(
146 schema["$defs"]["describe"]["properties"]["instancePolicy"]["enum"],
147 json!(["multiple", "singleton", "shared"])
148 );
149 }
150}