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::{BridgeBinding, TraitBridgeConfig};
9use alef_core::ir::{FieldDef, FunctionDef, MethodDef, ParamDef, PrimitiveType, TypeDef, TypeRef};
10use heck::ToSnakeCase;
11use std::collections::HashMap;
12
13/// Everything needed to generate a trait bridge for one trait.
14pub struct TraitBridgeSpec<'a> {
15    /// The trait definition from the IR.
16    pub trait_def: &'a TypeDef,
17    /// Bridge configuration from `alef.toml`.
18    pub bridge_config: &'a TraitBridgeConfig,
19    /// Core crate import path (e.g., `"kreuzberg"`).
20    pub core_import: &'a str,
21    /// Language-specific prefix for the wrapper type (e.g., `"Python"`, `"Js"`, `"Wasm"`).
22    pub wrapper_prefix: &'a str,
23    /// Map of type name → fully-qualified Rust path for qualifying `Named` types.
24    pub type_paths: HashMap<String, String>,
25    /// The crate's error type name (e.g., `"KreuzbergError"`). Defaults to `"Error"`.
26    pub error_type: String,
27    /// Error constructor pattern. `{msg}` is replaced with the message expression.
28    pub error_constructor: String,
29}
30
31impl<'a> TraitBridgeSpec<'a> {
32    /// Fully qualified error type path (e.g., `"kreuzberg::KreuzbergError"`).
33    ///
34    /// If `error_type` already looks fully-qualified (contains `::`) or is a generic
35    /// type expression (contains `<`), it is returned as-is without prefixing
36    /// `core_import`. This lets backends specify rich error types like
37    /// `"Box<dyn std::error::Error + Send + Sync>"` directly.
38    pub fn error_path(&self) -> String {
39        if self.error_type.contains("::") || self.error_type.contains('<') {
40            self.error_type.clone()
41        } else {
42            format!("{}::{}", self.core_import, self.error_type)
43        }
44    }
45
46    /// Generate an error construction expression from a message expression.
47    pub fn make_error(&self, msg_expr: &str) -> String {
48        self.error_constructor.replace("{msg}", msg_expr)
49    }
50
51    /// Wrapper struct name: `{prefix}{TraitName}Bridge` (e.g., `PythonOcrBackendBridge`).
52    pub fn wrapper_name(&self) -> String {
53        format!("{}{}Bridge", self.wrapper_prefix, self.trait_def.name)
54    }
55
56    /// Snake-case version of the trait name (e.g., `"ocr_backend"`).
57    pub fn trait_snake(&self) -> String {
58        self.trait_def.name.to_snake_case()
59    }
60
61    /// Full Rust path to the trait (e.g., `kreuzberg::OcrBackend`).
62    pub fn trait_path(&self) -> String {
63        self.trait_def.rust_path.replace('-', "_")
64    }
65
66    /// Methods that are required (no default impl) — must be provided by the foreign object.
67    pub fn required_methods(&self) -> Vec<&'a MethodDef> {
68        self.trait_def.methods.iter().filter(|m| !m.has_default_impl).collect()
69    }
70
71    /// Methods that have a default impl — optional on the foreign object.
72    pub fn optional_methods(&self) -> Vec<&'a MethodDef> {
73        self.trait_def.methods.iter().filter(|m| m.has_default_impl).collect()
74    }
75}
76
77/// Backend-specific trait bridge generation.
78///
79/// Each binding backend (PyO3, NAPI-RS, wasm-bindgen, etc.) implements this trait
80/// to provide the language-specific parts of bridge codegen. The shared functions
81/// in this module call these methods to fill in the backend-dependent pieces.
82pub trait TraitBridgeGenerator {
83    /// The type of the wrapped foreign object (e.g., `"Py<PyAny>"`, `"ThreadsafeFunction"`).
84    fn foreign_object_type(&self) -> &str;
85
86    /// Additional `use` imports needed for the bridge code.
87    fn bridge_imports(&self) -> Vec<String>;
88
89    /// Generate the body of a synchronous method bridge.
90    ///
91    /// The returned string is inserted inside the trait impl method. It should
92    /// call through to the foreign object and convert the result.
93    fn gen_sync_method_body(&self, method: &MethodDef, spec: &TraitBridgeSpec) -> String;
94
95    /// Generate the body of an async method bridge.
96    ///
97    /// The returned string is the body of a `Box::pin(async move { ... })` block.
98    fn gen_async_method_body(&self, method: &MethodDef, spec: &TraitBridgeSpec) -> String;
99
100    /// Generate the constructor body that validates and wraps the foreign object.
101    ///
102    /// Should check that the foreign object provides all required methods and
103    /// return `Self { ... }` on success.
104    fn gen_constructor(&self, spec: &TraitBridgeSpec) -> String;
105
106    /// Generate the complete registration function including attributes, signature, and body.
107    ///
108    /// Each backend needs different function signatures (PyO3 takes `py: Python`,
109    /// NAPI takes `#[napi]` with JS params, FFI takes `extern "C"` with raw pointers),
110    /// so the generator owns the full function.
111    fn gen_registration_fn(&self, spec: &TraitBridgeSpec) -> String;
112
113    /// Generate an unregistration function for the bridge.
114    ///
115    /// Default implementation returns an empty string — backends opt in by
116    /// emitting a function whose name is `spec.bridge_config.unregister_fn`
117    /// (when set) and whose body calls into the host crate's
118    /// `unregister_*(name)` plugin entry point.
119    fn gen_unregistration_fn(&self, _spec: &TraitBridgeSpec) -> String {
120        String::new()
121    }
122
123    /// Generate a clear-all-plugins function for the bridge.
124    ///
125    /// Default implementation returns an empty string — backends opt in by
126    /// emitting a function whose name is `spec.bridge_config.clear_fn`
127    /// (when set) and whose body calls into the host crate's `clear_*()`
128    /// plugin entry point. Typically used in test teardown.
129    fn gen_clear_fn(&self, _spec: &TraitBridgeSpec) -> String {
130        String::new()
131    }
132
133    /// Whether the `#[async_trait]` macro should require `Send` on its futures.
134    ///
135    /// Returns `true` (default) for most targets. WASM is single-threaded so its
136    /// trait bounds don't include `Send`; implementors should return `false` there.
137    fn async_trait_is_send(&self) -> bool {
138        true
139    }
140}
141
142// ---------------------------------------------------------------------------
143// Shared generation functions
144// ---------------------------------------------------------------------------
145
146/// Generate the wrapper struct holding the foreign object and cached fields.
147///
148/// Produces a struct like:
149/// ```ignore
150/// pub struct PythonOcrBackendBridge {
151///     inner: Py<PyAny>,
152///     cached_name: String,
153/// }
154/// ```
155pub fn gen_bridge_wrapper_struct(spec: &TraitBridgeSpec, generator: &dyn TraitBridgeGenerator) -> String {
156    let wrapper = spec.wrapper_name();
157    let foreign_type = generator.foreign_object_type();
158
159    crate::template_env::render(
160        "generators/trait_bridge/wrapper_struct.jinja",
161        minijinja::context! {
162            wrapper_prefix => spec.wrapper_prefix,
163            trait_name => &spec.trait_def.name,
164            wrapper_name => wrapper,
165            foreign_type => foreign_type,
166        },
167    )
168}
169
170/// Generate `impl std::fmt::Debug for Wrapper`.
171///
172/// Required by trait bounds on `Plugin` super-trait (and many others) that
173/// extend `Debug`. Without this, generic plugin-pattern bridges fail to
174/// compile when the user's trait has a `Debug` super-trait bound.
175fn gen_bridge_debug_impl(spec: &TraitBridgeSpec) -> String {
176    let wrapper = spec.wrapper_name();
177    format!(
178        "impl std::fmt::Debug for {wrapper} {{\n    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{\n        write!(f, \"{wrapper}\")\n    }}\n}}"
179    )
180}
181
182/// Generate `impl SuperTrait for Wrapper` when the bridge config specifies a super-trait.
183///
184/// Forwards `name()`, `version()`, `initialize()`, and `shutdown()` to the
185/// foreign object, using `cached_name` for `name()`.
186///
187/// The super-trait path is derived from the config's `super_trait` field. If it
188/// contains `::`, it's used as-is; otherwise it's qualified as `{core_import}::{super_trait}`.
189pub fn gen_bridge_plugin_impl(spec: &TraitBridgeSpec, generator: &dyn TraitBridgeGenerator) -> Option<String> {
190    let super_trait_name = spec.bridge_config.super_trait.as_deref()?;
191
192    let wrapper = spec.wrapper_name();
193    let core_import = spec.core_import;
194
195    // Derive the fully-qualified super-trait path
196    let super_trait_path = if super_trait_name.contains("::") {
197        super_trait_name.to_string()
198    } else {
199        format!("{core_import}::{super_trait_name}")
200    };
201
202    // Build synthetic MethodDefs for the Plugin methods and delegate to the generator
203    // for the actual call bodies. The Plugin trait interface is well-known: name(),
204    // version(), initialize(), shutdown().
205    let error_path = spec.error_path();
206
207    // version() -> String — delegate to foreign object
208    let version_method = MethodDef {
209        name: "version".to_string(),
210        params: vec![],
211        return_type: alef_core::ir::TypeRef::String,
212        is_async: false,
213        is_static: false,
214        error_type: None,
215        doc: String::new(),
216        receiver: Some(alef_core::ir::ReceiverKind::Ref),
217        sanitized: false,
218        trait_source: None,
219        returns_ref: false,
220        returns_cow: false,
221        return_newtype_wrapper: None,
222        has_default_impl: false,
223    };
224    let version_body = generator.gen_sync_method_body(&version_method, spec);
225
226    // initialize() -> Result<(), ErrorType>
227    let init_method = MethodDef {
228        name: "initialize".to_string(),
229        params: vec![],
230        return_type: alef_core::ir::TypeRef::Unit,
231        is_async: false,
232        is_static: false,
233        error_type: Some(error_path.clone()),
234        doc: String::new(),
235        receiver: Some(alef_core::ir::ReceiverKind::Ref),
236        sanitized: false,
237        trait_source: None,
238        returns_ref: false,
239        returns_cow: false,
240        return_newtype_wrapper: None,
241        has_default_impl: true,
242    };
243    let init_body = generator.gen_sync_method_body(&init_method, spec);
244
245    // shutdown() -> Result<(), ErrorType>
246    let shutdown_method = MethodDef {
247        name: "shutdown".to_string(),
248        params: vec![],
249        return_type: alef_core::ir::TypeRef::Unit,
250        is_async: false,
251        is_static: false,
252        error_type: Some(error_path.clone()),
253        doc: String::new(),
254        receiver: Some(alef_core::ir::ReceiverKind::Ref),
255        sanitized: false,
256        trait_source: None,
257        returns_ref: false,
258        returns_cow: false,
259        return_newtype_wrapper: None,
260        has_default_impl: true,
261    };
262    let shutdown_body = generator.gen_sync_method_body(&shutdown_method, spec);
263
264    // Split method bodies into lines for template iteration
265    let version_lines: Vec<&str> = version_body.lines().collect();
266    let init_lines: Vec<&str> = init_body.lines().collect();
267    let shutdown_lines: Vec<&str> = shutdown_body.lines().collect();
268
269    Some(crate::template_env::render(
270        "generators/trait_bridge/plugin_impl.jinja",
271        minijinja::context! {
272            super_trait_path => super_trait_path,
273            wrapper_name => wrapper,
274            error_path => error_path,
275            version_lines => version_lines,
276            init_lines => init_lines,
277            shutdown_lines => shutdown_lines,
278        },
279    ))
280}
281
282/// Generate `impl Trait for Wrapper` dispatching each method through the generator.
283///
284/// Every method on the trait (including those with `has_default_impl`) gets a
285/// generated body that forwards to the foreign object.
286pub fn gen_bridge_trait_impl(spec: &TraitBridgeSpec, generator: &dyn TraitBridgeGenerator) -> String {
287    let wrapper = spec.wrapper_name();
288    let trait_path = spec.trait_path();
289
290    // Check if the trait has async methods (needed for async_trait macro compatibility).
291    let has_async_methods = spec
292        .trait_def
293        .methods
294        .iter()
295        .any(|m| m.is_async && m.trait_source.is_none());
296    let async_trait_is_send = generator.async_trait_is_send();
297
298    // Filter out methods inherited from super-traits (they're handled by gen_bridge_plugin_impl)
299    let own_methods: Vec<_> = spec
300        .trait_def
301        .methods
302        .iter()
303        .filter(|m| m.trait_source.is_none())
304        .collect();
305
306    // Build method code with proper indentation
307    let mut methods_code = String::with_capacity(1024);
308    for (i, method) in own_methods.iter().enumerate() {
309        if i > 0 {
310            methods_code.push_str("\n\n");
311        }
312
313        // Build the method signature
314        let async_kw = if method.is_async { "async " } else { "" };
315        let receiver = match &method.receiver {
316            Some(alef_core::ir::ReceiverKind::Ref) => "&self",
317            Some(alef_core::ir::ReceiverKind::RefMut) => "&mut self",
318            Some(alef_core::ir::ReceiverKind::Owned) => "self",
319            None => "",
320        };
321
322        // Build params (excluding self), using format_param_type to respect is_ref/is_mut
323        let params: Vec<String> = method
324            .params
325            .iter()
326            .map(|p| format!("{}: {}", p.name, format_param_type(p, &spec.type_paths)))
327            .collect();
328
329        let all_params = if receiver.is_empty() {
330            params.join(", ")
331        } else if params.is_empty() {
332            receiver.to_string()
333        } else {
334            format!("{}, {}", receiver, params.join(", "))
335        };
336
337        // Return type — override the IR's error type with the configured crate error type
338        // so the impl matches the actual trait definition (the IR may extract a different
339        // error type like anyhow::Error from re-exports or type alias resolution).
340        let error_override = method.error_type.as_ref().map(|_| spec.error_path());
341        let ret = format_return_type(&method.return_type, error_override.as_deref(), &spec.type_paths);
342
343        // Generate body: async methods use Box::pin, sync methods call directly
344        let body = if method.is_async {
345            generator.gen_async_method_body(method, spec)
346        } else {
347            generator.gen_sync_method_body(method, spec)
348        };
349
350        // Indent body lines
351        let indented_body = body
352            .lines()
353            .map(|line| format!("        {line}"))
354            .collect::<Vec<_>>()
355            .join("\n");
356
357        methods_code.push_str(&crate::template_env::render(
358            "generators/trait_bridge/trait_method.jinja",
359            minijinja::context! {
360                async_kw => async_kw,
361                method_name => &method.name,
362                all_params => all_params,
363                ret => ret,
364                indented_body => &indented_body,
365            },
366        ));
367    }
368
369    crate::template_env::render(
370        "generators/trait_bridge/trait_impl.jinja",
371        minijinja::context! {
372            has_async_methods => has_async_methods,
373            async_trait_is_send => async_trait_is_send,
374            trait_path => trait_path,
375            wrapper_name => wrapper,
376            methods_code => methods_code,
377        },
378    )
379}
380
381/// Generate the `register_xxx()` function that wraps a foreign object and
382/// inserts it into the plugin registry.
383///
384/// Returns `None` when `bridge_config.register_fn` is absent (per-call bridge pattern).
385/// The generator owns the full function (attributes, signature, body) because each
386/// backend needs different signatures.
387pub fn gen_bridge_registration_fn(spec: &TraitBridgeSpec, generator: &dyn TraitBridgeGenerator) -> Option<String> {
388    spec.bridge_config.register_fn.as_deref()?;
389    Some(generator.gen_registration_fn(spec))
390}
391
392/// Generate the `unregister_xxx(name)` function that removes a previously
393/// registered plugin from the registry.
394///
395/// Returns `None` when `bridge_config.unregister_fn` is absent or when the
396/// backend hasn't opted in (returns the empty string from
397/// [`TraitBridgeGenerator::gen_unregistration_fn`]).
398pub fn gen_bridge_unregistration_fn(spec: &TraitBridgeSpec, generator: &dyn TraitBridgeGenerator) -> Option<String> {
399    spec.bridge_config.unregister_fn.as_deref()?;
400    let body = generator.gen_unregistration_fn(spec);
401    if body.is_empty() { None } else { Some(body) }
402}
403
404/// Generate the `clear_xxx()` function that removes all registered plugins
405/// of this type.
406///
407/// Returns `None` when `bridge_config.clear_fn` is absent or when the
408/// backend hasn't opted in (returns the empty string from
409/// [`TraitBridgeGenerator::gen_clear_fn`]).
410pub fn gen_bridge_clear_fn(spec: &TraitBridgeSpec, generator: &dyn TraitBridgeGenerator) -> Option<String> {
411    spec.bridge_config.clear_fn.as_deref()?;
412    let body = generator.gen_clear_fn(spec);
413    if body.is_empty() { None } else { Some(body) }
414}
415
416/// Resolve the FQN of a host-crate plugin function (e.g.
417/// `kreuzberg::plugins::ocr::unregister_ocr_backend`) given the bridge's
418/// `registry_getter` path. The convention used by host crates is:
419///
420/// - `registry_getter = "kreuzberg::plugins::registry::get_ocr_backend_registry"`
421/// - top-level fn      = `kreuzberg::plugins::ocr::unregister_ocr_backend`
422///
423/// We rewrite `::registry::get_*_registry` to `::<sub>::<fn_name>` where
424/// `<sub>` is the trait submodule name (extracted from `_*_registry`).
425/// When the heuristic fails (no `registry_getter`, unexpected shape), we
426/// fall back to `{core_import}::plugins::{fn_name}` so the user can rely on
427/// a re-export.
428///
429/// Shared by every backend that opts in to `unregister_*`/`clear_*` codegen
430/// (pyo3, napi, magnus, php, rustler, gleam, extendr, dart, swift, kotlin,
431/// wasm). Replaces the duplicated `<lang>_host_function_path` helpers that
432/// each backend used to define.
433pub fn host_function_path(spec: &TraitBridgeSpec, fn_name: &str) -> String {
434    if let Some(getter) = spec.bridge_config.registry_getter.as_deref() {
435        let last = getter.rsplit("::").next().unwrap_or("");
436        if let Some(sub) = last.strip_prefix("get_").and_then(|s| s.strip_suffix("_registry")) {
437            let prefix_end = getter.len() - last.len();
438            let prefix = &getter[..prefix_end];
439            let prefix = prefix.trim_end_matches("registry::");
440            return format!("{prefix}{sub}::{fn_name}");
441        }
442    }
443    format!("{}::plugins::{}", spec.core_import, fn_name)
444}
445
446/// Result of trait bridge generation: imports (to be added via `builder.add_import`)
447/// and the code body (to be added via `builder.add_item`).
448pub struct BridgeOutput {
449    /// Import paths (e.g., `"std::sync::Arc"`) — callers should add via `builder.add_import()`.
450    pub imports: Vec<String>,
451    /// The generated code (struct, impls, registration fn).
452    pub code: String,
453}
454
455/// Generate the complete trait bridge code block: struct, impls, and
456/// optionally a registration function.
457///
458/// Returns [`BridgeOutput`] with imports separated from code so callers can
459/// route imports through `builder.add_import()` (which deduplicates).
460pub fn gen_bridge_all(spec: &TraitBridgeSpec, generator: &dyn TraitBridgeGenerator) -> BridgeOutput {
461    let imports = generator.bridge_imports();
462    let mut out = String::with_capacity(4096);
463
464    // Wrapper struct
465    out.push_str(&gen_bridge_wrapper_struct(spec, generator));
466    out.push_str("\n\n");
467
468    // Debug impl (required by Plugin super-trait Debug bound)
469    out.push_str(&gen_bridge_debug_impl(spec));
470    out.push_str("\n\n");
471
472    // Constructor (impl block with new())
473    out.push_str(&generator.gen_constructor(spec));
474    out.push_str("\n\n");
475
476    // Plugin super-trait impl (if applicable)
477    if let Some(plugin_impl) = gen_bridge_plugin_impl(spec, generator) {
478        out.push_str(&plugin_impl);
479        out.push_str("\n\n");
480    }
481
482    // Trait impl
483    out.push_str(&gen_bridge_trait_impl(spec, generator));
484
485    // Registration function — only when register_fn is configured
486    if let Some(reg_fn_code) = gen_bridge_registration_fn(spec, generator) {
487        out.push_str("\n\n");
488        out.push_str(&reg_fn_code);
489    }
490
491    // Unregistration function — only when unregister_fn is configured AND
492    // the backend has opted in (non-empty body).
493    if let Some(unreg_fn_code) = gen_bridge_unregistration_fn(spec, generator) {
494        out.push_str("\n\n");
495        out.push_str(&unreg_fn_code);
496    }
497
498    // Clear-all function — only when clear_fn is configured AND the backend
499    // has opted in (non-empty body).
500    if let Some(clear_fn_code) = gen_bridge_clear_fn(spec, generator) {
501        out.push_str("\n\n");
502        out.push_str(&clear_fn_code);
503    }
504
505    BridgeOutput { imports, code: out }
506}
507
508// ---------------------------------------------------------------------------
509// Helpers
510// ---------------------------------------------------------------------------
511
512/// Format a `TypeRef` as a Rust type string for use in trait method signatures.
513///
514/// `type_paths` qualifies `Named` types with their full Rust path (e.g., `"Config"` →
515/// `"kreuzberg::Config"`). If a name isn't in `type_paths`, it's used as-is.
516pub fn format_type_ref(ty: &alef_core::ir::TypeRef, type_paths: &HashMap<String, String>) -> String {
517    use alef_core::ir::{PrimitiveType, TypeRef};
518    match ty {
519        TypeRef::Primitive(p) => match p {
520            PrimitiveType::Bool => "bool",
521            PrimitiveType::U8 => "u8",
522            PrimitiveType::U16 => "u16",
523            PrimitiveType::U32 => "u32",
524            PrimitiveType::U64 => "u64",
525            PrimitiveType::I8 => "i8",
526            PrimitiveType::I16 => "i16",
527            PrimitiveType::I32 => "i32",
528            PrimitiveType::I64 => "i64",
529            PrimitiveType::F32 => "f32",
530            PrimitiveType::F64 => "f64",
531            PrimitiveType::Usize => "usize",
532            PrimitiveType::Isize => "isize",
533        }
534        .to_string(),
535        TypeRef::String => "String".to_string(),
536        TypeRef::Char => "char".to_string(),
537        TypeRef::Bytes => "Vec<u8>".to_string(),
538        TypeRef::Optional(inner) => format!("Option<{}>", format_type_ref(inner, type_paths)),
539        TypeRef::Vec(inner) => format!("Vec<{}>", format_type_ref(inner, type_paths)),
540        TypeRef::Map(k, v) => format!(
541            "std::collections::HashMap<{}, {}>",
542            format_type_ref(k, type_paths),
543            format_type_ref(v, type_paths)
544        ),
545        TypeRef::Named(name) => type_paths.get(name.as_str()).cloned().unwrap_or_else(|| name.clone()),
546        TypeRef::Path => "std::path::PathBuf".to_string(),
547        TypeRef::Unit => "()".to_string(),
548        TypeRef::Json => "serde_json::Value".to_string(),
549        TypeRef::Duration => "std::time::Duration".to_string(),
550    }
551}
552
553/// Format a return type, wrapping in `Result` when an error type is present.
554pub fn format_return_type(
555    ty: &alef_core::ir::TypeRef,
556    error_type: Option<&str>,
557    type_paths: &HashMap<String, String>,
558) -> String {
559    let inner = format_type_ref(ty, type_paths);
560    match error_type {
561        Some(err) => format!("std::result::Result<{inner}, {err}>"),
562        None => inner,
563    }
564}
565
566/// Format a parameter type, respecting `is_ref`, `is_mut`, and `optional` from the IR.
567///
568/// Unlike [`format_type_ref`], this function produces reference types when the
569/// original Rust parameter was a `&T` or `&mut T`, and wraps in `Option<>` when
570/// `param.optional` is true:
571/// - `String + is_ref` → `&str`
572/// - `String + is_ref + optional` → `Option<&str>`
573/// - `Bytes + is_ref` → `&[u8]`
574/// - `Path + is_ref` → `&std::path::Path`
575/// - `Vec<T> + is_ref` → `&[T]`
576/// - `Named(n) + is_ref` → `&{qualified_name}`
577pub fn format_param_type(param: &ParamDef, type_paths: &HashMap<String, String>) -> String {
578    use alef_core::ir::TypeRef;
579    let base = if param.is_ref {
580        let mutability = if param.is_mut { "mut " } else { "" };
581        match &param.ty {
582            TypeRef::String => format!("&{mutability}str"),
583            TypeRef::Bytes => format!("&{mutability}[u8]"),
584            TypeRef::Path => format!("&{mutability}std::path::Path"),
585            TypeRef::Vec(inner) => format!("&{mutability}[{}]", format_type_ref(inner, type_paths)),
586            TypeRef::Named(name) => {
587                let qualified = type_paths.get(name.as_str()).cloned().unwrap_or_else(|| name.clone());
588                format!("&{mutability}{qualified}")
589            }
590            TypeRef::Optional(inner) => {
591                // Preserve the Option wrapper but apply the ref transformation to the inner type.
592                // e.g. Option<String> + is_ref → Option<&str>
593                //      Option<Vec<T>> + is_ref → Option<&[T]>
594                let inner_type_str = match inner.as_ref() {
595                    TypeRef::String => format!("&{mutability}str"),
596                    TypeRef::Bytes => format!("&{mutability}[u8]"),
597                    TypeRef::Path => format!("&{mutability}std::path::Path"),
598                    TypeRef::Vec(v) => format!("&{mutability}[{}]", format_type_ref(v, type_paths)),
599                    TypeRef::Named(name) => {
600                        let qualified = type_paths.get(name.as_str()).cloned().unwrap_or_else(|| name.clone());
601                        format!("&{mutability}{qualified}")
602                    }
603                    // Primitives and other Copy types: pass by value inside Option
604                    other => format_type_ref(other, type_paths),
605                };
606                // Already wrapped in Option — return directly to avoid double-wrapping below.
607                return format!("Option<{inner_type_str}>");
608            }
609            // All other types are Copy/small — pass by value even when is_ref is set
610            other => format_type_ref(other, type_paths),
611        }
612    } else {
613        format_type_ref(&param.ty, type_paths)
614    };
615
616    // Wrap in Option<> when the parameter is optional (e.g. `title: Option<&str>`).
617    // The TypeRef::Optional arm above returns early, so this only fires for the
618    // `optional: true` IR flag pattern where ty is the unwrapped inner type.
619    if param.optional {
620        format!("Option<{base}>")
621    } else {
622        base
623    }
624}
625
626// ---------------------------------------------------------------------------
627// Shared helpers — used by all backend trait_bridge modules.
628// ---------------------------------------------------------------------------
629
630/// Map a Rust primitive to its type string.
631pub fn prim(p: &PrimitiveType) -> &'static str {
632    use PrimitiveType::*;
633    match p {
634        Bool => "bool",
635        U8 => "u8",
636        U16 => "u16",
637        U32 => "u32",
638        U64 => "u64",
639        I8 => "i8",
640        I16 => "i16",
641        I32 => "i32",
642        I64 => "i64",
643        F32 => "f32",
644        F64 => "f64",
645        Usize => "usize",
646        Isize => "isize",
647    }
648}
649
650/// Map a `TypeRef` to its Rust source type string for use in trait bridge method
651/// signatures. `ci` is the core import path (e.g. `"kreuzberg"`), `tp` maps
652/// type names to fully-qualified paths.
653pub fn bridge_param_type(ty: &TypeRef, ci: &str, is_ref: bool, tp: &HashMap<String, String>) -> String {
654    match ty {
655        TypeRef::Bytes if is_ref => "&[u8]".into(),
656        TypeRef::Bytes => "Vec<u8>".into(),
657        TypeRef::String if is_ref => "&str".into(),
658        TypeRef::String => "String".into(),
659        TypeRef::Path if is_ref => "&std::path::Path".into(),
660        TypeRef::Path => "std::path::PathBuf".into(),
661        TypeRef::Named(n) => {
662            let qualified = tp.get(n).cloned().unwrap_or_else(|| format!("{ci}::{n}"));
663            if is_ref { format!("&{qualified}") } else { qualified }
664        }
665        TypeRef::Vec(inner) => format!("Vec<{}>", bridge_param_type(inner, ci, false, tp)),
666        TypeRef::Optional(inner) => format!("Option<{}>", bridge_param_type(inner, ci, false, tp)),
667        TypeRef::Primitive(p) => prim(p).into(),
668        TypeRef::Unit => "()".into(),
669        TypeRef::Char => "char".into(),
670        TypeRef::Map(k, v) => format!(
671            "std::collections::HashMap<{}, {}>",
672            bridge_param_type(k, ci, false, tp),
673            bridge_param_type(v, ci, false, tp)
674        ),
675        TypeRef::Json => "serde_json::Value".into(),
676        TypeRef::Duration => "std::time::Duration".into(),
677    }
678}
679
680/// Map a visitor method parameter type to the correct Rust type string, handling
681/// IR quirks:
682/// - `ty=String, optional=true, is_ref=true` → `Option<&str>` (IR collapses `Option<&str>`)
683/// - `ty=Vec<T>, is_ref=true` → `&[T]` (IR collapses `&[T]`)
684/// - Everything else delegates to [`bridge_param_type`].
685pub fn visitor_param_type(ty: &TypeRef, is_ref: bool, optional: bool, tp: &HashMap<String, String>) -> String {
686    if optional && matches!(ty, TypeRef::String) && is_ref {
687        return "Option<&str>".to_string();
688    }
689    if is_ref {
690        if let TypeRef::Vec(inner) = ty {
691            let inner_str = bridge_param_type(inner, "", false, tp);
692            return format!("&[{inner_str}]");
693        }
694    }
695    bridge_param_type(ty, "", is_ref, tp)
696}
697
698/// Find the first function parameter that matches a trait bridge configuration
699/// (by type alias or parameter name).
700///
701/// Bridges configured with `bind_via = "options_field"` are skipped — they live on a
702/// struct field rather than directly as a parameter, and are returned by
703/// [`find_bridge_field`] instead.
704pub fn find_bridge_param<'a>(
705    func: &FunctionDef,
706    bridges: &'a [TraitBridgeConfig],
707) -> Option<(usize, &'a TraitBridgeConfig)> {
708    for (idx, param) in func.params.iter().enumerate() {
709        let named = match &param.ty {
710            TypeRef::Named(n) => Some(n.as_str()),
711            TypeRef::Optional(inner) => {
712                if let TypeRef::Named(n) = inner.as_ref() {
713                    Some(n.as_str())
714                } else {
715                    None
716                }
717            }
718            _ => None,
719        };
720        for bridge in bridges {
721            if bridge.bind_via != BridgeBinding::FunctionParam {
722                continue;
723            }
724            if let Some(type_name) = named {
725                if bridge.type_alias.as_deref() == Some(type_name) {
726                    return Some((idx, bridge));
727                }
728            }
729            if bridge.param_name.as_deref() == Some(param.name.as_str()) {
730                return Some((idx, bridge));
731            }
732        }
733    }
734    None
735}
736
737/// Match info for a trait bridge whose handle lives as a struct field
738/// (`bind_via = "options_field"`).
739#[derive(Debug, Clone)]
740pub struct BridgeFieldMatch<'a> {
741    /// Index of the function parameter that carries the owning struct.
742    pub param_index: usize,
743    /// Name of the parameter (e.g., `"options"`).
744    pub param_name: String,
745    /// IR type name of the parameter, with any `Option<>` wrapper unwrapped.
746    pub options_type: String,
747    /// True if the param is `Option<TypeName>` rather than `TypeName`.
748    pub param_is_optional: bool,
749    /// Name of the field on `options_type` that holds the bridge handle.
750    pub field_name: String,
751    /// The matching field definition (carries the field's `TypeRef`).
752    pub field: &'a FieldDef,
753    /// The bridge configuration that produced the match.
754    pub bridge: &'a TraitBridgeConfig,
755}
756
757/// Find the first function parameter whose IR type carries a bridge field
758/// (`bind_via = "options_field"`).
759///
760/// For each function parameter whose IR type is `Named(N)` or `Optional<Named(N)>`,
761/// look up `N` in `types`. If `N` matches any bridge's `options_type`, search its
762/// fields for one whose name matches the bridge's resolved options field (or whose
763/// type's `Named` alias matches the bridge's `type_alias`). Returns the first match.
764///
765/// Bridges configured with `bind_via = "function_param"` are skipped — those go
766/// through [`find_bridge_param`] instead.
767pub fn find_bridge_field<'a>(
768    func: &FunctionDef,
769    types: &'a [TypeDef],
770    bridges: &'a [TraitBridgeConfig],
771) -> Option<BridgeFieldMatch<'a>> {
772    fn unwrap_named(ty: &TypeRef) -> Option<(&str, bool)> {
773        match ty {
774            TypeRef::Named(n) => Some((n.as_str(), false)),
775            TypeRef::Optional(inner) => {
776                if let TypeRef::Named(n) = inner.as_ref() {
777                    Some((n.as_str(), true))
778                } else {
779                    None
780                }
781            }
782            _ => None,
783        }
784    }
785
786    for (idx, param) in func.params.iter().enumerate() {
787        let Some((type_name, is_optional)) = unwrap_named(&param.ty) else {
788            continue;
789        };
790        let Some(type_def) = types.iter().find(|t| t.name == type_name) else {
791            continue;
792        };
793        for bridge in bridges {
794            if bridge.bind_via != BridgeBinding::OptionsField {
795                continue;
796            }
797            if bridge.options_type.as_deref() != Some(type_name) {
798                continue;
799            }
800            let field_name = bridge.resolved_options_field();
801            for field in &type_def.fields {
802                let matches_name = field_name.is_some_and(|n| field.name == n);
803                let matches_alias = bridge
804                    .type_alias
805                    .as_deref()
806                    .is_some_and(|alias| field_type_matches_alias(&field.ty, alias));
807                if matches_name || matches_alias {
808                    return Some(BridgeFieldMatch {
809                        param_index: idx,
810                        param_name: param.name.clone(),
811                        options_type: type_name.to_string(),
812                        param_is_optional: is_optional,
813                        field_name: field.name.clone(),
814                        field,
815                        bridge,
816                    });
817                }
818            }
819        }
820    }
821    None
822}
823
824/// True if `field_ty` references a `Named` type whose name equals `alias`,
825/// allowing for `Option<>` and `Vec<>` wrappers.
826fn field_type_matches_alias(field_ty: &TypeRef, alias: &str) -> bool {
827    match field_ty {
828        TypeRef::Named(n) => n == alias,
829        TypeRef::Optional(inner) | TypeRef::Vec(inner) => field_type_matches_alias(inner, alias),
830        _ => false,
831    }
832}
833
834/// Convert a snake_case string to camelCase.
835pub fn to_camel_case(s: &str) -> String {
836    let mut result = String::new();
837    let mut capitalize_next = false;
838    for ch in s.chars() {
839        if ch == '_' {
840            capitalize_next = true;
841        } else if capitalize_next {
842            result.push(ch.to_ascii_uppercase());
843            capitalize_next = false;
844        } else {
845            result.push(ch);
846        }
847    }
848    result
849}
850
851#[cfg(test)]
852mod tests {
853    use super::*;
854    use alef_core::config::TraitBridgeConfig;
855    use alef_core::ir::{MethodDef, ParamDef, PrimitiveType, ReceiverKind, TypeDef, TypeRef};
856
857    // ---------------------------------------------------------------------------
858    // Test helpers
859    // ---------------------------------------------------------------------------
860
861    fn make_trait_bridge_config(super_trait: Option<&str>, register_fn: Option<&str>) -> TraitBridgeConfig {
862        TraitBridgeConfig {
863            trait_name: "OcrBackend".to_string(),
864            super_trait: super_trait.map(str::to_string),
865            registry_getter: None,
866            register_fn: register_fn.map(str::to_string),
867            unregister_fn: None,
868            clear_fn: None,
869            type_alias: None,
870            param_name: None,
871            register_extra_args: None,
872            exclude_languages: Vec::new(),
873            bind_via: BridgeBinding::FunctionParam,
874            options_type: None,
875            options_field: None,
876        }
877    }
878
879    fn make_type_def(name: &str, rust_path: &str, methods: Vec<MethodDef>) -> TypeDef {
880        TypeDef {
881            name: name.to_string(),
882            rust_path: rust_path.to_string(),
883            original_rust_path: rust_path.to_string(),
884            fields: vec![],
885            methods,
886            is_opaque: true,
887            is_clone: false,
888            is_copy: false,
889            doc: String::new(),
890            cfg: None,
891            is_trait: true,
892            has_default: false,
893            has_stripped_cfg_fields: false,
894            is_return_type: false,
895            serde_rename_all: None,
896            has_serde: false,
897            super_traits: vec![],
898        }
899    }
900
901    fn make_method(
902        name: &str,
903        params: Vec<ParamDef>,
904        return_type: TypeRef,
905        is_async: bool,
906        has_default_impl: bool,
907        trait_source: Option<&str>,
908        error_type: Option<&str>,
909    ) -> MethodDef {
910        MethodDef {
911            name: name.to_string(),
912            params,
913            return_type,
914            is_async,
915            is_static: false,
916            error_type: error_type.map(str::to_string),
917            doc: String::new(),
918            receiver: Some(ReceiverKind::Ref),
919            sanitized: false,
920            trait_source: trait_source.map(str::to_string),
921            returns_ref: false,
922            returns_cow: false,
923            return_newtype_wrapper: None,
924            has_default_impl,
925        }
926    }
927
928    fn make_func(name: &str, params: Vec<ParamDef>) -> FunctionDef {
929        FunctionDef {
930            name: name.to_string(),
931            rust_path: format!("mylib::{name}"),
932            original_rust_path: String::new(),
933            params,
934            return_type: TypeRef::Unit,
935            is_async: false,
936            error_type: None,
937            doc: String::new(),
938            cfg: None,
939            sanitized: false,
940            return_sanitized: false,
941            returns_ref: false,
942            returns_cow: false,
943            return_newtype_wrapper: None,
944        }
945    }
946
947    fn make_field(name: &str, ty: TypeRef) -> FieldDef {
948        FieldDef {
949            name: name.to_string(),
950            ty,
951            optional: false,
952            default: None,
953            doc: String::new(),
954            sanitized: false,
955            is_boxed: false,
956            type_rust_path: None,
957            cfg: None,
958            typed_default: None,
959            core_wrapper: Default::default(),
960            vec_inner_core_wrapper: Default::default(),
961            newtype_wrapper: None,
962            serde_rename: None,
963        }
964    }
965
966    fn make_param(name: &str, ty: TypeRef, is_ref: bool) -> ParamDef {
967        ParamDef {
968            name: name.to_string(),
969            ty,
970            optional: false,
971            default: None,
972            sanitized: false,
973            typed_default: None,
974            is_ref,
975            is_mut: false,
976            newtype_wrapper: None,
977            original_type: None,
978        }
979    }
980
981    fn make_spec<'a>(
982        trait_def: &'a TypeDef,
983        bridge_config: &'a TraitBridgeConfig,
984        wrapper_prefix: &'a str,
985        type_paths: HashMap<String, String>,
986    ) -> TraitBridgeSpec<'a> {
987        TraitBridgeSpec {
988            trait_def,
989            bridge_config,
990            core_import: "mylib",
991            wrapper_prefix,
992            type_paths,
993            error_type: "MyError".to_string(),
994            error_constructor: "MyError::from({msg})".to_string(),
995        }
996    }
997
998    // ---------------------------------------------------------------------------
999    // Mock backend
1000    // ---------------------------------------------------------------------------
1001
1002    struct MockBridgeGenerator;
1003
1004    impl TraitBridgeGenerator for MockBridgeGenerator {
1005        fn foreign_object_type(&self) -> &str {
1006            "Py<PyAny>"
1007        }
1008
1009        fn bridge_imports(&self) -> Vec<String> {
1010            vec!["pyo3::prelude::*".to_string(), "pyo3::types::PyString".to_string()]
1011        }
1012
1013        fn gen_sync_method_body(&self, method: &MethodDef, _spec: &TraitBridgeSpec) -> String {
1014            format!("// sync body for {}", method.name)
1015        }
1016
1017        fn gen_async_method_body(&self, method: &MethodDef, _spec: &TraitBridgeSpec) -> String {
1018            format!("// async body for {}", method.name)
1019        }
1020
1021        fn gen_constructor(&self, spec: &TraitBridgeSpec) -> String {
1022            format!(
1023                "impl {} {{\n    pub fn new(obj: Py<PyAny>) -> Self {{ Self {{ inner: obj, cached_name: String::new() }} }}\n}}",
1024                spec.wrapper_name()
1025            )
1026        }
1027
1028        fn gen_registration_fn(&self, spec: &TraitBridgeSpec) -> String {
1029            let fn_name = spec.bridge_config.register_fn.as_deref().unwrap_or("register");
1030            format!("pub fn {fn_name}(obj: Py<PyAny>) {{ /* register */ }}")
1031        }
1032    }
1033
1034    // ---------------------------------------------------------------------------
1035    // TraitBridgeSpec helpers
1036    // ---------------------------------------------------------------------------
1037
1038    #[test]
1039    fn test_wrapper_name() {
1040        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1041        let config = make_trait_bridge_config(None, None);
1042        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1043        assert_eq!(spec.wrapper_name(), "PyOcrBackendBridge");
1044    }
1045
1046    #[test]
1047    fn test_trait_snake() {
1048        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1049        let config = make_trait_bridge_config(None, None);
1050        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1051        assert_eq!(spec.trait_snake(), "ocr_backend");
1052    }
1053
1054    #[test]
1055    fn test_trait_path_replaces_hyphens() {
1056        let trait_def = make_type_def("OcrBackend", "my-lib::OcrBackend", vec![]);
1057        let config = make_trait_bridge_config(None, None);
1058        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1059        assert_eq!(spec.trait_path(), "my_lib::OcrBackend");
1060    }
1061
1062    #[test]
1063    fn test_required_methods_filters_no_default_impl() {
1064        let methods = vec![
1065            make_method("process", vec![], TypeRef::String, false, false, None, None),
1066            make_method("initialize", vec![], TypeRef::Unit, false, true, None, None),
1067            make_method("detect", vec![], TypeRef::String, false, false, None, None),
1068        ];
1069        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", methods);
1070        let config = make_trait_bridge_config(None, None);
1071        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1072        let required = spec.required_methods();
1073        assert_eq!(required.len(), 2);
1074        assert!(required.iter().any(|m| m.name == "process"));
1075        assert!(required.iter().any(|m| m.name == "detect"));
1076    }
1077
1078    #[test]
1079    fn test_optional_methods_filters_has_default_impl() {
1080        let methods = vec![
1081            make_method("process", vec![], TypeRef::String, false, false, None, None),
1082            make_method("initialize", vec![], TypeRef::Unit, false, true, None, None),
1083            make_method("shutdown", vec![], TypeRef::Unit, false, true, None, None),
1084        ];
1085        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", methods);
1086        let config = make_trait_bridge_config(None, None);
1087        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1088        let optional = spec.optional_methods();
1089        assert_eq!(optional.len(), 2);
1090        assert!(optional.iter().any(|m| m.name == "initialize"));
1091        assert!(optional.iter().any(|m| m.name == "shutdown"));
1092    }
1093
1094    #[test]
1095    fn test_error_path() {
1096        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1097        let config = make_trait_bridge_config(None, None);
1098        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1099        assert_eq!(spec.error_path(), "mylib::MyError");
1100    }
1101
1102    // ---------------------------------------------------------------------------
1103    // format_type_ref
1104    // ---------------------------------------------------------------------------
1105
1106    #[test]
1107    fn test_format_type_ref_primitives() {
1108        let paths = HashMap::new();
1109        let cases: Vec<(TypeRef, &str)> = vec![
1110            (TypeRef::Primitive(PrimitiveType::Bool), "bool"),
1111            (TypeRef::Primitive(PrimitiveType::U8), "u8"),
1112            (TypeRef::Primitive(PrimitiveType::U16), "u16"),
1113            (TypeRef::Primitive(PrimitiveType::U32), "u32"),
1114            (TypeRef::Primitive(PrimitiveType::U64), "u64"),
1115            (TypeRef::Primitive(PrimitiveType::I8), "i8"),
1116            (TypeRef::Primitive(PrimitiveType::I16), "i16"),
1117            (TypeRef::Primitive(PrimitiveType::I32), "i32"),
1118            (TypeRef::Primitive(PrimitiveType::I64), "i64"),
1119            (TypeRef::Primitive(PrimitiveType::F32), "f32"),
1120            (TypeRef::Primitive(PrimitiveType::F64), "f64"),
1121            (TypeRef::Primitive(PrimitiveType::Usize), "usize"),
1122            (TypeRef::Primitive(PrimitiveType::Isize), "isize"),
1123        ];
1124        for (ty, expected) in cases {
1125            assert_eq!(format_type_ref(&ty, &paths), expected, "mismatch for {expected}");
1126        }
1127    }
1128
1129    #[test]
1130    fn test_format_type_ref_string() {
1131        assert_eq!(format_type_ref(&TypeRef::String, &HashMap::new()), "String");
1132    }
1133
1134    #[test]
1135    fn test_format_type_ref_char() {
1136        assert_eq!(format_type_ref(&TypeRef::Char, &HashMap::new()), "char");
1137    }
1138
1139    #[test]
1140    fn test_format_type_ref_bytes() {
1141        assert_eq!(format_type_ref(&TypeRef::Bytes, &HashMap::new()), "Vec<u8>");
1142    }
1143
1144    #[test]
1145    fn test_format_type_ref_path() {
1146        assert_eq!(format_type_ref(&TypeRef::Path, &HashMap::new()), "std::path::PathBuf");
1147    }
1148
1149    #[test]
1150    fn test_format_type_ref_unit() {
1151        assert_eq!(format_type_ref(&TypeRef::Unit, &HashMap::new()), "()");
1152    }
1153
1154    #[test]
1155    fn test_format_type_ref_json() {
1156        assert_eq!(format_type_ref(&TypeRef::Json, &HashMap::new()), "serde_json::Value");
1157    }
1158
1159    #[test]
1160    fn test_format_type_ref_duration() {
1161        assert_eq!(
1162            format_type_ref(&TypeRef::Duration, &HashMap::new()),
1163            "std::time::Duration"
1164        );
1165    }
1166
1167    #[test]
1168    fn test_format_type_ref_optional() {
1169        let ty = TypeRef::Optional(Box::new(TypeRef::String));
1170        assert_eq!(format_type_ref(&ty, &HashMap::new()), "Option<String>");
1171    }
1172
1173    #[test]
1174    fn test_format_type_ref_optional_nested() {
1175        let ty = TypeRef::Optional(Box::new(TypeRef::Optional(Box::new(TypeRef::Primitive(
1176            PrimitiveType::U32,
1177        )))));
1178        assert_eq!(format_type_ref(&ty, &HashMap::new()), "Option<Option<u32>>");
1179    }
1180
1181    #[test]
1182    fn test_format_type_ref_vec() {
1183        let ty = TypeRef::Vec(Box::new(TypeRef::Primitive(PrimitiveType::U8)));
1184        assert_eq!(format_type_ref(&ty, &HashMap::new()), "Vec<u8>");
1185    }
1186
1187    #[test]
1188    fn test_format_type_ref_vec_nested() {
1189        let ty = TypeRef::Vec(Box::new(TypeRef::Vec(Box::new(TypeRef::String))));
1190        assert_eq!(format_type_ref(&ty, &HashMap::new()), "Vec<Vec<String>>");
1191    }
1192
1193    #[test]
1194    fn test_format_type_ref_map() {
1195        let ty = TypeRef::Map(
1196            Box::new(TypeRef::String),
1197            Box::new(TypeRef::Primitive(PrimitiveType::I64)),
1198        );
1199        assert_eq!(
1200            format_type_ref(&ty, &HashMap::new()),
1201            "std::collections::HashMap<String, i64>"
1202        );
1203    }
1204
1205    #[test]
1206    fn test_format_type_ref_map_nested_value() {
1207        let ty = TypeRef::Map(
1208            Box::new(TypeRef::String),
1209            Box::new(TypeRef::Vec(Box::new(TypeRef::String))),
1210        );
1211        assert_eq!(
1212            format_type_ref(&ty, &HashMap::new()),
1213            "std::collections::HashMap<String, Vec<String>>"
1214        );
1215    }
1216
1217    #[test]
1218    fn test_format_type_ref_named_without_type_paths() {
1219        let ty = TypeRef::Named("Config".to_string());
1220        assert_eq!(format_type_ref(&ty, &HashMap::new()), "Config");
1221    }
1222
1223    #[test]
1224    fn test_format_type_ref_named_with_type_paths() {
1225        let ty = TypeRef::Named("Config".to_string());
1226        let mut paths = HashMap::new();
1227        paths.insert("Config".to_string(), "mylib::Config".to_string());
1228        assert_eq!(format_type_ref(&ty, &paths), "mylib::Config");
1229    }
1230
1231    #[test]
1232    fn test_format_type_ref_named_not_in_type_paths_falls_back_to_name() {
1233        let ty = TypeRef::Named("Unknown".to_string());
1234        let mut paths = HashMap::new();
1235        paths.insert("Other".to_string(), "mylib::Other".to_string());
1236        assert_eq!(format_type_ref(&ty, &paths), "Unknown");
1237    }
1238
1239    // ---------------------------------------------------------------------------
1240    // format_param_type
1241    // ---------------------------------------------------------------------------
1242
1243    #[test]
1244    fn test_format_param_type_string_ref() {
1245        let param = make_param("input", TypeRef::String, true);
1246        assert_eq!(format_param_type(&param, &HashMap::new()), "&str");
1247    }
1248
1249    #[test]
1250    fn test_format_param_type_string_owned() {
1251        let param = make_param("input", TypeRef::String, false);
1252        assert_eq!(format_param_type(&param, &HashMap::new()), "String");
1253    }
1254
1255    #[test]
1256    fn test_format_param_type_bytes_ref() {
1257        let param = make_param("data", TypeRef::Bytes, true);
1258        assert_eq!(format_param_type(&param, &HashMap::new()), "&[u8]");
1259    }
1260
1261    #[test]
1262    fn test_format_param_type_bytes_owned() {
1263        let param = make_param("data", TypeRef::Bytes, false);
1264        assert_eq!(format_param_type(&param, &HashMap::new()), "Vec<u8>");
1265    }
1266
1267    #[test]
1268    fn test_format_param_type_path_ref() {
1269        let param = make_param("path", TypeRef::Path, true);
1270        assert_eq!(format_param_type(&param, &HashMap::new()), "&std::path::Path");
1271    }
1272
1273    #[test]
1274    fn test_format_param_type_path_owned() {
1275        let param = make_param("path", TypeRef::Path, false);
1276        assert_eq!(format_param_type(&param, &HashMap::new()), "std::path::PathBuf");
1277    }
1278
1279    #[test]
1280    fn test_format_param_type_vec_ref() {
1281        let param = make_param("items", TypeRef::Vec(Box::new(TypeRef::String)), true);
1282        assert_eq!(format_param_type(&param, &HashMap::new()), "&[String]");
1283    }
1284
1285    #[test]
1286    fn test_format_param_type_vec_owned() {
1287        let param = make_param("items", TypeRef::Vec(Box::new(TypeRef::String)), false);
1288        assert_eq!(format_param_type(&param, &HashMap::new()), "Vec<String>");
1289    }
1290
1291    #[test]
1292    fn test_format_param_type_named_ref_with_type_paths() {
1293        let mut paths = HashMap::new();
1294        paths.insert("Config".to_string(), "mylib::Config".to_string());
1295        let param = make_param("cfg", TypeRef::Named("Config".to_string()), true);
1296        assert_eq!(format_param_type(&param, &paths), "&mylib::Config");
1297    }
1298
1299    #[test]
1300    fn test_format_param_type_named_ref_without_type_paths() {
1301        let param = make_param("cfg", TypeRef::Named("Config".to_string()), true);
1302        assert_eq!(format_param_type(&param, &HashMap::new()), "&Config");
1303    }
1304
1305    #[test]
1306    fn test_format_param_type_primitive_ref_passes_by_value() {
1307        // Copy types like u32 are passed by value even when is_ref is set
1308        let param = make_param("count", TypeRef::Primitive(PrimitiveType::U32), true);
1309        assert_eq!(format_param_type(&param, &HashMap::new()), "u32");
1310    }
1311
1312    #[test]
1313    fn test_format_param_type_unit_ref_passes_by_value() {
1314        let param = make_param("nothing", TypeRef::Unit, true);
1315        assert_eq!(format_param_type(&param, &HashMap::new()), "()");
1316    }
1317
1318    // ---------------------------------------------------------------------------
1319    // format_return_type
1320    // ---------------------------------------------------------------------------
1321
1322    #[test]
1323    fn test_format_return_type_without_error() {
1324        let result = format_return_type(&TypeRef::String, None, &HashMap::new());
1325        assert_eq!(result, "String");
1326    }
1327
1328    #[test]
1329    fn test_format_return_type_with_error() {
1330        let result = format_return_type(&TypeRef::String, Some("MyError"), &HashMap::new());
1331        assert_eq!(result, "std::result::Result<String, MyError>");
1332    }
1333
1334    #[test]
1335    fn test_format_return_type_unit_with_error() {
1336        let result = format_return_type(&TypeRef::Unit, Some("Box<dyn std::error::Error>"), &HashMap::new());
1337        assert_eq!(result, "std::result::Result<(), Box<dyn std::error::Error>>");
1338    }
1339
1340    #[test]
1341    fn test_format_return_type_named_with_type_paths_and_error() {
1342        let mut paths = HashMap::new();
1343        paths.insert("Output".to_string(), "mylib::Output".to_string());
1344        let result = format_return_type(&TypeRef::Named("Output".to_string()), Some("mylib::MyError"), &paths);
1345        assert_eq!(result, "std::result::Result<mylib::Output, mylib::MyError>");
1346    }
1347
1348    // ---------------------------------------------------------------------------
1349    // gen_bridge_wrapper_struct
1350    // ---------------------------------------------------------------------------
1351
1352    #[test]
1353    fn test_gen_bridge_wrapper_struct_contains_struct_name() {
1354        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1355        let config = make_trait_bridge_config(None, None);
1356        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1357        let generator = MockBridgeGenerator;
1358        let result = gen_bridge_wrapper_struct(&spec, &generator);
1359        assert!(
1360            result.contains("pub struct PyOcrBackendBridge"),
1361            "missing struct declaration in:\n{result}"
1362        );
1363    }
1364
1365    #[test]
1366    fn test_gen_bridge_wrapper_struct_contains_inner_field() {
1367        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1368        let config = make_trait_bridge_config(None, None);
1369        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1370        let generator = MockBridgeGenerator;
1371        let result = gen_bridge_wrapper_struct(&spec, &generator);
1372        assert!(result.contains("inner: Py<PyAny>"), "missing inner field in:\n{result}");
1373    }
1374
1375    #[test]
1376    fn test_gen_bridge_wrapper_struct_contains_cached_name() {
1377        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1378        let config = make_trait_bridge_config(None, None);
1379        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1380        let generator = MockBridgeGenerator;
1381        let result = gen_bridge_wrapper_struct(&spec, &generator);
1382        assert!(
1383            result.contains("cached_name: String"),
1384            "missing cached_name field in:\n{result}"
1385        );
1386    }
1387
1388    // ---------------------------------------------------------------------------
1389    // gen_bridge_plugin_impl
1390    // ---------------------------------------------------------------------------
1391
1392    #[test]
1393    fn test_gen_bridge_plugin_impl_returns_none_when_no_super_trait() {
1394        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1395        let config = make_trait_bridge_config(None, None);
1396        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1397        let generator = MockBridgeGenerator;
1398        assert!(gen_bridge_plugin_impl(&spec, &generator).is_none());
1399    }
1400
1401    #[test]
1402    fn test_gen_bridge_plugin_impl_returns_some_when_super_trait_configured() {
1403        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1404        let config = make_trait_bridge_config(Some("Plugin"), None);
1405        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1406        let generator = MockBridgeGenerator;
1407        assert!(gen_bridge_plugin_impl(&spec, &generator).is_some());
1408    }
1409
1410    #[test]
1411    fn test_gen_bridge_plugin_impl_uses_qualified_super_trait_path() {
1412        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1413        let config = make_trait_bridge_config(Some("Plugin"), None);
1414        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1415        let generator = MockBridgeGenerator;
1416        let result = gen_bridge_plugin_impl(&spec, &generator).unwrap();
1417        assert!(
1418            result.contains("impl mylib::Plugin for PyOcrBackendBridge"),
1419            "missing qualified super-trait path in:\n{result}"
1420        );
1421    }
1422
1423    #[test]
1424    fn test_gen_bridge_plugin_impl_uses_already_qualified_super_trait_path() {
1425        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1426        let config = make_trait_bridge_config(Some("other_crate::Plugin"), None);
1427        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1428        let generator = MockBridgeGenerator;
1429        let result = gen_bridge_plugin_impl(&spec, &generator).unwrap();
1430        assert!(
1431            result.contains("impl other_crate::Plugin for PyOcrBackendBridge"),
1432            "wrong super-trait path in:\n{result}"
1433        );
1434    }
1435
1436    #[test]
1437    fn test_gen_bridge_plugin_impl_contains_name_fn() {
1438        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1439        let config = make_trait_bridge_config(Some("Plugin"), None);
1440        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1441        let generator = MockBridgeGenerator;
1442        let result = gen_bridge_plugin_impl(&spec, &generator).unwrap();
1443        assert!(
1444            result.contains("fn name(") && result.contains("cached_name"),
1445            "missing name() using cached_name in:\n{result}"
1446        );
1447    }
1448
1449    #[test]
1450    fn test_gen_bridge_plugin_impl_contains_version_fn() {
1451        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1452        let config = make_trait_bridge_config(Some("Plugin"), None);
1453        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1454        let generator = MockBridgeGenerator;
1455        let result = gen_bridge_plugin_impl(&spec, &generator).unwrap();
1456        assert!(result.contains("fn version("), "missing version() in:\n{result}");
1457    }
1458
1459    #[test]
1460    fn test_gen_bridge_plugin_impl_contains_initialize_fn() {
1461        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1462        let config = make_trait_bridge_config(Some("Plugin"), None);
1463        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1464        let generator = MockBridgeGenerator;
1465        let result = gen_bridge_plugin_impl(&spec, &generator).unwrap();
1466        assert!(result.contains("fn initialize("), "missing initialize() in:\n{result}");
1467    }
1468
1469    #[test]
1470    fn test_gen_bridge_plugin_impl_contains_shutdown_fn() {
1471        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1472        let config = make_trait_bridge_config(Some("Plugin"), None);
1473        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1474        let generator = MockBridgeGenerator;
1475        let result = gen_bridge_plugin_impl(&spec, &generator).unwrap();
1476        assert!(result.contains("fn shutdown("), "missing shutdown() in:\n{result}");
1477    }
1478
1479    // ---------------------------------------------------------------------------
1480    // gen_bridge_trait_impl
1481    // ---------------------------------------------------------------------------
1482
1483    #[test]
1484    fn test_gen_bridge_trait_impl_includes_impl_header() {
1485        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1486        let config = make_trait_bridge_config(None, None);
1487        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1488        let generator = MockBridgeGenerator;
1489        let result = gen_bridge_trait_impl(&spec, &generator);
1490        assert!(
1491            result.contains("impl mylib::OcrBackend for PyOcrBackendBridge"),
1492            "missing impl header in:\n{result}"
1493        );
1494    }
1495
1496    #[test]
1497    fn test_gen_bridge_trait_impl_includes_method_signatures() {
1498        let methods = vec![make_method(
1499            "process",
1500            vec![],
1501            TypeRef::String,
1502            false,
1503            false,
1504            None,
1505            None,
1506        )];
1507        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", methods);
1508        let config = make_trait_bridge_config(None, None);
1509        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1510        let generator = MockBridgeGenerator;
1511        let result = gen_bridge_trait_impl(&spec, &generator);
1512        assert!(result.contains("fn process("), "missing method signature in:\n{result}");
1513    }
1514
1515    #[test]
1516    fn test_gen_bridge_trait_impl_includes_method_body_from_generator() {
1517        let methods = vec![make_method(
1518            "process",
1519            vec![],
1520            TypeRef::String,
1521            false,
1522            false,
1523            None,
1524            None,
1525        )];
1526        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", methods);
1527        let config = make_trait_bridge_config(None, None);
1528        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1529        let generator = MockBridgeGenerator;
1530        let result = gen_bridge_trait_impl(&spec, &generator);
1531        assert!(
1532            result.contains("// sync body for process"),
1533            "missing sync method body in:\n{result}"
1534        );
1535    }
1536
1537    #[test]
1538    fn test_gen_bridge_trait_impl_async_method_uses_async_body() {
1539        let methods = vec![make_method(
1540            "process_async",
1541            vec![],
1542            TypeRef::String,
1543            true,
1544            false,
1545            None,
1546            None,
1547        )];
1548        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", methods);
1549        let config = make_trait_bridge_config(None, None);
1550        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1551        let generator = MockBridgeGenerator;
1552        let result = gen_bridge_trait_impl(&spec, &generator);
1553        assert!(
1554            result.contains("// async body for process_async"),
1555            "missing async method body in:\n{result}"
1556        );
1557        assert!(
1558            result.contains("async fn process_async("),
1559            "missing async keyword in method signature in:\n{result}"
1560        );
1561    }
1562
1563    #[test]
1564    fn test_gen_bridge_trait_impl_filters_trait_source_methods() {
1565        // Methods with trait_source set come from super-traits and should be excluded
1566        let methods = vec![
1567            make_method("own_method", vec![], TypeRef::String, false, false, None, None),
1568            make_method(
1569                "inherited_method",
1570                vec![],
1571                TypeRef::String,
1572                false,
1573                false,
1574                Some("other_crate::OtherTrait"),
1575                None,
1576            ),
1577        ];
1578        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", methods);
1579        let config = make_trait_bridge_config(None, None);
1580        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1581        let generator = MockBridgeGenerator;
1582        let result = gen_bridge_trait_impl(&spec, &generator);
1583        assert!(
1584            result.contains("fn own_method("),
1585            "own method should be present in:\n{result}"
1586        );
1587        assert!(
1588            !result.contains("fn inherited_method("),
1589            "inherited method should be filtered out in:\n{result}"
1590        );
1591    }
1592
1593    #[test]
1594    fn test_gen_bridge_trait_impl_method_with_params() {
1595        let params = vec![
1596            make_param("input", TypeRef::String, true),
1597            make_param("count", TypeRef::Primitive(PrimitiveType::U32), false),
1598        ];
1599        let methods = vec![make_method(
1600            "process",
1601            params,
1602            TypeRef::String,
1603            false,
1604            false,
1605            None,
1606            None,
1607        )];
1608        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", methods);
1609        let config = make_trait_bridge_config(None, None);
1610        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1611        let generator = MockBridgeGenerator;
1612        let result = gen_bridge_trait_impl(&spec, &generator);
1613        assert!(result.contains("input: &str"), "missing &str param in:\n{result}");
1614        assert!(result.contains("count: u32"), "missing u32 param in:\n{result}");
1615    }
1616
1617    #[test]
1618    fn test_gen_bridge_trait_impl_return_type_with_error() {
1619        let methods = vec![make_method(
1620            "process",
1621            vec![],
1622            TypeRef::String,
1623            false,
1624            false,
1625            None,
1626            Some("MyError"),
1627        )];
1628        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", methods);
1629        let config = make_trait_bridge_config(None, None);
1630        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1631        let generator = MockBridgeGenerator;
1632        let result = gen_bridge_trait_impl(&spec, &generator);
1633        assert!(
1634            result.contains("-> std::result::Result<String, mylib::MyError>"),
1635            "missing std::result::Result return type in:\n{result}"
1636        );
1637    }
1638
1639    // ---------------------------------------------------------------------------
1640    // gen_bridge_registration_fn
1641    // ---------------------------------------------------------------------------
1642
1643    #[test]
1644    fn test_gen_bridge_registration_fn_returns_none_without_register_fn() {
1645        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1646        let config = make_trait_bridge_config(None, None);
1647        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1648        let generator = MockBridgeGenerator;
1649        assert!(gen_bridge_registration_fn(&spec, &generator).is_none());
1650    }
1651
1652    #[test]
1653    fn test_gen_bridge_registration_fn_returns_some_with_register_fn() {
1654        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1655        let config = make_trait_bridge_config(None, Some("register_ocr_backend"));
1656        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1657        let generator = MockBridgeGenerator;
1658        let result = gen_bridge_registration_fn(&spec, &generator);
1659        assert!(result.is_some());
1660        let code = result.unwrap();
1661        assert!(
1662            code.contains("register_ocr_backend"),
1663            "missing register fn name in:\n{code}"
1664        );
1665    }
1666
1667    // ---------------------------------------------------------------------------
1668    // gen_bridge_all
1669    // ---------------------------------------------------------------------------
1670
1671    #[test]
1672    fn test_gen_bridge_all_includes_imports() {
1673        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1674        let config = make_trait_bridge_config(None, None);
1675        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1676        let generator = MockBridgeGenerator;
1677        let output = gen_bridge_all(&spec, &generator);
1678        assert!(output.imports.contains(&"pyo3::prelude::*".to_string()));
1679        assert!(output.imports.contains(&"pyo3::types::PyString".to_string()));
1680    }
1681
1682    #[test]
1683    fn test_gen_bridge_all_includes_wrapper_struct() {
1684        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1685        let config = make_trait_bridge_config(None, None);
1686        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1687        let generator = MockBridgeGenerator;
1688        let output = gen_bridge_all(&spec, &generator);
1689        assert!(
1690            output.code.contains("pub struct PyOcrBackendBridge"),
1691            "missing struct in:\n{}",
1692            output.code
1693        );
1694    }
1695
1696    #[test]
1697    fn test_gen_bridge_all_includes_constructor() {
1698        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1699        let config = make_trait_bridge_config(None, None);
1700        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1701        let generator = MockBridgeGenerator;
1702        let output = gen_bridge_all(&spec, &generator);
1703        assert!(
1704            output.code.contains("pub fn new("),
1705            "missing constructor in:\n{}",
1706            output.code
1707        );
1708    }
1709
1710    #[test]
1711    fn test_gen_bridge_all_includes_trait_impl() {
1712        let methods = vec![make_method(
1713            "process",
1714            vec![],
1715            TypeRef::String,
1716            false,
1717            false,
1718            None,
1719            None,
1720        )];
1721        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", methods);
1722        let config = make_trait_bridge_config(None, None);
1723        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1724        let generator = MockBridgeGenerator;
1725        let output = gen_bridge_all(&spec, &generator);
1726        assert!(
1727            output.code.contains("impl mylib::OcrBackend for PyOcrBackendBridge"),
1728            "missing trait impl in:\n{}",
1729            output.code
1730        );
1731    }
1732
1733    #[test]
1734    fn test_gen_bridge_all_includes_plugin_impl_when_super_trait_set() {
1735        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1736        let config = make_trait_bridge_config(Some("Plugin"), None);
1737        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1738        let generator = MockBridgeGenerator;
1739        let output = gen_bridge_all(&spec, &generator);
1740        assert!(
1741            output.code.contains("impl mylib::Plugin for PyOcrBackendBridge"),
1742            "missing plugin impl in:\n{}",
1743            output.code
1744        );
1745    }
1746
1747    #[test]
1748    fn test_gen_bridge_all_no_plugin_impl_when_no_super_trait() {
1749        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1750        let config = make_trait_bridge_config(None, None);
1751        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1752        let generator = MockBridgeGenerator;
1753        let output = gen_bridge_all(&spec, &generator);
1754        assert!(
1755            !output.code.contains("fn name(") || !output.code.contains("cached_name"),
1756            "unexpected plugin impl present without super_trait"
1757        );
1758    }
1759
1760    #[test]
1761    fn test_gen_bridge_all_includes_registration_fn_when_configured() {
1762        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1763        let config = make_trait_bridge_config(None, Some("register_ocr_backend"));
1764        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1765        let generator = MockBridgeGenerator;
1766        let output = gen_bridge_all(&spec, &generator);
1767        assert!(
1768            output.code.contains("register_ocr_backend"),
1769            "missing registration fn in:\n{}",
1770            output.code
1771        );
1772    }
1773
1774    #[test]
1775    fn test_gen_bridge_all_no_registration_fn_when_absent() {
1776        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1777        let config = make_trait_bridge_config(None, None);
1778        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1779        let generator = MockBridgeGenerator;
1780        let output = gen_bridge_all(&spec, &generator);
1781        assert!(
1782            !output.code.contains("register_ocr_backend"),
1783            "unexpected registration fn present:\n{}",
1784            output.code
1785        );
1786    }
1787
1788    #[test]
1789    fn test_gen_bridge_all_ordering_struct_before_trait_impl() {
1790        let trait_def = make_type_def("OcrBackend", "mylib::OcrBackend", vec![]);
1791        let config = make_trait_bridge_config(None, None);
1792        let spec = make_spec(&trait_def, &config, "Py", HashMap::new());
1793        let generator = MockBridgeGenerator;
1794        let output = gen_bridge_all(&spec, &generator);
1795        let struct_pos = output.code.find("pub struct PyOcrBackendBridge").unwrap();
1796        let impl_pos = output
1797            .code
1798            .find("impl mylib::OcrBackend for PyOcrBackendBridge")
1799            .unwrap();
1800        assert!(struct_pos < impl_pos, "struct should appear before trait impl");
1801    }
1802
1803    // ---------------------------------------------------------------------------
1804    // find_bridge_param / find_bridge_field
1805    // ---------------------------------------------------------------------------
1806
1807    fn make_bridge(
1808        type_alias: Option<&str>,
1809        param_name: Option<&str>,
1810        bind_via: BridgeBinding,
1811        options_type: Option<&str>,
1812        options_field: Option<&str>,
1813    ) -> TraitBridgeConfig {
1814        TraitBridgeConfig {
1815            trait_name: "HtmlVisitor".to_string(),
1816            super_trait: None,
1817            registry_getter: None,
1818            register_fn: None,
1819            unregister_fn: None,
1820            clear_fn: None,
1821            type_alias: type_alias.map(str::to_string),
1822            param_name: param_name.map(str::to_string),
1823            register_extra_args: None,
1824            exclude_languages: vec![],
1825            bind_via,
1826            options_type: options_type.map(str::to_string),
1827            options_field: options_field.map(str::to_string),
1828        }
1829    }
1830
1831    #[test]
1832    fn find_bridge_param_returns_first_param_match_in_function_param_mode() {
1833        let func = make_func(
1834            "convert",
1835            vec![
1836                make_param("html", TypeRef::String, true),
1837                make_param("visitor", TypeRef::Named("VisitorHandle".to_string()), false),
1838            ],
1839        );
1840        let bridges = vec![make_bridge(
1841            Some("VisitorHandle"),
1842            Some("visitor"),
1843            BridgeBinding::FunctionParam,
1844            None,
1845            None,
1846        )];
1847        let result = find_bridge_param(&func, &bridges).expect("bridge match");
1848        assert_eq!(result.0, 1);
1849    }
1850
1851    #[test]
1852    fn find_bridge_param_skips_options_field_bridges() {
1853        let func = make_func(
1854            "convert",
1855            vec![
1856                make_param("html", TypeRef::String, true),
1857                make_param("visitor", TypeRef::Named("VisitorHandle".to_string()), false),
1858            ],
1859        );
1860        let bridges = vec![make_bridge(
1861            Some("VisitorHandle"),
1862            Some("visitor"),
1863            BridgeBinding::OptionsField,
1864            Some("ConversionOptions"),
1865            Some("visitor"),
1866        )];
1867        assert!(
1868            find_bridge_param(&func, &bridges).is_none(),
1869            "bridges configured with bind_via=options_field must not be returned by find_bridge_param"
1870        );
1871    }
1872
1873    #[test]
1874    fn find_bridge_field_detects_field_via_alias() {
1875        let opts_type = TypeDef {
1876            name: "ConversionOptions".to_string(),
1877            rust_path: "mylib::ConversionOptions".to_string(),
1878            original_rust_path: String::new(),
1879            fields: vec![
1880                make_field("debug", TypeRef::Primitive(PrimitiveType::Bool)),
1881                make_field(
1882                    "visitor",
1883                    TypeRef::Optional(Box::new(TypeRef::Named("VisitorHandle".to_string()))),
1884                ),
1885            ],
1886            methods: vec![],
1887            is_opaque: false,
1888            is_clone: true,
1889            is_copy: false,
1890            doc: String::new(),
1891            cfg: None,
1892            is_trait: false,
1893            has_default: true,
1894            has_stripped_cfg_fields: false,
1895            is_return_type: false,
1896            serde_rename_all: None,
1897            has_serde: false,
1898            super_traits: vec![],
1899        };
1900        let func = make_func(
1901            "convert",
1902            vec![
1903                make_param("html", TypeRef::String, true),
1904                make_param(
1905                    "options",
1906                    TypeRef::Optional(Box::new(TypeRef::Named("ConversionOptions".to_string()))),
1907                    false,
1908                ),
1909            ],
1910        );
1911        let bridges = vec![make_bridge(
1912            Some("VisitorHandle"),
1913            Some("visitor"),
1914            BridgeBinding::OptionsField,
1915            Some("ConversionOptions"),
1916            None,
1917        )];
1918        let m = find_bridge_field(&func, std::slice::from_ref(&opts_type), &bridges).expect("bridge field match");
1919        assert_eq!(m.param_index, 1);
1920        assert_eq!(m.param_name, "options");
1921        assert_eq!(m.options_type, "ConversionOptions");
1922        assert!(m.param_is_optional);
1923        assert_eq!(m.field_name, "visitor");
1924    }
1925
1926    #[test]
1927    fn find_bridge_field_returns_none_for_function_param_bridge() {
1928        let opts_type = TypeDef {
1929            name: "ConversionOptions".to_string(),
1930            rust_path: "mylib::ConversionOptions".to_string(),
1931            original_rust_path: String::new(),
1932            fields: vec![make_field(
1933                "visitor",
1934                TypeRef::Optional(Box::new(TypeRef::Named("VisitorHandle".to_string()))),
1935            )],
1936            methods: vec![],
1937            is_opaque: false,
1938            is_clone: true,
1939            is_copy: false,
1940            doc: String::new(),
1941            cfg: None,
1942            is_trait: false,
1943            has_default: true,
1944            has_stripped_cfg_fields: false,
1945            is_return_type: false,
1946            serde_rename_all: None,
1947            has_serde: false,
1948            super_traits: vec![],
1949        };
1950        let func = make_func(
1951            "convert",
1952            vec![make_param(
1953                "options",
1954                TypeRef::Named("ConversionOptions".to_string()),
1955                false,
1956            )],
1957        );
1958        let bridges = vec![make_bridge(
1959            Some("VisitorHandle"),
1960            Some("visitor"),
1961            BridgeBinding::FunctionParam,
1962            None,
1963            None,
1964        )];
1965        assert!(find_bridge_field(&func, std::slice::from_ref(&opts_type), &bridges).is_none());
1966    }
1967}