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)]
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)]
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)]
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)]
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)]
128pub struct SetupConfig {
129 pub precondition: Option<String>,
131 pub before: Option<StringOrVec>,
133 pub install: Option<StringOrVec>,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct CleanConfig {
139 pub precondition: Option<String>,
141 pub before: Option<StringOrVec>,
143 pub clean: Option<StringOrVec>,
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct BuildCommandConfig {
149 pub precondition: Option<String>,
151 pub before: Option<StringOrVec>,
153 pub build: Option<StringOrVec>,
155 pub build_release: Option<StringOrVec>,
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct TextReplacement {
162 pub path: String,
164 pub search: String,
166 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 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 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 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 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 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 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 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#[derive(Debug, Clone, Serialize, Deserialize, Default)]
654pub struct SyncConfig {
655 #[serde(default)]
657 pub extra_paths: Vec<String>,
658 #[serde(default)]
660 pub text_replacements: Vec<TextReplacement>,
661}