use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
pub mod dto;
pub mod e2e;
pub mod extras;
pub mod languages;
pub mod output;
pub use dto::{
CsharpDtoStyle, DtoConfig, ElixirDtoStyle, GoDtoStyle, JavaDtoStyle, NodeDtoStyle, PhpDtoStyle, PythonDtoStyle,
RDtoStyle, RubyDtoStyle,
};
pub use e2e::E2eConfig;
pub use extras::{AdapterConfig, AdapterParam, AdapterPattern, Language};
pub use languages::{
CSharpConfig, CustomModulesConfig, CustomRegistration, CustomRegistrationsConfig, ElixirConfig, FfiConfig,
GoConfig, JavaConfig, NodeConfig, PhpConfig, PythonConfig, RConfig, RubyConfig, StubsConfig, WasmConfig,
};
pub use output::{
ExcludeConfig, IncludeConfig, LintConfig, OutputConfig, ReadmeConfig, ScaffoldConfig, SyncConfig, TestConfig,
TextReplacement,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AlefConfig {
#[serde(rename = "crate")]
pub crate_config: CrateConfig,
pub languages: Vec<Language>,
#[serde(default)]
pub exclude: ExcludeConfig,
#[serde(default)]
pub include: IncludeConfig,
#[serde(default)]
pub output: OutputConfig,
#[serde(default)]
pub python: Option<PythonConfig>,
#[serde(default)]
pub node: Option<NodeConfig>,
#[serde(default)]
pub ruby: Option<RubyConfig>,
#[serde(default)]
pub php: Option<PhpConfig>,
#[serde(default)]
pub elixir: Option<ElixirConfig>,
#[serde(default)]
pub wasm: Option<WasmConfig>,
#[serde(default)]
pub ffi: Option<FfiConfig>,
#[serde(default)]
pub go: Option<GoConfig>,
#[serde(default)]
pub java: Option<JavaConfig>,
#[serde(default)]
pub csharp: Option<CSharpConfig>,
#[serde(default)]
pub r: Option<RConfig>,
#[serde(default)]
pub scaffold: Option<ScaffoldConfig>,
#[serde(default)]
pub readme: Option<ReadmeConfig>,
#[serde(default)]
pub lint: Option<HashMap<String, LintConfig>>,
#[serde(default)]
pub test: Option<HashMap<String, TestConfig>>,
#[serde(default)]
pub custom_files: Option<HashMap<String, Vec<PathBuf>>>,
#[serde(default)]
pub adapters: Vec<AdapterConfig>,
#[serde(default)]
pub custom_modules: CustomModulesConfig,
#[serde(default)]
pub custom_registrations: CustomRegistrationsConfig,
#[serde(default)]
pub sync: Option<SyncConfig>,
#[serde(default)]
pub opaque_types: HashMap<String, String>,
#[serde(default)]
pub generate: GenerateConfig,
#[serde(default)]
pub generate_overrides: HashMap<String, GenerateConfig>,
#[serde(default)]
pub dto: DtoConfig,
#[serde(default)]
pub e2e: Option<E2eConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrateConfig {
pub name: String,
pub sources: Vec<PathBuf>,
#[serde(default = "default_version_from")]
pub version_from: String,
#[serde(default)]
pub core_import: Option<String>,
#[serde(default)]
pub workspace_root: Option<PathBuf>,
#[serde(default)]
pub skip_core_import: bool,
#[serde(default)]
pub features: Vec<String>,
#[serde(default)]
pub path_mappings: HashMap<String, String>,
}
fn default_version_from() -> String {
"Cargo.toml".to_string()
}
fn default_true() -> bool {
true
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GenerateConfig {
#[serde(default = "default_true")]
pub bindings: bool,
#[serde(default = "default_true")]
pub errors: bool,
#[serde(default = "default_true")]
pub configs: bool,
#[serde(default = "default_true")]
pub async_wrappers: bool,
#[serde(default = "default_true")]
pub type_conversions: bool,
#[serde(default = "default_true")]
pub package_metadata: bool,
#[serde(default = "default_true")]
pub public_api: bool,
}
impl Default for GenerateConfig {
fn default() -> Self {
Self {
bindings: true,
errors: true,
configs: true,
async_wrappers: true,
type_conversions: true,
package_metadata: true,
public_api: true,
}
}
}
impl AlefConfig {
pub fn features_for_language(&self, lang: extras::Language) -> &[String] {
let override_features = match lang {
extras::Language::Python => self.python.as_ref().and_then(|c| c.features.as_deref()),
extras::Language::Node => self.node.as_ref().and_then(|c| c.features.as_deref()),
extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.features.as_deref()),
extras::Language::Php => self.php.as_ref().and_then(|c| c.features.as_deref()),
extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.features.as_deref()),
extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.features.as_deref()),
extras::Language::Ffi => self.ffi.as_ref().and_then(|c| c.features.as_deref()),
extras::Language::Go => self.go.as_ref().and_then(|c| c.features.as_deref()),
extras::Language::Java => self.java.as_ref().and_then(|c| c.features.as_deref()),
extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.features.as_deref()),
extras::Language::R => self.r.as_ref().and_then(|c| c.features.as_deref()),
};
override_features.unwrap_or(&self.crate_config.features)
}
pub fn core_import(&self) -> String {
self.crate_config
.core_import
.clone()
.unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
}
pub fn ffi_prefix(&self) -> String {
self.ffi
.as_ref()
.and_then(|f| f.prefix.as_ref())
.cloned()
.unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
}
pub fn ffi_lib_name(&self) -> String {
if let Some(name) = self.ffi.as_ref().and_then(|f| f.lib_name.as_ref()) {
return name.clone();
}
if let Some(ffi_path) = self.output.ffi.as_ref() {
let path = std::path::Path::new(ffi_path);
let components: Vec<_> = path
.components()
.filter_map(|c| {
if let std::path::Component::Normal(s) = c {
s.to_str()
} else {
None
}
})
.collect();
let crate_dir = components
.iter()
.rev()
.find(|&&s| s != "src" && s != "lib" && s != "include")
.copied();
if let Some(dir) = crate_dir {
return dir.replace('-', "_");
}
}
format!("{}_ffi", self.ffi_prefix())
}
pub fn ffi_header_name(&self) -> String {
self.ffi
.as_ref()
.and_then(|f| f.header_name.as_ref())
.cloned()
.unwrap_or_else(|| format!("{}.h", self.ffi_prefix()))
}
pub fn python_module_name(&self) -> String {
self.python
.as_ref()
.and_then(|p| p.module_name.as_ref())
.cloned()
.unwrap_or_else(|| format!("_{}", self.crate_config.name.replace('-', "_")))
}
pub fn node_package_name(&self) -> String {
self.node
.as_ref()
.and_then(|n| n.package_name.as_ref())
.cloned()
.unwrap_or_else(|| self.crate_config.name.clone())
}
pub fn ruby_gem_name(&self) -> String {
self.ruby
.as_ref()
.and_then(|r| r.gem_name.as_ref())
.cloned()
.unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
}
pub fn php_extension_name(&self) -> String {
self.php
.as_ref()
.and_then(|p| p.extension_name.as_ref())
.cloned()
.unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
}
pub fn elixir_app_name(&self) -> String {
self.elixir
.as_ref()
.and_then(|e| e.app_name.as_ref())
.cloned()
.unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
}
pub fn go_module(&self) -> String {
self.go
.as_ref()
.and_then(|g| g.module.as_ref())
.cloned()
.unwrap_or_else(|| format!("github.com/kreuzberg-dev/{}", self.crate_config.name))
}
pub fn java_package(&self) -> String {
self.java
.as_ref()
.and_then(|j| j.package.as_ref())
.cloned()
.unwrap_or_else(|| "dev.kreuzberg".to_string())
}
pub fn java_group_id(&self) -> String {
self.java_package()
}
pub fn csharp_namespace(&self) -> String {
self.csharp
.as_ref()
.and_then(|c| c.namespace.as_ref())
.cloned()
.unwrap_or_else(|| {
use heck::ToPascalCase;
self.crate_config.name.to_pascal_case()
})
}
pub fn core_crate_dir(&self) -> String {
if let Some(first_source) = self.crate_config.sources.first() {
let path = std::path::Path::new(first_source);
let mut current = path.parent();
while let Some(dir) = current {
if dir.file_name().is_some_and(|n| n == "src") {
if let Some(crate_dir) = dir.parent() {
if let Some(dir_name) = crate_dir.file_name() {
return dir_name.to_string_lossy().into_owned();
}
}
break;
}
current = dir.parent();
}
}
self.crate_config.name.clone()
}
pub fn r_package_name(&self) -> String {
self.r
.as_ref()
.and_then(|r| r.package_name.as_ref())
.cloned()
.unwrap_or_else(|| self.crate_config.name.clone())
}
pub fn resolved_version(&self) -> Option<String> {
let content = std::fs::read_to_string(&self.crate_config.version_from).ok()?;
let value: toml::Value = toml::from_str(&content).ok()?;
if let Some(v) = value
.get("workspace")
.and_then(|w| w.get("package"))
.and_then(|p| p.get("version"))
.and_then(|v| v.as_str())
{
return Some(v.to_string());
}
value
.get("package")
.and_then(|p| p.get("version"))
.and_then(|v| v.as_str())
.map(|v| v.to_string())
}
pub fn serde_rename_all_for_language(&self, lang: extras::Language) -> String {
let override_val = match lang {
extras::Language::Python => self.python.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
extras::Language::Node => self.node.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
extras::Language::Ruby => self.ruby.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
extras::Language::Php => self.php.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
extras::Language::Elixir => self.elixir.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
extras::Language::Wasm => self.wasm.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
extras::Language::Ffi => self.ffi.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
extras::Language::Go => self.go.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
extras::Language::Java => self.java.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
extras::Language::Csharp => self.csharp.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
extras::Language::R => self.r.as_ref().and_then(|c| c.serde_rename_all.as_deref()),
};
if let Some(val) = override_val {
return val.to_string();
}
match lang {
extras::Language::Node | extras::Language::Wasm | extras::Language::Java | extras::Language::Csharp => {
"camelCase".to_string()
}
extras::Language::Python
| extras::Language::Ruby
| extras::Language::Php
| extras::Language::Go
| extras::Language::Ffi
| extras::Language::Elixir
| extras::Language::R => "snake_case".to_string(),
}
}
pub fn rewrite_path(&self, rust_path: &str) -> String {
let mut mappings: Vec<_> = self.crate_config.path_mappings.iter().collect();
mappings.sort_by(|a, b| b.0.len().cmp(&a.0.len()));
for (from, to) in &mappings {
if rust_path.starts_with(from.as_str()) {
return format!("{}{}", to, &rust_path[from.len()..]);
}
}
rust_path.to_string()
}
}
pub fn resolve_output_dir(config_path: Option<&PathBuf>, crate_name: &str, default: &str) -> String {
config_path
.map(|p| p.to_string_lossy().replace("{name}", crate_name))
.unwrap_or_else(|| default.replace("{name}", crate_name))
}
pub fn detect_serde_available(output_dir: &str) -> bool {
let src_path = std::path::Path::new(output_dir);
let mut dir = src_path;
loop {
let cargo_toml = dir.join("Cargo.toml");
if cargo_toml.exists() {
return cargo_toml_has_serde(&cargo_toml);
}
match dir.parent() {
Some(parent) if !parent.as_os_str().is_empty() => dir = parent,
_ => break,
}
}
false
}
fn cargo_toml_has_serde(path: &std::path::Path) -> bool {
let content = match std::fs::read_to_string(path) {
Ok(c) => c,
Err(_) => return false,
};
let has_serde_json = content.contains("serde_json");
let has_serde_dep = content.lines().any(|line| {
let trimmed = line.trim();
trimmed.starts_with("serde ")
|| trimmed.starts_with("serde=")
|| trimmed.starts_with("serde.")
|| trimmed == "[dependencies.serde]"
});
has_serde_json && has_serde_dep
}