serde_avro_fast 2.1.0

An idiomatic implementation of serde/avro (de)serialization
Documentation
#![allow(missing_docs)]

use serde_avro_fast::Schema;

#[test]
fn forbids_zero_sized_cycles() {
	let schema = r#"
	{
		"type": "record",
		"name": "A",
		"fields": [
			{
				"name": "a",
				"type": {
					"type": "record",
					"name": "B",
					"fields": [
						{
							"name": "b",
							"type": "A"
						}
					]
				}
			}
		]
	}"#;
	assert_eq!(
		schema.parse::<Schema>().unwrap_err().to_string(),
		"The schema contains a record that ends up always containing itself"
	);
}

#[test]
fn from_schemata_resolves_named_dependency() {
	let main = r#"{
		"type": "record",
		"name": "Bar",
		"fields": [{ "name": "b", "type": "Foo" }]
	}"#;
	let dep = r#"{
		"type": "record",
		"name": "Foo",
		"fields": [{ "name": "a", "type": "int" }]
	}"#;
	let schema = Schema::from_schemata(main, [dep]).unwrap();
	assert_eq!(
		schema.json(),
		r#"{"type":"record","name":"Bar","fields":[{"name":"b","type":{"type":"record","name":"Foo","fields":[{"name":"a","type":"int"}]}}]}"#
	);
}

#[test]
fn from_schemata_dependency_order_does_not_matter() {
	let main = r#"{
		"type": "record",
		"name": "Root",
		"fields": [{ "name": "x", "type": "Alpha" }]
	}"#;
	let dep_alpha = r#"{
		"type": "record",
		"name": "Alpha",
		"fields": [{ "name": "b", "type": "Beta" }]
	}"#;
	let dep_beta = r#"{
		"type": "record",
		"name": "Beta",
		"fields": [{ "name": "v", "type": "int" }]
	}"#;
	let normalized = r#"{
		"type": "record",
		"name": "Root",
		"fields": [{
			"name": "x",
			"type": {
				"type": "record",
				"name": "Alpha",
				"fields": [{
					"name": "b",
					"type": {
						"type": "record",
						"name": "Beta",
						"fields": [{ "name": "v", "type": "int" }]
					}
				}]
			}
		}]
	}"#;

	let s1 = Schema::from_schemata(main, [dep_alpha, dep_beta]).unwrap();
	let s2 = Schema::from_schemata(main, [dep_beta, dep_alpha]).unwrap();
	let s_normalized: Schema = normalized.parse().unwrap();

	assert_eq!(s1.rabin_fingerprint(), s2.rabin_fingerprint());
	assert_eq!(s1.rabin_fingerprint(), s_normalized.rabin_fingerprint());
	assert_eq!(s1.json(), s2.json());
}

#[test]
fn from_schemata_allows_forward_references_across_documents() {
	let main = r#"{
		"type": "record",
		"name": "Root",
		"fields": [{ "name": "f", "type": "Foo" }]
	}"#;
	let dep_foo = r#"{
		"type": "record",
		"name": "Foo",
		"fields": [{ "name": "b", "type": "Bar" }]
	}"#;
	let dep_bar = r#"{
		"type": "record",
		"name": "Bar",
		"fields": [{ "name": "v", "type": "string" }]
	}"#;

	let schema = Schema::from_schemata(main, [dep_foo, dep_bar]).unwrap();
	assert_eq!(
		schema.json(),
		r#"{"type":"record","name":"Root","fields":[{"name":"f","type":{"type":"record","name":"Foo","fields":[{"name":"b","type":{"type":"record","name":"Bar","fields":[{"name":"v","type":"string"}]}}]}}]}"#
	);
}

#[test]
fn from_schemata_rejects_duplicate_across_dependencies() {
	let main = r#"{
		"type": "record",
		"name": "Main",
		"fields": [{ "name": "f", "type": "Foo" }]
	}"#;
	let dep1 = r#"{
		"type": "record",
		"name": "Foo",
		"fields": [{ "name": "a", "type": "int" }]
	}"#;
	let dep2 = r#"{
		"type": "record",
		"name": "Foo",
		"fields": [{ "name": "b", "type": "long" }]
	}"#;

	let err = Schema::from_schemata(main, [dep1, dep2]).unwrap_err();
	let msg = err.to_string();
	assert!(
		msg.contains("duplicate") || msg.contains("Duplicate"),
		"expected duplicate error, got: {msg}"
	);
}

#[test]
fn from_schemata_rejects_duplicate_between_main_and_dependency() {
	let main = r#"{
		"type": "record",
		"name": "Foo",
		"fields": [{ "name": "a", "type": "int" }]
	}"#;
	let dep = r#"{
		"type": "record",
		"name": "Foo",
		"fields": [{ "name": "b", "type": "long" }]
	}"#;

	let err = Schema::from_schemata(main, [dep]).unwrap_err();
	let msg = err.to_string();
	assert!(
		msg.contains("duplicate") || msg.contains("Duplicate"),
		"expected duplicate error between main and dep, got: {msg}"
	);
}

#[test]
fn from_schemata_rejects_unknown_reference_from_main() {
	let main = r#"{
		"type": "record",
		"name": "Main",
		"fields": [{ "name": "f", "type": "Missing" }]
	}"#;

	let err = Schema::from_schemata(main, Vec::<&str>::new()).unwrap_err();
	let msg = err.to_string();
	assert!(
		msg.contains("unknown reference") || msg.contains("Unknown"),
		"expected unknown reference error, got: {msg}"
	);
}

#[test]
fn from_schemata_rejects_unknown_reference_from_dependency() {
	let main = r#"{
		"type": "record",
		"name": "Main",
		"fields": [{ "name": "f", "type": "Dep" }]
	}"#;
	let dep = r#"{
		"type": "record",
		"name": "Dep",
		"fields": [{ "name": "x", "type": "Nonexistent" }]
	}"#;

	let err = Schema::from_schemata(main, [dep]).unwrap_err();
	let msg = err.to_string();
	assert!(
		msg.contains("unknown reference") || msg.contains("Unknown"),
		"expected unknown reference error from dependency, got: {msg}"
	);
}

#[test]
fn from_schemata_prunes_unreferenced_nodes() {
	let main = r#"{
		"type": "record",
		"name": "Main",
		"fields": [{ "name": "f", "type": "Used" }]
	}"#;
	let dep_used = r#"{
		"type": "record",
		"name": "Used",
		"fields": [{ "name": "v", "type": "int" }]
	}"#;
	let dep_unused = r#"{
		"type": "record",
		"name": "Unused",
		"fields": [{ "name": "w", "type": "string" }]
	}"#;

	let schema = Schema::from_schemata(main, [dep_used, dep_unused]).unwrap();
	assert_eq!(
		schema.json(),
		r#"{"type":"record","name":"Main","fields":[{"name":"f","type":{"type":"record","name":"Used","fields":[{"name":"v","type":"int"}]}}]}"#
	);
}