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::{FixedSchema, Name, Names, Namespace, UnionSchema, UuidSchema};
20use serde_json::Map;
21use std::borrow::Cow;
22use std::collections::HashMap;
23
24/// Trait for types that serve as an Avro data model. Derive implementation available
25/// through `derive` feature. Do not implement directly!
26/// Implement [`AvroSchemaComponent`] to get this trait
27/// through a blanket implementation.
28pub trait AvroSchema {
29    fn get_schema() -> Schema;
30}
31
32/// Trait for types that serve as fully defined components inside an Avro data model. Derive
33/// implementation available through `derive` feature. This is what is implemented by
34/// the `derive(AvroSchema)` macro.
35///
36/// # Implementation guide
37///
38/// ### Simple implementation
39/// To construct a non named simple schema, it is possible to ignore the input argument making the
40/// general form implementation look like
41/// ```ignore
42/// impl AvroSchemaComponent for AType {
43///     fn get_schema_in_ctxt(_: &mut Names, _: &Namespace) -> Schema {
44///        Schema::?
45///    }
46///}
47/// ```
48///
49/// ### Passthrough implementation
50///
51/// To construct a schema for a Type that acts as in "inner" type, such as for smart pointers, simply
52/// pass through the arguments to the inner type
53/// ```ignore
54/// impl AvroSchemaComponent for PassthroughType {
55///     fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema {
56///        InnerType::get_schema_in_ctxt(named_schemas, enclosing_namespace)
57///    }
58///}
59/// ```
60///
61/// ### Complex implementation
62///
63/// To implement this for Named schema there is a general form needed to avoid creating invalid
64/// schemas or infinite loops.
65/// ```ignore
66/// impl AvroSchemaComponent for ComplexType {
67///     fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema {
68///         // Create the fully qualified name for your type given the enclosing namespace
69///         let name =  apache_avro::schema::Name::new("MyName")
70///             .expect("Unable to parse schema name")
71///             .fully_qualified_name(enclosing_namespace);
72///         let enclosing_namespace = &name.namespace;
73///         // Check, if your name is already defined, and if so, return a ref to that name
74///         if named_schemas.contains_key(&name) {
75///             apache_avro::schema::Schema::Ref{name: name.clone()}
76///         } else {
77///             named_schemas.insert(name.clone(), apache_avro::schema::Schema::Ref{name: name.clone()});
78///             // YOUR SCHEMA DEFINITION HERE with the name equivalent to "MyName".
79///             // For non-simple sub types delegate to their implementation of AvroSchemaComponent
80///         }
81///    }
82///}
83/// ```
84pub trait AvroSchemaComponent {
85    fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema;
86}
87
88impl<T> AvroSchema for T
89where
90    T: AvroSchemaComponent + ?Sized,
91{
92    fn get_schema() -> Schema {
93        T::get_schema_in_ctxt(&mut HashMap::default(), &None)
94    }
95}
96
97macro_rules! impl_schema (
98    ($type:ty, $variant_constructor:expr) => (
99        impl AvroSchemaComponent for $type {
100            fn get_schema_in_ctxt(_: &mut Names, _: &Namespace) -> Schema {
101                $variant_constructor
102            }
103        }
104    );
105);
106
107impl_schema!(bool, Schema::Boolean);
108impl_schema!(i8, Schema::Int);
109impl_schema!(i16, Schema::Int);
110impl_schema!(i32, Schema::Int);
111impl_schema!(i64, Schema::Long);
112impl_schema!(u8, Schema::Int);
113impl_schema!(u16, Schema::Int);
114impl_schema!(u32, Schema::Long);
115impl_schema!(f32, Schema::Float);
116impl_schema!(f64, Schema::Double);
117impl_schema!(String, Schema::String);
118impl_schema!(str, Schema::String);
119impl_schema!(char, Schema::String);
120
121impl<T> AvroSchemaComponent for &T
122where
123    T: AvroSchemaComponent + ?Sized,
124{
125    fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema {
126        T::get_schema_in_ctxt(named_schemas, enclosing_namespace)
127    }
128}
129
130impl<T> AvroSchemaComponent for &mut T
131where
132    T: AvroSchemaComponent + ?Sized,
133{
134    fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema {
135        T::get_schema_in_ctxt(named_schemas, enclosing_namespace)
136    }
137}
138
139impl<T> AvroSchemaComponent for [T]
140where
141    T: AvroSchemaComponent,
142{
143    fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema {
144        Schema::array(T::get_schema_in_ctxt(named_schemas, enclosing_namespace))
145    }
146}
147
148impl<const N: usize, T> AvroSchemaComponent for [T; N]
149where
150    T: AvroSchemaComponent,
151{
152    fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema {
153        Schema::array(T::get_schema_in_ctxt(named_schemas, enclosing_namespace))
154    }
155}
156
157impl<T> AvroSchemaComponent for Vec<T>
158where
159    T: AvroSchemaComponent,
160{
161    fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema {
162        Schema::array(T::get_schema_in_ctxt(named_schemas, enclosing_namespace))
163    }
164}
165
166impl<T> AvroSchemaComponent for Option<T>
167where
168    T: AvroSchemaComponent,
169{
170    fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema {
171        let variants = vec![
172            Schema::Null,
173            T::get_schema_in_ctxt(named_schemas, enclosing_namespace),
174        ];
175
176        Schema::Union(
177            UnionSchema::new(variants).expect("Option<T> must produce a valid (non-nested) union"),
178        )
179    }
180}
181
182impl<T> AvroSchemaComponent for Map<String, T>
183where
184    T: AvroSchemaComponent,
185{
186    fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema {
187        Schema::map(T::get_schema_in_ctxt(named_schemas, enclosing_namespace))
188    }
189}
190
191impl<T> AvroSchemaComponent for HashMap<String, T>
192where
193    T: AvroSchemaComponent,
194{
195    fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema {
196        Schema::map(T::get_schema_in_ctxt(named_schemas, enclosing_namespace))
197    }
198}
199
200impl<T> AvroSchemaComponent for Box<T>
201where
202    T: AvroSchemaComponent,
203{
204    fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema {
205        T::get_schema_in_ctxt(named_schemas, enclosing_namespace)
206    }
207}
208
209impl<T> AvroSchemaComponent for std::sync::Mutex<T>
210where
211    T: AvroSchemaComponent,
212{
213    fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema {
214        T::get_schema_in_ctxt(named_schemas, enclosing_namespace)
215    }
216}
217
218impl<T> AvroSchemaComponent for Cow<'_, T>
219where
220    T: AvroSchemaComponent + Clone,
221{
222    fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema {
223        T::get_schema_in_ctxt(named_schemas, enclosing_namespace)
224    }
225}
226
227impl AvroSchemaComponent for core::time::Duration {
228    /// The schema is [`Schema::Duration`] with the name `duration`.
229    ///
230    /// This is a lossy conversion as this Avro type does not store the amount of nanoseconds.
231    #[expect(clippy::map_entry, reason = "We don't use the value from the map")]
232    fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema {
233        let name = Name::new("duration")
234            .expect("Name is valid")
235            .fully_qualified_name(enclosing_namespace);
236        if named_schemas.contains_key(&name) {
237            Schema::Ref { name }
238        } else {
239            let schema = Schema::Duration(FixedSchema {
240                name: name.clone(),
241                aliases: None,
242                doc: None,
243                size: 12,
244                default: None,
245                attributes: Default::default(),
246            });
247            named_schemas.insert(name, schema.clone());
248            schema
249        }
250    }
251}
252
253impl AvroSchemaComponent for uuid::Uuid {
254    /// The schema is [`Schema::Uuid`] with the name `uuid`.
255    ///
256    /// The underlying schema is [`Schema::Fixed`] with a size of 16.
257    #[expect(clippy::map_entry, reason = "We don't use the value from the map")]
258    fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema {
259        let name = Name::new("uuid")
260            .expect("Name is valid")
261            .fully_qualified_name(enclosing_namespace);
262        if named_schemas.contains_key(&name) {
263            Schema::Ref { name }
264        } else {
265            let schema = Schema::Uuid(UuidSchema::Fixed(FixedSchema {
266                name: name.clone(),
267                aliases: None,
268                doc: None,
269                size: 16,
270                default: None,
271                attributes: Default::default(),
272            }));
273            named_schemas.insert(name, schema.clone());
274            schema
275        }
276    }
277}
278
279impl AvroSchemaComponent for u64 {
280    /// The schema is [`Schema::Fixed`] of size 8 with the name `u64`.
281    #[expect(clippy::map_entry, reason = "We don't use the value from the map")]
282    fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema {
283        let name = Name::new("u64")
284            .expect("Name is valid")
285            .fully_qualified_name(enclosing_namespace);
286        if named_schemas.contains_key(&name) {
287            Schema::Ref { name }
288        } else {
289            let schema = Schema::Fixed(FixedSchema {
290                name: name.clone(),
291                aliases: None,
292                doc: None,
293                size: 8,
294                default: None,
295                attributes: Default::default(),
296            });
297            named_schemas.insert(name, schema.clone());
298            schema
299        }
300    }
301}
302
303impl AvroSchemaComponent for u128 {
304    /// The schema is [`Schema::Fixed`] of size 16 with the name `u128`.
305    #[expect(clippy::map_entry, reason = "We don't use the value from the map")]
306    fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema {
307        let name = Name::new("u128")
308            .expect("Name is valid")
309            .fully_qualified_name(enclosing_namespace);
310        if named_schemas.contains_key(&name) {
311            Schema::Ref { name }
312        } else {
313            let schema = Schema::Fixed(FixedSchema {
314                name: name.clone(),
315                aliases: None,
316                doc: None,
317                size: 16,
318                default: None,
319                attributes: Default::default(),
320            });
321            named_schemas.insert(name, schema.clone());
322            schema
323        }
324    }
325}
326
327impl AvroSchemaComponent for i128 {
328    /// The schema is [`Schema::Fixed`] of size 16 with the name `i128`.
329    #[expect(clippy::map_entry, reason = "We don't use the value from the map")]
330    fn get_schema_in_ctxt(named_schemas: &mut Names, enclosing_namespace: &Namespace) -> Schema {
331        let name = Name::new("i128")
332            .expect("Name is valid")
333            .fully_qualified_name(enclosing_namespace);
334        if named_schemas.contains_key(&name) {
335            Schema::Ref { name }
336        } else {
337            let schema = Schema::Fixed(FixedSchema {
338                name: name.clone(),
339                aliases: None,
340                doc: None,
341                size: 16,
342                default: None,
343                attributes: Default::default(),
344            });
345            named_schemas.insert(name, schema.clone());
346            schema
347        }
348    }
349}
350
351#[cfg(test)]
352mod tests {
353    use crate::schema::{FixedSchema, Name};
354    use crate::{AvroSchema, Schema};
355    use apache_avro_test_helper::TestResult;
356
357    #[test]
358    fn avro_rs_401_str() -> TestResult {
359        let schema = str::get_schema();
360        assert_eq!(schema, Schema::String);
361
362        Ok(())
363    }
364
365    #[test]
366    fn avro_rs_401_references() -> TestResult {
367        let schema_ref = <&str>::get_schema();
368        let schema_ref_mut = <&mut str>::get_schema();
369
370        assert_eq!(schema_ref, Schema::String);
371        assert_eq!(schema_ref_mut, Schema::String);
372
373        Ok(())
374    }
375
376    #[test]
377    fn avro_rs_401_slice() -> TestResult {
378        let schema = <[u8]>::get_schema();
379        assert_eq!(schema, Schema::array(Schema::Int));
380
381        Ok(())
382    }
383
384    #[test]
385    fn avro_rs_401_array() -> TestResult {
386        let schema = <[u8; 55]>::get_schema();
387        assert_eq!(schema, Schema::array(Schema::Int));
388
389        Ok(())
390    }
391
392    #[test]
393    fn avro_rs_401_option_ref_slice_array() -> TestResult {
394        let schema = <Option<&[[u8; 55]]>>::get_schema();
395        assert_eq!(
396            schema,
397            Schema::union(vec![
398                Schema::Null,
399                Schema::array(Schema::array(Schema::Int))
400            ])?
401        );
402
403        Ok(())
404    }
405
406    #[test]
407    fn avro_rs_414_char() -> TestResult {
408        let schema = char::get_schema();
409        assert_eq!(schema, Schema::String);
410
411        Ok(())
412    }
413
414    #[test]
415    fn avro_rs_414_u64() -> TestResult {
416        let schema = u64::get_schema();
417        assert_eq!(
418            schema,
419            Schema::Fixed(FixedSchema {
420                name: Name::new("u64")?,
421                aliases: None,
422                doc: None,
423                size: 8,
424                default: None,
425                attributes: Default::default(),
426            })
427        );
428
429        Ok(())
430    }
431
432    #[test]
433    fn avro_rs_414_i128() -> TestResult {
434        let schema = i128::get_schema();
435        assert_eq!(
436            schema,
437            Schema::Fixed(FixedSchema {
438                name: Name::new("i128")?,
439                aliases: None,
440                doc: None,
441                size: 16,
442                default: None,
443                attributes: Default::default(),
444            })
445        );
446
447        Ok(())
448    }
449
450    #[test]
451    fn avro_rs_414_u128() -> TestResult {
452        let schema = u128::get_schema();
453        assert_eq!(
454            schema,
455            Schema::Fixed(FixedSchema {
456                name: Name::new("u128")?,
457                aliases: None,
458                doc: None,
459                size: 16,
460                default: None,
461                attributes: Default::default(),
462            })
463        );
464
465        Ok(())
466    }
467}