1use alef_core::ir::{ErrorDef, ErrorVariant};
2
3const PYTHON_BUILTIN_EXCEPTIONS: &[&str] = &[
5 "ConnectionError",
6 "TimeoutError",
7 "PermissionError",
8 "FileNotFoundError",
9 "ValueError",
10 "TypeError",
11 "RuntimeError",
12 "OSError",
13 "IOError",
14 "KeyError",
15 "IndexError",
16 "AttributeError",
17 "ImportError",
18 "MemoryError",
19 "OverflowError",
20 "StopIteration",
21 "RecursionError",
22 "SystemError",
23 "ReferenceError",
24 "BufferError",
25 "EOFError",
26 "LookupError",
27 "ArithmeticError",
28 "AssertionError",
29 "BlockingIOError",
30 "BrokenPipeError",
31 "ChildProcessError",
32 "FileExistsError",
33 "InterruptedError",
34 "IsADirectoryError",
35 "NotADirectoryError",
36 "ProcessLookupError",
37 "UnicodeError",
38];
39
40fn error_base_prefix(error_name: &str) -> &str {
43 error_name.strip_suffix("Error").unwrap_or(error_name)
44}
45
46pub fn python_exception_name(variant_name: &str, error_name: &str) -> String {
52 let candidate = if variant_name.ends_with("Error") {
53 variant_name.to_string()
54 } else {
55 format!("{}Error", variant_name)
56 };
57
58 if PYTHON_BUILTIN_EXCEPTIONS.contains(&candidate.as_str()) {
59 let prefix = error_base_prefix(error_name);
60 if candidate.starts_with(prefix) {
62 candidate
63 } else {
64 format!("{}{}", prefix, candidate)
65 }
66 } else {
67 candidate
68 }
69}
70
71pub fn gen_pyo3_error_types(error: &ErrorDef, module_name: &str) -> String {
75 let mut lines = Vec::with_capacity(error.variants.len() + 2);
76 lines.push("// Error types".to_string());
77
78 for variant in &error.variants {
80 let variant_name = python_exception_name(&variant.name, &error.name);
81 lines.push(format!(
82 "pyo3::create_exception!({module_name}, {}, pyo3::exceptions::PyException);",
83 variant_name
84 ));
85 }
86
87 lines.push(format!(
89 "pyo3::create_exception!({module_name}, {}, pyo3::exceptions::PyException);",
90 error.name
91 ));
92
93 lines.join("\n")
94}
95
96pub fn gen_pyo3_error_converter(error: &ErrorDef, core_import: &str) -> String {
99 let rust_path = if error.rust_path.is_empty() {
100 format!("{core_import}::{}", error.name)
101 } else {
102 error.rust_path.replace('-', "_")
103 };
104
105 let fn_name = format!("{}_to_py_err", to_snake_case(&error.name));
106
107 let mut lines = Vec::new();
108 lines.push(format!("/// Convert a `{rust_path}` error to a Python exception."));
109 lines.push(format!("fn {fn_name}(e: {rust_path}) -> pyo3::PyErr {{"));
110 lines.push(" let msg = e.to_string();".to_string());
111 lines.push(" #[allow(unreachable_patterns)]".to_string());
112 lines.push(" match &e {".to_string());
113
114 for variant in &error.variants {
115 let pattern = if variant.is_unit {
116 format!("{rust_path}::{}", variant.name)
117 } else {
118 format!("{rust_path}::{}(..)", variant.name)
119 };
120 let variant_exc_name = python_exception_name(&variant.name, &error.name);
121 lines.push(format!(" {pattern} => {}::new_err(msg),", variant_exc_name));
122 }
123
124 lines.push(format!(" _ => {}::new_err(msg),", error.name));
126 lines.push(" }".to_string());
127 lines.push("}".to_string());
128 lines.join("\n")
129}
130
131pub fn gen_pyo3_error_registration(error: &ErrorDef) -> Vec<String> {
135 let mut registrations = Vec::with_capacity(error.variants.len() + 1);
136
137 for variant in &error.variants {
138 let variant_exc_name = python_exception_name(&variant.name, &error.name);
139 registrations.push(format!(
140 " m.add(\"{}\", m.py().get_type::<{}>())?;",
141 variant_exc_name, variant_exc_name
142 ));
143 }
144
145 registrations.push(format!(
147 " m.add(\"{}\", m.py().get_type::<{}>())?;",
148 error.name, error.name
149 ));
150
151 registrations
152}
153
154pub fn converter_fn_name(error: &ErrorDef) -> String {
156 format!("{}_to_py_err", to_snake_case(&error.name))
157}
158
159fn to_snake_case(s: &str) -> String {
161 let mut result = String::with_capacity(s.len() + 4);
162 for (i, c) in s.chars().enumerate() {
163 if c.is_uppercase() {
164 if i > 0 {
165 result.push('_');
166 }
167 result.push(c.to_ascii_lowercase());
168 } else {
169 result.push(c);
170 }
171 }
172 result
173}
174
175pub fn gen_napi_error_types(error: &ErrorDef) -> String {
181 let mut lines = Vec::with_capacity(error.variants.len() + 4);
182 lines.push("// Error variant name constants".to_string());
183 for variant in &error.variants {
184 lines.push(format!(
185 "pub const {}_ERROR_{}: &str = \"{}\";",
186 to_screaming_snake(&error.name),
187 to_screaming_snake(&variant.name),
188 variant.name,
189 ));
190 }
191 lines.join("\n")
192}
193
194pub fn gen_napi_error_converter(error: &ErrorDef, core_import: &str) -> String {
196 let rust_path = if error.rust_path.is_empty() {
197 format!("{core_import}::{}", error.name)
198 } else {
199 error.rust_path.replace('-', "_")
200 };
201
202 let fn_name = format!("{}_to_napi_err", to_snake_case(&error.name));
203
204 let mut lines = Vec::new();
205 lines.push(format!("/// Convert a `{rust_path}` error to a NAPI error."));
206 lines.push("#[allow(dead_code)]".to_string());
207 lines.push(format!("fn {fn_name}(e: {rust_path}) -> napi::Error {{"));
208 lines.push(" let msg = e.to_string();".to_string());
209 lines.push(" #[allow(unreachable_patterns)]".to_string());
210 lines.push(" match &e {".to_string());
211
212 for variant in &error.variants {
213 let pattern = if variant.is_unit {
214 format!("{rust_path}::{}", variant.name)
215 } else {
216 format!("{rust_path}::{}(..)", variant.name)
217 };
218 lines.push(format!(
219 " {pattern} => napi::Error::new(napi::Status::GenericFailure, format!(\"[{}] {{}}\", msg)),",
220 variant.name,
221 ));
222 }
223
224 lines.push(" _ => napi::Error::new(napi::Status::GenericFailure, msg),".to_string());
226 lines.push(" }".to_string());
227 lines.push("}".to_string());
228 lines.join("\n")
229}
230
231pub fn napi_converter_fn_name(error: &ErrorDef) -> String {
233 format!("{}_to_napi_err", to_snake_case(&error.name))
234}
235
236pub fn gen_wasm_error_converter(error: &ErrorDef, core_import: &str) -> String {
242 let rust_path = if error.rust_path.is_empty() {
243 format!("{core_import}::{}", error.name)
244 } else {
245 error.rust_path.replace('-', "_")
246 };
247
248 let fn_name = format!("{}_to_js_value", to_snake_case(&error.name));
249
250 let mut lines = Vec::new();
251 lines.push(format!("/// Convert a `{rust_path}` error to a `JsValue` string."));
252 lines.push("#[allow(dead_code)]".to_string());
253 lines.push(format!("fn {fn_name}(e: {rust_path}) -> wasm_bindgen::JsValue {{"));
254 lines.push(" wasm_bindgen::JsValue::from_str(&e.to_string())".to_string());
255 lines.push("}".to_string());
256 lines.join("\n")
257}
258
259pub fn wasm_converter_fn_name(error: &ErrorDef) -> String {
261 format!("{}_to_js_value", to_snake_case(&error.name))
262}
263
264pub fn gen_php_error_converter(error: &ErrorDef, core_import: &str) -> String {
270 let rust_path = if error.rust_path.is_empty() {
271 format!("{core_import}::{}", error.name)
272 } else {
273 error.rust_path.replace('-', "_")
274 };
275
276 let fn_name = format!("{}_to_php_err", to_snake_case(&error.name));
277
278 let mut lines = Vec::new();
279 lines.push(format!("/// Convert a `{rust_path}` error to a PHP exception."));
280 lines.push("#[allow(dead_code)]".to_string());
281 lines.push(format!(
282 "fn {fn_name}(e: {rust_path}) -> ext_php_rs::exception::PhpException {{"
283 ));
284 lines.push(" let msg = e.to_string();".to_string());
285 lines.push(" #[allow(unreachable_patterns)]".to_string());
286 lines.push(" match &e {".to_string());
287
288 for variant in &error.variants {
289 let pattern = if variant.is_unit {
290 format!("{rust_path}::{}", variant.name)
291 } else {
292 format!("{rust_path}::{}(..)", variant.name)
293 };
294 lines.push(format!(
295 " {pattern} => ext_php_rs::exception::PhpException::default(format!(\"[{}] {{}}\", msg)),",
296 variant.name,
297 ));
298 }
299
300 lines.push(" _ => ext_php_rs::exception::PhpException::default(msg),".to_string());
302 lines.push(" }".to_string());
303 lines.push("}".to_string());
304 lines.join("\n")
305}
306
307pub fn php_converter_fn_name(error: &ErrorDef) -> String {
309 format!("{}_to_php_err", to_snake_case(&error.name))
310}
311
312pub fn gen_magnus_error_converter(error: &ErrorDef, core_import: &str) -> String {
318 let rust_path = if error.rust_path.is_empty() {
319 format!("{core_import}::{}", error.name)
320 } else {
321 error.rust_path.replace('-', "_")
322 };
323
324 let fn_name = format!("{}_to_magnus_err", to_snake_case(&error.name));
325
326 let mut lines = Vec::new();
327 lines.push(format!("/// Convert a `{rust_path}` error to a Magnus runtime error."));
328 lines.push("#[allow(dead_code)]".to_string());
329 lines.push(format!("fn {fn_name}(e: {rust_path}) -> magnus::Error {{"));
330 lines.push(" let msg = e.to_string();".to_string());
331 lines.push(" magnus::Error::new(magnus::exception::runtime_error(), msg)".to_string());
332 lines.push("}".to_string());
333 lines.join("\n")
334}
335
336pub fn magnus_converter_fn_name(error: &ErrorDef) -> String {
338 format!("{}_to_magnus_err", to_snake_case(&error.name))
339}
340
341pub fn gen_rustler_error_converter(error: &ErrorDef, core_import: &str) -> String {
347 let rust_path = if error.rust_path.is_empty() {
348 format!("{core_import}::{}", error.name)
349 } else {
350 error.rust_path.replace('-', "_")
351 };
352
353 let fn_name = format!("{}_to_rustler_err", to_snake_case(&error.name));
354
355 let mut lines = Vec::new();
356 lines.push(format!("/// Convert a `{rust_path}` error to a Rustler error string."));
357 lines.push("#[allow(dead_code)]".to_string());
358 lines.push(format!("fn {fn_name}(e: {rust_path}) -> String {{"));
359 lines.push(" e.to_string()".to_string());
360 lines.push("}".to_string());
361 lines.join("\n")
362}
363
364pub fn rustler_converter_fn_name(error: &ErrorDef) -> String {
366 format!("{}_to_rustler_err", to_snake_case(&error.name))
367}
368
369pub fn gen_ffi_error_codes(error: &ErrorDef) -> String {
378 let prefix = to_screaming_snake(&error.name);
379 let prefix_lower = to_snake_case(&error.name);
380
381 let mut lines = Vec::new();
382 lines.push(format!("/// Error codes for `{}`.", error.name));
383 lines.push("typedef enum {".to_string());
384 lines.push(format!(" {}_NONE = 0,", prefix));
385
386 for (i, variant) in error.variants.iter().enumerate() {
387 let variant_screaming = to_screaming_snake(&variant.name);
388 lines.push(format!(" {}_{} = {},", prefix, variant_screaming, i + 1));
389 }
390
391 lines.push(format!("}} {}_t;\n", prefix_lower));
392
393 lines.push(format!(
395 "/// Return a static string describing the error code.\nconst char* {}_error_message({}_t code);",
396 prefix_lower, prefix_lower
397 ));
398
399 lines.join("\n")
400}
401
402pub fn gen_go_error_types(error: &ErrorDef) -> String {
408 let mut lines = Vec::new();
409
410 lines.push("var (".to_string());
412 for variant in &error.variants {
413 let err_name = format!("Err{}", variant.name);
414 let msg = variant_display_message(variant);
415 lines.push(format!(" {} = errors.New(\"{}\")", err_name, msg));
416 }
417 lines.push(")\n".to_string());
418
419 lines.push(format!("// {} is a structured error type.", error.name));
421 lines.push(format!("type {} struct {{", error.name));
422 lines.push(" Code string".to_string());
423 lines.push(" Message string".to_string());
424 lines.push("}\n".to_string());
425
426 lines.push(format!(
427 "func (e *{}) Error() string {{ return e.Message }}",
428 error.name
429 ));
430
431 lines.join("\n")
432}
433
434pub fn gen_java_error_types(error: &ErrorDef, package: &str) -> Vec<(String, String)> {
444 let mut files = Vec::with_capacity(error.variants.len() + 1);
445
446 let base_name = format!("{}Exception", error.name);
448 let mut base = String::with_capacity(512);
449 base.push_str(&format!(
450 "// DO NOT EDIT - auto-generated by alef\npackage {};\n\n",
451 package
452 ));
453 if !error.doc.is_empty() {
454 base.push_str(&format!("/** {} */\n", error.doc));
455 }
456 base.push_str(&format!("public class {} extends Exception {{\n", base_name));
457 base.push_str(&format!(
458 " public {}(String message) {{\n super(message);\n }}\n\n",
459 base_name
460 ));
461 base.push_str(&format!(
462 " public {}(String message, Throwable cause) {{\n super(message, cause);\n }}\n",
463 base_name
464 ));
465 base.push_str("}\n");
466 files.push((base_name.clone(), base));
467
468 for variant in &error.variants {
470 let class_name = format!("{}Exception", variant.name);
471 let mut content = String::with_capacity(512);
472 content.push_str(&format!(
473 "// DO NOT EDIT - auto-generated by alef\npackage {};\n\n",
474 package
475 ));
476 if !variant.doc.is_empty() {
477 content.push_str(&format!("/** {} */\n", variant.doc));
478 }
479 content.push_str(&format!("public class {} extends {} {{\n", class_name, base_name));
480 content.push_str(&format!(
481 " public {}(String message) {{\n super(message);\n }}\n\n",
482 class_name
483 ));
484 content.push_str(&format!(
485 " public {}(String message, Throwable cause) {{\n super(message, cause);\n }}\n",
486 class_name
487 ));
488 content.push_str("}\n");
489 files.push((class_name, content));
490 }
491
492 files
493}
494
495pub fn gen_csharp_error_types(error: &ErrorDef, namespace: &str) -> Vec<(String, String)> {
505 let mut files = Vec::with_capacity(error.variants.len() + 1);
506
507 let base_name = format!("{}Exception", error.name);
508
509 {
511 let mut out = String::with_capacity(512);
512 out.push_str("// This file is auto-generated by alef. DO NOT EDIT.\nusing System;\n\n");
513 out.push_str(&format!("namespace {};\n\n", namespace));
514 if !error.doc.is_empty() {
515 out.push_str("/// <summary>\n");
516 for line in error.doc.lines() {
517 out.push_str(&format!("/// {}\n", line));
518 }
519 out.push_str("/// </summary>\n");
520 }
521 out.push_str(&format!("public class {} : Exception\n{{\n", base_name));
522 out.push_str(&format!(
523 " public {}(string message) : base(message) {{ }}\n\n",
524 base_name
525 ));
526 out.push_str(&format!(
527 " public {}(string message, Exception innerException) : base(message, innerException) {{ }}\n",
528 base_name
529 ));
530 out.push_str("}\n");
531 files.push((base_name.clone(), out));
532 }
533
534 for variant in &error.variants {
536 let class_name = format!("{}Exception", variant.name);
537 let mut out = String::with_capacity(512);
538 out.push_str("// This file is auto-generated by alef. DO NOT EDIT.\nusing System;\n\n");
539 out.push_str(&format!("namespace {};\n\n", namespace));
540 if !variant.doc.is_empty() {
541 out.push_str("/// <summary>\n");
542 for line in variant.doc.lines() {
543 out.push_str(&format!("/// {}\n", line));
544 }
545 out.push_str("/// </summary>\n");
546 }
547 out.push_str(&format!("public class {} : {}\n{{\n", class_name, base_name));
548 out.push_str(&format!(
549 " public {}(string message) : base(message) {{ }}\n\n",
550 class_name
551 ));
552 out.push_str(&format!(
553 " public {}(string message, Exception innerException) : base(message, innerException) {{ }}\n",
554 class_name
555 ));
556 out.push_str("}\n");
557 files.push((class_name, out));
558 }
559
560 files
561}
562
563fn to_screaming_snake(s: &str) -> String {
569 let mut result = String::with_capacity(s.len() + 4);
570 for (i, c) in s.chars().enumerate() {
571 if c.is_uppercase() {
572 if i > 0 {
573 result.push('_');
574 }
575 result.push(c.to_ascii_uppercase());
576 } else {
577 result.push(c.to_ascii_uppercase());
578 }
579 }
580 result
581}
582
583fn variant_display_message(variant: &ErrorVariant) -> String {
588 if let Some(tmpl) = &variant.message_template {
589 let msg = tmpl
591 .replace("{0}", "")
592 .replace("{source}", "")
593 .trim_end_matches(": ")
594 .trim()
595 .to_string();
596 if msg.is_empty() {
597 to_snake_case(&variant.name).replace('_', " ")
598 } else {
599 msg
600 }
601 } else {
602 to_snake_case(&variant.name).replace('_', " ")
603 }
604}
605
606#[cfg(test)]
607mod tests {
608 use super::*;
609 use alef_core::ir::{ErrorDef, ErrorVariant};
610
611 fn sample_error() -> ErrorDef {
612 ErrorDef {
613 name: "ConversionError".to_string(),
614 rust_path: "html_to_markdown_rs::ConversionError".to_string(),
615 variants: vec![
616 ErrorVariant {
617 name: "ParseError".to_string(),
618 message_template: Some("HTML parsing error: {0}".to_string()),
619 fields: vec![],
620 has_source: false,
621 has_from: false,
622 is_unit: false,
623 doc: String::new(),
624 },
625 ErrorVariant {
626 name: "IoError".to_string(),
627 message_template: Some("I/O error: {0}".to_string()),
628 fields: vec![],
629 has_source: false,
630 has_from: true,
631 is_unit: false,
632 doc: String::new(),
633 },
634 ErrorVariant {
635 name: "Other".to_string(),
636 message_template: Some("Conversion error: {0}".to_string()),
637 fields: vec![],
638 has_source: false,
639 has_from: false,
640 is_unit: false,
641 doc: String::new(),
642 },
643 ],
644 doc: "Error type for conversion operations.".to_string(),
645 }
646 }
647
648 #[test]
649 fn test_gen_error_types() {
650 let error = sample_error();
651 let output = gen_pyo3_error_types(&error, "_module");
652 assert!(output.contains("pyo3::create_exception!(_module, ParseError, pyo3::exceptions::PyException);"));
653 assert!(output.contains("pyo3::create_exception!(_module, IoError, pyo3::exceptions::PyException);"));
654 assert!(output.contains("pyo3::create_exception!(_module, OtherError, pyo3::exceptions::PyException);"));
655 assert!(output.contains("pyo3::create_exception!(_module, ConversionError, pyo3::exceptions::PyException);"));
656 }
657
658 #[test]
659 fn test_gen_error_converter() {
660 let error = sample_error();
661 let output = gen_pyo3_error_converter(&error, "html_to_markdown_rs");
662 assert!(
663 output.contains("fn conversion_error_to_py_err(e: html_to_markdown_rs::ConversionError) -> pyo3::PyErr {")
664 );
665 assert!(output.contains("html_to_markdown_rs::ConversionError::ParseError(..) => ParseError::new_err(msg),"));
666 assert!(output.contains("html_to_markdown_rs::ConversionError::IoError(..) => IoError::new_err(msg),"));
667 }
668
669 #[test]
670 fn test_gen_error_registration() {
671 let error = sample_error();
672 let regs = gen_pyo3_error_registration(&error);
673 assert_eq!(regs.len(), 4); assert!(regs[0].contains("\"ParseError\""));
675 assert!(regs[3].contains("\"ConversionError\""));
676 }
677
678 #[test]
679 fn test_unit_variant_pattern() {
680 let error = ErrorDef {
681 name: "MyError".to_string(),
682 rust_path: "my_crate::MyError".to_string(),
683 variants: vec![ErrorVariant {
684 name: "NotFound".to_string(),
685 message_template: Some("not found".to_string()),
686 fields: vec![],
687 has_source: false,
688 has_from: false,
689 is_unit: true,
690 doc: String::new(),
691 }],
692 doc: String::new(),
693 };
694 let output = gen_pyo3_error_converter(&error, "my_crate");
695 assert!(output.contains("my_crate::MyError::NotFound => NotFoundError::new_err(msg),"));
696 assert!(!output.contains("NotFound(..)"));
698 }
699
700 #[test]
705 fn test_gen_napi_error_types() {
706 let error = sample_error();
707 let output = gen_napi_error_types(&error);
708 assert!(output.contains("CONVERSION_ERROR_ERROR_PARSE_ERROR"));
709 assert!(output.contains("CONVERSION_ERROR_ERROR_IO_ERROR"));
710 assert!(output.contains("CONVERSION_ERROR_ERROR_OTHER"));
711 }
712
713 #[test]
714 fn test_gen_napi_error_converter() {
715 let error = sample_error();
716 let output = gen_napi_error_converter(&error, "html_to_markdown_rs");
717 assert!(
718 output
719 .contains("fn conversion_error_to_napi_err(e: html_to_markdown_rs::ConversionError) -> napi::Error {")
720 );
721 assert!(output.contains("napi::Error::new(napi::Status::GenericFailure,"));
722 assert!(output.contains("[ParseError]"));
723 assert!(output.contains("[IoError]"));
724 assert!(output.contains("#[allow(dead_code)]"));
725 }
726
727 #[test]
728 fn test_napi_unit_variant() {
729 let error = ErrorDef {
730 name: "MyError".to_string(),
731 rust_path: "my_crate::MyError".to_string(),
732 variants: vec![ErrorVariant {
733 name: "NotFound".to_string(),
734 message_template: None,
735 fields: vec![],
736 has_source: false,
737 has_from: false,
738 is_unit: true,
739 doc: String::new(),
740 }],
741 doc: String::new(),
742 };
743 let output = gen_napi_error_converter(&error, "my_crate");
744 assert!(output.contains("my_crate::MyError::NotFound =>"));
745 assert!(!output.contains("NotFound(..)"));
746 }
747
748 #[test]
753 fn test_gen_wasm_error_converter() {
754 let error = sample_error();
755 let output = gen_wasm_error_converter(&error, "html_to_markdown_rs");
756 assert!(output.contains(
757 "fn conversion_error_to_js_value(e: html_to_markdown_rs::ConversionError) -> wasm_bindgen::JsValue {"
758 ));
759 assert!(output.contains("JsValue::from_str(&e.to_string())"));
760 assert!(output.contains("#[allow(dead_code)]"));
761 }
762
763 #[test]
768 fn test_gen_php_error_converter() {
769 let error = sample_error();
770 let output = gen_php_error_converter(&error, "html_to_markdown_rs");
771 assert!(output.contains("fn conversion_error_to_php_err(e: html_to_markdown_rs::ConversionError) -> ext_php_rs::exception::PhpException {"));
772 assert!(output.contains("PhpException::default(format!(\"[ParseError] {}\", msg))"));
773 assert!(output.contains("#[allow(dead_code)]"));
774 }
775
776 #[test]
781 fn test_gen_magnus_error_converter() {
782 let error = sample_error();
783 let output = gen_magnus_error_converter(&error, "html_to_markdown_rs");
784 assert!(
785 output.contains(
786 "fn conversion_error_to_magnus_err(e: html_to_markdown_rs::ConversionError) -> magnus::Error {"
787 )
788 );
789 assert!(output.contains("magnus::Error::new(magnus::exception::runtime_error(), msg)"));
790 assert!(output.contains("#[allow(dead_code)]"));
791 }
792
793 #[test]
798 fn test_gen_rustler_error_converter() {
799 let error = sample_error();
800 let output = gen_rustler_error_converter(&error, "html_to_markdown_rs");
801 assert!(
802 output.contains("fn conversion_error_to_rustler_err(e: html_to_markdown_rs::ConversionError) -> String {")
803 );
804 assert!(output.contains("e.to_string()"));
805 assert!(output.contains("#[allow(dead_code)]"));
806 }
807
808 #[test]
813 fn test_to_screaming_snake() {
814 assert_eq!(to_screaming_snake("ConversionError"), "CONVERSION_ERROR");
815 assert_eq!(to_screaming_snake("IoError"), "IO_ERROR");
816 assert_eq!(to_screaming_snake("Other"), "OTHER");
817 }
818
819 #[test]
824 fn test_gen_ffi_error_codes() {
825 let error = sample_error();
826 let output = gen_ffi_error_codes(&error);
827 assert!(output.contains("CONVERSION_ERROR_NONE = 0"));
828 assert!(output.contains("CONVERSION_ERROR_PARSE_ERROR = 1"));
829 assert!(output.contains("CONVERSION_ERROR_IO_ERROR = 2"));
830 assert!(output.contains("CONVERSION_ERROR_OTHER = 3"));
831 assert!(output.contains("conversion_error_t;"));
832 assert!(output.contains("conversion_error_error_message(conversion_error_t code)"));
833 }
834
835 #[test]
840 fn test_gen_go_error_types() {
841 let error = sample_error();
842 let output = gen_go_error_types(&error);
843 assert!(output.contains("ErrParseError = errors.New("));
844 assert!(output.contains("ErrIoError = errors.New("));
845 assert!(output.contains("ErrOther = errors.New("));
846 assert!(output.contains("type ConversionError struct {"));
847 assert!(output.contains("Code string"));
848 assert!(output.contains("func (e *ConversionError) Error() string"));
849 }
850
851 #[test]
856 fn test_gen_java_error_types() {
857 let error = sample_error();
858 let files = gen_java_error_types(&error, "dev.kreuzberg.test");
859 assert_eq!(files.len(), 4);
861 assert_eq!(files[0].0, "ConversionErrorException");
863 assert!(
864 files[0]
865 .1
866 .contains("public class ConversionErrorException extends Exception")
867 );
868 assert!(files[0].1.contains("package dev.kreuzberg.test;"));
869 assert_eq!(files[1].0, "ParseErrorException");
871 assert!(
872 files[1]
873 .1
874 .contains("public class ParseErrorException extends ConversionErrorException")
875 );
876 assert_eq!(files[2].0, "IoErrorException");
877 assert_eq!(files[3].0, "OtherException");
878 }
879
880 #[test]
885 fn test_gen_csharp_error_types() {
886 let error = sample_error();
887 let files = gen_csharp_error_types(&error, "Kreuzberg.Test");
888 assert_eq!(files.len(), 4);
890 assert_eq!(files[0].0, "ConversionErrorException");
892 assert!(files[0].1.contains("public class ConversionErrorException : Exception"));
893 assert!(files[0].1.contains("namespace Kreuzberg.Test;"));
894 assert_eq!(files[1].0, "ParseErrorException");
896 assert!(
897 files[1]
898 .1
899 .contains("public class ParseErrorException : ConversionErrorException")
900 );
901 assert_eq!(files[2].0, "IoErrorException");
902 assert_eq!(files[3].0, "OtherException");
903 }
904
905 #[test]
910 fn test_python_exception_name_no_conflict() {
911 assert_eq!(python_exception_name("ParseError", "ConversionError"), "ParseError");
913 assert_eq!(python_exception_name("Other", "ConversionError"), "OtherError");
915 }
916
917 #[test]
918 fn test_python_exception_name_shadows_builtin() {
919 assert_eq!(
921 python_exception_name("Connection", "CrawlError"),
922 "CrawlConnectionError"
923 );
924 assert_eq!(python_exception_name("Timeout", "CrawlError"), "CrawlTimeoutError");
926 assert_eq!(
928 python_exception_name("ConnectionError", "CrawlError"),
929 "CrawlConnectionError"
930 );
931 }
932
933 #[test]
934 fn test_python_exception_name_no_double_prefix() {
935 assert_eq!(
937 python_exception_name("CrawlConnectionError", "CrawlError"),
938 "CrawlConnectionError"
939 );
940 }
941}