blob: 8d992e0e207db6bcd361fc877c33e8ed61d2aa26 [file] [log] [blame]
// 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::{
schema::{Name, RecordField},
to_avro_datum, to_value,
types::{Record, Value},
Codec, Error, Reader, Schema, Writer,
};
use lazy_static::lazy_static;
use apache_avro_test_helper::init;
const PRIMITIVE_EXAMPLES: &[(&str, bool)] = &[
(r#""null""#, true),
(r#"{"type": "null"}"#, true),
(r#""boolean""#, true),
(r#"{"type": "boolean"}"#, true),
(r#""string""#, true),
(r#"{"type": "string"}"#, true),
(r#""bytes""#, true),
(r#"{"type": "bytes"}"#, true),
(r#""int""#, true),
(r#"{"type": "int"}"#, true),
(r#""long""#, true),
(r#"{"type": "long"}"#, true),
(r#""float""#, true),
(r#"{"type": "float"}"#, true),
(r#""double""#, true),
(r#"{"type": "double"}"#, true),
(r#""true""#, false),
(r#"true"#, false),
(r#"{"no_type": "test"}"#, false),
(r#"{"type": "panther"}"#, false),
];
const FIXED_EXAMPLES: &[(&str, bool)] = &[
(r#"{"type": "fixed", "name": "Test", "size": 1}"#, true),
(
r#"{
"type": "fixed",
"name": "MyFixed",
"namespace": "org.apache.hadoop.avro",
"size": 1
}"#,
true,
),
(r#"{"type": "fixed", "name": "MissingSize"}"#, false),
(r#"{"type": "fixed", "size": 314}"#, false),
];
const ENUM_EXAMPLES: &[(&str, bool)] = &[
(
r#"{"type": "enum", "name": "Test", "symbols": ["A", "B"]}"#,
true,
),
(
r#"{
"type": "enum",
"name": "Status",
"symbols": "Normal Caution Critical"
}"#,
false,
),
(
r#"{
"type": "enum",
"name": [ 0, 1, 1, 2, 3, 5, 8 ],
"symbols": ["Golden", "Mean"]
}"#,
false,
),
(
r#"{
"type": "enum",
"symbols" : ["I", "will", "fail", "no", "name"]
}"#,
false,
),
(
r#"{
"type": "enum",
"name": "Test"
"symbols" : ["AA", "AA"]
}"#,
false,
),
];
const ARRAY_EXAMPLES: &[(&str, bool)] = &[
(r#"{"type": "array", "items": "long"}"#, true),
(
r#"{
"type": "array",
"items": {"type": "enum", "name": "Test", "symbols": ["A", "B"]}
}"#,
true,
),
];
const MAP_EXAMPLES: &[(&str, bool)] = &[
(r#"{"type": "map", "values": "long"}"#, true),
(
r#"{
"type": "map",
"values": {"type": "enum", "name": "Test", "symbols": ["A", "B"]}
}"#,
true,
),
];
const UNION_EXAMPLES: &[(&str, bool)] = &[
(r#"["string", "null", "long"]"#, true),
(r#"["null", "null"]"#, false),
(r#"["long", "long"]"#, false),
(
r#"[
{"type": "array", "items": "long"}
{"type": "array", "items": "string"}
]"#,
false,
),
];
const RECORD_EXAMPLES: &[(&str, bool)] = &[
(
r#"{
"type": "record",
"name": "Test",
"fields": [{"name": "f", "type": "long"}]
}"#,
true,
),
(
r#"{
"type": "error",
"name": "Test",
"fields": [{"name": "f", "type": "long"}]
}"#,
false,
),
(
r#"{
"type": "record",
"name": "Node",
"fields": [
{"name": "label", "type": "string"},
{"name": "children", "type": {"type": "array", "items": "Node"}}
]
}"#,
true,
),
(
r#"{
"type": "record",
"name": "Lisp",
"fields": [
{
"name": "value",
"type": [
"null", "string",
{
"type": "record",
"name": "Cons",
"fields": [
{"name": "car", "type": "Lisp"},
{"name": "cdr", "type": "Lisp"}
]
}
]
}
]
}"#,
true,
),
(
r#"{
"type": "record",
"name": "HandshakeRequest",
"namespace": "org.apache.avro.ipc",
"fields": [
{"name": "clientHash", "type": {"type": "fixed", "name": "MD5", "size": 16}},
{"name": "clientProtocol", "type": ["null", "string"]},
{"name": "serverHash", "type": "MD5"},
{"name": "meta", "type": ["null", {"type": "map", "values": "bytes"}]}
]
}"#,
true,
),
(
r#"{
"type":"record",
"name":"HandshakeResponse",
"namespace":"org.apache.avro.ipc",
"fields":[
{
"name":"match",
"type":{
"type":"enum",
"name":"HandshakeMatch",
"symbols":["BOTH", "CLIENT", "NONE"]
}
},
{"name":"serverProtocol", "type":["null", "string"]},
{
"name":"serverHash",
"type":["null", {"name":"MD5", "size":16, "type":"fixed"}]
},
{
"name":"meta",
"type":["null", {"type":"map", "values":"bytes"}]
}
]
}"#,
true,
),
(
r#"{
"type":"record",
"name":"HandshakeResponse",
"namespace":"org.apache.avro.ipc",
"fields":[
{
"name":"match",
"type":{
"type":"enum",
"name":"HandshakeMatch",
"symbols":["BOTH", "CLIENT", "NONE"]
}
},
{"name":"serverProtocol", "type":["null", "string"]},
{
"name":"serverHash",
"type":["null", { "name":"MD5", "size":16, "type":"fixed"}]
},
{"name":"meta", "type":["null", { "type":"map", "values":"bytes"}]}
]
}"#,
true,
),
// Unions may not contain more than one schema with the same type, except for the named
// types record, fixed and enum. For example, unions containing two array types or two map
// types are not permitted, but two types with different names are permitted.
// (Names permit efficient resolution when reading and writing unions.)
(
r#"{
"type": "record",
"name": "ipAddr",
"fields": [
{
"name": "addr",
"type": [
{"name": "IPv6", "type": "fixed", "size": 16},
{"name": "IPv4", "type": "fixed", "size": 4}
]
}
]
}"#,
true,
),
(
r#"{
"type": "record",
"name": "Address",
"fields": [
{"type": "string"},
{"type": "string", "name": "City"}
]
}"#,
false,
),
(
r#"{
"type": "record",
"name": "Event",
"fields": [{"name": "Sponsor"}, {"name": "City", "type": "string"}]
}"#,
false,
),
(
r#"{
"type": "record",
"fields": "His vision, from the constantly passing bars,"
"name",
"Rainer"
}"#,
false,
),
(
r#"{
"name": ["Tom", "Jerry"],
"type": "record",
"fields": [{"name": "name", "type": "string"}]
}"#,
false,
),
];
const DOC_EXAMPLES: &[(&str, bool)] = &[
(
r#"{
"type": "record",
"name": "TestDoc",
"doc": "Doc string",
"fields": [{"name": "name", "type": "string", "doc" : "Doc String"}]
}"#,
true,
),
(
r#"{"type": "enum", "name": "Test", "symbols": ["A", "B"], "doc": "Doc String"}"#,
true,
),
(
r#"{"type": "fixed", "name": "Test", "size": 1, "doc": "Fixed Doc String"}"#,
true,
),
];
const OTHER_ATTRIBUTES_EXAMPLES: &[(&str, bool)] = &[
(
r#"{
"type": "record",
"name": "TestRecord",
"cp_string": "string",
"cp_int": 1,
"cp_array": [ 1, 2, 3, 4],
"fields": [
{"name": "f1", "type": "string", "cp_object": {"a":1,"b":2}},
{"name": "f2", "type": "long", "cp_null": null}
]
}"#,
true,
),
(
r#"{"type": "map", "values": "long", "cp_boolean": true}"#,
true,
),
(
r#"{
"type": "enum",
"name": "TestEnum",
"symbols": [ "one", "two", "three" ],
"cp_float" : 1.0
}"#,
true,
),
(r#"{"type": "long", "date": "true"}"#, true),
];
const DECIMAL_LOGICAL_TYPE: &[(&str, bool)] = &[
(
r#"{
"type": {
"type": "fixed",
"name": "TestDecimal",
"size": 10
},
"logicalType": "decimal",
"precision": 4,
"scale": 2
}"#,
true,
),
(
r#"{
"type": {
"type": "fixed",
"name": "ScaleIsImplicitlyZero",
"size": 10
},
"logicalType": "decimal",
"precision": 4
}"#,
true,
),
(
r#"{
"type": {
"type": "fixed",
"name": "PrecisionMustBeGreaterThanZero",
"size": 10
},
"logicalType": "decimal",
"precision": 0
}"#,
false,
),
(
r#"{
"type": "bytes",
"logicalType": "decimal",
"precision": 4,
"scale": 2
}"#,
true,
),
(
r#"{
"type": "bytes",
"logicalType": "decimal",
"precision": 2,
"scale": -2
}"#,
false,
),
(
r#"{
"type": "bytes",
"logicalType": "decimal",
"precision": -2,
"scale": 2
}"#,
false,
),
(
r#"{
"type": "bytes",
"logicalType": "decimal",
"precision": 2,
"scale": 3
}"#,
false,
),
(
r#"{
"type": "fixed",
"logicalType": "decimal",
"name": "TestDecimal",
"precision": -10,
"scale": 2,
"size": 5
}"#,
false,
),
(
r#"{
"type": "fixed",
"logicalType": "decimal",
"name": "TestDecimal",
"precision": 2,
"scale": 3,
"size": 2
}"#,
false,
),
(
r#"{
"type": "fixed",
"logicalType": "decimal",
"name": "TestDecimal",
"precision": 2,
"scale": 2,
"size": -2
}"#,
false,
),
];
const DECIMAL_LOGICAL_TYPE_ATTRIBUTES: &[(&str, bool)] = &[
/*
// TODO: (#93) support logical types and attributes and uncomment
(
r#"{
"type": "fixed",
"logicalType": "decimal",
"name": "TestDecimal",
"precision": 4,
"scale": 2,
"size": 2
}"#,
true
),
(
r#"{
"type": "bytes",
"logicalType": "decimal",
"precision": 4
}"#,
true
),
*/
];
const DATE_LOGICAL_TYPE: &[(&str, bool)] = &[
(r#"{"type": "int", "logicalType": "date"}"#, true),
// this is valid even though its logical type is "date1", because unknown logical types are
// ignored
(r#"{"type": "int", "logicalType": "date1"}"#, true),
// this is still valid because unknown logicalType should be ignored
(r#"{"type": "long", "logicalType": "date"}"#, true),
];
const TIMEMILLIS_LOGICAL_TYPE: &[(&str, bool)] = &[
(r#"{"type": "int", "logicalType": "time-millis"}"#, true),
// this is valid even though its logical type is "time-milis" (missing the second "l"),
// because unknown logical types are ignored
(r#"{"type": "int", "logicalType": "time-milis"}"#, true),
// this is still valid because unknown logicalType should be ignored
(r#"{"type": "long", "logicalType": "time-millis"}"#, true),
];
const TIMEMICROS_LOGICAL_TYPE: &[(&str, bool)] = &[
(r#"{"type": "long", "logicalType": "time-micros"}"#, true),
// this is valid even though its logical type is "time-micro" (missing the last "s"), because
// unknown logical types are ignored
(r#"{"type": "long", "logicalType": "time-micro"}"#, true),
// this is still valid because unknown logicalType should be ignored
(r#"{"type": "int", "logicalType": "time-micros"}"#, true),
];
const TIMESTAMPMILLIS_LOGICAL_TYPE: &[(&str, bool)] = &[
(
r#"{"type": "long", "logicalType": "timestamp-millis"}"#,
true,
),
// this is valid even though its logical type is "timestamp-milis" (missing the second "l"), because
// unknown logical types are ignored
(
r#"{"type": "long", "logicalType": "timestamp-milis"}"#,
true,
),
(
// this is still valid because unknown logicalType should be ignored
r#"{"type": "int", "logicalType": "timestamp-millis"}"#,
true,
),
];
const TIMESTAMPMICROS_LOGICAL_TYPE: &[(&str, bool)] = &[
(
r#"{"type": "long", "logicalType": "timestamp-micros"}"#,
true,
),
// this is valid even though its logical type is "timestamp-micro" (missing the last "s"), because
// unknown logical types are ignored
(
r#"{"type": "long", "logicalType": "timestamp-micro"}"#,
true,
),
(
// this is still valid because unknown logicalType should be ignored
r#"{"type": "int", "logicalType": "timestamp-micros"}"#,
true,
),
];
lazy_static! {
static ref EXAMPLES: Vec<(&'static str, bool)> = Vec::new()
.iter()
.copied()
.chain(PRIMITIVE_EXAMPLES.iter().copied())
.chain(FIXED_EXAMPLES.iter().copied())
.chain(ENUM_EXAMPLES.iter().copied())
.chain(ARRAY_EXAMPLES.iter().copied())
.chain(MAP_EXAMPLES.iter().copied())
.chain(UNION_EXAMPLES.iter().copied())
.chain(RECORD_EXAMPLES.iter().copied())
.chain(DOC_EXAMPLES.iter().copied())
.chain(OTHER_ATTRIBUTES_EXAMPLES.iter().copied())
.chain(DECIMAL_LOGICAL_TYPE.iter().copied())
.chain(DECIMAL_LOGICAL_TYPE_ATTRIBUTES.iter().copied())
.chain(DATE_LOGICAL_TYPE.iter().copied())
.chain(TIMEMILLIS_LOGICAL_TYPE.iter().copied())
.chain(TIMEMICROS_LOGICAL_TYPE.iter().copied())
.chain(TIMESTAMPMILLIS_LOGICAL_TYPE.iter().copied())
.chain(TIMESTAMPMICROS_LOGICAL_TYPE.iter().copied())
.collect();
static ref VALID_EXAMPLES: Vec<(&'static str, bool)> =
EXAMPLES.iter().copied().filter(|s| s.1).collect();
}
#[test]
fn test_correct_recursive_extraction() {
init();
let raw_outer_schema = r#"{
"type": "record",
"name": "X",
"fields": [
{
"name": "y",
"type": {
"type": "record",
"name": "Y",
"fields": [
{
"name": "Z",
"type": "X"
}
]
}
}
]
}"#;
let outer_schema = Schema::parse_str(raw_outer_schema).unwrap();
if let Schema::Record {
fields: outer_fields,
..
} = outer_schema
{
let inner_schema = &outer_fields[0].schema;
if let Schema::Record {
fields: inner_fields,
..
} = inner_schema
{
if let Schema::Record {
name: recursive_type,
..
} = &inner_fields[0].schema
{
assert_eq!("X", recursive_type.name.as_str());
}
} else {
panic!("inner schema {:?} should have been a record", inner_schema)
}
} else {
panic!("outer schema {:?} should have been a record", outer_schema)
}
}
#[test]
fn test_parse() {
init();
for (raw_schema, valid) in EXAMPLES.iter() {
let schema = Schema::parse_str(raw_schema);
if *valid {
assert!(
schema.is_ok(),
"schema {} was supposed to be valid; error: {:?}",
raw_schema,
schema,
)
} else {
assert!(
schema.is_err(),
"schema {} was supposed to be invalid",
raw_schema
)
}
}
}
#[test]
/// Test that the string generated by an Avro Schema object is, in fact, a valid Avro schema.
fn test_valid_cast_to_string_after_parse() {
init();
for (raw_schema, _) in VALID_EXAMPLES.iter() {
let schema = Schema::parse_str(raw_schema).unwrap();
Schema::parse_str(schema.canonical_form().as_str()).unwrap();
}
}
#[test]
/// 1. Given a string, parse it to get Avro schema "original".
/// 2. Serialize "original" to a string and parse that string to generate Avro schema "round trip".
/// 3. Ensure "original" and "round trip" schemas are equivalent.
fn test_equivalence_after_round_trip() {
init();
for (raw_schema, _) in VALID_EXAMPLES.iter() {
let original_schema = Schema::parse_str(raw_schema).unwrap();
let round_trip_schema =
Schema::parse_str(original_schema.canonical_form().as_str()).unwrap();
assert_eq!(original_schema, round_trip_schema);
}
}
#[test]
/// Test that a list of schemas whose definitions do not depend on each other produces the same
/// result as parsing each element of the list individually
fn test_parse_list_without_cross_deps() {
init();
let schema_str_1 = r#"{
"name": "A",
"type": "record",
"fields": [
{"name": "field_one", "type": "float"}
]
}"#;
let schema_str_2 = r#"{
"name": "B",
"type": "fixed",
"size": 16
}"#;
let schema_strs = [schema_str_1, schema_str_2];
let schemas = Schema::parse_list(&schema_strs).expect("Test failed");
for schema_str in &schema_strs {
let parsed = Schema::parse_str(schema_str).expect("Test failed");
assert!(schemas.contains(&parsed));
}
}
#[test]
/// Test that the parsing of a list of schemas, whose definitions do depend on each other, can
/// perform the necessary schema composition. This should work regardless of the order in which
/// the schemas are input.
/// However, the output order is guaranteed to be the same as the input order.
fn test_parse_list_with_cross_deps_basic() {
init();
let schema_a_str = r#"{
"name": "A",
"type": "record",
"fields": [
{"name": "field_one", "type": "float"}
]
}"#;
let schema_b_str = r#"{
"name": "B",
"type": "record",
"fields": [
{"name": "field_one", "type": "A"}
]
}"#;
let schema_strs_first = [schema_a_str, schema_b_str];
let schema_strs_second = [schema_b_str, schema_a_str];
let schemas_first = Schema::parse_list(&schema_strs_first).expect("Test failed");
let schemas_second = Schema::parse_list(&schema_strs_second).expect("Test failed");
assert_eq!(schemas_first[0], schemas_second[1]);
assert_eq!(schemas_first[1], schemas_second[0]);
}
#[test]
fn test_parse_list_recursive_type() {
init();
let schema_str_1 = r#"{
"name": "A",
"doc": "A's schema",
"type": "record",
"fields": [
{"name": "a_field_one", "type": "B"}
]
}"#;
let schema_str_2 = r#"{
"name": "B",
"doc": "B's schema",
"type": "record",
"fields": [
{"name": "b_field_one", "type": "A"}
]
}"#;
let schema_strs_first = [schema_str_1, schema_str_2];
let schema_strs_second = [schema_str_2, schema_str_1];
let _ = Schema::parse_list(&schema_strs_first).expect("Test failed");
let _ = Schema::parse_list(&schema_strs_second).expect("Test failed");
}
#[test]
/// Test that schema composition resolves namespaces.
fn test_parse_list_with_cross_deps_and_namespaces() {
init();
let schema_a_str = r#"{
"name": "A",
"type": "record",
"namespace": "namespace",
"fields": [
{"name": "field_one", "type": "float"}
]
}"#;
let schema_b_str = r#"{
"name": "B",
"type": "record",
"fields": [
{"name": "field_one", "type": "namespace.A"}
]
}"#;
let schemas_first = Schema::parse_list(&[schema_a_str, schema_b_str]).expect("Test failed");
let schemas_second = Schema::parse_list(&[schema_b_str, schema_a_str]).expect("Test failed");
assert_eq!(schemas_first[0], schemas_second[1]);
assert_eq!(schemas_first[1], schemas_second[0]);
}
#[test]
/// Test that schema composition fails on namespace errors.
fn test_parse_list_with_cross_deps_and_namespaces_error() {
init();
let schema_str_1 = r#"{
"name": "A",
"type": "record",
"namespace": "namespace",
"fields": [
{"name": "field_one", "type": "float"}
]
}"#;
let schema_str_2 = r#"{
"name": "B",
"type": "record",
"fields": [
{"name": "field_one", "type": "A"}
]
}"#;
let schema_strs_first = [schema_str_1, schema_str_2];
let schema_strs_second = [schema_str_2, schema_str_1];
let _ = Schema::parse_list(&schema_strs_first).expect_err("Test failed");
let _ = Schema::parse_list(&schema_strs_second).expect_err("Test failed");
}
#[test]
// <https://issues.apache.org/jira/browse/AVRO-3216>
// test that field's RecordSchema could be referenced by a following field by full name
fn test_parse_reused_record_schema_by_fullname() {
init();
let schema_str = r#"
{
"type" : "record",
"name" : "Weather",
"namespace" : "test",
"doc" : "A weather reading.",
"fields" : [
{
"name" : "station",
"type" : {
"type" : "string",
"avro.java.string" : "String"
}
},
{
"name" : "max_temp",
"type" : {
"type" : "record",
"name" : "Temp",
"namespace": "prefix",
"doc" : "A temperature reading.",
"fields" : [ {
"name" : "temp",
"type" : "long"
} ]
}
}, {
"name" : "min_temp",
"type" : "prefix.Temp"
}
]
}
"#;
let schema = Schema::parse_str(schema_str);
assert!(schema.is_ok());
match schema.unwrap() {
Schema::Record {
ref name,
aliases: _,
doc: _,
ref fields,
lookup: _,
} => {
assert_eq!(name.fullname(None), "test.Weather", "Name does not match!");
assert_eq!(fields.len(), 3, "The number of the fields is not correct!");
let RecordField {
ref name,
doc: _,
default: _,
ref schema,
order: _,
position: _,
} = fields.get(2).unwrap();
assert_eq!(name, "min_temp");
match schema {
Schema::Ref { ref name } => {
assert_eq!(name.fullname(None), "prefix.Temp", "Name does not match!");
}
unexpected => unreachable!("Unexpected schema type: {:?}", unexpected),
}
}
unexpected => unreachable!("Unexpected schema type: {:?}", unexpected),
}
}
/// Return all permutations of an input slice
fn permutations<T>(list: &[T]) -> Vec<Vec<&T>> {
let size = list.len();
let indices = permutation_indices((0..size).collect());
let mut perms = Vec::new();
for perm_map in &indices {
let mut perm = Vec::new();
for ix in perm_map {
perm.push(&list[*ix]);
}
perms.push(perm)
}
perms
}
/// Return all permutations of the indices of a vector
fn permutation_indices(indices: Vec<usize>) -> Vec<Vec<usize>> {
let size = indices.len();
let mut perms: Vec<Vec<usize>> = Vec::new();
if size == 1 {
perms.push(indices);
return perms;
}
for index in 0..size {
let (head, tail) = indices.split_at(index);
let (first, rest) = tail.split_at(1);
let mut head = head.to_vec();
head.extend_from_slice(rest);
for mut sub_index in permutation_indices(head) {
sub_index.insert(0, first[0]);
perms.push(sub_index);
}
}
perms
}
#[test]
/// Test that a type that depends on more than one other type is parsed correctly when all
/// definitions are passed in as a list. This should work regardless of the ordering of the list.
fn test_parse_list_multiple_dependencies() {
init();
let schema_a_str = r#"{
"name": "A",
"type": "record",
"fields": [
{"name": "field_one", "type": ["null", "B", "C"]}
]
}"#;
let schema_b_str = r#"{
"name": "B",
"type": "fixed",
"size": 16
}"#;
let schema_c_str = r#"{
"name": "C",
"type": "record",
"fields": [
{"name": "field_one", "type": "string"}
]
}"#;
let parsed =
Schema::parse_list(&[schema_a_str, schema_b_str, schema_c_str]).expect("Test failed");
let schema_strs = vec![schema_a_str, schema_b_str, schema_c_str];
for schema_str_perm in permutations(&schema_strs) {
let schema_str_perm: Vec<&str> = schema_str_perm.iter().map(|s| **s).collect();
let schemas = Schema::parse_list(&schema_str_perm).expect("Test failed");
assert_eq!(schemas.len(), 3);
for parsed_schema in &parsed {
assert!(schemas.contains(parsed_schema));
}
}
}
#[test]
/// Test that a type that is depended on by more than one other type is parsed correctly when all
/// definitions are passed in as a list. This should work regardless of the ordering of the list.
fn test_parse_list_shared_dependency() {
init();
let schema_a_str = r#"{
"name": "A",
"type": "record",
"fields": [
{"name": "field_one", "type": {"type": "array", "items": "C"}}
]
}"#;
let schema_b_str = r#"{
"name": "B",
"type": "record",
"fields": [
{"name": "field_one", "type": {"type": "map", "values": "C"}}
]
}"#;
let schema_c_str = r#"{
"name": "C",
"type": "record",
"fields": [
{"name": "field_one", "type": "string"}
]
}"#;
let parsed =
Schema::parse_list(&[schema_a_str, schema_b_str, schema_c_str]).expect("Test failed");
let schema_strs = vec![schema_a_str, schema_b_str, schema_c_str];
for schema_str_perm in permutations(&schema_strs) {
let schema_str_perm: Vec<&str> = schema_str_perm.iter().map(|s| **s).collect();
let schemas = Schema::parse_list(&schema_str_perm).expect("Test failed");
assert_eq!(schemas.len(), 3);
for parsed_schema in &parsed {
assert!(schemas.contains(parsed_schema));
}
}
}
#[test]
/// Test that trying to parse two schemas with the same fullname returns an Error
fn test_name_collision_error() {
init();
let schema_str_1 = r#"{
"name": "foo.A",
"type": "record",
"fields": [
{"name": "field_one", "type": "double"}
]
}"#;
let schema_str_2 = r#"{
"name": "A",
"type": "record",
"namespace": "foo",
"fields": [
{"name": "field_two", "type": "string"}
]
}"#;
let _ = Schema::parse_list(&[schema_str_1, schema_str_2]).expect_err("Test failed");
}
#[test]
/// Test that having the same name but different fullnames does not return an error
fn test_namespace_prevents_collisions() {
init();
let schema_str_1 = r#"{
"name": "A",
"type": "record",
"fields": [
{"name": "field_one", "type": "double"}
]
}"#;
let schema_str_2 = r#"{
"name": "A",
"type": "record",
"namespace": "foo",
"fields": [
{"name": "field_two", "type": "string"}
]
}"#;
let parsed = Schema::parse_list(&[schema_str_1, schema_str_2]).expect("Test failed");
let parsed_1 = Schema::parse_str(schema_str_1).expect("Test failed");
let parsed_2 = Schema::parse_str(schema_str_2).expect("Test failed");
assert_eq!(parsed, vec!(parsed_1, parsed_2));
}
// The fullname is determined in one of the following ways:
// * A name and namespace are both specified. For example,
// one might use "name": "X", "namespace": "org.foo"
// to indicate the fullname "org.foo.X".
// * A fullname is specified. If the name specified contains
// a dot, then it is assumed to be a fullname, and any
// namespace also specified is ignored. For example,
// use "name": "org.foo.X" to indicate the
// fullname "org.foo.X".
// * A name only is specified, i.e., a name that contains no
// dots. In this case the namespace is taken from the most
// tightly enclosing schema or protocol. For example,
// if "name": "X" is specified, and this occurs
// within a field of the record definition /// of "org.foo.Y", then the fullname is "org.foo.X".
// References to previously defined names are as in the latter
// two cases above: if they contain a dot they are a fullname, if
// they do not contain a dot, the namespace is the namespace of
// the enclosing definition.
// Primitive type names have no namespace and their names may
// not be defined in any namespace. A schema may only contain
// multiple definitions of a fullname if the definitions are
// equivalent.
#[test]
fn test_fullname_name_and_namespace_specified() {
init();
let name: Name =
serde_json::from_str(r#"{"name": "a", "namespace": "o.a.h", "aliases": null}"#).unwrap();
let fullname = name.fullname(None);
assert_eq!("o.a.h.a", fullname);
}
#[test]
fn test_fullname_fullname_and_namespace_specified() {
init();
let name: Name = serde_json::from_str(r#"{"name": "a.b.c.d", "namespace": "o.a.h"}"#).unwrap();
assert_eq!(&name.name, "d");
assert_eq!(name.namespace, Some("a.b.c".to_owned()));
let fullname = name.fullname(None);
assert_eq!("a.b.c.d", fullname);
}
#[test]
fn test_fullname_name_and_default_namespace_specified() {
init();
let name: Name = serde_json::from_str(r#"{"name": "a", "namespace": null}"#).unwrap();
assert_eq!(&name.name, "a");
assert_eq!(name.namespace, None);
let fullname = name.fullname(Some("b.c.d".into()));
assert_eq!("b.c.d.a", fullname);
}
#[test]
fn test_fullname_fullname_and_default_namespace_specified() {
init();
let name: Name = serde_json::from_str(r#"{"name": "a.b.c.d", "namespace": null}"#).unwrap();
assert_eq!(&name.name, "d");
assert_eq!(name.namespace, Some("a.b.c".to_owned()));
let fullname = name.fullname(Some("o.a.h".into()));
assert_eq!("a.b.c.d", fullname);
}
#[test]
fn test_avro_3452_parsing_name_without_namespace() {
init();
let name: Name = serde_json::from_str(r#"{"name": "a.b.c.d"}"#).unwrap();
assert_eq!(&name.name, "d");
assert_eq!(name.namespace, Some("a.b.c".to_owned()));
let fullname = name.fullname(None);
assert_eq!("a.b.c.d", fullname);
}
#[test]
fn test_avro_3452_parsing_name_with_leading_dot_without_namespace() {
init();
let name: Name = serde_json::from_str(r#"{"name": ".a"}"#).unwrap();
assert_eq!(&name.name, "a");
assert_eq!(name.namespace, None);
assert_eq!("a", name.fullname(None));
}
#[test]
fn test_avro_3452_parse_json_without_name_field() {
init();
let result: serde_json::error::Result<Name> = serde_json::from_str(r#"{"unknown": "a"}"#);
assert!(&result.is_err());
assert_eq!(result.unwrap_err().to_string(), "No `name` field");
}
#[test]
fn test_fullname_fullname_namespace_and_default_namespace_specified() {
init();
let name: Name =
serde_json::from_str(r#"{"name": "a.b.c.d", "namespace": "o.a.a", "aliases": null}"#)
.unwrap();
assert_eq!(&name.name, "d");
assert_eq!(name.namespace, Some("a.b.c".to_owned()));
let fullname = name.fullname(Some("o.a.h".into()));
assert_eq!("a.b.c.d", fullname);
}
#[test]
fn test_fullname_name_namespace_and_default_namespace_specified() {
init();
let name: Name =
serde_json::from_str(r#"{"name": "a", "namespace": "o.a.a", "aliases": null}"#).unwrap();
assert_eq!(&name.name, "a");
assert_eq!(name.namespace, Some("o.a.a".to_owned()));
let fullname = name.fullname(Some("o.a.h".into()));
assert_eq!("o.a.a.a", fullname);
}
#[test]
fn test_doc_attributes() {
init();
fn assert_doc(schema: &Schema) {
match schema {
Schema::Enum { doc, .. } => assert!(doc.is_some()),
Schema::Record { doc, .. } => assert!(doc.is_some()),
Schema::Fixed { doc, .. } => assert!(doc.is_some()),
Schema::String => (),
_ => unreachable!("Unexpected schema type: {:?}", schema),
}
}
for (raw_schema, _) in DOC_EXAMPLES.iter() {
let original_schema = Schema::parse_str(raw_schema).unwrap();
assert_doc(&original_schema);
if let Schema::Record { fields, .. } = original_schema {
for f in fields {
assert_doc(&f.schema)
}
}
}
}
/*
TODO: (#94) add support for user-defined attributes and uncomment (may need some tweaks to compile)
#[test]
fn test_other_attributes() {
fn assert_attribute_type(attribute: (String, serde_json::Value)) {
match attribute.1.as_ref() {
"cp_boolean" => assert!(attribute.2.is_bool()),
"cp_int" => assert!(attribute.2.is_i64()),
"cp_object" => assert!(attribute.2.is_object()),
"cp_float" => assert!(attribute.2.is_f64()),
"cp_array" => assert!(attribute.2.is_array()),
}
}
for (raw_schema, _) in OTHER_ATTRIBUTES_EXAMPLES.iter() {
let schema = Schema::parse_str(raw_schema).unwrap();
// all inputs have at least some user-defined attributes
assert!(schema.other_attributes.is_some());
for prop in schema.other_attributes.unwrap().iter() {
assert_attribute_type(prop);
}
if let Schema::Record { fields, .. } = schema {
for f in fields {
// all fields in the record have at least some user-defined attributes
assert!(f.schema.other_attributes.is_some());
for prop in f.schema.other_attributes.unwrap().iter() {
assert_attribute_type(prop);
}
}
}
}
}
*/
#[test]
fn test_root_error_is_not_swallowed_on_parse_error() -> Result<(), String> {
init();
let raw_schema = r#"/not/a/real/file"#;
let error = Schema::parse_str(raw_schema).unwrap_err();
if let Error::ParseSchemaJson(e) = error {
assert!(
e.to_string().contains("expected value at line 1 column 1"),
"{}",
e
);
Ok(())
} else {
Err(format!(
"Expected serde_json::error::Error, got {:?}",
error
))
}
}
// AVRO-3302
#[test]
fn test_record_schema_with_cyclic_references() {
init();
let schema = Schema::parse_str(
r#"
{
"type": "record",
"name": "test",
"fields": [{
"name": "recordField",
"type": {
"type": "record",
"name": "Node",
"fields": [
{"name": "label", "type": "string"},
{"name": "children", "type": {"type": "array", "items": "Node"}}
]
}
}]
}
"#,
)
.unwrap();
let mut datum = Record::new(&schema).unwrap();
datum.put(
"recordField",
Value::Record(vec![
("label".into(), Value::String("level_1".into())),
(
"children".into(),
Value::Array(vec![Value::Record(vec![
("label".into(), Value::String("level_2".into())),
(
"children".into(),
Value::Array(vec![Value::Record(vec![
("label".into(), Value::String("level_3".into())),
(
"children".into(),
Value::Array(vec![Value::Record(vec![
("label".into(), Value::String("level_4".into())),
("children".into(), Value::Array(vec![])),
])]),
),
])]),
),
])]),
),
]),
);
let mut writer = Writer::with_codec(&schema, Vec::new(), Codec::Null);
if let Err(err) = writer.append(datum) {
panic!("An error occurred while writing datum: {:?}", err)
}
let bytes = writer.into_inner().unwrap();
assert_eq!(316, bytes.len());
match Reader::new(&mut bytes.as_slice()) {
Ok(mut reader) => match reader.next() {
Some(value) => log::debug!("{:?}", value.unwrap()),
None => panic!("No value was read!"),
},
Err(err) => panic!("An error occurred while reading datum: {:?}", err),
}
}
/*
// TODO: (#93) add support for logical type and attributes and uncomment (may need some tweaks to compile)
#[test]
fn test_decimal_valid_type_attributes() {
init();
let fixed_decimal = Schema::parse_str(DECIMAL_LOGICAL_TYPE_ATTRIBUTES[0]).unwrap();
assert_eq!(4, fixed_decimal.get_attribute("precision"));
assert_eq!(2, fixed_decimal.get_attribute("scale"));
assert_eq!(2, fixed_decimal.get_attribute("size"));
let bytes_decimal = Schema::parse_str(DECIMAL_LOGICAL_TYPE_ATTRIBUTES[1]).unwrap();
assert_eq!(4, bytes_decimal.get_attribute("precision"));
assert_eq!(0, bytes_decimal.get_attribute("scale"));
}
*/
// https://github.com/flavray/avro-rs/issues/47
#[test]
fn avro_old_issue_47() {
init();
let schema_str = r#"
{
"type": "record",
"name": "my_record",
"fields": [
{"name": "a", "type": "long"},
{"name": "b", "type": "string"}
]
}"#;
let schema = Schema::parse_str(schema_str).unwrap();
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
pub struct MyRecord {
b: String,
a: i64,
}
let record = MyRecord {
b: "hello".to_string(),
a: 1,
};
let _ = to_avro_datum(&schema, to_value(record).unwrap()).unwrap();
}