fallow_api/
combined_output.rs1use std::path::Path;
4use std::time::Duration;
5
6use fallow_output::{
7 CHECK_SCHEMA_VERSION, CombinedMeta, CombinedOutput, HealthReport, RootEnvelopeMode, check_meta,
8 dupes_meta, health_meta, serialize_combined_json_output, strip_root_prefix,
9};
10use fallow_types::envelope::{ElapsedMs, SchemaVersion, ToolVersion};
11use fallow_types::output::NextStep;
12use fallow_types::results::AnalysisResults;
13
14use crate::{
15 CheckJsonExtraOutputs, CheckJsonPayloadInput, DupesReportPayload, serialize_check_json_payload,
16};
17
18pub struct CombinedCheckJsonSection<'a> {
20 pub results: &'a AnalysisResults,
21 pub root: &'a Path,
22 pub elapsed: Duration,
23 pub config_fixable: bool,
24 pub extras: CheckJsonExtraOutputs,
25}
26
27pub struct CombinedJsonOutputInput<'a> {
29 pub check: Option<CombinedCheckJsonSection<'a>>,
30 pub dupes: Option<&'a DupesReportPayload>,
31 pub health: Option<&'a HealthReport>,
32 pub root: &'a Path,
33 pub elapsed: Duration,
34 pub explain: bool,
35 pub next_steps: Vec<NextStep>,
36 pub envelope_mode: RootEnvelopeMode,
37 pub telemetry_analysis_run_id: Option<&'a str>,
38}
39
40pub fn serialize_combined_json(
46 input: CombinedJsonOutputInput<'_>,
47) -> Result<serde_json::Value, serde_json::Error> {
48 let check = input.check.map(serialize_combined_check_json).transpose()?;
49 let dupes = serialize_combined_dupes_json(input.dupes, input.root)?;
50 let health = serialize_combined_health_json(input.health, input.root)?;
51
52 let output = CombinedOutput {
53 schema_version: SchemaVersion(CHECK_SCHEMA_VERSION),
54 version: ToolVersion(env!("CARGO_PKG_VERSION").to_string()),
55 elapsed_ms: ElapsedMs(elapsed_ms_for_output(input.elapsed)),
56 meta: input
57 .explain
58 .then(|| combined_meta_for_output(check.is_some(), dupes.is_some(), health.is_some())),
59 check,
60 dupes,
61 health,
62 next_steps: input.next_steps,
63 };
64
65 serialize_combined_json_output(output, input.envelope_mode, input.telemetry_analysis_run_id)
66}
67
68fn serialize_combined_check_json(
69 section: CombinedCheckJsonSection<'_>,
70) -> Result<serde_json::Value, serde_json::Error> {
71 serialize_check_json_payload(CheckJsonPayloadInput {
72 results: section.results,
73 root: section.root,
74 elapsed: section.elapsed,
75 config_fixable: section.config_fixable,
76 extras: section.extras,
77 workspace_diagnostics: Vec::new(),
78 })
79}
80
81pub fn serialize_combined_dupes_json(
88 dupes: Option<&DupesReportPayload>,
89 root: &Path,
90) -> Result<Option<serde_json::Value>, serde_json::Error> {
91 let Some(payload) = dupes else {
92 return Ok(None);
93 };
94 let mut json = serde_json::to_value(payload)?;
95 let root_prefix = format!("{}/", root.display());
96 strip_root_prefix(&mut json, &root_prefix);
97 Ok(Some(json))
98}
99
100pub fn serialize_combined_health_json(
106 health: Option<&HealthReport>,
107 root: &Path,
108) -> Result<Option<serde_json::Value>, serde_json::Error> {
109 let Some(report) = health else {
110 return Ok(None);
111 };
112 let mut json = serde_json::to_value(report)?;
113 let root_prefix = format!("{}/", root.display());
114 strip_root_prefix(&mut json, &root_prefix);
115 Ok(Some(json))
116}
117
118fn elapsed_ms_for_output(elapsed: Duration) -> u64 {
119 u64::try_from(elapsed.as_millis()).unwrap_or(u64::MAX)
120}
121
122fn combined_meta_for_output(
123 include_check: bool,
124 include_dupes: bool,
125 include_health: bool,
126) -> CombinedMeta {
127 CombinedMeta {
128 check: include_check.then(check_meta),
129 dupes: include_dupes.then(dupes_meta),
130 health: include_health.then(health_meta),
131 telemetry: None,
132 }
133}
134
135#[cfg(test)]
136mod tests {
137 use std::time::Duration;
138
139 use fallow_output::RootEnvelopeMode;
140
141 use super::{CombinedJsonOutputInput, serialize_combined_json};
142
143 #[test]
144 fn combined_json_root_contains_stable_envelope_fields() {
145 let root = serialize_combined_json(CombinedJsonOutputInput {
146 check: None,
147 dupes: None,
148 health: None,
149 root: std::path::Path::new("."),
150 elapsed: Duration::from_millis(42),
151 explain: false,
152 next_steps: Vec::new(),
153 envelope_mode: RootEnvelopeMode::Tagged,
154 telemetry_analysis_run_id: None,
155 })
156 .expect("combined JSON root");
157
158 assert_eq!(
159 root.get("kind").and_then(serde_json::Value::as_str),
160 Some("combined")
161 );
162 assert_eq!(
163 root.get("elapsed_ms").and_then(serde_json::Value::as_u64),
164 Some(42)
165 );
166 assert!(root.get("schema_version").is_some());
167 assert!(root.get("version").is_some());
168 }
169}