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