This is an automated email from the ASF dual-hosted git repository. mgrigorov pushed a commit to branch extract-schema-name-to-its-own-module in repository https://gitbox.apache.org/repos/asf/avro-rs.git
commit a9a005d9a148e1899d60e59486586a6f5bd577c9 Author: Martin Tzvetanov Grigorov <[email protected]> AuthorDate: Tue Jan 20 10:42:45 2026 +0200 chore: Extract Name/Alias/Documentation to src/schema/name.rs No functional changes! --- avro/src/schema/mod.rs | 235 +----------------------------------------- avro/src/schema/name.rs | 265 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 269 insertions(+), 231 deletions(-) diff --git a/avro/src/schema/mod.rs b/avro/src/schema/mod.rs index d915f46..08ef661 100644 --- a/avro/src/schema/mod.rs +++ b/avro/src/schema/mod.rs @@ -20,12 +20,10 @@ use crate::{ AvroResult, error::{Details, Error}, schema_equality, types, - util::MapHelper, - validator::{validate_namespace, validate_schema_name}, }; use digest::Digest; use serde::{ - Deserialize, Serialize, Serializer, + Serialize, Serializer, ser::{SerializeMap, SerializeSeq}, }; use serde_json::{Map, Value}; @@ -39,6 +37,8 @@ use std::{ }; use strum_macros::{Display, EnumDiscriminants}; +mod name; +pub use name::{Alias, Aliases, Documentation, Name, Names, NamesRef, Namespace}; mod record; use record::RecordSchemaParseLocation; pub use record::{ @@ -229,195 +229,6 @@ impl From<&types::Value> for SchemaKind { } } -/// Represents names for `record`, `enum` and `fixed` Avro schemas. -/// -/// Each of these `Schema`s have a `fullname` composed of two parts: -/// * a name -/// * a namespace -/// -/// `aliases` can also be defined, to facilitate schema evolution. -/// -/// More information about schema names can be found in the -/// [Avro specification](https://avro.apache.org/docs/current/specification/#names) -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub struct Name { - pub name: String, - pub namespace: Namespace, -} - -/// Represents documentation for complex Avro schemas. -pub type Documentation = Option<String>; -/// Represents the aliases for Named Schema -pub type Aliases = Option<Vec<Alias>>; -/// Represents Schema lookup within a schema env -pub type Names = HashMap<Name, Schema>; -/// Represents Schema lookup within a schema -pub type NamesRef<'a> = HashMap<Name, &'a Schema>; -/// Represents the namespace for Named Schema -pub type Namespace = Option<String>; - -impl Name { - /// Create a new `Name`. - /// Parses the optional `namespace` from the `name` string. - /// `aliases` will not be defined. - pub fn new(name: &str) -> AvroResult<Self> { - let (name, namespace) = Name::get_name_and_namespace(name)?; - Ok(Self { - name, - namespace: namespace.filter(|ns| !ns.is_empty()), - }) - } - - fn get_name_and_namespace(name: &str) -> AvroResult<(String, Namespace)> { - validate_schema_name(name) - } - - /// Parse a `serde_json::Value` into a `Name`. - pub(crate) fn parse( - complex: &Map<String, Value>, - enclosing_namespace: &Namespace, - ) -> AvroResult<Self> { - let (name, namespace_from_name) = complex - .name() - .map(|name| Name::get_name_and_namespace(name.as_str()).unwrap()) - .ok_or(Details::GetNameField)?; - // FIXME Reading name from the type is wrong ! The name there is just a metadata (AVRO-3430) - let type_name = match complex.get("type") { - Some(Value::Object(complex_type)) => complex_type.name().or(None), - _ => None, - }; - - let namespace = namespace_from_name - .or_else(|| { - complex - .string("namespace") - .or_else(|| enclosing_namespace.clone()) - }) - .filter(|ns| !ns.is_empty()); - - if let Some(ref ns) = namespace { - validate_namespace(ns)?; - } - - Ok(Self { - name: type_name.unwrap_or(name), - namespace, - }) - } - - /// Return the `fullname` of this `Name` - /// - /// More information about fullnames can be found in the - /// [Avro specification](https://avro.apache.org/docs/current/specification/#names) - pub fn fullname(&self, default_namespace: Namespace) -> String { - if self.name.contains('.') { - self.name.clone() - } else { - let namespace = self.namespace.clone().or(default_namespace); - - match namespace { - Some(ref namespace) if !namespace.is_empty() => { - format!("{}.{}", namespace, self.name) - } - _ => self.name.clone(), - } - } - } - - /// Return the fully qualified name needed for indexing or searching for the schema within a schema/schema env context. Puts the enclosing namespace into the name's namespace for clarity in schema/schema env parsing - /// ```ignore - /// use apache_avro::schema::Name; - /// - /// assert_eq!( - /// Name::new("some_name")?.fully_qualified_name(&Some("some_namespace".into())), - /// Name::new("some_namespace.some_name")? - /// ); - /// assert_eq!( - /// Name::new("some_namespace.some_name")?.fully_qualified_name(&Some("other_namespace".into())), - /// Name::new("some_namespace.some_name")? - /// ); - /// ``` - pub fn fully_qualified_name(&self, enclosing_namespace: &Namespace) -> Name { - Name { - name: self.name.clone(), - namespace: self - .namespace - .clone() - .or_else(|| enclosing_namespace.clone().filter(|ns| !ns.is_empty())), - } - } -} - -impl From<&str> for Name { - fn from(name: &str) -> Self { - Name::new(name).unwrap() - } -} - -impl fmt::Display for Name { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(&self.fullname(None)[..]) - } -} - -impl<'de> Deserialize<'de> for Name { - fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> - where - D: serde::de::Deserializer<'de>, - { - Value::deserialize(deserializer).and_then(|value| { - use serde::de::Error; - if let Value::Object(json) = value { - Name::parse(&json, &None).map_err(Error::custom) - } else { - Err(Error::custom(format!("Expected a JSON object: {value:?}"))) - } - }) - } -} - -/// Newtype pattern for `Name` to better control the `serde_json::Value` representation. -/// Aliases are serialized as an array of plain strings in the JSON representation. -#[derive(Clone, Debug, Hash, PartialEq, Eq)] -pub struct Alias(Name); - -impl Alias { - pub fn new(name: &str) -> AvroResult<Self> { - Name::new(name).map(Self) - } - - pub fn name(&self) -> String { - self.0.name.clone() - } - - pub fn namespace(&self) -> Namespace { - self.0.namespace.clone() - } - - pub fn fullname(&self, default_namespace: Namespace) -> String { - self.0.fullname(default_namespace) - } - - pub fn fully_qualified_name(&self, default_namespace: &Namespace) -> Name { - self.0.fully_qualified_name(default_namespace) - } -} - -impl From<&str> for Alias { - fn from(name: &str) -> Self { - Alias::new(name).unwrap() - } -} - -impl Serialize for Alias { - fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> - where - S: Serializer, - { - serializer.serialize_str(&self.fullname(None)) - } -} - #[derive(Debug)] pub struct ResolvedSchema<'s> { names_ref: NamesRef<'s>, @@ -1767,6 +1578,7 @@ mod tests { TestResult, logger::{assert_logged, assert_not_logged}, }; + use serde::{Deserialize, Serialize}; use serde_json::json; #[test] @@ -3014,34 +2826,6 @@ mod tests { Ok(()) } - #[test] - /// Zero-length namespace is considered as no-namespace. - fn test_namespace_from_name_with_empty_value() -> TestResult { - let name = Name::new(".name")?; - assert_eq!(name.name, "name"); - assert_eq!(name.namespace, None); - - Ok(()) - } - - #[test] - /// Whitespace is not allowed in the name. - fn test_name_with_whitespace_value() { - match Name::new(" ").map_err(Error::into_details) { - Err(Details::InvalidSchemaName(_, _)) => {} - _ => panic!("Expected an Details::InvalidSchemaName!"), - } - } - - #[test] - /// The name must be non-empty. - fn test_name_with_no_name_part() { - match Name::new("space.").map_err(Error::into_details) { - Err(Details::InvalidSchemaName(_, _)) => {} - _ => panic!("Expected an Details::InvalidSchemaName!"), - } - } - #[test] fn avro_3448_test_proper_resolution_inner_record_inherited_namespace() -> TestResult { let schema = r#" @@ -5731,17 +5515,6 @@ mod tests { Ok(()) } - /// A test cases showing that names and namespaces can be constructed - /// entirely by underscores. - #[test] - fn test_avro_3897_funny_valid_names_and_namespaces() -> TestResult { - for funny_name in ["_", "_._", "__._", "_.__", "_._._"] { - let name = Name::new(funny_name); - assert!(name.is_ok()); - } - Ok(()) - } - #[test] fn test_avro_3896_decimal_schema() -> TestResult { // bytes decimal, represented as native logical type. diff --git a/avro/src/schema/name.rs b/avro/src/schema/name.rs new file mode 100644 index 0000000..4dca670 --- /dev/null +++ b/avro/src/schema/name.rs @@ -0,0 +1,265 @@ +// 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. + +use std::collections::HashMap; +use std::fmt; + +use serde::{Deserialize, Serialize, Serializer}; +use serde_json::{Map, Value}; + +use crate::{ + AvroResult, Schema, + error::Details, + util::MapHelper, + validator::{validate_namespace, validate_schema_name}, +}; + +/// Represents names for `record`, `enum` and `fixed` Avro schemas. +/// +/// Each of these `Schema`s have a `fullname` composed of two parts: +/// * a name +/// * a namespace +/// +/// `aliases` can also be defined, to facilitate schema evolution. +/// +/// More information about schema names can be found in the +/// [Avro specification](https://avro.apache.org/docs/current/specification/#names) +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct Name { + pub name: String, + pub namespace: Namespace, +} + +/// Represents documentation for complex Avro schemas. +pub type Documentation = Option<String>; +/// Represents the aliases for Named Schema +pub type Aliases = Option<Vec<Alias>>; +/// Represents Schema lookup within a schema env +pub type Names = HashMap<Name, Schema>; +/// Represents Schema lookup within a schema +pub type NamesRef<'a> = HashMap<Name, &'a Schema>; +/// Represents the namespace for Named Schema +pub type Namespace = Option<String>; + +impl Name { + /// Create a new `Name`. + /// Parses the optional `namespace` from the `name` string. + /// `aliases` will not be defined. + pub fn new(name: &str) -> AvroResult<Self> { + let (name, namespace) = Name::get_name_and_namespace(name)?; + Ok(Self { + name, + namespace: namespace.filter(|ns| !ns.is_empty()), + }) + } + + fn get_name_and_namespace(name: &str) -> AvroResult<(String, Namespace)> { + validate_schema_name(name) + } + + /// Parse a `serde_json::Value` into a `Name`. + pub(crate) fn parse( + complex: &Map<String, Value>, + enclosing_namespace: &Namespace, + ) -> AvroResult<Self> { + let (name, namespace_from_name) = complex + .name() + .map(|name| Name::get_name_and_namespace(name.as_str()).unwrap()) + .ok_or(Details::GetNameField)?; + // FIXME Reading name from the type is wrong ! The name there is just a metadata (AVRO-3430) + let type_name = match complex.get("type") { + Some(Value::Object(complex_type)) => complex_type.name().or(None), + _ => None, + }; + + let namespace = namespace_from_name + .or_else(|| { + complex + .string("namespace") + .or_else(|| enclosing_namespace.clone()) + }) + .filter(|ns| !ns.is_empty()); + + if let Some(ref ns) = namespace { + validate_namespace(ns)?; + } + + Ok(Self { + name: type_name.unwrap_or(name), + namespace, + }) + } + + /// Return the `fullname` of this `Name` + /// + /// More information about fullnames can be found in the + /// [Avro specification](https://avro.apache.org/docs/current/specification/#names) + pub fn fullname(&self, default_namespace: Namespace) -> String { + if self.name.contains('.') { + self.name.clone() + } else { + let namespace = self.namespace.clone().or(default_namespace); + + match namespace { + Some(ref namespace) if !namespace.is_empty() => { + format!("{}.{}", namespace, self.name) + } + _ => self.name.clone(), + } + } + } + + /// Return the fully qualified name needed for indexing or searching for the schema within a schema/schema env context. Puts the enclosing namespace into the name's namespace for clarity in schema/schema env parsing + /// ```ignore + /// use apache_avro::schema::Name; + /// + /// assert_eq!( + /// Name::new("some_name")?.fully_qualified_name(&Some("some_namespace".into())), + /// Name::new("some_namespace.some_name")? + /// ); + /// assert_eq!( + /// Name::new("some_namespace.some_name")?.fully_qualified_name(&Some("other_namespace".into())), + /// Name::new("some_namespace.some_name")? + /// ); + /// ``` + pub fn fully_qualified_name(&self, enclosing_namespace: &Namespace) -> Name { + Name { + name: self.name.clone(), + namespace: self + .namespace + .clone() + .or_else(|| enclosing_namespace.clone().filter(|ns| !ns.is_empty())), + } + } +} + +impl From<&str> for Name { + fn from(name: &str) -> Self { + Name::new(name).unwrap() + } +} + +impl fmt::Display for Name { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.fullname(None)[..]) + } +} + +impl<'de> Deserialize<'de> for Name { + fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> + where + D: serde::de::Deserializer<'de>, + { + Value::deserialize(deserializer).and_then(|value| { + use serde::de::Error; + if let Value::Object(json) = value { + Name::parse(&json, &None).map_err(Error::custom) + } else { + Err(Error::custom(format!("Expected a JSON object: {value:?}"))) + } + }) + } +} + +/// Newtype pattern for `Name` to better control the `serde_json::Value` representation. +/// Aliases are serialized as an array of plain strings in the JSON representation. +#[derive(Clone, Debug, Hash, PartialEq, Eq)] +pub struct Alias(Name); + +impl Alias { + pub fn new(name: &str) -> AvroResult<Self> { + Name::new(name).map(Self) + } + + pub fn name(&self) -> String { + self.0.name.clone() + } + + pub fn namespace(&self) -> Namespace { + self.0.namespace.clone() + } + + pub fn fullname(&self, default_namespace: Namespace) -> String { + self.0.fullname(default_namespace) + } + + pub fn fully_qualified_name(&self, default_namespace: &Namespace) -> Name { + self.0.fully_qualified_name(default_namespace) + } +} + +impl From<&str> for Alias { + fn from(name: &str) -> Self { + Alias::new(name).unwrap() + } +} + +impl Serialize for Alias { + fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> + where + S: Serializer, + { + serializer.serialize_str(&self.fullname(None)) + } +} + +#[cfg(test)] +mod tests { + use crate::Error; + + use super::*; + use apache_avro_test_helper::TestResult; + + #[test] + /// Zero-length namespace is considered as no-namespace. + fn test_namespace_from_name_with_empty_value() -> TestResult { + let name = Name::new(".name")?; + assert_eq!(name.name, "name"); + assert_eq!(name.namespace, None); + + Ok(()) + } + + #[test] + /// Whitespace is not allowed in the name. + fn test_name_with_whitespace_value() { + match Name::new(" ").map_err(Error::into_details) { + Err(Details::InvalidSchemaName(_, _)) => {} + _ => panic!("Expected an Details::InvalidSchemaName!"), + } + } + + #[test] + /// The name must be non-empty. + fn test_name_with_no_name_part() { + match Name::new("space.").map_err(Error::into_details) { + Err(Details::InvalidSchemaName(_, _)) => {} + _ => panic!("Expected an Details::InvalidSchemaName!"), + } + } + + /// A test cases showing that names and namespaces can be constructed + /// entirely by underscores. + #[test] + fn test_avro_3897_funny_valid_names_and_namespaces() -> TestResult { + for funny_name in ["_", "_._", "__._", "_.__", "_._._"] { + let name = Name::new(funny_name); + assert!(name.is_ok()); + } + Ok(()) + } +}
