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    pub plugins: Option<Vec<String>>,
22    pub files: Option<Vec<String>>,
23    pub entry_points: Option<Vec<ListEntryPointOutput>>,
24    pub boundaries: Option<Boundaries>,
25    pub workspaces: Option<WorkspacesOutput<Diagnostic>>,
26}
27
28/// Build the typed list output body before optional root wrapping.
29#[must_use]
30pub fn build_list_json_output<Boundaries, Diagnostic>(
31    input: ListJsonOutputInput<Boundaries, Diagnostic>,
32) -> ListOutput<Boundaries, Diagnostic> {
33    let plugins = input.plugins.map(|plugins| {
34        plugins
35            .into_iter()
36            .map(|name| ListPluginOutput { name })
37            .collect()
38    });
39    let file_count = input.files.as_ref().map(Vec::len);
40    let entry_point_count = input.entry_points.as_ref().map(Vec::len);
41    let (workspace_count, workspaces, workspace_diagnostics) =
42        input.workspaces.map_or((None, None, None), |workspaces| {
43            (
44                Some(workspaces.workspace_count),
45                Some(workspaces.workspaces),
46                Some(workspaces.workspace_diagnostics),
47            )
48        });
49
50    ListOutput {
51        plugins,
52        file_count,
53        files: input.files,
54        entry_point_count,
55        entry_points: input.entry_points,
56        boundaries: input.boundaries,
57        workspace_count,
58        workspaces,
59        workspace_diagnostics,
60    }
61}
62
63/// Serialize a typed `fallow list --format json` payload.
64///
65/// # Errors
66///
67/// Returns a serde error when the selected list output cannot be converted to
68/// JSON.
69pub fn serialize_list_json_output<Boundaries, Diagnostic>(
70    input: ListJsonOutputInput<Boundaries, Diagnostic>,
71    mode: RootEnvelopeMode,
72    envelope: ListJsonEnvelope,
73) -> Result<serde_json::Value, serde_json::Error>
74where
75    Boundaries: Serialize,
76    Diagnostic: Serialize,
77{
78    let output = build_list_json_output(input);
79    match envelope {
80        ListJsonEnvelope::Plain => serde_json::to_value(output),
81        ListJsonEnvelope::Boundaries => {
82            fallow_output::serialize_list_boundaries_json_output(output, mode)
83        }
84        ListJsonEnvelope::Workspaces => {
85            fallow_output::serialize_list_workspaces_json_output(output, mode)
86        }
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use fallow_output::ListEntryPointOutput;
93    use serde_json::json;
94
95    use super::*;
96
97    #[test]
98    fn list_json_output_preserves_plain_legacy_body() {
99        let value = serialize_list_json_output::<serde_json::Value, serde_json::Value>(
100            ListJsonOutputInput {
101                plugins: Some(vec!["react".to_string()]),
102                files: Some(vec!["src/index.ts".to_string()]),
103                entry_points: Some(vec![ListEntryPointOutput {
104                    path: "src/index.ts".to_string(),
105                    source: "package.json main".to_string(),
106                }]),
107                boundaries: None,
108                workspaces: None,
109            },
110            RootEnvelopeMode::Tagged,
111            ListJsonEnvelope::Plain,
112        )
113        .expect("list output should serialize");
114
115        assert_eq!(value["plugins"][0]["name"], "react");
116        assert_eq!(value["file_count"], 1);
117        assert_eq!(value["files"], json!(["src/index.ts"]));
118        assert_eq!(value["entry_point_count"], 1);
119        assert!(value.get("kind").is_none());
120    }
121
122    #[test]
123    fn list_json_output_wraps_boundary_payloads() {
124        let value = serialize_list_json_output::<serde_json::Value, serde_json::Value>(
125            ListJsonOutputInput {
126                plugins: None,
127                files: None,
128                entry_points: None,
129                boundaries: Some(json!({"configured": false})),
130                workspaces: None,
131            },
132            RootEnvelopeMode::Tagged,
133            ListJsonEnvelope::Boundaries,
134        )
135        .expect("list output should serialize");
136
137        assert_eq!(value["kind"], "list-boundaries");
138        assert_eq!(value["boundaries"]["configured"], false);
139    }
140}