Skip to main content

alef_adapters/
async_method.rs

1use alef_core::config::{AdapterConfig, AlefConfig, Language};
2
3/// Generate just the method body (what goes inside `{ ... }`) for an async method adapter.
4pub fn generate_body(adapter: &AdapterConfig, language: Language, config: &AlefConfig) -> anyhow::Result<String> {
5    let body = match language {
6        Language::Python => gen_python_body(adapter, config),
7        Language::Node => gen_node_body(adapter, config),
8        Language::Ruby => gen_ruby_body(adapter, config),
9        Language::Php => gen_php_body(adapter, config),
10        Language::Elixir => gen_elixir_body(adapter, config),
11        Language::Wasm => gen_wasm_body(adapter, config),
12        Language::Ffi => gen_ffi_body(adapter, config),
13        Language::Go => gen_go_body(adapter, config),
14        Language::Java => gen_java_body(adapter, config),
15        Language::Csharp => gen_csharp_body(adapter, config),
16        Language::R => gen_r_body(adapter, config),
17        Language::Rust => anyhow::bail!("Rust does not need generated binding adapters"),
18    };
19    Ok(body)
20}
21
22/// Build the call arguments with `.into()` conversion.
23fn call_args(adapter: &AdapterConfig) -> Vec<String> {
24    adapter
25        .params
26        .iter()
27        .map(|p| {
28            if p.optional {
29                format!("{}.map(Into::into)", p.name)
30            } else {
31                format!("{}.into()", p.name)
32            }
33        })
34        .collect()
35}
36
37/// Build conversion let-bindings for core types (used in Python async).
38fn core_let_bindings(adapter: &AdapterConfig, core_import: &str) -> Vec<String> {
39    adapter
40        .params
41        .iter()
42        .map(|p| {
43            if p.optional {
44                format!(
45                    "let core_{name} = {name}.map(|v| -> {core_import}::{ty} {{ v.into() }});",
46                    name = p.name,
47                    core_import = core_import,
48                    ty = p.ty,
49                )
50            } else {
51                format!(
52                    "let core_{name}: {core_import}::{ty} = {name}.into();",
53                    name = p.name,
54                    core_import = core_import,
55                    ty = p.ty,
56                )
57            }
58        })
59        .collect()
60}
61
62/// Build core call arguments (prefixed with core_).
63fn core_call_args(adapter: &AdapterConfig) -> Vec<String> {
64    adapter.params.iter().map(|p| format!("core_{}", p.name)).collect()
65}
66
67// ---------------------------------------------------------------------------
68// Python (PyO3)
69// ---------------------------------------------------------------------------
70
71fn gen_python_body(adapter: &AdapterConfig, config: &AlefConfig) -> String {
72    let core_path = &adapter.core_path;
73    let returns = adapter.returns.as_deref().unwrap_or("()");
74    let core_import = config.core_import();
75
76    let let_bindings = core_let_bindings(adapter, &core_import);
77    let core_args = core_call_args(adapter);
78    let core_call_str = core_args.join(", ");
79
80    let bindings_block = if let_bindings.is_empty() {
81        String::new()
82    } else {
83        format!("{}\n    ", let_bindings.join("\n    "))
84    };
85
86    format!(
87        "let inner = self.inner.clone();\n    \
88         {bindings_block}\
89         pyo3_async_runtimes::tokio::future_into_py(py, async move {{\n        \
90             let result = inner.{core_path}({core_call_str}).await\n            \
91                 .map_err(|e| PyErr::new::<PyRuntimeError, _>(e.to_string()))?;\n        \
92             Ok({returns}::from(result))\n    \
93         }})"
94    )
95}
96
97// ---------------------------------------------------------------------------
98// Node (NAPI)
99// ---------------------------------------------------------------------------
100
101fn gen_node_body(adapter: &AdapterConfig, _config: &AlefConfig) -> String {
102    let core_path = &adapter.core_path;
103    let returns = adapter.returns.as_deref().unwrap_or("()");
104
105    let args = call_args(adapter);
106    let call_str = args.join(", ");
107
108    format!(
109        "let core_req = {call_str};\n    \
110         self.inner.{core_path}(core_req).await\n        \
111         .map({returns}::from)\n        \
112         .map_err(|e| napi::Error::new(napi::Status::GenericFailure, e.to_string()))"
113    )
114}
115
116// ---------------------------------------------------------------------------
117// Ruby (Magnus)
118// ---------------------------------------------------------------------------
119
120fn gen_ruby_body(adapter: &AdapterConfig, _config: &AlefConfig) -> String {
121    let core_path = &adapter.core_path;
122    let returns = adapter.returns.as_deref().unwrap_or("()");
123
124    let args = call_args(adapter);
125    let call_str = args.join(", ");
126
127    format!(
128        "let rt = tokio::runtime::Runtime::new()\n        \
129             .map_err(|e| magnus::Error::new(magnus::exception::runtime_error(), e.to_string()))?;\n    \
130         let core_req = {call_str};\n    \
131         rt.block_on(async {{ self.inner.{core_path}(core_req).await }})\n        \
132         .map({returns}::from)\n        \
133         .map_err(|e| magnus::Error::new(magnus::exception::runtime_error(), e.to_string()))"
134    )
135}
136
137// ---------------------------------------------------------------------------
138// PHP (ext-php-rs)
139// ---------------------------------------------------------------------------
140
141fn gen_php_body(adapter: &AdapterConfig, _config: &AlefConfig) -> String {
142    let core_path = &adapter.core_path;
143    let returns = adapter.returns.as_deref().unwrap_or("()");
144
145    let args = call_args(adapter);
146    let call_str = args.join(", ");
147
148    format!(
149        "WORKER_RUNTIME.block_on(async {{\n        \
150             self.inner.{core_path}({call_str}.into()).await\n    \
151         }})\n    \
152         .map({returns}::from)\n    \
153         .map_err(|e| ext_php_rs::exception::PhpException::default(e.to_string()).into())"
154    )
155}
156
157// ---------------------------------------------------------------------------
158// Elixir (Rustler)
159// ---------------------------------------------------------------------------
160
161fn gen_elixir_body(adapter: &AdapterConfig, _config: &AlefConfig) -> String {
162    let core_path = &adapter.core_path;
163    let returns = adapter.returns.as_deref().unwrap_or("()");
164
165    let args = call_args(adapter);
166    let call_str = args.join(", ");
167
168    format!(
169        "let rt = tokio::runtime::Runtime::new().map_err(|e| e.to_string())?;\n    \
170         rt.block_on(async {{ client.inner.{core_path}({call_str}).await }})\n        \
171         .map({returns}::from)\n        \
172         .map_err(|e| e.to_string())"
173    )
174}
175
176// ---------------------------------------------------------------------------
177// WASM (wasm-bindgen)
178// ---------------------------------------------------------------------------
179
180fn gen_wasm_body(adapter: &AdapterConfig, _config: &AlefConfig) -> String {
181    let core_path = &adapter.core_path;
182    let returns = adapter.returns.as_deref().unwrap_or("JsValue");
183
184    let args = call_args(adapter);
185    let call_str = args.join(", ");
186
187    format!(
188        "self.inner.{core_path}({call_str}).await\n        \
189         .map({returns}::from)\n        \
190         .map_err(|e| JsValue::from_str(&e.to_string()))"
191    )
192}
193
194// ---------------------------------------------------------------------------
195// FFI (C ABI) -- async becomes sync via block_on
196// ---------------------------------------------------------------------------
197
198fn gen_ffi_body(adapter: &AdapterConfig, config: &AlefConfig) -> String {
199    let core_path = &adapter.core_path;
200    let prefix = config.ffi_prefix();
201    let owner_type = adapter.owner_type.as_deref().unwrap_or("Self");
202    let owner_snake = to_snake_case(owner_type);
203    let _ = prefix;
204    let _ = owner_snake;
205
206    let conversions: Vec<String> = adapter
207        .params
208        .iter()
209        .map(|p| {
210            if p.ty == "String" || p.ty == "&str" {
211                format!(
212                    "let {name} = unsafe {{ std::ffi::CStr::from_ptr({name}) }}\n        \
213                     .to_str()\n        \
214                     .unwrap_or_default()\n        \
215                     .to_owned();",
216                    name = p.name,
217                )
218            } else {
219                format!(
220                    "let {name}_str = unsafe {{ std::ffi::CStr::from_ptr({name}_json) }}\n        \
221                     .to_str()\n        \
222                     .unwrap_or_default();\n    \
223                     let {name}: {ty} = match serde_json::from_str({name}_str) {{\n        \
224                         Ok(v) => v,\n        \
225                         Err(e) => {{\n            \
226                             update_last_error(e);\n            \
227                             return std::ptr::null_mut();\n        \
228                         }}\n    \
229                     }};",
230                    name = p.name,
231                    ty = p.ty,
232                )
233            }
234        })
235        .collect();
236
237    let call_args_list: Vec<String> = adapter.params.iter().map(|p| p.name.clone()).collect();
238    let call_str = call_args_list.join(", ");
239    let conversion_block = if conversions.is_empty() {
240        String::new()
241    } else {
242        format!("{}\n    ", conversions.join("\n    "))
243    };
244
245    format!(
246        "let client = unsafe {{ &*client }};\n    \
247         {conversion_block}\
248         let rt = match tokio::runtime::Runtime::new() {{\n        \
249             Ok(rt) => rt,\n        \
250             Err(e) => {{\n            \
251                 update_last_error(e);\n            \
252                 return std::ptr::null_mut();\n        \
253             }}\n    \
254         }};\n    \
255         match rt.block_on(async {{ client.inner.{core_path}({call_str}).await }}) {{\n        \
256             Ok(result) => {{\n            \
257                 let json = serde_json::to_string(&result).unwrap_or_default();\n            \
258                 std::ffi::CString::new(json).unwrap_or_default().into_raw()\n        \
259             }}\n        \
260             Err(e) => {{\n            \
261                 update_last_error(e);\n            \
262                 std::ptr::null_mut()\n        \
263             }}\n    \
264         }}"
265    )
266}
267
268// ---------------------------------------------------------------------------
269// Go (wraps C FFI)
270// ---------------------------------------------------------------------------
271
272fn gen_go_body(adapter: &AdapterConfig, config: &AlefConfig) -> String {
273    let name = &adapter.name;
274    let prefix = config.ffi_prefix();
275    let returns = adapter.returns.as_deref().unwrap_or("string");
276    let owner_type = adapter.owner_type.as_deref().unwrap_or("Client");
277    let owner_snake = to_snake_case(owner_type);
278
279    let marshal_block: Vec<String> = adapter
280        .params
281        .iter()
282        .filter(|p| p.ty != "String" && p.ty != "&str")
283        .map(|p| {
284            format!(
285                "{name}JSON, err := json.Marshal({name})\n    \
286                 if err != nil {{\n        \
287                     return nil, err\n    \
288                 }}",
289                name = p.name,
290            )
291        })
292        .collect();
293
294    let c_call_args: Vec<String> = adapter
295        .params
296        .iter()
297        .map(|p| {
298            if p.ty == "String" || p.ty == "&str" {
299                format!("C.CString({})", p.name)
300            } else {
301                format!("C.CString(string({name}JSON))", name = p.name)
302            }
303        })
304        .collect();
305
306    let call_str = c_call_args.join(", ");
307    let marshal_str = if marshal_block.is_empty() {
308        String::new()
309    } else {
310        format!("{}\n    ", marshal_block.join("\n    "))
311    };
312
313    format!(
314        "{marshal_str}\
315         result := C.{prefix}_{owner_snake}_{name}(c.ptr, {call_str})\n    \
316         if result == nil {{\n        \
317             return nil, fmt.Errorf(\"%s\", lastError())\n    \
318         }}\n    \
319         defer C.free(unsafe.Pointer(result))\n    \
320         var out {returns}\n    \
321         if err := json.Unmarshal([]byte(C.GoString(result)), &out); err != nil {{\n        \
322             return nil, err\n    \
323         }}\n    \
324         return &out, nil"
325    )
326}
327
328// ---------------------------------------------------------------------------
329// Java (Panama FFI)
330// ---------------------------------------------------------------------------
331
332fn gen_java_body(adapter: &AdapterConfig, config: &AlefConfig) -> String {
333    let name = &adapter.name;
334    let prefix = config.ffi_prefix();
335    let owner_type = adapter.owner_type.as_deref().unwrap_or("Client");
336    let owner_snake = to_snake_case(owner_type);
337
338    let arg_pass = if adapter.params.is_empty() {
339        String::new()
340    } else {
341        format!(
342            ", {}",
343            adapter
344                .params
345                .iter()
346                .map(|p| {
347                    if p.ty == "String" || p.ty == "&str" {
348                        format!("arena.allocateFrom({})", p.name)
349                    } else {
350                        p.name.clone()
351                    }
352                })
353                .collect::<Vec<_>>()
354                .join(", ")
355        )
356    };
357
358    format!(
359        "try (var arena = Arena.ofConfined()) {{\n\
360         \x20           var result = (MemorySegment) {prefix}_{owner_snake}_{name}.invokeExact(this.handle, arena{arg_pass});\n\
361         \x20           if (result.equals(MemorySegment.NULL)) {{\n\
362         \x20               throw new RuntimeException(lastError());\n\
363         \x20           }}\n\
364         \x20           return result.getString(0);\n\
365         \x20       }}"
366    )
367}
368
369// ---------------------------------------------------------------------------
370// C# (P/Invoke)
371// ---------------------------------------------------------------------------
372
373fn gen_csharp_body(adapter: &AdapterConfig, config: &AlefConfig) -> String {
374    let name = &adapter.name;
375    let prefix = config.ffi_prefix();
376    let owner_type = adapter.owner_type.as_deref().unwrap_or("Client");
377    let owner_snake = to_snake_case(owner_type);
378
379    let call_args_list: Vec<String> = adapter.params.iter().map(|p| p.name.clone()).collect();
380    let call_str = call_args_list.join(", ");
381    let call_pass = if call_str.is_empty() {
382        String::new()
383    } else {
384        format!(", {}", call_str)
385    };
386
387    format!(
388        "var ptr = {prefix}_{owner_snake}_{name}_native(this.handle{call_pass});\n\
389         \x20       if (ptr == IntPtr.Zero)\n\
390         \x20           throw new InvalidOperationException(GetLastError());\n\
391         \x20       try {{ return Marshal.PtrToStringUTF8(ptr)!; }}\n\
392         \x20       finally {{ FreeString(ptr); }}"
393    )
394}
395
396// ---------------------------------------------------------------------------
397// R (extendr)
398// ---------------------------------------------------------------------------
399
400fn gen_r_body(adapter: &AdapterConfig, _config: &AlefConfig) -> String {
401    let core_path = &adapter.core_path;
402    let returns = adapter.returns.as_deref().unwrap_or("Robj");
403
404    let args = call_args(adapter);
405    let call_str = args.join(", ");
406
407    format!(
408        "let rt = tokio::runtime::Runtime::new()\n        \
409             .map_err(|e| extendr_api::Error::Other(e.to_string()))?;\n    \
410         rt.block_on(async {{ self.inner.{core_path}({call_str}).await }})\n        \
411         .map({returns}::from)\n        \
412         .map_err(|e| extendr_api::Error::Other(e.to_string()))"
413    )
414}
415
416// ---------------------------------------------------------------------------
417// Helpers
418// ---------------------------------------------------------------------------
419
420fn to_snake_case(s: &str) -> String {
421    let mut result = String::new();
422    for (i, ch) in s.chars().enumerate() {
423        if ch.is_uppercase() {
424            if i > 0 {
425                result.push('_');
426            }
427            result.extend(ch.to_lowercase());
428        } else {
429            result.push(ch);
430        }
431    }
432    result
433}