pub mod authorization;
pub mod cache;
pub mod compilation;
pub mod cost;
pub mod federation;
pub mod schema_patterns;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
#[non_exhaustive]
pub enum IssueSeverity {
#[serde(rename = "critical")]
Critical,
#[serde(rename = "warning")]
Warning,
#[serde(rename = "info")]
Info,
}
impl IssueSeverity {
pub const fn weight(&self) -> u32 {
match self {
IssueSeverity::Critical => 3,
IssueSeverity::Warning => 2,
IssueSeverity::Info => 1,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FederationIssue {
pub severity: IssueSeverity,
pub message: String,
pub suggestion: String,
pub entity: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CostWarning {
pub severity: IssueSeverity,
pub message: String,
pub suggestion: String,
pub worst_case_complexity: Option<u32>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CacheIssue {
pub severity: IssueSeverity,
pub message: String,
pub suggestion: String,
pub affected: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthIssue {
pub severity: IssueSeverity,
pub message: String,
pub suggestion: String,
pub affected_field: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SchemaIssue {
pub severity: IssueSeverity,
pub message: String,
pub suggestion: String,
pub affected_type: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DesignAudit {
pub federation_issues: Vec<FederationIssue>,
pub cost_warnings: Vec<CostWarning>,
pub cache_issues: Vec<CacheIssue>,
pub auth_issues: Vec<AuthIssue>,
pub schema_issues: Vec<SchemaIssue>,
}
impl DesignAudit {
pub const fn new() -> Self {
Self {
federation_issues: Vec::new(),
cost_warnings: Vec::new(),
cache_issues: Vec::new(),
auth_issues: Vec::new(),
schema_issues: Vec::new(),
}
}
pub fn from_schema_json(json: &str) -> Result<Self, serde_json::Error> {
let schema: serde_json::Value = serde_json::from_str(json)?;
let mut audit = Self::new();
federation::analyze(&schema, &mut audit);
cost::analyze(&schema, &mut audit);
cache::analyze(&schema, &mut audit);
authorization::analyze(&schema, &mut audit);
schema_patterns::analyze(&schema, &mut audit);
compilation::analyze(&schema, &mut audit);
Ok(audit)
}
pub fn score(&self) -> u8 {
let mut score: f64 = 100.0;
for issue in &self.federation_issues {
let penalty = match issue.severity {
IssueSeverity::Critical => 25.0,
IssueSeverity::Warning => 15.0,
IssueSeverity::Info => 3.0,
};
score -= penalty;
}
for warning in &self.cost_warnings {
let penalty = match warning.severity {
IssueSeverity::Critical => 20.0,
IssueSeverity::Warning => 8.0,
IssueSeverity::Info => 2.0,
};
score -= penalty;
}
for issue in &self.cache_issues {
let penalty = match issue.severity {
IssueSeverity::Critical => 15.0,
IssueSeverity::Warning => 6.0,
IssueSeverity::Info => 1.0,
};
score -= penalty;
}
for issue in &self.auth_issues {
let penalty = match issue.severity {
IssueSeverity::Critical => 25.0,
IssueSeverity::Warning => 12.0,
IssueSeverity::Info => 2.0,
};
score -= penalty;
}
for issue in &self.schema_issues {
let penalty = match issue.severity {
IssueSeverity::Critical => 15.0,
IssueSeverity::Warning => 5.0,
IssueSeverity::Info => 1.0,
};
score -= penalty;
}
let score = score.clamp(0.0, 100.0);
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
{
score as u8
}
}
pub fn severity_count(&self, severity: IssueSeverity) -> usize {
let fed_count = self.federation_issues.iter().filter(|i| i.severity == severity).count();
let cost_count = self.cost_warnings.iter().filter(|w| w.severity == severity).count();
let cache_count = self.cache_issues.iter().filter(|i| i.severity == severity).count();
let auth_count = self.auth_issues.iter().filter(|i| i.severity == severity).count();
let schema_count = self.schema_issues.iter().filter(|i| i.severity == severity).count();
fed_count + cost_count + cache_count + auth_count + schema_count
}
pub fn all_issues(&self) -> Vec<String> {
let mut issues = Vec::new();
for issue in &self.federation_issues {
issues.push(format!("{:?}: {}", issue.severity, issue.message));
}
for warning in &self.cost_warnings {
issues.push(format!("{:?}: {}", warning.severity, warning.message));
}
for issue in &self.cache_issues {
issues.push(format!("{:?}: {}", issue.severity, issue.message));
}
for issue in &self.auth_issues {
issues.push(format!("{:?}: {}", issue.severity, issue.message));
}
for issue in &self.schema_issues {
issues.push(format!("{:?}: {}", issue.severity, issue.message));
}
issues
}
}
impl Default for DesignAudit {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_issue_severity_weight() {
assert_eq!(IssueSeverity::Critical.weight(), 3);
assert_eq!(IssueSeverity::Warning.weight(), 2);
assert_eq!(IssueSeverity::Info.weight(), 1);
}
#[test]
fn test_empty_audit_score() {
let audit = DesignAudit::new();
assert_eq!(audit.score(), 100);
}
#[test]
fn test_severity_count_empty() {
let audit = DesignAudit::new();
assert_eq!(audit.severity_count(IssueSeverity::Critical), 0);
assert_eq!(audit.severity_count(IssueSeverity::Warning), 0);
assert_eq!(audit.severity_count(IssueSeverity::Info), 0);
}
}