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