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 publish;
14pub mod setup_defaults;
15pub mod test_defaults;
16pub mod trait_bridge;
17pub mod update_defaults;
18
19pub use dto::{
21 CsharpDtoStyle, DtoConfig, ElixirDtoStyle, GoDtoStyle, JavaDtoStyle, NodeDtoStyle, PhpDtoStyle, PythonDtoStyle,
22 RDtoStyle, RubyDtoStyle,
23};
24pub use e2e::E2eConfig;
25pub use extras::{AdapterConfig, AdapterParam, AdapterPattern, Language};
26pub use languages::{
27 CSharpConfig, CustomModulesConfig, CustomRegistration, CustomRegistrationsConfig, ElixirConfig, FfiConfig,
28 GoConfig, JavaConfig, NodeConfig, PhpConfig, PythonConfig, RConfig, RubyConfig, StubsConfig, WasmConfig,
29};
30pub use output::{
31 BuildCommandConfig, CleanConfig, ExcludeConfig, IncludeConfig, LintConfig, OutputConfig, ReadmeConfig,
32 ScaffoldConfig, SetupConfig, SyncConfig, TestConfig, TextReplacement, UpdateConfig,
33};
34pub use publish::{PublishConfig, PublishLanguageConfig, VendorMode};
35pub use trait_bridge::TraitBridgeConfig;
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct AlefConfig {
40 #[serde(default)]
43 pub version: Option<String>,
44 #[serde(rename = "crate")]
45 pub crate_config: CrateConfig,
46 pub languages: Vec<Language>,
47 #[serde(default)]
48 pub exclude: ExcludeConfig,
49 #[serde(default)]
50 pub include: IncludeConfig,
51 #[serde(default)]
52 pub output: OutputConfig,
53 #[serde(default)]
54 pub python: Option<PythonConfig>,
55 #[serde(default)]
56 pub node: Option<NodeConfig>,
57 #[serde(default)]
58 pub ruby: Option<RubyConfig>,
59 #[serde(default)]
60 pub php: Option<PhpConfig>,
61 #[serde(default)]
62 pub elixir: Option<ElixirConfig>,
63 #[serde(default)]
64 pub wasm: Option<WasmConfig>,
65 #[serde(default)]
66 pub ffi: Option<FfiConfig>,
67 #[serde(default)]
68 pub go: Option<GoConfig>,
69 #[serde(default)]
70 pub java: Option<JavaConfig>,
71 #[serde(default)]
72 pub csharp: Option<CSharpConfig>,
73 #[serde(default)]
74 pub r: Option<RConfig>,
75 #[serde(default)]
76 pub scaffold: Option<ScaffoldConfig>,
77 #[serde(default)]
78 pub readme: Option<ReadmeConfig>,
79 #[serde(default)]
80 pub lint: Option<HashMap<String, LintConfig>>,
81 #[serde(default)]
82 pub update: Option<HashMap<String, UpdateConfig>>,
83 #[serde(default)]
84 pub test: Option<HashMap<String, TestConfig>>,
85 #[serde(default)]
86 pub setup: Option<HashMap<String, SetupConfig>>,
87 #[serde(default)]
88 pub clean: Option<HashMap<String, CleanConfig>>,
89 #[serde(default)]
90 pub build_commands: Option<HashMap<String, BuildCommandConfig>>,
91 #[serde(default)]
93 pub publish: Option<PublishConfig>,
94 #[serde(default)]
95 pub custom_files: Option<HashMap<String, Vec<PathBuf>>>,
96 #[serde(default)]
97 pub adapters: Vec<AdapterConfig>,
98 #[serde(default)]
99 pub custom_modules: CustomModulesConfig,
100 #[serde(default)]
101 pub custom_registrations: CustomRegistrationsConfig,
102 #[serde(default)]
103 pub sync: Option<SyncConfig>,
104 #[serde(default)]
108 pub opaque_types: HashMap<String, String>,
109 #[serde(default)]
111 pub generate: GenerateConfig,
112 #[serde(default)]
114 pub generate_overrides: HashMap<String, GenerateConfig>,
115 #[serde(default)]
117 pub dto: DtoConfig,
118 #[serde(default)]
120 pub e2e: Option<E2eConfig>,
121 #[serde(default)]
124 pub trait_bridges: Vec<TraitBridgeConfig>,
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct CrateConfig {
129 pub name: String,
130 pub sources: Vec<PathBuf>,
131 #[serde(default = "default_version_from")]
132 pub version_from: String,
133 #[serde(default)]
134 pub core_import: Option<String>,
135 #[serde(default)]
137 pub workspace_root: Option<PathBuf>,
138 #[serde(default)]
140 pub skip_core_import: bool,
141 #[serde(default)]
145 pub error_type: Option<String>,
146 #[serde(default)]
151 pub error_constructor: Option<String>,
152 #[serde(default)]
156 pub features: Vec<String>,
157 #[serde(default)]
160 pub path_mappings: HashMap<String, String>,
161 #[serde(default)]
165 pub extra_dependencies: HashMap<String, toml::Value>,
166 #[serde(default = "default_true")]
170 pub auto_path_mappings: bool,
171 #[serde(default)]
176 pub source_crates: Vec<SourceCrate>,
177}
178
179#[derive(Debug, Clone, Serialize, Deserialize)]
181pub struct SourceCrate {
182 pub name: String,
184 pub sources: Vec<PathBuf>,
186}
187
188fn default_version_from() -> String {
189 "Cargo.toml".to_string()
190}
191
192fn default_true() -> bool {
193 true
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct GenerateConfig {
201 #[serde(default = "default_true")]
203 pub bindings: bool,
204 #[serde(default = "default_true")]
206 pub errors: bool,
207 #[serde(default = "default_true")]
209 pub configs: bool,
210 #[serde(default = "default_true")]
212 pub async_wrappers: bool,
213 #[serde(default = "default_true")]
215 pub type_conversions: bool,
216 #[serde(default = "default_true")]
218 pub package_metadata: bool,
219 #[serde(default = "default_true")]
221 pub public_api: bool,
222 #[serde(default = "default_true")]
225 pub reverse_conversions: bool,
226}
227
228impl Default for GenerateConfig {
229 fn default() -> Self {
230 Self {
231 bindings: true,
232 errors: true,
233 configs: true,
234 async_wrappers: true,
235 type_conversions: true,
236 package_metadata: true,
237 public_api: true,
238 reverse_conversions: true,
239 }
240 }
241}
242
243impl AlefConfig {
248 pub fn resolve_field_name(&self, lang: extras::Language, type_name: &str, field_name: &str) -> Option<String> {
260 let explicit_key = format!("{type_name}.{field_name}");
262 let explicit = match lang {
263 extras::Language::Python => self.python.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
264 extras::Language::Node => self.node.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
265 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
266 extras::Language::Php => self.php.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
267 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
268 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
269 extras::Language::Ffi => self.ffi.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
270 extras::Language::Go => self.go.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
271 extras::Language::Java => self.java.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
272 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
273 extras::Language::R => self.r.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
274 extras::Language::Rust => None,
275 };
276 if let Some(renamed) = explicit {
277 if renamed != field_name {
278 return Some(renamed.clone());
279 }
280 return None;
281 }
282
283 match lang {
285 extras::Language::Python => crate::keywords::python_safe_name(field_name),
286 _ => None,
291 }
292 }
293
294 pub fn features_for_language(&self, lang: extras::Language) -> &[String] {
297 let override_features = match lang {
298 extras::Language::Python => self.python.as_ref().and_then(|c| c.features.as_deref()),
299 extras::Language::Node => self.node.as_ref().and_then(|c| c.features.as_deref()),
300 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.features.as_deref()),
301 extras::Language::Php => self.php.as_ref().and_then(|c| c.features.as_deref()),
302 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.features.as_deref()),
303 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.features.as_deref()),
304 extras::Language::Ffi => self.ffi.as_ref().and_then(|c| c.features.as_deref()),
305 extras::Language::Go => self.go.as_ref().and_then(|c| c.features.as_deref()),
306 extras::Language::Java => self.java.as_ref().and_then(|c| c.features.as_deref()),
307 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.features.as_deref()),
308 extras::Language::R => self.r.as_ref().and_then(|c| c.features.as_deref()),
309 extras::Language::Rust => None, };
311 override_features.unwrap_or(&self.crate_config.features)
312 }
313
314 pub fn extra_deps_for_language(&self, lang: extras::Language) -> HashMap<String, toml::Value> {
318 let mut deps = self.crate_config.extra_dependencies.clone();
319 let lang_deps = match lang {
320 extras::Language::Python => self.python.as_ref().map(|c| &c.extra_dependencies),
321 extras::Language::Node => self.node.as_ref().map(|c| &c.extra_dependencies),
322 extras::Language::Ruby => self.ruby.as_ref().map(|c| &c.extra_dependencies),
323 extras::Language::Php => self.php.as_ref().map(|c| &c.extra_dependencies),
324 extras::Language::Elixir => self.elixir.as_ref().map(|c| &c.extra_dependencies),
325 extras::Language::Wasm => self.wasm.as_ref().map(|c| &c.extra_dependencies),
326 _ => None,
327 };
328 if let Some(lang_deps) = lang_deps {
329 deps.extend(lang_deps.iter().map(|(k, v)| (k.clone(), v.clone())));
330 }
331 deps
332 }
333
334 pub fn package_dir(&self, lang: extras::Language) -> String {
339 let override_path = match lang {
340 extras::Language::Python => self.python.as_ref().and_then(|c| c.scaffold_output.as_ref()),
341 extras::Language::Node => self.node.as_ref().and_then(|c| c.scaffold_output.as_ref()),
342 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.scaffold_output.as_ref()),
343 extras::Language::Php => self.php.as_ref().and_then(|c| c.scaffold_output.as_ref()),
344 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.scaffold_output.as_ref()),
345 _ => None,
346 };
347 if let Some(p) = override_path {
348 p.to_string_lossy().to_string()
349 } else {
350 match lang {
351 extras::Language::Python => "packages/python".to_string(),
352 extras::Language::Node => "packages/node".to_string(),
353 extras::Language::Ruby => "packages/ruby".to_string(),
354 extras::Language::Php => "packages/php".to_string(),
355 extras::Language::Elixir => "packages/elixir".to_string(),
356 _ => format!("packages/{lang}"),
357 }
358 }
359 }
360
361 pub fn lint_config_for_language(&self, lang: extras::Language) -> output::LintConfig {
366 if let Some(lint_map) = &self.lint {
367 let lang_str = lang.to_string();
368 if let Some(explicit) = lint_map.get(&lang_str) {
369 return explicit.clone();
370 }
371 }
372 let output_dir = self.package_dir(lang);
373 lint_defaults::default_lint_config(lang, &output_dir)
374 }
375
376 pub fn update_config_for_language(&self, lang: extras::Language) -> output::UpdateConfig {
381 if let Some(update_map) = &self.update {
382 let lang_str = lang.to_string();
383 if let Some(explicit) = update_map.get(&lang_str) {
384 return explicit.clone();
385 }
386 }
387 let output_dir = self.package_dir(lang);
388 update_defaults::default_update_config(lang, &output_dir)
389 }
390
391 pub fn test_config_for_language(&self, lang: extras::Language) -> output::TestConfig {
396 if let Some(test_map) = &self.test {
397 let lang_str = lang.to_string();
398 if let Some(explicit) = test_map.get(&lang_str) {
399 return explicit.clone();
400 }
401 }
402 let output_dir = self.package_dir(lang);
403 test_defaults::default_test_config(lang, &output_dir)
404 }
405
406 pub fn setup_config_for_language(&self, lang: extras::Language) -> output::SetupConfig {
411 if let Some(setup_map) = &self.setup {
412 let lang_str = lang.to_string();
413 if let Some(explicit) = setup_map.get(&lang_str) {
414 return explicit.clone();
415 }
416 }
417 let output_dir = self.package_dir(lang);
418 setup_defaults::default_setup_config(lang, &output_dir)
419 }
420
421 pub fn clean_config_for_language(&self, lang: extras::Language) -> output::CleanConfig {
426 if let Some(clean_map) = &self.clean {
427 let lang_str = lang.to_string();
428 if let Some(explicit) = clean_map.get(&lang_str) {
429 return explicit.clone();
430 }
431 }
432 let output_dir = self.package_dir(lang);
433 clean_defaults::default_clean_config(lang, &output_dir)
434 }
435
436 pub fn build_command_config_for_language(&self, lang: extras::Language) -> output::BuildCommandConfig {
441 if let Some(build_map) = &self.build_commands {
442 let lang_str = lang.to_string();
443 if let Some(explicit) = build_map.get(&lang_str) {
444 return explicit.clone();
445 }
446 }
447 let output_dir = self.package_dir(lang);
448 let crate_name = &self.crate_config.name;
449 build_defaults::default_build_config(lang, &output_dir, crate_name)
450 }
451
452 pub fn core_import(&self) -> String {
454 self.crate_config
455 .core_import
456 .clone()
457 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
458 }
459
460 pub fn error_type(&self) -> String {
462 self.crate_config
463 .error_type
464 .clone()
465 .unwrap_or_else(|| "Error".to_string())
466 }
467
468 pub fn error_constructor(&self) -> String {
471 self.crate_config
472 .error_constructor
473 .clone()
474 .unwrap_or_else(|| format!("{}::{}::from({{msg}})", self.core_import(), self.error_type()))
475 }
476
477 pub fn ffi_prefix(&self) -> String {
479 self.ffi
480 .as_ref()
481 .and_then(|f| f.prefix.as_ref())
482 .cloned()
483 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
484 }
485
486 pub fn ffi_lib_name(&self) -> String {
494 if let Some(name) = self.ffi.as_ref().and_then(|f| f.lib_name.as_ref()) {
496 return name.clone();
497 }
498
499 if let Some(ffi_path) = self.output.ffi.as_ref() {
502 let path = std::path::Path::new(ffi_path);
503 let components: Vec<_> = path
506 .components()
507 .filter_map(|c| {
508 if let std::path::Component::Normal(s) = c {
509 s.to_str()
510 } else {
511 None
512 }
513 })
514 .collect();
515 let crate_dir = components
518 .iter()
519 .rev()
520 .find(|&&s| s != "src" && s != "lib" && s != "include")
521 .copied();
522 if let Some(dir) = crate_dir {
523 return dir.replace('-', "_");
524 }
525 }
526
527 format!("{}_ffi", self.ffi_prefix())
529 }
530
531 pub fn ffi_header_name(&self) -> String {
533 self.ffi
534 .as_ref()
535 .and_then(|f| f.header_name.as_ref())
536 .cloned()
537 .unwrap_or_else(|| format!("{}.h", self.ffi_prefix()))
538 }
539
540 pub fn python_module_name(&self) -> String {
542 self.python
543 .as_ref()
544 .and_then(|p| p.module_name.as_ref())
545 .cloned()
546 .unwrap_or_else(|| format!("_{}", self.crate_config.name.replace('-', "_")))
547 }
548
549 pub fn python_pip_name(&self) -> String {
553 self.python
554 .as_ref()
555 .and_then(|p| p.pip_name.as_ref())
556 .cloned()
557 .unwrap_or_else(|| self.crate_config.name.clone())
558 }
559
560 pub fn php_autoload_namespace(&self) -> String {
565 use heck::ToPascalCase;
566 let ext = self.php_extension_name();
567 if ext.contains('_') {
568 ext.split('_')
569 .map(|p| p.to_pascal_case())
570 .collect::<Vec<_>>()
571 .join("\\")
572 } else {
573 ext.to_pascal_case()
574 }
575 }
576
577 pub fn node_package_name(&self) -> String {
579 self.node
580 .as_ref()
581 .and_then(|n| n.package_name.as_ref())
582 .cloned()
583 .unwrap_or_else(|| self.crate_config.name.clone())
584 }
585
586 pub fn ruby_gem_name(&self) -> String {
588 self.ruby
589 .as_ref()
590 .and_then(|r| r.gem_name.as_ref())
591 .cloned()
592 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
593 }
594
595 pub fn php_extension_name(&self) -> String {
597 self.php
598 .as_ref()
599 .and_then(|p| p.extension_name.as_ref())
600 .cloned()
601 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
602 }
603
604 pub fn elixir_app_name(&self) -> String {
606 self.elixir
607 .as_ref()
608 .and_then(|e| e.app_name.as_ref())
609 .cloned()
610 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
611 }
612
613 pub fn go_module(&self) -> String {
615 self.go
616 .as_ref()
617 .and_then(|g| g.module.as_ref())
618 .cloned()
619 .unwrap_or_else(|| format!("github.com/kreuzberg-dev/{}", self.crate_config.name))
620 }
621
622 pub fn github_repo(&self) -> String {
629 if let Some(e2e) = &self.e2e {
630 if let Some(url) = &e2e.registry.github_repo {
631 return url.clone();
632 }
633 }
634 self.scaffold
635 .as_ref()
636 .and_then(|s| s.repository.as_ref())
637 .cloned()
638 .unwrap_or_else(|| format!("https://github.com/kreuzberg-dev/{}", self.crate_config.name))
639 }
640
641 pub fn java_package(&self) -> String {
643 self.java
644 .as_ref()
645 .and_then(|j| j.package.as_ref())
646 .cloned()
647 .unwrap_or_else(|| "dev.kreuzberg".to_string())
648 }
649
650 pub fn java_group_id(&self) -> String {
655 self.java_package()
656 }
657
658 pub fn csharp_namespace(&self) -> String {
660 self.csharp
661 .as_ref()
662 .and_then(|c| c.namespace.as_ref())
663 .cloned()
664 .unwrap_or_else(|| {
665 use heck::ToPascalCase;
666 self.crate_config.name.to_pascal_case()
667 })
668 }
669
670 pub fn core_crate_dir(&self) -> String {
676 if let Some(first_source) = self.crate_config.sources.first() {
679 let path = std::path::Path::new(first_source);
680 let mut current = path.parent();
681 while let Some(dir) = current {
682 if dir.file_name().is_some_and(|n| n == "src") {
683 if let Some(crate_dir) = dir.parent() {
684 if let Some(dir_name) = crate_dir.file_name() {
685 return dir_name.to_string_lossy().into_owned();
686 }
687 }
688 break;
689 }
690 current = dir.parent();
691 }
692 }
693 self.crate_config.name.clone()
694 }
695
696 pub fn wasm_type_prefix(&self) -> String {
699 self.wasm
700 .as_ref()
701 .and_then(|w| w.type_prefix.as_ref())
702 .cloned()
703 .unwrap_or_else(|| "Wasm".to_string())
704 }
705
706 pub fn node_type_prefix(&self) -> String {
709 self.node
710 .as_ref()
711 .and_then(|n| n.type_prefix.as_ref())
712 .cloned()
713 .unwrap_or_else(|| "Js".to_string())
714 }
715
716 pub fn r_package_name(&self) -> String {
718 self.r
719 .as_ref()
720 .and_then(|r| r.package_name.as_ref())
721 .cloned()
722 .unwrap_or_else(|| self.crate_config.name.clone())
723 }
724
725 pub fn resolved_version(&self) -> Option<String> {
728 let content = std::fs::read_to_string(&self.crate_config.version_from).ok()?;
729 let value: toml::Value = toml::from_str(&content).ok()?;
730 if let Some(v) = value
731 .get("workspace")
732 .and_then(|w| w.get("package"))
733 .and_then(|p| p.get("version"))
734 .and_then(|v| v.as_str())
735 {
736 return Some(v.to_string());
737 }
738 value
739 .get("package")
740 .and_then(|p| p.get("version"))
741 .and_then(|v| v.as_str())
742 .map(|v| v.to_string())
743 }
744
745 pub fn serde_rename_all_for_language(&self, lang: extras::Language) -> String {
753 let override_val = match lang {
755 extras::Language::Python => self.python.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
756 extras::Language::Node => self.node.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
757 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
758 extras::Language::Php => self.php.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
759 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
760 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
761 extras::Language::Ffi => self.ffi.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
762 extras::Language::Go => self.go.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
763 extras::Language::Java => self.java.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
764 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
765 extras::Language::R => self.r.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
766 extras::Language::Rust => None, };
768
769 if let Some(val) = override_val {
770 return val.to_string();
771 }
772
773 match lang {
775 extras::Language::Node | extras::Language::Wasm | extras::Language::Java | extras::Language::Csharp => {
776 "camelCase".to_string()
777 }
778 extras::Language::Python
779 | extras::Language::Ruby
780 | extras::Language::Php
781 | extras::Language::Go
782 | extras::Language::Ffi
783 | extras::Language::Elixir
784 | extras::Language::R
785 | extras::Language::Rust => "snake_case".to_string(),
786 }
787 }
788
789 pub fn rewrite_path(&self, rust_path: &str) -> String {
792 let mut mappings: Vec<_> = self.crate_config.path_mappings.iter().collect();
794 mappings.sort_by_key(|b| std::cmp::Reverse(b.0.len()));
795
796 for (from, to) in &mappings {
797 if rust_path.starts_with(from.as_str()) {
798 return format!("{}{}", to, &rust_path[from.len()..]);
799 }
800 }
801 rust_path.to_string()
802 }
803
804 pub fn effective_path_mappings(&self) -> HashMap<String, String> {
814 let mut mappings = HashMap::new();
815
816 if self.crate_config.auto_path_mappings {
817 let core_import = self.core_import();
818
819 for source in &self.crate_config.sources {
820 let source_str = source.to_string_lossy();
821 if let Some(after_crates) = find_after_crates_prefix(&source_str) {
823 if let Some(slash_pos) = after_crates.find('/') {
825 let crate_dir = &after_crates[..slash_pos];
826 let crate_ident = crate_dir.replace('-', "_");
827 if crate_ident != core_import && !mappings.contains_key(&crate_ident) {
829 mappings.insert(crate_ident, core_import.clone());
830 }
831 }
832 }
833 }
834 }
835
836 for (from, to) in &self.crate_config.path_mappings {
838 mappings.insert(from.clone(), to.clone());
839 }
840
841 mappings
842 }
843}
844
845fn find_after_crates_prefix(path: &str) -> Option<&str> {
852 if let Some(pos) = path.find("/crates/") {
856 return Some(&path[pos + "/crates/".len()..]);
857 }
858 if let Some(stripped) = path.strip_prefix("crates/") {
859 return Some(stripped);
860 }
861 None
862}
863
864pub fn resolve_output_dir(config_path: Option<&PathBuf>, crate_name: &str, default: &str) -> String {
867 config_path
868 .map(|p| p.to_string_lossy().replace("{name}", crate_name))
869 .unwrap_or_else(|| default.replace("{name}", crate_name))
870}
871
872pub fn detect_serde_available(output_dir: &str) -> bool {
878 let src_path = std::path::Path::new(output_dir);
879 let mut dir = src_path;
881 loop {
882 let cargo_toml = dir.join("Cargo.toml");
883 if cargo_toml.exists() {
884 return cargo_toml_has_serde(&cargo_toml);
885 }
886 match dir.parent() {
887 Some(parent) if !parent.as_os_str().is_empty() => dir = parent,
888 _ => break,
889 }
890 }
891 false
892}
893
894fn cargo_toml_has_serde(path: &std::path::Path) -> bool {
900 let content = match std::fs::read_to_string(path) {
901 Ok(c) => c,
902 Err(_) => return false,
903 };
904
905 let has_serde_json = content.contains("serde_json");
906 let has_serde_dep = content.lines().any(|line| {
910 let trimmed = line.trim();
911 trimmed.starts_with("serde ")
913 || trimmed.starts_with("serde=")
914 || trimmed.starts_with("serde.")
915 || trimmed == "[dependencies.serde]"
916 });
917
918 has_serde_json && has_serde_dep
919}
920
921#[cfg(test)]
922mod tests {
923 use super::*;
924
925 fn minimal_config() -> AlefConfig {
926 toml::from_str(
927 r#"
928languages = ["python", "node", "rust"]
929
930[crate]
931name = "test-lib"
932sources = ["src/lib.rs"]
933"#,
934 )
935 .unwrap()
936 }
937
938 #[test]
939 fn lint_config_falls_back_to_defaults() {
940 let config = minimal_config();
941 assert!(config.lint.is_none());
942
943 let py = config.lint_config_for_language(Language::Python);
944 assert!(py.format.is_some());
945 assert!(py.check.is_some());
946 assert!(py.typecheck.is_some());
947
948 let node = config.lint_config_for_language(Language::Node);
949 assert!(node.format.is_some());
950 assert!(node.check.is_some());
951 }
952
953 #[test]
954 fn lint_config_explicit_overrides_default() {
955 let config: AlefConfig = toml::from_str(
956 r#"
957languages = ["python"]
958
959[crate]
960name = "test-lib"
961sources = ["src/lib.rs"]
962
963[lint.python]
964format = "custom-formatter"
965check = "custom-checker"
966"#,
967 )
968 .unwrap();
969
970 let py = config.lint_config_for_language(Language::Python);
971 assert_eq!(py.format.unwrap().commands(), vec!["custom-formatter"]);
972 assert_eq!(py.check.unwrap().commands(), vec!["custom-checker"]);
973 assert!(py.typecheck.is_none()); }
975
976 #[test]
977 fn lint_config_partial_override_does_not_merge() {
978 let config: AlefConfig = toml::from_str(
979 r#"
980languages = ["python"]
981
982[crate]
983name = "test-lib"
984sources = ["src/lib.rs"]
985
986[lint.python]
987format = "only-format"
988"#,
989 )
990 .unwrap();
991
992 let py = config.lint_config_for_language(Language::Python);
993 assert_eq!(py.format.unwrap().commands(), vec!["only-format"]);
994 assert!(py.check.is_none());
996 assert!(py.typecheck.is_none());
997 }
998
999 #[test]
1000 fn lint_config_unconfigured_language_uses_defaults() {
1001 let config: AlefConfig = toml::from_str(
1002 r#"
1003languages = ["python", "node"]
1004
1005[crate]
1006name = "test-lib"
1007sources = ["src/lib.rs"]
1008
1009[lint.python]
1010format = "custom"
1011"#,
1012 )
1013 .unwrap();
1014
1015 let py = config.lint_config_for_language(Language::Python);
1017 assert_eq!(py.format.unwrap().commands(), vec!["custom"]);
1018
1019 let node = config.lint_config_for_language(Language::Node);
1021 let fmt = node.format.unwrap().commands().join(" ");
1022 assert!(fmt.contains("oxfmt"));
1023 }
1024
1025 #[test]
1026 fn update_config_falls_back_to_defaults() {
1027 let config = minimal_config();
1028 assert!(config.update.is_none());
1029
1030 let py = config.update_config_for_language(Language::Python);
1031 assert!(py.update.is_some());
1032 assert!(py.upgrade.is_some());
1033
1034 let rust = config.update_config_for_language(Language::Rust);
1035 let update = rust.update.unwrap().commands().join(" ");
1036 assert!(update.contains("cargo update"));
1037 }
1038
1039 #[test]
1040 fn update_config_explicit_overrides_default() {
1041 let config: AlefConfig = toml::from_str(
1042 r#"
1043languages = ["rust"]
1044
1045[crate]
1046name = "test-lib"
1047sources = ["src/lib.rs"]
1048
1049[update.rust]
1050update = "my-custom-update"
1051upgrade = ["step1", "step2"]
1052"#,
1053 )
1054 .unwrap();
1055
1056 let rust = config.update_config_for_language(Language::Rust);
1057 assert_eq!(rust.update.unwrap().commands(), vec!["my-custom-update"]);
1058 assert_eq!(rust.upgrade.unwrap().commands(), vec!["step1", "step2"]);
1059 }
1060
1061 #[test]
1062 fn test_config_falls_back_to_defaults() {
1063 let config = minimal_config();
1064 assert!(config.test.is_none());
1065
1066 let py = config.test_config_for_language(Language::Python);
1067 assert!(py.command.is_some());
1068 assert!(py.coverage.is_some());
1069 assert!(py.e2e.is_none());
1070
1071 let rust = config.test_config_for_language(Language::Rust);
1072 let cmd = rust.command.unwrap().commands().join(" ");
1073 assert!(cmd.contains("cargo test"));
1074 }
1075
1076 #[test]
1077 fn test_config_explicit_overrides_default() {
1078 let config: AlefConfig = toml::from_str(
1079 r#"
1080languages = ["python"]
1081
1082[crate]
1083name = "test-lib"
1084sources = ["src/lib.rs"]
1085
1086[test.python]
1087command = "my-custom-test"
1088"#,
1089 )
1090 .unwrap();
1091
1092 let py = config.test_config_for_language(Language::Python);
1093 assert_eq!(py.command.unwrap().commands(), vec!["my-custom-test"]);
1094 assert!(py.coverage.is_none()); }
1096
1097 #[test]
1098 fn setup_config_falls_back_to_defaults() {
1099 let config = minimal_config();
1100 assert!(config.setup.is_none());
1101
1102 let py = config.setup_config_for_language(Language::Python);
1103 assert!(py.install.is_some());
1104 let install = py.install.unwrap().commands().join(" ");
1105 assert!(install.contains("uv sync"));
1106
1107 let rust = config.setup_config_for_language(Language::Rust);
1108 let install = rust.install.unwrap().commands().join(" ");
1109 assert!(install.contains("rustup update"));
1110 }
1111
1112 #[test]
1113 fn setup_config_explicit_overrides_default() {
1114 let config: AlefConfig = toml::from_str(
1115 r#"
1116languages = ["python"]
1117
1118[crate]
1119name = "test-lib"
1120sources = ["src/lib.rs"]
1121
1122[setup.python]
1123install = "my-custom-install"
1124"#,
1125 )
1126 .unwrap();
1127
1128 let py = config.setup_config_for_language(Language::Python);
1129 assert_eq!(py.install.unwrap().commands(), vec!["my-custom-install"]);
1130 }
1131
1132 #[test]
1133 fn clean_config_falls_back_to_defaults() {
1134 let config = minimal_config();
1135 assert!(config.clean.is_none());
1136
1137 let py = config.clean_config_for_language(Language::Python);
1138 assert!(py.clean.is_some());
1139 let clean = py.clean.unwrap().commands().join(" ");
1140 assert!(clean.contains("__pycache__"));
1141
1142 let rust = config.clean_config_for_language(Language::Rust);
1143 let clean = rust.clean.unwrap().commands().join(" ");
1144 assert!(clean.contains("cargo clean"));
1145 }
1146
1147 #[test]
1148 fn clean_config_explicit_overrides_default() {
1149 let config: AlefConfig = toml::from_str(
1150 r#"
1151languages = ["rust"]
1152
1153[crate]
1154name = "test-lib"
1155sources = ["src/lib.rs"]
1156
1157[clean.rust]
1158clean = "my-custom-clean"
1159"#,
1160 )
1161 .unwrap();
1162
1163 let rust = config.clean_config_for_language(Language::Rust);
1164 assert_eq!(rust.clean.unwrap().commands(), vec!["my-custom-clean"]);
1165 }
1166
1167 #[test]
1168 fn build_command_config_falls_back_to_defaults() {
1169 let config = minimal_config();
1170 assert!(config.build_commands.is_none());
1171
1172 let py = config.build_command_config_for_language(Language::Python);
1173 assert!(py.build.is_some());
1174 assert!(py.build_release.is_some());
1175 let build = py.build.unwrap().commands().join(" ");
1176 assert!(build.contains("maturin develop"));
1177
1178 let rust = config.build_command_config_for_language(Language::Rust);
1179 let build = rust.build.unwrap().commands().join(" ");
1180 assert!(build.contains("cargo build --workspace"));
1181 }
1182
1183 #[test]
1184 fn build_command_config_explicit_overrides_default() {
1185 let config: AlefConfig = toml::from_str(
1186 r#"
1187languages = ["rust"]
1188
1189[crate]
1190name = "test-lib"
1191sources = ["src/lib.rs"]
1192
1193[build_commands.rust]
1194build = "my-custom-build"
1195build_release = "my-custom-build --release"
1196"#,
1197 )
1198 .unwrap();
1199
1200 let rust = config.build_command_config_for_language(Language::Rust);
1201 assert_eq!(rust.build.unwrap().commands(), vec!["my-custom-build"]);
1202 assert_eq!(
1203 rust.build_release.unwrap().commands(),
1204 vec!["my-custom-build --release"]
1205 );
1206 }
1207
1208 #[test]
1209 fn build_command_config_uses_crate_name() {
1210 let config = minimal_config();
1211 let py = config.build_command_config_for_language(Language::Python);
1212 let build = py.build.unwrap().commands().join(" ");
1213 assert!(
1214 build.contains("test-lib-py"),
1215 "Python build should reference crate name, got: {build}"
1216 );
1217 }
1218
1219 #[test]
1220 fn package_dir_defaults_are_correct() {
1221 let config = minimal_config();
1222 assert_eq!(config.package_dir(Language::Python), "packages/python");
1223 assert_eq!(config.package_dir(Language::Node), "packages/node");
1224 assert_eq!(config.package_dir(Language::Ruby), "packages/ruby");
1225 assert_eq!(config.package_dir(Language::Go), "packages/go");
1226 assert_eq!(config.package_dir(Language::Java), "packages/java");
1227 }
1228
1229 #[test]
1230 fn explicit_lint_config_preserves_precondition_and_before() {
1231 let config: AlefConfig = toml::from_str(
1232 r#"
1233languages = ["go"]
1234
1235[crate]
1236name = "test"
1237sources = ["src/lib.rs"]
1238
1239[lint.go]
1240precondition = "test -f target/release/libtest_ffi.so"
1241before = "cargo build --release -p test-ffi"
1242format = "gofmt -w packages/go"
1243check = "golangci-lint run ./..."
1244"#,
1245 )
1246 .unwrap();
1247
1248 let lint = config.lint_config_for_language(Language::Go);
1249 assert_eq!(
1250 lint.precondition.as_deref(),
1251 Some("test -f target/release/libtest_ffi.so"),
1252 "precondition should be preserved from explicit config"
1253 );
1254 assert_eq!(
1255 lint.before.unwrap().commands(),
1256 vec!["cargo build --release -p test-ffi"],
1257 "before should be preserved from explicit config"
1258 );
1259 }
1260
1261 #[test]
1262 fn explicit_lint_config_with_before_list_preserves_all_commands() {
1263 let config: AlefConfig = toml::from_str(
1264 r#"
1265languages = ["go"]
1266
1267[crate]
1268name = "test"
1269sources = ["src/lib.rs"]
1270
1271[lint.go]
1272before = ["cargo build --release -p test-ffi", "cp target/release/libtest_ffi.so packages/go/"]
1273check = "golangci-lint run ./..."
1274"#,
1275 )
1276 .unwrap();
1277
1278 let lint = config.lint_config_for_language(Language::Go);
1279 assert!(lint.precondition.is_none(), "precondition should be None when not set");
1280 assert_eq!(
1281 lint.before.unwrap().commands(),
1282 vec![
1283 "cargo build --release -p test-ffi",
1284 "cp target/release/libtest_ffi.so packages/go/"
1285 ],
1286 "before list should be preserved from explicit config"
1287 );
1288 }
1289
1290 #[test]
1291 fn default_lint_config_has_no_precondition_or_before() {
1292 let config = minimal_config();
1293 let py = config.lint_config_for_language(Language::Python);
1294 assert!(
1295 py.precondition.is_none(),
1296 "default lint config should have no precondition"
1297 );
1298 assert!(py.before.is_none(), "default lint config should have no before");
1299
1300 let go = config.lint_config_for_language(Language::Go);
1301 assert!(
1302 go.precondition.is_none(),
1303 "default Go lint config should have no precondition"
1304 );
1305 assert!(go.before.is_none(), "default Go lint config should have no before");
1306 }
1307
1308 #[test]
1309 fn explicit_test_config_preserves_precondition_and_before() {
1310 let config: AlefConfig = toml::from_str(
1311 r#"
1312languages = ["python"]
1313
1314[crate]
1315name = "test"
1316sources = ["src/lib.rs"]
1317
1318[test.python]
1319precondition = "test -f target/release/libtest.so"
1320before = "maturin develop"
1321command = "pytest"
1322"#,
1323 )
1324 .unwrap();
1325
1326 let test = config.test_config_for_language(Language::Python);
1327 assert_eq!(
1328 test.precondition.as_deref(),
1329 Some("test -f target/release/libtest.so"),
1330 "test precondition should be preserved"
1331 );
1332 assert_eq!(
1333 test.before.unwrap().commands(),
1334 vec!["maturin develop"],
1335 "test before should be preserved"
1336 );
1337 }
1338
1339 #[test]
1340 fn default_test_config_has_no_precondition_or_before() {
1341 let config = minimal_config();
1342 let py = config.test_config_for_language(Language::Python);
1343 assert!(
1344 py.precondition.is_none(),
1345 "default test config should have no precondition"
1346 );
1347 assert!(py.before.is_none(), "default test config should have no before");
1348 }
1349
1350 #[test]
1351 fn explicit_setup_config_preserves_precondition_and_before() {
1352 let config: AlefConfig = toml::from_str(
1353 r#"
1354languages = ["python"]
1355
1356[crate]
1357name = "test"
1358sources = ["src/lib.rs"]
1359
1360[setup.python]
1361precondition = "which uv"
1362before = "pip install uv"
1363install = "uv sync"
1364"#,
1365 )
1366 .unwrap();
1367
1368 let setup = config.setup_config_for_language(Language::Python);
1369 assert_eq!(
1370 setup.precondition.as_deref(),
1371 Some("which uv"),
1372 "setup precondition should be preserved"
1373 );
1374 assert_eq!(
1375 setup.before.unwrap().commands(),
1376 vec!["pip install uv"],
1377 "setup before should be preserved"
1378 );
1379 }
1380
1381 #[test]
1382 fn default_setup_config_has_no_precondition_or_before() {
1383 let config = minimal_config();
1384 let py = config.setup_config_for_language(Language::Python);
1385 assert!(
1386 py.precondition.is_none(),
1387 "default setup config should have no precondition"
1388 );
1389 assert!(py.before.is_none(), "default setup config should have no before");
1390 }
1391
1392 #[test]
1393 fn explicit_update_config_preserves_precondition_and_before() {
1394 let config: AlefConfig = toml::from_str(
1395 r#"
1396languages = ["rust"]
1397
1398[crate]
1399name = "test"
1400sources = ["src/lib.rs"]
1401
1402[update.rust]
1403precondition = "test -f Cargo.lock"
1404before = "cargo fetch"
1405update = "cargo update"
1406"#,
1407 )
1408 .unwrap();
1409
1410 let update = config.update_config_for_language(Language::Rust);
1411 assert_eq!(
1412 update.precondition.as_deref(),
1413 Some("test -f Cargo.lock"),
1414 "update precondition should be preserved"
1415 );
1416 assert_eq!(
1417 update.before.unwrap().commands(),
1418 vec!["cargo fetch"],
1419 "update before should be preserved"
1420 );
1421 }
1422
1423 #[test]
1424 fn default_update_config_has_no_precondition_or_before() {
1425 let config = minimal_config();
1426 let rust = config.update_config_for_language(Language::Rust);
1427 assert!(
1428 rust.precondition.is_none(),
1429 "default update config should have no precondition"
1430 );
1431 assert!(rust.before.is_none(), "default update config should have no before");
1432 }
1433
1434 #[test]
1435 fn explicit_clean_config_preserves_precondition_and_before() {
1436 let config: AlefConfig = toml::from_str(
1437 r#"
1438languages = ["rust"]
1439
1440[crate]
1441name = "test"
1442sources = ["src/lib.rs"]
1443
1444[clean.rust]
1445precondition = "test -d target"
1446before = "echo cleaning"
1447clean = "cargo clean"
1448"#,
1449 )
1450 .unwrap();
1451
1452 let clean = config.clean_config_for_language(Language::Rust);
1453 assert_eq!(
1454 clean.precondition.as_deref(),
1455 Some("test -d target"),
1456 "clean precondition should be preserved"
1457 );
1458 assert_eq!(
1459 clean.before.unwrap().commands(),
1460 vec!["echo cleaning"],
1461 "clean before should be preserved"
1462 );
1463 }
1464
1465 #[test]
1466 fn default_clean_config_has_no_precondition_or_before() {
1467 let config = minimal_config();
1468 let rust = config.clean_config_for_language(Language::Rust);
1469 assert!(
1470 rust.precondition.is_none(),
1471 "default clean config should have no precondition"
1472 );
1473 assert!(rust.before.is_none(), "default clean config should have no before");
1474 }
1475
1476 #[test]
1477 fn explicit_build_command_config_preserves_precondition_and_before() {
1478 let config: AlefConfig = toml::from_str(
1479 r#"
1480languages = ["go"]
1481
1482[crate]
1483name = "test"
1484sources = ["src/lib.rs"]
1485
1486[build_commands.go]
1487precondition = "which go"
1488before = "cargo build --release -p test-ffi"
1489build = "cd packages/go && go build ./..."
1490build_release = "cd packages/go && go build -ldflags='-s -w' ./..."
1491"#,
1492 )
1493 .unwrap();
1494
1495 let build = config.build_command_config_for_language(Language::Go);
1496 assert_eq!(
1497 build.precondition.as_deref(),
1498 Some("which go"),
1499 "build precondition should be preserved"
1500 );
1501 assert_eq!(
1502 build.before.unwrap().commands(),
1503 vec!["cargo build --release -p test-ffi"],
1504 "build before should be preserved"
1505 );
1506 }
1507
1508 #[test]
1509 fn default_build_command_config_has_no_precondition_or_before() {
1510 let config = minimal_config();
1511 let rust = config.build_command_config_for_language(Language::Rust);
1512 assert!(
1513 rust.precondition.is_none(),
1514 "default build command config should have no precondition"
1515 );
1516 assert!(
1517 rust.before.is_none(),
1518 "default build command config should have no before"
1519 );
1520 }
1521
1522 #[test]
1523 fn version_defaults_to_none_when_omitted() {
1524 let config = minimal_config();
1525 assert!(config.version.is_none());
1526 }
1527
1528 #[test]
1529 fn version_parses_from_top_level_key() {
1530 let config: AlefConfig = toml::from_str(
1531 r#"
1532version = "0.7.7"
1533languages = ["python"]
1534
1535[crate]
1536name = "test-lib"
1537sources = ["src/lib.rs"]
1538"#,
1539 )
1540 .unwrap();
1541 assert_eq!(config.version.as_deref(), Some("0.7.7"));
1542 }
1543}