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::{BridgeBinding, 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 {
908 use heck::ToPascalCase;
909 if let Some(ns) = self.php.as_ref().and_then(|p| p.namespace.as_ref()) {
911 return ns.clone();
912 }
913 let ext = self.php_extension_name();
914 if ext.contains('_') {
915 ext.split('_')
916 .map(|p| p.to_pascal_case())
917 .collect::<Vec<_>>()
918 .join("\\")
919 } else {
920 ext.to_pascal_case()
921 }
922 }
923
924 pub fn node_package_name(&self) -> String {
926 self.node
927 .as_ref()
928 .and_then(|n| n.package_name.as_ref())
929 .cloned()
930 .unwrap_or_else(|| self.crate_config.name.clone())
931 }
932
933 pub fn ruby_gem_name(&self) -> String {
935 self.ruby
936 .as_ref()
937 .and_then(|r| r.gem_name.as_ref())
938 .cloned()
939 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
940 }
941
942 pub fn php_extension_name(&self) -> String {
944 self.php
945 .as_ref()
946 .and_then(|p| p.extension_name.as_ref())
947 .cloned()
948 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
949 }
950
951 pub fn elixir_app_name(&self) -> String {
953 self.elixir
954 .as_ref()
955 .and_then(|e| e.app_name.as_ref())
956 .cloned()
957 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
958 }
959
960 pub fn try_go_module(&self) -> Result<String, String> {
969 if let Some(module) = self.go.as_ref().and_then(|g| g.module.as_ref()) {
970 return Ok(module.clone());
971 }
972 if let Ok(repo) = self.try_github_repo() {
973 if let Some(module) = derive_go_module_from_repo(&repo) {
974 return Ok(module);
975 }
976 }
977 Err(format!(
978 "no Go module configured — set `[go] module = \"...\"` or `[scaffold] repository = \"https://<host>/<org>/...\"` for crate `{}`",
979 self.crate_config.name
980 ))
981 }
982
983 pub fn go_module(&self) -> String {
985 self.try_go_module()
986 .unwrap_or_else(|_| format!("example.invalid/{}", self.crate_config.name))
987 }
988
989 pub fn try_github_repo(&self) -> Result<String, String> {
999 if let Some(e2e) = &self.e2e {
1000 if let Some(url) = &e2e.registry.github_repo {
1001 return Ok(url.clone());
1002 }
1003 }
1004 if let Some(url) = self.scaffold.as_ref().and_then(|s| s.repository.as_ref()) {
1005 return Ok(url.clone());
1006 }
1007 Err(format!(
1008 "no repository URL configured — set `[scaffold] repository = \"...\"` (or `[e2e.registry] github_repo`) for crate `{}`",
1009 self.crate_config.name
1010 ))
1011 }
1012
1013 pub fn github_repo(&self) -> String {
1021 self.try_github_repo()
1022 .unwrap_or_else(|_| format!("https://example.invalid/{}", self.crate_config.name))
1023 }
1024
1025 pub fn try_java_package(&self) -> Result<String, String> {
1034 if let Some(pkg) = self.java.as_ref().and_then(|j| j.package.as_ref()) {
1035 return Ok(pkg.clone());
1036 }
1037 if let Ok(repo) = self.try_github_repo() {
1038 if let Some(pkg) = derive_reverse_dns_package(&repo) {
1039 return Ok(pkg);
1040 }
1041 }
1042 Err(format!(
1043 "no Java package configured — set `[java] package = \"...\"` or `[scaffold] repository = \"https://<host>/<org>/...\"` for crate `{}`",
1044 self.crate_config.name
1045 ))
1046 }
1047
1048 pub fn java_package(&self) -> String {
1055 self.try_java_package()
1056 .unwrap_or_else(|_| "unconfigured.alef".to_string())
1057 }
1058
1059 pub fn java_group_id(&self) -> String {
1064 self.java_package()
1065 }
1066
1067 pub fn try_kotlin_package(&self) -> Result<String, String> {
1075 if let Some(pkg) = self.kotlin.as_ref().and_then(|k| k.package.as_ref()) {
1076 return Ok(pkg.clone());
1077 }
1078 if let Ok(repo) = self.try_github_repo() {
1079 if let Some(pkg) = derive_reverse_dns_package(&repo) {
1080 return Ok(pkg);
1081 }
1082 }
1083 Err(format!(
1084 "no Kotlin package configured — set `[kotlin] package = \"...\"` or `[scaffold] repository = \"https://<host>/<org>/...\"` for crate `{}`",
1085 self.crate_config.name
1086 ))
1087 }
1088
1089 pub fn kotlin_package(&self) -> String {
1091 self.try_kotlin_package()
1092 .unwrap_or_else(|_| "unconfigured.alef".to_string())
1093 }
1094
1095 pub fn kotlin_target(&self) -> KotlinTarget {
1100 self.kotlin.as_ref().map(|k| k.target).unwrap_or_default()
1101 }
1102
1103 pub fn dart_pubspec_name(&self) -> String {
1108 self.dart
1109 .as_ref()
1110 .and_then(|d| d.pubspec_name.as_ref())
1111 .cloned()
1112 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
1113 }
1114
1115 pub fn dart_frb_version(&self) -> String {
1118 self.dart
1119 .as_ref()
1120 .and_then(|d| d.frb_version.as_ref())
1121 .cloned()
1122 .unwrap_or_else(|| crate::template_versions::cargo::FLUTTER_RUST_BRIDGE.to_string())
1123 }
1124
1125 pub fn swift_module(&self) -> String {
1130 self.swift
1131 .as_ref()
1132 .and_then(|s| s.module_name.as_ref())
1133 .cloned()
1134 .unwrap_or_else(|| {
1135 use heck::ToUpperCamelCase;
1136 self.crate_config.name.to_upper_camel_case()
1137 })
1138 }
1139
1140 pub fn swift_bridge_version(&self) -> String {
1143 self.swift
1144 .as_ref()
1145 .and_then(|s| s.swift_bridge_version.as_ref())
1146 .cloned()
1147 .unwrap_or_else(|| crate::template_versions::cargo::SWIFT_BRIDGE.to_string())
1148 }
1149
1150 pub fn swift_min_macos(&self) -> String {
1152 self.swift
1153 .as_ref()
1154 .and_then(|s| s.min_macos_version.as_ref())
1155 .cloned()
1156 .unwrap_or_else(|| crate::template_versions::toolchain::SWIFT_MIN_MACOS.to_string())
1157 }
1158
1159 pub fn swift_min_ios(&self) -> String {
1161 self.swift
1162 .as_ref()
1163 .and_then(|s| s.min_ios_version.as_ref())
1164 .cloned()
1165 .unwrap_or_else(|| crate::template_versions::toolchain::SWIFT_MIN_IOS.to_string())
1166 }
1167
1168 pub fn gleam_app_name(&self) -> String {
1170 self.gleam
1171 .as_ref()
1172 .and_then(|g| g.app_name.as_ref())
1173 .cloned()
1174 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
1175 }
1176
1177 pub fn gleam_nif_module(&self) -> String {
1181 use heck::ToUpperCamelCase;
1182 self.gleam
1183 .as_ref()
1184 .and_then(|g| g.nif_module.as_ref())
1185 .cloned()
1186 .unwrap_or_else(|| {
1187 let pascal = self
1188 .elixir
1189 .as_ref()
1190 .and_then(|e| e.app_name.as_deref())
1191 .unwrap_or(&self.crate_config.name)
1192 .to_upper_camel_case();
1193 format!("Elixir.{pascal}.Native")
1194 })
1195 }
1196
1197 pub fn zig_module_name(&self) -> String {
1199 self.zig
1200 .as_ref()
1201 .and_then(|z| z.module_name.as_ref())
1202 .cloned()
1203 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
1204 }
1205
1206 pub fn csharp_namespace(&self) -> String {
1208 self.csharp
1209 .as_ref()
1210 .and_then(|c| c.namespace.as_ref())
1211 .cloned()
1212 .unwrap_or_else(|| {
1213 use heck::ToPascalCase;
1214 self.crate_config.name.to_pascal_case()
1215 })
1216 }
1217
1218 pub fn csharp_package_id(&self) -> String {
1225 self.csharp
1226 .as_ref()
1227 .and_then(|c| c.package_id.as_ref())
1228 .cloned()
1229 .unwrap_or_else(|| self.csharp_namespace())
1230 }
1231
1232 pub fn core_crate_dir(&self) -> String {
1238 if let Some(first_source) = self.crate_config.sources.first() {
1241 let path = std::path::Path::new(first_source);
1242 let mut current = path.parent();
1243 while let Some(dir) = current {
1244 if dir.file_name().is_some_and(|n| n == "src") {
1245 if let Some(crate_dir) = dir.parent() {
1246 if let Some(dir_name) = crate_dir.file_name() {
1247 return dir_name.to_string_lossy().into_owned();
1248 }
1249 }
1250 break;
1251 }
1252 current = dir.parent();
1253 }
1254 }
1255 self.crate_config.name.clone()
1256 }
1257
1258 pub fn core_crate_for_language(&self, lang: extras::Language) -> String {
1263 let override_name = match lang {
1264 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.core_crate_override.as_deref()),
1265 extras::Language::Dart => self.dart.as_ref().and_then(|c| c.core_crate_override.as_deref()),
1266 extras::Language::Swift => self.swift.as_ref().and_then(|c| c.core_crate_override.as_deref()),
1267 _ => None,
1268 };
1269 match override_name {
1270 Some(name) => name.to_string(),
1271 None => self.core_crate_dir(),
1272 }
1273 }
1274
1275 pub fn core_import_for_language(&self, lang: extras::Language) -> String {
1281 let override_name = match lang {
1282 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.core_crate_override.as_deref()),
1283 extras::Language::Dart => self.dart.as_ref().and_then(|c| c.core_crate_override.as_deref()),
1284 extras::Language::Swift => self.swift.as_ref().and_then(|c| c.core_crate_override.as_deref()),
1285 _ => None,
1286 };
1287 match override_name {
1288 Some(name) => name.replace('-', "_"),
1289 None => self.core_import(),
1290 }
1291 }
1292
1293 pub fn wasm_type_prefix(&self) -> String {
1296 self.wasm
1297 .as_ref()
1298 .and_then(|w| w.type_prefix.as_ref())
1299 .cloned()
1300 .unwrap_or_else(|| "Wasm".to_string())
1301 }
1302
1303 pub fn node_type_prefix(&self) -> String {
1306 self.node
1307 .as_ref()
1308 .and_then(|n| n.type_prefix.as_ref())
1309 .cloned()
1310 .unwrap_or_else(|| "Js".to_string())
1311 }
1312
1313 pub fn r_package_name(&self) -> String {
1315 self.r
1316 .as_ref()
1317 .and_then(|r| r.package_name.as_ref())
1318 .cloned()
1319 .unwrap_or_else(|| self.crate_config.name.clone())
1320 }
1321
1322 pub fn resolved_version(&self) -> Option<String> {
1325 let content = std::fs::read_to_string(&self.crate_config.version_from).ok()?;
1326 let value: toml::Value = toml::from_str(&content).ok()?;
1327 if let Some(v) = value
1328 .get("workspace")
1329 .and_then(|w| w.get("package"))
1330 .and_then(|p| p.get("version"))
1331 .and_then(|v| v.as_str())
1332 {
1333 return Some(v.to_string());
1334 }
1335 value
1336 .get("package")
1337 .and_then(|p| p.get("version"))
1338 .and_then(|v| v.as_str())
1339 .map(|v| v.to_string())
1340 }
1341
1342 pub fn serde_rename_all_for_language(&self, lang: extras::Language) -> String {
1350 let override_val = match lang {
1352 extras::Language::Python => self.python.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1353 extras::Language::Node => self.node.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1354 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1355 extras::Language::Php => self.php.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1356 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1357 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1358 extras::Language::Ffi => self.ffi.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1359 extras::Language::Gleam => self.gleam.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1360 extras::Language::Go => self.go.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1361 extras::Language::Java => self.java.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1362 extras::Language::Kotlin => self.kotlin.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1363 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1364 extras::Language::R => self.r.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1365 extras::Language::Zig => self.zig.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1366 extras::Language::Dart => self.dart.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1367 extras::Language::Swift => self.swift.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
1368 extras::Language::Rust => None, };
1370
1371 if let Some(val) = override_val {
1372 return val.to_string();
1373 }
1374
1375 match lang {
1377 extras::Language::Node | extras::Language::Wasm | extras::Language::Java | extras::Language::Csharp => {
1378 "camelCase".to_string()
1379 }
1380 extras::Language::Python
1381 | extras::Language::Ruby
1382 | extras::Language::Php
1383 | extras::Language::Go
1384 | extras::Language::Ffi
1385 | extras::Language::Elixir
1386 | extras::Language::R
1387 | extras::Language::Rust
1388 | extras::Language::Kotlin
1389 | extras::Language::Gleam
1390 | extras::Language::Zig
1391 | extras::Language::Swift
1392 | extras::Language::Dart => "snake_case".to_string(),
1393 }
1394 }
1395
1396 pub fn rewrite_path(&self, rust_path: &str) -> String {
1399 let mut mappings: Vec<_> = self.crate_config.path_mappings.iter().collect();
1401 mappings.sort_by_key(|b| std::cmp::Reverse(b.0.len()));
1402
1403 for (from, to) in &mappings {
1404 if rust_path.starts_with(from.as_str()) {
1405 return format!("{}{}", to, &rust_path[from.len()..]);
1406 }
1407 }
1408 rust_path.to_string()
1409 }
1410
1411 pub fn effective_path_mappings(&self) -> HashMap<String, String> {
1421 let mut mappings = HashMap::new();
1422
1423 if self.crate_config.auto_path_mappings {
1424 let core_import = self.core_import();
1425
1426 for source in &self.crate_config.sources {
1427 let source_str = source.to_string_lossy();
1428 if let Some(after_crates) = find_after_crates_prefix(&source_str) {
1430 if let Some(slash_pos) = after_crates.find('/') {
1432 let crate_dir = &after_crates[..slash_pos];
1433 let crate_ident = crate_dir.replace('-', "_");
1434 if crate_ident != core_import && !mappings.contains_key(&crate_ident) {
1436 mappings.insert(crate_ident, core_import.clone());
1437 }
1438 }
1439 }
1440 }
1441 }
1442
1443 for (from, to) in &self.crate_config.path_mappings {
1445 mappings.insert(from.clone(), to.clone());
1446 }
1447
1448 mappings
1449 }
1450}
1451
1452fn find_after_crates_prefix(path: &str) -> Option<&str> {
1459 if let Some(pos) = path.find("/crates/") {
1463 return Some(&path[pos + "/crates/".len()..]);
1464 }
1465 if let Some(stripped) = path.strip_prefix("crates/") {
1466 return Some(stripped);
1467 }
1468 None
1469}
1470
1471pub fn resolve_output_dir(config_path: Option<&PathBuf>, crate_name: &str, default: &str) -> String {
1474 config_path
1475 .map(|p| p.to_string_lossy().replace("{name}", crate_name))
1476 .unwrap_or_else(|| default.replace("{name}", crate_name))
1477}
1478
1479pub fn detect_serde_available(output_dir: &str) -> bool {
1485 let src_path = std::path::Path::new(output_dir);
1486 let mut dir = src_path;
1488 loop {
1489 let cargo_toml = dir.join("Cargo.toml");
1490 if cargo_toml.exists() {
1491 return cargo_toml_has_serde(&cargo_toml);
1492 }
1493 match dir.parent() {
1494 Some(parent) if !parent.as_os_str().is_empty() => dir = parent,
1495 _ => break,
1496 }
1497 }
1498 false
1499}
1500
1501fn cargo_toml_has_serde(path: &std::path::Path) -> bool {
1507 let content = match std::fs::read_to_string(path) {
1508 Ok(c) => c,
1509 Err(_) => return false,
1510 };
1511
1512 let has_serde_json = content.contains("serde_json");
1513 let has_serde_dep = content.lines().any(|line| {
1517 let trimmed = line.trim();
1518 trimmed.starts_with("serde ")
1520 || trimmed.starts_with("serde=")
1521 || trimmed.starts_with("serde.")
1522 || trimmed == "[dependencies.serde]"
1523 });
1524
1525 has_serde_json && has_serde_dep
1526}
1527
1528#[cfg(test)]
1529mod tests {
1530 use super::*;
1531
1532 fn minimal_config() -> AlefConfig {
1533 toml::from_str(
1534 r#"
1535languages = ["python", "node", "rust"]
1536
1537[crate]
1538name = "test-lib"
1539sources = ["src/lib.rs"]
1540"#,
1541 )
1542 .unwrap()
1543 }
1544
1545 #[test]
1546 fn lint_config_falls_back_to_defaults() {
1547 let config = minimal_config();
1548 assert!(config.lint.is_none());
1549
1550 let py = config.lint_config_for_language(Language::Python);
1551 assert!(py.format.is_some());
1552 assert!(py.check.is_some());
1553 assert!(py.typecheck.is_some());
1554
1555 let node = config.lint_config_for_language(Language::Node);
1556 assert!(node.format.is_some());
1557 assert!(node.check.is_some());
1558 }
1559
1560 #[test]
1561 fn lint_config_explicit_overrides_default() {
1562 let config: AlefConfig = toml::from_str(
1563 r#"
1564languages = ["python"]
1565
1566[crate]
1567name = "test-lib"
1568sources = ["src/lib.rs"]
1569
1570[lint.python]
1571format = "custom-formatter"
1572check = "custom-checker"
1573"#,
1574 )
1575 .unwrap();
1576
1577 let py = config.lint_config_for_language(Language::Python);
1578 assert_eq!(py.format.unwrap().commands(), vec!["custom-formatter"]);
1579 assert_eq!(py.check.unwrap().commands(), vec!["custom-checker"]);
1580 assert!(py.typecheck.is_none()); }
1582
1583 #[test]
1584 fn lint_config_partial_override_does_not_merge() {
1585 let config: AlefConfig = toml::from_str(
1586 r#"
1587languages = ["python"]
1588
1589[crate]
1590name = "test-lib"
1591sources = ["src/lib.rs"]
1592
1593[lint.python]
1594format = "only-format"
1595"#,
1596 )
1597 .unwrap();
1598
1599 let py = config.lint_config_for_language(Language::Python);
1600 assert_eq!(py.format.unwrap().commands(), vec!["only-format"]);
1601 assert!(py.check.is_none());
1603 assert!(py.typecheck.is_none());
1604 }
1605
1606 #[test]
1607 fn lint_config_unconfigured_language_uses_defaults() {
1608 let config: AlefConfig = toml::from_str(
1609 r#"
1610languages = ["python", "node"]
1611
1612[crate]
1613name = "test-lib"
1614sources = ["src/lib.rs"]
1615
1616[lint.python]
1617format = "custom"
1618"#,
1619 )
1620 .unwrap();
1621
1622 let py = config.lint_config_for_language(Language::Python);
1624 assert_eq!(py.format.unwrap().commands(), vec!["custom"]);
1625
1626 let node = config.lint_config_for_language(Language::Node);
1628 let fmt = node.format.unwrap().commands().join(" ");
1629 assert!(fmt.contains("oxfmt"));
1630 }
1631
1632 #[test]
1633 fn update_config_falls_back_to_defaults() {
1634 let config = minimal_config();
1635 assert!(config.update.is_none());
1636
1637 let py = config.update_config_for_language(Language::Python);
1638 assert!(py.update.is_some());
1639 assert!(py.upgrade.is_some());
1640
1641 let rust = config.update_config_for_language(Language::Rust);
1642 let update = rust.update.unwrap().commands().join(" ");
1643 assert!(update.contains("cargo update"));
1644 }
1645
1646 #[test]
1647 fn update_config_explicit_overrides_default() {
1648 let config: AlefConfig = toml::from_str(
1649 r#"
1650languages = ["rust"]
1651
1652[crate]
1653name = "test-lib"
1654sources = ["src/lib.rs"]
1655
1656[update.rust]
1657update = "my-custom-update"
1658upgrade = ["step1", "step2"]
1659"#,
1660 )
1661 .unwrap();
1662
1663 let rust = config.update_config_for_language(Language::Rust);
1664 assert_eq!(rust.update.unwrap().commands(), vec!["my-custom-update"]);
1665 assert_eq!(rust.upgrade.unwrap().commands(), vec!["step1", "step2"]);
1666 }
1667
1668 #[test]
1669 fn test_config_falls_back_to_defaults() {
1670 let config = minimal_config();
1671 assert!(config.test.is_none());
1672
1673 let py = config.test_config_for_language(Language::Python);
1674 assert!(py.command.is_some());
1675 assert!(py.coverage.is_some());
1676 assert!(py.e2e.is_none());
1677
1678 let rust = config.test_config_for_language(Language::Rust);
1679 let cmd = rust.command.unwrap().commands().join(" ");
1680 assert!(cmd.contains("cargo test"));
1681 }
1682
1683 #[test]
1684 fn test_config_explicit_overrides_default() {
1685 let config: AlefConfig = toml::from_str(
1686 r#"
1687languages = ["python"]
1688
1689[crate]
1690name = "test-lib"
1691sources = ["src/lib.rs"]
1692
1693[test.python]
1694command = "my-custom-test"
1695"#,
1696 )
1697 .unwrap();
1698
1699 let py = config.test_config_for_language(Language::Python);
1700 assert_eq!(py.command.unwrap().commands(), vec!["my-custom-test"]);
1701 assert!(py.coverage.is_none()); }
1703
1704 #[test]
1705 fn setup_config_falls_back_to_defaults() {
1706 let config = minimal_config();
1707 assert!(config.setup.is_none());
1708
1709 let py = config.setup_config_for_language(Language::Python);
1710 assert!(py.install.is_some());
1711 let install = py.install.unwrap().commands().join(" ");
1712 assert!(install.contains("uv sync"));
1713
1714 let rust = config.setup_config_for_language(Language::Rust);
1715 let install = rust.install.unwrap().commands().join(" ");
1716 assert!(install.contains("rustup update"));
1717 }
1718
1719 #[test]
1720 fn setup_config_explicit_overrides_default() {
1721 let config: AlefConfig = toml::from_str(
1722 r#"
1723languages = ["python"]
1724
1725[crate]
1726name = "test-lib"
1727sources = ["src/lib.rs"]
1728
1729[setup.python]
1730install = "my-custom-install"
1731"#,
1732 )
1733 .unwrap();
1734
1735 let py = config.setup_config_for_language(Language::Python);
1736 assert_eq!(py.install.unwrap().commands(), vec!["my-custom-install"]);
1737 }
1738
1739 #[test]
1740 fn clean_config_falls_back_to_defaults() {
1741 let config = minimal_config();
1742 assert!(config.clean.is_none());
1743
1744 let py = config.clean_config_for_language(Language::Python);
1745 assert!(py.clean.is_some());
1746 let clean = py.clean.unwrap().commands().join(" ");
1747 assert!(clean.contains("__pycache__"));
1748
1749 let rust = config.clean_config_for_language(Language::Rust);
1750 let clean = rust.clean.unwrap().commands().join(" ");
1751 assert!(clean.contains("cargo clean"));
1752 }
1753
1754 #[test]
1755 fn clean_config_explicit_overrides_default() {
1756 let config: AlefConfig = toml::from_str(
1757 r#"
1758languages = ["rust"]
1759
1760[crate]
1761name = "test-lib"
1762sources = ["src/lib.rs"]
1763
1764[clean.rust]
1765clean = "my-custom-clean"
1766"#,
1767 )
1768 .unwrap();
1769
1770 let rust = config.clean_config_for_language(Language::Rust);
1771 assert_eq!(rust.clean.unwrap().commands(), vec!["my-custom-clean"]);
1772 }
1773
1774 #[test]
1775 fn build_command_config_falls_back_to_defaults() {
1776 let config = minimal_config();
1777 assert!(config.build_commands.is_none());
1778
1779 let py = config.build_command_config_for_language(Language::Python);
1780 assert!(py.build.is_some());
1781 assert!(py.build_release.is_some());
1782 let build = py.build.unwrap().commands().join(" ");
1783 assert!(build.contains("maturin develop"));
1784
1785 let rust = config.build_command_config_for_language(Language::Rust);
1786 let build = rust.build.unwrap().commands().join(" ");
1787 assert!(build.contains("cargo build --workspace"));
1788 }
1789
1790 #[test]
1791 fn build_command_config_explicit_overrides_default() {
1792 let config: AlefConfig = toml::from_str(
1793 r#"
1794languages = ["rust"]
1795
1796[crate]
1797name = "test-lib"
1798sources = ["src/lib.rs"]
1799
1800[build_commands.rust]
1801build = "my-custom-build"
1802build_release = "my-custom-build --release"
1803"#,
1804 )
1805 .unwrap();
1806
1807 let rust = config.build_command_config_for_language(Language::Rust);
1808 assert_eq!(rust.build.unwrap().commands(), vec!["my-custom-build"]);
1809 assert_eq!(
1810 rust.build_release.unwrap().commands(),
1811 vec!["my-custom-build --release"]
1812 );
1813 }
1814
1815 #[test]
1816 fn build_command_config_uses_crate_name() {
1817 let config = minimal_config();
1818 let py = config.build_command_config_for_language(Language::Python);
1819 let build = py.build.unwrap().commands().join(" ");
1820 assert!(
1821 build.contains("test-lib-py"),
1822 "Python build should reference crate name, got: {build}"
1823 );
1824 }
1825
1826 #[test]
1827 fn package_dir_defaults_are_correct() {
1828 let config = minimal_config();
1829 assert_eq!(config.package_dir(Language::Python), "packages/python");
1830 assert_eq!(config.package_dir(Language::Node), "packages/node");
1831 assert_eq!(config.package_dir(Language::Ruby), "packages/ruby");
1832 assert_eq!(config.package_dir(Language::Go), "packages/go");
1833 assert_eq!(config.package_dir(Language::Java), "packages/java");
1834 }
1835
1836 #[test]
1837 fn explicit_lint_config_preserves_precondition_and_before() {
1838 let config: AlefConfig = toml::from_str(
1839 r#"
1840languages = ["go"]
1841
1842[crate]
1843name = "test"
1844sources = ["src/lib.rs"]
1845
1846[lint.go]
1847precondition = "test -f target/release/libtest_ffi.so"
1848before = "cargo build --release -p test-ffi"
1849format = "gofmt -w packages/go"
1850check = "golangci-lint run ./..."
1851"#,
1852 )
1853 .unwrap();
1854
1855 let lint = config.lint_config_for_language(Language::Go);
1856 assert_eq!(
1857 lint.precondition.as_deref(),
1858 Some("test -f target/release/libtest_ffi.so"),
1859 "precondition should be preserved from explicit config"
1860 );
1861 assert_eq!(
1862 lint.before.unwrap().commands(),
1863 vec!["cargo build --release -p test-ffi"],
1864 "before should be preserved from explicit config"
1865 );
1866 }
1867
1868 #[test]
1869 fn explicit_lint_config_with_before_list_preserves_all_commands() {
1870 let config: AlefConfig = toml::from_str(
1871 r#"
1872languages = ["go"]
1873
1874[crate]
1875name = "test"
1876sources = ["src/lib.rs"]
1877
1878[lint.go]
1879before = ["cargo build --release -p test-ffi", "cp target/release/libtest_ffi.so packages/go/"]
1880check = "golangci-lint run ./..."
1881"#,
1882 )
1883 .unwrap();
1884
1885 let lint = config.lint_config_for_language(Language::Go);
1886 assert!(lint.precondition.is_none(), "precondition should be None when not set");
1887 assert_eq!(
1888 lint.before.unwrap().commands(),
1889 vec![
1890 "cargo build --release -p test-ffi",
1891 "cp target/release/libtest_ffi.so packages/go/"
1892 ],
1893 "before list should be preserved from explicit config"
1894 );
1895 }
1896
1897 #[test]
1898 fn default_lint_config_has_command_v_precondition() {
1899 let config = minimal_config();
1900 let py = config.lint_config_for_language(Language::Python);
1901 assert_eq!(py.precondition.as_deref(), Some("command -v ruff >/dev/null 2>&1"));
1902 assert!(py.before.is_none(), "default lint config should have no before");
1903
1904 let go = config.lint_config_for_language(Language::Go);
1905 assert_eq!(go.precondition.as_deref(), Some("command -v gofmt >/dev/null 2>&1"));
1906 assert!(go.before.is_none(), "default Go lint config should have no before");
1907 }
1908
1909 #[test]
1910 fn explicit_test_config_preserves_precondition_and_before() {
1911 let config: AlefConfig = toml::from_str(
1912 r#"
1913languages = ["python"]
1914
1915[crate]
1916name = "test"
1917sources = ["src/lib.rs"]
1918
1919[test.python]
1920precondition = "test -f target/release/libtest.so"
1921before = "maturin develop"
1922command = "pytest"
1923"#,
1924 )
1925 .unwrap();
1926
1927 let test = config.test_config_for_language(Language::Python);
1928 assert_eq!(
1929 test.precondition.as_deref(),
1930 Some("test -f target/release/libtest.so"),
1931 "test precondition should be preserved"
1932 );
1933 assert_eq!(
1934 test.before.unwrap().commands(),
1935 vec!["maturin develop"],
1936 "test before should be preserved"
1937 );
1938 }
1939
1940 #[test]
1941 fn default_test_config_has_command_v_precondition() {
1942 let config = minimal_config();
1943 let py = config.test_config_for_language(Language::Python);
1944 assert_eq!(py.precondition.as_deref(), Some("command -v uv >/dev/null 2>&1"));
1945 assert!(py.before.is_none(), "default test config should have no before");
1946 }
1947
1948 #[test]
1949 fn explicit_setup_config_preserves_precondition_and_before() {
1950 let config: AlefConfig = toml::from_str(
1951 r#"
1952languages = ["python"]
1953
1954[crate]
1955name = "test"
1956sources = ["src/lib.rs"]
1957
1958[setup.python]
1959precondition = "which uv"
1960before = "pip install uv"
1961install = "uv sync"
1962"#,
1963 )
1964 .unwrap();
1965
1966 let setup = config.setup_config_for_language(Language::Python);
1967 assert_eq!(
1968 setup.precondition.as_deref(),
1969 Some("which uv"),
1970 "setup precondition should be preserved"
1971 );
1972 assert_eq!(
1973 setup.before.unwrap().commands(),
1974 vec!["pip install uv"],
1975 "setup before should be preserved"
1976 );
1977 }
1978
1979 #[test]
1980 fn default_setup_config_has_command_v_precondition() {
1981 let config = minimal_config();
1982 let py = config.setup_config_for_language(Language::Python);
1983 assert_eq!(py.precondition.as_deref(), Some("command -v uv >/dev/null 2>&1"));
1984 assert!(py.before.is_none(), "default setup config should have no before");
1985 }
1986
1987 #[test]
1988 fn explicit_update_config_preserves_precondition_and_before() {
1989 let config: AlefConfig = toml::from_str(
1990 r#"
1991languages = ["rust"]
1992
1993[crate]
1994name = "test"
1995sources = ["src/lib.rs"]
1996
1997[update.rust]
1998precondition = "test -f Cargo.lock"
1999before = "cargo fetch"
2000update = "cargo update"
2001"#,
2002 )
2003 .unwrap();
2004
2005 let update = config.update_config_for_language(Language::Rust);
2006 assert_eq!(
2007 update.precondition.as_deref(),
2008 Some("test -f Cargo.lock"),
2009 "update precondition should be preserved"
2010 );
2011 assert_eq!(
2012 update.before.unwrap().commands(),
2013 vec!["cargo fetch"],
2014 "update before should be preserved"
2015 );
2016 }
2017
2018 #[test]
2019 fn default_update_config_has_command_v_precondition() {
2020 let config = minimal_config();
2021 let rust = config.update_config_for_language(Language::Rust);
2022 assert_eq!(rust.precondition.as_deref(), Some("command -v cargo >/dev/null 2>&1"));
2023 assert!(rust.before.is_none(), "default update config should have no before");
2024 }
2025
2026 #[test]
2027 fn explicit_clean_config_preserves_precondition_and_before() {
2028 let config: AlefConfig = toml::from_str(
2029 r#"
2030languages = ["rust"]
2031
2032[crate]
2033name = "test"
2034sources = ["src/lib.rs"]
2035
2036[clean.rust]
2037precondition = "test -d target"
2038before = "echo cleaning"
2039clean = "cargo clean"
2040"#,
2041 )
2042 .unwrap();
2043
2044 let clean = config.clean_config_for_language(Language::Rust);
2045 assert_eq!(
2046 clean.precondition.as_deref(),
2047 Some("test -d target"),
2048 "clean precondition should be preserved"
2049 );
2050 assert_eq!(
2051 clean.before.unwrap().commands(),
2052 vec!["echo cleaning"],
2053 "clean before should be preserved"
2054 );
2055 }
2056
2057 #[test]
2058 fn default_clean_config_precondition_matches_toolchain_use() {
2059 let config = minimal_config();
2060 let rust = config.clean_config_for_language(Language::Rust);
2062 assert_eq!(rust.precondition.as_deref(), Some("command -v cargo >/dev/null 2>&1"));
2063 assert!(rust.before.is_none(), "default clean config should have no before");
2064
2065 let py = config.clean_config_for_language(Language::Python);
2067 assert!(
2068 py.precondition.is_none(),
2069 "pure-shell clean should not have a precondition"
2070 );
2071 }
2072
2073 #[test]
2074 fn explicit_build_command_config_preserves_precondition_and_before() {
2075 let config: AlefConfig = toml::from_str(
2076 r#"
2077languages = ["go"]
2078
2079[crate]
2080name = "test"
2081sources = ["src/lib.rs"]
2082
2083[build_commands.go]
2084precondition = "which go"
2085before = "cargo build --release -p test-ffi"
2086build = "cd packages/go && go build ./..."
2087build_release = "cd packages/go && go build -ldflags='-s -w' ./..."
2088"#,
2089 )
2090 .unwrap();
2091
2092 let build = config.build_command_config_for_language(Language::Go);
2093 assert_eq!(
2094 build.precondition.as_deref(),
2095 Some("which go"),
2096 "build precondition should be preserved"
2097 );
2098 assert_eq!(
2099 build.before.unwrap().commands(),
2100 vec!["cargo build --release -p test-ffi"],
2101 "build before should be preserved"
2102 );
2103 }
2104
2105 #[test]
2106 fn default_build_command_config_has_command_v_precondition() {
2107 let config = minimal_config();
2108 let rust = config.build_command_config_for_language(Language::Rust);
2109 assert_eq!(rust.precondition.as_deref(), Some("command -v cargo >/dev/null 2>&1"));
2110 assert!(
2111 rust.before.is_none(),
2112 "default build command config should have no before"
2113 );
2114 }
2115
2116 #[test]
2117 fn version_defaults_to_none_when_omitted() {
2118 let config = minimal_config();
2119 assert!(config.version.is_none());
2120 }
2121
2122 #[test]
2123 fn version_parses_from_top_level_key() {
2124 let config: AlefConfig = toml::from_str(
2125 r#"
2126version = "0.7.7"
2127languages = ["python"]
2128
2129[crate]
2130name = "test-lib"
2131sources = ["src/lib.rs"]
2132"#,
2133 )
2134 .unwrap();
2135 assert_eq!(config.version.as_deref(), Some("0.7.7"));
2136 }
2137}