Skip to main content

alef_codegen/
error_gen.rs

1use alef_core::ir::{ErrorDef, ErrorVariant};
2
3/// Generate `pyo3::create_exception!` macros for each error variant plus the base error type.
4/// Appends "Error" suffix to variant names that don't already have it (N818 compliance).
5pub fn gen_pyo3_error_types(error: &ErrorDef, module_name: &str) -> String {
6    let mut lines = Vec::with_capacity(error.variants.len() + 2);
7    lines.push("// Error types".to_string());
8
9    // One exception per variant (with Error suffix if needed)
10    for variant in &error.variants {
11        let variant_name = if variant.name.ends_with("Error") {
12            variant.name.clone()
13        } else {
14            format!("{}Error", variant.name)
15        };
16        lines.push(format!(
17            "pyo3::create_exception!({module_name}, {}, pyo3::exceptions::PyException);",
18            variant_name
19        ));
20    }
21
22    // Base exception for the enum itself
23    lines.push(format!(
24        "pyo3::create_exception!({module_name}, {}, pyo3::exceptions::PyException);",
25        error.name
26    ));
27
28    lines.join("\n")
29}
30
31/// Generate a `to_py_err` converter function that maps each Rust error variant to a Python exception.
32/// Uses Error-suffixed names for variant exceptions (N818 compliance).
33pub fn gen_pyo3_error_converter(error: &ErrorDef, core_import: &str) -> String {
34    let rust_path = if error.rust_path.is_empty() {
35        format!("{core_import}::{}", error.name)
36    } else {
37        error.rust_path.replace('-', "_")
38    };
39
40    let fn_name = format!("{}_to_py_err", to_snake_case(&error.name));
41
42    let mut lines = Vec::new();
43    lines.push(format!("/// Convert a `{rust_path}` error to a Python exception."));
44    lines.push(format!("fn {fn_name}(e: {rust_path}) -> pyo3::PyErr {{"));
45    lines.push("    let msg = e.to_string();".to_string());
46    lines.push("    #[allow(unreachable_patterns)]".to_string());
47    lines.push("    match &e {".to_string());
48
49    for variant in &error.variants {
50        let pattern = if variant.is_unit {
51            format!("{rust_path}::{}", variant.name)
52        } else {
53            format!("{rust_path}::{}(..)", variant.name)
54        };
55        let variant_exc_name = if variant.name.ends_with("Error") {
56            variant.name.clone()
57        } else {
58            format!("{}Error", variant.name)
59        };
60        lines.push(format!("        {pattern} => {}::new_err(msg),", variant_exc_name));
61    }
62
63    // Catch-all for cfg-gated variants not in the IR
64    lines.push(format!("        _ => {}::new_err(msg),", error.name));
65    lines.push("    }".to_string());
66    lines.push("}".to_string());
67    lines.join("\n")
68}
69
70/// Generate `m.add(...)` registration calls for each exception type.
71/// Uses Error-suffixed names for variant exceptions (N818 compliance).
72pub fn gen_pyo3_error_registration(error: &ErrorDef) -> Vec<String> {
73    let mut registrations = Vec::with_capacity(error.variants.len() + 1);
74
75    for variant in &error.variants {
76        let variant_exc_name = if variant.name.ends_with("Error") {
77            variant.name.clone()
78        } else {
79            format!("{}Error", variant.name)
80        };
81        registrations.push(format!(
82            "    m.add(\"{}\", m.py().get_type::<{}>())?;",
83            variant_exc_name, variant_exc_name
84        ));
85    }
86
87    // Base exception
88    registrations.push(format!(
89        "    m.add(\"{}\", m.py().get_type::<{}>())?;",
90        error.name, error.name
91    ));
92
93    registrations
94}
95
96/// Return the converter function name for a given error type.
97pub fn converter_fn_name(error: &ErrorDef) -> String {
98    format!("{}_to_py_err", to_snake_case(&error.name))
99}
100
101/// Simple CamelCase to snake_case conversion.
102fn to_snake_case(s: &str) -> String {
103    let mut result = String::with_capacity(s.len() + 4);
104    for (i, c) in s.chars().enumerate() {
105        if c.is_uppercase() {
106            if i > 0 {
107                result.push('_');
108            }
109            result.push(c.to_ascii_lowercase());
110        } else {
111            result.push(c);
112        }
113    }
114    result
115}
116
117// ---------------------------------------------------------------------------
118// NAPI (Node.js) error generation
119// ---------------------------------------------------------------------------
120
121/// Generate a `JsError` enum with string constants for each error variant name.
122pub fn gen_napi_error_types(error: &ErrorDef) -> String {
123    let mut lines = Vec::with_capacity(error.variants.len() + 4);
124    lines.push("// Error variant name constants".to_string());
125    for variant in &error.variants {
126        lines.push(format!(
127            "pub const {}_ERROR_{}: &str = \"{}\";",
128            to_screaming_snake(&error.name),
129            to_screaming_snake(&variant.name),
130            variant.name,
131        ));
132    }
133    lines.join("\n")
134}
135
136/// Generate a converter function that maps a core error to `napi::Error`.
137pub fn gen_napi_error_converter(error: &ErrorDef, core_import: &str) -> String {
138    let rust_path = if error.rust_path.is_empty() {
139        format!("{core_import}::{}", error.name)
140    } else {
141        error.rust_path.replace('-', "_")
142    };
143
144    let fn_name = format!("{}_to_napi_err", to_snake_case(&error.name));
145
146    let mut lines = Vec::new();
147    lines.push(format!("/// Convert a `{rust_path}` error to a NAPI error."));
148    lines.push("#[allow(dead_code)]".to_string());
149    lines.push(format!("fn {fn_name}(e: {rust_path}) -> napi::Error {{"));
150    lines.push("    let msg = e.to_string();".to_string());
151    lines.push("    #[allow(unreachable_patterns)]".to_string());
152    lines.push("    match &e {".to_string());
153
154    for variant in &error.variants {
155        let pattern = if variant.is_unit {
156            format!("{rust_path}::{}", variant.name)
157        } else {
158            format!("{rust_path}::{}(..)", variant.name)
159        };
160        lines.push(format!(
161            "        {pattern} => napi::Error::new(napi::Status::GenericFailure, format!(\"[{}] {{}}\", msg)),",
162            variant.name,
163        ));
164    }
165
166    // Catch-all for cfg-gated variants not in the IR
167    lines.push("        _ => napi::Error::new(napi::Status::GenericFailure, msg),".to_string());
168    lines.push("    }".to_string());
169    lines.push("}".to_string());
170    lines.join("\n")
171}
172
173/// Return the NAPI converter function name for a given error type.
174pub fn napi_converter_fn_name(error: &ErrorDef) -> String {
175    format!("{}_to_napi_err", to_snake_case(&error.name))
176}
177
178// ---------------------------------------------------------------------------
179// WASM (wasm-bindgen) error generation
180// ---------------------------------------------------------------------------
181
182/// Generate a converter function that maps a core error to `JsValue`.
183pub fn gen_wasm_error_converter(error: &ErrorDef, core_import: &str) -> String {
184    let rust_path = if error.rust_path.is_empty() {
185        format!("{core_import}::{}", error.name)
186    } else {
187        error.rust_path.replace('-', "_")
188    };
189
190    let fn_name = format!("{}_to_js_value", to_snake_case(&error.name));
191
192    let mut lines = Vec::new();
193    lines.push(format!("/// Convert a `{rust_path}` error to a `JsValue` string."));
194    lines.push("#[allow(dead_code)]".to_string());
195    lines.push(format!("fn {fn_name}(e: {rust_path}) -> wasm_bindgen::JsValue {{"));
196    lines.push("    wasm_bindgen::JsValue::from_str(&e.to_string())".to_string());
197    lines.push("}".to_string());
198    lines.join("\n")
199}
200
201/// Return the WASM converter function name for a given error type.
202pub fn wasm_converter_fn_name(error: &ErrorDef) -> String {
203    format!("{}_to_js_value", to_snake_case(&error.name))
204}
205
206// ---------------------------------------------------------------------------
207// PHP (ext-php-rs) error generation
208// ---------------------------------------------------------------------------
209
210/// Generate a converter function that maps a core error to `PhpException`.
211pub fn gen_php_error_converter(error: &ErrorDef, core_import: &str) -> String {
212    let rust_path = if error.rust_path.is_empty() {
213        format!("{core_import}::{}", error.name)
214    } else {
215        error.rust_path.replace('-', "_")
216    };
217
218    let fn_name = format!("{}_to_php_err", to_snake_case(&error.name));
219
220    let mut lines = Vec::new();
221    lines.push(format!("/// Convert a `{rust_path}` error to a PHP exception."));
222    lines.push("#[allow(dead_code)]".to_string());
223    lines.push(format!(
224        "fn {fn_name}(e: {rust_path}) -> ext_php_rs::exception::PhpException {{"
225    ));
226    lines.push("    let msg = e.to_string();".to_string());
227    lines.push("    #[allow(unreachable_patterns)]".to_string());
228    lines.push("    match &e {".to_string());
229
230    for variant in &error.variants {
231        let pattern = if variant.is_unit {
232            format!("{rust_path}::{}", variant.name)
233        } else {
234            format!("{rust_path}::{}(..)", variant.name)
235        };
236        lines.push(format!(
237            "        {pattern} => ext_php_rs::exception::PhpException::default(format!(\"[{}] {{}}\", msg)),",
238            variant.name,
239        ));
240    }
241
242    // Catch-all for cfg-gated variants not in the IR
243    lines.push("        _ => ext_php_rs::exception::PhpException::default(msg),".to_string());
244    lines.push("    }".to_string());
245    lines.push("}".to_string());
246    lines.join("\n")
247}
248
249/// Return the PHP converter function name for a given error type.
250pub fn php_converter_fn_name(error: &ErrorDef) -> String {
251    format!("{}_to_php_err", to_snake_case(&error.name))
252}
253
254// ---------------------------------------------------------------------------
255// Magnus (Ruby) error generation
256// ---------------------------------------------------------------------------
257
258/// Generate a converter function that maps a core error to `magnus::Error`.
259pub fn gen_magnus_error_converter(error: &ErrorDef, core_import: &str) -> String {
260    let rust_path = if error.rust_path.is_empty() {
261        format!("{core_import}::{}", error.name)
262    } else {
263        error.rust_path.replace('-', "_")
264    };
265
266    let fn_name = format!("{}_to_magnus_err", to_snake_case(&error.name));
267
268    let mut lines = Vec::new();
269    lines.push(format!("/// Convert a `{rust_path}` error to a Magnus runtime error."));
270    lines.push("#[allow(dead_code)]".to_string());
271    lines.push(format!("fn {fn_name}(e: {rust_path}) -> magnus::Error {{"));
272    lines.push("    let msg = e.to_string();".to_string());
273    lines.push("    magnus::Error::new(magnus::exception::runtime_error(), msg)".to_string());
274    lines.push("}".to_string());
275    lines.join("\n")
276}
277
278/// Return the Magnus converter function name for a given error type.
279pub fn magnus_converter_fn_name(error: &ErrorDef) -> String {
280    format!("{}_to_magnus_err", to_snake_case(&error.name))
281}
282
283// ---------------------------------------------------------------------------
284// Rustler (Elixir) error generation
285// ---------------------------------------------------------------------------
286
287/// Generate a converter function that maps a core error to a Rustler error tuple `{:error, reason}`.
288pub fn gen_rustler_error_converter(error: &ErrorDef, core_import: &str) -> String {
289    let rust_path = if error.rust_path.is_empty() {
290        format!("{core_import}::{}", error.name)
291    } else {
292        error.rust_path.replace('-', "_")
293    };
294
295    let fn_name = format!("{}_to_rustler_err", to_snake_case(&error.name));
296
297    let mut lines = Vec::new();
298    lines.push(format!("/// Convert a `{rust_path}` error to a Rustler error string."));
299    lines.push("#[allow(dead_code)]".to_string());
300    lines.push(format!("fn {fn_name}(e: {rust_path}) -> String {{"));
301    lines.push("    e.to_string()".to_string());
302    lines.push("}".to_string());
303    lines.join("\n")
304}
305
306/// Return the Rustler converter function name for a given error type.
307pub fn rustler_converter_fn_name(error: &ErrorDef) -> String {
308    format!("{}_to_rustler_err", to_snake_case(&error.name))
309}
310
311// ---------------------------------------------------------------------------
312// FFI (C) error code generation
313// ---------------------------------------------------------------------------
314
315/// Generate a C enum of error codes plus an error-message function declaration.
316///
317/// Produces a `typedef enum` with `PREFIX_ERROR_NONE = 0` followed by one entry
318/// per variant, plus a function that returns the default message for a given code.
319pub fn gen_ffi_error_codes(error: &ErrorDef) -> String {
320    let prefix = to_screaming_snake(&error.name);
321    let prefix_lower = to_snake_case(&error.name);
322
323    let mut lines = Vec::new();
324    lines.push(format!("/// Error codes for `{}`.", error.name));
325    lines.push("typedef enum {".to_string());
326    lines.push(format!("    {}_NONE = 0,", prefix));
327
328    for (i, variant) in error.variants.iter().enumerate() {
329        let variant_screaming = to_screaming_snake(&variant.name);
330        lines.push(format!("    {}_{} = {},", prefix, variant_screaming, i + 1));
331    }
332
333    lines.push(format!("}} {}_t;\n", prefix_lower));
334
335    // Error message function
336    lines.push(format!(
337        "/// Return a static string describing the error code.\nconst char* {}_error_message({}_t code);",
338        prefix_lower, prefix_lower
339    ));
340
341    lines.join("\n")
342}
343
344// ---------------------------------------------------------------------------
345// Go error type generation
346// ---------------------------------------------------------------------------
347
348/// Generate Go sentinel errors and a structured error type for an `ErrorDef`.
349pub fn gen_go_error_types(error: &ErrorDef) -> String {
350    let mut lines = Vec::new();
351
352    // Sentinel errors
353    lines.push("var (".to_string());
354    for variant in &error.variants {
355        let err_name = format!("Err{}", variant.name);
356        let msg = variant_display_message(variant);
357        lines.push(format!("    {} = errors.New(\"{}\")", err_name, msg));
358    }
359    lines.push(")\n".to_string());
360
361    // Structured error type
362    lines.push(format!("// {} is a structured error type.", error.name));
363    lines.push(format!("type {} struct {{", error.name));
364    lines.push("    Code    string".to_string());
365    lines.push("    Message string".to_string());
366    lines.push("}\n".to_string());
367
368    lines.push(format!(
369        "func (e *{}) Error() string {{ return e.Message }}",
370        error.name
371    ));
372
373    lines.join("\n")
374}
375
376// ---------------------------------------------------------------------------
377// Java error type generation
378// ---------------------------------------------------------------------------
379
380/// Generate Java exception sub-classes for each error variant.
381///
382/// Returns a `Vec` of `(class_name, file_content)` tuples: the base exception
383/// class followed by one per-variant exception.  The caller writes each to a
384/// separate `.java` file.
385pub fn gen_java_error_types(error: &ErrorDef, package: &str) -> Vec<(String, String)> {
386    let mut files = Vec::with_capacity(error.variants.len() + 1);
387
388    // Base exception class
389    let base_name = format!("{}Exception", error.name);
390    let mut base = String::with_capacity(512);
391    base.push_str(&format!(
392        "// DO NOT EDIT - auto-generated by alef\npackage {};\n\n",
393        package
394    ));
395    if !error.doc.is_empty() {
396        base.push_str(&format!("/** {} */\n", error.doc));
397    }
398    base.push_str(&format!("public class {} extends Exception {{\n", base_name));
399    base.push_str(&format!(
400        "    public {}(String message) {{\n        super(message);\n    }}\n\n",
401        base_name
402    ));
403    base.push_str(&format!(
404        "    public {}(String message, Throwable cause) {{\n        super(message, cause);\n    }}\n",
405        base_name
406    ));
407    base.push_str("}\n");
408    files.push((base_name.clone(), base));
409
410    // Per-variant exception classes
411    for variant in &error.variants {
412        let class_name = format!("{}Exception", variant.name);
413        let mut content = String::with_capacity(512);
414        content.push_str(&format!(
415            "// DO NOT EDIT - auto-generated by alef\npackage {};\n\n",
416            package
417        ));
418        if !variant.doc.is_empty() {
419            content.push_str(&format!("/** {} */\n", variant.doc));
420        }
421        content.push_str(&format!("public class {} extends {} {{\n", class_name, base_name));
422        content.push_str(&format!(
423            "    public {}(String message) {{\n        super(message);\n    }}\n\n",
424            class_name
425        ));
426        content.push_str(&format!(
427            "    public {}(String message, Throwable cause) {{\n        super(message, cause);\n    }}\n",
428            class_name
429        ));
430        content.push_str("}\n");
431        files.push((class_name, content));
432    }
433
434    files
435}
436
437// ---------------------------------------------------------------------------
438// C# error type generation
439// ---------------------------------------------------------------------------
440
441/// Generate C# exception sub-classes for each error variant.
442///
443/// Returns a `Vec` of `(class_name, file_content)` tuples: the base exception
444/// class followed by one per-variant exception.  The caller writes each to a
445/// separate `.cs` file.
446pub fn gen_csharp_error_types(error: &ErrorDef, namespace: &str) -> Vec<(String, String)> {
447    let mut files = Vec::with_capacity(error.variants.len() + 1);
448
449    let base_name = format!("{}Exception", error.name);
450
451    // Base exception class
452    {
453        let mut out = String::with_capacity(512);
454        out.push_str("// This file is auto-generated by alef. DO NOT EDIT.\nusing System;\n\n");
455        out.push_str(&format!("namespace {};\n\n", namespace));
456        if !error.doc.is_empty() {
457            out.push_str("/// <summary>\n");
458            for line in error.doc.lines() {
459                out.push_str(&format!("/// {}\n", line));
460            }
461            out.push_str("/// </summary>\n");
462        }
463        out.push_str(&format!("public class {} : Exception\n{{\n", base_name));
464        out.push_str(&format!(
465            "    public {}(string message) : base(message) {{ }}\n\n",
466            base_name
467        ));
468        out.push_str(&format!(
469            "    public {}(string message, Exception innerException) : base(message, innerException) {{ }}\n",
470            base_name
471        ));
472        out.push_str("}\n");
473        files.push((base_name.clone(), out));
474    }
475
476    // Per-variant exception classes
477    for variant in &error.variants {
478        let class_name = format!("{}Exception", variant.name);
479        let mut out = String::with_capacity(512);
480        out.push_str("// This file is auto-generated by alef. DO NOT EDIT.\nusing System;\n\n");
481        out.push_str(&format!("namespace {};\n\n", namespace));
482        if !variant.doc.is_empty() {
483            out.push_str("/// <summary>\n");
484            for line in variant.doc.lines() {
485                out.push_str(&format!("/// {}\n", line));
486            }
487            out.push_str("/// </summary>\n");
488        }
489        out.push_str(&format!("public class {} : {}\n{{\n", class_name, base_name));
490        out.push_str(&format!(
491            "    public {}(string message) : base(message) {{ }}\n\n",
492            class_name
493        ));
494        out.push_str(&format!(
495            "    public {}(string message, Exception innerException) : base(message, innerException) {{ }}\n",
496            class_name
497        ));
498        out.push_str("}\n");
499        files.push((class_name, out));
500    }
501
502    files
503}
504
505// ---------------------------------------------------------------------------
506// Helpers
507// ---------------------------------------------------------------------------
508
509/// Convert CamelCase to SCREAMING_SNAKE_CASE.
510fn to_screaming_snake(s: &str) -> String {
511    let mut result = String::with_capacity(s.len() + 4);
512    for (i, c) in s.chars().enumerate() {
513        if c.is_uppercase() {
514            if i > 0 {
515                result.push('_');
516            }
517            result.push(c.to_ascii_uppercase());
518        } else {
519            result.push(c.to_ascii_uppercase());
520        }
521    }
522    result
523}
524
525/// Generate a human-readable message for an error variant.
526///
527/// Uses the `message_template` if present, otherwise falls back to a
528/// space-separated version of the variant name (e.g. "ParseError" -> "parse error").
529fn variant_display_message(variant: &ErrorVariant) -> String {
530    if let Some(tmpl) = &variant.message_template {
531        // Strip format placeholders like {0}, {source}, etc.
532        let msg = tmpl
533            .replace("{0}", "")
534            .replace("{source}", "")
535            .trim_end_matches(": ")
536            .trim()
537            .to_string();
538        if msg.is_empty() {
539            to_snake_case(&variant.name).replace('_', " ")
540        } else {
541            msg
542        }
543    } else {
544        to_snake_case(&variant.name).replace('_', " ")
545    }
546}
547
548#[cfg(test)]
549mod tests {
550    use super::*;
551    use alef_core::ir::{ErrorDef, ErrorVariant};
552
553    fn sample_error() -> ErrorDef {
554        ErrorDef {
555            name: "ConversionError".to_string(),
556            rust_path: "html_to_markdown_rs::ConversionError".to_string(),
557            variants: vec![
558                ErrorVariant {
559                    name: "ParseError".to_string(),
560                    message_template: Some("HTML parsing error: {0}".to_string()),
561                    fields: vec![],
562                    has_source: false,
563                    has_from: false,
564                    is_unit: false,
565                    doc: String::new(),
566                },
567                ErrorVariant {
568                    name: "IoError".to_string(),
569                    message_template: Some("I/O error: {0}".to_string()),
570                    fields: vec![],
571                    has_source: false,
572                    has_from: true,
573                    is_unit: false,
574                    doc: String::new(),
575                },
576                ErrorVariant {
577                    name: "Other".to_string(),
578                    message_template: Some("Conversion error: {0}".to_string()),
579                    fields: vec![],
580                    has_source: false,
581                    has_from: false,
582                    is_unit: false,
583                    doc: String::new(),
584                },
585            ],
586            doc: "Error type for conversion operations.".to_string(),
587        }
588    }
589
590    #[test]
591    fn test_gen_error_types() {
592        let error = sample_error();
593        let output = gen_pyo3_error_types(&error, "_module");
594        assert!(output.contains("pyo3::create_exception!(_module, ParseError, pyo3::exceptions::PyException);"));
595        assert!(output.contains("pyo3::create_exception!(_module, IoError, pyo3::exceptions::PyException);"));
596        assert!(output.contains("pyo3::create_exception!(_module, OtherError, pyo3::exceptions::PyException);"));
597        assert!(output.contains("pyo3::create_exception!(_module, ConversionError, pyo3::exceptions::PyException);"));
598    }
599
600    #[test]
601    fn test_gen_error_converter() {
602        let error = sample_error();
603        let output = gen_pyo3_error_converter(&error, "html_to_markdown_rs");
604        assert!(
605            output.contains("fn conversion_error_to_py_err(e: html_to_markdown_rs::ConversionError) -> pyo3::PyErr {")
606        );
607        assert!(output.contains("html_to_markdown_rs::ConversionError::ParseError(..) => ParseError::new_err(msg),"));
608        assert!(output.contains("html_to_markdown_rs::ConversionError::IoError(..) => IoError::new_err(msg),"));
609    }
610
611    #[test]
612    fn test_gen_error_registration() {
613        let error = sample_error();
614        let regs = gen_pyo3_error_registration(&error);
615        assert_eq!(regs.len(), 4); // 3 variants + 1 base
616        assert!(regs[0].contains("\"ParseError\""));
617        assert!(regs[3].contains("\"ConversionError\""));
618    }
619
620    #[test]
621    fn test_unit_variant_pattern() {
622        let error = ErrorDef {
623            name: "MyError".to_string(),
624            rust_path: "my_crate::MyError".to_string(),
625            variants: vec![ErrorVariant {
626                name: "NotFound".to_string(),
627                message_template: Some("not found".to_string()),
628                fields: vec![],
629                has_source: false,
630                has_from: false,
631                is_unit: true,
632                doc: String::new(),
633            }],
634            doc: String::new(),
635        };
636        let output = gen_pyo3_error_converter(&error, "my_crate");
637        assert!(output.contains("my_crate::MyError::NotFound => NotFoundError::new_err(msg),"));
638        // Ensure no (..) for unit variants
639        assert!(!output.contains("NotFound(..)"));
640    }
641
642    // -----------------------------------------------------------------------
643    // NAPI tests
644    // -----------------------------------------------------------------------
645
646    #[test]
647    fn test_gen_napi_error_types() {
648        let error = sample_error();
649        let output = gen_napi_error_types(&error);
650        assert!(output.contains("CONVERSION_ERROR_ERROR_PARSE_ERROR"));
651        assert!(output.contains("CONVERSION_ERROR_ERROR_IO_ERROR"));
652        assert!(output.contains("CONVERSION_ERROR_ERROR_OTHER"));
653    }
654
655    #[test]
656    fn test_gen_napi_error_converter() {
657        let error = sample_error();
658        let output = gen_napi_error_converter(&error, "html_to_markdown_rs");
659        assert!(
660            output
661                .contains("fn conversion_error_to_napi_err(e: html_to_markdown_rs::ConversionError) -> napi::Error {")
662        );
663        assert!(output.contains("napi::Error::new(napi::Status::GenericFailure,"));
664        assert!(output.contains("[ParseError]"));
665        assert!(output.contains("[IoError]"));
666        assert!(output.contains("#[allow(dead_code)]"));
667    }
668
669    #[test]
670    fn test_napi_unit_variant() {
671        let error = ErrorDef {
672            name: "MyError".to_string(),
673            rust_path: "my_crate::MyError".to_string(),
674            variants: vec![ErrorVariant {
675                name: "NotFound".to_string(),
676                message_template: None,
677                fields: vec![],
678                has_source: false,
679                has_from: false,
680                is_unit: true,
681                doc: String::new(),
682            }],
683            doc: String::new(),
684        };
685        let output = gen_napi_error_converter(&error, "my_crate");
686        assert!(output.contains("my_crate::MyError::NotFound =>"));
687        assert!(!output.contains("NotFound(..)"));
688    }
689
690    // -----------------------------------------------------------------------
691    // WASM tests
692    // -----------------------------------------------------------------------
693
694    #[test]
695    fn test_gen_wasm_error_converter() {
696        let error = sample_error();
697        let output = gen_wasm_error_converter(&error, "html_to_markdown_rs");
698        assert!(output.contains(
699            "fn conversion_error_to_js_value(e: html_to_markdown_rs::ConversionError) -> wasm_bindgen::JsValue {"
700        ));
701        assert!(output.contains("JsValue::from_str(&e.to_string())"));
702        assert!(output.contains("#[allow(dead_code)]"));
703    }
704
705    // -----------------------------------------------------------------------
706    // PHP tests
707    // -----------------------------------------------------------------------
708
709    #[test]
710    fn test_gen_php_error_converter() {
711        let error = sample_error();
712        let output = gen_php_error_converter(&error, "html_to_markdown_rs");
713        assert!(output.contains("fn conversion_error_to_php_err(e: html_to_markdown_rs::ConversionError) -> ext_php_rs::exception::PhpException {"));
714        assert!(output.contains("PhpException::default(format!(\"[ParseError] {}\", msg))"));
715        assert!(output.contains("#[allow(dead_code)]"));
716    }
717
718    // -----------------------------------------------------------------------
719    // Magnus tests
720    // -----------------------------------------------------------------------
721
722    #[test]
723    fn test_gen_magnus_error_converter() {
724        let error = sample_error();
725        let output = gen_magnus_error_converter(&error, "html_to_markdown_rs");
726        assert!(
727            output.contains(
728                "fn conversion_error_to_magnus_err(e: html_to_markdown_rs::ConversionError) -> magnus::Error {"
729            )
730        );
731        assert!(output.contains("magnus::Error::new(magnus::exception::runtime_error(), msg)"));
732        assert!(output.contains("#[allow(dead_code)]"));
733    }
734
735    // -----------------------------------------------------------------------
736    // Rustler tests
737    // -----------------------------------------------------------------------
738
739    #[test]
740    fn test_gen_rustler_error_converter() {
741        let error = sample_error();
742        let output = gen_rustler_error_converter(&error, "html_to_markdown_rs");
743        assert!(
744            output.contains("fn conversion_error_to_rustler_err(e: html_to_markdown_rs::ConversionError) -> String {")
745        );
746        assert!(output.contains("e.to_string()"));
747        assert!(output.contains("#[allow(dead_code)]"));
748    }
749
750    // -----------------------------------------------------------------------
751    // Helper tests
752    // -----------------------------------------------------------------------
753
754    #[test]
755    fn test_to_screaming_snake() {
756        assert_eq!(to_screaming_snake("ConversionError"), "CONVERSION_ERROR");
757        assert_eq!(to_screaming_snake("IoError"), "IO_ERROR");
758        assert_eq!(to_screaming_snake("Other"), "OTHER");
759    }
760
761    // -----------------------------------------------------------------------
762    // FFI (C) tests
763    // -----------------------------------------------------------------------
764
765    #[test]
766    fn test_gen_ffi_error_codes() {
767        let error = sample_error();
768        let output = gen_ffi_error_codes(&error);
769        assert!(output.contains("CONVERSION_ERROR_NONE = 0"));
770        assert!(output.contains("CONVERSION_ERROR_PARSE_ERROR = 1"));
771        assert!(output.contains("CONVERSION_ERROR_IO_ERROR = 2"));
772        assert!(output.contains("CONVERSION_ERROR_OTHER = 3"));
773        assert!(output.contains("conversion_error_t;"));
774        assert!(output.contains("conversion_error_error_message(conversion_error_t code)"));
775    }
776
777    // -----------------------------------------------------------------------
778    // Go tests
779    // -----------------------------------------------------------------------
780
781    #[test]
782    fn test_gen_go_error_types() {
783        let error = sample_error();
784        let output = gen_go_error_types(&error);
785        assert!(output.contains("ErrParseError = errors.New("));
786        assert!(output.contains("ErrIoError = errors.New("));
787        assert!(output.contains("ErrOther = errors.New("));
788        assert!(output.contains("type ConversionError struct {"));
789        assert!(output.contains("Code    string"));
790        assert!(output.contains("func (e *ConversionError) Error() string"));
791    }
792
793    // -----------------------------------------------------------------------
794    // Java tests
795    // -----------------------------------------------------------------------
796
797    #[test]
798    fn test_gen_java_error_types() {
799        let error = sample_error();
800        let files = gen_java_error_types(&error, "dev.kreuzberg.test");
801        // base + 3 variants
802        assert_eq!(files.len(), 4);
803        // Base class
804        assert_eq!(files[0].0, "ConversionErrorException");
805        assert!(
806            files[0]
807                .1
808                .contains("public class ConversionErrorException extends Exception")
809        );
810        assert!(files[0].1.contains("package dev.kreuzberg.test;"));
811        // Variant classes
812        assert_eq!(files[1].0, "ParseErrorException");
813        assert!(
814            files[1]
815                .1
816                .contains("public class ParseErrorException extends ConversionErrorException")
817        );
818        assert_eq!(files[2].0, "IoErrorException");
819        assert_eq!(files[3].0, "OtherException");
820    }
821
822    // -----------------------------------------------------------------------
823    // C# tests
824    // -----------------------------------------------------------------------
825
826    #[test]
827    fn test_gen_csharp_error_types() {
828        let error = sample_error();
829        let files = gen_csharp_error_types(&error, "Kreuzberg.Test");
830        // base + 3 variants
831        assert_eq!(files.len(), 4);
832        // Base class
833        assert_eq!(files[0].0, "ConversionErrorException");
834        assert!(files[0].1.contains("public class ConversionErrorException : Exception"));
835        assert!(files[0].1.contains("namespace Kreuzberg.Test;"));
836        // Variant classes
837        assert_eq!(files[1].0, "ParseErrorException");
838        assert!(
839            files[1]
840                .1
841                .contains("public class ParseErrorException : ConversionErrorException")
842        );
843        assert_eq!(files[2].0, "IoErrorException");
844        assert_eq!(files[3].0, "OtherException");
845    }
846}