alef_core/config/trait_bridge.rs
1use serde::{Deserialize, Serialize};
2
3/// Configuration for generating trait bridge code that allows foreign language
4/// objects to implement Rust traits via FFI.
5#[derive(Debug, Clone, Default, Serialize, Deserialize)]
6pub struct TraitBridgeConfig {
7 /// Name of the Rust trait to bridge (e.g., `"OcrBackend"`).
8 pub trait_name: String,
9 /// Super-trait that requires forwarding (e.g., `"Plugin"`).
10 /// When set, the bridge generates an `impl SuperTrait for Wrapper` block.
11 #[serde(default)]
12 pub super_trait: Option<String>,
13 /// Rust path to the registry getter function
14 /// (e.g., `"kreuzberg::plugins::registry::get_ocr_backend_registry"`).
15 /// Optional — when set, the generated registration function inserts the bridge into a registry.
16 #[serde(default)]
17 pub registry_getter: Option<String>,
18 /// Name of the registration function to generate
19 /// (e.g., `"register_ocr_backend"`).
20 /// Optional — when set, a `#[pyfunction]` registration function is generated.
21 /// When absent, only the wrapper struct and trait impl are emitted (per-call bridge pattern).
22 #[serde(default)]
23 pub register_fn: Option<String>,
24 /// Name of the unregister function to generate
25 /// (e.g., `"unregister_ocr_backend"`).
26 /// Optional — when set, a host-language wrapper that removes a previously
27 /// registered plugin from the registry is emitted alongside `register_fn`.
28 /// The function takes the plugin name as a string.
29 #[serde(default)]
30 pub unregister_fn: Option<String>,
31 /// Name of the clear function to generate
32 /// (e.g., `"clear_ocr_backends"`).
33 /// Optional — when set, a host-language wrapper that removes ALL registered
34 /// plugins of this type is emitted alongside `register_fn`. The function
35 /// takes no arguments and is typically used in test teardown.
36 #[serde(default)]
37 pub clear_fn: Option<String>,
38 /// Named type alias in the IR that maps to this bridge (e.g., `"VisitorHandle"`).
39 ///
40 /// When a function parameter has a `TypeRef::Named` matching this alias, code
41 /// generators replace the parameter type with the language-native callback object
42 /// (e.g., `Py<PyAny>` for Python) and emit wrapping code to construct the bridge.
43 #[serde(default)]
44 pub type_alias: Option<String>,
45 /// Parameter name override — when the extractor sanitizes the type (e.g., `VisitorHandle`
46 /// becomes `String` because it is a type alias over `Rc<RefCell<dyn Trait>>`), use the
47 /// parameter name instead of the IR type to detect which parameter to bridge.
48 ///
49 /// For example, `param_name = "visitor"` ensures that a sanitized `visitor: Option<String>`
50 /// parameter is still treated as a bridge param for this trait.
51 #[serde(default)]
52 pub param_name: Option<String>,
53 /// Extra arguments to append to the `registry.register(arc, ...)` call.
54 /// Example: `"0"` produces `registry.register(arc, 0)`.
55 #[serde(default)]
56 pub register_extra_args: Option<String>,
57 /// Language backends that should NOT generate this trait bridge.
58 /// Use backend names as they appear in `Backend::name()`, e.g. `["elixir", "wasm"]`.
59 /// When a backend's name is listed here, the bridge struct and all related code are
60 /// omitted from that backend's output.
61 #[serde(default)]
62 pub exclude_languages: Vec<String>,
63 /// How the bridge attaches to the public API.
64 ///
65 /// - `"function_param"` (default): the bridge object arrives as a function argument
66 /// at the position of any `param_name`-matching parameter. This is the legacy mode.
67 /// - `"options_field"`: the bridge object lives as a field on a configured options
68 /// struct that itself arrives as a function argument. Backends emit a host-language
69 /// field on that struct instead of a separate function parameter; the bridge object
70 /// is attached to `options.<field>` before the underlying core call.
71 #[serde(default)]
72 pub bind_via: BridgeBinding,
73 /// IR type name that owns the bridge field when `bind_via = "options_field"` (e.g.,
74 /// `"ConversionOptions"`). Required in that mode; ignored otherwise.
75 #[serde(default)]
76 pub options_type: Option<String>,
77 /// Field name on `options_type` that holds the bridge handle when
78 /// `bind_via = "options_field"` (e.g., `"visitor"`). When omitted, defaults to
79 /// `param_name`. Ignored when `bind_via = "function_param"`.
80 #[serde(default)]
81 pub options_field: Option<String>,
82}
83
84/// How a trait bridge attaches to the public API.
85///
86/// See [`TraitBridgeConfig::bind_via`] for the user-facing description.
87#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
88#[serde(rename_all = "snake_case")]
89pub enum BridgeBinding {
90 /// The bridge arrives as a positional function argument. Legacy default.
91 #[default]
92 FunctionParam,
93 /// The bridge lives as a field on a configured options struct.
94 OptionsField,
95}
96
97impl TraitBridgeConfig {
98 /// Resolve the field name on `options_type` that holds this bridge.
99 ///
100 /// Falls back to [`Self::param_name`] when [`Self::options_field`] is unset, matching
101 /// the convention that the field name and parameter name are the same in most cases.
102 /// Returns `None` if neither is set.
103 pub fn resolved_options_field(&self) -> Option<&str> {
104 self.options_field.as_deref().or(self.param_name.as_deref())
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 fn sample_toml(bind_via: &str) -> String {
113 format!(
114 r#"
115trait_name = "HtmlVisitor"
116type_alias = "VisitorHandle"
117param_name = "visitor"
118bind_via = "{bind_via}"
119options_type = "ConversionOptions"
120"#
121 )
122 }
123
124 #[test]
125 fn parses_options_field_binding() {
126 let cfg: TraitBridgeConfig = toml::from_str(&sample_toml("options_field")).unwrap();
127 assert_eq!(cfg.bind_via, BridgeBinding::OptionsField);
128 assert_eq!(cfg.options_type.as_deref(), Some("ConversionOptions"));
129 assert_eq!(cfg.resolved_options_field(), Some("visitor"));
130 }
131
132 #[test]
133 fn defaults_to_function_param_when_omitted() {
134 let toml_src = r#"
135trait_name = "OcrBackend"
136type_alias = "BackendHandle"
137"#;
138 let cfg: TraitBridgeConfig = toml::from_str(toml_src).unwrap();
139 assert_eq!(cfg.bind_via, BridgeBinding::FunctionParam);
140 assert!(cfg.options_type.is_none());
141 }
142
143 #[test]
144 fn options_field_falls_back_to_param_name() {
145 let toml_src = r#"
146trait_name = "HtmlVisitor"
147param_name = "visitor"
148bind_via = "options_field"
149options_type = "ConversionOptions"
150"#;
151 let cfg: TraitBridgeConfig = toml::from_str(toml_src).unwrap();
152 assert_eq!(cfg.resolved_options_field(), Some("visitor"));
153 }
154
155 #[test]
156 fn parses_unregister_and_clear_fns() {
157 let toml_src = r#"
158trait_name = "OcrBackend"
159register_fn = "register_ocr_backend"
160unregister_fn = "unregister_ocr_backend"
161clear_fn = "clear_ocr_backends"
162"#;
163 let cfg: TraitBridgeConfig = toml::from_str(toml_src).unwrap();
164 assert_eq!(cfg.unregister_fn.as_deref(), Some("unregister_ocr_backend"));
165 assert_eq!(cfg.clear_fn.as_deref(), Some("clear_ocr_backends"));
166 }
167
168 #[test]
169 fn unregister_and_clear_default_to_none() {
170 let toml_src = r#"
171trait_name = "OcrBackend"
172"#;
173 let cfg: TraitBridgeConfig = toml::from_str(toml_src).unwrap();
174 assert!(cfg.unregister_fn.is_none());
175 assert!(cfg.clear_fn.is_none());
176 }
177
178 #[test]
179 fn explicit_options_field_overrides_param_name() {
180 let toml_src = r#"
181trait_name = "HtmlVisitor"
182param_name = "visitor"
183bind_via = "options_field"
184options_type = "ConversionOptions"
185options_field = "callback"
186"#;
187 let cfg: TraitBridgeConfig = toml::from_str(toml_src).unwrap();
188 assert_eq!(cfg.resolved_options_field(), Some("callback"));
189 }
190}