1use ahash::AHashSet;
2use alef_core::ir::{ErrorDef, ErrorVariant};
3
4use crate::conversions::is_tuple_variant;
5
6fn error_variant_wildcard_pattern(rust_path: &str, variant: &ErrorVariant) -> String {
9 if variant.is_unit {
10 format!("{rust_path}::{}", variant.name)
11 } else if is_tuple_variant(&variant.fields) {
12 format!("{rust_path}::{}(..)", variant.name)
13 } else {
14 format!("{rust_path}::{} {{ .. }}", variant.name)
15 }
16}
17
18const PYTHON_BUILTIN_EXCEPTIONS: &[&str] = &[
20 "ConnectionError",
21 "TimeoutError",
22 "PermissionError",
23 "FileNotFoundError",
24 "ValueError",
25 "TypeError",
26 "RuntimeError",
27 "OSError",
28 "IOError",
29 "KeyError",
30 "IndexError",
31 "AttributeError",
32 "ImportError",
33 "MemoryError",
34 "OverflowError",
35 "StopIteration",
36 "RecursionError",
37 "SystemError",
38 "ReferenceError",
39 "BufferError",
40 "EOFError",
41 "LookupError",
42 "ArithmeticError",
43 "AssertionError",
44 "BlockingIOError",
45 "BrokenPipeError",
46 "ChildProcessError",
47 "FileExistsError",
48 "InterruptedError",
49 "IsADirectoryError",
50 "NotADirectoryError",
51 "ProcessLookupError",
52 "UnicodeError",
53];
54
55fn error_base_prefix(error_name: &str) -> &str {
58 error_name.strip_suffix("Error").unwrap_or(error_name)
59}
60
61pub fn python_exception_name(variant_name: &str, error_name: &str) -> String {
67 let candidate = if variant_name.ends_with("Error") {
68 variant_name.to_string()
69 } else {
70 format!("{}Error", variant_name)
71 };
72
73 if PYTHON_BUILTIN_EXCEPTIONS.contains(&candidate.as_str()) {
74 let prefix = error_base_prefix(error_name);
75 if candidate.starts_with(prefix) {
77 candidate
78 } else {
79 format!("{}{}", prefix, candidate)
80 }
81 } else {
82 candidate
83 }
84}
85
86pub fn gen_pyo3_error_types(error: &ErrorDef, module_name: &str, seen_exceptions: &mut AHashSet<String>) -> String {
90 let mut lines = Vec::with_capacity(error.variants.len() + 2);
91 lines.push("// Error types".to_string());
92
93 for variant in &error.variants {
95 let variant_name = python_exception_name(&variant.name, &error.name);
96 if seen_exceptions.insert(variant_name.clone()) {
97 lines.push(format!(
98 "pyo3::create_exception!({module_name}, {}, pyo3::exceptions::PyException);",
99 variant_name
100 ));
101 }
102 }
103
104 if seen_exceptions.insert(error.name.clone()) {
106 lines.push(format!(
107 "pyo3::create_exception!({module_name}, {}, pyo3::exceptions::PyException);",
108 error.name
109 ));
110 }
111
112 lines.join("\n")
113}
114
115pub fn gen_pyo3_error_converter(error: &ErrorDef, core_import: &str) -> String {
118 let rust_path = if error.rust_path.is_empty() {
119 format!("{core_import}::{}", error.name)
120 } else {
121 error.rust_path.replace('-', "_")
122 };
123
124 let fn_name = format!("{}_to_py_err", to_snake_case(&error.name));
125
126 let mut lines = Vec::new();
127 lines.push(format!("/// Convert a `{rust_path}` error to a Python exception."));
128 lines.push(format!("fn {fn_name}(e: {rust_path}) -> pyo3::PyErr {{"));
129 lines.push(" let msg = e.to_string();".to_string());
130 lines.push(" #[allow(unreachable_patterns)]".to_string());
131 lines.push(" match &e {".to_string());
132
133 for variant in &error.variants {
134 let pattern = error_variant_wildcard_pattern(&rust_path, variant);
135 let variant_exc_name = python_exception_name(&variant.name, &error.name);
136 lines.push(format!(" {pattern} => {}::new_err(msg),", variant_exc_name));
137 }
138
139 lines.push(format!(" _ => {}::new_err(msg),", error.name));
141 lines.push(" }".to_string());
142 lines.push("}".to_string());
143 lines.join("\n")
144}
145
146pub fn gen_pyo3_error_registration(error: &ErrorDef, seen_registrations: &mut AHashSet<String>) -> Vec<String> {
150 let mut registrations = Vec::with_capacity(error.variants.len() + 1);
151
152 for variant in &error.variants {
153 let variant_exc_name = python_exception_name(&variant.name, &error.name);
154 if seen_registrations.insert(variant_exc_name.clone()) {
155 registrations.push(format!(
156 " m.add(\"{}\", m.py().get_type::<{}>())?;",
157 variant_exc_name, variant_exc_name
158 ));
159 }
160 }
161
162 if seen_registrations.insert(error.name.clone()) {
164 registrations.push(format!(
165 " m.add(\"{}\", m.py().get_type::<{}>())?;",
166 error.name, error.name
167 ));
168 }
169
170 registrations
171}
172
173pub fn converter_fn_name(error: &ErrorDef) -> String {
175 format!("{}_to_py_err", to_snake_case(&error.name))
176}
177
178fn to_snake_case(s: &str) -> String {
180 let mut result = String::with_capacity(s.len() + 4);
181 for (i, c) in s.chars().enumerate() {
182 if c.is_uppercase() {
183 if i > 0 {
184 result.push('_');
185 }
186 result.push(c.to_ascii_lowercase());
187 } else {
188 result.push(c);
189 }
190 }
191 result
192}
193
194pub fn gen_napi_error_types(error: &ErrorDef) -> String {
200 let mut lines = Vec::with_capacity(error.variants.len() + 4);
201 lines.push("// Error variant name constants".to_string());
202 for variant in &error.variants {
203 lines.push(format!(
204 "pub const {}_ERROR_{}: &str = \"{}\";",
205 to_screaming_snake(&error.name),
206 to_screaming_snake(&variant.name),
207 variant.name,
208 ));
209 }
210 lines.join("\n")
211}
212
213pub fn gen_napi_error_converter(error: &ErrorDef, core_import: &str) -> String {
215 let rust_path = if error.rust_path.is_empty() {
216 format!("{core_import}::{}", error.name)
217 } else {
218 error.rust_path.replace('-', "_")
219 };
220
221 let fn_name = format!("{}_to_napi_err", to_snake_case(&error.name));
222
223 let mut lines = Vec::new();
224 lines.push(format!("/// Convert a `{rust_path}` error to a NAPI error."));
225 lines.push("#[allow(dead_code)]".to_string());
226 lines.push(format!("fn {fn_name}(e: {rust_path}) -> napi::Error {{"));
227 lines.push(" let msg = e.to_string();".to_string());
228 lines.push(" #[allow(unreachable_patterns)]".to_string());
229 lines.push(" match &e {".to_string());
230
231 for variant in &error.variants {
232 let pattern = error_variant_wildcard_pattern(&rust_path, variant);
233 lines.push(format!(
234 " {pattern} => napi::Error::new(napi::Status::GenericFailure, format!(\"[{}] {{}}\", msg)),",
235 variant.name,
236 ));
237 }
238
239 lines.push(" _ => napi::Error::new(napi::Status::GenericFailure, msg),".to_string());
241 lines.push(" }".to_string());
242 lines.push("}".to_string());
243 lines.join("\n")
244}
245
246pub fn napi_converter_fn_name(error: &ErrorDef) -> String {
248 format!("{}_to_napi_err", to_snake_case(&error.name))
249}
250
251pub fn gen_wasm_error_converter(error: &ErrorDef, core_import: &str) -> String {
259 let rust_path = if error.rust_path.is_empty() {
260 format!("{core_import}::{}", error.name)
261 } else {
262 error.rust_path.replace('-', "_")
263 };
264
265 let fn_name = format!("{}_to_js_value", to_snake_case(&error.name));
266 let code_fn_name = format!("{}_error_code", to_snake_case(&error.name));
267
268 let mut lines = Vec::new();
269
270 lines.push(format!("/// Return the error code string for a `{rust_path}` variant."));
272 lines.push("#[allow(dead_code)]".to_string());
273 lines.push(format!("fn {code_fn_name}(e: &{rust_path}) -> &'static str {{"));
274 lines.push(" #[allow(unreachable_patterns)]".to_string());
275 lines.push(" match e {".to_string());
276 for variant in &error.variants {
277 let pattern = error_variant_wildcard_pattern(&rust_path, variant);
278 let code = to_snake_case(&variant.name);
279 lines.push(format!(" {pattern} => \"{code}\","));
280 }
281 lines.push(format!(" _ => \"{}\",", to_snake_case(&error.name)));
282 lines.push(" }".to_string());
283 lines.push("}".to_string());
284
285 lines.push(String::new());
286
287 lines.push(format!(
289 "/// Convert a `{rust_path}` error to a `JsValue` object with `code` and `message` fields."
290 ));
291 lines.push("#[allow(dead_code)]".to_string());
292 lines.push(format!("fn {fn_name}(e: {rust_path}) -> wasm_bindgen::JsValue {{"));
293 lines.push(format!(" let code = {code_fn_name}(&e);"));
294 lines.push(" let message = e.to_string();".to_string());
295 lines.push(" let obj = js_sys::Object::new();".to_string());
296 lines.push(" js_sys::Reflect::set(&obj, &\"code\".into(), &code.into()).ok();".to_string());
297 lines.push(" js_sys::Reflect::set(&obj, &\"message\".into(), &message.into()).ok();".to_string());
298 lines.push(" obj.into()".to_string());
299 lines.push("}".to_string());
300
301 lines.join("\n")
302}
303
304pub fn wasm_converter_fn_name(error: &ErrorDef) -> String {
306 format!("{}_to_js_value", to_snake_case(&error.name))
307}
308
309pub fn gen_php_error_converter(error: &ErrorDef, core_import: &str) -> String {
315 let rust_path = if error.rust_path.is_empty() {
316 format!("{core_import}::{}", error.name)
317 } else {
318 error.rust_path.replace('-', "_")
319 };
320
321 let fn_name = format!("{}_to_php_err", to_snake_case(&error.name));
322
323 let mut lines = Vec::new();
324 lines.push(format!("/// Convert a `{rust_path}` error to a PHP exception."));
325 lines.push("#[allow(dead_code)]".to_string());
326 lines.push(format!(
327 "fn {fn_name}(e: {rust_path}) -> ext_php_rs::exception::PhpException {{"
328 ));
329 lines.push(" let msg = e.to_string();".to_string());
330 lines.push(" #[allow(unreachable_patterns)]".to_string());
331 lines.push(" match &e {".to_string());
332
333 for variant in &error.variants {
334 let pattern = error_variant_wildcard_pattern(&rust_path, variant);
335 lines.push(format!(
336 " {pattern} => ext_php_rs::exception::PhpException::default(format!(\"[{}] {{}}\", msg)),",
337 variant.name,
338 ));
339 }
340
341 lines.push(" _ => ext_php_rs::exception::PhpException::default(msg),".to_string());
343 lines.push(" }".to_string());
344 lines.push("}".to_string());
345 lines.join("\n")
346}
347
348pub fn php_converter_fn_name(error: &ErrorDef) -> String {
350 format!("{}_to_php_err", to_snake_case(&error.name))
351}
352
353pub fn gen_magnus_error_converter(error: &ErrorDef, core_import: &str) -> String {
359 let rust_path = if error.rust_path.is_empty() {
360 format!("{core_import}::{}", error.name)
361 } else {
362 error.rust_path.replace('-', "_")
363 };
364
365 let fn_name = format!("{}_to_magnus_err", to_snake_case(&error.name));
366
367 let mut lines = Vec::new();
368 lines.push(format!("/// Convert a `{rust_path}` error to a Magnus runtime error."));
369 lines.push("#[allow(dead_code)]".to_string());
370 lines.push(format!("fn {fn_name}(e: {rust_path}) -> magnus::Error {{"));
371 lines.push(" let msg = e.to_string();".to_string());
372 lines.push(
373 " magnus::Error::new(unsafe { magnus::Ruby::get_unchecked() }.exception_runtime_error(), msg)".to_string(),
374 );
375 lines.push("}".to_string());
376 lines.join("\n")
377}
378
379pub fn magnus_converter_fn_name(error: &ErrorDef) -> String {
381 format!("{}_to_magnus_err", to_snake_case(&error.name))
382}
383
384pub fn gen_rustler_error_converter(error: &ErrorDef, core_import: &str) -> String {
390 let rust_path = if error.rust_path.is_empty() {
391 format!("{core_import}::{}", error.name)
392 } else {
393 error.rust_path.replace('-', "_")
394 };
395
396 let fn_name = format!("{}_to_rustler_err", to_snake_case(&error.name));
397
398 let mut lines = Vec::new();
399 lines.push(format!("/// Convert a `{rust_path}` error to a Rustler error string."));
400 lines.push("#[allow(dead_code)]".to_string());
401 lines.push(format!("fn {fn_name}(e: {rust_path}) -> String {{"));
402 lines.push(" e.to_string()".to_string());
403 lines.push("}".to_string());
404 lines.join("\n")
405}
406
407pub fn rustler_converter_fn_name(error: &ErrorDef) -> String {
409 format!("{}_to_rustler_err", to_snake_case(&error.name))
410}
411
412pub fn gen_ffi_error_codes(error: &ErrorDef) -> String {
421 let prefix = to_screaming_snake(&error.name);
422 let prefix_lower = to_snake_case(&error.name);
423
424 let mut lines = Vec::new();
425 lines.push(format!("/// Error codes for `{}`.", error.name));
426 lines.push("typedef enum {".to_string());
427 lines.push(format!(" {}_NONE = 0,", prefix));
428
429 for (i, variant) in error.variants.iter().enumerate() {
430 let variant_screaming = to_screaming_snake(&variant.name);
431 lines.push(format!(" {}_{} = {},", prefix, variant_screaming, i + 1));
432 }
433
434 lines.push(format!("}} {}_t;\n", prefix_lower));
435
436 lines.push(format!(
438 "/// Return a static string describing the error code.\nconst char* {}_error_message({}_t code);",
439 prefix_lower, prefix_lower
440 ));
441
442 lines.join("\n")
443}
444
445pub fn gen_go_error_types(error: &ErrorDef) -> String {
451 let mut lines = Vec::new();
452
453 lines.push("var (".to_string());
455 for variant in &error.variants {
456 let err_name = format!("Err{}", variant.name);
457 let msg = variant_display_message(variant);
458 lines.push(format!(" {} = errors.New(\"{}\")", err_name, msg));
459 }
460 lines.push(")\n".to_string());
461
462 lines.push(format!("// {} is a structured error type.", error.name));
464 lines.push(format!("type {} struct {{", error.name));
465 lines.push(" Code string".to_string());
466 lines.push(" Message string".to_string());
467 lines.push("}\n".to_string());
468
469 lines.push(format!(
470 "func (e *{}) Error() string {{ return e.Message }}",
471 error.name
472 ));
473
474 lines.join("\n")
475}
476
477pub fn gen_java_error_types(error: &ErrorDef, package: &str) -> Vec<(String, String)> {
487 let mut files = Vec::with_capacity(error.variants.len() + 1);
488
489 let base_name = format!("{}Exception", error.name);
491 let mut base = String::with_capacity(512);
492 base.push_str(&format!(
493 "// DO NOT EDIT - auto-generated by alef\npackage {};\n\n",
494 package
495 ));
496 if !error.doc.is_empty() {
497 base.push_str(&format!("/** {} */\n", error.doc));
498 }
499 base.push_str(&format!("public class {} extends Exception {{\n", base_name));
500 base.push_str(&format!(
501 " public {}(String message) {{\n super(message);\n }}\n\n",
502 base_name
503 ));
504 base.push_str(&format!(
505 " public {}(String message, Throwable cause) {{\n super(message, cause);\n }}\n",
506 base_name
507 ));
508 base.push_str("}\n");
509 files.push((base_name.clone(), base));
510
511 for variant in &error.variants {
513 let class_name = format!("{}Exception", variant.name);
514 let mut content = String::with_capacity(512);
515 content.push_str(&format!(
516 "// DO NOT EDIT - auto-generated by alef\npackage {};\n\n",
517 package
518 ));
519 if !variant.doc.is_empty() {
520 content.push_str(&format!("/** {} */\n", variant.doc));
521 }
522 content.push_str(&format!("public class {} extends {} {{\n", class_name, base_name));
523 content.push_str(&format!(
524 " public {}(String message) {{\n super(message);\n }}\n\n",
525 class_name
526 ));
527 content.push_str(&format!(
528 " public {}(String message, Throwable cause) {{\n super(message, cause);\n }}\n",
529 class_name
530 ));
531 content.push_str("}\n");
532 files.push((class_name, content));
533 }
534
535 files
536}
537
538pub fn gen_csharp_error_types(error: &ErrorDef, namespace: &str) -> Vec<(String, String)> {
548 let mut files = Vec::with_capacity(error.variants.len() + 1);
549
550 let base_name = format!("{}Exception", error.name);
551
552 {
554 let mut out = String::with_capacity(512);
555 out.push_str("// This file is auto-generated by alef. DO NOT EDIT.\nusing System;\n\n");
556 out.push_str(&format!("namespace {};\n\n", namespace));
557 if !error.doc.is_empty() {
558 out.push_str("/// <summary>\n");
559 for line in error.doc.lines() {
560 out.push_str(&format!("/// {}\n", line));
561 }
562 out.push_str("/// </summary>\n");
563 }
564 out.push_str(&format!("public class {} : Exception\n{{\n", base_name));
565 out.push_str(&format!(
566 " public {}(string message) : base(message) {{ }}\n\n",
567 base_name
568 ));
569 out.push_str(&format!(
570 " public {}(string message, Exception innerException) : base(message, innerException) {{ }}\n",
571 base_name
572 ));
573 out.push_str("}\n");
574 files.push((base_name.clone(), out));
575 }
576
577 for variant in &error.variants {
579 let class_name = format!("{}Exception", variant.name);
580 let mut out = String::with_capacity(512);
581 out.push_str("// This file is auto-generated by alef. DO NOT EDIT.\nusing System;\n\n");
582 out.push_str(&format!("namespace {};\n\n", namespace));
583 if !variant.doc.is_empty() {
584 out.push_str("/// <summary>\n");
585 for line in variant.doc.lines() {
586 out.push_str(&format!("/// {}\n", line));
587 }
588 out.push_str("/// </summary>\n");
589 }
590 out.push_str(&format!("public class {} : {}\n{{\n", class_name, base_name));
591 out.push_str(&format!(
592 " public {}(string message) : base(message) {{ }}\n\n",
593 class_name
594 ));
595 out.push_str(&format!(
596 " public {}(string message, Exception innerException) : base(message, innerException) {{ }}\n",
597 class_name
598 ));
599 out.push_str("}\n");
600 files.push((class_name, out));
601 }
602
603 files
604}
605
606fn to_screaming_snake(s: &str) -> String {
612 let mut result = String::with_capacity(s.len() + 4);
613 for (i, c) in s.chars().enumerate() {
614 if c.is_uppercase() {
615 if i > 0 {
616 result.push('_');
617 }
618 result.push(c.to_ascii_uppercase());
619 } else {
620 result.push(c.to_ascii_uppercase());
621 }
622 }
623 result
624}
625
626fn variant_display_message(variant: &ErrorVariant) -> String {
631 if let Some(tmpl) = &variant.message_template {
632 let msg = tmpl
634 .replace("{0}", "")
635 .replace("{source}", "")
636 .trim_end_matches(": ")
637 .trim()
638 .to_string();
639 if msg.is_empty() {
640 to_snake_case(&variant.name).replace('_', " ")
641 } else {
642 msg
643 }
644 } else {
645 to_snake_case(&variant.name).replace('_', " ")
646 }
647}
648
649#[cfg(test)]
650mod tests {
651 use super::*;
652 use alef_core::ir::{ErrorDef, ErrorVariant};
653
654 use alef_core::ir::{CoreWrapper, FieldDef, TypeRef};
655
656 fn tuple_field(index: usize) -> FieldDef {
658 FieldDef {
659 name: format!("_{index}"),
660 ty: TypeRef::String,
661 optional: false,
662 default: None,
663 doc: String::new(),
664 sanitized: false,
665 is_boxed: false,
666 type_rust_path: None,
667 cfg: None,
668 typed_default: None,
669 core_wrapper: CoreWrapper::None,
670 vec_inner_core_wrapper: CoreWrapper::None,
671 newtype_wrapper: None,
672 }
673 }
674
675 fn named_field(name: &str) -> FieldDef {
677 FieldDef {
678 name: name.to_string(),
679 ty: TypeRef::String,
680 optional: false,
681 default: None,
682 doc: String::new(),
683 sanitized: false,
684 is_boxed: false,
685 type_rust_path: None,
686 cfg: None,
687 typed_default: None,
688 core_wrapper: CoreWrapper::None,
689 vec_inner_core_wrapper: CoreWrapper::None,
690 newtype_wrapper: None,
691 }
692 }
693
694 fn sample_error() -> ErrorDef {
695 ErrorDef {
696 name: "ConversionError".to_string(),
697 rust_path: "html_to_markdown_rs::ConversionError".to_string(),
698 original_rust_path: String::new(),
699 variants: vec![
700 ErrorVariant {
701 name: "ParseError".to_string(),
702 message_template: Some("HTML parsing error: {0}".to_string()),
703 fields: vec![tuple_field(0)],
704 has_source: false,
705 has_from: false,
706 is_unit: false,
707 doc: String::new(),
708 },
709 ErrorVariant {
710 name: "IoError".to_string(),
711 message_template: Some("I/O error: {0}".to_string()),
712 fields: vec![tuple_field(0)],
713 has_source: false,
714 has_from: true,
715 is_unit: false,
716 doc: String::new(),
717 },
718 ErrorVariant {
719 name: "Other".to_string(),
720 message_template: Some("Conversion error: {0}".to_string()),
721 fields: vec![tuple_field(0)],
722 has_source: false,
723 has_from: false,
724 is_unit: false,
725 doc: String::new(),
726 },
727 ],
728 doc: "Error type for conversion operations.".to_string(),
729 }
730 }
731
732 #[test]
733 fn test_gen_error_types() {
734 let error = sample_error();
735 let output = gen_pyo3_error_types(&error, "_module", &mut AHashSet::new());
736 assert!(output.contains("pyo3::create_exception!(_module, ParseError, pyo3::exceptions::PyException);"));
737 assert!(output.contains("pyo3::create_exception!(_module, IoError, pyo3::exceptions::PyException);"));
738 assert!(output.contains("pyo3::create_exception!(_module, OtherError, pyo3::exceptions::PyException);"));
739 assert!(output.contains("pyo3::create_exception!(_module, ConversionError, pyo3::exceptions::PyException);"));
740 }
741
742 #[test]
743 fn test_gen_error_converter() {
744 let error = sample_error();
745 let output = gen_pyo3_error_converter(&error, "html_to_markdown_rs");
746 assert!(
747 output.contains("fn conversion_error_to_py_err(e: html_to_markdown_rs::ConversionError) -> pyo3::PyErr {")
748 );
749 assert!(output.contains("html_to_markdown_rs::ConversionError::ParseError(..) => ParseError::new_err(msg),"));
750 assert!(output.contains("html_to_markdown_rs::ConversionError::IoError(..) => IoError::new_err(msg),"));
751 }
752
753 #[test]
754 fn test_gen_error_registration() {
755 let error = sample_error();
756 let regs = gen_pyo3_error_registration(&error, &mut AHashSet::new());
757 assert_eq!(regs.len(), 4); assert!(regs[0].contains("\"ParseError\""));
759 assert!(regs[3].contains("\"ConversionError\""));
760 }
761
762 #[test]
763 fn test_unit_variant_pattern() {
764 let error = ErrorDef {
765 name: "MyError".to_string(),
766 rust_path: "my_crate::MyError".to_string(),
767 original_rust_path: String::new(),
768 variants: vec![ErrorVariant {
769 name: "NotFound".to_string(),
770 message_template: Some("not found".to_string()),
771 fields: vec![],
772 has_source: false,
773 has_from: false,
774 is_unit: true,
775 doc: String::new(),
776 }],
777 doc: String::new(),
778 };
779 let output = gen_pyo3_error_converter(&error, "my_crate");
780 assert!(output.contains("my_crate::MyError::NotFound => NotFoundError::new_err(msg),"));
781 assert!(!output.contains("NotFound(..)"));
783 }
784
785 #[test]
786 fn test_struct_variant_pattern() {
787 let error = ErrorDef {
788 name: "MyError".to_string(),
789 rust_path: "my_crate::MyError".to_string(),
790 original_rust_path: String::new(),
791 variants: vec![ErrorVariant {
792 name: "Parsing".to_string(),
793 message_template: Some("parsing error: {message}".to_string()),
794 fields: vec![named_field("message")],
795 has_source: false,
796 has_from: false,
797 is_unit: false,
798 doc: String::new(),
799 }],
800 doc: String::new(),
801 };
802 let output = gen_pyo3_error_converter(&error, "my_crate");
803 assert!(
804 output.contains("my_crate::MyError::Parsing { .. } => ParsingError::new_err(msg),"),
805 "Struct variants must use {{ .. }} pattern, got:\n{output}"
806 );
807 assert!(!output.contains("Parsing(..)"));
809 }
810
811 #[test]
816 fn test_gen_napi_error_types() {
817 let error = sample_error();
818 let output = gen_napi_error_types(&error);
819 assert!(output.contains("CONVERSION_ERROR_ERROR_PARSE_ERROR"));
820 assert!(output.contains("CONVERSION_ERROR_ERROR_IO_ERROR"));
821 assert!(output.contains("CONVERSION_ERROR_ERROR_OTHER"));
822 }
823
824 #[test]
825 fn test_gen_napi_error_converter() {
826 let error = sample_error();
827 let output = gen_napi_error_converter(&error, "html_to_markdown_rs");
828 assert!(
829 output
830 .contains("fn conversion_error_to_napi_err(e: html_to_markdown_rs::ConversionError) -> napi::Error {")
831 );
832 assert!(output.contains("napi::Error::new(napi::Status::GenericFailure,"));
833 assert!(output.contains("[ParseError]"));
834 assert!(output.contains("[IoError]"));
835 assert!(output.contains("#[allow(dead_code)]"));
836 }
837
838 #[test]
839 fn test_napi_unit_variant() {
840 let error = ErrorDef {
841 name: "MyError".to_string(),
842 rust_path: "my_crate::MyError".to_string(),
843 original_rust_path: String::new(),
844 variants: vec![ErrorVariant {
845 name: "NotFound".to_string(),
846 message_template: None,
847 fields: vec![],
848 has_source: false,
849 has_from: false,
850 is_unit: true,
851 doc: String::new(),
852 }],
853 doc: String::new(),
854 };
855 let output = gen_napi_error_converter(&error, "my_crate");
856 assert!(output.contains("my_crate::MyError::NotFound =>"));
857 assert!(!output.contains("NotFound(..)"));
858 }
859
860 #[test]
865 fn test_gen_wasm_error_converter() {
866 let error = sample_error();
867 let output = gen_wasm_error_converter(&error, "html_to_markdown_rs");
868 assert!(output.contains(
870 "fn conversion_error_to_js_value(e: html_to_markdown_rs::ConversionError) -> wasm_bindgen::JsValue {"
871 ));
872 assert!(output.contains("js_sys::Object::new()"));
874 assert!(output.contains("js_sys::Reflect::set(&obj, &\"code\".into(), &code.into()).ok()"));
875 assert!(output.contains("js_sys::Reflect::set(&obj, &\"message\".into(), &message.into()).ok()"));
876 assert!(output.contains("obj.into()"));
877 assert!(
879 output
880 .contains("fn conversion_error_error_code(e: &html_to_markdown_rs::ConversionError) -> &'static str {")
881 );
882 assert!(output.contains("\"parse_error\""));
883 assert!(output.contains("\"io_error\""));
884 assert!(output.contains("\"other\""));
885 assert!(output.contains("#[allow(dead_code)]"));
886 }
887
888 #[test]
893 fn test_gen_php_error_converter() {
894 let error = sample_error();
895 let output = gen_php_error_converter(&error, "html_to_markdown_rs");
896 assert!(output.contains("fn conversion_error_to_php_err(e: html_to_markdown_rs::ConversionError) -> ext_php_rs::exception::PhpException {"));
897 assert!(output.contains("PhpException::default(format!(\"[ParseError] {}\", msg))"));
898 assert!(output.contains("#[allow(dead_code)]"));
899 }
900
901 #[test]
906 fn test_gen_magnus_error_converter() {
907 let error = sample_error();
908 let output = gen_magnus_error_converter(&error, "html_to_markdown_rs");
909 assert!(
910 output.contains(
911 "fn conversion_error_to_magnus_err(e: html_to_markdown_rs::ConversionError) -> magnus::Error {"
912 )
913 );
914 assert!(
915 output.contains(
916 "magnus::Error::new(unsafe { magnus::Ruby::get_unchecked() }.exception_runtime_error(), msg)"
917 )
918 );
919 assert!(output.contains("#[allow(dead_code)]"));
920 }
921
922 #[test]
927 fn test_gen_rustler_error_converter() {
928 let error = sample_error();
929 let output = gen_rustler_error_converter(&error, "html_to_markdown_rs");
930 assert!(
931 output.contains("fn conversion_error_to_rustler_err(e: html_to_markdown_rs::ConversionError) -> String {")
932 );
933 assert!(output.contains("e.to_string()"));
934 assert!(output.contains("#[allow(dead_code)]"));
935 }
936
937 #[test]
942 fn test_to_screaming_snake() {
943 assert_eq!(to_screaming_snake("ConversionError"), "CONVERSION_ERROR");
944 assert_eq!(to_screaming_snake("IoError"), "IO_ERROR");
945 assert_eq!(to_screaming_snake("Other"), "OTHER");
946 }
947
948 #[test]
953 fn test_gen_ffi_error_codes() {
954 let error = sample_error();
955 let output = gen_ffi_error_codes(&error);
956 assert!(output.contains("CONVERSION_ERROR_NONE = 0"));
957 assert!(output.contains("CONVERSION_ERROR_PARSE_ERROR = 1"));
958 assert!(output.contains("CONVERSION_ERROR_IO_ERROR = 2"));
959 assert!(output.contains("CONVERSION_ERROR_OTHER = 3"));
960 assert!(output.contains("conversion_error_t;"));
961 assert!(output.contains("conversion_error_error_message(conversion_error_t code)"));
962 }
963
964 #[test]
969 fn test_gen_go_error_types() {
970 let error = sample_error();
971 let output = gen_go_error_types(&error);
972 assert!(output.contains("ErrParseError = errors.New("));
973 assert!(output.contains("ErrIoError = errors.New("));
974 assert!(output.contains("ErrOther = errors.New("));
975 assert!(output.contains("type ConversionError struct {"));
976 assert!(output.contains("Code string"));
977 assert!(output.contains("func (e *ConversionError) Error() string"));
978 }
979
980 #[test]
985 fn test_gen_java_error_types() {
986 let error = sample_error();
987 let files = gen_java_error_types(&error, "dev.kreuzberg.test");
988 assert_eq!(files.len(), 4);
990 assert_eq!(files[0].0, "ConversionErrorException");
992 assert!(
993 files[0]
994 .1
995 .contains("public class ConversionErrorException extends Exception")
996 );
997 assert!(files[0].1.contains("package dev.kreuzberg.test;"));
998 assert_eq!(files[1].0, "ParseErrorException");
1000 assert!(
1001 files[1]
1002 .1
1003 .contains("public class ParseErrorException extends ConversionErrorException")
1004 );
1005 assert_eq!(files[2].0, "IoErrorException");
1006 assert_eq!(files[3].0, "OtherException");
1007 }
1008
1009 #[test]
1014 fn test_gen_csharp_error_types() {
1015 let error = sample_error();
1016 let files = gen_csharp_error_types(&error, "Kreuzberg.Test");
1017 assert_eq!(files.len(), 4);
1019 assert_eq!(files[0].0, "ConversionErrorException");
1021 assert!(files[0].1.contains("public class ConversionErrorException : Exception"));
1022 assert!(files[0].1.contains("namespace Kreuzberg.Test;"));
1023 assert_eq!(files[1].0, "ParseErrorException");
1025 assert!(
1026 files[1]
1027 .1
1028 .contains("public class ParseErrorException : ConversionErrorException")
1029 );
1030 assert_eq!(files[2].0, "IoErrorException");
1031 assert_eq!(files[3].0, "OtherException");
1032 }
1033
1034 #[test]
1039 fn test_python_exception_name_no_conflict() {
1040 assert_eq!(python_exception_name("ParseError", "ConversionError"), "ParseError");
1042 assert_eq!(python_exception_name("Other", "ConversionError"), "OtherError");
1044 }
1045
1046 #[test]
1047 fn test_python_exception_name_shadows_builtin() {
1048 assert_eq!(
1050 python_exception_name("Connection", "CrawlError"),
1051 "CrawlConnectionError"
1052 );
1053 assert_eq!(python_exception_name("Timeout", "CrawlError"), "CrawlTimeoutError");
1055 assert_eq!(
1057 python_exception_name("ConnectionError", "CrawlError"),
1058 "CrawlConnectionError"
1059 );
1060 }
1061
1062 #[test]
1063 fn test_python_exception_name_no_double_prefix() {
1064 assert_eq!(
1066 python_exception_name("CrawlConnectionError", "CrawlError"),
1067 "CrawlConnectionError"
1068 );
1069 }
1070}