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
221pub fn derive_reverse_dns_package(repo_url: &str) -> Option<String> {
235 let after_scheme = repo_url.split_once("://").map(|(_, rest)| rest).unwrap_or(repo_url);
236 let mut parts = after_scheme.split('/').filter(|s| !s.is_empty());
237 let host = parts.next()?;
238 let org = parts.next()?;
239
240 let host_reversed: Vec<String> = host
241 .split('.')
242 .filter(|s| !s.is_empty())
243 .rev()
244 .map(|s| s.replace('-', "_"))
245 .collect();
246 if host_reversed.is_empty() {
247 return None;
248 }
249
250 let mut pkg = host_reversed.join(".");
251 pkg.push('.');
252 pkg.push_str(&org.replace('-', "_"));
253 Some(pkg)
254}
255
256pub fn derive_go_module_from_repo(repo_url: &str) -> Option<String> {
266 let after_scheme = repo_url.split_once("://").map(|(_, rest)| rest).unwrap_or(repo_url);
267 let trimmed = after_scheme.trim_end_matches('/');
268 let mut parts = trimmed.split('/');
269 let host = parts.next().filter(|s| !s.is_empty())?;
270 let org = parts.next().filter(|s| !s.is_empty())?;
271 let repo_segment = parts.next().filter(|s| !s.is_empty());
272
273 let mut module = format!("{host}/{org}");
274 if let Some(repo) = repo_segment {
275 module.push('/');
276 module.push_str(repo);
277 }
278 Some(module)
279}
280
281pub fn derive_repo_org(repo_url: &str) -> Option<String> {
291 let after_scheme = repo_url.split_once("://").map(|(_, rest)| rest).unwrap_or(repo_url);
292 let mut parts = after_scheme.split('/').filter(|s| !s.is_empty());
293 let _host = parts.next()?;
294 let org = parts.next()?;
295 Some(org.to_string())
296}
297
298#[cfg(test)]
299mod derive_reverse_dns_tests {
300 use super::derive_reverse_dns_package;
301
302 #[test]
303 fn github_org_with_hyphen_underscores_in_package() {
304 assert_eq!(
305 derive_reverse_dns_package("https://github.com/kreuzberg-dev/kreuzberg"),
306 Some("com.github.kreuzberg_dev".to_string())
307 );
308 }
309
310 #[test]
311 fn other_host_reverses_correctly() {
312 assert_eq!(
313 derive_reverse_dns_package("https://gitlab.com/foo/bar"),
314 Some("com.gitlab.foo".to_string())
315 );
316 }
317
318 #[test]
319 fn missing_org_returns_none() {
320 assert_eq!(derive_reverse_dns_package("https://github.com/"), None);
321 assert_eq!(derive_reverse_dns_package("https://github.com"), None);
322 }
323
324 #[test]
325 fn no_scheme_still_parses() {
326 assert_eq!(
327 derive_reverse_dns_package("github.com/foo/bar"),
328 Some("com.github.foo".to_string())
329 );
330 }
331
332 #[test]
333 fn placeholder_url_derives_predictably() {
334 assert_eq!(
335 derive_reverse_dns_package("https://example.invalid/my-lib"),
336 Some("invalid.example.my_lib".to_string())
337 );
338 }
339}
340
341#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct GenerateConfig {
346 #[serde(default = "default_true")]
348 pub bindings: bool,
349 #[serde(default = "default_true")]
351 pub errors: bool,
352 #[serde(default = "default_true")]
354 pub configs: bool,
355 #[serde(default = "default_true")]
357 pub async_wrappers: bool,
358 #[serde(default = "default_true")]
360 pub type_conversions: bool,
361 #[serde(default = "default_true")]
363 pub package_metadata: bool,
364 #[serde(default = "default_true")]
366 pub public_api: bool,
367 #[serde(default = "default_true")]
370 pub reverse_conversions: bool,
371}
372
373impl Default for GenerateConfig {
374 fn default() -> Self {
375 Self {
376 bindings: true,
377 errors: true,
378 configs: true,
379 async_wrappers: true,
380 type_conversions: true,
381 package_metadata: true,
382 public_api: true,
383 reverse_conversions: true,
384 }
385 }
386}
387
388#[derive(Debug, Clone, Serialize, Deserialize)]
392pub struct FormatConfig {
393 #[serde(default = "default_true")]
397 pub enabled: bool,
398 #[serde(default)]
402 pub command: Option<String>,
403}
404
405impl Default for FormatConfig {
406 fn default() -> Self {
407 Self {
408 enabled: true,
409 command: None,
410 }
411 }
412}
413
414impl AlefConfig {
419 pub fn resolve_field_name(&self, lang: extras::Language, type_name: &str, field_name: &str) -> Option<String> {
431 let explicit_key = format!("{type_name}.{field_name}");
433 let explicit = match lang {
434 extras::Language::Python => self.python.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
435 extras::Language::Node => self.node.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
436 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
437 extras::Language::Php => self.php.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
438 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
439 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
440 extras::Language::Ffi => self.ffi.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
441 extras::Language::Gleam => self.gleam.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
442 extras::Language::Go => self.go.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
443 extras::Language::Java => self.java.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
444 extras::Language::Kotlin => self.kotlin.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
445 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
446 extras::Language::R => self.r.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
447 extras::Language::Zig => self.zig.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
448 extras::Language::Dart => self.dart.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
449 extras::Language::Swift => self.swift.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
450 extras::Language::Rust => None,
451 };
452 if let Some(renamed) = explicit {
453 if renamed != field_name {
454 return Some(renamed.clone());
455 }
456 return None;
457 }
458
459 match lang {
461 extras::Language::Python => crate::keywords::python_safe_name(field_name),
462 _ => None,
467 }
468 }
469
470 pub fn features_for_language(&self, lang: extras::Language) -> &[String] {
473 let override_features = match lang {
474 extras::Language::Python => self.python.as_ref().and_then(|c| c.features.as_deref()),
475 extras::Language::Node => self.node.as_ref().and_then(|c| c.features.as_deref()),
476 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.features.as_deref()),
477 extras::Language::Php => self.php.as_ref().and_then(|c| c.features.as_deref()),
478 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.features.as_deref()),
479 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.features.as_deref()),
480 extras::Language::Ffi => self.ffi.as_ref().and_then(|c| c.features.as_deref()),
481 extras::Language::Gleam => self.gleam.as_ref().and_then(|c| c.features.as_deref()),
482 extras::Language::Go => self.go.as_ref().and_then(|c| c.features.as_deref()),
483 extras::Language::Java => self.java.as_ref().and_then(|c| c.features.as_deref()),
484 extras::Language::Kotlin => self.kotlin.as_ref().and_then(|c| c.features.as_deref()),
485 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.features.as_deref()),
486 extras::Language::R => self.r.as_ref().and_then(|c| c.features.as_deref()),
487 extras::Language::Zig => self.zig.as_ref().and_then(|c| c.features.as_deref()),
488 extras::Language::Dart => self.dart.as_ref().and_then(|c| c.features.as_deref()),
489 extras::Language::Swift => self.swift.as_ref().and_then(|c| c.features.as_deref()),
490 extras::Language::Rust => None, };
492 override_features.unwrap_or(&self.crate_config.features)
493 }
494
495 pub fn extra_deps_for_language(&self, lang: extras::Language) -> HashMap<String, toml::Value> {
499 let mut deps = self.crate_config.extra_dependencies.clone();
500 let lang_deps = match lang {
501 extras::Language::Python => self.python.as_ref().map(|c| &c.extra_dependencies),
502 extras::Language::Node => self.node.as_ref().map(|c| &c.extra_dependencies),
503 extras::Language::Ruby => self.ruby.as_ref().map(|c| &c.extra_dependencies),
504 extras::Language::Php => self.php.as_ref().map(|c| &c.extra_dependencies),
505 extras::Language::Elixir => self.elixir.as_ref().map(|c| &c.extra_dependencies),
506 extras::Language::Wasm => self.wasm.as_ref().map(|c| &c.extra_dependencies),
507 _ => None,
508 };
509 if let Some(lang_deps) = lang_deps {
510 deps.extend(lang_deps.iter().map(|(k, v)| (k.clone(), v.clone())));
511 }
512 deps
513 }
514
515 pub fn package_dir(&self, lang: extras::Language) -> String {
520 let override_path = match lang {
521 extras::Language::Python => self.python.as_ref().and_then(|c| c.scaffold_output.as_ref()),
522 extras::Language::Node => self.node.as_ref().and_then(|c| c.scaffold_output.as_ref()),
523 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.scaffold_output.as_ref()),
524 extras::Language::Php => self.php.as_ref().and_then(|c| c.scaffold_output.as_ref()),
525 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.scaffold_output.as_ref()),
526 _ => None,
527 };
528 if let Some(p) = override_path {
529 p.to_string_lossy().to_string()
530 } else {
531 match lang {
532 extras::Language::Python => "packages/python".to_string(),
533 extras::Language::Node => "packages/node".to_string(),
534 extras::Language::Ruby => "packages/ruby".to_string(),
535 extras::Language::Php => "packages/php".to_string(),
536 extras::Language::Elixir => "packages/elixir".to_string(),
537 _ => format!("packages/{lang}"),
538 }
539 }
540 }
541
542 pub fn validate(&self) -> Result<(), crate::error::AlefError> {
549 validation::validate(self)
550 }
551
552 pub fn lint_config_for_language(&self, lang: extras::Language) -> output::LintConfig {
557 if let Some(lint_map) = &self.lint {
558 let lang_str = lang.to_string();
559 if let Some(explicit) = lint_map.get(&lang_str) {
560 return explicit.clone();
561 }
562 }
563 let output_dir = self.package_dir(lang);
564 let run_wrapper = self.run_wrapper_for_language(lang);
565 let extra_lint_paths = self.extra_lint_paths_for_language(lang);
566 let project_file = self.project_file_for_language(lang);
567 let ctx = LangContext {
568 tools: &self.tools,
569 run_wrapper,
570 extra_lint_paths,
571 project_file,
572 };
573 lint_defaults::default_lint_config(lang, &output_dir, &ctx)
574 }
575
576 pub fn update_config_for_language(&self, lang: extras::Language) -> output::UpdateConfig {
581 if let Some(update_map) = &self.update {
582 let lang_str = lang.to_string();
583 if let Some(explicit) = update_map.get(&lang_str) {
584 return explicit.clone();
585 }
586 }
587 let output_dir = self.package_dir(lang);
588 let ctx = LangContext {
589 tools: &self.tools,
590 run_wrapper: None,
591 extra_lint_paths: &[],
592 project_file: None,
593 };
594 update_defaults::default_update_config(lang, &output_dir, &ctx)
595 }
596
597 pub fn test_config_for_language(&self, lang: extras::Language) -> output::TestConfig {
602 if let Some(test_map) = &self.test {
603 let lang_str = lang.to_string();
604 if let Some(explicit) = test_map.get(&lang_str) {
605 return explicit.clone();
606 }
607 }
608 let output_dir = self.package_dir(lang);
609 let run_wrapper = self.run_wrapper_for_language(lang);
610 let project_file = self.project_file_for_language(lang);
611 let ctx = LangContext {
612 tools: &self.tools,
613 run_wrapper,
614 extra_lint_paths: &[],
615 project_file,
616 };
617 test_defaults::default_test_config(lang, &output_dir, &ctx)
618 }
619
620 pub fn setup_config_for_language(&self, lang: extras::Language) -> output::SetupConfig {
625 if let Some(setup_map) = &self.setup {
626 let lang_str = lang.to_string();
627 if let Some(explicit) = setup_map.get(&lang_str) {
628 return explicit.clone();
629 }
630 }
631 let output_dir = self.package_dir(lang);
632 let ctx = LangContext {
633 tools: &self.tools,
634 run_wrapper: None,
635 extra_lint_paths: &[],
636 project_file: None,
637 };
638 setup_defaults::default_setup_config(lang, &output_dir, &ctx)
639 }
640
641 pub fn clean_config_for_language(&self, lang: extras::Language) -> output::CleanConfig {
646 if let Some(clean_map) = &self.clean {
647 let lang_str = lang.to_string();
648 if let Some(explicit) = clean_map.get(&lang_str) {
649 return explicit.clone();
650 }
651 }
652 let output_dir = self.package_dir(lang);
653 let ctx = LangContext {
654 tools: &self.tools,
655 run_wrapper: None,
656 extra_lint_paths: &[],
657 project_file: None,
658 };
659 clean_defaults::default_clean_config(lang, &output_dir, &ctx)
660 }
661
662 pub fn build_command_config_for_language(&self, lang: extras::Language) -> output::BuildCommandConfig {
667 if let Some(build_map) = &self.build_commands {
668 let lang_str = lang.to_string();
669 if let Some(explicit) = build_map.get(&lang_str) {
670 return explicit.clone();
671 }
672 }
673 let output_dir = self.package_dir(lang);
674 let crate_name = &self.crate_config.name;
675 let run_wrapper = self.run_wrapper_for_language(lang);
676 let project_file = self.project_file_for_language(lang);
677 let ctx = LangContext {
678 tools: &self.tools,
679 run_wrapper,
680 extra_lint_paths: &[],
681 project_file,
682 };
683 build_defaults::default_build_config(lang, &output_dir, crate_name, &ctx)
684 }
685
686 pub fn core_import(&self) -> String {
688 self.crate_config
689 .core_import
690 .clone()
691 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
692 }
693
694 pub fn error_type(&self) -> String {
696 self.crate_config
697 .error_type
698 .clone()
699 .unwrap_or_else(|| "Error".to_string())
700 }
701
702 pub fn error_constructor(&self) -> String {
705 self.crate_config
706 .error_constructor
707 .clone()
708 .unwrap_or_else(|| format!("{}::{}::from({{msg}})", self.core_import(), self.error_type()))
709 }
710
711 pub fn run_wrapper_for_language(&self, lang: extras::Language) -> Option<&str> {
714 match lang {
715 extras::Language::Python => self.python.as_ref().and_then(|c| c.run_wrapper.as_deref()),
716 extras::Language::Node => self.node.as_ref().and_then(|c| c.run_wrapper.as_deref()),
717 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.run_wrapper.as_deref()),
718 extras::Language::Php => self.php.as_ref().and_then(|c| c.run_wrapper.as_deref()),
719 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.run_wrapper.as_deref()),
720 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.run_wrapper.as_deref()),
721 extras::Language::Go => self.go.as_ref().and_then(|c| c.run_wrapper.as_deref()),
722 extras::Language::Java => self.java.as_ref().and_then(|c| c.run_wrapper.as_deref()),
723 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.run_wrapper.as_deref()),
724 extras::Language::R => self.r.as_ref().and_then(|c| c.run_wrapper.as_deref()),
725 extras::Language::Kotlin => self.kotlin.as_ref().and_then(|c| c.run_wrapper.as_deref()),
726 extras::Language::Dart => self.dart.as_ref().and_then(|c| c.run_wrapper.as_deref()),
727 extras::Language::Swift => self.swift.as_ref().and_then(|c| c.run_wrapper.as_deref()),
728 extras::Language::Gleam => self.gleam.as_ref().and_then(|c| c.run_wrapper.as_deref()),
729 extras::Language::Zig => self.zig.as_ref().and_then(|c| c.run_wrapper.as_deref()),
730 extras::Language::Ffi | extras::Language::Rust => None,
731 }
732 }
733
734 pub fn extra_lint_paths_for_language(&self, lang: extras::Language) -> &[String] {
737 match lang {
738 extras::Language::Python => self
739 .python
740 .as_ref()
741 .map(|c| c.extra_lint_paths.as_slice())
742 .unwrap_or(&[]),
743 extras::Language::Node => self.node.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
744 extras::Language::Ruby => self.ruby.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
745 extras::Language::Php => self.php.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
746 extras::Language::Elixir => self
747 .elixir
748 .as_ref()
749 .map(|c| c.extra_lint_paths.as_slice())
750 .unwrap_or(&[]),
751 extras::Language::Wasm => self.wasm.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
752 extras::Language::Go => self.go.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
753 extras::Language::Java => self.java.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
754 extras::Language::Csharp => self
755 .csharp
756 .as_ref()
757 .map(|c| c.extra_lint_paths.as_slice())
758 .unwrap_or(&[]),
759 extras::Language::R => self.r.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
760 extras::Language::Kotlin => self
761 .kotlin
762 .as_ref()
763 .map(|c| c.extra_lint_paths.as_slice())
764 .unwrap_or(&[]),
765 extras::Language::Dart => self.dart.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
766 extras::Language::Swift => self
767 .swift
768 .as_ref()
769 .map(|c| c.extra_lint_paths.as_slice())
770 .unwrap_or(&[]),
771 extras::Language::Gleam => self
772 .gleam
773 .as_ref()
774 .map(|c| c.extra_lint_paths.as_slice())
775 .unwrap_or(&[]),
776 extras::Language::Zig => self.zig.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
777 extras::Language::Ffi | extras::Language::Rust => &[],
778 }
779 }
780
781 pub fn project_file_for_language(&self, lang: extras::Language) -> Option<&str> {
784 match lang {
785 extras::Language::Java => self.java.as_ref().and_then(|c| c.project_file.as_deref()),
786 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.project_file.as_deref()),
787 _ => None,
788 }
789 }
790
791 pub fn ffi_prefix(&self) -> String {
793 self.ffi
794 .as_ref()
795 .and_then(|f| f.prefix.as_ref())
796 .cloned()
797 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
798 }
799
800 pub fn ffi_lib_name(&self) -> String {
808 if let Some(name) = self.ffi.as_ref().and_then(|f| f.lib_name.as_ref()) {
810 return name.clone();
811 }
812
813 if let Some(ffi_path) = self.output.ffi.as_ref() {
816 let path = std::path::Path::new(ffi_path);
817 let components: Vec<_> = path
820 .components()
821 .filter_map(|c| {
822 if let std::path::Component::Normal(s) = c {
823 s.to_str()
824 } else {
825 None
826 }
827 })
828 .collect();
829 let crate_dir = components
832 .iter()
833 .rev()
834 .find(|&&s| s != "src" && s != "lib" && s != "include")
835 .copied();
836 if let Some(dir) = crate_dir {
837 return dir.replace('-', "_");
838 }
839 }
840
841 format!("{}_ffi", self.ffi_prefix())
843 }
844
845 pub fn ffi_header_name(&self) -> String {
847 self.ffi
848 .as_ref()
849 .and_then(|f| f.header_name.as_ref())
850 .cloned()
851 .unwrap_or_else(|| format!("{}.h", self.ffi_prefix()))
852 }
853
854 pub fn dart_style(&self) -> languages::DartStyle {
856 self.dart.as_ref().map(|d| d.style).unwrap_or_default()
857 }
858
859 pub fn python_module_name(&self) -> String {
861 self.python
862 .as_ref()
863 .and_then(|p| p.module_name.as_ref())
864 .cloned()
865 .unwrap_or_else(|| format!("_{}", self.crate_config.name.replace('-', "_")))
866 }
867
868 pub fn python_pip_name(&self) -> String {
872 self.python
873 .as_ref()
874 .and_then(|p| p.pip_name.as_ref())
875 .cloned()
876 .unwrap_or_else(|| self.crate_config.name.clone())
877 }
878
879 pub fn php_autoload_namespace(&self) -> String {
884 use heck::ToPascalCase;
885 let ext = self.php_extension_name();
886 if ext.contains('_') {
887 ext.split('_')
888 .map(|p| p.to_pascal_case())
889 .collect::<Vec<_>>()
890 .join("\\")
891 } else {
892 ext.to_pascal_case()
893 }
894 }
895
896 pub fn node_package_name(&self) -> String {
898 self.node
899 .as_ref()
900 .and_then(|n| n.package_name.as_ref())
901 .cloned()
902 .unwrap_or_else(|| self.crate_config.name.clone())
903 }
904
905 pub fn ruby_gem_name(&self) -> String {
907 self.ruby
908 .as_ref()
909 .and_then(|r| r.gem_name.as_ref())
910 .cloned()
911 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
912 }
913
914 pub fn php_extension_name(&self) -> String {
916 self.php
917 .as_ref()
918 .and_then(|p| p.extension_name.as_ref())
919 .cloned()
920 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
921 }
922
923 pub fn elixir_app_name(&self) -> String {
925 self.elixir
926 .as_ref()
927 .and_then(|e| e.app_name.as_ref())
928 .cloned()
929 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
930 }
931
932 pub fn try_go_module(&self) -> Result<String, String> {
941 if let Some(module) = self.go.as_ref().and_then(|g| g.module.as_ref()) {
942 return Ok(module.clone());
943 }
944 if let Ok(repo) = self.try_github_repo() {
945 if let Some(module) = derive_go_module_from_repo(&repo) {
946 return Ok(module);
947 }
948 }
949 Err(format!(
950 "no Go module configured — set `[go] module = \"...\"` or `[scaffold] repository = \"https://<host>/<org>/...\"` for crate `{}`",
951 self.crate_config.name
952 ))
953 }
954
955 pub fn go_module(&self) -> String {
957 self.try_go_module()
958 .unwrap_or_else(|_| format!("example.invalid/{}", self.crate_config.name))
959 }
960
961 pub fn try_github_repo(&self) -> Result<String, String> {
971 if let Some(e2e) = &self.e2e {
972 if let Some(url) = &e2e.registry.github_repo {
973 return Ok(url.clone());
974 }
975 }
976 if let Some(url) = self.scaffold.as_ref().and_then(|s| s.repository.as_ref()) {
977 return Ok(url.clone());
978 }
979 Err(format!(
980 "no repository URL configured — set `[scaffold] repository = \"...\"` (or `[e2e.registry] github_repo`) for crate `{}`",
981 self.crate_config.name
982 ))
983 }
984
985 pub fn github_repo(&self) -> String {
993 self.try_github_repo()
994 .unwrap_or_else(|_| format!("https://example.invalid/{}", self.crate_config.name))
995 }
996
997 pub fn try_java_package(&self) -> Result<String, String> {
1006 if let Some(pkg) = self.java.as_ref().and_then(|j| j.package.as_ref()) {
1007 return Ok(pkg.clone());
1008 }
1009 if let Ok(repo) = self.try_github_repo() {
1010 if let Some(pkg) = derive_reverse_dns_package(&repo) {
1011 return Ok(pkg);
1012 }
1013 }
1014 Err(format!(
1015 "no Java package configured — set `[java] package = \"...\"` or `[scaffold] repository = \"https://<host>/<org>/...\"` for crate `{}`",
1016 self.crate_config.name
1017 ))
1018 }
1019
1020 pub fn java_package(&self) -> String {
1027 self.try_java_package()
1028 .unwrap_or_else(|_| "unconfigured.alef".to_string())
1029 }
1030
1031 pub fn java_group_id(&self) -> String {
1036 self.java_package()
1037 }
1038
1039 pub fn try_kotlin_package(&self) -> Result<String, String> {
1047 if let Some(pkg) = self.kotlin.as_ref().and_then(|k| k.package.as_ref()) {
1048 return Ok(pkg.clone());
1049 }
1050 if let Ok(repo) = self.try_github_repo() {
1051 if let Some(pkg) = derive_reverse_dns_package(&repo) {
1052 return Ok(pkg);
1053 }
1054 }
1055 Err(format!(
1056 "no Kotlin package configured — set `[kotlin] package = \"...\"` or `[scaffold] repository = \"https://<host>/<org>/...\"` for crate `{}`",
1057 self.crate_config.name
1058 ))
1059 }
1060
1061 pub fn kotlin_package(&self) -> String {
1063 self.try_kotlin_package()
1064 .unwrap_or_else(|_| "unconfigured.alef".to_string())
1065 }
1066
1067 pub fn kotlin_target(&self) -> KotlinTarget {
1072 self.kotlin.as_ref().map(|k| k.target).unwrap_or_default()
1073 }
1074
1075 pub fn dart_pubspec_name(&self) -> String {
1080 self.dart
1081 .as_ref()
1082 .and_then(|d| d.pubspec_name.as_ref())
1083 .cloned()
1084 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
1085 }
1086
1087 pub fn dart_frb_version(&self) -> String {
1090 self.dart
1091 .as_ref()
1092 .and_then(|d| d.frb_version.as_ref())
1093 .cloned()
1094 .unwrap_or_else(|| crate::template_versions::cargo::FLUTTER_RUST_BRIDGE.to_string())
1095 }
1096
1097 pub fn swift_module(&self) -> String {
1102 self.swift
1103 .as_ref()
1104 .and_then(|s| s.module_name.as_ref())
1105 .cloned()
1106 .unwrap_or_else(|| {
1107 use heck::ToUpperCamelCase;
1108 self.crate_config.name.to_upper_camel_case()
1109 })
1110 }
1111
1112 pub fn swift_bridge_version(&self) -> String {
1115 self.swift
1116 .as_ref()
1117 .and_then(|s| s.swift_bridge_version.as_ref())
1118 .cloned()
1119 .unwrap_or_else(|| crate::template_versions::cargo::SWIFT_BRIDGE.to_string())
1120 }
1121
1122 pub fn swift_min_macos(&self) -> String {
1124 self.swift
1125 .as_ref()
1126 .and_then(|s| s.min_macos_version.as_ref())
1127 .cloned()
1128 .unwrap_or_else(|| crate::template_versions::toolchain::SWIFT_MIN_MACOS.to_string())
1129 }
1130
1131 pub fn swift_min_ios(&self) -> String {
1133 self.swift
1134 .as_ref()
1135 .and_then(|s| s.min_ios_version.as_ref())
1136 .cloned()
1137 .unwrap_or_else(|| crate::template_versions::toolchain::SWIFT_MIN_IOS.to_string())
1138 }
1139
1140 pub fn gleam_app_name(&self) -> String {
1142 self.gleam
1143 .as_ref()
1144 .and_then(|g| g.app_name.as_ref())
1145 .cloned()
1146 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
1147 }
1148
1149 pub fn gleam_nif_module(&self) -> String {
1153 use heck::ToUpperCamelCase;
1154 self.gleam
1155 .as_ref()
1156 .and_then(|g| g.nif_module.as_ref())
1157 .cloned()
1158 .unwrap_or_else(|| {
1159 let pascal = self
1160 .elixir
1161 .as_ref()
1162 .and_then(|e| e.app_name.as_deref())
1163 .unwrap_or(&self.crate_config.name)
1164 .to_upper_camel_case();
1165 format!("Elixir.{pascal}.Native")
1166 })
1167 }
1168
1169 pub fn zig_module_name(&self) -> String {
1171 self.zig
1172 .as_ref()
1173 .and_then(|z| z.module_name.as_ref())
1174 .cloned()
1175 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
1176 }
1177
1178 pub fn csharp_namespace(&self) -> String {
1180 self.csharp
1181 .as_ref()
1182 .and_then(|c| c.namespace.as_ref())
1183 .cloned()
1184 .unwrap_or_else(|| {
1185 use heck::ToPascalCase;
1186 self.crate_config.name.to_pascal_case()
1187 })
1188 }
1189
1190 pub fn csharp_package_id(&self) -> String {
1197 self.csharp
1198 .as_ref()
1199 .and_then(|c| c.package_id.as_ref())
1200 .cloned()
1201 .unwrap_or_else(|| self.csharp_namespace())
1202 }
1203
1204 pub fn core_crate_dir(&self) -> String {
1210 if let Some(first_source) = self.crate_config.sources.first() {
1213 let path = std::path::Path::new(first_source);
1214 let mut current = path.parent();
1215 while let Some(dir) = current {
1216 if dir.file_name().is_some_and(|n| n == "src") {
1217 if let Some(crate_dir) = dir.parent() {
1218 if let Some(dir_name) = crate_dir.file_name() {
1219 return dir_name.to_string_lossy().into_owned();
1220 }
1221 }
1222 break;
1223 }
1224 current = dir.parent();
1225 }
1226 }
1227 self.crate_config.name.clone()
1228 }
1229
1230 pub fn wasm_type_prefix(&self) -> String {
1233 self.wasm
1234 .as_ref()
1235 .and_then(|w| w.type_prefix.as_ref())
1236 .cloned()
1237 .unwrap_or_else(|| "Wasm".to_string())
1238 }
1239
1240 pub fn node_type_prefix(&self) -> String {
1243 self.node
1244 .as_ref()
1245 .and_then(|n| n.type_prefix.as_ref())
1246 .cloned()
1247 .unwrap_or_else(|| "Js".to_string())
1248 }
1249
1250 pub fn r_package_name(&self) -> String {
1252 self.r
1253 .as_ref()
1254 .and_then(|r| r.package_name.as_ref())
1255 .cloned()
1256 .unwrap_or_else(|| self.crate_config.name.clone())
1257 }
1258
1259 pub fn resolved_version(&self) -> Option<String> {
1262 let content = std::fs::read_to_string(&self.crate_config.version_from).ok()?;
1263 let value: toml::Value = toml::from_str(&content).ok()?;
1264 if let Some(v) = value
1265 .get("workspace")
1266 .and_then(|w| w.get("package"))
1267 .and_then(|p| p.get("version"))
1268 .and_then(|v| v.as_str())
1269 {
1270 return Some(v.to_string());
1271 }
1272 value
1273 .get("package")
1274 .and_then(|p| p.get("version"))
1275 .and_then(|v| v.as_str())
1276 .map(|v| v.to_string())
1277 }
1278
1279 pub fn serde_rename_all_for_language(&self, lang: extras::Language) -> String {
1287 let override_val = match lang {
1289 extras::Language::Python => self.python.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1290 extras::Language::Node => self.node.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1291 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1292 extras::Language::Php => self.php.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1293 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1294 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1295 extras::Language::Ffi => self.ffi.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1296 extras::Language::Gleam => self.gleam.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1297 extras::Language::Go => self.go.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1298 extras::Language::Java => self.java.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1299 extras::Language::Kotlin => self.kotlin.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1300 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1301 extras::Language::R => self.r.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1302 extras::Language::Zig => self.zig.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1303 extras::Language::Dart => self.dart.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1304 extras::Language::Swift => self.swift.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1305 extras::Language::Rust => None, };
1307
1308 if let Some(val) = override_val {
1309 return val.to_string();
1310 }
1311
1312 match lang {
1314 extras::Language::Node | extras::Language::Wasm | extras::Language::Java | extras::Language::Csharp => {
1315 "camelCase".to_string()
1316 }
1317 extras::Language::Python
1318 | extras::Language::Ruby
1319 | extras::Language::Php
1320 | extras::Language::Go
1321 | extras::Language::Ffi
1322 | extras::Language::Elixir
1323 | extras::Language::R
1324 | extras::Language::Rust
1325 | extras::Language::Kotlin
1326 | extras::Language::Gleam
1327 | extras::Language::Zig
1328 | extras::Language::Swift
1329 | extras::Language::Dart => "snake_case".to_string(),
1330 }
1331 }
1332
1333 pub fn rewrite_path(&self, rust_path: &str) -> String {
1336 let mut mappings: Vec<_> = self.crate_config.path_mappings.iter().collect();
1338 mappings.sort_by_key(|b| std::cmp::Reverse(b.0.len()));
1339
1340 for (from, to) in &mappings {
1341 if rust_path.starts_with(from.as_str()) {
1342 return format!("{}{}", to, &rust_path[from.len()..]);
1343 }
1344 }
1345 rust_path.to_string()
1346 }
1347
1348 pub fn effective_path_mappings(&self) -> HashMap<String, String> {
1358 let mut mappings = HashMap::new();
1359
1360 if self.crate_config.auto_path_mappings {
1361 let core_import = self.core_import();
1362
1363 for source in &self.crate_config.sources {
1364 let source_str = source.to_string_lossy();
1365 if let Some(after_crates) = find_after_crates_prefix(&source_str) {
1367 if let Some(slash_pos) = after_crates.find('/') {
1369 let crate_dir = &after_crates[..slash_pos];
1370 let crate_ident = crate_dir.replace('-', "_");
1371 if crate_ident != core_import && !mappings.contains_key(&crate_ident) {
1373 mappings.insert(crate_ident, core_import.clone());
1374 }
1375 }
1376 }
1377 }
1378 }
1379
1380 for (from, to) in &self.crate_config.path_mappings {
1382 mappings.insert(from.clone(), to.clone());
1383 }
1384
1385 mappings
1386 }
1387}
1388
1389fn find_after_crates_prefix(path: &str) -> Option<&str> {
1396 if let Some(pos) = path.find("/crates/") {
1400 return Some(&path[pos + "/crates/".len()..]);
1401 }
1402 if let Some(stripped) = path.strip_prefix("crates/") {
1403 return Some(stripped);
1404 }
1405 None
1406}
1407
1408pub fn resolve_output_dir(config_path: Option<&PathBuf>, crate_name: &str, default: &str) -> String {
1411 config_path
1412 .map(|p| p.to_string_lossy().replace("{name}", crate_name))
1413 .unwrap_or_else(|| default.replace("{name}", crate_name))
1414}
1415
1416pub fn detect_serde_available(output_dir: &str) -> bool {
1422 let src_path = std::path::Path::new(output_dir);
1423 let mut dir = src_path;
1425 loop {
1426 let cargo_toml = dir.join("Cargo.toml");
1427 if cargo_toml.exists() {
1428 return cargo_toml_has_serde(&cargo_toml);
1429 }
1430 match dir.parent() {
1431 Some(parent) if !parent.as_os_str().is_empty() => dir = parent,
1432 _ => break,
1433 }
1434 }
1435 false
1436}
1437
1438fn cargo_toml_has_serde(path: &std::path::Path) -> bool {
1444 let content = match std::fs::read_to_string(path) {
1445 Ok(c) => c,
1446 Err(_) => return false,
1447 };
1448
1449 let has_serde_json = content.contains("serde_json");
1450 let has_serde_dep = content.lines().any(|line| {
1454 let trimmed = line.trim();
1455 trimmed.starts_with("serde ")
1457 || trimmed.starts_with("serde=")
1458 || trimmed.starts_with("serde.")
1459 || trimmed == "[dependencies.serde]"
1460 });
1461
1462 has_serde_json && has_serde_dep
1463}
1464
1465#[cfg(test)]
1466mod tests {
1467 use super::*;
1468
1469 fn minimal_config() -> AlefConfig {
1470 toml::from_str(
1471 r#"
1472languages = ["python", "node", "rust"]
1473
1474[crate]
1475name = "test-lib"
1476sources = ["src/lib.rs"]
1477"#,
1478 )
1479 .unwrap()
1480 }
1481
1482 #[test]
1483 fn lint_config_falls_back_to_defaults() {
1484 let config = minimal_config();
1485 assert!(config.lint.is_none());
1486
1487 let py = config.lint_config_for_language(Language::Python);
1488 assert!(py.format.is_some());
1489 assert!(py.check.is_some());
1490 assert!(py.typecheck.is_some());
1491
1492 let node = config.lint_config_for_language(Language::Node);
1493 assert!(node.format.is_some());
1494 assert!(node.check.is_some());
1495 }
1496
1497 #[test]
1498 fn lint_config_explicit_overrides_default() {
1499 let config: AlefConfig = toml::from_str(
1500 r#"
1501languages = ["python"]
1502
1503[crate]
1504name = "test-lib"
1505sources = ["src/lib.rs"]
1506
1507[lint.python]
1508format = "custom-formatter"
1509check = "custom-checker"
1510"#,
1511 )
1512 .unwrap();
1513
1514 let py = config.lint_config_for_language(Language::Python);
1515 assert_eq!(py.format.unwrap().commands(), vec!["custom-formatter"]);
1516 assert_eq!(py.check.unwrap().commands(), vec!["custom-checker"]);
1517 assert!(py.typecheck.is_none()); }
1519
1520 #[test]
1521 fn lint_config_partial_override_does_not_merge() {
1522 let config: AlefConfig = toml::from_str(
1523 r#"
1524languages = ["python"]
1525
1526[crate]
1527name = "test-lib"
1528sources = ["src/lib.rs"]
1529
1530[lint.python]
1531format = "only-format"
1532"#,
1533 )
1534 .unwrap();
1535
1536 let py = config.lint_config_for_language(Language::Python);
1537 assert_eq!(py.format.unwrap().commands(), vec!["only-format"]);
1538 assert!(py.check.is_none());
1540 assert!(py.typecheck.is_none());
1541 }
1542
1543 #[test]
1544 fn lint_config_unconfigured_language_uses_defaults() {
1545 let config: AlefConfig = toml::from_str(
1546 r#"
1547languages = ["python", "node"]
1548
1549[crate]
1550name = "test-lib"
1551sources = ["src/lib.rs"]
1552
1553[lint.python]
1554format = "custom"
1555"#,
1556 )
1557 .unwrap();
1558
1559 let py = config.lint_config_for_language(Language::Python);
1561 assert_eq!(py.format.unwrap().commands(), vec!["custom"]);
1562
1563 let node = config.lint_config_for_language(Language::Node);
1565 let fmt = node.format.unwrap().commands().join(" ");
1566 assert!(fmt.contains("oxfmt"));
1567 }
1568
1569 #[test]
1570 fn update_config_falls_back_to_defaults() {
1571 let config = minimal_config();
1572 assert!(config.update.is_none());
1573
1574 let py = config.update_config_for_language(Language::Python);
1575 assert!(py.update.is_some());
1576 assert!(py.upgrade.is_some());
1577
1578 let rust = config.update_config_for_language(Language::Rust);
1579 let update = rust.update.unwrap().commands().join(" ");
1580 assert!(update.contains("cargo update"));
1581 }
1582
1583 #[test]
1584 fn update_config_explicit_overrides_default() {
1585 let config: AlefConfig = toml::from_str(
1586 r#"
1587languages = ["rust"]
1588
1589[crate]
1590name = "test-lib"
1591sources = ["src/lib.rs"]
1592
1593[update.rust]
1594update = "my-custom-update"
1595upgrade = ["step1", "step2"]
1596"#,
1597 )
1598 .unwrap();
1599
1600 let rust = config.update_config_for_language(Language::Rust);
1601 assert_eq!(rust.update.unwrap().commands(), vec!["my-custom-update"]);
1602 assert_eq!(rust.upgrade.unwrap().commands(), vec!["step1", "step2"]);
1603 }
1604
1605 #[test]
1606 fn test_config_falls_back_to_defaults() {
1607 let config = minimal_config();
1608 assert!(config.test.is_none());
1609
1610 let py = config.test_config_for_language(Language::Python);
1611 assert!(py.command.is_some());
1612 assert!(py.coverage.is_some());
1613 assert!(py.e2e.is_none());
1614
1615 let rust = config.test_config_for_language(Language::Rust);
1616 let cmd = rust.command.unwrap().commands().join(" ");
1617 assert!(cmd.contains("cargo test"));
1618 }
1619
1620 #[test]
1621 fn test_config_explicit_overrides_default() {
1622 let config: AlefConfig = toml::from_str(
1623 r#"
1624languages = ["python"]
1625
1626[crate]
1627name = "test-lib"
1628sources = ["src/lib.rs"]
1629
1630[test.python]
1631command = "my-custom-test"
1632"#,
1633 )
1634 .unwrap();
1635
1636 let py = config.test_config_for_language(Language::Python);
1637 assert_eq!(py.command.unwrap().commands(), vec!["my-custom-test"]);
1638 assert!(py.coverage.is_none()); }
1640
1641 #[test]
1642 fn setup_config_falls_back_to_defaults() {
1643 let config = minimal_config();
1644 assert!(config.setup.is_none());
1645
1646 let py = config.setup_config_for_language(Language::Python);
1647 assert!(py.install.is_some());
1648 let install = py.install.unwrap().commands().join(" ");
1649 assert!(install.contains("uv sync"));
1650
1651 let rust = config.setup_config_for_language(Language::Rust);
1652 let install = rust.install.unwrap().commands().join(" ");
1653 assert!(install.contains("rustup update"));
1654 }
1655
1656 #[test]
1657 fn setup_config_explicit_overrides_default() {
1658 let config: AlefConfig = toml::from_str(
1659 r#"
1660languages = ["python"]
1661
1662[crate]
1663name = "test-lib"
1664sources = ["src/lib.rs"]
1665
1666[setup.python]
1667install = "my-custom-install"
1668"#,
1669 )
1670 .unwrap();
1671
1672 let py = config.setup_config_for_language(Language::Python);
1673 assert_eq!(py.install.unwrap().commands(), vec!["my-custom-install"]);
1674 }
1675
1676 #[test]
1677 fn clean_config_falls_back_to_defaults() {
1678 let config = minimal_config();
1679 assert!(config.clean.is_none());
1680
1681 let py = config.clean_config_for_language(Language::Python);
1682 assert!(py.clean.is_some());
1683 let clean = py.clean.unwrap().commands().join(" ");
1684 assert!(clean.contains("__pycache__"));
1685
1686 let rust = config.clean_config_for_language(Language::Rust);
1687 let clean = rust.clean.unwrap().commands().join(" ");
1688 assert!(clean.contains("cargo clean"));
1689 }
1690
1691 #[test]
1692 fn clean_config_explicit_overrides_default() {
1693 let config: AlefConfig = toml::from_str(
1694 r#"
1695languages = ["rust"]
1696
1697[crate]
1698name = "test-lib"
1699sources = ["src/lib.rs"]
1700
1701[clean.rust]
1702clean = "my-custom-clean"
1703"#,
1704 )
1705 .unwrap();
1706
1707 let rust = config.clean_config_for_language(Language::Rust);
1708 assert_eq!(rust.clean.unwrap().commands(), vec!["my-custom-clean"]);
1709 }
1710
1711 #[test]
1712 fn build_command_config_falls_back_to_defaults() {
1713 let config = minimal_config();
1714 assert!(config.build_commands.is_none());
1715
1716 let py = config.build_command_config_for_language(Language::Python);
1717 assert!(py.build.is_some());
1718 assert!(py.build_release.is_some());
1719 let build = py.build.unwrap().commands().join(" ");
1720 assert!(build.contains("maturin develop"));
1721
1722 let rust = config.build_command_config_for_language(Language::Rust);
1723 let build = rust.build.unwrap().commands().join(" ");
1724 assert!(build.contains("cargo build --workspace"));
1725 }
1726
1727 #[test]
1728 fn build_command_config_explicit_overrides_default() {
1729 let config: AlefConfig = toml::from_str(
1730 r#"
1731languages = ["rust"]
1732
1733[crate]
1734name = "test-lib"
1735sources = ["src/lib.rs"]
1736
1737[build_commands.rust]
1738build = "my-custom-build"
1739build_release = "my-custom-build --release"
1740"#,
1741 )
1742 .unwrap();
1743
1744 let rust = config.build_command_config_for_language(Language::Rust);
1745 assert_eq!(rust.build.unwrap().commands(), vec!["my-custom-build"]);
1746 assert_eq!(
1747 rust.build_release.unwrap().commands(),
1748 vec!["my-custom-build --release"]
1749 );
1750 }
1751
1752 #[test]
1753 fn build_command_config_uses_crate_name() {
1754 let config = minimal_config();
1755 let py = config.build_command_config_for_language(Language::Python);
1756 let build = py.build.unwrap().commands().join(" ");
1757 assert!(
1758 build.contains("test-lib-py"),
1759 "Python build should reference crate name, got: {build}"
1760 );
1761 }
1762
1763 #[test]
1764 fn package_dir_defaults_are_correct() {
1765 let config = minimal_config();
1766 assert_eq!(config.package_dir(Language::Python), "packages/python");
1767 assert_eq!(config.package_dir(Language::Node), "packages/node");
1768 assert_eq!(config.package_dir(Language::Ruby), "packages/ruby");
1769 assert_eq!(config.package_dir(Language::Go), "packages/go");
1770 assert_eq!(config.package_dir(Language::Java), "packages/java");
1771 }
1772
1773 #[test]
1774 fn explicit_lint_config_preserves_precondition_and_before() {
1775 let config: AlefConfig = toml::from_str(
1776 r#"
1777languages = ["go"]
1778
1779[crate]
1780name = "test"
1781sources = ["src/lib.rs"]
1782
1783[lint.go]
1784precondition = "test -f target/release/libtest_ffi.so"
1785before = "cargo build --release -p test-ffi"
1786format = "gofmt -w packages/go"
1787check = "golangci-lint run ./..."
1788"#,
1789 )
1790 .unwrap();
1791
1792 let lint = config.lint_config_for_language(Language::Go);
1793 assert_eq!(
1794 lint.precondition.as_deref(),
1795 Some("test -f target/release/libtest_ffi.so"),
1796 "precondition should be preserved from explicit config"
1797 );
1798 assert_eq!(
1799 lint.before.unwrap().commands(),
1800 vec!["cargo build --release -p test-ffi"],
1801 "before should be preserved from explicit config"
1802 );
1803 }
1804
1805 #[test]
1806 fn explicit_lint_config_with_before_list_preserves_all_commands() {
1807 let config: AlefConfig = toml::from_str(
1808 r#"
1809languages = ["go"]
1810
1811[crate]
1812name = "test"
1813sources = ["src/lib.rs"]
1814
1815[lint.go]
1816before = ["cargo build --release -p test-ffi", "cp target/release/libtest_ffi.so packages/go/"]
1817check = "golangci-lint run ./..."
1818"#,
1819 )
1820 .unwrap();
1821
1822 let lint = config.lint_config_for_language(Language::Go);
1823 assert!(lint.precondition.is_none(), "precondition should be None when not set");
1824 assert_eq!(
1825 lint.before.unwrap().commands(),
1826 vec![
1827 "cargo build --release -p test-ffi",
1828 "cp target/release/libtest_ffi.so packages/go/"
1829 ],
1830 "before list should be preserved from explicit config"
1831 );
1832 }
1833
1834 #[test]
1835 fn default_lint_config_has_command_v_precondition() {
1836 let config = minimal_config();
1837 let py = config.lint_config_for_language(Language::Python);
1838 assert_eq!(py.precondition.as_deref(), Some("command -v ruff >/dev/null 2>&1"));
1839 assert!(py.before.is_none(), "default lint config should have no before");
1840
1841 let go = config.lint_config_for_language(Language::Go);
1842 assert_eq!(go.precondition.as_deref(), Some("command -v gofmt >/dev/null 2>&1"));
1843 assert!(go.before.is_none(), "default Go lint config should have no before");
1844 }
1845
1846 #[test]
1847 fn explicit_test_config_preserves_precondition_and_before() {
1848 let config: AlefConfig = toml::from_str(
1849 r#"
1850languages = ["python"]
1851
1852[crate]
1853name = "test"
1854sources = ["src/lib.rs"]
1855
1856[test.python]
1857precondition = "test -f target/release/libtest.so"
1858before = "maturin develop"
1859command = "pytest"
1860"#,
1861 )
1862 .unwrap();
1863
1864 let test = config.test_config_for_language(Language::Python);
1865 assert_eq!(
1866 test.precondition.as_deref(),
1867 Some("test -f target/release/libtest.so"),
1868 "test precondition should be preserved"
1869 );
1870 assert_eq!(
1871 test.before.unwrap().commands(),
1872 vec!["maturin develop"],
1873 "test before should be preserved"
1874 );
1875 }
1876
1877 #[test]
1878 fn default_test_config_has_command_v_precondition() {
1879 let config = minimal_config();
1880 let py = config.test_config_for_language(Language::Python);
1881 assert_eq!(py.precondition.as_deref(), Some("command -v uv >/dev/null 2>&1"));
1882 assert!(py.before.is_none(), "default test config should have no before");
1883 }
1884
1885 #[test]
1886 fn explicit_setup_config_preserves_precondition_and_before() {
1887 let config: AlefConfig = toml::from_str(
1888 r#"
1889languages = ["python"]
1890
1891[crate]
1892name = "test"
1893sources = ["src/lib.rs"]
1894
1895[setup.python]
1896precondition = "which uv"
1897before = "pip install uv"
1898install = "uv sync"
1899"#,
1900 )
1901 .unwrap();
1902
1903 let setup = config.setup_config_for_language(Language::Python);
1904 assert_eq!(
1905 setup.precondition.as_deref(),
1906 Some("which uv"),
1907 "setup precondition should be preserved"
1908 );
1909 assert_eq!(
1910 setup.before.unwrap().commands(),
1911 vec!["pip install uv"],
1912 "setup before should be preserved"
1913 );
1914 }
1915
1916 #[test]
1917 fn default_setup_config_has_command_v_precondition() {
1918 let config = minimal_config();
1919 let py = config.setup_config_for_language(Language::Python);
1920 assert_eq!(py.precondition.as_deref(), Some("command -v uv >/dev/null 2>&1"));
1921 assert!(py.before.is_none(), "default setup config should have no before");
1922 }
1923
1924 #[test]
1925 fn explicit_update_config_preserves_precondition_and_before() {
1926 let config: AlefConfig = toml::from_str(
1927 r#"
1928languages = ["rust"]
1929
1930[crate]
1931name = "test"
1932sources = ["src/lib.rs"]
1933
1934[update.rust]
1935precondition = "test -f Cargo.lock"
1936before = "cargo fetch"
1937update = "cargo update"
1938"#,
1939 )
1940 .unwrap();
1941
1942 let update = config.update_config_for_language(Language::Rust);
1943 assert_eq!(
1944 update.precondition.as_deref(),
1945 Some("test -f Cargo.lock"),
1946 "update precondition should be preserved"
1947 );
1948 assert_eq!(
1949 update.before.unwrap().commands(),
1950 vec!["cargo fetch"],
1951 "update before should be preserved"
1952 );
1953 }
1954
1955 #[test]
1956 fn default_update_config_has_command_v_precondition() {
1957 let config = minimal_config();
1958 let rust = config.update_config_for_language(Language::Rust);
1959 assert_eq!(rust.precondition.as_deref(), Some("command -v cargo >/dev/null 2>&1"));
1960 assert!(rust.before.is_none(), "default update config should have no before");
1961 }
1962
1963 #[test]
1964 fn explicit_clean_config_preserves_precondition_and_before() {
1965 let config: AlefConfig = toml::from_str(
1966 r#"
1967languages = ["rust"]
1968
1969[crate]
1970name = "test"
1971sources = ["src/lib.rs"]
1972
1973[clean.rust]
1974precondition = "test -d target"
1975before = "echo cleaning"
1976clean = "cargo clean"
1977"#,
1978 )
1979 .unwrap();
1980
1981 let clean = config.clean_config_for_language(Language::Rust);
1982 assert_eq!(
1983 clean.precondition.as_deref(),
1984 Some("test -d target"),
1985 "clean precondition should be preserved"
1986 );
1987 assert_eq!(
1988 clean.before.unwrap().commands(),
1989 vec!["echo cleaning"],
1990 "clean before should be preserved"
1991 );
1992 }
1993
1994 #[test]
1995 fn default_clean_config_precondition_matches_toolchain_use() {
1996 let config = minimal_config();
1997 let rust = config.clean_config_for_language(Language::Rust);
1999 assert_eq!(rust.precondition.as_deref(), Some("command -v cargo >/dev/null 2>&1"));
2000 assert!(rust.before.is_none(), "default clean config should have no before");
2001
2002 let py = config.clean_config_for_language(Language::Python);
2004 assert!(
2005 py.precondition.is_none(),
2006 "pure-shell clean should not have a precondition"
2007 );
2008 }
2009
2010 #[test]
2011 fn explicit_build_command_config_preserves_precondition_and_before() {
2012 let config: AlefConfig = toml::from_str(
2013 r#"
2014languages = ["go"]
2015
2016[crate]
2017name = "test"
2018sources = ["src/lib.rs"]
2019
2020[build_commands.go]
2021precondition = "which go"
2022before = "cargo build --release -p test-ffi"
2023build = "cd packages/go && go build ./..."
2024build_release = "cd packages/go && go build -ldflags='-s -w' ./..."
2025"#,
2026 )
2027 .unwrap();
2028
2029 let build = config.build_command_config_for_language(Language::Go);
2030 assert_eq!(
2031 build.precondition.as_deref(),
2032 Some("which go"),
2033 "build precondition should be preserved"
2034 );
2035 assert_eq!(
2036 build.before.unwrap().commands(),
2037 vec!["cargo build --release -p test-ffi"],
2038 "build before should be preserved"
2039 );
2040 }
2041
2042 #[test]
2043 fn default_build_command_config_has_command_v_precondition() {
2044 let config = minimal_config();
2045 let rust = config.build_command_config_for_language(Language::Rust);
2046 assert_eq!(rust.precondition.as_deref(), Some("command -v cargo >/dev/null 2>&1"));
2047 assert!(
2048 rust.before.is_none(),
2049 "default build command config should have no before"
2050 );
2051 }
2052
2053 #[test]
2054 fn version_defaults_to_none_when_omitted() {
2055 let config = minimal_config();
2056 assert!(config.version.is_none());
2057 }
2058
2059 #[test]
2060 fn version_parses_from_top_level_key() {
2061 let config: AlefConfig = toml::from_str(
2062 r#"
2063version = "0.7.7"
2064languages = ["python"]
2065
2066[crate]
2067name = "test-lib"
2068sources = ["src/lib.rs"]
2069"#,
2070 )
2071 .unwrap();
2072 assert_eq!(config.version.as_deref(), Some("0.7.7"));
2073 }
2074}