1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use std::path::PathBuf;
4
5pub mod dto;
6pub mod e2e;
7pub mod extras;
8pub mod languages;
9pub mod output;
10pub mod trait_bridge;
11
12pub use dto::{
14 CsharpDtoStyle, DtoConfig, ElixirDtoStyle, GoDtoStyle, JavaDtoStyle, NodeDtoStyle, PhpDtoStyle, PythonDtoStyle,
15 RDtoStyle, RubyDtoStyle,
16};
17pub use e2e::E2eConfig;
18pub use extras::{AdapterConfig, AdapterParam, AdapterPattern, Language};
19pub use languages::{
20 CSharpConfig, CustomModulesConfig, CustomRegistration, CustomRegistrationsConfig, ElixirConfig, FfiConfig,
21 GoConfig, JavaConfig, NodeConfig, PhpConfig, PythonConfig, RConfig, RubyConfig, StubsConfig, WasmConfig,
22};
23pub use output::{
24 ExcludeConfig, IncludeConfig, LintConfig, OutputConfig, ReadmeConfig, ScaffoldConfig, SyncConfig, TestConfig,
25 TextReplacement,
26};
27pub use trait_bridge::TraitBridgeConfig;
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct AlefConfig {
32 #[serde(rename = "crate")]
33 pub crate_config: CrateConfig,
34 pub languages: Vec<Language>,
35 #[serde(default)]
36 pub exclude: ExcludeConfig,
37 #[serde(default)]
38 pub include: IncludeConfig,
39 #[serde(default)]
40 pub output: OutputConfig,
41 #[serde(default)]
42 pub python: Option<PythonConfig>,
43 #[serde(default)]
44 pub node: Option<NodeConfig>,
45 #[serde(default)]
46 pub ruby: Option<RubyConfig>,
47 #[serde(default)]
48 pub php: Option<PhpConfig>,
49 #[serde(default)]
50 pub elixir: Option<ElixirConfig>,
51 #[serde(default)]
52 pub wasm: Option<WasmConfig>,
53 #[serde(default)]
54 pub ffi: Option<FfiConfig>,
55 #[serde(default)]
56 pub go: Option<GoConfig>,
57 #[serde(default)]
58 pub java: Option<JavaConfig>,
59 #[serde(default)]
60 pub csharp: Option<CSharpConfig>,
61 #[serde(default)]
62 pub r: Option<RConfig>,
63 #[serde(default)]
64 pub scaffold: Option<ScaffoldConfig>,
65 #[serde(default)]
66 pub readme: Option<ReadmeConfig>,
67 #[serde(default)]
68 pub lint: Option<HashMap<String, LintConfig>>,
69 #[serde(default)]
70 pub test: Option<HashMap<String, TestConfig>>,
71 #[serde(default)]
72 pub custom_files: Option<HashMap<String, Vec<PathBuf>>>,
73 #[serde(default)]
74 pub adapters: Vec<AdapterConfig>,
75 #[serde(default)]
76 pub custom_modules: CustomModulesConfig,
77 #[serde(default)]
78 pub custom_registrations: CustomRegistrationsConfig,
79 #[serde(default)]
80 pub sync: Option<SyncConfig>,
81 #[serde(default)]
85 pub opaque_types: HashMap<String, String>,
86 #[serde(default)]
88 pub generate: GenerateConfig,
89 #[serde(default)]
91 pub generate_overrides: HashMap<String, GenerateConfig>,
92 #[serde(default)]
94 pub dto: DtoConfig,
95 #[serde(default)]
97 pub e2e: Option<E2eConfig>,
98 #[serde(default)]
101 pub trait_bridges: Vec<TraitBridgeConfig>,
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct CrateConfig {
106 pub name: String,
107 pub sources: Vec<PathBuf>,
108 #[serde(default = "default_version_from")]
109 pub version_from: String,
110 #[serde(default)]
111 pub core_import: Option<String>,
112 #[serde(default)]
114 pub workspace_root: Option<PathBuf>,
115 #[serde(default)]
117 pub skip_core_import: bool,
118 #[serde(default)]
122 pub error_type: Option<String>,
123 #[serde(default)]
128 pub error_constructor: Option<String>,
129 #[serde(default)]
133 pub features: Vec<String>,
134 #[serde(default)]
137 pub path_mappings: HashMap<String, String>,
138 #[serde(default)]
142 pub extra_dependencies: HashMap<String, toml::Value>,
143 #[serde(default = "default_true")]
147 pub auto_path_mappings: bool,
148 #[serde(default)]
153 pub source_crates: Vec<SourceCrate>,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct SourceCrate {
159 pub name: String,
161 pub sources: Vec<PathBuf>,
163}
164
165fn default_version_from() -> String {
166 "Cargo.toml".to_string()
167}
168
169fn default_true() -> bool {
170 true
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct GenerateConfig {
178 #[serde(default = "default_true")]
180 pub bindings: bool,
181 #[serde(default = "default_true")]
183 pub errors: bool,
184 #[serde(default = "default_true")]
186 pub configs: bool,
187 #[serde(default = "default_true")]
189 pub async_wrappers: bool,
190 #[serde(default = "default_true")]
192 pub type_conversions: bool,
193 #[serde(default = "default_true")]
195 pub package_metadata: bool,
196 #[serde(default = "default_true")]
198 pub public_api: bool,
199 #[serde(default = "default_true")]
202 pub reverse_conversions: bool,
203}
204
205impl Default for GenerateConfig {
206 fn default() -> Self {
207 Self {
208 bindings: true,
209 errors: true,
210 configs: true,
211 async_wrappers: true,
212 type_conversions: true,
213 package_metadata: true,
214 public_api: true,
215 reverse_conversions: true,
216 }
217 }
218}
219
220impl AlefConfig {
225 pub fn features_for_language(&self, lang: extras::Language) -> &[String] {
228 let override_features = match lang {
229 extras::Language::Python => self.python.as_ref().and_then(|c| c.features.as_deref()),
230 extras::Language::Node => self.node.as_ref().and_then(|c| c.features.as_deref()),
231 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.features.as_deref()),
232 extras::Language::Php => self.php.as_ref().and_then(|c| c.features.as_deref()),
233 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.features.as_deref()),
234 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.features.as_deref()),
235 extras::Language::Ffi => self.ffi.as_ref().and_then(|c| c.features.as_deref()),
236 extras::Language::Go => self.go.as_ref().and_then(|c| c.features.as_deref()),
237 extras::Language::Java => self.java.as_ref().and_then(|c| c.features.as_deref()),
238 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.features.as_deref()),
239 extras::Language::R => self.r.as_ref().and_then(|c| c.features.as_deref()),
240 extras::Language::Rust => None, };
242 override_features.unwrap_or(&self.crate_config.features)
243 }
244
245 pub fn extra_deps_for_language(&self, lang: extras::Language) -> HashMap<String, toml::Value> {
249 let mut deps = self.crate_config.extra_dependencies.clone();
250 let lang_deps = match lang {
251 extras::Language::Python => self.python.as_ref().map(|c| &c.extra_dependencies),
252 extras::Language::Node => self.node.as_ref().map(|c| &c.extra_dependencies),
253 extras::Language::Ruby => self.ruby.as_ref().map(|c| &c.extra_dependencies),
254 extras::Language::Php => self.php.as_ref().map(|c| &c.extra_dependencies),
255 extras::Language::Elixir => self.elixir.as_ref().map(|c| &c.extra_dependencies),
256 extras::Language::Wasm => self.wasm.as_ref().map(|c| &c.extra_dependencies),
257 _ => None,
258 };
259 if let Some(lang_deps) = lang_deps {
260 deps.extend(lang_deps.iter().map(|(k, v)| (k.clone(), v.clone())));
261 }
262 deps
263 }
264
265 pub fn package_dir(&self, lang: extras::Language) -> String {
270 let override_path = match lang {
271 extras::Language::Python => self.python.as_ref().and_then(|c| c.scaffold_output.as_ref()),
272 extras::Language::Node => self.node.as_ref().and_then(|c| c.scaffold_output.as_ref()),
273 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.scaffold_output.as_ref()),
274 extras::Language::Php => self.php.as_ref().and_then(|c| c.scaffold_output.as_ref()),
275 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.scaffold_output.as_ref()),
276 _ => None,
277 };
278 if let Some(p) = override_path {
279 p.to_string_lossy().to_string()
280 } else {
281 match lang {
282 extras::Language::Python => "packages/python".to_string(),
283 extras::Language::Node => "packages/node".to_string(),
284 extras::Language::Ruby => "packages/ruby".to_string(),
285 extras::Language::Php => "packages/php".to_string(),
286 extras::Language::Elixir => "packages/elixir".to_string(),
287 _ => format!("packages/{lang}"),
288 }
289 }
290 }
291
292 pub fn core_import(&self) -> String {
294 self.crate_config
295 .core_import
296 .clone()
297 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
298 }
299
300 pub fn error_type(&self) -> String {
302 self.crate_config
303 .error_type
304 .clone()
305 .unwrap_or_else(|| "Error".to_string())
306 }
307
308 pub fn error_constructor(&self) -> String {
311 self.crate_config
312 .error_constructor
313 .clone()
314 .unwrap_or_else(|| format!("{}::{}::from({{msg}})", self.core_import(), self.error_type()))
315 }
316
317 pub fn ffi_prefix(&self) -> String {
319 self.ffi
320 .as_ref()
321 .and_then(|f| f.prefix.as_ref())
322 .cloned()
323 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
324 }
325
326 pub fn ffi_lib_name(&self) -> String {
334 if let Some(name) = self.ffi.as_ref().and_then(|f| f.lib_name.as_ref()) {
336 return name.clone();
337 }
338
339 if let Some(ffi_path) = self.output.ffi.as_ref() {
342 let path = std::path::Path::new(ffi_path);
343 let components: Vec<_> = path
346 .components()
347 .filter_map(|c| {
348 if let std::path::Component::Normal(s) = c {
349 s.to_str()
350 } else {
351 None
352 }
353 })
354 .collect();
355 let crate_dir = components
358 .iter()
359 .rev()
360 .find(|&&s| s != "src" && s != "lib" && s != "include")
361 .copied();
362 if let Some(dir) = crate_dir {
363 return dir.replace('-', "_");
364 }
365 }
366
367 format!("{}_ffi", self.ffi_prefix())
369 }
370
371 pub fn ffi_header_name(&self) -> String {
373 self.ffi
374 .as_ref()
375 .and_then(|f| f.header_name.as_ref())
376 .cloned()
377 .unwrap_or_else(|| format!("{}.h", self.ffi_prefix()))
378 }
379
380 pub fn python_module_name(&self) -> String {
382 self.python
383 .as_ref()
384 .and_then(|p| p.module_name.as_ref())
385 .cloned()
386 .unwrap_or_else(|| format!("_{}", self.crate_config.name.replace('-', "_")))
387 }
388
389 pub fn python_pip_name(&self) -> String {
393 self.python
394 .as_ref()
395 .and_then(|p| p.pip_name.as_ref())
396 .cloned()
397 .unwrap_or_else(|| self.crate_config.name.clone())
398 }
399
400 pub fn php_autoload_namespace(&self) -> String {
405 use heck::ToPascalCase;
406 let ext = self.php_extension_name();
407 if ext.contains('_') {
408 ext.split('_')
409 .map(|p| p.to_pascal_case())
410 .collect::<Vec<_>>()
411 .join("\\")
412 } else {
413 ext.to_pascal_case()
414 }
415 }
416
417 pub fn node_package_name(&self) -> String {
419 self.node
420 .as_ref()
421 .and_then(|n| n.package_name.as_ref())
422 .cloned()
423 .unwrap_or_else(|| self.crate_config.name.clone())
424 }
425
426 pub fn ruby_gem_name(&self) -> String {
428 self.ruby
429 .as_ref()
430 .and_then(|r| r.gem_name.as_ref())
431 .cloned()
432 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
433 }
434
435 pub fn php_extension_name(&self) -> String {
437 self.php
438 .as_ref()
439 .and_then(|p| p.extension_name.as_ref())
440 .cloned()
441 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
442 }
443
444 pub fn elixir_app_name(&self) -> String {
446 self.elixir
447 .as_ref()
448 .and_then(|e| e.app_name.as_ref())
449 .cloned()
450 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
451 }
452
453 pub fn go_module(&self) -> String {
455 self.go
456 .as_ref()
457 .and_then(|g| g.module.as_ref())
458 .cloned()
459 .unwrap_or_else(|| format!("github.com/kreuzberg-dev/{}", self.crate_config.name))
460 }
461
462 pub fn github_repo(&self) -> String {
469 if let Some(e2e) = &self.e2e {
470 if let Some(url) = &e2e.registry.github_repo {
471 return url.clone();
472 }
473 }
474 self.scaffold
475 .as_ref()
476 .and_then(|s| s.repository.as_ref())
477 .cloned()
478 .unwrap_or_else(|| format!("https://github.com/kreuzberg-dev/{}", self.crate_config.name))
479 }
480
481 pub fn java_package(&self) -> String {
483 self.java
484 .as_ref()
485 .and_then(|j| j.package.as_ref())
486 .cloned()
487 .unwrap_or_else(|| "dev.kreuzberg".to_string())
488 }
489
490 pub fn java_group_id(&self) -> String {
495 self.java_package()
496 }
497
498 pub fn csharp_namespace(&self) -> String {
500 self.csharp
501 .as_ref()
502 .and_then(|c| c.namespace.as_ref())
503 .cloned()
504 .unwrap_or_else(|| {
505 use heck::ToPascalCase;
506 self.crate_config.name.to_pascal_case()
507 })
508 }
509
510 pub fn core_crate_dir(&self) -> String {
516 if let Some(first_source) = self.crate_config.sources.first() {
519 let path = std::path::Path::new(first_source);
520 let mut current = path.parent();
521 while let Some(dir) = current {
522 if dir.file_name().is_some_and(|n| n == "src") {
523 if let Some(crate_dir) = dir.parent() {
524 if let Some(dir_name) = crate_dir.file_name() {
525 return dir_name.to_string_lossy().into_owned();
526 }
527 }
528 break;
529 }
530 current = dir.parent();
531 }
532 }
533 self.crate_config.name.clone()
534 }
535
536 pub fn wasm_type_prefix(&self) -> String {
539 self.wasm
540 .as_ref()
541 .and_then(|w| w.type_prefix.as_ref())
542 .cloned()
543 .unwrap_or_else(|| "Wasm".to_string())
544 }
545
546 pub fn node_type_prefix(&self) -> String {
549 self.node
550 .as_ref()
551 .and_then(|n| n.type_prefix.as_ref())
552 .cloned()
553 .unwrap_or_else(|| "Js".to_string())
554 }
555
556 pub fn r_package_name(&self) -> String {
558 self.r
559 .as_ref()
560 .and_then(|r| r.package_name.as_ref())
561 .cloned()
562 .unwrap_or_else(|| self.crate_config.name.clone())
563 }
564
565 pub fn resolved_version(&self) -> Option<String> {
568 let content = std::fs::read_to_string(&self.crate_config.version_from).ok()?;
569 let value: toml::Value = toml::from_str(&content).ok()?;
570 if let Some(v) = value
571 .get("workspace")
572 .and_then(|w| w.get("package"))
573 .and_then(|p| p.get("version"))
574 .and_then(|v| v.as_str())
575 {
576 return Some(v.to_string());
577 }
578 value
579 .get("package")
580 .and_then(|p| p.get("version"))
581 .and_then(|v| v.as_str())
582 .map(|v| v.to_string())
583 }
584
585 pub fn serde_rename_all_for_language(&self, lang: extras::Language) -> String {
593 let override_val = match lang {
595 extras::Language::Python => self.python.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
596 extras::Language::Node => self.node.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
597 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
598 extras::Language::Php => self.php.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
599 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
600 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
601 extras::Language::Ffi => self.ffi.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
602 extras::Language::Go => self.go.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
603 extras::Language::Java => self.java.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
604 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
605 extras::Language::R => self.r.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
606 extras::Language::Rust => None, };
608
609 if let Some(val) = override_val {
610 return val.to_string();
611 }
612
613 match lang {
615 extras::Language::Node | extras::Language::Wasm | extras::Language::Java | extras::Language::Csharp => {
616 "camelCase".to_string()
617 }
618 extras::Language::Python
619 | extras::Language::Ruby
620 | extras::Language::Php
621 | extras::Language::Go
622 | extras::Language::Ffi
623 | extras::Language::Elixir
624 | extras::Language::R
625 | extras::Language::Rust => "snake_case".to_string(),
626 }
627 }
628
629 pub fn rewrite_path(&self, rust_path: &str) -> String {
632 let mut mappings: Vec<_> = self.crate_config.path_mappings.iter().collect();
634 mappings.sort_by_key(|b| std::cmp::Reverse(b.0.len()));
635
636 for (from, to) in &mappings {
637 if rust_path.starts_with(from.as_str()) {
638 return format!("{}{}", to, &rust_path[from.len()..]);
639 }
640 }
641 rust_path.to_string()
642 }
643
644 pub fn effective_path_mappings(&self) -> HashMap<String, String> {
654 let mut mappings = HashMap::new();
655
656 if self.crate_config.auto_path_mappings {
657 let core_import = self.core_import();
658
659 for source in &self.crate_config.sources {
660 let source_str = source.to_string_lossy();
661 if let Some(after_crates) = find_after_crates_prefix(&source_str) {
663 if let Some(slash_pos) = after_crates.find('/') {
665 let crate_dir = &after_crates[..slash_pos];
666 let crate_ident = crate_dir.replace('-', "_");
667 if crate_ident != core_import && !mappings.contains_key(&crate_ident) {
669 mappings.insert(crate_ident, core_import.clone());
670 }
671 }
672 }
673 }
674 }
675
676 for (from, to) in &self.crate_config.path_mappings {
678 mappings.insert(from.clone(), to.clone());
679 }
680
681 mappings
682 }
683}
684
685fn find_after_crates_prefix(path: &str) -> Option<&str> {
692 if let Some(pos) = path.find("/crates/") {
696 return Some(&path[pos + "/crates/".len()..]);
697 }
698 if let Some(stripped) = path.strip_prefix("crates/") {
699 return Some(stripped);
700 }
701 None
702}
703
704pub fn resolve_output_dir(config_path: Option<&PathBuf>, crate_name: &str, default: &str) -> String {
707 config_path
708 .map(|p| p.to_string_lossy().replace("{name}", crate_name))
709 .unwrap_or_else(|| default.replace("{name}", crate_name))
710}
711
712pub fn detect_serde_available(output_dir: &str) -> bool {
718 let src_path = std::path::Path::new(output_dir);
719 let mut dir = src_path;
721 loop {
722 let cargo_toml = dir.join("Cargo.toml");
723 if cargo_toml.exists() {
724 return cargo_toml_has_serde(&cargo_toml);
725 }
726 match dir.parent() {
727 Some(parent) if !parent.as_os_str().is_empty() => dir = parent,
728 _ => break,
729 }
730 }
731 false
732}
733
734fn cargo_toml_has_serde(path: &std::path::Path) -> bool {
740 let content = match std::fs::read_to_string(path) {
741 Ok(c) => c,
742 Err(_) => return false,
743 };
744
745 let has_serde_json = content.contains("serde_json");
746 let has_serde_dep = content.lines().any(|line| {
750 let trimmed = line.trim();
751 trimmed.starts_with("serde ")
753 || trimmed.starts_with("serde=")
754 || trimmed.starts_with("serde.")
755 || trimmed == "[dependencies.serde]"
756 });
757
758 has_serde_json && has_serde_dep
759}