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 #[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 gleam: Option<PathBuf>,
35 pub go: Option<PathBuf>,
36 pub java: Option<PathBuf>,
37 pub kotlin: Option<PathBuf>,
38 pub dart: Option<PathBuf>,
39 pub swift: Option<PathBuf>,
40 pub csharp: Option<PathBuf>,
41 pub r: Option<PathBuf>,
42 pub zig: Option<PathBuf>,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct ScaffoldConfig {
47 pub description: Option<String>,
48 pub license: Option<String>,
49 pub repository: Option<String>,
50 pub homepage: Option<String>,
51 #[serde(default)]
52 pub authors: Vec<String>,
53 #[serde(default)]
54 pub keywords: Vec<String>,
55 pub cargo: Option<ScaffoldCargo>,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize, Default)]
67pub struct ScaffoldCargo {
68 #[serde(default)]
72 pub targets: ScaffoldCargoTargets,
73 #[serde(default)]
76 pub env: HashMap<String, ScaffoldCargoEnvValue>,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct ScaffoldCargoTargets {
82 #[serde(default = "default_true")]
83 pub macos_dynamic_lookup: bool,
84 #[serde(default = "default_true")]
85 pub x86_64_pc_windows_msvc: bool,
86 #[serde(default = "default_true")]
87 pub i686_pc_windows_msvc: bool,
88 #[serde(default = "default_true")]
89 pub aarch64_unknown_linux_gnu: bool,
90 #[serde(default = "default_true")]
91 pub x86_64_unknown_linux_musl: bool,
92 #[serde(default = "default_true")]
93 pub wasm32_unknown_unknown: bool,
94}
95
96impl Default for ScaffoldCargoTargets {
97 fn default() -> Self {
98 Self {
99 macos_dynamic_lookup: true,
100 x86_64_pc_windows_msvc: true,
101 i686_pc_windows_msvc: true,
102 aarch64_unknown_linux_gnu: true,
103 x86_64_unknown_linux_musl: true,
104 wasm32_unknown_unknown: true,
105 }
106 }
107}
108
109fn default_true() -> bool {
110 true
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
116#[serde(untagged)]
117pub enum ScaffoldCargoEnvValue {
118 Plain(String),
119 Structured {
120 value: String,
121 #[serde(default)]
122 relative: bool,
123 },
124}
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct ReadmeConfig {
128 pub template_dir: Option<PathBuf>,
129 pub snippets_dir: Option<PathBuf>,
130 pub config: Option<PathBuf>,
132 pub output_pattern: Option<String>,
133 pub discord_url: Option<String>,
135 pub banner_url: Option<String>,
137 #[serde(default)]
141 pub languages: HashMap<String, JsonValue>,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
148#[serde(untagged)]
149pub enum StringOrVec {
150 Single(String),
151 Multiple(Vec<String>),
152}
153
154impl StringOrVec {
155 pub fn commands(&self) -> Vec<&str> {
157 match self {
158 StringOrVec::Single(s) => vec![s.as_str()],
159 StringOrVec::Multiple(v) => v.iter().map(String::as_str).collect(),
160 }
161 }
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
165pub struct LintConfig {
166 pub precondition: Option<String>,
168 pub before: Option<StringOrVec>,
170 pub format: Option<StringOrVec>,
171 pub check: Option<StringOrVec>,
172 pub typecheck: Option<StringOrVec>,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
176pub struct UpdateConfig {
177 pub precondition: Option<String>,
179 pub before: Option<StringOrVec>,
181 pub update: Option<StringOrVec>,
183 pub upgrade: Option<StringOrVec>,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
188pub struct TestConfig {
189 pub precondition: Option<String>,
191 pub before: Option<StringOrVec>,
193 pub command: Option<StringOrVec>,
195 pub e2e: Option<StringOrVec>,
197 pub coverage: Option<StringOrVec>,
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
202pub struct SetupConfig {
203 pub precondition: Option<String>,
205 pub before: Option<StringOrVec>,
207 pub install: Option<StringOrVec>,
209 #[serde(default = "default_setup_timeout")]
211 pub timeout_seconds: u64,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
215pub struct CleanConfig {
216 pub precondition: Option<String>,
218 pub before: Option<StringOrVec>,
220 pub clean: Option<StringOrVec>,
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
225pub struct BuildCommandConfig {
226 pub precondition: Option<String>,
228 pub before: Option<StringOrVec>,
230 pub build: Option<StringOrVec>,
232 pub build_release: Option<StringOrVec>,
234}
235
236fn default_setup_timeout() -> u64 {
237 600
238}
239
240#[derive(Debug, Clone, Serialize, Deserialize)]
242pub struct TextReplacement {
243 pub path: String,
245 pub search: String,
247 pub replace: String,
249}
250
251#[cfg(test)]
252mod tests {
253 use super::*;
254
255 #[test]
256 fn string_or_vec_single_from_toml() {
257 let toml_str = r#"format = "ruff format""#;
258 #[derive(Deserialize)]
259 struct T {
260 format: StringOrVec,
261 }
262 let t: T = toml::from_str(toml_str).unwrap();
263 assert_eq!(t.format.commands(), vec!["ruff format"]);
264 }
265
266 #[test]
267 fn string_or_vec_multiple_from_toml() {
268 let toml_str = r#"format = ["cmd1", "cmd2", "cmd3"]"#;
269 #[derive(Deserialize)]
270 struct T {
271 format: StringOrVec,
272 }
273 let t: T = toml::from_str(toml_str).unwrap();
274 assert_eq!(t.format.commands(), vec!["cmd1", "cmd2", "cmd3"]);
275 }
276
277 #[test]
278 fn lint_config_backward_compat_string() {
279 let toml_str = r#"
280format = "ruff format ."
281check = "ruff check ."
282typecheck = "mypy ."
283"#;
284 let cfg: LintConfig = toml::from_str(toml_str).unwrap();
285 assert_eq!(cfg.format.unwrap().commands(), vec!["ruff format ."]);
286 assert_eq!(cfg.check.unwrap().commands(), vec!["ruff check ."]);
287 assert_eq!(cfg.typecheck.unwrap().commands(), vec!["mypy ."]);
288 }
289
290 #[test]
291 fn lint_config_array_commands() {
292 let toml_str = r#"
293format = ["cmd1", "cmd2"]
294check = "single-check"
295"#;
296 let cfg: LintConfig = toml::from_str(toml_str).unwrap();
297 assert_eq!(cfg.format.unwrap().commands(), vec!["cmd1", "cmd2"]);
298 assert_eq!(cfg.check.unwrap().commands(), vec!["single-check"]);
299 assert!(cfg.typecheck.is_none());
300 }
301
302 #[test]
303 fn lint_config_all_optional() {
304 let toml_str = "";
305 let cfg: LintConfig = toml::from_str(toml_str).unwrap();
306 assert!(cfg.format.is_none());
307 assert!(cfg.check.is_none());
308 assert!(cfg.typecheck.is_none());
309 }
310
311 #[test]
312 fn update_config_from_toml() {
313 let toml_str = r#"
314update = "cargo update"
315upgrade = ["cargo upgrade --incompatible", "cargo update"]
316"#;
317 let cfg: UpdateConfig = toml::from_str(toml_str).unwrap();
318 assert_eq!(cfg.update.unwrap().commands(), vec!["cargo update"]);
319 assert_eq!(
320 cfg.upgrade.unwrap().commands(),
321 vec!["cargo upgrade --incompatible", "cargo update"]
322 );
323 }
324
325 #[test]
326 fn update_config_all_optional() {
327 let toml_str = "";
328 let cfg: UpdateConfig = toml::from_str(toml_str).unwrap();
329 assert!(cfg.update.is_none());
330 assert!(cfg.upgrade.is_none());
331 }
332
333 #[test]
334 fn string_or_vec_empty_array_from_toml() {
335 let toml_str = "format = []";
336 #[derive(Deserialize)]
337 struct T {
338 format: StringOrVec,
339 }
340 let t: T = toml::from_str(toml_str).unwrap();
341 assert!(matches!(t.format, StringOrVec::Multiple(_)));
342 assert!(t.format.commands().is_empty());
343 }
344
345 #[test]
346 fn string_or_vec_single_element_array_from_toml() {
347 let toml_str = r#"format = ["cmd"]"#;
348 #[derive(Deserialize)]
349 struct T {
350 format: StringOrVec,
351 }
352 let t: T = toml::from_str(toml_str).unwrap();
353 assert_eq!(t.format.commands(), vec!["cmd"]);
354 }
355
356 #[test]
357 fn setup_config_single_string() {
358 let toml_str = r#"install = "uv sync""#;
359 let cfg: SetupConfig = toml::from_str(toml_str).unwrap();
360 assert_eq!(cfg.install.unwrap().commands(), vec!["uv sync"]);
361 }
362
363 #[test]
364 fn setup_config_array_commands() {
365 let toml_str = r#"install = ["step1", "step2"]"#;
366 let cfg: SetupConfig = toml::from_str(toml_str).unwrap();
367 assert_eq!(cfg.install.unwrap().commands(), vec!["step1", "step2"]);
368 }
369
370 #[test]
371 fn setup_config_all_optional() {
372 let toml_str = "";
373 let cfg: SetupConfig = toml::from_str(toml_str).unwrap();
374 assert!(cfg.install.is_none());
375 }
376
377 #[test]
378 fn clean_config_single_string() {
379 let toml_str = r#"clean = "rm -rf dist""#;
380 let cfg: CleanConfig = toml::from_str(toml_str).unwrap();
381 assert_eq!(cfg.clean.unwrap().commands(), vec!["rm -rf dist"]);
382 }
383
384 #[test]
385 fn clean_config_array_commands() {
386 let toml_str = r#"clean = ["step1", "step2"]"#;
387 let cfg: CleanConfig = toml::from_str(toml_str).unwrap();
388 assert_eq!(cfg.clean.unwrap().commands(), vec!["step1", "step2"]);
389 }
390
391 #[test]
392 fn clean_config_all_optional() {
393 let toml_str = "";
394 let cfg: CleanConfig = toml::from_str(toml_str).unwrap();
395 assert!(cfg.clean.is_none());
396 }
397
398 #[test]
399 fn build_command_config_single_strings() {
400 let toml_str = r#"
401build = "cargo build"
402build_release = "cargo build --release"
403"#;
404 let cfg: BuildCommandConfig = toml::from_str(toml_str).unwrap();
405 assert_eq!(cfg.build.unwrap().commands(), vec!["cargo build"]);
406 assert_eq!(cfg.build_release.unwrap().commands(), vec!["cargo build --release"]);
407 }
408
409 #[test]
410 fn build_command_config_array_commands() {
411 let toml_str = r#"
412build = ["step1", "step2"]
413build_release = ["step1 --release", "step2 --release"]
414"#;
415 let cfg: BuildCommandConfig = toml::from_str(toml_str).unwrap();
416 assert_eq!(cfg.build.unwrap().commands(), vec!["step1", "step2"]);
417 assert_eq!(
418 cfg.build_release.unwrap().commands(),
419 vec!["step1 --release", "step2 --release"]
420 );
421 }
422
423 #[test]
424 fn build_command_config_all_optional() {
425 let toml_str = "";
426 let cfg: BuildCommandConfig = toml::from_str(toml_str).unwrap();
427 assert!(cfg.build.is_none());
428 assert!(cfg.build_release.is_none());
429 }
430
431 #[test]
432 fn test_config_backward_compat_string() {
433 let toml_str = r#"command = "pytest""#;
434 let cfg: TestConfig = toml::from_str(toml_str).unwrap();
435 assert_eq!(cfg.command.unwrap().commands(), vec!["pytest"]);
436 assert!(cfg.e2e.is_none());
437 assert!(cfg.coverage.is_none());
438 }
439
440 #[test]
441 fn test_config_array_command() {
442 let toml_str = r#"command = ["cmd1", "cmd2"]"#;
443 let cfg: TestConfig = toml::from_str(toml_str).unwrap();
444 assert_eq!(cfg.command.unwrap().commands(), vec!["cmd1", "cmd2"]);
445 }
446
447 #[test]
448 fn test_config_with_coverage() {
449 let toml_str = r#"
450command = "pytest"
451coverage = "pytest --cov=. --cov-report=term-missing"
452"#;
453 let cfg: TestConfig = toml::from_str(toml_str).unwrap();
454 assert_eq!(cfg.command.unwrap().commands(), vec!["pytest"]);
455 assert_eq!(
456 cfg.coverage.unwrap().commands(),
457 vec!["pytest --cov=. --cov-report=term-missing"]
458 );
459 assert!(cfg.e2e.is_none());
460 }
461
462 #[test]
463 fn test_config_all_optional() {
464 let toml_str = "";
465 let cfg: TestConfig = toml::from_str(toml_str).unwrap();
466 assert!(cfg.command.is_none());
467 assert!(cfg.e2e.is_none());
468 assert!(cfg.coverage.is_none());
469 }
470
471 #[test]
472 fn full_alef_toml_with_lint_and_update() {
473 let toml_str = r#"
474languages = ["python", "node"]
475
476[crate]
477name = "test"
478sources = ["src/lib.rs"]
479
480[lint.python]
481format = "ruff format ."
482check = "ruff check --fix ."
483
484[lint.node]
485format = ["npx oxfmt", "npx oxlint --fix"]
486
487[update.python]
488update = "uv sync --upgrade"
489upgrade = "uv sync --all-packages --all-extras --upgrade"
490
491[update.node]
492update = "pnpm up -r"
493upgrade = ["corepack up", "pnpm up --latest -r -w"]
494"#;
495 let cfg: super::super::AlefConfig = toml::from_str(toml_str).unwrap();
496 let lint_map = cfg.lint.as_ref().unwrap();
497 assert!(lint_map.contains_key("python"));
498 assert!(lint_map.contains_key("node"));
499
500 let py_lint = lint_map.get("python").unwrap();
501 assert_eq!(py_lint.format.as_ref().unwrap().commands(), vec!["ruff format ."]);
502
503 let node_lint = lint_map.get("node").unwrap();
504 assert_eq!(
505 node_lint.format.as_ref().unwrap().commands(),
506 vec!["npx oxfmt", "npx oxlint --fix"]
507 );
508
509 let update_map = cfg.update.as_ref().unwrap();
510 assert!(update_map.contains_key("python"));
511 assert!(update_map.contains_key("node"));
512
513 let node_update = update_map.get("node").unwrap();
514 assert_eq!(node_update.update.as_ref().unwrap().commands(), vec!["pnpm up -r"]);
515 assert_eq!(
516 node_update.upgrade.as_ref().unwrap().commands(),
517 vec!["corepack up", "pnpm up --latest -r -w"]
518 );
519 }
520
521 #[test]
522 fn lint_config_with_precondition_and_before() {
523 let toml_str = r#"
524precondition = "test -f target/release/libfoo.so"
525before = "cargo build --release -p foo-ffi"
526format = "gofmt -w packages/go"
527check = "golangci-lint run ./..."
528"#;
529 let cfg: LintConfig = toml::from_str(toml_str).unwrap();
530 assert_eq!(cfg.precondition.as_deref(), Some("test -f target/release/libfoo.so"));
531 assert_eq!(cfg.before.unwrap().commands(), vec!["cargo build --release -p foo-ffi"]);
532 assert!(cfg.format.is_some());
533 assert!(cfg.check.is_some());
534 }
535
536 #[test]
537 fn test_config_with_before_list() {
538 let toml_str = r#"
539before = ["cd packages/python && maturin develop", "echo ready"]
540command = "pytest"
541"#;
542 let cfg: TestConfig = toml::from_str(toml_str).unwrap();
543 assert!(cfg.precondition.is_none());
544 assert_eq!(
545 cfg.before.unwrap().commands(),
546 vec!["cd packages/python && maturin develop", "echo ready"]
547 );
548 assert_eq!(cfg.command.unwrap().commands(), vec!["pytest"]);
549 }
550
551 #[test]
552 fn setup_config_with_precondition() {
553 let toml_str = r#"
554precondition = "which rustup"
555install = "rustup update"
556"#;
557 let cfg: SetupConfig = toml::from_str(toml_str).unwrap();
558 assert_eq!(cfg.precondition.as_deref(), Some("which rustup"));
559 assert!(cfg.before.is_none());
560 assert!(cfg.install.is_some());
561 }
562
563 #[test]
564 fn build_command_config_with_before() {
565 let toml_str = r#"
566before = "cargo build --release -p my-lib-ffi"
567build = "cd packages/go && go build ./..."
568"#;
569 let cfg: BuildCommandConfig = toml::from_str(toml_str).unwrap();
570 assert!(cfg.precondition.is_none());
571 assert_eq!(
572 cfg.before.unwrap().commands(),
573 vec!["cargo build --release -p my-lib-ffi"]
574 );
575 assert!(cfg.build.is_some());
576 }
577
578 #[test]
579 fn clean_config_precondition_and_before_optional() {
580 let toml_str = r#"clean = "cargo clean""#;
581 let cfg: CleanConfig = toml::from_str(toml_str).unwrap();
582 assert!(cfg.precondition.is_none());
583 assert!(cfg.before.is_none());
584 assert!(cfg.clean.is_some());
585 }
586
587 #[test]
588 fn update_config_with_precondition() {
589 let toml_str = r#"
590precondition = "test -f Cargo.lock"
591update = "cargo update"
592"#;
593 let cfg: UpdateConfig = toml::from_str(toml_str).unwrap();
594 assert_eq!(cfg.precondition.as_deref(), Some("test -f Cargo.lock"));
595 assert!(cfg.before.is_none());
596 assert!(cfg.update.is_some());
597 }
598
599 #[test]
600 fn full_alef_toml_with_precondition_and_before_across_sections() {
601 let toml_str = r#"
602languages = ["go", "python"]
603
604[crate]
605name = "mylib"
606sources = ["src/lib.rs"]
607
608[lint.go]
609precondition = "test -f target/release/libmylib_ffi.so"
610before = "cargo build --release -p mylib-ffi"
611format = "gofmt -w packages/go"
612check = "golangci-lint run ./..."
613
614[lint.python]
615format = "ruff format packages/python"
616check = "ruff check --fix packages/python"
617
618[test.go]
619precondition = "test -f target/release/libmylib_ffi.so"
620before = ["cargo build --release -p mylib-ffi", "cp target/release/libmylib_ffi.so packages/go/"]
621command = "cd packages/go && go test ./..."
622
623[test.python]
624command = "cd packages/python && uv run pytest"
625
626[build_commands.go]
627precondition = "which go"
628before = "cargo build --release -p mylib-ffi"
629build = "cd packages/go && go build ./..."
630build_release = "cd packages/go && go build -ldflags='-s -w' ./..."
631
632[update.go]
633precondition = "test -d packages/go"
634update = "cd packages/go && go get -u ./..."
635
636[setup.python]
637precondition = "which uv"
638install = "cd packages/python && uv sync"
639
640[clean.go]
641before = "echo cleaning go"
642clean = "cd packages/go && go clean -cache"
643"#;
644 let cfg: super::super::AlefConfig = toml::from_str(toml_str).unwrap();
645
646 let lint_map = cfg.lint.as_ref().unwrap();
648 let go_lint = lint_map.get("go").unwrap();
649 assert_eq!(
650 go_lint.precondition.as_deref(),
651 Some("test -f target/release/libmylib_ffi.so"),
652 "lint.go precondition should be preserved"
653 );
654 assert_eq!(
655 go_lint.before.as_ref().unwrap().commands(),
656 vec!["cargo build --release -p mylib-ffi"],
657 "lint.go before should be preserved"
658 );
659 assert!(go_lint.format.is_some());
660 assert!(go_lint.check.is_some());
661
662 let py_lint = lint_map.get("python").unwrap();
664 assert!(
665 py_lint.precondition.is_none(),
666 "lint.python should have no precondition"
667 );
668 assert!(py_lint.before.is_none(), "lint.python should have no before");
669
670 let test_map = cfg.test.as_ref().unwrap();
672 let go_test = test_map.get("go").unwrap();
673 assert_eq!(
674 go_test.precondition.as_deref(),
675 Some("test -f target/release/libmylib_ffi.so"),
676 "test.go precondition should be preserved"
677 );
678 assert_eq!(
679 go_test.before.as_ref().unwrap().commands(),
680 vec![
681 "cargo build --release -p mylib-ffi",
682 "cp target/release/libmylib_ffi.so packages/go/"
683 ],
684 "test.go before list should be preserved"
685 );
686
687 let build_map = cfg.build_commands.as_ref().unwrap();
689 let go_build = build_map.get("go").unwrap();
690 assert_eq!(
691 go_build.precondition.as_deref(),
692 Some("which go"),
693 "build_commands.go precondition should be preserved"
694 );
695 assert_eq!(
696 go_build.before.as_ref().unwrap().commands(),
697 vec!["cargo build --release -p mylib-ffi"],
698 "build_commands.go before should be preserved"
699 );
700
701 let update_map = cfg.update.as_ref().unwrap();
703 let go_update = update_map.get("go").unwrap();
704 assert_eq!(
705 go_update.precondition.as_deref(),
706 Some("test -d packages/go"),
707 "update.go precondition should be preserved"
708 );
709 assert!(go_update.before.is_none(), "update.go before should be None");
710
711 let setup_map = cfg.setup.as_ref().unwrap();
713 let py_setup = setup_map.get("python").unwrap();
714 assert_eq!(
715 py_setup.precondition.as_deref(),
716 Some("which uv"),
717 "setup.python precondition should be preserved"
718 );
719 assert!(py_setup.before.is_none(), "setup.python before should be None");
720
721 let clean_map = cfg.clean.as_ref().unwrap();
723 let go_clean = clean_map.get("go").unwrap();
724 assert!(go_clean.precondition.is_none(), "clean.go precondition should be None");
725 assert_eq!(
726 go_clean.before.as_ref().unwrap().commands(),
727 vec!["echo cleaning go"],
728 "clean.go before should be preserved"
729 );
730 }
731}
732
733#[derive(Debug, Clone, Serialize, Deserialize, Default)]
735pub struct SyncConfig {
736 #[serde(default)]
738 pub extra_paths: Vec<String>,
739 #[serde(default)]
741 pub text_replacements: Vec<TextReplacement>,
742}