use super::*;
fn json_config() -> CodeGenConfig {
CodeGenConfig {
generate_json: true,
generate_views: false,
..CodeGenConfig::default()
}
}
#[test]
fn test_json_enum_has_custom_impls_and_from_proto_name() {
let mut file = proto3_file("color.proto");
file.enum_type.push(EnumDescriptorProto {
name: Some("Color".to_string()),
value: vec![
enum_value("RED", 0),
enum_value("GREEN", 1),
enum_value("BLUE", 2),
],
..Default::default()
});
let files =
generate(&[file], &["color.proto".to_string()], &json_config()).expect("should generate");
let content = &joined(&files);
assert!(
content.contains("impl ::serde::Serialize for Color"),
"missing custom Serialize impl on enum: {content}"
);
assert!(
content.contains("impl<'de> ::serde::Deserialize<'de> for Color"),
"missing custom Deserialize impl on enum: {content}"
);
assert!(
content.contains("fn from_proto_name"),
"missing from_proto_name impl: {content}"
);
assert!(
content.contains(r#""RED" => ::core::option::Option::Some(Self::RED)"#),
"missing RED arm in from_proto_name: {content}"
);
}
#[test]
fn test_json_enum_alias_in_from_proto_name() {
let mut file = proto3_file("status.proto");
file.enum_type.push(EnumDescriptorProto {
name: Some("Status".to_string()),
value: vec![
enum_value("UNKNOWN", 0),
enum_value("STARTED", 1),
enum_value("RUNNING", 1), ],
options: (crate::generated::descriptor::EnumOptions {
allow_alias: Some(true),
..Default::default()
})
.into(),
..Default::default()
});
let files =
generate(&[file], &["status.proto".to_string()], &json_config()).expect("should generate");
let content = &joined(&files);
assert!(
content.contains(r#""STARTED" => ::core::option::Option::Some(Self::STARTED)"#),
"missing STARTED arm: {content}"
);
assert!(
content.contains(r#""RUNNING" => ::core::option::Option::Some(Self::STARTED)"#),
"missing RUNNING alias arm: {content}"
);
}
#[test]
fn test_json_message_has_derive_and_field_attrs() {
let mut file = proto3_file("scalars_json.proto");
file.message_type.push(DescriptorProto {
name: Some("Msg".to_string()),
field: vec![
FieldDescriptorProto {
name: Some("count".to_string()),
number: Some(1),
label: Some(Label::LABEL_OPTIONAL),
r#type: Some(Type::TYPE_INT32),
json_name: Some("count".to_string()),
..Default::default()
},
FieldDescriptorProto {
name: Some("big_num".to_string()),
number: Some(2),
label: Some(Label::LABEL_OPTIONAL),
r#type: Some(Type::TYPE_INT64),
json_name: Some("bigNum".to_string()),
..Default::default()
},
FieldDescriptorProto {
name: Some("data".to_string()),
number: Some(3),
label: Some(Label::LABEL_OPTIONAL),
r#type: Some(Type::TYPE_BYTES),
json_name: Some("data".to_string()),
..Default::default()
},
FieldDescriptorProto {
name: Some("ratio".to_string()),
number: Some(4),
label: Some(Label::LABEL_OPTIONAL),
r#type: Some(Type::TYPE_FLOAT),
json_name: Some("ratio".to_string()),
..Default::default()
},
],
..Default::default()
});
let files = generate(&[file], &["scalars_json.proto".to_string()], &json_config())
.expect("should generate");
let content = &joined(&files);
assert!(
content.contains("derive(::serde::Serialize, ::serde::Deserialize)"),
"missing serde derive on struct: {content}"
);
assert!(
content.contains("serde(default)"),
"missing #[serde(default)] on struct: {content}"
);
assert!(
content.contains(r#"rename = "count""#),
"missing rename for count: {content}"
);
assert!(
content.contains("is_zero_i32"),
"missing skip_serializing_if for count: {content}"
);
assert!(
content.contains(r#"with = "::buffa::json_helpers::int64""#),
"missing int64 with attr: {content}"
);
assert!(
content.contains("is_zero_i64"),
"missing skip_serializing_if for bigNum: {content}"
);
assert!(
content.contains(r#"with = "::buffa::json_helpers::bytes""#),
"missing bytes with attr: {content}"
);
assert!(
content.contains("is_empty_bytes"),
"missing skip_serializing_if for data: {content}"
);
assert!(
content.contains(r#"with = "::buffa::json_helpers::float""#),
"missing float with attr: {content}"
);
assert!(
content.contains("is_zero_f32"),
"missing skip_serializing_if for ratio: {content}"
);
assert!(
content.contains("serde(skip)"),
"missing serde(skip) for __buffa_unknown_fields: {content}"
);
}
#[test]
fn test_json_oneof_field_is_flattened() {
let mut file = proto3_file("oneof_json.proto");
file.message_type.push(DescriptorProto {
name: Some("WithOneof".to_string()),
field: vec![FieldDescriptorProto {
name: Some("count".to_string()),
number: Some(1),
label: Some(Label::LABEL_OPTIONAL),
r#type: Some(Type::TYPE_INT32),
oneof_index: Some(0),
json_name: Some("count".to_string()),
..Default::default()
}],
oneof_decl: vec![OneofDescriptorProto {
name: Some("kind".to_string()),
..Default::default()
}],
..Default::default()
});
let files = generate(&[file], &["oneof_json.proto".to_string()], &json_config())
.expect("should generate");
let content = &joined(&files);
assert!(
content.contains("serde(flatten)"),
"oneof field must have serde(flatten): {content}"
);
assert!(
content.contains("impl serde::Serialize for Kind"),
"oneof enum must have Serialize impl: {content}"
);
}
#[test]
fn test_json_oneof_deserialize_null_and_duplicate_handling() {
let mut file = proto3_file("oneof_deser.proto");
file.message_type.push(DescriptorProto {
name: Some("WithOneof".to_string()),
field: vec![
FieldDescriptorProto {
name: Some("count".to_string()),
number: Some(1),
label: Some(Label::LABEL_OPTIONAL),
r#type: Some(Type::TYPE_INT32),
oneof_index: Some(0),
json_name: Some("count".to_string()),
..Default::default()
},
FieldDescriptorProto {
name: Some("name".to_string()),
number: Some(2),
label: Some(Label::LABEL_OPTIONAL),
r#type: Some(Type::TYPE_STRING),
oneof_index: Some(0),
json_name: Some("name".to_string()),
..Default::default()
},
],
oneof_decl: vec![OneofDescriptorProto {
name: Some("kind".to_string()),
..Default::default()
}],
..Default::default()
});
let files = generate(&[file], &["oneof_deser.proto".to_string()], &json_config())
.expect("should generate");
let content = &joined(&files);
assert!(
content.contains("NullableDeserializeSeed"),
"oneof deserialize must use NullableDeserializeSeed: {content}"
);
assert!(
content.contains("__oneof_kind.is_some()"),
"oneof deserialize must check for duplicate fields: {content}"
);
assert!(
content.contains("struct _DeserSeed"),
"helper-using variant must define _DeserSeed: {content}"
);
assert!(
content.contains("Deserialize<'de> for WithOneof"),
"message must have custom Deserialize impl: {content}"
);
assert!(
!content.contains("Deserialize<'de> for Kind"),
"oneof enum must NOT have Deserialize impl: {content}"
);
assert!(
content.contains("DefaultDeserializeSeed"),
"default-serde variant must use DefaultDeserializeSeed: {content}"
);
}
#[test]
fn test_json_oneof_value_variant_forwards_null() {
let mut file = proto3_file("value_oneof.proto");
file.message_type.push(DescriptorProto {
name: Some("Wrapper".to_string()),
field: vec![
FieldDescriptorProto {
name: Some("text".to_string()),
number: Some(1),
label: Some(Label::LABEL_OPTIONAL),
r#type: Some(Type::TYPE_STRING),
oneof_index: Some(0),
..Default::default()
},
FieldDescriptorProto {
name: Some("meta".to_string()),
number: Some(2),
label: Some(Label::LABEL_OPTIONAL),
r#type: Some(Type::TYPE_MESSAGE),
type_name: Some(".google.protobuf.Value".to_string()),
oneof_index: Some(0),
..Default::default()
},
],
oneof_decl: vec![OneofDescriptorProto {
name: Some("payload".to_string()),
..Default::default()
}],
..Default::default()
});
let mut value_file = proto3_file("google/protobuf/struct.proto");
value_file.package = Some("google.protobuf".to_string());
value_file.message_type.push(DescriptorProto {
name: Some("Value".to_string()),
..Default::default()
});
let files = generate(
&[value_file, file],
&["value_oneof.proto".to_string()],
&json_config(),
)
.expect("should generate");
let content = &joined(&files);
let nullable_count = content.matches("NullableDeserializeSeed").count();
assert_eq!(
nullable_count, 1,
"Value variant must NOT use NullableDeserializeSeed (only text should): {content}"
);
}
#[test]
fn test_no_serde_attrs_without_generate_json_flag() {
let mut file = proto3_file("plain.proto");
file.message_type.push(DescriptorProto {
name: Some("Msg".to_string()),
field: vec![make_field(
"big_num",
1,
Label::LABEL_OPTIONAL,
Type::TYPE_INT64,
)],
..Default::default()
});
let files = generate(
&[file],
&["plain.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("should generate");
let content = &joined(&files);
assert!(
!content.contains("serde"),
"serde attrs must be absent without generate_json: {content}"
);
}
#[test]
fn test_json_extension_range_uses_buffa_serde_json_reexport() {
let mut file = proto3_file("ext_json.proto");
file.package = Some("pkg".to_string());
file.message_type.push(DescriptorProto {
name: Some("Target".to_string()),
extension_range: vec![
crate::generated::descriptor::descriptor_proto::ExtensionRange {
start: Some(100),
end: Some(200),
..Default::default()
},
],
..Default::default()
});
let cfg = CodeGenConfig {
generate_json: true,
generate_views: false,
preserve_unknown_fields: true,
..CodeGenConfig::default()
};
let files = generate(&[file], &["ext_json.proto".to_string()], &cfg).expect("should generate");
let content = &joined(&files);
assert!(
content.contains("::buffa::serde_json::Value"),
"extension JSON deserialize must route through ::buffa::serde_json re-export: {content}"
);
assert_eq!(
content.matches("::serde_json::").count(),
content.matches("::buffa::serde_json::").count(),
"bare ::serde_json:: path leaked into generated output: {content}"
);
}
#[test]
fn test_json_any_const_emitted_per_message() {
let mut file = proto3_file("any_entry.proto");
file.package = Some("acme".to_string());
file.message_type.push(DescriptorProto {
name: Some("Widget".to_string()),
..Default::default()
});
let files = generate(&[file], &["any_entry.proto".to_string()], &json_config())
.expect("should generate");
let content = &joined(&files);
assert!(
content.contains("pub const __WIDGET_JSON_ANY: ::buffa::type_registry::JsonAnyEntry"),
"missing JSON Any const: {content}"
);
assert!(
content.contains(r#"type_url: "type.googleapis.com/acme.Widget""#),
"wrong type_url: {content}"
);
assert!(
content.contains("::buffa::type_registry::any_to_json::<Widget>"),
"missing any_to_json fn pointer: {content}"
);
assert!(
content.contains("::buffa::type_registry::any_from_json::<Widget>"),
"missing any_from_json fn pointer: {content}"
);
assert!(
content.contains("is_wkt: false"),
"user messages must emit is_wkt: false: {content}"
);
assert!(
!content.contains("__WIDGET_TEXT_ANY"),
"TEXT_ANY must be absent with generate_text off: {content}"
);
}
#[test]
fn test_register_types_emitted_with_json_any_only() {
let mut file = proto3_file("reg.proto");
file.message_type.push(DescriptorProto {
name: Some("Foo".to_string()),
..Default::default()
});
file.message_type.push(DescriptorProto {
name: Some("Bar".to_string()),
..Default::default()
});
let files =
generate(&[file], &["reg.proto".to_string()], &json_config()).expect("should generate");
let content = &joined(&files);
assert!(
content.contains("pub fn register_types(reg: &mut ::buffa::type_registry::TypeRegistry)"),
"missing register_types fn: {content}"
);
assert!(
content.contains("reg.register_json_any(super::__FOO_JSON_ANY)"),
"missing Foo JSON Any registration: {content}"
);
assert!(
content.contains("reg.register_json_any(super::__BAR_JSON_ANY)"),
"missing Bar JSON Any registration: {content}"
);
assert!(
!content.contains("register_text_any"),
"register_text_any must be absent without generate_text: {content}"
);
}
#[test]
fn test_register_types_includes_nested_message_any_entries() {
let mut file = proto3_file("nested_any.proto");
file.message_type.push(DescriptorProto {
name: Some("Outer".to_string()),
nested_type: vec![DescriptorProto {
name: Some("Inner".to_string()),
..Default::default()
}],
..Default::default()
});
let files = generate(&[file], &["nested_any.proto".to_string()], &json_config())
.expect("should generate");
let content = &joined(&files);
assert!(
content.contains("reg.register_json_any(super::__OUTER_JSON_ANY)"),
"missing top-level Outer: {content}"
);
assert!(
content.contains("reg.register_json_any(super::outer::__INNER_JSON_ANY)"),
"missing nested Inner path: {content}"
);
}
#[test]
fn test_any_entry_not_emitted_without_generate_json_or_text() {
let mut file = proto3_file("noany.proto");
file.message_type.push(DescriptorProto {
name: Some("Msg".to_string()),
..Default::default()
});
let files = generate(
&[file],
&["noany.proto".to_string()],
&CodeGenConfig::default(),
)
.expect("should generate");
let content = &joined(&files);
assert!(
!content.contains("_JSON_ANY") && !content.contains("_TEXT_ANY"),
"Any consts must be absent: {content}"
);
assert!(
!content.contains("register_types"),
"register_types must be absent: {content}"
);
}
#[test]
fn test_text_any_emitted_independent_of_json() {
let mut file = proto3_file("textonly.proto");
file.message_type.push(DescriptorProto {
name: Some("Msg".to_string()),
..Default::default()
});
let cfg = CodeGenConfig {
generate_text: true,
..Default::default()
};
let files = generate(&[file], &["textonly.proto".to_string()], &cfg).expect("should generate");
let content = &joined(&files);
assert!(
content.contains("pub const __MSG_TEXT_ANY: ::buffa::type_registry::TextAnyEntry"),
"missing TEXT_ANY const: {content}"
);
assert!(
content.contains("::buffa::type_registry::any_encode_text::<Msg>"),
"missing any_encode_text fn pointer: {content}"
);
assert!(
!content.contains("__MSG_JSON_ANY"),
"JSON_ANY must be absent with generate_json off: {content}"
);
assert!(
content.contains("reg.register_text_any(super::__MSG_TEXT_ANY)"),
"missing register_text_any call: {content}"
);
assert!(
!content.contains("register_json_any"),
"register_json_any must be absent: {content}"
);
}
#[test]
fn message_named_result_does_not_shadow_std_result_in_serde() {
let result_msg = DescriptorProto {
name: Some("Result".into()),
field: vec![make_field(
"value",
1,
Label::LABEL_OPTIONAL,
Type::TYPE_INT64,
)],
..Default::default()
};
let parent = DescriptorProto {
name: Some("ParseJob".into()),
nested_type: vec![result_msg],
..Default::default()
};
let mut file = proto3_file("job.proto");
file.package = Some("pkg".into());
file.message_type.push(parent);
let files =
generate(&[file], &["job.proto".to_string()], &json_config()).expect("should generate");
let content = &joined(&files);
assert!(
!content.contains("fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self"),
"serde Deserialize must not use bare `Result<Self, ...>` — it shadows \
the proto message named Result.\nGenerated code:\n{content}"
);
assert!(
content.contains("::core::result::Result<Self"),
"serde Deserialize should use ::core::result::Result.\nGenerated code:\n{content}"
);
}