Skip to main content

cargo_crap/report/
json.rs

1//! `--format json` and `--format json --baseline …` envelope output.
2//!
3//! Outputs a versioned, schema-tagged envelope so consumers can detect
4//! breaking changes between releases. The envelope shape is mirrored on
5//! input as well — `delta::load_baseline` deserializes the same struct.
6
7use crate::delta::{DeltaEntry, DeltaReport};
8use crate::merge::CrapEntry;
9use anyhow::Result;
10use std::io::Write;
11
12/// Schema/release version stamped onto every JSON envelope so consumers can
13/// detect breaking changes between releases. Mirrors the crate version.
14pub const SCHEMA_VERSION: &str = env!("CARGO_PKG_VERSION");
15
16/// Build the published HTTPS URL for a schema file in this repo.
17///
18/// `concat!` only takes literals, so the base URL is repeated by expansion
19/// rather than reference. Centralized here so a repo move or schema-version
20/// bump changes only this macro and the filename arguments below.
21macro_rules! schema_url {
22    ($file:literal) => {
23        concat!(
24            "https://raw.githubusercontent.com/minikin/cargo-crap/main/schemas/",
25            $file
26        )
27    };
28}
29
30/// Stable HTTPS URL of the JSON Schema describing the absolute envelope shape.
31pub const REPORT_SCHEMA_URL: &str = schema_url!("report-v1.json");
32
33/// Stable HTTPS URL of the JSON Schema describing the delta envelope shape.
34///
35/// Bumped to `delta-v2.json` in spec 13: adds the `moved` status value and
36/// the optional `previous_file` field. Consumers reading v1 see one new
37/// enum value and one new optional field — strictly additive.
38pub const DELTA_SCHEMA_URL: &str = schema_url!("delta-v2.json");
39
40/// JSON wire format for `--format json` output and `--baseline` input.
41#[derive(serde::Serialize, serde::Deserialize)]
42pub struct Envelope {
43    /// URL of the JSON Schema this document conforms to. Optional on input
44    /// (older baselines may predate the field) and always emitted on output.
45    #[serde(rename = "$schema", default, skip_serializing_if = "Option::is_none")]
46    pub schema: Option<String>,
47    pub version: String,
48    pub entries: Vec<CrapEntry>,
49}
50
51pub(crate) fn render_json(
52    entries: &[CrapEntry],
53    out: &mut dyn Write,
54) -> Result<()> {
55    let envelope = Envelope {
56        schema: Some(REPORT_SCHEMA_URL.to_string()),
57        version: SCHEMA_VERSION.to_string(),
58        entries: entries.to_vec(),
59    };
60    serde_json::to_writer_pretty(&mut *out, &envelope)?;
61    out.write_all(b"\n")?;
62    Ok(())
63}
64
65pub(crate) fn render_delta_json(
66    report: &DeltaReport,
67    out: &mut dyn Write,
68) -> Result<()> {
69    #[derive(serde::Serialize)]
70    struct DeltaOutput<'a> {
71        #[serde(rename = "$schema")]
72        schema: &'static str,
73        version: &'static str,
74        entries: &'a [DeltaEntry],
75        removed: &'a [crate::delta::RemovedEntry],
76    }
77    serde_json::to_writer_pretty(
78        &mut *out,
79        &DeltaOutput {
80            schema: DELTA_SCHEMA_URL,
81            version: SCHEMA_VERSION,
82            entries: &report.entries,
83            removed: &report.removed,
84        },
85    )?;
86    out.write_all(b"\n")?;
87    Ok(())
88}
89
90#[cfg(test)]
91mod tests {
92    use super::super::test_support::sample;
93    use super::super::{Format, render};
94    use super::*;
95    use std::path::PathBuf;
96
97    #[test]
98    fn json_output_is_envelope_with_version_and_entries() {
99        let mut buf = Vec::new();
100        render(&sample(), 30.0, Format::Json, None, &mut buf).unwrap();
101        let parsed: serde_json::Value = serde_json::from_slice(&buf).unwrap();
102        assert!(parsed.is_object(), "JSON output must be an envelope object");
103        assert_eq!(
104            parsed["version"].as_str(),
105            Some(SCHEMA_VERSION),
106            "version field must equal SCHEMA_VERSION"
107        );
108        assert!(
109            parsed["entries"].is_array(),
110            "entries field must be an array"
111        );
112        assert_eq!(
113            parsed["entries"].as_array().map(std::vec::Vec::len),
114            Some(2)
115        );
116    }
117
118    #[test]
119    fn json_format_unaffected_by_links() {
120        use super::super::SourceLinks;
121        let entries = vec![CrapEntry {
122            file: PathBuf::from("src/a.rs"),
123            function: "foo".into(),
124            line: 1,
125            cyclomatic: 1.0,
126            coverage: Some(100.0),
127            crap: 1.0,
128            crate_name: None,
129        }];
130        let links = SourceLinks::new("https://github.com/o/r".into(), "sha".into());
131        let mut buf = Vec::new();
132        render(&entries, 30.0, Format::Json, Some(&links), &mut buf).unwrap();
133        let s = String::from_utf8(buf).unwrap();
134        assert!(
135            !s.contains("](https://"),
136            "JSON output must not contain markdown links:\n{s}"
137        );
138    }
139}