Skip to main content

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 std::{
19    borrow::Cow,
20    collections::{HashMap, HashSet},
21};
22
23use crate::{
24    Schema,
25    schema::{FixedSchema, Name, NamespaceRef, RecordField, RecordSchema, UnionSchema, UuidSchema},
26};
27
28/// Trait for types that serve as an Avro data model.
29///
30/// **Do not implement directly!** Either derive it or implement [`AvroSchemaComponent`] to get this trait
31/// through a blanket implementation.
32///
33/// ## Deriving `AvroSchema`
34///
35/// Using the custom derive requires that you enable the `"derive"` cargo
36/// feature in your `Cargo.toml`:
37///
38/// ```toml
39/// [dependencies]
40/// apache-avro = { version = "..", features = ["derive"] }
41/// ```
42///
43/// Then, you add the `#[derive(AvroSchema)]` annotation to your `struct` and
44/// `enum` type definition:
45///
46/// ```
47/// # use serde::{Serialize, Deserialize};
48/// # use apache_avro::AvroSchema;
49/// #[derive(AvroSchema, Serialize, Deserialize)]
50/// pub struct Foo {
51///     bar: Vec<Bar>,
52/// }
53///
54/// #[derive(AvroSchema, Serialize, Deserialize)]
55/// pub enum Bar {
56///     Spam,
57///     Maps
58/// }
59/// ```
60///
61/// This will implement [`AvroSchemaComponent`] for the type, and `AvroSchema`
62/// through the blanket implementation for `T: AvroSchemaComponent`.
63///
64/// When deriving `struct`s, every member must also implement `AvroSchemaComponent`.
65///
66/// ## Changing the generated schema
67///
68/// The derive macro will read both the `avro` and `serde` attributes to modify the generated schema.
69/// It will also check for compatibility between the various attributes.
70///
71/// #### Container attributes
72///
73///  - `#[serde(rename = "name")]`
74///
75// TODO: Should we check if `name` contains any dots? As that would imply a namespace
76///    Set the `name` of the schema to the given string. Defaults to the name of the type.
77///
78///  - `#[avro(namespace = "some.name.space")]`
79///
80///    Set the `namespace` of the schema. This will be the relative namespace if the schema is included
81///    in another schema.
82///
83///  - `#[avro(doc = "Some documentation")]`
84///
85///    Set the `doc` attribute of the schema. Defaults to the documentation of the type.
86///
87///  - `#[avro(default = r#"{"field": 42, "other": "Spam"}"#)]`
88///
89///    Provide the default value for this type when it is used in a field.
90///
91///  - `#[avro(alias = "name")]`
92///
93///    Set the `alias` attribute of the schema. Can be specified multiple times.
94///
95///  - `#[serde(rename_all = "camelCase")]`
96///
97///    Rename all the fields or variants in the schema to follow the given case convention. The possible values
98///    are `"lowercase"`, `"UPPERCASE"`, `"PascalCase"`, `"camelCase"`, `"snake_case"`, `"kebab-case"`,
99///    `"SCREAMING_SNAKE_CASE"`, `"SCREAMING-KEBAB-CASE"`.
100///
101///  - `#[serde(transparent)]`
102///
103///    Use the schema of the inner field directly. Is only allowed on structs with only one unskipped field.
104///
105///
106/// #### Variant attributes
107///
108///  - `#[serde(rename = "name")]`
109///
110///    Rename the variant to the given name.
111///
112///
113/// #### Field attributes
114///
115///  - `#[serde(rename = "name")]`
116///
117///    Rename the field name to the given name.
118///
119///  - `#[avro(doc = "Some documentation")]`
120///
121///    Set the `doc` attribute of the field. Defaults to the documentation of the field.
122///
123///  - `#[avro(default = ..)]`
124///
125///    Control the `default` attribute of the field. When not used, it will use [`AvroSchemaComponent::field_default`]
126///    to get the default value for a type. To remove the `default` attribute for a field, set `default` to `false`: `#[avro(default = false)]`.
127///
128///    To override or set a default value, provide a JSON string:
129///
130///      - Null: `#[avro(default = "null")]`
131///      - Boolean: `#[avro(default = "true")]`.
132///      - Number: `#[avro(default = "42")]` or `#[avro(default = "42.5")]`
133///      - String: `#[avro(default = r#""String needs extra quotes""#)]`.
134///      - Array: `#[avro(default = r#"["One", "Two", "Three"]"#)]`.
135///      - Object: `#[avro(default = r#"{"One": 1}"#)]`.
136///
137///    See [the specification](https://avro.apache.org/docs/++version++/specification/#schema-record)
138///    for details on how to map a type to a JSON value.
139///
140///  - `#[serde(alias = "name")]`
141///
142///    Set the `alias` attribute of the field. Can be specified multiple times.
143///
144///  - `#[serde(flatten)]`
145///
146///    Flatten the content of this field into the container it is defined in.
147///
148///  - `#[serde(skip)]`
149///
150///    Do not include this field in the schema.
151///
152///  - `#[serde(skip_serializing)]`
153///
154///    When combined with `#[serde(skip_deserializing)]`, don't include this field in the schema.
155///    Otherwise, it will be included in the schema and the `#[avro(default)]` attribute **must** be
156///    set. That value will be used for serializing.
157///
158///  - `#[serde(skip_serializing_if)]`
159///
160///    Conditionally use the value of the field or the value provided by `#[avro(default)]`. The
161///    `#[avro(default)]` attribute **must** be set.
162///
163///  - `#[avro(with)]` and `#[serde(with = "module")]`
164///
165///    Override the schema used for this field. See [Working with foreign types](#working-with-foreign-types).
166///
167/// #### Incompatible Serde attributes
168///
169/// The derive macro is compatible with most Serde attributes, but it is incompatible with
170/// the following attributes:
171///
172/// - Container attributes
173///     - `tag`
174///     - `content`
175///     - `untagged`
176///     - `variant_identifier`
177///     - `field_identifier`
178///     - `rename_all(serialize = "..", deserialize = "..")` where `serialize` != `deserialize`
179/// - Variant attributes
180///     - `other`
181///     - `untagged`
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)]`
371#[doc(hidden)]
372pub fn get_record_fields_in_ctxt(
373    named_schemas: &mut HashSet<Name>,
374    enclosing_namespace: NamespaceRef,
375    schema_fn: fn(named_schemas: &mut HashSet<Name>, enclosing_namespace: NamespaceRef) -> Schema,
376) -> Option<Vec<RecordField>> {
377    let mut record = match schema_fn(named_schemas, enclosing_namespace) {
378        Schema::Record(record) => record,
379        Schema::Ref { name } => {
380            // This schema already exists in `named_schemas` so temporarily remove it so we can
381            // get the actual schema.
382            assert!(
383                named_schemas.remove(&name),
384                "Name '{name}' should exist in `named_schemas` otherwise Ref is invalid: {named_schemas:?}"
385            );
386            // Get the schema
387            let schema = schema_fn(named_schemas, enclosing_namespace);
388            // Reinsert the old value
389            named_schemas.insert(name);
390
391            // Now check if we actually got a record and return the fields if that is the case
392            let Schema::Record(record) = schema else {
393                return None;
394            };
395            return Some(record.fields);
396        }
397        _ => return None,
398    };
399    // This schema did not yet exist in `named_schemas`, so we need to remove it if and only if
400    // it isn't used somewhere in the schema (recursive type).
401
402    // Find the first Schema::Ref that has the target name
403    fn find_first_ref<'a>(schema: &'a mut Schema, target: &Name) -> Option<&'a mut Schema> {
404        match schema {
405            Schema::Ref { name } if name == target => Some(schema),
406            Schema::Array(array) => find_first_ref(&mut array.items, target),
407            Schema::Map(map) => find_first_ref(&mut map.types, target),
408            Schema::Union(union) => {
409                for schema in &mut union.schemas {
410                    if let Some(schema) = find_first_ref(schema, target) {
411                        return Some(schema);
412                    }
413                }
414                None
415            }
416            Schema::Record(record) => {
417                assert_ne!(
418                    &record.name, target,
419                    "Only expecting a Ref named {target:?}"
420                );
421                for field in &mut record.fields {
422                    if let Some(schema) = find_first_ref(&mut field.schema, target) {
423                        return Some(schema);
424                    }
425                }
426                None
427            }
428            _ => None,
429        }
430    }
431
432    // Prepare the fields for the new record. All named types will become references.
433    let new_fields = record
434        .fields
435        .iter()
436        .map(|field| RecordField {
437            name: field.name.clone(),
438            doc: field.doc.clone(),
439            aliases: field.aliases.clone(),
440            default: field.default.clone(),
441            schema: if field.schema.is_named() {
442                Schema::Ref {
443                    name: field.schema.name().expect("Schema is named").clone(),
444                }
445            } else {
446                field.schema.clone()
447            },
448            custom_attributes: field.custom_attributes.clone(),
449        })
450        .collect();
451
452    // Remove the name in case it is not used
453    named_schemas.remove(&record.name);
454
455    // Find the first reference to this schema so we can replace it with the actual schema
456    for field in &mut record.fields {
457        if let Some(schema) = find_first_ref(&mut field.schema, &record.name) {
458            let new_schema = RecordSchema {
459                name: record.name,
460                aliases: record.aliases,
461                doc: record.doc,
462                fields: new_fields,
463                lookup: record.lookup,
464                attributes: record.attributes,
465            };
466
467            let name = match std::mem::replace(schema, Schema::Record(new_schema)) {
468                Schema::Ref { name } => name,
469                schema => {
470                    panic!("Only expected `Schema::Ref` from `find_first_ref`, got: {schema:?}")
471                }
472            };
473
474            // The schema is used, so reinsert it
475            named_schemas.insert(name.clone());
476
477            break;
478        }
479    }
480
481    Some(record.fields)
482}
483
484impl<T> AvroSchema for T
485where
486    T: AvroSchemaComponent + ?Sized,
487{
488    fn get_schema() -> Schema {
489        T::get_schema_in_ctxt(&mut HashSet::default(), None)
490    }
491}
492
493macro_rules! impl_schema (
494    ($type:ty, $variant_constructor:expr) => (
495        impl AvroSchemaComponent for $type {
496            fn get_schema_in_ctxt(_: &mut HashSet<Name>, _: NamespaceRef) -> Schema {
497                $variant_constructor
498            }
499
500            fn get_record_fields_in_ctxt(_: &mut HashSet<Name>, _: NamespaceRef) -> Option<Vec<RecordField>> {
501                None
502            }
503        }
504    );
505);
506
507impl_schema!(bool, Schema::Boolean);
508impl_schema!(i8, Schema::Int);
509impl_schema!(i16, Schema::Int);
510impl_schema!(i32, Schema::Int);
511impl_schema!(i64, Schema::Long);
512impl_schema!(u8, Schema::Int);
513impl_schema!(u16, Schema::Int);
514impl_schema!(u32, Schema::Long);
515impl_schema!(f32, Schema::Float);
516impl_schema!(f64, Schema::Double);
517impl_schema!(String, Schema::String);
518impl_schema!(str, Schema::String);
519impl_schema!(char, Schema::String);
520impl_schema!((), Schema::Null);
521
522macro_rules! impl_passthrough_schema (
523    ($type:ty where T: AvroSchemaComponent + ?Sized $(+ $bound:tt)*) => (
524        impl<T: AvroSchemaComponent $(+ $bound)* + ?Sized> AvroSchemaComponent for $type {
525            fn get_schema_in_ctxt(named_schemas: &mut HashSet<Name>, enclosing_namespace: NamespaceRef) -> Schema {
526                T::get_schema_in_ctxt(named_schemas, enclosing_namespace)
527            }
528
529            fn get_record_fields_in_ctxt(named_schemas: &mut HashSet<Name>, enclosing_namespace: NamespaceRef) -> Option<Vec<RecordField>> {
530                T::get_record_fields_in_ctxt(named_schemas, enclosing_namespace)
531            }
532
533            fn field_default() -> Option<serde_json::Value> {
534                T::field_default()
535            }
536        }
537    );
538);
539
540impl_passthrough_schema!(&T where T: AvroSchemaComponent + ?Sized);
541impl_passthrough_schema!(&mut T where T: AvroSchemaComponent + ?Sized);
542impl_passthrough_schema!(Box<T> where T: AvroSchemaComponent + ?Sized);
543impl_passthrough_schema!(Cow<'_, T> where T: AvroSchemaComponent + ?Sized + ToOwned);
544impl_passthrough_schema!(std::sync::Mutex<T> where T: AvroSchemaComponent + ?Sized);
545
546macro_rules! impl_array_schema (
547    ($type:ty where T: AvroSchemaComponent) => (
548        impl<T: AvroSchemaComponent> AvroSchemaComponent for $type {
549            fn get_schema_in_ctxt(named_schemas: &mut HashSet<Name>, enclosing_namespace: NamespaceRef) -> Schema {
550                Schema::array(T::get_schema_in_ctxt(named_schemas, enclosing_namespace)).build()
551            }
552
553            fn get_record_fields_in_ctxt(_: &mut HashSet<Name>, _: NamespaceRef) -> Option<Vec<RecordField>> {
554                None
555            }
556        }
557    );
558);
559
560impl_array_schema!([T] where T: AvroSchemaComponent);
561impl_array_schema!(Vec<T> where T: AvroSchemaComponent);
562
563impl<T> AvroSchemaComponent for HashMap<String, T>
564where
565    T: AvroSchemaComponent,
566{
567    fn get_schema_in_ctxt(
568        named_schemas: &mut HashSet<Name>,
569        enclosing_namespace: NamespaceRef,
570    ) -> Schema {
571        Schema::map(T::get_schema_in_ctxt(named_schemas, enclosing_namespace)).build()
572    }
573
574    fn get_record_fields_in_ctxt(
575        _: &mut HashSet<Name>,
576        _: NamespaceRef,
577    ) -> Option<Vec<RecordField>> {
578        None
579    }
580}
581
582impl<T> AvroSchemaComponent for Option<T>
583where
584    T: AvroSchemaComponent,
585{
586    fn get_schema_in_ctxt(
587        named_schemas: &mut HashSet<Name>,
588        enclosing_namespace: NamespaceRef,
589    ) -> Schema {
590        let variants = vec![
591            Schema::Null,
592            T::get_schema_in_ctxt(named_schemas, enclosing_namespace),
593        ];
594
595        Schema::Union(
596            UnionSchema::new(variants).expect("Option<T> must produce a valid (non-nested) union"),
597        )
598    }
599
600    fn get_record_fields_in_ctxt(
601        _: &mut HashSet<Name>,
602        _: NamespaceRef,
603    ) -> Option<Vec<RecordField>> {
604        None
605    }
606
607    fn field_default() -> Option<serde_json::Value> {
608        Some(serde_json::Value::Null)
609    }
610}
611
612impl AvroSchemaComponent for core::time::Duration {
613    /// The schema is [`Schema::Record`] with the name `org.apache.avro.rust.Duration`.
614    ///
615    /// It has two fields:
616    /// - `secs` with the schema `Schema::Fixed(name: "org.apache.avro.rust.u64", size: 8)`
617    /// - `nanos` with the schema `Schema::Long`
618    fn get_schema_in_ctxt(
619        named_schemas: &mut HashSet<Name>,
620        enclosing_namespace: NamespaceRef,
621    ) -> Schema {
622        let name = Name::new("org.apache.avro.rust.Duration").expect("Name is valid");
623        if named_schemas.contains(&name) {
624            Schema::Ref { name }
625        } else {
626            named_schemas.insert(name.clone());
627            Schema::record(name)
628                .fields(
629                    Self::get_record_fields_in_ctxt(named_schemas, enclosing_namespace)
630                        .expect("Unreachable!"),
631                )
632                .build()
633        }
634    }
635
636    fn get_record_fields_in_ctxt(
637        named_schemas: &mut HashSet<Name>,
638        enclosing_namespace: NamespaceRef,
639    ) -> Option<Vec<RecordField>> {
640        Some(vec![
641            // Secs is an u64
642            RecordField::builder()
643                .name("secs")
644                .schema(u64::get_schema_in_ctxt(named_schemas, enclosing_namespace))
645                .build(),
646            // Nanos is an u32
647            RecordField::builder()
648                .name("nanos")
649                .schema(Schema::Long)
650                .build(),
651        ])
652    }
653}
654
655impl AvroSchemaComponent for uuid::Uuid {
656    /// The schema is [`Schema::Uuid`] with the name `org.apache.avro.rust.Uuid`.
657    ///
658    /// The underlying schema is [`Schema::Fixed`] with a size of 16.
659    ///
660    /// If you're using `human_readable: true` you need to override this schema with a `Schema::String`.
661    fn get_schema_in_ctxt(named_schemas: &mut HashSet<Name>, _: NamespaceRef) -> Schema {
662        let name = Name::new("org.apache.avro.rust.Uuid").expect("Name is valid");
663        if named_schemas.contains(&name) {
664            Schema::Ref { name }
665        } else {
666            let schema = Schema::Uuid(UuidSchema::Fixed(FixedSchema {
667                name: name.clone(),
668                aliases: None,
669                doc: None,
670                size: 16,
671                attributes: Default::default(),
672            }));
673            named_schemas.insert(name);
674            schema
675        }
676    }
677
678    fn get_record_fields_in_ctxt(
679        _: &mut HashSet<Name>,
680        _: NamespaceRef,
681    ) -> Option<Vec<RecordField>> {
682        None
683    }
684}
685
686impl AvroSchemaComponent for u64 {
687    /// The schema is [`Schema::Fixed`] of size 8 with the name `org.apache.avro.rust.u64`.
688    fn get_schema_in_ctxt(named_schemas: &mut HashSet<Name>, _: NamespaceRef) -> Schema {
689        let name = Name::new("org.apache.avro.rust.u64").expect("Name is valid");
690        if named_schemas.contains(&name) {
691            Schema::Ref { name }
692        } else {
693            let schema = Schema::Fixed(FixedSchema {
694                name: name.clone(),
695                aliases: None,
696                doc: None,
697                size: 8,
698                attributes: Default::default(),
699            });
700            named_schemas.insert(name);
701            schema
702        }
703    }
704
705    fn get_record_fields_in_ctxt(
706        _: &mut HashSet<Name>,
707        _: NamespaceRef,
708    ) -> Option<Vec<RecordField>> {
709        None
710    }
711}
712
713impl AvroSchemaComponent for u128 {
714    /// The schema is [`Schema::Fixed`] of size 16 with the name `org.apache.avro.rust.u128`.
715    fn get_schema_in_ctxt(named_schemas: &mut HashSet<Name>, _: NamespaceRef) -> Schema {
716        let name = Name::new("org.apache.avro.rust.u128").expect("Name is valid");
717        if named_schemas.contains(&name) {
718            Schema::Ref { name }
719        } else {
720            let schema = Schema::Fixed(FixedSchema {
721                name: name.clone(),
722                aliases: None,
723                doc: None,
724                size: 16,
725                attributes: Default::default(),
726            });
727            named_schemas.insert(name);
728            schema
729        }
730    }
731
732    fn get_record_fields_in_ctxt(
733        _: &mut HashSet<Name>,
734        _: NamespaceRef,
735    ) -> Option<Vec<RecordField>> {
736        None
737    }
738}
739
740impl AvroSchemaComponent for i128 {
741    /// The schema is [`Schema::Fixed`] of size 16 with the name `org.apache.avro.rust.i128`.
742    fn get_schema_in_ctxt(named_schemas: &mut HashSet<Name>, _: NamespaceRef) -> Schema {
743        let name = Name::new("org.apache.avro.rust.i128").expect("Name is valid");
744        if named_schemas.contains(&name) {
745            Schema::Ref { name }
746        } else {
747            let schema = Schema::Fixed(FixedSchema {
748                name: name.clone(),
749                aliases: None,
750                doc: None,
751                size: 16,
752                attributes: Default::default(),
753            });
754            named_schemas.insert(name);
755            schema
756        }
757    }
758
759    fn get_record_fields_in_ctxt(
760        _: &mut HashSet<Name>,
761        _: NamespaceRef,
762    ) -> Option<Vec<RecordField>> {
763        None
764    }
765}
766
767/// Schema definition for `[T; N]`
768///
769/// Schema is defined as follows:
770/// - 0-sized arrays: [`Schema::Null`]
771/// - 1-sized arrays: `T::get_schema_in_ctxt`
772/// - N-sized arrays: [`Schema::Record`] with a field for every index
773///
774/// If you need or want a [`Schema::Array`], [`Schema::Bytes`], or [`Schema::Fixed`] instead,
775/// use [`apache_avro::serde::array`], [`apache_avro::serde::bytes`], or [`apache_avro::serde::fixed`] respectively.
776///
777/// [`apache_avro::serde::array`]: crate::serde::array
778/// [`apache_avro::serde::bytes`]: crate::serde::bytes
779/// [`apache_avro::serde::fixed`]: crate::serde::fixed
780impl<const N: usize, T: AvroSchemaComponent> AvroSchemaComponent for [T; N] {
781    fn get_schema_in_ctxt(
782        named_schemas: &mut HashSet<Name>,
783        enclosing_namespace: NamespaceRef,
784    ) -> Schema {
785        if N == 0 {
786            Schema::Null
787        } else if N == 1 {
788            T::get_schema_in_ctxt(named_schemas, enclosing_namespace)
789        } else {
790            let t_schema = T::get_schema_in_ctxt(named_schemas, enclosing_namespace);
791            let name = Name::new_with_enclosing_namespace(
792                format!("A{N}_{}", t_schema.unique_normalized_name()),
793                enclosing_namespace,
794            )
795            .expect("Name is valid");
796            if named_schemas.contains(&name) {
797                Schema::Ref { name }
798            } else {
799                named_schemas.insert(name.clone());
800
801                let t_default = T::field_default();
802                // If T is a named schema or contains named schemas, they'll now be a reference.
803                let t_ref = T::get_schema_in_ctxt(named_schemas, enclosing_namespace);
804                let fields = std::iter::once(
805                    RecordField::builder()
806                        .name("field_0".to_string())
807                        .schema(t_schema)
808                        .maybe_default(t_default.clone())
809                        .build(),
810                )
811                .chain((1..N).map(|n| {
812                    RecordField::builder()
813                        .name(format!("field_{n}"))
814                        .schema(t_ref.clone())
815                        .maybe_default(t_default.clone())
816                        .build()
817                }))
818                .collect();
819
820                Schema::record(name).fields(fields).build()
821            }
822        }
823    }
824
825    fn get_record_fields_in_ctxt(
826        named_schemas: &mut HashSet<Name>,
827        enclosing_namespace: NamespaceRef,
828    ) -> Option<Vec<RecordField>> {
829        if N == 0 {
830            None
831        } else if N == 1 {
832            T::get_record_fields_in_ctxt(named_schemas, enclosing_namespace)
833        } else {
834            let t_schema = T::get_schema_in_ctxt(named_schemas, enclosing_namespace);
835            let t_default = T::field_default();
836            // If T is a named schema or contains named schemas, they'll now be a reference.
837            let t_ref = T::get_schema_in_ctxt(named_schemas, enclosing_namespace);
838            let fields = std::iter::once(
839                RecordField::builder()
840                    .name("field_0".to_string())
841                    .schema(t_schema)
842                    .maybe_default(t_default.clone())
843                    .build(),
844            )
845            .chain((1..N).map(|n| {
846                RecordField::builder()
847                    .name(format!("field_{n}"))
848                    .schema(t_ref.clone())
849                    .maybe_default(t_default.clone())
850                    .build()
851            }))
852            .collect();
853            Some(fields)
854        }
855    }
856
857    /// `None` for 0-sized and N-sized arrays, `T::field_default` for 1-sized arrays
858    fn field_default() -> Option<serde_json::Value> {
859        if N == 1 { T::field_default() } else { None }
860    }
861}
862
863/// Schema definition for `(T₁, T₂, …, Tₙ)`.
864///
865/// Implemented for tuples of up to 16 elements.
866///
867/// Schema is defined as follows:
868/// - 1-tuple: `T::get_schema_in_ctxt`
869/// - N-tuple: [`Schema::Record`] with a field for every element
870#[cfg_attr(docsrs, doc(fake_variadic))]
871impl<T: AvroSchemaComponent> AvroSchemaComponent for (T,) {
872    fn get_schema_in_ctxt(
873        named_schemas: &mut HashSet<Name>,
874        enclosing_namespace: NamespaceRef,
875    ) -> Schema {
876        T::get_schema_in_ctxt(named_schemas, enclosing_namespace)
877    }
878
879    fn get_record_fields_in_ctxt(
880        named_schemas: &mut HashSet<Name>,
881        enclosing_namespace: NamespaceRef,
882    ) -> Option<Vec<RecordField>> {
883        T::get_record_fields_in_ctxt(named_schemas, enclosing_namespace)
884    }
885
886    /// `None` for N-tuples, `T::field_default()` for 1-tuple.
887    fn field_default() -> Option<serde_json::Value> {
888        T::field_default()
889    }
890}
891
892macro_rules! tuple_impls {
893    ($($len:expr => ($($name:ident)+))+) => {
894        $(
895            #[cfg_attr(docsrs, doc(hidden))]
896            impl<$($name: AvroSchemaComponent),+> AvroSchemaComponent for ($($name),+) {
897                fn get_schema_in_ctxt(named_schemas: &mut HashSet<Name>, enclosing_namespace: NamespaceRef) -> Schema {
898                    let schemas: [Schema; $len] = [$($name::get_schema_in_ctxt(named_schemas, enclosing_namespace)),+];
899
900                    let mut name = format!("T{}", $len);
901                    for schema in &schemas {
902                        name.push('_');
903                        name.push_str(&schema.unique_normalized_name());
904                    }
905                    let name = Name::new_with_enclosing_namespace(name, enclosing_namespace).expect("Name is valid");
906
907                    if named_schemas.contains(&name) {
908                        Schema::Ref { name }
909                    } else {
910                        named_schemas.insert(name.clone());
911
912                        let defaults: [Option<serde_json::Value>; $len] = [$($name::field_default()),+];
913
914                        let fields = schemas.into_iter().zip(defaults.into_iter()).enumerate().map(|(n, (schema, default))| {
915                            RecordField::builder()
916                                .name(format!("field_{n}"))
917                                .schema(schema)
918                                .maybe_default(default)
919                                .build()
920                        }).collect();
921
922                        Schema::record(name).fields(fields).build()
923                    }
924                }
925            }
926        )+
927    }
928}
929
930tuple_impls! {
931    2 => (T0 T1)
932    3 => (T0 T1 T2)
933    4 => (T0 T1 T2 T3)
934    5 => (T0 T1 T2 T3 T4)
935    6 => (T0 T1 T2 T3 T4 T5)
936    7 => (T0 T1 T2 T3 T4 T5 T6)
937    8 => (T0 T1 T2 T3 T4 T5 T6 T7)
938    9 => (T0 T1 T2 T3 T4 T5 T6 T7 T8)
939    10 => (T0 T1 T2 T3 T4 T5 T6 T7 T8 T9)
940    11 => (T0 T1 T2 T3 T4 T5 T6 T7 T8 T9 T10)
941    12 => (T0 T1 T2 T3 T4 T5 T6 T7 T8 T9 T10 T11)
942    13 => (T0 T1 T2 T3 T4 T5 T6 T7 T8 T9 T10 T11 T12)
943    14 => (T0 T1 T2 T3 T4 T5 T6 T7 T8 T9 T10 T11 T12 T13)
944    15 => (T0 T1 T2 T3 T4 T5 T6 T7 T8 T9 T10 T11 T12 T13 T14)
945    16 => (T0 T1 T2 T3 T4 T5 T6 T7 T8 T9 T10 T11 T12 T13 T14 T15)
946}
947
948#[cfg(test)]
949mod tests {
950    use apache_avro_test_helper::TestResult;
951
952    use crate::{
953        AvroSchema, Schema,
954        reader::datum::GenericDatumReader,
955        schema::{FixedSchema, Name},
956        writer::datum::GenericDatumWriter,
957    };
958
959    #[test]
960    fn avro_rs_401_str() -> TestResult {
961        let schema = str::get_schema();
962        assert_eq!(schema, Schema::String);
963
964        Ok(())
965    }
966
967    #[test]
968    fn avro_rs_401_references() -> TestResult {
969        let schema_ref = <&str>::get_schema();
970        let schema_ref_mut = <&mut str>::get_schema();
971
972        assert_eq!(schema_ref, Schema::String);
973        assert_eq!(schema_ref_mut, Schema::String);
974
975        Ok(())
976    }
977
978    #[test]
979    fn avro_rs_401_slice() -> TestResult {
980        let schema = <[u8]>::get_schema();
981        assert_eq!(schema, Schema::array(Schema::Int).build());
982
983        Ok(())
984    }
985
986    #[test]
987    fn avro_rs_401_option_ref_slice_array() -> TestResult {
988        let schema = <Option<&[u8]>>::get_schema();
989        assert_eq!(
990            schema,
991            Schema::union(vec![Schema::Null, Schema::array(Schema::Int).build()])?
992        );
993
994        Ok(())
995    }
996
997    #[test]
998    fn avro_rs_414_char() -> TestResult {
999        let schema = char::get_schema();
1000        assert_eq!(schema, Schema::String);
1001
1002        Ok(())
1003    }
1004
1005    #[test]
1006    fn avro_rs_414_u64() -> TestResult {
1007        let schema = u64::get_schema();
1008        assert_eq!(
1009            schema,
1010            Schema::Fixed(FixedSchema {
1011                name: Name::new("org.apache.avro.rust.u64")?,
1012                aliases: None,
1013                doc: None,
1014                size: 8,
1015                attributes: Default::default(),
1016            })
1017        );
1018
1019        Ok(())
1020    }
1021
1022    #[test]
1023    fn avro_rs_414_i128() -> TestResult {
1024        let schema = i128::get_schema();
1025        assert_eq!(
1026            schema,
1027            Schema::Fixed(FixedSchema {
1028                name: Name::new("org.apache.avro.rust.i128")?,
1029                aliases: None,
1030                doc: None,
1031                size: 16,
1032                attributes: Default::default(),
1033            })
1034        );
1035
1036        Ok(())
1037    }
1038
1039    #[test]
1040    fn avro_rs_414_u128() -> TestResult {
1041        let schema = u128::get_schema();
1042        assert_eq!(
1043            schema,
1044            Schema::Fixed(FixedSchema {
1045                name: Name::new("org.apache.avro.rust.u128")?,
1046                aliases: None,
1047                doc: None,
1048                size: 16,
1049                attributes: Default::default(),
1050            })
1051        );
1052
1053        Ok(())
1054    }
1055
1056    #[test]
1057    fn avro_rs_486_unit() -> TestResult {
1058        let schema = <()>::get_schema();
1059        assert_eq!(schema, Schema::Null);
1060
1061        Ok(())
1062    }
1063
1064    #[test]
1065    #[should_panic(
1066        expected = "Option<T> must produce a valid (non-nested) union: Error { details: Unions cannot contain duplicate types, found at least two Null }"
1067    )]
1068    fn avro_rs_489_some_unit() {
1069        <Option<()>>::get_schema();
1070    }
1071
1072    #[test]
1073    #[should_panic(
1074        expected = "Option<T> must produce a valid (non-nested) union: Error { details: Unions may not directly contain a union }"
1075    )]
1076    fn avro_rs_489_option_option() {
1077        <Option<Option<i32>>>::get_schema();
1078    }
1079
1080    #[test]
1081    fn avro_rs_512_std_time_duration() -> TestResult {
1082        let schema = Schema::parse_str(
1083            r#"{
1084            "type": "record",
1085            "name": "Duration",
1086            "namespace": "org.apache.avro.rust",
1087            "fields": [
1088                { "name": "secs", "type": {"type": "fixed", "name": "u64", "namespace": "org.apache.avro.rust", "size": 8} },
1089                { "name": "nanos", "type": "long" }
1090            ]
1091        }"#,
1092        )?;
1093        let zero = std::time::Duration::ZERO;
1094        let max = std::time::Duration::MAX;
1095        assert_eq!(schema, std::time::Duration::get_schema());
1096
1097        let writer = GenericDatumWriter::builder(&schema).build()?;
1098        let written_zero = writer.write_ser_to_vec(&zero)?;
1099        let written_max = writer.write_ser_to_vec(&max)?;
1100
1101        let reader = GenericDatumReader::builder(&schema).build()?;
1102        let read_zero = reader.read_deser(&mut &written_zero[..])?;
1103        assert_eq!(zero, read_zero);
1104        let read_max = reader.read_deser(&mut &written_max[..])?;
1105        assert_eq!(max, read_max);
1106        Ok(())
1107    }
1108
1109    #[test]
1110    fn avro_rs_512_0_array() -> TestResult {
1111        assert_eq!(Schema::Null, <[String; 0]>::get_schema());
1112        assert_eq!(Schema::Null, <[(); 0]>::get_schema());
1113        assert_eq!(Schema::Null, <[bool; 0]>::get_schema());
1114        Ok(())
1115    }
1116
1117    #[test]
1118    fn avro_rs_512_1_array() -> TestResult {
1119        assert_eq!(Schema::String, <[String; 1]>::get_schema());
1120        assert_eq!(Schema::Null, <[(); 1]>::get_schema());
1121        assert_eq!(Schema::Boolean, <[bool; 1]>::get_schema());
1122        Ok(())
1123    }
1124
1125    #[test]
1126    fn avro_rs_512_n_array() -> TestResult {
1127        let schema = Schema::parse_str(
1128            r#"{
1129            "type": "record",
1130            "name": "A5_s",
1131            "fields": [
1132                { "name": "field_0", "type": "string" },
1133                { "name": "field_1", "type": "string" },
1134                { "name": "field_2", "type": "string" },
1135                { "name": "field_3", "type": "string" },
1136                { "name": "field_4", "type": "string" }
1137            ]
1138        }"#,
1139        )?;
1140
1141        assert_eq!(schema, <[String; 5]>::get_schema());
1142        Ok(())
1143    }
1144
1145    #[test]
1146    fn avro_rs_512_n_array_complex_type() -> TestResult {
1147        let schema = Schema::parse_str(
1148            r#"{
1149            "type": "record",
1150            "name": "A2_u2_n_r25_org_apache_avro_rust_Uuid",
1151            "fields": [
1152                { "name": "field_0", "type": ["null", {"type": "fixed", "logicalType": "uuid", "size": 16, "name": "Uuid", "namespace": "org.apache.avro.rust"}], "default": null },
1153                { "name": "field_1", "type": ["null", "org.apache.avro.rust.Uuid"], "default": null }
1154            ]
1155        }"#,
1156        )?;
1157
1158        assert_eq!(schema, <[Option<uuid::Uuid>; 2]>::get_schema());
1159        Ok(())
1160    }
1161
1162    #[test]
1163    fn avro_rs_512_1_tuple() -> TestResult {
1164        assert_eq!(Schema::String, <(String,)>::get_schema());
1165        assert_eq!(Schema::Null, <((),)>::get_schema());
1166        assert_eq!(Schema::Boolean, <(bool,)>::get_schema());
1167        Ok(())
1168    }
1169
1170    #[test]
1171    fn avro_rs_512_n_tuple() -> TestResult {
1172        let schema = Schema::parse_str(
1173            r#"{
1174            "type": "record",
1175            "name": "T5_s_i_l_B_n",
1176            "fields": [
1177                { "name": "field_0", "type": "string" },
1178                { "name": "field_1", "type": "int" },
1179                { "name": "field_2", "type": "long" },
1180                { "name": "field_3", "type": "boolean" },
1181                { "name": "field_4", "type": "null" }
1182            ]
1183        }"#,
1184        )?;
1185
1186        assert_eq!(schema, <(String, i32, i64, bool, ())>::get_schema());
1187        Ok(())
1188    }
1189
1190    #[test]
1191    fn avro_rs_512_n_tuple_complex_type() -> TestResult {
1192        let schema = Schema::parse_str(
1193            r#"{
1194            "type": "record",
1195            "name": "T3_u2_n_r25_org_apache_avro_rust_Uuid_r25_org_apache_avro_rust_Uuid_s",
1196            "fields": [
1197                { "name": "field_0", "type": ["null", {"type": "fixed", "logicalType": "uuid", "size": 16, "name": "Uuid", "namespace": "org.apache.avro.rust"}], "default": null },
1198                { "name": "field_1", "type": "org.apache.avro.rust.Uuid" },
1199                { "name": "field_2", "type": "string" }
1200            ]
1201        }"#,
1202        )?;
1203
1204        assert_eq!(
1205            schema,
1206            <(Option<uuid::Uuid>, uuid::Uuid, String)>::get_schema()
1207        );
1208        Ok(())
1209    }
1210}