Skip to main content

fraiseql_core/design/
mod.rs

1//! Design Quality Analysis Engine
2//!
3//! Provides linting and quality enforcement for GraphQL schema architecture.
4//! Detects anti-patterns and provides actionable recommendations aligned with
5//! FraiseQL's compilation model.
6
7pub mod authorization;
8pub mod cache;
9pub mod compilation;
10pub mod cost;
11pub mod federation;
12pub mod schema_patterns;
13
14use serde::{Deserialize, Serialize};
15
16/// Severity level for design issues
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(rename_all = "lowercase")]
19#[non_exhaustive]
20pub enum IssueSeverity {
21    /// Critical issues that may cause performance problems or bugs
22    #[serde(rename = "critical")]
23    Critical,
24    /// Warning issues that should be addressed
25    #[serde(rename = "warning")]
26    Warning,
27    /// Informational suggestions for improvement
28    #[serde(rename = "info")]
29    Info,
30}
31
32impl IssueSeverity {
33    /// Get numeric weight for scoring (critical=3, warning=2, info=1)
34    #[must_use]
35    pub const fn weight(&self) -> u32 {
36        match self {
37            IssueSeverity::Critical => 3,
38            IssueSeverity::Warning => 2,
39            IssueSeverity::Info => 1,
40        }
41    }
42}
43
44/// Federation-related design issue
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct FederationIssue {
47    /// Severity level of the issue
48    pub severity:   IssueSeverity,
49    /// Clear message describing the issue
50    pub message:    String,
51    /// Actionable suggestion for fixing the issue
52    pub suggestion: String,
53    /// Affected entity or component (if applicable)
54    pub entity:     Option<String>,
55}
56
57/// Cost analysis warning
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct CostWarning {
60    /// Severity level of the warning
61    pub severity:              IssueSeverity,
62    /// Clear message describing the issue
63    pub message:               String,
64    /// Actionable suggestion for fixing the issue
65    pub suggestion:            String,
66    /// Worst-case complexity score if applicable
67    pub worst_case_complexity: Option<u32>,
68}
69
70/// Cache coherency issue
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct CacheIssue {
73    /// Severity level of the issue
74    pub severity:   IssueSeverity,
75    /// Clear message describing the issue
76    pub message:    String,
77    /// Actionable suggestion for fixing the issue
78    pub suggestion: String,
79    /// Affected entity or field (if applicable)
80    pub affected:   Option<String>,
81}
82
83/// Authorization boundary issue
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct AuthIssue {
86    /// Severity level of the issue
87    pub severity:       IssueSeverity,
88    /// Clear message describing the issue
89    pub message:        String,
90    /// Actionable suggestion for fixing the issue
91    pub suggestion:     String,
92    /// Affected field or scope (if applicable)
93    pub affected_field: Option<String>,
94}
95
96/// Schema design issue
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct SchemaIssue {
99    /// Severity level of the issue
100    pub severity:      IssueSeverity,
101    /// Clear message describing the issue
102    pub message:       String,
103    /// Actionable suggestion for fixing the issue
104    pub suggestion:    String,
105    /// Affected type or pattern (if applicable)
106    pub affected_type: Option<String>,
107}
108
109/// Complete design quality audit
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct DesignAudit {
112    /// Federation-related issues
113    pub federation_issues: Vec<FederationIssue>,
114    /// Cost analysis warnings
115    pub cost_warnings:     Vec<CostWarning>,
116    /// Cache coherency issues
117    pub cache_issues:      Vec<CacheIssue>,
118    /// Authorization boundary issues
119    pub auth_issues:       Vec<AuthIssue>,
120    /// Schema design issues
121    pub schema_issues:     Vec<SchemaIssue>,
122}
123
124impl DesignAudit {
125    /// Create a new empty audit
126    #[must_use]
127    pub const fn new() -> Self {
128        Self {
129            federation_issues: Vec::new(),
130            cost_warnings:     Vec::new(),
131            cache_issues:      Vec::new(),
132            auth_issues:       Vec::new(),
133            schema_issues:     Vec::new(),
134        }
135    }
136
137    /// Analyze a schema from JSON string.
138    ///
139    /// # Errors
140    ///
141    /// Returns [`serde_json::Error`] if `json` is not valid JSON.
142    pub fn from_schema_json(json: &str) -> Result<Self, serde_json::Error> {
143        // Parse the schema JSON
144        let schema: serde_json::Value = serde_json::from_str(json)?;
145
146        let mut audit = Self::new();
147
148        // Run all analysis engines
149        federation::analyze(&schema, &mut audit);
150        cost::analyze(&schema, &mut audit);
151        cache::analyze(&schema, &mut audit);
152        authorization::analyze(&schema, &mut audit);
153        schema_patterns::analyze(&schema, &mut audit);
154        compilation::analyze(&schema, &mut audit);
155
156        Ok(audit)
157    }
158
159    /// Calculate overall design quality score (0-100)
160    #[must_use]
161    pub fn score(&self) -> u8 {
162        // Base score
163        let mut score: f64 = 100.0;
164
165        // Deduct points for each issue based on severity
166        // Critical issues are heavily penalized
167        for issue in &self.federation_issues {
168            let penalty = match issue.severity {
169                IssueSeverity::Critical => 25.0,
170                IssueSeverity::Warning => 15.0,
171                IssueSeverity::Info => 3.0,
172            };
173            score -= penalty;
174        }
175        for warning in &self.cost_warnings {
176            let penalty = match warning.severity {
177                IssueSeverity::Critical => 20.0,
178                IssueSeverity::Warning => 8.0,
179                IssueSeverity::Info => 2.0,
180            };
181            score -= penalty;
182        }
183        for issue in &self.cache_issues {
184            let penalty = match issue.severity {
185                IssueSeverity::Critical => 15.0,
186                IssueSeverity::Warning => 6.0,
187                IssueSeverity::Info => 1.0,
188            };
189            score -= penalty;
190        }
191        for issue in &self.auth_issues {
192            let penalty = match issue.severity {
193                IssueSeverity::Critical => 25.0,
194                IssueSeverity::Warning => 12.0,
195                IssueSeverity::Info => 2.0,
196            };
197            score -= penalty;
198        }
199        for issue in &self.schema_issues {
200            let penalty = match issue.severity {
201                IssueSeverity::Critical => 15.0,
202                IssueSeverity::Warning => 5.0,
203                IssueSeverity::Info => 1.0,
204            };
205            score -= penalty;
206        }
207
208        // Clamp to 0-100
209        let score = score.clamp(0.0, 100.0);
210        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
211        // Reason: score is clamped to 0.0..=100.0, so truncation to u8 and sign loss are both safe
212        {
213            score as u8
214        }
215    }
216
217    /// Count issues by severity level
218    #[must_use]
219    pub fn severity_count(&self, severity: IssueSeverity) -> usize {
220        let fed_count = self.federation_issues.iter().filter(|i| i.severity == severity).count();
221        let cost_count = self.cost_warnings.iter().filter(|w| w.severity == severity).count();
222        let cache_count = self.cache_issues.iter().filter(|i| i.severity == severity).count();
223        let auth_count = self.auth_issues.iter().filter(|i| i.severity == severity).count();
224        let schema_count = self.schema_issues.iter().filter(|i| i.severity == severity).count();
225
226        fed_count + cost_count + cache_count + auth_count + schema_count
227    }
228
229    /// Get all issues as a flat list
230    #[must_use]
231    pub fn all_issues(&self) -> Vec<String> {
232        let mut issues = Vec::new();
233
234        for issue in &self.federation_issues {
235            issues.push(format!("{:?}: {}", issue.severity, issue.message));
236        }
237        for warning in &self.cost_warnings {
238            issues.push(format!("{:?}: {}", warning.severity, warning.message));
239        }
240        for issue in &self.cache_issues {
241            issues.push(format!("{:?}: {}", issue.severity, issue.message));
242        }
243        for issue in &self.auth_issues {
244            issues.push(format!("{:?}: {}", issue.severity, issue.message));
245        }
246        for issue in &self.schema_issues {
247            issues.push(format!("{:?}: {}", issue.severity, issue.message));
248        }
249
250        issues
251    }
252}
253
254impl Default for DesignAudit {
255    fn default() -> Self {
256        Self::new()
257    }
258}
259
260#[cfg(test)]
261mod tests;