Skip to main content

apache_avro/schema/record/
field.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::AvroResult;
19use crate::error::Details;
20use crate::schema::{Documentation, Name, Names, Parser, Schema, SchemaKind};
21use crate::types;
22use crate::util::MapHelper;
23use crate::validator::validate_record_field_name;
24use log::warn;
25use serde::ser::SerializeMap;
26use serde::{Serialize, Serializer};
27use serde_json::{Map, Value};
28use std::collections::BTreeMap;
29use std::fmt::{Debug, Formatter};
30
31/// Represents a `field` in a `record` Avro schema.
32#[derive(bon::Builder, Clone, PartialEq)]
33pub struct RecordField {
34    /// Name of the field.
35    #[builder(into)]
36    pub name: String,
37    /// Documentation of the field.
38    #[builder(default)]
39    pub doc: Documentation,
40    /// Aliases of the field's name. They have no namespace.
41    #[builder(default)]
42    pub aliases: Vec<String>,
43    /// Default value of the field.
44    /// This value will be used when reading Avro datum if schema resolution
45    /// is enabled.
46    pub default: Option<Value>,
47    /// Schema of the field.
48    pub schema: Schema,
49    /// A collection of all unknown fields in the record field.
50    #[builder(default = BTreeMap::new())]
51    pub custom_attributes: BTreeMap<String, Value>,
52}
53
54impl Debug for RecordField {
55    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
56        let mut debug = f.debug_struct("RecordField");
57        debug.field("name", &self.name);
58        if let Some(doc) = &self.doc {
59            debug.field("doc", &doc);
60        }
61        if !self.aliases.is_empty() {
62            debug.field("aliases", &self.aliases);
63        }
64        if let Some(default) = &self.default {
65            debug.field("default", &default);
66        }
67        debug.field("schema", &self.schema);
68        if !self.custom_attributes.is_empty() {
69            debug.field("custom_attributes", &self.custom_attributes);
70        }
71        if self.doc.is_none()
72            || self.aliases.is_empty()
73            || self.default.is_none()
74            || self.custom_attributes.is_empty()
75        {
76            debug.finish_non_exhaustive()
77        } else {
78            debug.finish()
79        }
80    }
81}
82
83impl RecordField {
84    /// Parse a `serde_json::Value` into a `RecordField`.
85    pub(crate) fn parse(
86        field: &Map<String, Value>,
87        parser: &mut Parser,
88        enclosing_record: &Name,
89    ) -> AvroResult<Self> {
90        let name = field.name().ok_or(Details::GetNameFieldFromRecord)?;
91
92        validate_record_field_name(name)?;
93
94        let ty = field.get("type").ok_or(Details::GetRecordFieldTypeField)?;
95        let schema = parser.parse(ty, enclosing_record.namespace())?;
96
97        if let Some(logical_type) = field.get("logicalType") {
98            warn!(
99                "Ignored the {enclosing_record}.logicalType property (`{logical_type}`). It should probably be nested inside the `type` for the field"
100            );
101        }
102
103        let default = field.get("default").cloned();
104        Self::resolve_default_value(
105            &schema,
106            name,
107            &enclosing_record.fullname(None),
108            parser.get_parsed_schemas(),
109            &default,
110        )?;
111
112        let aliases = field
113            .get("aliases")
114            .and_then(|aliases| {
115                aliases.as_array().map(|aliases| {
116                    aliases
117                        .iter()
118                        .flat_map(|alias| alias.as_str())
119                        .map(|alias| alias.to_string())
120                        .collect::<Vec<String>>()
121                })
122            })
123            .unwrap_or_default();
124
125        Ok(RecordField {
126            name: name.into(),
127            doc: field.doc(),
128            default,
129            aliases,
130            custom_attributes: RecordField::get_field_custom_attributes(field),
131            schema,
132        })
133    }
134
135    fn resolve_default_value(
136        field_schema: &Schema,
137        field_name: &str,
138        record_name: &str,
139        names: &Names,
140        default: &Option<Value>,
141    ) -> AvroResult<()> {
142        if let Some(value) = default {
143            let avro_value = types::Value::try_from(value.clone())?;
144            if let Schema::Union(union_schema) = field_schema {
145                let schemas = &union_schema.schemas;
146                let resolved = schemas.iter().any(|schema| {
147                    avro_value
148                        .to_owned()
149                        .resolve_internal(schema, names, schema.namespace(), &None)
150                        .is_ok()
151                });
152
153                if !resolved {
154                    let schema: Option<&Schema> = schemas.first();
155                    return match schema {
156                        Some(first_schema) => Err(Details::GetDefaultUnion(
157                            SchemaKind::from(first_schema),
158                            types::ValueKind::from(avro_value),
159                        )
160                        .into()),
161                        None => Err(Details::EmptyUnion.into()),
162                    };
163                }
164            } else {
165                let resolved = avro_value
166                    .resolve_internal(field_schema, names, field_schema.namespace(), &None)
167                    .is_ok();
168
169                if !resolved {
170                    let schemata = names.values().cloned().collect::<Vec<_>>();
171                    return Err(Details::GetDefaultRecordField(
172                        field_name.to_string(),
173                        record_name.to_string(),
174                        field_schema
175                            .independent_canonical_form(&schemata)
176                            .unwrap_or_else(|_| field_schema.canonical_form()),
177                        value.clone(),
178                    )
179                    .into());
180                }
181            };
182        }
183
184        Ok(())
185    }
186
187    fn get_field_custom_attributes(field: &Map<String, Value>) -> BTreeMap<String, Value> {
188        let mut custom_attributes: BTreeMap<String, Value> = BTreeMap::new();
189        for (key, value) in field {
190            match key.as_str() {
191                "type" | "name" | "doc" | "default" | "aliases" => continue,
192                _ => custom_attributes.insert(key.clone(), value.clone()),
193            };
194        }
195        custom_attributes
196    }
197
198    /// Returns true if this `RecordField` is nullable, meaning the schema is a `UnionSchema` where the first variant is `Null`.
199    pub fn is_nullable(&self) -> bool {
200        match self.schema {
201            Schema::Union(ref inner) => inner.is_nullable(),
202            _ => false,
203        }
204    }
205}
206
207impl Serialize for RecordField {
208    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
209    where
210        S: Serializer,
211    {
212        let mut map = serializer.serialize_map(None)?;
213        map.serialize_entry("name", &self.name)?;
214        map.serialize_entry("type", &self.schema)?;
215
216        if let Some(default) = &self.default {
217            map.serialize_entry("default", default)?;
218        }
219
220        if let Some(doc) = &self.doc {
221            map.serialize_entry("doc", doc)?;
222        }
223
224        if !self.aliases.is_empty() {
225            map.serialize_entry("aliases", &self.aliases)?;
226        }
227
228        for attr in &self.custom_attributes {
229            map.serialize_entry(attr.0, attr.1)?;
230        }
231
232        map.end()
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239    use crate::schema::{Name, Schema, UnionSchema};
240    use apache_avro_test_helper::TestResult;
241    use serde_json::json;
242
243    #[test]
244    fn test_avro_3621_nullable_record_field() -> TestResult {
245        let nullable_record_field = RecordField::builder()
246            .name("next".to_string())
247            .schema(Schema::Union(UnionSchema::new(vec![
248                Schema::Null,
249                Schema::Ref {
250                    name: Name::new("LongList")?,
251                },
252            ])?))
253            .build();
254
255        assert!(nullable_record_field.is_nullable());
256
257        let non_nullable_record_field = RecordField::builder()
258            .name("next".to_string())
259            .default(json!(2))
260            .schema(Schema::Long)
261            .build();
262
263        assert!(!non_nullable_record_field.is_nullable());
264        Ok(())
265    }
266
267    #[test]
268    fn avro_rs_419_name_into() -> TestResult {
269        let field = RecordField::builder()
270            .name("str_slice")
271            .schema(Schema::Boolean)
272            .build();
273        assert_eq!(field.name, "str_slice");
274
275        let field = RecordField::builder()
276            .name("String".to_string())
277            .schema(Schema::Boolean)
278            .build();
279        assert_eq!(field.name, "String");
280
281        Ok(())
282    }
283}