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    fields
86        .iter()
87        .enumerate()
88        .map(|(i, field)| (field.name.clone(), i))
89        .collect()
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use crate::Schema;
96    use apache_avro_test_helper::TestResult;
97    use pretty_assertions::assert_eq;
98
99    #[test]
100    fn avro_rs_403_record_schema_builder_no_fields() -> TestResult {
101        let name = Name::new("TestRecord")?;
102
103        let record_schema = RecordSchema::builder().name(name.clone()).build();
104
105        assert_eq!(record_schema.name, name);
106        assert_eq!(record_schema.aliases, None);
107        assert_eq!(record_schema.doc, None);
108        assert_eq!(record_schema.fields.len(), 0);
109        assert_eq!(record_schema.lookup.len(), 0);
110        assert_eq!(record_schema.attributes.len(), 0);
111
112        Ok(())
113    }
114
115    #[test]
116    fn avro_rs_403_record_schema_builder_no_fields_with_aliases() -> TestResult {
117        let name = Name::new("TestRecord")?;
118
119        let record_schema = RecordSchema::builder()
120            .name(name.clone())
121            .aliases(Some(vec!["alias_1".try_into()?]))
122            .build();
123
124        assert_eq!(record_schema.name, name);
125        assert_eq!(record_schema.aliases, Some(vec!["alias_1".try_into()?]));
126        assert_eq!(record_schema.doc, None);
127        assert_eq!(record_schema.fields.len(), 0);
128        assert_eq!(record_schema.lookup.len(), 0);
129        assert_eq!(record_schema.attributes.len(), 0);
130
131        Ok(())
132    }
133
134    #[test]
135    fn avro_rs_403_record_schema_builder_no_fields_with_doc() -> TestResult {
136        let name = Name::new("TestRecord")?;
137
138        let record_schema = RecordSchema::builder()
139            .name(name.clone())
140            .doc(Some("some_doc".into()))
141            .build();
142
143        assert_eq!(record_schema.name, name);
144        assert_eq!(record_schema.aliases, None);
145        assert_eq!(record_schema.doc, Some("some_doc".into()));
146        assert_eq!(record_schema.fields.len(), 0);
147        assert_eq!(record_schema.lookup.len(), 0);
148        assert_eq!(record_schema.attributes.len(), 0);
149
150        Ok(())
151    }
152
153    #[test]
154    fn avro_rs_403_record_schema_builder_no_fields_with_attributes() -> TestResult {
155        let name = Name::new("TestRecord")?;
156        let attrs: BTreeMap<String, Value> = [
157            ("bool_key".into(), Value::Bool(true)),
158            ("key_2".into(), Value::String("value_2".into())),
159        ]
160        .into_iter()
161        .collect();
162
163        let record_schema = RecordSchema::builder()
164            .name(name.clone())
165            .attributes(attrs.clone())
166            .build();
167
168        assert_eq!(record_schema.name, name);
169        assert_eq!(record_schema.aliases, None);
170        assert_eq!(record_schema.doc, None);
171        assert_eq!(record_schema.fields.len(), 0);
172        assert_eq!(record_schema.lookup.len(), 0);
173        assert_eq!(record_schema.attributes, attrs);
174
175        Ok(())
176    }
177
178    #[test]
179    fn avro_rs_403_record_schema_builder_with_fields() -> TestResult {
180        let name = Name::new("TestRecord")?;
181        let fields = vec![
182            RecordField::builder()
183                .name("field1_null")
184                .schema(Schema::Null)
185                .build(),
186            RecordField::builder()
187                .name("field2_bool")
188                .schema(Schema::Boolean)
189                .build(),
190        ];
191
192        let record_schema = RecordSchema::builder()
193            .name(name.clone())
194            .fields(fields.clone())
195            .build();
196
197        let expected_lookup: BTreeMap<String, usize> =
198            [("field1_null".into(), 0), ("field2_bool".into(), 1)]
199                .iter()
200                .cloned()
201                .collect();
202
203        assert_eq!(record_schema.name, name);
204        assert_eq!(record_schema.aliases, None);
205        assert_eq!(record_schema.doc, None);
206        assert_eq!(record_schema.fields, fields);
207        assert_eq!(record_schema.lookup, expected_lookup);
208        assert_eq!(record_schema.attributes.len(), 0);
209
210        Ok(())
211    }
212
213    #[test]
214    fn avro_rs_419_name_into() -> TestResult {
215        let schema = RecordSchema::builder().try_name("str_slice")?.build();
216        assert_eq!(schema.name, "str_slice".try_into()?);
217
218        let schema = RecordSchema::builder()
219            .try_name("String".to_string())?
220            .build();
221        assert_eq!(schema.name, "String".try_into()?);
222
223        Ok(())
224    }
225}