Skip to main content

fallow_output/
list_envelopes.rs

1//! List command output envelopes.
2
3use crate::root_envelopes::{RootEnvelopeMode, serialize_named_json_output};
4use serde::Serialize;
5
6/// Plain body emitted by `fallow list --format json` before an optional
7/// command-specific root envelope is attached.
8#[derive(Debug, Clone, Serialize)]
9#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
10pub struct ListOutput<Boundaries, Diagnostic> {
11    #[serde(default, skip_serializing_if = "Option::is_none")]
12    pub plugins: Option<Vec<ListPluginOutput>>,
13    #[serde(default, skip_serializing_if = "Option::is_none")]
14    pub file_count: Option<usize>,
15    #[serde(default, skip_serializing_if = "Option::is_none")]
16    pub files: Option<Vec<String>>,
17    #[serde(default, skip_serializing_if = "Option::is_none")]
18    pub entry_point_count: Option<usize>,
19    #[serde(default, skip_serializing_if = "Option::is_none")]
20    pub entry_points: Option<Vec<ListEntryPointOutput>>,
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub boundaries: Option<Boundaries>,
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    pub workspace_count: Option<usize>,
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub workspaces: Option<Vec<WorkspaceInfo>>,
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub workspace_diagnostics: Option<Vec<Diagnostic>>,
29}
30
31/// One active plugin in `fallow list --plugins --format json`.
32#[derive(Debug, Clone, Serialize)]
33#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
34pub struct ListPluginOutput {
35    pub name: String,
36}
37
38/// One entry point in `fallow list --entry-points --format json`.
39#[derive(Debug, Clone, Serialize)]
40#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
41pub struct ListEntryPointOutput {
42    pub path: String,
43    pub source: String,
44}
45
46/// Envelope emitted by `fallow list --boundaries --format json`. Surfaces
47/// the architecture boundary zones, rules, and the user's pre-expansion
48/// `autoDiscover` logical groups so consumers can render grouping intent that
49/// expansion would otherwise flatten out of `zones[]`.
50#[derive(Debug, Clone, Serialize)]
51#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
52#[cfg_attr(
53    feature = "schema",
54    schemars(title = "fallow list --boundaries --format json")
55)]
56pub struct ListBoundariesOutput<Status, Rule> {
57    pub boundaries: BoundariesListing<Status, Rule>,
58}
59
60/// `fallow workspaces --format json` envelope.
61#[derive(Debug, Clone, Serialize)]
62#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
63#[cfg_attr(
64    feature = "schema",
65    schemars(title = "fallow workspaces --format json")
66)]
67pub struct WorkspacesOutput<Diagnostic> {
68    /// Number of workspace package entries in `workspaces`.
69    pub workspace_count: usize,
70    /// Workspace packages discovered from package manager and tsconfig workspace
71    /// declarations. Paths are project-root-relative and use forward slashes.
72    pub workspaces: Vec<WorkspaceInfo>,
73    /// Workspace discovery diagnostics produced while reading workspace
74    /// declarations. Present for compatibility with the current wire contract,
75    /// even when empty.
76    pub workspace_diagnostics: Vec<Diagnostic>,
77}
78
79/// One workspace package emitted by `fallow workspaces --format json`.
80#[derive(Debug, Clone, Serialize)]
81#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
82pub struct WorkspaceInfo {
83    /// Package name from the workspace package.json. This is the value accepted
84    /// by `--workspace <name>`.
85    pub name: String,
86    /// Project-root-relative path to the workspace directory, normalized to
87    /// forward slashes for cross-platform JSON consumers.
88    pub path: String,
89    /// Whether the package is a generated or platform-specific dependency
90    /// package rather than a hand-authored workspace.
91    pub is_internal_dependency: bool,
92}
93
94/// `boundaries` block carried by [`ListBoundariesOutput`].
95#[derive(Debug, Clone, Serialize)]
96#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
97pub struct BoundariesListing<Status, Rule> {
98    pub configured: bool,
99    pub zone_count: usize,
100    pub zones: Vec<BoundariesListZone>,
101    pub rule_count: usize,
102    pub rules: Vec<BoundariesListRule>,
103    pub logical_group_count: usize,
104    pub logical_groups: Vec<BoundariesListLogicalGroup<Status, Rule>>,
105}
106
107/// A boundary zone after preset and `autoDiscover` expansion. Each entry
108/// classifies files into a single zone via glob patterns.
109#[derive(Debug, Clone, Serialize)]
110#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
111pub struct BoundariesListZone {
112    pub name: String,
113    pub patterns: Vec<String>,
114    pub file_count: usize,
115}
116
117/// A boundary import rule, expanded to operate on concrete child zone
118/// names after `autoDiscover` flattening. The user's pre-expansion rule
119/// (keyed on the logical parent name, if any) is preserved on the
120/// corresponding [`BoundariesListLogicalGroup::authored_rule`].
121#[derive(Debug, Clone, Serialize)]
122#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
123pub struct BoundariesListRule {
124    pub from: String,
125    pub allow: Vec<String>,
126}
127
128/// A pre-expansion `autoDiscover` logical group surfaced for observability.
129/// Captured during expansion so consumers can see the user-authored parent
130/// name and grouping intent after expansion would otherwise flatten it out of
131/// [`BoundariesListing::zones`].
132#[derive(Debug, Clone, Serialize)]
133#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
134pub struct BoundariesListLogicalGroup<Status, Rule> {
135    pub name: String,
136    pub children: Vec<String>,
137    pub auto_discover: Vec<String>,
138    pub status: Status,
139    pub source_zone_index: usize,
140    pub file_count: usize,
141    #[serde(default, skip_serializing_if = "Option::is_none")]
142    pub authored_rule: Option<Rule>,
143    #[serde(default, skip_serializing_if = "Option::is_none")]
144    pub fallback_zone: Option<String>,
145    #[serde(default, skip_serializing_if = "Option::is_none")]
146    pub merged_from: Option<Vec<usize>>,
147    #[serde(default, skip_serializing_if = "Option::is_none")]
148    pub original_zone_root: Option<String>,
149    #[serde(default, skip_serializing_if = "Vec::is_empty")]
150    pub child_source_indices: Vec<usize>,
151}
152
153/// Serialize `fallow list --boundaries --format json`.
154///
155/// # Errors
156///
157/// Returns a serde error when the list output cannot be converted to JSON.
158pub fn serialize_list_boundaries_json_output<T: Serialize>(
159    output: T,
160    mode: RootEnvelopeMode,
161) -> Result<serde_json::Value, serde_json::Error> {
162    serialize_named_json_output(output, "list-boundaries", mode)
163}
164
165/// Serialize `fallow list --workspaces --format json`.
166///
167/// # Errors
168///
169/// Returns a serde error when the list output cannot be converted to JSON.
170pub fn serialize_list_workspaces_json_output<T: Serialize>(
171    output: T,
172    mode: RootEnvelopeMode,
173) -> Result<serde_json::Value, serde_json::Error> {
174    serialize_named_json_output(output, "list-workspaces", mode)
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use serde_json::json;
181
182    #[test]
183    fn list_boundaries_json_output_uses_output_owned_root_contract() {
184        let value = serialize_list_boundaries_json_output(
185            json!({"boundaries": {}}),
186            RootEnvelopeMode::Tagged,
187        )
188        .expect("list boundaries output should serialize");
189
190        assert_eq!(value["kind"], "list-boundaries");
191    }
192
193    #[test]
194    fn list_workspaces_json_output_uses_output_owned_root_contract() {
195        let value = serialize_list_workspaces_json_output(
196            json!({"workspace_count": 0, "workspaces": []}),
197            RootEnvelopeMode::Tagged,
198        )
199        .expect("list workspaces output should serialize");
200
201        assert_eq!(value["kind"], "list-workspaces");
202    }
203}