fraiseql_server/routes/api/
design.rs1use axum::{Json, extract::State};
6use fraiseql_core::{
7 db::traits::DatabaseAdapter,
8 design::{DesignAudit, IssueSeverity},
9};
10use serde::{Deserialize, Serialize};
11
12use crate::routes::{
13 api::types::{ApiError, ApiResponse},
14 graphql::AppState,
15};
16
17#[derive(Debug, Clone, Deserialize)]
19pub struct DesignAuditRequest {
20 pub schema: serde_json::Value,
22}
23
24#[derive(Debug, Clone, Serialize)]
26pub struct DesignIssueResponse {
27 pub severity: String,
29 pub message: String,
31 pub suggestion: String,
33 pub affected: Option<String>,
35}
36
37#[derive(Debug, Clone, Serialize)]
39pub struct CategoryAuditResponse {
40 pub score: u8,
42 pub issues: Vec<DesignIssueResponse>,
44}
45
46#[derive(Debug, Clone, Serialize)]
48pub struct SeverityCountResponse {
49 pub critical: usize,
51 pub warning: usize,
53 pub info: usize,
55}
56
57#[derive(Debug, Clone, Serialize)]
59pub struct DesignAuditResponse {
60 pub overall_score: u8,
62 pub severity_counts: SeverityCountResponse,
64 pub federation: CategoryAuditResponse,
66 pub cost: CategoryAuditResponse,
68 pub cache: CategoryAuditResponse,
70 pub authorization: CategoryAuditResponse,
72 pub compilation: CategoryAuditResponse,
74}
75
76pub async fn federation_audit_handler<A: DatabaseAdapter>(
82 State(_state): State<AppState<A>>,
83 Json(req): Json<DesignAuditRequest>,
84) -> std::result::Result<Json<ApiResponse<CategoryAuditResponse>>, ApiError> {
85 let audit = DesignAudit::from_schema_json(&req.schema.to_string())
86 .map_err(|e| ApiError::parse_error(format!("Invalid schema: {}", e)))?;
87
88 let issues: Vec<DesignIssueResponse> = audit
89 .federation_issues
90 .iter()
91 .map(|issue| DesignIssueResponse {
92 severity: format!("{:?}", issue.severity).to_lowercase(),
93 message: issue.message.clone(),
94 suggestion: issue.suggestion.clone(),
95 affected: issue.entity.clone(),
96 })
97 .collect();
98
99 let score = if issues.is_empty() {
100 100
101 } else {
102 let count = u32::try_from(issues.len()).unwrap_or(u32::MAX);
103 (100u32 - (count * 10)).clamp(0, 100) as u8
104 };
105
106 Ok(Json(ApiResponse {
107 status: "success".to_string(),
108 data: CategoryAuditResponse { score, issues },
109 }))
110}
111
112pub async fn cost_audit_handler<A: DatabaseAdapter>(
118 State(_state): State<AppState<A>>,
119 Json(req): Json<DesignAuditRequest>,
120) -> std::result::Result<Json<ApiResponse<CategoryAuditResponse>>, ApiError> {
121 let audit = DesignAudit::from_schema_json(&req.schema.to_string())
122 .map_err(|e| ApiError::parse_error(format!("Invalid schema: {}", e)))?;
123
124 let issues: Vec<DesignIssueResponse> = audit
125 .cost_warnings
126 .iter()
127 .map(|warning| DesignIssueResponse {
128 severity: format!("{:?}", warning.severity).to_lowercase(),
129 message: warning.message.clone(),
130 suggestion: warning.suggestion.clone(),
131 affected: warning.worst_case_complexity.map(|c| format!("complexity: {}", c)),
132 })
133 .collect();
134
135 let score = if issues.is_empty() {
136 100
137 } else {
138 let count = u32::try_from(issues.len()).unwrap_or(u32::MAX);
139 (100u32 - (count * 8)).clamp(0, 100) as u8
140 };
141
142 Ok(Json(ApiResponse {
143 status: "success".to_string(),
144 data: CategoryAuditResponse { score, issues },
145 }))
146}
147
148pub async fn cache_audit_handler<A: DatabaseAdapter>(
154 State(_state): State<AppState<A>>,
155 Json(req): Json<DesignAuditRequest>,
156) -> std::result::Result<Json<ApiResponse<CategoryAuditResponse>>, ApiError> {
157 let audit = DesignAudit::from_schema_json(&req.schema.to_string())
158 .map_err(|e| ApiError::parse_error(format!("Invalid schema: {}", e)))?;
159
160 let issues: Vec<DesignIssueResponse> = audit
161 .cache_issues
162 .iter()
163 .map(|issue| DesignIssueResponse {
164 severity: format!("{:?}", issue.severity).to_lowercase(),
165 message: issue.message.clone(),
166 suggestion: issue.suggestion.clone(),
167 affected: issue.affected.clone(),
168 })
169 .collect();
170
171 let score = if issues.is_empty() {
172 100
173 } else {
174 let count = u32::try_from(issues.len()).unwrap_or(u32::MAX);
175 (100u32 - (count * 6)).clamp(0, 100) as u8
176 };
177
178 Ok(Json(ApiResponse {
179 status: "success".to_string(),
180 data: CategoryAuditResponse { score, issues },
181 }))
182}
183
184pub async fn auth_audit_handler<A: DatabaseAdapter>(
190 State(_state): State<AppState<A>>,
191 Json(req): Json<DesignAuditRequest>,
192) -> std::result::Result<Json<ApiResponse<CategoryAuditResponse>>, ApiError> {
193 let audit = DesignAudit::from_schema_json(&req.schema.to_string())
194 .map_err(|e| ApiError::parse_error(format!("Invalid schema: {}", e)))?;
195
196 let issues: Vec<DesignIssueResponse> = audit
197 .auth_issues
198 .iter()
199 .map(|issue| DesignIssueResponse {
200 severity: format!("{:?}", issue.severity).to_lowercase(),
201 message: issue.message.clone(),
202 suggestion: issue.suggestion.clone(),
203 affected: issue.affected_field.clone(),
204 })
205 .collect();
206
207 let score = if issues.is_empty() {
208 100
209 } else {
210 let count = u32::try_from(issues.len()).unwrap_or(u32::MAX);
211 (100u32 - (count * 12)).clamp(0, 100) as u8
212 };
213
214 Ok(Json(ApiResponse {
215 status: "success".to_string(),
216 data: CategoryAuditResponse { score, issues },
217 }))
218}
219
220pub async fn compilation_audit_handler<A: DatabaseAdapter>(
226 State(_state): State<AppState<A>>,
227 Json(req): Json<DesignAuditRequest>,
228) -> std::result::Result<Json<ApiResponse<CategoryAuditResponse>>, ApiError> {
229 let audit = DesignAudit::from_schema_json(&req.schema.to_string())
230 .map_err(|e| ApiError::parse_error(format!("Invalid schema: {}", e)))?;
231
232 let issues: Vec<DesignIssueResponse> = audit
233 .schema_issues
234 .iter()
235 .map(|issue| DesignIssueResponse {
236 severity: format!("{:?}", issue.severity).to_lowercase(),
237 message: issue.message.clone(),
238 suggestion: issue.suggestion.clone(),
239 affected: issue.affected_type.clone(),
240 })
241 .collect();
242
243 let score = if issues.is_empty() {
244 100
245 } else {
246 let count = u32::try_from(issues.len()).unwrap_or(u32::MAX);
247 (100u32 - (count * 10)).clamp(0, 100) as u8
248 };
249
250 Ok(Json(ApiResponse {
251 status: "success".to_string(),
252 data: CategoryAuditResponse { score, issues },
253 }))
254}
255
256pub async fn overall_design_audit_handler<A: DatabaseAdapter>(
262 State(_state): State<AppState<A>>,
263 Json(req): Json<DesignAuditRequest>,
264) -> std::result::Result<Json<ApiResponse<DesignAuditResponse>>, ApiError> {
265 let audit = DesignAudit::from_schema_json(&req.schema.to_string())
266 .map_err(|e| ApiError::parse_error(format!("Invalid schema: {}", e)))?;
267
268 let federation_issues: Vec<DesignIssueResponse> = audit
270 .federation_issues
271 .iter()
272 .map(|issue| DesignIssueResponse {
273 severity: format!("{:?}", issue.severity).to_lowercase(),
274 message: issue.message.clone(),
275 suggestion: issue.suggestion.clone(),
276 affected: issue.entity.clone(),
277 })
278 .collect();
279
280 let cost_issues: Vec<DesignIssueResponse> = audit
282 .cost_warnings
283 .iter()
284 .map(|warning| DesignIssueResponse {
285 severity: format!("{:?}", warning.severity).to_lowercase(),
286 message: warning.message.clone(),
287 suggestion: warning.suggestion.clone(),
288 affected: warning.worst_case_complexity.map(|c| format!("complexity: {}", c)),
289 })
290 .collect();
291
292 let cache_issues: Vec<DesignIssueResponse> = audit
294 .cache_issues
295 .iter()
296 .map(|issue| DesignIssueResponse {
297 severity: format!("{:?}", issue.severity).to_lowercase(),
298 message: issue.message.clone(),
299 suggestion: issue.suggestion.clone(),
300 affected: issue.affected.clone(),
301 })
302 .collect();
303
304 let auth_issues: Vec<DesignIssueResponse> = audit
306 .auth_issues
307 .iter()
308 .map(|issue| DesignIssueResponse {
309 severity: format!("{:?}", issue.severity).to_lowercase(),
310 message: issue.message.clone(),
311 suggestion: issue.suggestion.clone(),
312 affected: issue.affected_field.clone(),
313 })
314 .collect();
315
316 let compilation_issues: Vec<DesignIssueResponse> = audit
318 .schema_issues
319 .iter()
320 .map(|issue| DesignIssueResponse {
321 severity: format!("{:?}", issue.severity).to_lowercase(),
322 message: issue.message.clone(),
323 suggestion: issue.suggestion.clone(),
324 affected: issue.affected_type.clone(),
325 })
326 .collect();
327
328 let severity_counts = SeverityCountResponse {
329 critical: audit.severity_count(IssueSeverity::Critical),
330 warning: audit.severity_count(IssueSeverity::Warning),
331 info: audit.severity_count(IssueSeverity::Info),
332 };
333
334 let fed_score = if federation_issues.is_empty() {
335 100
336 } else {
337 let count = u32::try_from(federation_issues.len()).unwrap_or(u32::MAX);
338 (100u32 - (count * 10)).clamp(0, 100) as u8
339 };
340
341 let cost_score = if cost_issues.is_empty() {
342 100
343 } else {
344 let count = u32::try_from(cost_issues.len()).unwrap_or(u32::MAX);
345 (100u32 - (count * 8)).clamp(0, 100) as u8
346 };
347
348 let cache_score = if cache_issues.is_empty() {
349 100
350 } else {
351 let count = u32::try_from(cache_issues.len()).unwrap_or(u32::MAX);
352 (100u32 - (count * 6)).clamp(0, 100) as u8
353 };
354
355 let auth_score = if auth_issues.is_empty() {
356 100
357 } else {
358 let count = u32::try_from(auth_issues.len()).unwrap_or(u32::MAX);
359 (100u32 - (count * 12)).clamp(0, 100) as u8
360 };
361
362 let comp_score = if compilation_issues.is_empty() {
363 100
364 } else {
365 let count = u32::try_from(compilation_issues.len()).unwrap_or(u32::MAX);
366 (100u32 - (count * 10)).clamp(0, 100) as u8
367 };
368
369 let response = DesignAuditResponse {
370 overall_score: audit.score(),
371 severity_counts,
372 federation: CategoryAuditResponse {
373 score: fed_score,
374 issues: federation_issues,
375 },
376 cost: CategoryAuditResponse {
377 score: cost_score,
378 issues: cost_issues,
379 },
380 cache: CategoryAuditResponse {
381 score: cache_score,
382 issues: cache_issues,
383 },
384 authorization: CategoryAuditResponse {
385 score: auth_score,
386 issues: auth_issues,
387 },
388 compilation: CategoryAuditResponse {
389 score: comp_score,
390 issues: compilation_issues,
391 },
392 };
393
394 Ok(Json(ApiResponse {
395 status: "success".to_string(),
396 data: response,
397 }))
398}
399
400#[cfg(test)]
401mod tests {
402 #![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::*;
413
414 #[test]
415 fn test_severity_count_response() {
416 let resp = SeverityCountResponse {
417 critical: 1,
418 warning: 3,
419 info: 5,
420 };
421 let json = serde_json::to_string(&resp).unwrap();
422 assert!(json.contains("\"critical\":1"));
423 }
424}