1pub fn gen_pyo3_error_types(error: &ErrorDef, module_name: &str, seen_exceptions: &mut AHashSet<String>) -> String {
2 let mut variant_names = Vec::new();
3 for variant in &error.variants {
4 let variant_name = python_exception_name(&variant.name, &error.name);
5 if seen_exceptions.insert(variant_name.clone()) {
6 variant_names.push(variant_name);
7 }
8 }
9
10 let include_base = seen_exceptions.insert(error.name.clone());
11
12 crate::codegen::template_env::render(
13 "error_gen/pyo3_error_types.jinja",
14 minijinja::context! {
15 variant_names => variant_names,
16 module_name => module_name,
17 error_name => error.name.as_str(),
18 include_base => include_base,
19 },
20 )
21}
22
23pub fn gen_pyo3_error_converter(error: &ErrorDef, core_import: &str) -> String {
32 let rust_path = if error.rust_path.is_empty() {
33 format!("{core_import}::{}", error.name)
34 } else {
35 let normalized = error.rust_path.replace('-', "_");
36 let segments: Vec<&str> = normalized.split("::").collect();
37 if segments.len() > 2 {
38 let crate_name = segments[0];
39 let error_name = segments[segments.len() - 1];
40 format!("{crate_name}::{error_name}")
41 } else {
42 normalized
43 }
44 };
45
46 let fn_name = format!("{}_to_py_err", to_snake_case(&error.name));
47 let has_methods = !error.methods.is_empty();
48
49 let ctor_args = |code: u32| -> String {
55 if has_methods {
56 format!("(msg, {code}u32, e.status_code(), e.is_transient(), e.error_type().to_string())")
57 } else {
58 "msg".to_string()
59 }
60 };
61
62 let mut arms = Vec::with_capacity(error.variants.len());
63 for variant in &error.variants {
64 let pattern = error_variant_wildcard_pattern(&rust_path, variant);
65 let variant_exc_name = python_exception_name(&variant.name, &error.name);
66 let code = variant
67 .taxonomy(&error.rust_path)
68 .map_or(crate::core::ir::ApiSurface::FFI_ERROR_CODE_UNKNOWN, |taxonomy| {
69 taxonomy.code
70 });
71 arms.push(format!(
72 "{pattern} => {variant_exc_name}::new_err({}),",
73 ctor_args(code)
74 ));
75 }
76 let default_arm = format!(
77 "_ => {}::new_err({}),",
78 error.name,
79 ctor_args(crate::core::ir::ApiSurface::FFI_ERROR_CODE_UNKNOWN)
80 );
81
82 crate::codegen::template_env::render(
83 "error_gen/pyo3_error_converter.jinja",
84 minijinja::context! {
85 rust_path => rust_path.as_str(),
86 fn_name => fn_name.as_str(),
87 arms => arms,
88 default_arm => default_arm.as_str(),
89 },
90 )
91}
92
93pub fn gen_pyo3_error_registration(error: &ErrorDef, seen_registrations: &mut AHashSet<String>) -> Vec<String> {
97 let mut registrations = Vec::with_capacity(error.variants.len() + 1);
98
99 for variant in &error.variants {
100 let variant_exc_name = python_exception_name(&variant.name, &error.name);
101 if seen_registrations.insert(variant_exc_name.clone()) {
102 registrations.push(format!(
103 " m.add(\"{}\", m.py().get_type::<{}>())?;",
104 variant_exc_name, variant_exc_name
105 ));
106 }
107 }
108
109 if seen_registrations.insert(error.name.clone()) {
110 registrations.push(format!(
111 " m.add(\"{}\", m.py().get_type::<{}>())?;",
112 error.name, error.name
113 ));
114 }
115
116 registrations
117}
118
119pub fn converter_fn_name(error: &ErrorDef) -> String {
121 format!("{}_to_py_err", to_snake_case(&error.name))
122}
123
124pub fn gen_pyo3_error_methods_impl(error: &ErrorDef) -> String {
125 if error.methods.is_empty() {
126 return String::new();
127 }
128
129 let struct_name = format!("{}Info", error.name);
130 let snake_name = to_snake_case(&error.name);
131 let fn_name = format!("{snake_name}_info");
132
133 let mut fields = vec![" pub code: u32,".to_string()];
134 let mut getters = vec![
135 concat!(
136 " /// Stable numeric error code identifying the specific error variant\n",
137 " /// (see the crate's FFI error taxonomy).\n",
138 " #[getter]\n",
139 " fn code(&self) -> u32 {\n",
140 " self.code\n",
141 " }",
142 )
143 .to_string(),
144 ];
145
146 let has_status_code = error.methods.iter().any(|m| m.name == "status_code");
147 let has_is_transient = error.methods.iter().any(|m| m.name == "is_transient");
148 let has_error_type = error.methods.iter().any(|m| m.name == "error_type");
149
150 if has_status_code {
151 fields.push(" pub status_code: u16,".to_string());
152 getters.push(
153 concat!(
154 " /// HTTP status code for this error (0 means no associated status).\n",
155 " #[getter]\n",
156 " fn status_code(&self) -> u16 {\n",
157 " self.status_code\n",
158 " }",
159 )
160 .to_string(),
161 );
162 }
163 if has_is_transient {
164 fields.push(" pub is_transient: bool,".to_string());
165 getters.push(
166 concat!(
167 " /// Returns `true` if the error is transient and a retry may succeed.\n",
168 " #[getter]\n",
169 " fn is_transient(&self) -> bool {\n",
170 " self.is_transient\n",
171 " }",
172 )
173 .to_string(),
174 );
175 }
176 if has_error_type {
177 fields.push(" pub error_type: String,".to_string());
178 getters.push(
179 concat!(
180 " /// Machine-readable error category string for matching and logging.\n",
181 " #[getter]\n",
182 " fn error_type(&self) -> String {\n",
183 " self.error_type.clone()\n",
184 " }",
185 )
186 .to_string(),
187 );
188 }
189 for method in &error.methods {
190 match method.name.as_str() {
191 "status_code" | "is_transient" | "error_type" => {}
192 other => getters.push(format!(
193 " // Not emitted: getter for method `{other}` on `{struct_name}`"
194 )),
195 }
196 }
197
198 let mut ctor_fields = vec![
199 " code: args\n\
200 \x20 .as_ref()\n\
201 \x20 .and_then(|a| a.get_item(1).ok())\n\
202 \x20 .and_then(|v| v.extract::<u32>().ok())\n\
203 \x20 .unwrap_or(0),",
204 ];
205 if has_status_code {
206 ctor_fields.push(
207 " status_code: args\n\
208 \x20 .as_ref()\n\
209 \x20 .and_then(|a| a.get_item(2).ok())\n\
210 \x20 .and_then(|v| v.extract::<u16>().ok())\n\
211 \x20 .unwrap_or(0),",
212 );
213 }
214 if has_is_transient {
215 ctor_fields.push(
216 " is_transient: args\n\
217 \x20 .as_ref()\n\
218 \x20 .and_then(|a| a.get_item(3).ok())\n\
219 \x20 .and_then(|v| v.extract::<bool>().ok())\n\
220 \x20 .unwrap_or(false),",
221 );
222 }
223 if has_error_type {
224 ctor_fields.push(
225 " error_type: args\n\
226 \x20 .as_ref()\n\
227 \x20 .and_then(|a| a.get_item(4).ok())\n\
228 \x20 .and_then(|v| v.extract::<String>().ok())\n\
229 \x20 .unwrap_or_default(),",
230 );
231 }
232
233 let struct_def = format!(
234 "#[pyclass(name = \"{struct_name}\")]\npub struct {struct_name} {{\n{}\n}}",
235 fields.join("\n")
236 );
237
238 let impl_block = format!("#[pymethods]\nimpl {struct_name} {{\n{}\n}}", getters.join("\n\n"));
239
240 let free_fn = format!(
241 "/// Build a `{struct_name}` from any exception raised by the `{error_name}` hierarchy.\n\
242 ///\n\
243 /// The converter stores `(message, code, status_code, is_transient, error_type)` in\n\
244 /// the exception args tuple; this function extracts those values at indices 1–4.\n\
245 #[pyfunction]\n\
246 pub fn {fn_name}(err: pyo3::Bound<'_, pyo3::types::PyAny>) -> {struct_name} {{\n\
247 let args = err.getattr(\"args\").ok();\n\
248 {struct_name} {{\n\
249 {ctor}\n\
250 }}\n\
251 }}",
252 error_name = error.name,
253 ctor = ctor_fields.join("\n"),
254 );
255
256 format!("{struct_def}\n\n{impl_block}\n\n{free_fn}")
257}
258
259pub fn pyo3_error_has_methods(error: &ErrorDef) -> bool {
262 !error.methods.is_empty()
263}
264
265pub fn pyo3_error_info_struct_name(error: &ErrorDef) -> String {
267 format!("{}Info", error.name)
268}
269
270pub fn pyo3_error_info_fn_name(error: &ErrorDef) -> String {
272 format!("{}_info", to_snake_case(&error.name))
273}
274
275use crate::core::ir::ErrorDef;
276use ahash::AHashSet;
281
282use super::shared::{error_variant_wildcard_pattern, python_exception_name, to_snake_case};