use axum::{Json, extract::State};
use fraiseql_core::{
db::traits::DatabaseAdapter,
design::{DesignAudit, IssueSeverity},
};
use serde::{Deserialize, Serialize};
use crate::routes::{
api::types::{ApiError, ApiResponse},
graphql::AppState,
};
#[derive(Debug, Clone, Deserialize)]
pub struct DesignAuditRequest {
pub schema: serde_json::Value,
}
#[derive(Debug, Clone, Serialize)]
pub struct DesignIssueResponse {
pub severity: String,
pub message: String,
pub suggestion: String,
pub affected: Option<String>,
}
#[derive(Debug, Clone, Serialize)]
pub struct CategoryAuditResponse {
pub score: u8,
pub issues: Vec<DesignIssueResponse>,
}
#[derive(Debug, Clone, Serialize)]
pub struct SeverityCountResponse {
pub critical: usize,
pub warning: usize,
pub info: usize,
}
#[derive(Debug, Clone, Serialize)]
pub struct DesignAuditResponse {
pub overall_score: u8,
pub severity_counts: SeverityCountResponse,
pub federation: CategoryAuditResponse,
pub cost: CategoryAuditResponse,
pub cache: CategoryAuditResponse,
pub authorization: CategoryAuditResponse,
pub compilation: CategoryAuditResponse,
}
pub async fn federation_audit_handler<A: DatabaseAdapter>(
State(_state): State<AppState<A>>,
Json(req): Json<DesignAuditRequest>,
) -> std::result::Result<Json<ApiResponse<CategoryAuditResponse>>, ApiError> {
let audit = DesignAudit::from_schema_json(&req.schema.to_string())
.map_err(|e| ApiError::parse_error(format!("Invalid schema: {}", e)))?;
let issues: Vec<DesignIssueResponse> = audit
.federation_issues
.iter()
.map(|issue| DesignIssueResponse {
severity: format!("{:?}", issue.severity).to_lowercase(),
message: issue.message.clone(),
suggestion: issue.suggestion.clone(),
affected: issue.entity.clone(),
})
.collect();
let score = if issues.is_empty() {
100
} else {
let count = u32::try_from(issues.len()).unwrap_or(u32::MAX);
(100u32 - (count * 10)).clamp(0, 100) as u8
};
Ok(Json(ApiResponse {
status: "success".to_string(),
data: CategoryAuditResponse { score, issues },
}))
}
pub async fn cost_audit_handler<A: DatabaseAdapter>(
State(_state): State<AppState<A>>,
Json(req): Json<DesignAuditRequest>,
) -> std::result::Result<Json<ApiResponse<CategoryAuditResponse>>, ApiError> {
let audit = DesignAudit::from_schema_json(&req.schema.to_string())
.map_err(|e| ApiError::parse_error(format!("Invalid schema: {}", e)))?;
let issues: Vec<DesignIssueResponse> = audit
.cost_warnings
.iter()
.map(|warning| DesignIssueResponse {
severity: format!("{:?}", warning.severity).to_lowercase(),
message: warning.message.clone(),
suggestion: warning.suggestion.clone(),
affected: warning.worst_case_complexity.map(|c| format!("complexity: {}", c)),
})
.collect();
let score = if issues.is_empty() {
100
} else {
let count = u32::try_from(issues.len()).unwrap_or(u32::MAX);
(100u32 - (count * 8)).clamp(0, 100) as u8
};
Ok(Json(ApiResponse {
status: "success".to_string(),
data: CategoryAuditResponse { score, issues },
}))
}
pub async fn cache_audit_handler<A: DatabaseAdapter>(
State(_state): State<AppState<A>>,
Json(req): Json<DesignAuditRequest>,
) -> std::result::Result<Json<ApiResponse<CategoryAuditResponse>>, ApiError> {
let audit = DesignAudit::from_schema_json(&req.schema.to_string())
.map_err(|e| ApiError::parse_error(format!("Invalid schema: {}", e)))?;
let issues: Vec<DesignIssueResponse> = audit
.cache_issues
.iter()
.map(|issue| DesignIssueResponse {
severity: format!("{:?}", issue.severity).to_lowercase(),
message: issue.message.clone(),
suggestion: issue.suggestion.clone(),
affected: issue.affected.clone(),
})
.collect();
let score = if issues.is_empty() {
100
} else {
let count = u32::try_from(issues.len()).unwrap_or(u32::MAX);
(100u32 - (count * 6)).clamp(0, 100) as u8
};
Ok(Json(ApiResponse {
status: "success".to_string(),
data: CategoryAuditResponse { score, issues },
}))
}
pub async fn auth_audit_handler<A: DatabaseAdapter>(
State(_state): State<AppState<A>>,
Json(req): Json<DesignAuditRequest>,
) -> std::result::Result<Json<ApiResponse<CategoryAuditResponse>>, ApiError> {
let audit = DesignAudit::from_schema_json(&req.schema.to_string())
.map_err(|e| ApiError::parse_error(format!("Invalid schema: {}", e)))?;
let issues: Vec<DesignIssueResponse> = audit
.auth_issues
.iter()
.map(|issue| DesignIssueResponse {
severity: format!("{:?}", issue.severity).to_lowercase(),
message: issue.message.clone(),
suggestion: issue.suggestion.clone(),
affected: issue.affected_field.clone(),
})
.collect();
let score = if issues.is_empty() {
100
} else {
let count = u32::try_from(issues.len()).unwrap_or(u32::MAX);
(100u32 - (count * 12)).clamp(0, 100) as u8
};
Ok(Json(ApiResponse {
status: "success".to_string(),
data: CategoryAuditResponse { score, issues },
}))
}
pub async fn compilation_audit_handler<A: DatabaseAdapter>(
State(_state): State<AppState<A>>,
Json(req): Json<DesignAuditRequest>,
) -> std::result::Result<Json<ApiResponse<CategoryAuditResponse>>, ApiError> {
let audit = DesignAudit::from_schema_json(&req.schema.to_string())
.map_err(|e| ApiError::parse_error(format!("Invalid schema: {}", e)))?;
let issues: Vec<DesignIssueResponse> = audit
.schema_issues
.iter()
.map(|issue| DesignIssueResponse {
severity: format!("{:?}", issue.severity).to_lowercase(),
message: issue.message.clone(),
suggestion: issue.suggestion.clone(),
affected: issue.affected_type.clone(),
})
.collect();
let score = if issues.is_empty() {
100
} else {
let count = u32::try_from(issues.len()).unwrap_or(u32::MAX);
(100u32 - (count * 10)).clamp(0, 100) as u8
};
Ok(Json(ApiResponse {
status: "success".to_string(),
data: CategoryAuditResponse { score, issues },
}))
}
pub async fn overall_design_audit_handler<A: DatabaseAdapter>(
State(_state): State<AppState<A>>,
Json(req): Json<DesignAuditRequest>,
) -> std::result::Result<Json<ApiResponse<DesignAuditResponse>>, ApiError> {
let audit = DesignAudit::from_schema_json(&req.schema.to_string())
.map_err(|e| ApiError::parse_error(format!("Invalid schema: {}", e)))?;
let federation_issues: Vec<DesignIssueResponse> = audit
.federation_issues
.iter()
.map(|issue| DesignIssueResponse {
severity: format!("{:?}", issue.severity).to_lowercase(),
message: issue.message.clone(),
suggestion: issue.suggestion.clone(),
affected: issue.entity.clone(),
})
.collect();
let cost_issues: Vec<DesignIssueResponse> = audit
.cost_warnings
.iter()
.map(|warning| DesignIssueResponse {
severity: format!("{:?}", warning.severity).to_lowercase(),
message: warning.message.clone(),
suggestion: warning.suggestion.clone(),
affected: warning.worst_case_complexity.map(|c| format!("complexity: {}", c)),
})
.collect();
let cache_issues: Vec<DesignIssueResponse> = audit
.cache_issues
.iter()
.map(|issue| DesignIssueResponse {
severity: format!("{:?}", issue.severity).to_lowercase(),
message: issue.message.clone(),
suggestion: issue.suggestion.clone(),
affected: issue.affected.clone(),
})
.collect();
let auth_issues: Vec<DesignIssueResponse> = audit
.auth_issues
.iter()
.map(|issue| DesignIssueResponse {
severity: format!("{:?}", issue.severity).to_lowercase(),
message: issue.message.clone(),
suggestion: issue.suggestion.clone(),
affected: issue.affected_field.clone(),
})
.collect();
let compilation_issues: Vec<DesignIssueResponse> = audit
.schema_issues
.iter()
.map(|issue| DesignIssueResponse {
severity: format!("{:?}", issue.severity).to_lowercase(),
message: issue.message.clone(),
suggestion: issue.suggestion.clone(),
affected: issue.affected_type.clone(),
})
.collect();
let severity_counts = SeverityCountResponse {
critical: audit.severity_count(IssueSeverity::Critical),
warning: audit.severity_count(IssueSeverity::Warning),
info: audit.severity_count(IssueSeverity::Info),
};
let fed_score = if federation_issues.is_empty() {
100
} else {
let count = u32::try_from(federation_issues.len()).unwrap_or(u32::MAX);
(100u32 - (count * 10)).clamp(0, 100) as u8
};
let cost_score = if cost_issues.is_empty() {
100
} else {
let count = u32::try_from(cost_issues.len()).unwrap_or(u32::MAX);
(100u32 - (count * 8)).clamp(0, 100) as u8
};
let cache_score = if cache_issues.is_empty() {
100
} else {
let count = u32::try_from(cache_issues.len()).unwrap_or(u32::MAX);
(100u32 - (count * 6)).clamp(0, 100) as u8
};
let auth_score = if auth_issues.is_empty() {
100
} else {
let count = u32::try_from(auth_issues.len()).unwrap_or(u32::MAX);
(100u32 - (count * 12)).clamp(0, 100) as u8
};
let comp_score = if compilation_issues.is_empty() {
100
} else {
let count = u32::try_from(compilation_issues.len()).unwrap_or(u32::MAX);
(100u32 - (count * 10)).clamp(0, 100) as u8
};
let response = DesignAuditResponse {
overall_score: audit.score(),
severity_counts,
federation: CategoryAuditResponse {
score: fed_score,
issues: federation_issues,
},
cost: CategoryAuditResponse {
score: cost_score,
issues: cost_issues,
},
cache: CategoryAuditResponse {
score: cache_score,
issues: cache_issues,
},
authorization: CategoryAuditResponse {
score: auth_score,
issues: auth_issues,
},
compilation: CategoryAuditResponse {
score: comp_score,
issues: compilation_issues,
},
};
Ok(Json(ApiResponse {
status: "success".to_string(),
data: response,
}))
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)] #![allow(clippy::cast_precision_loss)] #![allow(clippy::cast_sign_loss)] #![allow(clippy::cast_possible_truncation)] #![allow(clippy::cast_possible_wrap)] #![allow(clippy::missing_panics_doc)] #![allow(clippy::missing_errors_doc)] #![allow(missing_docs)] #![allow(clippy::items_after_statements)]
use super::*;
#[test]
fn test_severity_count_response() {
let resp = SeverityCountResponse {
critical: 1,
warning: 3,
info: 5,
};
let json = serde_json::to_string(&resp).unwrap();
assert!(json.contains("\"critical\":1"));
}
}