Skip to main content

fallow_api/
json_output.rs

1//! Shared JSON output assembly for CLI and programmatic consumers.
2
3use std::path::Path;
4use std::time::Duration;
5
6use fallow_output::{
7    CHECK_SCHEMA_VERSION, CheckGroupedEntry, CheckGroupedOutput, CheckOutput, CheckOutputInput,
8    DupesOutput, DupesOutputInput, GroupByMode, RootEnvelopeMode,
9    apply_config_fixable_to_duplicate_exports, build_check_output, build_dupes_output,
10    harmonize_multi_kind_suppress_line_actions as harmonize_typed_suppress_line_actions,
11    strip_root_prefix,
12};
13use fallow_types::duplicates::DuplicationReport;
14use fallow_types::envelope::{
15    BaselineDeltas, BaselineMatch, ElapsedMs, Meta, RegressionResult, SchemaVersion, ToolVersion,
16};
17use fallow_types::output::NextStep;
18use fallow_types::results::AnalysisResults;
19use fallow_types::workspace::WorkspaceDiagnostic;
20
21use crate::{DupesReportPayload, DuplicationGroup, DuplicationGrouping, ResultGroup};
22
23/// Inputs for `fallow dead-code --format json` output assembly.
24pub struct CheckJsonOutputInput<'a> {
25    pub results: &'a AnalysisResults,
26    pub root: &'a Path,
27    pub elapsed: Duration,
28    pub config_fixable: bool,
29    pub meta: Option<Meta>,
30    pub extras: CheckJsonExtraOutputs,
31    pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
32    pub next_steps: Vec<NextStep>,
33    pub envelope_mode: RootEnvelopeMode,
34    pub telemetry_analysis_run_id: Option<&'a str>,
35}
36
37/// Inputs for the dead-code JSON payload without a root envelope.
38pub struct CheckJsonPayloadInput<'a> {
39    pub results: &'a AnalysisResults,
40    pub root: &'a Path,
41    pub elapsed: Duration,
42    pub config_fixable: bool,
43    pub extras: CheckJsonExtraOutputs,
44    pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
45}
46
47/// Optional root sections for dead-code JSON envelopes.
48///
49/// These fields are part of the output contract, but they are computed by
50/// caller-specific workflows such as baseline and regression gates.
51#[derive(Debug, Clone, Default)]
52pub struct CheckJsonExtraOutputs {
53    pub baseline_deltas: Option<BaselineDeltas>,
54    pub baseline: Option<BaselineMatch>,
55    pub regression: Option<RegressionResult>,
56}
57
58struct CheckJsonEnvelopeInput<'a> {
59    results: &'a AnalysisResults,
60    elapsed: Duration,
61    config_fixable: bool,
62    meta: Option<Meta>,
63    extras: CheckJsonExtraOutputs,
64    workspace_diagnostics: Vec<WorkspaceDiagnostic>,
65    next_steps: Vec<NextStep>,
66}
67
68/// Inputs for grouped dead-code JSON output assembly.
69pub struct GroupedCheckJsonOutputInput<'a> {
70    pub groups: &'a [ResultGroup],
71    pub original: &'a AnalysisResults,
72    pub root: &'a Path,
73    pub elapsed: Duration,
74    pub grouped_by: GroupByMode,
75    pub config_fixable: bool,
76    pub meta: Option<Meta>,
77    pub next_steps: Vec<NextStep>,
78    pub envelope_mode: RootEnvelopeMode,
79    pub telemetry_analysis_run_id: Option<&'a str>,
80}
81
82/// Inputs for `fallow dupes --format json` output assembly.
83pub struct DuplicationJsonOutputInput<'a> {
84    pub report: &'a DuplicationReport,
85    pub root: &'a Path,
86    pub elapsed: Duration,
87    pub meta: Option<Meta>,
88    pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
89    pub next_steps: Vec<NextStep>,
90    pub envelope_mode: RootEnvelopeMode,
91    pub telemetry_analysis_run_id: Option<&'a str>,
92}
93
94/// Inputs for grouped duplication JSON output assembly.
95pub struct GroupedDuplicationJsonOutputInput<'a> {
96    pub report: &'a DuplicationReport,
97    pub grouping: &'a DuplicationGrouping,
98    pub root: &'a Path,
99    pub elapsed: Duration,
100    pub meta: Option<Meta>,
101    pub workspace_diagnostics: Vec<WorkspaceDiagnostic>,
102    pub next_steps: Vec<NextStep>,
103    pub envelope_mode: RootEnvelopeMode,
104    pub telemetry_analysis_run_id: Option<&'a str>,
105}
106
107/// Build and serialize dead-code JSON through the API-owned output boundary.
108///
109/// # Errors
110///
111/// Returns a serde error when the typed envelope cannot be converted to JSON.
112pub fn serialize_check_json(
113    input: CheckJsonOutputInput<'_>,
114) -> Result<serde_json::Value, serde_json::Error> {
115    let envelope = build_check_json_envelope(CheckJsonEnvelopeInput {
116        results: input.results,
117        elapsed: input.elapsed,
118        config_fixable: input.config_fixable,
119        meta: input.meta,
120        extras: input.extras,
121        workspace_diagnostics: input.workspace_diagnostics,
122        next_steps: input.next_steps,
123    });
124    let mut output = fallow_output::serialize_check_json_output(
125        envelope,
126        input.envelope_mode,
127        input.telemetry_analysis_run_id,
128    )?;
129    strip_json_root_prefix(&mut output, input.root);
130    Ok(output)
131}
132
133/// Build a dead-code JSON payload without adding a root envelope.
134///
135/// # Errors
136///
137/// Returns a serde error when the typed envelope cannot be converted to JSON.
138pub fn serialize_check_json_payload(
139    input: CheckJsonPayloadInput<'_>,
140) -> Result<serde_json::Value, serde_json::Error> {
141    let envelope = build_check_json_envelope(CheckJsonEnvelopeInput {
142        results: input.results,
143        elapsed: input.elapsed,
144        config_fixable: input.config_fixable,
145        meta: None,
146        extras: input.extras,
147        workspace_diagnostics: input.workspace_diagnostics,
148        next_steps: Vec::new(),
149    });
150    let mut output = serde_json::to_value(envelope)?;
151    strip_json_root_prefix(&mut output, input.root);
152    Ok(output)
153}
154
155/// Build and serialize grouped dead-code JSON through the API output boundary.
156///
157/// # Errors
158///
159/// Returns a serde error when the typed envelope cannot be converted to JSON.
160pub fn serialize_grouped_check_json(
161    input: GroupedCheckJsonOutputInput<'_>,
162) -> Result<serde_json::Value, serde_json::Error> {
163    let entries = input
164        .groups
165        .iter()
166        .map(|group| {
167            let mut results = group.results.clone();
168            apply_config_fixable_to_duplicate_exports(&mut results, input.config_fixable);
169            harmonize_typed_suppress_line_actions(&mut results);
170            CheckGroupedEntry {
171                key: group.key.clone(),
172                owners: group.owners.clone(),
173                total_issues: results.total_issues(),
174                results,
175            }
176        })
177        .collect();
178
179    let envelope = CheckGroupedOutput {
180        schema_version: SchemaVersion(CHECK_SCHEMA_VERSION),
181        version: ToolVersion(env!("CARGO_PKG_VERSION").to_string()),
182        elapsed_ms: ElapsedMs(input.elapsed.as_millis() as u64),
183        grouped_by: input.grouped_by,
184        total_issues: input.original.total_issues(),
185        groups: entries,
186        meta: input.meta,
187        next_steps: input.next_steps,
188    };
189
190    let mut output = fallow_output::serialize_check_grouped_json_output(
191        envelope,
192        input.envelope_mode,
193        input.telemetry_analysis_run_id,
194    )?;
195    let root_prefix = format!("{}/", input.root.display());
196    if let Some(arr) = output
197        .get_mut("groups")
198        .and_then(serde_json::Value::as_array_mut)
199    {
200        for entry in arr {
201            strip_root_prefix(entry, &root_prefix);
202        }
203    }
204    Ok(output)
205}
206
207/// Build and serialize duplication JSON through the API-owned output boundary.
208///
209/// # Errors
210///
211/// Returns a serde error when the typed envelope cannot be converted to JSON.
212pub fn serialize_duplication_json(
213    input: DuplicationJsonOutputInput<'_>,
214) -> Result<serde_json::Value, serde_json::Error> {
215    let payload = DupesReportPayload::from_report(input.report);
216    let envelope: DupesOutput<DupesReportPayload, DuplicationGroup> =
217        build_dupes_output(DupesOutputInput {
218            schema_version: CHECK_SCHEMA_VERSION,
219            version: env!("CARGO_PKG_VERSION").to_string(),
220            elapsed: input.elapsed,
221            report: payload,
222            grouped_by: None,
223            total_issues: None,
224            groups: None,
225            meta: input.meta,
226            workspace_diagnostics: input.workspace_diagnostics,
227            next_steps: input.next_steps,
228        });
229    let mut output = fallow_output::serialize_dupes_json_output(
230        envelope,
231        input.envelope_mode,
232        input.telemetry_analysis_run_id,
233    )?;
234    let root_prefix = format!("{}/", input.root.display());
235    strip_root_prefix(&mut output, &root_prefix);
236    Ok(output)
237}
238
239/// Build and serialize grouped duplication JSON through the API output boundary.
240///
241/// # Errors
242///
243/// Returns a serde error when the typed envelope cannot be converted to JSON.
244pub fn serialize_grouped_duplication_json(
245    input: GroupedDuplicationJsonOutputInput<'_>,
246) -> Result<serde_json::Value, serde_json::Error> {
247    let root_prefix = format!("{}/", input.root.display());
248    let payload = DupesReportPayload::from_report(input.report);
249    let envelope: DupesOutput<DupesReportPayload, DuplicationGroup> =
250        build_dupes_output(DupesOutputInput {
251            schema_version: CHECK_SCHEMA_VERSION,
252            version: env!("CARGO_PKG_VERSION").to_string(),
253            elapsed: input.elapsed,
254            report: payload,
255            grouped_by: Some(group_by_mode_from_label(input.grouping.mode)),
256            total_issues: Some(input.report.clone_groups.len()),
257            groups: None,
258            meta: input.meta,
259            workspace_diagnostics: input.workspace_diagnostics,
260            next_steps: input.next_steps,
261        });
262    let mut output = fallow_output::serialize_dupes_json_output(
263        envelope,
264        input.envelope_mode,
265        input.telemetry_analysis_run_id,
266    )?;
267    strip_root_prefix(&mut output, &root_prefix);
268
269    let group_values = input
270        .grouping
271        .groups
272        .iter()
273        .map(|group| {
274            let mut value = serde_json::to_value(group)?;
275            strip_root_prefix(&mut value, &root_prefix);
276            Ok(value)
277        })
278        .collect::<Result<Vec<_>, serde_json::Error>>()?;
279
280    if let serde_json::Value::Object(ref mut map) = output {
281        map.insert("groups".to_string(), serde_json::Value::Array(group_values));
282    }
283
284    Ok(output)
285}
286
287fn build_check_json_envelope(input: CheckJsonEnvelopeInput<'_>) -> CheckOutput {
288    let mut output = build_check_output(CheckOutputInput {
289        schema_version: CHECK_SCHEMA_VERSION,
290        version: env!("CARGO_PKG_VERSION").to_string(),
291        elapsed: input.elapsed,
292        results: input.results.clone(),
293        config_fixable: input.config_fixable,
294        meta: input.meta,
295        workspace_diagnostics: input.workspace_diagnostics,
296        next_steps: input.next_steps,
297    });
298    output.baseline_deltas = input.extras.baseline_deltas;
299    output.baseline = input.extras.baseline;
300    output.regression = input.extras.regression;
301    output
302}
303
304fn strip_json_root_prefix(output: &mut serde_json::Value, root: &Path) {
305    let root_prefix = format!("{}/", root.display());
306    strip_root_prefix(output, &root_prefix);
307}
308
309fn group_by_mode_from_label(label: &str) -> GroupByMode {
310    match label {
311        "directory" => GroupByMode::Directory,
312        "package" => GroupByMode::Package,
313        "section" => GroupByMode::Section,
314        _ => GroupByMode::Owner,
315    }
316}