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}