use std::{fs, path::Path};
use anyhow::Result;
use fraiseql_core::design::DesignAudit;
use serde::Serialize;
use crate::output::CommandResult;
#[derive(Debug, Clone, Default)]
pub struct LintCategoryFilter {
pub federation: bool,
pub cost: bool,
pub cache: bool,
pub auth: bool,
pub compilation: bool,
}
impl LintCategoryFilter {
pub fn is_all(&self) -> bool {
!self.federation && !self.cost && !self.cache && !self.auth && !self.compilation
}
}
#[derive(Debug, Clone)]
pub struct LintOptions {
pub fail_on_critical: bool,
pub fail_on_warning: bool,
pub filter: LintCategoryFilter,
}
#[derive(Debug, Serialize)]
pub struct LintResponse {
pub overall_score: u8,
pub severity_counts: SeverityCounts,
pub categories: CategoryScores,
}
#[derive(Debug, Serialize)]
pub struct SeverityCounts {
pub critical: usize,
pub warning: usize,
pub info: usize,
}
#[derive(Debug, Serialize)]
pub struct CategoryScores {
pub federation: u8,
pub cost: u8,
pub cache: u8,
pub authorization: u8,
pub compilation: u8,
}
pub fn run(schema_path: &str, opts: LintOptions) -> Result<CommandResult> {
if !Path::new(schema_path).exists() {
return Err(anyhow::anyhow!("Schema file not found: {schema_path}"));
}
let schema_json = fs::read_to_string(schema_path)?;
let _schema: serde_json::Value = serde_json::from_str(&schema_json)?;
let audit = DesignAudit::from_schema_json(&schema_json)?;
let f = &opts.filter;
let show_all = f.is_all();
let fed_issues = if show_all || f.federation {
audit.federation_issues.len()
} else {
0
};
let cost_issues = if show_all || f.cost {
audit.cost_warnings.len()
} else {
0
};
let cache_issues = if show_all || f.cache {
audit.cache_issues.len()
} else {
0
};
let auth_issues = if show_all || f.auth {
audit.auth_issues.len()
} else {
0
};
let comp_issues = if show_all || f.compilation {
audit.schema_issues.len()
} else {
0
};
let visible_critical = if show_all {
audit.severity_count(fraiseql_core::design::IssueSeverity::Critical)
} else {
use fraiseql_core::design::IssueSeverity;
let mut n = 0;
if f.federation {
n += audit
.federation_issues
.iter()
.filter(|i| i.severity == IssueSeverity::Critical)
.count();
}
if f.cost {
n += audit
.cost_warnings
.iter()
.filter(|i| i.severity == IssueSeverity::Critical)
.count();
}
if f.cache {
n += audit
.cache_issues
.iter()
.filter(|i| i.severity == IssueSeverity::Critical)
.count();
}
if f.auth {
n += audit
.auth_issues
.iter()
.filter(|i| i.severity == IssueSeverity::Critical)
.count();
}
if f.compilation {
n += audit
.schema_issues
.iter()
.filter(|i| i.severity == IssueSeverity::Critical)
.count();
}
n
};
if opts.fail_on_critical && visible_critical > 0 {
return Ok(CommandResult::error(
"lint",
"Design audit failed: critical issues found",
"DESIGN_AUDIT_FAILED",
));
}
let visible_warning = if show_all {
audit.severity_count(fraiseql_core::design::IssueSeverity::Warning)
} else {
use fraiseql_core::design::IssueSeverity;
let mut n = 0;
if f.federation {
n += audit
.federation_issues
.iter()
.filter(|i| i.severity == IssueSeverity::Warning)
.count();
}
if f.cost {
n += audit
.cost_warnings
.iter()
.filter(|i| i.severity == IssueSeverity::Warning)
.count();
}
if f.cache {
n += audit
.cache_issues
.iter()
.filter(|i| i.severity == IssueSeverity::Warning)
.count();
}
if f.auth {
n += audit
.auth_issues
.iter()
.filter(|i| i.severity == IssueSeverity::Warning)
.count();
}
if f.compilation {
n += audit
.schema_issues
.iter()
.filter(|i| i.severity == IssueSeverity::Warning)
.count();
}
n
};
if opts.fail_on_warning && visible_warning > 0 {
return Ok(CommandResult::error(
"lint",
"Design audit failed: warning issues found",
"DESIGN_AUDIT_FAILED",
));
}
let score_from_count = |count: usize, penalty: u32| -> u8 {
let n = u32::try_from(count).unwrap_or(u32::MAX);
#[allow(clippy::cast_possible_truncation)] let score = 100u32.saturating_sub(n * penalty) as u8;
score
};
let fed_score = if fed_issues == 0 {
100
} else {
score_from_count(fed_issues, 10)
};
let cost_score = if cost_issues == 0 {
100
} else {
score_from_count(cost_issues, 8)
};
let cache_score = if cache_issues == 0 {
100
} else {
score_from_count(cache_issues, 6)
};
let auth_score = if auth_issues == 0 {
100
} else {
score_from_count(auth_issues, 12)
};
let comp_score = if comp_issues == 0 {
100
} else {
score_from_count(comp_issues, 10)
};
let severity_counts = SeverityCounts {
critical: visible_critical,
warning: visible_warning,
info: if show_all {
audit.severity_count(fraiseql_core::design::IssueSeverity::Info)
} else {
0 },
};
let response = LintResponse {
overall_score: audit.score(),
severity_counts,
categories: CategoryScores {
federation: fed_score,
cost: cost_score,
cache: cache_score,
authorization: auth_score,
compilation: comp_score,
},
};
Ok(CommandResult::success("lint", serde_json::to_value(&response)?))
}