data_modelling_core/validation/
schema.rs

1//! JSON Schema validation helpers
2//!
3//! Provides schema validation for various file formats (ODCS, ODCL, ODPS, CADS, Decision, Knowledge, etc.)
4//! This module is gated by the `schema-validation` feature and is available to all SDK consumers.
5
6/// Format validation error with path information
7#[cfg(feature = "schema-validation")]
8fn format_validation_error(error: &jsonschema::ValidationError, schema_type: &str) -> String {
9    // Extract instance path (JSON path where error occurred)
10    let instance_path = error.instance_path();
11
12    // Format the path nicely - Location implements Display/Debug
13    let path_str = instance_path.to_string();
14    let path_str = if path_str == "/" || path_str.is_empty() {
15        "root".to_string()
16    } else {
17        path_str
18    };
19
20    // Get the error message
21    let error_message = error.to_string();
22
23    format!(
24        "{} validation failed at path '{}': {}",
25        schema_type, path_str, error_message
26    )
27}
28
29/// Validate an ODCS file against the ODCS JSON Schema
30/// Automatically detects and validates ODCL format files against ODCL schema
31/// Returns a string error for use by both CLI and import/export modules
32#[cfg(feature = "schema-validation")]
33pub fn validate_odcs_internal(content: &str) -> Result<(), String> {
34    use jsonschema::Validator;
35    use serde_json::Value;
36
37    // Parse YAML content to check format
38    let data: Value =
39        serde_yaml::from_str(content).map_err(|e| format!("Failed to parse YAML: {}", e))?;
40
41    // Check if this is an ODCL format file (legacy format)
42    // ODCL files have "dataContractSpecification" field or simple "name"/"columns" structure
43    let is_odcl_format = if let Some(obj) = data.as_object() {
44        // Check for ODCL v3 format (dataContractSpecification)
45        obj.contains_key("dataContractSpecification")
46            // Check for simple ODCL format (name + columns, but no apiVersion/kind/schema)
47            || (obj.contains_key("name")
48                && obj.contains_key("columns")
49                && !obj.contains_key("apiVersion")
50                && !obj.contains_key("kind")
51                && !obj.contains_key("schema"))
52    } else {
53        false
54    };
55
56    // Validate against ODCL schema if ODCL format detected
57    if is_odcl_format {
58        return validate_odcl_internal(content);
59    }
60
61    // Load ODCS JSON Schema
62    let schema_content = include_str!("../../../../schemas/odcs-json-schema-v3.1.0.json");
63    let schema: Value = serde_json::from_str(schema_content)
64        .map_err(|e| format!("Failed to load ODCS schema: {}", e))?;
65
66    let validator =
67        Validator::new(&schema).map_err(|e| format!("Failed to compile ODCS schema: {}", e))?;
68
69    // Validate against ODCS schema
70    if let Err(error) = validator.validate(&data) {
71        // Extract path information from validation error
72        let error_msg = format_validation_error(&error, "ODCS");
73        return Err(error_msg);
74    }
75
76    Ok(())
77}
78
79#[cfg(not(feature = "schema-validation"))]
80pub fn validate_odcs_internal(_content: &str) -> Result<(), String> {
81    // Validation disabled - feature not enabled
82    Ok(())
83}
84
85/// Validate an ODCL file against the ODCL JSON Schema
86#[cfg(feature = "schema-validation")]
87pub fn validate_odcl_internal(content: &str) -> Result<(), String> {
88    use jsonschema::Validator;
89    use serde_json::Value;
90
91    // Load ODCL JSON Schema
92    let schema_content = include_str!("../../../../schemas/odcl-json-schema-1.2.1.json");
93    let schema: Value = serde_json::from_str(schema_content)
94        .map_err(|e| format!("Failed to load ODCL schema: {}", e))?;
95
96    let validator =
97        Validator::new(&schema).map_err(|e| format!("Failed to compile ODCL schema: {}", e))?;
98
99    // Parse YAML content
100    let data: Value =
101        serde_yaml::from_str(content).map_err(|e| format!("Failed to parse YAML: {}", e))?;
102
103    // Validate
104    if let Err(error) = validator.validate(&data) {
105        let error_msg = format_validation_error(&error, "ODCL");
106        return Err(error_msg);
107    }
108
109    Ok(())
110}
111
112#[cfg(not(feature = "schema-validation"))]
113pub fn validate_odcl_internal(_content: &str) -> Result<(), String> {
114    // Validation disabled - feature not enabled
115    Ok(())
116}
117
118/// Validate an OpenAPI file against the OpenAPI JSON Schema
119#[cfg(feature = "schema-validation")]
120pub fn validate_openapi_internal(content: &str) -> Result<(), String> {
121    use jsonschema::Validator;
122    use serde_json::Value;
123
124    // Load OpenAPI JSON Schema
125    let schema_content = include_str!("../../../../schemas/openapi-3.1.1.json");
126    let schema: Value = serde_json::from_str(schema_content)
127        .map_err(|e| format!("Failed to load OpenAPI schema: {}", e))?;
128
129    let validator =
130        Validator::new(&schema).map_err(|e| format!("Failed to compile OpenAPI schema: {}", e))?;
131
132    // Parse YAML or JSON content
133    let data: Value = if content.trim_start().starts_with('{') {
134        serde_json::from_str(content).map_err(|e| format!("Failed to parse JSON: {}", e))?
135    } else {
136        serde_yaml::from_str(content).map_err(|e| format!("Failed to parse YAML: {}", e))?
137    };
138
139    // Validate
140    if let Err(error) = validator.validate(&data) {
141        return Err(format!("OpenAPI validation failed: {}", error));
142    }
143
144    Ok(())
145}
146
147#[cfg(not(feature = "schema-validation"))]
148pub fn validate_openapi_internal(_content: &str) -> Result<(), String> {
149    // Validation disabled - feature not enabled
150    Ok(())
151}
152
153/// Validate Protobuf file syntax
154pub fn validate_protobuf_internal(content: &str) -> Result<(), String> {
155    // Basic syntax validation - check for common proto keywords
156    if !content.contains("syntax") && !content.contains("message") && !content.contains("enum") {
157        return Err("File does not appear to be a valid Protobuf file".to_string());
158    }
159
160    // Check for balanced braces (basic syntax check)
161    let open_braces = content.matches('{').count();
162    let close_braces = content.matches('}').count();
163    if open_braces != close_braces {
164        return Err(format!(
165            "Unbalanced braces in Protobuf file ({} open, {} close)",
166            open_braces, close_braces
167        ));
168    }
169
170    Ok(())
171}
172
173/// Validate AVRO file against AVRO specification
174pub fn validate_avro_internal(content: &str) -> Result<(), String> {
175    // Parse as JSON
176    let _value: serde_json::Value =
177        serde_json::from_str(content).map_err(|e| format!("Failed to parse AVRO JSON: {}", e))?;
178
179    // Basic validation - check for required AVRO fields
180    // More comprehensive validation would require an AVRO schema validator crate
181    Ok(())
182}
183
184/// Validate JSON Schema file
185#[cfg(feature = "schema-validation")]
186pub fn validate_json_schema_internal(content: &str) -> Result<(), String> {
187    use jsonschema::Validator;
188    use serde_json::Value;
189
190    // Parse JSON Schema
191    let schema: Value =
192        serde_json::from_str(content).map_err(|e| format!("Failed to parse JSON Schema: {}", e))?;
193
194    // Try to compile the schema (this validates the schema itself)
195    Validator::new(&schema).map_err(|e| format!("Invalid JSON Schema: {}", e))?;
196
197    Ok(())
198}
199
200#[cfg(not(feature = "schema-validation"))]
201pub fn validate_json_schema_internal(_content: &str) -> Result<(), String> {
202    // Validation disabled - feature not enabled
203    Ok(())
204}
205
206/// Internal ODPS validation function that returns a string error (used by both CLI and import/export modules)
207#[cfg(feature = "schema-validation")]
208pub fn validate_odps_internal(content: &str) -> Result<(), String> {
209    use jsonschema::Validator;
210    use serde_json::Value;
211
212    // Load ODPS JSON Schema
213    let schema_content = include_str!("../../../../schemas/odps-json-schema-latest.json");
214    let schema: Value = serde_json::from_str(schema_content)
215        .map_err(|e| format!("Failed to load ODPS schema: {}", e))?;
216
217    let validator =
218        Validator::new(&schema).map_err(|e| format!("Failed to compile ODPS schema: {}", e))?;
219
220    // Parse YAML content
221    let data: Value =
222        serde_yaml::from_str(content).map_err(|e| format!("Failed to parse YAML: {}", e))?;
223
224    // Validate
225    if let Err(error) = validator.validate(&data) {
226        let instance_path = error.instance_path();
227        let path_str = instance_path.to_string();
228        let path_str = if path_str == "/" || path_str.is_empty() {
229            "root".to_string()
230        } else {
231            path_str
232        };
233        return Err(format!(
234            "ODPS validation failed at path '{}': {}",
235            path_str, error
236        ));
237    }
238
239    Ok(())
240}
241
242#[cfg(not(feature = "schema-validation"))]
243pub fn validate_odps_internal(_content: &str) -> Result<(), String> {
244    // Validation disabled - feature not enabled
245    Ok(())
246}
247
248/// Internal CADS validation function that returns a string error (used by export modules)
249#[cfg(feature = "schema-validation")]
250pub fn validate_cads_internal(content: &str) -> Result<(), String> {
251    use jsonschema::Validator;
252    use serde_json::Value;
253
254    // Load CADS JSON Schema
255    let schema_content = include_str!("../../../../schemas/cads.schema.json");
256    let schema: Value = serde_json::from_str(schema_content)
257        .map_err(|e| format!("Failed to load CADS schema: {}", e))?;
258
259    let validator =
260        Validator::new(&schema).map_err(|e| format!("Failed to compile CADS schema: {}", e))?;
261
262    // Parse YAML content
263    let data: Value =
264        serde_yaml::from_str(content).map_err(|e| format!("Failed to parse YAML: {}", e))?;
265
266    // Validate
267    if let Err(error) = validator.validate(&data) {
268        let instance_path = error.instance_path();
269        let path_str = instance_path.to_string();
270        let path_str = if path_str == "/" || path_str.is_empty() {
271            "root".to_string()
272        } else {
273            path_str
274        };
275        return Err(format!(
276            "CADS validation failed at path '{}': {}",
277            path_str, error
278        ));
279    }
280
281    Ok(())
282}
283
284#[cfg(not(feature = "schema-validation"))]
285pub fn validate_cads_internal(_content: &str) -> Result<(), String> {
286    // Validation disabled - feature not enabled
287    Ok(())
288}
289
290/// Validate SQL syntax using sqlparser
291pub fn validate_sql_internal(content: &str) -> Result<(), String> {
292    use sqlparser::dialect::GenericDialect;
293    use sqlparser::parser::Parser;
294
295    let dialect = GenericDialect {};
296
297    Parser::parse_sql(&dialect, content).map_err(|e| format!("SQL validation failed: {}", e))?;
298
299    Ok(())
300}
301
302/// Validate a workspace.yaml file against the workspace JSON Schema
303#[cfg(feature = "schema-validation")]
304pub fn validate_workspace_internal(content: &str) -> Result<(), String> {
305    use jsonschema::Validator;
306    use serde_json::Value;
307
308    // Load workspace JSON Schema
309    let schema_content = include_str!("../../../../schemas/workspace-schema.json");
310    let schema: Value = serde_json::from_str(schema_content)
311        .map_err(|e| format!("Failed to load workspace schema: {}", e))?;
312
313    let validator = Validator::new(&schema)
314        .map_err(|e| format!("Failed to compile workspace schema: {}", e))?;
315
316    // Parse YAML content
317    let data: Value =
318        serde_yaml::from_str(content).map_err(|e| format!("Failed to parse YAML: {}", e))?;
319
320    // Validate
321    if let Err(error) = validator.validate(&data) {
322        let error_msg = format_validation_error(&error, "Workspace");
323        return Err(error_msg);
324    }
325
326    Ok(())
327}
328
329#[cfg(not(feature = "schema-validation"))]
330pub fn validate_workspace_internal(_content: &str) -> Result<(), String> {
331    // Validation disabled - feature not enabled
332    Ok(())
333}
334
335/// Validate a relationships.yaml file
336pub fn validate_relationships_internal(content: &str) -> Result<(), String> {
337    use serde_json::Value;
338
339    // Parse YAML content
340    let data: Value =
341        serde_yaml::from_str(content).map_err(|e| format!("Failed to parse YAML: {}", e))?;
342
343    // Check structure - should be an object with "relationships" array or a direct array
344    let relationships = data
345        .get("relationships")
346        .and_then(|v| v.as_array())
347        .or_else(|| data.as_array());
348
349    if let Some(rels) = relationships {
350        for (i, rel) in rels.iter().enumerate() {
351            // Each relationship should have source_table_id and target_table_id
352            if rel.get("source_table_id").is_none() {
353                return Err(format!("Relationship {} is missing 'source_table_id'", i));
354            }
355            if rel.get("target_table_id").is_none() {
356                return Err(format!("Relationship {} is missing 'target_table_id'", i));
357            }
358        }
359    }
360
361    Ok(())
362}
363
364/// Internal decision validation function that returns a string error (used by import/export modules)
365#[cfg(feature = "schema-validation")]
366pub fn validate_decision_internal(content: &str) -> Result<(), String> {
367    use jsonschema::Validator;
368    use serde_json::Value;
369
370    // Load Decision JSON Schema
371    let schema_content = include_str!("../../../../schemas/decision-schema.json");
372    let schema: Value = serde_json::from_str(schema_content)
373        .map_err(|e| format!("Failed to load decision schema: {}", e))?;
374
375    let validator =
376        Validator::new(&schema).map_err(|e| format!("Failed to compile decision schema: {}", e))?;
377
378    // Parse YAML content
379    let data: Value =
380        serde_yaml::from_str(content).map_err(|e| format!("Failed to parse YAML: {}", e))?;
381
382    // Validate
383    if let Err(error) = validator.validate(&data) {
384        let instance_path = error.instance_path();
385        let path_str = instance_path.to_string();
386        let path_str = if path_str == "/" || path_str.is_empty() {
387            "root".to_string()
388        } else {
389            path_str
390        };
391        return Err(format!(
392            "Decision validation failed at path '{}': {}",
393            path_str, error
394        ));
395    }
396
397    Ok(())
398}
399
400#[cfg(not(feature = "schema-validation"))]
401pub fn validate_decision_internal(_content: &str) -> Result<(), String> {
402    // Validation disabled - feature not enabled
403    Ok(())
404}
405
406/// Internal knowledge validation function that returns a string error (used by import/export modules)
407#[cfg(feature = "schema-validation")]
408pub fn validate_knowledge_internal(content: &str) -> Result<(), String> {
409    use jsonschema::Validator;
410    use serde_json::Value;
411
412    // Load Knowledge JSON Schema
413    let schema_content = include_str!("../../../../schemas/knowledge-schema.json");
414    let schema: Value = serde_json::from_str(schema_content)
415        .map_err(|e| format!("Failed to load knowledge schema: {}", e))?;
416
417    let validator = Validator::new(&schema)
418        .map_err(|e| format!("Failed to compile knowledge schema: {}", e))?;
419
420    // Parse YAML content
421    let data: Value =
422        serde_yaml::from_str(content).map_err(|e| format!("Failed to parse YAML: {}", e))?;
423
424    // Validate
425    if let Err(error) = validator.validate(&data) {
426        let instance_path = error.instance_path();
427        let path_str = instance_path.to_string();
428        let path_str = if path_str == "/" || path_str.is_empty() {
429            "root".to_string()
430        } else {
431            path_str
432        };
433        return Err(format!(
434            "Knowledge validation failed at path '{}': {}",
435            path_str, error
436        ));
437    }
438
439    Ok(())
440}
441
442#[cfg(not(feature = "schema-validation"))]
443pub fn validate_knowledge_internal(_content: &str) -> Result<(), String> {
444    // Validation disabled - feature not enabled
445    Ok(())
446}
447
448/// Validate a decisions index (decisions.yaml) file against the decisions-index JSON Schema
449#[cfg(feature = "schema-validation")]
450pub fn validate_decisions_index_internal(content: &str) -> Result<(), String> {
451    use jsonschema::Validator;
452    use serde_json::Value;
453
454    // Load Decisions Index JSON Schema
455    let schema_content = include_str!("../../../../schemas/decisions-index-schema.json");
456    let schema: Value = serde_json::from_str(schema_content)
457        .map_err(|e| format!("Failed to load decisions-index schema: {}", e))?;
458
459    let validator = Validator::new(&schema)
460        .map_err(|e| format!("Failed to compile decisions-index schema: {}", e))?;
461
462    // Parse YAML content
463    let data: Value =
464        serde_yaml::from_str(content).map_err(|e| format!("Failed to parse YAML: {}", e))?;
465
466    // Validate
467    if let Err(error) = validator.validate(&data) {
468        let error_msg = format_validation_error(&error, "Decisions Index");
469        return Err(error_msg);
470    }
471
472    Ok(())
473}
474
475#[cfg(not(feature = "schema-validation"))]
476pub fn validate_decisions_index_internal(_content: &str) -> Result<(), String> {
477    // Validation disabled - feature not enabled
478    Ok(())
479}
480
481/// Validate a knowledge index (knowledge.yaml) file against the knowledge-index JSON Schema
482#[cfg(feature = "schema-validation")]
483pub fn validate_knowledge_index_internal(content: &str) -> Result<(), String> {
484    use jsonschema::Validator;
485    use serde_json::Value;
486
487    // Load Knowledge Index JSON Schema
488    let schema_content = include_str!("../../../../schemas/knowledge-index-schema.json");
489    let schema: Value = serde_json::from_str(schema_content)
490        .map_err(|e| format!("Failed to load knowledge-index schema: {}", e))?;
491
492    let validator = Validator::new(&schema)
493        .map_err(|e| format!("Failed to compile knowledge-index schema: {}", e))?;
494
495    // Parse YAML content
496    let data: Value =
497        serde_yaml::from_str(content).map_err(|e| format!("Failed to parse YAML: {}", e))?;
498
499    // Validate
500    if let Err(error) = validator.validate(&data) {
501        let error_msg = format_validation_error(&error, "Knowledge Index");
502        return Err(error_msg);
503    }
504
505    Ok(())
506}
507
508#[cfg(not(feature = "schema-validation"))]
509pub fn validate_knowledge_index_internal(_content: &str) -> Result<(), String> {
510    // Validation disabled - feature not enabled
511    Ok(())
512}