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