blob: 45027989f5954e7b3a4281fa5f9cc6eda8631621 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//! # Custom name validation
//!
//! By default, the library follows the rules specified in the [Avro specification](https://avro.apache.org/docs/++version++/specification/#names).
//!
//! Some of the other Apache Avro language SDKs are more flexible in their name validation. For
//! interoperability with those SDKs, the library provides a way to customize the name validation.
//!
//! ```
//! # use apache_avro::{AvroResult, validator::{SchemaNameValidator, set_schema_name_validator}};
//! # use regex_lite::Regex;
//! # use std::sync::OnceLock;
//! struct DontAllowNamespaces;
//!
//! impl SchemaNameValidator for DontAllowNamespaces {
//! fn regex(&self) -> &'static Regex {
//! static SCHEMA_NAME_ONCE: OnceLock<Regex> = OnceLock::new();
//! SCHEMA_NAME_ONCE.get_or_init(|| {
//! Regex::new(
//! // Disallows any namespace. By naming the group `name`, the default
//! // implementation of `SchemaNameValidator::validate` can be reused.
//! r"^(?P<name>[A-Za-z_][A-Za-z0-9_]*)$",
//! ).expect("Regex is valid")
//! })
//! }
//! }
//!
//! // don't parse any schema before registering the custom validator(s)!
//!
//! if set_schema_name_validator(Box::new(DontAllowNamespaces)).is_err() {
//! // `.unwrap()` doesn't work as the return type does not implement `Debug`
//! panic!("There was already a schema validator configured")
//! }
//!
//! // ... use the library
//! ```
//!
//! Similar logic could be applied to the schema namespace, enum symbols and field names validation.
//!
//! **Note**: the library allows to set a validator only once per the application lifetime!
//! If the application parses schemas before setting a validator, the default validator will be
//! registered and used!
use crate::{AvroResult, error::Details};
use log::debug;
use regex_lite::Regex;
use std::sync::OnceLock;
/// A validator that validates names and namespaces according to the Avro specification.
struct SpecificationValidator;
/// A trait that validates schema names.
///
/// To register a custom one use [`set_schema_name_validator`].
pub trait SchemaNameValidator: Send + Sync {
/// The regex used to validate the schema name.
///
/// When the name part of the full name is provided as a capture group named `name`, the
/// default implementation of [`Self::validate`] can be used.
///
/// The default implementation uses the Avro specified regex.
fn regex(&self) -> &'static Regex {
static SCHEMA_NAME_ONCE: OnceLock<Regex> = OnceLock::new();
SCHEMA_NAME_ONCE.get_or_init(|| {
Regex::new(
// An optional namespace (with optional dots) followed by a name without any dots in it.
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()
})
}
/// Validates the schema name and returns the start byte of the name.
///
/// Requires that the implementation of [`Self::regex`] provides a capture group named `name`
/// that captures the name part of the full name.
///
/// Should return [`Details::InvalidSchemaName`] if it is invalid.
fn validate(&self, schema_name: &str) -> AvroResult<usize> {
let regex = SchemaNameValidator::regex(self);
let caps = regex
.captures(schema_name)
.ok_or_else(|| Details::InvalidSchemaName(schema_name.to_string(), regex.as_str()))?;
Ok(caps
.name("name")
.ok_or(Details::InvalidSchemaNameValidatorImplementation)?
.start())
}
}
impl SchemaNameValidator for SpecificationValidator {}
static NAME_VALIDATOR_ONCE: OnceLock<Box<dyn SchemaNameValidator + Send + Sync>> = OnceLock::new();
/// Sets a custom schema name validator.
///
/// Returns `Err(validator)` if a validator is already configured.
///
/// **Note**: This function must be called before parsing any schema because this will
/// register the default validator and the registration is one time only!
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<usize> {
NAME_VALIDATOR_ONCE
.get_or_init(|| {
debug!("Going to use the default name validator.");
Box::new(SpecificationValidator)
})
.validate(schema_name)
}
/// A trait that validates schema namespaces.
///
/// To register a custom one use [`set_schema_namespace_validator`].
pub trait SchemaNamespaceValidator: Send + Sync {
/// The regex used to validate the schema namespace.
///
/// The default implementation uses the Avro specified regex.
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()
})
}
/// Validates a schema namespace.
///
/// Should return [`Details::InvalidNamespace`] if it is invalid.
fn validate(&self, namespace: &str) -> AvroResult<()> {
let regex = SchemaNamespaceValidator::regex(self);
if !regex.is_match(namespace) {
Err(Details::InvalidNamespace(namespace.to_string(), regex.as_str()).into())
} else {
Ok(())
}
}
}
impl SchemaNamespaceValidator for SpecificationValidator {}
static NAMESPACE_VALIDATOR_ONCE: OnceLock<Box<dyn SchemaNamespaceValidator + Send + Sync>> =
OnceLock::new();
/// Sets a custom schema namespace validator.
///
/// Returns `Err(validator)` if a validator is already configured.
///
/// **Note**: This function must be called before parsing any schema because this will
/// register the default validator and the registration is one time only!
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)
}
/// A trait that validates enum symbol names.
///
/// To register a custom one use [`set_enum_symbol_name_validator`].
pub trait EnumSymbolNameValidator: Send + Sync {
/// The regex used to validate the symbols of enums.
///
/// The default implementation uses the Avro specified regex.
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())
}
/// Validate the symbol of an enum.
///
/// Should return [`Details::EnumSymbolName`] if it is invalid.
fn validate(&self, symbol: &str) -> AvroResult<()> {
let regex = EnumSymbolNameValidator::regex(self);
if !regex.is_match(symbol) {
return Err(Details::EnumSymbolName(symbol.to_string()).into());
}
Ok(())
}
}
impl EnumSymbolNameValidator for SpecificationValidator {}
static ENUM_SYMBOL_NAME_VALIDATOR_ONCE: OnceLock<Box<dyn EnumSymbolNameValidator + Send + Sync>> =
OnceLock::new();
/// Sets a custom enum symbol name validator.
///
/// Returns `Err(validator)` if a validator is already configured.
///
/// **Note**: This function must be called before parsing any schema because this will
/// register the default validator and the registration is one time only!
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)
}
/// A trait that validates record field names.
///
/// To register a custom one use [`set_record_field_name_validator`].
pub trait RecordFieldNameValidator: Send + Sync {
/// The regex used to validate the record field names.
///
/// The default implementation uses the Avro specified regex.
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())
}
/// Validate the name of a record field.
///
/// Should return [`Details::FieldName`] if it is invalid.
fn validate(&self, field_name: &str) -> AvroResult<()> {
let regex = RecordFieldNameValidator::regex(self);
if !regex.is_match(field_name) {
return Err(Details::FieldName(field_name.to_string()).into());
}
Ok(())
}
}
impl RecordFieldNameValidator for SpecificationValidator {}
static RECORD_FIELD_NAME_VALIDATOR_ONCE: OnceLock<Box<dyn RecordFieldNameValidator + Send + Sync>> =
OnceLock::new();
/// Sets a custom record field name validator.
///
/// Returns `Err(validator)` if a validator is already configured.
///
/// **Note**: This function must be called before parsing any schema because this will
/// register the default validator and the registration is one time only!
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 = Details::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 = Details::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(())
}
}