Skip to main content

alef_core/config/
mod.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use std::path::PathBuf;
4
5pub mod dto;
6pub mod extras;
7pub mod languages;
8pub mod output;
9
10// Re-exports for backward compatibility — all types were previously flat in config.rs.
11pub use dto::{
12    CsharpDtoStyle, DtoConfig, ElixirDtoStyle, GoDtoStyle, JavaDtoStyle, NodeDtoStyle, PhpDtoStyle, PythonDtoStyle,
13    RDtoStyle, RubyDtoStyle,
14};
15pub use extras::{AdapterConfig, AdapterParam, AdapterPattern, Language};
16pub use languages::{
17    CSharpConfig, CustomModulesConfig, CustomRegistration, CustomRegistrationsConfig, ElixirConfig, FfiConfig,
18    GoConfig, JavaConfig, NodeConfig, PhpConfig, PythonConfig, RConfig, RubyConfig, StubsConfig, WasmConfig,
19};
20pub use output::{
21    ExcludeConfig, IncludeConfig, LintConfig, OutputConfig, ReadmeConfig, ScaffoldConfig, SyncConfig, TestConfig,
22    TextReplacement,
23};
24
25/// Root configuration from alef.toml.
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct AlefConfig {
28    #[serde(rename = "crate")]
29    pub crate_config: CrateConfig,
30    pub languages: Vec<Language>,
31    #[serde(default)]
32    pub exclude: ExcludeConfig,
33    #[serde(default)]
34    pub include: IncludeConfig,
35    #[serde(default)]
36    pub output: OutputConfig,
37    #[serde(default)]
38    pub python: Option<PythonConfig>,
39    #[serde(default)]
40    pub node: Option<NodeConfig>,
41    #[serde(default)]
42    pub ruby: Option<RubyConfig>,
43    #[serde(default)]
44    pub php: Option<PhpConfig>,
45    #[serde(default)]
46    pub elixir: Option<ElixirConfig>,
47    #[serde(default)]
48    pub wasm: Option<WasmConfig>,
49    #[serde(default)]
50    pub ffi: Option<FfiConfig>,
51    #[serde(default)]
52    pub go: Option<GoConfig>,
53    #[serde(default)]
54    pub java: Option<JavaConfig>,
55    #[serde(default)]
56    pub csharp: Option<CSharpConfig>,
57    #[serde(default)]
58    pub r: Option<RConfig>,
59    #[serde(default)]
60    pub scaffold: Option<ScaffoldConfig>,
61    #[serde(default)]
62    pub readme: Option<ReadmeConfig>,
63    #[serde(default)]
64    pub lint: Option<HashMap<String, LintConfig>>,
65    #[serde(default)]
66    pub test: Option<HashMap<String, TestConfig>>,
67    #[serde(default)]
68    pub custom_files: Option<HashMap<String, Vec<PathBuf>>>,
69    #[serde(default)]
70    pub adapters: Vec<AdapterConfig>,
71    #[serde(default)]
72    pub custom_modules: CustomModulesConfig,
73    #[serde(default)]
74    pub custom_registrations: CustomRegistrationsConfig,
75    #[serde(default)]
76    pub sync: Option<SyncConfig>,
77    /// Declare opaque types from external crates that alef can't extract.
78    /// Map of type name → Rust path (e.g., "Tree" = "tree_sitter_language_pack::Tree").
79    /// These get opaque wrapper structs in all backends.
80    #[serde(default)]
81    pub opaque_types: HashMap<String, String>,
82    /// Controls which generation passes alef runs (all default to true).
83    #[serde(default)]
84    pub generate: GenerateConfig,
85    /// Per-language overrides for generate flags (key = language name, e.g., "python").
86    #[serde(default)]
87    pub generate_overrides: HashMap<String, GenerateConfig>,
88    /// Per-language DTO/type generation style (dataclass vs TypedDict, zod vs interface, etc.).
89    #[serde(default)]
90    pub dto: DtoConfig,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct CrateConfig {
95    pub name: String,
96    pub sources: Vec<PathBuf>,
97    #[serde(default = "default_version_from")]
98    pub version_from: String,
99    #[serde(default)]
100    pub core_import: Option<String>,
101    /// Optional workspace root path for resolving `pub use` re-exports from sibling crates.
102    #[serde(default)]
103    pub workspace_root: Option<PathBuf>,
104    /// When true, skip adding `use {core_import};` to generated bindings.
105    #[serde(default)]
106    pub skip_core_import: bool,
107    /// Maps extracted rust_path prefixes to actual import paths in binding crates.
108    /// Example: { "spikard" = "spikard_http" } rewrites "spikard::ServerConfig" to "spikard_http::ServerConfig"
109    #[serde(default)]
110    pub path_mappings: HashMap<String, String>,
111}
112
113fn default_version_from() -> String {
114    "Cargo.toml".to_string()
115}
116
117fn default_true() -> bool {
118    true
119}
120
121/// Controls which generation passes alef runs.
122/// All flags default to `true`; set to `false` to skip a pass.
123/// Can be overridden per-language via `[generate_overrides.<lang>]`.
124#[derive(Debug, Clone, Serialize, Deserialize)]
125pub struct GenerateConfig {
126    /// Generate low-level struct wrappers, From impls, module init (default: true)
127    #[serde(default = "default_true")]
128    pub bindings: bool,
129    /// Generate error type hierarchies from thiserror enums (default: true)
130    #[serde(default = "default_true")]
131    pub errors: bool,
132    /// Generate config builder constructors from Default types (default: true)
133    #[serde(default = "default_true")]
134    pub configs: bool,
135    /// Generate async/sync function pairs with runtime management (default: true)
136    #[serde(default = "default_true")]
137    pub async_wrappers: bool,
138    /// Generate recursive type marshaling helpers (default: true)
139    #[serde(default = "default_true")]
140    pub type_conversions: bool,
141    /// Generate package manifests (pyproject.toml, package.json, etc.) (default: true)
142    #[serde(default = "default_true")]
143    pub package_metadata: bool,
144    /// Generate idiomatic public API wrappers (default: true)
145    #[serde(default = "default_true")]
146    pub public_api: bool,
147}
148
149impl Default for GenerateConfig {
150    fn default() -> Self {
151        Self {
152            bindings: true,
153            errors: true,
154            configs: true,
155            async_wrappers: true,
156            type_conversions: true,
157            package_metadata: true,
158            public_api: true,
159        }
160    }
161}
162
163// ---------------------------------------------------------------------------
164// Shared config resolution helpers
165// ---------------------------------------------------------------------------
166
167impl AlefConfig {
168    /// Get the core crate import path (e.g., "liter_llm"). Used by codegen to call into the core crate.
169    pub fn core_import(&self) -> String {
170        self.crate_config
171            .core_import
172            .clone()
173            .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
174    }
175
176    /// Get the FFI prefix (e.g., "kreuzberg"). Used by FFI, Go, Java, C# backends.
177    pub fn ffi_prefix(&self) -> String {
178        self.ffi
179            .as_ref()
180            .and_then(|f| f.prefix.as_ref())
181            .cloned()
182            .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
183    }
184
185    /// Get the FFI native library name (for Go cgo, Java Panama, C# P/Invoke).
186    ///
187    /// Resolution order:
188    /// 1. `[ffi] lib_name` explicit override
189    /// 2. Directory name of `output.ffi` path with hyphens replaced by underscores
190    ///    (e.g. `crates/html-to-markdown-ffi/src/` → `html_to_markdown_ffi`)
191    /// 3. `{ffi_prefix}_ffi` fallback
192    pub fn ffi_lib_name(&self) -> String {
193        // 1. Explicit override in [ffi] section.
194        if let Some(name) = self.ffi.as_ref().and_then(|f| f.lib_name.as_ref()) {
195            return name.clone();
196        }
197
198        // 2. Derive from output.ffi path: take the last meaningful directory component
199        //    (skip trailing "src" or similar), then replace hyphens with underscores.
200        if let Some(ffi_path) = self.output.ffi.as_ref() {
201            let path = std::path::Path::new(ffi_path);
202            // Walk components from the end to find the crate directory name.
203            // Skip components like "src" that are inside the crate dir.
204            let components: Vec<_> = path
205                .components()
206                .filter_map(|c| {
207                    if let std::path::Component::Normal(s) = c {
208                        s.to_str()
209                    } else {
210                        None
211                    }
212                })
213                .collect();
214            // The crate name is typically the last component that looks like a crate dir
215            // (i.e. not "src", "lib", or similar). Search from the end.
216            let crate_dir = components
217                .iter()
218                .rev()
219                .find(|&&s| s != "src" && s != "lib" && s != "include")
220                .copied();
221            if let Some(dir) = crate_dir {
222                return dir.replace('-', "_");
223            }
224        }
225
226        // 3. Default fallback.
227        format!("{}_ffi", self.ffi_prefix())
228    }
229
230    /// Get the FFI header name.
231    pub fn ffi_header_name(&self) -> String {
232        self.ffi
233            .as_ref()
234            .and_then(|f| f.header_name.as_ref())
235            .cloned()
236            .unwrap_or_else(|| format!("{}.h", self.ffi_prefix()))
237    }
238
239    /// Get the Python module name.
240    pub fn python_module_name(&self) -> String {
241        self.python
242            .as_ref()
243            .and_then(|p| p.module_name.as_ref())
244            .cloned()
245            .unwrap_or_else(|| format!("_{}", self.crate_config.name.replace('-', "_")))
246    }
247
248    /// Get the Node package name.
249    pub fn node_package_name(&self) -> String {
250        self.node
251            .as_ref()
252            .and_then(|n| n.package_name.as_ref())
253            .cloned()
254            .unwrap_or_else(|| self.crate_config.name.clone())
255    }
256
257    /// Get the Ruby gem name.
258    pub fn ruby_gem_name(&self) -> String {
259        self.ruby
260            .as_ref()
261            .and_then(|r| r.gem_name.as_ref())
262            .cloned()
263            .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
264    }
265
266    /// Get the PHP extension name.
267    pub fn php_extension_name(&self) -> String {
268        self.php
269            .as_ref()
270            .and_then(|p| p.extension_name.as_ref())
271            .cloned()
272            .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
273    }
274
275    /// Get the Elixir app name.
276    pub fn elixir_app_name(&self) -> String {
277        self.elixir
278            .as_ref()
279            .and_then(|e| e.app_name.as_ref())
280            .cloned()
281            .unwrap_or_else(|| self.crate_config.name.replace('-', "_"))
282    }
283
284    /// Get the Go module path.
285    pub fn go_module(&self) -> String {
286        self.go
287            .as_ref()
288            .and_then(|g| g.module.as_ref())
289            .cloned()
290            .unwrap_or_else(|| format!("github.com/kreuzberg-dev/{}", self.crate_config.name))
291    }
292
293    /// Get the Java package name.
294    pub fn java_package(&self) -> String {
295        self.java
296            .as_ref()
297            .and_then(|j| j.package.as_ref())
298            .cloned()
299            .unwrap_or_else(|| "dev.kreuzberg".to_string())
300    }
301
302    /// Get the C# namespace.
303    pub fn csharp_namespace(&self) -> String {
304        self.csharp
305            .as_ref()
306            .and_then(|c| c.namespace.as_ref())
307            .cloned()
308            .unwrap_or_else(|| {
309                use heck::ToPascalCase;
310                self.crate_config.name.to_pascal_case()
311            })
312    }
313
314    /// Get the R package name.
315    pub fn r_package_name(&self) -> String {
316        self.r
317            .as_ref()
318            .and_then(|r| r.package_name.as_ref())
319            .cloned()
320            .unwrap_or_else(|| self.crate_config.name.clone())
321    }
322
323    /// Rewrite a rust_path using path_mappings.
324    /// Matches the longest prefix first.
325    pub fn rewrite_path(&self, rust_path: &str) -> String {
326        // Sort mappings by key length descending (longest prefix first)
327        let mut mappings: Vec<_> = self.crate_config.path_mappings.iter().collect();
328        mappings.sort_by(|a, b| b.0.len().cmp(&a.0.len()));
329
330        for (from, to) in &mappings {
331            if rust_path.starts_with(from.as_str()) {
332                return format!("{}{}", to, &rust_path[from.len()..]);
333            }
334        }
335        rust_path.to_string()
336    }
337}
338
339/// Helper function to resolve output directory path from config.
340/// Replaces {name} placeholder with the crate name.
341pub fn resolve_output_dir(config_path: Option<&PathBuf>, crate_name: &str, default: &str) -> String {
342    config_path
343        .map(|p| p.to_string_lossy().replace("{name}", crate_name))
344        .unwrap_or_else(|| default.replace("{name}", crate_name))
345}
346
347/// Detect whether `serde` and `serde_json` are available in a binding crate's Cargo.toml.
348///
349/// `output_dir` is the generated source directory (e.g., `crates/spikard-py/src/`).
350/// The function walks up to find the crate's Cargo.toml and checks its `[dependencies]`
351/// for both `serde` and `serde_json`.
352pub fn detect_serde_available(output_dir: &str) -> bool {
353    let src_path = std::path::Path::new(output_dir);
354    // Walk up from the output dir to find Cargo.toml (usually output_dir is `crates/foo/src/`)
355    let mut dir = src_path;
356    loop {
357        let cargo_toml = dir.join("Cargo.toml");
358        if cargo_toml.exists() {
359            return cargo_toml_has_serde(&cargo_toml);
360        }
361        match dir.parent() {
362            Some(parent) if !parent.as_os_str().is_empty() => dir = parent,
363            _ => break,
364        }
365    }
366    false
367}
368
369/// Check if a Cargo.toml has both `serde` (with derive feature) and `serde_json` in its dependencies.
370///
371/// The `serde::Serialize` derive macro requires `serde` as a direct dependency with the `derive`
372/// feature enabled. Having only `serde_json` is not sufficient since it only pulls in `serde`
373/// transitively without the derive proc-macro.
374fn cargo_toml_has_serde(path: &std::path::Path) -> bool {
375    let content = match std::fs::read_to_string(path) {
376        Ok(c) => c,
377        Err(_) => return false,
378    };
379
380    let has_serde_json = content.contains("serde_json");
381    // Check for `serde` as a direct dependency (not just serde_json).
382    // Must match "serde" as a TOML key, not as a substring of "serde_json".
383    // Valid patterns: `serde = `, `serde.`, `[dependencies.serde]`
384    let has_serde_dep = content.lines().any(|line| {
385        let trimmed = line.trim();
386        // Match `serde = ...` or `serde.workspace = true` etc., but not `serde_json`
387        trimmed.starts_with("serde ")
388            || trimmed.starts_with("serde=")
389            || trimmed.starts_with("serde.")
390            || trimmed == "[dependencies.serde]"
391    });
392
393    has_serde_json && has_serde_dep
394}