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 let normalized = error.rust_path.replace('-', "_");
122 let segments: Vec<&str> = normalized.split("::").collect();
126 if segments.len() > 2 {
127 let crate_name = segments[0];
128 let error_name = segments[segments.len() - 1];
129 format!("{crate_name}::{error_name}")
130 } else {
131 normalized
132 }
133 };
134
135 let fn_name = format!("{}_to_py_err", to_snake_case(&error.name));
136
137 let mut lines = Vec::new();
138 lines.push(format!("/// Convert a `{rust_path}` error to a Python exception."));
139 lines.push(format!("fn {fn_name}(e: {rust_path}) -> pyo3::PyErr {{"));
140 lines.push(" let msg = e.to_string();".to_string());
141 lines.push(" #[allow(unreachable_patterns)]".to_string());
142 lines.push(" match &e {".to_string());
143
144 for variant in &error.variants {
145 let pattern = error_variant_wildcard_pattern(&rust_path, variant);
146 let variant_exc_name = python_exception_name(&variant.name, &error.name);
147 lines.push(format!(" {pattern} => {}::new_err(msg),", variant_exc_name));
148 }
149
150 lines.push(format!(" _ => {}::new_err(msg),", error.name));
152 lines.push(" }".to_string());
153 lines.push("}".to_string());
154 lines.join("\n")
155}
156
157pub fn gen_pyo3_error_registration(error: &ErrorDef, seen_registrations: &mut AHashSet<String>) -> Vec<String> {
161 let mut registrations = Vec::with_capacity(error.variants.len() + 1);
162
163 for variant in &error.variants {
164 let variant_exc_name = python_exception_name(&variant.name, &error.name);
165 if seen_registrations.insert(variant_exc_name.clone()) {
166 registrations.push(format!(
167 " m.add(\"{}\", m.py().get_type::<{}>())?;",
168 variant_exc_name, variant_exc_name
169 ));
170 }
171 }
172
173 if seen_registrations.insert(error.name.clone()) {
175 registrations.push(format!(
176 " m.add(\"{}\", m.py().get_type::<{}>())?;",
177 error.name, error.name
178 ));
179 }
180
181 registrations
182}
183
184pub fn converter_fn_name(error: &ErrorDef) -> String {
186 format!("{}_to_py_err", to_snake_case(&error.name))
187}
188
189fn to_snake_case(s: &str) -> String {
191 let mut result = String::with_capacity(s.len() + 4);
192 for (i, c) in s.chars().enumerate() {
193 if c.is_uppercase() {
194 if i > 0 {
195 result.push('_');
196 }
197 result.push(c.to_ascii_lowercase());
198 } else {
199 result.push(c);
200 }
201 }
202 result
203}
204
205pub fn gen_napi_error_types(error: &ErrorDef) -> String {
211 let mut lines = Vec::with_capacity(error.variants.len() + 4);
212 lines.push("// Error variant name constants".to_string());
213 for variant in &error.variants {
214 lines.push(format!(
215 "pub const {}_ERROR_{}: &str = \"{}\";",
216 to_screaming_snake(&error.name),
217 to_screaming_snake(&variant.name),
218 variant.name,
219 ));
220 }
221 lines.join("\n")
222}
223
224pub fn gen_napi_error_converter(error: &ErrorDef, core_import: &str) -> String {
226 let rust_path = if error.rust_path.is_empty() {
227 format!("{core_import}::{}", error.name)
228 } else {
229 error.rust_path.replace('-', "_")
230 };
231
232 let fn_name = format!("{}_to_napi_err", to_snake_case(&error.name));
233
234 let mut lines = Vec::new();
235 lines.push(format!("/// Convert a `{rust_path}` error to a NAPI error."));
236 lines.push("#[allow(dead_code)]".to_string());
237 lines.push(format!("fn {fn_name}(e: {rust_path}) -> napi::Error {{"));
238 lines.push(" let msg = e.to_string();".to_string());
239 lines.push(" #[allow(unreachable_patterns)]".to_string());
240 lines.push(" match &e {".to_string());
241
242 for variant in &error.variants {
243 let pattern = error_variant_wildcard_pattern(&rust_path, variant);
244 lines.push(format!(
245 " {pattern} => napi::Error::new(napi::Status::GenericFailure, format!(\"[{}] {{}}\", msg)),",
246 variant.name,
247 ));
248 }
249
250 lines.push(" _ => napi::Error::new(napi::Status::GenericFailure, msg),".to_string());
252 lines.push(" }".to_string());
253 lines.push("}".to_string());
254 lines.join("\n")
255}
256
257pub fn napi_converter_fn_name(error: &ErrorDef) -> String {
259 format!("{}_to_napi_err", to_snake_case(&error.name))
260}
261
262pub fn gen_wasm_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_js_value", to_snake_case(&error.name));
277 let code_fn_name = format!("{}_error_code", to_snake_case(&error.name));
278
279 let mut lines = Vec::new();
280
281 lines.push(format!("/// Return the error code string for a `{rust_path}` variant."));
283 lines.push("#[allow(dead_code)]".to_string());
284 lines.push(format!("fn {code_fn_name}(e: &{rust_path}) -> &'static str {{"));
285 lines.push(" #[allow(unreachable_patterns)]".to_string());
286 lines.push(" match e {".to_string());
287 for variant in &error.variants {
288 let pattern = error_variant_wildcard_pattern(&rust_path, variant);
289 let code = to_snake_case(&variant.name);
290 lines.push(format!(" {pattern} => \"{code}\","));
291 }
292 lines.push(format!(" _ => \"{}\",", to_snake_case(&error.name)));
293 lines.push(" }".to_string());
294 lines.push("}".to_string());
295
296 lines.push(String::new());
297
298 lines.push(format!(
300 "/// Convert a `{rust_path}` error to a `JsValue` object with `code` and `message` fields."
301 ));
302 lines.push("#[allow(dead_code)]".to_string());
303 lines.push(format!("fn {fn_name}(e: {rust_path}) -> wasm_bindgen::JsValue {{"));
304 lines.push(format!(" let code = {code_fn_name}(&e);"));
305 lines.push(" let message = e.to_string();".to_string());
306 lines.push(" let obj = js_sys::Object::new();".to_string());
307 lines.push(" js_sys::Reflect::set(&obj, &\"code\".into(), &code.into()).ok();".to_string());
308 lines.push(" js_sys::Reflect::set(&obj, &\"message\".into(), &message.into()).ok();".to_string());
309 lines.push(" obj.into()".to_string());
310 lines.push("}".to_string());
311
312 lines.join("\n")
313}
314
315pub fn wasm_converter_fn_name(error: &ErrorDef) -> String {
317 format!("{}_to_js_value", to_snake_case(&error.name))
318}
319
320pub fn gen_php_error_converter(error: &ErrorDef, core_import: &str) -> String {
326 let rust_path = if error.rust_path.is_empty() {
327 format!("{core_import}::{}", error.name)
328 } else {
329 error.rust_path.replace('-', "_")
330 };
331
332 let fn_name = format!("{}_to_php_err", to_snake_case(&error.name));
333
334 let mut lines = Vec::new();
335 lines.push(format!("/// Convert a `{rust_path}` error to a PHP exception."));
336 lines.push("#[allow(dead_code)]".to_string());
337 lines.push(format!(
338 "fn {fn_name}(e: {rust_path}) -> ext_php_rs::exception::PhpException {{"
339 ));
340 lines.push(" let msg = e.to_string();".to_string());
341 lines.push(" #[allow(unreachable_patterns)]".to_string());
342 lines.push(" match &e {".to_string());
343
344 for variant in &error.variants {
345 let pattern = error_variant_wildcard_pattern(&rust_path, variant);
346 lines.push(format!(
347 " {pattern} => ext_php_rs::exception::PhpException::default(format!(\"[{}] {{}}\", msg)),",
348 variant.name,
349 ));
350 }
351
352 lines.push(" _ => ext_php_rs::exception::PhpException::default(msg),".to_string());
354 lines.push(" }".to_string());
355 lines.push("}".to_string());
356 lines.join("\n")
357}
358
359pub fn php_converter_fn_name(error: &ErrorDef) -> String {
361 format!("{}_to_php_err", to_snake_case(&error.name))
362}
363
364pub fn gen_magnus_error_converter(error: &ErrorDef, core_import: &str) -> String {
370 let rust_path = if error.rust_path.is_empty() {
371 format!("{core_import}::{}", error.name)
372 } else {
373 error.rust_path.replace('-', "_")
374 };
375
376 let fn_name = format!("{}_to_magnus_err", to_snake_case(&error.name));
377
378 let mut lines = Vec::new();
379 lines.push(format!("/// Convert a `{rust_path}` error to a Magnus runtime error."));
380 lines.push("#[allow(dead_code)]".to_string());
381 lines.push(format!("fn {fn_name}(e: {rust_path}) -> magnus::Error {{"));
382 lines.push(" let msg = e.to_string();".to_string());
383 lines.push(
384 " magnus::Error::new(unsafe { magnus::Ruby::get_unchecked() }.exception_runtime_error(), msg)".to_string(),
385 );
386 lines.push("}".to_string());
387 lines.join("\n")
388}
389
390pub fn magnus_converter_fn_name(error: &ErrorDef) -> String {
392 format!("{}_to_magnus_err", to_snake_case(&error.name))
393}
394
395pub fn gen_rustler_error_converter(error: &ErrorDef, core_import: &str) -> String {
401 let rust_path = if error.rust_path.is_empty() {
402 format!("{core_import}::{}", error.name)
403 } else {
404 error.rust_path.replace('-', "_")
405 };
406
407 let fn_name = format!("{}_to_rustler_err", to_snake_case(&error.name));
408
409 let mut lines = Vec::new();
410 lines.push(format!("/// Convert a `{rust_path}` error to a Rustler error string."));
411 lines.push("#[allow(dead_code)]".to_string());
412 lines.push(format!("fn {fn_name}(e: {rust_path}) -> String {{"));
413 lines.push(" e.to_string()".to_string());
414 lines.push("}".to_string());
415 lines.join("\n")
416}
417
418pub fn rustler_converter_fn_name(error: &ErrorDef) -> String {
420 format!("{}_to_rustler_err", to_snake_case(&error.name))
421}
422
423pub fn gen_ffi_error_codes(error: &ErrorDef) -> String {
432 let prefix = to_screaming_snake(&error.name);
433 let prefix_lower = to_snake_case(&error.name);
434
435 let mut lines = Vec::new();
436 lines.push(format!("/// Error codes for `{}`.", error.name));
437 lines.push("typedef enum {".to_string());
438 lines.push(format!(" {}_NONE = 0,", prefix));
439
440 for (i, variant) in error.variants.iter().enumerate() {
441 let variant_screaming = to_screaming_snake(&variant.name);
442 lines.push(format!(" {}_{} = {},", prefix, variant_screaming, i + 1));
443 }
444
445 lines.push(format!("}} {}_t;\n", prefix_lower));
446
447 lines.push(format!(
449 "/// Return a static string describing the error code.\nconst char* {}_error_message({}_t code);",
450 prefix_lower, prefix_lower
451 ));
452
453 lines.join("\n")
454}
455
456pub fn gen_go_error_types(error: &ErrorDef, pkg_name: &str) -> String {
467 let sentinels = gen_go_sentinel_errors(std::slice::from_ref(error));
468 let structured = gen_go_error_struct(error, pkg_name);
469 format!("{}\n\n{}", sentinels, structured)
470}
471
472pub fn gen_go_sentinel_errors(errors: &[ErrorDef]) -> String {
483 if errors.is_empty() {
484 return String::new();
485 }
486 let mut variant_counts: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
487 for err in errors {
488 for v in &err.variants {
489 *variant_counts.entry(v.name.as_str()).or_insert(0) += 1;
490 }
491 }
492 let mut seen = std::collections::HashSet::new();
493 let mut lines = Vec::new();
494 lines.push("var (".to_string());
495 for err in errors {
496 let parent_base = error_base_prefix(&err.name);
497 for variant in &err.variants {
498 let collides = variant_counts.get(variant.name.as_str()).copied().unwrap_or(0) > 1;
499 let const_name = if collides {
500 format!("Err{}{}", parent_base, variant.name)
501 } else {
502 format!("Err{}", variant.name)
503 };
504 if !seen.insert(const_name.clone()) {
505 continue;
506 }
507 let msg = variant_display_message(variant);
508 lines.push(format!("\t// {} is returned when {}.", const_name, msg));
509 lines.push(format!("\t{} = errors.New(\"{}\")", const_name, msg));
510 }
511 }
512 lines.push(")".to_string());
513 lines.join("\n")
514}
515
516pub fn gen_go_error_struct(error: &ErrorDef, pkg_name: &str) -> String {
520 let go_type_name = strip_package_prefix(&error.name, pkg_name);
521 let mut lines = Vec::new();
522 lines.push(format!("// {} is a structured error type.", go_type_name));
523 lines.push(format!("type {} struct {{", go_type_name));
524 lines.push("\tCode string".to_string());
525 lines.push("\tMessage string".to_string());
526 lines.push("}\n".to_string());
527 lines.push(format!(
528 "func (e *{}) Error() string {{ return e.Message }}",
529 go_type_name
530 ));
531 lines.join("\n")
532}
533
534fn strip_package_prefix(type_name: &str, pkg_name: &str) -> String {
546 let type_lower = type_name.to_lowercase();
547 let pkg_lower = pkg_name.to_lowercase();
548 if type_lower.starts_with(&pkg_lower) && type_lower.len() > pkg_lower.len() {
549 type_name[pkg_lower.len()..].to_string()
551 } else {
552 type_name.to_string()
553 }
554}
555
556pub fn gen_java_error_types(error: &ErrorDef, package: &str) -> Vec<(String, String)> {
566 let mut files = Vec::with_capacity(error.variants.len() + 1);
567
568 let base_name = format!("{}Exception", error.name);
570 let mut base = String::with_capacity(512);
571 base.push_str(&format!(
572 "// DO NOT EDIT - auto-generated by alef\npackage {};\n\n",
573 package
574 ));
575 if !error.doc.is_empty() {
576 crate::doc_emission::emit_javadoc(&mut base, &error.doc, "");
583 }
584 base.push_str(&format!("public class {} extends Exception {{\n", base_name));
585 base.push_str(&format!(
586 " /** Creates a new {} with the given message. */\n public {}(final String message) {{\n super(message);\n }}\n\n",
587 base_name, base_name
588 ));
589 base.push_str(&format!(
590 " /** Creates a new {} with the given message and cause. */\n public {}(final String message, final Throwable cause) {{\n super(message, cause);\n }}\n",
591 base_name, base_name
592 ));
593 base.push_str("}\n");
594 files.push((base_name.clone(), base));
595
596 for variant in &error.variants {
598 let class_name = format!("{}Exception", variant.name);
599 let mut content = String::with_capacity(512);
600 content.push_str(&format!(
601 "// DO NOT EDIT - auto-generated by alef\npackage {};\n\n",
602 package
603 ));
604 if !variant.doc.is_empty() {
605 crate::doc_emission::emit_javadoc(&mut content, &variant.doc, "");
606 }
607 content.push_str(&format!("public class {} extends {} {{\n", class_name, base_name));
608 content.push_str(&format!(
609 " /** Creates a new {} with the given message. */\n public {}(final String message) {{\n super(message);\n }}\n\n",
610 class_name, class_name
611 ));
612 content.push_str(&format!(
613 " /** Creates a new {} with the given message and cause. */\n public {}(final String message, final Throwable cause) {{\n super(message, cause);\n }}\n",
614 class_name, class_name
615 ));
616 content.push_str("}\n");
617 files.push((class_name, content));
618 }
619
620 files
621}
622
623pub fn gen_csharp_error_types(error: &ErrorDef, namespace: &str) -> Vec<(String, String)> {
633 let mut files = Vec::with_capacity(error.variants.len() + 1);
634
635 let base_name = format!("{}Exception", error.name);
636
637 {
639 let mut out = String::with_capacity(512);
640 out.push_str("// This file is auto-generated by alef. DO NOT EDIT.\n#nullable enable\n\nusing System;\n\n");
641 out.push_str(&format!("namespace {};\n\n", namespace));
642 if !error.doc.is_empty() {
643 out.push_str("/// <summary>\n");
644 for line in error.doc.lines() {
645 out.push_str(&format!("/// {}\n", line));
646 }
647 out.push_str("/// </summary>\n");
648 }
649 out.push_str(&format!("public class {} : Exception\n{{\n", base_name));
650 out.push_str(&format!(
651 " public {}(string message) : base(message) {{ }}\n\n",
652 base_name
653 ));
654 out.push_str(&format!(
655 " public {}(string message, Exception innerException) : base(message, innerException) {{ }}\n",
656 base_name
657 ));
658 out.push_str("}\n");
659 files.push((base_name.clone(), out));
660 }
661
662 for variant in &error.variants {
664 let class_name = format!("{}Exception", variant.name);
665 let mut out = String::with_capacity(512);
666 out.push_str("// This file is auto-generated by alef. DO NOT EDIT.\n#nullable enable\n\nusing System;\n\n");
667 out.push_str(&format!("namespace {};\n\n", namespace));
668 if !variant.doc.is_empty() {
669 out.push_str("/// <summary>\n");
670 for line in variant.doc.lines() {
671 out.push_str(&format!("/// {}\n", line));
672 }
673 out.push_str("/// </summary>\n");
674 }
675 out.push_str(&format!("public class {} : {}\n{{\n", class_name, base_name));
676 out.push_str(&format!(
677 " public {}(string message) : base(message) {{ }}\n\n",
678 class_name
679 ));
680 out.push_str(&format!(
681 " public {}(string message, Exception innerException) : base(message, innerException) {{ }}\n",
682 class_name
683 ));
684 out.push_str("}\n");
685 files.push((class_name, out));
686 }
687
688 files
689}
690
691fn to_screaming_snake(s: &str) -> String {
697 let mut result = String::with_capacity(s.len() + 4);
698 for (i, c) in s.chars().enumerate() {
699 if c.is_uppercase() {
700 if i > 0 {
701 result.push('_');
702 }
703 result.push(c.to_ascii_uppercase());
704 } else {
705 result.push(c.to_ascii_uppercase());
706 }
707 }
708 result
709}
710
711fn variant_display_message(variant: &ErrorVariant) -> String {
716 if let Some(tmpl) = &variant.message_template {
717 let msg = tmpl
719 .replace("{0}", "")
720 .replace("{source}", "")
721 .trim_end_matches(": ")
722 .trim()
723 .to_string();
724 if msg.is_empty() {
725 to_snake_case(&variant.name).replace('_', " ")
726 } else {
727 let mut chars = msg.chars();
729 match chars.next() {
730 Some(c) => c.to_lowercase().to_string() + chars.as_str(),
731 None => msg,
732 }
733 }
734 } else {
735 to_snake_case(&variant.name).replace('_', " ")
736 }
737}
738
739#[cfg(test)]
740mod tests {
741 use super::*;
742 use alef_core::ir::{ErrorDef, ErrorVariant};
743
744 use alef_core::ir::{CoreWrapper, FieldDef, TypeRef};
745
746 fn tuple_field(index: usize) -> FieldDef {
748 FieldDef {
749 name: format!("_{index}"),
750 ty: TypeRef::String,
751 optional: false,
752 default: None,
753 doc: String::new(),
754 sanitized: false,
755 is_boxed: false,
756 type_rust_path: None,
757 cfg: None,
758 typed_default: None,
759 core_wrapper: CoreWrapper::None,
760 vec_inner_core_wrapper: CoreWrapper::None,
761 newtype_wrapper: None,
762 }
763 }
764
765 fn named_field(name: &str) -> FieldDef {
767 FieldDef {
768 name: name.to_string(),
769 ty: TypeRef::String,
770 optional: false,
771 default: None,
772 doc: String::new(),
773 sanitized: false,
774 is_boxed: false,
775 type_rust_path: None,
776 cfg: None,
777 typed_default: None,
778 core_wrapper: CoreWrapper::None,
779 vec_inner_core_wrapper: CoreWrapper::None,
780 newtype_wrapper: None,
781 }
782 }
783
784 fn sample_error() -> ErrorDef {
785 ErrorDef {
786 name: "ConversionError".to_string(),
787 rust_path: "html_to_markdown_rs::ConversionError".to_string(),
788 original_rust_path: String::new(),
789 variants: vec![
790 ErrorVariant {
791 name: "ParseError".to_string(),
792 message_template: Some("HTML parsing error: {0}".to_string()),
793 fields: vec![tuple_field(0)],
794 has_source: false,
795 has_from: false,
796 is_unit: false,
797 doc: String::new(),
798 },
799 ErrorVariant {
800 name: "IoError".to_string(),
801 message_template: Some("I/O error: {0}".to_string()),
802 fields: vec![tuple_field(0)],
803 has_source: false,
804 has_from: true,
805 is_unit: false,
806 doc: String::new(),
807 },
808 ErrorVariant {
809 name: "Other".to_string(),
810 message_template: Some("Conversion error: {0}".to_string()),
811 fields: vec![tuple_field(0)],
812 has_source: false,
813 has_from: false,
814 is_unit: false,
815 doc: String::new(),
816 },
817 ],
818 doc: "Error type for conversion operations.".to_string(),
819 }
820 }
821
822 #[test]
823 fn test_gen_error_types() {
824 let error = sample_error();
825 let output = gen_pyo3_error_types(&error, "_module", &mut AHashSet::new());
826 assert!(output.contains("pyo3::create_exception!(_module, ParseError, pyo3::exceptions::PyException);"));
827 assert!(output.contains("pyo3::create_exception!(_module, IoError, pyo3::exceptions::PyException);"));
828 assert!(output.contains("pyo3::create_exception!(_module, OtherError, pyo3::exceptions::PyException);"));
829 assert!(output.contains("pyo3::create_exception!(_module, ConversionError, pyo3::exceptions::PyException);"));
830 }
831
832 #[test]
833 fn test_gen_error_converter() {
834 let error = sample_error();
835 let output = gen_pyo3_error_converter(&error, "html_to_markdown_rs");
836 assert!(
837 output.contains("fn conversion_error_to_py_err(e: html_to_markdown_rs::ConversionError) -> pyo3::PyErr {")
838 );
839 assert!(output.contains("html_to_markdown_rs::ConversionError::ParseError(..) => ParseError::new_err(msg),"));
840 assert!(output.contains("html_to_markdown_rs::ConversionError::IoError(..) => IoError::new_err(msg),"));
841 }
842
843 #[test]
844 fn test_gen_error_registration() {
845 let error = sample_error();
846 let regs = gen_pyo3_error_registration(&error, &mut AHashSet::new());
847 assert_eq!(regs.len(), 4); assert!(regs[0].contains("\"ParseError\""));
849 assert!(regs[3].contains("\"ConversionError\""));
850 }
851
852 #[test]
853 fn test_unit_variant_pattern() {
854 let error = ErrorDef {
855 name: "MyError".to_string(),
856 rust_path: "my_crate::MyError".to_string(),
857 original_rust_path: String::new(),
858 variants: vec![ErrorVariant {
859 name: "NotFound".to_string(),
860 message_template: Some("not found".to_string()),
861 fields: vec![],
862 has_source: false,
863 has_from: false,
864 is_unit: true,
865 doc: String::new(),
866 }],
867 doc: String::new(),
868 };
869 let output = gen_pyo3_error_converter(&error, "my_crate");
870 assert!(output.contains("my_crate::MyError::NotFound => NotFoundError::new_err(msg),"));
871 assert!(!output.contains("NotFound(..)"));
873 }
874
875 #[test]
876 fn test_struct_variant_pattern() {
877 let error = ErrorDef {
878 name: "MyError".to_string(),
879 rust_path: "my_crate::MyError".to_string(),
880 original_rust_path: String::new(),
881 variants: vec![ErrorVariant {
882 name: "Parsing".to_string(),
883 message_template: Some("parsing error: {message}".to_string()),
884 fields: vec![named_field("message")],
885 has_source: false,
886 has_from: false,
887 is_unit: false,
888 doc: String::new(),
889 }],
890 doc: String::new(),
891 };
892 let output = gen_pyo3_error_converter(&error, "my_crate");
893 assert!(
894 output.contains("my_crate::MyError::Parsing { .. } => ParsingError::new_err(msg),"),
895 "Struct variants must use {{ .. }} pattern, got:\n{output}"
896 );
897 assert!(!output.contains("Parsing(..)"));
899 }
900
901 #[test]
906 fn test_gen_napi_error_types() {
907 let error = sample_error();
908 let output = gen_napi_error_types(&error);
909 assert!(output.contains("CONVERSION_ERROR_ERROR_PARSE_ERROR"));
910 assert!(output.contains("CONVERSION_ERROR_ERROR_IO_ERROR"));
911 assert!(output.contains("CONVERSION_ERROR_ERROR_OTHER"));
912 }
913
914 #[test]
915 fn test_gen_napi_error_converter() {
916 let error = sample_error();
917 let output = gen_napi_error_converter(&error, "html_to_markdown_rs");
918 assert!(
919 output
920 .contains("fn conversion_error_to_napi_err(e: html_to_markdown_rs::ConversionError) -> napi::Error {")
921 );
922 assert!(output.contains("napi::Error::new(napi::Status::GenericFailure,"));
923 assert!(output.contains("[ParseError]"));
924 assert!(output.contains("[IoError]"));
925 assert!(output.contains("#[allow(dead_code)]"));
926 }
927
928 #[test]
929 fn test_napi_unit_variant() {
930 let error = ErrorDef {
931 name: "MyError".to_string(),
932 rust_path: "my_crate::MyError".to_string(),
933 original_rust_path: String::new(),
934 variants: vec![ErrorVariant {
935 name: "NotFound".to_string(),
936 message_template: None,
937 fields: vec![],
938 has_source: false,
939 has_from: false,
940 is_unit: true,
941 doc: String::new(),
942 }],
943 doc: String::new(),
944 };
945 let output = gen_napi_error_converter(&error, "my_crate");
946 assert!(output.contains("my_crate::MyError::NotFound =>"));
947 assert!(!output.contains("NotFound(..)"));
948 }
949
950 #[test]
955 fn test_gen_wasm_error_converter() {
956 let error = sample_error();
957 let output = gen_wasm_error_converter(&error, "html_to_markdown_rs");
958 assert!(output.contains(
960 "fn conversion_error_to_js_value(e: html_to_markdown_rs::ConversionError) -> wasm_bindgen::JsValue {"
961 ));
962 assert!(output.contains("js_sys::Object::new()"));
964 assert!(output.contains("js_sys::Reflect::set(&obj, &\"code\".into(), &code.into()).ok()"));
965 assert!(output.contains("js_sys::Reflect::set(&obj, &\"message\".into(), &message.into()).ok()"));
966 assert!(output.contains("obj.into()"));
967 assert!(
969 output
970 .contains("fn conversion_error_error_code(e: &html_to_markdown_rs::ConversionError) -> &'static str {")
971 );
972 assert!(output.contains("\"parse_error\""));
973 assert!(output.contains("\"io_error\""));
974 assert!(output.contains("\"other\""));
975 assert!(output.contains("#[allow(dead_code)]"));
976 }
977
978 #[test]
983 fn test_gen_php_error_converter() {
984 let error = sample_error();
985 let output = gen_php_error_converter(&error, "html_to_markdown_rs");
986 assert!(output.contains("fn conversion_error_to_php_err(e: html_to_markdown_rs::ConversionError) -> ext_php_rs::exception::PhpException {"));
987 assert!(output.contains("PhpException::default(format!(\"[ParseError] {}\", msg))"));
988 assert!(output.contains("#[allow(dead_code)]"));
989 }
990
991 #[test]
996 fn test_gen_magnus_error_converter() {
997 let error = sample_error();
998 let output = gen_magnus_error_converter(&error, "html_to_markdown_rs");
999 assert!(
1000 output.contains(
1001 "fn conversion_error_to_magnus_err(e: html_to_markdown_rs::ConversionError) -> magnus::Error {"
1002 )
1003 );
1004 assert!(
1005 output.contains(
1006 "magnus::Error::new(unsafe { magnus::Ruby::get_unchecked() }.exception_runtime_error(), msg)"
1007 )
1008 );
1009 assert!(output.contains("#[allow(dead_code)]"));
1010 }
1011
1012 #[test]
1017 fn test_gen_rustler_error_converter() {
1018 let error = sample_error();
1019 let output = gen_rustler_error_converter(&error, "html_to_markdown_rs");
1020 assert!(
1021 output.contains("fn conversion_error_to_rustler_err(e: html_to_markdown_rs::ConversionError) -> String {")
1022 );
1023 assert!(output.contains("e.to_string()"));
1024 assert!(output.contains("#[allow(dead_code)]"));
1025 }
1026
1027 #[test]
1032 fn test_to_screaming_snake() {
1033 assert_eq!(to_screaming_snake("ConversionError"), "CONVERSION_ERROR");
1034 assert_eq!(to_screaming_snake("IoError"), "IO_ERROR");
1035 assert_eq!(to_screaming_snake("Other"), "OTHER");
1036 }
1037
1038 #[test]
1043 fn test_gen_ffi_error_codes() {
1044 let error = sample_error();
1045 let output = gen_ffi_error_codes(&error);
1046 assert!(output.contains("CONVERSION_ERROR_NONE = 0"));
1047 assert!(output.contains("CONVERSION_ERROR_PARSE_ERROR = 1"));
1048 assert!(output.contains("CONVERSION_ERROR_IO_ERROR = 2"));
1049 assert!(output.contains("CONVERSION_ERROR_OTHER = 3"));
1050 assert!(output.contains("conversion_error_t;"));
1051 assert!(output.contains("conversion_error_error_message(conversion_error_t code)"));
1052 }
1053
1054 #[test]
1059 fn test_gen_go_error_types() {
1060 let error = sample_error();
1061 let output = gen_go_error_types(&error, "mylib");
1063 assert!(output.contains("ErrParseError = errors.New("));
1064 assert!(output.contains("ErrIoError = errors.New("));
1065 assert!(output.contains("ErrOther = errors.New("));
1066 assert!(output.contains("type ConversionError struct {"));
1067 assert!(output.contains("Code string"));
1068 assert!(output.contains("func (e *ConversionError) Error() string"));
1069 assert!(output.contains("// ErrParseError is returned when"));
1071 assert!(output.contains("// ErrIoError is returned when"));
1072 assert!(output.contains("// ErrOther is returned when"));
1073 }
1074
1075 #[test]
1076 fn test_gen_go_error_types_stutter_strip() {
1077 let error = sample_error();
1078 let output = gen_go_error_types(&error, "conversion");
1081 assert!(
1082 output.contains("type Error struct {"),
1083 "expected stutter strip, got:\n{output}"
1084 );
1085 assert!(
1086 output.contains("func (e *Error) Error() string"),
1087 "expected stutter strip, got:\n{output}"
1088 );
1089 assert!(output.contains("ErrParseError = errors.New("));
1091 }
1092
1093 #[test]
1098 fn test_gen_java_error_types() {
1099 let error = sample_error();
1100 let files = gen_java_error_types(&error, "dev.kreuzberg.test");
1101 assert_eq!(files.len(), 4);
1103 assert_eq!(files[0].0, "ConversionErrorException");
1105 assert!(
1106 files[0]
1107 .1
1108 .contains("public class ConversionErrorException extends Exception")
1109 );
1110 assert!(files[0].1.contains("package dev.kreuzberg.test;"));
1111 assert_eq!(files[1].0, "ParseErrorException");
1113 assert!(
1114 files[1]
1115 .1
1116 .contains("public class ParseErrorException extends ConversionErrorException")
1117 );
1118 assert_eq!(files[2].0, "IoErrorException");
1119 assert_eq!(files[3].0, "OtherException");
1120 }
1121
1122 #[test]
1127 fn test_gen_csharp_error_types() {
1128 let error = sample_error();
1129 let files = gen_csharp_error_types(&error, "Kreuzberg.Test");
1130 assert_eq!(files.len(), 4);
1132 assert_eq!(files[0].0, "ConversionErrorException");
1134 assert!(files[0].1.contains("public class ConversionErrorException : Exception"));
1135 assert!(files[0].1.contains("namespace Kreuzberg.Test;"));
1136 assert_eq!(files[1].0, "ParseErrorException");
1138 assert!(
1139 files[1]
1140 .1
1141 .contains("public class ParseErrorException : ConversionErrorException")
1142 );
1143 assert_eq!(files[2].0, "IoErrorException");
1144 assert_eq!(files[3].0, "OtherException");
1145 }
1146
1147 #[test]
1152 fn test_python_exception_name_no_conflict() {
1153 assert_eq!(python_exception_name("ParseError", "ConversionError"), "ParseError");
1155 assert_eq!(python_exception_name("Other", "ConversionError"), "OtherError");
1157 }
1158
1159 #[test]
1160 fn test_python_exception_name_shadows_builtin() {
1161 assert_eq!(
1163 python_exception_name("Connection", "CrawlError"),
1164 "CrawlConnectionError"
1165 );
1166 assert_eq!(python_exception_name("Timeout", "CrawlError"), "CrawlTimeoutError");
1168 assert_eq!(
1170 python_exception_name("ConnectionError", "CrawlError"),
1171 "CrawlConnectionError"
1172 );
1173 }
1174
1175 #[test]
1176 fn test_python_exception_name_no_double_prefix() {
1177 assert_eq!(
1179 python_exception_name("CrawlConnectionError", "CrawlError"),
1180 "CrawlConnectionError"
1181 );
1182 }
1183}