alef_core/config/workspace.rs
1//! Workspace-level shared defaults for multi-crate alef workspaces.
2//!
3//! A `[workspace]` section in `alef.toml` collects defaults that apply to every
4//! `[[crates]]` entry unless that crate overrides the field. The fields here
5//! are the cross-crate concerns (tooling, DTO style, default pipelines, output
6//! templates) — anything that is fundamentally per-crate (sources, language
7//! module names, publish settings) lives on [`crate::config::raw_crate::RawCrateConfig`]
8//! instead.
9//!
10//! See `crates/alef-core/src/config/resolved.rs` for how workspace defaults
11//! merge into a per-crate [`crate::config::resolved::ResolvedCrateConfig`].
12
13use serde::{Deserialize, Serialize};
14use std::collections::HashMap;
15
16use super::dto::DtoConfig;
17use super::extras::Language;
18use super::output::{
19 BuildCommandConfig, CleanConfig, GeneratedHeaderConfig, LintConfig, OutputTemplate, PrecommitConfig,
20 ScaffoldConfig, SetupConfig, SyncConfig, TestConfig, UpdateConfig,
21};
22use super::tools::ToolsConfig;
23use super::{FormatConfig, GenerateConfig};
24
25/// Workspace-level configuration shared across all `[[crates]]` entries.
26///
27/// Every field is optional; an empty `[workspace]` section is valid and means
28/// every crate uses Alef's built-in defaults (or its own per-crate values).
29///
30/// Resolution rule (highest priority first):
31/// 1. Per-crate value on `[[crates]]`.
32/// 2. Workspace default on `[workspace]`.
33/// 3. Built-in default (compiled into Alef).
34#[derive(Debug, Clone, Default, Serialize, Deserialize)]
35#[serde(deny_unknown_fields)]
36pub struct WorkspaceConfig {
37 /// Pinned alef CLI version (e.g. `"0.13.0"`). Used by the `install-alef`
38 /// helper to install the exact version this workspace expects.
39 ///
40 /// In the legacy single-crate schema this lived at `version` at the top
41 /// level. The new schema renames it to `[workspace] alef_version` so it
42 /// can never collide with any per-crate version field.
43 #[serde(default)]
44 pub alef_version: Option<String>,
45
46 /// Default list of target languages for crates that do not specify their
47 /// own. A per-crate `languages` array overrides this entirely.
48 #[serde(default)]
49 pub languages: Vec<Language>,
50
51 /// Global package-manager and dev-tool preferences. Inherited by every
52 /// crate; cannot be overridden per-crate today.
53 #[serde(default)]
54 pub tools: ToolsConfig,
55
56 /// Default DTO/type generation styles per language. A per-crate `[crates.dto]`
57 /// table replaces this wholesale (no field-level merge).
58 #[serde(default)]
59 pub dto: DtoConfig,
60
61 /// Default post-generation formatting flags. A per-crate value replaces
62 /// this wholesale.
63 #[serde(default)]
64 pub format: FormatConfig,
65
66 /// Default per-language formatting overrides (e.g., disable `mix format`
67 /// for elixir). Merged with per-crate `format_overrides` by language key:
68 /// per-crate keys win wholesale; missing keys fall through to this map.
69 /// Note: there is no field-level merge inside a single `FormatConfig`.
70 #[serde(default)]
71 pub format_overrides: HashMap<String, FormatConfig>,
72
73 /// Default generation-pass flags (which passes alef runs).
74 #[serde(default)]
75 pub generate: GenerateConfig,
76
77 /// Default per-language generation flag overrides. Merged with per-crate
78 /// `generate_overrides` by language key: per-crate keys win wholesale;
79 /// missing keys fall through to this map.
80 #[serde(default)]
81 pub generate_overrides: HashMap<String, GenerateConfig>,
82
83 /// Per-language output path templates with `{crate}` and `{lang}` placeholders.
84 /// A per-crate explicit `[crates.output]` path always wins over the template.
85 #[serde(default)]
86 pub output_template: OutputTemplate,
87
88 /// Default package metadata for generated manifests and README context.
89 /// Per-crate `[scaffold]` values override this field-by-field.
90 #[serde(default)]
91 pub scaffold: Option<ScaffoldConfig>,
92
93 /// Default generated-file header metadata.
94 /// Per-crate `[scaffold.generated_header]` values override this field-by-field.
95 #[serde(default)]
96 pub generated_header: Option<GeneratedHeaderConfig>,
97
98 /// Default pre-commit scaffold metadata.
99 /// Per-crate `[scaffold.precommit]` values override this field-by-field.
100 #[serde(default)]
101 pub precommit: Option<PrecommitConfig>,
102
103 /// Default lint pipeline keyed by language code (`"python"`, `"node"`, …).
104 /// Merged field-wise with per-crate `[crates.lint.<lang>]`.
105 #[serde(default)]
106 pub lint: HashMap<String, LintConfig>,
107
108 /// Default test pipeline keyed by language code.
109 #[serde(default)]
110 pub test: HashMap<String, TestConfig>,
111
112 /// Default setup pipeline keyed by language code.
113 #[serde(default)]
114 pub setup: HashMap<String, SetupConfig>,
115
116 /// Default update pipeline keyed by language code.
117 #[serde(default)]
118 pub update: HashMap<String, UpdateConfig>,
119
120 /// Default clean pipeline keyed by language code.
121 #[serde(default)]
122 pub clean: HashMap<String, CleanConfig>,
123
124 /// Default build pipeline keyed by language code.
125 #[serde(default)]
126 pub build_commands: HashMap<String, BuildCommandConfig>,
127
128 /// Workspace-wide opaque types — types from external crates that alef can't
129 /// extract. Map of type name → fully-qualified Rust path. These get opaque
130 /// wrapper structs across all language backends, in every crate that
131 /// references them.
132 #[serde(default)]
133 pub opaque_types: HashMap<String, String>,
134
135 /// Workspace-wide version sync rules. A per-crate publish step still runs
136 /// independently per crate; sync rules in this section apply globally.
137 #[serde(default)]
138 pub sync: Option<SyncConfig>,
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn workspace_config_deserializes_empty() {
147 let cfg: WorkspaceConfig = toml::from_str("").unwrap();
148 assert!(cfg.alef_version.is_none());
149 assert!(cfg.languages.is_empty());
150 assert!(cfg.lint.is_empty());
151 assert!(cfg.opaque_types.is_empty());
152 assert!(cfg.sync.is_none());
153 }
154
155 #[test]
156 fn workspace_config_deserializes_full() {
157 let toml_str = r#"
158alef_version = "0.13.0"
159languages = ["python", "node"]
160
161[output_template]
162python = "packages/python/{crate}/"
163node = "packages/node/{crate}/"
164
165[lint.python]
166precondition = "command -v ruff >/dev/null 2>&1"
167check = "ruff check ."
168
169[test.python]
170command = "uv run pytest"
171
172[opaque_types]
173Tree = "tree_sitter::Tree"
174"#;
175 let cfg: WorkspaceConfig = toml::from_str(toml_str).unwrap();
176 assert_eq!(cfg.alef_version.as_deref(), Some("0.13.0"));
177 assert_eq!(cfg.languages.len(), 2);
178 assert_eq!(cfg.output_template.python.as_deref(), Some("packages/python/{crate}/"));
179 assert!(cfg.lint.contains_key("python"));
180 assert!(cfg.test.contains_key("python"));
181 assert_eq!(
182 cfg.opaque_types.get("Tree").map(String::as_str),
183 Some("tree_sitter::Tree")
184 );
185 }
186}