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