1use crate::{AvroResult, error::Details, schema::Namespace};
55use log::debug;
56use regex_lite::Regex;
57use std::sync::OnceLock;
58
59struct SpecificationValidator;
61
62pub trait SchemaNameValidator: Send + Sync {
66 fn regex(&self) -> &'static Regex {
70 static SCHEMA_NAME_ONCE: OnceLock<Regex> = OnceLock::new();
71 SCHEMA_NAME_ONCE.get_or_init(|| {
72 Regex::new(
73 r"^((?P<namespace>([A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*)?)\.)?(?P<name>[A-Za-z_][A-Za-z0-9_]*)$",
75 )
76 .unwrap()
77 })
78 }
79
80 fn validate(&self, schema_name: &str) -> AvroResult<(String, Namespace)>;
84}
85
86impl SchemaNameValidator for SpecificationValidator {
87 fn validate(&self, schema_name: &str) -> AvroResult<(String, Namespace)> {
88 let regex = SchemaNameValidator::regex(self);
89 let caps = regex
90 .captures(schema_name)
91 .ok_or_else(|| Details::InvalidSchemaName(schema_name.to_string(), regex.as_str()))?;
92 Ok((
93 caps["name"].to_string(),
94 caps.name("namespace").map(|s| s.as_str().to_string()),
95 ))
96 }
97}
98
99static NAME_VALIDATOR_ONCE: OnceLock<Box<dyn SchemaNameValidator + Send + Sync>> = OnceLock::new();
100
101pub fn set_schema_name_validator(
108 validator: Box<dyn SchemaNameValidator + Send + Sync>,
109) -> Result<(), Box<dyn SchemaNameValidator + Send + Sync>> {
110 debug!("Setting a custom schema name validator.");
111 NAME_VALIDATOR_ONCE.set(validator)
112}
113
114pub(crate) fn validate_schema_name(schema_name: &str) -> AvroResult<(String, Namespace)> {
115 NAME_VALIDATOR_ONCE
116 .get_or_init(|| {
117 debug!("Going to use the default name validator.");
118 Box::new(SpecificationValidator)
119 })
120 .validate(schema_name)
121}
122
123pub trait SchemaNamespaceValidator: Send + Sync {
127 fn regex(&self) -> &'static Regex {
131 static NAMESPACE_ONCE: OnceLock<Regex> = OnceLock::new();
132 NAMESPACE_ONCE.get_or_init(|| {
133 Regex::new(r"^([A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*)?$").unwrap()
134 })
135 }
136
137 fn validate(&self, namespace: &str) -> AvroResult<()>;
141}
142
143impl SchemaNamespaceValidator for SpecificationValidator {
144 fn validate(&self, ns: &str) -> AvroResult<()> {
145 let regex = SchemaNamespaceValidator::regex(self);
146 if !regex.is_match(ns) {
147 Err(Details::InvalidNamespace(ns.to_string(), regex.as_str()).into())
148 } else {
149 Ok(())
150 }
151 }
152}
153
154static NAMESPACE_VALIDATOR_ONCE: OnceLock<Box<dyn SchemaNamespaceValidator + Send + Sync>> =
155 OnceLock::new();
156
157pub fn set_schema_namespace_validator(
164 validator: Box<dyn SchemaNamespaceValidator + Send + Sync>,
165) -> Result<(), Box<dyn SchemaNamespaceValidator + Send + Sync>> {
166 NAMESPACE_VALIDATOR_ONCE.set(validator)
167}
168
169pub(crate) fn validate_namespace(ns: &str) -> AvroResult<()> {
170 NAMESPACE_VALIDATOR_ONCE
171 .get_or_init(|| {
172 debug!("Going to use the default namespace validator.");
173 Box::new(SpecificationValidator)
174 })
175 .validate(ns)
176}
177
178pub trait EnumSymbolNameValidator: Send + Sync {
182 fn regex(&self) -> &'static Regex {
186 static ENUM_SYMBOL_NAME_ONCE: OnceLock<Regex> = OnceLock::new();
187 ENUM_SYMBOL_NAME_ONCE.get_or_init(|| Regex::new(r"^[A-Za-z_][A-Za-z0-9_]*$").unwrap())
188 }
189
190 fn validate(&self, name: &str) -> AvroResult<()>;
194}
195
196impl EnumSymbolNameValidator for SpecificationValidator {
197 fn validate(&self, symbol: &str) -> AvroResult<()> {
198 let regex = EnumSymbolNameValidator::regex(self);
199 if !regex.is_match(symbol) {
200 return Err(Details::EnumSymbolName(symbol.to_string()).into());
201 }
202
203 Ok(())
204 }
205}
206
207static ENUM_SYMBOL_NAME_VALIDATOR_ONCE: OnceLock<Box<dyn EnumSymbolNameValidator + Send + Sync>> =
208 OnceLock::new();
209
210pub fn set_enum_symbol_name_validator(
217 validator: Box<dyn EnumSymbolNameValidator + Send + Sync>,
218) -> Result<(), Box<dyn EnumSymbolNameValidator + Send + Sync>> {
219 ENUM_SYMBOL_NAME_VALIDATOR_ONCE.set(validator)
220}
221
222pub(crate) fn validate_enum_symbol_name(symbol: &str) -> AvroResult<()> {
223 ENUM_SYMBOL_NAME_VALIDATOR_ONCE
224 .get_or_init(|| {
225 debug!("Going to use the default enum symbol name validator.");
226 Box::new(SpecificationValidator)
227 })
228 .validate(symbol)
229}
230
231pub trait RecordFieldNameValidator: Send + Sync {
235 fn regex(&self) -> &'static Regex {
239 static FIELD_NAME_ONCE: OnceLock<Regex> = OnceLock::new();
240 FIELD_NAME_ONCE.get_or_init(|| Regex::new(r"^[A-Za-z_][A-Za-z0-9_]*$").unwrap())
241 }
242
243 fn validate(&self, name: &str) -> AvroResult<()>;
247}
248
249impl RecordFieldNameValidator for SpecificationValidator {
250 fn validate(&self, field_name: &str) -> AvroResult<()> {
251 let regex = RecordFieldNameValidator::regex(self);
252 if !regex.is_match(field_name) {
253 return Err(Details::FieldName(field_name.to_string()).into());
254 }
255
256 Ok(())
257 }
258}
259
260static RECORD_FIELD_NAME_VALIDATOR_ONCE: OnceLock<Box<dyn RecordFieldNameValidator + Send + Sync>> =
261 OnceLock::new();
262
263pub fn set_record_field_name_validator(
270 validator: Box<dyn RecordFieldNameValidator + Send + Sync>,
271) -> Result<(), Box<dyn RecordFieldNameValidator + Send + Sync>> {
272 RECORD_FIELD_NAME_VALIDATOR_ONCE.set(validator)
273}
274
275pub(crate) fn validate_record_field_name(field_name: &str) -> AvroResult<()> {
276 RECORD_FIELD_NAME_VALIDATOR_ONCE
277 .get_or_init(|| {
278 debug!("Going to use the default record field name validator.");
279 Box::new(SpecificationValidator)
280 })
281 .validate(field_name)
282}
283
284#[cfg(test)]
285mod tests {
286 use super::*;
287 use crate::schema::Name;
288 use apache_avro_test_helper::TestResult;
289
290 #[test]
291 fn avro_3900_default_name_validator_with_valid_ns() -> TestResult {
292 validate_schema_name("example")?;
293 Ok(())
294 }
295
296 #[test]
297 fn avro_3900_default_name_validator_with_invalid_ns() -> TestResult {
298 assert!(validate_schema_name("com-example").is_err());
299 Ok(())
300 }
301
302 #[test]
303 fn test_avro_3897_disallow_invalid_namespaces_in_fully_qualified_name() -> TestResult {
304 let full_name = "ns.0.record1";
305 let name = Name::new(full_name);
306 assert!(name.is_err());
307 let validator = SpecificationValidator;
308 let expected = Details::InvalidSchemaName(
309 full_name.to_string(),
310 SchemaNameValidator::regex(&validator).as_str(),
311 )
312 .to_string();
313 let err = name.map_err(|e| e.to_string()).err().unwrap();
314 pretty_assertions::assert_eq!(expected, err);
315
316 let full_name = "ns..record1";
317 let name = Name::new(full_name);
318 assert!(name.is_err());
319 let expected = Details::InvalidSchemaName(
320 full_name.to_string(),
321 SchemaNameValidator::regex(&validator).as_str(),
322 )
323 .to_string();
324 let err = name.map_err(|e| e.to_string()).err().unwrap();
325 pretty_assertions::assert_eq!(expected, err);
326 Ok(())
327 }
328
329 #[test]
330 fn avro_3900_default_namespace_validator_with_valid_ns() -> TestResult {
331 validate_namespace("com.example")?;
332 Ok(())
333 }
334
335 #[test]
336 fn avro_3900_default_namespace_validator_with_invalid_ns() -> TestResult {
337 assert!(validate_namespace("com-example").is_err());
338 Ok(())
339 }
340
341 #[test]
342 fn avro_3900_default_enum_symbol_validator_with_valid_symbol_name() -> TestResult {
343 validate_enum_symbol_name("spades")?;
344 Ok(())
345 }
346
347 #[test]
348 fn avro_3900_default_enum_symbol_validator_with_invalid_symbol_name() -> TestResult {
349 assert!(validate_enum_symbol_name("com-example").is_err());
350 Ok(())
351 }
352
353 #[test]
354 fn avro_3900_default_record_field_validator_with_valid_name() -> TestResult {
355 validate_record_field_name("test")?;
356 Ok(())
357 }
358
359 #[test]
360 fn avro_3900_default_record_field_validator_with_invalid_name() -> TestResult {
361 assert!(validate_record_field_name("com-example").is_err());
362 Ok(())
363 }
364}