Skip to main content

vfa_tui/models/
report.rs

1//! Headless reporter data models — output format, report types, and structured output.
2
3use serde::Serialize;
4
5/// Which kind of report to produce in headless mode.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum ReportType {
8    Coverage,
9    Violations,
10    Drift,
11    Stale,
12    Gates,
13    Integrity,
14    Versions,
15    Dependencies,
16    Lifecycle,
17    Summary,
18    All,
19}
20
21/// Output format requested by the caller.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub enum OutputFormat {
24    Json,
25    Markdown,
26    Table,
27}
28
29/// Top-level envelope for a single headless report section.
30///
31/// `data` is flattened into the JSON output so that consumers receive a single
32/// object with `report_type`, `timestamp`, `console_version`, `exit_code`, and
33/// all report-specific keys at the top level.
34#[derive(Debug, Serialize)]
35pub struct HeadlessOutput {
36    /// String representation of the [`ReportType`] that produced this output.
37    pub report_type: String,
38    /// ISO 8601 timestamp of when the report was generated.
39    pub timestamp: String,
40    /// Version of the `vfa-tui` binary that produced this output.
41    pub console_version: String,
42    /// Proposed exit code for this report section.
43    pub exit_code: i32,
44    /// Arbitrary report payload — flattened into the enclosing JSON object.
45    #[serde(flatten)]
46    pub data: serde_json::Value,
47}
48
49#[cfg(test)]
50mod tests {
51    use super::*;
52
53    #[test]
54    fn report_type_equality() {
55        assert_eq!(ReportType::Coverage, ReportType::Coverage);
56        assert_ne!(ReportType::Coverage, ReportType::Violations);
57        assert_eq!(ReportType::All, ReportType::All);
58    }
59
60    #[test]
61    fn output_format_equality() {
62        assert_eq!(OutputFormat::Json, OutputFormat::Json);
63        assert_ne!(OutputFormat::Json, OutputFormat::Markdown);
64        assert_eq!(OutputFormat::Table, OutputFormat::Table);
65    }
66
67    #[test]
68    fn headless_output_serializes_correctly() {
69        let output = HeadlessOutput {
70            report_type: "coverage".to_string(),
71            timestamp: "2025-01-01T00:00:00.000Z".to_string(),
72            console_version: "1.0.0".to_string(),
73            exit_code: 0,
74            data: serde_json::json!({"aggregate_score": 95.0}),
75        };
76        let json = serde_json::to_string(&output).expect("serialize");
77        let parsed: serde_json::Value = serde_json::from_str(&json).expect("parse");
78
79        assert_eq!(parsed["report_type"], "coverage");
80        assert_eq!(parsed["timestamp"], "2025-01-01T00:00:00.000Z");
81        assert_eq!(parsed["console_version"], "1.0.0");
82        assert_eq!(parsed["exit_code"], 0);
83        // flattened key from `data`:
84        assert_eq!(parsed["aggregate_score"], 95.0);
85    }
86
87    #[test]
88    fn headless_output_flatten_merges_data() {
89        // Confirm that `#[serde(flatten)]` on `data` merges keys at the top level.
90        let output = HeadlessOutput {
91            report_type: "gates".to_string(),
92            timestamp: "2025-06-01T00:00:00.000Z".to_string(),
93            console_version: "2.0.0".to_string(),
94            exit_code: 1,
95            data: serde_json::json!({"gates_passed": 3, "gates_failed": 1}),
96        };
97        let json = serde_json::to_string(&output).unwrap();
98        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
99
100        assert_eq!(parsed["exit_code"], 1);
101        assert_eq!(parsed["gates_passed"], 3);
102        assert_eq!(parsed["gates_failed"], 1);
103        // ensure no nested "data" key leaks through
104        assert!(parsed.get("data").is_none());
105    }
106
107    #[test]
108    fn report_type_all_variants_are_distinct() {
109        let variants = [
110            ReportType::Coverage,
111            ReportType::Violations,
112            ReportType::Drift,
113            ReportType::Stale,
114            ReportType::Gates,
115            ReportType::Integrity,
116            ReportType::Versions,
117            ReportType::Dependencies,
118            ReportType::Lifecycle,
119            ReportType::Summary,
120            ReportType::All,
121        ];
122        // Each variant is equal only to itself.
123        for (i, a) in variants.iter().enumerate() {
124            for (j, b) in variants.iter().enumerate() {
125                if i == j {
126                    assert_eq!(a, b);
127                } else {
128                    assert_ne!(a, b);
129                }
130            }
131        }
132    }
133}