mod codec;
pub use apache_avro::AvroSchema;
pub use codec::AvroCodec;
use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::sync::{OnceLock, RwLock};
use apache_avro::Schema;
use apache_avro::reader::datum::{GenericDatumReader, SpecificDatumReader};
use apache_avro::types::Value as AvroValue;
use apache_avro::writer::datum::GenericDatumWriter;
use bytes::BufMut;
use ruststream::BytesMut;
use serde::Serialize;
use serde::de::DeserializeOwned;
use crate::error::KafkaError;
use crate::schema_registry::{RegisteredSchema, SchemaRegistry};
struct Prepared<T: AvroSchema> {
writer: GenericDatumWriter<'static>,
reader: SpecificDatumReader<T>,
}
static PREPARED: OnceLock<RwLock<HashMap<TypeId, &'static (dyn Any + Send + Sync)>>> =
OnceLock::new();
fn prepared<T>() -> Result<&'static Prepared<T>, KafkaError>
where
T: AvroSchema + Send + Sync + 'static,
{
let cache = PREPARED.get_or_init(|| RwLock::new(HashMap::new()));
let key = TypeId::of::<T>();
if let Some(entry) = cache
.read()
.expect("prepared schema cache mutex poisoned")
.get(&key)
{
return Ok(downcast(*entry));
}
let schema: &'static Schema = Box::leak(Box::new(T::get_schema()));
let writer = GenericDatumWriter::builder(schema)
.build()
.map_err(KafkaError::wire_format)?;
let reader = SpecificDatumReader::<T>::builder()
.build()
.map_err(KafkaError::wire_format)?;
let entry: &'static (dyn Any + Send + Sync) = Box::leak(Box::new(Prepared { writer, reader }));
let stored = *cache
.write()
.expect("prepared schema cache mutex poisoned")
.entry(key)
.or_insert(entry);
Ok(downcast(stored))
}
fn downcast<T: AvroSchema + 'static>(
entry: &'static (dyn Any + Send + Sync),
) -> &'static Prepared<T> {
entry
.downcast_ref()
.expect("the prepared entry of a type id is that type's")
}
pub fn encode<T>(value: &T, buf: &mut BytesMut) -> Result<(), KafkaError>
where
T: AvroSchema + Serialize + Send + Sync + 'static,
{
let prepared = prepared::<T>()?;
prepared
.writer
.write_ser(&mut buf.writer(), value)
.map_err(KafkaError::wire_format)?;
Ok(())
}
pub fn decode<T>(payload: &[u8]) -> Result<T, KafkaError>
where
T: AvroSchema + DeserializeOwned + Send + Sync + 'static,
{
let prepared = prepared::<T>()?;
let mut cursor = payload;
prepared
.reader
.read(&mut cursor)
.map_err(KafkaError::wire_format)
}
pub(crate) fn schema_json(schema: &Schema) -> Result<String, KafkaError> {
serde_json::to_string(schema).map_err(KafkaError::wire_format)
}
pub(crate) fn avro_to_json(
registry: &SchemaRegistry,
schema: &RegisteredSchema,
datum: &[u8],
) -> Result<Vec<u8>, KafkaError> {
let writer = registry.parsed_avro(schema)?;
let mut cursor = datum;
let value = GenericDatumReader::builder(&writer)
.build()
.map_err(KafkaError::schema_registry)?
.read_value(&mut cursor)
.map_err(KafkaError::schema_registry)?;
let json: serde_json::Value =
apache_avro::from_value(&value).map_err(KafkaError::schema_registry)?;
serde_json::to_vec(&json).map_err(KafkaError::schema_registry)
}
pub(crate) fn json_to_avro(
registry: &SchemaRegistry,
schema: &RegisteredSchema,
payload: &[u8],
) -> Result<Vec<u8>, KafkaError> {
let parsed = registry.parsed_avro(schema)?;
let json: serde_json::Value =
serde_json::from_slice(payload).map_err(KafkaError::schema_registry)?;
let value = AvroValue::try_from(json)
.map_err(KafkaError::schema_registry)?
.resolve(&parsed)
.map_err(KafkaError::schema_registry)?;
GenericDatumWriter::builder(&parsed)
.build()
.map_err(KafkaError::schema_registry)?
.write_value_to_vec(value)
.map_err(KafkaError::schema_registry)
}
#[cfg(test)]
mod tests {
use serde::{Deserialize, Serialize};
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
use super::*;
#[derive(Debug, PartialEq, Serialize, Deserialize, AvroSchema)]
struct Order {
id: i64,
item: String,
}
async fn registry_with_order(id: u32) -> (MockServer, SchemaRegistry, RegisteredSchema) {
let server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/subjects/orders-value/versions"))
.respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ "id": id })))
.mount(&server)
.await;
let registry = SchemaRegistry::new(server.uri());
registry
.register_avro::<Order>("orders-value")
.await
.expect("register");
let schema = registry.cached_subject("orders-value").expect("cached");
(server, registry, (*schema).clone())
}
#[test]
fn a_value_roundtrips_through_its_own_schema() {
let order = Order {
id: 42,
item: "anvil".to_owned(),
};
let mut buf = BytesMut::new();
encode(&order, &mut buf).expect("encode");
assert_ne!(
&buf[..],
br#"{"id":42,"item":"anvil"}"#,
"the wire form is Avro"
);
assert_eq!(decode::<Order>(&buf).expect("decode"), order);
}
#[test]
fn positive_integers_encode_as_the_declared_numeric_type() {
let mut buf = BytesMut::new();
encode(
&Order {
id: i64::from(u32::MAX),
item: String::new(),
},
&mut buf,
)
.expect("encode");
assert_eq!(
decode::<Order>(&buf).expect("decode").id,
i64::from(u32::MAX),
);
}
#[tokio::test]
async fn json_avro_json_roundtrips() {
let (_server, registry, schema) = registry_with_order(7).await;
let json = br#"{"id":42,"item":"anvil"}"#;
let datum = json_to_avro(®istry, &schema, json).expect("encode");
assert_ne!(datum.as_slice(), json, "the wire form is Avro, not JSON");
let back = avro_to_json(®istry, &schema, &datum).expect("decode");
let order: Order = serde_json::from_slice(&back).expect("deserialize");
assert_eq!(
order,
Order {
id: 42,
item: "anvil".to_owned(),
},
);
}
#[tokio::test]
async fn schema_mismatches_error_clearly() {
let (_server, registry, schema) = registry_with_order(7).await;
let err = json_to_avro(®istry, &schema, br#"{"id":"not-a-number"}"#)
.expect_err("a document violating the schema must fail");
assert!(matches!(err, KafkaError::SchemaRegistry(_)));
}
}