Skip to main content

fallow_output/
list_envelopes.rs

1//! List command output envelopes.
2
3use crate::root_envelopes::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    /// Active plugins; present for `--plugins`.
12    #[serde(default, skip_serializing_if = "Option::is_none")]
13    pub plugins: Option<Vec<ListPluginOutput>>,
14    /// Number of analyzable files; present for `--files`.
15    #[serde(default, skip_serializing_if = "Option::is_none")]
16    pub file_count: Option<usize>,
17    /// Analyzable file paths relative to the root; present for `--files`.
18    #[serde(default, skip_serializing_if = "Option::is_none")]
19    pub files: Option<Vec<String>>,
20    /// Number of entry points; present for `--entry-points`.
21    #[serde(default, skip_serializing_if = "Option::is_none")]
22    pub entry_point_count: Option<usize>,
23    /// Detected entry points; present for `--entry-points`.
24    #[serde(default, skip_serializing_if = "Option::is_none")]
25    pub entry_points: Option<Vec<ListEntryPointOutput>>,
26    /// Boundary listing; present for `--boundaries`.
27    #[serde(default, skip_serializing_if = "Option::is_none")]
28    pub boundaries: Option<Boundaries>,
29    /// Startup import weight per runtime entry point; present for
30    /// `--entry-weight`.
31    #[serde(default, skip_serializing_if = "Option::is_none")]
32    pub entry_weight: Option<crate::EntryWeightListing>,
33    /// Number of workspace packages; present for `--workspaces`.
34    #[serde(default, skip_serializing_if = "Option::is_none")]
35    pub workspace_count: Option<usize>,
36    /// Workspace packages; present for `--workspaces`.
37    #[serde(default, skip_serializing_if = "Option::is_none")]
38    pub workspaces: Option<Vec<WorkspaceInfo>>,
39    /// Workspace-discovery diagnostics; present for `--workspaces`. Also
40    /// carries the plugin stage's `plugin-config-unreadable` and
41    /// `plugin-effect-not-modeled` entries when that stage ran (for
42    /// `--plugins` or `--entry-points`) and recorded one, so it is present on
43    /// such a listing even without `--workspaces`.
44    #[serde(default, skip_serializing_if = "Option::is_none")]
45    pub workspace_diagnostics: Option<Vec<Diagnostic>>,
46}
47
48/// One active plugin in `fallow list --plugins --format json`.
49#[derive(Debug, Clone, Serialize)]
50#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
51pub struct ListPluginOutput {
52    /// Plugin name, e.g. `nextjs`.
53    pub name: String,
54}
55
56/// One entry point in `fallow list --entry-points --format json`.
57#[derive(Debug, Clone, Serialize)]
58#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
59pub struct ListEntryPointOutput {
60    /// File path relative to the analysed root.
61    pub path: String,
62    /// What declared the entry point, e.g. a plugin or config pattern.
63    pub source: String,
64}
65
66/// Envelope emitted by `fallow list --boundaries --format json`. Surfaces
67/// the architecture boundary zones, rules, and the user's pre-expansion
68/// `autoDiscover` logical groups so consumers can render grouping intent that
69/// expansion would otherwise flatten out of `zones[]`.
70#[derive(Debug, Clone, Serialize)]
71#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
72#[cfg_attr(
73    feature = "schema",
74    schemars(title = "fallow list --boundaries --format json")
75)]
76pub struct ListBoundariesOutput<Status, Rule> {
77    /// Boundary zones, rules, and pre-expansion logical groups.
78    pub boundaries: BoundariesListing<Status, Rule>,
79}
80
81/// `fallow workspaces --format json` envelope.
82#[derive(Debug, Clone, Serialize)]
83#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
84#[cfg_attr(
85    feature = "schema",
86    schemars(title = "fallow workspaces --format json")
87)]
88pub struct WorkspacesOutput<Diagnostic> {
89    /// Number of workspace package entries in `workspaces`.
90    pub workspace_count: usize,
91    /// Workspace packages discovered from package manager and tsconfig workspace
92    /// declarations. Paths are project-root-relative and use forward slashes.
93    pub workspaces: Vec<WorkspaceInfo>,
94    /// Workspace discovery diagnostics produced while reading workspace
95    /// declarations. Paths are project-root-relative and use forward slashes,
96    /// like `workspaces[].path` and like the `workspace_diagnostics[]` array on
97    /// the analysis envelopes. Present for compatibility with the current wire
98    /// contract, even when empty.
99    pub workspace_diagnostics: Vec<Diagnostic>,
100}
101
102/// One workspace package emitted by `fallow workspaces --format json`.
103#[derive(Debug, Clone, Serialize)]
104#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
105pub struct WorkspaceInfo {
106    /// Package name from the workspace package.json. This is the value accepted
107    /// by `--workspace <name>`.
108    pub name: String,
109    /// Project-root-relative path to the workspace directory, normalized to
110    /// forward slashes for cross-platform JSON consumers.
111    pub path: String,
112    /// Whether the package is a generated or platform-specific dependency
113    /// package rather than a hand-authored workspace.
114    pub is_internal_dependency: bool,
115}
116
117/// `boundaries` block carried by [`ListBoundariesOutput`].
118#[derive(Debug, Clone, Serialize)]
119#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
120pub struct BoundariesListing<Status, Rule> {
121    /// Whether the project configures architecture boundaries at all.
122    pub configured: bool,
123    /// Number of entries in `zones`.
124    pub zone_count: usize,
125    /// Boundary zones after preset and `autoDiscover` expansion.
126    pub zones: Vec<BoundariesListZone>,
127    /// Number of entries in `rules`.
128    pub rule_count: usize,
129    /// Import rules operating on expanded zone names.
130    pub rules: Vec<BoundariesListRule>,
131    /// Number of entries in `logical_groups`.
132    pub logical_group_count: usize,
133    /// Pre-expansion `autoDiscover` logical groups.
134    pub logical_groups: Vec<BoundariesListLogicalGroup<Status, Rule>>,
135}
136
137/// A boundary zone after preset and `autoDiscover` expansion. Each entry
138/// classifies files into a single zone via glob patterns.
139#[derive(Debug, Clone, Serialize)]
140#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
141pub struct BoundariesListZone {
142    /// Zone name referenced by rules.
143    pub name: String,
144    /// Glob patterns that classify files into the zone.
145    pub patterns: Vec<String>,
146    /// Number of analyzable files the zone matched.
147    pub file_count: usize,
148}
149
150/// A boundary import rule, expanded to operate on concrete child zone
151/// names after `autoDiscover` flattening. The user's pre-expansion rule
152/// (keyed on the logical parent name, if any) is preserved on the
153/// corresponding [`BoundariesListLogicalGroup::authored_rule`].
154#[derive(Debug, Clone, Serialize)]
155#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
156pub struct BoundariesListRule {
157    /// Zone the rule constrains imports from.
158    pub from: String,
159    /// Zone names the `from` zone may import.
160    pub allow: Vec<String>,
161}
162
163/// A pre-expansion `autoDiscover` logical group surfaced for observability.
164/// Captured during expansion so consumers can see the user-authored parent
165/// name and grouping intent after expansion would otherwise flatten it out of
166/// [`BoundariesListing::zones`].
167#[derive(Debug, Clone, Serialize)]
168#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
169pub struct BoundariesListLogicalGroup<Status, Rule> {
170    /// User-authored parent zone name.
171    pub name: String,
172    /// Child zone names produced by discovery.
173    pub children: Vec<String>,
174    /// Authored `autoDiscover` paths.
175    pub auto_discover: Vec<String>,
176    /// Discovery outcome (ok / empty / invalid path).
177    pub status: Status,
178    /// Index of the authored entry in the pre-expansion `zones[]` config.
179    pub source_zone_index: usize,
180    /// Files matched across the group's zones.
181    pub file_count: usize,
182    /// User's pre-expansion rule keyed on the parent name, when authored.
183    #[serde(default, skip_serializing_if = "Option::is_none")]
184    pub authored_rule: Option<Rule>,
185    /// Zone that keeps the parent's own patterns when the parent kept any.
186    #[serde(default, skip_serializing_if = "Option::is_none")]
187    pub fallback_zone: Option<String>,
188    /// `zones[]` indices of duplicate parents merged into this group.
189    #[serde(default, skip_serializing_if = "Option::is_none")]
190    pub merged_from: Option<Vec<usize>>,
191    /// Authored parent `root`, when one was declared.
192    #[serde(default, skip_serializing_if = "Option::is_none")]
193    pub original_zone_root: Option<String>,
194    /// Per-child indices into the pre-expansion `zones[]` config.
195    #[serde(default, skip_serializing_if = "Vec::is_empty")]
196    pub child_source_indices: Vec<usize>,
197}
198
199/// Serialize `fallow list --boundaries --format json`.
200///
201/// # Errors
202///
203/// Returns a serde error when the list output cannot be converted to JSON.
204pub fn serialize_list_boundaries_json_output<T: Serialize>(
205    output: T,
206) -> Result<serde_json::Value, serde_json::Error> {
207    serialize_named_json_output(output, "list-boundaries")
208}
209
210/// Serialize `fallow list --workspaces --format json`.
211///
212/// # Errors
213///
214/// Returns a serde error when the list output cannot be converted to JSON.
215pub fn serialize_list_workspaces_json_output<T: Serialize>(
216    output: T,
217) -> Result<serde_json::Value, serde_json::Error> {
218    serialize_named_json_output(output, "list-workspaces")
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224    use serde_json::json;
225
226    #[test]
227    fn list_boundaries_json_output_uses_output_owned_root_contract() {
228        let value = serialize_list_boundaries_json_output(json!({"boundaries": {}}))
229            .expect("list boundaries output should serialize");
230
231        assert_eq!(value["kind"], "list-boundaries");
232    }
233
234    #[test]
235    fn list_workspaces_json_output_uses_output_owned_root_contract() {
236        let value =
237            serialize_list_workspaces_json_output(json!({"workspace_count": 0, "workspaces": []}))
238                .expect("list workspaces output should serialize");
239
240        assert_eq!(value["kind"], "list-workspaces");
241    }
242}