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 tools;
17pub mod trait_bridge;
18pub mod update_defaults;
19pub mod validation;
20
21pub use dto::{
23 CsharpDtoStyle, DtoConfig, ElixirDtoStyle, GoDtoStyle, JavaDtoStyle, NodeDtoStyle, PhpDtoStyle, PythonDtoStyle,
24 RDtoStyle, RubyDtoStyle,
25};
26pub use e2e::E2eConfig;
27pub use extras::{AdapterConfig, AdapterParam, AdapterPattern, Language};
28pub use languages::{
29 CSharpConfig, CustomModulesConfig, CustomRegistration, CustomRegistrationsConfig, ElixirConfig, FfiConfig,
30 GoConfig, JavaConfig, NodeConfig, PhpConfig, PythonConfig, RConfig, RubyConfig, StubsConfig, WasmConfig,
31};
32pub use output::{
33 BuildCommandConfig, CleanConfig, ExcludeConfig, IncludeConfig, LintConfig, OutputConfig, ReadmeConfig,
34 ScaffoldConfig, SetupConfig, SyncConfig, TestConfig, TextReplacement, UpdateConfig,
35};
36pub use publish::{PublishConfig, PublishLanguageConfig, VendorMode};
37pub use tools::{DEFAULT_RUST_DEV_TOOLS, LangContext, ToolsConfig, require_tool, require_tools};
38pub use trait_bridge::TraitBridgeConfig;
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct AlefConfig {
43 #[serde(default)]
46 pub version: Option<String>,
47 #[serde(rename = "crate")]
48 pub crate_config: CrateConfig,
49 pub languages: Vec<Language>,
50 #[serde(default)]
51 pub exclude: ExcludeConfig,
52 #[serde(default)]
53 pub include: IncludeConfig,
54 #[serde(default)]
55 pub output: OutputConfig,
56 #[serde(default)]
57 pub python: Option<PythonConfig>,
58 #[serde(default)]
59 pub node: Option<NodeConfig>,
60 #[serde(default)]
61 pub ruby: Option<RubyConfig>,
62 #[serde(default)]
63 pub php: Option<PhpConfig>,
64 #[serde(default)]
65 pub elixir: Option<ElixirConfig>,
66 #[serde(default)]
67 pub wasm: Option<WasmConfig>,
68 #[serde(default)]
69 pub ffi: Option<FfiConfig>,
70 #[serde(default)]
71 pub go: Option<GoConfig>,
72 #[serde(default)]
73 pub java: Option<JavaConfig>,
74 #[serde(default)]
75 pub csharp: Option<CSharpConfig>,
76 #[serde(default)]
77 pub r: Option<RConfig>,
78 #[serde(default)]
79 pub scaffold: Option<ScaffoldConfig>,
80 #[serde(default)]
81 pub readme: Option<ReadmeConfig>,
82 #[serde(default)]
83 pub lint: Option<HashMap<String, LintConfig>>,
84 #[serde(default)]
85 pub update: Option<HashMap<String, UpdateConfig>>,
86 #[serde(default)]
87 pub test: Option<HashMap<String, TestConfig>>,
88 #[serde(default)]
89 pub setup: Option<HashMap<String, SetupConfig>>,
90 #[serde(default)]
91 pub clean: Option<HashMap<String, CleanConfig>>,
92 #[serde(default)]
93 pub build_commands: Option<HashMap<String, BuildCommandConfig>>,
94 #[serde(default)]
96 pub publish: Option<PublishConfig>,
97 #[serde(default)]
98 pub custom_files: Option<HashMap<String, Vec<PathBuf>>>,
99 #[serde(default)]
100 pub adapters: Vec<AdapterConfig>,
101 #[serde(default)]
102 pub custom_modules: CustomModulesConfig,
103 #[serde(default)]
104 pub custom_registrations: CustomRegistrationsConfig,
105 #[serde(default)]
106 pub sync: Option<SyncConfig>,
107 #[serde(default)]
111 pub opaque_types: HashMap<String, String>,
112 #[serde(default)]
114 pub generate: GenerateConfig,
115 #[serde(default)]
117 pub generate_overrides: HashMap<String, GenerateConfig>,
118 #[serde(default)]
120 pub dto: DtoConfig,
121 #[serde(default)]
123 pub e2e: Option<E2eConfig>,
124 #[serde(default)]
127 pub trait_bridges: Vec<TraitBridgeConfig>,
128 #[serde(default)]
132 pub tools: ToolsConfig,
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 validate(&self) -> Result<(), crate::error::AlefError> {
376 validation::validate(self)
377 }
378
379 pub fn lint_config_for_language(&self, lang: extras::Language) -> output::LintConfig {
384 if let Some(lint_map) = &self.lint {
385 let lang_str = lang.to_string();
386 if let Some(explicit) = lint_map.get(&lang_str) {
387 return explicit.clone();
388 }
389 }
390 let output_dir = self.package_dir(lang);
391 let run_wrapper = self.run_wrapper_for_language(lang);
392 let extra_lint_paths = self.extra_lint_paths_for_language(lang);
393 let project_file = self.project_file_for_language(lang);
394 let ctx = LangContext {
395 tools: &self.tools,
396 run_wrapper,
397 extra_lint_paths,
398 project_file,
399 };
400 lint_defaults::default_lint_config(lang, &output_dir, &ctx)
401 }
402
403 pub fn update_config_for_language(&self, lang: extras::Language) -> output::UpdateConfig {
408 if let Some(update_map) = &self.update {
409 let lang_str = lang.to_string();
410 if let Some(explicit) = update_map.get(&lang_str) {
411 return explicit.clone();
412 }
413 }
414 let output_dir = self.package_dir(lang);
415 let ctx = LangContext {
416 tools: &self.tools,
417 run_wrapper: None,
418 extra_lint_paths: &[],
419 project_file: None,
420 };
421 update_defaults::default_update_config(lang, &output_dir, &ctx)
422 }
423
424 pub fn test_config_for_language(&self, lang: extras::Language) -> output::TestConfig {
429 if let Some(test_map) = &self.test {
430 let lang_str = lang.to_string();
431 if let Some(explicit) = test_map.get(&lang_str) {
432 return explicit.clone();
433 }
434 }
435 let output_dir = self.package_dir(lang);
436 let run_wrapper = self.run_wrapper_for_language(lang);
437 let project_file = self.project_file_for_language(lang);
438 let ctx = LangContext {
439 tools: &self.tools,
440 run_wrapper,
441 extra_lint_paths: &[],
442 project_file,
443 };
444 test_defaults::default_test_config(lang, &output_dir, &ctx)
445 }
446
447 pub fn setup_config_for_language(&self, lang: extras::Language) -> output::SetupConfig {
452 if let Some(setup_map) = &self.setup {
453 let lang_str = lang.to_string();
454 if let Some(explicit) = setup_map.get(&lang_str) {
455 return explicit.clone();
456 }
457 }
458 let output_dir = self.package_dir(lang);
459 let ctx = LangContext {
460 tools: &self.tools,
461 run_wrapper: None,
462 extra_lint_paths: &[],
463 project_file: None,
464 };
465 setup_defaults::default_setup_config(lang, &output_dir, &ctx)
466 }
467
468 pub fn clean_config_for_language(&self, lang: extras::Language) -> output::CleanConfig {
473 if let Some(clean_map) = &self.clean {
474 let lang_str = lang.to_string();
475 if let Some(explicit) = clean_map.get(&lang_str) {
476 return explicit.clone();
477 }
478 }
479 let output_dir = self.package_dir(lang);
480 let ctx = LangContext {
481 tools: &self.tools,
482 run_wrapper: None,
483 extra_lint_paths: &[],
484 project_file: None,
485 };
486 clean_defaults::default_clean_config(lang, &output_dir, &ctx)
487 }
488
489 pub fn build_command_config_for_language(&self, lang: extras::Language) -> output::BuildCommandConfig {
494 if let Some(build_map) = &self.build_commands {
495 let lang_str = lang.to_string();
496 if let Some(explicit) = build_map.get(&lang_str) {
497 return explicit.clone();
498 }
499 }
500 let output_dir = self.package_dir(lang);
501 let crate_name = &self.crate_config.name;
502 let run_wrapper = self.run_wrapper_for_language(lang);
503 let project_file = self.project_file_for_language(lang);
504 let ctx = LangContext {
505 tools: &self.tools,
506 run_wrapper,
507 extra_lint_paths: &[],
508 project_file,
509 };
510 build_defaults::default_build_config(lang, &output_dir, crate_name, &ctx)
511 }
512
513 pub fn core_import(&self) -> String {
515 self.crate_config
516 .core_import
517 .clone()
518 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
519 }
520
521 pub fn error_type(&self) -> String {
523 self.crate_config
524 .error_type
525 .clone()
526 .unwrap_or_else(|| "Error".to_string())
527 }
528
529 pub fn error_constructor(&self) -> String {
532 self.crate_config
533 .error_constructor
534 .clone()
535 .unwrap_or_else(|| format!("{}::{}::from({{msg}})", self.core_import(), self.error_type()))
536 }
537
538 pub fn run_wrapper_for_language(&self, lang: extras::Language) -> Option<&str> {
541 match lang {
542 extras::Language::Python => self.python.as_ref().and_then(|c| c.run_wrapper.as_deref()),
543 extras::Language::Node => self.node.as_ref().and_then(|c| c.run_wrapper.as_deref()),
544 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.run_wrapper.as_deref()),
545 extras::Language::Php => self.php.as_ref().and_then(|c| c.run_wrapper.as_deref()),
546 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.run_wrapper.as_deref()),
547 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.run_wrapper.as_deref()),
548 extras::Language::Go => self.go.as_ref().and_then(|c| c.run_wrapper.as_deref()),
549 extras::Language::Java => self.java.as_ref().and_then(|c| c.run_wrapper.as_deref()),
550 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.run_wrapper.as_deref()),
551 extras::Language::R => self.r.as_ref().and_then(|c| c.run_wrapper.as_deref()),
552 _ => None,
553 }
554 }
555
556 pub fn extra_lint_paths_for_language(&self, lang: extras::Language) -> &[String] {
559 match lang {
560 extras::Language::Python => self
561 .python
562 .as_ref()
563 .map(|c| c.extra_lint_paths.as_slice())
564 .unwrap_or(&[]),
565 extras::Language::Node => self.node.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
566 extras::Language::Ruby => self.ruby.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
567 extras::Language::Php => self.php.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
568 extras::Language::Elixir => self
569 .elixir
570 .as_ref()
571 .map(|c| c.extra_lint_paths.as_slice())
572 .unwrap_or(&[]),
573 extras::Language::Wasm => self.wasm.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
574 extras::Language::Go => self.go.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
575 extras::Language::Java => self.java.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
576 extras::Language::Csharp => self
577 .csharp
578 .as_ref()
579 .map(|c| c.extra_lint_paths.as_slice())
580 .unwrap_or(&[]),
581 extras::Language::R => self.r.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
582 _ => &[],
583 }
584 }
585
586 pub fn project_file_for_language(&self, lang: extras::Language) -> Option<&str> {
589 match lang {
590 extras::Language::Java => self.java.as_ref().and_then(|c| c.project_file.as_deref()),
591 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.project_file.as_deref()),
592 _ => None,
593 }
594 }
595
596 pub fn ffi_prefix(&self) -> String {
598 self.ffi
599 .as_ref()
600 .and_then(|f| f.prefix.as_ref())
601 .cloned()
602 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
603 }
604
605 pub fn ffi_lib_name(&self) -> String {
613 if let Some(name) = self.ffi.as_ref().and_then(|f| f.lib_name.as_ref()) {
615 return name.clone();
616 }
617
618 if let Some(ffi_path) = self.output.ffi.as_ref() {
621 let path = std::path::Path::new(ffi_path);
622 let components: Vec<_> = path
625 .components()
626 .filter_map(|c| {
627 if let std::path::Component::Normal(s) = c {
628 s.to_str()
629 } else {
630 None
631 }
632 })
633 .collect();
634 let crate_dir = components
637 .iter()
638 .rev()
639 .find(|&&s| s != "src" && s != "lib" && s != "include")
640 .copied();
641 if let Some(dir) = crate_dir {
642 return dir.replace('-', "_");
643 }
644 }
645
646 format!("{}_ffi", self.ffi_prefix())
648 }
649
650 pub fn ffi_header_name(&self) -> String {
652 self.ffi
653 .as_ref()
654 .and_then(|f| f.header_name.as_ref())
655 .cloned()
656 .unwrap_or_else(|| format!("{}.h", self.ffi_prefix()))
657 }
658
659 pub fn python_module_name(&self) -> String {
661 self.python
662 .as_ref()
663 .and_then(|p| p.module_name.as_ref())
664 .cloned()
665 .unwrap_or_else(|| format!("_{}", self.crate_config.name.replace('-', "_")))
666 }
667
668 pub fn python_pip_name(&self) -> String {
672 self.python
673 .as_ref()
674 .and_then(|p| p.pip_name.as_ref())
675 .cloned()
676 .unwrap_or_else(|| self.crate_config.name.clone())
677 }
678
679 pub fn php_autoload_namespace(&self) -> String {
684 use heck::ToPascalCase;
685 let ext = self.php_extension_name();
686 if ext.contains('_') {
687 ext.split('_')
688 .map(|p| p.to_pascal_case())
689 .collect::<Vec<_>>()
690 .join("\\")
691 } else {
692 ext.to_pascal_case()
693 }
694 }
695
696 pub fn node_package_name(&self) -> String {
698 self.node
699 .as_ref()
700 .and_then(|n| n.package_name.as_ref())
701 .cloned()
702 .unwrap_or_else(|| self.crate_config.name.clone())
703 }
704
705 pub fn ruby_gem_name(&self) -> String {
707 self.ruby
708 .as_ref()
709 .and_then(|r| r.gem_name.as_ref())
710 .cloned()
711 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
712 }
713
714 pub fn php_extension_name(&self) -> String {
716 self.php
717 .as_ref()
718 .and_then(|p| p.extension_name.as_ref())
719 .cloned()
720 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
721 }
722
723 pub fn elixir_app_name(&self) -> String {
725 self.elixir
726 .as_ref()
727 .and_then(|e| e.app_name.as_ref())
728 .cloned()
729 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
730 }
731
732 pub fn go_module(&self) -> String {
734 self.go
735 .as_ref()
736 .and_then(|g| g.module.as_ref())
737 .cloned()
738 .unwrap_or_else(|| format!("github.com/kreuzberg-dev/{}", self.crate_config.name))
739 }
740
741 pub fn github_repo(&self) -> String {
748 if let Some(e2e) = &self.e2e {
749 if let Some(url) = &e2e.registry.github_repo {
750 return url.clone();
751 }
752 }
753 self.scaffold
754 .as_ref()
755 .and_then(|s| s.repository.as_ref())
756 .cloned()
757 .unwrap_or_else(|| format!("https://github.com/kreuzberg-dev/{}", self.crate_config.name))
758 }
759
760 pub fn java_package(&self) -> String {
762 self.java
763 .as_ref()
764 .and_then(|j| j.package.as_ref())
765 .cloned()
766 .unwrap_or_else(|| "dev.kreuzberg".to_string())
767 }
768
769 pub fn java_group_id(&self) -> String {
774 self.java_package()
775 }
776
777 pub fn csharp_namespace(&self) -> String {
779 self.csharp
780 .as_ref()
781 .and_then(|c| c.namespace.as_ref())
782 .cloned()
783 .unwrap_or_else(|| {
784 use heck::ToPascalCase;
785 self.crate_config.name.to_pascal_case()
786 })
787 }
788
789 pub fn core_crate_dir(&self) -> String {
795 if let Some(first_source) = self.crate_config.sources.first() {
798 let path = std::path::Path::new(first_source);
799 let mut current = path.parent();
800 while let Some(dir) = current {
801 if dir.file_name().is_some_and(|n| n == "src") {
802 if let Some(crate_dir) = dir.parent() {
803 if let Some(dir_name) = crate_dir.file_name() {
804 return dir_name.to_string_lossy().into_owned();
805 }
806 }
807 break;
808 }
809 current = dir.parent();
810 }
811 }
812 self.crate_config.name.clone()
813 }
814
815 pub fn wasm_type_prefix(&self) -> String {
818 self.wasm
819 .as_ref()
820 .and_then(|w| w.type_prefix.as_ref())
821 .cloned()
822 .unwrap_or_else(|| "Wasm".to_string())
823 }
824
825 pub fn node_type_prefix(&self) -> String {
828 self.node
829 .as_ref()
830 .and_then(|n| n.type_prefix.as_ref())
831 .cloned()
832 .unwrap_or_else(|| "Js".to_string())
833 }
834
835 pub fn r_package_name(&self) -> String {
837 self.r
838 .as_ref()
839 .and_then(|r| r.package_name.as_ref())
840 .cloned()
841 .unwrap_or_else(|| self.crate_config.name.clone())
842 }
843
844 pub fn resolved_version(&self) -> Option<String> {
847 let content = std::fs::read_to_string(&self.crate_config.version_from).ok()?;
848 let value: toml::Value = toml::from_str(&content).ok()?;
849 if let Some(v) = value
850 .get("workspace")
851 .and_then(|w| w.get("package"))
852 .and_then(|p| p.get("version"))
853 .and_then(|v| v.as_str())
854 {
855 return Some(v.to_string());
856 }
857 value
858 .get("package")
859 .and_then(|p| p.get("version"))
860 .and_then(|v| v.as_str())
861 .map(|v| v.to_string())
862 }
863
864 pub fn serde_rename_all_for_language(&self, lang: extras::Language) -> String {
872 let override_val = match lang {
874 extras::Language::Python => self.python.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
875 extras::Language::Node => self.node.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
876 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
877 extras::Language::Php => self.php.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
878 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
879 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
880 extras::Language::Ffi => self.ffi.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
881 extras::Language::Go => self.go.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
882 extras::Language::Java => self.java.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
883 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
884 extras::Language::R => self.r.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
885 extras::Language::Rust => None, };
887
888 if let Some(val) = override_val {
889 return val.to_string();
890 }
891
892 match lang {
894 extras::Language::Node | extras::Language::Wasm | extras::Language::Java | extras::Language::Csharp => {
895 "camelCase".to_string()
896 }
897 extras::Language::Python
898 | extras::Language::Ruby
899 | extras::Language::Php
900 | extras::Language::Go
901 | extras::Language::Ffi
902 | extras::Language::Elixir
903 | extras::Language::R
904 | extras::Language::Rust => "snake_case".to_string(),
905 }
906 }
907
908 pub fn rewrite_path(&self, rust_path: &str) -> String {
911 let mut mappings: Vec<_> = self.crate_config.path_mappings.iter().collect();
913 mappings.sort_by_key(|b| std::cmp::Reverse(b.0.len()));
914
915 for (from, to) in &mappings {
916 if rust_path.starts_with(from.as_str()) {
917 return format!("{}{}", to, &rust_path[from.len()..]);
918 }
919 }
920 rust_path.to_string()
921 }
922
923 pub fn effective_path_mappings(&self) -> HashMap<String, String> {
933 let mut mappings = HashMap::new();
934
935 if self.crate_config.auto_path_mappings {
936 let core_import = self.core_import();
937
938 for source in &self.crate_config.sources {
939 let source_str = source.to_string_lossy();
940 if let Some(after_crates) = find_after_crates_prefix(&source_str) {
942 if let Some(slash_pos) = after_crates.find('/') {
944 let crate_dir = &after_crates[..slash_pos];
945 let crate_ident = crate_dir.replace('-', "_");
946 if crate_ident != core_import && !mappings.contains_key(&crate_ident) {
948 mappings.insert(crate_ident, core_import.clone());
949 }
950 }
951 }
952 }
953 }
954
955 for (from, to) in &self.crate_config.path_mappings {
957 mappings.insert(from.clone(), to.clone());
958 }
959
960 mappings
961 }
962}
963
964fn find_after_crates_prefix(path: &str) -> Option<&str> {
971 if let Some(pos) = path.find("/crates/") {
975 return Some(&path[pos + "/crates/".len()..]);
976 }
977 if let Some(stripped) = path.strip_prefix("crates/") {
978 return Some(stripped);
979 }
980 None
981}
982
983pub fn resolve_output_dir(config_path: Option<&PathBuf>, crate_name: &str, default: &str) -> String {
986 config_path
987 .map(|p| p.to_string_lossy().replace("{name}", crate_name))
988 .unwrap_or_else(|| default.replace("{name}", crate_name))
989}
990
991pub fn detect_serde_available(output_dir: &str) -> bool {
997 let src_path = std::path::Path::new(output_dir);
998 let mut dir = src_path;
1000 loop {
1001 let cargo_toml = dir.join("Cargo.toml");
1002 if cargo_toml.exists() {
1003 return cargo_toml_has_serde(&cargo_toml);
1004 }
1005 match dir.parent() {
1006 Some(parent) if !parent.as_os_str().is_empty() => dir = parent,
1007 _ => break,
1008 }
1009 }
1010 false
1011}
1012
1013fn cargo_toml_has_serde(path: &std::path::Path) -> bool {
1019 let content = match std::fs::read_to_string(path) {
1020 Ok(c) => c,
1021 Err(_) => return false,
1022 };
1023
1024 let has_serde_json = content.contains("serde_json");
1025 let has_serde_dep = content.lines().any(|line| {
1029 let trimmed = line.trim();
1030 trimmed.starts_with("serde ")
1032 || trimmed.starts_with("serde=")
1033 || trimmed.starts_with("serde.")
1034 || trimmed == "[dependencies.serde]"
1035 });
1036
1037 has_serde_json && has_serde_dep
1038}
1039
1040#[cfg(test)]
1041mod tests {
1042 use super::*;
1043
1044 fn minimal_config() -> AlefConfig {
1045 toml::from_str(
1046 r#"
1047languages = ["python", "node", "rust"]
1048
1049[crate]
1050name = "test-lib"
1051sources = ["src/lib.rs"]
1052"#,
1053 )
1054 .unwrap()
1055 }
1056
1057 #[test]
1058 fn lint_config_falls_back_to_defaults() {
1059 let config = minimal_config();
1060 assert!(config.lint.is_none());
1061
1062 let py = config.lint_config_for_language(Language::Python);
1063 assert!(py.format.is_some());
1064 assert!(py.check.is_some());
1065 assert!(py.typecheck.is_some());
1066
1067 let node = config.lint_config_for_language(Language::Node);
1068 assert!(node.format.is_some());
1069 assert!(node.check.is_some());
1070 }
1071
1072 #[test]
1073 fn lint_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[lint.python]
1083format = "custom-formatter"
1084check = "custom-checker"
1085"#,
1086 )
1087 .unwrap();
1088
1089 let py = config.lint_config_for_language(Language::Python);
1090 assert_eq!(py.format.unwrap().commands(), vec!["custom-formatter"]);
1091 assert_eq!(py.check.unwrap().commands(), vec!["custom-checker"]);
1092 assert!(py.typecheck.is_none()); }
1094
1095 #[test]
1096 fn lint_config_partial_override_does_not_merge() {
1097 let config: AlefConfig = toml::from_str(
1098 r#"
1099languages = ["python"]
1100
1101[crate]
1102name = "test-lib"
1103sources = ["src/lib.rs"]
1104
1105[lint.python]
1106format = "only-format"
1107"#,
1108 )
1109 .unwrap();
1110
1111 let py = config.lint_config_for_language(Language::Python);
1112 assert_eq!(py.format.unwrap().commands(), vec!["only-format"]);
1113 assert!(py.check.is_none());
1115 assert!(py.typecheck.is_none());
1116 }
1117
1118 #[test]
1119 fn lint_config_unconfigured_language_uses_defaults() {
1120 let config: AlefConfig = toml::from_str(
1121 r#"
1122languages = ["python", "node"]
1123
1124[crate]
1125name = "test-lib"
1126sources = ["src/lib.rs"]
1127
1128[lint.python]
1129format = "custom"
1130"#,
1131 )
1132 .unwrap();
1133
1134 let py = config.lint_config_for_language(Language::Python);
1136 assert_eq!(py.format.unwrap().commands(), vec!["custom"]);
1137
1138 let node = config.lint_config_for_language(Language::Node);
1140 let fmt = node.format.unwrap().commands().join(" ");
1141 assert!(fmt.contains("oxfmt"));
1142 }
1143
1144 #[test]
1145 fn update_config_falls_back_to_defaults() {
1146 let config = minimal_config();
1147 assert!(config.update.is_none());
1148
1149 let py = config.update_config_for_language(Language::Python);
1150 assert!(py.update.is_some());
1151 assert!(py.upgrade.is_some());
1152
1153 let rust = config.update_config_for_language(Language::Rust);
1154 let update = rust.update.unwrap().commands().join(" ");
1155 assert!(update.contains("cargo update"));
1156 }
1157
1158 #[test]
1159 fn update_config_explicit_overrides_default() {
1160 let config: AlefConfig = toml::from_str(
1161 r#"
1162languages = ["rust"]
1163
1164[crate]
1165name = "test-lib"
1166sources = ["src/lib.rs"]
1167
1168[update.rust]
1169update = "my-custom-update"
1170upgrade = ["step1", "step2"]
1171"#,
1172 )
1173 .unwrap();
1174
1175 let rust = config.update_config_for_language(Language::Rust);
1176 assert_eq!(rust.update.unwrap().commands(), vec!["my-custom-update"]);
1177 assert_eq!(rust.upgrade.unwrap().commands(), vec!["step1", "step2"]);
1178 }
1179
1180 #[test]
1181 fn test_config_falls_back_to_defaults() {
1182 let config = minimal_config();
1183 assert!(config.test.is_none());
1184
1185 let py = config.test_config_for_language(Language::Python);
1186 assert!(py.command.is_some());
1187 assert!(py.coverage.is_some());
1188 assert!(py.e2e.is_none());
1189
1190 let rust = config.test_config_for_language(Language::Rust);
1191 let cmd = rust.command.unwrap().commands().join(" ");
1192 assert!(cmd.contains("cargo test"));
1193 }
1194
1195 #[test]
1196 fn test_config_explicit_overrides_default() {
1197 let config: AlefConfig = toml::from_str(
1198 r#"
1199languages = ["python"]
1200
1201[crate]
1202name = "test-lib"
1203sources = ["src/lib.rs"]
1204
1205[test.python]
1206command = "my-custom-test"
1207"#,
1208 )
1209 .unwrap();
1210
1211 let py = config.test_config_for_language(Language::Python);
1212 assert_eq!(py.command.unwrap().commands(), vec!["my-custom-test"]);
1213 assert!(py.coverage.is_none()); }
1215
1216 #[test]
1217 fn setup_config_falls_back_to_defaults() {
1218 let config = minimal_config();
1219 assert!(config.setup.is_none());
1220
1221 let py = config.setup_config_for_language(Language::Python);
1222 assert!(py.install.is_some());
1223 let install = py.install.unwrap().commands().join(" ");
1224 assert!(install.contains("uv sync"));
1225
1226 let rust = config.setup_config_for_language(Language::Rust);
1227 let install = rust.install.unwrap().commands().join(" ");
1228 assert!(install.contains("rustup update"));
1229 }
1230
1231 #[test]
1232 fn setup_config_explicit_overrides_default() {
1233 let config: AlefConfig = toml::from_str(
1234 r#"
1235languages = ["python"]
1236
1237[crate]
1238name = "test-lib"
1239sources = ["src/lib.rs"]
1240
1241[setup.python]
1242install = "my-custom-install"
1243"#,
1244 )
1245 .unwrap();
1246
1247 let py = config.setup_config_for_language(Language::Python);
1248 assert_eq!(py.install.unwrap().commands(), vec!["my-custom-install"]);
1249 }
1250
1251 #[test]
1252 fn clean_config_falls_back_to_defaults() {
1253 let config = minimal_config();
1254 assert!(config.clean.is_none());
1255
1256 let py = config.clean_config_for_language(Language::Python);
1257 assert!(py.clean.is_some());
1258 let clean = py.clean.unwrap().commands().join(" ");
1259 assert!(clean.contains("__pycache__"));
1260
1261 let rust = config.clean_config_for_language(Language::Rust);
1262 let clean = rust.clean.unwrap().commands().join(" ");
1263 assert!(clean.contains("cargo clean"));
1264 }
1265
1266 #[test]
1267 fn clean_config_explicit_overrides_default() {
1268 let config: AlefConfig = toml::from_str(
1269 r#"
1270languages = ["rust"]
1271
1272[crate]
1273name = "test-lib"
1274sources = ["src/lib.rs"]
1275
1276[clean.rust]
1277clean = "my-custom-clean"
1278"#,
1279 )
1280 .unwrap();
1281
1282 let rust = config.clean_config_for_language(Language::Rust);
1283 assert_eq!(rust.clean.unwrap().commands(), vec!["my-custom-clean"]);
1284 }
1285
1286 #[test]
1287 fn build_command_config_falls_back_to_defaults() {
1288 let config = minimal_config();
1289 assert!(config.build_commands.is_none());
1290
1291 let py = config.build_command_config_for_language(Language::Python);
1292 assert!(py.build.is_some());
1293 assert!(py.build_release.is_some());
1294 let build = py.build.unwrap().commands().join(" ");
1295 assert!(build.contains("maturin develop"));
1296
1297 let rust = config.build_command_config_for_language(Language::Rust);
1298 let build = rust.build.unwrap().commands().join(" ");
1299 assert!(build.contains("cargo build --workspace"));
1300 }
1301
1302 #[test]
1303 fn build_command_config_explicit_overrides_default() {
1304 let config: AlefConfig = toml::from_str(
1305 r#"
1306languages = ["rust"]
1307
1308[crate]
1309name = "test-lib"
1310sources = ["src/lib.rs"]
1311
1312[build_commands.rust]
1313build = "my-custom-build"
1314build_release = "my-custom-build --release"
1315"#,
1316 )
1317 .unwrap();
1318
1319 let rust = config.build_command_config_for_language(Language::Rust);
1320 assert_eq!(rust.build.unwrap().commands(), vec!["my-custom-build"]);
1321 assert_eq!(
1322 rust.build_release.unwrap().commands(),
1323 vec!["my-custom-build --release"]
1324 );
1325 }
1326
1327 #[test]
1328 fn build_command_config_uses_crate_name() {
1329 let config = minimal_config();
1330 let py = config.build_command_config_for_language(Language::Python);
1331 let build = py.build.unwrap().commands().join(" ");
1332 assert!(
1333 build.contains("test-lib-py"),
1334 "Python build should reference crate name, got: {build}"
1335 );
1336 }
1337
1338 #[test]
1339 fn package_dir_defaults_are_correct() {
1340 let config = minimal_config();
1341 assert_eq!(config.package_dir(Language::Python), "packages/python");
1342 assert_eq!(config.package_dir(Language::Node), "packages/node");
1343 assert_eq!(config.package_dir(Language::Ruby), "packages/ruby");
1344 assert_eq!(config.package_dir(Language::Go), "packages/go");
1345 assert_eq!(config.package_dir(Language::Java), "packages/java");
1346 }
1347
1348 #[test]
1349 fn explicit_lint_config_preserves_precondition_and_before() {
1350 let config: AlefConfig = toml::from_str(
1351 r#"
1352languages = ["go"]
1353
1354[crate]
1355name = "test"
1356sources = ["src/lib.rs"]
1357
1358[lint.go]
1359precondition = "test -f target/release/libtest_ffi.so"
1360before = "cargo build --release -p test-ffi"
1361format = "gofmt -w packages/go"
1362check = "golangci-lint run ./..."
1363"#,
1364 )
1365 .unwrap();
1366
1367 let lint = config.lint_config_for_language(Language::Go);
1368 assert_eq!(
1369 lint.precondition.as_deref(),
1370 Some("test -f target/release/libtest_ffi.so"),
1371 "precondition should be preserved from explicit config"
1372 );
1373 assert_eq!(
1374 lint.before.unwrap().commands(),
1375 vec!["cargo build --release -p test-ffi"],
1376 "before should be preserved from explicit config"
1377 );
1378 }
1379
1380 #[test]
1381 fn explicit_lint_config_with_before_list_preserves_all_commands() {
1382 let config: AlefConfig = toml::from_str(
1383 r#"
1384languages = ["go"]
1385
1386[crate]
1387name = "test"
1388sources = ["src/lib.rs"]
1389
1390[lint.go]
1391before = ["cargo build --release -p test-ffi", "cp target/release/libtest_ffi.so packages/go/"]
1392check = "golangci-lint run ./..."
1393"#,
1394 )
1395 .unwrap();
1396
1397 let lint = config.lint_config_for_language(Language::Go);
1398 assert!(lint.precondition.is_none(), "precondition should be None when not set");
1399 assert_eq!(
1400 lint.before.unwrap().commands(),
1401 vec![
1402 "cargo build --release -p test-ffi",
1403 "cp target/release/libtest_ffi.so packages/go/"
1404 ],
1405 "before list should be preserved from explicit config"
1406 );
1407 }
1408
1409 #[test]
1410 fn default_lint_config_has_command_v_precondition() {
1411 let config = minimal_config();
1412 let py = config.lint_config_for_language(Language::Python);
1413 assert_eq!(py.precondition.as_deref(), Some("command -v ruff >/dev/null 2>&1"));
1414 assert!(py.before.is_none(), "default lint config should have no before");
1415
1416 let go = config.lint_config_for_language(Language::Go);
1417 assert_eq!(go.precondition.as_deref(), Some("command -v gofmt >/dev/null 2>&1"));
1418 assert!(go.before.is_none(), "default Go lint config should have no before");
1419 }
1420
1421 #[test]
1422 fn explicit_test_config_preserves_precondition_and_before() {
1423 let config: AlefConfig = toml::from_str(
1424 r#"
1425languages = ["python"]
1426
1427[crate]
1428name = "test"
1429sources = ["src/lib.rs"]
1430
1431[test.python]
1432precondition = "test -f target/release/libtest.so"
1433before = "maturin develop"
1434command = "pytest"
1435"#,
1436 )
1437 .unwrap();
1438
1439 let test = config.test_config_for_language(Language::Python);
1440 assert_eq!(
1441 test.precondition.as_deref(),
1442 Some("test -f target/release/libtest.so"),
1443 "test precondition should be preserved"
1444 );
1445 assert_eq!(
1446 test.before.unwrap().commands(),
1447 vec!["maturin develop"],
1448 "test before should be preserved"
1449 );
1450 }
1451
1452 #[test]
1453 fn default_test_config_has_command_v_precondition() {
1454 let config = minimal_config();
1455 let py = config.test_config_for_language(Language::Python);
1456 assert_eq!(py.precondition.as_deref(), Some("command -v uv >/dev/null 2>&1"));
1457 assert!(py.before.is_none(), "default test config should have no before");
1458 }
1459
1460 #[test]
1461 fn explicit_setup_config_preserves_precondition_and_before() {
1462 let config: AlefConfig = toml::from_str(
1463 r#"
1464languages = ["python"]
1465
1466[crate]
1467name = "test"
1468sources = ["src/lib.rs"]
1469
1470[setup.python]
1471precondition = "which uv"
1472before = "pip install uv"
1473install = "uv sync"
1474"#,
1475 )
1476 .unwrap();
1477
1478 let setup = config.setup_config_for_language(Language::Python);
1479 assert_eq!(
1480 setup.precondition.as_deref(),
1481 Some("which uv"),
1482 "setup precondition should be preserved"
1483 );
1484 assert_eq!(
1485 setup.before.unwrap().commands(),
1486 vec!["pip install uv"],
1487 "setup before should be preserved"
1488 );
1489 }
1490
1491 #[test]
1492 fn default_setup_config_has_command_v_precondition() {
1493 let config = minimal_config();
1494 let py = config.setup_config_for_language(Language::Python);
1495 assert_eq!(py.precondition.as_deref(), Some("command -v uv >/dev/null 2>&1"));
1496 assert!(py.before.is_none(), "default setup config should have no before");
1497 }
1498
1499 #[test]
1500 fn explicit_update_config_preserves_precondition_and_before() {
1501 let config: AlefConfig = toml::from_str(
1502 r#"
1503languages = ["rust"]
1504
1505[crate]
1506name = "test"
1507sources = ["src/lib.rs"]
1508
1509[update.rust]
1510precondition = "test -f Cargo.lock"
1511before = "cargo fetch"
1512update = "cargo update"
1513"#,
1514 )
1515 .unwrap();
1516
1517 let update = config.update_config_for_language(Language::Rust);
1518 assert_eq!(
1519 update.precondition.as_deref(),
1520 Some("test -f Cargo.lock"),
1521 "update precondition should be preserved"
1522 );
1523 assert_eq!(
1524 update.before.unwrap().commands(),
1525 vec!["cargo fetch"],
1526 "update before should be preserved"
1527 );
1528 }
1529
1530 #[test]
1531 fn default_update_config_has_command_v_precondition() {
1532 let config = minimal_config();
1533 let rust = config.update_config_for_language(Language::Rust);
1534 assert_eq!(rust.precondition.as_deref(), Some("command -v cargo >/dev/null 2>&1"));
1535 assert!(rust.before.is_none(), "default update config should have no before");
1536 }
1537
1538 #[test]
1539 fn explicit_clean_config_preserves_precondition_and_before() {
1540 let config: AlefConfig = toml::from_str(
1541 r#"
1542languages = ["rust"]
1543
1544[crate]
1545name = "test"
1546sources = ["src/lib.rs"]
1547
1548[clean.rust]
1549precondition = "test -d target"
1550before = "echo cleaning"
1551clean = "cargo clean"
1552"#,
1553 )
1554 .unwrap();
1555
1556 let clean = config.clean_config_for_language(Language::Rust);
1557 assert_eq!(
1558 clean.precondition.as_deref(),
1559 Some("test -d target"),
1560 "clean precondition should be preserved"
1561 );
1562 assert_eq!(
1563 clean.before.unwrap().commands(),
1564 vec!["echo cleaning"],
1565 "clean before should be preserved"
1566 );
1567 }
1568
1569 #[test]
1570 fn default_clean_config_precondition_matches_toolchain_use() {
1571 let config = minimal_config();
1572 let rust = config.clean_config_for_language(Language::Rust);
1574 assert_eq!(rust.precondition.as_deref(), Some("command -v cargo >/dev/null 2>&1"));
1575 assert!(rust.before.is_none(), "default clean config should have no before");
1576
1577 let py = config.clean_config_for_language(Language::Python);
1579 assert!(
1580 py.precondition.is_none(),
1581 "pure-shell clean should not have a precondition"
1582 );
1583 }
1584
1585 #[test]
1586 fn explicit_build_command_config_preserves_precondition_and_before() {
1587 let config: AlefConfig = toml::from_str(
1588 r#"
1589languages = ["go"]
1590
1591[crate]
1592name = "test"
1593sources = ["src/lib.rs"]
1594
1595[build_commands.go]
1596precondition = "which go"
1597before = "cargo build --release -p test-ffi"
1598build = "cd packages/go && go build ./..."
1599build_release = "cd packages/go && go build -ldflags='-s -w' ./..."
1600"#,
1601 )
1602 .unwrap();
1603
1604 let build = config.build_command_config_for_language(Language::Go);
1605 assert_eq!(
1606 build.precondition.as_deref(),
1607 Some("which go"),
1608 "build precondition should be preserved"
1609 );
1610 assert_eq!(
1611 build.before.unwrap().commands(),
1612 vec!["cargo build --release -p test-ffi"],
1613 "build before should be preserved"
1614 );
1615 }
1616
1617 #[test]
1618 fn default_build_command_config_has_command_v_precondition() {
1619 let config = minimal_config();
1620 let rust = config.build_command_config_for_language(Language::Rust);
1621 assert_eq!(rust.precondition.as_deref(), Some("command -v cargo >/dev/null 2>&1"));
1622 assert!(
1623 rust.before.is_none(),
1624 "default build command config should have no before"
1625 );
1626 }
1627
1628 #[test]
1629 fn version_defaults_to_none_when_omitted() {
1630 let config = minimal_config();
1631 assert!(config.version.is_none());
1632 }
1633
1634 #[test]
1635 fn version_parses_from_top_level_key() {
1636 let config: AlefConfig = toml::from_str(
1637 r#"
1638version = "0.7.7"
1639languages = ["python"]
1640
1641[crate]
1642name = "test-lib"
1643sources = ["src/lib.rs"]
1644"#,
1645 )
1646 .unwrap();
1647 assert_eq!(config.version.as_deref(), Some("0.7.7"));
1648 }
1649}