use super::super::FfiBackend;
use super::common::*;
use crate::backends::ffi::gen_bindings::types::gen_field_accessor;
use crate::core::backend::Backend;
use crate::core::ir::*;
#[test]
fn test_legacy_visitor_callbacks_use_configured_function_signature() {
let config = resolved_one(
r#"
[workspace]
languages = ["ffi"]
[[crates]]
name = "my-lib"
sources = ["src/lib.rs"]
[crates.ffi]
prefix = "doc"
visitor_callbacks = true
[[crates.trait_bridges]]
trait_name = "HtmlVisitor"
type_alias = "RenderHandle"
param_name = "renderer"
context_type = "NodeContext"
result_type = "VisitResult"
"#,
);
let mut api = visitor_api();
api.types.push(TypeDef {
name: "RenderSettings".to_string(),
rust_path: "my_lib::RenderSettings".to_string(),
fields: vec![],
is_clone: true,
..TypeDef::default()
});
api.types.push(TypeDef {
name: "RenderedDocument".to_string(),
rust_path: "my_lib::RenderedDocument".to_string(),
fields: vec![],
is_clone: true,
is_return_type: true,
..TypeDef::default()
});
api.functions.push(FunctionDef {
name: "render_document".to_string(),
rust_path: "my_lib::render_document".to_string(),
original_rust_path: String::new(),
params: vec![
ParamDef {
name: "source".to_string(),
ty: TypeRef::String,
is_ref: false,
..ParamDef::default()
},
ParamDef {
name: "settings".to_string(),
ty: TypeRef::Optional(Box::new(TypeRef::Named("RenderSettings".to_string()))),
optional: true,
..ParamDef::default()
},
ParamDef {
name: "renderer".to_string(),
ty: TypeRef::Named("RenderHandle".to_string()),
optional: true,
..ParamDef::default()
},
],
return_type: TypeRef::Named("RenderedDocument".to_string()),
is_async: false,
error_type: Some("RenderError".to_string()),
doc: String::new(),
cfg: None,
sanitized: true,
return_sanitized: false,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
});
let backend = FfiBackend;
let files = backend.generate_bindings(&api, &config).unwrap();
let lib = files.iter().find(|f| f.path.ends_with("lib.rs")).unwrap();
assert!(lib.content.contains("fn doc_render_document("));
assert!(lib.content.contains("fn doc_render_document_with_visitor("));
assert!(lib.content.contains("settings: *const my_lib::RenderSettings"));
assert!(lib.content.contains(") -> *mut my_lib::RenderedDocument"));
assert!(
lib.content
.contains("match my_lib::render_document(source_rs, settings_rs, None)")
);
assert!(
lib.content
.contains("match my_lib::render_document(source_rs, settings_rs, visitor_handle)")
);
assert!(!lib.content.contains("my_lib::convert("));
assert!(
!lib.content.contains("ConversionOptions") && !lib.content.contains("ConversionResult"),
"legacy visitor callback path must not assume conversion-shaped names"
);
}
#[test]
fn test_core_import_parameterization_uses_configured_import_not_hardcoded_sample_crate() {
let config = resolved_one(
r#"
[workspace]
languages = ["ffi"]
[[crates]]
name = "my-custom-lib"
sources = ["src/lib.rs"]
core_import = "my_custom_lib"
"#,
);
let api = sample_api();
let backend = FfiBackend;
let files = backend.generate_bindings(&api, &config).unwrap();
let lib = files.iter().find(|f| f.path.ends_with("lib.rs")).unwrap();
assert!(
!lib.content.contains("sample_crate::"),
"generated code must not hard-code 'sample_crate::' when core_import is 'my_custom_lib'; got:\n{}",
&lib.content[..lib.content.len().min(2000)]
);
assert!(
lib.content.contains("my_custom_lib::"),
"generated code must use the configured core_import 'my_custom_lib::' as a type qualifier"
);
}
#[test]
fn test_bytes_result_return_uses_out_params_and_emits_free_bytes() {
let api = ApiSurface {
crate_name: "my-lib".to_string(),
version: "1.0.0".to_string(),
types: vec![],
functions: vec![FunctionDef {
name: "render_page".to_string(),
rust_path: "my_lib::render_page".to_string(),
original_rust_path: String::new(),
params: vec![ParamDef {
name: "page_index".to_string(),
ty: TypeRef::Primitive(PrimitiveType::U32),
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,
vec_inner_is_ref: false,
map_is_btree: false,
core_wrapper: crate::core::ir::CoreWrapper::None,
}],
return_type: TypeRef::Bytes,
is_async: false,
error_type: Some("MyError".to_string()),
doc: "Render a page to PNG bytes.".to_string(),
cfg: None,
sanitized: false,
return_sanitized: false,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
}],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
services: vec![],
handler_contracts: vec![],
unsupported_public_items: Vec::new(),
};
let config = sample_config();
let backend = FfiBackend;
let files = backend.generate_bindings(&api, &config).unwrap();
let lib = files.iter().find(|f| f.path.ends_with("lib.rs")).unwrap();
assert!(
lib.content.contains("out_ptr: *mut *mut u8"),
"Result<Vec<u8>> function must have out_ptr out-param"
);
assert!(
lib.content.contains("out_len: *mut usize"),
"Result<Vec<u8>> function must have out_len out-param"
);
assert!(
lib.content.contains("out_cap: *mut usize"),
"Result<Vec<u8>> function must have out_cap out-param"
);
assert!(
lib.content.contains("fn my_lib_render_page("),
"function must be emitted with the correct FFI name"
);
assert!(
lib.content.contains("into_raw_parts()"),
"Result<Vec<u8>> success arm must use Vec::into_raw_parts()"
);
assert!(
lib.content.contains("fn my_lib_free_bytes("),
"module must include my_lib_free_bytes companion function"
);
assert!(
lib.content.contains("Vec::from_raw_parts(ptr, len, cap)"),
"free_bytes must reconstruct and drop the Vec via Vec::from_raw_parts"
);
}
#[test]
fn test_streaming_adapter_emits_iterator_handle_functions() {
let config = resolved_one(
r#"
[workspace]
languages = ["ffi"]
[[crates]]
name = "my-lib"
sources = ["src/lib.rs"]
[crates.ffi]
prefix = "ml"
[[crates.adapters]]
name = "chat_stream"
pattern = "streaming"
core_path = "chat_stream"
owner_type = "DefaultClient"
item_type = "ChatChunk"
error_type = "MyError"
request_type = "my_lib::ChatRequest"
[[crates.adapters.params]]
name = "req"
type = "ChatRequest"
"#,
);
let api = ApiSurface {
crate_name: "my-lib".to_string(),
version: "1.0.0".to_string(),
types: vec![TypeDef {
name: "DefaultClient".to_string(),
rust_path: "my_lib::DefaultClient".to_string(),
original_rust_path: String::new(),
fields: vec![],
methods: vec![MethodDef {
name: "chat_stream".to_string(),
params: vec![],
return_type: TypeRef::Unit,
is_async: true,
is_static: false,
error_type: Some("MyError".to_string()),
doc: String::new(),
sanitized: false,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
receiver: Some(ReceiverKind::Ref),
trait_source: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
}],
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,
is_variant_wrapper: false,
has_lifetime_params: false,
version: Default::default(),
}],
functions: vec![],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
services: vec![],
handler_contracts: vec![],
unsupported_public_items: Vec::new(),
};
let backend = FfiBackend;
let files = backend.generate_bindings(&api, &config).unwrap();
let lib = files.iter().find(|f| f.path.ends_with("lib.rs")).unwrap();
assert!(
lib.content.contains("MlDefaultClientChatStreamStreamHandle"),
"handle struct must be emitted: got\n{}",
&lib.content[..lib.content.len().min(3000)]
);
assert!(
lib.content.contains("fn ml_default_client_chat_stream_start("),
"_start function must be emitted"
);
assert!(
lib.content.contains("fn ml_default_client_chat_stream_next("),
"_next function must be emitted"
);
assert!(
lib.content.contains("fn ml_default_client_chat_stream_free("),
"_free function must be emitted"
);
assert!(
lib.content.contains("#[unsafe(no_mangle)]"),
"functions must be marked #[unsafe(no_mangle)]"
);
assert!(
lib.content
.contains("pub unsafe extern \"C\" fn ml_default_client_chat_stream_start"),
"_start must be pub unsafe extern C"
);
assert!(
lib.content
.contains("pub unsafe extern \"C\" fn ml_default_client_chat_stream_next"),
"_next must be pub unsafe extern C"
);
assert!(
lib.content
.contains("pub unsafe extern \"C\" fn ml_default_client_chat_stream_free"),
"_free must be pub unsafe extern C"
);
assert!(
lib.content.contains("-> *mut my_lib::ChatChunk"),
"_next must return *mut my_lib::ChatChunk"
);
assert!(
lib.content.contains("if !handle.is_null()"),
"_free must check for null before dropping"
);
assert!(
lib.content.contains("// SAFETY:"),
"generated code must include SAFETY comments on unsafe blocks"
);
assert!(
lib.content.contains("set_last_error"),
"_next must call set_last_error on error"
);
}
#[test]
fn test_client_constructors_emits_type_new_function() {
let config = resolved_one(
r#"
[workspace]
languages = ["ffi"]
[[crates]]
name = "my-lib"
sources = ["src/lib.rs"]
[crates.ffi]
prefix = "ml"
[workspace.client_constructors.DefaultClient]
body = "my_lib::DefaultClient::new(api_key)"
error_type = "String"
[[workspace.client_constructors.DefaultClient.params]]
name = "api_key"
type = "*const std::ffi::c_char"
"#,
);
let api = ApiSurface {
crate_name: "my-lib".to_string(),
version: "1.0.0".to_string(),
types: vec![TypeDef {
name: "DefaultClient".to_string(),
rust_path: "my_lib::DefaultClient".to_string(),
original_rust_path: String::new(),
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,
is_variant_wrapper: false,
has_lifetime_params: false,
version: Default::default(),
}],
functions: vec![],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
services: vec![],
handler_contracts: vec![],
unsupported_public_items: Vec::new(),
};
let backend = FfiBackend;
let files = backend.generate_bindings(&api, &config).unwrap();
let lib = files.iter().find(|f| f.path.ends_with("lib.rs")).unwrap();
assert!(
lib.content.contains("fn ml_default_client_new("),
"should emit _new function: got\n{}",
&lib.content[..lib.content.len().min(2000)]
);
assert!(
lib.content.contains("api_key: *const std::ffi::c_char"),
"should include typed param in signature"
);
assert!(
lib.content.contains("-> *mut my_lib::DefaultClient"),
"should return *mut TypeName"
);
assert!(
lib.content.contains("clear_last_error"),
"should call clear_last_error at function entry"
);
assert!(
lib.content.contains("set_last_error"),
"should call set_last_error on Err path"
);
assert!(
lib.content.contains("Box::into_raw(Box::new(val))"),
"should box the value on Ok path"
);
}
fn ahashmap_cow_api() -> ApiSurface {
ApiSurface {
crate_name: "my-lib".to_string(),
version: "1.0.0".to_string(),
types: vec![],
functions: vec![FunctionDef {
name: "calculate_quality_score".to_string(),
rust_path: "my_lib::calculate_quality_score".to_string(),
original_rust_path: String::new(),
params: vec![
ParamDef {
name: "text".to_string(),
ty: TypeRef::String,
optional: false,
default: None,
sanitized: false,
typed_default: None,
is_ref: true,
is_mut: false,
newtype_wrapper: None,
original_type: None,
map_is_ahash: false,
map_key_is_cow: false,
vec_inner_is_ref: false,
map_is_btree: false,
core_wrapper: crate::core::ir::CoreWrapper::None,
},
ParamDef {
name: "metadata".to_string(),
ty: TypeRef::Map(Box::new(TypeRef::String), Box::new(TypeRef::Json)),
optional: true,
default: None,
sanitized: false,
typed_default: None,
is_ref: true,
is_mut: false,
newtype_wrapper: None,
original_type: None,
map_is_ahash: true,
map_key_is_cow: true,
vec_inner_is_ref: false,
map_is_btree: false,
core_wrapper: crate::core::ir::CoreWrapper::None,
},
],
return_type: TypeRef::Primitive(PrimitiveType::F64),
is_async: false,
error_type: None,
doc: "Calculate quality score for text.".to_string(),
cfg: None,
sanitized: false,
return_sanitized: false,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
}],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
services: vec![],
handler_contracts: vec![],
unsupported_public_items: Vec::new(),
}
}
#[test]
fn test_optional_ahashmap_cow_key_uses_as_ref_not_as_deref() {
let api = ahashmap_cow_api();
let config = sample_config();
let backend = FfiBackend;
let files = backend.generate_bindings(&api, &config).unwrap();
let lib = files.iter().find(|f| f.path.ends_with("lib.rs")).unwrap();
assert!(
lib.content.contains("ahash::AHashMap<std::borrow::Cow<'static, str>,"),
"should deserialize into AHashMap<Cow<'static, str>, ...>, got:\n{}",
if lib.content.len() > 3000 {
&lib.content[lib.content.len() - 3000..]
} else {
&lib.content
}
);
assert!(
lib.content.contains("metadata_rs.as_ref()"),
"should pass metadata_rs.as_ref() (not .as_deref()), got:\n{}",
if lib.content.len() > 3000 {
&lib.content[lib.content.len() - 3000..]
} else {
&lib.content
}
);
assert!(
!lib.content.contains("metadata_rs.as_deref()"),
"must NOT use .as_deref() on HashMap — HashMap does not impl Deref"
);
}
#[test]
fn test_optional_vec_still_uses_as_deref() {
let api = ApiSurface {
crate_name: "my-lib".to_string(),
version: "1.0.0".to_string(),
types: vec![],
functions: vec![FunctionDef {
name: "process_items".to_string(),
rust_path: "my_lib::process_items".to_string(),
original_rust_path: String::new(),
params: vec![ParamDef {
name: "items".to_string(),
ty: TypeRef::Vec(Box::new(TypeRef::String)),
optional: true,
default: None,
sanitized: false,
typed_default: None,
is_ref: true,
is_mut: false,
newtype_wrapper: None,
original_type: None,
map_is_ahash: false,
map_key_is_cow: false,
vec_inner_is_ref: false,
map_is_btree: false,
core_wrapper: crate::core::ir::CoreWrapper::None,
}],
return_type: TypeRef::Unit,
is_async: false,
error_type: None,
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,
version: Default::default(),
}],
enums: vec![],
errors: vec![],
excluded_type_paths: ::std::collections::HashMap::new(),
excluded_trait_names: ::std::collections::HashSet::new(),
services: vec![],
handler_contracts: vec![],
unsupported_public_items: Vec::new(),
};
let config = sample_config();
let backend = FfiBackend;
let files = backend.generate_bindings(&api, &config).unwrap();
let lib = files.iter().find(|f| f.path.ends_with("lib.rs")).unwrap();
assert!(
lib.content.contains("items_rs.as_deref()"),
"Optional Vec<String> with is_ref=true should still use .as_deref()"
);
}
#[test]
fn test_optional_bytes_field_accessor_emits_out_len_and_length_writes() {
let field = FieldDef {
name: "data".to_string(),
ty: TypeRef::Bytes,
optional: true,
default: None,
doc: String::new(),
sanitized: false,
is_boxed: false,
type_rust_path: None,
cfg: None,
typed_default: None,
core_wrapper: crate::core::ir::CoreWrapper::None,
vec_inner_core_wrapper: crate::core::ir::CoreWrapper::None,
newtype_wrapper: None,
serde_rename: None,
serde_flatten: false,
original_type: None,
binding_excluded: false,
binding_exclusion_reason: None,
};
let typ = TypeDef {
name: "EmailAttachment".to_string(),
rust_path: "my_lib::EmailAttachment".to_string(),
original_rust_path: String::new(),
fields: vec![field.clone()],
methods: vec![],
is_opaque: false,
is_clone: true,
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,
is_variant_wrapper: false,
has_lifetime_params: false,
version: Default::default(),
};
let code = gen_field_accessor(
&typ,
&field,
"kr",
"my_lib",
&ahash::AHashMap::<String, String>::new(),
&ahash::AHashSet::<String>::new(),
&ahash::AHashSet::<String>::new(),
&::std::collections::HashMap::<String, String>::new(),
);
assert!(
code.contains("out_len: *mut usize"),
"optional Bytes field accessor must declare out_len param (issue #118), got:\n{code}"
);
assert!(
code.contains("*out_len"),
"optional Bytes field must write length to out_len (Some path writes real len, None writes 0), got:\n{code}"
);
assert!(
code.contains("*out_len = 0"),
"optional Bytes None arm must write 0 to out_len, got:\n{code}"
);
assert!(
code.contains("!out_len.is_null()"),
"optional Bytes field must null-check out_len before writing, got:\n{code}"
);
}