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 variants: vec![
699 ErrorVariant {
700 name: "ParseError".to_string(),
701 message_template: Some("HTML parsing error: {0}".to_string()),
702 fields: vec![tuple_field(0)],
703 has_source: false,
704 has_from: false,
705 is_unit: false,
706 doc: String::new(),
707 },
708 ErrorVariant {
709 name: "IoError".to_string(),
710 message_template: Some("I/O error: {0}".to_string()),
711 fields: vec![tuple_field(0)],
712 has_source: false,
713 has_from: true,
714 is_unit: false,
715 doc: String::new(),
716 },
717 ErrorVariant {
718 name: "Other".to_string(),
719 message_template: Some("Conversion error: {0}".to_string()),
720 fields: vec![tuple_field(0)],
721 has_source: false,
722 has_from: false,
723 is_unit: false,
724 doc: String::new(),
725 },
726 ],
727 doc: "Error type for conversion operations.".to_string(),
728 }
729 }
730
731 #[test]
732 fn test_gen_error_types() {
733 let error = sample_error();
734 let output = gen_pyo3_error_types(&error, "_module", &mut AHashSet::new());
735 assert!(output.contains("pyo3::create_exception!(_module, ParseError, pyo3::exceptions::PyException);"));
736 assert!(output.contains("pyo3::create_exception!(_module, IoError, pyo3::exceptions::PyException);"));
737 assert!(output.contains("pyo3::create_exception!(_module, OtherError, pyo3::exceptions::PyException);"));
738 assert!(output.contains("pyo3::create_exception!(_module, ConversionError, pyo3::exceptions::PyException);"));
739 }
740
741 #[test]
742 fn test_gen_error_converter() {
743 let error = sample_error();
744 let output = gen_pyo3_error_converter(&error, "html_to_markdown_rs");
745 assert!(
746 output.contains("fn conversion_error_to_py_err(e: html_to_markdown_rs::ConversionError) -> pyo3::PyErr {")
747 );
748 assert!(output.contains("html_to_markdown_rs::ConversionError::ParseError(..) => ParseError::new_err(msg),"));
749 assert!(output.contains("html_to_markdown_rs::ConversionError::IoError(..) => IoError::new_err(msg),"));
750 }
751
752 #[test]
753 fn test_gen_error_registration() {
754 let error = sample_error();
755 let regs = gen_pyo3_error_registration(&error, &mut AHashSet::new());
756 assert_eq!(regs.len(), 4); assert!(regs[0].contains("\"ParseError\""));
758 assert!(regs[3].contains("\"ConversionError\""));
759 }
760
761 #[test]
762 fn test_unit_variant_pattern() {
763 let error = ErrorDef {
764 name: "MyError".to_string(),
765 rust_path: "my_crate::MyError".to_string(),
766 variants: vec![ErrorVariant {
767 name: "NotFound".to_string(),
768 message_template: Some("not found".to_string()),
769 fields: vec![],
770 has_source: false,
771 has_from: false,
772 is_unit: true,
773 doc: String::new(),
774 }],
775 doc: String::new(),
776 };
777 let output = gen_pyo3_error_converter(&error, "my_crate");
778 assert!(output.contains("my_crate::MyError::NotFound => NotFoundError::new_err(msg),"));
779 assert!(!output.contains("NotFound(..)"));
781 }
782
783 #[test]
784 fn test_struct_variant_pattern() {
785 let error = ErrorDef {
786 name: "MyError".to_string(),
787 rust_path: "my_crate::MyError".to_string(),
788 variants: vec![ErrorVariant {
789 name: "Parsing".to_string(),
790 message_template: Some("parsing error: {message}".to_string()),
791 fields: vec![named_field("message")],
792 has_source: false,
793 has_from: false,
794 is_unit: false,
795 doc: String::new(),
796 }],
797 doc: String::new(),
798 };
799 let output = gen_pyo3_error_converter(&error, "my_crate");
800 assert!(
801 output.contains("my_crate::MyError::Parsing { .. } => ParsingError::new_err(msg),"),
802 "Struct variants must use {{ .. }} pattern, got:\n{output}"
803 );
804 assert!(!output.contains("Parsing(..)"));
806 }
807
808 #[test]
813 fn test_gen_napi_error_types() {
814 let error = sample_error();
815 let output = gen_napi_error_types(&error);
816 assert!(output.contains("CONVERSION_ERROR_ERROR_PARSE_ERROR"));
817 assert!(output.contains("CONVERSION_ERROR_ERROR_IO_ERROR"));
818 assert!(output.contains("CONVERSION_ERROR_ERROR_OTHER"));
819 }
820
821 #[test]
822 fn test_gen_napi_error_converter() {
823 let error = sample_error();
824 let output = gen_napi_error_converter(&error, "html_to_markdown_rs");
825 assert!(
826 output
827 .contains("fn conversion_error_to_napi_err(e: html_to_markdown_rs::ConversionError) -> napi::Error {")
828 );
829 assert!(output.contains("napi::Error::new(napi::Status::GenericFailure,"));
830 assert!(output.contains("[ParseError]"));
831 assert!(output.contains("[IoError]"));
832 assert!(output.contains("#[allow(dead_code)]"));
833 }
834
835 #[test]
836 fn test_napi_unit_variant() {
837 let error = ErrorDef {
838 name: "MyError".to_string(),
839 rust_path: "my_crate::MyError".to_string(),
840 variants: vec![ErrorVariant {
841 name: "NotFound".to_string(),
842 message_template: None,
843 fields: vec![],
844 has_source: false,
845 has_from: false,
846 is_unit: true,
847 doc: String::new(),
848 }],
849 doc: String::new(),
850 };
851 let output = gen_napi_error_converter(&error, "my_crate");
852 assert!(output.contains("my_crate::MyError::NotFound =>"));
853 assert!(!output.contains("NotFound(..)"));
854 }
855
856 #[test]
861 fn test_gen_wasm_error_converter() {
862 let error = sample_error();
863 let output = gen_wasm_error_converter(&error, "html_to_markdown_rs");
864 assert!(output.contains(
866 "fn conversion_error_to_js_value(e: html_to_markdown_rs::ConversionError) -> wasm_bindgen::JsValue {"
867 ));
868 assert!(output.contains("js_sys::Object::new()"));
870 assert!(output.contains("js_sys::Reflect::set(&obj, &\"code\".into(), &code.into()).ok()"));
871 assert!(output.contains("js_sys::Reflect::set(&obj, &\"message\".into(), &message.into()).ok()"));
872 assert!(output.contains("obj.into()"));
873 assert!(
875 output
876 .contains("fn conversion_error_error_code(e: &html_to_markdown_rs::ConversionError) -> &'static str {")
877 );
878 assert!(output.contains("\"parse_error\""));
879 assert!(output.contains("\"io_error\""));
880 assert!(output.contains("\"other\""));
881 assert!(output.contains("#[allow(dead_code)]"));
882 }
883
884 #[test]
889 fn test_gen_php_error_converter() {
890 let error = sample_error();
891 let output = gen_php_error_converter(&error, "html_to_markdown_rs");
892 assert!(output.contains("fn conversion_error_to_php_err(e: html_to_markdown_rs::ConversionError) -> ext_php_rs::exception::PhpException {"));
893 assert!(output.contains("PhpException::default(format!(\"[ParseError] {}\", msg))"));
894 assert!(output.contains("#[allow(dead_code)]"));
895 }
896
897 #[test]
902 fn test_gen_magnus_error_converter() {
903 let error = sample_error();
904 let output = gen_magnus_error_converter(&error, "html_to_markdown_rs");
905 assert!(
906 output.contains(
907 "fn conversion_error_to_magnus_err(e: html_to_markdown_rs::ConversionError) -> magnus::Error {"
908 )
909 );
910 assert!(
911 output.contains(
912 "magnus::Error::new(unsafe { magnus::Ruby::get_unchecked() }.exception_runtime_error(), msg)"
913 )
914 );
915 assert!(output.contains("#[allow(dead_code)]"));
916 }
917
918 #[test]
923 fn test_gen_rustler_error_converter() {
924 let error = sample_error();
925 let output = gen_rustler_error_converter(&error, "html_to_markdown_rs");
926 assert!(
927 output.contains("fn conversion_error_to_rustler_err(e: html_to_markdown_rs::ConversionError) -> String {")
928 );
929 assert!(output.contains("e.to_string()"));
930 assert!(output.contains("#[allow(dead_code)]"));
931 }
932
933 #[test]
938 fn test_to_screaming_snake() {
939 assert_eq!(to_screaming_snake("ConversionError"), "CONVERSION_ERROR");
940 assert_eq!(to_screaming_snake("IoError"), "IO_ERROR");
941 assert_eq!(to_screaming_snake("Other"), "OTHER");
942 }
943
944 #[test]
949 fn test_gen_ffi_error_codes() {
950 let error = sample_error();
951 let output = gen_ffi_error_codes(&error);
952 assert!(output.contains("CONVERSION_ERROR_NONE = 0"));
953 assert!(output.contains("CONVERSION_ERROR_PARSE_ERROR = 1"));
954 assert!(output.contains("CONVERSION_ERROR_IO_ERROR = 2"));
955 assert!(output.contains("CONVERSION_ERROR_OTHER = 3"));
956 assert!(output.contains("conversion_error_t;"));
957 assert!(output.contains("conversion_error_error_message(conversion_error_t code)"));
958 }
959
960 #[test]
965 fn test_gen_go_error_types() {
966 let error = sample_error();
967 let output = gen_go_error_types(&error);
968 assert!(output.contains("ErrParseError = errors.New("));
969 assert!(output.contains("ErrIoError = errors.New("));
970 assert!(output.contains("ErrOther = errors.New("));
971 assert!(output.contains("type ConversionError struct {"));
972 assert!(output.contains("Code string"));
973 assert!(output.contains("func (e *ConversionError) Error() string"));
974 }
975
976 #[test]
981 fn test_gen_java_error_types() {
982 let error = sample_error();
983 let files = gen_java_error_types(&error, "dev.kreuzberg.test");
984 assert_eq!(files.len(), 4);
986 assert_eq!(files[0].0, "ConversionErrorException");
988 assert!(
989 files[0]
990 .1
991 .contains("public class ConversionErrorException extends Exception")
992 );
993 assert!(files[0].1.contains("package dev.kreuzberg.test;"));
994 assert_eq!(files[1].0, "ParseErrorException");
996 assert!(
997 files[1]
998 .1
999 .contains("public class ParseErrorException extends ConversionErrorException")
1000 );
1001 assert_eq!(files[2].0, "IoErrorException");
1002 assert_eq!(files[3].0, "OtherException");
1003 }
1004
1005 #[test]
1010 fn test_gen_csharp_error_types() {
1011 let error = sample_error();
1012 let files = gen_csharp_error_types(&error, "Kreuzberg.Test");
1013 assert_eq!(files.len(), 4);
1015 assert_eq!(files[0].0, "ConversionErrorException");
1017 assert!(files[0].1.contains("public class ConversionErrorException : Exception"));
1018 assert!(files[0].1.contains("namespace Kreuzberg.Test;"));
1019 assert_eq!(files[1].0, "ParseErrorException");
1021 assert!(
1022 files[1]
1023 .1
1024 .contains("public class ParseErrorException : ConversionErrorException")
1025 );
1026 assert_eq!(files[2].0, "IoErrorException");
1027 assert_eq!(files[3].0, "OtherException");
1028 }
1029
1030 #[test]
1035 fn test_python_exception_name_no_conflict() {
1036 assert_eq!(python_exception_name("ParseError", "ConversionError"), "ParseError");
1038 assert_eq!(python_exception_name("Other", "ConversionError"), "OtherError");
1040 }
1041
1042 #[test]
1043 fn test_python_exception_name_shadows_builtin() {
1044 assert_eq!(
1046 python_exception_name("Connection", "CrawlError"),
1047 "CrawlConnectionError"
1048 );
1049 assert_eq!(python_exception_name("Timeout", "CrawlError"), "CrawlTimeoutError");
1051 assert_eq!(
1053 python_exception_name("ConnectionError", "CrawlError"),
1054 "CrawlConnectionError"
1055 );
1056 }
1057
1058 #[test]
1059 fn test_python_exception_name_no_double_prefix() {
1060 assert_eq!(
1062 python_exception_name("CrawlConnectionError", "CrawlError"),
1063 "CrawlConnectionError"
1064 );
1065 }
1066}