1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use std::path::PathBuf;
4
5pub mod build_defaults;
6pub mod clean_defaults;
7pub mod dto;
8pub mod e2e;
9pub mod extras;
10pub mod languages;
11pub mod lint_defaults;
12pub mod output;
13pub mod setup_defaults;
14pub mod test_defaults;
15pub mod trait_bridge;
16pub mod update_defaults;
17
18pub use dto::{
20 CsharpDtoStyle, DtoConfig, ElixirDtoStyle, GoDtoStyle, JavaDtoStyle, NodeDtoStyle, PhpDtoStyle, PythonDtoStyle,
21 RDtoStyle, RubyDtoStyle,
22};
23pub use e2e::E2eConfig;
24pub use extras::{AdapterConfig, AdapterParam, AdapterPattern, Language};
25pub use languages::{
26 CSharpConfig, CustomModulesConfig, CustomRegistration, CustomRegistrationsConfig, ElixirConfig, FfiConfig,
27 GoConfig, JavaConfig, NodeConfig, PhpConfig, PythonConfig, RConfig, RubyConfig, StubsConfig, WasmConfig,
28};
29pub use output::{
30 BuildCommandConfig, CleanConfig, ExcludeConfig, IncludeConfig, LintConfig, OutputConfig, ReadmeConfig,
31 ScaffoldConfig, SetupConfig, SyncConfig, TestConfig, TextReplacement, UpdateConfig,
32};
33pub use trait_bridge::TraitBridgeConfig;
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct AlefConfig {
38 #[serde(rename = "crate")]
39 pub crate_config: CrateConfig,
40 pub languages: Vec<Language>,
41 #[serde(default)]
42 pub exclude: ExcludeConfig,
43 #[serde(default)]
44 pub include: IncludeConfig,
45 #[serde(default)]
46 pub output: OutputConfig,
47 #[serde(default)]
48 pub python: Option<PythonConfig>,
49 #[serde(default)]
50 pub node: Option<NodeConfig>,
51 #[serde(default)]
52 pub ruby: Option<RubyConfig>,
53 #[serde(default)]
54 pub php: Option<PhpConfig>,
55 #[serde(default)]
56 pub elixir: Option<ElixirConfig>,
57 #[serde(default)]
58 pub wasm: Option<WasmConfig>,
59 #[serde(default)]
60 pub ffi: Option<FfiConfig>,
61 #[serde(default)]
62 pub go: Option<GoConfig>,
63 #[serde(default)]
64 pub java: Option<JavaConfig>,
65 #[serde(default)]
66 pub csharp: Option<CSharpConfig>,
67 #[serde(default)]
68 pub r: Option<RConfig>,
69 #[serde(default)]
70 pub scaffold: Option<ScaffoldConfig>,
71 #[serde(default)]
72 pub readme: Option<ReadmeConfig>,
73 #[serde(default)]
74 pub lint: Option<HashMap<String, LintConfig>>,
75 #[serde(default)]
76 pub update: Option<HashMap<String, UpdateConfig>>,
77 #[serde(default)]
78 pub test: Option<HashMap<String, TestConfig>>,
79 #[serde(default)]
80 pub setup: Option<HashMap<String, SetupConfig>>,
81 #[serde(default)]
82 pub clean: Option<HashMap<String, CleanConfig>>,
83 #[serde(default)]
84 pub build_commands: Option<HashMap<String, BuildCommandConfig>>,
85 #[serde(default)]
86 pub custom_files: Option<HashMap<String, Vec<PathBuf>>>,
87 #[serde(default)]
88 pub adapters: Vec<AdapterConfig>,
89 #[serde(default)]
90 pub custom_modules: CustomModulesConfig,
91 #[serde(default)]
92 pub custom_registrations: CustomRegistrationsConfig,
93 #[serde(default)]
94 pub sync: Option<SyncConfig>,
95 #[serde(default)]
99 pub opaque_types: HashMap<String, String>,
100 #[serde(default)]
102 pub generate: GenerateConfig,
103 #[serde(default)]
105 pub generate_overrides: HashMap<String, GenerateConfig>,
106 #[serde(default)]
108 pub dto: DtoConfig,
109 #[serde(default)]
111 pub e2e: Option<E2eConfig>,
112 #[serde(default)]
115 pub trait_bridges: Vec<TraitBridgeConfig>,
116}
117
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct CrateConfig {
120 pub name: String,
121 pub sources: Vec<PathBuf>,
122 #[serde(default = "default_version_from")]
123 pub version_from: String,
124 #[serde(default)]
125 pub core_import: Option<String>,
126 #[serde(default)]
128 pub workspace_root: Option<PathBuf>,
129 #[serde(default)]
131 pub skip_core_import: bool,
132 #[serde(default)]
136 pub error_type: Option<String>,
137 #[serde(default)]
142 pub error_constructor: Option<String>,
143 #[serde(default)]
147 pub features: Vec<String>,
148 #[serde(default)]
151 pub path_mappings: HashMap<String, String>,
152 #[serde(default)]
156 pub extra_dependencies: HashMap<String, toml::Value>,
157 #[serde(default = "default_true")]
161 pub auto_path_mappings: bool,
162 #[serde(default)]
167 pub source_crates: Vec<SourceCrate>,
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct SourceCrate {
173 pub name: String,
175 pub sources: Vec<PathBuf>,
177}
178
179fn default_version_from() -> String {
180 "Cargo.toml".to_string()
181}
182
183fn default_true() -> bool {
184 true
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct GenerateConfig {
192 #[serde(default = "default_true")]
194 pub bindings: bool,
195 #[serde(default = "default_true")]
197 pub errors: bool,
198 #[serde(default = "default_true")]
200 pub configs: bool,
201 #[serde(default = "default_true")]
203 pub async_wrappers: bool,
204 #[serde(default = "default_true")]
206 pub type_conversions: bool,
207 #[serde(default = "default_true")]
209 pub package_metadata: bool,
210 #[serde(default = "default_true")]
212 pub public_api: bool,
213 #[serde(default = "default_true")]
216 pub reverse_conversions: bool,
217}
218
219impl Default for GenerateConfig {
220 fn default() -> Self {
221 Self {
222 bindings: true,
223 errors: true,
224 configs: true,
225 async_wrappers: true,
226 type_conversions: true,
227 package_metadata: true,
228 public_api: true,
229 reverse_conversions: true,
230 }
231 }
232}
233
234impl AlefConfig {
239 pub fn features_for_language(&self, lang: extras::Language) -> &[String] {
242 let override_features = match lang {
243 extras::Language::Python => self.python.as_ref().and_then(|c| c.features.as_deref()),
244 extras::Language::Node => self.node.as_ref().and_then(|c| c.features.as_deref()),
245 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.features.as_deref()),
246 extras::Language::Php => self.php.as_ref().and_then(|c| c.features.as_deref()),
247 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.features.as_deref()),
248 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.features.as_deref()),
249 extras::Language::Ffi => self.ffi.as_ref().and_then(|c| c.features.as_deref()),
250 extras::Language::Go => self.go.as_ref().and_then(|c| c.features.as_deref()),
251 extras::Language::Java => self.java.as_ref().and_then(|c| c.features.as_deref()),
252 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.features.as_deref()),
253 extras::Language::R => self.r.as_ref().and_then(|c| c.features.as_deref()),
254 extras::Language::Rust => None, };
256 override_features.unwrap_or(&self.crate_config.features)
257 }
258
259 pub fn extra_deps_for_language(&self, lang: extras::Language) -> HashMap<String, toml::Value> {
263 let mut deps = self.crate_config.extra_dependencies.clone();
264 let lang_deps = match lang {
265 extras::Language::Python => self.python.as_ref().map(|c| &c.extra_dependencies),
266 extras::Language::Node => self.node.as_ref().map(|c| &c.extra_dependencies),
267 extras::Language::Ruby => self.ruby.as_ref().map(|c| &c.extra_dependencies),
268 extras::Language::Php => self.php.as_ref().map(|c| &c.extra_dependencies),
269 extras::Language::Elixir => self.elixir.as_ref().map(|c| &c.extra_dependencies),
270 extras::Language::Wasm => self.wasm.as_ref().map(|c| &c.extra_dependencies),
271 _ => None,
272 };
273 if let Some(lang_deps) = lang_deps {
274 deps.extend(lang_deps.iter().map(|(k, v)| (k.clone(), v.clone())));
275 }
276 deps
277 }
278
279 pub fn package_dir(&self, lang: extras::Language) -> String {
284 let override_path = match lang {
285 extras::Language::Python => self.python.as_ref().and_then(|c| c.scaffold_output.as_ref()),
286 extras::Language::Node => self.node.as_ref().and_then(|c| c.scaffold_output.as_ref()),
287 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.scaffold_output.as_ref()),
288 extras::Language::Php => self.php.as_ref().and_then(|c| c.scaffold_output.as_ref()),
289 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.scaffold_output.as_ref()),
290 _ => None,
291 };
292 if let Some(p) = override_path {
293 p.to_string_lossy().to_string()
294 } else {
295 match lang {
296 extras::Language::Python => "packages/python".to_string(),
297 extras::Language::Node => "packages/node".to_string(),
298 extras::Language::Ruby => "packages/ruby".to_string(),
299 extras::Language::Php => "packages/php".to_string(),
300 extras::Language::Elixir => "packages/elixir".to_string(),
301 _ => format!("packages/{lang}"),
302 }
303 }
304 }
305
306 pub fn lint_config_for_language(&self, lang: extras::Language) -> output::LintConfig {
311 if let Some(lint_map) = &self.lint {
312 let lang_str = lang.to_string();
313 if let Some(explicit) = lint_map.get(&lang_str) {
314 return explicit.clone();
315 }
316 }
317 let output_dir = self.package_dir(lang);
318 lint_defaults::default_lint_config(lang, &output_dir)
319 }
320
321 pub fn update_config_for_language(&self, lang: extras::Language) -> output::UpdateConfig {
326 if let Some(update_map) = &self.update {
327 let lang_str = lang.to_string();
328 if let Some(explicit) = update_map.get(&lang_str) {
329 return explicit.clone();
330 }
331 }
332 let output_dir = self.package_dir(lang);
333 update_defaults::default_update_config(lang, &output_dir)
334 }
335
336 pub fn test_config_for_language(&self, lang: extras::Language) -> output::TestConfig {
341 if let Some(test_map) = &self.test {
342 let lang_str = lang.to_string();
343 if let Some(explicit) = test_map.get(&lang_str) {
344 return explicit.clone();
345 }
346 }
347 let output_dir = self.package_dir(lang);
348 test_defaults::default_test_config(lang, &output_dir)
349 }
350
351 pub fn setup_config_for_language(&self, lang: extras::Language) -> output::SetupConfig {
356 if let Some(setup_map) = &self.setup {
357 let lang_str = lang.to_string();
358 if let Some(explicit) = setup_map.get(&lang_str) {
359 return explicit.clone();
360 }
361 }
362 let output_dir = self.package_dir(lang);
363 setup_defaults::default_setup_config(lang, &output_dir)
364 }
365
366 pub fn clean_config_for_language(&self, lang: extras::Language) -> output::CleanConfig {
371 if let Some(clean_map) = &self.clean {
372 let lang_str = lang.to_string();
373 if let Some(explicit) = clean_map.get(&lang_str) {
374 return explicit.clone();
375 }
376 }
377 let output_dir = self.package_dir(lang);
378 clean_defaults::default_clean_config(lang, &output_dir)
379 }
380
381 pub fn build_command_config_for_language(&self, lang: extras::Language) -> output::BuildCommandConfig {
386 if let Some(build_map) = &self.build_commands {
387 let lang_str = lang.to_string();
388 if let Some(explicit) = build_map.get(&lang_str) {
389 return explicit.clone();
390 }
391 }
392 let output_dir = self.package_dir(lang);
393 let crate_name = &self.crate_config.name;
394 build_defaults::default_build_config(lang, &output_dir, crate_name)
395 }
396
397 pub fn core_import(&self) -> String {
399 self.crate_config
400 .core_import
401 .clone()
402 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
403 }
404
405 pub fn error_type(&self) -> String {
407 self.crate_config
408 .error_type
409 .clone()
410 .unwrap_or_else(|| "Error".to_string())
411 }
412
413 pub fn error_constructor(&self) -> String {
416 self.crate_config
417 .error_constructor
418 .clone()
419 .unwrap_or_else(|| format!("{}::{}::from({{msg}})", self.core_import(), self.error_type()))
420 }
421
422 pub fn ffi_prefix(&self) -> String {
424 self.ffi
425 .as_ref()
426 .and_then(|f| f.prefix.as_ref())
427 .cloned()
428 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
429 }
430
431 pub fn ffi_lib_name(&self) -> String {
439 if let Some(name) = self.ffi.as_ref().and_then(|f| f.lib_name.as_ref()) {
441 return name.clone();
442 }
443
444 if let Some(ffi_path) = self.output.ffi.as_ref() {
447 let path = std::path::Path::new(ffi_path);
448 let components: Vec<_> = path
451 .components()
452 .filter_map(|c| {
453 if let std::path::Component::Normal(s) = c {
454 s.to_str()
455 } else {
456 None
457 }
458 })
459 .collect();
460 let crate_dir = components
463 .iter()
464 .rev()
465 .find(|&&s| s != "src" && s != "lib" && s != "include")
466 .copied();
467 if let Some(dir) = crate_dir {
468 return dir.replace('-', "_");
469 }
470 }
471
472 format!("{}_ffi", self.ffi_prefix())
474 }
475
476 pub fn ffi_header_name(&self) -> String {
478 self.ffi
479 .as_ref()
480 .and_then(|f| f.header_name.as_ref())
481 .cloned()
482 .unwrap_or_else(|| format!("{}.h", self.ffi_prefix()))
483 }
484
485 pub fn python_module_name(&self) -> String {
487 self.python
488 .as_ref()
489 .and_then(|p| p.module_name.as_ref())
490 .cloned()
491 .unwrap_or_else(|| format!("_{}", self.crate_config.name.replace('-', "_")))
492 }
493
494 pub fn python_pip_name(&self) -> String {
498 self.python
499 .as_ref()
500 .and_then(|p| p.pip_name.as_ref())
501 .cloned()
502 .unwrap_or_else(|| self.crate_config.name.clone())
503 }
504
505 pub fn php_autoload_namespace(&self) -> String {
510 use heck::ToPascalCase;
511 let ext = self.php_extension_name();
512 if ext.contains('_') {
513 ext.split('_')
514 .map(|p| p.to_pascal_case())
515 .collect::<Vec<_>>()
516 .join("\\")
517 } else {
518 ext.to_pascal_case()
519 }
520 }
521
522 pub fn node_package_name(&self) -> String {
524 self.node
525 .as_ref()
526 .and_then(|n| n.package_name.as_ref())
527 .cloned()
528 .unwrap_or_else(|| self.crate_config.name.clone())
529 }
530
531 pub fn ruby_gem_name(&self) -> String {
533 self.ruby
534 .as_ref()
535 .and_then(|r| r.gem_name.as_ref())
536 .cloned()
537 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
538 }
539
540 pub fn php_extension_name(&self) -> String {
542 self.php
543 .as_ref()
544 .and_then(|p| p.extension_name.as_ref())
545 .cloned()
546 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
547 }
548
549 pub fn elixir_app_name(&self) -> String {
551 self.elixir
552 .as_ref()
553 .and_then(|e| e.app_name.as_ref())
554 .cloned()
555 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
556 }
557
558 pub fn go_module(&self) -> String {
560 self.go
561 .as_ref()
562 .and_then(|g| g.module.as_ref())
563 .cloned()
564 .unwrap_or_else(|| format!("github.com/kreuzberg-dev/{}", self.crate_config.name))
565 }
566
567 pub fn github_repo(&self) -> String {
574 if let Some(e2e) = &self.e2e {
575 if let Some(url) = &e2e.registry.github_repo {
576 return url.clone();
577 }
578 }
579 self.scaffold
580 .as_ref()
581 .and_then(|s| s.repository.as_ref())
582 .cloned()
583 .unwrap_or_else(|| format!("https://github.com/kreuzberg-dev/{}", self.crate_config.name))
584 }
585
586 pub fn java_package(&self) -> String {
588 self.java
589 .as_ref()
590 .and_then(|j| j.package.as_ref())
591 .cloned()
592 .unwrap_or_else(|| "dev.kreuzberg".to_string())
593 }
594
595 pub fn java_group_id(&self) -> String {
600 self.java_package()
601 }
602
603 pub fn csharp_namespace(&self) -> String {
605 self.csharp
606 .as_ref()
607 .and_then(|c| c.namespace.as_ref())
608 .cloned()
609 .unwrap_or_else(|| {
610 use heck::ToPascalCase;
611 self.crate_config.name.to_pascal_case()
612 })
613 }
614
615 pub fn core_crate_dir(&self) -> String {
621 if let Some(first_source) = self.crate_config.sources.first() {
624 let path = std::path::Path::new(first_source);
625 let mut current = path.parent();
626 while let Some(dir) = current {
627 if dir.file_name().is_some_and(|n| n == "src") {
628 if let Some(crate_dir) = dir.parent() {
629 if let Some(dir_name) = crate_dir.file_name() {
630 return dir_name.to_string_lossy().into_owned();
631 }
632 }
633 break;
634 }
635 current = dir.parent();
636 }
637 }
638 self.crate_config.name.clone()
639 }
640
641 pub fn wasm_type_prefix(&self) -> String {
644 self.wasm
645 .as_ref()
646 .and_then(|w| w.type_prefix.as_ref())
647 .cloned()
648 .unwrap_or_else(|| "Wasm".to_string())
649 }
650
651 pub fn node_type_prefix(&self) -> String {
654 self.node
655 .as_ref()
656 .and_then(|n| n.type_prefix.as_ref())
657 .cloned()
658 .unwrap_or_else(|| "Js".to_string())
659 }
660
661 pub fn r_package_name(&self) -> String {
663 self.r
664 .as_ref()
665 .and_then(|r| r.package_name.as_ref())
666 .cloned()
667 .unwrap_or_else(|| self.crate_config.name.clone())
668 }
669
670 pub fn resolved_version(&self) -> Option<String> {
673 let content = std::fs::read_to_string(&self.crate_config.version_from).ok()?;
674 let value: toml::Value = toml::from_str(&content).ok()?;
675 if let Some(v) = value
676 .get("workspace")
677 .and_then(|w| w.get("package"))
678 .and_then(|p| p.get("version"))
679 .and_then(|v| v.as_str())
680 {
681 return Some(v.to_string());
682 }
683 value
684 .get("package")
685 .and_then(|p| p.get("version"))
686 .and_then(|v| v.as_str())
687 .map(|v| v.to_string())
688 }
689
690 pub fn serde_rename_all_for_language(&self, lang: extras::Language) -> String {
698 let override_val = match lang {
700 extras::Language::Python => self.python.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
701 extras::Language::Node => self.node.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
702 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
703 extras::Language::Php => self.php.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
704 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
705 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
706 extras::Language::Ffi => self.ffi.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
707 extras::Language::Go => self.go.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
708 extras::Language::Java => self.java.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
709 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
710 extras::Language::R => self.r.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
711 extras::Language::Rust => None, };
713
714 if let Some(val) = override_val {
715 return val.to_string();
716 }
717
718 match lang {
720 extras::Language::Node | extras::Language::Wasm | extras::Language::Java | extras::Language::Csharp => {
721 "camelCase".to_string()
722 }
723 extras::Language::Python
724 | extras::Language::Ruby
725 | extras::Language::Php
726 | extras::Language::Go
727 | extras::Language::Ffi
728 | extras::Language::Elixir
729 | extras::Language::R
730 | extras::Language::Rust => "snake_case".to_string(),
731 }
732 }
733
734 pub fn rewrite_path(&self, rust_path: &str) -> String {
737 let mut mappings: Vec<_> = self.crate_config.path_mappings.iter().collect();
739 mappings.sort_by_key(|b| std::cmp::Reverse(b.0.len()));
740
741 for (from, to) in &mappings {
742 if rust_path.starts_with(from.as_str()) {
743 return format!("{}{}", to, &rust_path[from.len()..]);
744 }
745 }
746 rust_path.to_string()
747 }
748
749 pub fn effective_path_mappings(&self) -> HashMap<String, String> {
759 let mut mappings = HashMap::new();
760
761 if self.crate_config.auto_path_mappings {
762 let core_import = self.core_import();
763
764 for source in &self.crate_config.sources {
765 let source_str = source.to_string_lossy();
766 if let Some(after_crates) = find_after_crates_prefix(&source_str) {
768 if let Some(slash_pos) = after_crates.find('/') {
770 let crate_dir = &after_crates[..slash_pos];
771 let crate_ident = crate_dir.replace('-', "_");
772 if crate_ident != core_import && !mappings.contains_key(&crate_ident) {
774 mappings.insert(crate_ident, core_import.clone());
775 }
776 }
777 }
778 }
779 }
780
781 for (from, to) in &self.crate_config.path_mappings {
783 mappings.insert(from.clone(), to.clone());
784 }
785
786 mappings
787 }
788}
789
790fn find_after_crates_prefix(path: &str) -> Option<&str> {
797 if let Some(pos) = path.find("/crates/") {
801 return Some(&path[pos + "/crates/".len()..]);
802 }
803 if let Some(stripped) = path.strip_prefix("crates/") {
804 return Some(stripped);
805 }
806 None
807}
808
809pub fn resolve_output_dir(config_path: Option<&PathBuf>, crate_name: &str, default: &str) -> String {
812 config_path
813 .map(|p| p.to_string_lossy().replace("{name}", crate_name))
814 .unwrap_or_else(|| default.replace("{name}", crate_name))
815}
816
817pub fn detect_serde_available(output_dir: &str) -> bool {
823 let src_path = std::path::Path::new(output_dir);
824 let mut dir = src_path;
826 loop {
827 let cargo_toml = dir.join("Cargo.toml");
828 if cargo_toml.exists() {
829 return cargo_toml_has_serde(&cargo_toml);
830 }
831 match dir.parent() {
832 Some(parent) if !parent.as_os_str().is_empty() => dir = parent,
833 _ => break,
834 }
835 }
836 false
837}
838
839fn cargo_toml_has_serde(path: &std::path::Path) -> bool {
845 let content = match std::fs::read_to_string(path) {
846 Ok(c) => c,
847 Err(_) => return false,
848 };
849
850 let has_serde_json = content.contains("serde_json");
851 let has_serde_dep = content.lines().any(|line| {
855 let trimmed = line.trim();
856 trimmed.starts_with("serde ")
858 || trimmed.starts_with("serde=")
859 || trimmed.starts_with("serde.")
860 || trimmed == "[dependencies.serde]"
861 });
862
863 has_serde_json && has_serde_dep
864}
865
866#[cfg(test)]
867mod tests {
868 use super::*;
869
870 fn minimal_config() -> AlefConfig {
871 toml::from_str(
872 r#"
873languages = ["python", "node", "rust"]
874
875[crate]
876name = "test-lib"
877sources = ["src/lib.rs"]
878"#,
879 )
880 .unwrap()
881 }
882
883 #[test]
884 fn lint_config_falls_back_to_defaults() {
885 let config = minimal_config();
886 assert!(config.lint.is_none());
887
888 let py = config.lint_config_for_language(Language::Python);
889 assert!(py.format.is_some());
890 assert!(py.check.is_some());
891 assert!(py.typecheck.is_some());
892
893 let node = config.lint_config_for_language(Language::Node);
894 assert!(node.format.is_some());
895 assert!(node.check.is_some());
896 }
897
898 #[test]
899 fn lint_config_explicit_overrides_default() {
900 let config: AlefConfig = toml::from_str(
901 r#"
902languages = ["python"]
903
904[crate]
905name = "test-lib"
906sources = ["src/lib.rs"]
907
908[lint.python]
909format = "custom-formatter"
910check = "custom-checker"
911"#,
912 )
913 .unwrap();
914
915 let py = config.lint_config_for_language(Language::Python);
916 assert_eq!(py.format.unwrap().commands(), vec!["custom-formatter"]);
917 assert_eq!(py.check.unwrap().commands(), vec!["custom-checker"]);
918 assert!(py.typecheck.is_none()); }
920
921 #[test]
922 fn lint_config_partial_override_does_not_merge() {
923 let config: AlefConfig = toml::from_str(
924 r#"
925languages = ["python"]
926
927[crate]
928name = "test-lib"
929sources = ["src/lib.rs"]
930
931[lint.python]
932format = "only-format"
933"#,
934 )
935 .unwrap();
936
937 let py = config.lint_config_for_language(Language::Python);
938 assert_eq!(py.format.unwrap().commands(), vec!["only-format"]);
939 assert!(py.check.is_none());
941 assert!(py.typecheck.is_none());
942 }
943
944 #[test]
945 fn lint_config_unconfigured_language_uses_defaults() {
946 let config: AlefConfig = toml::from_str(
947 r#"
948languages = ["python", "node"]
949
950[crate]
951name = "test-lib"
952sources = ["src/lib.rs"]
953
954[lint.python]
955format = "custom"
956"#,
957 )
958 .unwrap();
959
960 let py = config.lint_config_for_language(Language::Python);
962 assert_eq!(py.format.unwrap().commands(), vec!["custom"]);
963
964 let node = config.lint_config_for_language(Language::Node);
966 let fmt = node.format.unwrap().commands().join(" ");
967 assert!(fmt.contains("oxfmt"));
968 }
969
970 #[test]
971 fn update_config_falls_back_to_defaults() {
972 let config = minimal_config();
973 assert!(config.update.is_none());
974
975 let py = config.update_config_for_language(Language::Python);
976 assert!(py.update.is_some());
977 assert!(py.upgrade.is_some());
978
979 let rust = config.update_config_for_language(Language::Rust);
980 let update = rust.update.unwrap().commands().join(" ");
981 assert!(update.contains("cargo update"));
982 }
983
984 #[test]
985 fn update_config_explicit_overrides_default() {
986 let config: AlefConfig = toml::from_str(
987 r#"
988languages = ["rust"]
989
990[crate]
991name = "test-lib"
992sources = ["src/lib.rs"]
993
994[update.rust]
995update = "my-custom-update"
996upgrade = ["step1", "step2"]
997"#,
998 )
999 .unwrap();
1000
1001 let rust = config.update_config_for_language(Language::Rust);
1002 assert_eq!(rust.update.unwrap().commands(), vec!["my-custom-update"]);
1003 assert_eq!(rust.upgrade.unwrap().commands(), vec!["step1", "step2"]);
1004 }
1005
1006 #[test]
1007 fn test_config_falls_back_to_defaults() {
1008 let config = minimal_config();
1009 assert!(config.test.is_none());
1010
1011 let py = config.test_config_for_language(Language::Python);
1012 assert!(py.command.is_some());
1013 assert!(py.coverage.is_some());
1014 assert!(py.e2e.is_none());
1015
1016 let rust = config.test_config_for_language(Language::Rust);
1017 let cmd = rust.command.unwrap().commands().join(" ");
1018 assert!(cmd.contains("cargo test"));
1019 }
1020
1021 #[test]
1022 fn test_config_explicit_overrides_default() {
1023 let config: AlefConfig = toml::from_str(
1024 r#"
1025languages = ["python"]
1026
1027[crate]
1028name = "test-lib"
1029sources = ["src/lib.rs"]
1030
1031[test.python]
1032command = "my-custom-test"
1033"#,
1034 )
1035 .unwrap();
1036
1037 let py = config.test_config_for_language(Language::Python);
1038 assert_eq!(py.command.unwrap().commands(), vec!["my-custom-test"]);
1039 assert!(py.coverage.is_none()); }
1041
1042 #[test]
1043 fn setup_config_falls_back_to_defaults() {
1044 let config = minimal_config();
1045 assert!(config.setup.is_none());
1046
1047 let py = config.setup_config_for_language(Language::Python);
1048 assert!(py.install.is_some());
1049 let install = py.install.unwrap().commands().join(" ");
1050 assert!(install.contains("uv sync"));
1051
1052 let rust = config.setup_config_for_language(Language::Rust);
1053 let install = rust.install.unwrap().commands().join(" ");
1054 assert!(install.contains("rustup update"));
1055 }
1056
1057 #[test]
1058 fn setup_config_explicit_overrides_default() {
1059 let config: AlefConfig = toml::from_str(
1060 r#"
1061languages = ["python"]
1062
1063[crate]
1064name = "test-lib"
1065sources = ["src/lib.rs"]
1066
1067[setup.python]
1068install = "my-custom-install"
1069"#,
1070 )
1071 .unwrap();
1072
1073 let py = config.setup_config_for_language(Language::Python);
1074 assert_eq!(py.install.unwrap().commands(), vec!["my-custom-install"]);
1075 }
1076
1077 #[test]
1078 fn clean_config_falls_back_to_defaults() {
1079 let config = minimal_config();
1080 assert!(config.clean.is_none());
1081
1082 let py = config.clean_config_for_language(Language::Python);
1083 assert!(py.clean.is_some());
1084 let clean = py.clean.unwrap().commands().join(" ");
1085 assert!(clean.contains("__pycache__"));
1086
1087 let rust = config.clean_config_for_language(Language::Rust);
1088 let clean = rust.clean.unwrap().commands().join(" ");
1089 assert!(clean.contains("cargo clean"));
1090 }
1091
1092 #[test]
1093 fn clean_config_explicit_overrides_default() {
1094 let config: AlefConfig = toml::from_str(
1095 r#"
1096languages = ["rust"]
1097
1098[crate]
1099name = "test-lib"
1100sources = ["src/lib.rs"]
1101
1102[clean.rust]
1103clean = "my-custom-clean"
1104"#,
1105 )
1106 .unwrap();
1107
1108 let rust = config.clean_config_for_language(Language::Rust);
1109 assert_eq!(rust.clean.unwrap().commands(), vec!["my-custom-clean"]);
1110 }
1111
1112 #[test]
1113 fn build_command_config_falls_back_to_defaults() {
1114 let config = minimal_config();
1115 assert!(config.build_commands.is_none());
1116
1117 let py = config.build_command_config_for_language(Language::Python);
1118 assert!(py.build.is_some());
1119 assert!(py.build_release.is_some());
1120 let build = py.build.unwrap().commands().join(" ");
1121 assert!(build.contains("maturin develop"));
1122
1123 let rust = config.build_command_config_for_language(Language::Rust);
1124 let build = rust.build.unwrap().commands().join(" ");
1125 assert!(build.contains("cargo build --workspace"));
1126 }
1127
1128 #[test]
1129 fn build_command_config_explicit_overrides_default() {
1130 let config: AlefConfig = toml::from_str(
1131 r#"
1132languages = ["rust"]
1133
1134[crate]
1135name = "test-lib"
1136sources = ["src/lib.rs"]
1137
1138[build_commands.rust]
1139build = "my-custom-build"
1140build_release = "my-custom-build --release"
1141"#,
1142 )
1143 .unwrap();
1144
1145 let rust = config.build_command_config_for_language(Language::Rust);
1146 assert_eq!(rust.build.unwrap().commands(), vec!["my-custom-build"]);
1147 assert_eq!(
1148 rust.build_release.unwrap().commands(),
1149 vec!["my-custom-build --release"]
1150 );
1151 }
1152
1153 #[test]
1154 fn build_command_config_uses_crate_name() {
1155 let config = minimal_config();
1156 let py = config.build_command_config_for_language(Language::Python);
1157 let build = py.build.unwrap().commands().join(" ");
1158 assert!(
1159 build.contains("test-lib-py"),
1160 "Python build should reference crate name, got: {build}"
1161 );
1162 }
1163
1164 #[test]
1165 fn package_dir_defaults_are_correct() {
1166 let config = minimal_config();
1167 assert_eq!(config.package_dir(Language::Python), "packages/python");
1168 assert_eq!(config.package_dir(Language::Node), "packages/node");
1169 assert_eq!(config.package_dir(Language::Ruby), "packages/ruby");
1170 assert_eq!(config.package_dir(Language::Go), "packages/go");
1171 assert_eq!(config.package_dir(Language::Java), "packages/java");
1172 }
1173
1174 #[test]
1175 fn explicit_lint_config_preserves_precondition_and_before() {
1176 let config: AlefConfig = toml::from_str(
1177 r#"
1178languages = ["go"]
1179
1180[crate]
1181name = "test"
1182sources = ["src/lib.rs"]
1183
1184[lint.go]
1185precondition = "test -f target/release/libtest_ffi.so"
1186before = "cargo build --release -p test-ffi"
1187format = "gofmt -w packages/go"
1188check = "golangci-lint run ./..."
1189"#,
1190 )
1191 .unwrap();
1192
1193 let lint = config.lint_config_for_language(Language::Go);
1194 assert_eq!(
1195 lint.precondition.as_deref(),
1196 Some("test -f target/release/libtest_ffi.so"),
1197 "precondition should be preserved from explicit config"
1198 );
1199 assert_eq!(
1200 lint.before.unwrap().commands(),
1201 vec!["cargo build --release -p test-ffi"],
1202 "before should be preserved from explicit config"
1203 );
1204 }
1205
1206 #[test]
1207 fn explicit_lint_config_with_before_list_preserves_all_commands() {
1208 let config: AlefConfig = toml::from_str(
1209 r#"
1210languages = ["go"]
1211
1212[crate]
1213name = "test"
1214sources = ["src/lib.rs"]
1215
1216[lint.go]
1217before = ["cargo build --release -p test-ffi", "cp target/release/libtest_ffi.so packages/go/"]
1218check = "golangci-lint run ./..."
1219"#,
1220 )
1221 .unwrap();
1222
1223 let lint = config.lint_config_for_language(Language::Go);
1224 assert!(lint.precondition.is_none(), "precondition should be None when not set");
1225 assert_eq!(
1226 lint.before.unwrap().commands(),
1227 vec![
1228 "cargo build --release -p test-ffi",
1229 "cp target/release/libtest_ffi.so packages/go/"
1230 ],
1231 "before list should be preserved from explicit config"
1232 );
1233 }
1234
1235 #[test]
1236 fn default_lint_config_has_no_precondition_or_before() {
1237 let config = minimal_config();
1238 let py = config.lint_config_for_language(Language::Python);
1239 assert!(
1240 py.precondition.is_none(),
1241 "default lint config should have no precondition"
1242 );
1243 assert!(py.before.is_none(), "default lint config should have no before");
1244
1245 let go = config.lint_config_for_language(Language::Go);
1246 assert!(
1247 go.precondition.is_none(),
1248 "default Go lint config should have no precondition"
1249 );
1250 assert!(go.before.is_none(), "default Go lint config should have no before");
1251 }
1252
1253 #[test]
1254 fn explicit_test_config_preserves_precondition_and_before() {
1255 let config: AlefConfig = toml::from_str(
1256 r#"
1257languages = ["python"]
1258
1259[crate]
1260name = "test"
1261sources = ["src/lib.rs"]
1262
1263[test.python]
1264precondition = "test -f target/release/libtest.so"
1265before = "maturin develop"
1266command = "pytest"
1267"#,
1268 )
1269 .unwrap();
1270
1271 let test = config.test_config_for_language(Language::Python);
1272 assert_eq!(
1273 test.precondition.as_deref(),
1274 Some("test -f target/release/libtest.so"),
1275 "test precondition should be preserved"
1276 );
1277 assert_eq!(
1278 test.before.unwrap().commands(),
1279 vec!["maturin develop"],
1280 "test before should be preserved"
1281 );
1282 }
1283
1284 #[test]
1285 fn default_test_config_has_no_precondition_or_before() {
1286 let config = minimal_config();
1287 let py = config.test_config_for_language(Language::Python);
1288 assert!(
1289 py.precondition.is_none(),
1290 "default test config should have no precondition"
1291 );
1292 assert!(py.before.is_none(), "default test config should have no before");
1293 }
1294
1295 #[test]
1296 fn explicit_setup_config_preserves_precondition_and_before() {
1297 let config: AlefConfig = toml::from_str(
1298 r#"
1299languages = ["python"]
1300
1301[crate]
1302name = "test"
1303sources = ["src/lib.rs"]
1304
1305[setup.python]
1306precondition = "which uv"
1307before = "pip install uv"
1308install = "uv sync"
1309"#,
1310 )
1311 .unwrap();
1312
1313 let setup = config.setup_config_for_language(Language::Python);
1314 assert_eq!(
1315 setup.precondition.as_deref(),
1316 Some("which uv"),
1317 "setup precondition should be preserved"
1318 );
1319 assert_eq!(
1320 setup.before.unwrap().commands(),
1321 vec!["pip install uv"],
1322 "setup before should be preserved"
1323 );
1324 }
1325
1326 #[test]
1327 fn default_setup_config_has_no_precondition_or_before() {
1328 let config = minimal_config();
1329 let py = config.setup_config_for_language(Language::Python);
1330 assert!(
1331 py.precondition.is_none(),
1332 "default setup config should have no precondition"
1333 );
1334 assert!(py.before.is_none(), "default setup config should have no before");
1335 }
1336
1337 #[test]
1338 fn explicit_update_config_preserves_precondition_and_before() {
1339 let config: AlefConfig = toml::from_str(
1340 r#"
1341languages = ["rust"]
1342
1343[crate]
1344name = "test"
1345sources = ["src/lib.rs"]
1346
1347[update.rust]
1348precondition = "test -f Cargo.lock"
1349before = "cargo fetch"
1350update = "cargo update"
1351"#,
1352 )
1353 .unwrap();
1354
1355 let update = config.update_config_for_language(Language::Rust);
1356 assert_eq!(
1357 update.precondition.as_deref(),
1358 Some("test -f Cargo.lock"),
1359 "update precondition should be preserved"
1360 );
1361 assert_eq!(
1362 update.before.unwrap().commands(),
1363 vec!["cargo fetch"],
1364 "update before should be preserved"
1365 );
1366 }
1367
1368 #[test]
1369 fn default_update_config_has_no_precondition_or_before() {
1370 let config = minimal_config();
1371 let rust = config.update_config_for_language(Language::Rust);
1372 assert!(
1373 rust.precondition.is_none(),
1374 "default update config should have no precondition"
1375 );
1376 assert!(rust.before.is_none(), "default update config should have no before");
1377 }
1378
1379 #[test]
1380 fn explicit_clean_config_preserves_precondition_and_before() {
1381 let config: AlefConfig = toml::from_str(
1382 r#"
1383languages = ["rust"]
1384
1385[crate]
1386name = "test"
1387sources = ["src/lib.rs"]
1388
1389[clean.rust]
1390precondition = "test -d target"
1391before = "echo cleaning"
1392clean = "cargo clean"
1393"#,
1394 )
1395 .unwrap();
1396
1397 let clean = config.clean_config_for_language(Language::Rust);
1398 assert_eq!(
1399 clean.precondition.as_deref(),
1400 Some("test -d target"),
1401 "clean precondition should be preserved"
1402 );
1403 assert_eq!(
1404 clean.before.unwrap().commands(),
1405 vec!["echo cleaning"],
1406 "clean before should be preserved"
1407 );
1408 }
1409
1410 #[test]
1411 fn default_clean_config_has_no_precondition_or_before() {
1412 let config = minimal_config();
1413 let rust = config.clean_config_for_language(Language::Rust);
1414 assert!(
1415 rust.precondition.is_none(),
1416 "default clean config should have no precondition"
1417 );
1418 assert!(rust.before.is_none(), "default clean config should have no before");
1419 }
1420
1421 #[test]
1422 fn explicit_build_command_config_preserves_precondition_and_before() {
1423 let config: AlefConfig = toml::from_str(
1424 r#"
1425languages = ["go"]
1426
1427[crate]
1428name = "test"
1429sources = ["src/lib.rs"]
1430
1431[build_commands.go]
1432precondition = "which go"
1433before = "cargo build --release -p test-ffi"
1434build = "cd packages/go && go build ./..."
1435build_release = "cd packages/go && go build -ldflags='-s -w' ./..."
1436"#,
1437 )
1438 .unwrap();
1439
1440 let build = config.build_command_config_for_language(Language::Go);
1441 assert_eq!(
1442 build.precondition.as_deref(),
1443 Some("which go"),
1444 "build precondition should be preserved"
1445 );
1446 assert_eq!(
1447 build.before.unwrap().commands(),
1448 vec!["cargo build --release -p test-ffi"],
1449 "build before should be preserved"
1450 );
1451 }
1452
1453 #[test]
1454 fn default_build_command_config_has_no_precondition_or_before() {
1455 let config = minimal_config();
1456 let rust = config.build_command_config_for_language(Language::Rust);
1457 assert!(
1458 rust.precondition.is_none(),
1459 "default build command config should have no precondition"
1460 );
1461 assert!(
1462 rust.before.is_none(),
1463 "default build command config should have no before"
1464 );
1465 }
1466}