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