Skip to main content

alef_core/config/
output.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value as JsonValue;
3use std::collections::HashMap;
4use std::path::PathBuf;
5
6#[derive(Debug, Clone, Default, Serialize, Deserialize)]
7pub struct ExcludeConfig {
8    #[serde(default)]
9    pub types: Vec<String>,
10    #[serde(default)]
11    pub functions: Vec<String>,
12    /// Exclude specific methods: "TypeName.method_name"
13    #[serde(default)]
14    pub methods: Vec<String>,
15}
16
17#[derive(Debug, Clone, Default, Serialize, Deserialize)]
18pub struct IncludeConfig {
19    #[serde(default)]
20    pub types: Vec<String>,
21    #[serde(default)]
22    pub functions: Vec<String>,
23}
24
25#[derive(Debug, Clone, Default, Serialize, Deserialize)]
26pub struct OutputConfig {
27    pub python: Option<PathBuf>,
28    pub node: Option<PathBuf>,
29    pub ruby: Option<PathBuf>,
30    pub php: Option<PathBuf>,
31    pub elixir: Option<PathBuf>,
32    pub wasm: Option<PathBuf>,
33    pub ffi: Option<PathBuf>,
34    pub go: Option<PathBuf>,
35    pub java: Option<PathBuf>,
36    pub csharp: Option<PathBuf>,
37    pub r: Option<PathBuf>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct ScaffoldConfig {
42    pub description: Option<String>,
43    pub license: Option<String>,
44    pub repository: Option<String>,
45    pub homepage: Option<String>,
46    #[serde(default)]
47    pub authors: Vec<String>,
48    #[serde(default)]
49    pub keywords: Vec<String>,
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct ReadmeConfig {
54    pub template_dir: Option<PathBuf>,
55    pub snippets_dir: Option<PathBuf>,
56    /// Deprecated: path to an external YAML config file. Prefer inline fields below.
57    pub config: Option<PathBuf>,
58    pub output_pattern: Option<String>,
59    /// Discord invite URL used in README templates.
60    pub discord_url: Option<String>,
61    /// Banner image URL used in README templates.
62    pub banner_url: Option<String>,
63    /// Per-language README configuration, keyed by language code
64    /// (e.g. "python", "typescript", "ruby"). Values are flexible JSON objects
65    /// that map directly to minijinja template context variables.
66    #[serde(default)]
67    pub languages: HashMap<String, JsonValue>,
68}
69
70/// A value that can be either a single string or a list of strings.
71///
72/// Deserializes from both `"cmd"` and `["cmd1", "cmd2"]` in TOML/JSON.
73#[derive(Debug, Clone, Serialize, Deserialize)]
74#[serde(untagged)]
75pub enum StringOrVec {
76    Single(String),
77    Multiple(Vec<String>),
78}
79
80impl StringOrVec {
81    /// Return all commands as a slice-like iterator.
82    pub fn commands(&self) -> Vec<&str> {
83        match self {
84            StringOrVec::Single(s) => vec![s.as_str()],
85            StringOrVec::Multiple(v) => v.iter().map(String::as_str).collect(),
86        }
87    }
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct LintConfig {
92    pub format: Option<StringOrVec>,
93    pub check: Option<StringOrVec>,
94    pub typecheck: Option<StringOrVec>,
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct UpdateConfig {
99    /// Command(s) for safe dependency updates (compatible versions only).
100    pub update: Option<StringOrVec>,
101    /// Command(s) for aggressive updates (including incompatible/major bumps).
102    pub upgrade: Option<StringOrVec>,
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize, Default)]
106pub struct TestConfig {
107    /// Command to run unit/integration tests for this language.
108    pub command: Option<StringOrVec>,
109    /// Command to run e2e tests for this language.
110    pub e2e: Option<StringOrVec>,
111    /// Command to run tests with coverage for this language.
112    pub coverage: Option<StringOrVec>,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
116pub struct SetupConfig {
117    /// Command(s) to install dependencies for this language.
118    pub install: Option<StringOrVec>,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct CleanConfig {
123    /// Command(s) to clean build artifacts for this language.
124    pub clean: Option<StringOrVec>,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct BuildCommandConfig {
129    /// Command(s) to build in debug mode.
130    pub build: Option<StringOrVec>,
131    /// Command(s) to build in release mode.
132    pub build_release: Option<StringOrVec>,
133}
134
135/// A single text replacement rule for version sync.
136#[derive(Debug, Clone, Serialize, Deserialize)]
137pub struct TextReplacement {
138    /// Glob pattern for files to process.
139    pub path: String,
140    /// Regex pattern to search for (may contain `{version}` placeholder).
141    pub search: String,
142    /// Replacement string (may contain `{version}` placeholder).
143    pub replace: String,
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    #[test]
151    fn string_or_vec_single_from_toml() {
152        let toml_str = r#"format = "ruff format""#;
153        #[derive(Deserialize)]
154        struct T {
155            format: StringOrVec,
156        }
157        let t: T = toml::from_str(toml_str).unwrap();
158        assert_eq!(t.format.commands(), vec!["ruff format"]);
159    }
160
161    #[test]
162    fn string_or_vec_multiple_from_toml() {
163        let toml_str = r#"format = ["cmd1", "cmd2", "cmd3"]"#;
164        #[derive(Deserialize)]
165        struct T {
166            format: StringOrVec,
167        }
168        let t: T = toml::from_str(toml_str).unwrap();
169        assert_eq!(t.format.commands(), vec!["cmd1", "cmd2", "cmd3"]);
170    }
171
172    #[test]
173    fn lint_config_backward_compat_string() {
174        let toml_str = r#"
175format = "ruff format ."
176check = "ruff check ."
177typecheck = "mypy ."
178"#;
179        let cfg: LintConfig = toml::from_str(toml_str).unwrap();
180        assert_eq!(cfg.format.unwrap().commands(), vec!["ruff format ."]);
181        assert_eq!(cfg.check.unwrap().commands(), vec!["ruff check ."]);
182        assert_eq!(cfg.typecheck.unwrap().commands(), vec!["mypy ."]);
183    }
184
185    #[test]
186    fn lint_config_array_commands() {
187        let toml_str = r#"
188format = ["cmd1", "cmd2"]
189check = "single-check"
190"#;
191        let cfg: LintConfig = toml::from_str(toml_str).unwrap();
192        assert_eq!(cfg.format.unwrap().commands(), vec!["cmd1", "cmd2"]);
193        assert_eq!(cfg.check.unwrap().commands(), vec!["single-check"]);
194        assert!(cfg.typecheck.is_none());
195    }
196
197    #[test]
198    fn lint_config_all_optional() {
199        let toml_str = "";
200        let cfg: LintConfig = toml::from_str(toml_str).unwrap();
201        assert!(cfg.format.is_none());
202        assert!(cfg.check.is_none());
203        assert!(cfg.typecheck.is_none());
204    }
205
206    #[test]
207    fn update_config_from_toml() {
208        let toml_str = r#"
209update = "cargo update"
210upgrade = ["cargo upgrade --incompatible", "cargo update"]
211"#;
212        let cfg: UpdateConfig = toml::from_str(toml_str).unwrap();
213        assert_eq!(cfg.update.unwrap().commands(), vec!["cargo update"]);
214        assert_eq!(
215            cfg.upgrade.unwrap().commands(),
216            vec!["cargo upgrade --incompatible", "cargo update"]
217        );
218    }
219
220    #[test]
221    fn update_config_all_optional() {
222        let toml_str = "";
223        let cfg: UpdateConfig = toml::from_str(toml_str).unwrap();
224        assert!(cfg.update.is_none());
225        assert!(cfg.upgrade.is_none());
226    }
227
228    #[test]
229    fn string_or_vec_empty_array_from_toml() {
230        let toml_str = "format = []";
231        #[derive(Deserialize)]
232        struct T {
233            format: StringOrVec,
234        }
235        let t: T = toml::from_str(toml_str).unwrap();
236        assert!(matches!(t.format, StringOrVec::Multiple(_)));
237        assert!(t.format.commands().is_empty());
238    }
239
240    #[test]
241    fn string_or_vec_single_element_array_from_toml() {
242        let toml_str = r#"format = ["cmd"]"#;
243        #[derive(Deserialize)]
244        struct T {
245            format: StringOrVec,
246        }
247        let t: T = toml::from_str(toml_str).unwrap();
248        assert_eq!(t.format.commands(), vec!["cmd"]);
249    }
250
251    #[test]
252    fn setup_config_single_string() {
253        let toml_str = r#"install = "uv sync""#;
254        let cfg: SetupConfig = toml::from_str(toml_str).unwrap();
255        assert_eq!(cfg.install.unwrap().commands(), vec!["uv sync"]);
256    }
257
258    #[test]
259    fn setup_config_array_commands() {
260        let toml_str = r#"install = ["step1", "step2"]"#;
261        let cfg: SetupConfig = toml::from_str(toml_str).unwrap();
262        assert_eq!(cfg.install.unwrap().commands(), vec!["step1", "step2"]);
263    }
264
265    #[test]
266    fn setup_config_all_optional() {
267        let toml_str = "";
268        let cfg: SetupConfig = toml::from_str(toml_str).unwrap();
269        assert!(cfg.install.is_none());
270    }
271
272    #[test]
273    fn clean_config_single_string() {
274        let toml_str = r#"clean = "rm -rf dist""#;
275        let cfg: CleanConfig = toml::from_str(toml_str).unwrap();
276        assert_eq!(cfg.clean.unwrap().commands(), vec!["rm -rf dist"]);
277    }
278
279    #[test]
280    fn clean_config_array_commands() {
281        let toml_str = r#"clean = ["step1", "step2"]"#;
282        let cfg: CleanConfig = toml::from_str(toml_str).unwrap();
283        assert_eq!(cfg.clean.unwrap().commands(), vec!["step1", "step2"]);
284    }
285
286    #[test]
287    fn clean_config_all_optional() {
288        let toml_str = "";
289        let cfg: CleanConfig = toml::from_str(toml_str).unwrap();
290        assert!(cfg.clean.is_none());
291    }
292
293    #[test]
294    fn build_command_config_single_strings() {
295        let toml_str = r#"
296build = "cargo build"
297build_release = "cargo build --release"
298"#;
299        let cfg: BuildCommandConfig = toml::from_str(toml_str).unwrap();
300        assert_eq!(cfg.build.unwrap().commands(), vec!["cargo build"]);
301        assert_eq!(cfg.build_release.unwrap().commands(), vec!["cargo build --release"]);
302    }
303
304    #[test]
305    fn build_command_config_array_commands() {
306        let toml_str = r#"
307build = ["step1", "step2"]
308build_release = ["step1 --release", "step2 --release"]
309"#;
310        let cfg: BuildCommandConfig = toml::from_str(toml_str).unwrap();
311        assert_eq!(cfg.build.unwrap().commands(), vec!["step1", "step2"]);
312        assert_eq!(
313            cfg.build_release.unwrap().commands(),
314            vec!["step1 --release", "step2 --release"]
315        );
316    }
317
318    #[test]
319    fn build_command_config_all_optional() {
320        let toml_str = "";
321        let cfg: BuildCommandConfig = toml::from_str(toml_str).unwrap();
322        assert!(cfg.build.is_none());
323        assert!(cfg.build_release.is_none());
324    }
325
326    #[test]
327    fn test_config_backward_compat_string() {
328        let toml_str = r#"command = "pytest""#;
329        let cfg: TestConfig = toml::from_str(toml_str).unwrap();
330        assert_eq!(cfg.command.unwrap().commands(), vec!["pytest"]);
331        assert!(cfg.e2e.is_none());
332        assert!(cfg.coverage.is_none());
333    }
334
335    #[test]
336    fn test_config_array_command() {
337        let toml_str = r#"command = ["cmd1", "cmd2"]"#;
338        let cfg: TestConfig = toml::from_str(toml_str).unwrap();
339        assert_eq!(cfg.command.unwrap().commands(), vec!["cmd1", "cmd2"]);
340    }
341
342    #[test]
343    fn test_config_with_coverage() {
344        let toml_str = r#"
345command = "pytest"
346coverage = "pytest --cov=. --cov-report=term-missing"
347"#;
348        let cfg: TestConfig = toml::from_str(toml_str).unwrap();
349        assert_eq!(cfg.command.unwrap().commands(), vec!["pytest"]);
350        assert_eq!(
351            cfg.coverage.unwrap().commands(),
352            vec!["pytest --cov=. --cov-report=term-missing"]
353        );
354        assert!(cfg.e2e.is_none());
355    }
356
357    #[test]
358    fn test_config_all_optional() {
359        let toml_str = "";
360        let cfg: TestConfig = toml::from_str(toml_str).unwrap();
361        assert!(cfg.command.is_none());
362        assert!(cfg.e2e.is_none());
363        assert!(cfg.coverage.is_none());
364    }
365
366    #[test]
367    fn full_alef_toml_with_lint_and_update() {
368        let toml_str = r#"
369languages = ["python", "node"]
370
371[crate]
372name = "test"
373sources = ["src/lib.rs"]
374
375[lint.python]
376format = "ruff format ."
377check = "ruff check --fix ."
378
379[lint.node]
380format = ["npx oxfmt", "npx oxlint --fix"]
381
382[update.python]
383update = "uv sync --upgrade"
384upgrade = "uv sync --all-packages --all-extras --upgrade"
385
386[update.node]
387update = "pnpm up -r"
388upgrade = ["corepack up", "pnpm up --latest -r -w"]
389"#;
390        let cfg: super::super::AlefConfig = toml::from_str(toml_str).unwrap();
391        let lint_map = cfg.lint.as_ref().unwrap();
392        assert!(lint_map.contains_key("python"));
393        assert!(lint_map.contains_key("node"));
394
395        let py_lint = lint_map.get("python").unwrap();
396        assert_eq!(py_lint.format.as_ref().unwrap().commands(), vec!["ruff format ."]);
397
398        let node_lint = lint_map.get("node").unwrap();
399        assert_eq!(
400            node_lint.format.as_ref().unwrap().commands(),
401            vec!["npx oxfmt", "npx oxlint --fix"]
402        );
403
404        let update_map = cfg.update.as_ref().unwrap();
405        assert!(update_map.contains_key("python"));
406        assert!(update_map.contains_key("node"));
407
408        let node_update = update_map.get("node").unwrap();
409        assert_eq!(node_update.update.as_ref().unwrap().commands(), vec!["pnpm up -r"]);
410        assert_eq!(
411            node_update.upgrade.as_ref().unwrap().commands(),
412            vec!["corepack up", "pnpm up --latest -r -w"]
413        );
414    }
415}
416
417/// Configuration for the `sync-versions` command.
418#[derive(Debug, Clone, Serialize, Deserialize, Default)]
419pub struct SyncConfig {
420    /// Extra file paths to update version in (glob patterns).
421    #[serde(default)]
422    pub extra_paths: Vec<String>,
423    /// Arbitrary text replacements applied during version sync.
424    #[serde(default)]
425    pub text_replacements: Vec<TextReplacement>,
426}