use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{
ApiSurface, EntrypointDef, EntrypointKind, HandlerContractDef, MethodDef, ParamDef, ReceiverKind, RegistrationDef,
ServiceDef, TypeRef,
};
const PREFIX: &str = "demo";
const ADVERSARIAL_SERVICE_NAMES: &[&str] = &["HTTPRouter", "UTF8Gateway", "Base64Relay", "_InternalBus", "AHub"];
const ADVERSARIAL_METHOD_NAMES: &[&str] = &["addURLHandler", "utf8Route", "Base64Sink", "_hidden", "run"];
fn param(name: &str) -> ParamDef {
ParamDef {
name: name.to_string(),
ty: TypeRef::String,
..ParamDef::default()
}
}
fn dispatch_method() -> MethodDef {
MethodDef {
name: "handle".to_string(),
params: vec![ParamDef {
name: "req".to_string(),
ty: TypeRef::Named("RequestData".to_string()),
..ParamDef::default()
}],
return_type: TypeRef::Named("ResponseData".to_string()),
is_async: true,
receiver: Some(ReceiverKind::Ref),
..MethodDef::default()
}
}
fn surface(service_name: &str, method_name: &str) -> ApiSurface {
let registration = RegistrationDef {
method: method_name.to_string(),
callback_param: "handler".to_string(),
callback_contract: "RequestHandler".to_string(),
metadata_params: vec![param("path")],
receiver: Some(ReceiverKind::RefMut),
return_type: TypeRef::Unit,
error_type: Some("HandlerError".to_string()),
..RegistrationDef::default()
};
let entrypoint = EntrypointDef {
method: method_name.to_string(),
kind: EntrypointKind::Run,
is_async: true,
params: vec![param("addr")],
return_type: TypeRef::Unit,
error_type: Some("IoError".to_string()),
doc: String::new(),
};
let contract = HandlerContractDef {
trait_name: "RequestHandler".to_string(),
rust_path: "demo_core::RequestHandler".to_string(),
dispatch: dispatch_method(),
optional_methods: vec![],
wire_request_type: Some("RequestData".to_string()),
wire_response_type: Some("ResponseData".to_string()),
dispatch_extra_params: vec![],
wire_param_name: None,
dispatch_return_type: None,
response_adapter: None,
doc: String::new(),
};
ApiSurface {
crate_name: PREFIX.to_string(),
version: "1.0.0".to_string(),
services: vec![ServiceDef {
name: service_name.to_string(),
rust_path: format!("demo_core::{service_name}"),
constructor: MethodDef {
name: "new".to_string(),
return_type: TypeRef::Unit,
is_static: true,
..MethodDef::default()
},
configurators: vec![],
registrations: vec![registration],
entrypoints: vec![entrypoint],
doc: String::new(),
cfg: None,
}],
handler_contracts: vec![contract],
..ApiSurface::default()
}
}
fn config() -> ResolvedCrateConfig {
ResolvedCrateConfig {
name: PREFIX.to_string(),
..ResolvedCrateConfig::default()
}
}
fn generated_go(api: &ApiSurface) -> String {
super::service_api::generate(api, &config(), "demo", PREFIX)
.expect("go service generation")
.into_iter()
.next()
.expect("a service surface produces one Go file")
.content
}
fn generated_ffi(api: &ApiSurface) -> String {
crate::backends::ffi::gen_bindings::service_api::generate(api, &config())
.expect("ffi service generation")
.into_iter()
.next()
.expect("a service surface produces one Rust file")
.content
}
fn called_c_symbols(generated: &str) -> Vec<String> {
let mut symbols = Vec::new();
for line in generated.lines().filter(|line| !line.trim_start().starts_with("//")) {
let mut rest = line;
while let Some(at) = rest.find("C.") {
rest = &rest[at + 2..];
let end = rest
.find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
.unwrap_or(rest.len());
let (symbol, tail) = rest.split_at(end);
if tail.starts_with('(') && !symbol.is_empty() {
symbols.push(symbol.to_string());
}
rest = tail;
}
}
symbols
}
fn exported_c_symbols(generated: &str) -> Vec<String> {
const MARKER: &str = "extern \"C\" fn ";
let mut symbols = Vec::new();
let mut rest = generated;
while let Some(at) = rest.find(MARKER) {
rest = &rest[at + MARKER.len()..];
let end = rest
.find(|c: char| !c.is_ascii_alphanumeric() && c != '_')
.unwrap_or(rest.len());
let (symbol, tail) = rest.split_at(end);
if !symbol.is_empty() {
symbols.push(symbol.to_string());
}
rest = tail;
}
symbols
}
#[test]
fn should_only_call_service_symbols_the_ffi_backend_exports() {
for service_name in ADVERSARIAL_SERVICE_NAMES {
for method_name in ADVERSARIAL_METHOD_NAMES {
let api = surface(service_name, method_name);
let exported = exported_c_symbols(&generated_ffi(&api));
let called = called_c_symbols(&generated_go(&api));
let service_calls: Vec<&String> = called
.iter()
.filter(|symbol| symbol.starts_with(&format!("{PREFIX}_")))
.collect();
assert!(
!service_calls.is_empty(),
"`{service_name}::{method_name}` produced no prefixed cgo call to check"
);
for symbol in service_calls {
assert!(
exported.contains(symbol),
"`{service_name}::{method_name}` calls `{symbol}`, but the FFI backend exports {exported:?}"
);
}
}
}
}
#[test]
fn should_emit_each_service_symbol_shape_with_its_own_infix() {
let api = surface("HTTPRouter", "addURLHandler");
let called = called_c_symbols(&generated_go(&api));
for expected in [
"demo_http_router_new",
"demo_http_router_free",
"demo_http_router_register_add_url_handler",
"demo_http_router_ep_add_url_handler",
] {
assert!(
called.contains(&expected.to_string()),
"expected a call to `{expected}`, got {called:?}"
);
}
}