Skip to main content

alef/codegen/error_gen/
native.rs

1pub fn gen_magnus_error_methods_struct(error: &ErrorDef, core_import: &str) -> String {
2    if error.methods.is_empty() {
3        return String::new();
4    }
5
6    let rust_path = if error.rust_path.is_empty() {
7        format!("{core_import}::{}", error.name)
8    } else {
9        error.rust_path.replace('-', "_")
10    };
11
12    let struct_name = format!("{}Info", error.name);
13
14    let mut fields = Vec::new();
15    let mut methods = Vec::new();
16    let mut ctor_assignments = Vec::new();
17
18    for method in &error.methods {
19        match method.name.as_str() {
20            "status_code" => {
21                fields.push("    status_code: u16,".to_string());
22                methods.push(
23                    concat!(
24                        "    /// HTTP status code for this error (0 means no associated status).\n",
25                        "    pub fn status_code(&self) -> u16 {\n",
26                        "        self.status_code\n",
27                        "    }",
28                    )
29                    .to_string(),
30                );
31                ctor_assignments.push("        status_code: e.status_code(),".to_string());
32            }
33            "is_transient" => {
34                fields.push("    is_transient: bool,".to_string());
35                methods.push(
36                    concat!(
37                        "    /// Returns `true` if the error is transient and a retry may succeed.\n",
38                        "    pub fn transient(&self) -> bool {\n",
39                        "        self.is_transient\n",
40                        "    }",
41                    )
42                    .to_string(),
43                );
44                ctor_assignments.push("        is_transient: e.is_transient(),".to_string());
45            }
46            "error_type" => {
47                fields.push("    error_type: String,".to_string());
48                methods.push(
49                    concat!(
50                        "    /// Machine-readable error category string for matching and logging.\n",
51                        "    pub fn error_type(&self) -> String {\n",
52                        "        self.error_type.clone()\n",
53                        "    }",
54                    )
55                    .to_string(),
56                );
57                ctor_assignments.push("        error_type: e.error_type().to_string(),".to_string());
58            }
59            other => {
60                methods.push(format!("    // Not emitted: method `{other}` on `{struct_name}`"));
61            }
62        }
63    }
64
65    let struct_def = format!(
66        "#[magnus::wrap(class = \"{struct_name}\", free_immediately, size)]\npub struct {struct_name} {{\n{}\n}}",
67        fields.join("\n")
68    );
69
70    let from_fn = format!(
71        "#[allow(dead_code)]\nfn {snake_name}_info(e: &{rust_path}) -> {struct_name} {{\n    {struct_name} {{\n{}\n    }}\n}}",
72        ctor_assignments.join("\n"),
73        snake_name = to_snake_case(&error.name),
74    );
75
76    let impl_block = format!("impl {struct_name} {{\n{}\n}}", methods.join("\n\n"));
77
78    format!("{struct_def}\n\n{from_fn}\n\n{impl_block}")
79}
80
81/// Returns the `define_class` + `define_method` registration lines for the error info struct.
82pub fn magnus_error_methods_registrations(error: &ErrorDef) -> Vec<String> {
83    if error.methods.is_empty() {
84        return Vec::new();
85    }
86    let struct_name = format!("{}Info", error.name);
87    let snake = to_snake_case(&error.name);
88    let class_var = format!("{snake}_info_class");
89    let mut lines = Vec::new();
90    lines.push(format!(
91        "    let {class_var} = module.define_class(\"{struct_name}\", ruby.class_object())?;"
92    ));
93    for method in &error.methods {
94        let (ruby_name, rust_fn) = if method.name == "is_transient" {
95            ("transient?".to_string(), "transient".to_string())
96        } else {
97            (method.name.clone(), method.name.clone())
98        };
99        lines.push(format!(
100            "    {class_var}.define_method(\"{ruby_name}\", magnus::method!({struct_name}::{rust_fn}, 0))?;"
101        ));
102    }
103    lines
104}
105
106/// Generate a converter function that maps a core error to `PhpException`.
107pub fn gen_php_error_converter(error: &ErrorDef, core_import: &str) -> String {
108    let rust_path = if error.rust_path.is_empty() {
109        format!("{core_import}::{}", error.name)
110    } else {
111        error.rust_path.replace('-', "_")
112    };
113
114    let fn_name = format!("{}_to_php_err", to_snake_case(&error.name));
115
116    let mut variants = Vec::new();
117    for variant in &error.variants {
118        let pattern = error_variant_wildcard_pattern(&rust_path, variant);
119        variants.push((pattern, variant.name.clone()));
120    }
121
122    crate::codegen::template_env::render(
123        "error_gen/php_error_converter.jinja",
124        minijinja::context! {
125            rust_path => rust_path.as_str(),
126            fn_name => fn_name.as_str(),
127            variants => variants,
128        },
129    )
130}
131
132/// Return the PHP converter function name for a given error type.
133pub fn php_converter_fn_name(error: &ErrorDef) -> String {
134    format!("{}_to_php_err", to_snake_case(&error.name))
135}
136
137/// Generate a `#[php_class]` + `#[php_impl]` block for the error type, storing
138/// the whitelisted introspection method return values as Rust fields exposed via
139/// `#[php_method]`.
140///
141/// Returns an empty string when `error.methods` is empty.
142pub fn gen_php_error_methods_impl(error: &ErrorDef, core_import: &str) -> String {
143    if error.methods.is_empty() {
144        return String::new();
145    }
146
147    let rust_path = if error.rust_path.is_empty() {
148        format!("{core_import}::{}", error.name)
149    } else {
150        error.rust_path.replace('-', "_")
151    };
152
153    let struct_name = format!("{}Info", error.name);
154
155    let mut fields = Vec::new();
156    let mut methods = Vec::new();
157    let mut ctor_assignments = Vec::new();
158
159    for method in &error.methods {
160        match method.name.as_str() {
161            "status_code" => {
162                fields.push("    pub status_code: u16,".to_string());
163                methods.push(
164                    concat!(
165                        "    /// HTTP status code for this error (0 means no associated status).\n",
166                        "    pub fn status_code(&self) -> u16 {\n",
167                        "        self.status_code\n",
168                        "    }",
169                    )
170                    .to_string(),
171                );
172                ctor_assignments.push("        status_code: e.status_code(),".to_string());
173            }
174            "is_transient" => {
175                fields.push("    pub is_transient: bool,".to_string());
176                methods.push(
177                    concat!(
178                        "    /// Returns `true` if the error is transient and a retry may succeed.\n",
179                        "    pub fn is_transient(&self) -> bool {\n",
180                        "        self.is_transient\n",
181                        "    }",
182                    )
183                    .to_string(),
184                );
185                ctor_assignments.push("        is_transient: e.is_transient(),".to_string());
186            }
187            "error_type" => {
188                fields.push("    pub error_type: String,".to_string());
189                methods.push(
190                    concat!(
191                        "    /// Machine-readable error category string for matching and logging.\n",
192                        "    pub fn error_type(&self) -> String {\n",
193                        "        self.error_type.clone()\n",
194                        "    }",
195                    )
196                    .to_string(),
197                );
198                ctor_assignments.push("        error_type: e.error_type().to_string(),".to_string());
199            }
200            other => {
201                methods.push(format!("    // Not emitted: method for `{other}` on `{struct_name}`"));
202            }
203        }
204    }
205
206    let struct_def = format!("#[php_class]\npub struct {struct_name} {{\n{}\n}}", fields.join("\n"));
207
208    let from_fn = format!(
209        "#[allow(dead_code)]\nfn {snake_name}_info(e: &{rust_path}) -> {struct_name} {{\n    {struct_name} {{\n{}\n    }}\n}}",
210        ctor_assignments.join("\n"),
211        snake_name = to_snake_case(&error.name),
212    );
213
214    let impl_block = format!("#[php_impl]\nimpl {struct_name} {{\n{}\n}}", methods.join("\n\n"));
215
216    format!("{struct_def}\n\n{from_fn}\n\n{impl_block}")
217}
218
219/// Generate a converter function that maps a core error to `magnus::Error`.
220pub fn gen_magnus_error_converter(error: &ErrorDef, core_import: &str) -> String {
221    let rust_path = if error.rust_path.is_empty() {
222        format!("{core_import}::{}", error.name)
223    } else {
224        error.rust_path.replace('-', "_")
225    };
226
227    let fn_name = format!("{}_to_magnus_err", to_snake_case(&error.name));
228
229    crate::codegen::template_env::render(
230        "error_gen/magnus_error_converter.jinja",
231        minijinja::context! {
232            rust_path => rust_path.as_str(),
233            fn_name => fn_name.as_str(),
234        },
235    )
236}
237
238/// Return the Magnus converter function name for a given error type.
239pub fn magnus_converter_fn_name(error: &ErrorDef) -> String {
240    format!("{}_to_magnus_err", to_snake_case(&error.name))
241}
242
243/// Generate a converter function that maps a core error to a Rustler error tuple `{:error, reason}`.
244pub fn gen_rustler_error_converter(error: &ErrorDef, core_import: &str) -> String {
245    let rust_path = if error.rust_path.is_empty() {
246        format!("{core_import}::{}", error.name)
247    } else {
248        error.rust_path.replace('-', "_")
249    };
250
251    let fn_name = format!("{}_to_rustler_err", to_snake_case(&error.name));
252
253    crate::codegen::template_env::render(
254        "error_gen/rustler_error_converter.jinja",
255        minijinja::context! {
256            rust_path => rust_path.as_str(),
257            fn_name => fn_name.as_str(),
258        },
259    )
260}
261
262/// Return the Rustler converter function name for a given error type.
263pub fn rustler_converter_fn_name(error: &ErrorDef) -> String {
264    format!("{}_to_rustler_err", to_snake_case(&error.name))
265}
266
267/// Generate a C enum of error codes plus an error-message function declaration.
268///
269/// Produces a `typedef enum` with `PREFIX_ERROR_NONE = 0` followed by one entry
270/// per variant, plus a function that returns the default message for a given code.
271pub fn gen_ffi_error_codes(error: &ErrorDef) -> String {
272    let prefix = to_screaming_snake(&error.name);
273    let prefix_lower = to_snake_case(&error.name);
274
275    let mut variant_variants = Vec::new();
276    for (i, variant) in error.variants.iter().enumerate() {
277        let variant_screaming = to_screaming_snake(&variant.name);
278        variant_variants.push((variant_screaming, (i + 1).to_string()));
279    }
280
281    crate::codegen::template_env::render(
282        "error_gen/ffi_error_codes.jinja",
283        minijinja::context! {
284            error_name => error.name.as_str(),
285            prefix => prefix.as_str(),
286            prefix_lower => prefix_lower.as_str(),
287            variant_variants => variant_variants,
288        },
289    )
290}
291
292/// Generate `#[unsafe(no_mangle)] extern "C"` helper functions for the whitelisted
293/// introspection methods (`status_code`, `is_transient`, `error_type`) declared
294/// in `error.methods`.
295///
296/// Each function follows the opaque-pointer convention: accepts a
297/// `*const {rust_path}` (null-checked before dereference) and returns the
298/// method's value. For `error_type` an additional `*_error_type_free` companion
299/// is emitted so callers can release the `CString`-allocated memory.
300///
301/// Symbol names come from [`crate::codegen::c_consumer::method_symbol`] — the same derivation
302/// the opaque-type method wrappers and the docs renderers use — so an error type with an
303/// internal acronym (`GraphQLError`) cannot be spelled one way here and another way elsewhere
304/// in the same header.
305///
306/// Returns an empty string when `error.methods` is empty.
307pub fn gen_ffi_error_methods(error: &ErrorDef, core_import: &str, api_prefix: &str) -> String {
308    if error.methods.is_empty() {
309        return String::new();
310    }
311
312    let rust_path = if error.rust_path.is_empty() {
313        format!("{core_import}::{}", error.name)
314    } else {
315        error.rust_path.replace('-', "_")
316    };
317
318    let mut items: Vec<String> = Vec::new();
319
320    for method in &error.methods {
321        match method.name.as_str() {
322            "status_code" => {
323                let fn_name = crate::codegen::c_consumer::method_symbol(api_prefix, &error.name, "status_code");
324                items.push(format!(
325                    "/// Return the HTTP status code for the error pointed to by `err`.\n\
326                     /// Returns `0` if `err` is null.\n\
327                     #[unsafe(no_mangle)]\n\
328                     pub unsafe extern \"C\" fn {fn_name}(err: *const {rust_path}) -> u16 {{\n\
329                         if err.is_null() {{\n\
330                             return 0;\n\
331                         }}\n\
332                         // SAFETY: caller guarantees `err` points to a live `{rust_path}` value\n\
333                         // allocated by this library, or is null (checked above).\n\
334                         unsafe {{ (*err).status_code() }}\n\
335                     }}"
336                ));
337            }
338            "is_transient" => {
339                let fn_name = crate::codegen::c_consumer::method_symbol(api_prefix, &error.name, "is_transient");
340                items.push(format!(
341                    "/// Return whether the error pointed to by `err` is transient.\n\
342                     /// Returns `false` if `err` is null.\n\
343                     #[unsafe(no_mangle)]\n\
344                     pub unsafe extern \"C\" fn {fn_name}(err: *const {rust_path}) -> bool {{\n\
345                         if err.is_null() {{\n\
346                             return false;\n\
347                         }}\n\
348                         // SAFETY: caller guarantees `err` points to a live `{rust_path}` value\n\
349                         // allocated by this library, or is null (checked above).\n\
350                         unsafe {{ (*err).is_transient() }}\n\
351                     }}"
352                ));
353            }
354            "error_type" => {
355                let fn_name = crate::codegen::c_consumer::method_symbol(api_prefix, &error.name, "error_type");
356                let free_fn_name = format!("{fn_name}_free");
357                items.push(format!(
358                    "/// Return the machine-readable error category string for the error pointed\n\
359                     /// to by `err` as a heap-allocated, NUL-terminated C string.\n\
360                     /// The caller must free the returned pointer with `{free_fn_name}`.\n\
361                     /// Returns a null pointer if `err` is null.\n\
362                     #[unsafe(no_mangle)]\n\
363                     pub unsafe extern \"C\" fn {fn_name}(err: *const {rust_path}) -> *mut std::ffi::c_char {{\n\
364                         if err.is_null() {{\n\
365                             return std::ptr::null_mut();\n\
366                         }}\n\
367                         // SAFETY: caller guarantees `err` points to a live `{rust_path}` value\n\
368                         // allocated by this library, or is null (checked above).\n\
369                         let s = unsafe {{ (*err).error_type() }};\n\
370                         // SAFETY: `error_type()` returns a `'static str` containing no NUL bytes.\n\
371                         std::ffi::CString::new(s)\n\
372                             .map(|c| c.into_raw())\n\
373                             .unwrap_or(std::ptr::null_mut())\n\
374                     }}\n\n\
375                     /// Free a string previously returned by `{fn_name}`.\n\
376                     /// Passing a null pointer is a no-op.\n\
377                     #[unsafe(no_mangle)]\n\
378                     pub unsafe extern \"C\" fn {free_fn_name}(ptr: *mut std::ffi::c_char) {{\n\
379                         if !ptr.is_null() {{\n\
380                             // SAFETY: `ptr` was allocated by `CString::into_raw` inside\n\
381                             // `{fn_name}` and is now being reclaimed by the matching\n\
382                             // `CString::from_raw`.  Passing null is explicitly allowed.\n\
383                             unsafe {{ drop(std::ffi::CString::from_raw(ptr)) }};\n\
384                         }}\n\
385                     }}"
386                ));
387            }
388            other => {
389                items.push(format!(
390                    "// Not emitted: FFI helper for method `{other}` on `{rust_path}`"
391                ));
392            }
393        }
394    }
395
396    items.join("\n\n")
397}
398
399/// Generate Go sentinel errors and a structured error type for an `ErrorDef`.
400///
401/// `pkg_name` is the Go package name (e.g. `"samplellm"`). When the error struct
402/// name starts with the package name (case-insensitively), the package-name
403/// prefix is stripped to avoid the revive `exported` stutter lint error
404/// (e.g. `SampleLlmError` in package `samplellm` → exported as `Error`).
405use crate::core::ir::ErrorDef;
406
407use super::shared::{error_variant_wildcard_pattern, to_screaming_snake, to_snake_case};