apache_avro/schema/record/
field.rs1use 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#[derive(bon::Builder, Clone, PartialEq)]
33pub struct RecordField {
34 #[builder(into)]
36 pub name: String,
37 #[builder(default)]
39 pub doc: Documentation,
40 #[builder(default)]
42 pub aliases: Vec<String>,
43 pub default: Option<Value>,
47 pub schema: Schema,
49 #[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 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 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}