Skip to main content

fallow_api/
list_output.rs

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