This is an automated email from the ASF dual-hosted git repository.
mgrigorov pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/avro-rs.git
The following commit(s) were added to refs/heads/main by this push:
new 66b94cb chore: Extract Name/Namespace/Alias/Aliases to
src/schema/name.rs (#417)
66b94cb is described below
commit 66b94cb1a0ce305eb0c96dea05e882aa9548b9d8
Author: Martin Grigorov <[email protected]>
AuthorDate: Tue Jan 20 14:36:14 2026 +0200
chore: Extract Name/Namespace/Alias/Aliases to src/schema/name.rs (#417)
* chore: Extract Name/Namespace/Alias/Aliases to src/schema/name.rs
No functional changes!
* Move Documentation typealias back to schema/mod.rs
---
avro/src/schema/mod.rs | 238 ++-----------------------------------------
avro/src/schema/name.rs | 263 ++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 270 insertions(+), 231 deletions(-)
diff --git a/avro/src/schema/mod.rs b/avro/src/schema/mod.rs
index d915f46..7f453f4 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, Name, Names, NamesRef, Namespace};
mod record;
use record::RecordSchemaParseLocation;
pub use record::{
@@ -49,6 +49,9 @@ pub use union::UnionSchema;
mod parser;
use parser::Parser;
+/// Represents documentation for complex Avro schemas.
+pub type Documentation = Option<String>;
+
/// Represents an Avro schema fingerprint
/// More information about Avro schema fingerprints can be found in the
/// [Avro Schema Fingerprint
documentation](https://avro.apache.org/docs/current/specification/#schema-fingerprints)
@@ -229,195 +232,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 +1581,7 @@ mod tests {
TestResult,
logger::{assert_logged, assert_not_logged},
};
+ use serde::{Deserialize, Serialize};
use serde_json::json;
#[test]
@@ -3014,34 +2829,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 +5518,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..7873dc0
--- /dev/null
+++ b/avro/src/schema/name.rs
@@ -0,0 +1,263 @@
+// 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 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(())
+ }
+}