1pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18#[serde(rename_all = "lowercase")]
19#[non_exhaustive]
20pub enum IssueSeverity {
21 #[serde(rename = "critical")]
23 Critical,
24 #[serde(rename = "warning")]
26 Warning,
27 #[serde(rename = "info")]
29 Info,
30}
31
32impl IssueSeverity {
33 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct FederationIssue {
47 pub severity: IssueSeverity,
49 pub message: String,
51 pub suggestion: String,
53 pub entity: Option<String>,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct CostWarning {
60 pub severity: IssueSeverity,
62 pub message: String,
64 pub suggestion: String,
66 pub worst_case_complexity: Option<u32>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct CacheIssue {
73 pub severity: IssueSeverity,
75 pub message: String,
77 pub suggestion: String,
79 pub affected: Option<String>,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct AuthIssue {
86 pub severity: IssueSeverity,
88 pub message: String,
90 pub suggestion: String,
92 pub affected_field: Option<String>,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct SchemaIssue {
99 pub severity: IssueSeverity,
101 pub message: String,
103 pub suggestion: String,
105 pub affected_type: Option<String>,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct DesignAudit {
112 pub federation_issues: Vec<FederationIssue>,
114 pub cost_warnings: Vec<CostWarning>,
116 pub cache_issues: Vec<CacheIssue>,
118 pub auth_issues: Vec<AuthIssue>,
120 pub schema_issues: Vec<SchemaIssue>,
122}
123
124impl DesignAudit {
125 #[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 pub fn from_schema_json(json: &str) -> Result<Self, serde_json::Error> {
143 let schema: serde_json::Value = serde_json::from_str(json)?;
145
146 let mut audit = Self::new();
147
148 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 #[must_use]
161 pub fn score(&self) -> u8 {
162 let mut score: f64 = 100.0;
164
165 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 let score = score.clamp(0.0, 100.0);
210 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
211 {
213 score as u8
214 }
215 }
216
217 #[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 #[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;