mhome-core-api 1.10.0

Shared wire types and service contracts for the mHome core
Documentation
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};

use crate::NodeInstancePolicy;

pub const NODE_DESCRIBE_SCHEMA_VERSION: &str = "mhome.node.describe.v1";
pub const NODE_READINESS_SCHEMA_VERSION: &str = "mhome.node.readiness.v1";
pub const NODE_SERVICE_PROTOCOL_V1_SCHEMA: &str =
    include_str!("../contract/node-service-protocol-v1.json");

pub fn node_describe_target(node_type: &str) -> String {
    format!("/{node_type}/describe")
}

pub fn node_readiness_target(node_type: &str) -> String {
    format!("/{node_type}/readiness")
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct NodeDescribeRequest {}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NodeDescription {
    pub schema_version: String,
    pub node_type: String,
    pub service_id: String,
    pub service_version: String,
    pub instance_policy: NodeInstancePolicy,
    pub routes: Vec<String>,
    pub capabilities: Vec<String>,
    pub details: Map<String, Value>,
}

#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct NodeReadinessRequest {}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NodeReadinessState {
    Starting,
    Ready,
    Degraded,
    Failed,
    Stopping,
    Stopped,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NodeReadinessReason {
    pub code: String,
    pub message: String,
    pub retryable: bool,
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct NodeReadinessSnapshot {
    pub schema_version: String,
    pub node_type: String,
    pub service_id: String,
    pub service_version: String,
    pub process_generation: String,
    pub state: NodeReadinessState,
    pub ready: bool,
    pub revision: u64,
    pub updated_at_ms: i64,
    pub reason: Option<NodeReadinessReason>,
    pub details: Map<String, Value>,
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    #[test]
    fn empty_requests_reject_unknown_fields() {
        assert!(serde_json::from_value::<NodeDescribeRequest>(json!({})).is_ok());
        assert!(serde_json::from_value::<NodeReadinessRequest>(json!({})).is_ok());
        assert!(
            serde_json::from_value::<NodeDescribeRequest>(json!({"nodeId": "legacy"})).is_err()
        );
        assert!(serde_json::from_value::<NodeReadinessRequest>(json!({"refresh": true})).is_err());
    }

    #[test]
    fn response_contracts_are_additive_for_consumers() {
        let description: NodeDescription = serde_json::from_value(json!({
            "schemaVersion": NODE_DESCRIBE_SCHEMA_VERSION,
            "nodeType": "camera",
            "serviceId": "camera",
            "serviceVersion": "1.0.0",
            "instancePolicy": "multiple",
            "routes": ["/camera/describe", "/camera/readiness"],
            "capabilities": [],
            "details": {},
            "futureField": true
        }))
        .expect("additive description");
        assert_eq!(description.node_type, "camera");

        let readiness: NodeReadinessSnapshot = serde_json::from_value(json!({
            "schemaVersion": NODE_READINESS_SCHEMA_VERSION,
            "nodeType": "camera",
            "serviceId": "camera",
            "serviceVersion": "1.0.0",
            "processGeneration": "generation-1",
            "state": "degraded",
            "ready": true,
            "revision": 2,
            "updatedAtMs": 3,
            "reason": null,
            "details": {},
            "futureField": true
        }))
        .expect("additive readiness");
        assert_eq!(readiness.state, NodeReadinessState::Degraded);
    }

    #[test]
    fn canonical_schema_declares_every_wire_field_and_policy() {
        let schema: Value = serde_json::from_str(NODE_SERVICE_PROTOCOL_V1_SCHEMA).unwrap();
        assert_eq!(
            schema["$defs"]["emptyRequest"]["additionalProperties"],
            false
        );
        assert_eq!(schema["$defs"]["describe"]["additionalProperties"], true);
        assert_eq!(
            schema["$defs"]["readinessReason"]["additionalProperties"],
            true
        );
        assert_eq!(schema["$defs"]["readiness"]["additionalProperties"], true);
        assert_eq!(
            schema["$defs"]["describe"]["properties"]["schemaVersion"]["const"],
            NODE_DESCRIBE_SCHEMA_VERSION
        );
        assert_eq!(
            schema["$defs"]["readiness"]["properties"]["schemaVersion"]["const"],
            NODE_READINESS_SCHEMA_VERSION
        );
        assert_eq!(
            schema["$defs"]["describe"]["properties"]["instancePolicy"]["enum"],
            json!(["multiple", "singleton", "shared"])
        );
    }
}