Skip to main content

barbacane_wasm/
schema.rs

1//! Config schema validation for plugins.
2//!
3//! Per SPEC-003 section 2.2, each plugin has a config-schema.json that
4//! defines the JSON Schema for its configuration block in specs.
5
6use jsonschema::Validator;
7use serde_json::Value;
8
9use crate::error::WasmError;
10
11/// A compiled config schema for validating plugin configurations.
12pub struct ConfigSchema {
13    validator: Validator,
14}
15
16impl ConfigSchema {
17    /// Create a config schema from JSON Schema content.
18    pub fn from_json(schema_json: &str) -> Result<Self, WasmError> {
19        let schema: Value =
20            serde_json::from_str(schema_json).map_err(|e| WasmError::SchemaParse(e.to_string()))?;
21
22        let validator = Validator::new(&schema)
23            .map_err(|e| WasmError::SchemaParse(format!("invalid JSON Schema: {}", e)))?;
24
25        Ok(Self { validator })
26    }
27
28    /// Create a config schema from a parsed JSON value.
29    pub fn from_value(schema: &Value) -> Result<Self, WasmError> {
30        let validator = Validator::new(schema)
31            .map_err(|e| WasmError::SchemaParse(format!("invalid JSON Schema: {}", e)))?;
32
33        Ok(Self { validator })
34    }
35
36    /// Validate a config value against the schema.
37    pub fn validate(&self, config: &Value) -> Result<(), WasmError> {
38        self.validator
39            .validate(config)
40            .map_err(|e| WasmError::ConfigValidation(e.to_string()))
41    }
42
43    /// Create an empty schema that accepts any object.
44    ///
45    /// Per SPEC-003, if a plugin takes no config, the schema should be:
46    /// ```json
47    /// { "type": "object", "additionalProperties": false }
48    /// ```
49    pub fn empty() -> Result<Self, WasmError> {
50        Self::from_json(r#"{"type": "object", "additionalProperties": false}"#)
51    }
52
53    /// Create a permissive schema that accepts any value.
54    ///
55    /// Useful for testing or when no schema is provided.
56    pub fn any() -> Result<Self, WasmError> {
57        Self::from_json("{}")
58    }
59}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64    use serde_json::json;
65
66    #[test]
67    fn validate_against_schema() {
68        let schema = ConfigSchema::from_json(
69            r#"{
70            "type": "object",
71            "required": ["quota", "window"],
72            "properties": {
73                "quota": { "type": "integer", "minimum": 1 },
74                "window": { "type": "integer", "minimum": 1 }
75            }
76        }"#,
77        )
78        .unwrap();
79
80        // Valid config
81        let valid = json!({"quota": 100, "window": 60});
82        assert!(schema.validate(&valid).is_ok());
83
84        // Missing required field
85        let missing = json!({"quota": 100});
86        assert!(schema.validate(&missing).is_err());
87
88        // Invalid type
89        let wrong_type = json!({"quota": "100", "window": 60});
90        assert!(schema.validate(&wrong_type).is_err());
91
92        // Value too low
93        let too_low = json!({"quota": 0, "window": 60});
94        assert!(schema.validate(&too_low).is_err());
95    }
96
97    #[test]
98    fn empty_schema_rejects_properties() {
99        let schema = ConfigSchema::empty().unwrap();
100
101        // Empty object is valid
102        assert!(schema.validate(&json!({})).is_ok());
103
104        // Object with properties is invalid
105        assert!(schema.validate(&json!({"foo": "bar"})).is_err());
106    }
107
108    #[test]
109    fn any_schema_accepts_anything() {
110        let schema = ConfigSchema::any().unwrap();
111
112        assert!(schema.validate(&json!({})).is_ok());
113        assert!(schema.validate(&json!({"anything": "goes"})).is_ok());
114        assert!(schema.validate(&json!(null)).is_ok());
115        assert!(schema.validate(&json!(42)).is_ok());
116    }
117
118    #[test]
119    fn from_value() {
120        let schema_value = json!({
121            "type": "object",
122            "properties": {
123                "name": { "type": "string" }
124            }
125        });
126
127        let schema = ConfigSchema::from_value(&schema_value).unwrap();
128        assert!(schema.validate(&json!({"name": "test"})).is_ok());
129    }
130
131    #[test]
132    fn invalid_schema() {
133        // This is not a valid JSON Schema
134        let result = ConfigSchema::from_json(r#"{"type": "not-a-type"}"#);
135        // Note: jsonschema may accept this but fail on validation
136        // The important thing is we handle errors gracefully
137        match result {
138            Ok(_) => {} // Some schemas are permissive
139            Err(e) => {
140                assert!(e.to_string().contains("Schema") || e.to_string().contains("invalid"))
141            }
142        }
143    }
144}