1use std::collections::HashMap;
4use std::sync::{Arc, Mutex};
5
6use apache_avro::reader::datum::GenericDatumReader;
7use apache_avro::writer::datum::GenericDatumWriter;
8use apache_avro::{AvroSchema, Schema};
9use bytes::BufMut;
10use ruststream::BytesMut;
11use ruststream::codec::{Codec, CodecError};
12use serde::Serialize;
13use serde::de::DeserializeOwned;
14
15use crate::error::KafkaError;
16use crate::schema_registry::{
17 MissingSubject, Registration, SchemaPrefetch, SchemaRegistry, SchemaType, WIRE_MAGIC,
18 parse_envelope, serde_name,
19};
20
21struct Prepared {
28 schema: &'static Schema,
31 writer: GenericDatumWriter<'static>,
32 reader: GenericDatumReader<'static>,
33}
34
35impl Prepared {
36 fn build(writer_schema: Schema, reader_schema: Option<Schema>) -> Result<Self, KafkaError> {
37 let schema: &'static Schema = Box::leak(Box::new(writer_schema));
38 let reader_schema: Option<&'static Schema> =
39 reader_schema.map(|reader| &*Box::leak(Box::new(reader)));
40 Ok(Self {
41 schema,
42 writer: GenericDatumWriter::builder(schema)
43 .build()
44 .map_err(KafkaError::wire_format)?,
45 reader: GenericDatumReader::builder(schema)
46 .maybe_reader_schema(reader_schema)
47 .build()
48 .map_err(KafkaError::wire_format)?,
49 })
50 }
51}
52
53#[derive(Clone)]
59enum SchemaSource {
60 Local(&'static Prepared),
62 Registry {
64 registry: SchemaRegistry,
65 subjects: Arc<HashMap<&'static str, Registered>>,
72 prepared: Arc<Mutex<HashMap<u32, &'static Prepared>>>,
77 on_missing: MissingSubject,
78 },
79}
80
81struct Registered {
83 subject: String,
84 local: &'static Prepared,
87}
88
89impl Clone for Registered {
90 fn clone(&self) -> Self {
91 Self {
92 subject: self.subject.clone(),
93 local: self.local,
94 }
95 }
96}
97
98fn avro_record_name(schema: &Schema) -> Option<&'static str> {
102 match schema {
103 Schema::Record(record) => Some(String::leak(record.name.name().to_string())),
104 _ => None,
105 }
106}
107
108#[derive(Clone)]
170pub struct AvroCodec {
171 source: SchemaSource,
172 reader_schema: Option<Arc<Schema>>,
175 prefetch: Option<SchemaPrefetch>,
177}
178
179impl std::fmt::Debug for AvroCodec {
180 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
181 let mut out = f.debug_struct("AvroCodec");
182 match &self.source {
183 SchemaSource::Local(_) => out.field("source", &"local"),
184 SchemaSource::Registry { subjects, .. } => out.field(
185 "subjects",
186 &subjects.values().map(|r| &r.subject).collect::<Vec<_>>(),
187 ),
188 };
189 out.finish_non_exhaustive()
190 }
191}
192
193impl AvroCodec {
194 pub fn local(schema: Schema) -> Result<Self, KafkaError> {
205 Ok(Self {
206 source: SchemaSource::Local(Box::leak(Box::new(Prepared::build(schema, None)?))),
207 reader_schema: None,
208 prefetch: None,
209 })
210 }
211
212 #[must_use]
265 pub fn registry(prefetch: &SchemaPrefetch) -> Self {
266 Self {
267 source: SchemaSource::Registry {
268 registry: prefetch.registry().clone(),
269 subjects: Arc::new(HashMap::new()),
270 prepared: Arc::new(Mutex::new(HashMap::new())),
271 on_missing: prefetch.missing_subject(),
272 },
273 reader_schema: None,
274 prefetch: Some(prefetch.clone()),
275 }
276 }
277
278 #[must_use]
348 pub fn register<T: Serialize + AvroSchema>(mut self, subject: impl Into<String>) -> Self {
349 let subject = subject.into();
350 let schema = T::get_schema();
351 let name = avro_record_name(&schema).unwrap_or_else(|| {
352 panic!(
353 "the Avro schema of a registered type is not a named record, so nothing \
354 identifies its values on the publish path; register a record type under \
355 {subject:?}"
356 )
357 });
358 let definition = crate::avro::schema_json(&schema).unwrap_or_else(|err| {
359 panic!(
360 "the schema of the type registered under {subject:?} \
361 cannot be serialized: {err}"
362 )
363 });
364
365 let SchemaSource::Registry { subjects, .. } = &mut self.source else {
366 panic!("register is for a registry codec; a local one publishes a bare datum");
367 };
368 let table = Arc::get_mut(subjects).expect("not yet shared with a mount");
369 let local = Box::leak(Box::new(Prepared::build(schema, None).unwrap_or_else(
370 |err| panic!("the schema registered under {subject:?}: {err}"),
371 )));
372 if let Some(existing) = table.insert(
373 name,
374 Registered {
375 subject: subject.clone(),
376 local,
377 },
378 ) && existing.subject != subject
379 {
380 panic!(
381 "two types registered on this codec are both called {name:?} to serde, so a \
382 publish could not tell {:?} from {subject:?}; give one of them a distinct \
383 `#[serde(rename = \"..\")]`",
384 existing.subject,
385 );
386 }
387 if let Some(prefetch) = &self.prefetch {
388 prefetch.record(Registration {
389 subject,
390 schema_type: SchemaType::Avro,
391 definition,
392 });
393 }
394 self
395 }
396
397 pub fn resolve_onto(mut self, schema: Schema) -> Result<Self, KafkaError> {
409 let schema = Arc::new(schema);
410 if let SchemaSource::Local(prepared) = &self.source {
411 self.source = SchemaSource::Local(Box::leak(Box::new(Prepared::build(
414 prepared.schema.clone(),
415 Some((*schema).clone()),
416 )?)));
417 }
418 self.reader_schema = Some(schema);
419 Ok(self)
420 }
421
422 fn prepared_for(&self, id: u32) -> Result<&'static Prepared, CodecError> {
424 let SchemaSource::Registry {
425 registry, prepared, ..
426 } = &self.source
427 else {
428 unreachable!("a local codec never looks a schema id up");
429 };
430 if let Some(found) = prepared
431 .lock()
432 .expect("prepared schema cache mutex poisoned")
433 .get(&id)
434 {
435 return Ok(found);
436 }
437 let schema = registry.cached_schema(id).ok_or_else(|| {
442 decode_error(format!(
443 "no schema is cached for id {id}: the delivery's writer schema was not resolved \
444 before it reached the codec. Attach a SchemaPrefetch to the broker \
445 (KafkaBroker::schema_prefetch), and check the registry is reachable - a \
446 synchronous codec cannot fetch it here"
447 ))
448 })?;
449 let parsed = registry.parsed_avro(&schema).map_err(decode_source)?;
450 let built: &'static Prepared = Box::leak(Box::new(
451 Prepared::build((*parsed).clone(), self.reader_schema.as_deref().cloned())
452 .map_err(decode_source)?,
453 ));
454 Ok(prepared
455 .lock()
456 .expect("prepared schema cache mutex poisoned")
457 .entry(id)
458 .or_insert(built))
459 }
460}
461
462impl Codec for AvroCodec {
463 fn encode<T: Serialize>(&self, value: &T) -> Result<BytesMut, CodecError> {
464 let mut buf = BytesMut::new();
465 let prepared = match &self.source {
466 SchemaSource::Local(prepared) => *prepared,
467 SchemaSource::Registry {
468 registry,
469 subjects,
470 on_missing,
471 ..
472 } => {
473 let name = serde_name(value).ok_or_else(|| {
475 encode_error(
476 "this codec frames by the name serde gives a value's type, and this \
477 value has none - a bare number, a sequence or a map. Publish a named \
478 struct, or use a codec that needs no subject"
479 .to_owned(),
480 )
481 })?;
482 let registered = subjects.get(name).ok_or_else(|| {
483 encode_error(format!(
484 "no subject is registered for messages named {name:?}, so this codec has \
485 no id to frame them with. Add `.register::<{name}>(\"..\")` to it; the \
486 types it does carry are {:?}. A type that sets an Avro record name \
487 differing from its serde name registers under the Avro one and arrives \
488 here under the serde one, which also looks like this",
489 subjects.keys().collect::<Vec<_>>(),
490 ))
491 })?;
492 match registry.cached_subject(®istered.subject) {
493 Some(schema) => {
494 buf.put_u8(WIRE_MAGIC);
495 buf.put_u32(schema.id());
496 self.prepared_for(schema.id())
497 .map_err(|err| encode_error(err.to_string()))?
498 }
499 None if *on_missing == MissingSubject::PublishUnframed => registered.local,
502 None => {
503 return Err(encode_error(format!(
504 "no schema is cached for subject {:?}, which messages named \
505 {name:?} publish under, so there is no id to frame them with. \
506 Attach the SchemaPrefetch this codec was built from to the broker \
507 (KafkaBroker::schema_prefetch) so connect resolves it; if it was \
508 resolved and has since gone, the subject was deleted from the \
509 registry and MissingSubject says what to do about that",
510 registered.subject,
511 )));
512 }
513 }
514 }
515 };
516 let value = apache_avro::to_value(value)
520 .map_err(encode_source)?
521 .resolve(prepared.schema)
522 .map_err(|err| {
523 encode_error(format!(
524 "the value does not fit the schema this codec writes: {err}"
525 ))
526 })?;
527 prepared
528 .writer
529 .write_value(&mut (&mut buf).writer(), value)
530 .map_err(encode_source)?;
531 Ok(buf)
532 }
533
534 fn decode<T: DeserializeOwned>(&self, bytes: &[u8]) -> Result<T, CodecError> {
535 let (prepared, mut datum) = match &self.source {
536 SchemaSource::Local(prepared) => (*prepared, bytes),
537 SchemaSource::Registry { .. } => {
538 let (id, datum) = parse_envelope(bytes).ok_or_else(|| {
539 decode_error(format!(
540 "the delivery does not carry the Confluent wire format (a zero magic \
541 byte and a 4-byte schema id), so no writer schema can be named for it; \
542 its first bytes are {:02x?}",
543 &bytes[..bytes.len().min(8)],
544 ))
545 })?;
546 (self.prepared_for(id)?, datum)
547 }
548 };
549 let value = prepared
550 .reader
551 .read_value(&mut datum)
552 .map_err(decode_source)?;
553 apache_avro::from_value(&value).map_err(decode_source)
554 }
555}
556
557fn encode_error(message: String) -> CodecError {
558 CodecError::Encode(Box::new(KafkaError::malformed(message)))
559}
560
561fn encode_source(err: impl std::error::Error + Send + Sync + 'static) -> CodecError {
562 CodecError::Encode(Box::new(err))
563}
564
565fn decode_error(message: String) -> CodecError {
566 CodecError::Decode(Box::new(KafkaError::malformed(message)))
567}
568
569fn decode_source(err: impl std::error::Error + Send + Sync + 'static) -> CodecError {
570 CodecError::Decode(Box::new(err))
571}
572
573#[cfg(test)]
574mod tests {
575 use apache_avro::AvroSchema;
576 use serde::Deserialize;
577 use wiremock::matchers::{method, path};
578 use wiremock::{Mock, MockServer, ResponseTemplate};
579
580 use super::*;
581 use crate::schema_registry::SchemaType;
582
583 #[derive(Debug, PartialEq, Serialize, Deserialize, AvroSchema)]
584 #[serde(rename = "CodecOrder")]
585 struct Order {
586 id: i64,
587 item: String,
588 }
589
590 #[derive(Debug, PartialEq, Serialize, Deserialize, AvroSchema)]
593 #[serde(rename = "CodecOrder")]
594 struct OrderV2 {
595 id: i64,
596 item: String,
597 #[avro(default = r#""none""#)]
598 note: String,
599 }
600
601 fn order() -> Order {
602 Order {
603 id: 42,
604 item: "anvil".to_owned(),
605 }
606 }
607
608 #[test]
609 fn a_local_schema_round_trips_with_no_registry() {
610 let codec = AvroCodec::local(Order::get_schema()).expect("local");
611 let bytes = codec.encode(&order()).expect("encode");
612
613 assert_ne!(
614 &bytes[..],
615 br#"{"id":42,"item":"anvil"}"#,
616 "the wire is Avro"
617 );
618 assert_eq!(codec.decode::<Order>(&bytes).expect("decode"), order());
619 }
620
621 #[test]
622 fn a_local_codec_writes_no_envelope() {
623 let codec = AvroCodec::local(Order::get_schema()).expect("local");
624 let bytes = codec.encode(&order()).expect("encode");
625
626 assert!(
627 parse_envelope(&bytes).is_none() || bytes[0] != WIRE_MAGIC,
628 "a local codec puts a bare datum on the wire",
629 );
630 }
631
632 #[test]
633 fn a_value_the_schema_does_not_describe_is_refused() {
634 #[derive(Serialize)]
635 struct Other {
636 nope: String,
637 }
638 let codec = AvroCodec::local(Order::get_schema()).expect("local");
639
640 let err = codec
641 .encode(&Other {
642 nope: "x".to_owned(),
643 })
644 .expect_err("a value of another shape");
645 assert!(err.to_string().contains("does not fit the schema"));
646 }
647
648 #[test]
649 fn a_reader_schema_fills_what_the_writer_never_wrote() {
650 let writing = AvroCodec::local(Order::get_schema()).expect("writer");
651 let bytes = writing.encode(&order()).expect("encode");
652
653 let reading = AvroCodec::local(Order::get_schema()).expect("reader");
655 assert!(reading.decode::<OrderV2>(&bytes).is_err());
656
657 let resolving = AvroCodec::local(Order::get_schema())
658 .expect("reader")
659 .resolve_onto(OrderV2::get_schema())
660 .expect("resolve");
661 assert_eq!(
662 resolving.decode::<OrderV2>(&bytes).expect("resolve"),
663 OrderV2 {
664 id: 42,
665 item: "anvil".to_owned(),
666 note: "none".to_owned(),
667 },
668 );
669 }
670
671 async fn registry_with_order(id: u32) -> (MockServer, SchemaPrefetch) {
672 let server = MockServer::start().await;
673 let definition = crate::avro::schema_json(&Order::get_schema()).expect("json");
674 Mock::given(method("GET"))
675 .and(path("/subjects/orders-value/versions/latest"))
676 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
677 "id": id,
678 "version": 1,
679 "schema": definition,
680 "schemaType": "AVRO",
681 })))
682 .mount(&server)
683 .await;
684 let prefetch = SchemaPrefetch::new(SchemaRegistry::new(server.uri()));
685 (server, prefetch)
686 }
687
688 #[tokio::test]
689 async fn a_registry_codec_frames_with_the_subjects_id() {
690 let (_server, prefetch) = registry_with_order(11).await;
691 let codec = AvroCodec::registry(&prefetch).register::<Order>("orders-value");
692 prefetch.warm_subjects().await.expect("warm");
693
694 let bytes = codec.encode(&order()).expect("encode");
695 let (id, datum) = parse_envelope(&bytes).expect("the wire format");
696 assert_eq!(id, 11);
697 assert_ne!(datum, b"");
698
699 assert_eq!(codec.decode::<Order>(&bytes).expect("decode"), order());
701 }
702
703 #[tokio::test]
706 async fn one_codec_frames_each_registered_type_under_its_own_subject() {
707 #[derive(Debug, PartialEq, Serialize, Deserialize, AvroSchema)]
708 #[serde(rename = "CodecShipment")]
709 struct Shipment {
710 id: i64,
711 }
712
713 let (server, prefetch) = registry_with_order(11).await;
714 Mock::given(method("GET"))
715 .and(path("/subjects/shipments-value/versions/latest"))
716 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
717 "id": 12,
718 "version": 1,
719 "schema": crate::avro::schema_json(&Shipment::get_schema()).expect("json"),
720 "schemaType": "AVRO",
721 })))
722 .mount(&server)
723 .await;
724
725 let codec = AvroCodec::registry(&prefetch)
726 .register::<Order>("orders-value")
727 .register::<Shipment>("shipments-value");
728 prefetch.warm_subjects().await.expect("warm");
729
730 let (id, _) = parse_envelope(&codec.encode(&order()).expect("encode")).expect("framed");
731 assert_eq!(id, 11);
732 let shipped = codec.encode(&Shipment { id: 3 }).expect("encode");
733 let (id, _) = parse_envelope(&shipped).expect("framed");
734 assert_eq!(id, 12, "the second type framed under its own subject");
735 assert_eq!(
736 codec.decode::<Shipment>(&shipped).expect("decode"),
737 Shipment { id: 3 },
738 );
739 }
740
741 #[tokio::test]
742 async fn an_unregistered_type_names_itself_and_what_the_codec_carries() {
743 #[derive(Serialize)]
744 #[serde(rename = "Stranger")]
745 struct Stranger {
746 id: i64,
747 }
748
749 let (_server, prefetch) = registry_with_order(11).await;
750 let codec = AvroCodec::registry(&prefetch).register::<Order>("orders-value");
751 prefetch.warm_subjects().await.expect("warm");
752
753 let err = codec
754 .encode(&Stranger { id: 1 })
755 .expect_err("not registered");
756 assert!(err.to_string().contains("Stranger"));
757 assert!(err.to_string().contains("CodecOrder"), "{err}");
758 }
759
760 #[tokio::test]
761 async fn an_unnamed_value_is_refused_rather_than_guessed_at() {
762 let (_server, prefetch) = registry_with_order(11).await;
763 let codec = AvroCodec::registry(&prefetch).register::<Order>("orders-value");
764
765 let err = codec.encode(&7i64).expect_err("no name to key by");
766 assert!(err.to_string().contains("has none"));
767 }
768
769 #[tokio::test]
770 async fn an_unresolved_subject_names_the_attachment_it_is_missing() {
771 let (_server, prefetch) = registry_with_order(11).await;
772 let codec = AvroCodec::registry(&prefetch).register::<Order>("orders-value");
774
775 let err = codec.encode(&order()).expect_err("cold subject");
776 assert!(err.to_string().contains("schema_prefetch"));
777 }
778
779 #[tokio::test]
780 async fn an_unresolved_id_names_the_attachment_it_is_missing() {
781 let (_server, prefetch) = registry_with_order(11).await;
782 let codec = AvroCodec::registry(&prefetch).register::<Order>("orders-value");
783 let framed = [0u8, 0, 0, 0, 99, 1];
784
785 let err = codec.decode::<Order>(&framed).expect_err("cold id");
786 assert!(err.to_string().contains("no schema is cached for id 99"));
787 assert!(err.to_string().contains("SchemaPrefetch"));
788 }
789
790 #[tokio::test]
791 async fn an_unframed_delivery_on_a_registry_codec_is_refused() {
792 let (_server, prefetch) = registry_with_order(11).await;
793 let codec = AvroCodec::registry(&prefetch).register::<Order>("orders-value");
794
795 let err = codec
796 .decode::<Order>(br#"{"id":42}"#)
797 .expect_err("not framed");
798 assert!(err.to_string().contains("Confluent wire format"));
799 }
800
801 #[tokio::test]
802 async fn the_prefetch_resolves_a_deliverys_writer_schema() {
803 let (server, prefetch) = registry_with_order(11).await;
804 let definition = crate::avro::schema_json(&Order::get_schema()).expect("json");
805 Mock::given(method("GET"))
806 .and(path("/schemas/ids/11"))
807 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
808 "schema": definition,
809 "schemaType": "AVRO",
810 })))
811 .expect(1)
812 .mount(&server)
813 .await;
814
815 let codec = AvroCodec::registry(&prefetch).register::<Order>("orders-value");
816 let framed = [0u8, 0, 0, 0, 11, 84, 10, 97, 110, 118, 105, 108];
817
818 assert!(codec.decode::<Order>(&framed).is_err());
820
821 prefetch.warm_delivery(&framed).await;
823 assert_eq!(codec.decode::<Order>(&framed).expect("decode"), order());
824 }
825
826 #[tokio::test]
827 async fn a_missing_subject_fails_startup_rather_than_the_first_publish() {
828 let server = MockServer::start().await;
829 Mock::given(method("GET"))
830 .and(path("/subjects/absent-value/versions/latest"))
831 .respond_with(ResponseTemplate::new(404))
832 .mount(&server)
833 .await;
834 let prefetch = SchemaPrefetch::new(SchemaRegistry::new(server.uri()));
835 let _codec = AvroCodec::registry(&prefetch).register::<Order>("absent-value");
836
837 let err = prefetch.warm_subjects().await.expect_err("absent subject");
838 assert!(err.to_string().contains("absent-value"));
839 }
840
841 #[tokio::test]
845 async fn an_unsigned_field_against_a_long_schema_is_reported() {
846 #[derive(Serialize)]
849 #[serde(rename = "CodecOrder")]
850 struct Wide {
851 id: u64,
852 item: String,
853 }
854 let (_server, prefetch) = registry_with_order(11).await;
855 let codec = AvroCodec::registry(&prefetch).register::<Order>("orders-value");
856 prefetch.warm_subjects().await.expect("warm");
857
858 let err = codec
859 .encode(&Wide {
860 id: 7,
861 item: "anvil".to_owned(),
862 })
863 .expect_err("u64 against a long schema");
864 assert!(err.to_string().contains("does not fit the schema"));
865 }
866
867 #[tokio::test]
868 async fn the_registered_flavor_is_checked_before_it_is_parsed() {
869 let server = MockServer::start().await;
870 Mock::given(method("GET"))
871 .and(path("/subjects/json-value/versions/latest"))
872 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
873 "id": 3,
874 "version": 1,
875 "schema": "{\"type\":\"object\"}",
876 "schemaType": "JSON",
877 })))
878 .mount(&server)
879 .await;
880 let prefetch = SchemaPrefetch::new(SchemaRegistry::new(server.uri()));
881 let codec = AvroCodec::registry(&prefetch).register::<Order>("json-value");
882 prefetch.warm_subjects().await.expect("warm");
883
884 let err = codec.encode(&order()).expect_err("not an avro schema");
886 assert!(matches!(err, CodecError::Encode(_)));
887 let _ = SchemaType::Json;
888 }
889}