apache_avro/serde/derive.rs
1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements. See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership. The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License. You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied. See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use crate::Schema;
19use crate::schema::{
20 FixedSchema, Name, NamespaceRef, RecordField, RecordSchema, UnionSchema, UuidSchema,
21};
22use std::borrow::Cow;
23use std::collections::{HashMap, HashSet};
24
25/// Trait for types that serve as an Avro data model.
26///
27/// **Do not implement directly!** Either derive it or implement [`AvroSchemaComponent`] to get this trait
28/// through a blanket implementation.
29///
30/// ## Deriving `AvroSchema`
31///
32/// Using the custom derive requires that you enable the `"derive"` cargo
33/// feature in your `Cargo.toml`:
34///
35/// ```toml
36/// [dependencies]
37/// apache-avro = { version = "..", features = ["derive"] }
38/// ```
39///
40/// Then, you add the `#[derive(AvroSchema)]` annotation to your `struct` and
41/// `enum` type definition:
42///
43/// ```
44/// # use serde::{Serialize, Deserialize};
45/// # use apache_avro::AvroSchema;
46/// #[derive(AvroSchema, Serialize, Deserialize)]
47/// pub struct Foo {
48/// bar: Vec<Bar>,
49/// }
50///
51/// #[derive(AvroSchema, Serialize, Deserialize)]
52/// pub enum Bar {
53/// Spam,
54/// Maps
55/// }
56/// ```
57///
58/// This will implement [`AvroSchemaComponent`] for the type, and `AvroSchema`
59/// through the blanket implementation for `T: AvroSchemaComponent`.
60///
61/// When deriving `struct`s, every member must also implement `AvroSchemaComponent`.
62///
63/// ## Changing the generated schema
64///
65/// The derive macro will read both the `avro` and `serde` attributes to modify the generated schema.
66/// It will also check for compatibility between the various attributes.
67///
68/// #### Container attributes
69///
70/// - `#[serde(rename = "name")]`
71///
72// TODO: Should we check if `name` contains any dots? As that would imply a namespace
73/// Set the `name` of the schema to the given string. Defaults to the name of the type.
74///
75/// - `#[avro(namespace = "some.name.space")]`
76///
77/// Set the `namespace` of the schema. This will be the relative namespace if the schema is included
78/// in another schema.
79///
80/// - `#[avro(doc = "Some documentation")]`
81///
82/// Set the `doc` attribute of the schema. Defaults to the documentation of the type.
83///
84/// - `#[avro(default = r#"{"field": 42, "other": "Spam"}"#)]`
85///
86/// Provide the default value for this type when it is used in a field.
87///
88/// - `#[avro(alias = "name")]`
89///
90/// Set the `alias` attribute of the schema. Can be specified multiple times.
91///
92/// - `#[serde(rename_all = "camelCase")]`
93///
94/// Rename all the fields or variants in the schema to follow the given case convention. The possible values
95/// are `"lowercase"`, `"UPPERCASE"`, `"PascalCase"`, `"camelCase"`, `"snake_case"`, `"kebab-case"`,
96/// `"SCREAMING_SNAKE_CASE"`, `"SCREAMING-KEBAB-CASE"`.
97///
98/// - `#[serde(transparent)]`
99///
100/// Use the schema of the inner field directly. Is only allowed on structs with only one unskipped field.
101///
102///
103/// #### Variant attributes
104///
105/// - `#[serde(rename = "name")]`
106///
107/// Rename the variant to the given name.
108///
109///
110/// #### Field attributes
111///
112/// - `#[serde(rename = "name")]`
113///
114/// Rename the field name to the given name.
115///
116/// - `#[avro(doc = "Some documentation")]`
117///
118/// Set the `doc` attribute of the field. Defaults to the documentation of the field.
119///
120/// - `#[avro(default = ..)]`
121///
122/// Control the `default` attribute of the field. When not used, it will use [`AvroSchemaComponent::field_default`]
123/// to get the default value for a type. To remove the `default` attribute for a field, set `default` to `false`: `#[avro(default = false)]`.
124///
125/// To override or set a default value, provide a JSON string:
126///
127/// - Null: `#[avro(default = "null")]`
128/// - Boolean: `#[avro(default = "true")]`.
129/// - Number: `#[avro(default = "42")]` or `#[avro(default = "42.5")]`
130/// - String: `#[avro(default = r#""String needs extra quotes""#)]`.
131/// - Array: `#[avro(default = r#"["One", "Two", "Three"]"#)]`.
132/// - Object: `#[avro(default = r#"{"One": 1}"#)]`.
133///
134/// See [the specification](https://avro.apache.org/docs/++version++/specification/#schema-record)
135/// for details on how to map a type to a JSON value.
136///
137/// - `#[serde(alias = "name")]`
138///
139/// Set the `alias` attribute of the field. Can be specified multiple times.
140///
141/// - `#[serde(flatten)]`
142///
143/// Flatten the content of this field into the container it is defined in.
144///
145/// - `#[serde(skip)]`
146///
147/// Do not include this field in the schema.
148///
149/// - `#[serde(skip_serializing)]`
150///
151/// When combined with `#[serde(skip_deserializing)]`, don't include this field in the schema.
152/// Otherwise, it will be included in the schema and the `#[avro(default)]` attribute **must** be
153/// set. That value will be used for serializing.
154///
155/// - `#[serde(skip_serializing_if)]`
156///
157/// Conditionally use the value of the field or the value provided by `#[avro(default)]`. The
158/// `#[avro(default)]` attribute **must** be set.
159///
160/// - `#[avro(with)]` and `#[serde(with = "module")]`
161///
162/// Override the schema used for this field. See [Working with foreign types](#working-with-foreign-types).
163///
164/// #### Incompatible Serde attributes
165///
166/// The derive macro is compatible with most Serde attributes, but it is incompatible with
167/// the following attributes:
168///
169/// - Container attributes
170/// - `tag`
171/// - `content`
172/// - `untagged`
173/// - `variant_identifier`
174/// - `field_identifier`
175/// - `remote`
176/// - `rename_all(serialize = "..", deserialize = "..")` where `serialize` != `deserialize`
177/// - Variant attributes
178/// - `other`
179/// - `untagged`
180/// - Field attributes
181/// - `getter`
182///
183/// ## Working with foreign types
184///
185/// Most foreign types won't have a [`AvroSchema`] implementation. This crate implements it only
186/// for built-in types and [`uuid::Uuid`].
187///
188/// To still be able to derive schemas for fields of foreign types, the `#[avro(with)`]
189/// attribute can be used to get the schema for those fields. It can be used in two ways:
190///
191/// 1. In combination with `#[serde(with = "path::to::module)]`
192///
193/// To get the schema, it will call the functions `fn get_schema_in_ctxt(&mut HashSet<Name>, NamespaceRef) -> Schema`
194/// and `fn get_record_fields_in_ctxt(&mut HashSet<Name>, NamespaceRef) -> Option<Vec<RecordField>>` in the module provided
195/// to the Serde attribute. See [`AvroSchemaComponent`] for details on how to implement those
196/// functions.
197///
198/// 2. By providing a function directly, `#[avro(with = some_fn)]`.
199///
200/// To get the schema, it will call the function provided. It must have the signature
201/// `fn(&mut HashSet<Name>, NamespaceRef) -> Schema`. When this is used for a `transparent` struct, the
202/// default implementation of [`AvroSchemaComponent::get_record_fields_in_ctxt`] will be used.
203/// This is only recommended for primitive types, as the default implementation cannot be efficiently
204/// implemented for complex types.
205///
206pub trait AvroSchema {
207 /// Construct the full schema that represents this type.
208 ///
209 /// The returned schema is fully independent and contains only `Schema::Ref` to named types defined
210 /// earlier in the schema.
211 fn get_schema() -> Schema;
212}
213
214/// Trait for types that serve as fully defined components inside an Avro data model.
215///
216/// This trait can be derived with [`#[derive(AvroSchema)]`](AvroSchema) when the `derive` feature is enabled.
217///
218/// # Implementation guide
219///
220/// ### Implementation for returning primitive types
221/// When the schema you want to return is a primitive type (a type without a name), the function
222/// arguments can be ignored.
223///
224/// For example, you have a custom integer type:
225/// ```
226/// # use apache_avro::{Schema, serde::{AvroSchemaComponent}, schema::{Name, NamespaceRef, RecordField}};
227/// # use std::collections::HashSet;
228/// // Make sure to implement `Serialize` and `Deserialize` to use the right serialization methods
229/// pub struct U24([u8; 3]);
230/// impl AvroSchemaComponent for U24 {
231/// fn get_schema_in_ctxt(_: &mut HashSet<Name>, _: NamespaceRef) -> Schema {
232/// Schema::Int
233/// }
234///
235/// fn get_record_fields_in_ctxt(_: &mut HashSet<Name>, _: NamespaceRef) -> Option<Vec<RecordField>> {
236/// None // A Schema::Int is not a Schema::Record so there are no fields to return
237/// }
238///
239/// fn field_default() -> Option<serde_json::Value> {
240/// // Zero as default value. Can also be None if you don't want to provide a default value
241/// Some(0u8.into())
242/// }
243///}
244/// ```
245///
246/// ### Passthrough implementation
247///
248/// To construct a schema for a type is "transparent", such as for smart pointers, simply
249/// pass through the arguments to the inner type:
250/// ```
251/// # use apache_avro::{Schema, serde::{AvroSchemaComponent}, schema::{Name, NamespaceRef, RecordField}};
252/// # use serde::{Serialize, Deserialize};
253/// # use std::collections::HashSet;
254/// #[derive(Serialize, Deserialize)]
255/// #[serde(transparent)] // This attribute is important for all passthrough implementations!
256/// pub struct Transparent<T>(T);
257/// impl<T: AvroSchemaComponent> AvroSchemaComponent for Transparent<T> {
258/// fn get_schema_in_ctxt(named_schemas: &mut HashSet<Name>, enclosing_namespace: NamespaceRef) -> Schema {
259/// T::get_schema_in_ctxt(named_schemas, enclosing_namespace)
260/// }
261///
262/// fn get_record_fields_in_ctxt(named_schemas: &mut HashSet<Name>, enclosing_namespace: NamespaceRef) -> Option<Vec<RecordField>> {
263/// T::get_record_fields_in_ctxt(named_schemas, enclosing_namespace)
264/// }
265///
266/// fn field_default() -> Option<serde_json::Value> {
267/// T::field_default()
268/// }
269///}
270/// ```
271///
272/// ### Implementation for complex types
273/// When the schema you want to return is a complex type (a type with a name), special care has to
274/// be taken to avoid duplicate type definitions and getting the correct namespace.
275///
276/// Things to keep in mind:
277/// - If the fully qualified name already exists, return a [`Schema::Ref`]
278/// - Use the `AvroSchemaComponent` implementations to get the schemas for the subtypes
279/// - The ordering of fields in the schema **must** match with the ordering in Serde
280/// - Implement `get_record_fields_in_ctxt` as the default implementation has to be implemented
281/// with backtracking and a lot of cloning.
282/// - Even if your schema is not a record, still implement the function and just return `None`
283/// - Implement `field_default()` if you want to use `#[serde(skip_serializing{,_if})]`.
284///
285/// ```
286/// # use apache_avro::{Schema, serde::{AvroSchemaComponent}, schema::{Name, NamespaceRef, RecordField, RecordSchema}};
287/// # use serde::{Serialize, Deserialize};
288/// # use std::{time::Duration, collections::HashSet};
289/// pub struct Foo {
290/// one: String,
291/// two: i32,
292/// three: Option<Duration>
293/// }
294///
295/// impl AvroSchemaComponent for Foo {
296/// fn get_schema_in_ctxt(named_schemas: &mut HashSet<Name>, enclosing_namespace: NamespaceRef) -> Schema {
297/// // Create the fully qualified name for your type given the enclosing namespace
298/// let name = Name::new_with_enclosing_namespace("Foo", enclosing_namespace).expect("Name is valid");
299/// if named_schemas.contains(&name) {
300/// Schema::Ref { name }
301/// } else {
302/// let enclosing_namespace = name.namespace();
303/// // Do this before you start creating the schema, as otherwise recursive types will cause infinite recursion.
304/// named_schemas.insert(name.clone());
305/// let schema = Schema::Record(RecordSchema::builder()
306/// .name(name.clone())
307/// .fields(Self::get_record_fields_in_ctxt(named_schemas, enclosing_namespace).expect("Impossible!"))
308/// .build()
309/// );
310/// schema
311/// }
312/// }
313///
314/// fn get_record_fields_in_ctxt(named_schemas: &mut HashSet<Name>, enclosing_namespace: NamespaceRef) -> Option<Vec<RecordField>> {
315/// Some(vec![
316/// RecordField::builder()
317/// .name("one")
318/// .schema(String::get_schema_in_ctxt(named_schemas, enclosing_namespace))
319/// .build(),
320/// RecordField::builder()
321/// .name("two")
322/// .schema(i32::get_schema_in_ctxt(named_schemas, enclosing_namespace))
323/// .build(),
324/// RecordField::builder()
325/// .name("three")
326/// .schema(<Option<Duration>>::get_schema_in_ctxt(named_schemas, enclosing_namespace))
327/// .build(),
328/// ])
329/// }
330///
331/// fn field_default() -> Option<serde_json::Value> {
332/// // This type does not provide a default value
333/// None
334/// }
335///}
336/// ```
337pub trait AvroSchemaComponent {
338 /// Get the schema for this component
339 fn get_schema_in_ctxt(
340 named_schemas: &mut HashSet<Name>,
341 enclosing_namespace: NamespaceRef,
342 ) -> Schema;
343
344 /// Get the fields of this schema if it is a record.
345 ///
346 /// This returns `None` if the schema is not a record.
347 ///
348 /// The default implementation has to do a lot of extra work, so it is strongly recommended to
349 /// implement this function when manually implementing this trait.
350 fn get_record_fields_in_ctxt(
351 named_schemas: &mut HashSet<Name>,
352 enclosing_namespace: NamespaceRef,
353 ) -> Option<Vec<RecordField>> {
354 get_record_fields_in_ctxt(named_schemas, enclosing_namespace, Self::get_schema_in_ctxt)
355 }
356
357 /// The default value of this type when used for a record field.
358 ///
359 /// `None` means no default value, which is also the default implementation.
360 ///
361 /// Implementations of this trait provided by this crate return `None` except for `Option<T>`
362 /// which returns `Some(serde_json::Value::Null)`.
363 fn field_default() -> Option<serde_json::Value> {
364 None
365 }
366}
367
368/// Get the record fields from `schema_fn` without polluting `named_schemas` or causing duplicate names
369///
370/// This is public so the derive macro can use it for `#[avro(with = ||)]` and `#[avro(with = path)]`
371pub fn get_record_fields_in_ctxt(
372 named_schemas: &mut HashSet<Name>,
373 enclosing_namespace: NamespaceRef,
374 schema_fn: fn(named_schemas: &mut HashSet<Name>, enclosing_namespace: NamespaceRef) -> Schema,
375) -> Option<Vec<RecordField>> {
376 let mut record = match schema_fn(named_schemas, enclosing_namespace) {
377 Schema::Record(record) => record,
378 Schema::Ref { name } => {
379 // This schema already exists in `named_schemas` so temporarily remove it so we can
380 // get the actual schema.
381 assert!(
382 named_schemas.remove(&name),
383 "Name '{name}' should exist in `named_schemas` otherwise Ref is invalid: {named_schemas:?}"
384 );
385 // Get the schema
386 let schema = schema_fn(named_schemas, enclosing_namespace);
387 // Reinsert the old value
388 named_schemas.insert(name);
389
390 // Now check if we actually got a record and return the fields if that is the case
391 let Schema::Record(record) = schema else {
392 return None;
393 };
394 return Some(record.fields);
395 }
396 _ => return None,
397 };
398 // This schema did not yet exist in `named_schemas`, so we need to remove it if and only if
399 // it isn't used somewhere in the schema (recursive type).
400
401 // Find the first Schema::Ref that has the target name
402 fn find_first_ref<'a>(schema: &'a mut Schema, target: &Name) -> Option<&'a mut Schema> {
403 match schema {
404 Schema::Ref { name } if name == target => Some(schema),
405 Schema::Array(array) => find_first_ref(&mut array.items, target),
406 Schema::Map(map) => find_first_ref(&mut map.types, target),
407 Schema::Union(union) => {
408 for schema in &mut union.schemas {
409 if let Some(schema) = find_first_ref(schema, target) {
410 return Some(schema);
411 }
412 }
413 None
414 }
415 Schema::Record(record) => {
416 assert_ne!(
417 &record.name, target,
418 "Only expecting a Ref named {target:?}"
419 );
420 for field in &mut record.fields {
421 if let Some(schema) = find_first_ref(&mut field.schema, target) {
422 return Some(schema);
423 }
424 }
425 None
426 }
427 _ => None,
428 }
429 }
430
431 // Prepare the fields for the new record. All named types will become references.
432 let new_fields = record
433 .fields
434 .iter()
435 .map(|field| RecordField {
436 name: field.name.clone(),
437 doc: field.doc.clone(),
438 aliases: field.aliases.clone(),
439 default: field.default.clone(),
440 schema: if field.schema.is_named() {
441 Schema::Ref {
442 name: field.schema.name().expect("Schema is named").clone(),
443 }
444 } else {
445 field.schema.clone()
446 },
447 custom_attributes: field.custom_attributes.clone(),
448 })
449 .collect();
450
451 // Remove the name in case it is not used
452 named_schemas.remove(&record.name);
453
454 // Find the first reference to this schema so we can replace it with the actual schema
455 for field in &mut record.fields {
456 if let Some(schema) = find_first_ref(&mut field.schema, &record.name) {
457 let new_schema = RecordSchema {
458 name: record.name,
459 aliases: record.aliases,
460 doc: record.doc,
461 fields: new_fields,
462 lookup: record.lookup,
463 attributes: record.attributes,
464 };
465
466 let name = match std::mem::replace(schema, Schema::Record(new_schema)) {
467 Schema::Ref { name } => name,
468 schema => {
469 panic!("Only expected `Schema::Ref` from `find_first_ref`, got: {schema:?}")
470 }
471 };
472
473 // The schema is used, so reinsert it
474 named_schemas.insert(name.clone());
475
476 break;
477 }
478 }
479
480 Some(record.fields)
481}
482
483impl<T> AvroSchema for T
484where
485 T: AvroSchemaComponent + ?Sized,
486{
487 fn get_schema() -> Schema {
488 T::get_schema_in_ctxt(&mut HashSet::default(), None)
489 }
490}
491
492macro_rules! impl_schema (
493 ($type:ty, $variant_constructor:expr) => (
494 impl AvroSchemaComponent for $type {
495 fn get_schema_in_ctxt(_: &mut HashSet<Name>, _: NamespaceRef) -> Schema {
496 $variant_constructor
497 }
498
499 fn get_record_fields_in_ctxt(_: &mut HashSet<Name>, _: NamespaceRef) -> Option<Vec<RecordField>> {
500 None
501 }
502 }
503 );
504);
505
506impl_schema!(bool, Schema::Boolean);
507impl_schema!(i8, Schema::Int);
508impl_schema!(i16, Schema::Int);
509impl_schema!(i32, Schema::Int);
510impl_schema!(i64, Schema::Long);
511impl_schema!(u8, Schema::Int);
512impl_schema!(u16, Schema::Int);
513impl_schema!(u32, Schema::Long);
514impl_schema!(f32, Schema::Float);
515impl_schema!(f64, Schema::Double);
516impl_schema!(String, Schema::String);
517impl_schema!(str, Schema::String);
518impl_schema!(char, Schema::String);
519impl_schema!((), Schema::Null);
520
521macro_rules! impl_passthrough_schema (
522 ($type:ty where T: AvroSchemaComponent + ?Sized $(+ $bound:tt)*) => (
523 impl<T: AvroSchemaComponent $(+ $bound)* + ?Sized> AvroSchemaComponent for $type {
524 fn get_schema_in_ctxt(named_schemas: &mut HashSet<Name>, enclosing_namespace: NamespaceRef) -> Schema {
525 T::get_schema_in_ctxt(named_schemas, enclosing_namespace)
526 }
527
528 fn get_record_fields_in_ctxt(named_schemas: &mut HashSet<Name>, enclosing_namespace: NamespaceRef) -> Option<Vec<RecordField>> {
529 T::get_record_fields_in_ctxt(named_schemas, enclosing_namespace)
530 }
531
532 fn field_default() -> Option<serde_json::Value> {
533 T::field_default()
534 }
535 }
536 );
537);
538
539impl_passthrough_schema!(&T where T: AvroSchemaComponent + ?Sized);
540impl_passthrough_schema!(&mut T where T: AvroSchemaComponent + ?Sized);
541impl_passthrough_schema!(Box<T> where T: AvroSchemaComponent + ?Sized);
542impl_passthrough_schema!(Cow<'_, T> where T: AvroSchemaComponent + ?Sized + ToOwned);
543impl_passthrough_schema!(std::sync::Mutex<T> where T: AvroSchemaComponent + ?Sized);
544
545macro_rules! impl_array_schema (
546 ($type:ty where T: AvroSchemaComponent) => (
547 impl<T: AvroSchemaComponent> AvroSchemaComponent for $type {
548 fn get_schema_in_ctxt(named_schemas: &mut HashSet<Name>, enclosing_namespace: NamespaceRef) -> Schema {
549 Schema::array(T::get_schema_in_ctxt(named_schemas, enclosing_namespace)).build()
550 }
551
552 fn get_record_fields_in_ctxt(_: &mut HashSet<Name>, _: NamespaceRef) -> Option<Vec<RecordField>> {
553 None
554 }
555 }
556 );
557);
558
559impl_array_schema!([T] where T: AvroSchemaComponent);
560impl_array_schema!(Vec<T> where T: AvroSchemaComponent);
561// This doesn't work as the macro doesn't allow specifying the N parameter
562// impl_array_schema!([T; N] where T: AvroSchemaComponent);
563
564impl<const N: usize, T> AvroSchemaComponent for [T; N]
565where
566 T: AvroSchemaComponent,
567{
568 fn get_schema_in_ctxt(
569 named_schemas: &mut HashSet<Name>,
570 enclosing_namespace: NamespaceRef,
571 ) -> Schema {
572 Schema::array(T::get_schema_in_ctxt(named_schemas, enclosing_namespace)).build()
573 }
574
575 fn get_record_fields_in_ctxt(
576 _: &mut HashSet<Name>,
577 _: NamespaceRef,
578 ) -> Option<Vec<RecordField>> {
579 None
580 }
581}
582
583impl<T> AvroSchemaComponent for HashMap<String, T>
584where
585 T: AvroSchemaComponent,
586{
587 fn get_schema_in_ctxt(
588 named_schemas: &mut HashSet<Name>,
589 enclosing_namespace: NamespaceRef,
590 ) -> Schema {
591 Schema::map(T::get_schema_in_ctxt(named_schemas, enclosing_namespace)).build()
592 }
593
594 fn get_record_fields_in_ctxt(
595 _: &mut HashSet<Name>,
596 _: NamespaceRef,
597 ) -> Option<Vec<RecordField>> {
598 None
599 }
600}
601
602impl<T> AvroSchemaComponent for Option<T>
603where
604 T: AvroSchemaComponent,
605{
606 fn get_schema_in_ctxt(
607 named_schemas: &mut HashSet<Name>,
608 enclosing_namespace: NamespaceRef,
609 ) -> Schema {
610 let variants = vec![
611 Schema::Null,
612 T::get_schema_in_ctxt(named_schemas, enclosing_namespace),
613 ];
614
615 Schema::Union(
616 UnionSchema::new(variants).expect("Option<T> must produce a valid (non-nested) union"),
617 )
618 }
619
620 fn get_record_fields_in_ctxt(
621 _: &mut HashSet<Name>,
622 _: NamespaceRef,
623 ) -> Option<Vec<RecordField>> {
624 None
625 }
626
627 fn field_default() -> Option<serde_json::Value> {
628 Some(serde_json::Value::Null)
629 }
630}
631
632impl AvroSchemaComponent for core::time::Duration {
633 /// The schema is [`Schema::Duration`] with the name `duration`.
634 ///
635 /// This is a lossy conversion as this Avro type does not store the amount of nanoseconds.
636 fn get_schema_in_ctxt(
637 named_schemas: &mut HashSet<Name>,
638 enclosing_namespace: NamespaceRef,
639 ) -> Schema {
640 let name = Name::new_with_enclosing_namespace("duration", enclosing_namespace)
641 .expect("Name is valid");
642 if named_schemas.contains(&name) {
643 Schema::Ref { name }
644 } else {
645 let schema = Schema::Duration(FixedSchema {
646 name: name.clone(),
647 aliases: None,
648 doc: None,
649 size: 12,
650 attributes: Default::default(),
651 });
652 named_schemas.insert(name);
653 schema
654 }
655 }
656
657 fn get_record_fields_in_ctxt(
658 _: &mut HashSet<Name>,
659 _: NamespaceRef,
660 ) -> Option<Vec<RecordField>> {
661 None
662 }
663}
664
665impl AvroSchemaComponent for uuid::Uuid {
666 /// The schema is [`Schema::Uuid`] with the name `uuid`.
667 ///
668 /// The underlying schema is [`Schema::Fixed`] with a size of 16.
669 fn get_schema_in_ctxt(
670 named_schemas: &mut HashSet<Name>,
671 enclosing_namespace: NamespaceRef,
672 ) -> Schema {
673 let name =
674 Name::new_with_enclosing_namespace("uuid", enclosing_namespace).expect("Name is valid");
675 if named_schemas.contains(&name) {
676 Schema::Ref { name }
677 } else {
678 let schema = Schema::Uuid(UuidSchema::Fixed(FixedSchema {
679 name: name.clone(),
680 aliases: None,
681 doc: None,
682 size: 16,
683 attributes: Default::default(),
684 }));
685 named_schemas.insert(name);
686 schema
687 }
688 }
689
690 fn get_record_fields_in_ctxt(
691 _: &mut HashSet<Name>,
692 _: NamespaceRef,
693 ) -> Option<Vec<RecordField>> {
694 None
695 }
696}
697
698impl AvroSchemaComponent for u64 {
699 /// The schema is [`Schema::Fixed`] of size 8 with the name `u64`.
700 fn get_schema_in_ctxt(
701 named_schemas: &mut HashSet<Name>,
702 enclosing_namespace: NamespaceRef,
703 ) -> Schema {
704 let name =
705 Name::new_with_enclosing_namespace("u64", enclosing_namespace).expect("Name is valid");
706 if named_schemas.contains(&name) {
707 Schema::Ref { name }
708 } else {
709 let schema = Schema::Fixed(FixedSchema {
710 name: name.clone(),
711 aliases: None,
712 doc: None,
713 size: 8,
714 attributes: Default::default(),
715 });
716 named_schemas.insert(name);
717 schema
718 }
719 }
720
721 fn get_record_fields_in_ctxt(
722 _: &mut HashSet<Name>,
723 _: NamespaceRef,
724 ) -> Option<Vec<RecordField>> {
725 None
726 }
727}
728
729impl AvroSchemaComponent for u128 {
730 /// The schema is [`Schema::Fixed`] of size 16 with the name `u128`.
731 fn get_schema_in_ctxt(
732 named_schemas: &mut HashSet<Name>,
733 enclosing_namespace: NamespaceRef,
734 ) -> Schema {
735 let name =
736 Name::new_with_enclosing_namespace("u128", enclosing_namespace).expect("Name is valid");
737 if named_schemas.contains(&name) {
738 Schema::Ref { name }
739 } else {
740 let schema = Schema::Fixed(FixedSchema {
741 name: name.clone(),
742 aliases: None,
743 doc: None,
744 size: 16,
745 attributes: Default::default(),
746 });
747 named_schemas.insert(name);
748 schema
749 }
750 }
751
752 fn get_record_fields_in_ctxt(
753 _: &mut HashSet<Name>,
754 _: NamespaceRef,
755 ) -> Option<Vec<RecordField>> {
756 None
757 }
758}
759
760impl AvroSchemaComponent for i128 {
761 /// The schema is [`Schema::Fixed`] of size 16 with the name `i128`.
762 fn get_schema_in_ctxt(
763 named_schemas: &mut HashSet<Name>,
764 enclosing_namespace: NamespaceRef,
765 ) -> Schema {
766 let name =
767 Name::new_with_enclosing_namespace("i128", enclosing_namespace).expect("Name is valid");
768 if named_schemas.contains(&name) {
769 Schema::Ref { name }
770 } else {
771 let schema = Schema::Fixed(FixedSchema {
772 name: name.clone(),
773 aliases: None,
774 doc: None,
775 size: 16,
776 attributes: Default::default(),
777 });
778 named_schemas.insert(name);
779 schema
780 }
781 }
782
783 fn get_record_fields_in_ctxt(
784 _: &mut HashSet<Name>,
785 _: NamespaceRef,
786 ) -> Option<Vec<RecordField>> {
787 None
788 }
789}
790
791#[cfg(test)]
792mod tests {
793 use crate::{
794 AvroSchema, Schema,
795 schema::{FixedSchema, Name},
796 };
797 use apache_avro_test_helper::TestResult;
798
799 #[test]
800 fn avro_rs_401_str() -> TestResult {
801 let schema = str::get_schema();
802 assert_eq!(schema, Schema::String);
803
804 Ok(())
805 }
806
807 #[test]
808 fn avro_rs_401_references() -> TestResult {
809 let schema_ref = <&str>::get_schema();
810 let schema_ref_mut = <&mut str>::get_schema();
811
812 assert_eq!(schema_ref, Schema::String);
813 assert_eq!(schema_ref_mut, Schema::String);
814
815 Ok(())
816 }
817
818 #[test]
819 fn avro_rs_401_slice() -> TestResult {
820 let schema = <[u8]>::get_schema();
821 assert_eq!(schema, Schema::array(Schema::Int).build());
822
823 Ok(())
824 }
825
826 #[test]
827 fn avro_rs_401_array() -> TestResult {
828 let schema = <[u8; 55]>::get_schema();
829 assert_eq!(schema, Schema::array(Schema::Int).build());
830
831 Ok(())
832 }
833
834 #[test]
835 fn avro_rs_401_option_ref_slice_array() -> TestResult {
836 let schema = <Option<&[[u8; 55]]>>::get_schema();
837 assert_eq!(
838 schema,
839 Schema::union(vec![
840 Schema::Null,
841 Schema::array(Schema::array(Schema::Int).build()).build()
842 ])?
843 );
844
845 Ok(())
846 }
847
848 #[test]
849 fn avro_rs_414_char() -> TestResult {
850 let schema = char::get_schema();
851 assert_eq!(schema, Schema::String);
852
853 Ok(())
854 }
855
856 #[test]
857 fn avro_rs_414_u64() -> TestResult {
858 let schema = u64::get_schema();
859 assert_eq!(
860 schema,
861 Schema::Fixed(FixedSchema {
862 name: Name::new("u64")?,
863 aliases: None,
864 doc: None,
865 size: 8,
866 attributes: Default::default(),
867 })
868 );
869
870 Ok(())
871 }
872
873 #[test]
874 fn avro_rs_414_i128() -> TestResult {
875 let schema = i128::get_schema();
876 assert_eq!(
877 schema,
878 Schema::Fixed(FixedSchema {
879 name: Name::new("i128")?,
880 aliases: None,
881 doc: None,
882 size: 16,
883 attributes: Default::default(),
884 })
885 );
886
887 Ok(())
888 }
889
890 #[test]
891 fn avro_rs_414_u128() -> TestResult {
892 let schema = u128::get_schema();
893 assert_eq!(
894 schema,
895 Schema::Fixed(FixedSchema {
896 name: Name::new("u128")?,
897 aliases: None,
898 doc: None,
899 size: 16,
900 attributes: Default::default(),
901 })
902 );
903
904 Ok(())
905 }
906
907 #[test]
908 fn avro_rs_486_unit() -> TestResult {
909 let schema = <()>::get_schema();
910 assert_eq!(schema, Schema::Null);
911
912 Ok(())
913 }
914
915 #[test]
916 #[should_panic(
917 expected = "Option<T> must produce a valid (non-nested) union: Error { details: Unions cannot contain duplicate types, found at least two Null }"
918 )]
919 fn avro_rs_489_some_unit() {
920 <Option<()>>::get_schema();
921 }
922
923 #[test]
924 #[should_panic(
925 expected = "Option<T> must produce a valid (non-nested) union: Error { details: Unions may not directly contain a union }"
926 )]
927 fn avro_rs_489_option_option() {
928 <Option<Option<i32>>>::get_schema();
929 }
930}