mechutil 0.8.6

Utility structures and functions for mechatronics applications.
Documentation
//
// Copyright (C) 2024 - 2026 Automated Design Corp. All Rights Reserved.
//

//! Schema publishing for module Config types.
//!
//! Every module that exposes a Config type can derive `schemars::JsonSchema`
//! on it and override `ModuleHandler::config_schema()` to make its
//! configuration shape discoverable over IPC at `<domain>.schema`.
//!
//! The wire format is intentionally standardized so consumers (e.g., the
//! autocore-ide VS Code extension) can render any module's config without
//! per-module integration.
//!
//! ## Example
//!
//! ```ignore
//! use mechutil::ipc::{ModuleHandler, schema};
//! use schemars::JsonSchema;
//! use serde::{Serialize, Deserialize};
//!
//! #[derive(Serialize, Deserialize, JsonSchema)]
//! struct MyConfig {
//!     /// Friendly name for this instance
//!     pub name: String,
//!     pub port: u16,
//! }
//!
//! impl ModuleHandler for MyModule {
//!     // ... handle_message, on_initialize, etc.
//!
//!     fn config_schema(&self) -> Option<schema::SchemaEnvelope> {
//!         Some(schema::build_envelope::<MyConfig>(
//!             self.domain(),
//!             self.version(),
//!             Some(include_str!("ui_hints.json")),
//!         ))
//!     }
//! }
//! ```
//!
//! `<domain>.schema` is then automatically served by `ModuleHandlerExt::process_message`.

use schemars::{schema_for, JsonSchema};
use serde::{Deserialize, Serialize};
use serde_json::Value;

/// Envelope returned by `<domain>.schema` IPC calls.
///
/// Wire format is part of the public IPC contract — consumers depend on it.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaEnvelope {
    /// Module domain name (matches `ModuleHandler::domain()`).
    pub module: String,
    /// Module semver (matches `ModuleHandler::version()`).
    pub version: String,
    /// JSON Schema (Draft-07) describing the module's Config type.
    pub schema: Value,
    /// UI hints sidecar — widget overrides, ordering, group labels, etc.
    /// `Value::Null` when the module has no hints to provide.
    #[serde(default)]
    pub ui_hints: Value,
}

/// Build a `SchemaEnvelope` for a Config type.
///
/// Generates the JSON Schema via `schemars::schema_for!`, parses optional
/// UI hints as JSON, and assembles the envelope with module identity.
///
/// `ui_hints_json` is typically `Some(include_str!("ui_hints.json"))` so the
/// sidecar travels with the module binary.
pub fn build_envelope<T: JsonSchema>(
    module: &str,
    version: &str,
    ui_hints_json: Option<&str>,
) -> SchemaEnvelope {
    let schema = schema_for!(T);
    let schema_value = serde_json::to_value(schema).unwrap_or(Value::Null);

    let ui_hints = match ui_hints_json {
        Some(raw) => serde_json::from_str(raw).unwrap_or_else(|err| {
            log::warn!(
                "module {}: ui_hints sidecar failed to parse ({}); serving null",
                module,
                err
            );
            Value::Null
        }),
        None => Value::Null,
    };

    SchemaEnvelope {
        module: module.to_string(),
        version: version.to_string(),
        schema: schema_value,
        ui_hints,
    }
}

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

    #[derive(Serialize, Deserialize, JsonSchema)]
    struct DemoConfig {
        /// Human-readable name
        name: String,
        port: u16,
    }

    #[test]
    fn builds_envelope_with_schema_and_hints() {
        let hints = r#"{"properties.name.widget": "variable-picker"}"#;
        let env = build_envelope::<DemoConfig>("demo", "0.1.0", Some(hints));
        assert_eq!(env.module, "demo");
        assert_eq!(env.version, "0.1.0");
        assert!(env.schema.get("properties").is_some(), "schema has properties");
        assert_eq!(
            env.ui_hints["properties.name.widget"], "variable-picker"
        );
    }

    #[test]
    fn null_hints_when_omitted() {
        let env = build_envelope::<DemoConfig>("demo", "0.1.0", None);
        assert_eq!(env.ui_hints, Value::Null);
    }

    #[test]
    fn malformed_hints_become_null() {
        let env = build_envelope::<DemoConfig>("demo", "0.1.0", Some("{not json"));
        assert_eq!(env.ui_hints, Value::Null);
    }
}