1use schemars::schema_for;
8
9pub fn generate_config_schema() -> serde_json::Value {
30 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 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
55pub 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 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
84pub fn generate_persona_schema() -> serde_json::Value {
89 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 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
114pub fn generate_blueprint_schema() -> serde_json::Value {
120 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
335pub 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
345pub 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#[derive(Debug, Clone)]
361pub struct ValidationResult {
362 pub valid: bool,
364 pub file_path: String,
366 pub schema_type: String,
368 pub errors: Vec<String>,
370}
371
372impl ValidationResult {
373 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 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
394pub 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 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 serde_yaml::from_str(&content).map_err(|e| format!("Failed to parse YAML: {}", e))?
423 } else {
424 serde_json::from_str(&content).map_err(|e| format!("Failed to parse JSON: {}", e))?
426 };
427
428 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 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
454pub 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 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 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 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 let parsed: Result<serde_json::Value, _> = serde_json::from_str(&json);
507 assert!(parsed.is_ok());
508 }
509}