Skip to main content

AvroCodec

Struct AvroCodec 

Source
pub struct AvroCodec { /* private fields */ }
Expand description

An Avro Codec: the schema lives in the codec, and the messages riding it are ordinary serde types.

Avro is a schema-driven format with a serde front end, so the codec position is where it belongs: apache-avro converts a value through its own dynamic representation in both directions, guided by the schema the codec holds, and asks nothing of the message type beyond Serialize / Deserialize. A handler on this codec is an ordinary handler and its models are ordinary structs - no derive of ours, no wire type in the signature.

§The two schema sources

local pins one schema at construction. The wire carries a bare Avro datum, nothing reaches a registry, and the whole path is I/O-free - which is what a topic with a fixed schema, and every unit test, wants.

registry speaks the Confluent wire format: the encode side frames with the id its subject holds, and the decode side reads each delivery with the writer schema that delivery’s envelope names, so a producer still on an older version stays readable. It needs a SchemaPrefetch attached to the broker, because those lookups are async and a Codec is not; see that type for why the split falls where it does.

§Schema evolution

Reading a datum with its writer schema recovers what the writer wrote, and no more. A field the writer never had is filled from a reader schema’s default, which is Avro’s own schema resolution, so a consumer that has moved ahead of its producers names the schema it expects with resolve_onto. Without it a model with a field the writer lacks fails to deserialize, which is the honest outcome: the value is genuinely not on the wire.

§Examples

A topic with one fixed schema and no registry, consumed through an ordinary handler:

use apache_avro::AvroSchema;
use ruststream_rdkafka::KafkaError;
use ruststream_rdkafka::avro::AvroCodec;
use ruststream_rdkafka::prelude::*;
use serde::Deserialize;

#[derive(Deserialize, AvroSchema)]
struct Order {
    id: i64,
    item: String,
}

#[subscriber("orders")]
async fn place(order: &Order) -> HandlerOutcome {
    tracing::info!(order.id, item = %order.item, "placed");
    HandlerOutcome::ack()
}

fn app() -> Result<RustStream, KafkaError> {
    let codec = AvroCodec::local(Order::get_schema())?;
    let broker = KafkaBroker::new(["localhost:9092"]).default_group("orders-svc");
    Ok(RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker_codec(broker, codec, |b| {
        b.include(place);
    }))
}

Implementations§

Source§

impl AvroCodec

Source

pub fn local(schema: Schema) -> Result<Self, KafkaError>

A codec over one schema, known here and used for every message.

Nothing on this path reaches a registry and nothing frames an envelope: the wire carries the datum alone. Pass the schema the wire is supposed to carry - T::get_schema() when the model defines it, a parsed .avsc when the schema is the source of truth.

§Errors

Returns KafkaError::WireFormat when the schema’s own names cannot be resolved (a reference to a record the schema does not define).

Source

pub fn registry(prefetch: &SchemaPrefetch) -> Self

A codec on the Confluent wire format, publishing what register records.

Construction is synchronous and does no I/O; the subjects are resolved when the broker connects, and each delivery’s writer schema on its consume path, both by the prefetch this is built from.

§Examples

Each delivery decodes with the writer schema its envelope names, so a datum an older producer wrote stays readable; each reply is framed with its subject’s id:

use apache_avro::AvroSchema;
use ruststream_rdkafka::avro::AvroCodec;
use ruststream_rdkafka::prelude::*;
use ruststream_rdkafka::{SchemaPrefetch, SchemaRegistry};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, AvroSchema)]
struct Order {
    id: i64,
}

#[derive(Serialize, Deserialize, AvroSchema, Outgoing)]
struct Confirmation {
    id: i64,
}

#[subscriber("orders", reply("confirmations"))]
async fn confirm(order: &Order) -> Confirmation {
    Confirmation { id: order.id }
}

fn app() -> RustStream {
    let prefetch = SchemaPrefetch::new(SchemaRegistry::new("http://localhost:8081"));
    let codec = AvroCodec::registry(&prefetch).register::<Confirmation>("confirmations-value");
    let broker = KafkaBroker::new(["localhost:9092"])
        .default_group("orders-svc")
        .schema_prefetch(prefetch);
    // The scope's codec decodes every delivery; the reply names it again, at its own
    // position.
    let replies = codec.clone();
    RustStream::new(AppInfo::new("orders", "0.1.0")).with_broker_codec(broker, codec, |b| {
        b.include(confirm).out_reply(Publish::default()).codec(replies);
    })
}
Source

pub fn register<T: Serialize + AvroSchema>( self, subject: impl Into<String>, ) -> Self

Records that values of T publish under subject.

One codec serves as many message types as a router mounts through it, so this is a list rather than a second codec each time. The subject is looked up per publish by the name serde gives the value’s type, which is the only identity Codec::encode can recover - TypeId needs T: 'static, and encode bounds T by Serialize alone.

T::get_schema() is captured too, and used for exactly one thing: putting the subject back under MissingSubject::AutoRegister. What a datum is written with is always the schema the framed id names, so a drifted local model cannot silently produce records the id contradicts.

§Panics

Panics when two registered types share a serde name, which would make the subject of a publish ambiguous. That is a startup failure by design: the alternative is picking one of them per message and writing records under the wrong subject. Rename one with #[serde(rename = "..")]. Also panics when the prefetch’s internal mutex is poisoned, or when T’s own schema cannot be serialized.

§Examples

Registration is the publish side: decoding needs none of it, since the writer schema comes off the envelope’s id. The registry is named once, building the prefetch every codec is minted from:

use apache_avro::AvroSchema;
use ruststream_rdkafka::avro::AvroCodec;
use ruststream_rdkafka::prelude::*;
use ruststream_rdkafka::schema_registry::{SchemaPrefetch, SchemaRegistry};
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize, AvroSchema)]
pub struct Order {
    pub id: i64,
}

#[derive(Serialize, Deserialize, AvroSchema, Outgoing)]
#[outgoing(name = "confirmations")]
pub struct Confirmation {
    pub id: i64,
}

#[subscriber("orders", reply)]
async fn confirm(order: &Order) -> Confirmation {
    Confirmation { id: order.id }
}

pub fn app() -> RustStream {
    let prefetch = SchemaPrefetch::new(SchemaRegistry::new("http://localhost:8081"));
    let codec = AvroCodec::registry(&prefetch).register::<Confirmation>("confirmations-value");
    let broker = KafkaBroker::new(["localhost:9092"])
        .default_group("orders-svc")
        .schema_prefetch(prefetch);
    // The scope's codec decodes every delivery; the reply names it again, at its own
    // position.
    let replies = codec.clone();
    RustStream::new(AppInfo::new("orders", "0.1.0"))
        .with_broker_codec(broker, codec, |b| {
            b.include(confirm).out_reply(Publish::default()).codec(replies);
        })
}
Source

pub fn resolve_onto(self, schema: Schema) -> Result<Self, KafkaError>

Reads every delivery onto schema instead of the schema it was written with, so Avro’s resolution fills the fields this consumer expects and the writer never wrote.

This is the reader schema of Avro’s resolution rules, and it is the decode side only: a publish writes what the wire is supposed to carry, which is the writer schema either way.

§Errors

Returns KafkaError::WireFormat when the schema cannot be prepared against the local writer schema. A registry codec’s writer schemas arrive later, so a pairing that cannot resolve is reported by the decode that meets it.

Trait Implementations§

Source§

impl Clone for AvroCodec

Source§

fn clone(&self) -> Self

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Codec for AvroCodec

Source§

fn encode<T: Serialize>(&self, value: &T) -> Result<BytesMut, CodecError>

Encodes value into a mutable byte buffer. Read more
Source§

fn decode<T: DeserializeOwned>(&self, bytes: &[u8]) -> Result<T, CodecError>

Decodes bytes into a Rust value of type T. Read more
Source§

const CONTENT_TYPE: &'static str = "application/octet-stream"

The media type of the bytes this codec produces, reported as the contentType of every message it encodes or decodes in the generated AsyncAPI document. Read more
Source§

fn encode_into<T>( &self, value: &T, buf: &mut BytesMut, ) -> Result<(), CodecError>
where T: Serialize,

Encodes value into the end of a buffer the caller owns. Read more
Source§

impl Debug for AvroCodec

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> DynClone for T
where T: Clone,

Source§

fn __clone_box(&self, _: Private) -> *mut ()

Source§

impl<T> ErasedDestructor for T
where T: 'static,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> PublishedThrough<DefaultSlot> for T

Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more