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