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(
334 " magnus::Error::new(unsafe { magnus::Ruby::get_unchecked() }.exception_runtime_error(), msg)".to_string(),
335 );
336 lines.push("}".to_string());
337 lines.join("\n")
338}
339
340pub fn magnus_converter_fn_name(error: &ErrorDef) -> String {
342 format!("{}_to_magnus_err", to_snake_case(&error.name))
343}
344
345pub fn gen_rustler_error_converter(error: &ErrorDef, core_import: &str) -> String {
351 let rust_path = if error.rust_path.is_empty() {
352 format!("{core_import}::{}", error.name)
353 } else {
354 error.rust_path.replace('-', "_")
355 };
356
357 let fn_name = format!("{}_to_rustler_err", to_snake_case(&error.name));
358
359 let mut lines = Vec::new();
360 lines.push(format!("/// Convert a `{rust_path}` error to a Rustler error string."));
361 lines.push("#[allow(dead_code)]".to_string());
362 lines.push(format!("fn {fn_name}(e: {rust_path}) -> String {{"));
363 lines.push(" e.to_string()".to_string());
364 lines.push("}".to_string());
365 lines.join("\n")
366}
367
368pub fn rustler_converter_fn_name(error: &ErrorDef) -> String {
370 format!("{}_to_rustler_err", to_snake_case(&error.name))
371}
372
373pub fn gen_ffi_error_codes(error: &ErrorDef) -> String {
382 let prefix = to_screaming_snake(&error.name);
383 let prefix_lower = to_snake_case(&error.name);
384
385 let mut lines = Vec::new();
386 lines.push(format!("/// Error codes for `{}`.", error.name));
387 lines.push("typedef enum {".to_string());
388 lines.push(format!(" {}_NONE = 0,", prefix));
389
390 for (i, variant) in error.variants.iter().enumerate() {
391 let variant_screaming = to_screaming_snake(&variant.name);
392 lines.push(format!(" {}_{} = {},", prefix, variant_screaming, i + 1));
393 }
394
395 lines.push(format!("}} {}_t;\n", prefix_lower));
396
397 lines.push(format!(
399 "/// Return a static string describing the error code.\nconst char* {}_error_message({}_t code);",
400 prefix_lower, prefix_lower
401 ));
402
403 lines.join("\n")
404}
405
406pub fn gen_go_error_types(error: &ErrorDef) -> String {
412 let mut lines = Vec::new();
413
414 lines.push("var (".to_string());
416 for variant in &error.variants {
417 let err_name = format!("Err{}", variant.name);
418 let msg = variant_display_message(variant);
419 lines.push(format!(" {} = errors.New(\"{}\")", err_name, msg));
420 }
421 lines.push(")\n".to_string());
422
423 lines.push(format!("// {} is a structured error type.", error.name));
425 lines.push(format!("type {} struct {{", error.name));
426 lines.push(" Code string".to_string());
427 lines.push(" Message string".to_string());
428 lines.push("}\n".to_string());
429
430 lines.push(format!(
431 "func (e *{}) Error() string {{ return e.Message }}",
432 error.name
433 ));
434
435 lines.join("\n")
436}
437
438pub fn gen_java_error_types(error: &ErrorDef, package: &str) -> Vec<(String, String)> {
448 let mut files = Vec::with_capacity(error.variants.len() + 1);
449
450 let base_name = format!("{}Exception", error.name);
452 let mut base = String::with_capacity(512);
453 base.push_str(&format!(
454 "// DO NOT EDIT - auto-generated by alef\npackage {};\n\n",
455 package
456 ));
457 if !error.doc.is_empty() {
458 base.push_str(&format!("/** {} */\n", error.doc));
459 }
460 base.push_str(&format!("public class {} extends Exception {{\n", base_name));
461 base.push_str(&format!(
462 " public {}(String message) {{\n super(message);\n }}\n\n",
463 base_name
464 ));
465 base.push_str(&format!(
466 " public {}(String message, Throwable cause) {{\n super(message, cause);\n }}\n",
467 base_name
468 ));
469 base.push_str("}\n");
470 files.push((base_name.clone(), base));
471
472 for variant in &error.variants {
474 let class_name = format!("{}Exception", variant.name);
475 let mut content = String::with_capacity(512);
476 content.push_str(&format!(
477 "// DO NOT EDIT - auto-generated by alef\npackage {};\n\n",
478 package
479 ));
480 if !variant.doc.is_empty() {
481 content.push_str(&format!("/** {} */\n", variant.doc));
482 }
483 content.push_str(&format!("public class {} extends {} {{\n", class_name, base_name));
484 content.push_str(&format!(
485 " public {}(String message) {{\n super(message);\n }}\n\n",
486 class_name
487 ));
488 content.push_str(&format!(
489 " public {}(String message, Throwable cause) {{\n super(message, cause);\n }}\n",
490 class_name
491 ));
492 content.push_str("}\n");
493 files.push((class_name, content));
494 }
495
496 files
497}
498
499pub fn gen_csharp_error_types(error: &ErrorDef, namespace: &str) -> Vec<(String, String)> {
509 let mut files = Vec::with_capacity(error.variants.len() + 1);
510
511 let base_name = format!("{}Exception", error.name);
512
513 {
515 let mut out = String::with_capacity(512);
516 out.push_str("// This file is auto-generated by alef. DO NOT EDIT.\nusing System;\n\n");
517 out.push_str(&format!("namespace {};\n\n", namespace));
518 if !error.doc.is_empty() {
519 out.push_str("/// <summary>\n");
520 for line in error.doc.lines() {
521 out.push_str(&format!("/// {}\n", line));
522 }
523 out.push_str("/// </summary>\n");
524 }
525 out.push_str(&format!("public class {} : Exception\n{{\n", base_name));
526 out.push_str(&format!(
527 " public {}(string message) : base(message) {{ }}\n\n",
528 base_name
529 ));
530 out.push_str(&format!(
531 " public {}(string message, Exception innerException) : base(message, innerException) {{ }}\n",
532 base_name
533 ));
534 out.push_str("}\n");
535 files.push((base_name.clone(), out));
536 }
537
538 for variant in &error.variants {
540 let class_name = format!("{}Exception", variant.name);
541 let mut out = String::with_capacity(512);
542 out.push_str("// This file is auto-generated by alef. DO NOT EDIT.\nusing System;\n\n");
543 out.push_str(&format!("namespace {};\n\n", namespace));
544 if !variant.doc.is_empty() {
545 out.push_str("/// <summary>\n");
546 for line in variant.doc.lines() {
547 out.push_str(&format!("/// {}\n", line));
548 }
549 out.push_str("/// </summary>\n");
550 }
551 out.push_str(&format!("public class {} : {}\n{{\n", class_name, base_name));
552 out.push_str(&format!(
553 " public {}(string message) : base(message) {{ }}\n\n",
554 class_name
555 ));
556 out.push_str(&format!(
557 " public {}(string message, Exception innerException) : base(message, innerException) {{ }}\n",
558 class_name
559 ));
560 out.push_str("}\n");
561 files.push((class_name, out));
562 }
563
564 files
565}
566
567fn to_screaming_snake(s: &str) -> String {
573 let mut result = String::with_capacity(s.len() + 4);
574 for (i, c) in s.chars().enumerate() {
575 if c.is_uppercase() {
576 if i > 0 {
577 result.push('_');
578 }
579 result.push(c.to_ascii_uppercase());
580 } else {
581 result.push(c.to_ascii_uppercase());
582 }
583 }
584 result
585}
586
587fn variant_display_message(variant: &ErrorVariant) -> String {
592 if let Some(tmpl) = &variant.message_template {
593 let msg = tmpl
595 .replace("{0}", "")
596 .replace("{source}", "")
597 .trim_end_matches(": ")
598 .trim()
599 .to_string();
600 if msg.is_empty() {
601 to_snake_case(&variant.name).replace('_', " ")
602 } else {
603 msg
604 }
605 } else {
606 to_snake_case(&variant.name).replace('_', " ")
607 }
608}
609
610#[cfg(test)]
611mod tests {
612 use super::*;
613 use alef_core::ir::{ErrorDef, ErrorVariant};
614
615 use alef_core::ir::{CoreWrapper, FieldDef, TypeRef};
616
617 fn tuple_field(index: usize) -> FieldDef {
619 FieldDef {
620 name: format!("_{index}"),
621 ty: TypeRef::String,
622 optional: false,
623 default: None,
624 doc: String::new(),
625 sanitized: false,
626 is_boxed: false,
627 type_rust_path: None,
628 cfg: None,
629 typed_default: None,
630 core_wrapper: CoreWrapper::None,
631 vec_inner_core_wrapper: CoreWrapper::None,
632 newtype_wrapper: None,
633 }
634 }
635
636 fn named_field(name: &str) -> FieldDef {
638 FieldDef {
639 name: name.to_string(),
640 ty: TypeRef::String,
641 optional: false,
642 default: None,
643 doc: String::new(),
644 sanitized: false,
645 is_boxed: false,
646 type_rust_path: None,
647 cfg: None,
648 typed_default: None,
649 core_wrapper: CoreWrapper::None,
650 vec_inner_core_wrapper: CoreWrapper::None,
651 newtype_wrapper: None,
652 }
653 }
654
655 fn sample_error() -> ErrorDef {
656 ErrorDef {
657 name: "ConversionError".to_string(),
658 rust_path: "html_to_markdown_rs::ConversionError".to_string(),
659 variants: vec![
660 ErrorVariant {
661 name: "ParseError".to_string(),
662 message_template: Some("HTML parsing error: {0}".to_string()),
663 fields: vec![tuple_field(0)],
664 has_source: false,
665 has_from: false,
666 is_unit: false,
667 doc: String::new(),
668 },
669 ErrorVariant {
670 name: "IoError".to_string(),
671 message_template: Some("I/O error: {0}".to_string()),
672 fields: vec![tuple_field(0)],
673 has_source: false,
674 has_from: true,
675 is_unit: false,
676 doc: String::new(),
677 },
678 ErrorVariant {
679 name: "Other".to_string(),
680 message_template: Some("Conversion error: {0}".to_string()),
681 fields: vec![tuple_field(0)],
682 has_source: false,
683 has_from: false,
684 is_unit: false,
685 doc: String::new(),
686 },
687 ],
688 doc: "Error type for conversion operations.".to_string(),
689 }
690 }
691
692 #[test]
693 fn test_gen_error_types() {
694 let error = sample_error();
695 let output = gen_pyo3_error_types(&error, "_module");
696 assert!(output.contains("pyo3::create_exception!(_module, ParseError, pyo3::exceptions::PyException);"));
697 assert!(output.contains("pyo3::create_exception!(_module, IoError, pyo3::exceptions::PyException);"));
698 assert!(output.contains("pyo3::create_exception!(_module, OtherError, pyo3::exceptions::PyException);"));
699 assert!(output.contains("pyo3::create_exception!(_module, ConversionError, pyo3::exceptions::PyException);"));
700 }
701
702 #[test]
703 fn test_gen_error_converter() {
704 let error = sample_error();
705 let output = gen_pyo3_error_converter(&error, "html_to_markdown_rs");
706 assert!(
707 output.contains("fn conversion_error_to_py_err(e: html_to_markdown_rs::ConversionError) -> pyo3::PyErr {")
708 );
709 assert!(output.contains("html_to_markdown_rs::ConversionError::ParseError(..) => ParseError::new_err(msg),"));
710 assert!(output.contains("html_to_markdown_rs::ConversionError::IoError(..) => IoError::new_err(msg),"));
711 }
712
713 #[test]
714 fn test_gen_error_registration() {
715 let error = sample_error();
716 let regs = gen_pyo3_error_registration(&error);
717 assert_eq!(regs.len(), 4); assert!(regs[0].contains("\"ParseError\""));
719 assert!(regs[3].contains("\"ConversionError\""));
720 }
721
722 #[test]
723 fn test_unit_variant_pattern() {
724 let error = ErrorDef {
725 name: "MyError".to_string(),
726 rust_path: "my_crate::MyError".to_string(),
727 variants: vec![ErrorVariant {
728 name: "NotFound".to_string(),
729 message_template: Some("not found".to_string()),
730 fields: vec![],
731 has_source: false,
732 has_from: false,
733 is_unit: true,
734 doc: String::new(),
735 }],
736 doc: String::new(),
737 };
738 let output = gen_pyo3_error_converter(&error, "my_crate");
739 assert!(output.contains("my_crate::MyError::NotFound => NotFoundError::new_err(msg),"));
740 assert!(!output.contains("NotFound(..)"));
742 }
743
744 #[test]
745 fn test_struct_variant_pattern() {
746 let error = ErrorDef {
747 name: "MyError".to_string(),
748 rust_path: "my_crate::MyError".to_string(),
749 variants: vec![ErrorVariant {
750 name: "Parsing".to_string(),
751 message_template: Some("parsing error: {message}".to_string()),
752 fields: vec![named_field("message")],
753 has_source: false,
754 has_from: false,
755 is_unit: false,
756 doc: String::new(),
757 }],
758 doc: String::new(),
759 };
760 let output = gen_pyo3_error_converter(&error, "my_crate");
761 assert!(
762 output.contains("my_crate::MyError::Parsing { .. } => ParsingError::new_err(msg),"),
763 "Struct variants must use {{ .. }} pattern, got:\n{output}"
764 );
765 assert!(!output.contains("Parsing(..)"));
767 }
768
769 #[test]
774 fn test_gen_napi_error_types() {
775 let error = sample_error();
776 let output = gen_napi_error_types(&error);
777 assert!(output.contains("CONVERSION_ERROR_ERROR_PARSE_ERROR"));
778 assert!(output.contains("CONVERSION_ERROR_ERROR_IO_ERROR"));
779 assert!(output.contains("CONVERSION_ERROR_ERROR_OTHER"));
780 }
781
782 #[test]
783 fn test_gen_napi_error_converter() {
784 let error = sample_error();
785 let output = gen_napi_error_converter(&error, "html_to_markdown_rs");
786 assert!(
787 output
788 .contains("fn conversion_error_to_napi_err(e: html_to_markdown_rs::ConversionError) -> napi::Error {")
789 );
790 assert!(output.contains("napi::Error::new(napi::Status::GenericFailure,"));
791 assert!(output.contains("[ParseError]"));
792 assert!(output.contains("[IoError]"));
793 assert!(output.contains("#[allow(dead_code)]"));
794 }
795
796 #[test]
797 fn test_napi_unit_variant() {
798 let error = ErrorDef {
799 name: "MyError".to_string(),
800 rust_path: "my_crate::MyError".to_string(),
801 variants: vec![ErrorVariant {
802 name: "NotFound".to_string(),
803 message_template: None,
804 fields: vec![],
805 has_source: false,
806 has_from: false,
807 is_unit: true,
808 doc: String::new(),
809 }],
810 doc: String::new(),
811 };
812 let output = gen_napi_error_converter(&error, "my_crate");
813 assert!(output.contains("my_crate::MyError::NotFound =>"));
814 assert!(!output.contains("NotFound(..)"));
815 }
816
817 #[test]
822 fn test_gen_wasm_error_converter() {
823 let error = sample_error();
824 let output = gen_wasm_error_converter(&error, "html_to_markdown_rs");
825 assert!(output.contains(
826 "fn conversion_error_to_js_value(e: html_to_markdown_rs::ConversionError) -> wasm_bindgen::JsValue {"
827 ));
828 assert!(output.contains("JsValue::from_str(&e.to_string())"));
829 assert!(output.contains("#[allow(dead_code)]"));
830 }
831
832 #[test]
837 fn test_gen_php_error_converter() {
838 let error = sample_error();
839 let output = gen_php_error_converter(&error, "html_to_markdown_rs");
840 assert!(output.contains("fn conversion_error_to_php_err(e: html_to_markdown_rs::ConversionError) -> ext_php_rs::exception::PhpException {"));
841 assert!(output.contains("PhpException::default(format!(\"[ParseError] {}\", msg))"));
842 assert!(output.contains("#[allow(dead_code)]"));
843 }
844
845 #[test]
850 fn test_gen_magnus_error_converter() {
851 let error = sample_error();
852 let output = gen_magnus_error_converter(&error, "html_to_markdown_rs");
853 assert!(
854 output.contains(
855 "fn conversion_error_to_magnus_err(e: html_to_markdown_rs::ConversionError) -> magnus::Error {"
856 )
857 );
858 assert!(
859 output.contains(
860 "magnus::Error::new(unsafe { magnus::Ruby::get_unchecked() }.exception_runtime_error(), msg)"
861 )
862 );
863 assert!(output.contains("#[allow(dead_code)]"));
864 }
865
866 #[test]
871 fn test_gen_rustler_error_converter() {
872 let error = sample_error();
873 let output = gen_rustler_error_converter(&error, "html_to_markdown_rs");
874 assert!(
875 output.contains("fn conversion_error_to_rustler_err(e: html_to_markdown_rs::ConversionError) -> String {")
876 );
877 assert!(output.contains("e.to_string()"));
878 assert!(output.contains("#[allow(dead_code)]"));
879 }
880
881 #[test]
886 fn test_to_screaming_snake() {
887 assert_eq!(to_screaming_snake("ConversionError"), "CONVERSION_ERROR");
888 assert_eq!(to_screaming_snake("IoError"), "IO_ERROR");
889 assert_eq!(to_screaming_snake("Other"), "OTHER");
890 }
891
892 #[test]
897 fn test_gen_ffi_error_codes() {
898 let error = sample_error();
899 let output = gen_ffi_error_codes(&error);
900 assert!(output.contains("CONVERSION_ERROR_NONE = 0"));
901 assert!(output.contains("CONVERSION_ERROR_PARSE_ERROR = 1"));
902 assert!(output.contains("CONVERSION_ERROR_IO_ERROR = 2"));
903 assert!(output.contains("CONVERSION_ERROR_OTHER = 3"));
904 assert!(output.contains("conversion_error_t;"));
905 assert!(output.contains("conversion_error_error_message(conversion_error_t code)"));
906 }
907
908 #[test]
913 fn test_gen_go_error_types() {
914 let error = sample_error();
915 let output = gen_go_error_types(&error);
916 assert!(output.contains("ErrParseError = errors.New("));
917 assert!(output.contains("ErrIoError = errors.New("));
918 assert!(output.contains("ErrOther = errors.New("));
919 assert!(output.contains("type ConversionError struct {"));
920 assert!(output.contains("Code string"));
921 assert!(output.contains("func (e *ConversionError) Error() string"));
922 }
923
924 #[test]
929 fn test_gen_java_error_types() {
930 let error = sample_error();
931 let files = gen_java_error_types(&error, "dev.kreuzberg.test");
932 assert_eq!(files.len(), 4);
934 assert_eq!(files[0].0, "ConversionErrorException");
936 assert!(
937 files[0]
938 .1
939 .contains("public class ConversionErrorException extends Exception")
940 );
941 assert!(files[0].1.contains("package dev.kreuzberg.test;"));
942 assert_eq!(files[1].0, "ParseErrorException");
944 assert!(
945 files[1]
946 .1
947 .contains("public class ParseErrorException extends ConversionErrorException")
948 );
949 assert_eq!(files[2].0, "IoErrorException");
950 assert_eq!(files[3].0, "OtherException");
951 }
952
953 #[test]
958 fn test_gen_csharp_error_types() {
959 let error = sample_error();
960 let files = gen_csharp_error_types(&error, "Kreuzberg.Test");
961 assert_eq!(files.len(), 4);
963 assert_eq!(files[0].0, "ConversionErrorException");
965 assert!(files[0].1.contains("public class ConversionErrorException : Exception"));
966 assert!(files[0].1.contains("namespace Kreuzberg.Test;"));
967 assert_eq!(files[1].0, "ParseErrorException");
969 assert!(
970 files[1]
971 .1
972 .contains("public class ParseErrorException : ConversionErrorException")
973 );
974 assert_eq!(files[2].0, "IoErrorException");
975 assert_eq!(files[3].0, "OtherException");
976 }
977
978 #[test]
983 fn test_python_exception_name_no_conflict() {
984 assert_eq!(python_exception_name("ParseError", "ConversionError"), "ParseError");
986 assert_eq!(python_exception_name("Other", "ConversionError"), "OtherError");
988 }
989
990 #[test]
991 fn test_python_exception_name_shadows_builtin() {
992 assert_eq!(
994 python_exception_name("Connection", "CrawlError"),
995 "CrawlConnectionError"
996 );
997 assert_eq!(python_exception_name("Timeout", "CrawlError"), "CrawlTimeoutError");
999 assert_eq!(
1001 python_exception_name("ConnectionError", "CrawlError"),
1002 "CrawlConnectionError"
1003 );
1004 }
1005
1006 #[test]
1007 fn test_python_exception_name_no_double_prefix() {
1008 assert_eq!(
1010 python_exception_name("CrawlConnectionError", "CrawlError"),
1011 "CrawlConnectionError"
1012 );
1013 }
1014}