use crate::core::config::TraitBridgeConfig;
use crate::core::ir::{ApiSurface, MethodDef, ReceiverKind, TypeDef, TypeRef};
use heck::ToSnakeCase;
use super::conversions::frb_rust_type_excluded_aware;
use super::trait_types::{
trait_impl_param_conversion, trait_impl_param_type, trait_impl_return_conversion, trait_impl_return_type,
};
pub(crate) fn emit_trait_bridge(
out: &mut String,
trait_def: &TypeDef,
bridge_config: &TraitBridgeConfig,
api: &ApiSurface,
source_crate_name: &str,
type_paths: &std::collections::HashMap<String, String>,
) {
let trait_name = &trait_def.name;
let trait_snake = trait_name.to_snake_case();
let struct_name = format!("{trait_name}DartImpl");
let trait_path = if trait_def.rust_path.is_empty() {
format!("{source_crate_name}::{trait_name}")
} else {
trait_def.rust_path.replace('-', "_")
};
let own_methods: Vec<&MethodDef> = trait_def
.methods
.iter()
.filter(|m| m.trait_source.is_none() && !return_type_references_trait(&m.return_type, api))
.collect();
let has_plugin_super = trait_def
.super_traits
.iter()
.any(|s| s == "Plugin" || s.ends_with("::Plugin"));
let uses_type_alias = bridge_config.type_alias.is_some();
let callbacks_struct_name = if uses_type_alias {
struct_name.clone()
} else {
format!("{trait_name}DartCallbacks")
};
if uses_type_alias {
out.push_str("/// Internal Rust-side storage for Dart-provided visitor callbacks.\n");
out.push_str("/// Not exposed via FRB (private to the bridge crate); the public factory\n");
out.push_str("/// `create_{trait_snake}(...)` wraps this in the trait's configured `type_alias`\n");
out.push_str("/// (e.g. `VisitorHandle`) which FRB does expose as opaque.\n");
} else {
out.push_str("/// Internal Rust-side storage for Dart-provided plugin callbacks.\n");
out.push_str("/// Not exposed via FRB (private to the bridge crate). The public factory\n");
out.push_str("/// `create_{trait_snake}_dart_impl(...)` wraps an `Arc<dyn Trait + Send + Sync>`\n");
out.push_str("/// of this struct in the public opaque `{Trait}DartImpl` newtype. Hiding the\n");
out.push_str("/// closure fields behind the wrapper keeps FRB from walking them and silently\n");
out.push_str("/// dropping the factory (FRB v2 cannot generate callable Dart classes for\n");
out.push_str("/// `Box<dyn Fn(...)>` opaque-struct fields).\n");
}
out.push_str(&format!("struct {callbacks_struct_name} {{\n"));
if has_plugin_super {
out.push_str(" /// Plugin name used by the Plugin super-trait impl.\n");
out.push_str(" plugin_name: String,\n");
out.push_str(" /// Plugin version used by the Plugin super-trait impl.\n");
out.push_str(" plugin_version: String,\n");
}
for method in &own_methods {
let field_name = &method.name;
let callback_ty = dart_fn_future_callback_type(method, source_crate_name, type_paths, &api.excluded_type_paths);
out.push_str(&crate::backends::dart::template_env::render(
"rust_trait_struct_field.jinja",
minijinja::context! {
field_name => field_name.as_str(),
callback_ty => callback_ty,
},
));
}
out.push_str(&crate::backends::dart::template_env::render(
"rust_mirror_struct_close.jinja",
minijinja::context! {},
));
out.push_str(&format!(
"impl ::std::fmt::Debug for {callbacks_struct_name} {{\n fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {{\n f.debug_struct(\"{callbacks_struct_name}\").finish_non_exhaustive()\n }}\n}}\n"
));
out.push('\n');
if has_plugin_super {
let plugin_path = api
.types
.iter()
.find(|t| t.is_trait && (t.name == "Plugin" || t.name.ends_with("::Plugin")))
.map(|t| t.rust_path.replace('-', "_"))
.unwrap_or_else(|| format!("{source_crate_name}::plugins::Plugin"));
out.push_str(&crate::backends::dart::template_env::render(
"rust_plugin_impl_open.jinja",
minijinja::context! {
plugin_path => plugin_path.as_str(),
struct_name => callbacks_struct_name.as_str(),
},
));
out.push_str(" fn name(&self) -> &str {\n");
out.push_str(" &self.plugin_name\n");
out.push_str(" }\n");
out.push('\n');
out.push_str(" fn version(&self) -> String {\n");
out.push_str(" self.plugin_version.clone()\n");
out.push_str(" }\n");
out.push('\n');
out.push_str(&crate::backends::dart::template_env::render(
"rust_plugin_initialize.jinja",
minijinja::context! {
source_crate => source_crate_name,
},
));
out.push_str(" Ok(())\n");
out.push_str(" }\n");
out.push('\n');
out.push_str(&crate::backends::dart::template_env::render(
"rust_plugin_shutdown.jinja",
minijinja::context! {
source_crate => source_crate_name,
},
));
out.push_str(" Ok(())\n");
out.push_str(" }\n");
out.push_str("}\n");
out.push('\n');
}
let has_async = own_methods.iter().any(|m| m.is_async);
if has_async {
out.push_str("#[async_trait::async_trait]\n");
}
out.push_str(&crate::backends::dart::template_env::render(
"rust_trait_impl_open.jinja",
minijinja::context! {
trait_path => trait_path.as_str(),
struct_name => callbacks_struct_name.as_str(),
},
));
for method in &own_methods {
emit_trait_bridge_method(out, method, source_crate_name, type_paths, &api.excluded_type_paths);
out.push('\n');
}
out.push_str("}\n");
out.push('\n');
if !uses_type_alias {
out.push_str("/// Re-exported so FRB's generated `frb_generated.rs` (which strips `dyn` and the\n");
out.push_str(&format!(
"/// qualified path when copying the wrapper's inner type) can resolve `{trait_name}`\n"
));
out.push_str("/// as a bare ident via its `use crate::*;` preamble.\n");
out.push_str(&format!("pub use {trait_path};\n\n"));
out.push_str(&format!(
"/// Public opaque handle returned by `create_{trait_snake}_dart_impl(...)`.\n"
));
out.push_str(&format!(
"/// Wraps an `Arc<dyn {trait_name} + Send + Sync>` whose backing object carries the\n"
));
out.push_str("/// Dart-side callbacks (private to this crate). The wrapper has no closure\n");
out.push_str("/// fields itself, so FRB can bridge it as an opaque type without seeing the\n");
out.push_str("/// callbacks.\n");
out.push_str("#[frb(opaque)]\n");
out.push_str(&format!(
"pub struct {struct_name} {{\n pub field0: std::sync::Arc<dyn {trait_name} + Send + Sync>,\n}}\n"
));
out.push('\n');
}
if uses_type_alias {
let type_alias = bridge_config.type_alias.as_deref().unwrap_or("");
let alias_def = api.types.iter().find(|t| t.name == type_alias);
let inner_path = match alias_def {
Some(td) if !td.rust_path.is_empty() => td.rust_path.replace('-', "_"),
_ => format!("{}::{}", source_crate_name.replace('-', "_"), type_alias),
};
out.push_str(&format!(
"/// Construct a `{type_alias}` from Dart callback closures.\n"
));
out.push_str("/// FRB synthesises a Dart-callable function type for each closure parameter,\n");
out.push_str("/// which is the whole point of taking them as `impl Fn(...) -> DartFnFuture<R>`\n");
out.push_str("/// parameters rather than storing them as `Box<dyn Fn(...)>` fields on an\n");
out.push_str("/// opaque struct (FRB v2 cannot generate callable closure types in that shape).\n");
if has_plugin_super {
out.push_str("/// `plugin_name` and `plugin_version` are required for the Plugin super-trait.\n");
}
out.push_str(&format!("pub async fn create_{trait_snake}(\n"));
if has_plugin_super {
out.push_str(" plugin_name: String,\n");
out.push_str(" plugin_version: String,\n");
}
for method in &own_methods {
let param_name = &method.name;
let params: Vec<String> = method
.params
.iter()
.map(|p| frb_rust_type_excluded_aware(&p.ty, p.optional, &api.excluded_type_paths))
.collect();
let ret = frb_rust_type_excluded_aware(&method.return_type, false, &api.excluded_type_paths);
let params_str = params.join(", ");
out.push_str(&format!(
" {param_name}: impl Fn({params_str}) -> DartFnFuture<{ret}> + Send + Sync + 'static,\n"
));
}
out.push_str(&format!(") -> {type_alias} {{\n"));
out.push_str(&format!(" let __impl = {struct_name} {{\n"));
if has_plugin_super {
out.push_str(" plugin_name,\n");
out.push_str(" plugin_version,\n");
}
for method in &own_methods {
out.push_str(&format!(" {name}: Box::new({name}),\n", name = method.name));
}
out.push_str(" };\n");
out.push_str(&format!(
" let __inner: {inner_path} = std::sync::Arc::new(std::sync::Mutex::new(__impl));\n"
));
out.push_str(&format!(" {type_alias}::from(__inner)\n"));
out.push_str("}\n");
if bridge_config.bind_via == crate::core::config::BridgeBinding::OptionsField {
if let (Some(options_type), Some(field_raw)) = (
bridge_config.options_type.as_deref(),
bridge_config.resolved_options_field(),
) {
let field = field_raw.to_string();
let options_snake = options_type.to_snake_case();
let opts_def = api.types.iter().find(|t| t.name == options_type);
let core_options_path = match opts_def {
Some(td) if !td.rust_path.is_empty() => td.rust_path.replace('-', "_"),
_ => format!("{}::{}", source_crate_name.replace('-', "_"), options_type),
};
out.push('\n');
out.push_str(&format!(
"/// Build a `{options_type}` from a JSON blob and attach a Dart-built\n"
));
out.push_str(&format!(
"/// `{type_alias}` to its `{field}` field. The mirror struct uses `final`\n"
));
out.push_str("/// dart fields, so callers cannot patch the visitor in after JSON load —\n");
out.push_str("/// this helper does the merge on the Rust side instead.\n");
out.push_str("#[frb]\n");
out.push_str(&format!(
"pub fn create_{options_snake}_from_json_with_{field}(\n json: String,\n {field}: Option<{type_alias}>,\n) -> Result<{options_type}, String> {{\n"
));
out.push_str(&format!(
" let mut __core: {core_options_path} = serde_json::from_str(&json).map_err(|e| e.to_string())?;\n"
));
out.push_str(&format!(" __core.{field} = {field}.map(<{inner_path}>::from);\n"));
out.push_str(&format!(" Ok({options_type}::from(__core))\n"));
out.push_str("}\n");
}
}
} else {
out.push_str(&format!(
"/// Construct a `{struct_name}` from Dart callback closures.\n"
));
out.push_str("/// FRB synthesises a Dart-callable function type for each closure parameter,\n");
out.push_str("/// which is the whole point of taking them as `impl Fn(...) -> DartFnFuture<R>`\n");
out.push_str("/// parameters rather than storing them as `Box<dyn Fn(...)>` fields on an opaque\n");
out.push_str("/// struct (FRB v2 silently drops factories that return opaque structs whose fields\n");
out.push_str("/// it cannot bridge). The returned wrapper holds an `Arc<dyn Trait + Send + Sync>`\n");
out.push_str("/// whose backing object carries the supplied callbacks privately.\n");
if has_plugin_super {
out.push_str("/// `plugin_name` and `plugin_version` are required for the Plugin super-trait.\n");
}
out.push_str(&format!("pub fn create_{trait_snake}_dart_impl(\n"));
if has_plugin_super {
out.push_str(" plugin_name: String,\n");
out.push_str(" plugin_version: String,\n");
}
for method in &own_methods {
let param_name = &method.name;
let callback_ty =
dart_fn_future_factory_param_type(method, source_crate_name, type_paths, &api.excluded_type_paths);
out.push_str(&format!(" {param_name}: {callback_ty},\n"));
}
out.push_str(&format!(") -> {struct_name} {{\n"));
out.push_str(&format!(" let __impl = {callbacks_struct_name} {{\n"));
if has_plugin_super {
out.push_str(" plugin_name,\n");
out.push_str(" plugin_version,\n");
}
for method in &own_methods {
out.push_str(&format!(" {name}: Box::new({name}),\n", name = method.name));
}
out.push_str(" };\n");
out.push_str(&format!(
" {struct_name} {{ field0: std::sync::Arc::new(__impl) }}\n"
));
out.push_str("}\n");
}
emit_register_forwarder(out, bridge_config, &struct_name, source_crate_name);
emit_unregister_forwarder(out, bridge_config, source_crate_name);
emit_clear_forwarder(out, bridge_config, source_crate_name);
}
fn emit_register_forwarder(
out: &mut String,
bridge_config: &TraitBridgeConfig,
struct_name: &str,
source_crate_name: &str,
) {
let Some(register_fn) = bridge_config.register_fn.as_deref() else {
return;
};
let Some(registry_getter) = bridge_config.registry_getter.as_deref() else {
return;
};
let extra_args = bridge_config
.register_extra_args
.as_deref()
.map(|a| format!(", {a}"))
.unwrap_or_default();
let trait_path = format!("{source_crate_name}::plugins::{}", bridge_config.trait_name);
out.push_str(&crate::backends::dart::template_env::render(
"rust_trait_register_forwarder.jinja",
minijinja::context! {
trait_name => bridge_config.trait_name.as_str(),
registry_getter => registry_getter,
register_fn => register_fn,
struct_name => struct_name,
trait_path => trait_path.as_str(),
extra_args => extra_args.as_str(),
},
));
}
fn emit_unregister_forwarder(out: &mut String, bridge_config: &TraitBridgeConfig, _source_crate_name: &str) {
let Some(unregister_fn) = bridge_config.unregister_fn.as_deref() else {
return;
};
let Some(registry_getter) = bridge_config.registry_getter.as_deref() else {
return;
};
out.push_str(&crate::backends::dart::template_env::render(
"rust_trait_unregister_forwarder.jinja",
minijinja::context! {
trait_name => bridge_config.trait_name.as_str(),
registry_getter => registry_getter,
unregister_fn => unregister_fn,
},
));
}
fn emit_clear_forwarder(out: &mut String, bridge_config: &TraitBridgeConfig, _source_crate_name: &str) {
let Some(clear_fn) = bridge_config.clear_fn.as_deref() else {
return;
};
let Some(registry_getter) = bridge_config.registry_getter.as_deref() else {
return;
};
out.push_str(&crate::backends::dart::template_env::render(
"rust_trait_clear_forwarder.jinja",
minijinja::context! {
trait_name => bridge_config.trait_name.as_str(),
registry_getter => registry_getter,
clear_fn => clear_fn,
},
));
}
fn substitute_internal_document_in_rust_type(rust_type: &str, source_crate_name: &str) -> String {
let fully_qualified_internal = format!("{}::types::internal::InternalDocument", source_crate_name);
if rust_type.contains(&fully_qualified_internal) {
return rust_type.replace(&fully_qualified_internal, "InternalDocumentBridge");
}
let partial_qualified_internal = format!("{}::InternalDocument", source_crate_name);
if rust_type.contains(&partial_qualified_internal) {
return rust_type.replace(&partial_qualified_internal, "InternalDocumentBridge");
}
if rust_type.contains("InternalDocument") {
let mut result = String::new();
let mut chars = rust_type.chars().peekable();
while let Some(ch) = chars.next() {
if ch == 'I' {
let remaining: String = std::iter::once(ch)
.chain(chars.by_ref().take("nternalDocument".len()))
.collect();
if remaining == "InternalDocument" {
let before_ok =
result.is_empty() || !result.chars().last().is_some_and(|c| c.is_alphanumeric() || c == '_');
let after_ok = chars.peek().is_none_or(|&c| !c.is_alphanumeric() && c != '_');
if before_ok && after_ok {
result.push_str("InternalDocumentBridge");
continue;
} else {
result.push_str(&remaining);
continue;
}
} else {
result.push(ch);
}
} else {
result.push(ch);
}
}
return result;
}
rust_type.to_string()
}
fn dart_fn_future_callback_type(
method: &MethodDef,
source_crate_name: &str,
_type_paths: &std::collections::HashMap<String, String>,
excluded_type_paths: &std::collections::HashMap<String, String>,
) -> String {
let (params_str, dart_fn_ret) = dart_fn_future_params_and_ret(method, source_crate_name, excluded_type_paths);
format!("Box<dyn Fn({params_str}) -> {dart_fn_ret} + Send + Sync>")
}
fn dart_fn_future_factory_param_type(
method: &MethodDef,
source_crate_name: &str,
_type_paths: &std::collections::HashMap<String, String>,
excluded_type_paths: &std::collections::HashMap<String, String>,
) -> String {
let (params_str, dart_fn_ret) = dart_fn_future_params_and_ret(method, source_crate_name, excluded_type_paths);
format!("impl Fn({params_str}) -> {dart_fn_ret} + Send + Sync + 'static")
}
fn dart_fn_future_params_and_ret(
method: &MethodDef,
source_crate_name: &str,
excluded_type_paths: &std::collections::HashMap<String, String>,
) -> (String, String) {
let params: Vec<String> = method
.params
.iter()
.map(|p| {
let ty = frb_rust_type_excluded_aware(&p.ty, p.optional, excluded_type_paths);
substitute_internal_document_in_rust_type(&ty, source_crate_name)
})
.collect();
let ret = frb_rust_type_excluded_aware(&method.return_type, false, excluded_type_paths);
let ret_substituted = substitute_internal_document_in_rust_type(&ret, source_crate_name);
let dart_fn_ret = format!("DartFnFuture<{ret_substituted}>");
(params.join(", "), dart_fn_ret)
}
fn emit_trait_bridge_method(
out: &mut String,
method: &MethodDef,
source_crate_name: &str,
type_paths: &std::collections::HashMap<String, String>,
excluded_type_paths: &std::collections::HashMap<String, String>,
) {
let method_name = &method.name;
let self_receiver = match method.receiver {
Some(ReceiverKind::RefMut) => "&mut self",
Some(ReceiverKind::Owned) => "self",
_ => "&self",
};
let params_sig: Vec<String> = std::iter::once(self_receiver.to_string())
.chain(method.params.iter().map(|p| {
let orig_ty = trait_impl_param_type(p, source_crate_name, type_paths);
format!("{}: {orig_ty}", p.name)
}))
.collect();
let is_ref_slice_of_str = method.returns_ref
&& matches!(
&method.return_type,
TypeRef::Vec(inner) if matches!(inner.as_ref(), TypeRef::String)
);
let ret = if is_ref_slice_of_str {
"&[&str]".to_string()
} else {
trait_impl_return_type(&method.return_type, source_crate_name, type_paths)
};
let return_sig = if method.error_type.is_some() {
if matches!(method.return_type, TypeRef::Unit) {
format!("{source_crate_name}::Result<()>")
} else {
format!("{source_crate_name}::Result<{ret}>")
}
} else {
ret.clone()
};
let async_kw = if method.is_async { "async " } else { "" };
out.push_str(&crate::backends::dart::template_env::render(
"rust_method_signature.jinja",
minijinja::context! {
async_kw => async_kw,
method_name => method_name.as_str(),
params => params_sig.join(", "),
return_sig => return_sig.as_str(),
},
));
for p in &method.params {
let conv = trait_impl_param_conversion(p, excluded_type_paths);
if !conv.is_empty() {
out.push_str(&crate::backends::dart::template_env::render(
"rust_trait_method_param_conversion.jinja",
minijinja::context! {
conversion => conv,
},
));
}
}
let mut pre_bindings = String::new();
let call_args: Vec<String> = method
.params
.iter()
.map(|p| {
let is_internal_document = match &p.ty {
TypeRef::Named(name) => name.ends_with("InternalDocument"),
_ => false,
};
if is_internal_document {
let local = format!("__{}_local", p.name);
let expr = if p.optional {
if method.error_type.is_some() {
format!(
"{name}.map(|v| serde_json::to_string(&v).map(|json| InternalDocumentBridge {{ json }})).transpose()?",
name = p.name,
)
} else {
format!(
"{name}.map(|v| InternalDocumentBridge {{ json: serde_json::to_string(&v).expect(\"serialize InternalDocument for Dart trait bridge\") }})",
name = p.name,
)
}
} else if method.error_type.is_some() {
format!(
"InternalDocumentBridge {{ json: serde_json::to_string(&{name})? }}",
name = p.name,
)
} else {
format!(
"InternalDocumentBridge {{ json: serde_json::to_string(&{name}).expect(\"serialize InternalDocument for Dart trait bridge\") }}",
name = p.name,
)
};
let _ = std::fmt::Write::write_fmt(
&mut pre_bindings,
format_args!(" let {local} = {expr};\n", local = local, expr = expr),
);
local
} else {
p.name.clone()
}
})
.collect();
if !pre_bindings.is_empty() {
out.push_str(&pre_bindings);
}
let call_expr = format!("(self.{method_name})({})", call_args.join(", "));
let ret_conv = trait_impl_return_conversion(&method.return_type, source_crate_name);
let named_return_default = ret_conv == "__NAMED_RETURN_DEFAULT__";
let returns_internal_document = match &method.return_type {
TypeRef::Named(name) => name.ends_with("InternalDocument"),
_ => false,
};
if returns_internal_document {
let core_path = internal_document_core_path(&method.return_type, source_crate_name, excluded_type_paths);
if method.is_async {
if method.error_type.is_some() {
out.push_str(&format!(
" let __ret_bridge: InternalDocumentBridge = {call_expr}.await;\n\
\x20 let __ret: {core_path} = serde_json::from_str(&__ret_bridge.json)?;\n",
call_expr = call_expr,
core_path = core_path,
));
} else {
out.push_str(&format!(
" let __ret_bridge: InternalDocumentBridge = {call_expr}.await;\n\
\x20 let __ret: {core_path} = serde_json::from_str(&__ret_bridge.json)\n\
\x20 .expect(\"deserialize InternalDocument from Dart trait bridge\");\n",
call_expr = call_expr,
core_path = core_path,
));
}
} else {
out.push_str(" let __ret_bridge = ::tokio::runtime::Builder::new_current_thread()\n .build()\n .expect(\"build alef visitor tokio runtime\")\n");
out.push_str(&crate::backends::dart::template_env::render(
"rust_trait_method_block_on.jinja",
minijinja::context! {
call_expr => call_expr.as_str(),
},
));
if method.error_type.is_some() {
out.push_str(&format!(
" ;\n let __ret: {core_path} = serde_json::from_str(&__ret_bridge.json)?;\n",
core_path = core_path,
));
} else {
out.push_str(&format!(
" ;\n let __ret: {core_path} = serde_json::from_str(&__ret_bridge.json)\n .expect(\"deserialize InternalDocument from Dart trait bridge\");\n",
core_path = core_path,
));
}
}
if method.error_type.is_some() {
out.push_str(" Ok(__ret)\n");
} else {
out.push_str(" __ret\n");
}
out.push_str(" }\n");
return;
}
if method.error_type.is_some() {
if method.is_async {
if named_return_default {
out.push_str(&crate::backends::dart::template_env::render(
"rust_trait_method_default_await.jinja",
minijinja::context! {
call_expr => call_expr.as_str(),
return_expr => "Ok(Default::default())",
},
));
} else if ret_conv.is_empty() {
out.push_str(&crate::backends::dart::template_env::render(
"rust_trait_method_ok_await.jinja",
minijinja::context! {
call_expr => call_expr.as_str(),
},
));
} else {
out.push_str(&crate::backends::dart::template_env::render(
"rust_trait_method_await_result.jinja",
minijinja::context! {
call_expr => call_expr.as_str(),
ret_conv => ret_conv.as_str(),
},
));
}
} else {
out.push_str(" let __result = ::tokio::runtime::Builder::new_current_thread()\n .build()\n .expect(\"build alef visitor tokio runtime\")\n");
out.push_str(&crate::backends::dart::template_env::render(
"rust_trait_method_block_on.jinja",
minijinja::context! {
call_expr => call_expr.as_str(),
},
));
if named_return_default {
out.push_str(&crate::backends::dart::template_env::render(
"rust_trait_method_default_from_result.jinja",
minijinja::context! {
return_expr => "Ok(Default::default())",
},
));
} else {
out.push_str(&crate::backends::dart::template_env::render(
"rust_trait_method_ok_block_on.jinja",
minijinja::context! {
ret_conv => ret_conv.as_str(),
},
));
}
}
} else if method.is_async {
if named_return_default {
out.push_str(&crate::backends::dart::template_env::render(
"rust_trait_method_default_await.jinja",
minijinja::context! {
call_expr => call_expr.as_str(),
return_expr => "Default::default()",
},
));
} else if ret_conv.is_empty() {
out.push_str(&crate::backends::dart::template_env::render(
"rust_trait_method_await_plain.jinja",
minijinja::context! {
call_expr => call_expr.as_str(),
},
));
} else {
out.push_str(&crate::backends::dart::template_env::render(
"rust_trait_method_await_result.jinja",
minijinja::context! {
call_expr => call_expr.as_str(),
ret_conv => ret_conv.as_str(),
},
));
}
} else {
out.push_str(" let __result = ::tokio::runtime::Builder::new_current_thread()\n .build()\n .expect(\"build alef visitor tokio runtime\")\n");
out.push_str(&crate::backends::dart::template_env::render(
"rust_trait_method_block_on.jinja",
minijinja::context! {
call_expr => call_expr.as_str(),
},
));
if named_return_default {
out.push_str(&crate::backends::dart::template_env::render(
"rust_trait_method_default_from_result.jinja",
minijinja::context! {
return_expr => "Default::default()",
},
));
} else if is_ref_slice_of_str {
out.push_str(
" ;\n \
let __strs: Vec<&'static str> = __result\n \
.into_iter()\n \
.map(|s| -> &'static str { Box::leak(s.into_boxed_str()) })\n \
.collect();\n \
Box::leak(__strs.into_boxed_slice())\n",
);
} else {
out.push_str(&crate::backends::dart::template_env::render(
"rust_trait_method_plain_block_on_result.jinja",
minijinja::context! {
ret_conv => ret_conv.as_str(),
},
));
}
}
out.push_str(" }\n");
}
pub(crate) fn needs_internal_document_bridge(ty: &TypeRef) -> bool {
match ty {
TypeRef::Named(name) => name.ends_with("InternalDocument"),
TypeRef::Optional(inner) | TypeRef::Vec(inner) => needs_internal_document_bridge(inner),
TypeRef::Map(k, v) => needs_internal_document_bridge(k) || needs_internal_document_bridge(v),
_ => false,
}
}
pub(crate) fn emit_internal_document_bridge_type(out: &mut String) {
out.push_str(
"\n/// Opaque JSON carrier for Rust's internal `InternalDocument` trait contract.\n\
/// Dart code should pass this value back to Alef-generated bridge APIs rather\n\
/// than treating it as the public `ExtractionResult` DTO.\n\
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]\n\
pub struct InternalDocumentBridge {\n\
\x20 pub json: String,\n\
}\n",
);
}
fn internal_document_core_path(
ty: &TypeRef,
source_crate_name: &str,
excluded_type_paths: &std::collections::HashMap<String, String>,
) -> String {
match ty {
TypeRef::Named(name) => excluded_type_paths
.get(name)
.filter(|p| !p.is_empty())
.map(|p| p.replace('-', "_"))
.unwrap_or_else(|| format!("{source_crate_name}::{name}")),
_ => format!("{source_crate_name}::types::internal::InternalDocument"),
}
}
pub(crate) fn return_type_references_trait(ty: &TypeRef, api: &ApiSurface) -> bool {
match ty {
TypeRef::Named(name) => {
api.types.iter().any(|t| t.is_trait && &t.name == name) || api.excluded_trait_names.contains(name)
}
TypeRef::Optional(inner) | TypeRef::Vec(inner) => return_type_references_trait(inner, api),
TypeRef::Map(k, v) => return_type_references_trait(k, api) || return_type_references_trait(v, api),
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::ir::{ApiSurface, ReceiverKind, TypeDef, TypeRef};
fn empty_type_def(name: &str, is_trait: bool) -> TypeDef {
TypeDef {
name: name.to_string(),
rust_path: format!("demo::{name}"),
original_rust_path: String::new(),
fields: vec![],
methods: vec![],
is_opaque: false,
is_clone: false,
is_copy: false,
doc: String::new(),
cfg: None,
is_trait,
has_default: false,
has_stripped_cfg_fields: false,
is_return_type: false,
serde_rename_all: None,
has_serde: false,
super_traits: vec![],
binding_excluded: false,
binding_exclusion_reason: None,
is_variant_wrapper: false,
}
}
fn api_surface(types: Vec<TypeDef>, excluded_paths: Vec<(&str, &str)>, excluded_traits: Vec<&str>) -> ApiSurface {
ApiSurface {
types,
excluded_type_paths: excluded_paths
.into_iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect(),
excluded_trait_names: excluded_traits.into_iter().map(String::from).collect(),
services: vec![],
handler_contracts: vec![],
..ApiSurface::default()
}
}
#[test]
fn return_type_references_in_surface_trait() {
let api = api_surface(vec![empty_type_def("MyTrait", true)], vec![], vec![]);
let ret = TypeRef::Optional(Box::new(TypeRef::Named("MyTrait".into())));
assert!(return_type_references_trait(&ret, &api));
}
#[test]
fn return_type_references_excluded_trait_is_detected() {
let api = api_surface(
vec![],
vec![("SyncExtractor", "demo::extractors::SyncExtractor")],
vec!["SyncExtractor"],
);
let ret = TypeRef::Optional(Box::new(TypeRef::Named("SyncExtractor".into())));
assert!(return_type_references_trait(&ret, &api));
}
#[test]
fn return_type_with_excluded_struct_is_not_detected() {
let api = api_surface(
vec![],
vec![("InternalDocument", "demo::types::internal::InternalDocument")],
vec![],
);
let ret = TypeRef::Named("InternalDocument".into());
assert!(!return_type_references_trait(&ret, &api));
}
#[test]
fn return_type_with_unrelated_named_is_not_detected() {
let api = api_surface(vec![empty_type_def("MyStruct", false)], vec![], vec![]);
let ret = TypeRef::Optional(Box::new(TypeRef::Named("MyStruct".into())));
assert!(!return_type_references_trait(&ret, &api));
}
#[test]
fn internal_document_result_return_deserializes_with_error_mapping() {
let method = MethodDef {
name: "extract".to_string(),
params: vec![],
return_type: TypeRef::Named("InternalDocument".to_string()),
is_async: true,
is_static: false,
error_type: Some("Error".to_string()),
doc: String::new(),
receiver: Some(ReceiverKind::Ref),
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
};
let mut out = String::new();
let type_paths = std::collections::HashMap::from([(
"InternalDocument".to_string(),
"demo::types::internal::InternalDocument".to_string(),
)]);
let excluded_type_paths = type_paths.clone();
emit_trait_bridge_method(&mut out, &method, "demo", &type_paths, &excluded_type_paths);
assert!(
out.contains("serde_json::from_str(&__ret_bridge.json)?;"),
"Result-returning InternalDocument methods must propagate JSON decode errors, got:\n{out}",
);
assert!(
!out.contains("expect(\"deserialize InternalDocument from Dart trait bridge\")"),
"Result-returning InternalDocument methods must not panic on JSON decode, got:\n{out}",
);
}
#[test]
fn internal_document_result_param_serializes_with_error_mapping() {
let method = MethodDef {
name: "render".to_string(),
params: vec![crate::core::ir::ParamDef {
name: "document".to_string(),
ty: TypeRef::Named("InternalDocument".to_string()),
optional: false,
is_ref: true,
..Default::default()
}],
return_type: TypeRef::String,
is_async: true,
is_static: false,
error_type: Some("Error".to_string()),
doc: String::new(),
receiver: Some(ReceiverKind::Ref),
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
};
let mut out = String::new();
let type_paths = std::collections::HashMap::from([(
"InternalDocument".to_string(),
"demo::types::internal::InternalDocument".to_string(),
)]);
let excluded_type_paths = type_paths.clone();
emit_trait_bridge_method(&mut out, &method, "demo", &type_paths, &excluded_type_paths);
assert!(
out.contains("serde_json::to_string(&document)?"),
"Result-returning InternalDocument params must propagate JSON encode errors, got:\n{out}",
);
assert!(
!out.contains("expect(\"serialize InternalDocument for Dart trait bridge\")"),
"Result-returning InternalDocument params must not panic on JSON encode, got:\n{out}",
);
}
}