/ ferris-proof-config / src / schema.rs
schema.rs
  1  use anyhow::{anyhow, bail, Result};
  2  use jsonschema::{JSONSchema, ValidationError};
  3  use serde_json::{json, Value};
  4  use tracing::debug;
  5  
  6  pub struct SchemaValidator {
  7      config_schema: JSONSchema,
  8      module_schema: JSONSchema,
  9  }
 10  
 11  impl std::fmt::Debug for SchemaValidator {
 12      fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 13          f.debug_struct("SchemaValidator")
 14              .field("config_schema", &"<JSONSchema>")
 15              .field("module_schema", &"<JSONSchema>")
 16              .finish()
 17      }
 18  }
 19  
 20  impl SchemaValidator {
 21      pub fn new() -> Result<Self> {
 22          let config_schema = Self::build_config_schema()?;
 23          let module_schema = Self::build_module_schema()?;
 24  
 25          Ok(Self {
 26              config_schema,
 27              module_schema,
 28          })
 29      }
 30  
 31      /// Validate a root configuration file
 32      pub fn validate(&self, config: &Value) -> Result<()> {
 33          debug!("Validating configuration against schema");
 34  
 35          match self.config_schema.validate(config) {
 36              Ok(_) => {
 37                  debug!("Configuration schema validation passed");
 38                  Ok(())
 39              }
 40              Err(errors) => {
 41                  let error_messages: Vec<String> = errors
 42                      .into_iter()
 43                      .map(|e| format!("{:?} at {}", e.kind, e.instance_path))
 44                      .collect();
 45  
 46                  bail!(
 47                      "Configuration validation failed:\n{}",
 48                      error_messages.join("\n")
 49                  );
 50              }
 51          }
 52      }
 53  
 54      /// Validate a module configuration (partial config)
 55      pub fn validate_module(&self, config: &Value) -> Result<()> {
 56          debug!("Validating module configuration against schema");
 57  
 58          match self.module_schema.validate(config) {
 59              Ok(_) => {
 60                  debug!("Module configuration schema validation passed");
 61                  Ok(())
 62              }
 63              Err(errors) => {
 64                  let error_messages: Vec<String> = errors
 65                      .into_iter()
 66                      .map(|e| format!("{:?} at {}", e.kind, e.instance_path))
 67                      .collect();
 68  
 69                  bail!(
 70                      "Module configuration validation failed:\n{}",
 71                      error_messages.join("\n")
 72                  );
 73              }
 74          }
 75      }
 76  
 77      /// Get detailed validation errors with suggestions
 78      pub fn validate_with_details(&self, config: &Value) -> ValidationResult {
 79          let mut result = ValidationResult {
 80              is_valid: true,
 81              errors: Vec::new(),
 82              warnings: Vec::new(),
 83              suggestions: Vec::new(),
 84          };
 85  
 86          // Validate against schema
 87          if let Err(errors) = self.config_schema.validate(config) {
 88              result.is_valid = false;
 89              for error in errors {
 90                  let location = error.instance_path.to_string();
 91                  let message = format!("{:?}", error.kind);
 92  
 93                  let validation_error = ValidationErrorDetail {
 94                      field: if location.is_empty() {
 95                          "root".to_string()
 96                      } else {
 97                          location.clone()
 98                      },
 99                      message,
100                      location: location.clone(),
101                      expected_value: self.get_expected_value_for_field(&location),
102                      suggestion: self.get_suggestion_for_field(&location, &error),
103                  };
104  
105                  result.errors.push(validation_error);
106              }
107          }
108  
109          // Additional business logic validation
110          self.validate_business_logic(config, &mut result);
111  
112          result
113      }
114  
115      /// Validate business logic beyond schema validation
116      fn validate_business_logic(&self, config: &Value, result: &mut ValidationResult) {
117          // Check verification level vs enabled techniques
118          if let (Some(level), Some(techniques)) = (
119              config
120                  .get("profile")
121                  .and_then(|p| p.get("level"))
122                  .and_then(|l| l.as_str()),
123              config
124                  .get("profile")
125                  .and_then(|p| p.get("enabled_techniques"))
126                  .and_then(|t| t.as_array()),
127          ) {
128              self.validate_level_techniques_consistency(level, techniques, result);
129          }
130  
131          // Check threshold values
132          if let Some(thresholds) = config.get("thresholds") {
133              self.validate_thresholds(thresholds, result);
134          }
135  
136          // Check tool configurations
137          if let Some(tools) = config.get("tools") {
138              self.validate_tool_configs(tools, result);
139          }
140      }
141  
142      fn validate_level_techniques_consistency(
143          &self,
144          level: &str,
145          techniques: &[Value],
146          result: &mut ValidationResult,
147      ) {
148          let techniques_vec: Vec<String> = techniques
149              .iter()
150              .filter_map(|t| t.as_str().map(|s| s.to_string()))
151              .collect();
152  
153          match level {
154              "minimal" => {
155                  if !techniques_vec.contains(&"TypeSafety".to_string()) {
156                      result.errors.push(ValidationErrorDetail {
157                          field: "profile.enabled_techniques".to_string(),
158                          message: "Minimal level must include TypeSafety technique".to_string(),
159                          location: "profile.enabled_techniques".to_string(),
160                          expected_value: Some("TypeSafety".to_string()),
161                          suggestion: Some("Add TypeSafety to enabled_techniques or use a higher verification level".to_string()),
162                      });
163                      result.is_valid = false;
164                  }
165              }
166              "standard" => {
167                  if !techniques_vec.contains(&"PropertyTests".to_string()) {
168                      result.errors.push(ValidationErrorDetail {
169                          field: "profile.enabled_techniques".to_string(),
170                          message: "Standard level must include PropertyTests technique".to_string(),
171                          location: "profile.enabled_techniques".to_string(),
172                          expected_value: Some("PropertyTests".to_string()),
173                          suggestion: Some("Add PropertyTests to enabled_techniques or use a higher verification level".to_string()),
174                      });
175                      result.is_valid = false;
176                  }
177              }
178              "strict" => {
179                  if !techniques_vec.contains(&"SessionTypes".to_string()) {
180                      result.errors.push(ValidationErrorDetail {
181                          field: "profile.enabled_techniques".to_string(),
182                          message: "Strict level must include SessionTypes technique".to_string(),
183                          location: "profile.enabled_techniques".to_string(),
184                          expected_value: Some("SessionTypes".to_string()),
185                          suggestion: Some("Add SessionTypes to enabled_techniques or use the formal verification level".to_string()),
186                      });
187                      result.is_valid = false;
188                  }
189              }
190              "formal" => {
191                  if !techniques_vec.contains(&"FormalSpecs".to_string()) {
192                      result.errors.push(ValidationErrorDetail {
193                          field: "profile.enabled_techniques".to_string(),
194                          message: "Formal level must include FormalSpecs technique".to_string(),
195                          location: "profile.enabled_techniques".to_string(),
196                          expected_value: Some("FormalSpecs".to_string()),
197                          suggestion: Some("Add FormalSpecs to enabled_techniques".to_string()),
198                      });
199                      result.is_valid = false;
200                  }
201              }
202              _ => {
203                  result.errors.push(ValidationErrorDetail {
204                      field: "profile.level".to_string(),
205                      message: format!("Unknown verification level: {}", level),
206                      location: "profile.level".to_string(),
207                      expected_value: Some("minimal, standard, strict, formal".to_string()),
208                      suggestion: Some("Use one of: minimal, standard, strict, formal".to_string()),
209                  });
210                  result.is_valid = false;
211              }
212          }
213      }
214  
215      fn validate_thresholds(&self, thresholds: &Value, result: &mut ValidationResult) {
216          if let Some(max_time) = thresholds
217              .get("max_verification_time")
218              .and_then(|t| t.as_u64())
219          {
220              if max_time == 0 {
221                  result.errors.push(ValidationErrorDetail {
222                      field: "thresholds.max_verification_time".to_string(),
223                      message: "max_verification_time must be > 0".to_string(),
224                      location: "thresholds.max_verification_time".to_string(),
225                      expected_value: Some("positive integer".to_string()),
226                      suggestion: Some(
227                          "Set to at least 60 seconds for basic verification".to_string(),
228                      ),
229                  });
230                  result.is_valid = false;
231              }
232              if max_time > 3600 {
233                  result.warnings.push(ValidationWarning {
234                      field: "thresholds.max_verification_time".to_string(),
235                      message: "max_verification_time is very high (> 1 hour)".to_string(),
236                      location: "thresholds.max_verification_time".to_string(),
237                      suggestion: Some(
238                          "Consider using a shorter timeout for faster feedback".to_string(),
239                      ),
240                  });
241              }
242          }
243  
244          if let Some(max_memory) = thresholds.get("max_memory_usage").and_then(|m| m.as_u64()) {
245              if max_memory == 0 {
246                  result.errors.push(ValidationErrorDetail {
247                      field: "thresholds.max_memory_usage".to_string(),
248                      message: "max_memory_usage must be > 0".to_string(),
249                      location: "thresholds.max_memory_usage".to_string(),
250                      expected_value: Some("positive integer (bytes)".to_string()),
251                      suggestion: Some(
252                          "Set to at least 1073741824 (1GB) for basic verification".to_string(),
253                      ),
254                  });
255                  result.is_valid = false;
256              }
257          }
258      }
259  
260      fn validate_tool_configs(&self, tools: &Value, result: &mut ValidationResult) {
261          if let Some(proptest) = tools.get("proptest") {
262              if let Some(cases) = proptest.get("cases").and_then(|c| c.as_u64()) {
263                  if cases == 0 {
264                      result.errors.push(ValidationErrorDetail {
265                          field: "tools.proptest.cases".to_string(),
266                          message: "proptest.cases must be > 0".to_string(),
267                          location: "tools.proptest.cases".to_string(),
268                          expected_value: Some("positive integer".to_string()),
269                          suggestion: Some(
270                              "Set to at least 100 test cases for reasonable coverage".to_string(),
271                          ),
272                      });
273                      result.is_valid = false;
274                  }
275                  if cases > 100000 {
276                      result.warnings.push(ValidationWarning {
277                          field: "tools.proptest.cases".to_string(),
278                          message: "proptest.cases is very high (>100k)".to_string(),
279                          location: "tools.proptest.cases".to_string(),
280                          suggestion: Some(
281                              "Consider reducing cases for faster test execution".to_string(),
282                          ),
283                      });
284                  }
285              }
286          }
287      }
288  
289      fn get_expected_value_for_field(&self, field: &str) -> Option<String> {
290          match field {
291              "profile.level" => Some("one of: minimal, standard, strict, formal".to_string()),
292              "profile.enforcement" => Some("one of: advisory, warning, error".to_string()),
293              "thresholds.max_verification_time" => Some("positive integer (seconds)".to_string()),
294              "thresholds.max_memory_usage" => Some("positive integer (bytes)".to_string()),
295              "tools.proptest.cases" => Some("positive integer".to_string()),
296              _ => None,
297          }
298      }
299  
300      fn get_suggestion_for_field(&self, field: &str, _error: &ValidationError) -> Option<String> {
301          match field {
302              "profile.level" => Some("Use 'minimal' for basic checks, 'standard' for most projects, 'strict' for critical systems, or 'formal' for safety-critical systems".to_string()),
303              "profile.enforcement" => Some("Use 'advisory' during development, 'warning' for CI, or 'error' for production builds".to_string()),
304              _ => None,
305          }
306      }
307  
308      /// Build the JSON schema for root configuration
309      fn build_config_schema() -> Result<JSONSchema> {
310          let schema = json!({
311              "$schema": "http://json-schema.org/draft-07/schema#",
312              "title": "FerrisProof Configuration",
313              "description": "Configuration for FerrisProof multi-layer verification pipeline",
314              "type": "object",
315              "properties": {
316                  "profile": {
317                      "type": "object",
318                      "properties": {
319                          "level": {
320                              "type": "string",
321                              "enum": ["minimal", "standard", "strict", "formal"],
322                              "description": "Verification level for the project"
323                          },
324                          "enforcement": {
325                              "type": "string",
326                              "enum": ["advisory", "warning", "error"],
327                              "description": "How verification violations should be handled"
328                          },
329                          "enabled_techniques": {
330                              "type": "array",
331                              "items": {
332                                  "type": "string",
333                                  "enum": ["TypeSafety", "PropertyTests", "SessionTypes", "RefinementTypes", "ConcurrencyTesting", "FormalSpecs", "ModelChecking"]
334                              },
335                              "description": "List of verification techniques to enable"
336                          }
337                      },
338                      "required": ["level", "enforcement", "enabled_techniques"]
339                  },
340                  "tools": {
341                      "type": "object",
342                      "properties": {
343                          "tla_plus": {
344                              "$ref": "#/definitions/TlaPlusConfig"
345                          },
346                          "alloy": {
347                              "$ref": "#/definitions/AlloyConfig"
348                          },
349                          "proptest": {
350                              "$ref": "#/definitions/ProptestConfig"
351                          },
352                          "kani": {
353                              "$ref": "#/definitions/KaniConfig"
354                          }
355                      },
356                      "additionalProperties": false
357                  },
358                  "modules": {
359                      "type": "object",
360                      "patternProperties": {
361                          "^.*$": {
362                              "$ref": "#/definitions/ModuleConfig"
363                          }
364                      },
365                      "additionalProperties": false
366                  },
367                  "features": {
368                      "$ref": "#/definitions/FeatureConfig"
369                  },
370                  "thresholds": {
371                      "$ref": "#/definitions/Thresholds"
372                  },
373                  "ci": {
374                      "$ref": "#/definitions/CiConfig"
375                  }
376              },
377              "required": ["profile"],
378              "additionalProperties": false,
379              "definitions": {
380                  "TlaPlusConfig": {
381                      "type": "object",
382                      "properties": {
383                          "tlc_path": {
384                              "type": "string",
385                              "description": "Path to TLC model checker executable"
386                          },
387                          "timeout": {
388                              "type": "integer",
389                              "minimum": 1,
390                              "description": "Timeout in seconds for TLA+ model checking"
391                          },
392                          "workers": {
393                              "type": "integer",
394                              "minimum": 1,
395                              "description": "Number of parallel workers for model checking"
396                          }
397                      },
398                      "additionalProperties": false
399                  },
400                  "AlloyConfig": {
401                      "type": "object",
402                      "properties": {
403                          "analyzer_path": {
404                              "type": "string",
405                              "description": "Path to Alloy Analyzer executable"
406                          },
407                          "scope": {
408                              "type": "integer",
409                              "minimum": 1,
410                              "description": "Scope for Alloy analysis"
411                          }
412                      },
413                      "additionalProperties": false
414                  },
415                  "ProptestConfig": {
416                      "type": "object",
417                      "properties": {
418                          "cases": {
419                              "type": "integer",
420                              "minimum": 1,
421                              "description": "Number of test cases to generate"
422                          },
423                          "max_shrink_iters": {
424                              "type": "integer",
425                              "minimum": 1,
426                              "description": "Maximum iterations for test case shrinking"
427                          }
428                      },
429                      "additionalProperties": false
430                  },
431                  "KaniConfig": {
432                      "type": "object",
433                      "properties": {
434                          "cbmc_path": {
435                              "type": "string",
436                              "description": "Path to CBMC executable"
437                          },
438                          "unwind": {
439                              "type": "integer",
440                              "minimum": 1,
441                              "description": "Unwind depth for Kani verification"
442                          }
443                      },
444                      "additionalProperties": false
445                  },
446                  "ModuleConfig": {
447                      "type": "object",
448                      "properties": {
449                          "level": {
450                              "type": "string",
451                              "enum": ["minimal", "standard", "strict", "formal"]
452                          },
453                          "enforcement": {
454                              "type": "string",
455                              "enum": ["advisory", "warning", "error"]
456                          },
457                          "enabled_techniques": {
458                              "type": "array",
459                              "items": {
460                                  "type": "string"
461                              }
462                          },
463                          "spec_file": {
464                              "type": "string",
465                              "description": "Path to formal specification file"
466                          }
467                      },
468                      "additionalProperties": false
469                  },
470                  "FeatureConfig": {
471                      "type": "object",
472                      "properties": {
473                          "cache_enabled": {
474                              "type": "boolean",
475                              "description": "Enable verification result caching"
476                          },
477                          "parallel_execution": {
478                              "type": "boolean",
479                              "description": "Enable parallel verification execution"
480                          },
481                          "generate_reports": {
482                              "type": "boolean",
483                              "description": "Generate verification reports"
484                          }
485                      },
486                      "required": ["cache_enabled", "parallel_execution", "generate_reports"]
487                  },
488                  "Thresholds": {
489                      "type": "object",
490                      "properties": {
491                          "max_verification_time": {
492                              "type": "integer",
493                              "minimum": 1,
494                              "description": "Maximum verification time in seconds"
495                          },
496                          "max_memory_usage": {
497                              "type": "integer",
498                              "minimum": 1,
499                              "description": "Maximum memory usage in bytes"
500                          },
501                          "cache_ttl": {
502                              "type": "integer",
503                              "minimum": 1,
504                              "description": "Cache time-to-live in seconds"
505                          }
506                      },
507                      "required": ["max_verification_time", "max_memory_usage", "cache_ttl"]
508                  },
509                  "CiConfig": {
510                      "type": "object",
511                      "properties": {
512                          "fail_on_violations": {
513                              "type": "boolean",
514                              "description": "Fail CI builds on verification violations"
515                          },
516                          "generate_artifacts": {
517                              "type": "boolean",
518                              "description": "Generate verification artifacts in CI"
519                          },
520                          "upload_reports": {
521                              "type": "boolean",
522                              "description": "Upload verification reports"
523                          }
524                      },
525                      "required": ["fail_on_violations", "generate_artifacts", "upload_reports"]
526                  }
527              }
528          });
529  
530          JSONSchema::compile(&schema).map_err(|e| anyhow!("Failed to compile config schema: {}", e))
531      }
532  
533      /// Build the JSON schema for module configuration (partial config)
534      fn build_module_schema() -> Result<JSONSchema> {
535          let schema = json!({
536              "$schema": "http://json-schema.org/draft-07/schema#",
537              "title": "FerrisProof Module Configuration",
538              "description": "Partial configuration for specific modules",
539              "type": "object",
540              "properties": {
541                  "profile": {
542                      "type": "object",
543                      "properties": {
544                          "level": {
545                              "type": "string",
546                              "enum": ["minimal", "standard", "strict", "formal"]
547                          },
548                          "enforcement": {
549                              "type": "string",
550                              "enum": ["advisory", "warning", "error"]
551                          },
552                          "enabled_techniques": {
553                              "type": "array",
554                              "items": {
555                                  "type": "string"
556                              }
557                          }
558                      }
559                  },
560                  "modules": {
561                      "type": "object",
562                      "patternProperties": {
563                          "^.*$": {
564                              "$ref": "#/definitions/ModuleConfig"
565                          }
566                      }
567                  }
568              },
569              "additionalProperties": false,
570              "definitions": {
571                  "ModuleConfig": {
572                      "type": "object",
573                      "properties": {
574                          "level": {
575                              "type": "string",
576                              "enum": ["minimal", "standard", "strict", "formal"]
577                          },
578                          "enforcement": {
579                              "type": "string",
580                              "enum": ["advisory", "warning", "error"]
581                          },
582                          "enabled_techniques": {
583                              "type": "array",
584                              "items": {
585                                  "type": "string"
586                              }
587                          },
588                          "spec_file": {
589                              "type": "string"
590                          }
591                      }
592                  }
593              }
594          });
595  
596          JSONSchema::compile(&schema).map_err(|e| anyhow!("Failed to compile module schema: {}", e))
597      }
598  }
599  
600  impl Default for SchemaValidator {
601      fn default() -> Self {
602          Self::new().expect("Failed to create schema validator")
603      }
604  }
605  
606  #[derive(Debug, Clone)]
607  pub struct ValidationResult {
608      pub is_valid: bool,
609      pub errors: Vec<ValidationErrorDetail>,
610      pub warnings: Vec<ValidationWarning>,
611      pub suggestions: Vec<String>,
612  }
613  
614  #[derive(Debug, Clone)]
615  pub struct ValidationErrorDetail {
616      pub field: String,
617      pub message: String,
618      pub location: String,
619      pub expected_value: Option<String>,
620      pub suggestion: Option<String>,
621  }
622  
623  #[derive(Debug, Clone)]
624  pub struct ValidationWarning {
625      pub field: String,
626      pub message: String,
627      pub location: String,
628      pub suggestion: Option<String>,
629  }