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;
10
11pub use dto::{
13 CsharpDtoStyle, DtoConfig, ElixirDtoStyle, GoDtoStyle, JavaDtoStyle, NodeDtoStyle, PhpDtoStyle, PythonDtoStyle,
14 RDtoStyle, RubyDtoStyle,
15};
16pub use e2e::E2eConfig;
17pub use extras::{AdapterConfig, AdapterParam, AdapterPattern, Language};
18pub use languages::{
19 CSharpConfig, CustomModulesConfig, CustomRegistration, CustomRegistrationsConfig, ElixirConfig, FfiConfig,
20 GoConfig, JavaConfig, NodeConfig, PhpConfig, PythonConfig, RConfig, RubyConfig, StubsConfig, WasmConfig,
21};
22pub use output::{
23 ExcludeConfig, IncludeConfig, LintConfig, OutputConfig, ReadmeConfig, ScaffoldConfig, SyncConfig, TestConfig,
24 TextReplacement,
25};
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct AlefConfig {
30 #[serde(rename = "crate")]
31 pub crate_config: CrateConfig,
32 pub languages: Vec<Language>,
33 #[serde(default)]
34 pub exclude: ExcludeConfig,
35 #[serde(default)]
36 pub include: IncludeConfig,
37 #[serde(default)]
38 pub output: OutputConfig,
39 #[serde(default)]
40 pub python: Option<PythonConfig>,
41 #[serde(default)]
42 pub node: Option<NodeConfig>,
43 #[serde(default)]
44 pub ruby: Option<RubyConfig>,
45 #[serde(default)]
46 pub php: Option<PhpConfig>,
47 #[serde(default)]
48 pub elixir: Option<ElixirConfig>,
49 #[serde(default)]
50 pub wasm: Option<WasmConfig>,
51 #[serde(default)]
52 pub ffi: Option<FfiConfig>,
53 #[serde(default)]
54 pub go: Option<GoConfig>,
55 #[serde(default)]
56 pub java: Option<JavaConfig>,
57 #[serde(default)]
58 pub csharp: Option<CSharpConfig>,
59 #[serde(default)]
60 pub r: Option<RConfig>,
61 #[serde(default)]
62 pub scaffold: Option<ScaffoldConfig>,
63 #[serde(default)]
64 pub readme: Option<ReadmeConfig>,
65 #[serde(default)]
66 pub lint: Option<HashMap<String, LintConfig>>,
67 #[serde(default)]
68 pub test: Option<HashMap<String, TestConfig>>,
69 #[serde(default)]
70 pub custom_files: Option<HashMap<String, Vec<PathBuf>>>,
71 #[serde(default)]
72 pub adapters: Vec<AdapterConfig>,
73 #[serde(default)]
74 pub custom_modules: CustomModulesConfig,
75 #[serde(default)]
76 pub custom_registrations: CustomRegistrationsConfig,
77 #[serde(default)]
78 pub sync: Option<SyncConfig>,
79 #[serde(default)]
83 pub opaque_types: HashMap<String, String>,
84 #[serde(default)]
86 pub generate: GenerateConfig,
87 #[serde(default)]
89 pub generate_overrides: HashMap<String, GenerateConfig>,
90 #[serde(default)]
92 pub dto: DtoConfig,
93 #[serde(default)]
95 pub e2e: Option<E2eConfig>,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct CrateConfig {
100 pub name: String,
101 pub sources: Vec<PathBuf>,
102 #[serde(default = "default_version_from")]
103 pub version_from: String,
104 #[serde(default)]
105 pub core_import: Option<String>,
106 #[serde(default)]
108 pub workspace_root: Option<PathBuf>,
109 #[serde(default)]
111 pub skip_core_import: bool,
112 #[serde(default)]
116 pub features: Vec<String>,
117 #[serde(default)]
120 pub path_mappings: HashMap<String, String>,
121}
122
123fn default_version_from() -> String {
124 "Cargo.toml".to_string()
125}
126
127fn default_true() -> bool {
128 true
129}
130
131#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct GenerateConfig {
136 #[serde(default = "default_true")]
138 pub bindings: bool,
139 #[serde(default = "default_true")]
141 pub errors: bool,
142 #[serde(default = "default_true")]
144 pub configs: bool,
145 #[serde(default = "default_true")]
147 pub async_wrappers: bool,
148 #[serde(default = "default_true")]
150 pub type_conversions: bool,
151 #[serde(default = "default_true")]
153 pub package_metadata: bool,
154 #[serde(default = "default_true")]
156 pub public_api: bool,
157 #[serde(default = "default_true")]
160 pub reverse_conversions: bool,
161}
162
163impl Default for GenerateConfig {
164 fn default() -> Self {
165 Self {
166 bindings: true,
167 errors: true,
168 configs: true,
169 async_wrappers: true,
170 type_conversions: true,
171 package_metadata: true,
172 public_api: true,
173 reverse_conversions: true,
174 }
175 }
176}
177
178impl AlefConfig {
183 pub fn features_for_language(&self, lang: extras::Language) -> &[String] {
186 let override_features = match lang {
187 extras::Language::Python => self.python.as_ref().and_then(|c| c.features.as_deref()),
188 extras::Language::Node => self.node.as_ref().and_then(|c| c.features.as_deref()),
189 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.features.as_deref()),
190 extras::Language::Php => self.php.as_ref().and_then(|c| c.features.as_deref()),
191 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.features.as_deref()),
192 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.features.as_deref()),
193 extras::Language::Ffi => self.ffi.as_ref().and_then(|c| c.features.as_deref()),
194 extras::Language::Go => self.go.as_ref().and_then(|c| c.features.as_deref()),
195 extras::Language::Java => self.java.as_ref().and_then(|c| c.features.as_deref()),
196 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.features.as_deref()),
197 extras::Language::R => self.r.as_ref().and_then(|c| c.features.as_deref()),
198 extras::Language::Rust => None, };
200 override_features.unwrap_or(&self.crate_config.features)
201 }
202
203 pub fn core_import(&self) -> String {
205 self.crate_config
206 .core_import
207 .clone()
208 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
209 }
210
211 pub fn ffi_prefix(&self) -> String {
213 self.ffi
214 .as_ref()
215 .and_then(|f| f.prefix.as_ref())
216 .cloned()
217 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
218 }
219
220 pub fn ffi_lib_name(&self) -> String {
228 if let Some(name) = self.ffi.as_ref().and_then(|f| f.lib_name.as_ref()) {
230 return name.clone();
231 }
232
233 if let Some(ffi_path) = self.output.ffi.as_ref() {
236 let path = std::path::Path::new(ffi_path);
237 let components: Vec<_> = path
240 .components()
241 .filter_map(|c| {
242 if let std::path::Component::Normal(s) = c {
243 s.to_str()
244 } else {
245 None
246 }
247 })
248 .collect();
249 let crate_dir = components
252 .iter()
253 .rev()
254 .find(|&&s| s != "src" && s != "lib" && s != "include")
255 .copied();
256 if let Some(dir) = crate_dir {
257 return dir.replace('-', "_");
258 }
259 }
260
261 format!("{}_ffi", self.ffi_prefix())
263 }
264
265 pub fn ffi_header_name(&self) -> String {
267 self.ffi
268 .as_ref()
269 .and_then(|f| f.header_name.as_ref())
270 .cloned()
271 .unwrap_or_else(|| format!("{}.h", self.ffi_prefix()))
272 }
273
274 pub fn python_module_name(&self) -> String {
276 self.python
277 .as_ref()
278 .and_then(|p| p.module_name.as_ref())
279 .cloned()
280 .unwrap_or_else(|| format!("_{}", self.crate_config.name.replace('-', "_")))
281 }
282
283 pub fn python_pip_name(&self) -> String {
287 self.python
288 .as_ref()
289 .and_then(|p| p.pip_name.as_ref())
290 .cloned()
291 .unwrap_or_else(|| self.crate_config.name.clone())
292 }
293
294 pub fn php_autoload_namespace(&self) -> String {
299 use heck::ToPascalCase;
300 let ext = self.php_extension_name();
301 if ext.contains('_') {
302 ext.split('_')
303 .map(|p| p.to_pascal_case())
304 .collect::<Vec<_>>()
305 .join("\\")
306 } else {
307 ext.to_pascal_case()
308 }
309 }
310
311 pub fn node_package_name(&self) -> String {
313 self.node
314 .as_ref()
315 .and_then(|n| n.package_name.as_ref())
316 .cloned()
317 .unwrap_or_else(|| self.crate_config.name.clone())
318 }
319
320 pub fn ruby_gem_name(&self) -> String {
322 self.ruby
323 .as_ref()
324 .and_then(|r| r.gem_name.as_ref())
325 .cloned()
326 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
327 }
328
329 pub fn php_extension_name(&self) -> String {
331 self.php
332 .as_ref()
333 .and_then(|p| p.extension_name.as_ref())
334 .cloned()
335 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
336 }
337
338 pub fn elixir_app_name(&self) -> String {
340 self.elixir
341 .as_ref()
342 .and_then(|e| e.app_name.as_ref())
343 .cloned()
344 .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
345 }
346
347 pub fn go_module(&self) -> String {
349 self.go
350 .as_ref()
351 .and_then(|g| g.module.as_ref())
352 .cloned()
353 .unwrap_or_else(|| format!("github.com/kreuzberg-dev/{}", self.crate_config.name))
354 }
355
356 pub fn github_repo(&self) -> String {
363 if let Some(e2e) = &self.e2e {
364 if let Some(url) = &e2e.registry.github_repo {
365 return url.clone();
366 }
367 }
368 self.scaffold
369 .as_ref()
370 .and_then(|s| s.repository.as_ref())
371 .cloned()
372 .unwrap_or_else(|| format!("https://github.com/kreuzberg-dev/{}", self.crate_config.name))
373 }
374
375 pub fn java_package(&self) -> String {
377 self.java
378 .as_ref()
379 .and_then(|j| j.package.as_ref())
380 .cloned()
381 .unwrap_or_else(|| "dev.kreuzberg".to_string())
382 }
383
384 pub fn java_group_id(&self) -> String {
389 self.java_package()
390 }
391
392 pub fn csharp_namespace(&self) -> String {
394 self.csharp
395 .as_ref()
396 .and_then(|c| c.namespace.as_ref())
397 .cloned()
398 .unwrap_or_else(|| {
399 use heck::ToPascalCase;
400 self.crate_config.name.to_pascal_case()
401 })
402 }
403
404 pub fn core_crate_dir(&self) -> String {
410 if let Some(first_source) = self.crate_config.sources.first() {
413 let path = std::path::Path::new(first_source);
414 let mut current = path.parent();
415 while let Some(dir) = current {
416 if dir.file_name().is_some_and(|n| n == "src") {
417 if let Some(crate_dir) = dir.parent() {
418 if let Some(dir_name) = crate_dir.file_name() {
419 return dir_name.to_string_lossy().into_owned();
420 }
421 }
422 break;
423 }
424 current = dir.parent();
425 }
426 }
427 self.crate_config.name.clone()
428 }
429
430 pub fn wasm_type_prefix(&self) -> String {
433 self.wasm
434 .as_ref()
435 .and_then(|w| w.type_prefix.as_ref())
436 .cloned()
437 .unwrap_or_else(|| "Wasm".to_string())
438 }
439
440 pub fn node_type_prefix(&self) -> String {
443 self.node
444 .as_ref()
445 .and_then(|n| n.type_prefix.as_ref())
446 .cloned()
447 .unwrap_or_else(|| "Js".to_string())
448 }
449
450 pub fn r_package_name(&self) -> String {
452 self.r
453 .as_ref()
454 .and_then(|r| r.package_name.as_ref())
455 .cloned()
456 .unwrap_or_else(|| self.crate_config.name.clone())
457 }
458
459 pub fn resolved_version(&self) -> Option<String> {
462 let content = std::fs::read_to_string(&self.crate_config.version_from).ok()?;
463 let value: toml::Value = toml::from_str(&content).ok()?;
464 if let Some(v) = value
465 .get("workspace")
466 .and_then(|w| w.get("package"))
467 .and_then(|p| p.get("version"))
468 .and_then(|v| v.as_str())
469 {
470 return Some(v.to_string());
471 }
472 value
473 .get("package")
474 .and_then(|p| p.get("version"))
475 .and_then(|v| v.as_str())
476 .map(|v| v.to_string())
477 }
478
479 pub fn serde_rename_all_for_language(&self, lang: extras::Language) -> String {
487 let override_val = match lang {
489 extras::Language::Python => self.python.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
490 extras::Language::Node => self.node.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
491 extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
492 extras::Language::Php => self.php.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
493 extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
494 extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
495 extras::Language::Ffi => self.ffi.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
496 extras::Language::Go => self.go.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
497 extras::Language::Java => self.java.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
498 extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
499 extras::Language::R => self.r.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
500 extras::Language::Rust => None, };
502
503 if let Some(val) = override_val {
504 return val.to_string();
505 }
506
507 match lang {
509 extras::Language::Node | extras::Language::Wasm | extras::Language::Java | extras::Language::Csharp => {
510 "camelCase".to_string()
511 }
512 extras::Language::Python
513 | extras::Language::Ruby
514 | extras::Language::Php
515 | extras::Language::Go
516 | extras::Language::Ffi
517 | extras::Language::Elixir
518 | extras::Language::R
519 | extras::Language::Rust => "snake_case".to_string(),
520 }
521 }
522
523 pub fn rewrite_path(&self, rust_path: &str) -> String {
526 let mut mappings: Vec<_> = self.crate_config.path_mappings.iter().collect();
528 mappings.sort_by_key(|b| std::cmp::Reverse(b.0.len()));
529
530 for (from, to) in &mappings {
531 if rust_path.starts_with(from.as_str()) {
532 return format!("{}{}", to, &rust_path[from.len()..]);
533 }
534 }
535 rust_path.to_string()
536 }
537}
538
539pub fn resolve_output_dir(config_path: Option<&PathBuf>, crate_name: &str, default: &str) -> String {
542 config_path
543 .map(|p| p.to_string_lossy().replace("{name}", crate_name))
544 .unwrap_or_else(|| default.replace("{name}", crate_name))
545}
546
547pub fn detect_serde_available(output_dir: &str) -> bool {
553 let src_path = std::path::Path::new(output_dir);
554 let mut dir = src_path;
556 loop {
557 let cargo_toml = dir.join("Cargo.toml");
558 if cargo_toml.exists() {
559 return cargo_toml_has_serde(&cargo_toml);
560 }
561 match dir.parent() {
562 Some(parent) if !parent.as_os_str().is_empty() => dir = parent,
563 _ => break,
564 }
565 }
566 false
567}
568
569fn cargo_toml_has_serde(path: &std::path::Path) -> bool {
575 let content = match std::fs::read_to_string(path) {
576 Ok(c) => c,
577 Err(_) => return false,
578 };
579
580 let has_serde_json = content.contains("serde_json");
581 let has_serde_dep = content.lines().any(|line| {
585 let trimmed = line.trim();
586 trimmed.starts_with("serde ")
588 || trimmed.starts_with("serde=")
589 || trimmed.starts_with("serde.")
590 || trimmed == "[dependencies.serde]"
591 });
592
593 has_serde_json && has_serde_dep
594}