use crate::{schema::Namespace, AvroResult, Error};
use regex_lite::Regex;
use std::sync::OnceLock;
struct SpecificationValidator;
pub trait SchemaNameValidator: Send + Sync {
fn regex(&self) -> &'static Regex {
static SCHEMA_NAME_ONCE: OnceLock<Regex> = OnceLock::new();
SCHEMA_NAME_ONCE.get_or_init(|| {
Regex::new(
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_]*)$",
)
.unwrap()
})
}
fn validate(&self, schema_name: &str) -> AvroResult<(String, Namespace)>;
}
impl SchemaNameValidator for SpecificationValidator {
fn validate(&self, schema_name: &str) -> AvroResult<(String, Namespace)> {
let regex = SchemaNameValidator::regex(self);
let caps = regex
.captures(schema_name)
.ok_or_else(|| Error::InvalidSchemaName(schema_name.to_string(), regex.as_str()))?;
Ok((
caps["name"].to_string(),
caps.name("namespace").map(|s| s.as_str().to_string()),
))
}
}
static NAME_VALIDATOR_ONCE: OnceLock<Box<dyn SchemaNameValidator + Send + Sync>> = OnceLock::new();
pub fn set_schema_name_validator(
validator: Box<dyn SchemaNameValidator + Send + Sync>,
) -> Result<(), Box<dyn SchemaNameValidator + Send + Sync>> {
debug!("Setting a custom schema name validator.");
NAME_VALIDATOR_ONCE.set(validator)
}
pub(crate) fn validate_schema_name(schema_name: &str) -> AvroResult<(String, Namespace)> {
NAME_VALIDATOR_ONCE
.get_or_init(|| {
debug!("Going to use the default name validator.");
Box::new(SpecificationValidator)
})
.validate(schema_name)
}
pub trait SchemaNamespaceValidator: Send + Sync {
fn regex(&self) -> &'static Regex {
static NAMESPACE_ONCE: OnceLock<Regex> = OnceLock::new();
NAMESPACE_ONCE.get_or_init(|| {
Regex::new(r"^([A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*)?$").unwrap()
})
}
fn validate(&self, namespace: &str) -> AvroResult<()>;
}
impl SchemaNamespaceValidator for SpecificationValidator {
fn validate(&self, ns: &str) -> AvroResult<()> {
let regex = SchemaNamespaceValidator::regex(self);
if !regex.is_match(ns) {
return Err(Error::InvalidNamespace(ns.to_string(), regex.as_str()));
} else {
Ok(())
}
}
}
static NAMESPACE_VALIDATOR_ONCE: OnceLock<Box<dyn SchemaNamespaceValidator + Send + Sync>> =
OnceLock::new();
pub fn set_schema_namespace_validator(
validator: Box<dyn SchemaNamespaceValidator + Send + Sync>,
) -> Result<(), Box<dyn SchemaNamespaceValidator + Send + Sync>> {
NAMESPACE_VALIDATOR_ONCE.set(validator)
}
pub(crate) fn validate_namespace(ns: &str) -> AvroResult<()> {
NAMESPACE_VALIDATOR_ONCE
.get_or_init(|| {
debug!("Going to use the default namespace validator.");
Box::new(SpecificationValidator)
})
.validate(ns)
}
pub trait EnumSymbolNameValidator: Send + Sync {
fn regex(&self) -> &'static Regex {
static ENUM_SYMBOL_NAME_ONCE: OnceLock<Regex> = OnceLock::new();
ENUM_SYMBOL_NAME_ONCE.get_or_init(|| Regex::new(r"^[A-Za-z_][A-Za-z0-9_]*$").unwrap())
}
fn validate(&self, name: &str) -> AvroResult<()>;
}
impl EnumSymbolNameValidator for SpecificationValidator {
fn validate(&self, symbol: &str) -> AvroResult<()> {
let regex = EnumSymbolNameValidator::regex(self);
if !regex.is_match(symbol) {
return Err(Error::EnumSymbolName(symbol.to_string()));
}
Ok(())
}
}
static ENUM_SYMBOL_NAME_VALIDATOR_ONCE: OnceLock<Box<dyn EnumSymbolNameValidator + Send + Sync>> =
OnceLock::new();
pub fn set_enum_symbol_name_validator(
validator: Box<dyn EnumSymbolNameValidator + Send + Sync>,
) -> Result<(), Box<dyn EnumSymbolNameValidator + Send + Sync>> {
ENUM_SYMBOL_NAME_VALIDATOR_ONCE.set(validator)
}
pub(crate) fn validate_enum_symbol_name(symbol: &str) -> AvroResult<()> {
ENUM_SYMBOL_NAME_VALIDATOR_ONCE
.get_or_init(|| {
debug!("Going to use the default enum symbol name validator.");
Box::new(SpecificationValidator)
})
.validate(symbol)
}
pub trait RecordFieldNameValidator: Send + Sync {
fn regex(&self) -> &'static Regex {
static FIELD_NAME_ONCE: OnceLock<Regex> = OnceLock::new();
FIELD_NAME_ONCE.get_or_init(|| Regex::new(r"^[A-Za-z_][A-Za-z0-9_]*$").unwrap())
}
fn validate(&self, name: &str) -> AvroResult<()>;
}
impl RecordFieldNameValidator for SpecificationValidator {
fn validate(&self, field_name: &str) -> AvroResult<()> {
let regex = RecordFieldNameValidator::regex(self);
if !regex.is_match(field_name) {
return Err(Error::FieldName(field_name.to_string()));
}
Ok(())
}
}
static RECORD_FIELD_NAME_VALIDATOR_ONCE: OnceLock<Box<dyn RecordFieldNameValidator + Send + Sync>> =
OnceLock::new();
pub fn set_record_field_name_validator(
validator: Box<dyn RecordFieldNameValidator + Send + Sync>,
) -> Result<(), Box<dyn RecordFieldNameValidator + Send + Sync>> {
RECORD_FIELD_NAME_VALIDATOR_ONCE.set(validator)
}
pub(crate) fn validate_record_field_name(field_name: &str) -> AvroResult<()> {
RECORD_FIELD_NAME_VALIDATOR_ONCE
.get_or_init(|| {
debug!("Going to use the default record field name validator.");
Box::new(SpecificationValidator)
})
.validate(field_name)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::Name;
use apache_avro_test_helper::TestResult;
#[test]
fn avro_3900_default_name_validator_with_valid_ns() -> TestResult {
validate_schema_name("example")?;
Ok(())
}
#[test]
fn avro_3900_default_name_validator_with_invalid_ns() -> TestResult {
assert!(validate_schema_name("com-example").is_err());
Ok(())
}
#[test]
fn test_avro_3897_disallow_invalid_namespaces_in_fully_qualified_name() -> TestResult {
let full_name = "ns.0.record1";
let name = Name::new(full_name);
assert!(name.is_err());
let validator = SpecificationValidator;
let expected = Error::InvalidSchemaName(
full_name.to_string(),
SchemaNameValidator::regex(&validator).as_str(),
)
.to_string();
let err = name.map_err(|e| e.to_string()).err().unwrap();
pretty_assertions::assert_eq!(expected, err);
let full_name = "ns..record1";
let name = Name::new(full_name);
assert!(name.is_err());
let expected = Error::InvalidSchemaName(
full_name.to_string(),
SchemaNameValidator::regex(&validator).as_str(),
)
.to_string();
let err = name.map_err(|e| e.to_string()).err().unwrap();
pretty_assertions::assert_eq!(expected, err);
Ok(())
}
#[test]
fn avro_3900_default_namespace_validator_with_valid_ns() -> TestResult {
validate_namespace("com.example")?;
Ok(())
}
#[test]
fn avro_3900_default_namespace_validator_with_invalid_ns() -> TestResult {
assert!(validate_namespace("com-example").is_err());
Ok(())
}
#[test]
fn avro_3900_default_enum_symbol_validator_with_valid_symbol_name() -> TestResult {
validate_enum_symbol_name("spades")?;
Ok(())
}
#[test]
fn avro_3900_default_enum_symbol_validator_with_invalid_symbol_name() -> TestResult {
assert!(validate_enum_symbol_name("com-example").is_err());
Ok(())
}
#[test]
fn avro_3900_default_record_field_validator_with_valid_name() -> TestResult {
validate_record_field_name("test")?;
Ok(())
}
#[test]
fn avro_3900_default_record_field_validator_with_invalid_name() -> TestResult {
assert!(validate_record_field_name("com-example").is_err());
Ok(())
}
}