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, 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}