use super::*;
#[test]
fn test_has_serde_via_derive_still_detected() {
let source = r#"
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Config {
pub name: String,
pub timeout: u64,
}
"#;
let surface = extract_from_source(source);
assert_eq!(surface.types.len(), 1);
assert!(surface.types[0].has_serde, "derive-based serde must still be detected");
}
#[test]
fn test_has_serde_via_manual_impls_detected() {
let source = r#"
#[derive(Clone, Debug)]
pub struct NodeContext {
pub tag_name: String,
pub depth: usize,
}
impl serde::Serialize for NodeContext {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
unimplemented!()
}
}
impl<'de> serde::Deserialize<'de> for NodeContext {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
unimplemented!()
}
}
"#;
let surface = extract_from_source(source);
assert_eq!(surface.types.len(), 1);
assert!(
surface.types[0].has_serde,
"manual impl Serialize + impl Deserialize must set has_serde=true"
);
}
#[test]
fn test_has_serde_with_derived_serialize_and_manual_deserialize() {
let source = r#"
#[derive(Clone, serde::Serialize)]
pub struct AuthorizationConfig {
pub required_roles: Vec<String>,
}
impl<'de> serde::Deserialize<'de> for AuthorizationConfig {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
unimplemented!()
}
}
"#;
let surface = extract_from_source(source);
assert_eq!(surface.types.len(), 1);
assert!(
surface.types[0].has_serde,
"derived Serialize plus manual Deserialize must set has_serde=true"
);
}
#[test]
fn test_has_serde_with_manual_serialize_and_derived_deserialize() {
let source = r#"
#[derive(Clone, serde::Deserialize)]
pub struct AuthorizationConfig {
pub required_roles: Vec<String>,
}
impl serde::Serialize for AuthorizationConfig {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
unimplemented!()
}
}
"#;
let surface = extract_from_source(source);
assert_eq!(surface.types.len(), 1);
assert!(
surface.types[0].has_serde,
"manual Serialize plus derived Deserialize must set has_serde=true"
);
}
fn mixed_serde_parameter_surface() -> ApiSurface {
extract_from_source(
r#"
#[derive(Clone, serde::Serialize)]
pub struct AuthorizationConfig {
pub required_roles: Vec<String>,
}
impl<'de> serde::Deserialize<'de> for AuthorizationConfig {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
unimplemented!()
}
}
pub fn authorize(config: AuthorizationConfig) {}
"#,
)
}
#[test]
fn mixed_serde_named_parameter_symbols_match_all_ffi_hosts() {
use crate::backends::{csharp::CsharpBackend, ffi::FfiBackend, java::JavaBackend};
use crate::core::backend::Backend;
use crate::core::config::ResolvedCrateConfig;
let surface = mixed_serde_parameter_surface();
let config = ResolvedCrateConfig {
name: "test".to_owned(),
..ResolvedCrateConfig::default()
};
let ffi = FfiBackend.generate_bindings(&surface, &config).expect("generate FFI");
let java = JavaBackend.generate_bindings(&surface, &config).expect("generate Java");
let csharp = CsharpBackend.generate_bindings(&surface, &config).expect("generate C#");
let symbol = "test_authorization_config_from_json";
assert!(ffi.iter().any(|file| file.content.contains(symbol)));
assert!(java.iter().any(|file| file.content.contains(symbol)));
assert!(csharp.iter().any(|file| file.content.contains(symbol)));
}
#[test]
fn test_has_serde_with_lifetime_parameterised_manual_impls() {
let source = r#"
#[derive(Clone, Debug)]
pub struct Foo {
pub value: String,
}
impl serde::Serialize for Foo {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
unimplemented!()
}
}
impl<'de> serde::Deserialize<'de> for Foo {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
unimplemented!()
}
}
"#;
let surface = extract_from_source(source);
assert_eq!(surface.types.len(), 1);
assert!(
surface.types[0].has_serde,
"lifetime-parameterised manual serde impls must set has_serde=true"
);
}
#[test]
fn test_has_serde_only_serialize_not_set() {
let source = r#"
#[derive(Clone, Debug)]
pub struct Foo {
pub value: String,
}
impl serde::Serialize for Foo {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
unimplemented!()
}
}
"#;
let surface = extract_from_source(source);
assert_eq!(surface.types.len(), 1);
assert!(
!surface.types[0].has_serde,
"only Serialize without Deserialize must not set has_serde"
);
}
#[test]
fn test_internally_tagged_enum_preserves_named_value_fields() {
let source = r#"
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum MatchRule {
Exact { value: String },
Suffix { value: String },
Pattern { value: String },
}
"#;
let surface = extract_from_source(source);
let enum_def = surface.enums.first().expect("MatchRule should be extracted");
assert_eq!(enum_def.name, "MatchRule");
assert_eq!(enum_def.serde_tag.as_deref(), Some("kind"));
assert_eq!(enum_def.serde_rename_all.as_deref(), Some("snake_case"));
assert_eq!(enum_def.variants.len(), 3);
for variant in &enum_def.variants {
assert_eq!(variant.fields.len(), 1, "{} must retain its payload", variant.name);
assert_eq!(variant.fields[0].name, "value");
assert_eq!(variant.fields[0].ty, TypeRef::String);
assert!(!variant.is_tuple);
}
}
#[test]
fn test_adjacently_tagged_enum_preserves_tag_and_content_names() {
let source = r#"
#[derive(serde::Serialize, serde::Deserialize)]
#[serde(tag = "kind", content = "payload", rename_all = "snake_case")]
pub enum OperationResult {
Continue,
Custom(String),
}
"#;
let surface = extract_from_source(source);
let enum_def = surface.enums.first().expect("OperationResult should be extracted");
assert_eq!(enum_def.serde_tag.as_deref(), Some("kind"));
assert_eq!(enum_def.serde_content.as_deref(), Some("payload"));
assert_eq!(enum_def.serde_rename_all.as_deref(), Some("snake_case"));
}
#[test]
fn test_enum_rename_all_under_cfg_attr_any_is_honoured() {
let source = r#"
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(any(feature = "serde", feature = "metadata"), derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(any(feature = "serde", feature = "metadata"), serde(rename_all = "snake_case"))]
pub enum TierStrategy {
#[default]
Auto,
Tier2,
}
"#;
let surface = extract_from_source(source);
assert_eq!(surface.enums.len(), 1);
let enum_def = &surface.enums[0];
assert!(
enum_def.has_serde,
"cfg_attr-gated derive(Serialize, Deserialize) must set has_serde"
);
assert_eq!(
enum_def.serde_rename_all.as_deref(),
Some("snake_case"),
"rename_all nested inside cfg_attr(any(...), ...) must be extracted"
);
let auto_variant = enum_def
.variants
.iter()
.find(|v| v.name == "Auto")
.expect("Auto variant present");
let wire_name = crate::backends::java::gen_bindings::helpers::java_apply_rename_all(
&auto_variant.name,
enum_def.serde_rename_all.as_deref(),
);
assert_eq!(
wire_name, "auto",
"Java wire name must honour the cfg_attr-gated rename_all"
);
}
#[test]
fn test_extract_function_since_annotation_is_populated() {
let source = r#"
#[alef(since = "1.0.0")]
pub fn new_api() {}
"#;
let surface = extract_from_source(source);
assert_eq!(surface.functions.len(), 1);
assert_eq!(surface.functions[0].version.since.as_deref(), Some("1.0.0"));
assert!(surface.functions[0].version.deprecated.is_none());
}
#[test]
fn test_struct_serde_from_into_are_extracted_and_independent() {
let source = r#"
#[derive(Clone, serde::Serialize, serde::Deserialize)]
#[serde(into = "(i32, i32)")]
pub struct Point {
pub x: i32,
pub y: i32,
}
"#;
let surface = extract_from_source(source);
assert_eq!(surface.types.len(), 1);
let point = &surface.types[0];
assert_eq!(point.serde_container_conversion.into.as_deref(), Some("(i32, i32)"));
assert_eq!(point.serde_container_conversion.from, None);
assert_eq!(point.serde_container_conversion.try_from, None);
assert!(!point.serde_container_conversion.transparent);
}
#[test]
fn test_struct_serde_from_and_try_from_are_extracted_and_distinguished() {
let source = r#"
#[derive(Clone, serde::Serialize, serde::Deserialize)]
#[serde(from = "RangeWire", try_from = "RangeWire")]
pub struct Range {
pub start: i32,
pub end: i32,
}
"#;
let surface = extract_from_source(source);
assert_eq!(surface.types.len(), 1);
let range = &surface.types[0];
assert_eq!(range.serde_container_conversion.from.as_deref(), Some("RangeWire"));
assert_eq!(range.serde_container_conversion.try_from.as_deref(), Some("RangeWire"));
assert_eq!(range.serde_container_conversion.into, None);
}
#[test]
fn test_struct_serde_transparent_is_extracted() {
let source = r#"
#[derive(Clone, serde::Serialize, serde::Deserialize)]
#[serde(transparent)]
pub struct Pixels {
pub value: u32,
}
"#;
let surface = extract_from_source(source);
assert_eq!(surface.types.len(), 1);
assert!(surface.types[0].serde_container_conversion.transparent);
assert_eq!(surface.types[0].serde_container_conversion.from, None);
}
#[test]
fn test_struct_serde_container_conversion_attrs_default_absent() {
let source = r#"
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct PlainConfig {
pub name: String,
}
"#;
let surface = extract_from_source(source);
assert_eq!(surface.types.len(), 1);
let plain = &surface.types[0];
assert_eq!(plain.serde_container_conversion.from, None);
assert_eq!(plain.serde_container_conversion.into, None);
assert_eq!(plain.serde_container_conversion.try_from, None);
assert!(!plain.serde_container_conversion.transparent);
}
#[test]
fn test_struct_serde_from_under_cfg_attr_is_extracted() {
let source = r#"
#[derive(Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(from = "(f64, f64)", into = "(f64, f64)"))]
pub struct BoundingBox {
pub width: f64,
pub height: f64,
}
"#;
let surface = extract_from_source(source);
assert_eq!(surface.types.len(), 1);
let bbox = &surface.types[0];
assert_eq!(bbox.serde_container_conversion.from.as_deref(), Some("(f64, f64)"));
assert_eq!(bbox.serde_container_conversion.into.as_deref(), Some("(f64, f64)"));
}