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, DartConfig, DartStyle,
30 ElixirConfig, FfiConfig, GleamConfig, GoConfig, JavaConfig, KotlinConfig, KotlinTarget, NodeConfig, PhpConfig,
31 PythonConfig, RConfig, RubyConfig, StubsConfig, SwiftConfig, WasmConfig, ZigConfig,
32};
33pub use output::{
34 BuildCommandConfig, CleanConfig, ExcludeConfig, IncludeConfig, LintConfig, OutputConfig, ReadmeConfig,
35 ScaffoldConfig, SetupConfig, SyncConfig, TestConfig, TextReplacement, UpdateConfig,
36};
37pub use publish::{PublishConfig, PublishLanguageConfig, VendorMode};
38pub use tools::{DEFAULT_RUST_DEV_TOOLS, LangContext, ToolsConfig, require_tool, require_tools};
39pub use trait_bridge::TraitBridgeConfig;
40
41#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct AlefConfig {
44 #[serde(default)]
47 pub version: Option<String>,
48 #[serde(rename = "crate")]
49 pub crate_config: CrateConfig,
50 pub languages: Vec<Language>,
51 #[serde(default)]
52 pub exclude: ExcludeConfig,
53 #[serde(default)]
54 pub include: IncludeConfig,
55 #[serde(default)]
56 pub output: OutputConfig,
57 #[serde(default)]
58 pub python: Option<PythonConfig>,
59 #[serde(default)]
60 pub node: Option<NodeConfig>,
61 #[serde(default)]
62 pub ruby: Option<RubyConfig>,
63 #[serde(default)]
64 pub php: Option<PhpConfig>,
65 #[serde(default)]
66 pub elixir: Option<ElixirConfig>,
67 #[serde(default)]
68 pub wasm: Option<WasmConfig>,
69 #[serde(default)]
70 pub ffi: Option<FfiConfig>,
71 #[serde(default)]
72 pub gleam: Option<GleamConfig>,
73 #[serde(default)]
74 pub go: Option<GoConfig>,
75 #[serde(default)]
76 pub java: Option<JavaConfig>,
77 #[serde(default)]
78 pub dart: Option<DartConfig>,
79 #[serde(default)]
80 pub kotlin: Option<KotlinConfig>,
81 #[serde(default)]
82 pub swift: Option<SwiftConfig>,
83 #[serde(default)]
84 pub csharp: Option<CSharpConfig>,
85 #[serde(default)]
86 pub r: Option<RConfig>,
87 #[serde(default)]
88 pub zig: Option<ZigConfig>,
89 #[serde(default)]
90 pub scaffold: Option<ScaffoldConfig>,
91 #[serde(default)]
92 pub readme: Option<ReadmeConfig>,
93 #[serde(default)]
94 pub lint: Option<HashMap<String, LintConfig>>,
95 #[serde(default)]
96 pub update: Option<HashMap<String, UpdateConfig>>,
97 #[serde(default)]
98 pub test: Option<HashMap<String, TestConfig>>,
99 #[serde(default)]
100 pub setup: Option<HashMap<String, SetupConfig>>,
101 #[serde(default)]
102 pub clean: Option<HashMap<String, CleanConfig>>,
103 #[serde(default)]
104 pub build_commands: Option<HashMap<String, BuildCommandConfig>>,
105 #[serde(default)]
107 pub publish: Option<PublishConfig>,
108 #[serde(default)]
109 pub custom_files: Option<HashMap<String, Vec<PathBuf>>>,
110 #[serde(default)]
111 pub adapters: Vec<AdapterConfig>,
112 #[serde(default)]
113 pub custom_modules: CustomModulesConfig,
114 #[serde(default)]
115 pub custom_registrations: CustomRegistrationsConfig,
116 #[serde(default)]
117 pub sync: Option<SyncConfig>,
118 #[serde(default)]
122 pub opaque_types: HashMap<String, String>,
123 #[serde(default)]
125 pub generate: GenerateConfig,
126 #[serde(default)]
128 pub generate_overrides: HashMap<String, GenerateConfig>,
129 #[serde(default)]
131 pub format: FormatConfig,
132 #[serde(default)]
134 pub format_overrides: HashMap<String, FormatConfig>,
135 #[serde(default)]
137 pub dto: DtoConfig,
138 #[serde(default)]
140 pub e2e: Option<E2eConfig>,
141 #[serde(default)]
144 pub trait_bridges: Vec<TraitBridgeConfig>,
145 #[serde(default)]
149 pub tools: ToolsConfig,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
153pub struct CrateConfig {
154 pub name: String,
155 pub sources: Vec<PathBuf>,
156 #[serde(default = "default_version_from")]
157 pub version_from: String,
158 #[serde(default)]
159 pub core_import: Option<String>,
160 #[serde(default)]
162 pub workspace_root: Option<PathBuf>,
163 #[serde(default)]
165 pub skip_core_import: bool,
166 #[serde(default)]
170 pub error_type: Option<String>,
171 #[serde(default)]
176 pub error_constructor: Option<String>,
177 #[serde(default)]
181 pub features: Vec<String>,
182 #[serde(default)]
185 pub path_mappings: HashMap<String, String>,
186 #[serde(default)]
190 pub extra_dependencies: HashMap<String, toml::Value>,
191 #[serde(default = "default_true")]
195 pub auto_path_mappings: bool,
196 #[serde(default)]
201 pub source_crates: Vec<SourceCrate>,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize)]
206pub struct SourceCrate {
207 pub name: String,
209 pub sources: Vec<PathBuf>,
211}
212
213fn default_version_from() -> String {
214 "Cargo.toml".to_string()
215}
216
217fn default_true() -> bool {
218 true
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct GenerateConfig {
226 #[serde(default = "default_true")]
228 pub bindings: bool,
229 #[serde(default = "default_true")]
231 pub errors: bool,
232 #[serde(default = "default_true")]
234 pub configs: bool,
235 #[serde(default = "default_true")]
237 pub async_wrappers: bool,
238 #[serde(default = "default_true")]
240 pub type_conversions: bool,
241 #[serde(default = "default_true")]
243 pub package_metadata: bool,
244 #[serde(default = "default_true")]
246 pub public_api: bool,
247 #[serde(default = "default_true")]
250 pub reverse_conversions: bool,
251}
252
253impl Default for GenerateConfig {
254 fn default() -> Self {
255 Self {
256 bindings: true,
257 errors: true,
258 configs: true,
259 async_wrappers: true,
260 type_conversions: true,
261 package_metadata: true,
262 public_api: true,
263 reverse_conversions: true,
264 }
265 }
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
272pub struct FormatConfig {
273 #[serde(default = "default_true")]
277 pub enabled: bool,
278 #[serde(default)]
282 pub command: Option<String>,
283}
284
285impl Default for FormatConfig {
286 fn default() -> Self {
287 Self {
288 enabled: true,
289 command: None,
290 }
291 }
292}
293
294impl AlefConfig {
299 pub fn resolve_field_name(&self, lang: extras::Language, type_name: &str, field_name: &str) -> Option<String> {
311 let explicit_key = format!("{type_name}.{field_name}");
313 let explicit = match lang {
314 extras::Language::Python => self.python.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
315 extras::Language::Node => self.node.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
316 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
317 extras::Language::Php => self.php.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
318 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
319 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
320 extras::Language::Ffi => self.ffi.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
321 extras::Language::Gleam => self.gleam.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
322 extras::Language::Go => self.go.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
323 extras::Language::Java => self.java.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
324 extras::Language::Kotlin => self.kotlin.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
325 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
326 extras::Language::R => self.r.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
327 extras::Language::Zig => self.zig.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
328 extras::Language::Dart => self.dart.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
329 extras::Language::Swift => self.swift.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
330 extras::Language::Rust => None,
331 };
332 if let Some(renamed) = explicit {
333 if renamed != field_name {
334 return Some(renamed.clone());
335 }
336 return None;
337 }
338
339 match lang {
341 extras::Language::Python => crate::keywords::python_safe_name(field_name),
342 _ => None,
347 }
348 }
349
350 pub fn features_for_language(&self, lang: extras::Language) -> &[String] {
353 let override_features = match lang {
354 extras::Language::Python => self.python.as_ref().and_then(|c| c.features.as_deref()),
355 extras::Language::Node => self.node.as_ref().and_then(|c| c.features.as_deref()),
356 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.features.as_deref()),
357 extras::Language::Php => self.php.as_ref().and_then(|c| c.features.as_deref()),
358 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.features.as_deref()),
359 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.features.as_deref()),
360 extras::Language::Ffi => self.ffi.as_ref().and_then(|c| c.features.as_deref()),
361 extras::Language::Gleam => self.gleam.as_ref().and_then(|c| c.features.as_deref()),
362 extras::Language::Go => self.go.as_ref().and_then(|c| c.features.as_deref()),
363 extras::Language::Java => self.java.as_ref().and_then(|c| c.features.as_deref()),
364 extras::Language::Kotlin => self.kotlin.as_ref().and_then(|c| c.features.as_deref()),
365 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.features.as_deref()),
366 extras::Language::R => self.r.as_ref().and_then(|c| c.features.as_deref()),
367 extras::Language::Zig => self.zig.as_ref().and_then(|c| c.features.as_deref()),
368 extras::Language::Dart => self.dart.as_ref().and_then(|c| c.features.as_deref()),
369 extras::Language::Swift => self.swift.as_ref().and_then(|c| c.features.as_deref()),
370 extras::Language::Rust => None, };
372 override_features.unwrap_or(&self.crate_config.features)
373 }
374
375 pub fn extra_deps_for_language(&self, lang: extras::Language) -> HashMap<String, toml::Value> {
379 let mut deps = self.crate_config.extra_dependencies.clone();
380 let lang_deps = match lang {
381 extras::Language::Python => self.python.as_ref().map(|c| &c.extra_dependencies),
382 extras::Language::Node => self.node.as_ref().map(|c| &c.extra_dependencies),
383 extras::Language::Ruby => self.ruby.as_ref().map(|c| &c.extra_dependencies),
384 extras::Language::Php => self.php.as_ref().map(|c| &c.extra_dependencies),
385 extras::Language::Elixir => self.elixir.as_ref().map(|c| &c.extra_dependencies),
386 extras::Language::Wasm => self.wasm.as_ref().map(|c| &c.extra_dependencies),
387 _ => None,
388 };
389 if let Some(lang_deps) = lang_deps {
390 deps.extend(lang_deps.iter().map(|(k, v)| (k.clone(), v.clone())));
391 }
392 deps
393 }
394
395 pub fn package_dir(&self, lang: extras::Language) -> String {
400 let override_path = match lang {
401 extras::Language::Python => self.python.as_ref().and_then(|c| c.scaffold_output.as_ref()),
402 extras::Language::Node => self.node.as_ref().and_then(|c| c.scaffold_output.as_ref()),
403 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.scaffold_output.as_ref()),
404 extras::Language::Php => self.php.as_ref().and_then(|c| c.scaffold_output.as_ref()),
405 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.scaffold_output.as_ref()),
406 _ => None,
407 };
408 if let Some(p) = override_path {
409 p.to_string_lossy().to_string()
410 } else {
411 match lang {
412 extras::Language::Python => "packages/python".to_string(),
413 extras::Language::Node => "packages/node".to_string(),
414 extras::Language::Ruby => "packages/ruby".to_string(),
415 extras::Language::Php => "packages/php".to_string(),
416 extras::Language::Elixir => "packages/elixir".to_string(),
417 _ => format!("packages/{lang}"),
418 }
419 }
420 }
421
422 pub fn validate(&self) -> Result<(), crate::error::AlefError> {
429 validation::validate(self)
430 }
431
432 pub fn lint_config_for_language(&self, lang: extras::Language) -> output::LintConfig {
437 if let Some(lint_map) = &self.lint {
438 let lang_str = lang.to_string();
439 if let Some(explicit) = lint_map.get(&lang_str) {
440 return explicit.clone();
441 }
442 }
443 let output_dir = self.package_dir(lang);
444 let run_wrapper = self.run_wrapper_for_language(lang);
445 let extra_lint_paths = self.extra_lint_paths_for_language(lang);
446 let project_file = self.project_file_for_language(lang);
447 let ctx = LangContext {
448 tools: &self.tools,
449 run_wrapper,
450 extra_lint_paths,
451 project_file,
452 };
453 lint_defaults::default_lint_config(lang, &output_dir, &ctx)
454 }
455
456 pub fn update_config_for_language(&self, lang: extras::Language) -> output::UpdateConfig {
461 if let Some(update_map) = &self.update {
462 let lang_str = lang.to_string();
463 if let Some(explicit) = update_map.get(&lang_str) {
464 return explicit.clone();
465 }
466 }
467 let output_dir = self.package_dir(lang);
468 let ctx = LangContext {
469 tools: &self.tools,
470 run_wrapper: None,
471 extra_lint_paths: &[],
472 project_file: None,
473 };
474 update_defaults::default_update_config(lang, &output_dir, &ctx)
475 }
476
477 pub fn test_config_for_language(&self, lang: extras::Language) -> output::TestConfig {
482 if let Some(test_map) = &self.test {
483 let lang_str = lang.to_string();
484 if let Some(explicit) = test_map.get(&lang_str) {
485 return explicit.clone();
486 }
487 }
488 let output_dir = self.package_dir(lang);
489 let run_wrapper = self.run_wrapper_for_language(lang);
490 let project_file = self.project_file_for_language(lang);
491 let ctx = LangContext {
492 tools: &self.tools,
493 run_wrapper,
494 extra_lint_paths: &[],
495 project_file,
496 };
497 test_defaults::default_test_config(lang, &output_dir, &ctx)
498 }
499
500 pub fn setup_config_for_language(&self, lang: extras::Language) -> output::SetupConfig {
505 if let Some(setup_map) = &self.setup {
506 let lang_str = lang.to_string();
507 if let Some(explicit) = setup_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 setup_defaults::default_setup_config(lang, &output_dir, &ctx)
519 }
520
521 pub fn clean_config_for_language(&self, lang: extras::Language) -> output::CleanConfig {
526 if let Some(clean_map) = &self.clean {
527 let lang_str = lang.to_string();
528 if let Some(explicit) = clean_map.get(&lang_str) {
529 return explicit.clone();
530 }
531 }
532 let output_dir = self.package_dir(lang);
533 let ctx = LangContext {
534 tools: &self.tools,
535 run_wrapper: None,
536 extra_lint_paths: &[],
537 project_file: None,
538 };
539 clean_defaults::default_clean_config(lang, &output_dir, &ctx)
540 }
541
542 pub fn build_command_config_for_language(&self, lang: extras::Language) -> output::BuildCommandConfig {
547 if let Some(build_map) = &self.build_commands {
548 let lang_str = lang.to_string();
549 if let Some(explicit) = build_map.get(&lang_str) {
550 return explicit.clone();
551 }
552 }
553 let output_dir = self.package_dir(lang);
554 let crate_name = &self.crate_config.name;
555 let run_wrapper = self.run_wrapper_for_language(lang);
556 let project_file = self.project_file_for_language(lang);
557 let ctx = LangContext {
558 tools: &self.tools,
559 run_wrapper,
560 extra_lint_paths: &[],
561 project_file,
562 };
563 build_defaults::default_build_config(lang, &output_dir, crate_name, &ctx)
564 }
565
566 pub fn core_import(&self) -> String {
568 self.crate_config
569 .core_import
570 .clone()
571 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
572 }
573
574 pub fn error_type(&self) -> String {
576 self.crate_config
577 .error_type
578 .clone()
579 .unwrap_or_else(|| "Error".to_string())
580 }
581
582 pub fn error_constructor(&self) -> String {
585 self.crate_config
586 .error_constructor
587 .clone()
588 .unwrap_or_else(|| format!("{}::{}::from({{msg}})", self.core_import(), self.error_type()))
589 }
590
591 pub fn run_wrapper_for_language(&self, lang: extras::Language) -> Option<&str> {
594 match lang {
595 extras::Language::Python => self.python.as_ref().and_then(|c| c.run_wrapper.as_deref()),
596 extras::Language::Node => self.node.as_ref().and_then(|c| c.run_wrapper.as_deref()),
597 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.run_wrapper.as_deref()),
598 extras::Language::Php => self.php.as_ref().and_then(|c| c.run_wrapper.as_deref()),
599 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.run_wrapper.as_deref()),
600 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.run_wrapper.as_deref()),
601 extras::Language::Go => self.go.as_ref().and_then(|c| c.run_wrapper.as_deref()),
602 extras::Language::Java => self.java.as_ref().and_then(|c| c.run_wrapper.as_deref()),
603 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.run_wrapper.as_deref()),
604 extras::Language::R => self.r.as_ref().and_then(|c| c.run_wrapper.as_deref()),
605 extras::Language::Kotlin => self.kotlin.as_ref().and_then(|c| c.run_wrapper.as_deref()),
606 extras::Language::Dart => self.dart.as_ref().and_then(|c| c.run_wrapper.as_deref()),
607 extras::Language::Swift => self.swift.as_ref().and_then(|c| c.run_wrapper.as_deref()),
608 extras::Language::Gleam => self.gleam.as_ref().and_then(|c| c.run_wrapper.as_deref()),
609 extras::Language::Zig => self.zig.as_ref().and_then(|c| c.run_wrapper.as_deref()),
610 extras::Language::Ffi | extras::Language::Rust => None,
611 }
612 }
613
614 pub fn extra_lint_paths_for_language(&self, lang: extras::Language) -> &[String] {
617 match lang {
618 extras::Language::Python => self
619 .python
620 .as_ref()
621 .map(|c| c.extra_lint_paths.as_slice())
622 .unwrap_or(&[]),
623 extras::Language::Node => self.node.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
624 extras::Language::Ruby => self.ruby.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
625 extras::Language::Php => self.php.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
626 extras::Language::Elixir => self
627 .elixir
628 .as_ref()
629 .map(|c| c.extra_lint_paths.as_slice())
630 .unwrap_or(&[]),
631 extras::Language::Wasm => self.wasm.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
632 extras::Language::Go => self.go.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
633 extras::Language::Java => self.java.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
634 extras::Language::Csharp => self
635 .csharp
636 .as_ref()
637 .map(|c| c.extra_lint_paths.as_slice())
638 .unwrap_or(&[]),
639 extras::Language::R => self.r.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
640 extras::Language::Kotlin => self
641 .kotlin
642 .as_ref()
643 .map(|c| c.extra_lint_paths.as_slice())
644 .unwrap_or(&[]),
645 extras::Language::Dart => self.dart.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
646 extras::Language::Swift => self
647 .swift
648 .as_ref()
649 .map(|c| c.extra_lint_paths.as_slice())
650 .unwrap_or(&[]),
651 extras::Language::Gleam => self
652 .gleam
653 .as_ref()
654 .map(|c| c.extra_lint_paths.as_slice())
655 .unwrap_or(&[]),
656 extras::Language::Zig => self.zig.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
657 extras::Language::Ffi | extras::Language::Rust => &[],
658 }
659 }
660
661 pub fn project_file_for_language(&self, lang: extras::Language) -> Option<&str> {
664 match lang {
665 extras::Language::Java => self.java.as_ref().and_then(|c| c.project_file.as_deref()),
666 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.project_file.as_deref()),
667 _ => None,
668 }
669 }
670
671 pub fn ffi_prefix(&self) -> String {
673 self.ffi
674 .as_ref()
675 .and_then(|f| f.prefix.as_ref())
676 .cloned()
677 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
678 }
679
680 pub fn ffi_lib_name(&self) -> String {
688 if let Some(name) = self.ffi.as_ref().and_then(|f| f.lib_name.as_ref()) {
690 return name.clone();
691 }
692
693 if let Some(ffi_path) = self.output.ffi.as_ref() {
696 let path = std::path::Path::new(ffi_path);
697 let components: Vec<_> = path
700 .components()
701 .filter_map(|c| {
702 if let std::path::Component::Normal(s) = c {
703 s.to_str()
704 } else {
705 None
706 }
707 })
708 .collect();
709 let crate_dir = components
712 .iter()
713 .rev()
714 .find(|&&s| s != "src" && s != "lib" && s != "include")
715 .copied();
716 if let Some(dir) = crate_dir {
717 return dir.replace('-', "_");
718 }
719 }
720
721 format!("{}_ffi", self.ffi_prefix())
723 }
724
725 pub fn ffi_header_name(&self) -> String {
727 self.ffi
728 .as_ref()
729 .and_then(|f| f.header_name.as_ref())
730 .cloned()
731 .unwrap_or_else(|| format!("{}.h", self.ffi_prefix()))
732 }
733
734 pub fn dart_style(&self) -> languages::DartStyle {
736 self.dart.as_ref().map(|d| d.style).unwrap_or_default()
737 }
738
739 pub fn python_module_name(&self) -> String {
741 self.python
742 .as_ref()
743 .and_then(|p| p.module_name.as_ref())
744 .cloned()
745 .unwrap_or_else(|| format!("_{}", self.crate_config.name.replace('-', "_")))
746 }
747
748 pub fn python_pip_name(&self) -> String {
752 self.python
753 .as_ref()
754 .and_then(|p| p.pip_name.as_ref())
755 .cloned()
756 .unwrap_or_else(|| self.crate_config.name.clone())
757 }
758
759 pub fn php_autoload_namespace(&self) -> String {
764 use heck::ToPascalCase;
765 let ext = self.php_extension_name();
766 if ext.contains('_') {
767 ext.split('_')
768 .map(|p| p.to_pascal_case())
769 .collect::<Vec<_>>()
770 .join("\\")
771 } else {
772 ext.to_pascal_case()
773 }
774 }
775
776 pub fn node_package_name(&self) -> String {
778 self.node
779 .as_ref()
780 .and_then(|n| n.package_name.as_ref())
781 .cloned()
782 .unwrap_or_else(|| self.crate_config.name.clone())
783 }
784
785 pub fn ruby_gem_name(&self) -> String {
787 self.ruby
788 .as_ref()
789 .and_then(|r| r.gem_name.as_ref())
790 .cloned()
791 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
792 }
793
794 pub fn php_extension_name(&self) -> String {
796 self.php
797 .as_ref()
798 .and_then(|p| p.extension_name.as_ref())
799 .cloned()
800 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
801 }
802
803 pub fn elixir_app_name(&self) -> String {
805 self.elixir
806 .as_ref()
807 .and_then(|e| e.app_name.as_ref())
808 .cloned()
809 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
810 }
811
812 pub fn go_module(&self) -> String {
814 self.go
815 .as_ref()
816 .and_then(|g| g.module.as_ref())
817 .cloned()
818 .unwrap_or_else(|| format!("github.com/kreuzberg-dev/{}", self.crate_config.name))
819 }
820
821 pub fn github_repo(&self) -> String {
828 if let Some(e2e) = &self.e2e {
829 if let Some(url) = &e2e.registry.github_repo {
830 return url.clone();
831 }
832 }
833 self.scaffold
834 .as_ref()
835 .and_then(|s| s.repository.as_ref())
836 .cloned()
837 .unwrap_or_else(|| format!("https://github.com/kreuzberg-dev/{}", self.crate_config.name))
838 }
839
840 pub fn java_package(&self) -> String {
842 self.java
843 .as_ref()
844 .and_then(|j| j.package.as_ref())
845 .cloned()
846 .unwrap_or_else(|| "dev.kreuzberg".to_string())
847 }
848
849 pub fn java_group_id(&self) -> String {
854 self.java_package()
855 }
856
857 pub fn kotlin_package(&self) -> String {
859 self.kotlin
860 .as_ref()
861 .and_then(|k| k.package.as_ref())
862 .cloned()
863 .unwrap_or_else(|| "dev.kreuzberg".to_string())
864 }
865
866 pub fn kotlin_target(&self) -> KotlinTarget {
871 self.kotlin.as_ref().map(|k| k.target).unwrap_or_default()
872 }
873
874 pub fn dart_pubspec_name(&self) -> String {
879 self.dart
880 .as_ref()
881 .and_then(|d| d.pubspec_name.as_ref())
882 .cloned()
883 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
884 }
885
886 pub fn dart_frb_version(&self) -> String {
889 self.dart
890 .as_ref()
891 .and_then(|d| d.frb_version.as_ref())
892 .cloned()
893 .unwrap_or_else(|| crate::template_versions::cargo::FLUTTER_RUST_BRIDGE.to_string())
894 }
895
896 pub fn swift_module(&self) -> String {
901 self.swift
902 .as_ref()
903 .and_then(|s| s.module_name.as_ref())
904 .cloned()
905 .unwrap_or_else(|| {
906 use heck::ToUpperCamelCase;
907 self.crate_config.name.to_upper_camel_case()
908 })
909 }
910
911 pub fn swift_bridge_version(&self) -> String {
914 self.swift
915 .as_ref()
916 .and_then(|s| s.swift_bridge_version.as_ref())
917 .cloned()
918 .unwrap_or_else(|| crate::template_versions::cargo::SWIFT_BRIDGE.to_string())
919 }
920
921 pub fn swift_min_macos(&self) -> String {
923 self.swift
924 .as_ref()
925 .and_then(|s| s.min_macos_version.as_ref())
926 .cloned()
927 .unwrap_or_else(|| crate::template_versions::toolchain::SWIFT_MIN_MACOS.to_string())
928 }
929
930 pub fn swift_min_ios(&self) -> String {
932 self.swift
933 .as_ref()
934 .and_then(|s| s.min_ios_version.as_ref())
935 .cloned()
936 .unwrap_or_else(|| crate::template_versions::toolchain::SWIFT_MIN_IOS.to_string())
937 }
938
939 pub fn gleam_app_name(&self) -> String {
941 self.gleam
942 .as_ref()
943 .and_then(|g| g.app_name.as_ref())
944 .cloned()
945 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
946 }
947
948 pub fn gleam_nif_module(&self) -> String {
952 use heck::ToUpperCamelCase;
953 self.gleam
954 .as_ref()
955 .and_then(|g| g.nif_module.as_ref())
956 .cloned()
957 .unwrap_or_else(|| {
958 let pascal = self
959 .elixir
960 .as_ref()
961 .and_then(|e| e.app_name.as_deref())
962 .unwrap_or(&self.crate_config.name)
963 .to_upper_camel_case();
964 format!("Elixir.{pascal}.Native")
965 })
966 }
967
968 pub fn zig_module_name(&self) -> String {
970 self.zig
971 .as_ref()
972 .and_then(|z| z.module_name.as_ref())
973 .cloned()
974 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
975 }
976
977 pub fn csharp_namespace(&self) -> String {
979 self.csharp
980 .as_ref()
981 .and_then(|c| c.namespace.as_ref())
982 .cloned()
983 .unwrap_or_else(|| {
984 use heck::ToPascalCase;
985 self.crate_config.name.to_pascal_case()
986 })
987 }
988
989 pub fn core_crate_dir(&self) -> String {
995 if let Some(first_source) = self.crate_config.sources.first() {
998 let path = std::path::Path::new(first_source);
999 let mut current = path.parent();
1000 while let Some(dir) = current {
1001 if dir.file_name().is_some_and(|n| n == "src") {
1002 if let Some(crate_dir) = dir.parent() {
1003 if let Some(dir_name) = crate_dir.file_name() {
1004 return dir_name.to_string_lossy().into_owned();
1005 }
1006 }
1007 break;
1008 }
1009 current = dir.parent();
1010 }
1011 }
1012 self.crate_config.name.clone()
1013 }
1014
1015 pub fn wasm_type_prefix(&self) -> String {
1018 self.wasm
1019 .as_ref()
1020 .and_then(|w| w.type_prefix.as_ref())
1021 .cloned()
1022 .unwrap_or_else(|| "Wasm".to_string())
1023 }
1024
1025 pub fn node_type_prefix(&self) -> String {
1028 self.node
1029 .as_ref()
1030 .and_then(|n| n.type_prefix.as_ref())
1031 .cloned()
1032 .unwrap_or_else(|| "Js".to_string())
1033 }
1034
1035 pub fn r_package_name(&self) -> String {
1037 self.r
1038 .as_ref()
1039 .and_then(|r| r.package_name.as_ref())
1040 .cloned()
1041 .unwrap_or_else(|| self.crate_config.name.clone())
1042 }
1043
1044 pub fn resolved_version(&self) -> Option<String> {
1047 let content = std::fs::read_to_string(&self.crate_config.version_from).ok()?;
1048 let value: toml::Value = toml::from_str(&content).ok()?;
1049 if let Some(v) = value
1050 .get("workspace")
1051 .and_then(|w| w.get("package"))
1052 .and_then(|p| p.get("version"))
1053 .and_then(|v| v.as_str())
1054 {
1055 return Some(v.to_string());
1056 }
1057 value
1058 .get("package")
1059 .and_then(|p| p.get("version"))
1060 .and_then(|v| v.as_str())
1061 .map(|v| v.to_string())
1062 }
1063
1064 pub fn serde_rename_all_for_language(&self, lang: extras::Language) -> String {
1072 let override_val = match lang {
1074 extras::Language::Python => self.python.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1075 extras::Language::Node => self.node.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1076 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1077 extras::Language::Php => self.php.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1078 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1079 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1080 extras::Language::Ffi => self.ffi.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1081 extras::Language::Gleam => self.gleam.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1082 extras::Language::Go => self.go.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1083 extras::Language::Java => self.java.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1084 extras::Language::Kotlin => self.kotlin.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1085 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1086 extras::Language::R => self.r.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1087 extras::Language::Zig => self.zig.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1088 extras::Language::Dart => self.dart.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1089 extras::Language::Swift => self.swift.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1090 extras::Language::Rust => None, };
1092
1093 if let Some(val) = override_val {
1094 return val.to_string();
1095 }
1096
1097 match lang {
1099 extras::Language::Node | extras::Language::Wasm | extras::Language::Java | extras::Language::Csharp => {
1100 "camelCase".to_string()
1101 }
1102 extras::Language::Python
1103 | extras::Language::Ruby
1104 | extras::Language::Php
1105 | extras::Language::Go
1106 | extras::Language::Ffi
1107 | extras::Language::Elixir
1108 | extras::Language::R
1109 | extras::Language::Rust
1110 | extras::Language::Kotlin
1111 | extras::Language::Gleam
1112 | extras::Language::Zig
1113 | extras::Language::Swift
1114 | extras::Language::Dart => "snake_case".to_string(),
1115 }
1116 }
1117
1118 pub fn rewrite_path(&self, rust_path: &str) -> String {
1121 let mut mappings: Vec<_> = self.crate_config.path_mappings.iter().collect();
1123 mappings.sort_by_key(|b| std::cmp::Reverse(b.0.len()));
1124
1125 for (from, to) in &mappings {
1126 if rust_path.starts_with(from.as_str()) {
1127 return format!("{}{}", to, &rust_path[from.len()..]);
1128 }
1129 }
1130 rust_path.to_string()
1131 }
1132
1133 pub fn effective_path_mappings(&self) -> HashMap<String, String> {
1143 let mut mappings = HashMap::new();
1144
1145 if self.crate_config.auto_path_mappings {
1146 let core_import = self.core_import();
1147
1148 for source in &self.crate_config.sources {
1149 let source_str = source.to_string_lossy();
1150 if let Some(after_crates) = find_after_crates_prefix(&source_str) {
1152 if let Some(slash_pos) = after_crates.find('/') {
1154 let crate_dir = &after_crates[..slash_pos];
1155 let crate_ident = crate_dir.replace('-', "_");
1156 if crate_ident != core_import && !mappings.contains_key(&crate_ident) {
1158 mappings.insert(crate_ident, core_import.clone());
1159 }
1160 }
1161 }
1162 }
1163 }
1164
1165 for (from, to) in &self.crate_config.path_mappings {
1167 mappings.insert(from.clone(), to.clone());
1168 }
1169
1170 mappings
1171 }
1172}
1173
1174fn find_after_crates_prefix(path: &str) -> Option<&str> {
1181 if let Some(pos) = path.find("/crates/") {
1185 return Some(&path[pos + "/crates/".len()..]);
1186 }
1187 if let Some(stripped) = path.strip_prefix("crates/") {
1188 return Some(stripped);
1189 }
1190 None
1191}
1192
1193pub fn resolve_output_dir(config_path: Option<&PathBuf>, crate_name: &str, default: &str) -> String {
1196 config_path
1197 .map(|p| p.to_string_lossy().replace("{name}", crate_name))
1198 .unwrap_or_else(|| default.replace("{name}", crate_name))
1199}
1200
1201pub fn detect_serde_available(output_dir: &str) -> bool {
1207 let src_path = std::path::Path::new(output_dir);
1208 let mut dir = src_path;
1210 loop {
1211 let cargo_toml = dir.join("Cargo.toml");
1212 if cargo_toml.exists() {
1213 return cargo_toml_has_serde(&cargo_toml);
1214 }
1215 match dir.parent() {
1216 Some(parent) if !parent.as_os_str().is_empty() => dir = parent,
1217 _ => break,
1218 }
1219 }
1220 false
1221}
1222
1223fn cargo_toml_has_serde(path: &std::path::Path) -> bool {
1229 let content = match std::fs::read_to_string(path) {
1230 Ok(c) => c,
1231 Err(_) => return false,
1232 };
1233
1234 let has_serde_json = content.contains("serde_json");
1235 let has_serde_dep = content.lines().any(|line| {
1239 let trimmed = line.trim();
1240 trimmed.starts_with("serde ")
1242 || trimmed.starts_with("serde=")
1243 || trimmed.starts_with("serde.")
1244 || trimmed == "[dependencies.serde]"
1245 });
1246
1247 has_serde_json && has_serde_dep
1248}
1249
1250#[cfg(test)]
1251mod tests {
1252 use super::*;
1253
1254 fn minimal_config() -> AlefConfig {
1255 toml::from_str(
1256 r#"
1257languages = ["python", "node", "rust"]
1258
1259[crate]
1260name = "test-lib"
1261sources = ["src/lib.rs"]
1262"#,
1263 )
1264 .unwrap()
1265 }
1266
1267 #[test]
1268 fn lint_config_falls_back_to_defaults() {
1269 let config = minimal_config();
1270 assert!(config.lint.is_none());
1271
1272 let py = config.lint_config_for_language(Language::Python);
1273 assert!(py.format.is_some());
1274 assert!(py.check.is_some());
1275 assert!(py.typecheck.is_some());
1276
1277 let node = config.lint_config_for_language(Language::Node);
1278 assert!(node.format.is_some());
1279 assert!(node.check.is_some());
1280 }
1281
1282 #[test]
1283 fn lint_config_explicit_overrides_default() {
1284 let config: AlefConfig = toml::from_str(
1285 r#"
1286languages = ["python"]
1287
1288[crate]
1289name = "test-lib"
1290sources = ["src/lib.rs"]
1291
1292[lint.python]
1293format = "custom-formatter"
1294check = "custom-checker"
1295"#,
1296 )
1297 .unwrap();
1298
1299 let py = config.lint_config_for_language(Language::Python);
1300 assert_eq!(py.format.unwrap().commands(), vec!["custom-formatter"]);
1301 assert_eq!(py.check.unwrap().commands(), vec!["custom-checker"]);
1302 assert!(py.typecheck.is_none()); }
1304
1305 #[test]
1306 fn lint_config_partial_override_does_not_merge() {
1307 let config: AlefConfig = toml::from_str(
1308 r#"
1309languages = ["python"]
1310
1311[crate]
1312name = "test-lib"
1313sources = ["src/lib.rs"]
1314
1315[lint.python]
1316format = "only-format"
1317"#,
1318 )
1319 .unwrap();
1320
1321 let py = config.lint_config_for_language(Language::Python);
1322 assert_eq!(py.format.unwrap().commands(), vec!["only-format"]);
1323 assert!(py.check.is_none());
1325 assert!(py.typecheck.is_none());
1326 }
1327
1328 #[test]
1329 fn lint_config_unconfigured_language_uses_defaults() {
1330 let config: AlefConfig = toml::from_str(
1331 r#"
1332languages = ["python", "node"]
1333
1334[crate]
1335name = "test-lib"
1336sources = ["src/lib.rs"]
1337
1338[lint.python]
1339format = "custom"
1340"#,
1341 )
1342 .unwrap();
1343
1344 let py = config.lint_config_for_language(Language::Python);
1346 assert_eq!(py.format.unwrap().commands(), vec!["custom"]);
1347
1348 let node = config.lint_config_for_language(Language::Node);
1350 let fmt = node.format.unwrap().commands().join(" ");
1351 assert!(fmt.contains("oxfmt"));
1352 }
1353
1354 #[test]
1355 fn update_config_falls_back_to_defaults() {
1356 let config = minimal_config();
1357 assert!(config.update.is_none());
1358
1359 let py = config.update_config_for_language(Language::Python);
1360 assert!(py.update.is_some());
1361 assert!(py.upgrade.is_some());
1362
1363 let rust = config.update_config_for_language(Language::Rust);
1364 let update = rust.update.unwrap().commands().join(" ");
1365 assert!(update.contains("cargo update"));
1366 }
1367
1368 #[test]
1369 fn update_config_explicit_overrides_default() {
1370 let config: AlefConfig = toml::from_str(
1371 r#"
1372languages = ["rust"]
1373
1374[crate]
1375name = "test-lib"
1376sources = ["src/lib.rs"]
1377
1378[update.rust]
1379update = "my-custom-update"
1380upgrade = ["step1", "step2"]
1381"#,
1382 )
1383 .unwrap();
1384
1385 let rust = config.update_config_for_language(Language::Rust);
1386 assert_eq!(rust.update.unwrap().commands(), vec!["my-custom-update"]);
1387 assert_eq!(rust.upgrade.unwrap().commands(), vec!["step1", "step2"]);
1388 }
1389
1390 #[test]
1391 fn test_config_falls_back_to_defaults() {
1392 let config = minimal_config();
1393 assert!(config.test.is_none());
1394
1395 let py = config.test_config_for_language(Language::Python);
1396 assert!(py.command.is_some());
1397 assert!(py.coverage.is_some());
1398 assert!(py.e2e.is_none());
1399
1400 let rust = config.test_config_for_language(Language::Rust);
1401 let cmd = rust.command.unwrap().commands().join(" ");
1402 assert!(cmd.contains("cargo test"));
1403 }
1404
1405 #[test]
1406 fn test_config_explicit_overrides_default() {
1407 let config: AlefConfig = toml::from_str(
1408 r#"
1409languages = ["python"]
1410
1411[crate]
1412name = "test-lib"
1413sources = ["src/lib.rs"]
1414
1415[test.python]
1416command = "my-custom-test"
1417"#,
1418 )
1419 .unwrap();
1420
1421 let py = config.test_config_for_language(Language::Python);
1422 assert_eq!(py.command.unwrap().commands(), vec!["my-custom-test"]);
1423 assert!(py.coverage.is_none()); }
1425
1426 #[test]
1427 fn setup_config_falls_back_to_defaults() {
1428 let config = minimal_config();
1429 assert!(config.setup.is_none());
1430
1431 let py = config.setup_config_for_language(Language::Python);
1432 assert!(py.install.is_some());
1433 let install = py.install.unwrap().commands().join(" ");
1434 assert!(install.contains("uv sync"));
1435
1436 let rust = config.setup_config_for_language(Language::Rust);
1437 let install = rust.install.unwrap().commands().join(" ");
1438 assert!(install.contains("rustup update"));
1439 }
1440
1441 #[test]
1442 fn setup_config_explicit_overrides_default() {
1443 let config: AlefConfig = toml::from_str(
1444 r#"
1445languages = ["python"]
1446
1447[crate]
1448name = "test-lib"
1449sources = ["src/lib.rs"]
1450
1451[setup.python]
1452install = "my-custom-install"
1453"#,
1454 )
1455 .unwrap();
1456
1457 let py = config.setup_config_for_language(Language::Python);
1458 assert_eq!(py.install.unwrap().commands(), vec!["my-custom-install"]);
1459 }
1460
1461 #[test]
1462 fn clean_config_falls_back_to_defaults() {
1463 let config = minimal_config();
1464 assert!(config.clean.is_none());
1465
1466 let py = config.clean_config_for_language(Language::Python);
1467 assert!(py.clean.is_some());
1468 let clean = py.clean.unwrap().commands().join(" ");
1469 assert!(clean.contains("__pycache__"));
1470
1471 let rust = config.clean_config_for_language(Language::Rust);
1472 let clean = rust.clean.unwrap().commands().join(" ");
1473 assert!(clean.contains("cargo clean"));
1474 }
1475
1476 #[test]
1477 fn clean_config_explicit_overrides_default() {
1478 let config: AlefConfig = toml::from_str(
1479 r#"
1480languages = ["rust"]
1481
1482[crate]
1483name = "test-lib"
1484sources = ["src/lib.rs"]
1485
1486[clean.rust]
1487clean = "my-custom-clean"
1488"#,
1489 )
1490 .unwrap();
1491
1492 let rust = config.clean_config_for_language(Language::Rust);
1493 assert_eq!(rust.clean.unwrap().commands(), vec!["my-custom-clean"]);
1494 }
1495
1496 #[test]
1497 fn build_command_config_falls_back_to_defaults() {
1498 let config = minimal_config();
1499 assert!(config.build_commands.is_none());
1500
1501 let py = config.build_command_config_for_language(Language::Python);
1502 assert!(py.build.is_some());
1503 assert!(py.build_release.is_some());
1504 let build = py.build.unwrap().commands().join(" ");
1505 assert!(build.contains("maturin develop"));
1506
1507 let rust = config.build_command_config_for_language(Language::Rust);
1508 let build = rust.build.unwrap().commands().join(" ");
1509 assert!(build.contains("cargo build --workspace"));
1510 }
1511
1512 #[test]
1513 fn build_command_config_explicit_overrides_default() {
1514 let config: AlefConfig = toml::from_str(
1515 r#"
1516languages = ["rust"]
1517
1518[crate]
1519name = "test-lib"
1520sources = ["src/lib.rs"]
1521
1522[build_commands.rust]
1523build = "my-custom-build"
1524build_release = "my-custom-build --release"
1525"#,
1526 )
1527 .unwrap();
1528
1529 let rust = config.build_command_config_for_language(Language::Rust);
1530 assert_eq!(rust.build.unwrap().commands(), vec!["my-custom-build"]);
1531 assert_eq!(
1532 rust.build_release.unwrap().commands(),
1533 vec!["my-custom-build --release"]
1534 );
1535 }
1536
1537 #[test]
1538 fn build_command_config_uses_crate_name() {
1539 let config = minimal_config();
1540 let py = config.build_command_config_for_language(Language::Python);
1541 let build = py.build.unwrap().commands().join(" ");
1542 assert!(
1543 build.contains("test-lib-py"),
1544 "Python build should reference crate name, got: {build}"
1545 );
1546 }
1547
1548 #[test]
1549 fn package_dir_defaults_are_correct() {
1550 let config = minimal_config();
1551 assert_eq!(config.package_dir(Language::Python), "packages/python");
1552 assert_eq!(config.package_dir(Language::Node), "packages/node");
1553 assert_eq!(config.package_dir(Language::Ruby), "packages/ruby");
1554 assert_eq!(config.package_dir(Language::Go), "packages/go");
1555 assert_eq!(config.package_dir(Language::Java), "packages/java");
1556 }
1557
1558 #[test]
1559 fn explicit_lint_config_preserves_precondition_and_before() {
1560 let config: AlefConfig = toml::from_str(
1561 r#"
1562languages = ["go"]
1563
1564[crate]
1565name = "test"
1566sources = ["src/lib.rs"]
1567
1568[lint.go]
1569precondition = "test -f target/release/libtest_ffi.so"
1570before = "cargo build --release -p test-ffi"
1571format = "gofmt -w packages/go"
1572check = "golangci-lint run ./..."
1573"#,
1574 )
1575 .unwrap();
1576
1577 let lint = config.lint_config_for_language(Language::Go);
1578 assert_eq!(
1579 lint.precondition.as_deref(),
1580 Some("test -f target/release/libtest_ffi.so"),
1581 "precondition should be preserved from explicit config"
1582 );
1583 assert_eq!(
1584 lint.before.unwrap().commands(),
1585 vec!["cargo build --release -p test-ffi"],
1586 "before should be preserved from explicit config"
1587 );
1588 }
1589
1590 #[test]
1591 fn explicit_lint_config_with_before_list_preserves_all_commands() {
1592 let config: AlefConfig = toml::from_str(
1593 r#"
1594languages = ["go"]
1595
1596[crate]
1597name = "test"
1598sources = ["src/lib.rs"]
1599
1600[lint.go]
1601before = ["cargo build --release -p test-ffi", "cp target/release/libtest_ffi.so packages/go/"]
1602check = "golangci-lint run ./..."
1603"#,
1604 )
1605 .unwrap();
1606
1607 let lint = config.lint_config_for_language(Language::Go);
1608 assert!(lint.precondition.is_none(), "precondition should be None when not set");
1609 assert_eq!(
1610 lint.before.unwrap().commands(),
1611 vec![
1612 "cargo build --release -p test-ffi",
1613 "cp target/release/libtest_ffi.so packages/go/"
1614 ],
1615 "before list should be preserved from explicit config"
1616 );
1617 }
1618
1619 #[test]
1620 fn default_lint_config_has_command_v_precondition() {
1621 let config = minimal_config();
1622 let py = config.lint_config_for_language(Language::Python);
1623 assert_eq!(py.precondition.as_deref(), Some("command -v ruff >/dev/null 2>&1"));
1624 assert!(py.before.is_none(), "default lint config should have no before");
1625
1626 let go = config.lint_config_for_language(Language::Go);
1627 assert_eq!(go.precondition.as_deref(), Some("command -v gofmt >/dev/null 2>&1"));
1628 assert!(go.before.is_none(), "default Go lint config should have no before");
1629 }
1630
1631 #[test]
1632 fn explicit_test_config_preserves_precondition_and_before() {
1633 let config: AlefConfig = toml::from_str(
1634 r#"
1635languages = ["python"]
1636
1637[crate]
1638name = "test"
1639sources = ["src/lib.rs"]
1640
1641[test.python]
1642precondition = "test -f target/release/libtest.so"
1643before = "maturin develop"
1644command = "pytest"
1645"#,
1646 )
1647 .unwrap();
1648
1649 let test = config.test_config_for_language(Language::Python);
1650 assert_eq!(
1651 test.precondition.as_deref(),
1652 Some("test -f target/release/libtest.so"),
1653 "test precondition should be preserved"
1654 );
1655 assert_eq!(
1656 test.before.unwrap().commands(),
1657 vec!["maturin develop"],
1658 "test before should be preserved"
1659 );
1660 }
1661
1662 #[test]
1663 fn default_test_config_has_command_v_precondition() {
1664 let config = minimal_config();
1665 let py = config.test_config_for_language(Language::Python);
1666 assert_eq!(py.precondition.as_deref(), Some("command -v uv >/dev/null 2>&1"));
1667 assert!(py.before.is_none(), "default test config should have no before");
1668 }
1669
1670 #[test]
1671 fn explicit_setup_config_preserves_precondition_and_before() {
1672 let config: AlefConfig = toml::from_str(
1673 r#"
1674languages = ["python"]
1675
1676[crate]
1677name = "test"
1678sources = ["src/lib.rs"]
1679
1680[setup.python]
1681precondition = "which uv"
1682before = "pip install uv"
1683install = "uv sync"
1684"#,
1685 )
1686 .unwrap();
1687
1688 let setup = config.setup_config_for_language(Language::Python);
1689 assert_eq!(
1690 setup.precondition.as_deref(),
1691 Some("which uv"),
1692 "setup precondition should be preserved"
1693 );
1694 assert_eq!(
1695 setup.before.unwrap().commands(),
1696 vec!["pip install uv"],
1697 "setup before should be preserved"
1698 );
1699 }
1700
1701 #[test]
1702 fn default_setup_config_has_command_v_precondition() {
1703 let config = minimal_config();
1704 let py = config.setup_config_for_language(Language::Python);
1705 assert_eq!(py.precondition.as_deref(), Some("command -v uv >/dev/null 2>&1"));
1706 assert!(py.before.is_none(), "default setup config should have no before");
1707 }
1708
1709 #[test]
1710 fn explicit_update_config_preserves_precondition_and_before() {
1711 let config: AlefConfig = toml::from_str(
1712 r#"
1713languages = ["rust"]
1714
1715[crate]
1716name = "test"
1717sources = ["src/lib.rs"]
1718
1719[update.rust]
1720precondition = "test -f Cargo.lock"
1721before = "cargo fetch"
1722update = "cargo update"
1723"#,
1724 )
1725 .unwrap();
1726
1727 let update = config.update_config_for_language(Language::Rust);
1728 assert_eq!(
1729 update.precondition.as_deref(),
1730 Some("test -f Cargo.lock"),
1731 "update precondition should be preserved"
1732 );
1733 assert_eq!(
1734 update.before.unwrap().commands(),
1735 vec!["cargo fetch"],
1736 "update before should be preserved"
1737 );
1738 }
1739
1740 #[test]
1741 fn default_update_config_has_command_v_precondition() {
1742 let config = minimal_config();
1743 let rust = config.update_config_for_language(Language::Rust);
1744 assert_eq!(rust.precondition.as_deref(), Some("command -v cargo >/dev/null 2>&1"));
1745 assert!(rust.before.is_none(), "default update config should have no before");
1746 }
1747
1748 #[test]
1749 fn explicit_clean_config_preserves_precondition_and_before() {
1750 let config: AlefConfig = toml::from_str(
1751 r#"
1752languages = ["rust"]
1753
1754[crate]
1755name = "test"
1756sources = ["src/lib.rs"]
1757
1758[clean.rust]
1759precondition = "test -d target"
1760before = "echo cleaning"
1761clean = "cargo clean"
1762"#,
1763 )
1764 .unwrap();
1765
1766 let clean = config.clean_config_for_language(Language::Rust);
1767 assert_eq!(
1768 clean.precondition.as_deref(),
1769 Some("test -d target"),
1770 "clean precondition should be preserved"
1771 );
1772 assert_eq!(
1773 clean.before.unwrap().commands(),
1774 vec!["echo cleaning"],
1775 "clean before should be preserved"
1776 );
1777 }
1778
1779 #[test]
1780 fn default_clean_config_precondition_matches_toolchain_use() {
1781 let config = minimal_config();
1782 let rust = config.clean_config_for_language(Language::Rust);
1784 assert_eq!(rust.precondition.as_deref(), Some("command -v cargo >/dev/null 2>&1"));
1785 assert!(rust.before.is_none(), "default clean config should have no before");
1786
1787 let py = config.clean_config_for_language(Language::Python);
1789 assert!(
1790 py.precondition.is_none(),
1791 "pure-shell clean should not have a precondition"
1792 );
1793 }
1794
1795 #[test]
1796 fn explicit_build_command_config_preserves_precondition_and_before() {
1797 let config: AlefConfig = toml::from_str(
1798 r#"
1799languages = ["go"]
1800
1801[crate]
1802name = "test"
1803sources = ["src/lib.rs"]
1804
1805[build_commands.go]
1806precondition = "which go"
1807before = "cargo build --release -p test-ffi"
1808build = "cd packages/go && go build ./..."
1809build_release = "cd packages/go && go build -ldflags='-s -w' ./..."
1810"#,
1811 )
1812 .unwrap();
1813
1814 let build = config.build_command_config_for_language(Language::Go);
1815 assert_eq!(
1816 build.precondition.as_deref(),
1817 Some("which go"),
1818 "build precondition should be preserved"
1819 );
1820 assert_eq!(
1821 build.before.unwrap().commands(),
1822 vec!["cargo build --release -p test-ffi"],
1823 "build before should be preserved"
1824 );
1825 }
1826
1827 #[test]
1828 fn default_build_command_config_has_command_v_precondition() {
1829 let config = minimal_config();
1830 let rust = config.build_command_config_for_language(Language::Rust);
1831 assert_eq!(rust.precondition.as_deref(), Some("command -v cargo >/dev/null 2>&1"));
1832 assert!(
1833 rust.before.is_none(),
1834 "default build command config should have no before"
1835 );
1836 }
1837
1838 #[test]
1839 fn version_defaults_to_none_when_omitted() {
1840 let config = minimal_config();
1841 assert!(config.version.is_none());
1842 }
1843
1844 #[test]
1845 fn version_parses_from_top_level_key() {
1846 let config: AlefConfig = toml::from_str(
1847 r#"
1848version = "0.7.7"
1849languages = ["python"]
1850
1851[crate]
1852name = "test-lib"
1853sources = ["src/lib.rs"]
1854"#,
1855 )
1856 .unwrap();
1857 assert_eq!(config.version.as_deref(), Some("0.7.7"));
1858 }
1859}