1use alef_core::ir::{ErrorDef, ErrorVariant};
2
3use crate::conversions::is_tuple_variant;
4
5fn error_variant_wildcard_pattern(rust_path: &str, variant: &ErrorVariant) -> String {
8 if variant.is_unit {
9 format!("{rust_path}::{}", variant.name)
10 } else if is_tuple_variant(&variant.fields) {
11 format!("{rust_path}::{}(..)", variant.name)
12 } else {
13 format!("{rust_path}::{} {{ .. }}", variant.name)
14 }
15}
16
17const PYTHON_BUILTIN_EXCEPTIONS: &[&str] = &[
19 "ConnectionError",
20 "TimeoutError",
21 "PermissionError",
22 "FileNotFoundError",
23 "ValueError",
24 "TypeError",
25 "RuntimeError",
26 "OSError",
27 "IOError",
28 "KeyError",
29 "IndexError",
30 "AttributeError",
31 "ImportError",
32 "MemoryError",
33 "OverflowError",
34 "StopIteration",
35 "RecursionError",
36 "SystemError",
37 "ReferenceError",
38 "BufferError",
39 "EOFError",
40 "LookupError",
41 "ArithmeticError",
42 "AssertionError",
43 "BlockingIOError",
44 "BrokenPipeError",
45 "ChildProcessError",
46 "FileExistsError",
47 "InterruptedError",
48 "IsADirectoryError",
49 "NotADirectoryError",
50 "ProcessLookupError",
51 "UnicodeError",
52];
53
54fn error_base_prefix(error_name: &str) -> &str {
57 error_name.strip_suffix("Error").unwrap_or(error_name)
58}
59
60pub fn python_exception_name(variant_name: &str, error_name: &str) -> String {
66 let candidate = if variant_name.ends_with("Error") {
67 variant_name.to_string()
68 } else {
69 format!("{}Error", variant_name)
70 };
71
72 if PYTHON_BUILTIN_EXCEPTIONS.contains(&candidate.as_str()) {
73 let prefix = error_base_prefix(error_name);
74 if candidate.starts_with(prefix) {
76 candidate
77 } else {
78 format!("{}{}", prefix, candidate)
79 }
80 } else {
81 candidate
82 }
83}
84
85pub fn gen_pyo3_error_types(error: &ErrorDef, module_name: &str) -> String {
89 let mut lines = Vec::with_capacity(error.variants.len() + 2);
90 lines.push("// Error types".to_string());
91
92 for variant in &error.variants {
94 let variant_name = python_exception_name(&variant.name, &error.name);
95 lines.push(format!(
96 "pyo3::create_exception!({module_name}, {}, pyo3::exceptions::PyException);",
97 variant_name
98 ));
99 }
100
101 lines.push(format!(
103 "pyo3::create_exception!({module_name}, {}, pyo3::exceptions::PyException);",
104 error.name
105 ));
106
107 lines.join("\n")
108}
109
110pub fn gen_pyo3_error_converter(error: &ErrorDef, core_import: &str) -> String {
113 let rust_path = if error.rust_path.is_empty() {
114 format!("{core_import}::{}", error.name)
115 } else {
116 error.rust_path.replace('-', "_")
117 };
118
119 let fn_name = format!("{}_to_py_err", to_snake_case(&error.name));
120
121 let mut lines = Vec::new();
122 lines.push(format!("/// Convert a `{rust_path}` error to a Python exception."));
123 lines.push(format!("fn {fn_name}(e: {rust_path}) -> pyo3::PyErr {{"));
124 lines.push(" let msg = e.to_string();".to_string());
125 lines.push(" #[allow(unreachable_patterns)]".to_string());
126 lines.push(" match &e {".to_string());
127
128 for variant in &error.variants {
129 let pattern = error_variant_wildcard_pattern(&rust_path, variant);
130 let variant_exc_name = python_exception_name(&variant.name, &error.name);
131 lines.push(format!(" {pattern} => {}::new_err(msg),", variant_exc_name));
132 }
133
134 lines.push(format!(" _ => {}::new_err(msg),", error.name));
136 lines.push(" }".to_string());
137 lines.push("}".to_string());
138 lines.join("\n")
139}
140
141pub fn gen_pyo3_error_registration(error: &ErrorDef) -> Vec<String> {
145 let mut registrations = Vec::with_capacity(error.variants.len() + 1);
146
147 for variant in &error.variants {
148 let variant_exc_name = python_exception_name(&variant.name, &error.name);
149 registrations.push(format!(
150 " m.add(\"{}\", m.py().get_type::<{}>())?;",
151 variant_exc_name, variant_exc_name
152 ));
153 }
154
155 registrations.push(format!(
157 " m.add(\"{}\", m.py().get_type::<{}>())?;",
158 error.name, error.name
159 ));
160
161 registrations
162}
163
164pub fn converter_fn_name(error: &ErrorDef) -> String {
166 format!("{}_to_py_err", to_snake_case(&error.name))
167}
168
169fn to_snake_case(s: &str) -> String {
171 let mut result = String::with_capacity(s.len() + 4);
172 for (i, c) in s.chars().enumerate() {
173 if c.is_uppercase() {
174 if i > 0 {
175 result.push('_');
176 }
177 result.push(c.to_ascii_lowercase());
178 } else {
179 result.push(c);
180 }
181 }
182 result
183}
184
185pub fn gen_napi_error_types(error: &ErrorDef) -> String {
191 let mut lines = Vec::with_capacity(error.variants.len() + 4);
192 lines.push("// Error variant name constants".to_string());
193 for variant in &error.variants {
194 lines.push(format!(
195 "pub const {}_ERROR_{}: &str = \"{}\";",
196 to_screaming_snake(&error.name),
197 to_screaming_snake(&variant.name),
198 variant.name,
199 ));
200 }
201 lines.join("\n")
202}
203
204pub fn gen_napi_error_converter(error: &ErrorDef, core_import: &str) -> String {
206 let rust_path = if error.rust_path.is_empty() {
207 format!("{core_import}::{}", error.name)
208 } else {
209 error.rust_path.replace('-', "_")
210 };
211
212 let fn_name = format!("{}_to_napi_err", to_snake_case(&error.name));
213
214 let mut lines = Vec::new();
215 lines.push(format!("/// Convert a `{rust_path}` error to a NAPI error."));
216 lines.push("#[allow(dead_code)]".to_string());
217 lines.push(format!("fn {fn_name}(e: {rust_path}) -> napi::Error {{"));
218 lines.push(" let msg = e.to_string();".to_string());
219 lines.push(" #[allow(unreachable_patterns)]".to_string());
220 lines.push(" match &e {".to_string());
221
222 for variant in &error.variants {
223 let pattern = error_variant_wildcard_pattern(&rust_path, variant);
224 lines.push(format!(
225 " {pattern} => napi::Error::new(napi::Status::GenericFailure, format!(\"[{}] {{}}\", msg)),",
226 variant.name,
227 ));
228 }
229
230 lines.push(" _ => napi::Error::new(napi::Status::GenericFailure, msg),".to_string());
232 lines.push(" }".to_string());
233 lines.push("}".to_string());
234 lines.join("\n")
235}
236
237pub fn napi_converter_fn_name(error: &ErrorDef) -> String {
239 format!("{}_to_napi_err", to_snake_case(&error.name))
240}
241
242pub fn gen_wasm_error_converter(error: &ErrorDef, core_import: &str) -> String {
248 let rust_path = if error.rust_path.is_empty() {
249 format!("{core_import}::{}", error.name)
250 } else {
251 error.rust_path.replace('-', "_")
252 };
253
254 let fn_name = format!("{}_to_js_value", to_snake_case(&error.name));
255
256 let mut lines = Vec::new();
257 lines.push(format!("/// Convert a `{rust_path}` error to a `JsValue` string."));
258 lines.push("#[allow(dead_code)]".to_string());
259 lines.push(format!("fn {fn_name}(e: {rust_path}) -> wasm_bindgen::JsValue {{"));
260 lines.push(" wasm_bindgen::JsValue::from_str(&e.to_string())".to_string());
261 lines.push("}".to_string());
262 lines.join("\n")
263}
264
265pub fn wasm_converter_fn_name(error: &ErrorDef) -> String {
267 format!("{}_to_js_value", to_snake_case(&error.name))
268}
269
270pub fn gen_php_error_converter(error: &ErrorDef, core_import: &str) -> String {
276 let rust_path = if error.rust_path.is_empty() {
277 format!("{core_import}::{}", error.name)
278 } else {
279 error.rust_path.replace('-', "_")
280 };
281
282 let fn_name = format!("{}_to_php_err", to_snake_case(&error.name));
283
284 let mut lines = Vec::new();
285 lines.push(format!("/// Convert a `{rust_path}` error to a PHP exception."));
286 lines.push("#[allow(dead_code)]".to_string());
287 lines.push(format!(
288 "fn {fn_name}(e: {rust_path}) -> ext_php_rs::exception::PhpException {{"
289 ));
290 lines.push(" let msg = e.to_string();".to_string());
291 lines.push(" #[allow(unreachable_patterns)]".to_string());
292 lines.push(" match &e {".to_string());
293
294 for variant in &error.variants {
295 let pattern = error_variant_wildcard_pattern(&rust_path, variant);
296 lines.push(format!(
297 " {pattern} => ext_php_rs::exception::PhpException::default(format!(\"[{}] {{}}\", msg)),",
298 variant.name,
299 ));
300 }
301
302 lines.push(" _ => ext_php_rs::exception::PhpException::default(msg),".to_string());
304 lines.push(" }".to_string());
305 lines.push("}".to_string());
306 lines.join("\n")
307}
308
309pub fn php_converter_fn_name(error: &ErrorDef) -> String {
311 format!("{}_to_php_err", to_snake_case(&error.name))
312}
313
314pub fn gen_magnus_error_converter(error: &ErrorDef, core_import: &str) -> String {
320 let rust_path = if error.rust_path.is_empty() {
321 format!("{core_import}::{}", error.name)
322 } else {
323 error.rust_path.replace('-', "_")
324 };
325
326 let fn_name = format!("{}_to_magnus_err", to_snake_case(&error.name));
327
328 let mut lines = Vec::new();
329 lines.push(format!("/// Convert a `{rust_path}` error to a Magnus runtime error."));
330 lines.push("#[allow(dead_code)]".to_string());
331 lines.push(format!("fn {fn_name}(e: {rust_path}) -> magnus::Error {{"));
332 lines.push(" let msg = e.to_string();".to_string());
333 lines.push(" magnus::Error::new(magnus::exception::runtime_error(), msg)".to_string());
334 lines.push("}".to_string());
335 lines.join("\n")
336}
337
338pub fn magnus_converter_fn_name(error: &ErrorDef) -> String {
340 format!("{}_to_magnus_err", to_snake_case(&error.name))
341}
342
343pub fn gen_rustler_error_converter(error: &ErrorDef, core_import: &str) -> String {
349 let rust_path = if error.rust_path.is_empty() {
350 format!("{core_import}::{}", error.name)
351 } else {
352 error.rust_path.replace('-', "_")
353 };
354
355 let fn_name = format!("{}_to_rustler_err", to_snake_case(&error.name));
356
357 let mut lines = Vec::new();
358 lines.push(format!("/// Convert a `{rust_path}` error to a Rustler error string."));
359 lines.push("#[allow(dead_code)]".to_string());
360 lines.push(format!("fn {fn_name}(e: {rust_path}) -> String {{"));
361 lines.push(" e.to_string()".to_string());
362 lines.push("}".to_string());
363 lines.join("\n")
364}
365
366pub fn rustler_converter_fn_name(error: &ErrorDef) -> String {
368 format!("{}_to_rustler_err", to_snake_case(&error.name))
369}
370
371pub fn gen_ffi_error_codes(error: &ErrorDef) -> String {
380 let prefix = to_screaming_snake(&error.name);
381 let prefix_lower = to_snake_case(&error.name);
382
383 let mut lines = Vec::new();
384 lines.push(format!("/// Error codes for `{}`.", error.name));
385 lines.push("typedef enum {".to_string());
386 lines.push(format!(" {}_NONE = 0,", prefix));
387
388 for (i, variant) in error.variants.iter().enumerate() {
389 let variant_screaming = to_screaming_snake(&variant.name);
390 lines.push(format!(" {}_{} = {},", prefix, variant_screaming, i + 1));
391 }
392
393 lines.push(format!("}} {}_t;\n", prefix_lower));
394
395 lines.push(format!(
397 "/// Return a static string describing the error code.\nconst char* {}_error_message({}_t code);",
398 prefix_lower, prefix_lower
399 ));
400
401 lines.join("\n")
402}
403
404pub fn gen_go_error_types(error: &ErrorDef) -> String {
410 let mut lines = Vec::new();
411
412 lines.push("var (".to_string());
414 for variant in &error.variants {
415 let err_name = format!("Err{}", variant.name);
416 let msg = variant_display_message(variant);
417 lines.push(format!(" {} = errors.New(\"{}\")", err_name, msg));
418 }
419 lines.push(")\n".to_string());
420
421 lines.push(format!("// {} is a structured error type.", error.name));
423 lines.push(format!("type {} struct {{", error.name));
424 lines.push(" Code string".to_string());
425 lines.push(" Message string".to_string());
426 lines.push("}\n".to_string());
427
428 lines.push(format!(
429 "func (e *{}) Error() string {{ return e.Message }}",
430 error.name
431 ));
432
433 lines.join("\n")
434}
435
436pub fn gen_java_error_types(error: &ErrorDef, package: &str) -> Vec<(String, String)> {
446 let mut files = Vec::with_capacity(error.variants.len() + 1);
447
448 let base_name = format!("{}Exception", error.name);
450 let mut base = String::with_capacity(512);
451 base.push_str(&format!(
452 "// DO NOT EDIT - auto-generated by alef\npackage {};\n\n",
453 package
454 ));
455 if !error.doc.is_empty() {
456 base.push_str(&format!("/** {} */\n", error.doc));
457 }
458 base.push_str(&format!("public class {} extends Exception {{\n", base_name));
459 base.push_str(&format!(
460 " public {}(String message) {{\n super(message);\n }}\n\n",
461 base_name
462 ));
463 base.push_str(&format!(
464 " public {}(String message, Throwable cause) {{\n super(message, cause);\n }}\n",
465 base_name
466 ));
467 base.push_str("}\n");
468 files.push((base_name.clone(), base));
469
470 for variant in &error.variants {
472 let class_name = format!("{}Exception", variant.name);
473 let mut content = String::with_capacity(512);
474 content.push_str(&format!(
475 "// DO NOT EDIT - auto-generated by alef\npackage {};\n\n",
476 package
477 ));
478 if !variant.doc.is_empty() {
479 content.push_str(&format!("/** {} */\n", variant.doc));
480 }
481 content.push_str(&format!("public class {} extends {} {{\n", class_name, base_name));
482 content.push_str(&format!(
483 " public {}(String message) {{\n super(message);\n }}\n\n",
484 class_name
485 ));
486 content.push_str(&format!(
487 " public {}(String message, Throwable cause) {{\n super(message, cause);\n }}\n",
488 class_name
489 ));
490 content.push_str("}\n");
491 files.push((class_name, content));
492 }
493
494 files
495}
496
497pub fn gen_csharp_error_types(error: &ErrorDef, namespace: &str) -> Vec<(String, String)> {
507 let mut files = Vec::with_capacity(error.variants.len() + 1);
508
509 let base_name = format!("{}Exception", error.name);
510
511 {
513 let mut out = String::with_capacity(512);
514 out.push_str("// This file is auto-generated by alef. DO NOT EDIT.\nusing System;\n\n");
515 out.push_str(&format!("namespace {};\n\n", namespace));
516 if !error.doc.is_empty() {
517 out.push_str("/// <summary>\n");
518 for line in error.doc.lines() {
519 out.push_str(&format!("/// {}\n", line));
520 }
521 out.push_str("/// </summary>\n");
522 }
523 out.push_str(&format!("public class {} : Exception\n{{\n", base_name));
524 out.push_str(&format!(
525 " public {}(string message) : base(message) {{ }}\n\n",
526 base_name
527 ));
528 out.push_str(&format!(
529 " public {}(string message, Exception innerException) : base(message, innerException) {{ }}\n",
530 base_name
531 ));
532 out.push_str("}\n");
533 files.push((base_name.clone(), out));
534 }
535
536 for variant in &error.variants {
538 let class_name = format!("{}Exception", variant.name);
539 let mut out = String::with_capacity(512);
540 out.push_str("// This file is auto-generated by alef. DO NOT EDIT.\nusing System;\n\n");
541 out.push_str(&format!("namespace {};\n\n", namespace));
542 if !variant.doc.is_empty() {
543 out.push_str("/// <summary>\n");
544 for line in variant.doc.lines() {
545 out.push_str(&format!("/// {}\n", line));
546 }
547 out.push_str("/// </summary>\n");
548 }
549 out.push_str(&format!("public class {} : {}\n{{\n", class_name, base_name));
550 out.push_str(&format!(
551 " public {}(string message) : base(message) {{ }}\n\n",
552 class_name
553 ));
554 out.push_str(&format!(
555 " public {}(string message, Exception innerException) : base(message, innerException) {{ }}\n",
556 class_name
557 ));
558 out.push_str("}\n");
559 files.push((class_name, out));
560 }
561
562 files
563}
564
565fn to_screaming_snake(s: &str) -> String {
571 let mut result = String::with_capacity(s.len() + 4);
572 for (i, c) in s.chars().enumerate() {
573 if c.is_uppercase() {
574 if i > 0 {
575 result.push('_');
576 }
577 result.push(c.to_ascii_uppercase());
578 } else {
579 result.push(c.to_ascii_uppercase());
580 }
581 }
582 result
583}
584
585fn variant_display_message(variant: &ErrorVariant) -> String {
590 if let Some(tmpl) = &variant.message_template {
591 let msg = tmpl
593 .replace("{0}", "")
594 .replace("{source}", "")
595 .trim_end_matches(": ")
596 .trim()
597 .to_string();
598 if msg.is_empty() {
599 to_snake_case(&variant.name).replace('_', " ")
600 } else {
601 msg
602 }
603 } else {
604 to_snake_case(&variant.name).replace('_', " ")
605 }
606}
607
608#[cfg(test)]
609mod tests {
610 use super::*;
611 use alef_core::ir::{ErrorDef, ErrorVariant};
612
613 use alef_core::ir::{CoreWrapper, FieldDef, TypeRef};
614
615 fn tuple_field(index: usize) -> FieldDef {
617 FieldDef {
618 name: format!("_{index}"),
619 ty: TypeRef::String,
620 optional: false,
621 default: None,
622 doc: String::new(),
623 sanitized: false,
624 is_boxed: false,
625 type_rust_path: None,
626 cfg: None,
627 typed_default: None,
628 core_wrapper: CoreWrapper::None,
629 vec_inner_core_wrapper: CoreWrapper::None,
630 newtype_wrapper: None,
631 }
632 }
633
634 fn named_field(name: &str) -> FieldDef {
636 FieldDef {
637 name: name.to_string(),
638 ty: TypeRef::String,
639 optional: false,
640 default: None,
641 doc: String::new(),
642 sanitized: false,
643 is_boxed: false,
644 type_rust_path: None,
645 cfg: None,
646 typed_default: None,
647 core_wrapper: CoreWrapper::None,
648 vec_inner_core_wrapper: CoreWrapper::None,
649 newtype_wrapper: None,
650 }
651 }
652
653 fn sample_error() -> ErrorDef {
654 ErrorDef {
655 name: "ConversionError".to_string(),
656 rust_path: "html_to_markdown_rs::ConversionError".to_string(),
657 variants: vec![
658 ErrorVariant {
659 name: "ParseError".to_string(),
660 message_template: Some("HTML parsing error: {0}".to_string()),
661 fields: vec![tuple_field(0)],
662 has_source: false,
663 has_from: false,
664 is_unit: false,
665 doc: String::new(),
666 },
667 ErrorVariant {
668 name: "IoError".to_string(),
669 message_template: Some("I/O error: {0}".to_string()),
670 fields: vec![tuple_field(0)],
671 has_source: false,
672 has_from: true,
673 is_unit: false,
674 doc: String::new(),
675 },
676 ErrorVariant {
677 name: "Other".to_string(),
678 message_template: Some("Conversion error: {0}".to_string()),
679 fields: vec![tuple_field(0)],
680 has_source: false,
681 has_from: false,
682 is_unit: false,
683 doc: String::new(),
684 },
685 ],
686 doc: "Error type for conversion operations.".to_string(),
687 }
688 }
689
690 #[test]
691 fn test_gen_error_types() {
692 let error = sample_error();
693 let output = gen_pyo3_error_types(&error, "_module");
694 assert!(output.contains("pyo3::create_exception!(_module, ParseError, pyo3::exceptions::PyException);"));
695 assert!(output.contains("pyo3::create_exception!(_module, IoError, pyo3::exceptions::PyException);"));
696 assert!(output.contains("pyo3::create_exception!(_module, OtherError, pyo3::exceptions::PyException);"));
697 assert!(output.contains("pyo3::create_exception!(_module, ConversionError, pyo3::exceptions::PyException);"));
698 }
699
700 #[test]
701 fn test_gen_error_converter() {
702 let error = sample_error();
703 let output = gen_pyo3_error_converter(&error, "html_to_markdown_rs");
704 assert!(
705 output.contains("fn conversion_error_to_py_err(e: html_to_markdown_rs::ConversionError) -> pyo3::PyErr {")
706 );
707 assert!(output.contains("html_to_markdown_rs::ConversionError::ParseError(..) => ParseError::new_err(msg),"));
708 assert!(output.contains("html_to_markdown_rs::ConversionError::IoError(..) => IoError::new_err(msg),"));
709 }
710
711 #[test]
712 fn test_gen_error_registration() {
713 let error = sample_error();
714 let regs = gen_pyo3_error_registration(&error);
715 assert_eq!(regs.len(), 4); assert!(regs[0].contains("\"ParseError\""));
717 assert!(regs[3].contains("\"ConversionError\""));
718 }
719
720 #[test]
721 fn test_unit_variant_pattern() {
722 let error = ErrorDef {
723 name: "MyError".to_string(),
724 rust_path: "my_crate::MyError".to_string(),
725 variants: vec![ErrorVariant {
726 name: "NotFound".to_string(),
727 message_template: Some("not found".to_string()),
728 fields: vec![],
729 has_source: false,
730 has_from: false,
731 is_unit: true,
732 doc: String::new(),
733 }],
734 doc: String::new(),
735 };
736 let output = gen_pyo3_error_converter(&error, "my_crate");
737 assert!(output.contains("my_crate::MyError::NotFound => NotFoundError::new_err(msg),"));
738 assert!(!output.contains("NotFound(..)"));
740 }
741
742 #[test]
743 fn test_struct_variant_pattern() {
744 let error = ErrorDef {
745 name: "MyError".to_string(),
746 rust_path: "my_crate::MyError".to_string(),
747 variants: vec![ErrorVariant {
748 name: "Parsing".to_string(),
749 message_template: Some("parsing error: {message}".to_string()),
750 fields: vec![named_field("message")],
751 has_source: false,
752 has_from: false,
753 is_unit: false,
754 doc: String::new(),
755 }],
756 doc: String::new(),
757 };
758 let output = gen_pyo3_error_converter(&error, "my_crate");
759 assert!(
760 output.contains("my_crate::MyError::Parsing { .. } => ParsingError::new_err(msg),"),
761 "Struct variants must use {{ .. }} pattern, got:\n{output}"
762 );
763 assert!(!output.contains("Parsing(..)"));
765 }
766
767 #[test]
772 fn test_gen_napi_error_types() {
773 let error = sample_error();
774 let output = gen_napi_error_types(&error);
775 assert!(output.contains("CONVERSION_ERROR_ERROR_PARSE_ERROR"));
776 assert!(output.contains("CONVERSION_ERROR_ERROR_IO_ERROR"));
777 assert!(output.contains("CONVERSION_ERROR_ERROR_OTHER"));
778 }
779
780 #[test]
781 fn test_gen_napi_error_converter() {
782 let error = sample_error();
783 let output = gen_napi_error_converter(&error, "html_to_markdown_rs");
784 assert!(
785 output
786 .contains("fn conversion_error_to_napi_err(e: html_to_markdown_rs::ConversionError) -> napi::Error {")
787 );
788 assert!(output.contains("napi::Error::new(napi::Status::GenericFailure,"));
789 assert!(output.contains("[ParseError]"));
790 assert!(output.contains("[IoError]"));
791 assert!(output.contains("#[allow(dead_code)]"));
792 }
793
794 #[test]
795 fn test_napi_unit_variant() {
796 let error = ErrorDef {
797 name: "MyError".to_string(),
798 rust_path: "my_crate::MyError".to_string(),
799 variants: vec![ErrorVariant {
800 name: "NotFound".to_string(),
801 message_template: None,
802 fields: vec![],
803 has_source: false,
804 has_from: false,
805 is_unit: true,
806 doc: String::new(),
807 }],
808 doc: String::new(),
809 };
810 let output = gen_napi_error_converter(&error, "my_crate");
811 assert!(output.contains("my_crate::MyError::NotFound =>"));
812 assert!(!output.contains("NotFound(..)"));
813 }
814
815 #[test]
820 fn test_gen_wasm_error_converter() {
821 let error = sample_error();
822 let output = gen_wasm_error_converter(&error, "html_to_markdown_rs");
823 assert!(output.contains(
824 "fn conversion_error_to_js_value(e: html_to_markdown_rs::ConversionError) -> wasm_bindgen::JsValue {"
825 ));
826 assert!(output.contains("JsValue::from_str(&e.to_string())"));
827 assert!(output.contains("#[allow(dead_code)]"));
828 }
829
830 #[test]
835 fn test_gen_php_error_converter() {
836 let error = sample_error();
837 let output = gen_php_error_converter(&error, "html_to_markdown_rs");
838 assert!(output.contains("fn conversion_error_to_php_err(e: html_to_markdown_rs::ConversionError) -> ext_php_rs::exception::PhpException {"));
839 assert!(output.contains("PhpException::default(format!(\"[ParseError] {}\", msg))"));
840 assert!(output.contains("#[allow(dead_code)]"));
841 }
842
843 #[test]
848 fn test_gen_magnus_error_converter() {
849 let error = sample_error();
850 let output = gen_magnus_error_converter(&error, "html_to_markdown_rs");
851 assert!(
852 output.contains(
853 "fn conversion_error_to_magnus_err(e: html_to_markdown_rs::ConversionError) -> magnus::Error {"
854 )
855 );
856 assert!(output.contains("magnus::Error::new(magnus::exception::runtime_error(), msg)"));
857 assert!(output.contains("#[allow(dead_code)]"));
858 }
859
860 #[test]
865 fn test_gen_rustler_error_converter() {
866 let error = sample_error();
867 let output = gen_rustler_error_converter(&error, "html_to_markdown_rs");
868 assert!(
869 output.contains("fn conversion_error_to_rustler_err(e: html_to_markdown_rs::ConversionError) -> String {")
870 );
871 assert!(output.contains("e.to_string()"));
872 assert!(output.contains("#[allow(dead_code)]"));
873 }
874
875 #[test]
880 fn test_to_screaming_snake() {
881 assert_eq!(to_screaming_snake("ConversionError"), "CONVERSION_ERROR");
882 assert_eq!(to_screaming_snake("IoError"), "IO_ERROR");
883 assert_eq!(to_screaming_snake("Other"), "OTHER");
884 }
885
886 #[test]
891 fn test_gen_ffi_error_codes() {
892 let error = sample_error();
893 let output = gen_ffi_error_codes(&error);
894 assert!(output.contains("CONVERSION_ERROR_NONE = 0"));
895 assert!(output.contains("CONVERSION_ERROR_PARSE_ERROR = 1"));
896 assert!(output.contains("CONVERSION_ERROR_IO_ERROR = 2"));
897 assert!(output.contains("CONVERSION_ERROR_OTHER = 3"));
898 assert!(output.contains("conversion_error_t;"));
899 assert!(output.contains("conversion_error_error_message(conversion_error_t code)"));
900 }
901
902 #[test]
907 fn test_gen_go_error_types() {
908 let error = sample_error();
909 let output = gen_go_error_types(&error);
910 assert!(output.contains("ErrParseError = errors.New("));
911 assert!(output.contains("ErrIoError = errors.New("));
912 assert!(output.contains("ErrOther = errors.New("));
913 assert!(output.contains("type ConversionError struct {"));
914 assert!(output.contains("Code string"));
915 assert!(output.contains("func (e *ConversionError) Error() string"));
916 }
917
918 #[test]
923 fn test_gen_java_error_types() {
924 let error = sample_error();
925 let files = gen_java_error_types(&error, "dev.kreuzberg.test");
926 assert_eq!(files.len(), 4);
928 assert_eq!(files[0].0, "ConversionErrorException");
930 assert!(
931 files[0]
932 .1
933 .contains("public class ConversionErrorException extends Exception")
934 );
935 assert!(files[0].1.contains("package dev.kreuzberg.test;"));
936 assert_eq!(files[1].0, "ParseErrorException");
938 assert!(
939 files[1]
940 .1
941 .contains("public class ParseErrorException extends ConversionErrorException")
942 );
943 assert_eq!(files[2].0, "IoErrorException");
944 assert_eq!(files[3].0, "OtherException");
945 }
946
947 #[test]
952 fn test_gen_csharp_error_types() {
953 let error = sample_error();
954 let files = gen_csharp_error_types(&error, "Kreuzberg.Test");
955 assert_eq!(files.len(), 4);
957 assert_eq!(files[0].0, "ConversionErrorException");
959 assert!(files[0].1.contains("public class ConversionErrorException : Exception"));
960 assert!(files[0].1.contains("namespace Kreuzberg.Test;"));
961 assert_eq!(files[1].0, "ParseErrorException");
963 assert!(
964 files[1]
965 .1
966 .contains("public class ParseErrorException : ConversionErrorException")
967 );
968 assert_eq!(files[2].0, "IoErrorException");
969 assert_eq!(files[3].0, "OtherException");
970 }
971
972 #[test]
977 fn test_python_exception_name_no_conflict() {
978 assert_eq!(python_exception_name("ParseError", "ConversionError"), "ParseError");
980 assert_eq!(python_exception_name("Other", "ConversionError"), "OtherError");
982 }
983
984 #[test]
985 fn test_python_exception_name_shadows_builtin() {
986 assert_eq!(
988 python_exception_name("Connection", "CrawlError"),
989 "CrawlConnectionError"
990 );
991 assert_eq!(python_exception_name("Timeout", "CrawlError"), "CrawlTimeoutError");
993 assert_eq!(
995 python_exception_name("ConnectionError", "CrawlError"),
996 "CrawlConnectionError"
997 );
998 }
999
1000 #[test]
1001 fn test_python_exception_name_no_double_prefix() {
1002 assert_eq!(
1004 python_exception_name("CrawlConnectionError", "CrawlError"),
1005 "CrawlConnectionError"
1006 );
1007 }
1008}