use super::methods::gen_param_to_c;
use super::types::{emit_type_doc, go_return_expr};
use crate::backends::go::type_map::{go_optional_type, go_type};
use crate::codegen::naming::{go_param_name, to_go_name};
use crate::core::ir::{FunctionDef, MethodDef, ParamDef, TypeRef};
use heck::ToSnakeCase;
use std::collections::HashSet;
use std::fmt::Write as FmtWrite;
pub(super) fn params_require_marshal(params: &[ParamDef], opaque_names: &std::collections::HashSet<&str>) -> bool {
params.iter().any(|p| match &p.ty {
TypeRef::Named(name) => !opaque_names.contains(name.as_str()),
TypeRef::Vec(_) | TypeRef::Map(_, _) => true,
_ => false,
})
}
pub(super) fn is_bridge_param(
param: &ParamDef,
bridge_param_names: &HashSet<String>,
bridge_type_aliases: &HashSet<String>,
) -> bool {
if bridge_param_names.contains(param.name.as_str()) {
return true;
}
let type_name = match ¶m.ty {
TypeRef::Named(n) => Some(n.as_str()),
TypeRef::Optional(inner) => {
if let TypeRef::Named(n) = inner.as_ref() {
Some(n.as_str())
} else {
None
}
}
_ => None,
};
type_name.is_some_and(|n| bridge_type_aliases.contains(n))
}
fn is_bytes_result_func(func: &FunctionDef) -> bool {
func.error_type.is_some() && matches!(func.return_type, TypeRef::Bytes)
}
pub(super) fn is_bytes_result_method(method: &MethodDef) -> bool {
method.error_type.is_some() && matches!(method.return_type, TypeRef::Bytes)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn gen_function_wrapper(
func: &FunctionDef,
ffi_prefix: &str,
opaque_names: &std::collections::HashSet<&str>,
bridge_param_names: &HashSet<String>,
bridge_type_aliases: &HashSet<String>,
value_only_types: &std::collections::HashSet<String>,
enum_names: &std::collections::HashSet<String>,
ffi_param_enum_names: &std::collections::HashSet<String>,
) -> String {
let mut out = String::with_capacity(2048);
let func_go_name = to_go_name(&func.name);
emit_type_doc(&mut out, &func_go_name, &func.doc, "calls the FFI function.");
let is_bytes_result = is_bytes_result_func(func);
let non_bridge_params: Vec<_> = func
.params
.iter()
.filter(|p| !is_bridge_param(p, bridge_param_names, bridge_type_aliases))
.cloned()
.collect();
let marshals_params = params_require_marshal(&non_bridge_params, opaque_names);
let can_return_error = func.error_type.is_some() || marshals_params;
let return_type = if is_bytes_result {
"([]byte, error)".to_string()
} else if can_return_error {
if matches!(func.return_type, TypeRef::Unit) {
"error".to_string()
} else if matches!(
func.return_type,
TypeRef::Primitive(_) | TypeRef::Duration | TypeRef::String | TypeRef::Char | TypeRef::Path
) {
format!("({}, error)", go_type(&func.return_type))
} else {
format!("({}, error)", go_optional_type(&func.return_type))
}
} else if matches!(func.return_type, TypeRef::Unit) {
"".to_string()
} else if matches!(
func.return_type,
TypeRef::Primitive(_) | TypeRef::Duration | TypeRef::String | TypeRef::Char | TypeRef::Path
) {
go_type(&func.return_type).into_owned()
} else {
go_optional_type(&func.return_type).into_owned()
};
let func_snake = func.name.to_snake_case();
let ffi_name = format!("C.{}_{}", ffi_prefix, func_snake);
let mut param_strs: Vec<String> = Vec::new();
for p in func.params.iter() {
if is_bridge_param(p, bridge_param_names, bridge_type_aliases) {
continue;
}
let param_type: String = if p.optional {
go_optional_type(&p.ty).into_owned()
} else if let TypeRef::Named(name) = &p.ty {
if opaque_names.contains(name.as_str()) {
format!("*{}", go_type(&p.ty))
} else {
go_type(&p.ty).into_owned()
}
} else {
go_type(&p.ty).into_owned()
};
param_strs.push(format!("{} {}", go_param_name(&p.name), param_type));
}
let params_str = param_strs.join(", ");
let ret_type_str = if return_type.is_empty() {
"".to_string()
} else {
format!(" {}", return_type)
};
out.push_str(&crate::backends::go::template_env::render(
"function_signature.jinja",
minijinja::context! {
func_name => func_go_name,
params => ¶ms_str,
return_type => &ret_type_str,
},
));
let returns_value_and_error = can_return_error && !matches!(func.return_type, TypeRef::Unit);
let param_err_return_prefix: String = if returns_value_and_error {
format!("{}, ", crate::backends::go::type_map::go_zero_value(&func.return_type))
} else {
String::new()
};
for param in func.params.iter() {
if is_bridge_param(param, bridge_param_names, bridge_type_aliases) {
continue;
}
out.push_str(&gen_param_to_c(
param,
¶m_err_return_prefix,
can_return_error,
ffi_prefix,
opaque_names,
enum_names,
ffi_param_enum_names,
));
}
let c_params: Vec<String> = func
.params
.iter()
.flat_map(|p| -> Vec<String> {
if is_bridge_param(p, bridge_param_names, bridge_type_aliases) {
if p.sanitized { vec![] } else { vec!["nil".to_string()] }
} else {
let c_name = go_param_name(&format!("c_{}", p.name));
if matches!(p.ty, TypeRef::Bytes) {
vec![c_name.clone(), format!("{}Len", c_name)]
} else {
vec![c_name]
}
}
})
.collect();
let c_call = if is_bytes_result {
let mut all_params = c_params.clone();
all_params.push("&outPtr".to_string());
all_params.push("&outLen".to_string());
all_params.push("&outCap".to_string());
format!("{}({})", ffi_name, all_params.join(", "))
} else {
format!("{}({})", ffi_name, c_params.join(", "))
};
if is_bytes_result {
out.push_str(&crate::backends::go::template_env::render(
"bytes_result_call.jinja",
minijinja::context! {
c_call => &c_call,
ffi_prefix => ffi_prefix,
},
));
out.push_str(&crate::backends::go::template_env::render(
"function_body_end.jinja",
minijinja::Value::default(),
));
return out;
}
if can_return_error {
if matches!(func.return_type, TypeRef::Unit) {
out.push_str(&crate::backends::go::template_env::render(
"c_call_simple.jinja",
minijinja::context! {
c_call => &c_call,
},
));
if func.error_type.is_some() {
out.push_str("\treturn lastError()\n");
} else {
out.push_str("\treturn nil\n");
}
} else {
out.push_str(&crate::backends::go::template_env::render(
"c_ptr_assign.jinja",
minijinja::context! {
c_call => &c_call,
},
));
if func.error_type.is_some() {
out.push_str("\tif err := lastError(); err != nil {\n");
if matches!(
func.return_type,
TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json
) {
out.push_str("\t\tif ptr != nil {\n");
out.push_str(&crate::backends::go::template_env::render(
"free_string_on_error.jinja",
minijinja::context! {
ffi_prefix => ffi_prefix,
},
));
out.push_str("\t\t}\n");
}
if let TypeRef::Named(name) = &func.return_type {
let type_snake = name.to_snake_case();
out.push_str("\t\tif ptr != nil {\n");
out.push_str(&crate::backends::go::template_env::render(
"free_type_on_error.jinja",
minijinja::context! {
ffi_prefix => ffi_prefix,
type_snake => &type_snake,
},
));
out.push_str("\t\t}\n");
}
out.push_str(&format!(
"\t\treturn {}, err\n",
crate::backends::go::type_map::go_zero_value(&func.return_type)
));
out.push_str("\t}\n");
}
if matches!(
func.return_type,
TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json
) {
out.push_str(&crate::backends::go::template_env::render(
"free_string.jinja",
minijinja::context! {
ffi_prefix => ffi_prefix,
ptr => "ptr",
},
));
}
if let TypeRef::Named(name) = &func.return_type {
if !opaque_names.contains(name.as_str()) {
let type_snake = name.to_snake_case();
out.push_str(&crate::backends::go::template_env::render(
"free_type.jinja",
minijinja::context! {
ffi_prefix => ffi_prefix,
type_snake => &type_snake,
ptr => "ptr",
},
));
}
}
if can_return_error {
if let TypeRef::Named(name) = &func.return_type {
if !opaque_names.contains(name.as_str()) {
let type_snake = name.to_snake_case();
out.push_str(&crate::backends::go::template_env::render(
"c_json_to_json.jinja",
minijinja::context! {
ffi_prefix => ffi_prefix,
type_snake => &type_snake,
},
));
out.push_str("\tif jsonPtr == nil {\n");
out.push_str("\t\treturn nil, fmt.Errorf(\"failed to convert to JSON\")\n");
out.push_str("\t}\n");
out.push_str(&crate::backends::go::template_env::render(
"free_string.jinja",
minijinja::context! {
ffi_prefix => ffi_prefix,
ptr => "jsonPtr",
},
));
out.push_str(&crate::backends::go::template_env::render(
"var_decl_type.jinja",
minijinja::context! {
type_name => name.as_str(),
},
));
out.push_str(
"\tif err := json.Unmarshal([]byte(C.GoString(jsonPtr)), &result); err != nil {\n",
);
out.push_str("\t\treturn nil, fmt.Errorf(\"failed to unmarshal: %w\", err)\n");
out.push_str("\t}\n");
out.push_str("\treturn &result, nil\n");
} else {
let return_expr =
go_return_expr(&func.return_type, "ptr", ffi_prefix, opaque_names, value_only_types);
out.push_str(&crate::backends::go::template_env::render(
"method_return_simple.jinja",
minijinja::context! {
value => format!("{}, nil", return_expr),
},
));
}
} else if matches!(func.return_type, TypeRef::Vec(_)) {
if let TypeRef::Vec(inner) = &func.return_type {
let go_elem = go_type(inner);
out.push_str("\tif ptr == nil {\n");
out.push_str("\t\treturn nil, fmt.Errorf(\"failed to get result\")\n");
out.push_str("\t}\n");
out.push_str(&crate::backends::go::template_env::render(
"free_string.jinja",
minijinja::context! {
ffi_prefix => ffi_prefix,
ptr => "ptr",
},
));
out.push_str(&crate::backends::go::template_env::render(
"var_decl_slice.jinja",
minijinja::context! {
element_type => &go_elem,
},
));
out.push_str("\tif err := json.Unmarshal([]byte(C.GoString(ptr)), &result); err != nil {\n");
out.push_str("\t\treturn nil, fmt.Errorf(\"failed to unmarshal: %w\", err)\n");
out.push_str("\t}\n");
out.push_str(&crate::backends::go::template_env::render(
"method_return_simple.jinja",
minijinja::context! {
value => "result, nil",
},
));
}
} else {
let return_expr =
go_return_expr(&func.return_type, "ptr", ffi_prefix, opaque_names, value_only_types);
out.push_str(&crate::backends::go::template_env::render(
"method_return_simple.jinja",
minijinja::context! {
value => format!("{}, nil", return_expr),
},
));
}
} else {
let return_expr = go_return_expr(&func.return_type, "ptr", ffi_prefix, opaque_names, value_only_types);
out.push_str(&crate::backends::go::template_env::render(
"method_return_simple.jinja",
minijinja::context! {
value => return_expr,
},
));
}
}
} else if matches!(func.return_type, TypeRef::Unit) {
out.push_str(&crate::backends::go::template_env::render(
"c_call_simple.jinja",
minijinja::context! {
c_call => &c_call,
},
));
} else {
out.push_str(&crate::backends::go::template_env::render(
"c_ptr_assign.jinja",
minijinja::context! {
c_call => &c_call,
},
));
if matches!(
func.return_type,
TypeRef::String | TypeRef::Char | TypeRef::Path | TypeRef::Json
) {
out.push_str(&crate::backends::go::template_env::render(
"free_string.jinja",
minijinja::context! {
ffi_prefix => ffi_prefix,
ptr => "ptr",
},
));
}
if let TypeRef::Named(name) = &func.return_type {
if !opaque_names.contains(name.as_str()) {
let type_snake = name.to_snake_case();
out.push_str(&crate::backends::go::template_env::render(
"free_type.jinja",
minijinja::context! {
ffi_prefix => ffi_prefix,
type_snake => &type_snake,
ptr => "ptr",
},
));
}
}
let return_expr = go_return_expr(&func.return_type, "ptr", ffi_prefix, opaque_names, value_only_types);
out.push_str(&crate::backends::go::template_env::render(
"method_return_simple.jinja",
minijinja::context! {
value => return_expr,
},
));
}
out.push_str(&crate::backends::go::template_env::render(
"function_body_end.jinja",
minijinja::Value::default(),
));
out
}
pub(super) fn gen_convert_with_visitor_wrapper(
func: &FunctionDef,
ffi_prefix: &str,
opaque_names: &std::collections::HashSet<&str>,
_value_only_types: &std::collections::HashSet<String>,
) -> String {
let mut out = String::with_capacity(2048);
let func_go_name = to_go_name(&func.name);
emit_type_doc(&mut out, &func_go_name, &func.doc, "runs the generated conversion.");
let options_param = func.params.iter().find(|p| p.name == "options");
let mut param_strs: Vec<String> = Vec::new();
for p in &func.params {
let param_type = if p.optional {
go_optional_type(&p.ty).into_owned()
} else if let TypeRef::Named(name) = &p.ty {
if opaque_names.contains(name.as_str()) {
format!("*{}", go_type(&p.ty))
} else {
go_type(&p.ty).into_owned()
}
} else {
go_type(&p.ty).into_owned()
};
param_strs.push(format!("{} {}", go_param_name(&p.name), param_type));
}
let params_str = param_strs.join(", ");
out.push_str(&crate::backends::go::template_env::render(
"function_signature.jinja",
minijinja::context! {
func_name => func_go_name,
params => ¶ms_str,
return_type => " (*ConversionResult, error)",
},
));
if options_param.is_some() {
out.push_str("\tif options != nil && options.Visitor != nil {\n");
out.push_str("\t\treturn convertWithVisitorHelper(html, options, options.Visitor)\n");
out.push_str("\t}\n");
out.push('\n');
}
let func_snake = func.name.to_snake_case();
let ffi_name = format!("C.{}_{}", ffi_prefix, func_snake);
out.push_str("\tcHTML := C.CString(html)\n");
out.push_str("\tdefer C.free(unsafe.Pointer(cHTML))\n");
out.push('\n');
if options_param.is_some() {
out.push_str("\tvar cOptions *C.HTMConversionOptions\n");
out.push_str("\tif options != nil {\n");
out.push_str("\t\tjsonBytes, err := json.Marshal(options)\n");
out.push_str("\t\tif err != nil {\n");
out.push_str("\t\t\treturn nil, fmt.Errorf(\"failed to marshal options: %w\", err)\n");
out.push_str("\t\t}\n");
out.push_str("\t\ttmpStr := C.CString(string(jsonBytes))\n");
out.push_str(&crate::backends::go::template_env::render(
"c_options_from_json_with_name.jinja",
minijinja::context! {
ffi_prefix => ffi_prefix,
},
));
out.push_str("\t\tC.free(unsafe.Pointer(tmpStr))\n");
out.push_str(&crate::backends::go::template_env::render(
"c_options_defer_free_with_name.jinja",
minijinja::context! {
ffi_prefix => ffi_prefix,
},
));
out.push_str("\t}\n");
out.push('\n');
out.push_str(&crate::backends::go::template_env::render(
"c_ptr_assign_func.jinja",
minijinja::context! {
ffi_name => &ffi_name,
options_var => "cOptions",
},
));
} else {
out.push_str(&crate::backends::go::template_env::render(
"c_ptr_assign_func.jinja",
minijinja::context! {
ffi_name => &ffi_name,
options_var => "nil",
},
));
}
out.push_str("\tif ptr == nil {\n");
out.push_str("\t\tif err := lastError(); err != nil {\n");
out.push_str("\t\t\treturn nil, err\n");
out.push_str("\t\t}\n");
out.push_str("\t\treturn nil, fmt.Errorf(\"conversion returned nil\")\n");
out.push_str("\t}\n");
out.push_str(&crate::backends::go::template_env::render(
"c_conversion_result_free.jinja",
minijinja::context! {
ffi_prefix => ffi_prefix,
},
));
out.push('\n');
out.push_str(&crate::backends::go::template_env::render(
"c_conversion_result_to_json.jinja",
minijinja::context! {
ffi_prefix => ffi_prefix,
},
));
out.push_str("\tif jsonPtr == nil {\n");
out.push_str("\t\treturn nil, fmt.Errorf(\"failed to convert result to JSON\")\n");
out.push_str("\t}\n");
out.push_str(&crate::backends::go::template_env::render(
"c_free_string_defer.jinja",
minijinja::context! {
ffi_prefix => ffi_prefix,
},
));
out.push_str("\tvar result ConversionResult\n");
out.push_str("\tif err := json.Unmarshal([]byte(C.GoString(jsonPtr)), &result); err != nil {\n");
out.push_str("\t\treturn nil, fmt.Errorf(\"failed to unmarshal result: %w\", err)\n");
out.push_str("\t}\n");
out.push_str("\treturn &result, nil\n");
out.push_str(&crate::backends::go::template_env::render(
"function_body_end.jinja",
minijinja::Value::default(),
));
out
}
pub(super) fn gen_adapter_wrapper(
adapter: &crate::core::config::AdapterConfig,
_pkg_name: &str,
types: &[crate::core::ir::TypeDef],
) -> String {
let adapter_name = &adapter.name;
let go_func_name = to_go_name(adapter_name);
let owner_type = adapter.owner_type.as_deref().unwrap_or_else(|| {
panic!(
"go adapter `{adapter_name}`: streaming adapter requires `owner_type` in `[[adapters]]` config (the Rust handle type that owns the streaming method)"
)
});
let item_type = adapter.item_type.as_deref().unwrap_or_else(|| {
panic!(
"go adapter `{adapter_name}`: streaming adapter requires `item_type` in `[[adapters]]` config (the Rust item type yielded by the stream)"
)
});
let item_type_simple = item_type.rsplit("::").next().unwrap_or(item_type);
let request_type = adapter.request_type.as_deref().unwrap_or_else(|| {
panic!(
"go adapter `{adapter_name}`: streaming adapter requires `request_type` in `[[adapters]]` config (the Rust request payload type)"
)
});
let request_type_simple = request_type.rsplit("::").next().unwrap_or(request_type);
let (param_parts, request_construction) = if adapter.request_type.is_some() && adapter.params.len() == 1 {
let param = &adapter.params[0];
let param_ty_name = ¶m.ty;
let ir_type = types.iter().find(|t| &t.name == param_ty_name);
if let Some(ty_def) = ir_type {
if let Some(first_field) = ty_def.fields.first() {
let field_name = &first_field.name;
let field_name_go = to_go_name(field_name);
let go_field_type = match &first_field.ty {
TypeRef::String => "string".to_string(),
TypeRef::Vec(inner) if matches!(**inner, TypeRef::String) => "[]string".to_string(),
TypeRef::Vec(_) => "[]interface{}".to_string(),
other => {
crate::backends::go::type_map::go_type(other).into_owned()
}
};
let wrapper_params = vec![
format!("engine *{owner_type}"),
format!("{field_name_go} {go_field_type}"),
];
let struct_field_name = to_go_name(field_name);
let construction = format!("req := &{request_type_simple}{{{struct_field_name}: {field_name_go}}}\n\t");
(wrapper_params, Some(construction))
} else {
let mut params = vec![format!("engine *{owner_type}")];
for p in &adapter.params {
let go_param_type = match p.ty.as_str() {
"String" => "string".to_string(),
ty => {
ty.rsplit("::").next().unwrap_or(ty).to_string()
}
};
let param_name = go_param_name(&p.name);
params.push(format!("{param_name} {go_param_type}"));
}
(params, None)
}
} else {
let mut params = vec![format!("engine *{owner_type}")];
for p in &adapter.params {
let go_param_type = match p.ty.as_str() {
"String" => "string".to_string(),
ty => {
ty.rsplit("::").next().unwrap_or(ty).to_string()
}
};
let param_name = go_param_name(&p.name);
params.push(format!("{param_name} {go_param_type}"));
}
(params, None)
}
} else {
let mut params = vec![format!("engine *{owner_type}")];
for p in &adapter.params {
let go_param_type = match p.ty.as_str() {
"String" => "string".to_string(),
ty => {
ty.rsplit("::").next().unwrap_or(ty).to_string()
}
};
let param_name = go_param_name(&p.name);
params.push(format!("{param_name} {go_param_type}"));
}
(params, None)
};
let return_type = format!("<-chan {item_type_simple}, error");
let method_call_name = to_go_name(adapter_name);
let method_call = if request_construction.is_some() {
format!("engine.{}(*req)", method_call_name)
} else {
let param_args = adapter
.params
.iter()
.map(|p| go_param_name(&p.name))
.collect::<Vec<_>>()
.join(", ");
if param_args.is_empty() {
format!("engine.{}()", method_call_name)
} else {
format!("engine.{}({})", method_call_name, param_args)
}
};
let mut out = String::new();
let _ = writeln!(
out,
"// {go_func_name} wraps the {owner_type}.{method_call_name} streaming adapter,"
);
let _ = writeln!(
out,
"// exposing it as a module-level function for test and consumer convenience."
);
let _ = writeln!(
out,
"func {go_func_name}({}) ({}) {{",
param_parts.join(", "),
return_type
);
if let Some(construction) = request_construction {
let _ = write!(out, "\t{}", construction);
}
let _ = writeln!(out, "return {}", method_call);
let _ = writeln!(out, "}}");
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::ir::{ParamDef, PrimitiveType, TypeRef};
fn make_param(name: &str, ty: TypeRef) -> ParamDef {
ParamDef {
name: name.to_string(),
ty,
optional: false,
default: None,
sanitized: false,
typed_default: None,
is_ref: false,
is_mut: false,
newtype_wrapper: None,
original_type: None,
map_is_ahash: false,
map_key_is_cow: false,
}
}
#[test]
fn test_params_require_marshal_for_named_non_opaque() {
let params = vec![make_param("options", TypeRef::Named("Config".to_string()))];
let opaque: std::collections::HashSet<&str> = std::collections::HashSet::new();
assert!(params_require_marshal(¶ms, &opaque));
}
#[test]
fn test_params_require_marshal_false_for_opaque() {
let params = vec![make_param("client", TypeRef::Named("Client".to_string()))];
let opaque: std::collections::HashSet<&str> = ["Client"].into();
assert!(!params_require_marshal(¶ms, &opaque));
}
#[test]
fn test_is_bridge_param_matches_by_name() {
let param = make_param("visitor", TypeRef::Named("VisitorHandle".to_string()));
let bridge_names: HashSet<String> = ["visitor".to_string()].into();
let aliases: HashSet<String> = HashSet::new();
assert!(is_bridge_param(¶m, &bridge_names, &aliases));
}
#[test]
fn test_params_require_marshal_for_vec() {
let params = vec![make_param(
"items",
TypeRef::Vec(Box::new(TypeRef::Primitive(PrimitiveType::U32))),
)];
let opaque: std::collections::HashSet<&str> = std::collections::HashSet::new();
assert!(params_require_marshal(¶ms, &opaque));
}
fn make_bytes_result_func(name: &str, with_bytes_param: bool) -> FunctionDef {
let params = if with_bytes_param {
vec![ParamDef {
name: "data".to_string(),
ty: TypeRef::Bytes,
optional: false,
default: None,
sanitized: false,
typed_default: None,
is_ref: false,
is_mut: false,
newtype_wrapper: None,
original_type: None,
map_is_ahash: false,
map_key_is_cow: false,
}]
} else {
vec![]
};
FunctionDef {
name: name.to_string(),
rust_path: String::new(),
original_rust_path: String::new(),
params,
return_type: TypeRef::Bytes,
is_async: false,
error_type: Some("SampleCrateError".to_string()),
doc: String::new(),
cfg: None,
sanitized: false,
return_sanitized: false,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
binding_excluded: false,
binding_exclusion_reason: None,
}
}
fn make_bytes_result_method(name: &str) -> MethodDef {
MethodDef {
name: name.to_string(),
doc: String::new(),
params: vec![ParamDef {
name: "data".to_string(),
ty: TypeRef::Bytes,
optional: false,
default: None,
sanitized: false,
typed_default: None,
is_ref: false,
is_mut: false,
newtype_wrapper: None,
original_type: None,
map_is_ahash: false,
map_key_is_cow: false,
}],
return_type: TypeRef::Bytes,
is_static: false,
is_async: false,
error_type: Some("SampleCrateError".to_string()),
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,
}
}
#[test]
fn test_is_bytes_result_func_detects_bytes_with_error() {
let func = make_bytes_result_func("process_image", true);
assert!(is_bytes_result_func(&func));
}
#[test]
fn test_is_bytes_result_func_false_for_bytes_without_error() {
let mut func = make_bytes_result_func("get_data", false);
func.error_type = None;
assert!(!is_bytes_result_func(&func));
}
#[test]
fn test_is_bytes_result_func_false_for_string_with_error() {
let func = FunctionDef {
name: "get_text".to_string(),
rust_path: String::new(),
original_rust_path: String::new(),
params: vec![],
return_type: TypeRef::String,
is_async: false,
error_type: Some("SampleCrateError".to_string()),
doc: String::new(),
cfg: None,
sanitized: false,
return_sanitized: false,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
binding_excluded: false,
binding_exclusion_reason: None,
};
assert!(!is_bytes_result_func(&func));
}
#[test]
fn test_is_bytes_result_method_detects_correctly() {
let method = make_bytes_result_method("render_page");
assert!(is_bytes_result_method(&method));
}
#[test]
fn test_gen_function_wrapper_bytes_result_emits_out_params() {
let func = make_bytes_result_func("process_image", true);
let opaque: std::collections::HashSet<&str> = std::collections::HashSet::new();
let bridge_names: HashSet<String> = HashSet::new();
let bridge_aliases: HashSet<String> = HashSet::new();
let value_only_types: HashSet<String> = HashSet::new();
let enum_names: HashSet<String> = HashSet::new();
let ffi_param_enum_names: HashSet<String> = HashSet::new();
let out = gen_function_wrapper(
&func,
"krz",
&opaque,
&bridge_names,
&bridge_aliases,
&value_only_types,
&enum_names,
&ffi_param_enum_names,
);
assert!(out.contains("([]byte, error)"), "missing bytes return type in:\n{out}");
assert!(out.contains("var outPtr"), "missing outPtr in:\n{out}");
assert!(out.contains("outLen"), "missing outLen in:\n{out}");
assert!(out.contains("outCap"), "missing outCap in:\n{out}");
assert!(out.contains("&outPtr"), "missing &outPtr in:\n{out}");
assert!(out.contains("&outLen"), "missing &outLen in:\n{out}");
assert!(out.contains("&outCap"), "missing &outCap in:\n{out}");
assert!(out.contains("C.GoBytes"), "missing C.GoBytes in:\n{out}");
assert!(out.contains("krz_free_bytes"), "missing krz_free_bytes in:\n{out}");
}
}