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 ScaffoldCargo, ScaffoldCargoEnvValue, ScaffoldCargoTargets, ScaffoldConfig, SetupConfig, SyncConfig, TestConfig,
36 TextReplacement, UpdateConfig,
37};
38pub use publish::{PublishConfig, PublishLanguageConfig, VendorMode};
39pub use tools::{DEFAULT_RUST_DEV_TOOLS, LangContext, ToolsConfig, require_tool, require_tools};
40pub use trait_bridge::TraitBridgeConfig;
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct AlefConfig {
45 #[serde(default)]
48 pub version: Option<String>,
49 #[serde(rename = "crate")]
50 pub crate_config: CrateConfig,
51 pub languages: Vec<Language>,
52 #[serde(default)]
53 pub exclude: ExcludeConfig,
54 #[serde(default)]
55 pub include: IncludeConfig,
56 #[serde(default)]
57 pub output: OutputConfig,
58 #[serde(default)]
59 pub python: Option<PythonConfig>,
60 #[serde(default)]
61 pub node: Option<NodeConfig>,
62 #[serde(default)]
63 pub ruby: Option<RubyConfig>,
64 #[serde(default)]
65 pub php: Option<PhpConfig>,
66 #[serde(default)]
67 pub elixir: Option<ElixirConfig>,
68 #[serde(default)]
69 pub wasm: Option<WasmConfig>,
70 #[serde(default)]
71 pub ffi: Option<FfiConfig>,
72 #[serde(default)]
73 pub gleam: Option<GleamConfig>,
74 #[serde(default)]
75 pub go: Option<GoConfig>,
76 #[serde(default)]
77 pub java: Option<JavaConfig>,
78 #[serde(default)]
79 pub dart: Option<DartConfig>,
80 #[serde(default)]
81 pub kotlin: Option<KotlinConfig>,
82 #[serde(default)]
83 pub swift: Option<SwiftConfig>,
84 #[serde(default)]
85 pub csharp: Option<CSharpConfig>,
86 #[serde(default)]
87 pub r: Option<RConfig>,
88 #[serde(default)]
89 pub zig: Option<ZigConfig>,
90 #[serde(default)]
91 pub scaffold: Option<ScaffoldConfig>,
92 #[serde(default)]
93 pub readme: Option<ReadmeConfig>,
94 #[serde(default)]
95 pub lint: Option<HashMap<String, LintConfig>>,
96 #[serde(default)]
97 pub update: Option<HashMap<String, UpdateConfig>>,
98 #[serde(default)]
99 pub test: Option<HashMap<String, TestConfig>>,
100 #[serde(default)]
101 pub setup: Option<HashMap<String, SetupConfig>>,
102 #[serde(default)]
103 pub clean: Option<HashMap<String, CleanConfig>>,
104 #[serde(default)]
105 pub build_commands: Option<HashMap<String, BuildCommandConfig>>,
106 #[serde(default)]
108 pub publish: Option<PublishConfig>,
109 #[serde(default)]
110 pub custom_files: Option<HashMap<String, Vec<PathBuf>>>,
111 #[serde(default)]
112 pub adapters: Vec<AdapterConfig>,
113 #[serde(default)]
114 pub custom_modules: CustomModulesConfig,
115 #[serde(default)]
116 pub custom_registrations: CustomRegistrationsConfig,
117 #[serde(default)]
118 pub sync: Option<SyncConfig>,
119 #[serde(default)]
123 pub opaque_types: HashMap<String, String>,
124 #[serde(default)]
126 pub generate: GenerateConfig,
127 #[serde(default)]
129 pub generate_overrides: HashMap<String, GenerateConfig>,
130 #[serde(default)]
132 pub format: FormatConfig,
133 #[serde(default)]
135 pub format_overrides: HashMap<String, FormatConfig>,
136 #[serde(default)]
138 pub dto: DtoConfig,
139 #[serde(default)]
141 pub e2e: Option<E2eConfig>,
142 #[serde(default)]
145 pub trait_bridges: Vec<TraitBridgeConfig>,
146 #[serde(default)]
150 pub tools: ToolsConfig,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct CrateConfig {
155 pub name: String,
156 pub sources: Vec<PathBuf>,
157 #[serde(default = "default_version_from")]
158 pub version_from: String,
159 #[serde(default)]
160 pub core_import: Option<String>,
161 #[serde(default)]
163 pub workspace_root: Option<PathBuf>,
164 #[serde(default)]
166 pub skip_core_import: bool,
167 #[serde(default)]
171 pub error_type: Option<String>,
172 #[serde(default)]
177 pub error_constructor: Option<String>,
178 #[serde(default)]
182 pub features: Vec<String>,
183 #[serde(default)]
186 pub path_mappings: HashMap<String, String>,
187 #[serde(default)]
191 pub extra_dependencies: HashMap<String, toml::Value>,
192 #[serde(default = "default_true")]
196 pub auto_path_mappings: bool,
197 #[serde(default)]
202 pub source_crates: Vec<SourceCrate>,
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct SourceCrate {
208 pub name: String,
210 pub sources: Vec<PathBuf>,
212}
213
214fn default_version_from() -> String {
215 "Cargo.toml".to_string()
216}
217
218fn default_true() -> bool {
219 true
220}
221
222pub fn derive_reverse_dns_package(repo_url: &str) -> Option<String> {
236 let after_scheme = repo_url.split_once("://").map(|(_, rest)| rest).unwrap_or(repo_url);
237 let mut parts = after_scheme.split('/').filter(|s| !s.is_empty());
238 let host = parts.next()?;
239 let org = parts.next()?;
240
241 let host_reversed: Vec<String> = host
242 .split('.')
243 .filter(|s| !s.is_empty())
244 .rev()
245 .map(|s| s.replace('-', "_"))
246 .collect();
247 if host_reversed.is_empty() {
248 return None;
249 }
250
251 let mut pkg = host_reversed.join(".");
252 pkg.push('.');
253 pkg.push_str(&org.replace('-', "_"));
254 Some(pkg)
255}
256
257pub fn derive_go_module_from_repo(repo_url: &str) -> Option<String> {
267 let after_scheme = repo_url.split_once("://").map(|(_, rest)| rest).unwrap_or(repo_url);
268 let trimmed = after_scheme.trim_end_matches('/');
269 let mut parts = trimmed.split('/');
270 let host = parts.next().filter(|s| !s.is_empty())?;
271 let org = parts.next().filter(|s| !s.is_empty())?;
272 let repo_segment = parts.next().filter(|s| !s.is_empty());
273
274 let mut module = format!("{host}/{org}");
275 if let Some(repo) = repo_segment {
276 module.push('/');
277 module.push_str(repo);
278 }
279 Some(module)
280}
281
282pub fn derive_repo_org(repo_url: &str) -> Option<String> {
292 let after_scheme = repo_url.split_once("://").map(|(_, rest)| rest).unwrap_or(repo_url);
293 let mut parts = after_scheme.split('/').filter(|s| !s.is_empty());
294 let _host = parts.next()?;
295 let org = parts.next()?;
296 Some(org.to_string())
297}
298
299#[cfg(test)]
300mod derive_reverse_dns_tests {
301 use super::derive_reverse_dns_package;
302
303 #[test]
304 fn github_org_with_hyphen_underscores_in_package() {
305 assert_eq!(
306 derive_reverse_dns_package("https://github.com/kreuzberg-dev/kreuzberg"),
307 Some("com.github.kreuzberg_dev".to_string())
308 );
309 }
310
311 #[test]
312 fn other_host_reverses_correctly() {
313 assert_eq!(
314 derive_reverse_dns_package("https://gitlab.com/foo/bar"),
315 Some("com.gitlab.foo".to_string())
316 );
317 }
318
319 #[test]
320 fn missing_org_returns_none() {
321 assert_eq!(derive_reverse_dns_package("https://github.com/"), None);
322 assert_eq!(derive_reverse_dns_package("https://github.com"), None);
323 }
324
325 #[test]
326 fn no_scheme_still_parses() {
327 assert_eq!(
328 derive_reverse_dns_package("github.com/foo/bar"),
329 Some("com.github.foo".to_string())
330 );
331 }
332
333 #[test]
334 fn placeholder_url_derives_predictably() {
335 assert_eq!(
336 derive_reverse_dns_package("https://example.invalid/my-lib"),
337 Some("invalid.example.my_lib".to_string())
338 );
339 }
340}
341
342#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct GenerateConfig {
347 #[serde(default = "default_true")]
349 pub bindings: bool,
350 #[serde(default = "default_true")]
352 pub errors: bool,
353 #[serde(default = "default_true")]
355 pub configs: bool,
356 #[serde(default = "default_true")]
358 pub async_wrappers: bool,
359 #[serde(default = "default_true")]
361 pub type_conversions: bool,
362 #[serde(default = "default_true")]
364 pub package_metadata: bool,
365 #[serde(default = "default_true")]
367 pub public_api: bool,
368 #[serde(default = "default_true")]
371 pub reverse_conversions: bool,
372}
373
374impl Default for GenerateConfig {
375 fn default() -> Self {
376 Self {
377 bindings: true,
378 errors: true,
379 configs: true,
380 async_wrappers: true,
381 type_conversions: true,
382 package_metadata: true,
383 public_api: true,
384 reverse_conversions: true,
385 }
386 }
387}
388
389#[derive(Debug, Clone, Serialize, Deserialize)]
393pub struct FormatConfig {
394 #[serde(default = "default_true")]
398 pub enabled: bool,
399 #[serde(default)]
403 pub command: Option<String>,
404}
405
406impl Default for FormatConfig {
407 fn default() -> Self {
408 Self {
409 enabled: true,
410 command: None,
411 }
412 }
413}
414
415impl AlefConfig {
420 pub fn resolve_field_name(&self, lang: extras::Language, type_name: &str, field_name: &str) -> Option<String> {
432 let explicit_key = format!("{type_name}.{field_name}");
434 let explicit = match lang {
435 extras::Language::Python => self.python.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
436 extras::Language::Node => self.node.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
437 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
438 extras::Language::Php => self.php.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
439 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
440 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
441 extras::Language::Ffi => self.ffi.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
442 extras::Language::Gleam => self.gleam.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
443 extras::Language::Go => self.go.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
444 extras::Language::Java => self.java.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
445 extras::Language::Kotlin => self.kotlin.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
446 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
447 extras::Language::R => self.r.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
448 extras::Language::Zig => self.zig.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
449 extras::Language::Dart => self.dart.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
450 extras::Language::Swift => self.swift.as_ref().and_then(|c| c.rename_fields.get(&explicit_key)),
451 extras::Language::Rust => None,
452 };
453 if let Some(renamed) = explicit {
454 if renamed != field_name {
455 return Some(renamed.clone());
456 }
457 return None;
458 }
459
460 match lang {
462 extras::Language::Python => crate::keywords::python_safe_name(field_name),
463 _ => None,
468 }
469 }
470
471 pub fn features_for_language(&self, lang: extras::Language) -> &[String] {
474 let override_features = match lang {
475 extras::Language::Python => self.python.as_ref().and_then(|c| c.features.as_deref()),
476 extras::Language::Node => self.node.as_ref().and_then(|c| c.features.as_deref()),
477 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.features.as_deref()),
478 extras::Language::Php => self.php.as_ref().and_then(|c| c.features.as_deref()),
479 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.features.as_deref()),
480 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.features.as_deref()),
481 extras::Language::Ffi => self.ffi.as_ref().and_then(|c| c.features.as_deref()),
482 extras::Language::Gleam => self.gleam.as_ref().and_then(|c| c.features.as_deref()),
483 extras::Language::Go => self.go.as_ref().and_then(|c| c.features.as_deref()),
484 extras::Language::Java => self.java.as_ref().and_then(|c| c.features.as_deref()),
485 extras::Language::Kotlin => self.kotlin.as_ref().and_then(|c| c.features.as_deref()),
486 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.features.as_deref()),
487 extras::Language::R => self.r.as_ref().and_then(|c| c.features.as_deref()),
488 extras::Language::Zig => self.zig.as_ref().and_then(|c| c.features.as_deref()),
489 extras::Language::Dart => self.dart.as_ref().and_then(|c| c.features.as_deref()),
490 extras::Language::Swift => self.swift.as_ref().and_then(|c| c.features.as_deref()),
491 extras::Language::Rust => None, };
493 override_features.unwrap_or(&self.crate_config.features)
494 }
495
496 pub fn extra_deps_for_language(&self, lang: extras::Language) -> HashMap<String, toml::Value> {
500 let mut deps = self.crate_config.extra_dependencies.clone();
501 let lang_deps = match lang {
502 extras::Language::Python => self.python.as_ref().map(|c| &c.extra_dependencies),
503 extras::Language::Node => self.node.as_ref().map(|c| &c.extra_dependencies),
504 extras::Language::Ruby => self.ruby.as_ref().map(|c| &c.extra_dependencies),
505 extras::Language::Php => self.php.as_ref().map(|c| &c.extra_dependencies),
506 extras::Language::Elixir => self.elixir.as_ref().map(|c| &c.extra_dependencies),
507 extras::Language::Wasm => self.wasm.as_ref().map(|c| &c.extra_dependencies),
508 _ => None,
509 };
510 if let Some(lang_deps) = lang_deps {
511 deps.extend(lang_deps.iter().map(|(k, v)| (k.clone(), v.clone())));
512 }
513 let exclude: &[String] = match lang {
514 extras::Language::Wasm => self
515 .wasm
516 .as_ref()
517 .map(|c| c.exclude_extra_dependencies.as_slice())
518 .unwrap_or(&[]),
519 extras::Language::Dart => self
520 .dart
521 .as_ref()
522 .map(|c| c.exclude_extra_dependencies.as_slice())
523 .unwrap_or(&[]),
524 extras::Language::Swift => self
525 .swift
526 .as_ref()
527 .map(|c| c.exclude_extra_dependencies.as_slice())
528 .unwrap_or(&[]),
529 _ => &[],
530 };
531 for key in exclude {
532 deps.remove(key);
533 }
534 deps
535 }
536
537 pub fn package_dir(&self, lang: extras::Language) -> String {
542 let override_path = match lang {
543 extras::Language::Python => self.python.as_ref().and_then(|c| c.scaffold_output.as_ref()),
544 extras::Language::Node => self.node.as_ref().and_then(|c| c.scaffold_output.as_ref()),
545 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.scaffold_output.as_ref()),
546 extras::Language::Php => self.php.as_ref().and_then(|c| c.scaffold_output.as_ref()),
547 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.scaffold_output.as_ref()),
548 _ => None,
549 };
550 if let Some(p) = override_path {
551 p.to_string_lossy().to_string()
552 } else {
553 match lang {
554 extras::Language::Python => "packages/python".to_string(),
555 extras::Language::Node => "packages/node".to_string(),
556 extras::Language::Ruby => "packages/ruby".to_string(),
557 extras::Language::Php => "packages/php".to_string(),
558 extras::Language::Elixir => "packages/elixir".to_string(),
559 _ => format!("packages/{lang}"),
560 }
561 }
562 }
563
564 pub fn validate(&self) -> Result<(), crate::error::AlefError> {
571 validation::validate(self)
572 }
573
574 pub fn lint_config_for_language(&self, lang: extras::Language) -> output::LintConfig {
579 if let Some(lint_map) = &self.lint {
580 let lang_str = lang.to_string();
581 if let Some(explicit) = lint_map.get(&lang_str) {
582 return explicit.clone();
583 }
584 }
585 let output_dir = self.package_dir(lang);
586 let run_wrapper = self.run_wrapper_for_language(lang);
587 let extra_lint_paths = self.extra_lint_paths_for_language(lang);
588 let project_file = self.project_file_for_language(lang);
589 let ctx = LangContext {
590 tools: &self.tools,
591 run_wrapper,
592 extra_lint_paths,
593 project_file,
594 };
595 lint_defaults::default_lint_config(lang, &output_dir, &ctx)
596 }
597
598 pub fn update_config_for_language(&self, lang: extras::Language) -> output::UpdateConfig {
603 if let Some(update_map) = &self.update {
604 let lang_str = lang.to_string();
605 if let Some(explicit) = update_map.get(&lang_str) {
606 return explicit.clone();
607 }
608 }
609 let output_dir = self.package_dir(lang);
610 let ctx = LangContext {
611 tools: &self.tools,
612 run_wrapper: None,
613 extra_lint_paths: &[],
614 project_file: None,
615 };
616 update_defaults::default_update_config(lang, &output_dir, &ctx)
617 }
618
619 pub fn test_config_for_language(&self, lang: extras::Language) -> output::TestConfig {
624 if let Some(test_map) = &self.test {
625 let lang_str = lang.to_string();
626 if let Some(explicit) = test_map.get(&lang_str) {
627 return explicit.clone();
628 }
629 }
630 let output_dir = self.package_dir(lang);
631 let run_wrapper = self.run_wrapper_for_language(lang);
632 let project_file = self.project_file_for_language(lang);
633 let ctx = LangContext {
634 tools: &self.tools,
635 run_wrapper,
636 extra_lint_paths: &[],
637 project_file,
638 };
639 test_defaults::default_test_config(lang, &output_dir, &ctx)
640 }
641
642 pub fn setup_config_for_language(&self, lang: extras::Language) -> output::SetupConfig {
647 if let Some(setup_map) = &self.setup {
648 let lang_str = lang.to_string();
649 if let Some(explicit) = setup_map.get(&lang_str) {
650 return explicit.clone();
651 }
652 }
653 let output_dir = self.package_dir(lang);
654 let ctx = LangContext {
655 tools: &self.tools,
656 run_wrapper: None,
657 extra_lint_paths: &[],
658 project_file: None,
659 };
660 setup_defaults::default_setup_config(lang, &output_dir, &ctx)
661 }
662
663 pub fn clean_config_for_language(&self, lang: extras::Language) -> output::CleanConfig {
668 if let Some(clean_map) = &self.clean {
669 let lang_str = lang.to_string();
670 if let Some(explicit) = clean_map.get(&lang_str) {
671 return explicit.clone();
672 }
673 }
674 let output_dir = self.package_dir(lang);
675 let ctx = LangContext {
676 tools: &self.tools,
677 run_wrapper: None,
678 extra_lint_paths: &[],
679 project_file: None,
680 };
681 clean_defaults::default_clean_config(lang, &output_dir, &ctx)
682 }
683
684 pub fn build_command_config_for_language(&self, lang: extras::Language) -> output::BuildCommandConfig {
689 if let Some(build_map) = &self.build_commands {
690 let lang_str = lang.to_string();
691 if let Some(explicit) = build_map.get(&lang_str) {
692 return explicit.clone();
693 }
694 }
695 let output_dir = self.package_dir(lang);
696 let crate_name = &self.crate_config.name;
697 let run_wrapper = self.run_wrapper_for_language(lang);
698 let project_file = self.project_file_for_language(lang);
699 let ctx = LangContext {
700 tools: &self.tools,
701 run_wrapper,
702 extra_lint_paths: &[],
703 project_file,
704 };
705 build_defaults::default_build_config(lang, &output_dir, crate_name, &ctx)
706 }
707
708 pub fn core_import(&self) -> String {
710 self.crate_config
711 .core_import
712 .clone()
713 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
714 }
715
716 pub fn error_type(&self) -> String {
718 self.crate_config
719 .error_type
720 .clone()
721 .unwrap_or_else(|| "Error".to_string())
722 }
723
724 pub fn error_constructor(&self) -> String {
727 self.crate_config
728 .error_constructor
729 .clone()
730 .unwrap_or_else(|| format!("{}::{}::from({{msg}})", self.core_import(), self.error_type()))
731 }
732
733 pub fn run_wrapper_for_language(&self, lang: extras::Language) -> Option<&str> {
736 match lang {
737 extras::Language::Python => self.python.as_ref().and_then(|c| c.run_wrapper.as_deref()),
738 extras::Language::Node => self.node.as_ref().and_then(|c| c.run_wrapper.as_deref()),
739 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.run_wrapper.as_deref()),
740 extras::Language::Php => self.php.as_ref().and_then(|c| c.run_wrapper.as_deref()),
741 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.run_wrapper.as_deref()),
742 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.run_wrapper.as_deref()),
743 extras::Language::Go => self.go.as_ref().and_then(|c| c.run_wrapper.as_deref()),
744 extras::Language::Java => self.java.as_ref().and_then(|c| c.run_wrapper.as_deref()),
745 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.run_wrapper.as_deref()),
746 extras::Language::R => self.r.as_ref().and_then(|c| c.run_wrapper.as_deref()),
747 extras::Language::Kotlin => self.kotlin.as_ref().and_then(|c| c.run_wrapper.as_deref()),
748 extras::Language::Dart => self.dart.as_ref().and_then(|c| c.run_wrapper.as_deref()),
749 extras::Language::Swift => self.swift.as_ref().and_then(|c| c.run_wrapper.as_deref()),
750 extras::Language::Gleam => self.gleam.as_ref().and_then(|c| c.run_wrapper.as_deref()),
751 extras::Language::Zig => self.zig.as_ref().and_then(|c| c.run_wrapper.as_deref()),
752 extras::Language::Ffi | extras::Language::Rust => None,
753 }
754 }
755
756 pub fn extra_lint_paths_for_language(&self, lang: extras::Language) -> &[String] {
759 match lang {
760 extras::Language::Python => self
761 .python
762 .as_ref()
763 .map(|c| c.extra_lint_paths.as_slice())
764 .unwrap_or(&[]),
765 extras::Language::Node => self.node.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
766 extras::Language::Ruby => self.ruby.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
767 extras::Language::Php => self.php.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
768 extras::Language::Elixir => self
769 .elixir
770 .as_ref()
771 .map(|c| c.extra_lint_paths.as_slice())
772 .unwrap_or(&[]),
773 extras::Language::Wasm => self.wasm.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
774 extras::Language::Go => self.go.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
775 extras::Language::Java => self.java.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
776 extras::Language::Csharp => self
777 .csharp
778 .as_ref()
779 .map(|c| c.extra_lint_paths.as_slice())
780 .unwrap_or(&[]),
781 extras::Language::R => self.r.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
782 extras::Language::Kotlin => self
783 .kotlin
784 .as_ref()
785 .map(|c| c.extra_lint_paths.as_slice())
786 .unwrap_or(&[]),
787 extras::Language::Dart => self.dart.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
788 extras::Language::Swift => self
789 .swift
790 .as_ref()
791 .map(|c| c.extra_lint_paths.as_slice())
792 .unwrap_or(&[]),
793 extras::Language::Gleam => self
794 .gleam
795 .as_ref()
796 .map(|c| c.extra_lint_paths.as_slice())
797 .unwrap_or(&[]),
798 extras::Language::Zig => self.zig.as_ref().map(|c| c.extra_lint_paths.as_slice()).unwrap_or(&[]),
799 extras::Language::Ffi | extras::Language::Rust => &[],
800 }
801 }
802
803 pub fn project_file_for_language(&self, lang: extras::Language) -> Option<&str> {
806 match lang {
807 extras::Language::Java => self.java.as_ref().and_then(|c| c.project_file.as_deref()),
808 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.project_file.as_deref()),
809 _ => None,
810 }
811 }
812
813 pub fn ffi_prefix(&self) -> String {
815 self.ffi
816 .as_ref()
817 .and_then(|f| f.prefix.as_ref())
818 .cloned()
819 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
820 }
821
822 pub fn ffi_lib_name(&self) -> String {
830 if let Some(name) = self.ffi.as_ref().and_then(|f| f.lib_name.as_ref()) {
832 return name.clone();
833 }
834
835 if let Some(ffi_path) = self.output.ffi.as_ref() {
838 let path = std::path::Path::new(ffi_path);
839 let components: Vec<_> = path
842 .components()
843 .filter_map(|c| {
844 if let std::path::Component::Normal(s) = c {
845 s.to_str()
846 } else {
847 None
848 }
849 })
850 .collect();
851 let crate_dir = components
854 .iter()
855 .rev()
856 .find(|&&s| s != "src" && s != "lib" && s != "include")
857 .copied();
858 if let Some(dir) = crate_dir {
859 return dir.replace('-', "_");
860 }
861 }
862
863 format!("{}_ffi", self.ffi_prefix())
865 }
866
867 pub fn ffi_header_name(&self) -> String {
869 self.ffi
870 .as_ref()
871 .and_then(|f| f.header_name.as_ref())
872 .cloned()
873 .unwrap_or_else(|| format!("{}.h", self.ffi_prefix()))
874 }
875
876 pub fn dart_style(&self) -> languages::DartStyle {
878 self.dart.as_ref().map(|d| d.style).unwrap_or_default()
879 }
880
881 pub fn python_module_name(&self) -> String {
883 self.python
884 .as_ref()
885 .and_then(|p| p.module_name.as_ref())
886 .cloned()
887 .unwrap_or_else(|| format!("_{}", self.crate_config.name.replace('-', "_")))
888 }
889
890 pub fn python_pip_name(&self) -> String {
894 self.python
895 .as_ref()
896 .and_then(|p| p.pip_name.as_ref())
897 .cloned()
898 .unwrap_or_else(|| self.crate_config.name.clone())
899 }
900
901 pub fn php_autoload_namespace(&self) -> String {
906 use heck::ToPascalCase;
907 let ext = self.php_extension_name();
908 if ext.contains('_') {
909 ext.split('_')
910 .map(|p| p.to_pascal_case())
911 .collect::<Vec<_>>()
912 .join("\\")
913 } else {
914 ext.to_pascal_case()
915 }
916 }
917
918 pub fn node_package_name(&self) -> String {
920 self.node
921 .as_ref()
922 .and_then(|n| n.package_name.as_ref())
923 .cloned()
924 .unwrap_or_else(|| self.crate_config.name.clone())
925 }
926
927 pub fn ruby_gem_name(&self) -> String {
929 self.ruby
930 .as_ref()
931 .and_then(|r| r.gem_name.as_ref())
932 .cloned()
933 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
934 }
935
936 pub fn php_extension_name(&self) -> String {
938 self.php
939 .as_ref()
940 .and_then(|p| p.extension_name.as_ref())
941 .cloned()
942 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
943 }
944
945 pub fn elixir_app_name(&self) -> String {
947 self.elixir
948 .as_ref()
949 .and_then(|e| e.app_name.as_ref())
950 .cloned()
951 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
952 }
953
954 pub fn try_go_module(&self) -> Result<String, String> {
963 if let Some(module) = self.go.as_ref().and_then(|g| g.module.as_ref()) {
964 return Ok(module.clone());
965 }
966 if let Ok(repo) = self.try_github_repo() {
967 if let Some(module) = derive_go_module_from_repo(&repo) {
968 return Ok(module);
969 }
970 }
971 Err(format!(
972 "no Go module configured — set `[go] module = \"...\"` or `[scaffold] repository = \"https://<host>/<org>/...\"` for crate `{}`",
973 self.crate_config.name
974 ))
975 }
976
977 pub fn go_module(&self) -> String {
979 self.try_go_module()
980 .unwrap_or_else(|_| format!("example.invalid/{}", self.crate_config.name))
981 }
982
983 pub fn try_github_repo(&self) -> Result<String, String> {
993 if let Some(e2e) = &self.e2e {
994 if let Some(url) = &e2e.registry.github_repo {
995 return Ok(url.clone());
996 }
997 }
998 if let Some(url) = self.scaffold.as_ref().and_then(|s| s.repository.as_ref()) {
999 return Ok(url.clone());
1000 }
1001 Err(format!(
1002 "no repository URL configured — set `[scaffold] repository = \"...\"` (or `[e2e.registry] github_repo`) for crate `{}`",
1003 self.crate_config.name
1004 ))
1005 }
1006
1007 pub fn github_repo(&self) -> String {
1015 self.try_github_repo()
1016 .unwrap_or_else(|_| format!("https://example.invalid/{}", self.crate_config.name))
1017 }
1018
1019 pub fn try_java_package(&self) -> Result<String, String> {
1028 if let Some(pkg) = self.java.as_ref().and_then(|j| j.package.as_ref()) {
1029 return Ok(pkg.clone());
1030 }
1031 if let Ok(repo) = self.try_github_repo() {
1032 if let Some(pkg) = derive_reverse_dns_package(&repo) {
1033 return Ok(pkg);
1034 }
1035 }
1036 Err(format!(
1037 "no Java package configured — set `[java] package = \"...\"` or `[scaffold] repository = \"https://<host>/<org>/...\"` for crate `{}`",
1038 self.crate_config.name
1039 ))
1040 }
1041
1042 pub fn java_package(&self) -> String {
1049 self.try_java_package()
1050 .unwrap_or_else(|_| "unconfigured.alef".to_string())
1051 }
1052
1053 pub fn java_group_id(&self) -> String {
1058 self.java_package()
1059 }
1060
1061 pub fn try_kotlin_package(&self) -> Result<String, String> {
1069 if let Some(pkg) = self.kotlin.as_ref().and_then(|k| k.package.as_ref()) {
1070 return Ok(pkg.clone());
1071 }
1072 if let Ok(repo) = self.try_github_repo() {
1073 if let Some(pkg) = derive_reverse_dns_package(&repo) {
1074 return Ok(pkg);
1075 }
1076 }
1077 Err(format!(
1078 "no Kotlin package configured — set `[kotlin] package = \"...\"` or `[scaffold] repository = \"https://<host>/<org>/...\"` for crate `{}`",
1079 self.crate_config.name
1080 ))
1081 }
1082
1083 pub fn kotlin_package(&self) -> String {
1085 self.try_kotlin_package()
1086 .unwrap_or_else(|_| "unconfigured.alef".to_string())
1087 }
1088
1089 pub fn kotlin_target(&self) -> KotlinTarget {
1094 self.kotlin.as_ref().map(|k| k.target).unwrap_or_default()
1095 }
1096
1097 pub fn dart_pubspec_name(&self) -> String {
1102 self.dart
1103 .as_ref()
1104 .and_then(|d| d.pubspec_name.as_ref())
1105 .cloned()
1106 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
1107 }
1108
1109 pub fn dart_frb_version(&self) -> String {
1112 self.dart
1113 .as_ref()
1114 .and_then(|d| d.frb_version.as_ref())
1115 .cloned()
1116 .unwrap_or_else(|| crate::template_versions::cargo::FLUTTER_RUST_BRIDGE.to_string())
1117 }
1118
1119 pub fn swift_module(&self) -> String {
1124 self.swift
1125 .as_ref()
1126 .and_then(|s| s.module_name.as_ref())
1127 .cloned()
1128 .unwrap_or_else(|| {
1129 use heck::ToUpperCamelCase;
1130 self.crate_config.name.to_upper_camel_case()
1131 })
1132 }
1133
1134 pub fn swift_bridge_version(&self) -> String {
1137 self.swift
1138 .as_ref()
1139 .and_then(|s| s.swift_bridge_version.as_ref())
1140 .cloned()
1141 .unwrap_or_else(|| crate::template_versions::cargo::SWIFT_BRIDGE.to_string())
1142 }
1143
1144 pub fn swift_min_macos(&self) -> String {
1146 self.swift
1147 .as_ref()
1148 .and_then(|s| s.min_macos_version.as_ref())
1149 .cloned()
1150 .unwrap_or_else(|| crate::template_versions::toolchain::SWIFT_MIN_MACOS.to_string())
1151 }
1152
1153 pub fn swift_min_ios(&self) -> String {
1155 self.swift
1156 .as_ref()
1157 .and_then(|s| s.min_ios_version.as_ref())
1158 .cloned()
1159 .unwrap_or_else(|| crate::template_versions::toolchain::SWIFT_MIN_IOS.to_string())
1160 }
1161
1162 pub fn gleam_app_name(&self) -> String {
1164 self.gleam
1165 .as_ref()
1166 .and_then(|g| g.app_name.as_ref())
1167 .cloned()
1168 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
1169 }
1170
1171 pub fn gleam_nif_module(&self) -> String {
1175 use heck::ToUpperCamelCase;
1176 self.gleam
1177 .as_ref()
1178 .and_then(|g| g.nif_module.as_ref())
1179 .cloned()
1180 .unwrap_or_else(|| {
1181 let pascal = self
1182 .elixir
1183 .as_ref()
1184 .and_then(|e| e.app_name.as_deref())
1185 .unwrap_or(&self.crate_config.name)
1186 .to_upper_camel_case();
1187 format!("Elixir.{pascal}.Native")
1188 })
1189 }
1190
1191 pub fn zig_module_name(&self) -> String {
1193 self.zig
1194 .as_ref()
1195 .and_then(|z| z.module_name.as_ref())
1196 .cloned()
1197 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
1198 }
1199
1200 pub fn csharp_namespace(&self) -> String {
1202 self.csharp
1203 .as_ref()
1204 .and_then(|c| c.namespace.as_ref())
1205 .cloned()
1206 .unwrap_or_else(|| {
1207 use heck::ToPascalCase;
1208 self.crate_config.name.to_pascal_case()
1209 })
1210 }
1211
1212 pub fn csharp_package_id(&self) -> String {
1219 self.csharp
1220 .as_ref()
1221 .and_then(|c| c.package_id.as_ref())
1222 .cloned()
1223 .unwrap_or_else(|| self.csharp_namespace())
1224 }
1225
1226 pub fn core_crate_dir(&self) -> String {
1232 if let Some(first_source) = self.crate_config.sources.first() {
1235 let path = std::path::Path::new(first_source);
1236 let mut current = path.parent();
1237 while let Some(dir) = current {
1238 if dir.file_name().is_some_and(|n| n == "src") {
1239 if let Some(crate_dir) = dir.parent() {
1240 if let Some(dir_name) = crate_dir.file_name() {
1241 return dir_name.to_string_lossy().into_owned();
1242 }
1243 }
1244 break;
1245 }
1246 current = dir.parent();
1247 }
1248 }
1249 self.crate_config.name.clone()
1250 }
1251
1252 pub fn core_crate_for_language(&self, lang: extras::Language) -> String {
1257 let override_name = match lang {
1258 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.core_crate_override.as_deref()),
1259 extras::Language::Dart => self.dart.as_ref().and_then(|c| c.core_crate_override.as_deref()),
1260 extras::Language::Swift => self.swift.as_ref().and_then(|c| c.core_crate_override.as_deref()),
1261 _ => None,
1262 };
1263 match override_name {
1264 Some(name) => name.to_string(),
1265 None => self.core_crate_dir(),
1266 }
1267 }
1268
1269 pub fn core_import_for_language(&self, lang: extras::Language) -> String {
1275 let override_name = match lang {
1276 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.core_crate_override.as_deref()),
1277 extras::Language::Dart => self.dart.as_ref().and_then(|c| c.core_crate_override.as_deref()),
1278 extras::Language::Swift => self.swift.as_ref().and_then(|c| c.core_crate_override.as_deref()),
1279 _ => None,
1280 };
1281 match override_name {
1282 Some(name) => name.replace('-', "_"),
1283 None => self.core_import(),
1284 }
1285 }
1286
1287 pub fn wasm_type_prefix(&self) -> String {
1290 self.wasm
1291 .as_ref()
1292 .and_then(|w| w.type_prefix.as_ref())
1293 .cloned()
1294 .unwrap_or_else(|| "Wasm".to_string())
1295 }
1296
1297 pub fn node_type_prefix(&self) -> String {
1300 self.node
1301 .as_ref()
1302 .and_then(|n| n.type_prefix.as_ref())
1303 .cloned()
1304 .unwrap_or_else(|| "Js".to_string())
1305 }
1306
1307 pub fn r_package_name(&self) -> String {
1309 self.r
1310 .as_ref()
1311 .and_then(|r| r.package_name.as_ref())
1312 .cloned()
1313 .unwrap_or_else(|| self.crate_config.name.clone())
1314 }
1315
1316 pub fn resolved_version(&self) -> Option<String> {
1319 let content = std::fs::read_to_string(&self.crate_config.version_from).ok()?;
1320 let value: toml::Value = toml::from_str(&content).ok()?;
1321 if let Some(v) = value
1322 .get("workspace")
1323 .and_then(|w| w.get("package"))
1324 .and_then(|p| p.get("version"))
1325 .and_then(|v| v.as_str())
1326 {
1327 return Some(v.to_string());
1328 }
1329 value
1330 .get("package")
1331 .and_then(|p| p.get("version"))
1332 .and_then(|v| v.as_str())
1333 .map(|v| v.to_string())
1334 }
1335
1336 pub fn serde_rename_all_for_language(&self, lang: extras::Language) -> String {
1344 let override_val = match lang {
1346 extras::Language::Python => self.python.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1347 extras::Language::Node => self.node.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1348 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1349 extras::Language::Php => self.php.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1350 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1351 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1352 extras::Language::Ffi => self.ffi.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1353 extras::Language::Gleam => self.gleam.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1354 extras::Language::Go => self.go.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1355 extras::Language::Java => self.java.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1356 extras::Language::Kotlin => self.kotlin.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1357 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1358 extras::Language::R => self.r.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1359 extras::Language::Zig => self.zig.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1360 extras::Language::Dart => self.dart.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1361 extras::Language::Swift => self.swift.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1362 extras::Language::Rust => None, };
1364
1365 if let Some(val) = override_val {
1366 return val.to_string();
1367 }
1368
1369 match lang {
1371 extras::Language::Node | extras::Language::Wasm | extras::Language::Java | extras::Language::Csharp => {
1372 "camelCase".to_string()
1373 }
1374 extras::Language::Python
1375 | extras::Language::Ruby
1376 | extras::Language::Php
1377 | extras::Language::Go
1378 | extras::Language::Ffi
1379 | extras::Language::Elixir
1380 | extras::Language::R
1381 | extras::Language::Rust
1382 | extras::Language::Kotlin
1383 | extras::Language::Gleam
1384 | extras::Language::Zig
1385 | extras::Language::Swift
1386 | extras::Language::Dart => "snake_case".to_string(),
1387 }
1388 }
1389
1390 pub fn rewrite_path(&self, rust_path: &str) -> String {
1393 let mut mappings: Vec<_> = self.crate_config.path_mappings.iter().collect();
1395 mappings.sort_by_key(|b| std::cmp::Reverse(b.0.len()));
1396
1397 for (from, to) in &mappings {
1398 if rust_path.starts_with(from.as_str()) {
1399 return format!("{}{}", to, &rust_path[from.len()..]);
1400 }
1401 }
1402 rust_path.to_string()
1403 }
1404
1405 pub fn effective_path_mappings(&self) -> HashMap<String, String> {
1415 let mut mappings = HashMap::new();
1416
1417 if self.crate_config.auto_path_mappings {
1418 let core_import = self.core_import();
1419
1420 for source in &self.crate_config.sources {
1421 let source_str = source.to_string_lossy();
1422 if let Some(after_crates) = find_after_crates_prefix(&source_str) {
1424 if let Some(slash_pos) = after_crates.find('/') {
1426 let crate_dir = &after_crates[..slash_pos];
1427 let crate_ident = crate_dir.replace('-', "_");
1428 if crate_ident != core_import && !mappings.contains_key(&crate_ident) {
1430 mappings.insert(crate_ident, core_import.clone());
1431 }
1432 }
1433 }
1434 }
1435 }
1436
1437 for (from, to) in &self.crate_config.path_mappings {
1439 mappings.insert(from.clone(), to.clone());
1440 }
1441
1442 mappings
1443 }
1444}
1445
1446fn find_after_crates_prefix(path: &str) -> Option<&str> {
1453 if let Some(pos) = path.find("/crates/") {
1457 return Some(&path[pos + "/crates/".len()..]);
1458 }
1459 if let Some(stripped) = path.strip_prefix("crates/") {
1460 return Some(stripped);
1461 }
1462 None
1463}
1464
1465pub fn resolve_output_dir(config_path: Option<&PathBuf>, crate_name: &str, default: &str) -> String {
1468 config_path
1469 .map(|p| p.to_string_lossy().replace("{name}", crate_name))
1470 .unwrap_or_else(|| default.replace("{name}", crate_name))
1471}
1472
1473pub fn detect_serde_available(output_dir: &str) -> bool {
1479 let src_path = std::path::Path::new(output_dir);
1480 let mut dir = src_path;
1482 loop {
1483 let cargo_toml = dir.join("Cargo.toml");
1484 if cargo_toml.exists() {
1485 return cargo_toml_has_serde(&cargo_toml);
1486 }
1487 match dir.parent() {
1488 Some(parent) if !parent.as_os_str().is_empty() => dir = parent,
1489 _ => break,
1490 }
1491 }
1492 false
1493}
1494
1495fn cargo_toml_has_serde(path: &std::path::Path) -> bool {
1501 let content = match std::fs::read_to_string(path) {
1502 Ok(c) => c,
1503 Err(_) => return false,
1504 };
1505
1506 let has_serde_json = content.contains("serde_json");
1507 let has_serde_dep = content.lines().any(|line| {
1511 let trimmed = line.trim();
1512 trimmed.starts_with("serde ")
1514 || trimmed.starts_with("serde=")
1515 || trimmed.starts_with("serde.")
1516 || trimmed == "[dependencies.serde]"
1517 });
1518
1519 has_serde_json && has_serde_dep
1520}
1521
1522#[cfg(test)]
1523mod tests {
1524 use super::*;
1525
1526 fn minimal_config() -> AlefConfig {
1527 toml::from_str(
1528 r#"
1529languages = ["python", "node", "rust"]
1530
1531[crate]
1532name = "test-lib"
1533sources = ["src/lib.rs"]
1534"#,
1535 )
1536 .unwrap()
1537 }
1538
1539 #[test]
1540 fn lint_config_falls_back_to_defaults() {
1541 let config = minimal_config();
1542 assert!(config.lint.is_none());
1543
1544 let py = config.lint_config_for_language(Language::Python);
1545 assert!(py.format.is_some());
1546 assert!(py.check.is_some());
1547 assert!(py.typecheck.is_some());
1548
1549 let node = config.lint_config_for_language(Language::Node);
1550 assert!(node.format.is_some());
1551 assert!(node.check.is_some());
1552 }
1553
1554 #[test]
1555 fn lint_config_explicit_overrides_default() {
1556 let config: AlefConfig = toml::from_str(
1557 r#"
1558languages = ["python"]
1559
1560[crate]
1561name = "test-lib"
1562sources = ["src/lib.rs"]
1563
1564[lint.python]
1565format = "custom-formatter"
1566check = "custom-checker"
1567"#,
1568 )
1569 .unwrap();
1570
1571 let py = config.lint_config_for_language(Language::Python);
1572 assert_eq!(py.format.unwrap().commands(), vec!["custom-formatter"]);
1573 assert_eq!(py.check.unwrap().commands(), vec!["custom-checker"]);
1574 assert!(py.typecheck.is_none()); }
1576
1577 #[test]
1578 fn lint_config_partial_override_does_not_merge() {
1579 let config: AlefConfig = toml::from_str(
1580 r#"
1581languages = ["python"]
1582
1583[crate]
1584name = "test-lib"
1585sources = ["src/lib.rs"]
1586
1587[lint.python]
1588format = "only-format"
1589"#,
1590 )
1591 .unwrap();
1592
1593 let py = config.lint_config_for_language(Language::Python);
1594 assert_eq!(py.format.unwrap().commands(), vec!["only-format"]);
1595 assert!(py.check.is_none());
1597 assert!(py.typecheck.is_none());
1598 }
1599
1600 #[test]
1601 fn lint_config_unconfigured_language_uses_defaults() {
1602 let config: AlefConfig = toml::from_str(
1603 r#"
1604languages = ["python", "node"]
1605
1606[crate]
1607name = "test-lib"
1608sources = ["src/lib.rs"]
1609
1610[lint.python]
1611format = "custom"
1612"#,
1613 )
1614 .unwrap();
1615
1616 let py = config.lint_config_for_language(Language::Python);
1618 assert_eq!(py.format.unwrap().commands(), vec!["custom"]);
1619
1620 let node = config.lint_config_for_language(Language::Node);
1622 let fmt = node.format.unwrap().commands().join(" ");
1623 assert!(fmt.contains("oxfmt"));
1624 }
1625
1626 #[test]
1627 fn update_config_falls_back_to_defaults() {
1628 let config = minimal_config();
1629 assert!(config.update.is_none());
1630
1631 let py = config.update_config_for_language(Language::Python);
1632 assert!(py.update.is_some());
1633 assert!(py.upgrade.is_some());
1634
1635 let rust = config.update_config_for_language(Language::Rust);
1636 let update = rust.update.unwrap().commands().join(" ");
1637 assert!(update.contains("cargo update"));
1638 }
1639
1640 #[test]
1641 fn update_config_explicit_overrides_default() {
1642 let config: AlefConfig = toml::from_str(
1643 r#"
1644languages = ["rust"]
1645
1646[crate]
1647name = "test-lib"
1648sources = ["src/lib.rs"]
1649
1650[update.rust]
1651update = "my-custom-update"
1652upgrade = ["step1", "step2"]
1653"#,
1654 )
1655 .unwrap();
1656
1657 let rust = config.update_config_for_language(Language::Rust);
1658 assert_eq!(rust.update.unwrap().commands(), vec!["my-custom-update"]);
1659 assert_eq!(rust.upgrade.unwrap().commands(), vec!["step1", "step2"]);
1660 }
1661
1662 #[test]
1663 fn test_config_falls_back_to_defaults() {
1664 let config = minimal_config();
1665 assert!(config.test.is_none());
1666
1667 let py = config.test_config_for_language(Language::Python);
1668 assert!(py.command.is_some());
1669 assert!(py.coverage.is_some());
1670 assert!(py.e2e.is_none());
1671
1672 let rust = config.test_config_for_language(Language::Rust);
1673 let cmd = rust.command.unwrap().commands().join(" ");
1674 assert!(cmd.contains("cargo test"));
1675 }
1676
1677 #[test]
1678 fn test_config_explicit_overrides_default() {
1679 let config: AlefConfig = toml::from_str(
1680 r#"
1681languages = ["python"]
1682
1683[crate]
1684name = "test-lib"
1685sources = ["src/lib.rs"]
1686
1687[test.python]
1688command = "my-custom-test"
1689"#,
1690 )
1691 .unwrap();
1692
1693 let py = config.test_config_for_language(Language::Python);
1694 assert_eq!(py.command.unwrap().commands(), vec!["my-custom-test"]);
1695 assert!(py.coverage.is_none()); }
1697
1698 #[test]
1699 fn setup_config_falls_back_to_defaults() {
1700 let config = minimal_config();
1701 assert!(config.setup.is_none());
1702
1703 let py = config.setup_config_for_language(Language::Python);
1704 assert!(py.install.is_some());
1705 let install = py.install.unwrap().commands().join(" ");
1706 assert!(install.contains("uv sync"));
1707
1708 let rust = config.setup_config_for_language(Language::Rust);
1709 let install = rust.install.unwrap().commands().join(" ");
1710 assert!(install.contains("rustup update"));
1711 }
1712
1713 #[test]
1714 fn setup_config_explicit_overrides_default() {
1715 let config: AlefConfig = toml::from_str(
1716 r#"
1717languages = ["python"]
1718
1719[crate]
1720name = "test-lib"
1721sources = ["src/lib.rs"]
1722
1723[setup.python]
1724install = "my-custom-install"
1725"#,
1726 )
1727 .unwrap();
1728
1729 let py = config.setup_config_for_language(Language::Python);
1730 assert_eq!(py.install.unwrap().commands(), vec!["my-custom-install"]);
1731 }
1732
1733 #[test]
1734 fn clean_config_falls_back_to_defaults() {
1735 let config = minimal_config();
1736 assert!(config.clean.is_none());
1737
1738 let py = config.clean_config_for_language(Language::Python);
1739 assert!(py.clean.is_some());
1740 let clean = py.clean.unwrap().commands().join(" ");
1741 assert!(clean.contains("__pycache__"));
1742
1743 let rust = config.clean_config_for_language(Language::Rust);
1744 let clean = rust.clean.unwrap().commands().join(" ");
1745 assert!(clean.contains("cargo clean"));
1746 }
1747
1748 #[test]
1749 fn clean_config_explicit_overrides_default() {
1750 let config: AlefConfig = toml::from_str(
1751 r#"
1752languages = ["rust"]
1753
1754[crate]
1755name = "test-lib"
1756sources = ["src/lib.rs"]
1757
1758[clean.rust]
1759clean = "my-custom-clean"
1760"#,
1761 )
1762 .unwrap();
1763
1764 let rust = config.clean_config_for_language(Language::Rust);
1765 assert_eq!(rust.clean.unwrap().commands(), vec!["my-custom-clean"]);
1766 }
1767
1768 #[test]
1769 fn build_command_config_falls_back_to_defaults() {
1770 let config = minimal_config();
1771 assert!(config.build_commands.is_none());
1772
1773 let py = config.build_command_config_for_language(Language::Python);
1774 assert!(py.build.is_some());
1775 assert!(py.build_release.is_some());
1776 let build = py.build.unwrap().commands().join(" ");
1777 assert!(build.contains("maturin develop"));
1778
1779 let rust = config.build_command_config_for_language(Language::Rust);
1780 let build = rust.build.unwrap().commands().join(" ");
1781 assert!(build.contains("cargo build --workspace"));
1782 }
1783
1784 #[test]
1785 fn build_command_config_explicit_overrides_default() {
1786 let config: AlefConfig = toml::from_str(
1787 r#"
1788languages = ["rust"]
1789
1790[crate]
1791name = "test-lib"
1792sources = ["src/lib.rs"]
1793
1794[build_commands.rust]
1795build = "my-custom-build"
1796build_release = "my-custom-build --release"
1797"#,
1798 )
1799 .unwrap();
1800
1801 let rust = config.build_command_config_for_language(Language::Rust);
1802 assert_eq!(rust.build.unwrap().commands(), vec!["my-custom-build"]);
1803 assert_eq!(
1804 rust.build_release.unwrap().commands(),
1805 vec!["my-custom-build --release"]
1806 );
1807 }
1808
1809 #[test]
1810 fn build_command_config_uses_crate_name() {
1811 let config = minimal_config();
1812 let py = config.build_command_config_for_language(Language::Python);
1813 let build = py.build.unwrap().commands().join(" ");
1814 assert!(
1815 build.contains("test-lib-py"),
1816 "Python build should reference crate name, got: {build}"
1817 );
1818 }
1819
1820 #[test]
1821 fn package_dir_defaults_are_correct() {
1822 let config = minimal_config();
1823 assert_eq!(config.package_dir(Language::Python), "packages/python");
1824 assert_eq!(config.package_dir(Language::Node), "packages/node");
1825 assert_eq!(config.package_dir(Language::Ruby), "packages/ruby");
1826 assert_eq!(config.package_dir(Language::Go), "packages/go");
1827 assert_eq!(config.package_dir(Language::Java), "packages/java");
1828 }
1829
1830 #[test]
1831 fn explicit_lint_config_preserves_precondition_and_before() {
1832 let config: AlefConfig = toml::from_str(
1833 r#"
1834languages = ["go"]
1835
1836[crate]
1837name = "test"
1838sources = ["src/lib.rs"]
1839
1840[lint.go]
1841precondition = "test -f target/release/libtest_ffi.so"
1842before = "cargo build --release -p test-ffi"
1843format = "gofmt -w packages/go"
1844check = "golangci-lint run ./..."
1845"#,
1846 )
1847 .unwrap();
1848
1849 let lint = config.lint_config_for_language(Language::Go);
1850 assert_eq!(
1851 lint.precondition.as_deref(),
1852 Some("test -f target/release/libtest_ffi.so"),
1853 "precondition should be preserved from explicit config"
1854 );
1855 assert_eq!(
1856 lint.before.unwrap().commands(),
1857 vec!["cargo build --release -p test-ffi"],
1858 "before should be preserved from explicit config"
1859 );
1860 }
1861
1862 #[test]
1863 fn explicit_lint_config_with_before_list_preserves_all_commands() {
1864 let config: AlefConfig = toml::from_str(
1865 r#"
1866languages = ["go"]
1867
1868[crate]
1869name = "test"
1870sources = ["src/lib.rs"]
1871
1872[lint.go]
1873before = ["cargo build --release -p test-ffi", "cp target/release/libtest_ffi.so packages/go/"]
1874check = "golangci-lint run ./..."
1875"#,
1876 )
1877 .unwrap();
1878
1879 let lint = config.lint_config_for_language(Language::Go);
1880 assert!(lint.precondition.is_none(), "precondition should be None when not set");
1881 assert_eq!(
1882 lint.before.unwrap().commands(),
1883 vec![
1884 "cargo build --release -p test-ffi",
1885 "cp target/release/libtest_ffi.so packages/go/"
1886 ],
1887 "before list should be preserved from explicit config"
1888 );
1889 }
1890
1891 #[test]
1892 fn default_lint_config_has_command_v_precondition() {
1893 let config = minimal_config();
1894 let py = config.lint_config_for_language(Language::Python);
1895 assert_eq!(py.precondition.as_deref(), Some("command -v ruff >/dev/null 2>&1"));
1896 assert!(py.before.is_none(), "default lint config should have no before");
1897
1898 let go = config.lint_config_for_language(Language::Go);
1899 assert_eq!(go.precondition.as_deref(), Some("command -v gofmt >/dev/null 2>&1"));
1900 assert!(go.before.is_none(), "default Go lint config should have no before");
1901 }
1902
1903 #[test]
1904 fn explicit_test_config_preserves_precondition_and_before() {
1905 let config: AlefConfig = toml::from_str(
1906 r#"
1907languages = ["python"]
1908
1909[crate]
1910name = "test"
1911sources = ["src/lib.rs"]
1912
1913[test.python]
1914precondition = "test -f target/release/libtest.so"
1915before = "maturin develop"
1916command = "pytest"
1917"#,
1918 )
1919 .unwrap();
1920
1921 let test = config.test_config_for_language(Language::Python);
1922 assert_eq!(
1923 test.precondition.as_deref(),
1924 Some("test -f target/release/libtest.so"),
1925 "test precondition should be preserved"
1926 );
1927 assert_eq!(
1928 test.before.unwrap().commands(),
1929 vec!["maturin develop"],
1930 "test before should be preserved"
1931 );
1932 }
1933
1934 #[test]
1935 fn default_test_config_has_command_v_precondition() {
1936 let config = minimal_config();
1937 let py = config.test_config_for_language(Language::Python);
1938 assert_eq!(py.precondition.as_deref(), Some("command -v uv >/dev/null 2>&1"));
1939 assert!(py.before.is_none(), "default test config should have no before");
1940 }
1941
1942 #[test]
1943 fn explicit_setup_config_preserves_precondition_and_before() {
1944 let config: AlefConfig = toml::from_str(
1945 r#"
1946languages = ["python"]
1947
1948[crate]
1949name = "test"
1950sources = ["src/lib.rs"]
1951
1952[setup.python]
1953precondition = "which uv"
1954before = "pip install uv"
1955install = "uv sync"
1956"#,
1957 )
1958 .unwrap();
1959
1960 let setup = config.setup_config_for_language(Language::Python);
1961 assert_eq!(
1962 setup.precondition.as_deref(),
1963 Some("which uv"),
1964 "setup precondition should be preserved"
1965 );
1966 assert_eq!(
1967 setup.before.unwrap().commands(),
1968 vec!["pip install uv"],
1969 "setup before should be preserved"
1970 );
1971 }
1972
1973 #[test]
1974 fn default_setup_config_has_command_v_precondition() {
1975 let config = minimal_config();
1976 let py = config.setup_config_for_language(Language::Python);
1977 assert_eq!(py.precondition.as_deref(), Some("command -v uv >/dev/null 2>&1"));
1978 assert!(py.before.is_none(), "default setup config should have no before");
1979 }
1980
1981 #[test]
1982 fn explicit_update_config_preserves_precondition_and_before() {
1983 let config: AlefConfig = toml::from_str(
1984 r#"
1985languages = ["rust"]
1986
1987[crate]
1988name = "test"
1989sources = ["src/lib.rs"]
1990
1991[update.rust]
1992precondition = "test -f Cargo.lock"
1993before = "cargo fetch"
1994update = "cargo update"
1995"#,
1996 )
1997 .unwrap();
1998
1999 let update = config.update_config_for_language(Language::Rust);
2000 assert_eq!(
2001 update.precondition.as_deref(),
2002 Some("test -f Cargo.lock"),
2003 "update precondition should be preserved"
2004 );
2005 assert_eq!(
2006 update.before.unwrap().commands(),
2007 vec!["cargo fetch"],
2008 "update before should be preserved"
2009 );
2010 }
2011
2012 #[test]
2013 fn default_update_config_has_command_v_precondition() {
2014 let config = minimal_config();
2015 let rust = config.update_config_for_language(Language::Rust);
2016 assert_eq!(rust.precondition.as_deref(), Some("command -v cargo >/dev/null 2>&1"));
2017 assert!(rust.before.is_none(), "default update config should have no before");
2018 }
2019
2020 #[test]
2021 fn explicit_clean_config_preserves_precondition_and_before() {
2022 let config: AlefConfig = toml::from_str(
2023 r#"
2024languages = ["rust"]
2025
2026[crate]
2027name = "test"
2028sources = ["src/lib.rs"]
2029
2030[clean.rust]
2031precondition = "test -d target"
2032before = "echo cleaning"
2033clean = "cargo clean"
2034"#,
2035 )
2036 .unwrap();
2037
2038 let clean = config.clean_config_for_language(Language::Rust);
2039 assert_eq!(
2040 clean.precondition.as_deref(),
2041 Some("test -d target"),
2042 "clean precondition should be preserved"
2043 );
2044 assert_eq!(
2045 clean.before.unwrap().commands(),
2046 vec!["echo cleaning"],
2047 "clean before should be preserved"
2048 );
2049 }
2050
2051 #[test]
2052 fn default_clean_config_precondition_matches_toolchain_use() {
2053 let config = minimal_config();
2054 let rust = config.clean_config_for_language(Language::Rust);
2056 assert_eq!(rust.precondition.as_deref(), Some("command -v cargo >/dev/null 2>&1"));
2057 assert!(rust.before.is_none(), "default clean config should have no before");
2058
2059 let py = config.clean_config_for_language(Language::Python);
2061 assert!(
2062 py.precondition.is_none(),
2063 "pure-shell clean should not have a precondition"
2064 );
2065 }
2066
2067 #[test]
2068 fn explicit_build_command_config_preserves_precondition_and_before() {
2069 let config: AlefConfig = toml::from_str(
2070 r#"
2071languages = ["go"]
2072
2073[crate]
2074name = "test"
2075sources = ["src/lib.rs"]
2076
2077[build_commands.go]
2078precondition = "which go"
2079before = "cargo build --release -p test-ffi"
2080build = "cd packages/go && go build ./..."
2081build_release = "cd packages/go && go build -ldflags='-s -w' ./..."
2082"#,
2083 )
2084 .unwrap();
2085
2086 let build = config.build_command_config_for_language(Language::Go);
2087 assert_eq!(
2088 build.precondition.as_deref(),
2089 Some("which go"),
2090 "build precondition should be preserved"
2091 );
2092 assert_eq!(
2093 build.before.unwrap().commands(),
2094 vec!["cargo build --release -p test-ffi"],
2095 "build before should be preserved"
2096 );
2097 }
2098
2099 #[test]
2100 fn default_build_command_config_has_command_v_precondition() {
2101 let config = minimal_config();
2102 let rust = config.build_command_config_for_language(Language::Rust);
2103 assert_eq!(rust.precondition.as_deref(), Some("command -v cargo >/dev/null 2>&1"));
2104 assert!(
2105 rust.before.is_none(),
2106 "default build command config should have no before"
2107 );
2108 }
2109
2110 #[test]
2111 fn version_defaults_to_none_when_omitted() {
2112 let config = minimal_config();
2113 assert!(config.version.is_none());
2114 }
2115
2116 #[test]
2117 fn version_parses_from_top_level_key() {
2118 let config: AlefConfig = toml::from_str(
2119 r#"
2120version = "0.7.7"
2121languages = ["python"]
2122
2123[crate]
2124name = "test-lib"
2125sources = ["src/lib.rs"]
2126"#,
2127 )
2128 .unwrap();
2129 assert_eq!(config.version.as_deref(), Some("0.7.7"));
2130 }
2131}