// Licensed to the Apache Software Foundation (ASF) under one // or more contributor license agreements. See the NOTICE file // distributed with this work for additional information // regarding copyright ownership. The ASF licenses this file // to you under the Apache License, Version 2.0 (the // "License"); you may not use this file except in compliance // with the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, // software distributed under the License is distributed on an // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY // KIND, either express or implied. See the License for the // specific language governing permissions and limitations // under the License. use apache_avro::{ from_value, schema::{derive::AvroSchemaComponent, AvroSchema}, Reader, Schema, Writer, }; use apache_avro_derive::*; use proptest::prelude::*; use serde::{de::DeserializeOwned, ser::Serialize}; use std::collections::HashMap; #[macro_use] extern crate serde; #[cfg(test)] mod test_derive { use apache_avro::schema::{Alias, EnumSchema, RecordSchema}; use std::{borrow::Cow, sync::Mutex}; use super::*; /// Takes in a type that implements the right combination of traits and runs it through a Serde Cycle and asserts the result is the same fn serde_assert(obj: T) where T: std::fmt::Debug + Serialize + DeserializeOwned + AvroSchema + Clone + PartialEq, { assert_eq!(obj, serde(obj.clone())); } fn serde(obj: T) -> T where T: Serialize + DeserializeOwned + AvroSchema, { de(ser(obj)) } fn ser(obj: T) -> Vec where T: Serialize + AvroSchema, { let schema = T::get_schema(); let mut writer = Writer::new(&schema, Vec::new()); if let Err(e) = writer.append_ser(obj) { panic!("{e:?}"); } writer.into_inner().unwrap() } fn de(encoded: Vec) -> T where T: DeserializeOwned + AvroSchema, { assert!(!encoded.is_empty()); let schema = T::get_schema(); let mut reader = Reader::with_schema(&schema, &encoded[..]).unwrap(); if let Some(res) = reader.next() { match res { Ok(value) => { return from_value::(&value).unwrap(); } Err(e) => panic!("{e:?}"), } } unreachable!() } #[derive(Debug, Serialize, Deserialize, AvroSchema, Clone, PartialEq, Eq)] struct TestBasic { a: i32, b: String, } proptest! { #[test] fn test_smoke_test(a: i32, b: String) { let schema = r#" { "type":"record", "name":"TestBasic", "fields":[ { "name":"a", "type":"int" }, { "name":"b", "type":"string" } ] } "#; let schema = Schema::parse_str(schema).unwrap(); assert_eq!(schema, TestBasic::get_schema()); let test = TestBasic { a, b, }; serde_assert(test); }} #[derive(Debug, Serialize, Deserialize, AvroSchema, Clone, PartialEq, Eq)] #[avro(namespace = "com.testing.namespace")] struct TestBasicNamespace { a: i32, b: String, } #[test] fn test_basic_namespace() { let schema = r#" { "type":"record", "name":"com.testing.namespace.TestBasicNamespace", "fields":[ { "name":"a", "type":"int" }, { "name":"b", "type":"string" } ] } "#; let schema = Schema::parse_str(schema).unwrap(); assert_eq!(schema, TestBasicNamespace::get_schema()); if let Schema::Record(RecordSchema { name, .. }) = TestBasicNamespace::get_schema() { assert_eq!("com.testing.namespace".to_owned(), name.namespace.unwrap()) } else { panic!("TestBasicNamespace schema must be a record schema") } } #[derive(Debug, Serialize, Deserialize, AvroSchema, Clone, PartialEq, Eq)] #[avro(namespace = "com.testing.complex.namespace")] struct TestComplexNamespace { a: TestBasicNamespace, b: String, } #[test] fn test_complex_namespace() { let schema = r#" { "type":"record", "name":"com.testing.complex.namespace.TestComplexNamespace", "fields":[ { "name":"a", "type":{ "type":"record", "name":"com.testing.namespace.TestBasicNamespace", "fields":[ { "name":"a", "type":"int" }, { "name":"b", "type":"string" } ] } }, { "name":"b", "type":"string" } ] } "#; let schema = Schema::parse_str(schema).unwrap(); assert_eq!(schema, TestComplexNamespace::get_schema()); if let Schema::Record(RecordSchema { name, fields, .. }) = TestComplexNamespace::get_schema() { assert_eq!( "com.testing.complex.namespace".to_owned(), name.namespace.unwrap() ); let inner_schema = fields .iter() .filter(|field| field.name == "a") .map(|field| &field.schema) .next(); if let Some(Schema::Record(RecordSchema { name, .. })) = inner_schema { assert_eq!( "com.testing.namespace".to_owned(), name.namespace.clone().unwrap() ) } else { panic!("Field 'a' must have a record schema") } } else { panic!("TestComplexNamespace schema must be a record schema") } } #[derive(Debug, Serialize, Deserialize, AvroSchema, Clone, PartialEq)] struct TestAllSupportedBaseTypes { //Basics test a: bool, b: i8, c: i16, d: i32, e: u8, f: u16, g: i64, h: f32, i: f64, j: String, } proptest! { #[test] fn test_basic_types(a: bool, b: i8, c: i16, d: i32, e: u8, f: u16, g: i64, h: f32, i: f64, j: String) { let schema = r#" { "type":"record", "name":"TestAllSupportedBaseTypes", "fields":[ { "name":"a", "type": "boolean" }, { "name":"b", "type":"int" }, { "name":"c", "type":"int" }, { "name":"d", "type":"int" }, { "name":"e", "type":"int" }, { "name":"f", "type":"int" }, { "name":"g", "type":"long" }, { "name":"h", "type":"float" }, { "name":"i", "type":"double" }, { "name":"j", "type":"string" } ] } "#; let schema = Schema::parse_str(schema).unwrap(); assert_eq!(schema, TestAllSupportedBaseTypes::get_schema()); let all_basic = TestAllSupportedBaseTypes { a, b, c, d, e, f, g, h, i, j, }; serde_assert(all_basic); }} #[derive(Debug, Serialize, Deserialize, AvroSchema, Clone, PartialEq)] struct TestNested { a: i32, b: TestAllSupportedBaseTypes, } proptest! { #[test] fn test_inner_struct(a: bool, b: i8, c: i16, d: i32, e: u8, f: u16, g: i64, h: f32, i: f64, j: String, aa: i32) { let schema = r#" { "type":"record", "name":"TestNested", "fields":[ { "name":"a", "type":"int" }, { "name":"b", "type":{ "type":"record", "name":"TestAllSupportedBaseTypes", "fields":[ { "name":"a", "type": "boolean" }, { "name":"b", "type":"int" }, { "name":"c", "type":"int" }, { "name":"d", "type":"int" }, { "name":"e", "type":"int" }, { "name":"f", "type":"int" }, { "name":"g", "type":"long" }, { "name":"h", "type":"float" }, { "name":"i", "type":"double" }, { "name":"j", "type":"string" } ] } } ] } "#; let schema = Schema::parse_str(schema).unwrap(); assert_eq!(schema, TestNested::get_schema()); let all_basic = TestAllSupportedBaseTypes { a, b, c, d, e, f, g, h, i, j, }; let inner_struct = TestNested { a: aa, b: all_basic, }; serde_assert(inner_struct); }} #[derive(Debug, Serialize, Deserialize, AvroSchema, Clone, PartialEq, Eq)] struct TestOptional { a: Option, } proptest! { #[test] fn test_optional_field_some(a: i32) { let schema = r#" { "type":"record", "name":"TestOptional", "fields":[ { "name":"a", "type":["null","int"] } ] } "#; let schema = Schema::parse_str(schema).unwrap(); assert_eq!(schema, TestOptional::get_schema()); let optional_field = TestOptional { a: Some(a) }; serde_assert(optional_field); }} #[test] fn test_optional_field_none() { let optional_field = TestOptional { a: None }; serde_assert(optional_field); } /// Generic Containers #[derive(Debug, Serialize, Deserialize, AvroSchema, Clone, PartialEq)] struct TestGeneric { a: String, b: Vec, c: HashMap, } proptest! { #[test] fn test_generic_container_1(a: String, b: Vec, c: HashMap) { let schema = r#" { "type":"record", "name":"TestGeneric", "fields":[ { "name":"a", "type":"string" }, { "name":"b", "type": { "type":"array", "items":"int" } }, { "name":"c", "type": { "type":"map", "values":"int" } } ] } "#; let schema = Schema::parse_str(schema).unwrap(); assert_eq!(schema, TestGeneric::::get_schema()); let test_generic = TestGeneric:: { a, b, c, }; serde_assert(test_generic); }} proptest! { #[test] fn test_generic_container_2(a: bool, b: i8, c: i16, d: i32, e: u8, f: u16, g: i64, h: f32, i: f64, j: String) { let schema = r#" { "type":"record", "name":"TestGeneric", "fields":[ { "name":"a", "type":"string" }, { "name":"b", "type": { "type":"array", "items":{ "type":"record", "name":"TestAllSupportedBaseTypes", "fields":[ { "name":"a", "type": "boolean" }, { "name":"b", "type":"int" }, { "name":"c", "type":"int" }, { "name":"d", "type":"int" }, { "name":"e", "type":"int" }, { "name":"f", "type":"int" }, { "name":"g", "type":"long" }, { "name":"h", "type":"float" }, { "name":"i", "type":"double" }, { "name":"j", "type":"string" } ] } } }, { "name":"c", "type": { "type":"map", "values":"TestAllSupportedBaseTypes" } } ] } "#; let schema = Schema::parse_str(schema).unwrap(); assert_eq!( schema, TestGeneric::::get_schema() ); let test_generic = TestGeneric:: { a: "testing".to_owned(), b: vec![TestAllSupportedBaseTypes { a, b, c, d, e, f, g, h, i, j: j.clone(), }], c: vec![( "key".to_owned(), TestAllSupportedBaseTypes { a, b, c, d, e, f, g, h, i, j, }, )] .into_iter() .collect(), }; serde_assert(test_generic); }} #[derive(Debug, Serialize, Deserialize, AvroSchema, Clone, PartialEq, Eq)] enum TestAllowedEnum { A, B, C, D, } #[derive(Debug, Serialize, Deserialize, AvroSchema, Clone, PartialEq, Eq)] struct TestAllowedEnumNested { a: TestAllowedEnum, b: String, } #[test] fn test_enum() { let schema = r#" { "type":"record", "name":"TestAllowedEnumNested", "fields":[ { "name":"a", "type": { "type":"enum", "name":"TestAllowedEnum", "symbols":["A","B","C","D"] } }, { "name":"b", "type":"string" } ] } "#; let schema = Schema::parse_str(schema).unwrap(); assert_eq!(schema, TestAllowedEnumNested::get_schema()); let enum_included = TestAllowedEnumNested { a: TestAllowedEnum::B, b: "hey".to_owned(), }; serde_assert(enum_included); } #[derive(Debug, Serialize, Deserialize, AvroSchema, Clone, PartialEq)] struct ConsList { value: i32, next: Option>, } #[test] fn test_cons() { let schema = r#" { "type":"record", "name":"ConsList", "fields":[ { "name":"value", "type":"int" }, { "name":"next", "type":["null","ConsList"] } ] } "#; let schema = Schema::parse_str(schema).unwrap(); assert_eq!(schema, ConsList::get_schema()); let list = ConsList { value: 34, next: Some(Box::new(ConsList { value: 42, next: None, })), }; serde_assert(list) } #[derive(Debug, Serialize, Deserialize, AvroSchema, Clone, PartialEq)] struct ConsListGeneric { value: T, next: Option>>, } #[test] fn test_cons_generic() { let schema = r#" { "type":"record", "name":"ConsListGeneric", "fields":[ { "name":"value", "type":{ "type":"record", "name":"TestAllowedEnumNested", "fields":[ { "name":"a", "type": { "type":"enum", "name":"TestAllowedEnum", "symbols":["A","B","C","D"] } }, { "name":"b", "type":"string" } ] } }, { "name":"next", "type":["null","ConsListGeneric"] } ] } "#; let schema = Schema::parse_str(schema).unwrap(); assert_eq!( schema, ConsListGeneric::::get_schema() ); let list = ConsListGeneric:: { value: TestAllowedEnumNested { a: TestAllowedEnum::B, b: "testing".into(), }, next: Some(Box::new(ConsListGeneric:: { value: TestAllowedEnumNested { a: TestAllowedEnum::D, b: "testing2".into(), }, next: None, })), }; serde_assert(list) } #[derive(Debug, Serialize, Deserialize, AvroSchema, Clone, PartialEq, Eq)] struct TestSimpleArray { a: [i32; 4], } proptest! { #[test] fn test_simple_array(a: [i32; 4]) { let schema = r#" { "type":"record", "name":"TestSimpleArray", "fields":[ { "name":"a", "type": { "type":"array", "items":"int" } } ] } "#; let schema = Schema::parse_str(schema).unwrap(); assert_eq!(schema, TestSimpleArray::get_schema()); let test = TestSimpleArray { a }; serde_assert(test) }} #[derive(Debug, Serialize, Deserialize, AvroSchema, Clone, PartialEq)] struct TestComplexArray { a: [T; 2], } #[test] fn test_complex_array() { let schema = r#" { "type":"record", "name":"TestComplexArray", "fields":[ { "name":"a", "type": { "type":"array", "items":{ "type":"record", "name":"TestBasic", "fields":[ { "name":"a", "type":"int" }, { "name":"b", "type":"string" } ] } } } ] } "#; let schema = Schema::parse_str(schema).unwrap(); assert_eq!(schema, TestComplexArray::::get_schema()); let test = TestComplexArray:: { a: [ TestBasic { a: 27, b: "foo".to_owned(), }, TestBasic { a: 28, b: "bar".to_owned(), }, ], }; serde_assert(test) } #[derive(Debug, Serialize, Deserialize, AvroSchema, Clone, PartialEq, Eq)] struct Testu8 { a: Vec, b: [u8; 2], } proptest! { #[test] fn test_bytes_handled(a: Vec, b: [u8; 2]) { let test = Testu8 { a, b, }; serde_assert(test) // don't check for schema equality to allow for transitioning to bytes or fixed types in the future }} #[derive(Debug, Serialize, Deserialize, AvroSchema)] struct TestSmartPointers<'a> { a: String, b: Mutex>, c: Cow<'a, i32>, } #[test] fn test_smart_pointers() { let schema = r#" { "type":"record", "name":"TestSmartPointers", "fields":[ { "name":"a", "type": "string" }, { "name":"b", "type":{ "type":"array", "items":"long" } }, { "name":"c", "type":"int" } ] } "#; let schema = Schema::parse_str(schema).unwrap(); assert_eq!(schema, TestSmartPointers::get_schema()); let test = TestSmartPointers { a: "hey".into(), b: Mutex::new(vec![42]), c: Cow::Owned(32), }; // test serde with manual equality for mutex let test = serde(test); assert_eq!("hey", test.a); assert_eq!(vec![42], *test.b.lock().unwrap()); assert_eq!(Cow::Owned::(32), test.c); } #[derive(Debug, Serialize, AvroSchema, Clone, PartialEq)] struct TestReference<'a> { a: &'a Vec, b: &'static str, c: &'a f64, } proptest! { #[test] fn test_reference_struct(a: Vec, c: f64) { let schema = r#" { "type":"record", "name":"TestReference", "fields":[ { "name":"a", "type": { "type":"array", "items":"int" } }, { "name":"b", "type":"string" }, { "name":"c", "type":"double" } ] } "#; let schema = Schema::parse_str(schema).unwrap(); assert_eq!(schema, TestReference::get_schema()); // let a = vec![34]; // let c = 4.55555555_f64; let test = TestReference { a: &a, b: "testing_static", c: &c, }; ser(test); }} #[derive(Debug, Serialize, Deserialize, AvroSchema, Clone, PartialEq, Eq)] #[avro(namespace = "com.testing.namespace", doc = "A Documented Record")] struct TestBasicWithAttributes { #[avro(doc = "Milliseconds since Queen released Bohemian Rhapsody")] a: i32, #[avro(doc = "Full lyrics of Bohemian Rhapsody")] b: String, } #[test] fn test_basic_with_attributes() { let schema = r#" { "type":"record", "name":"com.testing.namespace.TestBasicWithAttributes", "doc":"A Documented Record", "fields":[ { "name":"a", "type":"int", "doc":"Milliseconds since Queen released Bohemian Rhapsody" }, { "name":"b", "type": "string", "doc": "Full lyrics of Bohemian Rhapsody" } ] } "#; let schema = Schema::parse_str(schema).unwrap(); if let Schema::Record(RecordSchema { name, doc, .. }) = TestBasicWithAttributes::get_schema() { assert_eq!("com.testing.namespace".to_owned(), name.namespace.unwrap()); assert_eq!("A Documented Record", doc.unwrap()) } else { panic!("TestBasicWithAttributes schema must be a record schema") } assert_eq!(schema, TestBasicWithAttributes::get_schema()); } #[derive(Debug, Serialize, Deserialize, AvroSchema, Clone, PartialEq, Eq)] #[avro(namespace = "com.testing.namespace")] /// A Documented Record struct TestBasicWithOuterDocAttributes { #[avro(doc = "Milliseconds since Queen released Bohemian Rhapsody")] a: i32, #[avro(doc = "Full lyrics of Bohemian Rhapsody")] b: String, } #[test] fn test_basic_with_out_doc_attributes() { let schema = r#" { "type":"record", "name":"com.testing.namespace.TestBasicWithOuterDocAttributes", "doc":"A Documented Record", "fields":[ { "name":"a", "type":"int", "doc":"Milliseconds since Queen released Bohemian Rhapsody" }, { "name":"b", "type": "string", "doc": "Full lyrics of Bohemian Rhapsody" } ] } "#; let schema = Schema::parse_str(schema).unwrap(); let derived_schema = TestBasicWithOuterDocAttributes::get_schema(); assert_eq!(&schema, &derived_schema); if let Schema::Record(RecordSchema { name, doc, .. }) = derived_schema { assert_eq!("com.testing.namespace".to_owned(), name.namespace.unwrap()); assert_eq!("A Documented Record", doc.unwrap()) } else { panic!("TestBasicWithOuterDocAttributes schema must be a record schema") } } #[derive(Debug, Serialize, Deserialize, AvroSchema, Clone, PartialEq, Eq)] #[avro(namespace = "com.testing.namespace")] /// A Documented Record /// that spans /// multiple lines struct TestBasicWithLargeDoc { #[avro(doc = "Milliseconds since Queen released Bohemian Rhapsody")] a: i32, #[avro(doc = "Full lyrics of Bohemian Rhapsody")] b: String, } #[test] fn test_basic_with_large_doc() { let schema = r#" { "type":"record", "name":"com.testing.namespace.TestBasicWithLargeDoc", "doc":"A Documented Record", "fields":[ { "name":"a", "type":"int", "doc":"Milliseconds since Queen released Bohemian Rhapsody" }, { "name":"b", "type": "string", "doc": "Full lyrics of Bohemian Rhapsody" } ] } "#; let schema = Schema::parse_str(schema).unwrap(); if let Schema::Record(RecordSchema { name, doc, .. }) = TestBasicWithLargeDoc::get_schema() { assert_eq!("com.testing.namespace".to_owned(), name.namespace.unwrap()); assert_eq!( "A Documented Record\nthat spans\nmultiple lines", doc.unwrap() ) } else { panic!("TestBasicWithLargeDoc schema must be a record schema") } assert_eq!(schema, TestBasicWithLargeDoc::get_schema()); } #[derive(Debug, Serialize, Deserialize, AvroSchema, Clone, PartialEq, Eq)] struct TestBasicWithBool { a: bool, b: Option, } proptest! { #[test] fn avro_3634_test_basic_with_bool(a in any::(), b in any::>()) { let schema = r#" { "type":"record", "name":"TestBasicWithBool", "fields":[ { "name":"a", "type":"boolean" }, { "name":"b", "type":["null","boolean"] } ] } "#; let schema = Schema::parse_str(schema).unwrap(); let derived_schema = TestBasicWithBool::get_schema(); if let Schema::Record(RecordSchema { name, .. }) = derived_schema { assert_eq!("TestBasicWithBool", name.fullname(None)) } else { panic!("TestBasicWithBool schema must be a record schema") } assert_eq!(schema, TestBasicWithBool::get_schema()); serde_assert(TestBasicWithBool { a, b }); }} #[derive(Debug, Serialize, Deserialize, AvroSchema, Clone, PartialEq, Eq)] struct TestBasicWithU32 { a: u32, } proptest! { #[test] fn test_basic_with_u32(a in any::()) { let schema = r#" { "type":"record", "name":"TestBasicWithU32", "fields":[ { "name":"a", "type":"long" } ] } "#; let schema = Schema::parse_str(schema).unwrap(); if let Schema::Record(RecordSchema { name, .. }) = TestBasicWithU32::get_schema() { assert_eq!("TestBasicWithU32", name.fullname(None)) } else { panic!("TestBasicWithU32 schema must be a record schema") } assert_eq!(schema, TestBasicWithU32::get_schema()); serde_assert(TestBasicWithU32 { a }); }} #[derive(Debug, Serialize, Deserialize, AvroSchema, Clone, PartialEq, Eq)] #[avro(alias = "a", alias = "b", alias = "c")] struct TestBasicStructWithAliases { a: i32, } #[test] fn test_basic_struct_with_aliases() { let schema = r#" { "type":"record", "name":"TestBasicStructWithAliases", "aliases":["a", "b", "c"], "fields":[ { "name":"a", "type":"int" } ] } "#; let schema = Schema::parse_str(schema).unwrap(); if let Schema::Record(RecordSchema { name, aliases, .. }) = TestBasicStructWithAliases::get_schema() { assert_eq!("TestBasicStructWithAliases", name.fullname(None)); assert_eq!( Some(vec![ Alias::new("a").unwrap(), Alias::new("b").unwrap(), Alias::new("c").unwrap() ]), aliases ); } else { panic!("TestBasicStructWithAliases schema must be a record schema") } assert_eq!(schema, TestBasicStructWithAliases::get_schema()); serde_assert(TestBasicStructWithAliases { a: i32::MAX }); } #[derive(Debug, Serialize, Deserialize, AvroSchema, Clone, PartialEq, Eq)] #[avro(alias = "d")] #[avro(alias = "e")] #[avro(alias = "f")] struct TestBasicStructWithAliases2 { a: i32, } #[test] fn test_basic_struct_with_aliases2() { let schema = r#" { "type":"record", "name":"TestBasicStructWithAliases2", "aliases":["d", "e", "f"], "fields":[ { "name":"a", "type":"int" } ] } "#; let schema = Schema::parse_str(schema).unwrap(); if let Schema::Record(RecordSchema { name, aliases, .. }) = TestBasicStructWithAliases2::get_schema() { assert_eq!("TestBasicStructWithAliases2", name.fullname(None)); assert_eq!( Some(vec![ Alias::new("d").unwrap(), Alias::new("e").unwrap(), Alias::new("f").unwrap() ]), aliases ); } else { panic!("TestBasicStructWithAliases2 schema must be a record schema") } assert_eq!(schema, TestBasicStructWithAliases2::get_schema()); serde_assert(TestBasicStructWithAliases2 { a: i32::MAX }); } #[derive(Debug, Serialize, Deserialize, AvroSchema, Clone, PartialEq, Eq)] #[avro(alias = "a", alias = "b", alias = "c")] enum TestBasicEnumWithAliases { A, B, } #[test] fn test_basic_enum_with_aliases() { let schema = r#" { "type":"enum", "name":"TestBasicEnumWithAliases", "aliases":["a", "b", "c"], "symbols":[ "A", "B" ] } "#; let schema = Schema::parse_str(schema).unwrap(); if let Schema::Enum(EnumSchema { name, aliases, .. }) = TestBasicEnumWithAliases::get_schema() { assert_eq!("TestBasicEnumWithAliases", name.fullname(None)); assert_eq!( Some(vec![ Alias::new("a").unwrap(), Alias::new("b").unwrap(), Alias::new("c").unwrap() ]), aliases ); } else { panic!("TestBasicEnumWithAliases schema must be an enum schema") } assert_eq!(schema, TestBasicEnumWithAliases::get_schema()); serde_assert(TestBasicEnumWithAliases::A); } #[derive(Debug, Serialize, Deserialize, AvroSchema, Clone, PartialEq, Eq)] #[avro(alias = "d")] #[avro(alias = "e")] #[avro(alias = "f")] enum TestBasicEnumWithAliases2 { A, B, } #[test] fn test_basic_enum_with_aliases2() { let schema = r#" { "type":"enum", "name":"TestBasicEnumWithAliases2", "aliases":["d", "e", "f"], "symbols":[ "A", "B" ] } "#; let schema = Schema::parse_str(schema).unwrap(); if let Schema::Enum(EnumSchema { name, aliases, .. }) = TestBasicEnumWithAliases2::get_schema() { assert_eq!("TestBasicEnumWithAliases2", name.fullname(None)); assert_eq!( Some(vec![ Alias::new("d").unwrap(), Alias::new("e").unwrap(), Alias::new("f").unwrap() ]), aliases ); } else { panic!("TestBasicEnumWithAliases2 schema must be an enum schema") } assert_eq!(schema, TestBasicEnumWithAliases2::get_schema()); serde_assert(TestBasicEnumWithAliases2::B); } #[test] fn test_basic_struct_with_defaults() { #[derive(Debug, Deserialize, Serialize, AvroSchema, Clone, PartialEq, Eq)] enum MyEnum { Foo, Bar, Baz, } #[derive(Debug, Serialize, Deserialize, AvroSchema, Clone, PartialEq)] struct TestBasicStructWithDefaultValues { #[avro(default = "123")] a: i32, #[avro(default = r#""The default value for 'b'""#)] b: String, #[avro(default = "true")] condition: bool, // no default value for 'c' c: f64, #[avro(default = r#"{"a": 1, "b": 2}"#)] map: HashMap, #[avro(default = "[1, 2, 3]")] array: Vec, #[avro(default = r#""Foo""#)] myenum: MyEnum, } let schema = r#" { "type":"record", "name":"TestBasicStructWithDefaultValues", "fields": [ { "name":"a", "type":"int", "default":123 }, { "name":"b", "type":"string", "default": "The default value for 'b'" }, { "name":"condition", "type":"boolean", "default":true }, { "name":"c", "type":"double" }, { "name":"map", "type":{ "type":"map", "values":"int" }, "default": { "a": 1, "b": 2 } }, { "name":"array", "type":{ "type":"array", "items":"int" }, "default": [1, 2, 3] }, { "name":"myenum", "type":{ "type":"enum", "name":"MyEnum", "symbols":["Foo", "Bar", "Baz"] }, "default":"Foo" } ] } "#; let schema = Schema::parse_str(schema).unwrap(); if let Schema::Record(RecordSchema { name, fields, .. }) = TestBasicStructWithDefaultValues::get_schema() { assert_eq!("TestBasicStructWithDefaultValues", name.fullname(None)); use serde_json::json; for field in fields { match field.name.as_str() { "a" => assert_eq!(Some(json!(123_i32)), field.default), "b" => assert_eq!( Some(json!(r#"The default value for 'b'"#.to_owned())), field.default ), "condition" => assert_eq!(Some(json!(true)), field.default), "array" => assert_eq!(Some(json!([1, 2, 3])), field.default), "map" => assert_eq!( Some(json!({ "a": 1, "b": 2 })), field.default ), "c" => assert_eq!(None, field.default), "myenum" => assert_eq!(Some(json!("Foo")), field.default), _ => panic!("Unexpected field name"), } } } else { panic!("TestBasicStructWithDefaultValues schema must be a record schema") } assert_eq!(schema, TestBasicStructWithDefaultValues::get_schema()); serde_assert(TestBasicStructWithDefaultValues { a: 321, b: "A custom value for 'b'".to_owned(), condition: false, c: 987.654, map: [("a".to_owned(), 1), ("b".to_owned(), 2)] .iter() .cloned() .collect(), array: vec![4, 5, 6], myenum: MyEnum::Bar, }); } #[test] fn avro_3633_test_basic_struct_with_skip_attribute() { // Note: If using the skip attribute together with serialization, // the serde's skip attribute needs also to be added #[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)] struct TestBasicStructNoSchema { field: bool, } #[derive(Debug, Serialize, Deserialize, AvroSchema, Clone, PartialEq)] struct TestBasicStructWithSkipAttribute { #[avro(skip)] #[serde(skip)] condition: bool, #[avro(skip = false)] a: f64, #[avro(skip)] #[serde(skip)] map: HashMap, array: Vec, #[avro(skip = true)] #[serde(skip)] mystruct: TestBasicStructNoSchema, b: i32, } let schema = r#" { "type":"record", "name":"TestBasicStructWithSkipAttribute", "fields": [ { "name":"a", "type":"double" }, { "name":"array", "type":{ "type":"array", "items":"int" } }, { "name":"b", "type":"int" } ] } "#; let schema = Schema::parse_str(schema).unwrap(); let derived_schema = TestBasicStructWithSkipAttribute::get_schema(); if let Schema::Record(RecordSchema { name, fields, .. }) = &derived_schema { assert_eq!("TestBasicStructWithSkipAttribute", name.fullname(None)); for field in fields { match field.name.as_str() { "condition" => panic!("Unexpected field 'condition'"), "mystruct" => panic!("Unexpected field 'mystruct'"), "map" => panic!("Unexpected field 'map'"), _ => {} } } } else { panic!( "TestBasicStructWithSkipAttribute schema must be a record schema: {derived_schema:?}" ) } assert_eq!(schema, derived_schema); // Note: If serde's `skip` attribute is used on a field, the field's type // needs the trait 'Default' to be implemented, since it is skipping the serialization process. // Copied or cloned objects within 'serde_assert()' doesn't "copy" (serialize/deserialze) // these fields, so no values are initialized here for skipped fields. serde_assert(TestBasicStructWithSkipAttribute { condition: bool::default(), // <- skipped a: 987.654, map: HashMap::default(), // <- skipped array: vec![4, 5, 6], mystruct: TestBasicStructNoSchema::default(), // <- skipped b: 321, }); } #[test] fn avro_3633_test_basic_struct_with_rename_attribute() { #[derive(Debug, Serialize, Deserialize, AvroSchema, Clone, PartialEq)] struct TestBasicStructWithRenameAttribute { #[avro(rename = "a1")] #[serde(rename = "a1")] a: bool, b: i32, #[avro(rename = "c1")] #[serde(rename = "c1")] c: f32, } let schema = r#" { "type":"record", "name":"TestBasicStructWithRenameAttribute", "fields": [ { "name":"a1", "type":"boolean" }, { "name":"b", "type":"int" }, { "name":"c1", "type":"float" } ] } "#; let schema = Schema::parse_str(schema).unwrap(); let derived_schema = TestBasicStructWithRenameAttribute::get_schema(); if let Schema::Record(RecordSchema { name, fields, .. }) = &derived_schema { assert_eq!("TestBasicStructWithRenameAttribute", name.fullname(None)); for field in fields { match field.name.as_str() { "a" => panic!("Unexpected field name 'a': must be 'a1'"), "c" => panic!("Unexpected field name 'c': must be 'c1'"), _ => {} } } } else { panic!( "TestBasicStructWithRenameAttribute schema must be a record schema: {derived_schema:?}" ) } assert_eq!(schema, derived_schema); serde_assert(TestBasicStructWithRenameAttribute { a: true, b: 321, c: 987.654, }); } #[test] fn test_avro_3663_raw_identifier_field_name() { #[derive(Debug, Serialize, Deserialize, AvroSchema, Clone, PartialEq)] struct TestRawIdent { r#type: bool, } let derived_schema = TestRawIdent::get_schema(); if let Schema::Record(RecordSchema { fields, .. }) = derived_schema { let field = fields.first().expect("TestRawIdent must contain a field"); assert_eq!(field.name, "type"); } else { panic!("Unexpected schema type for {derived_schema:?}") } } #[test] fn avro_3962_fields_documentation() { /// Foo docs #[derive(AvroSchema)] #[allow(dead_code)] struct Foo { /// a's Rustdoc a: i32, /// b's Rustdoc #[avro(doc = "attribute doc has priority over Rustdoc")] b: i32, } if let Schema::Record(RecordSchema { fields, .. }) = Foo::get_schema() { assert_eq!(fields[0].doc, Some("a's Rustdoc".to_string())); assert_eq!( fields[1].doc, Some("attribute doc has priority over Rustdoc".to_string()) ); } else { panic!("Unexpected schema type for Foo") } } }