Skip to main content

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}