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