Skip to main content

fallow_api/
list_output.rs

1//! Shared list command JSON output assembly.
2
3use fallow_output::{ListEntryPointOutput, ListOutput, ListPluginOutput, WorkspacesOutput};
4use serde::Serialize;
5
6/// Root envelope mode for a `fallow list --format json` payload.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum ListJsonEnvelope {
9    /// Emit the historical plain object without a `kind` field.
10    Plain,
11    /// Wrap as `kind: "list-boundaries"`.
12    Boundaries,
13    /// Wrap as `kind: "list-workspaces"`.
14    Workspaces,
15}
16
17/// Section data for serializing a `fallow list --format json` payload.
18pub struct ListJsonOutputInput<Boundaries, Diagnostic> {
19    /// Detected plugin names; `None` omits the plugins section entirely.
20    pub plugins: Option<Vec<String>>,
21    /// Analyzed file paths; `None` omits the files section and its count.
22    pub files: Option<Vec<String>>,
23    /// Resolved entry points with the source that declared each one; `None`
24    /// omits the section and its count.
25    pub entry_points: Option<Vec<ListEntryPointOutput>>,
26    /// Boundaries listing payload; `None` omits the section.
27    pub boundaries: Option<Boundaries>,
28    /// Startup import weight per runtime entry; `None` omits the section.
29    pub entry_weight: Option<fallow_output::EntryWeightListing>,
30    /// Workspace listing whose count, members, and diagnostics are flattened
31    /// into the output body; `None` omits all three fields.
32    pub workspaces: Option<WorkspacesOutput<Diagnostic>>,
33    /// Diagnostics the plugin stage recorded (`plugin-config-unreadable`,
34    /// `plugin-effect-not-modeled`), appended to `workspace_diagnostics`.
35    /// When the workspace listing is absent, a non-empty set still emits
36    /// `workspace_diagnostics`, so a listing of plugins or entry points states
37    /// the plugin problem that shaped it.
38    pub plugin_diagnostics: Vec<Diagnostic>,
39}
40
41/// Build the typed list output body before optional root wrapping.
42#[must_use]
43pub fn build_list_json_output<Boundaries, Diagnostic>(
44    input: ListJsonOutputInput<Boundaries, Diagnostic>,
45) -> ListOutput<Boundaries, Diagnostic> {
46    let plugins = input.plugins.map(|plugins| {
47        plugins
48            .into_iter()
49            .map(|name| ListPluginOutput { name })
50            .collect()
51    });
52    let file_count = input.files.as_ref().map(Vec::len);
53    let entry_point_count = input.entry_points.as_ref().map(Vec::len);
54    let (workspace_count, workspaces, mut workspace_diagnostics) =
55        input.workspaces.map_or((None, None, None), |workspaces| {
56            (
57                Some(workspaces.workspace_count),
58                Some(workspaces.workspaces),
59                Some(workspaces.workspace_diagnostics),
60            )
61        });
62    if !input.plugin_diagnostics.is_empty() {
63        workspace_diagnostics
64            .get_or_insert_with(Vec::new)
65            .extend(input.plugin_diagnostics);
66    }
67
68    ListOutput {
69        plugins,
70        file_count,
71        files: input.files,
72        entry_point_count,
73        entry_points: input.entry_points,
74        boundaries: input.boundaries,
75        entry_weight: input.entry_weight,
76        workspace_count,
77        workspaces,
78        workspace_diagnostics,
79    }
80}
81
82/// Serialize a typed `fallow list --format json` payload.
83///
84/// # Errors
85///
86/// Returns a serde error when the selected list output cannot be converted to
87/// JSON.
88pub fn serialize_list_json_output<Boundaries, Diagnostic>(
89    input: ListJsonOutputInput<Boundaries, Diagnostic>,
90    envelope: ListJsonEnvelope,
91) -> Result<serde_json::Value, serde_json::Error>
92where
93    Boundaries: Serialize,
94    Diagnostic: Serialize,
95{
96    let output = build_list_json_output(input);
97    match envelope {
98        ListJsonEnvelope::Plain => serde_json::to_value(output),
99        ListJsonEnvelope::Boundaries => {
100            fallow_output::serialize_list_boundaries_json_output(output)
101        }
102        ListJsonEnvelope::Workspaces => {
103            fallow_output::serialize_list_workspaces_json_output(output)
104        }
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use fallow_output::ListEntryPointOutput;
111    use serde_json::json;
112
113    use super::*;
114
115    #[test]
116    fn list_json_output_preserves_plain_legacy_body() {
117        let value = serialize_list_json_output::<serde_json::Value, serde_json::Value>(
118            ListJsonOutputInput {
119                plugins: Some(vec!["react".to_string()]),
120                files: Some(vec!["src/index.ts".to_string()]),
121                entry_points: Some(vec![ListEntryPointOutput {
122                    path: "src/index.ts".to_string(),
123                    source: "package.json main".to_string(),
124                }]),
125                boundaries: None,
126                entry_weight: None,
127                workspaces: None,
128                plugin_diagnostics: Vec::new(),
129            },
130            ListJsonEnvelope::Plain,
131        )
132        .expect("list output should serialize");
133
134        assert_eq!(value["plugins"][0]["name"], "react");
135        assert_eq!(value["file_count"], 1);
136        assert_eq!(value["files"], json!(["src/index.ts"]));
137        assert_eq!(value["entry_point_count"], 1);
138        assert!(value.get("kind").is_none());
139    }
140
141    #[test]
142    fn list_json_output_wraps_boundary_payloads() {
143        let value = serialize_list_json_output::<serde_json::Value, serde_json::Value>(
144            ListJsonOutputInput {
145                plugins: None,
146                files: None,
147                entry_points: None,
148                boundaries: Some(json!({"configured": false})),
149                entry_weight: None,
150                workspaces: None,
151                plugin_diagnostics: Vec::new(),
152            },
153            ListJsonEnvelope::Boundaries,
154        )
155        .expect("list output should serialize");
156
157        assert_eq!(value["kind"], "list-boundaries");
158        assert_eq!(value["boundaries"]["configured"], false);
159    }
160}