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    /// Shell command that must exit 0 for lint to run; skip with warning on failure.
93    pub precondition: Option<String>,
94    /// Command(s) to run before the main lint commands; aborts on failure.
95    pub before: Option<StringOrVec>,
96    pub format: Option<StringOrVec>,
97    pub check: Option<StringOrVec>,
98    pub typecheck: Option<StringOrVec>,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct UpdateConfig {
103    /// Shell command that must exit 0 for update to run; skip with warning on failure.
104    pub precondition: Option<String>,
105    /// Command(s) to run before the main update commands; aborts on failure.
106    pub before: Option<StringOrVec>,
107    /// Command(s) for safe dependency updates (compatible versions only).
108    pub update: Option<StringOrVec>,
109    /// Command(s) for aggressive updates (including incompatible/major bumps).
110    pub upgrade: Option<StringOrVec>,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize, Default)]
114pub struct TestConfig {
115    /// Shell command that must exit 0 for test to run; skip with warning on failure.
116    pub precondition: Option<String>,
117    /// Command(s) to run before the main test commands; aborts on failure.
118    pub before: Option<StringOrVec>,
119    /// Command to run unit/integration tests for this language.
120    pub command: Option<StringOrVec>,
121    /// Command to run e2e tests for this language.
122    pub e2e: Option<StringOrVec>,
123    /// Command to run tests with coverage for this language.
124    pub coverage: Option<StringOrVec>,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct SetupConfig {
129    /// Shell command that must exit 0 for setup to run; skip with warning on failure.
130    pub precondition: Option<String>,
131    /// Command(s) to run before the main setup commands; aborts on failure.
132    pub before: Option<StringOrVec>,
133    /// Command(s) to install dependencies for this language.
134    pub install: Option<StringOrVec>,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct CleanConfig {
139    /// Shell command that must exit 0 for clean to run; skip with warning on failure.
140    pub precondition: Option<String>,
141    /// Command(s) to run before the main clean commands; aborts on failure.
142    pub before: Option<StringOrVec>,
143    /// Command(s) to clean build artifacts for this language.
144    pub clean: Option<StringOrVec>,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct BuildCommandConfig {
149    /// Shell command that must exit 0 for build to run; skip with warning on failure.
150    pub precondition: Option<String>,
151    /// Command(s) to run before the main build commands; aborts on failure.
152    pub before: Option<StringOrVec>,
153    /// Command(s) to build in debug mode.
154    pub build: Option<StringOrVec>,
155    /// Command(s) to build in release mode.
156    pub build_release: Option<StringOrVec>,
157}
158
159/// A single text replacement rule for version sync.
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct TextReplacement {
162    /// Glob pattern for files to process.
163    pub path: String,
164    /// Regex pattern to search for (may contain `{version}` placeholder).
165    pub search: String,
166    /// Replacement string (may contain `{version}` placeholder).
167    pub replace: String,
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173
174    #[test]
175    fn string_or_vec_single_from_toml() {
176        let toml_str = r#"format = "ruff format""#;
177        #[derive(Deserialize)]
178        struct T {
179            format: StringOrVec,
180        }
181        let t: T = toml::from_str(toml_str).unwrap();
182        assert_eq!(t.format.commands(), vec!["ruff format"]);
183    }
184
185    #[test]
186    fn string_or_vec_multiple_from_toml() {
187        let toml_str = r#"format = ["cmd1", "cmd2", "cmd3"]"#;
188        #[derive(Deserialize)]
189        struct T {
190            format: StringOrVec,
191        }
192        let t: T = toml::from_str(toml_str).unwrap();
193        assert_eq!(t.format.commands(), vec!["cmd1", "cmd2", "cmd3"]);
194    }
195
196    #[test]
197    fn lint_config_backward_compat_string() {
198        let toml_str = r#"
199format = "ruff format ."
200check = "ruff check ."
201typecheck = "mypy ."
202"#;
203        let cfg: LintConfig = toml::from_str(toml_str).unwrap();
204        assert_eq!(cfg.format.unwrap().commands(), vec!["ruff format ."]);
205        assert_eq!(cfg.check.unwrap().commands(), vec!["ruff check ."]);
206        assert_eq!(cfg.typecheck.unwrap().commands(), vec!["mypy ."]);
207    }
208
209    #[test]
210    fn lint_config_array_commands() {
211        let toml_str = r#"
212format = ["cmd1", "cmd2"]
213check = "single-check"
214"#;
215        let cfg: LintConfig = toml::from_str(toml_str).unwrap();
216        assert_eq!(cfg.format.unwrap().commands(), vec!["cmd1", "cmd2"]);
217        assert_eq!(cfg.check.unwrap().commands(), vec!["single-check"]);
218        assert!(cfg.typecheck.is_none());
219    }
220
221    #[test]
222    fn lint_config_all_optional() {
223        let toml_str = "";
224        let cfg: LintConfig = toml::from_str(toml_str).unwrap();
225        assert!(cfg.format.is_none());
226        assert!(cfg.check.is_none());
227        assert!(cfg.typecheck.is_none());
228    }
229
230    #[test]
231    fn update_config_from_toml() {
232        let toml_str = r#"
233update = "cargo update"
234upgrade = ["cargo upgrade --incompatible", "cargo update"]
235"#;
236        let cfg: UpdateConfig = toml::from_str(toml_str).unwrap();
237        assert_eq!(cfg.update.unwrap().commands(), vec!["cargo update"]);
238        assert_eq!(
239            cfg.upgrade.unwrap().commands(),
240            vec!["cargo upgrade --incompatible", "cargo update"]
241        );
242    }
243
244    #[test]
245    fn update_config_all_optional() {
246        let toml_str = "";
247        let cfg: UpdateConfig = toml::from_str(toml_str).unwrap();
248        assert!(cfg.update.is_none());
249        assert!(cfg.upgrade.is_none());
250    }
251
252    #[test]
253    fn string_or_vec_empty_array_from_toml() {
254        let toml_str = "format = []";
255        #[derive(Deserialize)]
256        struct T {
257            format: StringOrVec,
258        }
259        let t: T = toml::from_str(toml_str).unwrap();
260        assert!(matches!(t.format, StringOrVec::Multiple(_)));
261        assert!(t.format.commands().is_empty());
262    }
263
264    #[test]
265    fn string_or_vec_single_element_array_from_toml() {
266        let toml_str = r#"format = ["cmd"]"#;
267        #[derive(Deserialize)]
268        struct T {
269            format: StringOrVec,
270        }
271        let t: T = toml::from_str(toml_str).unwrap();
272        assert_eq!(t.format.commands(), vec!["cmd"]);
273    }
274
275    #[test]
276    fn setup_config_single_string() {
277        let toml_str = r#"install = "uv sync""#;
278        let cfg: SetupConfig = toml::from_str(toml_str).unwrap();
279        assert_eq!(cfg.install.unwrap().commands(), vec!["uv sync"]);
280    }
281
282    #[test]
283    fn setup_config_array_commands() {
284        let toml_str = r#"install = ["step1", "step2"]"#;
285        let cfg: SetupConfig = toml::from_str(toml_str).unwrap();
286        assert_eq!(cfg.install.unwrap().commands(), vec!["step1", "step2"]);
287    }
288
289    #[test]
290    fn setup_config_all_optional() {
291        let toml_str = "";
292        let cfg: SetupConfig = toml::from_str(toml_str).unwrap();
293        assert!(cfg.install.is_none());
294    }
295
296    #[test]
297    fn clean_config_single_string() {
298        let toml_str = r#"clean = "rm -rf dist""#;
299        let cfg: CleanConfig = toml::from_str(toml_str).unwrap();
300        assert_eq!(cfg.clean.unwrap().commands(), vec!["rm -rf dist"]);
301    }
302
303    #[test]
304    fn clean_config_array_commands() {
305        let toml_str = r#"clean = ["step1", "step2"]"#;
306        let cfg: CleanConfig = toml::from_str(toml_str).unwrap();
307        assert_eq!(cfg.clean.unwrap().commands(), vec!["step1", "step2"]);
308    }
309
310    #[test]
311    fn clean_config_all_optional() {
312        let toml_str = "";
313        let cfg: CleanConfig = toml::from_str(toml_str).unwrap();
314        assert!(cfg.clean.is_none());
315    }
316
317    #[test]
318    fn build_command_config_single_strings() {
319        let toml_str = r#"
320build = "cargo build"
321build_release = "cargo build --release"
322"#;
323        let cfg: BuildCommandConfig = toml::from_str(toml_str).unwrap();
324        assert_eq!(cfg.build.unwrap().commands(), vec!["cargo build"]);
325        assert_eq!(cfg.build_release.unwrap().commands(), vec!["cargo build --release"]);
326    }
327
328    #[test]
329    fn build_command_config_array_commands() {
330        let toml_str = r#"
331build = ["step1", "step2"]
332build_release = ["step1 --release", "step2 --release"]
333"#;
334        let cfg: BuildCommandConfig = toml::from_str(toml_str).unwrap();
335        assert_eq!(cfg.build.unwrap().commands(), vec!["step1", "step2"]);
336        assert_eq!(
337            cfg.build_release.unwrap().commands(),
338            vec!["step1 --release", "step2 --release"]
339        );
340    }
341
342    #[test]
343    fn build_command_config_all_optional() {
344        let toml_str = "";
345        let cfg: BuildCommandConfig = toml::from_str(toml_str).unwrap();
346        assert!(cfg.build.is_none());
347        assert!(cfg.build_release.is_none());
348    }
349
350    #[test]
351    fn test_config_backward_compat_string() {
352        let toml_str = r#"command = "pytest""#;
353        let cfg: TestConfig = toml::from_str(toml_str).unwrap();
354        assert_eq!(cfg.command.unwrap().commands(), vec!["pytest"]);
355        assert!(cfg.e2e.is_none());
356        assert!(cfg.coverage.is_none());
357    }
358
359    #[test]
360    fn test_config_array_command() {
361        let toml_str = r#"command = ["cmd1", "cmd2"]"#;
362        let cfg: TestConfig = toml::from_str(toml_str).unwrap();
363        assert_eq!(cfg.command.unwrap().commands(), vec!["cmd1", "cmd2"]);
364    }
365
366    #[test]
367    fn test_config_with_coverage() {
368        let toml_str = r#"
369command = "pytest"
370coverage = "pytest --cov=. --cov-report=term-missing"
371"#;
372        let cfg: TestConfig = toml::from_str(toml_str).unwrap();
373        assert_eq!(cfg.command.unwrap().commands(), vec!["pytest"]);
374        assert_eq!(
375            cfg.coverage.unwrap().commands(),
376            vec!["pytest --cov=. --cov-report=term-missing"]
377        );
378        assert!(cfg.e2e.is_none());
379    }
380
381    #[test]
382    fn test_config_all_optional() {
383        let toml_str = "";
384        let cfg: TestConfig = toml::from_str(toml_str).unwrap();
385        assert!(cfg.command.is_none());
386        assert!(cfg.e2e.is_none());
387        assert!(cfg.coverage.is_none());
388    }
389
390    #[test]
391    fn full_alef_toml_with_lint_and_update() {
392        let toml_str = r#"
393languages = ["python", "node"]
394
395[crate]
396name = "test"
397sources = ["src/lib.rs"]
398
399[lint.python]
400format = "ruff format ."
401check = "ruff check --fix ."
402
403[lint.node]
404format = ["npx oxfmt", "npx oxlint --fix"]
405
406[update.python]
407update = "uv sync --upgrade"
408upgrade = "uv sync --all-packages --all-extras --upgrade"
409
410[update.node]
411update = "pnpm up -r"
412upgrade = ["corepack up", "pnpm up --latest -r -w"]
413"#;
414        let cfg: super::super::AlefConfig = toml::from_str(toml_str).unwrap();
415        let lint_map = cfg.lint.as_ref().unwrap();
416        assert!(lint_map.contains_key("python"));
417        assert!(lint_map.contains_key("node"));
418
419        let py_lint = lint_map.get("python").unwrap();
420        assert_eq!(py_lint.format.as_ref().unwrap().commands(), vec!["ruff format ."]);
421
422        let node_lint = lint_map.get("node").unwrap();
423        assert_eq!(
424            node_lint.format.as_ref().unwrap().commands(),
425            vec!["npx oxfmt", "npx oxlint --fix"]
426        );
427
428        let update_map = cfg.update.as_ref().unwrap();
429        assert!(update_map.contains_key("python"));
430        assert!(update_map.contains_key("node"));
431
432        let node_update = update_map.get("node").unwrap();
433        assert_eq!(node_update.update.as_ref().unwrap().commands(), vec!["pnpm up -r"]);
434        assert_eq!(
435            node_update.upgrade.as_ref().unwrap().commands(),
436            vec!["corepack up", "pnpm up --latest -r -w"]
437        );
438    }
439
440    #[test]
441    fn lint_config_with_precondition_and_before() {
442        let toml_str = r#"
443precondition = "test -f target/release/libfoo.so"
444before = "cargo build --release -p foo-ffi"
445format = "gofmt -w packages/go"
446check = "golangci-lint run ./..."
447"#;
448        let cfg: LintConfig = toml::from_str(toml_str).unwrap();
449        assert_eq!(cfg.precondition.as_deref(), Some("test -f target/release/libfoo.so"));
450        assert_eq!(cfg.before.unwrap().commands(), vec!["cargo build --release -p foo-ffi"]);
451        assert!(cfg.format.is_some());
452        assert!(cfg.check.is_some());
453    }
454
455    #[test]
456    fn test_config_with_before_list() {
457        let toml_str = r#"
458before = ["cd packages/python && maturin develop", "echo ready"]
459command = "pytest"
460"#;
461        let cfg: TestConfig = toml::from_str(toml_str).unwrap();
462        assert!(cfg.precondition.is_none());
463        assert_eq!(
464            cfg.before.unwrap().commands(),
465            vec!["cd packages/python && maturin develop", "echo ready"]
466        );
467        assert_eq!(cfg.command.unwrap().commands(), vec!["pytest"]);
468    }
469
470    #[test]
471    fn setup_config_with_precondition() {
472        let toml_str = r#"
473precondition = "which rustup"
474install = "rustup update"
475"#;
476        let cfg: SetupConfig = toml::from_str(toml_str).unwrap();
477        assert_eq!(cfg.precondition.as_deref(), Some("which rustup"));
478        assert!(cfg.before.is_none());
479        assert!(cfg.install.is_some());
480    }
481
482    #[test]
483    fn build_command_config_with_before() {
484        let toml_str = r#"
485before = "cargo build --release -p my-lib-ffi"
486build = "cd packages/go && go build ./..."
487"#;
488        let cfg: BuildCommandConfig = toml::from_str(toml_str).unwrap();
489        assert!(cfg.precondition.is_none());
490        assert_eq!(
491            cfg.before.unwrap().commands(),
492            vec!["cargo build --release -p my-lib-ffi"]
493        );
494        assert!(cfg.build.is_some());
495    }
496
497    #[test]
498    fn clean_config_precondition_and_before_optional() {
499        let toml_str = r#"clean = "cargo clean""#;
500        let cfg: CleanConfig = toml::from_str(toml_str).unwrap();
501        assert!(cfg.precondition.is_none());
502        assert!(cfg.before.is_none());
503        assert!(cfg.clean.is_some());
504    }
505
506    #[test]
507    fn update_config_with_precondition() {
508        let toml_str = r#"
509precondition = "test -f Cargo.lock"
510update = "cargo update"
511"#;
512        let cfg: UpdateConfig = toml::from_str(toml_str).unwrap();
513        assert_eq!(cfg.precondition.as_deref(), Some("test -f Cargo.lock"));
514        assert!(cfg.before.is_none());
515        assert!(cfg.update.is_some());
516    }
517
518    #[test]
519    fn full_alef_toml_with_precondition_and_before_across_sections() {
520        let toml_str = r#"
521languages = ["go", "python"]
522
523[crate]
524name = "mylib"
525sources = ["src/lib.rs"]
526
527[lint.go]
528precondition = "test -f target/release/libmylib_ffi.so"
529before = "cargo build --release -p mylib-ffi"
530format = "gofmt -w packages/go"
531check = "golangci-lint run ./..."
532
533[lint.python]
534format = "ruff format packages/python"
535check = "ruff check --fix packages/python"
536
537[test.go]
538precondition = "test -f target/release/libmylib_ffi.so"
539before = ["cargo build --release -p mylib-ffi", "cp target/release/libmylib_ffi.so packages/go/"]
540command = "cd packages/go && go test ./..."
541
542[test.python]
543command = "cd packages/python && uv run pytest"
544
545[build_commands.go]
546precondition = "which go"
547before = "cargo build --release -p mylib-ffi"
548build = "cd packages/go && go build ./..."
549build_release = "cd packages/go && go build -ldflags='-s -w' ./..."
550
551[update.go]
552precondition = "test -d packages/go"
553update = "cd packages/go && go get -u ./..."
554
555[setup.python]
556precondition = "which uv"
557install = "cd packages/python && uv sync"
558
559[clean.go]
560before = "echo cleaning go"
561clean = "cd packages/go && go clean -cache"
562"#;
563        let cfg: super::super::AlefConfig = toml::from_str(toml_str).unwrap();
564
565        // lint.go: precondition and before set
566        let lint_map = cfg.lint.as_ref().unwrap();
567        let go_lint = lint_map.get("go").unwrap();
568        assert_eq!(
569            go_lint.precondition.as_deref(),
570            Some("test -f target/release/libmylib_ffi.so"),
571            "lint.go precondition should be preserved"
572        );
573        assert_eq!(
574            go_lint.before.as_ref().unwrap().commands(),
575            vec!["cargo build --release -p mylib-ffi"],
576            "lint.go before should be preserved"
577        );
578        assert!(go_lint.format.is_some());
579        assert!(go_lint.check.is_some());
580
581        // lint.python: no precondition or before
582        let py_lint = lint_map.get("python").unwrap();
583        assert!(
584            py_lint.precondition.is_none(),
585            "lint.python should have no precondition"
586        );
587        assert!(py_lint.before.is_none(), "lint.python should have no before");
588
589        // test.go: precondition and multi-command before
590        let test_map = cfg.test.as_ref().unwrap();
591        let go_test = test_map.get("go").unwrap();
592        assert_eq!(
593            go_test.precondition.as_deref(),
594            Some("test -f target/release/libmylib_ffi.so"),
595            "test.go precondition should be preserved"
596        );
597        assert_eq!(
598            go_test.before.as_ref().unwrap().commands(),
599            vec![
600                "cargo build --release -p mylib-ffi",
601                "cp target/release/libmylib_ffi.so packages/go/"
602            ],
603            "test.go before list should be preserved"
604        );
605
606        // build_commands.go: precondition and before
607        let build_map = cfg.build_commands.as_ref().unwrap();
608        let go_build = build_map.get("go").unwrap();
609        assert_eq!(
610            go_build.precondition.as_deref(),
611            Some("which go"),
612            "build_commands.go precondition should be preserved"
613        );
614        assert_eq!(
615            go_build.before.as_ref().unwrap().commands(),
616            vec!["cargo build --release -p mylib-ffi"],
617            "build_commands.go before should be preserved"
618        );
619
620        // update.go: precondition only, no before
621        let update_map = cfg.update.as_ref().unwrap();
622        let go_update = update_map.get("go").unwrap();
623        assert_eq!(
624            go_update.precondition.as_deref(),
625            Some("test -d packages/go"),
626            "update.go precondition should be preserved"
627        );
628        assert!(go_update.before.is_none(), "update.go before should be None");
629
630        // setup.python: precondition only
631        let setup_map = cfg.setup.as_ref().unwrap();
632        let py_setup = setup_map.get("python").unwrap();
633        assert_eq!(
634            py_setup.precondition.as_deref(),
635            Some("which uv"),
636            "setup.python precondition should be preserved"
637        );
638        assert!(py_setup.before.is_none(), "setup.python before should be None");
639
640        // clean.go: before only, no precondition
641        let clean_map = cfg.clean.as_ref().unwrap();
642        let go_clean = clean_map.get("go").unwrap();
643        assert!(go_clean.precondition.is_none(), "clean.go precondition should be None");
644        assert_eq!(
645            go_clean.before.as_ref().unwrap().commands(),
646            vec!["echo cleaning go"],
647            "clean.go before should be preserved"
648        );
649    }
650}
651
652/// Configuration for the `sync-versions` command.
653#[derive(Debug, Clone, Serialize, Deserialize, Default)]
654pub struct SyncConfig {
655    /// Extra file paths to update version in (glob patterns).
656    #[serde(default)]
657    pub extra_paths: Vec<String>,
658    /// Arbitrary text replacements applied during version sync.
659    #[serde(default)]
660    pub text_replacements: Vec<TextReplacement>,
661}