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