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 features: Vec<String>,
123 #[serde(default)]
126 pub path_mappings: HashMap<String, String>,
127 #[serde(default)]
131 pub extra_dependencies: HashMap<String, toml::Value>,
132 #[serde(default = "default_true")]
136 pub auto_path_mappings: bool,
137}
138
139fn default_version_from() -> String {
140 "Cargo.toml".to_string()
141}
142
143fn default_true() -> bool {
144 true
145}
146
147#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct GenerateConfig {
152 #[serde(default = "default_true")]
154 pub bindings: bool,
155 #[serde(default = "default_true")]
157 pub errors: bool,
158 #[serde(default = "default_true")]
160 pub configs: bool,
161 #[serde(default = "default_true")]
163 pub async_wrappers: bool,
164 #[serde(default = "default_true")]
166 pub type_conversions: bool,
167 #[serde(default = "default_true")]
169 pub package_metadata: bool,
170 #[serde(default = "default_true")]
172 pub public_api: bool,
173 #[serde(default = "default_true")]
176 pub reverse_conversions: bool,
177}
178
179impl Default for GenerateConfig {
180 fn default() -> Self {
181 Self {
182 bindings: true,
183 errors: true,
184 configs: true,
185 async_wrappers: true,
186 type_conversions: true,
187 package_metadata: true,
188 public_api: true,
189 reverse_conversions: true,
190 }
191 }
192}
193
194impl AlefConfig {
199 pub fn features_for_language(&self, lang: extras::Language) -> &[String] {
202 let override_features = match lang {
203 extras::Language::Python => self.python.as_ref().and_then(|c| c.features.as_deref()),
204 extras::Language::Node => self.node.as_ref().and_then(|c| c.features.as_deref()),
205 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.features.as_deref()),
206 extras::Language::Php => self.php.as_ref().and_then(|c| c.features.as_deref()),
207 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.features.as_deref()),
208 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.features.as_deref()),
209 extras::Language::Ffi => self.ffi.as_ref().and_then(|c| c.features.as_deref()),
210 extras::Language::Go => self.go.as_ref().and_then(|c| c.features.as_deref()),
211 extras::Language::Java => self.java.as_ref().and_then(|c| c.features.as_deref()),
212 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.features.as_deref()),
213 extras::Language::R => self.r.as_ref().and_then(|c| c.features.as_deref()),
214 extras::Language::Rust => None, };
216 override_features.unwrap_or(&self.crate_config.features)
217 }
218
219 pub fn extra_deps_for_language(&self, lang: extras::Language) -> HashMap<String, toml::Value> {
223 let mut deps = self.crate_config.extra_dependencies.clone();
224 let lang_deps = match lang {
225 extras::Language::Python => self.python.as_ref().map(|c| &c.extra_dependencies),
226 extras::Language::Node => self.node.as_ref().map(|c| &c.extra_dependencies),
227 extras::Language::Ruby => self.ruby.as_ref().map(|c| &c.extra_dependencies),
228 extras::Language::Php => self.php.as_ref().map(|c| &c.extra_dependencies),
229 extras::Language::Elixir => self.elixir.as_ref().map(|c| &c.extra_dependencies),
230 _ => None,
231 };
232 if let Some(lang_deps) = lang_deps {
233 deps.extend(lang_deps.iter().map(|(k, v)| (k.clone(), v.clone())));
234 }
235 deps
236 }
237
238 pub fn core_import(&self) -> String {
240 self.crate_config
241 .core_import
242 .clone()
243 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
244 }
245
246 pub fn ffi_prefix(&self) -> String {
248 self.ffi
249 .as_ref()
250 .and_then(|f| f.prefix.as_ref())
251 .cloned()
252 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
253 }
254
255 pub fn ffi_lib_name(&self) -> String {
263 if let Some(name) = self.ffi.as_ref().and_then(|f| f.lib_name.as_ref()) {
265 return name.clone();
266 }
267
268 if let Some(ffi_path) = self.output.ffi.as_ref() {
271 let path = std::path::Path::new(ffi_path);
272 let components: Vec<_> = path
275 .components()
276 .filter_map(|c| {
277 if let std::path::Component::Normal(s) = c {
278 s.to_str()
279 } else {
280 None
281 }
282 })
283 .collect();
284 let crate_dir = components
287 .iter()
288 .rev()
289 .find(|&&s| s != "src" && s != "lib" && s != "include")
290 .copied();
291 if let Some(dir) = crate_dir {
292 return dir.replace('-', "_");
293 }
294 }
295
296 format!("{}_ffi", self.ffi_prefix())
298 }
299
300 pub fn ffi_header_name(&self) -> String {
302 self.ffi
303 .as_ref()
304 .and_then(|f| f.header_name.as_ref())
305 .cloned()
306 .unwrap_or_else(|| format!("{}.h", self.ffi_prefix()))
307 }
308
309 pub fn python_module_name(&self) -> String {
311 self.python
312 .as_ref()
313 .and_then(|p| p.module_name.as_ref())
314 .cloned()
315 .unwrap_or_else(|| format!("_{}", self.crate_config.name.replace('-', "_")))
316 }
317
318 pub fn python_pip_name(&self) -> String {
322 self.python
323 .as_ref()
324 .and_then(|p| p.pip_name.as_ref())
325 .cloned()
326 .unwrap_or_else(|| self.crate_config.name.clone())
327 }
328
329 pub fn php_autoload_namespace(&self) -> String {
334 use heck::ToPascalCase;
335 let ext = self.php_extension_name();
336 if ext.contains('_') {
337 ext.split('_')
338 .map(|p| p.to_pascal_case())
339 .collect::<Vec<_>>()
340 .join("\\")
341 } else {
342 ext.to_pascal_case()
343 }
344 }
345
346 pub fn node_package_name(&self) -> String {
348 self.node
349 .as_ref()
350 .and_then(|n| n.package_name.as_ref())
351 .cloned()
352 .unwrap_or_else(|| self.crate_config.name.clone())
353 }
354
355 pub fn ruby_gem_name(&self) -> String {
357 self.ruby
358 .as_ref()
359 .and_then(|r| r.gem_name.as_ref())
360 .cloned()
361 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
362 }
363
364 pub fn php_extension_name(&self) -> String {
366 self.php
367 .as_ref()
368 .and_then(|p| p.extension_name.as_ref())
369 .cloned()
370 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
371 }
372
373 pub fn elixir_app_name(&self) -> String {
375 self.elixir
376 .as_ref()
377 .and_then(|e| e.app_name.as_ref())
378 .cloned()
379 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
380 }
381
382 pub fn go_module(&self) -> String {
384 self.go
385 .as_ref()
386 .and_then(|g| g.module.as_ref())
387 .cloned()
388 .unwrap_or_else(|| format!("github.com/kreuzberg-dev/{}", self.crate_config.name))
389 }
390
391 pub fn github_repo(&self) -> String {
398 if let Some(e2e) = &self.e2e {
399 if let Some(url) = &e2e.registry.github_repo {
400 return url.clone();
401 }
402 }
403 self.scaffold
404 .as_ref()
405 .and_then(|s| s.repository.as_ref())
406 .cloned()
407 .unwrap_or_else(|| format!("https://github.com/kreuzberg-dev/{}", self.crate_config.name))
408 }
409
410 pub fn java_package(&self) -> String {
412 self.java
413 .as_ref()
414 .and_then(|j| j.package.as_ref())
415 .cloned()
416 .unwrap_or_else(|| "dev.kreuzberg".to_string())
417 }
418
419 pub fn java_group_id(&self) -> String {
424 self.java_package()
425 }
426
427 pub fn csharp_namespace(&self) -> String {
429 self.csharp
430 .as_ref()
431 .and_then(|c| c.namespace.as_ref())
432 .cloned()
433 .unwrap_or_else(|| {
434 use heck::ToPascalCase;
435 self.crate_config.name.to_pascal_case()
436 })
437 }
438
439 pub fn core_crate_dir(&self) -> String {
445 if let Some(first_source) = self.crate_config.sources.first() {
448 let path = std::path::Path::new(first_source);
449 let mut current = path.parent();
450 while let Some(dir) = current {
451 if dir.file_name().is_some_and(|n| n == "src") {
452 if let Some(crate_dir) = dir.parent() {
453 if let Some(dir_name) = crate_dir.file_name() {
454 return dir_name.to_string_lossy().into_owned();
455 }
456 }
457 break;
458 }
459 current = dir.parent();
460 }
461 }
462 self.crate_config.name.clone()
463 }
464
465 pub fn wasm_type_prefix(&self) -> String {
468 self.wasm
469 .as_ref()
470 .and_then(|w| w.type_prefix.as_ref())
471 .cloned()
472 .unwrap_or_else(|| "Wasm".to_string())
473 }
474
475 pub fn node_type_prefix(&self) -> String {
478 self.node
479 .as_ref()
480 .and_then(|n| n.type_prefix.as_ref())
481 .cloned()
482 .unwrap_or_else(|| "Js".to_string())
483 }
484
485 pub fn r_package_name(&self) -> String {
487 self.r
488 .as_ref()
489 .and_then(|r| r.package_name.as_ref())
490 .cloned()
491 .unwrap_or_else(|| self.crate_config.name.clone())
492 }
493
494 pub fn resolved_version(&self) -> Option<String> {
497 let content = std::fs::read_to_string(&self.crate_config.version_from).ok()?;
498 let value: toml::Value = toml::from_str(&content).ok()?;
499 if let Some(v) = value
500 .get("workspace")
501 .and_then(|w| w.get("package"))
502 .and_then(|p| p.get("version"))
503 .and_then(|v| v.as_str())
504 {
505 return Some(v.to_string());
506 }
507 value
508 .get("package")
509 .and_then(|p| p.get("version"))
510 .and_then(|v| v.as_str())
511 .map(|v| v.to_string())
512 }
513
514 pub fn serde_rename_all_for_language(&self, lang: extras::Language) -> String {
522 let override_val = match lang {
524 extras::Language::Python => self.python.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
525 extras::Language::Node => self.node.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
526 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
527 extras::Language::Php => self.php.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
528 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
529 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
530 extras::Language::Ffi => self.ffi.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
531 extras::Language::Go => self.go.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
532 extras::Language::Java => self.java.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
533 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
534 extras::Language::R => self.r.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
535 extras::Language::Rust => None, };
537
538 if let Some(val) = override_val {
539 return val.to_string();
540 }
541
542 match lang {
544 extras::Language::Node | extras::Language::Wasm | extras::Language::Java | extras::Language::Csharp => {
545 "camelCase".to_string()
546 }
547 extras::Language::Python
548 | extras::Language::Ruby
549 | extras::Language::Php
550 | extras::Language::Go
551 | extras::Language::Ffi
552 | extras::Language::Elixir
553 | extras::Language::R
554 | extras::Language::Rust => "snake_case".to_string(),
555 }
556 }
557
558 pub fn rewrite_path(&self, rust_path: &str) -> String {
561 let mut mappings: Vec<_> = self.crate_config.path_mappings.iter().collect();
563 mappings.sort_by_key(|b| std::cmp::Reverse(b.0.len()));
564
565 for (from, to) in &mappings {
566 if rust_path.starts_with(from.as_str()) {
567 return format!("{}{}", to, &rust_path[from.len()..]);
568 }
569 }
570 rust_path.to_string()
571 }
572
573 pub fn effective_path_mappings(&self) -> HashMap<String, String> {
583 let mut mappings = HashMap::new();
584
585 if self.crate_config.auto_path_mappings {
586 let core_import = self.core_import();
587
588 for source in &self.crate_config.sources {
589 let source_str = source.to_string_lossy();
590 if let Some(after_crates) = find_after_crates_prefix(&source_str) {
592 if let Some(slash_pos) = after_crates.find('/') {
594 let crate_dir = &after_crates[..slash_pos];
595 let crate_ident = crate_dir.replace('-', "_");
596 if crate_ident != core_import && !mappings.contains_key(&crate_ident) {
598 mappings.insert(crate_ident, core_import.clone());
599 }
600 }
601 }
602 }
603 }
604
605 for (from, to) in &self.crate_config.path_mappings {
607 mappings.insert(from.clone(), to.clone());
608 }
609
610 mappings
611 }
612}
613
614fn find_after_crates_prefix(path: &str) -> Option<&str> {
621 if let Some(pos) = path.find("/crates/") {
625 return Some(&path[pos + "/crates/".len()..]);
626 }
627 if let Some(stripped) = path.strip_prefix("crates/") {
628 return Some(stripped);
629 }
630 None
631}
632
633pub fn resolve_output_dir(config_path: Option<&PathBuf>, crate_name: &str, default: &str) -> String {
636 config_path
637 .map(|p| p.to_string_lossy().replace("{name}", crate_name))
638 .unwrap_or_else(|| default.replace("{name}", crate_name))
639}
640
641pub fn detect_serde_available(output_dir: &str) -> bool {
647 let src_path = std::path::Path::new(output_dir);
648 let mut dir = src_path;
650 loop {
651 let cargo_toml = dir.join("Cargo.toml");
652 if cargo_toml.exists() {
653 return cargo_toml_has_serde(&cargo_toml);
654 }
655 match dir.parent() {
656 Some(parent) if !parent.as_os_str().is_empty() => dir = parent,
657 _ => break,
658 }
659 }
660 false
661}
662
663fn cargo_toml_has_serde(path: &std::path::Path) -> bool {
669 let content = match std::fs::read_to_string(path) {
670 Ok(c) => c,
671 Err(_) => return false,
672 };
673
674 let has_serde_json = content.contains("serde_json");
675 let has_serde_dep = content.lines().any(|line| {
679 let trimmed = line.trim();
680 trimmed.starts_with("serde ")
682 || trimmed.starts_with("serde=")
683 || trimmed.starts_with("serde.")
684 || trimmed == "[dependencies.serde]"
685 });
686
687 has_serde_json && has_serde_dep
688}