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, CitationConfig, CleanConfig, GeneratedHeaderConfig, LintConfig, OutputTemplate,
20 PrecommitConfig, ScaffoldConfig, SetupConfig, SyncConfig, TestConfig, UpdateConfig,
21};
22use super::tools::ToolsConfig;
23use super::{FormatConfig, GenerateConfig};
24
25/// One parameter in a [`ClientConstructorConfig`].
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct ConstructorParam {
28 /// Parameter name as it appears in the generated function signature.
29 pub name: String,
30 /// Rust type of the parameter (e.g. `"*const c_char"` for FFI, `"&str"` for Rust-embedded).
31 #[serde(rename = "type")]
32 pub ty: String,
33}
34
35/// Custom constructor configuration for an opaque handle type.
36///
37/// When present under `[workspace.client_constructors.<TypeName>]`, every
38/// backend that wraps the type in an opaque handle emits a constructor whose
39/// body is the `body` template string with `{type_name}` and `{source_path}`
40/// substituted.
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct ClientConstructorConfig {
43 /// Ordered list of constructor parameters.
44 #[serde(default)]
45 pub params: Vec<ConstructorParam>,
46 /// Body template. Use `{type_name}` for the bare type name and
47 /// `{source_path}` for the fully-qualified core path.
48 pub body: String,
49 /// Error type returned by the constructor (`Result<Self, ErrType>`).
50 /// Defaults to `String` when absent.
51 #[serde(default)]
52 pub error_type: Option<String>,
53}
54
55/// Workspace-level configuration shared across all `[[crates]]` entries.
56///
57/// Every field is optional; an empty `[workspace]` section is valid and means
58/// every crate uses Alef's built-in defaults (or its own per-crate values).
59///
60/// Resolution rule (highest priority first):
61/// 1. Per-crate value on `[[crates]]`.
62/// 2. Workspace default on `[workspace]`.
63/// 3. Built-in default (compiled into Alef).
64#[derive(Debug, Clone, Default, Serialize, Deserialize)]
65#[serde(deny_unknown_fields)]
66pub struct WorkspaceConfig {
67 /// Pinned alef CLI version (e.g. `"0.13.0"`). Used by the `install-alef`
68 /// helper to install the exact version this workspace expects.
69 ///
70 /// In the legacy single-crate schema this lived at `version` at the top
71 /// level. The new schema renames it to `[workspace] alef_version` so it
72 /// can never collide with any per-crate version field.
73 #[serde(default)]
74 pub alef_version: Option<String>,
75
76 /// Default list of target languages for crates that do not specify their
77 /// own. A per-crate `languages` array overrides this entirely.
78 #[serde(default)]
79 pub languages: Vec<Language>,
80
81 /// Global package-manager and dev-tool preferences. Inherited by every
82 /// crate; cannot be overridden per-crate today.
83 #[serde(default)]
84 pub tools: ToolsConfig,
85
86 /// Default DTO/type generation styles per language. A per-crate `[crates.dto]`
87 /// table replaces this wholesale (no field-level merge).
88 #[serde(default)]
89 pub dto: DtoConfig,
90
91 /// Default post-generation formatting flags. A per-crate value replaces
92 /// this wholesale.
93 #[serde(default)]
94 pub format: FormatConfig,
95
96 /// Default per-language formatting overrides (e.g., disable `mix format`
97 /// for elixir). Merged with per-crate `format_overrides` by language key:
98 /// per-crate keys win wholesale; missing keys fall through to this map.
99 /// Note: there is no field-level merge inside a single `FormatConfig`.
100 #[serde(default)]
101 pub format_overrides: HashMap<String, FormatConfig>,
102
103 /// Default generation-pass flags (which passes alef runs).
104 #[serde(default)]
105 pub generate: GenerateConfig,
106
107 /// Default per-language generation flag overrides. Merged with per-crate
108 /// `generate_overrides` by language key: per-crate keys win wholesale;
109 /// missing keys fall through to this map.
110 #[serde(default)]
111 pub generate_overrides: HashMap<String, GenerateConfig>,
112
113 /// Per-language output path templates with `{crate}` and `{lang}` placeholders.
114 /// A per-crate explicit `[crates.output]` path always wins over the template.
115 #[serde(default)]
116 pub output_template: OutputTemplate,
117
118 /// Default package metadata for generated manifests and README context.
119 /// Per-crate `[scaffold]` values override this field-by-field.
120 #[serde(default)]
121 pub scaffold: Option<ScaffoldConfig>,
122
123 /// Default generated-file header metadata.
124 /// Per-crate `[scaffold.generated_header]` values override this field-by-field.
125 #[serde(default)]
126 pub generated_header: Option<GeneratedHeaderConfig>,
127
128 /// Default pre-commit scaffold metadata.
129 /// Per-crate `[scaffold.precommit]` values override this field-by-field.
130 #[serde(default)]
131 pub precommit: Option<PrecommitConfig>,
132
133 /// Default lint pipeline keyed by language code (`"python"`, `"node"`, …).
134 /// Merged field-wise with per-crate `[crates.lint.<lang>]`.
135 #[serde(default)]
136 pub lint: HashMap<String, LintConfig>,
137
138 /// Default test pipeline keyed by language code.
139 #[serde(default)]
140 pub test: HashMap<String, TestConfig>,
141
142 /// Default setup pipeline keyed by language code.
143 #[serde(default)]
144 pub setup: HashMap<String, SetupConfig>,
145
146 /// Default update pipeline keyed by language code.
147 #[serde(default)]
148 pub update: HashMap<String, UpdateConfig>,
149
150 /// Default clean pipeline keyed by language code.
151 #[serde(default)]
152 pub clean: HashMap<String, CleanConfig>,
153
154 /// Default build pipeline keyed by language code.
155 #[serde(default)]
156 pub build_commands: HashMap<String, BuildCommandConfig>,
157
158 /// Workspace-wide opaque types — types from external crates that alef can't
159 /// extract. Map of type name → fully-qualified Rust path. These get opaque
160 /// wrapper structs across all language backends, in every crate that
161 /// references them.
162 #[serde(default)]
163 pub opaque_types: HashMap<String, String>,
164
165 /// Per-type custom constructors emitted by every backend that supports
166 /// opaque handles. Key: type name (e.g. `"DefaultClient"`).
167 /// Value: [`ClientConstructorConfig`] describing params and a body template.
168 #[serde(default)]
169 pub client_constructors: HashMap<String, ClientConstructorConfig>,
170
171 /// Workspace-wide version sync rules. A per-crate publish step still runs
172 /// independently per crate; sync rules in this section apply globally.
173 #[serde(default)]
174 pub sync: Option<SyncConfig>,
175
176 /// Optional CITATION.cff metadata. When present, `alef sync-versions` writes
177 /// a fully rendered `CITATION.cff` at the repo root using these fields plus
178 /// the canonical workspace version. When absent, a hand-authored
179 /// CITATION.cff (if any) only has its `version:` line updated.
180 #[serde(default)]
181 pub citation: Option<CitationConfig>,
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187
188 #[test]
189 fn workspace_config_deserializes_empty() {
190 let cfg: WorkspaceConfig = toml::from_str("").unwrap();
191 assert!(cfg.alef_version.is_none());
192 assert!(cfg.languages.is_empty());
193 assert!(cfg.lint.is_empty());
194 assert!(cfg.opaque_types.is_empty());
195 assert!(cfg.sync.is_none());
196 }
197
198 #[test]
199 fn workspace_config_deserializes_full() {
200 let toml_str = r#"
201alef_version = "0.13.0"
202languages = ["python", "node"]
203
204[output_template]
205python = "packages/python/{crate}/"
206node = "packages/node/{crate}/"
207
208[lint.python]
209precondition = "command -v ruff >/dev/null 2>&1"
210check = "ruff check ."
211
212[test.python]
213command = "uv run pytest"
214
215[opaque_types]
216Tree = "tree_sitter::Tree"
217"#;
218 let cfg: WorkspaceConfig = toml::from_str(toml_str).unwrap();
219 assert_eq!(cfg.alef_version.as_deref(), Some("0.13.0"));
220 assert_eq!(cfg.languages.len(), 2);
221 assert_eq!(cfg.output_template.python.as_deref(), Some("packages/python/{crate}/"));
222 assert!(cfg.lint.contains_key("python"));
223 assert!(cfg.test.contains_key("python"));
224 assert_eq!(
225 cfg.opaque_types.get("Tree").map(String::as_str),
226 Some("tree_sitter::Tree")
227 );
228 }
229
230 #[test]
231 fn workspace_config_deserializes_client_constructors() {
232 let toml_str = r#"
233[client_constructors.DefaultClient]
234body = "{source_path}::new().map_err(|e| e.to_string())"
235
236[[client_constructors.DefaultClient.params]]
237name = "api_key"
238type = "*const std::ffi::c_char"
239"#;
240 let cfg: WorkspaceConfig = toml::from_str(toml_str).unwrap();
241 let ctor = cfg.client_constructors.get("DefaultClient").unwrap();
242 assert_eq!(ctor.params.len(), 1);
243 assert_eq!(ctor.params[0].name, "api_key");
244 assert_eq!(ctor.params[0].ty, "*const std::ffi::c_char");
245 assert!(ctor.body.contains("{source_path}"));
246 }
247}