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