Skip to main content

alef_codegen/generators/
trait_bridge.rs

1//! Shared trait bridge code generation.
2//!
3//! Generates wrapper structs that allow foreign language objects (Python, JS, etc.)
4//! to implement Rust traits via FFI. Each backend implements [`TraitBridgeGenerator`]
5//! to provide language-specific dispatch logic; the shared functions in this module
6//! handle the structural boilerplate.
7
8use alef_core::config::TraitBridgeConfig;
9use alef_core::ir::{MethodDef, ParamDef, TypeDef};
10use heck::ToSnakeCase;
11use std::collections::HashMap;
12use std::fmt::Write;
13
14/// Everything needed to generate a trait bridge for one trait.
15pub struct TraitBridgeSpec<'a> {
16    /// The trait definition from the IR.
17    pub trait_def: &'a TypeDef,
18    /// Bridge configuration from `alef.toml`.
19    pub bridge_config: &'a TraitBridgeConfig,
20    /// Core crate import path (e.g., `"kreuzberg"`).
21    pub core_import: &'a str,
22    /// Language-specific prefix for the wrapper type (e.g., `"Python"`, `"Js"`, `"Wasm"`).
23    pub wrapper_prefix: &'a str,
24    /// Map of type name → fully-qualified Rust path for qualifying `Named` types.
25    pub type_paths: HashMap<String, String>,
26    /// The crate's error type name (e.g., `"KreuzbergError"`). Defaults to `"Error"`.
27    pub error_type: String,
28    /// Error constructor pattern. `{msg}` is replaced with the message expression.
29    pub error_constructor: String,
30}
31
32impl<'a> TraitBridgeSpec<'a> {
33    /// Fully qualified error type path (e.g., `"kreuzberg::KreuzbergError"`).
34    pub fn error_path(&self) -> String {
35        format!("{}::{}", self.core_import, self.error_type)
36    }
37
38    /// Generate an error construction expression from a message expression.
39    pub fn make_error(&self, msg_expr: &str) -> String {
40        self.error_constructor.replace("{msg}", msg_expr)
41    }
42
43    /// Wrapper struct name: `{prefix}{TraitName}Bridge` (e.g., `PythonOcrBackendBridge`).
44    pub fn wrapper_name(&self) -> String {
45        format!("{}{}Bridge", self.wrapper_prefix, self.trait_def.name)
46    }
47
48    /// Snake-case version of the trait name (e.g., `"ocr_backend"`).
49    pub fn trait_snake(&self) -> String {
50        self.trait_def.name.to_snake_case()
51    }
52
53    /// Full Rust path to the trait (e.g., `kreuzberg::OcrBackend`).
54    pub fn trait_path(&self) -> String {
55        self.trait_def.rust_path.replace('-', "_")
56    }
57
58    /// Methods that are required (no default impl) — must be provided by the foreign object.
59    pub fn required_methods(&self) -> Vec<&'a MethodDef> {
60        self.trait_def.methods.iter().filter(|m| !m.has_default_impl).collect()
61    }
62
63    /// Methods that have a default impl — optional on the foreign object.
64    pub fn optional_methods(&self) -> Vec<&'a MethodDef> {
65        self.trait_def.methods.iter().filter(|m| m.has_default_impl).collect()
66    }
67}
68
69/// Backend-specific trait bridge generation.
70///
71/// Each binding backend (PyO3, NAPI-RS, wasm-bindgen, etc.) implements this trait
72/// to provide the language-specific parts of bridge codegen. The shared functions
73/// in this module call these methods to fill in the backend-dependent pieces.
74pub trait TraitBridgeGenerator {
75    /// The type of the wrapped foreign object (e.g., `"Py<PyAny>"`, `"ThreadsafeFunction"`).
76    fn foreign_object_type(&self) -> &str;
77
78    /// Additional `use` imports needed for the bridge code.
79    fn bridge_imports(&self) -> Vec<String>;
80
81    /// Generate the body of a synchronous method bridge.
82    ///
83    /// The returned string is inserted inside the trait impl method. It should
84    /// call through to the foreign object and convert the result.
85    fn gen_sync_method_body(&self, method: &MethodDef, spec: &TraitBridgeSpec) -> String;
86
87    /// Generate the body of an async method bridge.
88    ///
89    /// The returned string is the body of a `Box::pin(async move { ... })` block.
90    fn gen_async_method_body(&self, method: &MethodDef, spec: &TraitBridgeSpec) -> String;
91
92    /// Generate the constructor body that validates and wraps the foreign object.
93    ///
94    /// Should check that the foreign object provides all required methods and
95    /// return `Self { ... }` on success.
96    fn gen_constructor(&self, spec: &TraitBridgeSpec) -> String;
97
98    /// Generate the complete registration function including attributes, signature, and body.
99    ///
100    /// Each backend needs different function signatures (PyO3 takes `py: Python`,
101    /// NAPI takes `#[napi]` with JS params, FFI takes `extern "C"` with raw pointers),
102    /// so the generator owns the full function.
103    fn gen_registration_fn(&self, spec: &TraitBridgeSpec) -> String;
104}
105
106// ---------------------------------------------------------------------------
107// Shared generation functions
108// ---------------------------------------------------------------------------
109
110/// Generate the wrapper struct holding the foreign object and cached fields.
111///
112/// Produces a struct like:
113/// ```ignore
114/// pub struct PythonOcrBackendBridge {
115///     inner: Py<PyAny>,
116///     cached_name: String,
117/// }
118/// ```
119pub fn gen_bridge_wrapper_struct(spec: &TraitBridgeSpec, generator: &dyn TraitBridgeGenerator) -> String {
120    let wrapper = spec.wrapper_name();
121    let foreign_type = generator.foreign_object_type();
122    let mut out = String::with_capacity(512);
123
124    writeln!(
125        out,
126        "/// Wrapper that bridges a foreign {prefix} object to the `{trait_name}` trait.",
127        prefix = spec.wrapper_prefix,
128        trait_name = spec.trait_def.name,
129    )
130    .ok();
131    writeln!(out, "pub struct {wrapper} {{").ok();
132    writeln!(out, "    inner: {foreign_type},").ok();
133    writeln!(out, "    cached_name: String,").ok();
134    write!(out, "}}").ok();
135    out
136}
137
138/// Generate `impl SuperTrait for Wrapper` when the bridge config specifies a super-trait.
139///
140/// Forwards `name()`, `version()`, `initialize()`, and `shutdown()` to the
141/// foreign object, using `cached_name` for `name()`.
142///
143/// The super-trait path is derived from the config's `super_trait` field. If it
144/// contains `::`, it's used as-is; otherwise it's qualified as `{core_import}::{super_trait}`.
145pub fn gen_bridge_plugin_impl(spec: &TraitBridgeSpec, generator: &dyn TraitBridgeGenerator) -> Option<String> {
146    let super_trait_name = spec.bridge_config.super_trait.as_deref()?;
147
148    let wrapper = spec.wrapper_name();
149    let core_import = spec.core_import;
150
151    // Derive the fully-qualified super-trait path
152    let super_trait_path = if super_trait_name.contains("::") {
153        super_trait_name.to_string()
154    } else {
155        format!("{core_import}::{super_trait_name}")
156    };
157
158    // Build synthetic MethodDefs for the Plugin methods and delegate to the generator
159    // for the actual call bodies. The Plugin trait interface is well-known: name(),
160    // version(), initialize(), shutdown().
161    let mut out = String::with_capacity(1024);
162    writeln!(out, "impl {super_trait_path} for {wrapper} {{").ok();
163
164    // name() -> &str — uses cached field
165    writeln!(out, "    fn name(&self) -> &str {{").ok();
166    writeln!(out, "        &self.cached_name").ok();
167    writeln!(out, "    }}").ok();
168    writeln!(out).ok();
169
170    let error_path = spec.error_path();
171
172    // version() -> String — delegate to foreign object
173    writeln!(out, "    fn version(&self) -> String {{").ok();
174    let version_method = MethodDef {
175        name: "version".to_string(),
176        params: vec![],
177        return_type: alef_core::ir::TypeRef::String,
178        is_async: false,
179        is_static: false,
180        error_type: None,
181        doc: String::new(),
182        receiver: Some(alef_core::ir::ReceiverKind::Ref),
183        sanitized: false,
184        trait_source: None,
185        returns_ref: false,
186        returns_cow: false,
187        return_newtype_wrapper: None,
188        has_default_impl: false,
189    };
190    let version_body = generator.gen_sync_method_body(&version_method, spec);
191    for line in version_body.lines() {
192        writeln!(out, "        {}", line.trim_start()).ok();
193    }
194    writeln!(out, "    }}").ok();
195    writeln!(out).ok();
196
197    // initialize() -> Result<(), ErrorType>
198    writeln!(out, "    fn initialize(&self) -> Result<(), {error_path}> {{").ok();
199    let init_method = MethodDef {
200        name: "initialize".to_string(),
201        params: vec![],
202        return_type: alef_core::ir::TypeRef::Unit,
203        is_async: false,
204        is_static: false,
205        error_type: Some(error_path.clone()),
206        doc: String::new(),
207        receiver: Some(alef_core::ir::ReceiverKind::Ref),
208        sanitized: false,
209        trait_source: None,
210        returns_ref: false,
211        returns_cow: false,
212        return_newtype_wrapper: None,
213        has_default_impl: true,
214    };
215    let init_body = generator.gen_sync_method_body(&init_method, spec);
216    for line in init_body.lines() {
217        writeln!(out, "        {}", line.trim_start()).ok();
218    }
219    writeln!(out, "    }}").ok();
220    writeln!(out).ok();
221
222    // shutdown() -> Result<(), ErrorType>
223    writeln!(out, "    fn shutdown(&self) -> Result<(), {error_path}> {{").ok();
224    let shutdown_method = MethodDef {
225        name: "shutdown".to_string(),
226        params: vec![],
227        return_type: alef_core::ir::TypeRef::Unit,
228        is_async: false,
229        is_static: false,
230        error_type: Some(error_path.clone()),
231        doc: String::new(),
232        receiver: Some(alef_core::ir::ReceiverKind::Ref),
233        sanitized: false,
234        trait_source: None,
235        returns_ref: false,
236        returns_cow: false,
237        return_newtype_wrapper: None,
238        has_default_impl: true,
239    };
240    let shutdown_body = generator.gen_sync_method_body(&shutdown_method, spec);
241    for line in shutdown_body.lines() {
242        writeln!(out, "        {}", line.trim_start()).ok();
243    }
244    writeln!(out, "    }}").ok();
245    write!(out, "}}").ok();
246    Some(out)
247}
248
249/// Generate `impl Trait for Wrapper` dispatching each method through the generator.
250///
251/// Every method on the trait (including those with `has_default_impl`) gets a
252/// generated body that forwards to the foreign object.
253pub fn gen_bridge_trait_impl(spec: &TraitBridgeSpec, generator: &dyn TraitBridgeGenerator) -> String {
254    let wrapper = spec.wrapper_name();
255    let trait_path = spec.trait_path();
256    let mut out = String::with_capacity(2048);
257
258    // Add #[async_trait] when the trait has async methods (needed for async_trait macro compatibility)
259    let has_async_methods = spec.trait_def.methods.iter().any(|m| m.is_async && m.trait_source.is_none());
260    if has_async_methods {
261        writeln!(out, "#[async_trait::async_trait]").ok();
262    }
263    writeln!(out, "impl {trait_path} for {wrapper} {{").ok();
264
265    // Filter out methods inherited from super-traits (they're handled by gen_bridge_plugin_impl)
266    let own_methods: Vec<_> = spec
267        .trait_def
268        .methods
269        .iter()
270        .filter(|m| m.trait_source.is_none())
271        .collect();
272
273    for (i, method) in own_methods.iter().enumerate() {
274        if i > 0 {
275            writeln!(out).ok();
276        }
277
278        // Build the method signature
279        let async_kw = if method.is_async { "async " } else { "" };
280        let receiver = match &method.receiver {
281            Some(alef_core::ir::ReceiverKind::Ref) => "&self",
282            Some(alef_core::ir::ReceiverKind::RefMut) => "&mut self",
283            Some(alef_core::ir::ReceiverKind::Owned) => "self",
284            None => "",
285        };
286
287        // Build params (excluding self), using format_param_type to respect is_ref/is_mut
288        let params: Vec<String> = method
289            .params
290            .iter()
291            .map(|p| format!("{}: {}", p.name, format_param_type(p, &spec.type_paths)))
292            .collect();
293
294        let all_params = if receiver.is_empty() {
295            params.join(", ")
296        } else if params.is_empty() {
297            receiver.to_string()
298        } else {
299            format!("{}, {}", receiver, params.join(", "))
300        };
301
302        // Return type — override the IR's error type with the configured crate error type
303        // so the impl matches the actual trait definition (the IR may extract a different
304        // error type like anyhow::Error from re-exports or type alias resolution).
305        let error_override = method.error_type.as_ref().map(|_| spec.error_path());
306        let ret = format_return_type(&method.return_type, error_override.as_deref(), &spec.type_paths);
307
308        writeln!(out, "    {async_kw}fn {}({all_params}) -> {ret} {{", method.name).ok();
309
310        // Generate body: async methods use Box::pin, sync methods call directly
311        let body = if method.is_async {
312            generator.gen_async_method_body(method, spec)
313        } else {
314            generator.gen_sync_method_body(method, spec)
315        };
316
317        for line in body.lines() {
318            writeln!(out, "        {line}").ok();
319        }
320        writeln!(out, "    }}").ok();
321    }
322
323    write!(out, "}}").ok();
324    out
325}
326
327/// Generate the `register_xxx()` function that wraps a foreign object and
328/// inserts it into the plugin registry.
329///
330/// Returns `None` when `bridge_config.register_fn` is absent (per-call bridge pattern).
331/// The generator owns the full function (attributes, signature, body) because each
332/// backend needs different signatures.
333pub fn gen_bridge_registration_fn(spec: &TraitBridgeSpec, generator: &dyn TraitBridgeGenerator) -> Option<String> {
334    spec.bridge_config.register_fn.as_deref()?;
335    Some(generator.gen_registration_fn(spec))
336}
337
338/// Result of trait bridge generation: imports (to be added via `builder.add_import`)
339/// and the code body (to be added via `builder.add_item`).
340pub struct BridgeOutput {
341    /// Import paths (e.g., `"std::sync::Arc"`) — callers should add via `builder.add_import()`.
342    pub imports: Vec<String>,
343    /// The generated code (struct, impls, registration fn).
344    pub code: String,
345}
346
347/// Generate the complete trait bridge code block: struct, impls, and
348/// optionally a registration function.
349///
350/// Returns [`BridgeOutput`] with imports separated from code so callers can
351/// route imports through `builder.add_import()` (which deduplicates).
352pub fn gen_bridge_all(spec: &TraitBridgeSpec, generator: &dyn TraitBridgeGenerator) -> BridgeOutput {
353    let imports = generator.bridge_imports();
354    let mut out = String::with_capacity(4096);
355
356    // Wrapper struct
357    out.push_str(&gen_bridge_wrapper_struct(spec, generator));
358    writeln!(out).ok();
359    writeln!(out).ok();
360
361    // Constructor (impl block with new())
362    out.push_str(&generator.gen_constructor(spec));
363    writeln!(out).ok();
364    writeln!(out).ok();
365
366    // Plugin super-trait impl (if applicable)
367    if let Some(plugin_impl) = gen_bridge_plugin_impl(spec, generator) {
368        out.push_str(&plugin_impl);
369        writeln!(out).ok();
370        writeln!(out).ok();
371    }
372
373    // Trait impl
374    out.push_str(&gen_bridge_trait_impl(spec, generator));
375
376    // Registration function — only when register_fn is configured
377    if let Some(reg_fn_code) = gen_bridge_registration_fn(spec, generator) {
378        writeln!(out).ok();
379        writeln!(out).ok();
380        out.push_str(&reg_fn_code);
381    }
382
383    BridgeOutput { imports, code: out }
384}
385
386// ---------------------------------------------------------------------------
387// Helpers
388// ---------------------------------------------------------------------------
389
390/// Format a `TypeRef` as a Rust type string for use in trait method signatures.
391///
392/// `type_paths` qualifies `Named` types with their full Rust path (e.g., `"Config"` →
393/// `"kreuzberg::Config"`). If a name isn't in `type_paths`, it's used as-is.
394pub fn format_type_ref(ty: &alef_core::ir::TypeRef, type_paths: &HashMap<String, String>) -> String {
395    use alef_core::ir::{PrimitiveType, TypeRef};
396    match ty {
397        TypeRef::Primitive(p) => match p {
398            PrimitiveType::Bool => "bool",
399            PrimitiveType::U8 => "u8",
400            PrimitiveType::U16 => "u16",
401            PrimitiveType::U32 => "u32",
402            PrimitiveType::U64 => "u64",
403            PrimitiveType::I8 => "i8",
404            PrimitiveType::I16 => "i16",
405            PrimitiveType::I32 => "i32",
406            PrimitiveType::I64 => "i64",
407            PrimitiveType::F32 => "f32",
408            PrimitiveType::F64 => "f64",
409            PrimitiveType::Usize => "usize",
410            PrimitiveType::Isize => "isize",
411        }
412        .to_string(),
413        TypeRef::String => "String".to_string(),
414        TypeRef::Char => "char".to_string(),
415        TypeRef::Bytes => "Vec<u8>".to_string(),
416        TypeRef::Optional(inner) => format!("Option<{}>", format_type_ref(inner, type_paths)),
417        TypeRef::Vec(inner) => format!("Vec<{}>", format_type_ref(inner, type_paths)),
418        TypeRef::Map(k, v) => format!(
419            "std::collections::HashMap<{}, {}>",
420            format_type_ref(k, type_paths),
421            format_type_ref(v, type_paths)
422        ),
423        TypeRef::Named(name) => type_paths.get(name.as_str()).cloned().unwrap_or_else(|| name.clone()),
424        TypeRef::Path => "std::path::PathBuf".to_string(),
425        TypeRef::Unit => "()".to_string(),
426        TypeRef::Json => "serde_json::Value".to_string(),
427        TypeRef::Duration => "std::time::Duration".to_string(),
428    }
429}
430
431/// Format a return type, wrapping in `Result` when an error type is present.
432pub fn format_return_type(
433    ty: &alef_core::ir::TypeRef,
434    error_type: Option<&str>,
435    type_paths: &HashMap<String, String>,
436) -> String {
437    let inner = format_type_ref(ty, type_paths);
438    match error_type {
439        Some(err) => format!("Result<{inner}, {err}>"),
440        None => inner,
441    }
442}
443
444/// Format a parameter type, respecting `is_ref` and `is_mut` from the IR.
445///
446/// Unlike [`format_type_ref`], this function produces reference types when the
447/// original Rust parameter was a `&T` or `&mut T`:
448/// - `String + is_ref` → `&str`
449/// - `Bytes + is_ref` → `&[u8]`
450/// - `Path + is_ref` → `&std::path::Path`
451/// - `Vec<T> + is_ref` → `&[T]`
452/// - `Named(n) + is_ref` → `&{qualified_name}`
453pub fn format_param_type(param: &ParamDef, type_paths: &HashMap<String, String>) -> String {
454    use alef_core::ir::TypeRef;
455    if param.is_ref {
456        match &param.ty {
457            TypeRef::String => "&str".to_string(),
458            TypeRef::Bytes => "&[u8]".to_string(),
459            TypeRef::Path => "&std::path::Path".to_string(),
460            TypeRef::Vec(inner) => format!("&[{}]", format_type_ref(inner, type_paths)),
461            TypeRef::Named(name) => {
462                let qualified = type_paths.get(name.as_str()).cloned().unwrap_or_else(|| name.clone());
463                format!("&{qualified}")
464            }
465            // All other types are Copy/small — pass by value even when is_ref is set
466            other => format_type_ref(other, type_paths),
467        }
468    } else {
469        format_type_ref(&param.ty, type_paths)
470    }
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476    use alef_core::config::TraitBridgeConfig;
477    use alef_core::ir::{MethodDef, ParamDef, PrimitiveType, ReceiverKind, TypeDef, TypeRef};
478
479    // ---------------------------------------------------------------------------
480    // Test helpers
481    // ---------------------------------------------------------------------------
482
483    fn make_trait_bridge_config(super_trait: Option<&str>, register_fn: Option<&str>) -> TraitBridgeConfig {
484        TraitBridgeConfig {
485            trait_name: "OcrBackend".to_string(),
486            super_trait: super_trait.map(str::to_string),
487            registry_getter: None,
488            register_fn: register_fn.map(str::to_string),
489            type_alias: None,
490            param_name: None,
491        }
492    }
493
494    fn make_type_def(name: &str, rust_path: &str, methods: Vec<MethodDef>) -> TypeDef {
495        TypeDef {
496            name: name.to_string(),
497            rust_path: rust_path.to_string(),
498            original_rust_path: rust_path.to_string(),
499            fields: vec![],
500            methods,
501            is_opaque: true,
502            is_clone: false,
503            doc: String::new(),
504            cfg: None,
505            is_trait: true,
506            has_default: false,
507            has_stripped_cfg_fields: false,
508            is_return_type: false,
509            serde_rename_all: None,
510            has_serde: false,
511            super_traits: vec![],
512        }
513    }
514
515    fn make_method(
516        name: &str,
517        params: Vec<ParamDef>,
518        return_type: TypeRef,
519        is_async: bool,
520        has_default_impl: bool,
521        trait_source: Option<&str>,
522        error_type: Option<&str>,
523    ) -> MethodDef {
524        MethodDef {
525            name: name.to_string(),
526            params,
527            return_type,
528            is_async,
529            is_static: false,
530            error_type: error_type.map(str::to_string),
531            doc: String::new(),
532            receiver: Some(ReceiverKind::Ref),
533            sanitized: false,
534            trait_source: trait_source.map(str::to_string),
535            returns_ref: false,
536            returns_cow: false,
537            return_newtype_wrapper: None,
538            has_default_impl,
539        }
540    }
541
542    fn make_param(name: &str, ty: TypeRef, is_ref: bool) -> ParamDef {
543        ParamDef {
544            name: name.to_string(),
545            ty,
546            optional: false,
547            default: None,
548            sanitized: false,
549            typed_default: None,
550            is_ref,
551            is_mut: false,
552            newtype_wrapper: None,
553        }
554    }
555
556    fn make_spec<'a>(
557        trait_def: &'a TypeDef,
558        bridge_config: &'a TraitBridgeConfig,
559        wrapper_prefix: &'a str,
560        type_paths: HashMap<String, String>,
561    ) -> TraitBridgeSpec<'a> {
562        TraitBridgeSpec {
563            trait_def,
564            bridge_config,
565            core_import: "mylib",
566            wrapper_prefix,
567            type_paths,
568            error_type: "MyError".to_string(),
569            error_constructor: "MyError::from({msg})".to_string(),
570        }
571    }
572
573    // ---------------------------------------------------------------------------
574    // Mock backend
575    // ---------------------------------------------------------------------------
576
577    struct MockBridgeGenerator;
578
579    impl TraitBridgeGenerator for MockBridgeGenerator {
580        fn foreign_object_type(&self) -> &str {
581            "Py<PyAny>"
582        }
583
584        fn bridge_imports(&self) -> Vec<String> {
585            vec!["pyo3::prelude::*".to_string(), "pyo3::types::PyString".to_string()]
586        }
587
588        fn gen_sync_method_body(&self, method: &MethodDef, _spec: &TraitBridgeSpec) -> String {
589            format!("// sync body for {}", method.name)
590        }
591
592        fn gen_async_method_body(&self, method: &MethodDef, _spec: &TraitBridgeSpec) -> String {
593            format!("// async body for {}", method.name)
594        }
595
596        fn gen_constructor(&self, spec: &TraitBridgeSpec) -> String {
597            format!(
598                "impl {} {{\n    pub fn new(obj: Py<PyAny>) -> Self {{ Self {{ inner: obj, cached_name: String::new() }} }}\n}}",
599                spec.wrapper_name()
600            )
601        }
602
603        fn gen_registration_fn(&self, spec: &TraitBridgeSpec) -> String {
604            let fn_name = spec.bridge_config.register_fn.as_deref().unwrap_or("register");
605            format!("pub fn {fn_name}(obj: Py<PyAny>) {{ /* register */ }}")
606        }
607    }
608
609    // ---------------------------------------------------------------------------
610    // TraitBridgeSpec helpers
611    // ---------------------------------------------------------------------------
612
613    #[test]
614    fn test_wrapper_name() {
615        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
616        let config = make_trait_bridge_config(None, None);
617        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
618        assert_eq!(spec.wrapper_name(), "PyOcrBackendBridge");
619    }
620
621    #[test]
622    fn test_trait_snake() {
623        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
624        let config = make_trait_bridge_config(None, None);
625        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
626        assert_eq!(spec.trait_snake(), "ocr_backend");
627    }
628
629    #[test]
630    fn test_trait_path_replaces_hyphens() {
631        let trait_def = make_type_def("OcrBackend", "my-lib::OcrBackend", vec![]);
632        let config = make_trait_bridge_config(None, None);
633        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
634        assert_eq!(spec.trait_path(), "my_lib::OcrBackend");
635    }
636
637    #[test]
638    fn test_required_methods_filters_no_default_impl() {
639        let methods = vec![
640            make_method("process", vec![], TypeRef::String, false, false, None, None),
641            make_method("initialize", vec![], TypeRef::Unit, false, true, None, None),
642            make_method("detect", vec![], TypeRef::String, false, false, None, None),
643        ];
644        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", methods);
645        let config = make_trait_bridge_config(None, None);
646        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
647        let required = spec.required_methods();
648        assert_eq!(required.len(), 2);
649        assert!(required.iter().any(|m| m.name == "process"));
650        assert!(required.iter().any(|m| m.name == "detect"));
651    }
652
653    #[test]
654    fn test_optional_methods_filters_has_default_impl() {
655        let methods = vec![
656            make_method("process", vec![], TypeRef::String, false, false, None, None),
657            make_method("initialize", vec![], TypeRef::Unit, false, true, None, None),
658            make_method("shutdown", vec![], TypeRef::Unit, false, true, None, None),
659        ];
660        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", methods);
661        let config = make_trait_bridge_config(None, None);
662        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
663        let optional = spec.optional_methods();
664        assert_eq!(optional.len(), 2);
665        assert!(optional.iter().any(|m| m.name == "initialize"));
666        assert!(optional.iter().any(|m| m.name == "shutdown"));
667    }
668
669    #[test]
670    fn test_error_path() {
671        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
672        let config = make_trait_bridge_config(None, None);
673        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
674        assert_eq!(spec.error_path(), "mylib::MyError");
675    }
676
677    // ---------------------------------------------------------------------------
678    // format_type_ref
679    // ---------------------------------------------------------------------------
680
681    #[test]
682    fn test_format_type_ref_primitives() {
683        let paths = HashMap::new();
684        let cases: Vec<(TypeRef, &str)> = vec![
685            (TypeRef::Primitive(PrimitiveType::Bool), "bool"),
686            (TypeRef::Primitive(PrimitiveType::U8), "u8"),
687            (TypeRef::Primitive(PrimitiveType::U16), "u16"),
688            (TypeRef::Primitive(PrimitiveType::U32), "u32"),
689            (TypeRef::Primitive(PrimitiveType::U64), "u64"),
690            (TypeRef::Primitive(PrimitiveType::I8), "i8"),
691            (TypeRef::Primitive(PrimitiveType::I16), "i16"),
692            (TypeRef::Primitive(PrimitiveType::I32), "i32"),
693            (TypeRef::Primitive(PrimitiveType::I64), "i64"),
694            (TypeRef::Primitive(PrimitiveType::F32), "f32"),
695            (TypeRef::Primitive(PrimitiveType::F64), "f64"),
696            (TypeRef::Primitive(PrimitiveType::Usize), "usize"),
697            (TypeRef::Primitive(PrimitiveType::Isize), "isize"),
698        ];
699        for (ty, expected) in cases {
700            assert_eq!(format_type_ref(&ty, &paths), expected, "mismatch for {expected}");
701        }
702    }
703
704    #[test]
705    fn test_format_type_ref_string() {
706        assert_eq!(format_type_ref(&TypeRef::String, &HashMap::new()), "String");
707    }
708
709    #[test]
710    fn test_format_type_ref_char() {
711        assert_eq!(format_type_ref(&TypeRef::Char, &HashMap::new()), "char");
712    }
713
714    #[test]
715    fn test_format_type_ref_bytes() {
716        assert_eq!(format_type_ref(&TypeRef::Bytes, &HashMap::new()), "Vec<u8>");
717    }
718
719    #[test]
720    fn test_format_type_ref_path() {
721        assert_eq!(format_type_ref(&TypeRef::Path, &HashMap::new()), "std::path::PathBuf");
722    }
723
724    #[test]
725    fn test_format_type_ref_unit() {
726        assert_eq!(format_type_ref(&TypeRef::Unit, &HashMap::new()), "()");
727    }
728
729    #[test]
730    fn test_format_type_ref_json() {
731        assert_eq!(format_type_ref(&TypeRef::Json, &HashMap::new()), "serde_json::Value");
732    }
733
734    #[test]
735    fn test_format_type_ref_duration() {
736        assert_eq!(
737            format_type_ref(&TypeRef::Duration, &HashMap::new()),
738            "std::time::Duration"
739        );
740    }
741
742    #[test]
743    fn test_format_type_ref_optional() {
744        let ty = TypeRef::Optional(Box::new(TypeRef::String));
745        assert_eq!(format_type_ref(&ty, &HashMap::new()), "Option<String>");
746    }
747
748    #[test]
749    fn test_format_type_ref_optional_nested() {
750        let ty = TypeRef::Optional(Box::new(TypeRef::Optional(Box::new(TypeRef::Primitive(
751            PrimitiveType::U32,
752        )))));
753        assert_eq!(format_type_ref(&ty, &HashMap::new()), "Option<Option<u32>>");
754    }
755
756    #[test]
757    fn test_format_type_ref_vec() {
758        let ty = TypeRef::Vec(Box::new(TypeRef::Primitive(PrimitiveType::U8)));
759        assert_eq!(format_type_ref(&ty, &HashMap::new()), "Vec<u8>");
760    }
761
762    #[test]
763    fn test_format_type_ref_vec_nested() {
764        let ty = TypeRef::Vec(Box::new(TypeRef::Vec(Box::new(TypeRef::String))));
765        assert_eq!(format_type_ref(&ty, &HashMap::new()), "Vec<Vec<String>>");
766    }
767
768    #[test]
769    fn test_format_type_ref_map() {
770        let ty = TypeRef::Map(
771            Box::new(TypeRef::String),
772            Box::new(TypeRef::Primitive(PrimitiveType::I64)),
773        );
774        assert_eq!(
775            format_type_ref(&ty, &HashMap::new()),
776            "std::collections::HashMap<String, i64>"
777        );
778    }
779
780    #[test]
781    fn test_format_type_ref_map_nested_value() {
782        let ty = TypeRef::Map(
783            Box::new(TypeRef::String),
784            Box::new(TypeRef::Vec(Box::new(TypeRef::String))),
785        );
786        assert_eq!(
787            format_type_ref(&ty, &HashMap::new()),
788            "std::collections::HashMap<String, Vec<String>>"
789        );
790    }
791
792    #[test]
793    fn test_format_type_ref_named_without_type_paths() {
794        let ty = TypeRef::Named("Config".to_string());
795        assert_eq!(format_type_ref(&ty, &HashMap::new()), "Config");
796    }
797
798    #[test]
799    fn test_format_type_ref_named_with_type_paths() {
800        let ty = TypeRef::Named("Config".to_string());
801        let mut paths = HashMap::new();
802        paths.insert("Config".to_string(), "mylib::Config".to_string());
803        assert_eq!(format_type_ref(&ty, &paths), "mylib::Config");
804    }
805
806    #[test]
807    fn test_format_type_ref_named_not_in_type_paths_falls_back_to_name() {
808        let ty = TypeRef::Named("Unknown".to_string());
809        let mut paths = HashMap::new();
810        paths.insert("Other".to_string(), "mylib::Other".to_string());
811        assert_eq!(format_type_ref(&ty, &paths), "Unknown");
812    }
813
814    // ---------------------------------------------------------------------------
815    // format_param_type
816    // ---------------------------------------------------------------------------
817
818    #[test]
819    fn test_format_param_type_string_ref() {
820        let param = make_param("input", TypeRef::String, true);
821        assert_eq!(format_param_type(&param, &HashMap::new()), "&str");
822    }
823
824    #[test]
825    fn test_format_param_type_string_owned() {
826        let param = make_param("input", TypeRef::String, false);
827        assert_eq!(format_param_type(&param, &HashMap::new()), "String");
828    }
829
830    #[test]
831    fn test_format_param_type_bytes_ref() {
832        let param = make_param("data", TypeRef::Bytes, true);
833        assert_eq!(format_param_type(&param, &HashMap::new()), "&[u8]");
834    }
835
836    #[test]
837    fn test_format_param_type_bytes_owned() {
838        let param = make_param("data", TypeRef::Bytes, false);
839        assert_eq!(format_param_type(&param, &HashMap::new()), "Vec<u8>");
840    }
841
842    #[test]
843    fn test_format_param_type_path_ref() {
844        let param = make_param("path", TypeRef::Path, true);
845        assert_eq!(format_param_type(&param, &HashMap::new()), "&std::path::Path");
846    }
847
848    #[test]
849    fn test_format_param_type_path_owned() {
850        let param = make_param("path", TypeRef::Path, false);
851        assert_eq!(format_param_type(&param, &HashMap::new()), "std::path::PathBuf");
852    }
853
854    #[test]
855    fn test_format_param_type_vec_ref() {
856        let param = make_param("items", TypeRef::Vec(Box::new(TypeRef::String)), true);
857        assert_eq!(format_param_type(&param, &HashMap::new()), "&[String]");
858    }
859
860    #[test]
861    fn test_format_param_type_vec_owned() {
862        let param = make_param("items", TypeRef::Vec(Box::new(TypeRef::String)), false);
863        assert_eq!(format_param_type(&param, &HashMap::new()), "Vec<String>");
864    }
865
866    #[test]
867    fn test_format_param_type_named_ref_with_type_paths() {
868        let mut paths = HashMap::new();
869        paths.insert("Config".to_string(), "mylib::Config".to_string());
870        let param = make_param("cfg", TypeRef::Named("Config".to_string()), true);
871        assert_eq!(format_param_type(&param, &paths), "&mylib::Config");
872    }
873
874    #[test]
875    fn test_format_param_type_named_ref_without_type_paths() {
876        let param = make_param("cfg", TypeRef::Named("Config".to_string()), true);
877        assert_eq!(format_param_type(&param, &HashMap::new()), "&Config");
878    }
879
880    #[test]
881    fn test_format_param_type_primitive_ref_passes_by_value() {
882        // Copy types like u32 are passed by value even when is_ref is set
883        let param = make_param("count", TypeRef::Primitive(PrimitiveType::U32), true);
884        assert_eq!(format_param_type(&param, &HashMap::new()), "u32");
885    }
886
887    #[test]
888    fn test_format_param_type_unit_ref_passes_by_value() {
889        let param = make_param("nothing", TypeRef::Unit, true);
890        assert_eq!(format_param_type(&param, &HashMap::new()), "()");
891    }
892
893    // ---------------------------------------------------------------------------
894    // format_return_type
895    // ---------------------------------------------------------------------------
896
897    #[test]
898    fn test_format_return_type_without_error() {
899        let result = format_return_type(&TypeRef::String, None, &HashMap::new());
900        assert_eq!(result, "String");
901    }
902
903    #[test]
904    fn test_format_return_type_with_error() {
905        let result = format_return_type(&TypeRef::String, Some("MyError"), &HashMap::new());
906        assert_eq!(result, "Result<String, MyError>");
907    }
908
909    #[test]
910    fn test_format_return_type_unit_with_error() {
911        let result = format_return_type(&TypeRef::Unit, Some("Box<dyn std::error::Error>"), &HashMap::new());
912        assert_eq!(result, "Result<(), Box<dyn std::error::Error>>");
913    }
914
915    #[test]
916    fn test_format_return_type_named_with_type_paths_and_error() {
917        let mut paths = HashMap::new();
918        paths.insert("Output".to_string(), "mylib::Output".to_string());
919        let result = format_return_type(&TypeRef::Named("Output".to_string()), Some("mylib::MyError"), &paths);
920        assert_eq!(result, "Result<mylib::Output, mylib::MyError>");
921    }
922
923    // ---------------------------------------------------------------------------
924    // gen_bridge_wrapper_struct
925    // ---------------------------------------------------------------------------
926
927    #[test]
928    fn test_gen_bridge_wrapper_struct_contains_struct_name() {
929        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
930        let config = make_trait_bridge_config(None, None);
931        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
932        let generator = MockBridgeGenerator;
933        let result = gen_bridge_wrapper_struct(&spec, &generator);
934        assert!(
935            result.contains("pub struct PyOcrBackendBridge"),
936            "missing struct declaration in:\n{result}"
937        );
938    }
939
940    #[test]
941    fn test_gen_bridge_wrapper_struct_contains_inner_field() {
942        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
943        let config = make_trait_bridge_config(None, None);
944        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
945        let generator = MockBridgeGenerator;
946        let result = gen_bridge_wrapper_struct(&spec, &generator);
947        assert!(result.contains("inner: Py<PyAny>"), "missing inner field in:\n{result}");
948    }
949
950    #[test]
951    fn test_gen_bridge_wrapper_struct_contains_cached_name() {
952        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
953        let config = make_trait_bridge_config(None, None);
954        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
955        let generator = MockBridgeGenerator;
956        let result = gen_bridge_wrapper_struct(&spec, &generator);
957        assert!(
958            result.contains("cached_name: String"),
959            "missing cached_name field in:\n{result}"
960        );
961    }
962
963    // ---------------------------------------------------------------------------
964    // gen_bridge_plugin_impl
965    // ---------------------------------------------------------------------------
966
967    #[test]
968    fn test_gen_bridge_plugin_impl_returns_none_when_no_super_trait() {
969        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
970        let config = make_trait_bridge_config(None, None);
971        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
972        let generator = MockBridgeGenerator;
973        assert!(gen_bridge_plugin_impl(&spec, &generator).is_none());
974    }
975
976    #[test]
977    fn test_gen_bridge_plugin_impl_returns_some_when_super_trait_configured() {
978        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
979        let config = make_trait_bridge_config(Some("Plugin"), None);
980        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
981        let generator = MockBridgeGenerator;
982        assert!(gen_bridge_plugin_impl(&spec, &generator).is_some());
983    }
984
985    #[test]
986    fn test_gen_bridge_plugin_impl_uses_qualified_super_trait_path() {
987        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
988        let config = make_trait_bridge_config(Some("Plugin"), None);
989        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
990        let generator = MockBridgeGenerator;
991        let result = gen_bridge_plugin_impl(&spec, &generator).unwrap();
992        assert!(
993            result.contains("impl mylib::Plugin for PyOcrBackendBridge"),
994            "missing qualified super-trait path in:\n{result}"
995        );
996    }
997
998    #[test]
999    fn test_gen_bridge_plugin_impl_uses_already_qualified_super_trait_path() {
1000        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1001        let config = make_trait_bridge_config(Some("other_crate::Plugin"), None);
1002        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1003        let generator = MockBridgeGenerator;
1004        let result = gen_bridge_plugin_impl(&spec, &generator).unwrap();
1005        assert!(
1006            result.contains("impl other_crate::Plugin for PyOcrBackendBridge"),
1007            "wrong super-trait path in:\n{result}"
1008        );
1009    }
1010
1011    #[test]
1012    fn test_gen_bridge_plugin_impl_contains_name_fn() {
1013        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1014        let config = make_trait_bridge_config(Some("Plugin"), None);
1015        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1016        let generator = MockBridgeGenerator;
1017        let result = gen_bridge_plugin_impl(&spec, &generator).unwrap();
1018        assert!(
1019            result.contains("fn name(") && result.contains("cached_name"),
1020            "missing name() using cached_name in:\n{result}"
1021        );
1022    }
1023
1024    #[test]
1025    fn test_gen_bridge_plugin_impl_contains_version_fn() {
1026        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1027        let config = make_trait_bridge_config(Some("Plugin"), None);
1028        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1029        let generator = MockBridgeGenerator;
1030        let result = gen_bridge_plugin_impl(&spec, &generator).unwrap();
1031        assert!(result.contains("fn version("), "missing version() in:\n{result}");
1032    }
1033
1034    #[test]
1035    fn test_gen_bridge_plugin_impl_contains_initialize_fn() {
1036        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1037        let config = make_trait_bridge_config(Some("Plugin"), None);
1038        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1039        let generator = MockBridgeGenerator;
1040        let result = gen_bridge_plugin_impl(&spec, &generator).unwrap();
1041        assert!(result.contains("fn initialize("), "missing initialize() in:\n{result}");
1042    }
1043
1044    #[test]
1045    fn test_gen_bridge_plugin_impl_contains_shutdown_fn() {
1046        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1047        let config = make_trait_bridge_config(Some("Plugin"), None);
1048        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1049        let generator = MockBridgeGenerator;
1050        let result = gen_bridge_plugin_impl(&spec, &generator).unwrap();
1051        assert!(result.contains("fn shutdown("), "missing shutdown() in:\n{result}");
1052    }
1053
1054    // ---------------------------------------------------------------------------
1055    // gen_bridge_trait_impl
1056    // ---------------------------------------------------------------------------
1057
1058    #[test]
1059    fn test_gen_bridge_trait_impl_includes_impl_header() {
1060        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1061        let config = make_trait_bridge_config(None, None);
1062        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1063        let generator = MockBridgeGenerator;
1064        let result = gen_bridge_trait_impl(&spec, &generator);
1065        assert!(
1066            result.contains("impl mylib::OcrBackend for PyOcrBackendBridge"),
1067            "missing impl header in:\n{result}"
1068        );
1069    }
1070
1071    #[test]
1072    fn test_gen_bridge_trait_impl_includes_method_signatures() {
1073        let methods = vec![make_method(
1074            "process",
1075            vec![],
1076            TypeRef::String,
1077            false,
1078            false,
1079            None,
1080            None,
1081        )];
1082        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", methods);
1083        let config = make_trait_bridge_config(None, None);
1084        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1085        let generator = MockBridgeGenerator;
1086        let result = gen_bridge_trait_impl(&spec, &generator);
1087        assert!(result.contains("fn process("), "missing method signature in:\n{result}");
1088    }
1089
1090    #[test]
1091    fn test_gen_bridge_trait_impl_includes_method_body_from_generator() {
1092        let methods = vec![make_method(
1093            "process",
1094            vec![],
1095            TypeRef::String,
1096            false,
1097            false,
1098            None,
1099            None,
1100        )];
1101        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", methods);
1102        let config = make_trait_bridge_config(None, None);
1103        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1104        let generator = MockBridgeGenerator;
1105        let result = gen_bridge_trait_impl(&spec, &generator);
1106        assert!(
1107            result.contains("// sync body for process"),
1108            "missing sync method body in:\n{result}"
1109        );
1110    }
1111
1112    #[test]
1113    fn test_gen_bridge_trait_impl_async_method_uses_async_body() {
1114        let methods = vec![make_method(
1115            "process_async",
1116            vec![],
1117            TypeRef::String,
1118            true,
1119            false,
1120            None,
1121            None,
1122        )];
1123        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", methods);
1124        let config = make_trait_bridge_config(None, None);
1125        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1126        let generator = MockBridgeGenerator;
1127        let result = gen_bridge_trait_impl(&spec, &generator);
1128        assert!(
1129            result.contains("// async body for process_async"),
1130            "missing async method body in:\n{result}"
1131        );
1132        assert!(
1133            result.contains("async fn process_async("),
1134            "missing async keyword in method signature in:\n{result}"
1135        );
1136    }
1137
1138    #[test]
1139    fn test_gen_bridge_trait_impl_filters_trait_source_methods() {
1140        // Methods with trait_source set come from super-traits and should be excluded
1141        let methods = vec![
1142            make_method("own_method", vec![], TypeRef::String, false, false, None, None),
1143            make_method(
1144                "inherited_method",
1145                vec![],
1146                TypeRef::String,
1147                false,
1148                false,
1149                Some("other_crate::OtherTrait"),
1150                None,
1151            ),
1152        ];
1153        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", methods);
1154        let config = make_trait_bridge_config(None, None);
1155        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1156        let generator = MockBridgeGenerator;
1157        let result = gen_bridge_trait_impl(&spec, &generator);
1158        assert!(
1159            result.contains("fn own_method("),
1160            "own method should be present in:\n{result}"
1161        );
1162        assert!(
1163            !result.contains("fn inherited_method("),
1164            "inherited method should be filtered out in:\n{result}"
1165        );
1166    }
1167
1168    #[test]
1169    fn test_gen_bridge_trait_impl_method_with_params() {
1170        let params = vec![
1171            make_param("input", TypeRef::String, true),
1172            make_param("count", TypeRef::Primitive(PrimitiveType::U32), false),
1173        ];
1174        let methods = vec![make_method(
1175            "process",
1176            params,
1177            TypeRef::String,
1178            false,
1179            false,
1180            None,
1181            None,
1182        )];
1183        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", methods);
1184        let config = make_trait_bridge_config(None, None);
1185        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1186        let generator = MockBridgeGenerator;
1187        let result = gen_bridge_trait_impl(&spec, &generator);
1188        assert!(result.contains("input: &str"), "missing &str param in:\n{result}");
1189        assert!(result.contains("count: u32"), "missing u32 param in:\n{result}");
1190    }
1191
1192    #[test]
1193    fn test_gen_bridge_trait_impl_return_type_with_error() {
1194        let methods = vec![make_method(
1195            "process",
1196            vec![],
1197            TypeRef::String,
1198            false,
1199            false,
1200            None,
1201            Some("MyError"),
1202        )];
1203        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", methods);
1204        let config = make_trait_bridge_config(None, None);
1205        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1206        let generator = MockBridgeGenerator;
1207        let result = gen_bridge_trait_impl(&spec, &generator);
1208        assert!(
1209            result.contains("-> Result<String, MyError>"),
1210            "missing Result return type in:\n{result}"
1211        );
1212    }
1213
1214    // ---------------------------------------------------------------------------
1215    // gen_bridge_registration_fn
1216    // ---------------------------------------------------------------------------
1217
1218    #[test]
1219    fn test_gen_bridge_registration_fn_returns_none_without_register_fn() {
1220        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1221        let config = make_trait_bridge_config(None, None);
1222        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1223        let generator = MockBridgeGenerator;
1224        assert!(gen_bridge_registration_fn(&spec, &generator).is_none());
1225    }
1226
1227    #[test]
1228    fn test_gen_bridge_registration_fn_returns_some_with_register_fn() {
1229        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1230        let config = make_trait_bridge_config(None, Some("register_ocr_backend"));
1231        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1232        let generator = MockBridgeGenerator;
1233        let result = gen_bridge_registration_fn(&spec, &generator);
1234        assert!(result.is_some());
1235        let code = result.unwrap();
1236        assert!(
1237            code.contains("register_ocr_backend"),
1238            "missing register fn name in:\n{code}"
1239        );
1240    }
1241
1242    // ---------------------------------------------------------------------------
1243    // gen_bridge_all
1244    // ---------------------------------------------------------------------------
1245
1246    #[test]
1247    fn test_gen_bridge_all_includes_imports() {
1248        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1249        let config = make_trait_bridge_config(None, None);
1250        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1251        let generator = MockBridgeGenerator;
1252        let output = gen_bridge_all(&spec, &generator);
1253        assert!(output.imports.contains(&"pyo3::prelude::*".to_string()));
1254        assert!(output.imports.contains(&"pyo3::types::PyString".to_string()));
1255    }
1256
1257    #[test]
1258    fn test_gen_bridge_all_includes_wrapper_struct() {
1259        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1260        let config = make_trait_bridge_config(None, None);
1261        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1262        let generator = MockBridgeGenerator;
1263        let output = gen_bridge_all(&spec, &generator);
1264        assert!(
1265            output.code.contains("pub struct PyOcrBackendBridge"),
1266            "missing struct in:\n{}",
1267            output.code
1268        );
1269    }
1270
1271    #[test]
1272    fn test_gen_bridge_all_includes_constructor() {
1273        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1274        let config = make_trait_bridge_config(None, None);
1275        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1276        let generator = MockBridgeGenerator;
1277        let output = gen_bridge_all(&spec, &generator);
1278        assert!(
1279            output.code.contains("pub fn new("),
1280            "missing constructor in:\n{}",
1281            output.code
1282        );
1283    }
1284
1285    #[test]
1286    fn test_gen_bridge_all_includes_trait_impl() {
1287        let methods = vec![make_method(
1288            "process",
1289            vec![],
1290            TypeRef::String,
1291            false,
1292            false,
1293            None,
1294            None,
1295        )];
1296        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", methods);
1297        let config = make_trait_bridge_config(None, None);
1298        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1299        let generator = MockBridgeGenerator;
1300        let output = gen_bridge_all(&spec, &generator);
1301        assert!(
1302            output.code.contains("impl mylib::OcrBackend for PyOcrBackendBridge"),
1303            "missing trait impl in:\n{}",
1304            output.code
1305        );
1306    }
1307
1308    #[test]
1309    fn test_gen_bridge_all_includes_plugin_impl_when_super_trait_set() {
1310        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1311        let config = make_trait_bridge_config(Some("Plugin"), None);
1312        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1313        let generator = MockBridgeGenerator;
1314        let output = gen_bridge_all(&spec, &generator);
1315        assert!(
1316            output.code.contains("impl mylib::Plugin for PyOcrBackendBridge"),
1317            "missing plugin impl in:\n{}",
1318            output.code
1319        );
1320    }
1321
1322    #[test]
1323    fn test_gen_bridge_all_no_plugin_impl_when_no_super_trait() {
1324        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1325        let config = make_trait_bridge_config(None, None);
1326        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1327        let generator = MockBridgeGenerator;
1328        let output = gen_bridge_all(&spec, &generator);
1329        assert!(
1330            !output.code.contains("fn name(") || !output.code.contains("cached_name"),
1331            "unexpected plugin impl present without super_trait"
1332        );
1333    }
1334
1335    #[test]
1336    fn test_gen_bridge_all_includes_registration_fn_when_configured() {
1337        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1338        let config = make_trait_bridge_config(None, Some("register_ocr_backend"));
1339        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1340        let generator = MockBridgeGenerator;
1341        let output = gen_bridge_all(&spec, &generator);
1342        assert!(
1343            output.code.contains("register_ocr_backend"),
1344            "missing registration fn in:\n{}",
1345            output.code
1346        );
1347    }
1348
1349    #[test]
1350    fn test_gen_bridge_all_no_registration_fn_when_absent() {
1351        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1352        let config = make_trait_bridge_config(None, None);
1353        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1354        let generator = MockBridgeGenerator;
1355        let output = gen_bridge_all(&spec, &generator);
1356        assert!(
1357            !output.code.contains("register_ocr_backend"),
1358            "unexpected registration fn present:\n{}",
1359            output.code
1360        );
1361    }
1362
1363    #[test]
1364    fn test_gen_bridge_all_ordering_struct_before_trait_impl() {
1365        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1366        let config = make_trait_bridge_config(None, None);
1367        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1368        let generator = MockBridgeGenerator;
1369        let output = gen_bridge_all(&spec, &generator);
1370        let struct_pos = output.code.find("pub struct PyOcrBackendBridge").unwrap();
1371        let impl_pos = output
1372            .code
1373            .find("impl mylib::OcrBackend for PyOcrBackendBridge")
1374            .unwrap();
1375        assert!(struct_pos < impl_pos, "struct should appear before trait impl");
1376    }
1377}