use crate::core::error::{GraphRAGError, Result};
use serde_json::Value;
use std::path::Path;
#[cfg(feature = "json5-support")]
pub fn validate_config(config_value: &Value, schema_value: &Value) -> Result<()> {
use jsonschema::JSONSchema;
let schema = JSONSchema::compile(schema_value).map_err(|e| GraphRAGError::Config {
message: format!("Invalid JSON Schema: {}", e),
})?;
if let Err(errors) = schema.validate(config_value) {
let error_messages: Vec<String> = errors
.map(|error| format!("Validation error at '{}': {}", error.instance_path, error))
.collect();
return Err(GraphRAGError::Config {
message: format!(
"Configuration validation failed:\n{}",
error_messages.join("\n")
),
});
}
Ok(())
}
#[cfg(feature = "json5-support")]
pub fn load_schema<P: AsRef<Path>>(schema_path: P) -> Result<Value> {
let path = schema_path.as_ref();
let schema_str = std::fs::read_to_string(path).map_err(|e| GraphRAGError::Config {
message: format!("Failed to read schema file {:?}: {}", path, e),
})?;
serde_json::from_str(&schema_str).map_err(|e| GraphRAGError::Config {
message: format!("Failed to parse schema JSON: {}", e),
})
}
#[cfg(feature = "json5-support")]
pub fn validate_config_file<P1, P2>(config_path: P1, schema_path: P2) -> Result<()>
where
P1: AsRef<Path>,
P2: AsRef<Path>,
{
let config_str =
std::fs::read_to_string(config_path.as_ref()).map_err(|e| GraphRAGError::Config {
message: format!("Failed to read config file: {}", e),
})?;
let config_value: Value = if config_path
.as_ref()
.extension()
.and_then(|e| e.to_str())
.map(|e| e == "json5")
.unwrap_or(false)
{
json5::from_str(&config_str).map_err(|e| GraphRAGError::Config {
message: format!("Failed to parse JSON5 config: {}", e),
})?
} else {
serde_json::from_str(&config_str).map_err(|e| GraphRAGError::Config {
message: format!("Failed to parse JSON config: {}", e),
})?
};
let schema_value = load_schema(schema_path)?;
validate_config(&config_value, &schema_value)
}
#[cfg(feature = "json5-support")]
pub fn format_validation_error(error: &GraphRAGError) -> String {
match error {
GraphRAGError::Config { message } => {
if message.contains("Validation error at") {
let lines: Vec<&str> = message.lines().collect();
if lines.len() > 1 {
let mut formatted = String::from("❌ Configuration validation failed:\n\n");
for (i, line) in lines.iter().skip(1).enumerate() {
formatted.push_str(&format!(" {}. {}\n", i + 1, line));
}
formatted.push_str("\n💡 Tips:\n");
formatted.push_str(" - Check your config file for typos\n");
formatted.push_str(" - Verify required fields are present\n");
formatted.push_str(" - Ensure values are within valid ranges\n");
formatted.push_str(" - See examples in config/templates/\n");
return formatted;
}
}
format!("❌ Configuration error: {}", message)
},
_ => format!("❌ Error: {:?}", error),
}
}
#[derive(Debug, Clone)]
pub struct ValidationResult {
pub valid: bool,
pub errors: Vec<ValidationError>,
}
#[derive(Debug, Clone)]
pub struct ValidationError {
pub path: String,
pub message: String,
pub suggestion: Option<String>,
}
impl ValidationResult {
pub fn success() -> Self {
Self {
valid: true,
errors: Vec::new(),
}
}
pub fn failure(errors: Vec<ValidationError>) -> Self {
Self {
valid: false,
errors,
}
}
pub fn is_valid(&self) -> bool {
self.valid
}
pub fn format_errors(&self) -> String {
if self.valid {
return String::from("✅ Configuration is valid");
}
let mut formatted = String::from("❌ Configuration validation failed:\n\n");
for (i, error) in self.errors.iter().enumerate() {
formatted.push_str(&format!(
" {}. At '{}': {}\n",
i + 1,
error.path,
error.message
));
if let Some(suggestion) = &error.suggestion {
formatted.push_str(&format!(" 💡 Suggestion: {}\n", suggestion));
}
}
formatted
}
}
#[cfg(all(test, feature = "json5-support"))]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_validate_simple_config() {
let schema = json!({
"type": "object",
"required": ["name"],
"properties": {
"name": {
"type": "string"
},
"age": {
"type": "integer",
"minimum": 0
}
}
});
let valid_config = json!({
"name": "Test",
"age": 25
});
assert!(validate_config(&valid_config, &schema).is_ok());
let invalid_config = json!({
"age": 25
});
assert!(validate_config(&invalid_config, &schema).is_err());
let invalid_config = json!({
"name": "Test",
"age": "not a number"
});
assert!(validate_config(&invalid_config, &schema).is_err());
let invalid_config = json!({
"name": "Test",
"age": -1
});
assert!(validate_config(&invalid_config, &schema).is_err());
}
#[test]
fn test_validate_with_enum() {
let schema = json!({
"type": "object",
"properties": {
"mode": {
"type": "string",
"enum": ["semantic", "algorithmic", "hybrid"]
}
}
});
let valid = json!({"mode": "semantic"});
assert!(validate_config(&valid, &schema).is_ok());
let invalid = json!({"mode": "invalid"});
assert!(validate_config(&invalid, &schema).is_err());
}
#[test]
fn test_validation_result() {
let result = ValidationResult::success();
assert!(result.is_valid());
assert!(result.format_errors().contains("✅"));
let errors = vec![ValidationError {
path: "/mode/approach".to_string(),
message: "Invalid value".to_string(),
suggestion: Some("Use 'semantic', 'algorithmic', or 'hybrid'".to_string()),
}];
let result = ValidationResult::failure(errors);
assert!(!result.is_valid());
assert!(result.format_errors().contains("❌"));
assert!(result.format_errors().contains("💡"));
}
}