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 format: FormatConfig,
121 #[serde(default)]
123 pub format_overrides: HashMap<String, FormatConfig>,
124 #[serde(default)]
126 pub dto: DtoConfig,
127 #[serde(default)]
129 pub e2e: Option<E2eConfig>,
130 #[serde(default)]
133 pub trait_bridges: Vec<TraitBridgeConfig>,
134 #[serde(default)]
138 pub tools: ToolsConfig,
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct CrateConfig {
143 pub name: String,
144 pub sources: Vec<PathBuf>,
145 #[serde(default = "default_version_from")]
146 pub version_from: String,
147 #[serde(default)]
148 pub core_import: Option<String>,
149 #[serde(default)]
151 pub workspace_root: Option<PathBuf>,
152 #[serde(default)]
154 pub skip_core_import: bool,
155 #[serde(default)]
159 pub error_type: Option<String>,
160 #[serde(default)]
165 pub error_constructor: Option<String>,
166 #[serde(default)]
170 pub features: Vec<String>,
171 #[serde(default)]
174 pub path_mappings: HashMap<String, String>,
175 #[serde(default)]
179 pub extra_dependencies: HashMap<String, toml::Value>,
180 #[serde(default = "default_true")]
184 pub auto_path_mappings: bool,
185 #[serde(default)]
190 pub source_crates: Vec<SourceCrate>,
191}
192
193#[derive(Debug, Clone, Serialize, Deserialize)]
195pub struct SourceCrate {
196 pub name: String,
198 pub sources: Vec<PathBuf>,
200}
201
202fn default_version_from() -> String {
203 "Cargo.toml".to_string()
204}
205
206fn default_true() -> bool {
207 true
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct GenerateConfig {
215 #[serde(default = "default_true")]
217 pub bindings: bool,
218 #[serde(default = "default_true")]
220 pub errors: bool,
221 #[serde(default = "default_true")]
223 pub configs: bool,
224 #[serde(default = "default_true")]
226 pub async_wrappers: bool,
227 #[serde(default = "default_true")]
229 pub type_conversions: bool,
230 #[serde(default = "default_true")]
232 pub package_metadata: bool,
233 #[serde(default = "default_true")]
235 pub public_api: bool,
236 #[serde(default = "default_true")]
239 pub reverse_conversions: bool,
240}
241
242impl Default for GenerateConfig {
243 fn default() -> Self {
244 Self {
245 bindings: true,
246 errors: true,
247 configs: true,
248 async_wrappers: true,
249 type_conversions: true,
250 package_metadata: true,
251 public_api: true,
252 reverse_conversions: true,
253 }
254 }
255}
256
257#[derive(Debug, Clone, Serialize, Deserialize)]
261pub struct FormatConfig {
262 #[serde(default = "default_true")]
266 pub enabled: bool,
267 #[serde(default)]
271 pub command: Option<String>,
272}
273
274impl Default for FormatConfig {
275 fn default() -> Self {
276 Self {
277 enabled: true,
278 command: None,
279 }
280 }
281}
282
283impl AlefConfig {
288 pub fn resolve_field_name(&self, lang: extras::Language, type_name: &str, field_name: &str) -> Option<String> {
300 let explicit_key = format!("{type_name}.{field_name}");
302 let explicit = match lang {
303 extras::Language::Python => self.python.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
304 extras::Language::Node => self.node.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
305 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
306 extras::Language::Php => self.php.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
307 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
308 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
309 extras::Language::Ffi => self.ffi.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
310 extras::Language::Go => self.go.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
311 extras::Language::Java => self.java.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
312 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
313 extras::Language::R => self.r.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
314 extras::Language::Rust => None,
315 };
316 if let Some(renamed) = explicit {
317 if renamed != field_name {
318 return Some(renamed.clone());
319 }
320 return None;
321 }
322
323 match lang {
325 extras::Language::Python => crate::keywords::python_safe_name(field_name),
326 _ => None,
331 }
332 }
333
334 pub fn features_for_language(&self, lang: extras::Language) -> &[String] {
337 let override_features = match lang {
338 extras::Language::Python => self.python.as_ref().and_then(|c| c.features.as_deref()),
339 extras::Language::Node => self.node.as_ref().and_then(|c| c.features.as_deref()),
340 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.features.as_deref()),
341 extras::Language::Php => self.php.as_ref().and_then(|c| c.features.as_deref()),
342 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.features.as_deref()),
343 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.features.as_deref()),
344 extras::Language::Ffi => self.ffi.as_ref().and_then(|c| c.features.as_deref()),
345 extras::Language::Go => self.go.as_ref().and_then(|c| c.features.as_deref()),
346 extras::Language::Java => self.java.as_ref().and_then(|c| c.features.as_deref()),
347 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.features.as_deref()),
348 extras::Language::R => self.r.as_ref().and_then(|c| c.features.as_deref()),
349 extras::Language::Rust => None, };
351 override_features.unwrap_or(&self.crate_config.features)
352 }
353
354 pub fn extra_deps_for_language(&self, lang: extras::Language) -> HashMap<String, toml::Value> {
358 let mut deps = self.crate_config.extra_dependencies.clone();
359 let lang_deps = match lang {
360 extras::Language::Python => self.python.as_ref().map(|c| &c.extra_dependencies),
361 extras::Language::Node => self.node.as_ref().map(|c| &c.extra_dependencies),
362 extras::Language::Ruby => self.ruby.as_ref().map(|c| &c.extra_dependencies),
363 extras::Language::Php => self.php.as_ref().map(|c| &c.extra_dependencies),
364 extras::Language::Elixir => self.elixir.as_ref().map(|c| &c.extra_dependencies),
365 extras::Language::Wasm => self.wasm.as_ref().map(|c| &c.extra_dependencies),
366 _ => None,
367 };
368 if let Some(lang_deps) = lang_deps {
369 deps.extend(lang_deps.iter().map(|(k, v)| (k.clone(), v.clone())));
370 }
371 deps
372 }
373
374 pub fn package_dir(&self, lang: extras::Language) -> String {
379 let override_path = match lang {
380 extras::Language::Python => self.python.as_ref().and_then(|c| c.scaffold_output.as_ref()),
381 extras::Language::Node => self.node.as_ref().and_then(|c| c.scaffold_output.as_ref()),
382 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.scaffold_output.as_ref()),
383 extras::Language::Php => self.php.as_ref().and_then(|c| c.scaffold_output.as_ref()),
384 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.scaffold_output.as_ref()),
385 _ => None,
386 };
387 if let Some(p) = override_path {
388 p.to_string_lossy().to_string()
389 } else {
390 match lang {
391 extras::Language::Python => "packages/python".to_string(),
392 extras::Language::Node => "packages/node".to_string(),
393 extras::Language::Ruby => "packages/ruby".to_string(),
394 extras::Language::Php => "packages/php".to_string(),
395 extras::Language::Elixir => "packages/elixir".to_string(),
396 _ => format!("packages/{lang}"),
397 }
398 }
399 }
400
401 pub fn validate(&self) -> Result<(), crate::error::AlefError> {
408 validation::validate(self)
409 }
410
411 pub fn lint_config_for_language(&self, lang: extras::Language) -> output::LintConfig {
416 if let Some(lint_map) = &self.lint {
417 let lang_str = lang.to_string();
418 if let Some(explicit) = lint_map.get(&lang_str) {
419 return explicit.clone();
420 }
421 }
422 let output_dir = self.package_dir(lang);
423 let run_wrapper = self.run_wrapper_for_language(lang);
424 let extra_lint_paths = self.extra_lint_paths_for_language(lang);
425 let project_file = self.project_file_for_language(lang);
426 let ctx = LangContext {
427 tools: &self.tools,
428 run_wrapper,
429 extra_lint_paths,
430 project_file,
431 };
432 lint_defaults::default_lint_config(lang, &output_dir, &ctx)
433 }
434
435 pub fn update_config_for_language(&self, lang: extras::Language) -> output::UpdateConfig {
440 if let Some(update_map) = &self.update {
441 let lang_str = lang.to_string();
442 if let Some(explicit) = update_map.get(&lang_str) {
443 return explicit.clone();
444 }
445 }
446 let output_dir = self.package_dir(lang);
447 let ctx = LangContext {
448 tools: &self.tools,
449 run_wrapper: None,
450 extra_lint_paths: &[],
451 project_file: None,
452 };
453 update_defaults::default_update_config(lang, &output_dir, &ctx)
454 }
455
456 pub fn test_config_for_language(&self, lang: extras::Language) -> output::TestConfig {
461 if let Some(test_map) = &self.test {
462 let lang_str = lang.to_string();
463 if let Some(explicit) = test_map.get(&lang_str) {
464 return explicit.clone();
465 }
466 }
467 let output_dir = self.package_dir(lang);
468 let run_wrapper = self.run_wrapper_for_language(lang);
469 let project_file = self.project_file_for_language(lang);
470 let ctx = LangContext {
471 tools: &self.tools,
472 run_wrapper,
473 extra_lint_paths: &[],
474 project_file,
475 };
476 test_defaults::default_test_config(lang, &output_dir, &ctx)
477 }
478
479 pub fn setup_config_for_language(&self, lang: extras::Language) -> output::SetupConfig {
484 if let Some(setup_map) = &self.setup {
485 let lang_str = lang.to_string();
486 if let Some(explicit) = setup_map.get(&lang_str) {
487 return explicit.clone();
488 }
489 }
490 let output_dir = self.package_dir(lang);
491 let ctx = LangContext {
492 tools: &self.tools,
493 run_wrapper: None,
494 extra_lint_paths: &[],
495 project_file: None,
496 };
497 setup_defaults::default_setup_config(lang, &output_dir, &ctx)
498 }
499
500 pub fn clean_config_for_language(&self, lang: extras::Language) -> output::CleanConfig {
505 if let Some(clean_map) = &self.clean {
506 let lang_str = lang.to_string();
507 if let Some(explicit) = clean_map.get(&lang_str) {
508 return explicit.clone();
509 }
510 }
511 let output_dir = self.package_dir(lang);
512 let ctx = LangContext {
513 tools: &self.tools,
514 run_wrapper: None,
515 extra_lint_paths: &[],
516 project_file: None,
517 };
518 clean_defaults::default_clean_config(lang, &output_dir, &ctx)
519 }
520
521 pub fn build_command_config_for_language(&self, lang: extras::Language) -> output::BuildCommandConfig {
526 if let Some(build_map) = &self.build_commands {
527 let lang_str = lang.to_string();
528 if let Some(explicit) = build_map.get(&lang_str) {
529 return explicit.clone();
530 }
531 }
532 let output_dir = self.package_dir(lang);
533 let crate_name = &self.crate_config.name;
534 let run_wrapper = self.run_wrapper_for_language(lang);
535 let project_file = self.project_file_for_language(lang);
536 let ctx = LangContext {
537 tools: &self.tools,
538 run_wrapper,
539 extra_lint_paths: &[],
540 project_file,
541 };
542 build_defaults::default_build_config(lang, &output_dir, crate_name, &ctx)
543 }
544
545 pub fn core_import(&self) -> String {
547 self.crate_config
548 .core_import
549 .clone()
550 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
551 }
552
553 pub fn error_type(&self) -> String {
555 self.crate_config
556 .error_type
557 .clone()
558 .unwrap_or_else(|| "Error".to_string())
559 }
560
561 pub fn error_constructor(&self) -> String {
564 self.crate_config
565 .error_constructor
566 .clone()
567 .unwrap_or_else(|| format!("{}::{}::from({{msg}})", self.core_import(), self.error_type()))
568 }
569
570 pub fn run_wrapper_for_language(&self, lang: extras::Language) -> Option<&str> {
573 match lang {
574 extras::Language::Python => self.python.as_ref().and_then(|c| c.run_wrapper.as_deref()),
575 extras::Language::Node => self.node.as_ref().and_then(|c| c.run_wrapper.as_deref()),
576 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.run_wrapper.as_deref()),
577 extras::Language::Php => self.php.as_ref().and_then(|c| c.run_wrapper.as_deref()),
578 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.run_wrapper.as_deref()),
579 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.run_wrapper.as_deref()),
580 extras::Language::Go => self.go.as_ref().and_then(|c| c.run_wrapper.as_deref()),
581 extras::Language::Java => self.java.as_ref().and_then(|c| c.run_wrapper.as_deref()),
582 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.run_wrapper.as_deref()),
583 extras::Language::R => self.r.as_ref().and_then(|c| c.run_wrapper.as_deref()),
584 _ => None,
585 }
586 }
587
588 pub fn extra_lint_paths_for_language(&self, lang: extras::Language) -> &[String] {
591 match lang {
592 extras::Language::Python => self
593 .python
594 .as_ref()
595 .map(|c| c.extra_lint_paths.as_slice())
596 .unwrap_or(&[]),
597 extras::Language::Node => self.node.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
598 extras::Language::Ruby => self.ruby.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
599 extras::Language::Php => self.php.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
600 extras::Language::Elixir => self
601 .elixir
602 .as_ref()
603 .map(|c| c.extra_lint_paths.as_slice())
604 .unwrap_or(&[]),
605 extras::Language::Wasm => self.wasm.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
606 extras::Language::Go => self.go.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
607 extras::Language::Java => self.java.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
608 extras::Language::Csharp => self
609 .csharp
610 .as_ref()
611 .map(|c| c.extra_lint_paths.as_slice())
612 .unwrap_or(&[]),
613 extras::Language::R => self.r.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
614 _ => &[],
615 }
616 }
617
618 pub fn project_file_for_language(&self, lang: extras::Language) -> Option<&str> {
621 match lang {
622 extras::Language::Java => self.java.as_ref().and_then(|c| c.project_file.as_deref()),
623 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.project_file.as_deref()),
624 _ => None,
625 }
626 }
627
628 pub fn ffi_prefix(&self) -> String {
630 self.ffi
631 .as_ref()
632 .and_then(|f| f.prefix.as_ref())
633 .cloned()
634 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
635 }
636
637 pub fn ffi_lib_name(&self) -> String {
645 if let Some(name) = self.ffi.as_ref().and_then(|f| f.lib_name.as_ref()) {
647 return name.clone();
648 }
649
650 if let Some(ffi_path) = self.output.ffi.as_ref() {
653 let path = std::path::Path::new(ffi_path);
654 let components: Vec<_> = path
657 .components()
658 .filter_map(|c| {
659 if let std::path::Component::Normal(s) = c {
660 s.to_str()
661 } else {
662 None
663 }
664 })
665 .collect();
666 let crate_dir = components
669 .iter()
670 .rev()
671 .find(|&&s| s != "src" && s != "lib" && s != "include")
672 .copied();
673 if let Some(dir) = crate_dir {
674 return dir.replace('-', "_");
675 }
676 }
677
678 format!("{}_ffi", self.ffi_prefix())
680 }
681
682 pub fn ffi_header_name(&self) -> String {
684 self.ffi
685 .as_ref()
686 .and_then(|f| f.header_name.as_ref())
687 .cloned()
688 .unwrap_or_else(|| format!("{}.h", self.ffi_prefix()))
689 }
690
691 pub fn python_module_name(&self) -> String {
693 self.python
694 .as_ref()
695 .and_then(|p| p.module_name.as_ref())
696 .cloned()
697 .unwrap_or_else(|| format!("_{}", self.crate_config.name.replace('-', "_")))
698 }
699
700 pub fn python_pip_name(&self) -> String {
704 self.python
705 .as_ref()
706 .and_then(|p| p.pip_name.as_ref())
707 .cloned()
708 .unwrap_or_else(|| self.crate_config.name.clone())
709 }
710
711 pub fn php_autoload_namespace(&self) -> String {
716 use heck::ToPascalCase;
717 let ext = self.php_extension_name();
718 if ext.contains('_') {
719 ext.split('_')
720 .map(|p| p.to_pascal_case())
721 .collect::<Vec<_>>()
722 .join("\\")
723 } else {
724 ext.to_pascal_case()
725 }
726 }
727
728 pub fn node_package_name(&self) -> String {
730 self.node
731 .as_ref()
732 .and_then(|n| n.package_name.as_ref())
733 .cloned()
734 .unwrap_or_else(|| self.crate_config.name.clone())
735 }
736
737 pub fn ruby_gem_name(&self) -> String {
739 self.ruby
740 .as_ref()
741 .and_then(|r| r.gem_name.as_ref())
742 .cloned()
743 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
744 }
745
746 pub fn php_extension_name(&self) -> String {
748 self.php
749 .as_ref()
750 .and_then(|p| p.extension_name.as_ref())
751 .cloned()
752 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
753 }
754
755 pub fn elixir_app_name(&self) -> String {
757 self.elixir
758 .as_ref()
759 .and_then(|e| e.app_name.as_ref())
760 .cloned()
761 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
762 }
763
764 pub fn go_module(&self) -> String {
766 self.go
767 .as_ref()
768 .and_then(|g| g.module.as_ref())
769 .cloned()
770 .unwrap_or_else(|| format!("github.com/kreuzberg-dev/{}", self.crate_config.name))
771 }
772
773 pub fn github_repo(&self) -> String {
780 if let Some(e2e) = &self.e2e {
781 if let Some(url) = &e2e.registry.github_repo {
782 return url.clone();
783 }
784 }
785 self.scaffold
786 .as_ref()
787 .and_then(|s| s.repository.as_ref())
788 .cloned()
789 .unwrap_or_else(|| format!("https://github.com/kreuzberg-dev/{}", self.crate_config.name))
790 }
791
792 pub fn java_package(&self) -> String {
794 self.java
795 .as_ref()
796 .and_then(|j| j.package.as_ref())
797 .cloned()
798 .unwrap_or_else(|| "dev.kreuzberg".to_string())
799 }
800
801 pub fn java_group_id(&self) -> String {
806 self.java_package()
807 }
808
809 pub fn csharp_namespace(&self) -> String {
811 self.csharp
812 .as_ref()
813 .and_then(|c| c.namespace.as_ref())
814 .cloned()
815 .unwrap_or_else(|| {
816 use heck::ToPascalCase;
817 self.crate_config.name.to_pascal_case()
818 })
819 }
820
821 pub fn core_crate_dir(&self) -> String {
827 if let Some(first_source) = self.crate_config.sources.first() {
830 let path = std::path::Path::new(first_source);
831 let mut current = path.parent();
832 while let Some(dir) = current {
833 if dir.file_name().is_some_and(|n| n == "src") {
834 if let Some(crate_dir) = dir.parent() {
835 if let Some(dir_name) = crate_dir.file_name() {
836 return dir_name.to_string_lossy().into_owned();
837 }
838 }
839 break;
840 }
841 current = dir.parent();
842 }
843 }
844 self.crate_config.name.clone()
845 }
846
847 pub fn wasm_type_prefix(&self) -> String {
850 self.wasm
851 .as_ref()
852 .and_then(|w| w.type_prefix.as_ref())
853 .cloned()
854 .unwrap_or_else(|| "Wasm".to_string())
855 }
856
857 pub fn node_type_prefix(&self) -> String {
860 self.node
861 .as_ref()
862 .and_then(|n| n.type_prefix.as_ref())
863 .cloned()
864 .unwrap_or_else(|| "Js".to_string())
865 }
866
867 pub fn r_package_name(&self) -> String {
869 self.r
870 .as_ref()
871 .and_then(|r| r.package_name.as_ref())
872 .cloned()
873 .unwrap_or_else(|| self.crate_config.name.clone())
874 }
875
876 pub fn resolved_version(&self) -> Option<String> {
879 let content = std::fs::read_to_string(&self.crate_config.version_from).ok()?;
880 let value: toml::Value = toml::from_str(&content).ok()?;
881 if let Some(v) = value
882 .get("workspace")
883 .and_then(|w| w.get("package"))
884 .and_then(|p| p.get("version"))
885 .and_then(|v| v.as_str())
886 {
887 return Some(v.to_string());
888 }
889 value
890 .get("package")
891 .and_then(|p| p.get("version"))
892 .and_then(|v| v.as_str())
893 .map(|v| v.to_string())
894 }
895
896 pub fn serde_rename_all_for_language(&self, lang: extras::Language) -> String {
904 let override_val = match lang {
906 extras::Language::Python => self.python.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
907 extras::Language::Node => self.node.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
908 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
909 extras::Language::Php => self.php.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
910 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
911 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
912 extras::Language::Ffi => self.ffi.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
913 extras::Language::Go => self.go.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
914 extras::Language::Java => self.java.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
915 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
916 extras::Language::R => self.r.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
917 extras::Language::Rust => None, };
919
920 if let Some(val) = override_val {
921 return val.to_string();
922 }
923
924 match lang {
926 extras::Language::Node | extras::Language::Wasm | extras::Language::Java | extras::Language::Csharp => {
927 "camelCase".to_string()
928 }
929 extras::Language::Python
930 | extras::Language::Ruby
931 | extras::Language::Php
932 | extras::Language::Go
933 | extras::Language::Ffi
934 | extras::Language::Elixir
935 | extras::Language::R
936 | extras::Language::Rust => "snake_case".to_string(),
937 }
938 }
939
940 pub fn rewrite_path(&self, rust_path: &str) -> String {
943 let mut mappings: Vec<_> = self.crate_config.path_mappings.iter().collect();
945 mappings.sort_by_key(|b| std::cmp::Reverse(b.0.len()));
946
947 for (from, to) in &mappings {
948 if rust_path.starts_with(from.as_str()) {
949 return format!("{}{}", to, &rust_path[from.len()..]);
950 }
951 }
952 rust_path.to_string()
953 }
954
955 pub fn effective_path_mappings(&self) -> HashMap<String, String> {
965 let mut mappings = HashMap::new();
966
967 if self.crate_config.auto_path_mappings {
968 let core_import = self.core_import();
969
970 for source in &self.crate_config.sources {
971 let source_str = source.to_string_lossy();
972 if let Some(after_crates) = find_after_crates_prefix(&source_str) {
974 if let Some(slash_pos) = after_crates.find('/') {
976 let crate_dir = &after_crates[..slash_pos];
977 let crate_ident = crate_dir.replace('-', "_");
978 if crate_ident != core_import && !mappings.contains_key(&crate_ident) {
980 mappings.insert(crate_ident, core_import.clone());
981 }
982 }
983 }
984 }
985 }
986
987 for (from, to) in &self.crate_config.path_mappings {
989 mappings.insert(from.clone(), to.clone());
990 }
991
992 mappings
993 }
994}
995
996fn find_after_crates_prefix(path: &str) -> Option<&str> {
1003 if let Some(pos) = path.find("/crates/") {
1007 return Some(&path[pos + "/crates/".len()..]);
1008 }
1009 if let Some(stripped) = path.strip_prefix("crates/") {
1010 return Some(stripped);
1011 }
1012 None
1013}
1014
1015pub fn resolve_output_dir(config_path: Option<&PathBuf>, crate_name: &str, default: &str) -> String {
1018 config_path
1019 .map(|p| p.to_string_lossy().replace("{name}", crate_name))
1020 .unwrap_or_else(|| default.replace("{name}", crate_name))
1021}
1022
1023pub fn detect_serde_available(output_dir: &str) -> bool {
1029 let src_path = std::path::Path::new(output_dir);
1030 let mut dir = src_path;
1032 loop {
1033 let cargo_toml = dir.join("Cargo.toml");
1034 if cargo_toml.exists() {
1035 return cargo_toml_has_serde(&cargo_toml);
1036 }
1037 match dir.parent() {
1038 Some(parent) if !parent.as_os_str().is_empty() => dir = parent,
1039 _ => break,
1040 }
1041 }
1042 false
1043}
1044
1045fn cargo_toml_has_serde(path: &std::path::Path) -> bool {
1051 let content = match std::fs::read_to_string(path) {
1052 Ok(c) => c,
1053 Err(_) => return false,
1054 };
1055
1056 let has_serde_json = content.contains("serde_json");
1057 let has_serde_dep = content.lines().any(|line| {
1061 let trimmed = line.trim();
1062 trimmed.starts_with("serde ")
1064 || trimmed.starts_with("serde=")
1065 || trimmed.starts_with("serde.")
1066 || trimmed == "[dependencies.serde]"
1067 });
1068
1069 has_serde_json && has_serde_dep
1070}
1071
1072#[cfg(test)]
1073mod tests {
1074 use super::*;
1075
1076 fn minimal_config() -> AlefConfig {
1077 toml::from_str(
1078 r#"
1079languages = ["python", "node", "rust"]
1080
1081[crate]
1082name = "test-lib"
1083sources = ["src/lib.rs"]
1084"#,
1085 )
1086 .unwrap()
1087 }
1088
1089 #[test]
1090 fn lint_config_falls_back_to_defaults() {
1091 let config = minimal_config();
1092 assert!(config.lint.is_none());
1093
1094 let py = config.lint_config_for_language(Language::Python);
1095 assert!(py.format.is_some());
1096 assert!(py.check.is_some());
1097 assert!(py.typecheck.is_some());
1098
1099 let node = config.lint_config_for_language(Language::Node);
1100 assert!(node.format.is_some());
1101 assert!(node.check.is_some());
1102 }
1103
1104 #[test]
1105 fn lint_config_explicit_overrides_default() {
1106 let config: AlefConfig = toml::from_str(
1107 r#"
1108languages = ["python"]
1109
1110[crate]
1111name = "test-lib"
1112sources = ["src/lib.rs"]
1113
1114[lint.python]
1115format = "custom-formatter"
1116check = "custom-checker"
1117"#,
1118 )
1119 .unwrap();
1120
1121 let py = config.lint_config_for_language(Language::Python);
1122 assert_eq!(py.format.unwrap().commands(), vec!["custom-formatter"]);
1123 assert_eq!(py.check.unwrap().commands(), vec!["custom-checker"]);
1124 assert!(py.typecheck.is_none()); }
1126
1127 #[test]
1128 fn lint_config_partial_override_does_not_merge() {
1129 let config: AlefConfig = toml::from_str(
1130 r#"
1131languages = ["python"]
1132
1133[crate]
1134name = "test-lib"
1135sources = ["src/lib.rs"]
1136
1137[lint.python]
1138format = "only-format"
1139"#,
1140 )
1141 .unwrap();
1142
1143 let py = config.lint_config_for_language(Language::Python);
1144 assert_eq!(py.format.unwrap().commands(), vec!["only-format"]);
1145 assert!(py.check.is_none());
1147 assert!(py.typecheck.is_none());
1148 }
1149
1150 #[test]
1151 fn lint_config_unconfigured_language_uses_defaults() {
1152 let config: AlefConfig = toml::from_str(
1153 r#"
1154languages = ["python", "node"]
1155
1156[crate]
1157name = "test-lib"
1158sources = ["src/lib.rs"]
1159
1160[lint.python]
1161format = "custom"
1162"#,
1163 )
1164 .unwrap();
1165
1166 let py = config.lint_config_for_language(Language::Python);
1168 assert_eq!(py.format.unwrap().commands(), vec!["custom"]);
1169
1170 let node = config.lint_config_for_language(Language::Node);
1172 let fmt = node.format.unwrap().commands().join(" ");
1173 assert!(fmt.contains("oxfmt"));
1174 }
1175
1176 #[test]
1177 fn update_config_falls_back_to_defaults() {
1178 let config = minimal_config();
1179 assert!(config.update.is_none());
1180
1181 let py = config.update_config_for_language(Language::Python);
1182 assert!(py.update.is_some());
1183 assert!(py.upgrade.is_some());
1184
1185 let rust = config.update_config_for_language(Language::Rust);
1186 let update = rust.update.unwrap().commands().join(" ");
1187 assert!(update.contains("cargo update"));
1188 }
1189
1190 #[test]
1191 fn update_config_explicit_overrides_default() {
1192 let config: AlefConfig = toml::from_str(
1193 r#"
1194languages = ["rust"]
1195
1196[crate]
1197name = "test-lib"
1198sources = ["src/lib.rs"]
1199
1200[update.rust]
1201update = "my-custom-update"
1202upgrade = ["step1", "step2"]
1203"#,
1204 )
1205 .unwrap();
1206
1207 let rust = config.update_config_for_language(Language::Rust);
1208 assert_eq!(rust.update.unwrap().commands(), vec!["my-custom-update"]);
1209 assert_eq!(rust.upgrade.unwrap().commands(), vec!["step1", "step2"]);
1210 }
1211
1212 #[test]
1213 fn test_config_falls_back_to_defaults() {
1214 let config = minimal_config();
1215 assert!(config.test.is_none());
1216
1217 let py = config.test_config_for_language(Language::Python);
1218 assert!(py.command.is_some());
1219 assert!(py.coverage.is_some());
1220 assert!(py.e2e.is_none());
1221
1222 let rust = config.test_config_for_language(Language::Rust);
1223 let cmd = rust.command.unwrap().commands().join(" ");
1224 assert!(cmd.contains("cargo test"));
1225 }
1226
1227 #[test]
1228 fn test_config_explicit_overrides_default() {
1229 let config: AlefConfig = toml::from_str(
1230 r#"
1231languages = ["python"]
1232
1233[crate]
1234name = "test-lib"
1235sources = ["src/lib.rs"]
1236
1237[test.python]
1238command = "my-custom-test"
1239"#,
1240 )
1241 .unwrap();
1242
1243 let py = config.test_config_for_language(Language::Python);
1244 assert_eq!(py.command.unwrap().commands(), vec!["my-custom-test"]);
1245 assert!(py.coverage.is_none()); }
1247
1248 #[test]
1249 fn setup_config_falls_back_to_defaults() {
1250 let config = minimal_config();
1251 assert!(config.setup.is_none());
1252
1253 let py = config.setup_config_for_language(Language::Python);
1254 assert!(py.install.is_some());
1255 let install = py.install.unwrap().commands().join(" ");
1256 assert!(install.contains("uv sync"));
1257
1258 let rust = config.setup_config_for_language(Language::Rust);
1259 let install = rust.install.unwrap().commands().join(" ");
1260 assert!(install.contains("rustup update"));
1261 }
1262
1263 #[test]
1264 fn setup_config_explicit_overrides_default() {
1265 let config: AlefConfig = toml::from_str(
1266 r#"
1267languages = ["python"]
1268
1269[crate]
1270name = "test-lib"
1271sources = ["src/lib.rs"]
1272
1273[setup.python]
1274install = "my-custom-install"
1275"#,
1276 )
1277 .unwrap();
1278
1279 let py = config.setup_config_for_language(Language::Python);
1280 assert_eq!(py.install.unwrap().commands(), vec!["my-custom-install"]);
1281 }
1282
1283 #[test]
1284 fn clean_config_falls_back_to_defaults() {
1285 let config = minimal_config();
1286 assert!(config.clean.is_none());
1287
1288 let py = config.clean_config_for_language(Language::Python);
1289 assert!(py.clean.is_some());
1290 let clean = py.clean.unwrap().commands().join(" ");
1291 assert!(clean.contains("__pycache__"));
1292
1293 let rust = config.clean_config_for_language(Language::Rust);
1294 let clean = rust.clean.unwrap().commands().join(" ");
1295 assert!(clean.contains("cargo clean"));
1296 }
1297
1298 #[test]
1299 fn clean_config_explicit_overrides_default() {
1300 let config: AlefConfig = toml::from_str(
1301 r#"
1302languages = ["rust"]
1303
1304[crate]
1305name = "test-lib"
1306sources = ["src/lib.rs"]
1307
1308[clean.rust]
1309clean = "my-custom-clean"
1310"#,
1311 )
1312 .unwrap();
1313
1314 let rust = config.clean_config_for_language(Language::Rust);
1315 assert_eq!(rust.clean.unwrap().commands(), vec!["my-custom-clean"]);
1316 }
1317
1318 #[test]
1319 fn build_command_config_falls_back_to_defaults() {
1320 let config = minimal_config();
1321 assert!(config.build_commands.is_none());
1322
1323 let py = config.build_command_config_for_language(Language::Python);
1324 assert!(py.build.is_some());
1325 assert!(py.build_release.is_some());
1326 let build = py.build.unwrap().commands().join(" ");
1327 assert!(build.contains("maturin develop"));
1328
1329 let rust = config.build_command_config_for_language(Language::Rust);
1330 let build = rust.build.unwrap().commands().join(" ");
1331 assert!(build.contains("cargo build --workspace"));
1332 }
1333
1334 #[test]
1335 fn build_command_config_explicit_overrides_default() {
1336 let config: AlefConfig = toml::from_str(
1337 r#"
1338languages = ["rust"]
1339
1340[crate]
1341name = "test-lib"
1342sources = ["src/lib.rs"]
1343
1344[build_commands.rust]
1345build = "my-custom-build"
1346build_release = "my-custom-build --release"
1347"#,
1348 )
1349 .unwrap();
1350
1351 let rust = config.build_command_config_for_language(Language::Rust);
1352 assert_eq!(rust.build.unwrap().commands(), vec!["my-custom-build"]);
1353 assert_eq!(
1354 rust.build_release.unwrap().commands(),
1355 vec!["my-custom-build --release"]
1356 );
1357 }
1358
1359 #[test]
1360 fn build_command_config_uses_crate_name() {
1361 let config = minimal_config();
1362 let py = config.build_command_config_for_language(Language::Python);
1363 let build = py.build.unwrap().commands().join(" ");
1364 assert!(
1365 build.contains("test-lib-py"),
1366 "Python build should reference crate name, got: {build}"
1367 );
1368 }
1369
1370 #[test]
1371 fn package_dir_defaults_are_correct() {
1372 let config = minimal_config();
1373 assert_eq!(config.package_dir(Language::Python), "packages/python");
1374 assert_eq!(config.package_dir(Language::Node), "packages/node");
1375 assert_eq!(config.package_dir(Language::Ruby), "packages/ruby");
1376 assert_eq!(config.package_dir(Language::Go), "packages/go");
1377 assert_eq!(config.package_dir(Language::Java), "packages/java");
1378 }
1379
1380 #[test]
1381 fn explicit_lint_config_preserves_precondition_and_before() {
1382 let config: AlefConfig = toml::from_str(
1383 r#"
1384languages = ["go"]
1385
1386[crate]
1387name = "test"
1388sources = ["src/lib.rs"]
1389
1390[lint.go]
1391precondition = "test -f target/release/libtest_ffi.so"
1392before = "cargo build --release -p test-ffi"
1393format = "gofmt -w packages/go"
1394check = "golangci-lint run ./..."
1395"#,
1396 )
1397 .unwrap();
1398
1399 let lint = config.lint_config_for_language(Language::Go);
1400 assert_eq!(
1401 lint.precondition.as_deref(),
1402 Some("test -f target/release/libtest_ffi.so"),
1403 "precondition should be preserved from explicit config"
1404 );
1405 assert_eq!(
1406 lint.before.unwrap().commands(),
1407 vec!["cargo build --release -p test-ffi"],
1408 "before should be preserved from explicit config"
1409 );
1410 }
1411
1412 #[test]
1413 fn explicit_lint_config_with_before_list_preserves_all_commands() {
1414 let config: AlefConfig = toml::from_str(
1415 r#"
1416languages = ["go"]
1417
1418[crate]
1419name = "test"
1420sources = ["src/lib.rs"]
1421
1422[lint.go]
1423before = ["cargo build --release -p test-ffi", "cp target/release/libtest_ffi.so packages/go/"]
1424check = "golangci-lint run ./..."
1425"#,
1426 )
1427 .unwrap();
1428
1429 let lint = config.lint_config_for_language(Language::Go);
1430 assert!(lint.precondition.is_none(), "precondition should be None when not set");
1431 assert_eq!(
1432 lint.before.unwrap().commands(),
1433 vec![
1434 "cargo build --release -p test-ffi",
1435 "cp target/release/libtest_ffi.so packages/go/"
1436 ],
1437 "before list should be preserved from explicit config"
1438 );
1439 }
1440
1441 #[test]
1442 fn default_lint_config_has_command_v_precondition() {
1443 let config = minimal_config();
1444 let py = config.lint_config_for_language(Language::Python);
1445 assert_eq!(py.precondition.as_deref(), Some("command -v ruff >/dev/null 2>&1"));
1446 assert!(py.before.is_none(), "default lint config should have no before");
1447
1448 let go = config.lint_config_for_language(Language::Go);
1449 assert_eq!(go.precondition.as_deref(), Some("command -v gofmt >/dev/null 2>&1"));
1450 assert!(go.before.is_none(), "default Go lint config should have no before");
1451 }
1452
1453 #[test]
1454 fn explicit_test_config_preserves_precondition_and_before() {
1455 let config: AlefConfig = toml::from_str(
1456 r#"
1457languages = ["python"]
1458
1459[crate]
1460name = "test"
1461sources = ["src/lib.rs"]
1462
1463[test.python]
1464precondition = "test -f target/release/libtest.so"
1465before = "maturin develop"
1466command = "pytest"
1467"#,
1468 )
1469 .unwrap();
1470
1471 let test = config.test_config_for_language(Language::Python);
1472 assert_eq!(
1473 test.precondition.as_deref(),
1474 Some("test -f target/release/libtest.so"),
1475 "test precondition should be preserved"
1476 );
1477 assert_eq!(
1478 test.before.unwrap().commands(),
1479 vec!["maturin develop"],
1480 "test before should be preserved"
1481 );
1482 }
1483
1484 #[test]
1485 fn default_test_config_has_command_v_precondition() {
1486 let config = minimal_config();
1487 let py = config.test_config_for_language(Language::Python);
1488 assert_eq!(py.precondition.as_deref(), Some("command -v uv >/dev/null 2>&1"));
1489 assert!(py.before.is_none(), "default test config should have no before");
1490 }
1491
1492 #[test]
1493 fn explicit_setup_config_preserves_precondition_and_before() {
1494 let config: AlefConfig = toml::from_str(
1495 r#"
1496languages = ["python"]
1497
1498[crate]
1499name = "test"
1500sources = ["src/lib.rs"]
1501
1502[setup.python]
1503precondition = "which uv"
1504before = "pip install uv"
1505install = "uv sync"
1506"#,
1507 )
1508 .unwrap();
1509
1510 let setup = config.setup_config_for_language(Language::Python);
1511 assert_eq!(
1512 setup.precondition.as_deref(),
1513 Some("which uv"),
1514 "setup precondition should be preserved"
1515 );
1516 assert_eq!(
1517 setup.before.unwrap().commands(),
1518 vec!["pip install uv"],
1519 "setup before should be preserved"
1520 );
1521 }
1522
1523 #[test]
1524 fn default_setup_config_has_command_v_precondition() {
1525 let config = minimal_config();
1526 let py = config.setup_config_for_language(Language::Python);
1527 assert_eq!(py.precondition.as_deref(), Some("command -v uv >/dev/null 2>&1"));
1528 assert!(py.before.is_none(), "default setup config should have no before");
1529 }
1530
1531 #[test]
1532 fn explicit_update_config_preserves_precondition_and_before() {
1533 let config: AlefConfig = toml::from_str(
1534 r#"
1535languages = ["rust"]
1536
1537[crate]
1538name = "test"
1539sources = ["src/lib.rs"]
1540
1541[update.rust]
1542precondition = "test -f Cargo.lock"
1543before = "cargo fetch"
1544update = "cargo update"
1545"#,
1546 )
1547 .unwrap();
1548
1549 let update = config.update_config_for_language(Language::Rust);
1550 assert_eq!(
1551 update.precondition.as_deref(),
1552 Some("test -f Cargo.lock"),
1553 "update precondition should be preserved"
1554 );
1555 assert_eq!(
1556 update.before.unwrap().commands(),
1557 vec!["cargo fetch"],
1558 "update before should be preserved"
1559 );
1560 }
1561
1562 #[test]
1563 fn default_update_config_has_command_v_precondition() {
1564 let config = minimal_config();
1565 let rust = config.update_config_for_language(Language::Rust);
1566 assert_eq!(rust.precondition.as_deref(), Some("command -v cargo >/dev/null 2>&1"));
1567 assert!(rust.before.is_none(), "default update config should have no before");
1568 }
1569
1570 #[test]
1571 fn explicit_clean_config_preserves_precondition_and_before() {
1572 let config: AlefConfig = toml::from_str(
1573 r#"
1574languages = ["rust"]
1575
1576[crate]
1577name = "test"
1578sources = ["src/lib.rs"]
1579
1580[clean.rust]
1581precondition = "test -d target"
1582before = "echo cleaning"
1583clean = "cargo clean"
1584"#,
1585 )
1586 .unwrap();
1587
1588 let clean = config.clean_config_for_language(Language::Rust);
1589 assert_eq!(
1590 clean.precondition.as_deref(),
1591 Some("test -d target"),
1592 "clean precondition should be preserved"
1593 );
1594 assert_eq!(
1595 clean.before.unwrap().commands(),
1596 vec!["echo cleaning"],
1597 "clean before should be preserved"
1598 );
1599 }
1600
1601 #[test]
1602 fn default_clean_config_precondition_matches_toolchain_use() {
1603 let config = minimal_config();
1604 let rust = config.clean_config_for_language(Language::Rust);
1606 assert_eq!(rust.precondition.as_deref(), Some("command -v cargo >/dev/null 2>&1"));
1607 assert!(rust.before.is_none(), "default clean config should have no before");
1608
1609 let py = config.clean_config_for_language(Language::Python);
1611 assert!(
1612 py.precondition.is_none(),
1613 "pure-shell clean should not have a precondition"
1614 );
1615 }
1616
1617 #[test]
1618 fn explicit_build_command_config_preserves_precondition_and_before() {
1619 let config: AlefConfig = toml::from_str(
1620 r#"
1621languages = ["go"]
1622
1623[crate]
1624name = "test"
1625sources = ["src/lib.rs"]
1626
1627[build_commands.go]
1628precondition = "which go"
1629before = "cargo build --release -p test-ffi"
1630build = "cd packages/go && go build ./..."
1631build_release = "cd packages/go && go build -ldflags='-s -w' ./..."
1632"#,
1633 )
1634 .unwrap();
1635
1636 let build = config.build_command_config_for_language(Language::Go);
1637 assert_eq!(
1638 build.precondition.as_deref(),
1639 Some("which go"),
1640 "build precondition should be preserved"
1641 );
1642 assert_eq!(
1643 build.before.unwrap().commands(),
1644 vec!["cargo build --release -p test-ffi"],
1645 "build before should be preserved"
1646 );
1647 }
1648
1649 #[test]
1650 fn default_build_command_config_has_command_v_precondition() {
1651 let config = minimal_config();
1652 let rust = config.build_command_config_for_language(Language::Rust);
1653 assert_eq!(rust.precondition.as_deref(), Some("command -v cargo >/dev/null 2>&1"));
1654 assert!(
1655 rust.before.is_none(),
1656 "default build command config should have no before"
1657 );
1658 }
1659
1660 #[test]
1661 fn version_defaults_to_none_when_omitted() {
1662 let config = minimal_config();
1663 assert!(config.version.is_none());
1664 }
1665
1666 #[test]
1667 fn version_parses_from_top_level_key() {
1668 let config: AlefConfig = toml::from_str(
1669 r#"
1670version = "0.7.7"
1671languages = ["python"]
1672
1673[crate]
1674name = "test-lib"
1675sources = ["src/lib.rs"]
1676"#,
1677 )
1678 .unwrap();
1679 assert_eq!(config.version.as_deref(), Some("0.7.7"));
1680 }
1681}