mod functions;
mod methods;
pub(super) mod types;
use functions::{gen_adapter_wrapper, gen_convert_with_visitor_wrapper, gen_function_wrapper};
use methods::{gen_method_wrapper, gen_streaming_method_wrapper};
use types::{
gen_config_options, gen_enum_type, gen_last_error_helper, gen_opaque_type, gen_opaque_type_free_only,
gen_ptr_helper, gen_struct_type, gen_unmarshal_bytes_helper, is_passthrough_raw_message_enum, is_tuple_field,
};
use crate::core::backend::{Backend, BuildConfig, BuildDependency, Capabilities, GeneratedFile};
use crate::core::config::workspace::ClientConstructorConfig;
use crate::core::config::{AdapterPattern, Language, ResolvedCrateConfig, resolve_output_dir};
use crate::core::hash::{self, CommentStyle};
use crate::core::ir::{ApiSurface, TypeDef, TypeRef};
use heck::ToPascalCase;
use std::collections::HashSet;
use std::path::PathBuf;
pub struct GoBackend;
impl GoBackend {
fn package_name(module_path: &str) -> String {
module_path
.split('/')
.next_back()
.unwrap_or("binding")
.replace('-', "")
.to_lowercase()
}
}
impl Backend for GoBackend {
fn name(&self) -> &str {
"go"
}
fn language(&self) -> Language {
Language::Go
}
fn capabilities(&self) -> Capabilities {
Capabilities {
supports_async: true,
supports_classes: true,
supports_enums: true,
supports_option: true,
supports_result: true,
..Capabilities::default()
}
}
fn generate_bindings(&self, api: &ApiSurface, config: &ResolvedCrateConfig) -> anyhow::Result<Vec<GeneratedFile>> {
let module_path = config.go_module();
let pkg_name = config
.go
.as_ref()
.and_then(|g| g.package_name.clone())
.unwrap_or_else(|| Self::package_name(&module_path));
let ffi_prefix = config.ffi_prefix();
let output_dir = {
let mut d = resolve_output_dir(config.output_paths.get("go"), &config.name, "packages/go/");
if !d.ends_with('/') {
d.push('/');
}
d
};
let ffi_lib_name = config.ffi_lib_name();
let ffi_header = config.ffi_header_name();
let ffi_crate_dir = config
.output_paths
.get("ffi")
.and_then(|p| {
let path = p.as_path();
path.ancestors()
.find(|a| {
a.file_name()
.is_some_and(|n| n != "src" && n != "lib" && n != "include")
})
.map(|a| a.to_string_lossy().to_string())
})
.unwrap_or_else(|| format!("crates/{ffi_lib_name}"));
let bridge_param_names: HashSet<String> = config
.trait_bridges
.iter()
.filter_map(|b| b.param_name.clone())
.collect();
let bridge_type_aliases: HashSet<String> = config
.trait_bridges
.iter()
.filter_map(|b| b.type_alias.clone())
.collect();
let visitor_callbacks_enabled = config.ffi.as_ref().is_some_and(|f| f.visitor_callbacks);
let has_options_field_bridge = config
.trait_bridges
.iter()
.any(|b| b.bind_via == crate::core::config::BridgeBinding::OptionsField);
let has_visitor_bridge =
has_options_field_bridge || (!config.trait_bridges.is_empty() && visitor_callbacks_enabled);
let has_plugin_bridges = config.trait_bridges.iter().any(|b| b.register_fn.is_some());
let streaming_methods: std::collections::HashMap<(String, String), String> = config
.adapters
.iter()
.filter(|a| matches!(a.pattern, AdapterPattern::Streaming))
.filter_map(|a| {
let owner = a.owner_type.clone()?;
let item = a.item_type.clone()?;
Some(((owner, a.name.clone()), item))
})
.collect();
let ffi_exclude_functions: HashSet<String> = config
.ffi
.as_ref()
.map(|f| f.exclude_functions.iter().cloned().collect())
.unwrap_or_default();
let mut exclude_types: HashSet<String> = config
.ffi
.as_ref()
.map(|f| f.exclude_types.iter().cloned().collect())
.unwrap_or_default();
if let Some(go_config) = &config.go {
exclude_types.extend(go_config.exclude_types.iter().cloned());
}
let value_only_types: HashSet<String> = api
.types
.iter()
.filter(|t| !t.is_opaque && t.fields.iter().all(|f| {
matches!(f.ty, crate::core::ir::TypeRef::Primitive(_) | crate::core::ir::TypeRef::String | crate::core::ir::TypeRef::Char | crate::core::ir::TypeRef::Path)
|| matches!(&f.ty, crate::core::ir::TypeRef::Optional(inner) if matches!(inner.as_ref(), crate::core::ir::TypeRef::Primitive(_) | crate::core::ir::TypeRef::String | crate::core::ir::TypeRef::Char | crate::core::ir::TypeRef::Path))
}))
.map(|t| t.name.clone())
.collect();
let content = format_go_code(&strip_trailing_whitespace(&gen_go_file(
api,
config,
&ffi_prefix,
&pkg_name,
&ffi_lib_name,
&ffi_header,
&ffi_crate_dir,
&output_dir,
&bridge_param_names,
&bridge_type_aliases,
&streaming_methods,
&ffi_exclude_functions,
&exclude_types,
&value_only_types,
has_options_field_bridge,
)));
let _adapter_bodies = crate::adapters::build_adapter_bodies(config, Language::Go)?;
let depth = output_dir.trim_end_matches('/').matches('/').count() + 1;
let to_root = "../".repeat(depth);
let mut files = vec![GeneratedFile {
path: PathBuf::from(format!("{output_dir}binding.go")),
content,
generated_header: true,
}];
if has_visitor_bridge {
let visitor_bridge_cfg = config
.trait_bridges
.iter()
.find(|b| b.bind_via == crate::core::config::BridgeBinding::OptionsField);
let (vtable_trait_name, options_field) = visitor_bridge_cfg
.and_then(|b| {
let field = b.resolved_options_field()?;
Some((b.trait_name.clone(), field.to_string()))
})
.unwrap_or_else(|| ("HtmlVisitor".to_string(), "visitor".to_string()));
let trait_map: std::collections::HashMap<&str, &crate::core::ir::TypeDef> = api
.types
.iter()
.filter(|t| t.is_trait)
.map(|t| (t.name.as_str(), t))
.collect();
let visitor_trait = visitor_bridge_cfg.and_then(|b| trait_map.get(b.trait_name.as_str()).copied());
let visitor_content = if let Some(vt) = visitor_trait {
strip_trailing_whitespace(&crate::backends::go::gen_visitor::gen_visitor_file(
&pkg_name,
&ffi_prefix,
&ffi_header,
&ffi_crate_dir,
&to_root,
&vtable_trait_name,
&options_field,
vt,
))
} else {
eprintln!(
"[alef] gen_visitor_file(go): visitor trait `{vtable_trait_name}` not found in IR, skipping visitor.go"
);
String::new()
};
files.push(GeneratedFile {
path: PathBuf::from(format!("{output_dir}visitor.go")),
content: visitor_content,
generated_header: true,
});
}
if has_plugin_bridges {
let trait_bridges_content = strip_trailing_whitespace(&super::trait_bridge::gen_trait_bridges_file(
api,
config,
&pkg_name,
&ffi_prefix,
&ffi_header,
&ffi_crate_dir,
&to_root,
&config.name,
));
if !trait_bridges_content.trim().is_empty() && trait_bridges_content.len() > 100 {
files.push(GeneratedFile {
path: PathBuf::from(format!("{output_dir}trait_bridges.go")),
content: trait_bridges_content,
generated_header: true,
});
}
}
Ok(files)
}
fn generate_public_api(
&self,
_api: &ApiSurface,
_config: &ResolvedCrateConfig,
) -> anyhow::Result<Vec<GeneratedFile>> {
Ok(vec![])
}
fn build_config(&self) -> Option<BuildConfig> {
Some(BuildConfig {
tool: "go",
crate_suffix: "",
build_dep: BuildDependency::Ffi,
post_build: vec![],
})
}
}
fn strip_trailing_whitespace(content: &str) -> String {
let mut result: String = content
.lines()
.map(|line| line.trim_end())
.collect::<Vec<_>>()
.join("\n");
if !result.ends_with('\n') {
result.push('\n');
}
result
}
fn format_go_code(code: &str) -> String {
use std::io::Write;
use std::process::{Command, Stdio};
let child = Command::new("gofmt")
.arg("-s")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn();
match child {
Ok(mut c) => {
if let Some(ref mut stdin) = c.stdin.take() {
let _ = stdin.write_all(code.as_bytes());
}
match c.wait_with_output() {
Ok(output) if output.status.success() => {
String::from_utf8(output.stdout).unwrap_or_else(|_| code.to_string())
}
_ => code.to_string(),
}
}
Err(_) => code.to_string(),
}
}
fn is_ffi_enum_type(name: &str, ffi_enum_names: &HashSet<String>) -> bool {
ffi_enum_names.contains(name)
}
fn uses_ffi_enum_type(
func_params: &[crate::core::ir::ParamDef],
return_type: &TypeRef,
ffi_enum_names: &HashSet<String>,
opaque_names: &std::collections::HashSet<&str>,
) -> bool {
let named_is_problem = |n: &str| is_ffi_enum_type(n, ffi_enum_names) && !opaque_names.contains(n);
let return_uses = match return_type {
TypeRef::Named(n) => named_is_problem(n),
TypeRef::Optional(inner) => matches!(inner.as_ref(), TypeRef::Named(n) if named_is_problem(n)),
_ => false,
};
if return_uses {
return true;
}
func_params.iter().any(|p| match &p.ty {
TypeRef::Named(n) => named_is_problem(n),
TypeRef::Optional(inner) => matches!(inner.as_ref(), TypeRef::Named(n) if named_is_problem(n)),
_ => false,
})
}
fn references_excluded_type(ty: &TypeRef, exclude_types: &HashSet<String>) -> bool {
exclude_types.iter().any(|name| ty.references_named(name))
}
fn signature_references_excluded_type(
params: &[crate::core::ir::ParamDef],
return_type: &TypeRef,
exclude_types: &HashSet<String>,
) -> bool {
references_excluded_type(return_type, exclude_types)
|| params
.iter()
.any(|param| references_excluded_type(¶m.ty, exclude_types))
}
#[allow(clippy::too_many_arguments)]
fn gen_go_file(
api: &ApiSurface,
config: &ResolvedCrateConfig,
ffi_prefix: &str,
pkg_name: &str,
ffi_lib_name: &str,
ffi_header: &str,
ffi_crate_dir: &str,
go_output_dir: &str,
bridge_param_names: &HashSet<String>,
bridge_type_aliases: &HashSet<String>,
streaming_methods: &std::collections::HashMap<(String, String), String>,
ffi_exclude_functions: &HashSet<String>,
exclude_types: &HashSet<String>,
value_only_types: &HashSet<String>,
has_options_field_bridge: bool,
) -> String {
let mut out = String::with_capacity(4096);
out.push_str(&hash::header(CommentStyle::DoubleSlash));
out.push('\n');
let depth = go_output_dir.trim_end_matches('/').matches('/').count() + 1;
let to_root = "../".repeat(depth);
out.push_str(&crate::backends::go::template_env::render(
"package_doc_and_declaration.jinja",
minijinja::context! {
pkg_name => pkg_name,
crate_name => &config.name,
},
));
out.push_str(&crate::backends::go::template_env::render(
"cgo_preamble_binding.jinja",
minijinja::context! {
to_root => &to_root,
ffi_crate_dir => ffi_crate_dir,
ffi_lib_name => ffi_lib_name,
ffi_header => ffi_header,
},
));
out.push('\n');
let has_opaque_types = api.types.iter().any(|t| t.is_opaque);
let has_sync_functions = api.functions.iter().any(|f| !f.is_async);
let has_non_static_methods = api.types.iter().any(|t| t.methods.iter().any(|m| !m.is_static));
let needs_json_and_unsafe = has_sync_functions || has_non_static_methods;
let mut imports = vec!["fmt"];
if needs_json_and_unsafe {
imports.insert(0, "encoding/json");
imports.push("unsafe");
} else if has_opaque_types {
imports.push("unsafe");
}
if !api.errors.is_empty() {
imports.insert(1.min(imports.len()), "errors");
}
out.push_str(&crate::backends::go::template_env::render(
"imports_basic.jinja",
minijinja::context! {
imports => imports,
},
));
out.push_str(&gen_last_error_helper(ffi_prefix));
out.push_str("\n\n");
out.push_str(&gen_unmarshal_bytes_helper());
out.push_str("\n\n");
out.push_str(&gen_ptr_helper());
out.push_str("\n\n");
let has_plugin_bridges = config.trait_bridges.iter().any(|b| b.register_fn.is_some());
if has_plugin_bridges {
let bridges: Vec<_> = config
.trait_bridges
.iter()
.filter_map(|bridge_cfg| {
api.types
.iter()
.find(|t| t.name == bridge_cfg.trait_name)
.map(|trait_def| {
minijinja::Value::from_serialize(serde_json::json!({
"pascal_name": trait_def.name,
"methods": trait_def.methods.iter().map(|m| serde_json::json!({
"name": m.name.to_pascal_case(),
})).collect::<Vec<_>>(),
}))
})
})
.collect();
out.push_str(&crate::backends::go::template_env::render(
"plugin_bridge_exports.jinja",
minijinja::context! {
bridges => bridges,
},
));
out.push('\n');
}
if !api.errors.is_empty() {
out.push_str(&crate::codegen::error_gen::gen_go_sentinel_errors(&api.errors));
out.push_str("\n\n");
for error in &api.errors {
out.push_str(&crate::codegen::error_gen::gen_go_error_struct(error, pkg_name));
out.push_str("\n\n");
}
}
let bridge_associated_types = config.bridge_associated_types();
let visitor_types: std::collections::HashSet<&str> = if !bridge_param_names.is_empty() {
bridge_associated_types.iter().map(|s| s.as_str()).collect()
} else {
std::collections::HashSet::new()
};
let unit_enum_names: std::collections::HashSet<&str> = api
.enums
.iter()
.filter(|e| {
!exclude_types.contains(&e.name)
&& e.variants
.iter()
.all(|v| v.fields.is_empty() || v.fields.iter().all(is_tuple_field))
})
.filter(|e| !is_passthrough_raw_message_enum(e))
.map(|e| e.name.as_str())
.collect();
let passthrough_enum_names: std::collections::HashSet<&str> = api
.enums
.iter()
.filter(|e| is_passthrough_raw_message_enum(e))
.filter(|e| !exclude_types.contains(&e.name))
.map(|e| e.name.as_str())
.collect();
for enum_def in api
.enums
.iter()
.filter(|e| !visitor_types.contains(e.name.as_str()) && !exclude_types.contains(&e.name))
{
out.push_str(&gen_enum_type(enum_def));
out.push_str("\n\n");
}
let error_names: std::collections::HashSet<&str> = api.errors.iter().map(|e| e.name.as_str()).collect();
let opaque_names: std::collections::HashSet<&str> = api
.types
.iter()
.filter(|t| t.is_opaque)
.filter(|t| !exclude_types.contains(&t.name))
.map(|t| t.name.as_str())
.collect();
let ffi_enum_names: HashSet<String> = api.enums.iter().map(|e| e.name.clone()).collect();
let data_enum_names: std::collections::HashSet<&str> = api
.enums
.iter()
.filter(|e| {
!exclude_types.contains(&e.name)
&& e.variants
.iter()
.any(|v| !v.fields.is_empty() && v.fields.iter().any(|f| !is_tuple_field(f)))
})
.map(|e| e.name.as_str())
.collect();
let struct_names: std::collections::HashSet<&str> = api
.types
.iter()
.filter(|t| !t.is_opaque && !exclude_types.contains(&t.name))
.map(|t| t.name.as_str())
.collect();
for typ in api
.types
.iter()
.filter(|typ| !typ.is_trait && !visitor_types.contains(typ.name.as_str()) && !exclude_types.contains(&typ.name))
{
if typ.is_opaque {
if error_names.contains(typ.name.as_str()) {
out.push_str(&gen_opaque_type_free_only(typ, ffi_prefix));
out.push_str("\n\n");
} else {
out.push_str(&gen_opaque_type(typ, ffi_prefix));
out.push_str("\n\n");
}
if let Some(ctor) = config.client_constructors.get(&typ.name) {
out.push_str(&gen_go_opaque_constructor(typ, ffi_prefix, ctor));
out.push_str("\n\n");
}
} else {
out.push_str(&gen_struct_type(
typ,
&unit_enum_names,
&passthrough_enum_names,
&data_enum_names,
&struct_names,
));
out.push_str("\n\n");
let empty_functional_options = vec![];
let functional_options = config
.go
.as_ref()
.map(|g| &g.functional_options)
.unwrap_or(&empty_functional_options);
if !typ.name.ends_with("Update") && functional_options.contains(&typ.name) {
out.push_str(&gen_config_options(
typ,
&unit_enum_names,
&passthrough_enum_names,
&data_enum_names,
));
out.push_str("\n\n");
}
}
}
for func in api.functions.iter().filter(|f| {
!ffi_exclude_functions.contains(&f.name)
&& !signature_references_excluded_type(&f.params, &f.return_type, exclude_types)
&& !uses_ffi_enum_type(&f.params, &f.return_type, &ffi_enum_names, &opaque_names)
&& !crate::codegen::generators::trait_bridge::is_trait_bridge_managed_fn(&f.name, &config.trait_bridges)
}) {
if func.name == "convert" && has_options_field_bridge {
out.push_str(&gen_convert_with_visitor_wrapper(
func,
ffi_prefix,
&opaque_names,
value_only_types,
));
out.push_str("\n\n");
} else {
out.push_str(&gen_function_wrapper(
func,
ffi_prefix,
&opaque_names,
bridge_param_names,
bridge_type_aliases,
value_only_types,
));
out.push_str("\n\n");
}
}
for adapter in &config.adapters {
if !matches!(adapter.pattern, AdapterPattern::Streaming) {
continue;
}
if adapter.owner_type.is_none() || adapter.item_type.is_none() {
continue;
}
out.push_str(&gen_adapter_wrapper(adapter, pkg_name, &api.types));
out.push_str("\n\n");
}
for typ in api
.types
.iter()
.filter(|typ| !typ.is_trait && !exclude_types.contains(&typ.name))
{
if typ.is_opaque && error_names.contains(typ.name.as_str()) {
continue;
}
for method in &typ.methods {
if method.name == "default" {
continue;
}
if typ.is_opaque && method.is_static && matches!(method.return_type, TypeRef::Named(_)) {
continue;
}
if let Some(item_type) = streaming_methods.get(&(typ.name.clone(), method.name.clone())) {
out.push_str(&gen_streaming_method_wrapper(
typ,
method,
ffi_prefix,
item_type,
&data_enum_names,
&opaque_names,
value_only_types,
));
out.push_str("\n\n");
continue;
}
if ffi_exclude_functions.contains(&method.name) {
continue;
}
if signature_references_excluded_type(&method.params, &method.return_type, exclude_types) {
continue;
}
if uses_ffi_enum_type(&method.params, &method.return_type, &ffi_enum_names, &opaque_names) {
continue;
}
out.push_str(&gen_method_wrapper(
typ,
method,
ffi_prefix,
&opaque_names,
value_only_types,
));
out.push_str("\n\n");
}
}
out
}
fn ffi_ty_to_go(rust_ty: &str) -> &'static str {
let normalized = rust_ty.trim();
if normalized.contains("c_char") || normalized.contains("CStr") {
return "string";
}
if matches!(normalized, "u8" | "uint8_t") {
return "uint8";
}
if matches!(normalized, "u16" | "uint16_t") {
return "uint16";
}
if matches!(normalized, "u32" | "uint32_t") {
return "uint32";
}
if matches!(normalized, "u64" | "uint64_t" | "usize") {
return "uint64";
}
if matches!(normalized, "i8" | "int8_t") {
return "int8";
}
if matches!(normalized, "i16" | "int16_t") {
return "int16";
}
if matches!(normalized, "i32" | "int32_t" | "c_int") {
return "int32";
}
if matches!(normalized, "i64" | "int64_t" | "isize") {
return "int64";
}
if matches!(normalized, "bool") {
return "bool";
}
if matches!(normalized, "f32" | "float") {
return "float32";
}
if matches!(normalized, "f64" | "double") {
return "float64";
}
"unsafe.Pointer"
}
fn go_ctor_param_setup(go_name: &str, rust_ty: &str, ffi_prefix: &str) -> (String, String) {
let normalized = rust_ty.trim();
let c_name = format!("c{}{}", &go_name[..1].to_uppercase(), &go_name[1..]);
if normalized.contains("c_char") || normalized.contains("CStr") {
let setup = format!("\t{c_name} := C.CString({go_name})\n\tdefer C.free(unsafe.Pointer({c_name}))\n");
(c_name, setup)
} else if matches!(normalized, "bool") {
let setup = format!("\t{c_name} := C.bool({go_name})\n");
(c_name, setup)
} else if matches!(normalized, "f32" | "float") {
let setup = format!("\t{c_name} := C.float({go_name})\n");
(c_name, setup)
} else if matches!(normalized, "f64" | "double") {
let setup = format!("\t{c_name} := C.double({go_name})\n");
(c_name, setup)
} else if matches!(normalized, "u8" | "uint8_t") {
let setup = format!("\t{c_name} := C.uint8_t({go_name})\n");
(c_name, setup)
} else if matches!(normalized, "u16" | "uint16_t") {
let setup = format!("\t{c_name} := C.uint16_t({go_name})\n");
(c_name, setup)
} else if matches!(normalized, "u32" | "uint32_t") {
let setup = format!("\t{c_name} := C.uint32_t({go_name})\n");
(c_name, setup)
} else if matches!(normalized, "u64" | "uint64_t" | "usize") {
let setup = format!("\t{c_name} := C.uint64_t({go_name})\n");
(c_name, setup)
} else if matches!(normalized, "i8" | "int8_t") {
let setup = format!("\t{c_name} := C.int8_t({go_name})\n");
(c_name, setup)
} else if matches!(normalized, "i16" | "int16_t") {
let setup = format!("\t{c_name} := C.int16_t({go_name})\n");
(c_name, setup)
} else if matches!(normalized, "i32" | "int32_t" | "c_int") {
let setup = format!("\t{c_name} := C.int32_t({go_name})\n");
(c_name, setup)
} else if matches!(normalized, "i64" | "int64_t" | "isize") {
let setup = format!("\t{c_name} := C.int64_t({go_name})\n");
(c_name, setup)
} else {
let _ = ffi_prefix;
let setup = format!("\t{c_name} := {go_name}\n");
(c_name, setup)
}
}
fn gen_go_opaque_constructor(typ: &TypeDef, ffi_prefix: &str, ctor: &ClientConstructorConfig) -> String {
use crate::codegen::naming::go_type_name;
use heck::ToSnakeCase;
let go_name = go_type_name(&typ.name);
let type_snake = typ.name.to_snake_case();
let upper_prefix = ffi_prefix.to_uppercase();
let c_type = format!("{upper_prefix}{}", typ.name);
let go_params: String = ctor
.params
.iter()
.map(|p| format!("{} {}", p.name, ffi_ty_to_go(&p.ty)))
.collect::<Vec<_>>()
.join(", ");
let mut setup = String::new();
let c_args: Vec<String> = ctor
.params
.iter()
.map(|p| {
let (c_var, lines) = go_ctor_param_setup(&p.name, &p.ty, ffi_prefix);
setup.push_str(&lines);
c_var
})
.collect();
let c_call_args = c_args.join(", ");
format!(
"// New{go_name} creates a new {go_name} handle via the FFI constructor.\n\
func New{go_name}({go_params}) (*{go_name}, error) {{\n\
{setup}\
\tptr := C.{ffi_prefix}_{type_snake}_new({c_call_args})\n\
\tif ptr == nil {{\n\
\t\treturn nil, fmt.Errorf(\"new{go_name}: %s\", C.GoString(C.{ffi_prefix}_last_error_context()))\n\
\t}}\n\
\treturn &{go_name}{{ptr: unsafe.Pointer((*C.{c_type})(ptr))}}, nil\n\
}}"
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::config::NewAlefConfig;
fn resolved_one(toml: &str) -> ResolvedCrateConfig {
let cfg: NewAlefConfig = toml::from_str(toml).unwrap();
cfg.resolve().unwrap().remove(0)
}
fn make_config() -> ResolvedCrateConfig {
resolved_one(
r#"
[workspace]
languages = ["ffi", "go"]
[[crates]]
name = "test-lib"
sources = ["src/lib.rs"]
[crates.ffi]
prefix = "test"
[crates.go]
module = "github.com/test/test-lib"
"#,
)
}
#[test]
fn test_package_name_extracts_last_segment() {
assert_eq!(GoBackend::package_name("github.com/org/my-lib"), "mylib");
assert_eq!(GoBackend::package_name("binding"), "binding");
}
#[test]
fn test_strip_trailing_whitespace_normalizes_lines() {
let input = "line one \nline two\n";
let result = strip_trailing_whitespace(input);
assert_eq!(result, "line one\nline two\n");
}
#[test]
fn test_is_ffi_enum_type_returns_true_for_known_enum() {
let mut enum_names = HashSet::new();
enum_names.insert("Status".to_string());
assert!(is_ffi_enum_type("Status", &enum_names));
assert!(!is_ffi_enum_type("Config", &enum_names));
}
#[test]
fn test_generate_bindings_produces_binding_go_file() {
use crate::core::ir::ApiSurface;
let config = make_config();
let api = ApiSurface {
crate_name: "test-lib".to_string(),
version: "0.1.0".to_string(),
types: vec![],
functions: vec![],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
};
let backend = GoBackend;
let files = backend.generate_bindings(&api, &config).unwrap();
assert!(!files.is_empty());
assert!(files[0].path.to_string_lossy().contains("binding.go"));
}
#[test]
fn test_gen_go_opaque_constructor_emits_new_function() {
use crate::core::config::workspace::{ClientConstructorConfig, ConstructorParam};
use crate::core::ir::TypeDef;
let typ = TypeDef {
name: "TestClient".to_string(),
rust_path: "test_lib::TestClient".to_string(),
original_rust_path: "test_lib::TestClient".to_string(),
fields: vec![],
methods: vec![],
is_opaque: true,
is_clone: false,
is_copy: false,
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![],
doc: String::new(),
cfg: None,
binding_excluded: false,
binding_exclusion_reason: None,
};
let ctor = ClientConstructorConfig {
params: vec![ConstructorParam {
name: "api_key".to_string(),
ty: "*const std::ffi::c_char".to_string(),
}],
body: "TestClient::new(api_key)".to_string(),
error_type: None,
};
let output = gen_go_opaque_constructor(&typ, "test", &ctor);
assert!(
output.contains("func NewTestClient("),
"should contain func NewTestClient"
);
assert!(output.contains("api_key string"), "should contain api_key string param");
assert!(
output.contains("C.CString(api_key)"),
"should use C.CString for c_char param"
);
assert!(
output.contains("C.free(unsafe.Pointer("),
"should defer-free the C string"
);
assert!(
output.contains("C.test_test_client_new("),
"should call FFI constructor"
);
assert!(output.contains("return nil, fmt.Errorf"), "should return error on nil");
assert!(
output.contains("return &TestClient{ptr:"),
"should return handle on success"
);
}
}