Skip to main content

core_api/
node_service.rs

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}