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, Default, Serialize, Deserialize)]
39pub struct AlefMetaConfig {
40 #[serde(default)]
43 pub version: Option<String>,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct AlefConfig {
49 #[serde(default)]
51 pub alef: AlefMetaConfig,
52 #[serde(rename = "crate")]
53 pub crate_config: CrateConfig,
54 pub languages: Vec<Language>,
55 #[serde(default)]
56 pub exclude: ExcludeConfig,
57 #[serde(default)]
58 pub include: IncludeConfig,
59 #[serde(default)]
60 pub output: OutputConfig,
61 #[serde(default)]
62 pub python: Option<PythonConfig>,
63 #[serde(default)]
64 pub node: Option<NodeConfig>,
65 #[serde(default)]
66 pub ruby: Option<RubyConfig>,
67 #[serde(default)]
68 pub php: Option<PhpConfig>,
69 #[serde(default)]
70 pub elixir: Option<ElixirConfig>,
71 #[serde(default)]
72 pub wasm: Option<WasmConfig>,
73 #[serde(default)]
74 pub ffi: Option<FfiConfig>,
75 #[serde(default)]
76 pub go: Option<GoConfig>,
77 #[serde(default)]
78 pub java: Option<JavaConfig>,
79 #[serde(default)]
80 pub csharp: Option<CSharpConfig>,
81 #[serde(default)]
82 pub r: Option<RConfig>,
83 #[serde(default)]
84 pub scaffold: Option<ScaffoldConfig>,
85 #[serde(default)]
86 pub readme: Option<ReadmeConfig>,
87 #[serde(default)]
88 pub lint: Option<HashMap<String, LintConfig>>,
89 #[serde(default)]
90 pub update: Option<HashMap<String, UpdateConfig>>,
91 #[serde(default)]
92 pub test: Option<HashMap<String, TestConfig>>,
93 #[serde(default)]
94 pub setup: Option<HashMap<String, SetupConfig>>,
95 #[serde(default)]
96 pub clean: Option<HashMap<String, CleanConfig>>,
97 #[serde(default)]
98 pub build_commands: Option<HashMap<String, BuildCommandConfig>>,
99 #[serde(default)]
101 pub publish: Option<PublishConfig>,
102 #[serde(default)]
103 pub custom_files: Option<HashMap<String, Vec<PathBuf>>>,
104 #[serde(default)]
105 pub adapters: Vec<AdapterConfig>,
106 #[serde(default)]
107 pub custom_modules: CustomModulesConfig,
108 #[serde(default)]
109 pub custom_registrations: CustomRegistrationsConfig,
110 #[serde(default)]
111 pub sync: Option<SyncConfig>,
112 #[serde(default)]
116 pub opaque_types: HashMap<String, String>,
117 #[serde(default)]
119 pub generate: GenerateConfig,
120 #[serde(default)]
122 pub generate_overrides: HashMap<String, GenerateConfig>,
123 #[serde(default)]
125 pub dto: DtoConfig,
126 #[serde(default)]
128 pub e2e: Option<E2eConfig>,
129 #[serde(default)]
132 pub trait_bridges: Vec<TraitBridgeConfig>,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct CrateConfig {
137 pub name: String,
138 pub sources: Vec<PathBuf>,
139 #[serde(default = "default_version_from")]
140 pub version_from: String,
141 #[serde(default)]
142 pub core_import: Option<String>,
143 #[serde(default)]
145 pub workspace_root: Option<PathBuf>,
146 #[serde(default)]
148 pub skip_core_import: bool,
149 #[serde(default)]
153 pub error_type: Option<String>,
154 #[serde(default)]
159 pub error_constructor: Option<String>,
160 #[serde(default)]
164 pub features: Vec<String>,
165 #[serde(default)]
168 pub path_mappings: HashMap<String, String>,
169 #[serde(default)]
173 pub extra_dependencies: HashMap<String, toml::Value>,
174 #[serde(default = "default_true")]
178 pub auto_path_mappings: bool,
179 #[serde(default)]
184 pub source_crates: Vec<SourceCrate>,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct SourceCrate {
190 pub name: String,
192 pub sources: Vec<PathBuf>,
194}
195
196fn default_version_from() -> String {
197 "Cargo.toml".to_string()
198}
199
200fn default_true() -> bool {
201 true
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
208pub struct GenerateConfig {
209 #[serde(default = "default_true")]
211 pub bindings: bool,
212 #[serde(default = "default_true")]
214 pub errors: bool,
215 #[serde(default = "default_true")]
217 pub configs: bool,
218 #[serde(default = "default_true")]
220 pub async_wrappers: bool,
221 #[serde(default = "default_true")]
223 pub type_conversions: bool,
224 #[serde(default = "default_true")]
226 pub package_metadata: bool,
227 #[serde(default = "default_true")]
229 pub public_api: bool,
230 #[serde(default = "default_true")]
233 pub reverse_conversions: bool,
234}
235
236impl Default for GenerateConfig {
237 fn default() -> Self {
238 Self {
239 bindings: true,
240 errors: true,
241 configs: true,
242 async_wrappers: true,
243 type_conversions: true,
244 package_metadata: true,
245 public_api: true,
246 reverse_conversions: true,
247 }
248 }
249}
250
251impl AlefConfig {
256 pub fn resolve_field_name(&self, lang: extras::Language, type_name: &str, field_name: &str) -> Option<String> {
268 let explicit_key = format!("{type_name}.{field_name}");
270 let explicit = match lang {
271 extras::Language::Python => self.python.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
272 extras::Language::Node => self.node.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
273 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
274 extras::Language::Php => self.php.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
275 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
276 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
277 extras::Language::Ffi => self.ffi.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
278 extras::Language::Go => self.go.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
279 extras::Language::Java => self.java.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
280 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
281 extras::Language::R => self.r.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
282 extras::Language::Rust => None,
283 };
284 if let Some(renamed) = explicit {
285 if renamed != field_name {
286 return Some(renamed.clone());
287 }
288 return None;
289 }
290
291 match lang {
293 extras::Language::Python => crate::keywords::python_safe_name(field_name),
294 _ => None,
299 }
300 }
301
302 pub fn features_for_language(&self, lang: extras::Language) -> &[String] {
305 let override_features = match lang {
306 extras::Language::Python => self.python.as_ref().and_then(|c| c.features.as_deref()),
307 extras::Language::Node => self.node.as_ref().and_then(|c| c.features.as_deref()),
308 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.features.as_deref()),
309 extras::Language::Php => self.php.as_ref().and_then(|c| c.features.as_deref()),
310 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.features.as_deref()),
311 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.features.as_deref()),
312 extras::Language::Ffi => self.ffi.as_ref().and_then(|c| c.features.as_deref()),
313 extras::Language::Go => self.go.as_ref().and_then(|c| c.features.as_deref()),
314 extras::Language::Java => self.java.as_ref().and_then(|c| c.features.as_deref()),
315 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.features.as_deref()),
316 extras::Language::R => self.r.as_ref().and_then(|c| c.features.as_deref()),
317 extras::Language::Rust => None, };
319 override_features.unwrap_or(&self.crate_config.features)
320 }
321
322 pub fn extra_deps_for_language(&self, lang: extras::Language) -> HashMap<String, toml::Value> {
326 let mut deps = self.crate_config.extra_dependencies.clone();
327 let lang_deps = match lang {
328 extras::Language::Python => self.python.as_ref().map(|c| &c.extra_dependencies),
329 extras::Language::Node => self.node.as_ref().map(|c| &c.extra_dependencies),
330 extras::Language::Ruby => self.ruby.as_ref().map(|c| &c.extra_dependencies),
331 extras::Language::Php => self.php.as_ref().map(|c| &c.extra_dependencies),
332 extras::Language::Elixir => self.elixir.as_ref().map(|c| &c.extra_dependencies),
333 extras::Language::Wasm => self.wasm.as_ref().map(|c| &c.extra_dependencies),
334 _ => None,
335 };
336 if let Some(lang_deps) = lang_deps {
337 deps.extend(lang_deps.iter().map(|(k, v)| (k.clone(), v.clone())));
338 }
339 deps
340 }
341
342 pub fn package_dir(&self, lang: extras::Language) -> String {
347 let override_path = match lang {
348 extras::Language::Python => self.python.as_ref().and_then(|c| c.scaffold_output.as_ref()),
349 extras::Language::Node => self.node.as_ref().and_then(|c| c.scaffold_output.as_ref()),
350 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.scaffold_output.as_ref()),
351 extras::Language::Php => self.php.as_ref().and_then(|c| c.scaffold_output.as_ref()),
352 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.scaffold_output.as_ref()),
353 _ => None,
354 };
355 if let Some(p) = override_path {
356 p.to_string_lossy().to_string()
357 } else {
358 match lang {
359 extras::Language::Python => "packages/python".to_string(),
360 extras::Language::Node => "packages/node".to_string(),
361 extras::Language::Ruby => "packages/ruby".to_string(),
362 extras::Language::Php => "packages/php".to_string(),
363 extras::Language::Elixir => "packages/elixir".to_string(),
364 _ => format!("packages/{lang}"),
365 }
366 }
367 }
368
369 pub fn lint_config_for_language(&self, lang: extras::Language) -> output::LintConfig {
374 if let Some(lint_map) = &self.lint {
375 let lang_str = lang.to_string();
376 if let Some(explicit) = lint_map.get(&lang_str) {
377 return explicit.clone();
378 }
379 }
380 let output_dir = self.package_dir(lang);
381 lint_defaults::default_lint_config(lang, &output_dir)
382 }
383
384 pub fn update_config_for_language(&self, lang: extras::Language) -> output::UpdateConfig {
389 if let Some(update_map) = &self.update {
390 let lang_str = lang.to_string();
391 if let Some(explicit) = update_map.get(&lang_str) {
392 return explicit.clone();
393 }
394 }
395 let output_dir = self.package_dir(lang);
396 update_defaults::default_update_config(lang, &output_dir)
397 }
398
399 pub fn test_config_for_language(&self, lang: extras::Language) -> output::TestConfig {
404 if let Some(test_map) = &self.test {
405 let lang_str = lang.to_string();
406 if let Some(explicit) = test_map.get(&lang_str) {
407 return explicit.clone();
408 }
409 }
410 let output_dir = self.package_dir(lang);
411 test_defaults::default_test_config(lang, &output_dir)
412 }
413
414 pub fn setup_config_for_language(&self, lang: extras::Language) -> output::SetupConfig {
419 if let Some(setup_map) = &self.setup {
420 let lang_str = lang.to_string();
421 if let Some(explicit) = setup_map.get(&lang_str) {
422 return explicit.clone();
423 }
424 }
425 let output_dir = self.package_dir(lang);
426 setup_defaults::default_setup_config(lang, &output_dir)
427 }
428
429 pub fn clean_config_for_language(&self, lang: extras::Language) -> output::CleanConfig {
434 if let Some(clean_map) = &self.clean {
435 let lang_str = lang.to_string();
436 if let Some(explicit) = clean_map.get(&lang_str) {
437 return explicit.clone();
438 }
439 }
440 let output_dir = self.package_dir(lang);
441 clean_defaults::default_clean_config(lang, &output_dir)
442 }
443
444 pub fn build_command_config_for_language(&self, lang: extras::Language) -> output::BuildCommandConfig {
449 if let Some(build_map) = &self.build_commands {
450 let lang_str = lang.to_string();
451 if let Some(explicit) = build_map.get(&lang_str) {
452 return explicit.clone();
453 }
454 }
455 let output_dir = self.package_dir(lang);
456 let crate_name = &self.crate_config.name;
457 build_defaults::default_build_config(lang, &output_dir, crate_name)
458 }
459
460 pub fn core_import(&self) -> String {
462 self.crate_config
463 .core_import
464 .clone()
465 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
466 }
467
468 pub fn error_type(&self) -> String {
470 self.crate_config
471 .error_type
472 .clone()
473 .unwrap_or_else(|| "Error".to_string())
474 }
475
476 pub fn error_constructor(&self) -> String {
479 self.crate_config
480 .error_constructor
481 .clone()
482 .unwrap_or_else(|| format!("{}::{}::from({{msg}})", self.core_import(), self.error_type()))
483 }
484
485 pub fn ffi_prefix(&self) -> String {
487 self.ffi
488 .as_ref()
489 .and_then(|f| f.prefix.as_ref())
490 .cloned()
491 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
492 }
493
494 pub fn ffi_lib_name(&self) -> String {
502 if let Some(name) = self.ffi.as_ref().and_then(|f| f.lib_name.as_ref()) {
504 return name.clone();
505 }
506
507 if let Some(ffi_path) = self.output.ffi.as_ref() {
510 let path = std::path::Path::new(ffi_path);
511 let components: Vec<_> = path
514 .components()
515 .filter_map(|c| {
516 if let std::path::Component::Normal(s) = c {
517 s.to_str()
518 } else {
519 None
520 }
521 })
522 .collect();
523 let crate_dir = components
526 .iter()
527 .rev()
528 .find(|&&s| s != "src" && s != "lib" && s != "include")
529 .copied();
530 if let Some(dir) = crate_dir {
531 return dir.replace('-', "_");
532 }
533 }
534
535 format!("{}_ffi", self.ffi_prefix())
537 }
538
539 pub fn ffi_header_name(&self) -> String {
541 self.ffi
542 .as_ref()
543 .and_then(|f| f.header_name.as_ref())
544 .cloned()
545 .unwrap_or_else(|| format!("{}.h", self.ffi_prefix()))
546 }
547
548 pub fn python_module_name(&self) -> String {
550 self.python
551 .as_ref()
552 .and_then(|p| p.module_name.as_ref())
553 .cloned()
554 .unwrap_or_else(|| format!("_{}", self.crate_config.name.replace('-', "_")))
555 }
556
557 pub fn python_pip_name(&self) -> String {
561 self.python
562 .as_ref()
563 .and_then(|p| p.pip_name.as_ref())
564 .cloned()
565 .unwrap_or_else(|| self.crate_config.name.clone())
566 }
567
568 pub fn php_autoload_namespace(&self) -> String {
573 use heck::ToPascalCase;
574 let ext = self.php_extension_name();
575 if ext.contains('_') {
576 ext.split('_')
577 .map(|p| p.to_pascal_case())
578 .collect::<Vec<_>>()
579 .join("\\")
580 } else {
581 ext.to_pascal_case()
582 }
583 }
584
585 pub fn node_package_name(&self) -> String {
587 self.node
588 .as_ref()
589 .and_then(|n| n.package_name.as_ref())
590 .cloned()
591 .unwrap_or_else(|| self.crate_config.name.clone())
592 }
593
594 pub fn ruby_gem_name(&self) -> String {
596 self.ruby
597 .as_ref()
598 .and_then(|r| r.gem_name.as_ref())
599 .cloned()
600 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
601 }
602
603 pub fn php_extension_name(&self) -> String {
605 self.php
606 .as_ref()
607 .and_then(|p| p.extension_name.as_ref())
608 .cloned()
609 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
610 }
611
612 pub fn elixir_app_name(&self) -> String {
614 self.elixir
615 .as_ref()
616 .and_then(|e| e.app_name.as_ref())
617 .cloned()
618 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
619 }
620
621 pub fn go_module(&self) -> String {
623 self.go
624 .as_ref()
625 .and_then(|g| g.module.as_ref())
626 .cloned()
627 .unwrap_or_else(|| format!("github.com/kreuzberg-dev/{}", self.crate_config.name))
628 }
629
630 pub fn github_repo(&self) -> String {
637 if let Some(e2e) = &self.e2e {
638 if let Some(url) = &e2e.registry.github_repo {
639 return url.clone();
640 }
641 }
642 self.scaffold
643 .as_ref()
644 .and_then(|s| s.repository.as_ref())
645 .cloned()
646 .unwrap_or_else(|| format!("https://github.com/kreuzberg-dev/{}", self.crate_config.name))
647 }
648
649 pub fn java_package(&self) -> String {
651 self.java
652 .as_ref()
653 .and_then(|j| j.package.as_ref())
654 .cloned()
655 .unwrap_or_else(|| "dev.kreuzberg".to_string())
656 }
657
658 pub fn java_group_id(&self) -> String {
663 self.java_package()
664 }
665
666 pub fn csharp_namespace(&self) -> String {
668 self.csharp
669 .as_ref()
670 .and_then(|c| c.namespace.as_ref())
671 .cloned()
672 .unwrap_or_else(|| {
673 use heck::ToPascalCase;
674 self.crate_config.name.to_pascal_case()
675 })
676 }
677
678 pub fn core_crate_dir(&self) -> String {
684 if let Some(first_source) = self.crate_config.sources.first() {
687 let path = std::path::Path::new(first_source);
688 let mut current = path.parent();
689 while let Some(dir) = current {
690 if dir.file_name().is_some_and(|n| n == "src") {
691 if let Some(crate_dir) = dir.parent() {
692 if let Some(dir_name) = crate_dir.file_name() {
693 return dir_name.to_string_lossy().into_owned();
694 }
695 }
696 break;
697 }
698 current = dir.parent();
699 }
700 }
701 self.crate_config.name.clone()
702 }
703
704 pub fn wasm_type_prefix(&self) -> String {
707 self.wasm
708 .as_ref()
709 .and_then(|w| w.type_prefix.as_ref())
710 .cloned()
711 .unwrap_or_else(|| "Wasm".to_string())
712 }
713
714 pub fn node_type_prefix(&self) -> String {
717 self.node
718 .as_ref()
719 .and_then(|n| n.type_prefix.as_ref())
720 .cloned()
721 .unwrap_or_else(|| "Js".to_string())
722 }
723
724 pub fn r_package_name(&self) -> String {
726 self.r
727 .as_ref()
728 .and_then(|r| r.package_name.as_ref())
729 .cloned()
730 .unwrap_or_else(|| self.crate_config.name.clone())
731 }
732
733 pub fn resolved_version(&self) -> Option<String> {
736 let content = std::fs::read_to_string(&self.crate_config.version_from).ok()?;
737 let value: toml::Value = toml::from_str(&content).ok()?;
738 if let Some(v) = value
739 .get("workspace")
740 .and_then(|w| w.get("package"))
741 .and_then(|p| p.get("version"))
742 .and_then(|v| v.as_str())
743 {
744 return Some(v.to_string());
745 }
746 value
747 .get("package")
748 .and_then(|p| p.get("version"))
749 .and_then(|v| v.as_str())
750 .map(|v| v.to_string())
751 }
752
753 pub fn serde_rename_all_for_language(&self, lang: extras::Language) -> String {
761 let override_val = match lang {
763 extras::Language::Python => self.python.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
764 extras::Language::Node => self.node.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
765 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
766 extras::Language::Php => self.php.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
767 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
768 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
769 extras::Language::Ffi => self.ffi.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
770 extras::Language::Go => self.go.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
771 extras::Language::Java => self.java.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
772 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
773 extras::Language::R => self.r.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
774 extras::Language::Rust => None, };
776
777 if let Some(val) = override_val {
778 return val.to_string();
779 }
780
781 match lang {
783 extras::Language::Node | extras::Language::Wasm | extras::Language::Java | extras::Language::Csharp => {
784 "camelCase".to_string()
785 }
786 extras::Language::Python
787 | extras::Language::Ruby
788 | extras::Language::Php
789 | extras::Language::Go
790 | extras::Language::Ffi
791 | extras::Language::Elixir
792 | extras::Language::R
793 | extras::Language::Rust => "snake_case".to_string(),
794 }
795 }
796
797 pub fn rewrite_path(&self, rust_path: &str) -> String {
800 let mut mappings: Vec<_> = self.crate_config.path_mappings.iter().collect();
802 mappings.sort_by_key(|b| std::cmp::Reverse(b.0.len()));
803
804 for (from, to) in &mappings {
805 if rust_path.starts_with(from.as_str()) {
806 return format!("{}{}", to, &rust_path[from.len()..]);
807 }
808 }
809 rust_path.to_string()
810 }
811
812 pub fn effective_path_mappings(&self) -> HashMap<String, String> {
822 let mut mappings = HashMap::new();
823
824 if self.crate_config.auto_path_mappings {
825 let core_import = self.core_import();
826
827 for source in &self.crate_config.sources {
828 let source_str = source.to_string_lossy();
829 if let Some(after_crates) = find_after_crates_prefix(&source_str) {
831 if let Some(slash_pos) = after_crates.find('/') {
833 let crate_dir = &after_crates[..slash_pos];
834 let crate_ident = crate_dir.replace('-', "_");
835 if crate_ident != core_import && !mappings.contains_key(&crate_ident) {
837 mappings.insert(crate_ident, core_import.clone());
838 }
839 }
840 }
841 }
842 }
843
844 for (from, to) in &self.crate_config.path_mappings {
846 mappings.insert(from.clone(), to.clone());
847 }
848
849 mappings
850 }
851}
852
853fn find_after_crates_prefix(path: &str) -> Option<&str> {
860 if let Some(pos) = path.find("/crates/") {
864 return Some(&path[pos + "/crates/".len()..]);
865 }
866 if let Some(stripped) = path.strip_prefix("crates/") {
867 return Some(stripped);
868 }
869 None
870}
871
872pub fn resolve_output_dir(config_path: Option<&PathBuf>, crate_name: &str, default: &str) -> String {
875 config_path
876 .map(|p| p.to_string_lossy().replace("{name}", crate_name))
877 .unwrap_or_else(|| default.replace("{name}", crate_name))
878}
879
880pub fn detect_serde_available(output_dir: &str) -> bool {
886 let src_path = std::path::Path::new(output_dir);
887 let mut dir = src_path;
889 loop {
890 let cargo_toml = dir.join("Cargo.toml");
891 if cargo_toml.exists() {
892 return cargo_toml_has_serde(&cargo_toml);
893 }
894 match dir.parent() {
895 Some(parent) if !parent.as_os_str().is_empty() => dir = parent,
896 _ => break,
897 }
898 }
899 false
900}
901
902fn cargo_toml_has_serde(path: &std::path::Path) -> bool {
908 let content = match std::fs::read_to_string(path) {
909 Ok(c) => c,
910 Err(_) => return false,
911 };
912
913 let has_serde_json = content.contains("serde_json");
914 let has_serde_dep = content.lines().any(|line| {
918 let trimmed = line.trim();
919 trimmed.starts_with("serde ")
921 || trimmed.starts_with("serde=")
922 || trimmed.starts_with("serde.")
923 || trimmed == "[dependencies.serde]"
924 });
925
926 has_serde_json && has_serde_dep
927}
928
929#[cfg(test)]
930mod tests {
931 use super::*;
932
933 fn minimal_config() -> AlefConfig {
934 toml::from_str(
935 r#"
936languages = ["python", "node", "rust"]
937
938[crate]
939name = "test-lib"
940sources = ["src/lib.rs"]
941"#,
942 )
943 .unwrap()
944 }
945
946 #[test]
947 fn lint_config_falls_back_to_defaults() {
948 let config = minimal_config();
949 assert!(config.lint.is_none());
950
951 let py = config.lint_config_for_language(Language::Python);
952 assert!(py.format.is_some());
953 assert!(py.check.is_some());
954 assert!(py.typecheck.is_some());
955
956 let node = config.lint_config_for_language(Language::Node);
957 assert!(node.format.is_some());
958 assert!(node.check.is_some());
959 }
960
961 #[test]
962 fn lint_config_explicit_overrides_default() {
963 let config: AlefConfig = toml::from_str(
964 r#"
965languages = ["python"]
966
967[crate]
968name = "test-lib"
969sources = ["src/lib.rs"]
970
971[lint.python]
972format = "custom-formatter"
973check = "custom-checker"
974"#,
975 )
976 .unwrap();
977
978 let py = config.lint_config_for_language(Language::Python);
979 assert_eq!(py.format.unwrap().commands(), vec!["custom-formatter"]);
980 assert_eq!(py.check.unwrap().commands(), vec!["custom-checker"]);
981 assert!(py.typecheck.is_none()); }
983
984 #[test]
985 fn lint_config_partial_override_does_not_merge() {
986 let config: AlefConfig = toml::from_str(
987 r#"
988languages = ["python"]
989
990[crate]
991name = "test-lib"
992sources = ["src/lib.rs"]
993
994[lint.python]
995format = "only-format"
996"#,
997 )
998 .unwrap();
999
1000 let py = config.lint_config_for_language(Language::Python);
1001 assert_eq!(py.format.unwrap().commands(), vec!["only-format"]);
1002 assert!(py.check.is_none());
1004 assert!(py.typecheck.is_none());
1005 }
1006
1007 #[test]
1008 fn lint_config_unconfigured_language_uses_defaults() {
1009 let config: AlefConfig = toml::from_str(
1010 r#"
1011languages = ["python", "node"]
1012
1013[crate]
1014name = "test-lib"
1015sources = ["src/lib.rs"]
1016
1017[lint.python]
1018format = "custom"
1019"#,
1020 )
1021 .unwrap();
1022
1023 let py = config.lint_config_for_language(Language::Python);
1025 assert_eq!(py.format.unwrap().commands(), vec!["custom"]);
1026
1027 let node = config.lint_config_for_language(Language::Node);
1029 let fmt = node.format.unwrap().commands().join(" ");
1030 assert!(fmt.contains("oxfmt"));
1031 }
1032
1033 #[test]
1034 fn update_config_falls_back_to_defaults() {
1035 let config = minimal_config();
1036 assert!(config.update.is_none());
1037
1038 let py = config.update_config_for_language(Language::Python);
1039 assert!(py.update.is_some());
1040 assert!(py.upgrade.is_some());
1041
1042 let rust = config.update_config_for_language(Language::Rust);
1043 let update = rust.update.unwrap().commands().join(" ");
1044 assert!(update.contains("cargo update"));
1045 }
1046
1047 #[test]
1048 fn update_config_explicit_overrides_default() {
1049 let config: AlefConfig = toml::from_str(
1050 r#"
1051languages = ["rust"]
1052
1053[crate]
1054name = "test-lib"
1055sources = ["src/lib.rs"]
1056
1057[update.rust]
1058update = "my-custom-update"
1059upgrade = ["step1", "step2"]
1060"#,
1061 )
1062 .unwrap();
1063
1064 let rust = config.update_config_for_language(Language::Rust);
1065 assert_eq!(rust.update.unwrap().commands(), vec!["my-custom-update"]);
1066 assert_eq!(rust.upgrade.unwrap().commands(), vec!["step1", "step2"]);
1067 }
1068
1069 #[test]
1070 fn test_config_falls_back_to_defaults() {
1071 let config = minimal_config();
1072 assert!(config.test.is_none());
1073
1074 let py = config.test_config_for_language(Language::Python);
1075 assert!(py.command.is_some());
1076 assert!(py.coverage.is_some());
1077 assert!(py.e2e.is_none());
1078
1079 let rust = config.test_config_for_language(Language::Rust);
1080 let cmd = rust.command.unwrap().commands().join(" ");
1081 assert!(cmd.contains("cargo test"));
1082 }
1083
1084 #[test]
1085 fn test_config_explicit_overrides_default() {
1086 let config: AlefConfig = toml::from_str(
1087 r#"
1088languages = ["python"]
1089
1090[crate]
1091name = "test-lib"
1092sources = ["src/lib.rs"]
1093
1094[test.python]
1095command = "my-custom-test"
1096"#,
1097 )
1098 .unwrap();
1099
1100 let py = config.test_config_for_language(Language::Python);
1101 assert_eq!(py.command.unwrap().commands(), vec!["my-custom-test"]);
1102 assert!(py.coverage.is_none()); }
1104
1105 #[test]
1106 fn setup_config_falls_back_to_defaults() {
1107 let config = minimal_config();
1108 assert!(config.setup.is_none());
1109
1110 let py = config.setup_config_for_language(Language::Python);
1111 assert!(py.install.is_some());
1112 let install = py.install.unwrap().commands().join(" ");
1113 assert!(install.contains("uv sync"));
1114
1115 let rust = config.setup_config_for_language(Language::Rust);
1116 let install = rust.install.unwrap().commands().join(" ");
1117 assert!(install.contains("rustup update"));
1118 }
1119
1120 #[test]
1121 fn setup_config_explicit_overrides_default() {
1122 let config: AlefConfig = toml::from_str(
1123 r#"
1124languages = ["python"]
1125
1126[crate]
1127name = "test-lib"
1128sources = ["src/lib.rs"]
1129
1130[setup.python]
1131install = "my-custom-install"
1132"#,
1133 )
1134 .unwrap();
1135
1136 let py = config.setup_config_for_language(Language::Python);
1137 assert_eq!(py.install.unwrap().commands(), vec!["my-custom-install"]);
1138 }
1139
1140 #[test]
1141 fn clean_config_falls_back_to_defaults() {
1142 let config = minimal_config();
1143 assert!(config.clean.is_none());
1144
1145 let py = config.clean_config_for_language(Language::Python);
1146 assert!(py.clean.is_some());
1147 let clean = py.clean.unwrap().commands().join(" ");
1148 assert!(clean.contains("__pycache__"));
1149
1150 let rust = config.clean_config_for_language(Language::Rust);
1151 let clean = rust.clean.unwrap().commands().join(" ");
1152 assert!(clean.contains("cargo clean"));
1153 }
1154
1155 #[test]
1156 fn clean_config_explicit_overrides_default() {
1157 let config: AlefConfig = toml::from_str(
1158 r#"
1159languages = ["rust"]
1160
1161[crate]
1162name = "test-lib"
1163sources = ["src/lib.rs"]
1164
1165[clean.rust]
1166clean = "my-custom-clean"
1167"#,
1168 )
1169 .unwrap();
1170
1171 let rust = config.clean_config_for_language(Language::Rust);
1172 assert_eq!(rust.clean.unwrap().commands(), vec!["my-custom-clean"]);
1173 }
1174
1175 #[test]
1176 fn build_command_config_falls_back_to_defaults() {
1177 let config = minimal_config();
1178 assert!(config.build_commands.is_none());
1179
1180 let py = config.build_command_config_for_language(Language::Python);
1181 assert!(py.build.is_some());
1182 assert!(py.build_release.is_some());
1183 let build = py.build.unwrap().commands().join(" ");
1184 assert!(build.contains("maturin develop"));
1185
1186 let rust = config.build_command_config_for_language(Language::Rust);
1187 let build = rust.build.unwrap().commands().join(" ");
1188 assert!(build.contains("cargo build --workspace"));
1189 }
1190
1191 #[test]
1192 fn build_command_config_explicit_overrides_default() {
1193 let config: AlefConfig = toml::from_str(
1194 r#"
1195languages = ["rust"]
1196
1197[crate]
1198name = "test-lib"
1199sources = ["src/lib.rs"]
1200
1201[build_commands.rust]
1202build = "my-custom-build"
1203build_release = "my-custom-build --release"
1204"#,
1205 )
1206 .unwrap();
1207
1208 let rust = config.build_command_config_for_language(Language::Rust);
1209 assert_eq!(rust.build.unwrap().commands(), vec!["my-custom-build"]);
1210 assert_eq!(
1211 rust.build_release.unwrap().commands(),
1212 vec!["my-custom-build --release"]
1213 );
1214 }
1215
1216 #[test]
1217 fn build_command_config_uses_crate_name() {
1218 let config = minimal_config();
1219 let py = config.build_command_config_for_language(Language::Python);
1220 let build = py.build.unwrap().commands().join(" ");
1221 assert!(
1222 build.contains("test-lib-py"),
1223 "Python build should reference crate name, got: {build}"
1224 );
1225 }
1226
1227 #[test]
1228 fn package_dir_defaults_are_correct() {
1229 let config = minimal_config();
1230 assert_eq!(config.package_dir(Language::Python), "packages/python");
1231 assert_eq!(config.package_dir(Language::Node), "packages/node");
1232 assert_eq!(config.package_dir(Language::Ruby), "packages/ruby");
1233 assert_eq!(config.package_dir(Language::Go), "packages/go");
1234 assert_eq!(config.package_dir(Language::Java), "packages/java");
1235 }
1236
1237 #[test]
1238 fn explicit_lint_config_preserves_precondition_and_before() {
1239 let config: AlefConfig = toml::from_str(
1240 r#"
1241languages = ["go"]
1242
1243[crate]
1244name = "test"
1245sources = ["src/lib.rs"]
1246
1247[lint.go]
1248precondition = "test -f target/release/libtest_ffi.so"
1249before = "cargo build --release -p test-ffi"
1250format = "gofmt -w packages/go"
1251check = "golangci-lint run ./..."
1252"#,
1253 )
1254 .unwrap();
1255
1256 let lint = config.lint_config_for_language(Language::Go);
1257 assert_eq!(
1258 lint.precondition.as_deref(),
1259 Some("test -f target/release/libtest_ffi.so"),
1260 "precondition should be preserved from explicit config"
1261 );
1262 assert_eq!(
1263 lint.before.unwrap().commands(),
1264 vec!["cargo build --release -p test-ffi"],
1265 "before should be preserved from explicit config"
1266 );
1267 }
1268
1269 #[test]
1270 fn explicit_lint_config_with_before_list_preserves_all_commands() {
1271 let config: AlefConfig = toml::from_str(
1272 r#"
1273languages = ["go"]
1274
1275[crate]
1276name = "test"
1277sources = ["src/lib.rs"]
1278
1279[lint.go]
1280before = ["cargo build --release -p test-ffi", "cp target/release/libtest_ffi.so packages/go/"]
1281check = "golangci-lint run ./..."
1282"#,
1283 )
1284 .unwrap();
1285
1286 let lint = config.lint_config_for_language(Language::Go);
1287 assert!(lint.precondition.is_none(), "precondition should be None when not set");
1288 assert_eq!(
1289 lint.before.unwrap().commands(),
1290 vec![
1291 "cargo build --release -p test-ffi",
1292 "cp target/release/libtest_ffi.so packages/go/"
1293 ],
1294 "before list should be preserved from explicit config"
1295 );
1296 }
1297
1298 #[test]
1299 fn default_lint_config_has_no_precondition_or_before() {
1300 let config = minimal_config();
1301 let py = config.lint_config_for_language(Language::Python);
1302 assert!(
1303 py.precondition.is_none(),
1304 "default lint config should have no precondition"
1305 );
1306 assert!(py.before.is_none(), "default lint config should have no before");
1307
1308 let go = config.lint_config_for_language(Language::Go);
1309 assert!(
1310 go.precondition.is_none(),
1311 "default Go lint config should have no precondition"
1312 );
1313 assert!(go.before.is_none(), "default Go lint config should have no before");
1314 }
1315
1316 #[test]
1317 fn explicit_test_config_preserves_precondition_and_before() {
1318 let config: AlefConfig = toml::from_str(
1319 r#"
1320languages = ["python"]
1321
1322[crate]
1323name = "test"
1324sources = ["src/lib.rs"]
1325
1326[test.python]
1327precondition = "test -f target/release/libtest.so"
1328before = "maturin develop"
1329command = "pytest"
1330"#,
1331 )
1332 .unwrap();
1333
1334 let test = config.test_config_for_language(Language::Python);
1335 assert_eq!(
1336 test.precondition.as_deref(),
1337 Some("test -f target/release/libtest.so"),
1338 "test precondition should be preserved"
1339 );
1340 assert_eq!(
1341 test.before.unwrap().commands(),
1342 vec!["maturin develop"],
1343 "test before should be preserved"
1344 );
1345 }
1346
1347 #[test]
1348 fn default_test_config_has_no_precondition_or_before() {
1349 let config = minimal_config();
1350 let py = config.test_config_for_language(Language::Python);
1351 assert!(
1352 py.precondition.is_none(),
1353 "default test config should have no precondition"
1354 );
1355 assert!(py.before.is_none(), "default test config should have no before");
1356 }
1357
1358 #[test]
1359 fn explicit_setup_config_preserves_precondition_and_before() {
1360 let config: AlefConfig = toml::from_str(
1361 r#"
1362languages = ["python"]
1363
1364[crate]
1365name = "test"
1366sources = ["src/lib.rs"]
1367
1368[setup.python]
1369precondition = "which uv"
1370before = "pip install uv"
1371install = "uv sync"
1372"#,
1373 )
1374 .unwrap();
1375
1376 let setup = config.setup_config_for_language(Language::Python);
1377 assert_eq!(
1378 setup.precondition.as_deref(),
1379 Some("which uv"),
1380 "setup precondition should be preserved"
1381 );
1382 assert_eq!(
1383 setup.before.unwrap().commands(),
1384 vec!["pip install uv"],
1385 "setup before should be preserved"
1386 );
1387 }
1388
1389 #[test]
1390 fn default_setup_config_has_no_precondition_or_before() {
1391 let config = minimal_config();
1392 let py = config.setup_config_for_language(Language::Python);
1393 assert!(
1394 py.precondition.is_none(),
1395 "default setup config should have no precondition"
1396 );
1397 assert!(py.before.is_none(), "default setup config should have no before");
1398 }
1399
1400 #[test]
1401 fn explicit_update_config_preserves_precondition_and_before() {
1402 let config: AlefConfig = toml::from_str(
1403 r#"
1404languages = ["rust"]
1405
1406[crate]
1407name = "test"
1408sources = ["src/lib.rs"]
1409
1410[update.rust]
1411precondition = "test -f Cargo.lock"
1412before = "cargo fetch"
1413update = "cargo update"
1414"#,
1415 )
1416 .unwrap();
1417
1418 let update = config.update_config_for_language(Language::Rust);
1419 assert_eq!(
1420 update.precondition.as_deref(),
1421 Some("test -f Cargo.lock"),
1422 "update precondition should be preserved"
1423 );
1424 assert_eq!(
1425 update.before.unwrap().commands(),
1426 vec!["cargo fetch"],
1427 "update before should be preserved"
1428 );
1429 }
1430
1431 #[test]
1432 fn default_update_config_has_no_precondition_or_before() {
1433 let config = minimal_config();
1434 let rust = config.update_config_for_language(Language::Rust);
1435 assert!(
1436 rust.precondition.is_none(),
1437 "default update config should have no precondition"
1438 );
1439 assert!(rust.before.is_none(), "default update config should have no before");
1440 }
1441
1442 #[test]
1443 fn explicit_clean_config_preserves_precondition_and_before() {
1444 let config: AlefConfig = toml::from_str(
1445 r#"
1446languages = ["rust"]
1447
1448[crate]
1449name = "test"
1450sources = ["src/lib.rs"]
1451
1452[clean.rust]
1453precondition = "test -d target"
1454before = "echo cleaning"
1455clean = "cargo clean"
1456"#,
1457 )
1458 .unwrap();
1459
1460 let clean = config.clean_config_for_language(Language::Rust);
1461 assert_eq!(
1462 clean.precondition.as_deref(),
1463 Some("test -d target"),
1464 "clean precondition should be preserved"
1465 );
1466 assert_eq!(
1467 clean.before.unwrap().commands(),
1468 vec!["echo cleaning"],
1469 "clean before should be preserved"
1470 );
1471 }
1472
1473 #[test]
1474 fn default_clean_config_has_no_precondition_or_before() {
1475 let config = minimal_config();
1476 let rust = config.clean_config_for_language(Language::Rust);
1477 assert!(
1478 rust.precondition.is_none(),
1479 "default clean config should have no precondition"
1480 );
1481 assert!(rust.before.is_none(), "default clean config should have no before");
1482 }
1483
1484 #[test]
1485 fn explicit_build_command_config_preserves_precondition_and_before() {
1486 let config: AlefConfig = toml::from_str(
1487 r#"
1488languages = ["go"]
1489
1490[crate]
1491name = "test"
1492sources = ["src/lib.rs"]
1493
1494[build_commands.go]
1495precondition = "which go"
1496before = "cargo build --release -p test-ffi"
1497build = "cd packages/go && go build ./..."
1498build_release = "cd packages/go && go build -ldflags='-s -w' ./..."
1499"#,
1500 )
1501 .unwrap();
1502
1503 let build = config.build_command_config_for_language(Language::Go);
1504 assert_eq!(
1505 build.precondition.as_deref(),
1506 Some("which go"),
1507 "build precondition should be preserved"
1508 );
1509 assert_eq!(
1510 build.before.unwrap().commands(),
1511 vec!["cargo build --release -p test-ffi"],
1512 "build before should be preserved"
1513 );
1514 }
1515
1516 #[test]
1517 fn default_build_command_config_has_no_precondition_or_before() {
1518 let config = minimal_config();
1519 let rust = config.build_command_config_for_language(Language::Rust);
1520 assert!(
1521 rust.precondition.is_none(),
1522 "default build command config should have no precondition"
1523 );
1524 assert!(
1525 rust.before.is_none(),
1526 "default build command config should have no before"
1527 );
1528 }
1529
1530 #[test]
1531 fn alef_meta_defaults_when_omitted() {
1532 let config = minimal_config();
1533 assert!(config.alef.version.is_none());
1534 }
1535
1536 #[test]
1537 fn alef_meta_parses_version() {
1538 let config: AlefConfig = toml::from_str(
1539 r#"
1540languages = ["python"]
1541
1542[alef]
1543version = "0.7.5"
1544
1545[crate]
1546name = "test-lib"
1547sources = ["src/lib.rs"]
1548"#,
1549 )
1550 .unwrap();
1551 assert_eq!(config.alef.version.as_deref(), Some("0.7.5"));
1552 }
1553
1554 #[test]
1555 fn alef_meta_empty_section_defaults_version_to_none() {
1556 let config: AlefConfig = toml::from_str(
1557 r#"
1558languages = ["python"]
1559
1560[alef]
1561
1562[crate]
1563name = "test-lib"
1564sources = ["src/lib.rs"]
1565"#,
1566 )
1567 .unwrap();
1568 assert!(config.alef.version.is_none());
1569 }
1570}