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