Skip to main content

apache_avro/schema/record/
schema.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::{Aliases, Documentation, Name, RecordField};
19use serde_json::Value;
20use std::collections::BTreeMap;
21use std::fmt::{Debug, Formatter};
22
23/// A description of a Record schema.
24#[derive(bon::Builder, Clone)]
25pub struct RecordSchema {
26    /// The name of the schema
27    pub name: Name,
28    /// The aliases of the schema
29    #[builder(default)]
30    pub aliases: Aliases,
31    /// The documentation of the schema
32    #[builder(default)]
33    pub doc: Documentation,
34    /// The set of fields of the schema
35    #[builder(default)]
36    pub fields: Vec<RecordField>,
37    /// The `lookup` table maps field names to their position in the `Vec`
38    /// of `fields`.
39    #[builder(skip = calculate_lookup_table(&fields))]
40    pub lookup: BTreeMap<String, usize>,
41    /// The custom attributes of the schema
42    #[builder(default)]
43    pub attributes: BTreeMap<String, Value>,
44}
45
46impl Debug for RecordSchema {
47    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
48        let mut debug = f.debug_struct("RecordSchema");
49        debug.field("name", &self.name);
50        if let Some(aliases) = &self.aliases {
51            debug.field("default", aliases);
52        }
53        if let Some(doc) = &self.doc {
54            debug.field("doc", doc);
55        }
56        debug.field("fields", &self.fields);
57        if !self.attributes.is_empty() {
58            debug.field("attributes", &self.attributes);
59        }
60        if self.aliases.is_none() || self.doc.is_none() || self.attributes.is_empty() {
61            debug.finish_non_exhaustive()
62        } else {
63            debug.finish()
64        }
65    }
66}
67
68impl<S: record_schema_builder::State> RecordSchemaBuilder<S> {
69    /// Try to set a Name from the given string.
70    pub fn try_name<T>(
71        self,
72        name: T,
73    ) -> Result<RecordSchemaBuilder<record_schema_builder::SetName<S>>, <T as TryInto<Name>>::Error>
74    where
75        <S as record_schema_builder::State>::Name: record_schema_builder::IsUnset,
76        T: TryInto<Name>,
77    {
78        let name = name.try_into()?;
79        Ok(self.name(name))
80    }
81}
82
83/// Calculate the lookup table for the given fields.
84fn calculate_lookup_table(fields: &[RecordField]) -> BTreeMap<String, usize> {
85    let map: BTreeMap<_, _> = fields
86        .iter()
87        .enumerate()
88        .map(|(i, field)| (field.name.clone(), i))
89        .collect();
90    assert_eq!(map.len(), fields.len(), "Duplicate field names found");
91    map
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97    use crate::Schema;
98    use apache_avro_test_helper::TestResult;
99    use pretty_assertions::assert_eq;
100
101    #[test]
102    fn avro_rs_403_record_schema_builder_no_fields() -> TestResult {
103        let name = Name::new("TestRecord")?;
104
105        let record_schema = RecordSchema::builder().name(name.clone()).build();
106
107        assert_eq!(record_schema.name, name);
108        assert_eq!(record_schema.aliases, None);
109        assert_eq!(record_schema.doc, None);
110        assert_eq!(record_schema.fields.len(), 0);
111        assert_eq!(record_schema.lookup.len(), 0);
112        assert_eq!(record_schema.attributes.len(), 0);
113
114        Ok(())
115    }
116
117    #[test]
118    fn avro_rs_403_record_schema_builder_no_fields_with_aliases() -> TestResult {
119        let name = Name::new("TestRecord")?;
120
121        let record_schema = RecordSchema::builder()
122            .name(name.clone())
123            .aliases(Some(vec!["alias_1".try_into()?]))
124            .build();
125
126        assert_eq!(record_schema.name, name);
127        assert_eq!(record_schema.aliases, Some(vec!["alias_1".try_into()?]));
128        assert_eq!(record_schema.doc, None);
129        assert_eq!(record_schema.fields.len(), 0);
130        assert_eq!(record_schema.lookup.len(), 0);
131        assert_eq!(record_schema.attributes.len(), 0);
132
133        Ok(())
134    }
135
136    #[test]
137    fn avro_rs_403_record_schema_builder_no_fields_with_doc() -> TestResult {
138        let name = Name::new("TestRecord")?;
139
140        let record_schema = RecordSchema::builder()
141            .name(name.clone())
142            .doc(Some("some_doc".into()))
143            .build();
144
145        assert_eq!(record_schema.name, name);
146        assert_eq!(record_schema.aliases, None);
147        assert_eq!(record_schema.doc, Some("some_doc".into()));
148        assert_eq!(record_schema.fields.len(), 0);
149        assert_eq!(record_schema.lookup.len(), 0);
150        assert_eq!(record_schema.attributes.len(), 0);
151
152        Ok(())
153    }
154
155    #[test]
156    fn avro_rs_403_record_schema_builder_no_fields_with_attributes() -> TestResult {
157        let name = Name::new("TestRecord")?;
158        let attrs: BTreeMap<String, Value> = [
159            ("bool_key".into(), Value::Bool(true)),
160            ("key_2".into(), Value::String("value_2".into())),
161        ]
162        .into_iter()
163        .collect();
164
165        let record_schema = RecordSchema::builder()
166            .name(name.clone())
167            .attributes(attrs.clone())
168            .build();
169
170        assert_eq!(record_schema.name, name);
171        assert_eq!(record_schema.aliases, None);
172        assert_eq!(record_schema.doc, None);
173        assert_eq!(record_schema.fields.len(), 0);
174        assert_eq!(record_schema.lookup.len(), 0);
175        assert_eq!(record_schema.attributes, attrs);
176
177        Ok(())
178    }
179
180    #[test]
181    fn avro_rs_403_record_schema_builder_with_fields() -> TestResult {
182        let name = Name::new("TestRecord")?;
183        let fields = vec![
184            RecordField::builder()
185                .name("field1_null")
186                .schema(Schema::Null)
187                .build(),
188            RecordField::builder()
189                .name("field2_bool")
190                .schema(Schema::Boolean)
191                .build(),
192        ];
193
194        let record_schema = RecordSchema::builder()
195            .name(name.clone())
196            .fields(fields.clone())
197            .build();
198
199        let expected_lookup: BTreeMap<String, usize> =
200            [("field1_null".into(), 0), ("field2_bool".into(), 1)]
201                .iter()
202                .cloned()
203                .collect();
204
205        assert_eq!(record_schema.name, name);
206        assert_eq!(record_schema.aliases, None);
207        assert_eq!(record_schema.doc, None);
208        assert_eq!(record_schema.fields, fields);
209        assert_eq!(record_schema.lookup, expected_lookup);
210        assert_eq!(record_schema.attributes.len(), 0);
211
212        Ok(())
213    }
214
215    #[test]
216    fn avro_rs_419_name_into() -> TestResult {
217        let schema = RecordSchema::builder().try_name("str_slice")?.build();
218        assert_eq!(schema.name, "str_slice".try_into()?);
219
220        let schema = RecordSchema::builder()
221            .try_name("String".to_string())?
222            .build();
223        assert_eq!(schema.name, "String".try_into()?);
224
225        Ok(())
226    }
227}