Skip to main content

rustquty_core/
schema.rs

1//! JSON schemas for rustquty data structures.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6// ------------------------------------------------------------------------------------------------
7// MetricsSummary
8// ------------------------------------------------------------------------------------------------
9
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
11#[serde(rename_all = "camelCase")]
12pub struct MetricsSummary {
13    pub schema_version: String,
14    pub generated_at: String,
15    pub rustquty_version: String,
16    pub project: ProjectInfo,
17    pub collectors: CollectorsSummary,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
21#[serde(rename_all = "camelCase")]
22pub struct ProjectInfo {
23    pub name: String,
24    pub rust_edition: String,
25    pub workspace_root: String,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
29#[serde(rename_all = "camelCase")]
30pub struct CollectorsSummary {
31    pub fmt: FmtResult,
32    pub clippy: ClippyResult,
33    pub tests: TestResult,
34    pub coverage: CoverageResult,
35    pub deny: DenyResult,
36    pub audit: AuditResult,
37    pub hack: HackResult,
38    pub mutants: MutantsResult,
39    pub duplicates: DuplicatesResult,
40    pub loc: LocResult,
41    pub size: SizeResult,
42    pub complexity: ComplexityResult,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
46#[serde(rename_all = "camelCase")]
47pub struct FmtResult {
48    pub status: CollectorStatus,
49    #[serde(default)]
50    pub details: HashMap<String, serde_json::Value>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
54#[serde(rename_all = "camelCase")]
55pub struct ClippyResult {
56    pub status: CollectorStatus,
57    pub warning_count: u32,
58    #[serde(default)]
59    pub details: Vec<ClippyLint>,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
63#[serde(rename_all = "camelCase")]
64pub struct ClippyLint {
65    pub code: String,
66    pub message: String,
67    pub file: Option<String>,
68    pub line: Option<u32>,
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
72#[serde(rename_all = "camelCase")]
73pub struct TestResult {
74    pub status: CollectorStatus,
75    pub passed: u32,
76    pub failed: u32,
77    pub ignored: u32,
78    #[serde(default)]
79    pub runner: Option<String>,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
83#[serde(rename_all = "camelCase")]
84pub struct CoverageResult {
85    pub status: CollectorStatus,
86    pub line_percent: f64,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
90#[serde(rename_all = "camelCase")]
91pub struct DenyResult {
92    pub status: CollectorStatus,
93    pub banned_count: u32,
94    pub license_violations: u32,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
98#[serde(rename_all = "camelCase")]
99pub struct AuditResult {
100    pub status: CollectorStatus,
101    pub vulnerability_count: u32,
102    pub critical_count: u32,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
106#[serde(rename_all = "camelCase")]
107pub struct HackResult {
108    pub status: CollectorStatus,
109    pub feature_combinations_tested: u32,
110}
111
112#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
113#[serde(rename_all = "camelCase")]
114pub struct MutantsResult {
115    pub status: CollectorStatus,
116    pub mutation_score: f64,
117    pub caught: u32,
118    pub missed: u32,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
122#[serde(rename_all = "camelCase")]
123pub struct DuplicatesResult {
124    pub status: CollectorStatus,
125    pub total_lines: u32,
126    pub duplicate_lines: u32,
127    #[serde(default)]
128    pub files_with_duplicates: u32,
129    #[serde(default)]
130    pub duplicate_files: Vec<String>,
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
134#[serde(rename_all = "camelCase")]
135pub struct LocResult {
136    pub status: CollectorStatus,
137    pub total_lines: u32,
138    pub code_lines: u32,
139    pub comment_lines: u32,
140    pub blank_lines: u32,
141    pub long_lines: u32,
142    pub max_line_length_found: usize,
143    pub max_line_length_allowed: usize,
144    pub files: u32,
145    #[serde(default)]
146    pub files_with_long_lines: u32,
147    #[serde(default)]
148    pub long_line_files: Vec<String>,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
152#[serde(rename_all = "camelCase")]
153pub struct SizeResult {
154    pub status: CollectorStatus,
155    pub files: u32,
156    #[serde(default)]
157    pub max_lines_per_file: u32,
158    #[serde(default)]
159    pub max_code_lines_per_file: u32,
160    #[serde(default)]
161    pub max_lines_per_function: u32,
162    #[serde(default)]
163    pub max_parameters_per_function: u32,
164    #[serde(default)]
165    pub violations: Vec<SizeViolation>,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
169#[serde(rename_all = "camelCase")]
170pub struct SizeViolation {
171    #[serde(rename = "ruleId")]
172    pub rule_id: String,
173    pub file: String,
174    pub line: u32,
175    #[serde(default)]
176    pub function: Option<String>,
177    pub message: String,
178    pub actual: u32,
179    pub threshold: u32,
180    pub severity: String,
181}
182
183#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
184#[serde(rename_all = "camelCase")]
185pub struct ComplexityResult {
186    pub status: CollectorStatus,
187    pub functions: u32,
188    #[serde(default)]
189    pub max_cyclomatic_complexity: u32,
190    #[serde(default)]
191    pub max_nesting_depth: u32,
192    #[serde(default)]
193    pub complex_functions: u32,
194    #[serde(default)]
195    pub violations: Vec<ComplexityViolation>,
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
199#[serde(rename_all = "camelCase")]
200pub struct ComplexityViolation {
201    #[serde(rename = "ruleId")]
202    pub rule_id: String,
203    pub file: String,
204    pub line: u32,
205    #[serde(default)]
206    pub function: Option<String>,
207    pub message: String,
208    pub actual: u32,
209    pub threshold: u32,
210    pub severity: String,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
214#[serde(rename_all = "lowercase")]
215pub enum CollectorStatus {
216    Pass,
217    Fail,
218    Skipped,
219    Error,
220}
221
222// ------------------------------------------------------------------------------------------------
223// Baseline
224// ------------------------------------------------------------------------------------------------
225
226#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
227#[serde(rename_all = "camelCase")]
228pub struct Baseline {
229    pub schema_version: String,
230    pub created_at: String,
231    pub rustquty_version: String,
232    pub thresholds: Thresholds,
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
236#[serde(rename_all = "camelCase")]
237pub struct Thresholds {
238    pub fmt: FmtThreshold,
239    pub clippy: ClippyThreshold,
240    pub tests: TestThreshold,
241    pub coverage: CoverageThreshold,
242    pub deny: DenyThreshold,
243    pub audit: AuditThreshold,
244    pub hack: HackThreshold,
245    pub mutants: MutantsThreshold,
246    pub duplicates: DuplicatesThreshold,
247    pub loc: LocThreshold,
248    pub size: SizeThreshold,
249    pub complexity: ComplexityThreshold,
250}
251
252#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
253#[serde(rename_all = "camelCase")]
254pub struct FmtThreshold {
255    pub must_pass: bool,
256}
257
258#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
259#[serde(rename_all = "camelCase")]
260pub struct ClippyThreshold {
261    pub max_warnings: u32,
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
265#[serde(rename_all = "camelCase")]
266pub struct TestThreshold {
267    pub max_failures: u32,
268}
269
270#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
271#[serde(rename_all = "camelCase")]
272pub struct CoverageThreshold {
273    pub min_line_percent: f64,
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
277#[serde(rename_all = "camelCase")]
278pub struct DenyThreshold {
279    pub max_banned: u32,
280    pub max_license_violations: u32,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
284#[serde(rename_all = "camelCase")]
285pub struct AuditThreshold {
286    pub max_vulnerabilities: u32,
287    pub max_critical: u32,
288}
289
290#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
291#[serde(rename_all = "camelCase")]
292pub struct HackThreshold {
293    pub must_pass: bool,
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
297#[serde(rename_all = "camelCase")]
298pub struct MutantsThreshold {
299    pub min_score: f64,
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
303#[serde(rename_all = "camelCase")]
304pub struct DuplicatesThreshold {
305    pub max_duplicate_lines: u32,
306}
307
308#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
309#[serde(rename_all = "camelCase")]
310pub struct LocThreshold {
311    pub max_line_length: usize,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
315#[serde(rename_all = "camelCase")]
316pub struct SizeThreshold {
317    #[serde(default)]
318    pub max_lines_per_file: Option<u32>,
319    #[serde(default)]
320    pub max_code_lines_per_file: Option<u32>,
321    #[serde(default)]
322    pub max_lines_per_function: Option<u32>,
323    #[serde(default)]
324    pub max_parameters_per_function: Option<u32>,
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
328#[serde(rename_all = "camelCase")]
329pub struct ComplexityThreshold {
330    #[serde(default)]
331    pub max_cyclomatic_per_function: Option<u32>,
332    #[serde(default)]
333    pub max_nesting_depth: Option<u32>,
334}
335
336// ------------------------------------------------------------------------------------------------
337// QualityReport
338// ------------------------------------------------------------------------------------------------
339
340#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
341#[serde(rename_all = "camelCase")]
342pub struct QualityReport {
343    pub schema_version: String,
344    pub generated_at: String,
345    pub gate_result: GateResult,
346    #[serde(default)]
347    pub violations: Vec<Violation>,
348    pub summary: ReportSummary,
349}
350
351#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
352#[serde(rename_all = "lowercase")]
353pub enum GateResult {
354    Pass,
355    Fail,
356    Error,
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
360#[serde(rename_all = "camelCase")]
361pub struct Violation {
362    pub collector: String,
363    pub metric: String,
364    pub baseline_value: serde_json::Value,
365    pub current_value: serde_json::Value,
366    pub message: String,
367}
368
369#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
370#[serde(rename_all = "camelCase")]
371pub struct ReportSummary {
372    pub collectors_run: u32,
373    pub collectors_passed: u32,
374    pub collectors_failed: u32,
375    pub collectors_skipped: u32,
376}
377
378// ------------------------------------------------------------------------------------------------
379// Schema version errors
380// ------------------------------------------------------------------------------------------------
381
382#[derive(Debug, thiserror::Error)]
383pub enum SchemaVersionError {
384    #[error("unknown schema version: {0}")]
385    UnknownVersion(String),
386}
387
388impl MetricsSummary {
389    pub fn check_version(&self) -> Result<(), SchemaVersionError> {
390        if &self.schema_version != "1" {
391            return Err(SchemaVersionError::UnknownVersion(
392                self.schema_version.clone(),
393            ));
394        }
395        Ok(())
396    }
397}
398
399impl Baseline {
400    pub fn check_version(&self) -> Result<(), SchemaVersionError> {
401        if &self.schema_version != "1" {
402            return Err(SchemaVersionError::UnknownVersion(
403                self.schema_version.clone(),
404            ));
405        }
406        Ok(())
407    }
408}
409
410impl QualityReport {
411    pub fn check_version(&self) -> Result<(), SchemaVersionError> {
412        if &self.schema_version != "1" {
413            return Err(SchemaVersionError::UnknownVersion(
414                self.schema_version.clone(),
415            ));
416        }
417        Ok(())
418    }
419}
420
421// ------------------------------------------------------------------------------------------------
422// Round-trip tests
423// ------------------------------------------------------------------------------------------------
424
425#[cfg(test)]
426mod tests {
427    use super::*;
428
429    #[test]
430    fn test_metrics_summary_roundtrip() {
431        let json = r#"{
432          "schemaVersion": "1",
433          "generatedAt": "2026-05-04T12:00:00Z",
434          "rustqutyVersion": "0.1.0",
435          "project": {
436            "name": "test-project",
437            "rustEdition": "2021",
438            "workspaceRoot": "/path/to/project"
439          },
440          "collectors": {
441            "fmt": { "status": "pass", "details": {} },
442            "clippy": { "status": "pass", "warningCount": 0, "details": [] },
443            "tests": { "status": "pass", "passed": 10, "failed": 0, "ignored": 0 },
444            "coverage": { "status": "skipped", "linePercent": 0.0 },
445            "deny": { "status": "skipped", "bannedCount": 0, "licenseViolations": 0 },
446            "audit": { "status": "skipped", "vulnerabilityCount": 0, "criticalCount": 0 },
447            "hack": { "status": "skipped", "featureCombinationsTested": 0 },
448            "mutants": { "status": "skipped", "mutationScore": 0.0, "caught": 0, "missed": 0 },
449            "duplicates": { "status": "skipped", "totalLines": 0, "duplicateLines": 0, "filesWithDuplicates": 0, "duplicateFiles": [] },
450            "loc": { "status": "skipped", "totalLines": 0, "codeLines": 0, "commentLines": 0, "blankLines": 0, "longLines": 0, "maxLineLengthFound": 0, "maxLineLengthAllowed": 120, "files": 0, "filesWithLongLines": 0, "longLineFiles": [] },
451            "size": { "status": "skipped", "files": 0, "maxLinesPerFile": 0, "maxCodeLinesPerFile": 0, "maxLinesPerFunction": 0, "maxParametersPerFunction": 0, "violations": [] },
452            "complexity": { "status": "skipped", "functions": 0, "maxCyclomaticComplexity": 0, "maxNestingDepth": 0, "complexFunctions": 0, "violations": [] }
453          }
454        }"#;
455        let summary: MetricsSummary = serde_json::from_str(json).unwrap();
456        assert_eq!(summary.schema_version, "1");
457        let output = serde_json::to_string(&summary).unwrap();
458        assert!(output.contains("\"schemaVersion\":\"1\""));
459    }
460
461    #[test]
462    fn test_baseline_roundtrip() {
463        let json = r#"{
464          "schemaVersion": "1",
465          "createdAt": "2026-05-04T00:00:00Z",
466          "rustqutyVersion": "0.1.0",
467          "thresholds": {
468            "fmt": { "mustPass": true },
469            "clippy": { "maxWarnings": 0 },
470            "tests": { "maxFailures": 0 },
471            "coverage": { "minLinePercent": 80.0 },
472            "deny": { "maxBanned": 0, "maxLicenseViolations": 0 },
473            "audit": { "maxVulnerabilities": 0, "maxCritical": 0 },
474            "hack": { "mustPass": true },
475            "mutants": { "minScore": 0.8 },
476            "duplicates": { "maxDuplicateLines": 100 },
477            "loc": { "maxLineLength": 120 },
478            "size": { "maxLinesPerFile": 1000, "maxCodeLinesPerFile": 700, "maxLinesPerFunction": 80, "maxParametersPerFunction": 6 },
479            "complexity": { "maxCyclomaticPerFunction": 10, "maxNestingDepth": 5 }
480          }
481        }"#;
482        let baseline: Baseline = serde_json::from_str(json).unwrap();
483        assert_eq!(baseline.thresholds.coverage.min_line_percent, 80.0);
484        let output = serde_json::to_string(&baseline).unwrap();
485        assert!(output.contains("\"minLinePercent\":80.0"));
486    }
487
488    #[test]
489    fn test_quality_report_roundtrip() {
490        let json = r#"{
491          "schemaVersion": "1",
492          "generatedAt": "2026-05-04T12:00:00Z",
493          "gateResult": "fail",
494          "violations": [
495            {
496              "collector": "clippy",
497              "metric": "warningCount",
498              "baselineValue": "0",
499              "currentValue": "5",
500              "message": "clippy warning count exceeded baseline"
501            }
502          ],
503          "summary": {
504            "collectorsRun": 8,
505            "collectorsPassed": 7,
506            "collectorsFailed": 1,
507            "collectorsSkipped": 0
508          }
509        }"#;
510        let report: QualityReport = serde_json::from_str(json).unwrap();
511        assert_eq!(report.violations.len(), 1);
512        let output = serde_json::to_string(&report).unwrap();
513        assert!(output.contains("\"gateResult\":\"fail\""));
514    }
515
516    #[test]
517    fn test_unknown_schema_version_error() {
518        let json = r#"{
519          "schemaVersion": "99",
520          "generatedAt": "2026-05-04T12:00:00Z",
521          "rustqutyVersion": "0.1.0",
522          "project": {
523            "name": "test",
524            "rustEdition": "2021",
525            "workspaceRoot": "/path"
526          },
527          "collectors": {
528            "fmt": { "status": "pass", "details": {} },
529            "clippy": { "status": "pass", "warningCount": 0, "details": [] },
530            "tests": { "status": "pass", "passed": 0, "failed": 0, "ignored": 0 },
531            "coverage": { "status": "pass", "linePercent": 0.0 },
532            "deny": { "status": "pass", "bannedCount": 0, "licenseViolations": 0 },
533            "audit": { "status": "pass", "vulnerabilityCount": 0, "criticalCount": 0 },
534            "hack": { "status": "pass", "featureCombinationsTested": 0 },
535            "mutants": { "status": "pass", "mutationScore": 0.0, "caught": 0, "missed": 0 },
536            "duplicates": { "status": "pass", "totalLines": 1000, "duplicateLines": 0, "filesWithDuplicates": 0, "duplicateFiles": [] },
537            "loc": { "status": "pass", "totalLines": 1000, "codeLines": 800, "commentLines": 100, "blankLines": 100, "longLines": 0, "maxLineLengthFound": 100, "maxLineLengthAllowed": 120, "files": 10, "filesWithLongLines": 0, "longLineFiles": [] },
538            "size": { "status": "pass", "files": 10, "maxLinesPerFile": 500, "maxCodeLinesPerFile": 400, "maxLinesPerFunction": 80, "maxParametersPerFunction": 5, "violations": [] },
539            "complexity": { "status": "skipped", "functions": 0, "maxCyclomaticComplexity": 0, "maxNestingDepth": 0, "complexFunctions": 0, "violations": [] }
540          }
541        }"#;
542        let summary: MetricsSummary = serde_json::from_str(json).unwrap();
543        let err = summary.check_version().unwrap_err();
544        assert!(matches!(err, SchemaVersionError::UnknownVersion(v) if v == "99"));
545    }
546}