use std::fs;
use anyhow::Result;
use fraiseql_core::schema::{CompiledSchema, SchemaDependencyGraph};
use serde::Serialize;
use crate::output::CommandResult;
#[derive(Debug, Clone, Default)]
pub struct ValidateOptions {
pub check_cycles: bool,
pub check_unused: bool,
pub strict: bool,
pub filter_types: Vec<String>,
}
#[derive(Debug, Serialize)]
pub struct ValidationResult {
pub schema_path: String,
pub valid: bool,
pub type_count: usize,
pub query_count: usize,
pub mutation_count: usize,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub cycles: Vec<CycleError>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub unused_types: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub type_analysis: Option<Vec<TypeAnalysis>>,
}
#[derive(Debug, Serialize)]
pub struct CycleError {
pub types: Vec<String>,
pub path: String,
}
#[derive(Debug, Serialize)]
pub struct TypeAnalysis {
pub name: String,
pub dependencies: Vec<String>,
pub dependents: Vec<String>,
pub transitive_dependencies: Vec<String>,
}
pub fn run_with_options(input: &str, opts: ValidateOptions) -> Result<CommandResult> {
let schema_content = fs::read_to_string(input)?;
let schema: CompiledSchema = serde_json::from_str(&schema_content)?;
let graph = SchemaDependencyGraph::build(&schema);
let mut errors: Vec<String> = Vec::new();
let mut warnings: Vec<String> = Vec::new();
let mut cycles: Vec<CycleError> = Vec::new();
let mut unused_types: Vec<String> = Vec::new();
if opts.check_cycles {
let detected_cycles = graph.find_cycles();
for cycle in detected_cycles {
let cycle_error = CycleError {
types: cycle.nodes.clone(),
path: cycle.path_string(),
};
errors.push(format!("Circular dependency: {}", cycle.path_string()));
cycles.push(cycle_error);
}
}
if opts.check_unused {
let detected_unused = graph.find_unused();
for type_name in detected_unused {
if opts.strict {
errors.push(format!("Unused type: '{type_name}' has no incoming references"));
} else {
warnings.push(format!("Unused type: '{type_name}' has no incoming references"));
}
unused_types.push(type_name);
}
}
let type_analysis = if opts.filter_types.is_empty() {
None
} else {
let mut analyses = Vec::new();
for type_name in &opts.filter_types {
if graph.has_type(type_name) {
let deps = graph.dependencies_of(type_name);
let refs = graph.dependents_of(type_name);
let transitive = graph.transitive_dependencies(type_name);
analyses.push(TypeAnalysis {
name: type_name.clone(),
dependencies: deps,
dependents: refs,
transitive_dependencies: transitive.into_iter().collect(),
});
} else {
warnings.push(format!("Type '{type_name}' not found in schema"));
}
}
Some(analyses)
};
let result = ValidationResult {
schema_path: input.to_string(),
valid: errors.is_empty(),
type_count: schema.types.len(),
query_count: schema.queries.len(),
mutation_count: schema.mutations.len(),
cycles,
unused_types,
type_analysis,
};
let data = serde_json::to_value(&result)?;
if !errors.is_empty() {
Ok(CommandResult {
status: "validation-failed".to_string(),
command: "validate".to_string(),
data: Some(data),
message: Some(format!("{} validation error(s) found", errors.len())),
code: Some("VALIDATION_FAILED".to_string()),
errors,
warnings,
})
} else if !warnings.is_empty() {
Ok(CommandResult::success_with_warnings("validate", data, warnings))
} else {
Ok(CommandResult::success("validate", data))
}
}