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