use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{ApiSurface, EntrypointKind, HandlerContractDef, ServiceDef, TypeRef};
use std::collections::BTreeSet;
use std::path::PathBuf;
fn format_dart_comment(text: &str, indent: usize) -> String {
let trimmed = text.trim();
if trimmed.is_empty() {
return String::new();
}
let pad = " ".repeat(indent);
let mut out = String::new();
for line in trimmed.lines() {
if line.trim().is_empty() {
out.push_str(&pad);
out.push_str("///\n");
} else {
out.push_str(&pad);
out.push_str("/// ");
out.push_str(line);
out.push('\n');
}
}
out
}
fn entrypoint_return_representable(
ep: &crate::core::ir::EntrypointDef,
service: &ServiceDef,
api: &ApiSurface,
) -> bool {
if let Some(svc_type) = api.types.iter().find(|t| t.name == service.name) {
if let Some(method) = svc_type.methods.iter().find(|m| m.name == ep.method) {
if method.sanitized {
return false;
}
}
}
match &ep.return_type {
TypeRef::Unit | TypeRef::String | TypeRef::Char | TypeRef::Primitive(_) | TypeRef::Bytes => true,
TypeRef::Named(n) => api.types.iter().any(|t| t.name == *n),
_ => false,
}
}
fn find_contract<'a>(api: &'a ApiSurface, trait_name: &str) -> Option<&'a HandlerContractDef> {
api.handler_contracts.iter().find(|c| c.trait_name == trait_name)
}
pub fn gen_service_rust(api: &ApiSurface, config: &ResolvedCrateConfig) -> String {
let core_import = config.core_import_name();
let mut out = String::new();
out.push_str(&crate::backends::dart::template_env::render(
"service_api/file_header.rs.jinja",
minijinja::context! {},
));
out.push_str("\n\n");
let referenced_contracts: Vec<&HandlerContractDef> = {
let names: BTreeSet<&str> = api
.services
.iter()
.flat_map(|s| s.registrations.iter())
.map(|r| r.callback_contract.as_str())
.collect();
names.iter().filter_map(|n| find_contract(api, n)).collect()
};
for contract in &referenced_contracts {
gen_handler_bridge(&mut out, contract, &core_import);
out.push('\n');
}
for service in &api.services {
gen_service_owner(&mut out, service, api, &core_import);
out.push('\n');
}
out
}
fn gen_handler_bridge(out: &mut String, contract: &HandlerContractDef, core_import: &str) {
let trait_name = &contract.trait_name;
let bridge_name = format!("DartHandler{}", trait_name);
out.push_str(&crate::backends::dart::template_env::render(
"service_api/handler_bridge_doc.rs.jinja",
minijinja::context! {
trait_name => trait_name.as_str(),
},
));
out.push('\n');
out.push_str(&crate::backends::dart::template_env::render(
"service_api/handler_bridge_struct.rs.jinja",
minijinja::context! {
bridge_name => bridge_name.as_str(),
},
));
out.push_str("\n\n");
out.push_str(&crate::backends::dart::template_env::render(
"service_api/handler_bridge_constructor.rs.jinja",
minijinja::context! {
bridge_name => bridge_name.as_str(),
},
));
out.push_str("\n\n");
let req_type = contract.wire_request_type.as_deref().unwrap_or("serde_json::Value");
let resp_type = contract.wire_response_type.as_deref().unwrap_or("serde_json::Value");
let req_path = if req_type.contains("::") {
req_type.to_string()
} else {
format!("{core_import}::{req_type}")
};
let resp_path = if resp_type.contains("::") {
resp_type.to_string()
} else {
format!("{core_import}::{resp_type}")
};
let extra_params: String = contract
.dispatch_extra_params
.iter()
.map(|p| format!(", {p}"))
.collect();
let wire_param_name = contract.wire_param_name.as_deref().unwrap_or("request");
let dispatch_name = &contract.dispatch.name;
let box_err = "Box<dyn std::error::Error + Send + Sync>";
let wire_output = format!("Result<{resp_path}, {box_err}>");
let output_type = contract
.dispatch_return_type
.clone()
.unwrap_or_else(|| wire_output.clone());
let tail = match &contract.response_adapter {
Some(adapter) => format!("{adapter}(outcome)"),
None => "outcome".to_string(),
};
out.push_str(&crate::backends::dart::template_env::render(
"service_api/handler_bridge_impl_open.rs.jinja",
minijinja::context! {
core_import => core_import,
trait_name => trait_name.as_str(),
bridge_name => bridge_name.as_str(),
dispatch_name => dispatch_name.as_str(),
extra_params => &extra_params,
wire_param_name => wire_param_name,
req_path => req_path.as_str(),
resp_path => resp_path.as_str(),
wire_output => wire_output.as_str(),
box_err => box_err,
output_type => output_type.as_str(),
tail => tail.as_str(),
},
));
}
fn gen_service_owner(out: &mut String, service: &ServiceDef, api: &ApiSurface, _core_import: &str) {
let service_name = &service.name;
let owner_path = &service.rust_path;
if !service.doc.is_empty() {
let doc_lines = format_dart_comment(&service.doc, 0);
out.push_str(&crate::backends::dart::template_env::render(
"service_api/service_owner_doc.rs.jinja",
minijinja::context! {
service_name => service_name.as_str(),
doc => service.doc.as_str(),
doc_lines => doc_lines.as_str(),
},
));
} else {
out.push_str(&crate::backends::dart::template_env::render(
"service_api/default_service_owner_doc.rs.jinja",
minijinja::context! {
service_name => service_name.as_str(),
},
));
}
out.push('\n');
out.push_str(&crate::backends::dart::template_env::render(
"service_api/service_owner_struct.rs.jinja",
minijinja::context! {
service_name => service_name.as_str(),
owner_path => owner_path.as_str(),
},
));
out.push_str("\n\n");
let ctor_params = format_ctor_params(service);
let ctor_call = format_ctor_call(service);
out.push_str(&crate::backends::dart::template_env::render(
"service_api/service_owner_impl_open.rs.jinja",
minijinja::context! {
service_name => service_name.as_str(),
owner_path => owner_path.as_str(),
ctor_params => ctor_params.as_str(),
ctor_call => ctor_call.as_str(),
},
));
out.push('\n');
for config in &service.configurators {
let method_params = format_method_params(config);
let configurator_params: Vec<_> = config
.params
.iter()
.map(|p| {
let (is_opaque, is_named) = match &p.ty {
TypeRef::Named(n) => {
let opaque = api.types.iter().any(|t| t.name == *n && t.is_opaque);
(opaque, true)
}
_ => (false, false),
};
minijinja::context! {
name => p.name.as_str(),
rust_type => typeref_to_rust_type(&p.ty).as_str(),
is_opaque => is_opaque,
is_named => is_named,
}
})
.collect();
let is_owned = matches!(config.receiver, Some(crate::core::ir::ReceiverKind::Owned));
out.push_str(&crate::backends::dart::template_env::render(
"service_api/configurator_method.rs.jinja",
minijinja::context! {
config_name => config.name.as_str(),
method_params => method_params.as_str(),
configurator_params => configurator_params,
is_owned => is_owned,
},
));
out.push('\n');
}
for reg in &service.registrations {
let metadata_params: Vec<_> = reg
.metadata_params
.iter()
.map(|p| {
let is_opaque = matches!(&p.ty, TypeRef::Named(n)
if api.types.iter().any(|t| t.name == *n && t.is_opaque));
minijinja::context! {
name => p.name.as_str(),
rust_type => typeref_to_rust_type(&p.ty).as_str(),
is_opaque => is_opaque,
}
})
.collect();
let bridge_name = format!("DartHandler{}", reg.callback_contract);
let trait_path = find_contract(api, ®.callback_contract)
.map(|c| {
if c.rust_path.is_empty() {
c.trait_name.clone()
} else {
c.rust_path.clone()
}
})
.unwrap_or_else(|| reg.callback_contract.clone());
out.push_str(&crate::backends::dart::template_env::render(
"service_api/registration_method.rs.jinja",
minijinja::context! {
method_name => reg.method.as_str(),
callback_param => reg.callback_param.as_str(),
metadata_params => metadata_params,
bridge_name => bridge_name.as_str(),
trait_path => trait_path.as_str(),
},
));
out.push('\n');
for variant in ®.variants {
let signature_params: Vec<_> = variant
.signature_params
.iter()
.map(|p| {
minijinja::context! {
name => p.name.as_str(),
rust_type => typeref_to_rust_type(&p.ty).as_str(),
}
})
.collect();
let (wrapper_type_path, wrapper_local_type, constructor_method, wrapper_args) =
if let Some(wc) = &variant.wrapper_call {
let args: Vec<_> = wc
.args
.iter()
.map(|arg| match arg {
crate::core::ir::WrapperConstructorArg::Fixed {
param_name: _,
value_expr,
} => minijinja::context! {
kind => "fixed",
value_expr => value_expr.as_str(),
},
crate::core::ir::WrapperConstructorArg::Free { param } => minijinja::context! {
kind => "free",
param => minijinja::context! {
name => param.name.as_str(),
rust_type => typeref_to_rust_type(¶m.ty).as_str(),
},
},
})
.collect();
(
wc.wrapper_type_path.clone(),
wc.wrapper_type_name.clone(),
wc.constructor_method.clone(),
args,
)
} else {
(String::new(), String::new(), String::new(), vec![])
};
out.push_str(&crate::backends::dart::template_env::render(
"service_api/registration_variant.rs.jinja",
minijinja::context! {
variant_name => variant.name.as_str(),
signature_params => signature_params,
base_method_name => reg.method.as_str(),
wrapper_call => variant.wrapper_call.is_some(),
wrapper_type_path => wrapper_type_path.as_str(),
wrapper_local_type => wrapper_local_type.as_str(),
constructor_method => constructor_method.as_str(),
wrapper_args => wrapper_args,
},
));
out.push('\n');
}
}
for ep in &service.entrypoints {
gen_entrypoint_method(out, service, ep, api);
out.push('\n');
}
out.push_str(&crate::backends::dart::template_env::render(
"service_api/service_owner_impl_close.rs.jinja",
minijinja::context! {},
));
}
fn gen_entrypoint_method(
out: &mut String,
service: &ServiceDef,
ep: &crate::core::ir::EntrypointDef,
api: &ApiSurface,
) {
if matches!(ep.kind, EntrypointKind::Finalize) && !entrypoint_return_representable(ep, service, api) {
return;
}
let method_name = &ep.method;
let is_async = ep.is_async;
let return_type = typeref_to_rust_type(&ep.return_type);
let params: Vec<_> = ep
.params
.iter()
.map(|p| {
minijinja::context! {
name => p.name.as_str(),
rust_type => typeref_to_rust_type(&p.ty).as_str(),
}
})
.collect();
let registrations: Vec<_> = service
.registrations
.iter()
.map(|r| {
minijinja::context! {
method => r.method.as_str(),
}
})
.collect();
out.push_str(&crate::backends::dart::template_env::render(
"service_api/entrypoint_method.rs.jinja",
minijinja::context! {
method_name => method_name.as_str(),
is_async => is_async,
return_type => return_type.as_str(),
params => params,
registrations => registrations,
},
));
}
fn format_ctor_params(service: &ServiceDef) -> String {
service
.constructor
.params
.iter()
.map(|p| {
let ty = typeref_to_rust_type(&p.ty);
format!("{}: {}", p.name, ty)
})
.collect::<Vec<_>>()
.join(", ")
}
fn format_ctor_call(service: &ServiceDef) -> String {
if service.constructor.params.is_empty() {
"::new()".to_string()
} else {
let args = service
.constructor
.params
.iter()
.map(|p| p.name.clone())
.collect::<Vec<_>>()
.join(", ");
format!("::new({})", args)
}
}
fn format_method_params(method: &crate::core::ir::MethodDef) -> String {
let mut out = String::new();
for param in &method.params {
let ty = typeref_to_rust_type(¶m.ty);
out.push_str(&crate::backends::dart::template_env::render(
"service_api/method_param.rs.jinja",
minijinja::context! {
name => param.name.as_str(),
ty => ty.as_str(),
},
));
}
out
}
fn typeref_to_rust_type(ty: &TypeRef) -> String {
match ty {
TypeRef::String => "String".to_owned(),
TypeRef::Char => "char".to_owned(),
TypeRef::Primitive(p) => {
use crate::core::ir::PrimitiveType;
match p {
PrimitiveType::Bool => "bool".to_owned(),
PrimitiveType::U8 => "u8".to_owned(),
PrimitiveType::U16 => "u16".to_owned(),
PrimitiveType::U32 => "u32".to_owned(),
PrimitiveType::U64 => "u64".to_owned(),
PrimitiveType::I8 => "i8".to_owned(),
PrimitiveType::I16 => "i16".to_owned(),
PrimitiveType::I32 => "i32".to_owned(),
PrimitiveType::I64 => "i64".to_owned(),
PrimitiveType::F32 => "f32".to_owned(),
PrimitiveType::F64 => "f64".to_owned(),
PrimitiveType::Usize => "usize".to_owned(),
PrimitiveType::Isize => "isize".to_owned(),
}
}
TypeRef::Bytes => "Vec<u8>".to_owned(),
TypeRef::Unit => "()".to_owned(),
TypeRef::Optional(inner) => format!("Option<{}>", typeref_to_rust_type(inner)),
TypeRef::Vec(inner) => format!("Vec<{}>", typeref_to_rust_type(inner)),
TypeRef::Map(k, v) => format!(
"std::collections::HashMap<{}, {}>",
typeref_to_rust_type(k),
typeref_to_rust_type(v)
),
TypeRef::Named(n) => n.clone(),
TypeRef::Json => "serde_json::Value".to_owned(),
TypeRef::Path => "std::path::PathBuf".to_owned(),
TypeRef::Duration => "std::time::Duration".to_owned(),
}
}
pub fn generate(api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
if api.services.is_empty() {
return Ok(vec![]);
}
let rust_source = gen_service_rust(api, config);
Ok(vec![GeneratedFile {
path: PathBuf::from("packages/dart/rust/src/service_api.rs"),
content: rust_source,
generated_header: true,
}])
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::ir::{
EntrypointDef, EntrypointKind, HandlerContractDef, MethodDef, ParamDef, RegistrationDef, ServiceDef, TypeRef,
};
fn make_fixture_surface() -> ApiSurface {
let constructor = MethodDef {
name: "new".to_owned(),
params: vec![],
return_type: TypeRef::Unit,
is_async: false,
is_static: true,
error_type: None,
doc: "Create a new service owner.".to_owned(),
receiver: None,
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 registration = RegistrationDef {
method: "add_handler".to_owned(),
callback_param: "handler".to_owned(),
callback_contract: "RequestHandler".to_owned(),
metadata_params: vec![ParamDef {
name: "path".to_owned(),
ty: TypeRef::String,
optional: false,
default: None,
..ParamDef::default()
}],
receiver: Some(crate::core::ir::ReceiverKind::RefMut),
return_type: TypeRef::Unit,
error_type: Some("HandlerError".to_owned()),
doc: "Register a request handler.".to_owned(),
variants: vec![],
};
let run_entrypoint = EntrypointDef {
method: "run".to_owned(),
kind: EntrypointKind::Run,
is_async: true,
params: vec![ParamDef {
name: "addr".to_owned(),
ty: TypeRef::String,
optional: false,
default: None,
..ParamDef::default()
}],
return_type: TypeRef::Unit,
error_type: Some("IoError".to_owned()),
doc: "Start the service.".to_owned(),
};
let handler_contract = HandlerContractDef {
trait_name: "RequestHandler".to_owned(),
rust_path: "my_crate::RequestHandler".to_owned(),
dispatch: MethodDef {
name: "handle".to_owned(),
params: vec![ParamDef {
name: "req".to_owned(),
ty: TypeRef::Named("RequestData".to_owned()),
optional: false,
default: None,
..ParamDef::default()
}],
return_type: TypeRef::Named("Response".to_owned()),
is_async: true,
is_static: false,
error_type: None,
doc: "Handle a request.".to_owned(),
receiver: Some(crate::core::ir::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,
},
optional_methods: vec![],
wire_request_type: Some("RequestData".to_owned()),
wire_response_type: Some("Response".to_owned()),
dispatch_extra_params: vec![],
wire_param_name: None,
dispatch_return_type: None,
response_adapter: None,
doc: "Handler contract.".to_owned(),
};
ApiSurface {
crate_name: "test_crate".to_owned(),
version: "1.0.0".to_owned(),
services: vec![ServiceDef {
name: "TestService".to_owned(),
rust_path: "my_crate::TestService".to_owned(),
constructor,
configurators: vec![],
registrations: vec![registration],
entrypoints: vec![run_entrypoint],
doc: "Test service.".to_owned(),
cfg: None,
}],
handler_contracts: vec![handler_contract],
..ApiSurface::default()
}
}
#[test]
fn test_emit_service_owner_with_frb_opaque() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let rust = gen_service_rust(&api, &config);
assert!(
rust.contains("#[frb(opaque)]"),
"expected `#[frb(opaque)]` marker in:\n{rust}"
);
assert!(
rust.contains("pub struct TestService"),
"expected service owner struct in:\n{rust}"
);
assert!(
!rust.contains("registrations: tokio::sync::Mutex"),
"should not have registrations field in:\n{rust}"
);
}
#[test]
fn test_emit_registration_with_dartfnfuture() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let rust = gen_service_rust(&api, &config);
assert!(
rust.contains("DartFnFuture<String>"),
"expected `DartFnFuture<String>` in registration method:\n{rust}"
);
}
#[test]
fn test_emit_handler_bridge_with_manual_pin_box() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let rust = gen_service_rust(&api, &config);
assert!(
rust.contains("Pin<Box<dyn std::future::Future"),
"expected manual `Pin<Box<dyn Future>>` form in handler bridge:\n{rust}"
);
assert!(
!rust.contains("#[async_trait]"),
"should NOT emit #[async_trait] in:\n{rust}"
);
}
#[test]
fn test_no_dart_ffi_symbols() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let rust = gen_service_rust(&api, &config);
assert!(!rust.contains("dart:ffi"), "should not contain dart:ffi");
assert!(!rust.contains("NativeCallable"), "should not contain NativeCallable");
assert!(!rust.contains("lookupFunction"), "should not contain lookupFunction");
assert!(!rust.contains("ffi.Pointer"), "should not contain ffi.Pointer");
}
#[test]
fn test_skip_unrepresentable_finalize() {
let constructor = MethodDef {
name: "new".to_owned(),
params: vec![],
return_type: TypeRef::Unit,
is_async: false,
is_static: true,
error_type: None,
doc: String::new(),
receiver: None,
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 finalize_ep = EntrypointDef {
method: "into_router".to_owned(),
kind: EntrypointKind::Finalize,
is_async: false,
params: vec![],
return_type: TypeRef::Named("ExternalRouter".to_owned()), error_type: None,
doc: String::new(),
};
let api = ApiSurface {
crate_name: "test_crate".to_owned(),
version: "1.0.0".to_owned(),
services: vec![ServiceDef {
name: "TestService".to_owned(),
rust_path: "my_crate::TestService".to_owned(),
constructor,
configurators: vec![],
registrations: vec![],
entrypoints: vec![finalize_ep],
doc: String::new(),
cfg: None,
}],
..ApiSurface::default()
};
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let rust = gen_service_rust(&api, &config);
assert!(
!rust.contains("into_router"),
"should not emit unrepresentable finalize method:\n{rust}"
);
}
#[test]
fn test_skip_sanitized_finalize() {
let constructor = MethodDef {
name: "new".to_owned(),
params: vec![],
return_type: TypeRef::Unit,
is_async: false,
is_static: true,
error_type: None,
doc: String::new(),
receiver: None,
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 finalize_method = MethodDef {
name: "into_router".to_owned(),
params: vec![],
return_type: TypeRef::String, is_async: false,
is_static: false,
error_type: None,
doc: String::new(),
receiver: Some(crate::core::ir::ReceiverKind::RefMut),
sanitized: true, 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 finalize_ep = EntrypointDef {
method: "into_router".to_owned(),
kind: EntrypointKind::Finalize,
is_async: false,
params: vec![],
return_type: TypeRef::String, error_type: None,
doc: String::new(),
};
use crate::core::ir::TypeDef;
let service_type = TypeDef {
name: "TestService".to_owned(),
rust_path: "my_crate::TestService".to_owned(),
original_rust_path: String::new(),
fields: vec![],
methods: vec![finalize_method],
is_opaque: false,
is_clone: false,
is_copy: false,
doc: String::new(),
cfg: None,
is_trait: false,
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,
has_lifetime_params: false,
};
let api = ApiSurface {
crate_name: "test_crate".to_owned(),
version: "1.0.0".to_owned(),
types: vec![service_type],
services: vec![ServiceDef {
name: "TestService".to_owned(),
rust_path: "my_crate::TestService".to_owned(),
constructor,
configurators: vec![],
registrations: vec![],
entrypoints: vec![finalize_ep],
doc: String::new(),
cfg: None,
}],
..ApiSurface::default()
};
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let rust = gen_service_rust(&api, &config);
assert!(
!rust.contains("into_router"),
"should not emit finalize method when source is sanitized:\n{rust}"
);
}
#[test]
fn generate_returns_empty_for_no_services() {
let api = ApiSurface::default();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let files = generate(&api, &config).expect("generate should not fail");
assert!(files.is_empty(), "expected no files for surface without services");
}
#[test]
fn generate_returns_one_file_for_services() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let files = generate(&api, &config).expect("generate should not fail");
assert_eq!(files.len(), 1, "expected 1 generated file");
assert!(
files[0].path.to_string_lossy().ends_with("service_api.rs"),
"expected service_api.rs file"
);
}
#[test]
fn frb_user_callback_param_uses_non_shadowing_name() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let rust = gen_service_rust(&api, &config);
assert!(
rust.contains("cb: impl Fn(String) -> DartFnFuture<String>"),
"expected callback param named `cb` in:\n{rust}"
);
let service_method = rust
.split("pub fn add_handler(")
.nth(1)
.and_then(|body| body.split(") -> i32").next())
.expect("generated add_handler signature should be present");
assert!(
!service_method.contains("handler: impl Fn(String) -> DartFnFuture<String> + Send + Sync + 'static"),
"callback param must not be named `handler` to avoid FRB shadowing in:\n{rust}"
);
assert!(
rust.contains("::new(cb)"),
"expected callback forwarding via `cb` in:\n{rust}"
);
}
#[test]
fn test_registration_calls_inner_directly() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let rust = gen_service_rust(&api, &config);
assert!(
rust.contains("inner.add_handler("),
"expected immediate inner.add_handler() call in:\n{rust}"
);
assert!(
!rust.contains("for reg in registrations"),
"should not have registration draining loop in:\n{rust}"
);
assert!(
!rust.contains("match reg.method.as_str()"),
"should not have method dispatch match in:\n{rust}"
);
}
#[test]
fn test_emit_registration_variants() {
use crate::core::ir::{RegistrationVariant, WrapperConstructorArg, WrapperConstructorCall};
let constructor = MethodDef {
name: "new".to_owned(),
params: vec![],
return_type: TypeRef::Unit,
is_async: false,
is_static: true,
error_type: None,
doc: String::new(),
receiver: None,
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 variant_with_wrapper = RegistrationVariant {
name: "get".to_owned(),
overrides: vec![],
wrapper_call: Some(WrapperConstructorCall {
metadata_param: "builder".to_owned(),
wrapper_type_path: "my_crate::RouteBuilder".to_owned(),
wrapper_type_name: "RouteBuilder".to_owned(),
constructor_method: "new".to_owned(),
args: vec![
WrapperConstructorArg::Fixed {
param_name: "method".to_owned(),
value_expr: "my_crate::Method::GET".to_owned(),
},
WrapperConstructorArg::Free {
param: ParamDef {
name: "path".to_owned(),
ty: TypeRef::String,
optional: false,
default: None,
..ParamDef::default()
},
},
],
}),
signature_params: vec![ParamDef {
name: "path".to_owned(),
ty: TypeRef::String,
optional: false,
default: None,
..ParamDef::default()
}],
doc: Some("Register a GET handler.".to_owned()),
style: Default::default(),
};
let registration = RegistrationDef {
method: "route".to_owned(),
callback_param: "handler".to_owned(),
callback_contract: "RequestHandler".to_owned(),
metadata_params: vec![ParamDef {
name: "builder".to_owned(),
ty: TypeRef::Named("RouteBuilder".to_owned()),
optional: false,
default: None,
..ParamDef::default()
}],
receiver: Some(crate::core::ir::ReceiverKind::RefMut),
return_type: TypeRef::Unit,
error_type: Some("HandlerError".to_owned()),
doc: "Register a handler with a route builder.".to_owned(),
variants: vec![variant_with_wrapper],
};
let handler_contract = HandlerContractDef {
trait_name: "RequestHandler".to_owned(),
rust_path: "my_crate::RequestHandler".to_owned(),
dispatch: MethodDef {
name: "handle".to_owned(),
params: vec![ParamDef {
name: "req".to_owned(),
ty: TypeRef::Named("RequestData".to_owned()),
optional: false,
default: None,
..ParamDef::default()
}],
return_type: TypeRef::Named("Response".to_owned()),
is_async: true,
is_static: false,
error_type: None,
doc: String::new(),
receiver: Some(crate::core::ir::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,
},
optional_methods: vec![],
wire_request_type: Some("RequestData".to_owned()),
wire_response_type: Some("Response".to_owned()),
dispatch_extra_params: vec![],
wire_param_name: None,
dispatch_return_type: None,
response_adapter: None,
doc: String::new(),
};
let api = ApiSurface {
crate_name: "test_crate".to_owned(),
version: "1.0.0".to_owned(),
services: vec![ServiceDef {
name: "TestService".to_owned(),
rust_path: "my_crate::TestService".to_owned(),
constructor,
configurators: vec![],
registrations: vec![registration],
entrypoints: vec![],
doc: String::new(),
cfg: None,
}],
handler_contracts: vec![handler_contract],
..ApiSurface::default()
};
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let rust = gen_service_rust(&api, &config);
assert!(
rust.contains("pub fn get("),
"expected registration variant 'get' method in:\n{rust}"
);
assert!(
rust.contains("my_crate::RouteBuilder::new("),
"expected wrapper constructor call in:\n{rust}"
);
assert!(
rust.contains("my_crate::Method::GET"),
"expected fixed wrapper arg in:\n{rust}"
);
assert!(
rust.contains("self.route(RouteBuilder { inner }"),
"expected call to base route method with local newtype wrapper in:\n{rust}"
);
}
}