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