use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{ApiSurface, HandlerContractDef, RegistrationDef, ServiceDef, TypeRef, WrapperConstructorArg};
use heck::{ToSnakeCase, ToUpperCamelCase};
use std::path::PathBuf;
#[allow(dead_code)]
fn find_contract<'a>(api: &'a ApiSurface, trait_name: &str) -> Option<&'a HandlerContractDef> {
api.handler_contracts.iter().find(|c| c.trait_name == trait_name)
}
fn typeref_to_go_type(ty: &TypeRef) -> String {
match ty {
TypeRef::String => "string".to_owned(),
TypeRef::Char => "byte".to_owned(),
TypeRef::Primitive(p) => {
use crate::core::ir::PrimitiveType;
match p {
PrimitiveType::Bool => "bool".to_owned(),
PrimitiveType::U8 => "uint8".to_owned(),
PrimitiveType::U16 => "uint16".to_owned(),
PrimitiveType::U32 => "uint32".to_owned(),
PrimitiveType::U64 => "uint64".to_owned(),
PrimitiveType::I8 => "int8".to_owned(),
PrimitiveType::I16 => "int16".to_owned(),
PrimitiveType::I32 => "int32".to_owned(),
PrimitiveType::I64 => "int64".to_owned(),
PrimitiveType::F32 => "float32".to_owned(),
PrimitiveType::F64 => "float64".to_owned(),
PrimitiveType::Usize => "uintptr".to_owned(),
PrimitiveType::Isize => "intptr".to_owned(),
}
}
TypeRef::Bytes => "[]byte".to_owned(),
TypeRef::Unit => "".to_owned(), TypeRef::Named(n) => n.clone(),
TypeRef::Optional(inner) => format!("*{}", typeref_to_go_type(inner)),
TypeRef::Vec(inner) => format!("[]{}", typeref_to_go_type(inner)),
TypeRef::Map(k, v) => format!("map[{}]{}", typeref_to_go_type(k), typeref_to_go_type(v)),
TypeRef::Json => "interface{}".to_owned(),
TypeRef::Path => "string".to_owned(),
TypeRef::Duration => "time.Duration".to_owned(),
}
}
fn entrypoint_return_representable(ep: &crate::core::ir::EntrypointDef, api: &ApiSurface) -> bool {
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 gen_service_go(api: &ApiSurface, config: &ResolvedCrateConfig, pkg_name: &str, ffi_prefix: &str) -> String {
let mut out = String::new();
let ffi_header = config.ffi_header_name();
out.push_str(&crate::backends::go::template_env::render(
"service_file_preamble.jinja",
minijinja::context! {
pkg_name => pkg_name,
ffi_header => ffi_header,
},
));
out.push_str("// ──────────────────────────────────────────── Service Definitions ──\n\n");
for service in &api.services {
gen_service_c_imports_comment(&mut out, service, api, ffi_prefix);
}
out.push_str("// ──────────────────────────────────────────── Handler Registry ──\n\n");
gen_handler_registry(&mut out);
out.push_str("// ──────────────────────────────────────────── Go Service API ──\n\n");
for service in &api.services {
gen_service_struct(&mut out, service, api, ffi_prefix, api);
}
out
}
fn gen_service_c_imports_comment(out: &mut String, service: &ServiceDef, _api: &ApiSurface, ffi_prefix: &str) {
let service_snake = service.name.to_snake_case();
let service_lower = ffi_prefix.to_lowercase();
let registrations = service
.registrations
.iter()
.map(|reg| {
let reg_method_snake = reg.method.to_snake_case();
let params = reg
.metadata_params
.iter()
.map(|meta_param| {
minijinja::context! {
c_type => typeref_to_c_type(&meta_param.ty),
name => &meta_param.name,
}
})
.collect::<Vec<_>>();
minijinja::context! {
symbol => format!("{service_lower}_{service_snake}_register_{reg_method_snake}"),
params => params,
}
})
.collect::<Vec<_>>();
let entrypoints = service
.entrypoints
.iter()
.map(|ep| {
let ep_name_snake = ep.method.to_snake_case();
let params = ep
.params
.iter()
.map(|ep_param| {
minijinja::context! {
c_type => typeref_to_c_type(&ep_param.ty),
name => &ep_param.name,
}
})
.collect::<Vec<_>>();
minijinja::context! {
return_c_type => typeref_to_c_type(&ep.return_type),
symbol => format!("{service_lower}_{service_snake}_ep_{ep_name_snake}"),
params => params,
}
})
.collect::<Vec<_>>();
out.push_str(&crate::backends::go::template_env::render(
"service_c_imports_comment.jinja",
minijinja::context! {
service_name => &service.name,
service_lower => &service_lower,
service_snake => &service_snake,
registrations => registrations,
entrypoints => entrypoints,
},
));
}
fn typeref_to_c_type(ty: &TypeRef) -> String {
match ty {
TypeRef::String => "const char*".to_owned(),
TypeRef::Char => "char".to_owned(),
TypeRef::Primitive(p) => {
use crate::core::ir::PrimitiveType;
match p {
PrimitiveType::Bool => "bool".to_owned(),
PrimitiveType::U8 => "uint8_t".to_owned(),
PrimitiveType::U16 => "uint16_t".to_owned(),
PrimitiveType::U32 => "uint32_t".to_owned(),
PrimitiveType::U64 => "uint64_t".to_owned(),
PrimitiveType::I8 => "int8_t".to_owned(),
PrimitiveType::I16 => "int16_t".to_owned(),
PrimitiveType::I32 => "int32_t".to_owned(),
PrimitiveType::I64 => "int64_t".to_owned(),
PrimitiveType::F32 => "float".to_owned(),
PrimitiveType::F64 => "double".to_owned(),
PrimitiveType::Usize => "uintptr_t".to_owned(),
PrimitiveType::Isize => "intptr_t".to_owned(),
}
}
TypeRef::Bytes => "const uint8_t*".to_owned(),
TypeRef::Unit => "void".to_owned(),
_ => "void*".to_owned(),
}
}
fn service_c_arg_expr(param_name: &str, ty: &TypeRef, api: &ApiSurface, upper_prefix: &str) -> String {
match ty {
TypeRef::String => format!("C.CString({param_name})"),
TypeRef::Named(type_name) if api.types.iter().any(|t| t.name == *type_name) => {
format!("(*C.{upper_prefix}{type_name})(unsafe.Pointer({param_name}.ptr))")
}
_ => {
let c_type = typeref_to_c_type(ty);
format!("{c_type}({param_name})")
}
}
}
fn service_c_arg_expr_with_marshal(
param_name: &str,
ty: &TypeRef,
api: &ApiSurface,
upper_prefix: &str,
ffi_prefix: &str,
) -> (String, String) {
match ty {
TypeRef::String => (String::new(), format!("C.CString({param_name})")),
TypeRef::Named(type_name) => {
if let Some(typedef) = api.types.iter().find(|t| t.name == *type_name) {
if typedef.is_opaque {
(
String::new(),
format!("(*C.{upper_prefix}{type_name})(unsafe.Pointer({param_name}.ptr))"),
)
} else {
let var_name = format!("c_{param_name}");
let type_name_snake = type_name.to_snake_case();
let mut preprocessing = format!(
"\t{var_name}JSON, err := json.Marshal({param_name})\n\
\tif err != nil {{\n\
\t\treturn err\n\
\t}}\n\
\t{var_name} := C.{ffi_prefix}_{type_name_snake}_from_json(C.CString(string({var_name}JSON)))\n\
\tif {var_name} == nil {{\n\
\t\treturn errors.New(\"{type_name} config failed\")\n\
\t}}\n\
\tdefer C.{ffi_prefix}_{type_name_snake}_free({var_name})\n"
);
preprocessing = preprocessing
.replace("{var_name}", &var_name)
.replace("{param_name}", param_name)
.replace("{ffi_prefix}", ffi_prefix)
.replace("{type_name_snake}", &type_name_snake)
.replace("{type_name}", type_name);
let arg_expr = var_name.to_string();
(preprocessing, arg_expr)
}
} else {
(
String::new(),
format!("(*C.{upper_prefix}{type_name})(unsafe.Pointer({param_name}.ptr))"),
)
}
}
_ => {
let c_type = typeref_to_c_type(ty);
(String::new(), format!("{c_type}({param_name})"))
}
}
}
fn emit_service_call_arg(out: &mut String, expr: &str) {
out.push_str(&crate::backends::go::template_env::render(
"service_call_arg_line.jinja",
minijinja::context! {
expr => expr,
},
));
}
fn gen_handler_registry(out: &mut String) {
out.push_str(&crate::backends::go::template_env::render(
"service_handler_registry.jinja",
minijinja::context! {},
));
}
fn go_doc_block(doc: &str) -> String {
let mut out = String::from("//\n");
for line in doc.trim_end().lines() {
let line = line.trim_end();
if line.is_empty() {
out.push_str("//\n");
} else {
out.push_str(&crate::backends::go::template_env::render(
"go_doc_block_line.jinja",
minijinja::context! {
line => line,
},
));
}
}
out
}
fn gen_service_struct(
out: &mut String,
service: &ServiceDef,
api: &ApiSurface,
ffi_prefix: &str,
api_surface: &ApiSurface,
) {
let service_name = &service.name;
let service_snake = service_name.to_snake_case();
let service_lower = ffi_prefix.to_lowercase();
let upper_prefix = ffi_prefix.to_uppercase();
let doc_block = if service.doc.is_empty() {
String::new()
} else {
go_doc_block(&service.doc)
};
out.push_str(&crate::backends::go::template_env::render(
"service_struct.jinja",
minijinja::context! {
service_name => service_name,
upper_prefix => upper_prefix,
doc_block => doc_block,
},
));
out.push_str(&crate::backends::go::template_env::render(
"service_constructor.jinja",
minijinja::context! {
service_name => service_name,
service_lower => service_lower,
service_snake => service_snake,
},
));
out.push_str(&crate::backends::go::template_env::render(
"service_close_method.jinja",
minijinja::context! {
service_name => service_name,
service_lower => service_lower,
service_snake => service_snake,
upper_prefix => upper_prefix,
},
));
for reg in &service.registrations {
gen_registration_method(out, service, reg, api, ffi_prefix);
}
for reg in &service.registrations {
for variant in ®.variants {
gen_registration_variant(out, service, reg, variant, api, ffi_prefix);
}
}
for cfg in &service.configurators {
gen_configurator_method(out, service, cfg, api, ffi_prefix);
}
for ep in &service.entrypoints {
gen_entrypoint_method(out, service, ep, api_surface, ffi_prefix);
}
gen_start_background_method(out, service, ffi_prefix);
}
fn gen_registration_method(
out: &mut String,
service: &ServiceDef,
reg: &RegistrationDef,
api: &ApiSurface,
ffi_prefix: &str,
) {
let service_name = &service.name;
let service_snake = service_name.to_snake_case();
let service_lower = ffi_prefix.to_lowercase();
let method_name = ®.method;
let method_name_pascal = method_name.to_upper_camel_case();
let reg_method_snake = method_name.to_snake_case();
let mut params = vec!["handler HandlerFunc".to_owned()];
for meta_param in ®.metadata_params {
let mut go_type = typeref_to_go_type(&meta_param.ty);
if let TypeRef::Named(type_name) = &meta_param.ty {
if api.types.iter().any(|t| t.name == *type_name) {
go_type = format!("*{}", go_type);
}
}
params.push(format!("{} {}", meta_param.name, go_type));
}
let param_sig = params.join(", ");
out.push_str(&crate::backends::go::template_env::render(
"service_register_comment.jinja",
minijinja::context! {
method_name_pascal => &method_name_pascal,
method_name => method_name,
},
));
if !reg.doc.is_empty() {
out.push_str(&go_doc_block(®.doc));
}
let return_type = if reg.error_type.is_some() {
"error".to_owned()
} else {
"".to_owned()
};
let return_sig = if !return_type.is_empty() {
format!(" {}", return_type)
} else {
String::new()
};
let closed_return = if reg.error_type.is_some() {
"\t\treturn errors.New(\"service is closed\")\n"
} else {
"\t\tpanic(\"service is closed\")\n"
};
out.push_str(&crate::backends::go::template_env::render(
"service_method_header.jinja",
minijinja::context! {
service_name => service_name,
method_name => format!("Register{method_name_pascal}"),
params => ¶m_sig,
return_sig => &return_sig,
closed_return => closed_return,
},
));
out.push_str("\tctxID := registerHandler(handler)\n");
let upper_prefix = ffi_prefix.to_uppercase();
out.push_str(&crate::backends::go::template_env::render(
"service_registration_call_header.jinja",
minijinja::context! {
service_lower => &service_lower,
service_snake => &service_snake,
reg_method_snake => ®_method_snake,
upper_prefix => &upper_prefix,
service_name => service_name,
},
));
for meta_param in ®.metadata_params {
let expr = service_c_arg_expr(&meta_param.name, &meta_param.ty, api, &upper_prefix);
emit_service_call_arg(out, &expr);
}
out.push_str("\t)\n\n");
out.push_str(&crate::backends::go::template_env::render(
"service_registration_return.jinja",
minijinja::context! {
returns_error => reg.error_type.is_some(),
},
));
}
fn gen_registration_variant(
out: &mut String,
service: &ServiceDef,
reg: &RegistrationDef,
variant: &crate::core::ir::RegistrationVariant,
api: &ApiSurface,
ffi_prefix: &str,
) {
let service_name = &service.name;
let service_snake = service_name.to_snake_case();
let service_lower = ffi_prefix.to_lowercase();
let variant_name_pascal = variant.name.to_upper_camel_case();
let variant_name_snake = variant.name.to_snake_case();
let mut params = vec!["handler HandlerFunc".to_owned()];
for sig_param in &variant.signature_params {
let go_type = typeref_to_go_type(&sig_param.ty);
params.push(format!("{} {}", sig_param.name, go_type));
}
let param_sig = params.join(", ");
out.push_str(&crate::backends::go::template_env::render(
"service_variant_comment.jinja",
minijinja::context! {
variant_name_pascal => &variant_name_pascal,
variant_name => &variant.name,
},
));
if let Some(doc) = &variant.doc {
out.push_str(&go_doc_block(doc));
}
let return_type = if reg.error_type.is_some() {
"error".to_owned()
} else {
"".to_owned()
};
let return_sig = if !return_type.is_empty() {
format!(" {}", return_type)
} else {
String::new()
};
let closed_return = if reg.error_type.is_some() {
"\t\treturn errors.New(\"service is closed\")\n"
} else {
"\t\tpanic(\"service is closed\")\n"
};
out.push_str(&crate::backends::go::template_env::render(
"service_method_header.jinja",
minijinja::context! {
service_name => service_name,
method_name => &variant_name_pascal,
params => ¶m_sig,
return_sig => &return_sig,
closed_return => closed_return,
},
));
out.push_str("\tctxID := registerHandler(handler)\n");
let upper_prefix = ffi_prefix.to_uppercase();
out.push_str(&crate::backends::go::template_env::render(
"service_variant_call_header.jinja",
minijinja::context! {
service_lower => &service_lower,
service_snake => &service_snake,
variant_name_snake => &variant_name_snake,
upper_prefix => &upper_prefix,
service_name => service_name,
},
));
if let Some(wc) = &variant.wrapper_call {
for arg in &wc.args {
if let WrapperConstructorArg::Free { param } = arg {
let expr = service_c_arg_expr(¶m.name, ¶m.ty, api, &upper_prefix);
emit_service_call_arg(out, &expr);
}
}
} else {
for base_param in ®.metadata_params {
if variant.overrides.iter().any(|o| o.param_name == base_param.name) {
} else if let Some(sig_param) = variant.signature_params.iter().find(|s| s.name == base_param.name) {
let expr = service_c_arg_expr(&sig_param.name, &sig_param.ty, api, &upper_prefix);
emit_service_call_arg(out, &expr);
}
}
}
out.push_str("\t)\n\n");
out.push_str(&crate::backends::go::template_env::render(
"service_registration_return.jinja",
minijinja::context! {
returns_error => reg.error_type.is_some(),
},
));
}
fn gen_configurator_method(
out: &mut String,
service: &ServiceDef,
cfg: &crate::core::ir::MethodDef,
api: &ApiSurface,
ffi_prefix: &str,
) {
let service_name = &service.name;
let service_snake = service_name.to_snake_case();
let service_lower = ffi_prefix.to_lowercase();
let cfg_method_pascal = cfg.name.to_upper_camel_case();
let cfg_method_snake = cfg.name.to_snake_case();
let mut params = Vec::new();
for cfg_param in &cfg.params {
let go_type = typeref_to_go_type(&cfg_param.ty);
let final_type = if let TypeRef::Named(type_name) = &cfg_param.ty {
if api.types.iter().any(|t| t.name == *type_name) {
format!("*{}", go_type)
} else {
go_type
}
} else {
go_type
};
params.push(format!("{} {}", cfg_param.name, final_type));
}
let param_sig = if params.is_empty() {
String::new()
} else {
params.join(", ")
};
out.push_str(&crate::backends::go::template_env::render(
"service_configurator_comment.jinja",
minijinja::context! {
cfg_method_pascal => &cfg_method_pascal,
cfg_name => &cfg.name,
},
));
if !cfg.doc.is_empty() {
out.push_str(&go_doc_block(&cfg.doc));
}
out.push_str(&crate::backends::go::template_env::render(
"service_method_header.jinja",
minijinja::context! {
service_name => service_name,
method_name => &cfg_method_pascal,
params => ¶m_sig,
return_sig => " error",
closed_return => "\t\treturn errors.New(\"service is closed\")\n",
},
));
let upper_prefix = ffi_prefix.to_uppercase();
let mut cfg_args = Vec::new();
let mut preprocessing = String::new();
cfg_args.push(minijinja::context! {
expr => format!("(*C.{upper_prefix}{service_name}Opaque)(s.owner)"),
});
for cfg_param in &cfg.params {
let (pre, expr) =
service_c_arg_expr_with_marshal(&cfg_param.name, &cfg_param.ty, api, &upper_prefix, ffi_prefix);
preprocessing.push_str(&pre);
cfg_args.push(minijinja::context! {
expr => expr,
});
}
if !preprocessing.is_empty() {
out.push_str(&preprocessing);
}
out.push_str(&crate::backends::go::template_env::render(
"service_configurator_call.jinja",
minijinja::context! {
service_lower => &service_lower,
service_snake => &service_snake,
cfg_method_snake => &cfg_method_snake,
service_name => service_name,
args => cfg_args,
},
));
}
fn gen_entrypoint_method(
out: &mut String,
service: &ServiceDef,
ep: &crate::core::ir::EntrypointDef,
api: &ApiSurface,
ffi_prefix: &str,
) {
use crate::core::ir::EntrypointKind;
if matches!(ep.kind, EntrypointKind::Finalize) && !entrypoint_return_representable(ep, api) {
return;
}
let service_name = &service.name;
let service_snake = service_name.to_snake_case();
let service_lower = ffi_prefix.to_lowercase();
let ep_method = &ep.method;
let ep_method_pascal = ep_method.to_upper_camel_case();
let ep_name_snake = ep_method.to_snake_case();
let mut params = vec![];
for ep_param in &ep.params {
let go_type = typeref_to_go_type(&ep_param.ty);
params.push(format!("{} {}", ep_param.name, go_type));
}
let param_sig = if params.is_empty() {
String::new()
} else {
params.join(", ")
};
let upper_prefix = ffi_prefix.to_uppercase();
let opaque_return = match &ep.return_type {
TypeRef::Named(n) if api.types.iter().any(|t| t.name == *n) => Some(n.clone()),
_ => None,
};
let has_err = ep.error_type.is_some();
let return_sig = match (&opaque_return, has_err) {
(Some(t), true) => format!(" (*{t}, error)"),
(Some(t), false) => format!(" *{t}"),
(None, true) => " error".to_owned(),
(None, false) => String::new(),
};
out.push_str(&crate::backends::go::template_env::render(
"service_entrypoint_comment.jinja",
minijinja::context! {
ep_method_pascal => &ep_method_pascal,
ep_method => ep_method,
},
));
if !ep.doc.is_empty() {
out.push_str(&go_doc_block(&ep.doc));
}
let closed_return = match (&opaque_return, has_err) {
(Some(_), true) => "\t\treturn nil, errors.New(\"service is closed\")\n",
(Some(_), false) => "\t\treturn nil\n",
(None, true) => "\t\treturn errors.New(\"service is closed\")\n",
(None, false) => "\t\tpanic(\"service is closed\")\n",
};
out.push_str(&crate::backends::go::template_env::render(
"service_method_header.jinja",
minijinja::context! {
service_name => service_name,
method_name => &ep_method_pascal,
params => ¶m_sig,
return_sig => &return_sig,
closed_return => closed_return,
},
));
let capture = if opaque_return.is_some() || has_err {
"ret := "
} else {
""
};
out.push_str(&crate::backends::go::template_env::render(
"service_entrypoint_call_header.jinja",
minijinja::context! {
capture => capture,
service_lower => &service_lower,
service_snake => &service_snake,
ep_name_snake => &ep_name_snake,
upper_prefix => &upper_prefix,
service_name => service_name,
},
));
for ep_param in &ep.params {
let expr = service_c_arg_expr(&ep_param.name, &ep_param.ty, api, &upper_prefix);
emit_service_call_arg(out, &expr);
}
out.push_str("\t)\n");
match (&opaque_return, has_err) {
(Some(t), true) => {
out.push_str(&crate::backends::go::template_env::render(
"service_entrypoint_return_opaque_err.jinja",
minijinja::context! {
ep_method => ep_method,
return_type => t,
},
));
}
(Some(t), false) => {
out.push_str(&crate::backends::go::template_env::render(
"service_entrypoint_return_opaque.jinja",
minijinja::context! {
return_type => t,
},
));
}
(None, true) => {
out.push_str(&crate::backends::go::template_env::render(
"service_entrypoint_return_err.jinja",
minijinja::context! {
ep_method => ep_method,
},
));
}
(None, false) => {}
}
out.push_str("}\n\n");
}
pub fn generate(
api: &ApiSurface,
config: &ResolvedCrateConfig,
pkg_name: &str,
ffi_prefix: &str,
) -> anyhow::Result<Vec<GeneratedFile>> {
if api.services.is_empty() {
return Ok(vec![]);
}
let output_dir = {
let mut d =
crate::core::config::resolve_output_dir(config.output_paths.get("go"), &config.name, "packages/go/");
if !d.ends_with('/') {
d.push('/');
}
d
};
let service_go = gen_service_go(api, config, pkg_name, ffi_prefix);
Ok(vec![GeneratedFile {
path: PathBuf::from(&output_dir).join("service.go"),
content: service_go,
generated_header: true,
}])
}
fn gen_start_background_method(out: &mut String, service: &ServiceDef, _ffi_prefix: &str) {
let service_name = &service.name;
out.push_str(&crate::backends::go::template_env::render(
"service_start_background.jinja",
minijinja::context! {
service_name => service_name,
},
));
}
#[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 get_variant = crate::core::ir::RegistrationVariant {
name: "get".to_owned(),
overrides: vec![crate::core::ir::RegistrationVariantOverride {
param_name: "method".to_owned(),
value_expr: "\"GET\"".to_owned(),
}],
wrapper_call: None,
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: "add_handler".to_owned(),
callback_param: "handler".to_owned(),
callback_contract: "RequestHandler".to_owned(),
metadata_params: vec![
ParamDef {
name: "method".to_owned(),
ty: TypeRef::String,
optional: false,
default: None,
..ParamDef::default()
},
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![get_variant],
};
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_gen_service_go_produces_valid_go() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let go = gen_service_go(&api, &config, "binding", "TEST_CRATE");
assert!(go.contains("package binding"));
assert!(go.contains("TestService"));
assert!(go.contains("NewTestService"));
assert!(go.contains("RegisterAddHandler"));
assert!(go.contains("Run"));
assert!(go.contains("HandlerFunc"));
assert!(go.contains("handlerRegistry"));
assert!(go.contains("service_handler_callback"));
assert!(go.contains("/*\n#include <string.h>"));
assert!(go.contains("#include \"test_crate.h\""));
assert!(go.contains("//export service_handler_callback"));
assert!(go.contains("import \"C\""));
assert!(go.contains("*TEST_CRATETestServiceOpaque"));
}
#[test]
fn test_service_struct_is_generated() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let go = gen_service_go(&api, &config, "binding", "TEST_CRATE");
assert!(go.contains("type TestService struct"));
assert!(go.contains("owner unsafe.Pointer"));
assert!(go.contains("*TEST_CRATETestServiceOpaque"));
assert!(go.contains("mu sync.Mutex"));
}
#[test]
fn test_constructor_is_generated() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let go = gen_service_go(&api, &config, "binding", "test_crate");
assert!(go.contains("func NewTestService()"));
assert!(go.contains("test_crate_test_service_new"));
}
#[test]
fn test_registration_method_exists() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let go = gen_service_go(&api, &config, "binding", "test_crate");
assert!(go.contains("RegisterAddHandler"));
assert!(go.contains("handler HandlerFunc"));
assert!(go.contains("registerHandler(handler)"));
assert!(go.contains("C.get_service_handler_callback(),"));
assert!(go.contains("(*C.TEST_CRATETestServiceOpaque)"));
}
#[test]
fn test_entrypoint_method_exists() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let go = gen_service_go(&api, &config, "binding", "test_crate");
assert!(go.contains("func (s *TestService) Run("));
assert!(go.contains("test_crate_test_service_ep_run"));
assert!(go.contains("(*C.TEST_CRATETestServiceOpaque)"));
}
#[test]
fn test_handler_registry_and_trampoline() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let go = gen_service_go(&api, &config, "binding", "test_crate");
assert!(go.contains("handlerRegistry"));
assert!(go.contains("service_handler_callback"));
assert!(go.contains("invokeHandler"));
assert!(go.contains("registerHandler"));
assert!(go.contains("//export service_handler_callback"));
}
#[test]
fn test_c_ffi_imports_generated() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let go = gen_service_go(&api, &config, "binding", "test_crate");
assert!(go.contains("test_crate_test_service_new"));
assert!(go.contains("test_crate_test_service_free"));
assert!(go.contains("test_crate_test_service_register_add_handler"));
}
#[test]
fn test_registration_variant_method_exists() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let go = gen_service_go(&api, &config, "binding", "test_crate");
assert!(go.contains("func (s *TestService) Get("));
assert!(go.contains("handler HandlerFunc"));
assert!(go.contains("path string"));
assert!(go.contains("C.test_crate_test_service_get"));
assert!(!go.contains("C.test_crate_test_service_add_handler_get"));
assert!(go.contains("C.CString(path)"));
}
#[test]
fn test_start_background_method_exists() {
let api = make_fixture_surface();
let config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let go = gen_service_go(&api, &config, "binding", "test_crate");
assert!(go.contains("func (s *TestService) StartBackground("));
assert!(go.contains("type ServerHandle struct"));
assert!(go.contains("func (h *ServerHandle) Stop()"));
assert!(go.contains("host string, port uint16"));
assert!(go.contains("*ServerHandle, error"));
}
#[test]
fn test_registration_variant_wrapper_call_emits_free_args() {
use crate::core::ir::{WrapperConstructorArg, WrapperConstructorCall};
let mut api = make_fixture_surface();
let svc = &mut api.services[0];
let reg = &mut svc.registrations[0];
reg.variants[0] = crate::core::ir::RegistrationVariant {
name: "get".to_owned(),
overrides: vec![crate::core::ir::RegistrationVariantOverride {
param_name: "method".to_owned(),
value_expr: "\"GET\"".to_owned(),
}],
wrapper_call: Some(WrapperConstructorCall {
metadata_param: "builder".to_owned(),
wrapper_type_path: "test_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: "\"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 config = ResolvedCrateConfig {
name: "test_crate".to_owned(),
..ResolvedCrateConfig::default()
};
let go = gen_service_go(&api, &config, "binding", "test_crate");
assert!(go.contains("C.CString(path)"), "missing CString(path) in:\n{go}");
assert!(!go.contains("\"GET\""), "fixed arg must not be re-emitted:\n{go}");
}
}