mockforge_schema/
lib.rs

1//! JSON Schema generation for MockForge configuration files
2//!
3//! This crate provides functionality to generate JSON Schema definitions
4//! from MockForge's configuration structs, enabling IDE autocomplete and
5//! validation for `mockforge.yaml`, `mockforge.toml`, persona files, and blueprint files.
6
7use schemars::schema_for;
8
9/// Generate JSON Schema for MockForge ServerConfig (main config)
10///
11/// This function generates a complete JSON Schema that can be used by
12/// IDEs and editors to provide autocomplete, validation, and documentation
13/// for MockForge configuration files.
14///
15/// # Returns
16///
17/// A JSON Schema object as a serde_json::Value
18///
19/// # Example
20///
21/// ```rust
22/// use mockforge_schema::generate_config_schema;
23/// use serde_json;
24///
25/// let schema = generate_config_schema();
26/// let schema_json = serde_json::to_string_pretty(&schema).unwrap();
27/// println!("{}", schema_json);
28/// ```
29pub fn generate_config_schema() -> serde_json::Value {
30    // Generate schema from ServerConfig
31    // ServerConfig needs to have JsonSchema derive (via feature flag)
32    let schema = schema_for!(mockforge_core::ServerConfig);
33
34    let mut schema_value = serde_json::to_value(schema).expect("Failed to serialize schema");
35
36    // Add metadata for better IDE support
37    if let Some(obj) = schema_value.as_object_mut() {
38        obj.insert(
39            "$schema".to_string(),
40            serde_json::json!("http://json-schema.org/draft-07/schema#"),
41        );
42        obj.insert("title".to_string(), serde_json::json!("MockForge Server Configuration"));
43        obj.insert(
44            "description".to_string(),
45            serde_json::json!(
46                "Complete configuration schema for MockForge mock server. \
47             This schema provides autocomplete and validation for mockforge.yaml files."
48            ),
49        );
50    }
51
52    schema_value
53}
54
55/// Generate JSON Schema for Reality configuration
56///
57/// Generates schema for the Reality slider configuration used to control
58/// mock environment realism levels.
59pub fn generate_reality_schema() -> serde_json::Value {
60    let schema = schema_for!(mockforge_core::config::RealitySliderConfig);
61
62    let mut schema_value =
63        serde_json::to_value(schema).expect("Failed to serialize reality schema");
64
65    // Add metadata for better IDE support
66    if let Some(obj) = schema_value.as_object_mut() {
67        obj.insert(
68            "$schema".to_string(),
69            serde_json::json!("http://json-schema.org/draft-07/schema#"),
70        );
71        obj.insert("title".to_string(), serde_json::json!("MockForge Reality Configuration"));
72        obj.insert(
73            "description".to_string(),
74            serde_json::json!(
75                "Reality slider configuration for controlling mock environment realism. \
76             Maps reality levels (1-5) to specific subsystem settings."
77            ),
78        );
79    }
80
81    schema_value
82}
83
84/// Generate JSON Schema for Persona configuration
85///
86/// Generates schema for persona profiles that define consistent data patterns.
87/// Note: This generates a schema for the persona registry config structure.
88pub fn generate_persona_schema() -> serde_json::Value {
89    // Generate schema for PersonaRegistryConfig which contains persona definitions
90    let schema = schema_for!(mockforge_core::config::PersonaRegistryConfig);
91
92    let mut schema_value =
93        serde_json::to_value(schema).expect("Failed to serialize persona schema");
94
95    // Add metadata for better IDE support
96    if let Some(obj) = schema_value.as_object_mut() {
97        obj.insert(
98            "$schema".to_string(),
99            serde_json::json!("http://json-schema.org/draft-07/schema#"),
100        );
101        obj.insert("title".to_string(), serde_json::json!("MockForge Persona Configuration"));
102        obj.insert(
103            "description".to_string(),
104            serde_json::json!(
105                "Persona configuration for consistent, personality-driven data generation. \
106             Defines personas with unique IDs, domains, traits, and deterministic seeds."
107            ),
108        );
109    }
110
111    schema_value
112}
113
114/// Generate JSON Schema for Blueprint metadata
115///
116/// Generates schema for blueprint.yaml files that define app archetypes.
117/// Note: Blueprint structs are in mockforge-cli, so we generate a manual schema
118/// based on the known structure.
119pub fn generate_blueprint_schema() -> serde_json::Value {
120    // Manual schema for blueprint metadata since it's in a different crate
121    // This matches the BlueprintMetadata structure
122    serde_json::json!({
123        "$schema": "http://json-schema.org/draft-07/schema#",
124        "title": "MockForge Blueprint Configuration",
125        "description": "Blueprint metadata schema for predefined app archetypes. \
126                       Blueprints provide pre-configured personas, reality defaults, \
127                       flows, scenarios, and playground collections.",
128        "type": "object",
129        "required": ["manifest_version", "name", "version", "title", "description", "author", "category"],
130        "properties": {
131            "manifest_version": {
132                "type": "string",
133                "description": "Blueprint manifest version (e.g., '1.0')",
134                "example": "1.0"
135            },
136            "name": {
137                "type": "string",
138                "description": "Unique blueprint identifier (e.g., 'b2c-saas', 'ecommerce')",
139                "pattern": "^[a-z0-9-]+$"
140            },
141            "version": {
142                "type": "string",
143                "description": "Blueprint version (semver)",
144                "pattern": "^\\d+\\.\\d+\\.\\d+$"
145            },
146            "title": {
147                "type": "string",
148                "description": "Human-readable blueprint title"
149            },
150            "description": {
151                "type": "string",
152                "description": "Detailed description of what this blueprint provides"
153            },
154            "author": {
155                "type": "string",
156                "description": "Blueprint author name"
157            },
158            "author_email": {
159                "type": "string",
160                "format": "email",
161                "description": "Blueprint author email (optional)"
162            },
163            "category": {
164                "type": "string",
165                "description": "Blueprint category (e.g., 'saas', 'ecommerce', 'banking')",
166                "enum": ["saas", "ecommerce", "banking", "fintech", "healthcare", "other"]
167            },
168            "tags": {
169                "type": "array",
170                "items": {
171                    "type": "string"
172                },
173                "description": "Tags for categorizing and searching blueprints"
174            },
175            "setup": {
176                "type": "object",
177                "description": "What this blueprint sets up",
178                "properties": {
179                    "personas": {
180                        "type": "array",
181                        "items": {
182                            "type": "object",
183                            "required": ["id", "name"],
184                            "properties": {
185                                "id": {
186                                    "type": "string",
187                                    "description": "Persona identifier"
188                                },
189                                "name": {
190                                    "type": "string",
191                                    "description": "Persona display name"
192                                },
193                                "description": {
194                                    "type": "string",
195                                    "description": "Persona description (optional)"
196                                }
197                            }
198                        }
199                    },
200                    "reality": {
201                        "type": "object",
202                        "properties": {
203                            "level": {
204                                "type": "string",
205                                "enum": ["static", "light", "moderate", "high", "chaos"],
206                                "description": "Default reality level for this blueprint"
207                            },
208                            "description": {
209                                "type": "string",
210                                "description": "Why this reality level is chosen"
211                            }
212                        }
213                    },
214                    "flows": {
215                        "type": "array",
216                        "items": {
217                            "type": "object",
218                            "required": ["id", "name"],
219                            "properties": {
220                                "id": {
221                                    "type": "string"
222                                },
223                                "name": {
224                                    "type": "string"
225                                },
226                                "description": {
227                                    "type": "string"
228                                }
229                            }
230                        }
231                    },
232                    "scenarios": {
233                        "type": "array",
234                        "items": {
235                            "type": "object",
236                            "required": ["id", "name", "type", "file"],
237                            "properties": {
238                                "id": {
239                                    "type": "string",
240                                    "description": "Scenario identifier"
241                                },
242                                "name": {
243                                    "type": "string",
244                                    "description": "Scenario display name"
245                                },
246                                "type": {
247                                    "type": "string",
248                                    "enum": ["happy_path", "known_failure", "slow_path"],
249                                    "description": "Scenario type"
250                                },
251                                "description": {
252                                    "type": "string",
253                                    "description": "Scenario description (optional)"
254                                },
255                                "file": {
256                                    "type": "string",
257                                    "description": "Path to scenario YAML file"
258                                }
259                            }
260                        }
261                    },
262                    "playground": {
263                        "type": "object",
264                        "properties": {
265                            "enabled": {
266                                "type": "boolean",
267                                "default": true
268                            },
269                            "collection_file": {
270                                "type": "string",
271                                "description": "Path to playground collection file"
272                            }
273                        }
274                    }
275                }
276            },
277            "compatibility": {
278                "type": "object",
279                "properties": {
280                    "min_version": {
281                        "type": "string",
282                        "description": "Minimum MockForge version required"
283                    },
284                    "max_version": {
285                        "type": "string",
286                        "description": "Maximum MockForge version (null for latest)"
287                    },
288                    "required_features": {
289                        "type": "array",
290                        "items": {
291                            "type": "string"
292                        }
293                    },
294                    "protocols": {
295                        "type": "array",
296                        "items": {
297                            "type": "string",
298                            "enum": ["http", "websocket", "grpc", "graphql", "mqtt"]
299                        }
300                    }
301                }
302            },
303            "files": {
304                "type": "array",
305                "items": {
306                    "type": "string"
307                },
308                "description": "List of files included in this blueprint"
309            },
310            "readme": {
311                "type": "string",
312                "description": "Path to README file (optional)"
313            },
314            "contracts": {
315                "type": "array",
316                "items": {
317                    "type": "object",
318                    "required": ["file"],
319                    "properties": {
320                        "file": {
321                            "type": "string",
322                            "description": "Path to contract schema file"
323                        },
324                        "description": {
325                            "type": "string",
326                            "description": "Contract description (optional)"
327                        }
328                    }
329                }
330            }
331        }
332    })
333}
334
335/// Generate JSON Schema and return as a formatted JSON string
336///
337/// # Returns
338///
339/// A pretty-printed JSON string containing the schema
340pub fn generate_config_schema_json() -> String {
341    let schema = generate_config_schema();
342    serde_json::to_string_pretty(&schema).expect("Failed to format schema as JSON")
343}
344
345/// Generate all schemas and return them as a map
346///
347/// Returns a HashMap with schema names as keys and JSON Schema values.
348pub fn generate_all_schemas() -> std::collections::HashMap<String, serde_json::Value> {
349    let mut schemas = std::collections::HashMap::new();
350
351    schemas.insert("mockforge-config".to_string(), generate_config_schema());
352    schemas.insert("reality-config".to_string(), generate_reality_schema());
353    schemas.insert("persona-config".to_string(), generate_persona_schema());
354    schemas.insert("blueprint-config".to_string(), generate_blueprint_schema());
355
356    schemas
357}
358
359/// Validation result for config file validation
360#[derive(Debug, Clone)]
361pub struct ValidationResult {
362    /// Whether validation passed
363    pub valid: bool,
364    /// File path that was validated
365    pub file_path: String,
366    /// Schema type used for validation
367    pub schema_type: String,
368    /// Validation errors (empty if valid)
369    pub errors: Vec<String>,
370}
371
372impl ValidationResult {
373    /// Create a successful validation result
374    pub fn success(file_path: String, schema_type: String) -> Self {
375        Self {
376            valid: true,
377            file_path,
378            schema_type,
379            errors: Vec::new(),
380        }
381    }
382
383    /// Create a failed validation result
384    pub fn failure(file_path: String, schema_type: String, errors: Vec<String>) -> Self {
385        Self {
386            valid: false,
387            file_path,
388            schema_type,
389            errors,
390        }
391    }
392}
393
394/// Validate a config file against its corresponding JSON Schema
395///
396/// # Arguments
397///
398/// * `file_path` - Path to the config file (YAML or JSON)
399/// * `schema_type` - Type of schema to validate against (config, reality, persona, blueprint)
400/// * `schema` - The JSON Schema to validate against
401///
402/// # Returns
403///
404/// A ValidationResult indicating whether validation passed and any errors
405pub fn validate_config_file(
406    file_path: &std::path::Path,
407    schema_type: &str,
408    schema: &serde_json::Value,
409) -> Result<ValidationResult, Box<dyn std::error::Error>> {
410    use jsonschema::{Draft, Validator as SchemaValidator};
411    use std::fs;
412
413    // Read and parse the config file
414    let content = fs::read_to_string(file_path)?;
415    let config_value: serde_json::Value = if file_path
416        .extension()
417        .and_then(|ext| ext.to_str())
418        .map(|ext| ext.eq_ignore_ascii_case("yaml") || ext.eq_ignore_ascii_case("yml"))
419        .unwrap_or(false)
420    {
421        // Parse YAML
422        serde_yaml::from_str(&content).map_err(|e| format!("Failed to parse YAML: {}", e))?
423    } else {
424        // Parse JSON
425        serde_json::from_str(&content).map_err(|e| format!("Failed to parse JSON: {}", e))?
426    };
427
428    // Compile the schema
429    let compiled_schema = SchemaValidator::options()
430        .with_draft(Draft::Draft7)
431        .build(schema)
432        .map_err(|e| format!("Failed to compile schema: {}", e))?;
433
434    // Validate
435    let mut errors = Vec::new();
436    for error in compiled_schema.iter_errors(&config_value) {
437        errors.push(format!("{}: {}", error.instance_path, error));
438    }
439
440    if errors.is_empty() {
441        Ok(ValidationResult::success(
442            file_path.to_string_lossy().to_string(),
443            schema_type.to_string(),
444        ))
445    } else {
446        Ok(ValidationResult::failure(
447            file_path.to_string_lossy().to_string(),
448            schema_type.to_string(),
449            errors,
450        ))
451    }
452}
453
454/// Auto-detect schema type from file path or content
455///
456/// Attempts to determine which schema should be used to validate a file
457/// based on its path or content.
458pub fn detect_schema_type(file_path: &std::path::Path) -> Option<String> {
459    let file_name = file_path.file_name()?.to_string_lossy().to_lowercase();
460    let path_str = file_path.to_string_lossy().to_lowercase();
461
462    // Check file name patterns
463    if file_name == "mockforge.yaml"
464        || file_name == "mockforge.yml"
465        || file_name == "mockforge.json"
466    {
467        return Some("mockforge-config".to_string());
468    }
469
470    if file_name == "blueprint.yaml" || file_name == "blueprint.yml" {
471        return Some("blueprint-config".to_string());
472    }
473
474    if path_str.contains("reality") {
475        return Some("reality-config".to_string());
476    }
477
478    if path_str.contains("persona") {
479        return Some("persona-config".to_string());
480    }
481
482    // Default to main config
483    Some("mockforge-config".to_string())
484}
485
486#[cfg(test)]
487mod tests {
488    use super::*;
489
490    #[test]
491    fn test_schema_generation() {
492        let schema = generate_config_schema();
493        assert!(schema.is_object());
494
495        // Verify schema has required fields
496        let obj = schema.as_object().unwrap();
497        assert!(obj.contains_key("$schema") || obj.contains_key("type"));
498    }
499
500    #[test]
501    fn test_schema_json_formatting() {
502        let json = generate_config_schema_json();
503        assert!(!json.is_empty());
504
505        // Verify it's valid JSON
506        let parsed: Result<serde_json::Value, _> = serde_json::from_str(&json);
507        assert!(parsed.is_ok());
508    }
509}