Skip to main content

alef/core/config/
extras.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash, JsonSchema)]
5#[serde(rename_all = "lowercase")]
6pub enum Language {
7    Python,
8    Node,
9    Ruby,
10    Php,
11    Elixir,
12    Wasm,
13    Ffi,
14    Go,
15    Java,
16    Csharp,
17    R,
18    Rust,
19    Kotlin,
20    #[serde(rename = "kotlin_android", alias = "kotlin-android")]
21    KotlinAndroid,
22    Swift,
23    Dart,
24    Gleam,
25    Zig,
26    /// C consumer of the FFI layer — e2e test target, not a generated binding.
27    C,
28    /// Rust JNI shim crate emitter — paired with kotlin-android.
29    /// Emits `Java_*` symbols that mirror the Kotlin Bridge `external fun` declarations.
30    #[serde(rename = "jni")]
31    Jni,
32}
33
34impl Language {
35    /// ~keep Every `Language` variant, so name lists can be derived from the enum
36    /// rather than hand-maintained alongside it. Enumerated by hand because
37    /// `Language` has no derive-based variant iterator.
38    pub const ALL: [Language; 20] = [
39        Self::Python,
40        Self::Node,
41        Self::Ruby,
42        Self::Php,
43        Self::Elixir,
44        Self::Wasm,
45        Self::Ffi,
46        Self::Go,
47        Self::Java,
48        Self::Csharp,
49        Self::R,
50        Self::Rust,
51        Self::Kotlin,
52        Self::KotlinAndroid,
53        Self::Swift,
54        Self::Dart,
55        Self::Gleam,
56        Self::Zig,
57        Self::C,
58        Self::Jni,
59    ];
60}
61
62impl std::fmt::Display for Language {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        match self {
65            Self::Python => write!(f, "python"),
66            Self::Node => write!(f, "node"),
67            Self::Ruby => write!(f, "ruby"),
68            Self::Php => write!(f, "php"),
69            Self::Elixir => write!(f, "elixir"),
70            Self::Wasm => write!(f, "wasm"),
71            Self::Ffi => write!(f, "ffi"),
72            Self::Go => write!(f, "go"),
73            Self::Java => write!(f, "java"),
74            Self::Csharp => write!(f, "csharp"),
75            Self::R => write!(f, "r"),
76            Self::Rust => write!(f, "rust"),
77            Self::Kotlin => write!(f, "kotlin"),
78            Self::KotlinAndroid => write!(f, "kotlin_android"),
79            Self::Swift => write!(f, "swift"),
80            Self::Dart => write!(f, "dart"),
81            Self::Gleam => write!(f, "gleam"),
82            Self::Zig => write!(f, "zig"),
83            Self::C => write!(f, "c"),
84            Self::Jni => write!(f, "jni"),
85        }
86    }
87}
88
89/// A parameter in an adapter function.
90#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
91#[serde(deny_unknown_fields)]
92pub struct AdapterParam {
93    pub name: String,
94    #[serde(rename = "type")]
95    pub ty: String,
96    #[serde(default)]
97    pub optional: bool,
98}
99
100/// The kind of adapter pattern.
101#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
102#[serde(rename_all = "snake_case")]
103pub enum AdapterPattern {
104    SyncFunction,
105    AsyncMethod,
106    CallbackBridge,
107    Streaming,
108}
109
110/// Configuration for a single adapter.
111#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
112#[serde(deny_unknown_fields)]
113pub struct AdapterConfig {
114    pub name: String,
115    pub pattern: AdapterPattern,
116    /// Full Rust path to the core function/method (e.g., "sample_markdown_rs::convert")
117    pub core_path: String,
118    /// Parameters
119    #[serde(default)]
120    pub params: Vec<AdapterParam>,
121    /// Return type name
122    pub returns: Option<String>,
123    /// Error type name
124    pub error_type: Option<String>,
125    /// For async_method/streaming: the owning type name
126    pub owner_type: Option<String>,
127    /// For streaming: the item type
128    pub item_type: Option<String>,
129    /// For Python: release GIL during call
130    #[serde(default)]
131    pub gil_release: bool,
132    /// For callback_bridge: the Rust trait to implement (e.g., "MyHandler")
133    #[serde(default)]
134    pub trait_name: Option<String>,
135    /// For callback_bridge: the trait method name (e.g., "handle")
136    #[serde(default)]
137    pub trait_method: Option<String>,
138    /// For callback_bridge: whether to detect async callbacks at construction time
139    #[serde(default)]
140    pub detect_async: bool,
141    /// For streaming (FFI backend): full Rust type path of the request payload
142    /// deserialised from JSON (e.g. `"my_crate::ChatCompletionRequest"`).
143    /// Required when generating FFI streaming bodies — codegen will hard-fail
144    /// with a clear error if this field is absent on a streaming adapter.
145    #[serde(default)]
146    pub request_type: Option<String>,
147    /// Language backends for which this adapter should NOT be emitted.
148    ///
149    /// Mirrors the same field on `[[crates.e2e.calls.*]]`. Useful when a
150    /// consumer's core crate cannot compile on a given target (e.g.
151    /// a streaming crate on `wasm32-unknown-unknown` which has no working async
152    /// runtime for streaming). The adapter remains declared for every backend
153    /// where it works, with explicit per-backend opt-out rather than removing
154    /// the adapter entirely.
155    ///
156    /// Values must match the canonical TOML language names used in `languages`
157    /// (`"python"`, `"node"`, `"wasm"`, `"ruby"`, `"php"`, `"go"`,
158    /// `"java"`, `"csharp"`, `"elixir"`, `"kotlin"`, `"kotlin_android"`,
159    /// `"swift"`, `"dart"`, `"zig"`, `"ffi"`, `"r"`, `"gleam"`, `"c"`,
160    /// `"jni"`, `"rust"`). An unknown name fails at config-resolve time.
161    ///
162    /// Example: `skip_languages = ["wasm", "kotlin"]`
163    #[serde(default)]
164    pub skip_languages: Vec<String>,
165}
166
167/// Returns `true` when `lang_str` is a recognised canonical language name.
168pub fn is_known_language(lang_str: &str) -> bool {
169    matches!(
170        lang_str,
171        "python"
172            | "node"
173            | "ruby"
174            | "php"
175            | "elixir"
176            | "wasm"
177            | "ffi"
178            | "go"
179            | "java"
180            | "csharp"
181            | "r"
182            | "rust"
183            | "kotlin"
184            | "kotlin_android"
185            | "swift"
186            | "dart"
187            | "gleam"
188            | "zig"
189            | "c"
190            | "jni"
191    )
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    #[test]
199    fn adapter_config_skip_languages_deserializes() {
200        let toml_str = r#"
201            name = "crawl_stream"
202            pattern = "streaming"
203            core_path = "sample_crawler_core::crawl_stream"
204            owner_type = "CrawlEngine"
205            item_type = "CrawlResult"
206            skip_languages = ["wasm", "kotlin"]
207        "#;
208        let config: AdapterConfig = toml::from_str(toml_str).expect("deserialization failed");
209        assert_eq!(config.skip_languages, vec!["wasm", "kotlin"]);
210        assert_eq!(config.name, "crawl_stream");
211    }
212
213    #[test]
214    fn adapter_config_skip_languages_defaults_to_empty() {
215        let toml_str = r#"
216            name = "crawl_stream"
217            pattern = "streaming"
218            core_path = "sample_crawler_core::crawl_stream"
219        "#;
220        let config: AdapterConfig = toml::from_str(toml_str).expect("deserialization failed");
221        assert!(config.skip_languages.is_empty());
222    }
223
224    #[test]
225    fn is_known_language_accepts_all_canonical_names() {
226        for name in &[
227            "python",
228            "node",
229            "ruby",
230            "php",
231            "elixir",
232            "wasm",
233            "ffi",
234            "go",
235            "java",
236            "csharp",
237            "r",
238            "rust",
239            "kotlin",
240            "kotlin_android",
241            "swift",
242            "dart",
243            "gleam",
244            "zig",
245            "c",
246            "jni",
247        ] {
248            assert!(is_known_language(name), "{name} should be recognised");
249        }
250    }
251
252    #[test]
253    fn is_known_language_rejects_unknown() {
254        assert!(!is_known_language("wasm32"));
255        assert!(!is_known_language("kotlin-android"));
256        assert!(!is_known_language(""));
257    }
258
259    #[test]
260    fn all_lists_every_variant() {
261        for language in Language::ALL {
262            // ~keep: exhaustive over Language, so adding a variant stops this file
263            // compiling until it is also added to ALL — nothing else enforces that.
264            match language {
265                Language::Python
266                | Language::Node
267                | Language::Ruby
268                | Language::Php
269                | Language::Elixir
270                | Language::Wasm
271                | Language::Ffi
272                | Language::Go
273                | Language::Java
274                | Language::Csharp
275                | Language::R
276                | Language::Rust
277                | Language::Kotlin
278                | Language::KotlinAndroid
279                | Language::Swift
280                | Language::Dart
281                | Language::Gleam
282                | Language::Zig
283                | Language::C
284                | Language::Jni => {}
285            }
286        }
287        let distinct: std::collections::HashSet<String> = Language::ALL.iter().map(ToString::to_string).collect();
288        assert_eq!(
289            distinct.len(),
290            Language::ALL.len(),
291            "Language::ALL must not repeat a variant"
292        );
293    }
294}