1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
//! Workspace-level shared defaults for multi-crate alef workspaces.
//!
//! A `[workspace]` section in `alef.toml` collects defaults that apply to every
//! `[[crates]]` entry unless that crate overrides the field. The fields here
//! are the cross-crate concerns (tooling, DTO style, default pipelines, output
//! templates) — anything that is fundamentally per-crate (sources, language
//! module names, publish settings) lives on [`crate::core::config::raw_crate::RawCrateConfig`]
//! instead.
//!
//! See `crates/alef-core/src/config/resolved.rs` for how workspace defaults
//! merge into a per-crate [`crate::core::config::resolved::ResolvedCrateConfig`].
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use super::GenerateConfig;
use super::dto::DtoConfig;
use super::extras::Language;
use super::languages::{
CSharpConfig, DartConfig, ElixirConfig, FfiConfig, GleamConfig, GoConfig, JavaConfig, JniConfig,
KotlinAndroidConfig, KotlinConfig, NodeConfig, PhpConfig, PythonConfig, RConfig, RubyConfig, SwiftConfig,
WasmConfig, ZigConfig,
};
use super::output::{
CitationConfig, DocsConfig, GeneratedHeaderConfig, OutputTemplate, ScaffoldConfig, SyncConfig, TestConfig,
};
use super::ownership::OwnershipConfig;
use super::package_metadata::PackageMetadataConfig;
use super::poly::PolyConfig;
use super::tools::ToolsConfig;
/// One parameter in a [`ClientConstructorConfig`].
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ConstructorParam {
/// Parameter name as it appears in the generated function signature.
pub name: String,
/// Rust type of the parameter (e.g. `"*const c_char"` for FFI, `"&str"` for Rust-embedded).
#[serde(rename = "type")]
pub ty: String,
}
/// Custom constructor configuration for an opaque handle type.
///
/// When present under `[workspace.client_constructors.<TypeName>]`, every
/// backend that wraps the type in an opaque handle emits a constructor whose
/// body is the `body` template string with `{type_name}` and `{source_path}`
/// substituted.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ClientConstructorConfig {
/// Ordered list of constructor parameters.
#[serde(default)]
pub params: Vec<ConstructorParam>,
/// Body template. Use `{type_name}` for the bare type name and
/// `{source_path}` for the fully-qualified core path.
pub body: String,
/// Error type returned by the constructor (`Result<Self, ErrType>`).
/// Defaults to `String` when absent.
#[serde(default)]
pub error_type: Option<String>,
}
/// Workspace-level configuration shared across all `[[crates]]` entries.
///
/// Every field is optional; an empty `[workspace]` section is valid and means
/// every crate uses Alef's built-in defaults (or its own per-crate values).
///
/// Resolution rule (highest priority first):
/// 1. Per-crate value on `[[crates]]`.
/// 2. Workspace default on `[workspace]`.
/// 3. Built-in default (compiled into Alef).
#[derive(Debug, Clone, Default, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct WorkspaceConfig {
/// Pinned alef CLI version (e.g. `"0.13.0"`). Used by the `install-alef`
/// helper to install the exact version this workspace expects.
///
/// In the legacy single-crate schema this lived at `version` at the top
/// level. The new schema renames it to `[workspace] alef_version` so it
/// can never collide with any per-crate version field.
#[serde(default)]
pub alef_version: Option<String>,
/// Opt in to letting a clean, newer-than-pin alef build rewrite the `alef_version` pin above
/// automatically during `alef all`/`alef generate`/`alef scaffold`. Defaults to `false`:
/// version-pin synchronization stays an explicit release operation unless a project turns
/// this on. See [`crate::cli::version_pin::maybe_update_alef_toml_version_pin`] for the full
/// set of additional safety conditions this toggle does not bypass (the pin must still be
/// present, parse as semver, be strictly older than the running CLI, and the running build
/// must be stamped clean) -- turning this on does not make an unsafe rewrite safe, it only
/// allows the already-guarded rewrite to run unattended. ~keep
#[serde(default)]
pub auto_update_alef_version: bool,
/// Default list of target languages for crates that do not specify their
/// own. A per-crate `languages` array overrides this entirely.
#[serde(default)]
pub languages: Vec<Language>,
/// Per-target build opt-out toggles, shared across all crates.
///
/// Keys are canonical target families — `linux_x64`, `linux_arm64`,
/// `linux_x64_musl`, `linux_arm64_musl`, `mac_intel`, `mac_arm`,
/// `windows_x64`, `windows_arm64` (see
/// [`crate::publish::platform::CANONICAL_TARGET_KEYS`]). Setting a key to
/// `false` drops every matching target triple from every language's
/// generated target list (napi platforms, elixir nif targets, C# RIDs,
/// ruby cross-platforms, dart native RIDs). Absent keys default to enabled,
/// so an empty table leaves generated output unchanged. A per-crate
/// `[[crates]] targets` table overrides individual keys.
#[serde(default)]
pub targets: std::collections::BTreeMap<String, bool>,
/// Default Python backend settings.
#[serde(default)]
pub python: Option<PythonConfig>,
/// Default Node/N-API backend settings.
#[serde(default)]
pub node: Option<NodeConfig>,
/// Default Ruby/Magnus backend settings.
#[serde(default)]
pub ruby: Option<RubyConfig>,
/// Default PHP backend settings.
#[serde(default)]
pub php: Option<PhpConfig>,
/// Default Elixir/Rustler backend settings.
#[serde(default)]
pub elixir: Option<ElixirConfig>,
/// Default WASM backend settings.
#[serde(default)]
pub wasm: Option<WasmConfig>,
/// Default C FFI backend settings.
#[serde(default)]
pub ffi: Option<FfiConfig>,
/// Default Go backend settings.
#[serde(default)]
pub go: Option<GoConfig>,
/// Default Java backend settings.
#[serde(default)]
pub java: Option<JavaConfig>,
/// Default Dart backend settings.
#[serde(default)]
pub dart: Option<DartConfig>,
/// Default Kotlin backend settings.
#[serde(default)]
pub kotlin: Option<KotlinConfig>,
/// Default Kotlin Android backend settings.
#[serde(default)]
pub kotlin_android: Option<KotlinAndroidConfig>,
/// Default JNI backend settings.
#[serde(default)]
pub jni: Option<JniConfig>,
/// Default Swift backend settings.
#[serde(default)]
pub swift: Option<SwiftConfig>,
/// Default Gleam backend settings.
#[serde(default)]
pub gleam: Option<GleamConfig>,
/// Default C# backend settings.
#[serde(default)]
pub csharp: Option<CSharpConfig>,
/// Default R/extendr backend settings.
#[serde(default)]
pub r: Option<RConfig>,
/// Default Zig backend settings.
#[serde(default)]
pub zig: Option<ZigConfig>,
/// Global package-manager and dev-tool preferences. Inherited by every
/// crate; cannot be overridden per-crate today.
#[serde(default)]
pub tools: ToolsConfig,
/// Default DTO/type generation styles per language. A per-crate `[crates.dto]`
/// table replaces this wholesale (no field-level merge).
#[serde(default)]
pub dto: DtoConfig,
/// Default generation-pass flags (which passes alef runs).
#[serde(default)]
pub generate: GenerateConfig,
/// Default per-language generation flag overrides. Merged with per-crate
/// `generate_overrides` by language key: per-crate keys win wholesale;
/// missing keys fall through to this map.
#[serde(default)]
pub generate_overrides: HashMap<String, GenerateConfig>,
/// Per-language output path templates with `{crate}` and `{lang}` placeholders.
/// A per-crate explicit `[crates.output]` path always wins over the template.
#[serde(default)]
pub output_template: OutputTemplate,
/// Default package metadata for generated manifests and README context.
/// Per-crate `[scaffold]` values override this field-by-field.
#[serde(default)]
pub scaffold: Option<ScaffoldConfig>,
/// Centralized package metadata for generated language manifests.
/// Per-crate `[crates.package_metadata]` values override this field-by-field.
#[serde(default)]
pub package_metadata: Option<PackageMetadataConfig>,
/// Default generated-file header metadata.
/// Per-crate `[scaffold.generated_header]` values override this field-by-field.
#[serde(default)]
pub generated_header: Option<GeneratedHeaderConfig>,
/// Default test pipeline keyed by language code.
///
/// The only remaining per-command override table in `alef.toml`: 0.82.0 removed
/// `lint`/`setup`/`update`/`clean`/`build_commands` (alef now owns those commands
/// end to end), but `test.e2e` has no code default in any language but Dart, so a
/// consumer-supplied e2e command stays configurable here.
#[serde(default)]
pub test: HashMap<String, TestConfig>,
/// Workspace-wide opaque types — types from external crates that alef can't
/// extract. Map of type name → fully-qualified Rust path. These get opaque
/// wrapper structs across all language backends, in every crate that
/// references them.
#[serde(default)]
pub opaque_types: HashMap<String, String>,
/// Per-type custom constructors emitted by every backend that supports
/// opaque handles. Key: type name (e.g. `"DefaultClient"`).
/// Value: [`ClientConstructorConfig`] describing params and a body template.
#[serde(default)]
pub client_constructors: HashMap<String, ClientConstructorConfig>,
/// Workspace-wide version sync rules. A per-crate publish step still runs
/// independently per crate; sync rules in this section apply globally.
#[serde(default)]
pub sync: Option<SyncConfig>,
/// Optional CITATION.cff metadata. When present, `alef sync-versions` writes
/// a fully rendered `CITATION.cff` at the repo root using these fields plus
/// the canonical workspace version. When absent, a hand-authored
/// CITATION.cff (if any) only has its `version:` line updated.
#[serde(default)]
pub citation: Option<CitationConfig>,
/// Default template-driven docs generation config. Per-crate `[crates.docs]`
/// values override this field-by-field.
#[serde(default)]
pub docs: Option<DocsConfig>,
/// Repository-level `poly.toml` customisations merged into the emitter's
/// generated output. Because `poly.toml` is regenerated on every `alef`
/// run, any per-repo lint suppressions must live here to survive regen.
///
/// See [`PolyConfig`] for the full set of configurable knobs.
#[serde(default)]
pub poly: PolyConfig,
/// Extra clippy lints to allow in every generated Rust binding file, merged
/// (union, de-duplicated) with each backend's built-in default allow-list.
///
/// Entries may be bare lint names (`"single_match"`) or `clippy::`-prefixed
/// (`"clippy::single_match"`); both forms are accepted and normalised
/// internally. When absent or empty the emitted allow-list is byte-identical
/// to the backend default — no diff in consumers that do not set this field.
///
/// Example:
/// ```toml
/// [workspace]
/// extra_clippy_allows = ["single_match", "collapsible_match"]
/// ```
#[serde(default)]
pub extra_clippy_allows: Vec<String>,
/// Consumer-declared ownership dispositions for generated paths -- currently just
/// `user_owned`, the list of repo-relative globs naming paths this repository maintains by
/// hand and alef must never write over. See [`OwnershipConfig`] for the full contract and
/// for why it is not `[crates.verify] ignore_ephemeral` or an `exclude_*` knob.
///
/// Workspace-level rather than per-crate, unlike `[crates.verify]`: the patterns are
/// repo-relative, and the write guards that consult them see a path and a `base_dir` with
/// no notion of which crate emitted it. Keying the declaration per crate would require
/// every writer to re-derive that attribution, which is the "one fact, two derivations"
/// shape this repository's guards exist to avoid. ~keep
#[serde(default)]
pub ownership: OwnershipConfig,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn workspace_config_deserializes_empty() {
let cfg: WorkspaceConfig = toml::from_str("").unwrap();
assert!(cfg.alef_version.is_none());
assert!(
!cfg.auto_update_alef_version,
"auto_update_alef_version must default to off"
);
assert!(cfg.languages.is_empty());
assert!(cfg.test.is_empty());
assert!(cfg.opaque_types.is_empty());
assert!(cfg.sync.is_none());
}
#[test]
fn workspace_config_deserializes_auto_update_alef_version_opt_in() {
let cfg: WorkspaceConfig = toml::from_str("auto_update_alef_version = true\n").unwrap();
assert!(cfg.auto_update_alef_version);
}
#[test]
fn workspace_config_deserializes_full() {
let toml_str = r#"
alef_version = "0.13.0"
languages = ["python", "node"]
[output_template]
python = "packages/python/{crate}/"
node = "packages/node/{crate}/"
[test.python]
command = "uv run --no-sync pytest"
[opaque_types]
Tree = "tree_sitter::Tree"
"#;
let cfg: WorkspaceConfig = toml::from_str(toml_str).unwrap();
assert_eq!(cfg.alef_version.as_deref(), Some("0.13.0"));
assert_eq!(cfg.languages.len(), 2);
assert_eq!(cfg.output_template.python.as_deref(), Some("packages/python/{crate}/"));
assert!(cfg.test.contains_key("python"));
assert_eq!(
cfg.opaque_types.get("Tree").map(String::as_str),
Some("tree_sitter::Tree")
);
}
/// 0.82.0 removed `lint`/`setup`/`update`/`clean`/`build_commands` from `[workspace]`: a
/// leftover `[workspace.lint.<lang>]` in a consumer's `alef.toml` must now be a parse error
/// (`deny_unknown_fields`), not a silently-ignored table. Ran against `main` before that
/// removal, this exact fixture parsed cleanly (see the now-deleted
/// `workspace_config_deserializes_full`/`full_alef_toml_with_lint_and_update` assertions on
/// `cfg.lint`), which is the "watch it fail before your change" half of proving this check
/// actually fires. ~keep
#[test]
fn workspace_config_rejects_removed_lint_table() {
let toml_str = r#"
languages = ["python"]
[lint.python]
check = "ruff check ."
"#;
let err = toml::from_str::<WorkspaceConfig>(toml_str).expect_err("[workspace.lint] must no longer parse");
let message = err.to_string();
assert!(
message.contains("lint"),
"error should name the removed `lint` field: {message}"
);
}
/// Same proof for the other four removed tables, in one config so a regression in any one of
/// them is caught without five near-duplicate tests.
#[test]
fn workspace_config_rejects_removed_setup_update_clean_build_commands_tables() {
for (key, toml_snippet) in [
("setup", "[setup.python]\ninstall = \"uv sync\"\n"),
("update", "[update.python]\nupdate = \"uv sync --upgrade\"\n"),
("clean", "[clean.python]\nclean = \"rm -rf dist\"\n"),
(
"build_commands",
"[build_commands.python]\nbuild = \"maturin develop\"\n",
),
] {
let toml_str = format!("languages = [\"python\"]\n{toml_snippet}");
let err = toml::from_str::<WorkspaceConfig>(&toml_str)
.expect_err(&format!("[workspace.{key}] must no longer parse"));
assert!(
err.to_string().contains(key),
"error should name the removed `{key}` field: {err}"
);
}
}
#[test]
fn workspace_config_deserializes_client_constructors() {
let toml_str = r#"
[client_constructors.DefaultClient]
body = "{source_path}::new().map_err(|e| e.to_string())"
[[client_constructors.DefaultClient.params]]
name = "api_key"
type = "*const std::ffi::c_char"
"#;
let cfg: WorkspaceConfig = toml::from_str(toml_str).unwrap();
let ctor = cfg.client_constructors.get("DefaultClient").unwrap();
assert_eq!(ctor.params.len(), 1);
assert_eq!(ctor.params[0].name, "api_key");
assert_eq!(ctor.params[0].ty, "*const std::ffi::c_char");
assert!(ctor.body.contains("{source_path}"));
}
}