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 {
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
132pub fn php_converter_fn_name(error: &ErrorDef) -> String {
134 format!("{}_to_php_err", to_snake_case(&error.name))
135}
136
137pub 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
219pub 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
238pub fn magnus_converter_fn_name(error: &ErrorDef) -> String {
240 format!("{}_to_magnus_err", to_snake_case(&error.name))
241}
242
243pub 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
262pub fn rustler_converter_fn_name(error: &ErrorDef) -> String {
264 format!("{}_to_rustler_err", to_snake_case(&error.name))
265}
266
267pub 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
292pub 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
399use crate::core::ir::ErrorDef;
406
407use super::shared::{error_variant_wildcard_pattern, to_screaming_snake, to_snake_case};