Skip to main content

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    /// Methods that the FFI backend should NOT forward through the vtable.
64    /// These methods fall back to the trait's default implementation.
65    /// Useful for methods whose signatures involve trait-object references
66    /// (`&dyn Trait`) that can't traverse the C FFI boundary.
67    #[serde(default)]
68    pub ffi_skip_methods: Vec<String>,
69    /// How the bridge attaches to the public API.
70    ///
71    /// - `"function_param"` (default): the bridge object arrives as a function argument
72    ///   at the position of any `param_name`-matching parameter. This is the legacy mode.
73    /// - `"options_field"`: the bridge object lives as a field on a configured options
74    ///   struct that itself arrives as a function argument. Backends emit a host-language
75    ///   field on that struct instead of a separate function parameter; the bridge object
76    ///   is attached to `options.<field>` before the underlying core call.
77    #[serde(default)]
78    pub bind_via: BridgeBinding,
79    /// IR type name that owns the bridge field when `bind_via = "options_field"` (e.g.,
80    /// `"ConversionOptions"`). Required in that mode; ignored otherwise.
81    #[serde(default)]
82    pub options_type: Option<String>,
83    /// Field name on `options_type` that holds the bridge handle when
84    /// `bind_via = "options_field"` (e.g., `"visitor"`). When omitted, defaults to
85    /// `param_name`. Ignored when `bind_via = "function_param"`.
86    #[serde(default)]
87    pub options_field: Option<String>,
88    /// IR type name of the trait's context associated type (e.g., `"NodeContext"`).
89    ///
90    /// When set, backends skip generic record/enum codegen for this type and instead
91    /// emit the richer visitor-specific version. Replaces the former literal
92    /// `"NodeContext"` string comparisons scattered across backends.
93    #[serde(default)]
94    pub context_type: Option<String>,
95    /// IR type name of the trait's result associated type (e.g., `"VisitResult"`).
96    ///
97    /// When set, backends skip generic record/enum codegen for this type and instead
98    /// emit the richer visitor-specific version. Replaces the former literal
99    /// `"VisitResult"` string comparisons scattered across backends.
100    #[serde(default)]
101    pub result_type: Option<String>,
102}
103
104/// How a trait bridge attaches to the public API.
105///
106/// See [`TraitBridgeConfig::bind_via`] for the user-facing description.
107#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
108#[serde(rename_all = "snake_case")]
109pub enum BridgeBinding {
110    /// The bridge arrives as a positional function argument. Legacy default.
111    #[default]
112    FunctionParam,
113    /// The bridge lives as a field on a configured options struct.
114    OptionsField,
115}
116
117impl TraitBridgeConfig {
118    /// Resolve the field name on `options_type` that holds this bridge.
119    ///
120    /// Falls back to [`Self::param_name`] when [`Self::options_field`] is unset, matching
121    /// the convention that the field name and parameter name are the same in most cases.
122    /// Returns `None` if neither is set.
123    pub fn resolved_options_field(&self) -> Option<&str> {
124        self.options_field.as_deref().or(self.param_name.as_deref())
125    }
126
127    /// Return the names of associated types declared in `context_type` and `result_type`.
128    ///
129    /// Backends use this list to skip generic record/enum codegen for these types,
130    /// deferring to visitor-specific generators instead.
131    pub fn associated_type_names(&self) -> Vec<&str> {
132        let mut names = Vec::new();
133        if let Some(s) = self.context_type.as_deref() {
134            names.push(s);
135        }
136        if let Some(s) = self.result_type.as_deref() {
137            names.push(s);
138        }
139        names
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    fn sample_toml(bind_via: &str) -> String {
148        format!(
149            r#"
150trait_name = "HtmlVisitor"
151type_alias = "VisitorHandle"
152param_name = "visitor"
153bind_via = "{bind_via}"
154options_type = "ConversionOptions"
155"#
156        )
157    }
158
159    #[test]
160    fn parses_options_field_binding() {
161        let cfg: TraitBridgeConfig = toml::from_str(&sample_toml("options_field")).unwrap();
162        assert_eq!(cfg.bind_via, BridgeBinding::OptionsField);
163        assert_eq!(cfg.options_type.as_deref(), Some("ConversionOptions"));
164        assert_eq!(cfg.resolved_options_field(), Some("visitor"));
165    }
166
167    #[test]
168    fn defaults_to_function_param_when_omitted() {
169        let toml_src = r#"
170trait_name = "OcrBackend"
171type_alias = "BackendHandle"
172"#;
173        let cfg: TraitBridgeConfig = toml::from_str(toml_src).unwrap();
174        assert_eq!(cfg.bind_via, BridgeBinding::FunctionParam);
175        assert!(cfg.options_type.is_none());
176    }
177
178    #[test]
179    fn options_field_falls_back_to_param_name() {
180        let toml_src = r#"
181trait_name = "HtmlVisitor"
182param_name = "visitor"
183bind_via = "options_field"
184options_type = "ConversionOptions"
185"#;
186        let cfg: TraitBridgeConfig = toml::from_str(toml_src).unwrap();
187        assert_eq!(cfg.resolved_options_field(), Some("visitor"));
188    }
189
190    #[test]
191    fn parses_unregister_and_clear_fns() {
192        let toml_src = r#"
193trait_name = "OcrBackend"
194register_fn = "register_ocr_backend"
195unregister_fn = "unregister_ocr_backend"
196clear_fn = "clear_ocr_backends"
197"#;
198        let cfg: TraitBridgeConfig = toml::from_str(toml_src).unwrap();
199        assert_eq!(cfg.unregister_fn.as_deref(), Some("unregister_ocr_backend"));
200        assert_eq!(cfg.clear_fn.as_deref(), Some("clear_ocr_backends"));
201    }
202
203    #[test]
204    fn unregister_and_clear_default_to_none() {
205        let toml_src = r#"
206trait_name = "OcrBackend"
207"#;
208        let cfg: TraitBridgeConfig = toml::from_str(toml_src).unwrap();
209        assert!(cfg.unregister_fn.is_none());
210        assert!(cfg.clear_fn.is_none());
211    }
212
213    #[test]
214    fn explicit_options_field_overrides_param_name() {
215        let toml_src = r#"
216trait_name = "HtmlVisitor"
217param_name = "visitor"
218bind_via = "options_field"
219options_type = "ConversionOptions"
220options_field = "callback"
221"#;
222        let cfg: TraitBridgeConfig = toml::from_str(toml_src).unwrap();
223        assert_eq!(cfg.resolved_options_field(), Some("callback"));
224    }
225
226    #[test]
227    fn associated_type_names_returns_configured_names() {
228        let toml_src = r#"
229trait_name = "HtmlVisitor"
230context_type = "NodeContext"
231result_type = "VisitResult"
232"#;
233        let cfg: TraitBridgeConfig = toml::from_str(toml_src).unwrap();
234        assert_eq!(cfg.context_type.as_deref(), Some("NodeContext"));
235        assert_eq!(cfg.result_type.as_deref(), Some("VisitResult"));
236        let names = cfg.associated_type_names();
237        assert_eq!(names, vec!["NodeContext", "VisitResult"]);
238    }
239
240    #[test]
241    fn associated_type_names_empty_when_not_set() {
242        let toml_src = r#"
243trait_name = "OcrBackend"
244"#;
245        let cfg: TraitBridgeConfig = toml::from_str(toml_src).unwrap();
246        assert!(cfg.associated_type_names().is_empty());
247    }
248}