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