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    /// Workspace listing whose count, members, and diagnostics are flattened
29    /// into the output body; `None` omits all three fields.
30    pub workspaces: Option<WorkspacesOutput<Diagnostic>>,
31    /// Diagnostics the plugin stage recorded (`plugin-config-unreadable`,
32    /// `plugin-effect-not-modeled`), appended to `workspace_diagnostics`.
33    /// When the workspace listing is absent, a non-empty set still emits
34    /// `workspace_diagnostics`, so a listing of plugins or entry points states
35    /// the plugin problem that shaped it.
36    pub plugin_diagnostics: Vec<Diagnostic>,
37}
38
39/// Build the typed list output body before optional root wrapping.
40#[must_use]
41pub fn build_list_json_output<Boundaries, Diagnostic>(
42    input: ListJsonOutputInput<Boundaries, Diagnostic>,
43) -> ListOutput<Boundaries, Diagnostic> {
44    let plugins = input.plugins.map(|plugins| {
45        plugins
46            .into_iter()
47            .map(|name| ListPluginOutput { name })
48            .collect()
49    });
50    let file_count = input.files.as_ref().map(Vec::len);
51    let entry_point_count = input.entry_points.as_ref().map(Vec::len);
52    let (workspace_count, workspaces, mut workspace_diagnostics) =
53        input.workspaces.map_or((None, None, None), |workspaces| {
54            (
55                Some(workspaces.workspace_count),
56                Some(workspaces.workspaces),
57                Some(workspaces.workspace_diagnostics),
58            )
59        });
60    if !input.plugin_diagnostics.is_empty() {
61        workspace_diagnostics
62            .get_or_insert_with(Vec::new)
63            .extend(input.plugin_diagnostics);
64    }
65
66    ListOutput {
67        plugins,
68        file_count,
69        files: input.files,
70        entry_point_count,
71        entry_points: input.entry_points,
72        boundaries: input.boundaries,
73        workspace_count,
74        workspaces,
75        workspace_diagnostics,
76    }
77}
78
79/// Serialize a typed `fallow list --format json` payload.
80///
81/// # Errors
82///
83/// Returns a serde error when the selected list output cannot be converted to
84/// JSON.
85pub fn serialize_list_json_output<Boundaries, Diagnostic>(
86    input: ListJsonOutputInput<Boundaries, Diagnostic>,
87    envelope: ListJsonEnvelope,
88) -> Result<serde_json::Value, serde_json::Error>
89where
90    Boundaries: Serialize,
91    Diagnostic: Serialize,
92{
93    let output = build_list_json_output(input);
94    match envelope {
95        ListJsonEnvelope::Plain => serde_json::to_value(output),
96        ListJsonEnvelope::Boundaries => {
97            fallow_output::serialize_list_boundaries_json_output(output)
98        }
99        ListJsonEnvelope::Workspaces => {
100            fallow_output::serialize_list_workspaces_json_output(output)
101        }
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use fallow_output::ListEntryPointOutput;
108    use serde_json::json;
109
110    use super::*;
111
112    #[test]
113    fn list_json_output_preserves_plain_legacy_body() {
114        let value = serialize_list_json_output::<serde_json::Value, serde_json::Value>(
115            ListJsonOutputInput {
116                plugins: Some(vec!["react".to_string()]),
117                files: Some(vec!["src/index.ts".to_string()]),
118                entry_points: Some(vec![ListEntryPointOutput {
119                    path: "src/index.ts".to_string(),
120                    source: "package.json main".to_string(),
121                }]),
122                boundaries: None,
123                workspaces: None,
124                plugin_diagnostics: Vec::new(),
125            },
126            ListJsonEnvelope::Plain,
127        )
128        .expect("list output should serialize");
129
130        assert_eq!(value["plugins"][0]["name"], "react");
131        assert_eq!(value["file_count"], 1);
132        assert_eq!(value["files"], json!(["src/index.ts"]));
133        assert_eq!(value["entry_point_count"], 1);
134        assert!(value.get("kind").is_none());
135    }
136
137    #[test]
138    fn list_json_output_wraps_boundary_payloads() {
139        let value = serialize_list_json_output::<serde_json::Value, serde_json::Value>(
140            ListJsonOutputInput {
141                plugins: None,
142                files: None,
143                entry_points: None,
144                boundaries: Some(json!({"configured": false})),
145                workspaces: None,
146                plugin_diagnostics: Vec::new(),
147            },
148            ListJsonEnvelope::Boundaries,
149        )
150        .expect("list output should serialize");
151
152        assert_eq!(value["kind"], "list-boundaries");
153        assert_eq!(value["boundaries"]["configured"], false);
154    }
155}