use std::{fs, path::Path};
use anyhow::Result;
use fraiseql_core::design::DesignAudit;
use serde::Serialize;
use crate::output::CommandResult;
#[derive(Debug, Clone)]
pub struct LintOptions {
pub fail_on_critical: bool,
pub fail_on_warning: bool,
}
#[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 Ok(CommandResult::error(
"lint",
&format!("Schema file not found: {schema_path}"),
"FILE_NOT_FOUND",
));
}
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)?;
if opts.fail_on_critical
&& audit.severity_count(fraiseql_core::design::IssueSeverity::Critical) > 0
{
return Ok(CommandResult::error(
"lint",
"Design audit failed: critical issues found",
"DESIGN_AUDIT_FAILED",
));
}
if opts.fail_on_warning
&& audit.severity_count(fraiseql_core::design::IssueSeverity::Warning) > 0
{
return Ok(CommandResult::error(
"lint",
"Design audit failed: warning issues found",
"DESIGN_AUDIT_FAILED",
));
}
let fed_score = if audit.federation_issues.is_empty() {
100
} else {
let count = u32::try_from(audit.federation_issues.len()).unwrap_or(u32::MAX);
(100u32 - (count * 10)).clamp(0, 100) as u8
};
let cost_score = if audit.cost_warnings.is_empty() {
100
} else {
let count = u32::try_from(audit.cost_warnings.len()).unwrap_or(u32::MAX);
(100u32 - (count * 8)).clamp(0, 100) as u8
};
let cache_score = if audit.cache_issues.is_empty() {
100
} else {
let count = u32::try_from(audit.cache_issues.len()).unwrap_or(u32::MAX);
(100u32 - (count * 6)).clamp(0, 100) as u8
};
let auth_score = if audit.auth_issues.is_empty() {
100
} else {
let count = u32::try_from(audit.auth_issues.len()).unwrap_or(u32::MAX);
(100u32 - (count * 12)).clamp(0, 100) as u8
};
let comp_score = if audit.schema_issues.is_empty() {
100
} else {
let count = u32::try_from(audit.schema_issues.len()).unwrap_or(u32::MAX);
(100u32 - (count * 10)).clamp(0, 100) as u8
};
let severity_counts = SeverityCounts {
critical: audit.severity_count(fraiseql_core::design::IssueSeverity::Critical),
warning: audit.severity_count(fraiseql_core::design::IssueSeverity::Warning),
info: audit.severity_count(fraiseql_core::design::IssueSeverity::Info),
};
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)?))
}
#[cfg(test)]
mod tests {
use std::io::Write;
use tempfile::NamedTempFile;
use super::*;
fn default_opts() -> LintOptions {
LintOptions {
fail_on_critical: false,
fail_on_warning: false,
}
}
#[test]
fn test_lint_valid_schema() {
let schema_json = r#"{
"types": [
{
"name": "Query",
"fields": [
{"name": "users", "type": "[User!]"}
]
},
{
"name": "User",
"fields": [
{"name": "id", "type": "ID", "isPrimaryKey": true},
{"name": "name", "type": "String"}
]
}
]
}"#;
let mut file = NamedTempFile::new().unwrap();
file.write_all(schema_json.as_bytes()).unwrap();
let path = file.path().to_str().unwrap();
let result = run(path, default_opts());
assert!(result.is_ok());
let cmd_result = result.unwrap();
assert_eq!(cmd_result.status, "success");
assert_eq!(cmd_result.command, "lint");
assert!(cmd_result.data.is_some());
}
#[test]
fn test_lint_file_not_found() {
let result = run("nonexistent_schema.json", default_opts());
assert!(result.is_ok());
let cmd_result = result.unwrap();
assert_eq!(cmd_result.status, "error");
assert_eq!(cmd_result.code, Some("FILE_NOT_FOUND".to_string()));
}
#[test]
fn test_lint_returns_score() {
let schema_json = r#"{"types": []}"#;
let mut file = NamedTempFile::new().unwrap();
file.write_all(schema_json.as_bytes()).unwrap();
let path = file.path().to_str().unwrap();
let result = run(path, default_opts());
assert!(result.is_ok());
let cmd_result = result.unwrap();
if let Some(data) = &cmd_result.data {
assert!(data.get("overall_score").is_some());
assert!(data.get("severity_counts").is_some());
assert!(data.get("categories").is_some());
}
}
}