use std::error::Error;
use std::fs;
use std::path::PathBuf;
use std::process::ExitCode;
use std::time::{Duration, Instant};
use clap::Args;
use sara_core::graph::{KnowledgeGraph, KnowledgeGraphBuilder};
use sara_core::model::{Item, ItemType};
use sara_core::validation::{ValidationReport, pre_validate, validate};
use serde::Serialize;
use sara_core::config::{Config, OutputConfig};
use crate::output::{format_error, format_success, format_warning, print_warning};
#[derive(Debug, Clone, Copy, Default, clap::ValueEnum)]
pub enum CheckFormat {
#[default]
Text,
Json,
}
#[derive(Args, Debug)]
pub struct CheckArgs {
#[arg(long, value_name = "GIT_REF", help_heading = "Input")]
pub at: Option<String>,
#[arg(long, default_value = "text", help_heading = "Output")]
pub format: CheckFormat,
#[arg(short, long, help_heading = "Output")]
pub output: Option<PathBuf>,
#[arg(long, help_heading = "Validation")]
pub strict: bool,
}
#[derive(Debug, Serialize)]
struct CheckResult {
valid: bool,
items_checked: usize,
relationships_checked: usize,
items_by_type: std::collections::HashMap<ItemType, usize>,
parse_time_ms: u128,
#[serde(skip_serializing_if = "Vec::is_empty")]
errors: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
warnings: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
items: Option<Vec<Item>>,
}
pub fn run(args: &CheckArgs, config: &Config) -> Result<ExitCode, Box<dyn Error>> {
let start = Instant::now();
let output_config = &config.output;
let items = match args.at.as_deref() {
Some(git_ref) => super::parse_items_at(config, git_ref)?,
None => super::parse_items(config)?,
};
if items.is_empty() {
print_warning(output_config, "No items found in repositories");
return Ok(ExitCode::SUCCESS);
}
let strict = args.strict || config.validation.strict_mode;
let pre_report = pre_validate(&items, strict);
if !pre_report.is_valid() {
let parse_time = start.elapsed();
return handle_output(args, None, &pre_report, &parse_time, output_config);
}
let graph = KnowledgeGraphBuilder::new().add_items(items).build()?;
let report = validate(&graph, strict);
let report = consolidate_reports(report, pre_report);
let parse_time = start.elapsed();
handle_output(args, Some(&graph), &report, &parse_time, output_config)
}
fn consolidate_reports(
mut report: ValidationReport,
pre_report: ValidationReport,
) -> ValidationReport {
report.merge(pre_report);
report
}
fn handle_output(
args: &CheckArgs,
graph: Option<&KnowledgeGraph>,
report: &ValidationReport,
parse_time: &Duration,
output_config: &OutputConfig,
) -> Result<ExitCode, Box<dyn Error>> {
let output = match args.format {
CheckFormat::Text => build_text_output(report, parse_time, output_config),
CheckFormat::Json => {
let result = build_check_result(graph, report, parse_time);
serde_json::to_string_pretty(&result)?
}
};
write_output(&output, args.output.as_ref())?;
if report.is_valid() {
Ok(ExitCode::SUCCESS)
} else {
Ok(ExitCode::from(1))
}
}
fn build_check_result(
graph: Option<&KnowledgeGraph>,
report: &ValidationReport,
parse_time: &Duration,
) -> CheckResult {
let errors: Vec<String> = report.errors().iter().map(|e| e.to_string()).collect();
let warnings: Vec<String> = report.warnings().iter().map(|w| w.to_string()).collect();
let items = graph
.filter(|_| report.is_valid())
.map(|g| g.items().cloned().collect());
CheckResult {
valid: report.is_valid(),
items_checked: report.items_checked,
relationships_checked: report.relationships_checked,
items_by_type: report.items_by_type.clone(),
parse_time_ms: parse_time.as_millis(),
errors,
warnings,
items,
}
}
fn write_output(content: &str, output_path: Option<&PathBuf>) -> Result<(), Box<dyn Error>> {
match output_path {
Some(path) => {
fs::write(path, content)?;
}
None => {
println!("{content}");
}
}
Ok(())
}
fn build_text_output(
report: &ValidationReport,
parse_time: &Duration,
config: &OutputConfig,
) -> String {
let mut output = String::new();
let types_section = if report.items_by_type.is_empty() {
String::new()
} else {
let type_lines: Vec<_> = ItemType::all()
.iter()
.filter_map(|item_type| {
report
.items_by_type
.get(item_type)
.map(|count| format!(" {:35} {}", item_type.display_name(), count))
})
.collect();
format!("\nItems by type:\n{}\n", type_lines.join("\n"))
};
output.push_str(&format!(
"\n\
Check Results\n\
=============\n\n\
Items: {}\n\
Relationships: {}\n\
Parse time: {}ms\
{}",
report.items_checked,
report.relationships_checked,
parse_time.as_millis(),
types_section
));
if report.error_count() > 0 {
output.push('\n');
for error in report.errors() {
output.push_str(&format_error(config, &error.to_string()));
output.push('\n');
}
}
if report.warning_count() > 0 {
output.push('\n');
for warning in report.warnings() {
output.push_str(&format_warning(config, &warning.to_string()));
output.push('\n');
}
}
output.push('\n');
if report.is_valid() {
if report.warning_count() > 0 {
output.push_str(&format_success(
config,
&format!("Check passed with {} warning(s)", report.warning_count()),
));
} else {
output.push_str(&format_success(config, "Check passed"));
}
} else {
output.push_str(&format_error(
config,
&format!(
"Check failed with {} error(s) and {} warning(s)",
report.error_count(),
report.warning_count()
),
));
}
output
}