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, CitationConfig, CleanConfig, GeneratedHeaderConfig, LintConfig, OutputTemplate,
20    PrecommitConfig, 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    /// Optional CITATION.cff metadata. When present, `alef sync-versions` writes
141    /// a fully rendered `CITATION.cff` at the repo root using these fields plus
142    /// the canonical workspace version. When absent, a hand-authored
143    /// CITATION.cff (if any) only has its `version:` line updated.
144    #[serde(default)]
145    pub citation: Option<CitationConfig>,
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151
152    #[test]
153    fn workspace_config_deserializes_empty() {
154        let cfg: WorkspaceConfig = toml::from_str("").unwrap();
155        assert!(cfg.alef_version.is_none());
156        assert!(cfg.languages.is_empty());
157        assert!(cfg.lint.is_empty());
158        assert!(cfg.opaque_types.is_empty());
159        assert!(cfg.sync.is_none());
160    }
161
162    #[test]
163    fn workspace_config_deserializes_full() {
164        let toml_str = r#"
165alef_version = "0.13.0"
166languages = ["python", "node"]
167
168[output_template]
169python = "packages/python/{crate}/"
170node   = "packages/node/{crate}/"
171
172[lint.python]
173precondition = "command -v ruff >/dev/null 2>&1"
174check        = "ruff check ."
175
176[test.python]
177command = "uv run pytest"
178
179[opaque_types]
180Tree = "tree_sitter::Tree"
181"#;
182        let cfg: WorkspaceConfig = toml::from_str(toml_str).unwrap();
183        assert_eq!(cfg.alef_version.as_deref(), Some("0.13.0"));
184        assert_eq!(cfg.languages.len(), 2);
185        assert_eq!(cfg.output_template.python.as_deref(), Some("packages/python/{crate}/"));
186        assert!(cfg.lint.contains_key("python"));
187        assert!(cfg.test.contains_key("python"));
188        assert_eq!(
189            cfg.opaque_types.get("Tree").map(String::as_str),
190            Some("tree_sitter::Tree")
191        );
192    }
193}