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 features_for_language(&self, lang: extras::Language) -> &[String] {
247 let override_features = match lang {
248 extras::Language::Python => self.python.as_ref().and_then(|c| c.features.as_deref()),
249 extras::Language::Node => self.node.as_ref().and_then(|c| c.features.as_deref()),
250 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.features.as_deref()),
251 extras::Language::Php => self.php.as_ref().and_then(|c| c.features.as_deref()),
252 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.features.as_deref()),
253 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.features.as_deref()),
254 extras::Language::Ffi => self.ffi.as_ref().and_then(|c| c.features.as_deref()),
255 extras::Language::Go => self.go.as_ref().and_then(|c| c.features.as_deref()),
256 extras::Language::Java => self.java.as_ref().and_then(|c| c.features.as_deref()),
257 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.features.as_deref()),
258 extras::Language::R => self.r.as_ref().and_then(|c| c.features.as_deref()),
259 extras::Language::Rust => None, };
261 override_features.unwrap_or(&self.crate_config.features)
262 }
263
264 pub fn extra_deps_for_language(&self, lang: extras::Language) -> HashMap<String, toml::Value> {
268 let mut deps = self.crate_config.extra_dependencies.clone();
269 let lang_deps = match lang {
270 extras::Language::Python => self.python.as_ref().map(|c| &c.extra_dependencies),
271 extras::Language::Node => self.node.as_ref().map(|c| &c.extra_dependencies),
272 extras::Language::Ruby => self.ruby.as_ref().map(|c| &c.extra_dependencies),
273 extras::Language::Php => self.php.as_ref().map(|c| &c.extra_dependencies),
274 extras::Language::Elixir => self.elixir.as_ref().map(|c| &c.extra_dependencies),
275 extras::Language::Wasm => self.wasm.as_ref().map(|c| &c.extra_dependencies),
276 _ => None,
277 };
278 if let Some(lang_deps) = lang_deps {
279 deps.extend(lang_deps.iter().map(|(k, v)| (k.clone(), v.clone())));
280 }
281 deps
282 }
283
284 pub fn package_dir(&self, lang: extras::Language) -> String {
289 let override_path = match lang {
290 extras::Language::Python => self.python.as_ref().and_then(|c| c.scaffold_output.as_ref()),
291 extras::Language::Node => self.node.as_ref().and_then(|c| c.scaffold_output.as_ref()),
292 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.scaffold_output.as_ref()),
293 extras::Language::Php => self.php.as_ref().and_then(|c| c.scaffold_output.as_ref()),
294 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.scaffold_output.as_ref()),
295 _ => None,
296 };
297 if let Some(p) = override_path {
298 p.to_string_lossy().to_string()
299 } else {
300 match lang {
301 extras::Language::Python => "packages/python".to_string(),
302 extras::Language::Node => "packages/node".to_string(),
303 extras::Language::Ruby => "packages/ruby".to_string(),
304 extras::Language::Php => "packages/php".to_string(),
305 extras::Language::Elixir => "packages/elixir".to_string(),
306 _ => format!("packages/{lang}"),
307 }
308 }
309 }
310
311 pub fn lint_config_for_language(&self, lang: extras::Language) -> output::LintConfig {
316 if let Some(lint_map) = &self.lint {
317 let lang_str = lang.to_string();
318 if let Some(explicit) = lint_map.get(&lang_str) {
319 return explicit.clone();
320 }
321 }
322 let output_dir = self.package_dir(lang);
323 lint_defaults::default_lint_config(lang, &output_dir)
324 }
325
326 pub fn update_config_for_language(&self, lang: extras::Language) -> output::UpdateConfig {
331 if let Some(update_map) = &self.update {
332 let lang_str = lang.to_string();
333 if let Some(explicit) = update_map.get(&lang_str) {
334 return explicit.clone();
335 }
336 }
337 let output_dir = self.package_dir(lang);
338 update_defaults::default_update_config(lang, &output_dir)
339 }
340
341 pub fn test_config_for_language(&self, lang: extras::Language) -> output::TestConfig {
346 if let Some(test_map) = &self.test {
347 let lang_str = lang.to_string();
348 if let Some(explicit) = test_map.get(&lang_str) {
349 return explicit.clone();
350 }
351 }
352 let output_dir = self.package_dir(lang);
353 test_defaults::default_test_config(lang, &output_dir)
354 }
355
356 pub fn setup_config_for_language(&self, lang: extras::Language) -> output::SetupConfig {
361 if let Some(setup_map) = &self.setup {
362 let lang_str = lang.to_string();
363 if let Some(explicit) = setup_map.get(&lang_str) {
364 return explicit.clone();
365 }
366 }
367 let output_dir = self.package_dir(lang);
368 setup_defaults::default_setup_config(lang, &output_dir)
369 }
370
371 pub fn clean_config_for_language(&self, lang: extras::Language) -> output::CleanConfig {
376 if let Some(clean_map) = &self.clean {
377 let lang_str = lang.to_string();
378 if let Some(explicit) = clean_map.get(&lang_str) {
379 return explicit.clone();
380 }
381 }
382 let output_dir = self.package_dir(lang);
383 clean_defaults::default_clean_config(lang, &output_dir)
384 }
385
386 pub fn build_command_config_for_language(&self, lang: extras::Language) -> output::BuildCommandConfig {
391 if let Some(build_map) = &self.build_commands {
392 let lang_str = lang.to_string();
393 if let Some(explicit) = build_map.get(&lang_str) {
394 return explicit.clone();
395 }
396 }
397 let output_dir = self.package_dir(lang);
398 let crate_name = &self.crate_config.name;
399 build_defaults::default_build_config(lang, &output_dir, crate_name)
400 }
401
402 pub fn core_import(&self) -> String {
404 self.crate_config
405 .core_import
406 .clone()
407 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
408 }
409
410 pub fn error_type(&self) -> String {
412 self.crate_config
413 .error_type
414 .clone()
415 .unwrap_or_else(|| "Error".to_string())
416 }
417
418 pub fn error_constructor(&self) -> String {
421 self.crate_config
422 .error_constructor
423 .clone()
424 .unwrap_or_else(|| format!("{}::{}::from({{msg}})", self.core_import(), self.error_type()))
425 }
426
427 pub fn ffi_prefix(&self) -> String {
429 self.ffi
430 .as_ref()
431 .and_then(|f| f.prefix.as_ref())
432 .cloned()
433 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
434 }
435
436 pub fn ffi_lib_name(&self) -> String {
444 if let Some(name) = self.ffi.as_ref().and_then(|f| f.lib_name.as_ref()) {
446 return name.clone();
447 }
448
449 if let Some(ffi_path) = self.output.ffi.as_ref() {
452 let path = std::path::Path::new(ffi_path);
453 let components: Vec<_> = path
456 .components()
457 .filter_map(|c| {
458 if let std::path::Component::Normal(s) = c {
459 s.to_str()
460 } else {
461 None
462 }
463 })
464 .collect();
465 let crate_dir = components
468 .iter()
469 .rev()
470 .find(|&&s| s != "src" && s != "lib" && s != "include")
471 .copied();
472 if let Some(dir) = crate_dir {
473 return dir.replace('-', "_");
474 }
475 }
476
477 format!("{}_ffi", self.ffi_prefix())
479 }
480
481 pub fn ffi_header_name(&self) -> String {
483 self.ffi
484 .as_ref()
485 .and_then(|f| f.header_name.as_ref())
486 .cloned()
487 .unwrap_or_else(|| format!("{}.h", self.ffi_prefix()))
488 }
489
490 pub fn python_module_name(&self) -> String {
492 self.python
493 .as_ref()
494 .and_then(|p| p.module_name.as_ref())
495 .cloned()
496 .unwrap_or_else(|| format!("_{}", self.crate_config.name.replace('-', "_")))
497 }
498
499 pub fn python_pip_name(&self) -> String {
503 self.python
504 .as_ref()
505 .and_then(|p| p.pip_name.as_ref())
506 .cloned()
507 .unwrap_or_else(|| self.crate_config.name.clone())
508 }
509
510 pub fn php_autoload_namespace(&self) -> String {
515 use heck::ToPascalCase;
516 let ext = self.php_extension_name();
517 if ext.contains('_') {
518 ext.split('_')
519 .map(|p| p.to_pascal_case())
520 .collect::<Vec<_>>()
521 .join("\\")
522 } else {
523 ext.to_pascal_case()
524 }
525 }
526
527 pub fn node_package_name(&self) -> String {
529 self.node
530 .as_ref()
531 .and_then(|n| n.package_name.as_ref())
532 .cloned()
533 .unwrap_or_else(|| self.crate_config.name.clone())
534 }
535
536 pub fn ruby_gem_name(&self) -> String {
538 self.ruby
539 .as_ref()
540 .and_then(|r| r.gem_name.as_ref())
541 .cloned()
542 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
543 }
544
545 pub fn php_extension_name(&self) -> String {
547 self.php
548 .as_ref()
549 .and_then(|p| p.extension_name.as_ref())
550 .cloned()
551 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
552 }
553
554 pub fn elixir_app_name(&self) -> String {
556 self.elixir
557 .as_ref()
558 .and_then(|e| e.app_name.as_ref())
559 .cloned()
560 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
561 }
562
563 pub fn go_module(&self) -> String {
565 self.go
566 .as_ref()
567 .and_then(|g| g.module.as_ref())
568 .cloned()
569 .unwrap_or_else(|| format!("github.com/kreuzberg-dev/{}", self.crate_config.name))
570 }
571
572 pub fn github_repo(&self) -> String {
579 if let Some(e2e) = &self.e2e {
580 if let Some(url) = &e2e.registry.github_repo {
581 return url.clone();
582 }
583 }
584 self.scaffold
585 .as_ref()
586 .and_then(|s| s.repository.as_ref())
587 .cloned()
588 .unwrap_or_else(|| format!("https://github.com/kreuzberg-dev/{}", self.crate_config.name))
589 }
590
591 pub fn java_package(&self) -> String {
593 self.java
594 .as_ref()
595 .and_then(|j| j.package.as_ref())
596 .cloned()
597 .unwrap_or_else(|| "dev.kreuzberg".to_string())
598 }
599
600 pub fn java_group_id(&self) -> String {
605 self.java_package()
606 }
607
608 pub fn csharp_namespace(&self) -> String {
610 self.csharp
611 .as_ref()
612 .and_then(|c| c.namespace.as_ref())
613 .cloned()
614 .unwrap_or_else(|| {
615 use heck::ToPascalCase;
616 self.crate_config.name.to_pascal_case()
617 })
618 }
619
620 pub fn core_crate_dir(&self) -> String {
626 if let Some(first_source) = self.crate_config.sources.first() {
629 let path = std::path::Path::new(first_source);
630 let mut current = path.parent();
631 while let Some(dir) = current {
632 if dir.file_name().is_some_and(|n| n == "src") {
633 if let Some(crate_dir) = dir.parent() {
634 if let Some(dir_name) = crate_dir.file_name() {
635 return dir_name.to_string_lossy().into_owned();
636 }
637 }
638 break;
639 }
640 current = dir.parent();
641 }
642 }
643 self.crate_config.name.clone()
644 }
645
646 pub fn wasm_type_prefix(&self) -> String {
649 self.wasm
650 .as_ref()
651 .and_then(|w| w.type_prefix.as_ref())
652 .cloned()
653 .unwrap_or_else(|| "Wasm".to_string())
654 }
655
656 pub fn node_type_prefix(&self) -> String {
659 self.node
660 .as_ref()
661 .and_then(|n| n.type_prefix.as_ref())
662 .cloned()
663 .unwrap_or_else(|| "Js".to_string())
664 }
665
666 pub fn r_package_name(&self) -> String {
668 self.r
669 .as_ref()
670 .and_then(|r| r.package_name.as_ref())
671 .cloned()
672 .unwrap_or_else(|| self.crate_config.name.clone())
673 }
674
675 pub fn resolved_version(&self) -> Option<String> {
678 let content = std::fs::read_to_string(&self.crate_config.version_from).ok()?;
679 let value: toml::Value = toml::from_str(&content).ok()?;
680 if let Some(v) = value
681 .get("workspace")
682 .and_then(|w| w.get("package"))
683 .and_then(|p| p.get("version"))
684 .and_then(|v| v.as_str())
685 {
686 return Some(v.to_string());
687 }
688 value
689 .get("package")
690 .and_then(|p| p.get("version"))
691 .and_then(|v| v.as_str())
692 .map(|v| v.to_string())
693 }
694
695 pub fn serde_rename_all_for_language(&self, lang: extras::Language) -> String {
703 let override_val = match lang {
705 extras::Language::Python => self.python.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
706 extras::Language::Node => self.node.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
707 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
708 extras::Language::Php => self.php.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
709 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
710 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
711 extras::Language::Ffi => self.ffi.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
712 extras::Language::Go => self.go.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
713 extras::Language::Java => self.java.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
714 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
715 extras::Language::R => self.r.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
716 extras::Language::Rust => None, };
718
719 if let Some(val) = override_val {
720 return val.to_string();
721 }
722
723 match lang {
725 extras::Language::Node | extras::Language::Wasm | extras::Language::Java | extras::Language::Csharp => {
726 "camelCase".to_string()
727 }
728 extras::Language::Python
729 | extras::Language::Ruby
730 | extras::Language::Php
731 | extras::Language::Go
732 | extras::Language::Ffi
733 | extras::Language::Elixir
734 | extras::Language::R
735 | extras::Language::Rust => "snake_case".to_string(),
736 }
737 }
738
739 pub fn rewrite_path(&self, rust_path: &str) -> String {
742 let mut mappings: Vec<_> = self.crate_config.path_mappings.iter().collect();
744 mappings.sort_by_key(|b| std::cmp::Reverse(b.0.len()));
745
746 for (from, to) in &mappings {
747 if rust_path.starts_with(from.as_str()) {
748 return format!("{}{}", to, &rust_path[from.len()..]);
749 }
750 }
751 rust_path.to_string()
752 }
753
754 pub fn effective_path_mappings(&self) -> HashMap<String, String> {
764 let mut mappings = HashMap::new();
765
766 if self.crate_config.auto_path_mappings {
767 let core_import = self.core_import();
768
769 for source in &self.crate_config.sources {
770 let source_str = source.to_string_lossy();
771 if let Some(after_crates) = find_after_crates_prefix(&source_str) {
773 if let Some(slash_pos) = after_crates.find('/') {
775 let crate_dir = &after_crates[..slash_pos];
776 let crate_ident = crate_dir.replace('-', "_");
777 if crate_ident != core_import && !mappings.contains_key(&crate_ident) {
779 mappings.insert(crate_ident, core_import.clone());
780 }
781 }
782 }
783 }
784 }
785
786 for (from, to) in &self.crate_config.path_mappings {
788 mappings.insert(from.clone(), to.clone());
789 }
790
791 mappings
792 }
793}
794
795fn find_after_crates_prefix(path: &str) -> Option<&str> {
802 if let Some(pos) = path.find("/crates/") {
806 return Some(&path[pos + "/crates/".len()..]);
807 }
808 if let Some(stripped) = path.strip_prefix("crates/") {
809 return Some(stripped);
810 }
811 None
812}
813
814pub fn resolve_output_dir(config_path: Option<&PathBuf>, crate_name: &str, default: &str) -> String {
817 config_path
818 .map(|p| p.to_string_lossy().replace("{name}", crate_name))
819 .unwrap_or_else(|| default.replace("{name}", crate_name))
820}
821
822pub fn detect_serde_available(output_dir: &str) -> bool {
828 let src_path = std::path::Path::new(output_dir);
829 let mut dir = src_path;
831 loop {
832 let cargo_toml = dir.join("Cargo.toml");
833 if cargo_toml.exists() {
834 return cargo_toml_has_serde(&cargo_toml);
835 }
836 match dir.parent() {
837 Some(parent) if !parent.as_os_str().is_empty() => dir = parent,
838 _ => break,
839 }
840 }
841 false
842}
843
844fn cargo_toml_has_serde(path: &std::path::Path) -> bool {
850 let content = match std::fs::read_to_string(path) {
851 Ok(c) => c,
852 Err(_) => return false,
853 };
854
855 let has_serde_json = content.contains("serde_json");
856 let has_serde_dep = content.lines().any(|line| {
860 let trimmed = line.trim();
861 trimmed.starts_with("serde ")
863 || trimmed.starts_with("serde=")
864 || trimmed.starts_with("serde.")
865 || trimmed == "[dependencies.serde]"
866 });
867
868 has_serde_json && has_serde_dep
869}
870
871#[cfg(test)]
872mod tests {
873 use super::*;
874
875 fn minimal_config() -> AlefConfig {
876 toml::from_str(
877 r#"
878languages = ["python", "node", "rust"]
879
880[crate]
881name = "test-lib"
882sources = ["src/lib.rs"]
883"#,
884 )
885 .unwrap()
886 }
887
888 #[test]
889 fn lint_config_falls_back_to_defaults() {
890 let config = minimal_config();
891 assert!(config.lint.is_none());
892
893 let py = config.lint_config_for_language(Language::Python);
894 assert!(py.format.is_some());
895 assert!(py.check.is_some());
896 assert!(py.typecheck.is_some());
897
898 let node = config.lint_config_for_language(Language::Node);
899 assert!(node.format.is_some());
900 assert!(node.check.is_some());
901 }
902
903 #[test]
904 fn lint_config_explicit_overrides_default() {
905 let config: AlefConfig = toml::from_str(
906 r#"
907languages = ["python"]
908
909[crate]
910name = "test-lib"
911sources = ["src/lib.rs"]
912
913[lint.python]
914format = "custom-formatter"
915check = "custom-checker"
916"#,
917 )
918 .unwrap();
919
920 let py = config.lint_config_for_language(Language::Python);
921 assert_eq!(py.format.unwrap().commands(), vec!["custom-formatter"]);
922 assert_eq!(py.check.unwrap().commands(), vec!["custom-checker"]);
923 assert!(py.typecheck.is_none()); }
925
926 #[test]
927 fn lint_config_partial_override_does_not_merge() {
928 let config: AlefConfig = toml::from_str(
929 r#"
930languages = ["python"]
931
932[crate]
933name = "test-lib"
934sources = ["src/lib.rs"]
935
936[lint.python]
937format = "only-format"
938"#,
939 )
940 .unwrap();
941
942 let py = config.lint_config_for_language(Language::Python);
943 assert_eq!(py.format.unwrap().commands(), vec!["only-format"]);
944 assert!(py.check.is_none());
946 assert!(py.typecheck.is_none());
947 }
948
949 #[test]
950 fn lint_config_unconfigured_language_uses_defaults() {
951 let config: AlefConfig = toml::from_str(
952 r#"
953languages = ["python", "node"]
954
955[crate]
956name = "test-lib"
957sources = ["src/lib.rs"]
958
959[lint.python]
960format = "custom"
961"#,
962 )
963 .unwrap();
964
965 let py = config.lint_config_for_language(Language::Python);
967 assert_eq!(py.format.unwrap().commands(), vec!["custom"]);
968
969 let node = config.lint_config_for_language(Language::Node);
971 let fmt = node.format.unwrap().commands().join(" ");
972 assert!(fmt.contains("oxfmt"));
973 }
974
975 #[test]
976 fn update_config_falls_back_to_defaults() {
977 let config = minimal_config();
978 assert!(config.update.is_none());
979
980 let py = config.update_config_for_language(Language::Python);
981 assert!(py.update.is_some());
982 assert!(py.upgrade.is_some());
983
984 let rust = config.update_config_for_language(Language::Rust);
985 let update = rust.update.unwrap().commands().join(" ");
986 assert!(update.contains("cargo update"));
987 }
988
989 #[test]
990 fn update_config_explicit_overrides_default() {
991 let config: AlefConfig = toml::from_str(
992 r#"
993languages = ["rust"]
994
995[crate]
996name = "test-lib"
997sources = ["src/lib.rs"]
998
999[update.rust]
1000update = "my-custom-update"
1001upgrade = ["step1", "step2"]
1002"#,
1003 )
1004 .unwrap();
1005
1006 let rust = config.update_config_for_language(Language::Rust);
1007 assert_eq!(rust.update.unwrap().commands(), vec!["my-custom-update"]);
1008 assert_eq!(rust.upgrade.unwrap().commands(), vec!["step1", "step2"]);
1009 }
1010
1011 #[test]
1012 fn test_config_falls_back_to_defaults() {
1013 let config = minimal_config();
1014 assert!(config.test.is_none());
1015
1016 let py = config.test_config_for_language(Language::Python);
1017 assert!(py.command.is_some());
1018 assert!(py.coverage.is_some());
1019 assert!(py.e2e.is_none());
1020
1021 let rust = config.test_config_for_language(Language::Rust);
1022 let cmd = rust.command.unwrap().commands().join(" ");
1023 assert!(cmd.contains("cargo test"));
1024 }
1025
1026 #[test]
1027 fn test_config_explicit_overrides_default() {
1028 let config: AlefConfig = toml::from_str(
1029 r#"
1030languages = ["python"]
1031
1032[crate]
1033name = "test-lib"
1034sources = ["src/lib.rs"]
1035
1036[test.python]
1037command = "my-custom-test"
1038"#,
1039 )
1040 .unwrap();
1041
1042 let py = config.test_config_for_language(Language::Python);
1043 assert_eq!(py.command.unwrap().commands(), vec!["my-custom-test"]);
1044 assert!(py.coverage.is_none()); }
1046
1047 #[test]
1048 fn setup_config_falls_back_to_defaults() {
1049 let config = minimal_config();
1050 assert!(config.setup.is_none());
1051
1052 let py = config.setup_config_for_language(Language::Python);
1053 assert!(py.install.is_some());
1054 let install = py.install.unwrap().commands().join(" ");
1055 assert!(install.contains("uv sync"));
1056
1057 let rust = config.setup_config_for_language(Language::Rust);
1058 let install = rust.install.unwrap().commands().join(" ");
1059 assert!(install.contains("rustup update"));
1060 }
1061
1062 #[test]
1063 fn setup_config_explicit_overrides_default() {
1064 let config: AlefConfig = toml::from_str(
1065 r#"
1066languages = ["python"]
1067
1068[crate]
1069name = "test-lib"
1070sources = ["src/lib.rs"]
1071
1072[setup.python]
1073install = "my-custom-install"
1074"#,
1075 )
1076 .unwrap();
1077
1078 let py = config.setup_config_for_language(Language::Python);
1079 assert_eq!(py.install.unwrap().commands(), vec!["my-custom-install"]);
1080 }
1081
1082 #[test]
1083 fn clean_config_falls_back_to_defaults() {
1084 let config = minimal_config();
1085 assert!(config.clean.is_none());
1086
1087 let py = config.clean_config_for_language(Language::Python);
1088 assert!(py.clean.is_some());
1089 let clean = py.clean.unwrap().commands().join(" ");
1090 assert!(clean.contains("__pycache__"));
1091
1092 let rust = config.clean_config_for_language(Language::Rust);
1093 let clean = rust.clean.unwrap().commands().join(" ");
1094 assert!(clean.contains("cargo clean"));
1095 }
1096
1097 #[test]
1098 fn clean_config_explicit_overrides_default() {
1099 let config: AlefConfig = toml::from_str(
1100 r#"
1101languages = ["rust"]
1102
1103[crate]
1104name = "test-lib"
1105sources = ["src/lib.rs"]
1106
1107[clean.rust]
1108clean = "my-custom-clean"
1109"#,
1110 )
1111 .unwrap();
1112
1113 let rust = config.clean_config_for_language(Language::Rust);
1114 assert_eq!(rust.clean.unwrap().commands(), vec!["my-custom-clean"]);
1115 }
1116
1117 #[test]
1118 fn build_command_config_falls_back_to_defaults() {
1119 let config = minimal_config();
1120 assert!(config.build_commands.is_none());
1121
1122 let py = config.build_command_config_for_language(Language::Python);
1123 assert!(py.build.is_some());
1124 assert!(py.build_release.is_some());
1125 let build = py.build.unwrap().commands().join(" ");
1126 assert!(build.contains("maturin develop"));
1127
1128 let rust = config.build_command_config_for_language(Language::Rust);
1129 let build = rust.build.unwrap().commands().join(" ");
1130 assert!(build.contains("cargo build --workspace"));
1131 }
1132
1133 #[test]
1134 fn build_command_config_explicit_overrides_default() {
1135 let config: AlefConfig = toml::from_str(
1136 r#"
1137languages = ["rust"]
1138
1139[crate]
1140name = "test-lib"
1141sources = ["src/lib.rs"]
1142
1143[build_commands.rust]
1144build = "my-custom-build"
1145build_release = "my-custom-build --release"
1146"#,
1147 )
1148 .unwrap();
1149
1150 let rust = config.build_command_config_for_language(Language::Rust);
1151 assert_eq!(rust.build.unwrap().commands(), vec!["my-custom-build"]);
1152 assert_eq!(
1153 rust.build_release.unwrap().commands(),
1154 vec!["my-custom-build --release"]
1155 );
1156 }
1157
1158 #[test]
1159 fn build_command_config_uses_crate_name() {
1160 let config = minimal_config();
1161 let py = config.build_command_config_for_language(Language::Python);
1162 let build = py.build.unwrap().commands().join(" ");
1163 assert!(
1164 build.contains("test-lib-py"),
1165 "Python build should reference crate name, got: {build}"
1166 );
1167 }
1168
1169 #[test]
1170 fn package_dir_defaults_are_correct() {
1171 let config = minimal_config();
1172 assert_eq!(config.package_dir(Language::Python), "packages/python");
1173 assert_eq!(config.package_dir(Language::Node), "packages/node");
1174 assert_eq!(config.package_dir(Language::Ruby), "packages/ruby");
1175 assert_eq!(config.package_dir(Language::Go), "packages/go");
1176 assert_eq!(config.package_dir(Language::Java), "packages/java");
1177 }
1178
1179 #[test]
1180 fn explicit_lint_config_preserves_precondition_and_before() {
1181 let config: AlefConfig = toml::from_str(
1182 r#"
1183languages = ["go"]
1184
1185[crate]
1186name = "test"
1187sources = ["src/lib.rs"]
1188
1189[lint.go]
1190precondition = "test -f target/release/libtest_ffi.so"
1191before = "cargo build --release -p test-ffi"
1192format = "gofmt -w packages/go"
1193check = "golangci-lint run ./..."
1194"#,
1195 )
1196 .unwrap();
1197
1198 let lint = config.lint_config_for_language(Language::Go);
1199 assert_eq!(
1200 lint.precondition.as_deref(),
1201 Some("test -f target/release/libtest_ffi.so"),
1202 "precondition should be preserved from explicit config"
1203 );
1204 assert_eq!(
1205 lint.before.unwrap().commands(),
1206 vec!["cargo build --release -p test-ffi"],
1207 "before should be preserved from explicit config"
1208 );
1209 }
1210
1211 #[test]
1212 fn explicit_lint_config_with_before_list_preserves_all_commands() {
1213 let config: AlefConfig = toml::from_str(
1214 r#"
1215languages = ["go"]
1216
1217[crate]
1218name = "test"
1219sources = ["src/lib.rs"]
1220
1221[lint.go]
1222before = ["cargo build --release -p test-ffi", "cp target/release/libtest_ffi.so packages/go/"]
1223check = "golangci-lint run ./..."
1224"#,
1225 )
1226 .unwrap();
1227
1228 let lint = config.lint_config_for_language(Language::Go);
1229 assert!(lint.precondition.is_none(), "precondition should be None when not set");
1230 assert_eq!(
1231 lint.before.unwrap().commands(),
1232 vec![
1233 "cargo build --release -p test-ffi",
1234 "cp target/release/libtest_ffi.so packages/go/"
1235 ],
1236 "before list should be preserved from explicit config"
1237 );
1238 }
1239
1240 #[test]
1241 fn default_lint_config_has_no_precondition_or_before() {
1242 let config = minimal_config();
1243 let py = config.lint_config_for_language(Language::Python);
1244 assert!(
1245 py.precondition.is_none(),
1246 "default lint config should have no precondition"
1247 );
1248 assert!(py.before.is_none(), "default lint config should have no before");
1249
1250 let go = config.lint_config_for_language(Language::Go);
1251 assert!(
1252 go.precondition.is_none(),
1253 "default Go lint config should have no precondition"
1254 );
1255 assert!(go.before.is_none(), "default Go lint config should have no before");
1256 }
1257
1258 #[test]
1259 fn explicit_test_config_preserves_precondition_and_before() {
1260 let config: AlefConfig = toml::from_str(
1261 r#"
1262languages = ["python"]
1263
1264[crate]
1265name = "test"
1266sources = ["src/lib.rs"]
1267
1268[test.python]
1269precondition = "test -f target/release/libtest.so"
1270before = "maturin develop"
1271command = "pytest"
1272"#,
1273 )
1274 .unwrap();
1275
1276 let test = config.test_config_for_language(Language::Python);
1277 assert_eq!(
1278 test.precondition.as_deref(),
1279 Some("test -f target/release/libtest.so"),
1280 "test precondition should be preserved"
1281 );
1282 assert_eq!(
1283 test.before.unwrap().commands(),
1284 vec!["maturin develop"],
1285 "test before should be preserved"
1286 );
1287 }
1288
1289 #[test]
1290 fn default_test_config_has_no_precondition_or_before() {
1291 let config = minimal_config();
1292 let py = config.test_config_for_language(Language::Python);
1293 assert!(
1294 py.precondition.is_none(),
1295 "default test config should have no precondition"
1296 );
1297 assert!(py.before.is_none(), "default test config should have no before");
1298 }
1299
1300 #[test]
1301 fn explicit_setup_config_preserves_precondition_and_before() {
1302 let config: AlefConfig = toml::from_str(
1303 r#"
1304languages = ["python"]
1305
1306[crate]
1307name = "test"
1308sources = ["src/lib.rs"]
1309
1310[setup.python]
1311precondition = "which uv"
1312before = "pip install uv"
1313install = "uv sync"
1314"#,
1315 )
1316 .unwrap();
1317
1318 let setup = config.setup_config_for_language(Language::Python);
1319 assert_eq!(
1320 setup.precondition.as_deref(),
1321 Some("which uv"),
1322 "setup precondition should be preserved"
1323 );
1324 assert_eq!(
1325 setup.before.unwrap().commands(),
1326 vec!["pip install uv"],
1327 "setup before should be preserved"
1328 );
1329 }
1330
1331 #[test]
1332 fn default_setup_config_has_no_precondition_or_before() {
1333 let config = minimal_config();
1334 let py = config.setup_config_for_language(Language::Python);
1335 assert!(
1336 py.precondition.is_none(),
1337 "default setup config should have no precondition"
1338 );
1339 assert!(py.before.is_none(), "default setup config should have no before");
1340 }
1341
1342 #[test]
1343 fn explicit_update_config_preserves_precondition_and_before() {
1344 let config: AlefConfig = toml::from_str(
1345 r#"
1346languages = ["rust"]
1347
1348[crate]
1349name = "test"
1350sources = ["src/lib.rs"]
1351
1352[update.rust]
1353precondition = "test -f Cargo.lock"
1354before = "cargo fetch"
1355update = "cargo update"
1356"#,
1357 )
1358 .unwrap();
1359
1360 let update = config.update_config_for_language(Language::Rust);
1361 assert_eq!(
1362 update.precondition.as_deref(),
1363 Some("test -f Cargo.lock"),
1364 "update precondition should be preserved"
1365 );
1366 assert_eq!(
1367 update.before.unwrap().commands(),
1368 vec!["cargo fetch"],
1369 "update before should be preserved"
1370 );
1371 }
1372
1373 #[test]
1374 fn default_update_config_has_no_precondition_or_before() {
1375 let config = minimal_config();
1376 let rust = config.update_config_for_language(Language::Rust);
1377 assert!(
1378 rust.precondition.is_none(),
1379 "default update config should have no precondition"
1380 );
1381 assert!(rust.before.is_none(), "default update config should have no before");
1382 }
1383
1384 #[test]
1385 fn explicit_clean_config_preserves_precondition_and_before() {
1386 let config: AlefConfig = toml::from_str(
1387 r#"
1388languages = ["rust"]
1389
1390[crate]
1391name = "test"
1392sources = ["src/lib.rs"]
1393
1394[clean.rust]
1395precondition = "test -d target"
1396before = "echo cleaning"
1397clean = "cargo clean"
1398"#,
1399 )
1400 .unwrap();
1401
1402 let clean = config.clean_config_for_language(Language::Rust);
1403 assert_eq!(
1404 clean.precondition.as_deref(),
1405 Some("test -d target"),
1406 "clean precondition should be preserved"
1407 );
1408 assert_eq!(
1409 clean.before.unwrap().commands(),
1410 vec!["echo cleaning"],
1411 "clean before should be preserved"
1412 );
1413 }
1414
1415 #[test]
1416 fn default_clean_config_has_no_precondition_or_before() {
1417 let config = minimal_config();
1418 let rust = config.clean_config_for_language(Language::Rust);
1419 assert!(
1420 rust.precondition.is_none(),
1421 "default clean config should have no precondition"
1422 );
1423 assert!(rust.before.is_none(), "default clean config should have no before");
1424 }
1425
1426 #[test]
1427 fn explicit_build_command_config_preserves_precondition_and_before() {
1428 let config: AlefConfig = toml::from_str(
1429 r#"
1430languages = ["go"]
1431
1432[crate]
1433name = "test"
1434sources = ["src/lib.rs"]
1435
1436[build_commands.go]
1437precondition = "which go"
1438before = "cargo build --release -p test-ffi"
1439build = "cd packages/go && go build ./..."
1440build_release = "cd packages/go && go build -ldflags='-s -w' ./..."
1441"#,
1442 )
1443 .unwrap();
1444
1445 let build = config.build_command_config_for_language(Language::Go);
1446 assert_eq!(
1447 build.precondition.as_deref(),
1448 Some("which go"),
1449 "build precondition should be preserved"
1450 );
1451 assert_eq!(
1452 build.before.unwrap().commands(),
1453 vec!["cargo build --release -p test-ffi"],
1454 "build before should be preserved"
1455 );
1456 }
1457
1458 #[test]
1459 fn default_build_command_config_has_no_precondition_or_before() {
1460 let config = minimal_config();
1461 let rust = config.build_command_config_for_language(Language::Rust);
1462 assert!(
1463 rust.precondition.is_none(),
1464 "default build command config should have no precondition"
1465 );
1466 assert!(
1467 rust.before.is_none(),
1468 "default build command config should have no before"
1469 );
1470 }
1471}