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
81pub 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
106pub fn gen_php_error_converter(error: &ErrorDef, core_import: &str) -> String {
112 let rust_path = if error.rust_path.is_empty() {
113 format!("{core_import}::{}", error.name)
114 } else {
115 error.rust_path.replace('-', "_")
116 };
117
118 let fn_name = format!("{}_to_php_err", to_snake_case(&error.name));
119
120 let mut variants = Vec::new();
122 for variant in &error.variants {
123 let pattern = error_variant_wildcard_pattern(&rust_path, variant);
124 variants.push((pattern, variant.name.clone()));
125 }
126
127 crate::codegen::template_env::render(
128 "error_gen/php_error_converter.jinja",
129 minijinja::context! {
130 rust_path => rust_path.as_str(),
131 fn_name => fn_name.as_str(),
132 variants => variants,
133 },
134 )
135}
136
137pub fn php_converter_fn_name(error: &ErrorDef) -> String {
139 format!("{}_to_php_err", to_snake_case(&error.name))
140}
141
142pub fn gen_php_error_methods_impl(error: &ErrorDef, core_import: &str) -> String {
148 if error.methods.is_empty() {
149 return String::new();
150 }
151
152 let rust_path = if error.rust_path.is_empty() {
153 format!("{core_import}::{}", error.name)
154 } else {
155 error.rust_path.replace('-', "_")
156 };
157
158 let struct_name = format!("{}Info", error.name);
159
160 let mut fields = Vec::new();
161 let mut methods = Vec::new();
162 let mut ctor_assignments = Vec::new();
163
164 for method in &error.methods {
165 match method.name.as_str() {
166 "status_code" => {
167 fields.push(" pub status_code: u16,".to_string());
168 methods.push(
169 concat!(
170 " /// HTTP status code for this error (0 means no associated status).\n",
171 " pub fn status_code(&self) -> u16 {\n",
172 " self.status_code\n",
173 " }",
174 )
175 .to_string(),
176 );
177 ctor_assignments.push(" status_code: e.status_code(),".to_string());
178 }
179 "is_transient" => {
180 fields.push(" pub is_transient: bool,".to_string());
181 methods.push(
182 concat!(
183 " /// Returns `true` if the error is transient and a retry may succeed.\n",
184 " pub fn is_transient(&self) -> bool {\n",
185 " self.is_transient\n",
186 " }",
187 )
188 .to_string(),
189 );
190 ctor_assignments.push(" is_transient: e.is_transient(),".to_string());
191 }
192 "error_type" => {
193 fields.push(" pub error_type: String,".to_string());
194 methods.push(
195 concat!(
196 " /// Machine-readable error category string for matching and logging.\n",
197 " pub fn error_type(&self) -> String {\n",
198 " self.error_type.clone()\n",
199 " }",
200 )
201 .to_string(),
202 );
203 ctor_assignments.push(" error_type: e.error_type().to_string(),".to_string());
204 }
205 other => {
206 methods.push(format!(" // Not emitted: method for `{other}` on `{struct_name}`"));
207 }
208 }
209 }
210
211 let struct_def = format!("#[php_class]\npub struct {struct_name} {{\n{}\n}}", fields.join("\n"));
212
213 let from_fn = format!(
214 "#[allow(dead_code)]\nfn {snake_name}_info(e: &{rust_path}) -> {struct_name} {{\n {struct_name} {{\n{}\n }}\n}}",
215 ctor_assignments.join("\n"),
216 snake_name = to_snake_case(&error.name),
217 );
218
219 let impl_block = format!("#[php_impl]\nimpl {struct_name} {{\n{}\n}}", methods.join("\n\n"));
220
221 format!("{struct_def}\n\n{from_fn}\n\n{impl_block}")
222}
223
224pub fn gen_magnus_error_converter(error: &ErrorDef, core_import: &str) -> String {
230 let rust_path = if error.rust_path.is_empty() {
231 format!("{core_import}::{}", error.name)
232 } else {
233 error.rust_path.replace('-', "_")
234 };
235
236 let fn_name = format!("{}_to_magnus_err", to_snake_case(&error.name));
237
238 crate::codegen::template_env::render(
239 "error_gen/magnus_error_converter.jinja",
240 minijinja::context! {
241 rust_path => rust_path.as_str(),
242 fn_name => fn_name.as_str(),
243 },
244 )
245}
246
247pub fn magnus_converter_fn_name(error: &ErrorDef) -> String {
249 format!("{}_to_magnus_err", to_snake_case(&error.name))
250}
251
252pub fn gen_rustler_error_converter(error: &ErrorDef, core_import: &str) -> String {
258 let rust_path = if error.rust_path.is_empty() {
259 format!("{core_import}::{}", error.name)
260 } else {
261 error.rust_path.replace('-', "_")
262 };
263
264 let fn_name = format!("{}_to_rustler_err", to_snake_case(&error.name));
265
266 crate::codegen::template_env::render(
267 "error_gen/rustler_error_converter.jinja",
268 minijinja::context! {
269 rust_path => rust_path.as_str(),
270 fn_name => fn_name.as_str(),
271 },
272 )
273}
274
275pub fn rustler_converter_fn_name(error: &ErrorDef) -> String {
277 format!("{}_to_rustler_err", to_snake_case(&error.name))
278}
279
280pub fn gen_ffi_error_codes(error: &ErrorDef) -> String {
289 let prefix = to_screaming_snake(&error.name);
290 let prefix_lower = to_snake_case(&error.name);
291
292 let mut variant_variants = Vec::new();
294 for (i, variant) in error.variants.iter().enumerate() {
295 let variant_screaming = to_screaming_snake(&variant.name);
296 variant_variants.push((variant_screaming, (i + 1).to_string()));
297 }
298
299 crate::codegen::template_env::render(
300 "error_gen/ffi_error_codes.jinja",
301 minijinja::context! {
302 error_name => error.name.as_str(),
303 prefix => prefix.as_str(),
304 prefix_lower => prefix_lower.as_str(),
305 variant_variants => variant_variants,
306 },
307 )
308}
309
310pub fn gen_ffi_error_methods(error: &ErrorDef, core_import: &str, api_prefix: &str) -> String {
321 if error.methods.is_empty() {
322 return String::new();
323 }
324
325 let rust_path = if error.rust_path.is_empty() {
326 format!("{core_import}::{}", error.name)
327 } else {
328 error.rust_path.replace('-', "_")
329 };
330
331 let error_snake = to_snake_case(&error.name);
332 let mut items: Vec<String> = Vec::new();
333
334 for method in &error.methods {
335 match method.name.as_str() {
336 "status_code" => {
337 let fn_name = format!("{api_prefix}_{error_snake}_status_code");
338 items.push(format!(
339 "/// Return the HTTP status code for the error pointed to by `err`.\n\
340 /// Returns `0` if `err` is null.\n\
341 #[no_mangle]\n\
342 pub unsafe extern \"C\" fn {fn_name}(err: *const {rust_path}) -> u16 {{\n\
343 // SAFETY: caller guarantees `err` points to a live `{rust_path}` value\n\
344 // allocated by this library, or is null.\n\
345 if err.is_null() {{\n\
346 return 0;\n\
347 }}\n\
348 (*err).status_code()\n\
349 }}"
350 ));
351 }
352 "is_transient" => {
353 let fn_name = format!("{api_prefix}_{error_snake}_is_transient");
354 items.push(format!(
355 "/// Return whether the error pointed to by `err` is transient.\n\
356 /// Returns `false` if `err` is null.\n\
357 #[no_mangle]\n\
358 pub unsafe extern \"C\" fn {fn_name}(err: *const {rust_path}) -> bool {{\n\
359 // SAFETY: caller guarantees `err` points to a live `{rust_path}` value\n\
360 // allocated by this library, or is null.\n\
361 if err.is_null() {{\n\
362 return false;\n\
363 }}\n\
364 (*err).is_transient()\n\
365 }}"
366 ));
367 }
368 "error_type" => {
369 let fn_name = format!("{api_prefix}_{error_snake}_error_type");
370 let free_fn_name = format!("{fn_name}_free");
371 items.push(format!(
372 "/// Return the machine-readable error category string for the error pointed\n\
373 /// to by `err` as a heap-allocated, NUL-terminated C string.\n\
374 /// The caller must free the returned pointer with `{free_fn_name}`.\n\
375 /// Returns a null pointer if `err` is null.\n\
376 #[no_mangle]\n\
377 pub unsafe extern \"C\" fn {fn_name}(err: *const {rust_path}) -> *mut std::ffi::c_char {{\n\
378 // SAFETY: caller guarantees `err` points to a live `{rust_path}` value\n\
379 // allocated by this library, or is null.\n\
380 if err.is_null() {{\n\
381 return std::ptr::null_mut();\n\
382 }}\n\
383 let s = (*err).error_type();\n\
384 // SAFETY: `error_type()` returns a `'static str` containing no NUL bytes.\n\
385 std::ffi::CString::new(s)\n\
386 .map(|c| c.into_raw())\n\
387 .unwrap_or(std::ptr::null_mut())\n\
388 }}\n\n\
389 /// Free a string previously returned by `{fn_name}`.\n\
390 /// Passing a null pointer is a no-op.\n\
391 #[no_mangle]\n\
392 pub unsafe extern \"C\" fn {free_fn_name}(ptr: *mut std::ffi::c_char) {{\n\
393 // SAFETY: `ptr` was allocated by `CString::into_raw` inside\n\
394 // `{fn_name}` and is now being reclaimed by the matching\n\
395 // `CString::from_raw`. Passing null is explicitly allowed.\n\
396 if !ptr.is_null() {{\n\
397 drop(std::ffi::CString::from_raw(ptr));\n\
398 }}\n\
399 }}"
400 ));
401 }
402 other => {
403 items.push(format!(
405 "// Not emitted: FFI helper for method `{other}` on `{rust_path}`"
406 ));
407 }
408 }
409 }
410
411 items.join("\n\n")
412}
413
414use crate::core::ir::ErrorDef;
425
426use super::shared::{error_variant_wildcard_pattern, to_screaming_snake, to_snake_case};