apache_avro/schema/
builders.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::{
19    Alias, ArraySchema, EnumSchema, FixedSchema, MapSchema, Name, RecordField, RecordSchema,
20    UnionSchema,
21};
22use crate::types::Value;
23use crate::{AvroResult, Schema};
24use bon::bon;
25use serde_json::Value as JsonValue;
26use std::collections::{BTreeMap, HashMap};
27
28#[bon]
29impl Schema {
30    /// Returns a `Schema::Map` with the given types and optional default
31    /// and custom attributes.
32    #[builder(finish_fn = build)]
33    pub fn map(
34        #[builder(start_fn)] types: Schema,
35        default: Option<HashMap<String, Value>>,
36        attributes: Option<BTreeMap<String, JsonValue>>,
37    ) -> Self {
38        let attributes = attributes.unwrap_or_default();
39        Schema::Map(MapSchema {
40            types: Box::new(types),
41            default,
42            attributes,
43        })
44    }
45
46    /// Returns a `Schema::Array` with the given items and optional default
47    /// and custom attributes.
48    #[builder(finish_fn = build)]
49    pub fn array(
50        #[builder(start_fn)] items: Schema,
51        default: Option<Vec<Value>>,
52        attributes: Option<BTreeMap<String, JsonValue>>,
53    ) -> Self {
54        let attributes = attributes.unwrap_or_default();
55        Schema::Array(ArraySchema {
56            items: Box::new(items),
57            default,
58            attributes,
59        })
60    }
61
62    /// Returns a `Schema::Enum` with the given name, symbols and optional
63    /// aliases, doc, default and custom attributes.
64    #[builder(finish_fn = build)]
65    pub fn r#enum(
66        #[builder(start_fn)] name: Name,
67        #[builder(start_fn)] symbols: Vec<impl Into<String>>,
68        aliases: Option<Vec<Alias>>,
69        doc: Option<String>,
70        default: Option<String>,
71        attributes: Option<BTreeMap<String, JsonValue>>,
72    ) -> Self {
73        let attributes = attributes.unwrap_or_default();
74        let symbols = symbols.into_iter().map(Into::into).collect();
75        Schema::Enum(EnumSchema {
76            name,
77            symbols,
78            aliases,
79            doc,
80            default,
81            attributes,
82        })
83    }
84
85    /// Returns a `Schema::Fixed` with the given name, size and optional
86    /// aliases, doc and custom attributes.
87    #[builder(finish_fn = build)]
88    pub fn fixed(
89        #[builder(start_fn)] name: Name,
90        #[builder(start_fn)] size: usize,
91        aliases: Option<Vec<Alias>>,
92        doc: Option<String>,
93        attributes: Option<BTreeMap<String, JsonValue>>,
94    ) -> Self {
95        let attributes = attributes.unwrap_or_default();
96        Schema::Fixed(FixedSchema {
97            name,
98            size,
99            aliases,
100            doc,
101            attributes,
102        })
103    }
104
105    /// Returns a `Schema::Record` with the given name, size and optional
106    /// aliases, doc and custom attributes.
107    #[builder(finish_fn = build)]
108    pub fn record(
109        #[builder(start_fn)] name: Name,
110        fields: Option<Vec<RecordField>>,
111        aliases: Option<Vec<Alias>>,
112        doc: Option<String>,
113        attributes: Option<BTreeMap<String, JsonValue>>,
114    ) -> Self {
115        let fields = fields.unwrap_or_default();
116        let attributes = attributes.unwrap_or_default();
117        let record_schema = RecordSchema::builder()
118            .name(name)
119            .fields(fields)
120            .aliases(aliases)
121            .doc(doc)
122            .attributes(attributes)
123            .build();
124        Schema::Record(record_schema)
125    }
126
127    /// Returns a [`Schema::Union`] with the given variants.
128    ///
129    /// # Errors
130    /// Will return an error if `schemas` has duplicate unnamed schemas or if `schemas`
131    /// contains a union.
132    pub fn union(schemas: Vec<Schema>) -> AvroResult<Schema> {
133        UnionSchema::new(schemas).map(Schema::Union)
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140    use apache_avro_test_helper::TestResult;
141
142    #[test]
143    fn avro_rs_472_enum_builder_only_mandatory() -> TestResult {
144        let name = Name::new("enum_builder")?;
145        let symbols = vec!["A", "B", "C", "D", "E"];
146
147        let schema = Schema::r#enum(name.clone(), symbols.clone()).build();
148
149        if let Schema::Enum(enum_schema) = schema {
150            assert_eq!(enum_schema.name, name);
151            assert_eq!(enum_schema.symbols, symbols);
152            assert_eq!(enum_schema.aliases, None);
153            assert_eq!(enum_schema.doc, None);
154            assert_eq!(enum_schema.default, None);
155            assert_eq!(enum_schema.attributes, Default::default());
156        } else {
157            panic!("Expected a Schema::Enum, got: {schema}");
158        }
159
160        Ok(())
161    }
162
163    #[test]
164    fn avro_rs_472_enum_builder_with_optionals() -> TestResult {
165        let name = Name::new("enum_builder")?;
166        let symbols = vec!["A", "B", "C", "D", "E"];
167        let aliases = vec![Alias::new("alias")?];
168        let doc = "docu";
169        let default = "default value";
170        let attributes =
171            BTreeMap::from_iter([("key".to_string(), JsonValue::String("value".into()))]);
172
173        let schema = Schema::r#enum(name.clone(), symbols.clone())
174            .aliases(aliases.clone())
175            .doc(doc.into())
176            .default(default.into())
177            .attributes(attributes.clone())
178            .build();
179
180        if let Schema::Enum(enum_schema) = schema {
181            assert_eq!(enum_schema.name, name);
182            assert_eq!(enum_schema.symbols, symbols);
183            assert_eq!(enum_schema.aliases, Some(aliases));
184            assert_eq!(enum_schema.doc, Some(doc.into()));
185            assert_eq!(enum_schema.default, Some(default.into()));
186            assert_eq!(enum_schema.attributes, attributes);
187        } else {
188            panic!("Expected a Schema::Enum, got: {schema}");
189        }
190
191        Ok(())
192    }
193
194    #[test]
195    fn avro_rs_472_fixed_builder_only_mandatory() -> TestResult {
196        let name = Name::new("fixed_builder")?;
197        let size = 123;
198
199        let schema = Schema::fixed(name.clone(), size).build();
200
201        if let Schema::Fixed(fixed_schema) = schema {
202            assert_eq!(fixed_schema.name, name);
203            assert_eq!(fixed_schema.size, size);
204            assert_eq!(fixed_schema.aliases, None);
205            assert_eq!(fixed_schema.doc, None);
206            assert_eq!(fixed_schema.attributes, Default::default());
207        } else {
208            panic!("Expected a Schema::Fixed, got: {schema}");
209        }
210
211        Ok(())
212    }
213
214    #[test]
215    fn avro_rs_472_fixed_builder_with_optionals() -> TestResult {
216        let name = Name::new("fixed_builder")?;
217        let size = 234;
218        let aliases = vec![Alias::new("alias")?];
219        let doc = "docu";
220        let attributes =
221            BTreeMap::from_iter([("key".to_string(), JsonValue::String("value".into()))]);
222
223        let schema = Schema::fixed(name.clone(), size)
224            .aliases(aliases.clone())
225            .doc(doc.into())
226            .attributes(attributes.clone())
227            .build();
228
229        if let Schema::Fixed(fixed_schema) = schema {
230            assert_eq!(fixed_schema.name, name);
231            assert_eq!(fixed_schema.size, size);
232            assert_eq!(fixed_schema.aliases, Some(aliases));
233            assert_eq!(fixed_schema.doc, Some(doc.into()));
234            assert_eq!(fixed_schema.attributes, attributes);
235        } else {
236            panic!("Expected a Schema::Fixed, got: {schema}");
237        }
238
239        Ok(())
240    }
241
242    #[test]
243    fn avro_rs_472_record_builder_only_mandatory() -> TestResult {
244        let name = Name::new("record_builder")?;
245
246        let schema = Schema::record(name.clone()).build();
247
248        if let Schema::Record(record_schema) = schema {
249            assert_eq!(record_schema.name, name);
250            assert_eq!(record_schema.fields, vec![]);
251            assert_eq!(record_schema.aliases, None);
252            assert_eq!(record_schema.doc, None);
253            assert_eq!(record_schema.lookup, Default::default());
254            assert_eq!(record_schema.attributes, Default::default());
255        } else {
256            panic!("Expected a Schema::Record, got: {schema}");
257        }
258
259        Ok(())
260    }
261
262    #[test]
263    fn avro_rs_472_record_builder_with_optionals() -> TestResult {
264        let name = Name::new("record_builder")?;
265        let fields = vec![
266            RecordField::builder()
267                .name("f1")
268                .schema(Schema::Boolean)
269                .build(),
270            RecordField::builder()
271                .name("f2")
272                .schema(Schema::Int)
273                .build(),
274        ];
275        let aliases = vec![Alias::new("alias")?];
276        let doc = "docu";
277        let attributes =
278            BTreeMap::from_iter([("key".to_string(), JsonValue::String("value".into()))]);
279
280        let schema = Schema::record(name.clone())
281            .fields(fields.clone())
282            .aliases(aliases.clone())
283            .doc(doc.into())
284            .attributes(attributes.clone())
285            .build();
286
287        if let Schema::Record(fixed_schema) = schema {
288            assert_eq!(fixed_schema.name, name);
289            assert_eq!(fixed_schema.fields, fields);
290            assert_eq!(fixed_schema.aliases, Some(aliases));
291            assert_eq!(fixed_schema.doc, Some(doc.into()));
292            assert_eq!(
293                fixed_schema.lookup,
294                BTreeMap::from_iter([("f1".into(), 0), ("f2".into(), 1)])
295            );
296            assert_eq!(fixed_schema.attributes, attributes);
297        } else {
298            panic!("Expected a Schema::Record, got: {schema}");
299        }
300
301        Ok(())
302    }
303
304    #[test]
305    fn avro_rs_472_union_builder() -> TestResult {
306        let variants = vec![Schema::Null, Schema::Boolean, Schema::Int];
307
308        let schema = Schema::union(variants.clone())?;
309
310        if let Schema::Union(union_schema) = schema {
311            assert_eq!(union_schema.variants(), variants);
312        } else {
313            panic!("Expected a Schema::Union, got: {schema}");
314        }
315
316        Ok(())
317    }
318}