martin-g commented on code in PR #458:
URL: https://github.com/apache/avro-rs/pull/458#discussion_r2788189355
##########
avro/src/serde/de.rs:
##########
@@ -799,9 +850,19 @@ impl<'de> de::Deserializer<'de> for Deserializer<'de> {
Value::Record(fields) =>
visitor.visit_enum(EnumDeserializer::new(fields)),
Value::String(field) =>
visitor.visit_enum(EnumUnitDeserializer::new(field)),
Value::Union(idx, inner) => {
- if (*idx as usize) < variants.len() {
+ // if we came here from a some, we need to check if we are
deserializing a
+ // non-newtype enum
+ if self.deserializing_some
+ && let Value::Enum(_index, field) = inner.deref()
+ && variants.contains(&&**field)
+ {
+ return
visitor.visit_enum(EnumUnitDeserializer::new(field));
+ }
+ // Assume `null` is the first branch if deserializing some so
decrement the variant index
Review Comment:
This assumption is not guaranteed by the spec :-/
We should add some index/length checks to avoid panics/overflows.
##########
avro/src/serde/de.rs:
##########
@@ -638,6 +664,21 @@ impl<'de> de::Deserializer<'de> for Deserializer<'de> {
let d_bytes: [u8; 12] = d.into();
visitor.visit_byte_buf(Vec::from(d_bytes))
}
+ Value::Union(i, x) => {
+ if matches!(x.deref(), Value::Union(_, _)) {
+ Err(de::Error::custom(format!(
+ "Directly nested union types are not supported. Got
Value::Union({i}, {x:?})"
+ )))
+ } else {
+ Self::new(x.deref())
+ .deserialize_byte_buf(visitor)
+ .map_err(|e| {
+ de::Error::custom(format!(
+ "Attempted to deserialize Value::Union({i},
{x:?}) as bytes: {e:?}"
Review Comment:
```suggestion
"Attempted to deserialize Value::Union({i},
{x:?}) as byte_buf: {e:?}"
```
##########
avro/src/serde/ser_schema.rs:
##########
@@ -1460,19 +1477,37 @@ impl<'s, W: Write> SchemaAwareWriteSerializer<'s, W> {
encode_int(variant_index as i32, &mut self.writer)
}
Schema::Union(union_schema) => {
- if variant_index as usize >= union_schema.schemas.len() {
+ // If we came here from a some, we need to check if we are
serializing a
+ // non-newtype enum
+ if self.serializing_some {
+ for (i, variant_schema) in
union_schema.schemas.iter().enumerate() {
+ match variant_schema {
+ Schema::Enum(enum_schema) if enum_schema.name.name
== name => {
+ if variant_index as usize >=
enum_schema.symbols.len() {
+ return Err(create_error(format!(
+ "Variant index out of bounds: {}. The
Enum schema has '{}' symbols",
+ variant_index,
+ enum_schema.symbols.len()
+ )));
+ }
+ encode_int(i as i32, &mut self.writer)?;
+ return encode_int(variant_index as i32, &mut
self.writer);
+ }
+ _ => { /* skip */ }
+ }
+ }
+ }
+ let branch_index = variant_index as usize +
usize::from(self.serializing_some);
Review Comment:
Again, this assumes that the `null` is the first variant in the union.
##########
avro/tests/nullable_union.rs:
##########
@@ -0,0 +1,982 @@
+use apache_avro::{AvroResult, Reader, Schema, Writer, from_value,
types::Value};
+use serde::de::DeserializeOwned;
+use serde::{Deserialize, Serialize};
+
+struct TestCase<'a, T> {
+ input: T,
+ schema: &'a Schema,
+ expected_avro: &'a Value,
+}
+
+fn test_serialize<T>(test_case: TestCase<T>) -> AvroResult<()>
+where
+ T: Serialize + std::fmt::Debug,
+{
+ let mut writer = Writer::new(test_case.schema, Vec::new())?;
+ writer.append_ser(&test_case.input)?;
+ let bytes = writer.into_inner()?;
+ let mut reader = Reader::with_schema(test_case.schema, &bytes[..])?;
+ let read_avro_value = reader.next().unwrap()?;
+ assert_eq!(
+ &read_avro_value, test_case.expected_avro,
+ "serialization is not correct: expected: {:?}, got: {:?}, input: {:?}",
+ test_case.expected_avro, read_avro_value, test_case.input
+ );
+ Ok(())
+}
+
+fn test_deserialize<T>(test_case: TestCase<T>) -> AvroResult<()>
+where
+ T: DeserializeOwned + std::fmt::Debug + std::cmp::PartialEq,
+{
+ let deserialized: T = from_value(test_case.expected_avro)?;
+ assert_eq!(
+ deserialized, test_case.input,
+ "deserialization is not correct: expected: {:?}, got: {:?}",
+ test_case.input, deserialized,
+ );
+ Ok(())
+}
+
+mod nullable_enum {
+ use super::*;
+
+ const NULLABLE_ENUM_SCHEMA: &str = r#"
+ {
+ "name": "MyUnion",
+ "type": [
+ "null",
+ {
+ "type": "enum",
+ "name": "MyEnum",
+ "symbols": ["A", "B"]
+ }
+ ]
+ }
+ "#;
+
+ #[derive(Debug, Serialize, Deserialize, PartialEq)]
+ enum MyEnum {
+ A,
+ B,
+ }
+
+ #[derive(Debug, Serialize, Deserialize, PartialEq)]
+ enum MyUnionNullable {
+ Null,
+ MyEnum(MyEnum),
+ }
+
+ #[derive(Debug, Serialize, Deserialize, PartialEq)]
+ enum MyUnionAvroJsonEncoding {
+ MyEnum(MyEnum),
+ }
+
+ fn schema() -> Schema {
+ Schema::parse_str(NULLABLE_ENUM_SCHEMA).unwrap()
+ }
+
+ fn null_variant_expected_avro() -> Value {
+ Value::Union(0, Box::new(Value::Null))
+ }
+
+ fn a_variant_expected_avro() -> Value {
+ Value::Union(1, Box::new(Value::Enum(0, "A".to_string())))
+ }
+
+ #[test]
+ fn serialize_null_variant_enum_null() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: MyUnionNullable::Null,
+ schema: &schema(),
+ expected_avro: &null_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn deserialize_null_variant_enum_null() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: MyUnionNullable::Null,
+ schema: &schema(),
+ expected_avro: &null_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn serialize_rusty_null() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: None::<MyEnum>,
+ schema: &schema(),
+ expected_avro: &null_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn deserialize_rusty_null() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: None::<MyEnum>,
+ schema: &schema(),
+ expected_avro: &null_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn serialize_avro_json_encoding_compatible_null() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: None::<MyUnionAvroJsonEncoding>,
+ schema: &schema(),
+ expected_avro: &null_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn deserialize_avro_json_encoding_compatible_null() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: None::<MyUnionAvroJsonEncoding>,
+ schema: &schema(),
+ expected_avro: &null_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn serialize_null_variant_enum_my_enum_a() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: MyUnionNullable::MyEnum(MyEnum::A),
+ schema: &schema(),
+ expected_avro: &a_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn deserialize_null_variant_enum_my_enum_a() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: MyUnionNullable::MyEnum(MyEnum::A),
+ schema: &schema(),
+ expected_avro: &a_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn serialize_rusty_my_enum_a() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: Some(MyEnum::A),
+ schema: &schema(),
+ expected_avro: &a_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn deserialize_rusty_my_enum_a() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: Some(MyEnum::A),
+ schema: &schema(),
+ expected_avro: &a_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn serialize_avro_json_encoding_compatible_my_enum_a() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: Some(MyUnionAvroJsonEncoding::MyEnum(MyEnum::A)),
+ schema: &schema(),
+ expected_avro: &a_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn deserialize_avro_json_encoding_compatible_my_enum_a() -> AvroResult<()>
{
+ test_deserialize(TestCase {
+ input: Some(MyUnionAvroJsonEncoding::MyEnum(MyEnum::A)),
+ schema: &schema(),
+ expected_avro: &a_variant_expected_avro(),
+ })
+ }
+}
+
+mod nullable_primitive_int {
+ use super::*;
+
+ const NULLABLE_INT_SCHEMA: &str = r#"
+ {
+ "name": "MyUnion",
+ "type": [
+ "null",
+ "int"
Review Comment:
Please add a test where the `null` is not the first variant too.
##########
avro/tests/nullable_union.rs:
##########
@@ -0,0 +1,982 @@
+use apache_avro::{AvroResult, Reader, Schema, Writer, from_value,
types::Value};
+use serde::de::DeserializeOwned;
+use serde::{Deserialize, Serialize};
+
+struct TestCase<'a, T> {
+ input: T,
+ schema: &'a Schema,
+ expected_avro: &'a Value,
+}
+
+fn test_serialize<T>(test_case: TestCase<T>) -> AvroResult<()>
+where
+ T: Serialize + std::fmt::Debug,
+{
+ let mut writer = Writer::new(test_case.schema, Vec::new())?;
+ writer.append_ser(&test_case.input)?;
+ let bytes = writer.into_inner()?;
+ let mut reader = Reader::with_schema(test_case.schema, &bytes[..])?;
+ let read_avro_value = reader.next().unwrap()?;
+ assert_eq!(
+ &read_avro_value, test_case.expected_avro,
+ "serialization is not correct: expected: {:?}, got: {:?}, input: {:?}",
+ test_case.expected_avro, read_avro_value, test_case.input
+ );
+ Ok(())
+}
+
+fn test_deserialize<T>(test_case: TestCase<T>) -> AvroResult<()>
+where
+ T: DeserializeOwned + std::fmt::Debug + std::cmp::PartialEq,
+{
+ let deserialized: T = from_value(test_case.expected_avro)?;
+ assert_eq!(
+ deserialized, test_case.input,
+ "deserialization is not correct: expected: {:?}, got: {:?}",
+ test_case.input, deserialized,
+ );
+ Ok(())
+}
+
+mod nullable_enum {
+ use super::*;
+
+ const NULLABLE_ENUM_SCHEMA: &str = r#"
+ {
+ "name": "MyUnion",
+ "type": [
+ "null",
+ {
+ "type": "enum",
+ "name": "MyEnum",
+ "symbols": ["A", "B"]
+ }
+ ]
+ }
+ "#;
+
+ #[derive(Debug, Serialize, Deserialize, PartialEq)]
+ enum MyEnum {
+ A,
+ B,
+ }
+
+ #[derive(Debug, Serialize, Deserialize, PartialEq)]
+ enum MyUnionNullable {
+ Null,
+ MyEnum(MyEnum),
+ }
+
+ #[derive(Debug, Serialize, Deserialize, PartialEq)]
+ enum MyUnionAvroJsonEncoding {
+ MyEnum(MyEnum),
+ }
+
+ fn schema() -> Schema {
+ Schema::parse_str(NULLABLE_ENUM_SCHEMA).unwrap()
+ }
+
+ fn null_variant_expected_avro() -> Value {
+ Value::Union(0, Box::new(Value::Null))
+ }
+
+ fn a_variant_expected_avro() -> Value {
+ Value::Union(1, Box::new(Value::Enum(0, "A".to_string())))
+ }
+
+ #[test]
+ fn serialize_null_variant_enum_null() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: MyUnionNullable::Null,
+ schema: &schema(),
+ expected_avro: &null_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn deserialize_null_variant_enum_null() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: MyUnionNullable::Null,
+ schema: &schema(),
+ expected_avro: &null_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn serialize_rusty_null() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: None::<MyEnum>,
+ schema: &schema(),
+ expected_avro: &null_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn deserialize_rusty_null() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: None::<MyEnum>,
+ schema: &schema(),
+ expected_avro: &null_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn serialize_avro_json_encoding_compatible_null() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: None::<MyUnionAvroJsonEncoding>,
+ schema: &schema(),
+ expected_avro: &null_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn deserialize_avro_json_encoding_compatible_null() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: None::<MyUnionAvroJsonEncoding>,
+ schema: &schema(),
+ expected_avro: &null_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn serialize_null_variant_enum_my_enum_a() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: MyUnionNullable::MyEnum(MyEnum::A),
+ schema: &schema(),
+ expected_avro: &a_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn deserialize_null_variant_enum_my_enum_a() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: MyUnionNullable::MyEnum(MyEnum::A),
+ schema: &schema(),
+ expected_avro: &a_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn serialize_rusty_my_enum_a() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: Some(MyEnum::A),
+ schema: &schema(),
+ expected_avro: &a_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn deserialize_rusty_my_enum_a() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: Some(MyEnum::A),
+ schema: &schema(),
+ expected_avro: &a_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn serialize_avro_json_encoding_compatible_my_enum_a() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: Some(MyUnionAvroJsonEncoding::MyEnum(MyEnum::A)),
+ schema: &schema(),
+ expected_avro: &a_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn deserialize_avro_json_encoding_compatible_my_enum_a() -> AvroResult<()>
{
+ test_deserialize(TestCase {
+ input: Some(MyUnionAvroJsonEncoding::MyEnum(MyEnum::A)),
+ schema: &schema(),
+ expected_avro: &a_variant_expected_avro(),
+ })
+ }
+}
+
+mod nullable_primitive_int {
+ use super::*;
+
+ const NULLABLE_INT_SCHEMA: &str = r#"
+ {
+ "name": "MyUnion",
+ "type": [
+ "null",
+ "int"
+ ]
+ }
+ "#;
+
+ #[derive(Debug, Serialize, Deserialize, PartialEq)]
+ enum MyUnionNullable {
+ Null,
+ Int(i32),
+ }
+
+ #[derive(Debug, Serialize, Deserialize, PartialEq)]
+ enum MyUnionAvroJsonEncoding {
+ #[serde(rename = "int")]
+ Int(i32),
+ }
+
+ fn schema() -> Schema {
+ Schema::parse_str(NULLABLE_INT_SCHEMA).unwrap()
+ }
+
+ fn null_variant_expected_avro() -> Value {
+ Value::Union(0, Box::new(Value::Null))
+ }
+
+ fn int_variant_expected_avro(v: i32) -> Value {
+ Value::Union(1, Box::new(Value::Int(v)))
+ }
+
+ #[test]
+ fn serialize_null_variant_enum_null() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: MyUnionNullable::Null,
+ schema: &schema(),
+ expected_avro: &null_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn deserialize_null_variant_enum_null() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: MyUnionNullable::Null,
+ schema: &schema(),
+ expected_avro: &null_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn serialize_rusty_null() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: None::<i32>,
+ schema: &schema(),
+ expected_avro: &null_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn deserialize_rusty_null() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: None::<i32>,
+ schema: &schema(),
+ expected_avro: &null_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn serialize_avro_json_encoding_compatible_null() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: None::<MyUnionAvroJsonEncoding>,
+ schema: &schema(),
+ expected_avro: &null_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn deserialize_avro_json_encoding_compatible_null() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: None::<MyUnionAvroJsonEncoding>,
+ schema: &schema(),
+ expected_avro: &null_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn serialize_null_variant_enum_int_42() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: MyUnionNullable::Int(42),
+ schema: &schema(),
+ expected_avro: &int_variant_expected_avro(42),
+ })
+ }
+
+ #[test]
+ fn deserialize_null_variant_enum_int_42() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: MyUnionNullable::Int(42),
+ schema: &schema(),
+ expected_avro: &int_variant_expected_avro(42),
+ })
+ }
+
+ #[test]
+ fn serialize_rusty_int_42() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: Some(42_i32),
+ schema: &schema(),
+ expected_avro: &int_variant_expected_avro(42),
+ })
+ }
+
+ #[test]
+ fn deserialize_rusty_int_42() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: Some(42_i32),
+ schema: &schema(),
+ expected_avro: &int_variant_expected_avro(42),
+ })
+ }
+
+ #[test]
+ fn serialize_avro_json_encoding_compatible_int_42() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: Some(MyUnionAvroJsonEncoding::Int(42)),
+ schema: &schema(),
+ expected_avro: &int_variant_expected_avro(42),
+ })
+ }
+
+ #[test]
+ fn deserialize_avro_json_encoding_compatible_int_42() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: Some(MyUnionAvroJsonEncoding::Int(42)),
+ schema: &schema(),
+ expected_avro: &int_variant_expected_avro(42),
+ })
+ }
+}
+
+mod nullable_record {
+ use super::*;
+
+ const NULLABLE_RECORD_SCHEMA: &str = r#"
+ {
+ "name": "MyUnion",
+ "type": [
+ "null",
+ {
+ "type": "record",
+ "name": "MyRecord",
+ "fields": [
+ {"name": "a", "type": "int"}
+ ]
+ }
+ ]
+ }
+ "#;
+
+ #[derive(Debug, Serialize, Deserialize, PartialEq)]
+ struct MyRecord {
+ a: i32,
+ }
+
+ #[derive(Debug, Serialize, Deserialize, PartialEq)]
+ enum MyUnionNullable {
+ Null,
+ MyRecord(MyRecord),
+ }
+
+ #[derive(Debug, Serialize, Deserialize, PartialEq)]
+ enum MyUnionAvroJsonEncoding {
+ MyRecord(MyRecord),
+ }
+
+ fn schema() -> Schema {
+ Schema::parse_str(NULLABLE_RECORD_SCHEMA).unwrap()
+ }
+
+ fn null_variant_expected_avro() -> Value {
+ Value::Union(0, Box::new(Value::Null))
+ }
+
+ fn record_variant_expected_avro(a: i32) -> Value {
+ Value::Union(
+ 1,
+ Box::new(Value::Record(vec![("a".to_string(), Value::Int(a))])),
+ )
+ }
+
+ #[test]
+ fn serialize_null_variant_enum_null() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: MyUnionNullable::Null,
+ schema: &schema(),
+ expected_avro: &null_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn deserialize_null_variant_enum_null() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: MyUnionNullable::Null,
+ schema: &schema(),
+ expected_avro: &null_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn serialize_rusty_null() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: None::<MyRecord>,
+ schema: &schema(),
+ expected_avro: &null_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn deserialize_rusty_null() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: None::<MyRecord>,
+ schema: &schema(),
+ expected_avro: &null_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn serialize_avro_json_encoding_compatible_null() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: None::<MyUnionAvroJsonEncoding>,
+ schema: &schema(),
+ expected_avro: &null_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn deserialize_avro_json_encoding_compatible_null() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: None::<MyUnionAvroJsonEncoding>,
+ schema: &schema(),
+ expected_avro: &null_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn serialize_null_variant_enum_my_record_a_27() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: MyUnionNullable::MyRecord(MyRecord { a: 27 }),
+ schema: &schema(),
+ expected_avro: &record_variant_expected_avro(27),
+ })
+ }
+
+ #[test]
+ fn deserialize_null_variant_enum_my_record_a_27() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: MyUnionNullable::MyRecord(MyRecord { a: 27 }),
+ schema: &schema(),
+ expected_avro: &record_variant_expected_avro(27),
+ })
+ }
+
+ #[test]
+ fn serialize_rusty_my_record_a_27() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: Some(MyRecord { a: 27 }),
+ schema: &schema(),
+ expected_avro: &record_variant_expected_avro(27),
+ })
+ }
+
+ #[test]
+ fn deserialize_rusty_my_record_a_27() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: Some(MyRecord { a: 27 }),
+ schema: &schema(),
+ expected_avro: &record_variant_expected_avro(27),
+ })
+ }
+
+ #[test]
+ fn serialize_avro_json_encoding_compatible_my_record_a_27() ->
AvroResult<()> {
+ test_serialize(TestCase {
+ input: Some(MyUnionAvroJsonEncoding::MyRecord(MyRecord { a: 27 })),
+ schema: &schema(),
+ expected_avro: &record_variant_expected_avro(27),
+ })
+ }
+
+ #[test]
+ fn deserialize_avro_json_encoding_compatible_my_record_a_27() ->
AvroResult<()> {
+ test_deserialize(TestCase {
+ input: Some(MyUnionAvroJsonEncoding::MyRecord(MyRecord { a: 27 })),
+ schema: &schema(),
+ expected_avro: &record_variant_expected_avro(27),
+ })
+ }
+}
+
+mod nullable_int_enum_record {
+ use super::*;
+
+ const NULLABLE_INT_ENUM_RECORD_SCHEMA: &str = r#"
+ {
+ "name": "MyUnion",
+ "type": [
+ "null",
+ "int",
+ {
+ "type": "enum",
+ "name": "MyEnum",
+ "symbols": ["A", "B"]
+ },
+ {
+ "type": "record",
+ "name": "MyRecord",
+ "fields": [
+ {"name": "a", "type": "int"}
+ ]
+ }
+ ]
+ }
+ "#;
+
+ #[derive(Debug, Serialize, Deserialize, PartialEq)]
+ enum MyEnum {
+ A,
+ B,
+ }
+
+ #[derive(Debug, Serialize, Deserialize, PartialEq)]
+ struct MyRecord {
+ a: i32,
+ }
+
+ #[derive(Debug, Serialize, Deserialize, PartialEq)]
+ enum MyUnionNullable {
+ Null,
+ Int(i32),
+ MyEnum(MyEnum),
+ MyRecord(MyRecord),
+ }
+
+ #[derive(Debug, Serialize, Deserialize, PartialEq)]
+ #[serde(untagged)]
+ enum MyUnionUntagged {
+ Int(i32),
+ MyEnum(MyEnum),
+ MyRecord(MyRecord),
+ }
+
+ #[derive(Debug, Serialize, Deserialize, PartialEq)]
+ enum MyUnionAvroJsonEncoding {
+ Int(i32),
+ MyEnum(MyEnum),
+ MyRecord(MyRecord),
+ }
+
+ fn schema() -> Schema {
+ Schema::parse_str(NULLABLE_INT_ENUM_RECORD_SCHEMA).unwrap()
+ }
+
+ fn null_variant_expected_avro() -> Value {
+ Value::Union(0, Box::new(Value::Null))
+ }
+
+ fn int_variant_expected_avro(v: i32) -> Value {
+ Value::Union(1, Box::new(Value::Int(v)))
+ }
+
+ fn a_variant_expected_avro() -> Value {
+ Value::Union(2, Box::new(Value::Enum(0, "A".to_string())))
+ }
+
+ fn record_variant_expected_avro(a: i32) -> Value {
+ Value::Union(
+ 3,
+ Box::new(Value::Record(vec![("a".to_string(), Value::Int(a))])),
+ )
+ }
+
+ #[test]
+ fn serialize_null_variant_enum_null() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: MyUnionNullable::Null,
+ schema: &schema(),
+ expected_avro: &null_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn deserialize_null_variant_enum_null() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: MyUnionNullable::Null,
+ schema: &schema(),
+ expected_avro: &null_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn serialize_rusty_null() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: None::<MyUnionAvroJsonEncoding>,
+ schema: &schema(),
+ expected_avro: &null_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn deserialize_rusty_null() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: None::<MyUnionAvroJsonEncoding>,
+ schema: &schema(),
+ expected_avro: &null_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn serialize_rusty_untagged_null() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: None::<MyUnionUntagged>,
+ schema: &schema(),
+ expected_avro: &null_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn deserialize_rusty_untagged_null() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: None::<MyUnionUntagged>,
+ schema: &schema(),
+ expected_avro: &null_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn serialize_null_variant_enum_int_42() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: MyUnionNullable::Int(42),
+ schema: &schema(),
+ expected_avro: &int_variant_expected_avro(42),
+ })
+ }
+
+ #[test]
+ fn deserialize_null_variant_enum_int_42() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: MyUnionNullable::Int(42),
+ schema: &schema(),
+ expected_avro: &int_variant_expected_avro(42),
+ })
+ }
+
+ #[test]
+ fn serialize_rusty_int_42() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: Some(MyUnionAvroJsonEncoding::Int(42)),
+ schema: &schema(),
+ expected_avro: &int_variant_expected_avro(42),
+ })
+ }
+
+ #[test]
+ fn deserialize_rusty_int_42() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: Some(MyUnionAvroJsonEncoding::Int(42)),
+ schema: &schema(),
+ expected_avro: &int_variant_expected_avro(42),
+ })
+ }
+
+ #[test]
+ fn serialize_rusty_untagged_int_42() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: Some(MyUnionUntagged::Int(42)),
+ schema: &schema(),
+ expected_avro: &int_variant_expected_avro(42),
+ })
+ }
+
+ #[test]
+ fn deserialize_rusty_untagged_int_42() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: Some(MyUnionUntagged::Int(42)),
+ schema: &schema(),
+ expected_avro: &int_variant_expected_avro(42),
+ })
+ }
+
+ #[test]
+ fn serialize_null_variant_enum_my_enum_a() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: MyUnionNullable::MyEnum(MyEnum::A),
+ schema: &schema(),
+ expected_avro: &a_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn deserialize_null_variant_enum_my_enum_a() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: MyUnionNullable::MyEnum(MyEnum::A),
+ schema: &schema(),
+ expected_avro: &a_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn serialize_rusty_my_enum_a() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: Some(MyUnionAvroJsonEncoding::MyEnum(MyEnum::A)),
+ schema: &schema(),
+ expected_avro: &a_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn deserialize_rusty_my_enum_a() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: Some(MyUnionAvroJsonEncoding::MyEnum(MyEnum::A)),
+ schema: &schema(),
+ expected_avro: &a_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn serialize_rusty_untagged_my_enum_a() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: Some(MyUnionUntagged::MyEnum(MyEnum::A)),
+ schema: &schema(),
+ expected_avro: &a_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn deserialize_rusty_untagged_my_enum_a() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: Some(MyUnionUntagged::MyEnum(MyEnum::A)),
+ schema: &schema(),
+ expected_avro: &a_variant_expected_avro(),
+ })
+ }
+
+ #[test]
+ fn serialize_null_variant_enum_my_record_a_27() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: MyUnionNullable::MyRecord(MyRecord { a: 27 }),
+ schema: &schema(),
+ expected_avro: &record_variant_expected_avro(27),
+ })
+ }
+
+ #[test]
+ fn deserialize_null_variant_enum_my_record_a_27() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: MyUnionNullable::MyRecord(MyRecord { a: 27 }),
+ schema: &schema(),
+ expected_avro: &record_variant_expected_avro(27),
+ })
+ }
+
+ #[test]
+ fn serialize_rusty_my_record_a_27() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: Some(MyUnionAvroJsonEncoding::MyRecord(MyRecord { a: 27 })),
+ schema: &schema(),
+ expected_avro: &record_variant_expected_avro(27),
+ })
+ }
+
+ #[test]
+ fn deserialize_rusty_my_record_a_27() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: Some(MyUnionAvroJsonEncoding::MyRecord(MyRecord { a: 27 })),
+ schema: &schema(),
+ expected_avro: &record_variant_expected_avro(27),
+ })
+ }
+
+ #[test]
+ fn serialize_rusty_untagged_my_record_a_27() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: Some(MyUnionUntagged::MyRecord(MyRecord { a: 27 })),
+ schema: &schema(),
+ expected_avro: &record_variant_expected_avro(27),
+ })
+ }
+
+ #[test]
+ fn deserialize_rusty_untagged_my_record_a_27() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: Some(MyUnionUntagged::MyRecord(MyRecord { a: 27 })),
+ schema: &schema(),
+ expected_avro: &record_variant_expected_avro(27),
+ })
+ }
+}
+
+mod nullable_untagged_pitfall {
+ use super::*;
+
+ const NULLABLE_RECORD_SCHEMA: &str = r#"
+ {
+ "name": "MyUnion",
+ "type": [
+ "null",
+ {
+ "type": "record",
+ "name": "MyRecordA",
+ "fields": [
+ {"name": "a", "type": "int"}
+ ]
+ },
+ {
+ "type": "record",
+ "name": "MyRecordB",
+ "fields": [
+ {"name": "a", "type": "int"}
+ ]
+ }
+ ]
+ }
+ "#;
+
+ #[derive(Debug, Serialize, Deserialize, PartialEq)]
+ struct MyRecordA {
+ a: i32,
+ }
+
+ #[derive(Debug, Serialize, Deserialize, PartialEq)]
+ struct MyRecordB {
+ a: i32,
+ }
+
+ #[derive(Debug, Serialize, Deserialize, PartialEq)]
+ enum MyUnionNullable {
+ Null,
+ MyRecordA(MyRecordA),
+ MyRecordB(MyRecordB),
+ }
+
+ #[derive(Debug, Serialize, Deserialize, PartialEq)]
+ #[serde(untagged)]
+ enum MyUnionUntagged {
+ MyRecordA(MyRecordA),
+ MyRecordB(MyRecordB),
+ }
+
+ #[derive(Debug, Serialize, Deserialize, PartialEq)]
+ enum MyUnionAvroJsonEncoding {
+ MyRecordA(MyRecordA),
+ MyRecordB(MyRecordB),
+ }
+
+ fn schema() -> Schema {
+ Schema::parse_str(NULLABLE_RECORD_SCHEMA).unwrap()
+ }
+
+ fn record_a_variant_expected_avro(a: i32) -> Value {
+ Value::Union(
+ 1,
+ Box::new(Value::Record(vec![("a".to_string(), Value::Int(a))])),
+ )
+ }
+
+ fn record_b_variant_expected_avro(a: i32) -> Value {
+ Value::Union(
+ 2,
+ Box::new(Value::Record(vec![("a".to_string(), Value::Int(a))])),
+ )
+ }
+
+ #[test]
+ fn serialize_null_variant_enum_my_record_a_27() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: MyUnionNullable::MyRecordA(MyRecordA { a: 27 }),
+ schema: &schema(),
+ expected_avro: &record_a_variant_expected_avro(27),
+ })
+ }
+
+ #[test]
+ fn deserialize_null_variant_enum_my_record_a_27() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: MyUnionNullable::MyRecordA(MyRecordA { a: 27 }),
+ schema: &schema(),
+ expected_avro: &record_a_variant_expected_avro(27),
+ })
+ }
+
+ #[test]
+ fn serialize_rusty_my_record_a_27() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: Some(MyUnionAvroJsonEncoding::MyRecordA(MyRecordA { a: 27
})),
+ schema: &schema(),
+ expected_avro: &record_a_variant_expected_avro(27),
+ })
+ }
+
+ #[test]
+ fn deserialize_rusty_my_record_a_27() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: Some(MyUnionAvroJsonEncoding::MyRecordA(MyRecordA { a: 27
})),
+ schema: &schema(),
+ expected_avro: &record_a_variant_expected_avro(27),
+ })
+ }
+
+ #[test]
+ fn serialize_rusty_untagged_my_record_a_27() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: Some(MyUnionUntagged::MyRecordA(MyRecordA { a: 27 })),
+ schema: &schema(),
+ expected_avro: &record_a_variant_expected_avro(27),
+ })
+ }
+
+ #[test]
+ fn deserialize_rusty_untagged_my_record_a_27() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: Some(MyUnionUntagged::MyRecordA(MyRecordA { a: 27 })),
+ schema: &schema(),
+ expected_avro: &record_a_variant_expected_avro(27),
+ })
+ }
+
+ #[test]
+ fn serialize_null_variant_enum_my_record_b_27() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: MyUnionNullable::MyRecordB(MyRecordB { a: 27 }),
+ schema: &schema(),
+ expected_avro: &record_b_variant_expected_avro(27),
+ })
+ }
+
+ #[test]
+ fn deserialize_null_variant_enum_my_record_b_27() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: MyUnionNullable::MyRecordB(MyRecordB { a: 27 }),
+ schema: &schema(),
+ expected_avro: &record_b_variant_expected_avro(27),
+ })
+ }
+
+ #[test]
+ fn serialize_rusty_my_record_b_27() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: Some(MyUnionAvroJsonEncoding::MyRecordB(MyRecordB { a: 27
})),
+ schema: &schema(),
+ expected_avro: &record_b_variant_expected_avro(27),
+ })
+ }
+
+ #[test]
+ fn deserialize_rusty_my_record_b_27() -> AvroResult<()> {
+ test_deserialize(TestCase {
+ input: Some(MyUnionAvroJsonEncoding::MyRecordB(MyRecordB { a: 27
})),
+ schema: &schema(),
+ expected_avro: &record_b_variant_expected_avro(27),
+ })
+ }
+
+ #[test]
+ fn serialize_rusty_untagged_my_record_b_27() -> AvroResult<()> {
+ test_serialize(TestCase {
+ input: Some(MyUnionUntagged::MyRecordB(MyRecordB { a: 27 })),
+ schema: &schema(),
+ expected_avro: &record_b_variant_expected_avro(27),
+ })
+ }
+
+ #[test]
+ #[ignore]
Review Comment:
Please add a comment why it is ignored
##########
avro/src/serde/ser_schema.rs:
##########
@@ -1835,17 +1878,17 @@ impl<'s, W: Write> SchemaAwareWriteSerializer<'s, W> {
match schema {
Schema::Union(union_schema) => {
- let variant_schema = union_schema
- .schemas
- .get(variant_index as usize)
- .ok_or_else(|| {
- create_error(format!(
- "Cannot find variant at position {variant_index}
in {union_schema:?}"
- ))
- })?;
+ let branch_index = variant_index as usize +
usize::from(self.serializing_some);
+ if branch_index >= union_schema.schemas.len() {
+ return Err(create_error(format!(
+ "Variant index out of bounds: {}. The union schema has
'{}' schemas",
+ variant_index,
Review Comment:
```suggestion
branch_index,
```
##########
avro/src/serde/ser_schema.rs:
##########
@@ -629,6 +630,22 @@ impl<'s, W: Write> SchemaAwareWriteSerializer<'s, W> {
root_schema: schema,
names,
enclosing_namespace,
+ serializing_some: false,
+ }
+ }
+
+ pub fn new_serializing_some(
Review Comment:
```suggestion
fn new_serializing_some(
```
##########
avro/src/serde/ser_schema.rs:
##########
@@ -1703,17 +1742,21 @@ impl<'s, W: Write> SchemaAwareWriteSerializer<'s, W> {
match schema {
Schema::Union(union_schema) => {
- let variant_schema = union_schema
- .schemas
- .get(variant_index as usize)
- .ok_or_else(|| {
- create_error(format!(
- "Cannot find a variant at position {variant_index}
in {union_schema:?}"
- ))
- })?;
+ let branch_index = variant_index as usize +
usize::from(self.serializing_some);
+ if branch_index >= union_schema.schemas.len() {
+ return Err(create_error(format!(
+ "Variant index out of bounds: {}. The union schema has
'{}' schemas",
+ variant_index,
Review Comment:
```suggestion
branch_index,
```
##########
avro/src/serde/ser_schema.rs:
##########
@@ -1524,17 +1559,21 @@ impl<'s, W: Write> SchemaAwareWriteSerializer<'s, W> {
match schema {
Schema::Union(union_schema) => {
- let variant_schema = union_schema
- .schemas
- .get(variant_index as usize)
- .ok_or_else(|| {
- create_error(format!(
- "No variant schema at position {variant_index} for
{union_schema:?}"
- ))
- })?;
+ let branch_index = variant_index as usize +
usize::from(self.serializing_some);
+ if branch_index >= union_schema.schemas.len() {
+ return Err(create_error(format!(
+ "Variant index out of bounds: {}. The union schema has
'{}' schemas",
+ variant_index,
Review Comment:
```suggestion
branch_index,
```
##########
avro/src/serde/de.rs:
##########
@@ -75,7 +76,17 @@ struct UnionDeserializer<'de> {
impl<'de> Deserializer<'de> {
pub fn new(input: &'de Value) -> Self {
- Deserializer { input }
+ Deserializer {
+ input,
+ deserializing_some: false,
+ }
+ }
+
+ pub fn new_deserializing_some(input: &'de Value) -> Self {
Review Comment:
```suggestion
fn new_deserializing_some(input: &'de Value) -> Self {
```
##########
avro/src/serde/ser_schema.rs:
##########
@@ -1460,19 +1477,37 @@ impl<'s, W: Write> SchemaAwareWriteSerializer<'s, W> {
encode_int(variant_index as i32, &mut self.writer)
}
Schema::Union(union_schema) => {
- if variant_index as usize >= union_schema.schemas.len() {
+ // If we came here from a some, we need to check if we are
serializing a
+ // non-newtype enum
+ if self.serializing_some {
+ for (i, variant_schema) in
union_schema.schemas.iter().enumerate() {
+ match variant_schema {
+ Schema::Enum(enum_schema) if enum_schema.name.name
== name => {
+ if variant_index as usize >=
enum_schema.symbols.len() {
+ return Err(create_error(format!(
+ "Variant index out of bounds: {}. The
Enum schema has '{}' symbols",
+ variant_index,
+ enum_schema.symbols.len()
+ )));
+ }
+ encode_int(i as i32, &mut self.writer)?;
+ return encode_int(variant_index as i32, &mut
self.writer);
+ }
Review Comment:
This should probably support Schema::Ref too, to refer to Schema::Enum
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]