This is an automated email from the ASF dual-hosted git repository.
kriskras99 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 1933c43 feat!: Add a `UnionSchemaBuilder` (#489)
1933c43 is described below
commit 1933c43599740664fa5e0b184a45a130d9909946
Author: Kriskras99 <[email protected]>
AuthorDate: Fri Feb 27 13:24:03 2026 +0100
feat!: Add a `UnionSchemaBuilder` (#489)
* feat: Add a `UnionSchemaBuilder`
This also fixes a issue with the original `new` implementation where
it would insert named types in the `variant_index` and then
`find_schema_with_known_schemata` would use the fast path without
checking the schema.
`find_schema_with_known_schemata` has also been simplified to use
`known_schemata` directly instead of rebuilding it with the current
schema, as this would cause duplicate schema errors after the incorrect
fast path was removed.
Another issue fixed is that logical types cannot be added if their base type
is already in the union, and the same in reverse.
The `UnionSchemaBuilder::variant_ignore_duplicates` and
`UnionSchemaBuilder::contains`
are needed for `avro_derive` to implement full support for enums.
* fix: Support type promotion and resolve edge cases
* fix: Panic when `Option<()>::get_schema()` is called
* fix: Remove `PartialOrd,Ord` implementations from `Name`
---------
Co-authored-by: default <[email protected]>
---
avro/src/error.rs | 14 +-
avro/src/schema/mod.rs | 2 +-
avro/src/schema/union.rs | 561 ++++++++++++++++++++++++++++++++++++++++++-----
avro/src/serde/derive.rs | 16 ++
avro/src/types.rs | 9 +-
5 files changed, 532 insertions(+), 70 deletions(-)
diff --git a/avro/src/error.rs b/avro/src/error.rs
index 50a09af..12bee1e 100644
--- a/avro/src/error.rs
+++ b/avro/src/error.rs
@@ -300,8 +300,18 @@ pub enum Details {
#[error("Unions may not directly contain a union")]
GetNestedUnion,
- #[error("Unions cannot contain duplicate types")]
- GetUnionDuplicate,
+ #[error(
+ "Found two different maps while building Union: Schema::Map({0:?}),
Schema::Map({1:?})"
+ )]
+ GetUnionDuplicateMap(Schema, Schema),
+
+ #[error(
+ "Found two different arrays while building Union:
Schema::Array({0:?}), Schema::Array({1:?})"
+ )]
+ GetUnionDuplicateArray(Schema, Schema),
+
+ #[error("Unions cannot contain duplicate types, found at least two {0:?}")]
+ GetUnionDuplicate(SchemaKind),
#[error("Unions cannot contain more than one named schema with the same
name: {0}")]
GetUnionDuplicateNamedSchemas(String),
diff --git a/avro/src/schema/mod.rs b/avro/src/schema/mod.rs
index 5f5d81c..70d3066 100644
--- a/avro/src/schema/mod.rs
+++ b/avro/src/schema/mod.rs
@@ -33,7 +33,7 @@ pub use crate::schema::{
RecordField, RecordFieldBuilder, RecordFieldOrder, RecordSchema,
RecordSchemaBuilder,
},
resolve::ResolvedSchema,
- union::UnionSchema,
+ union::{UnionSchema, UnionSchemaBuilder},
};
use crate::{
AvroResult,
diff --git a/avro/src/schema/union.rs b/avro/src/schema/union.rs
index bca79aa..938e765 100644
--- a/avro/src/schema/union.rs
+++ b/avro/src/schema/union.rs
@@ -15,24 +15,30 @@
// specific language governing permissions and limitations
// under the License.
-use crate::AvroResult;
use crate::error::Details;
-use crate::schema::{Name, Namespace, ResolvedSchema, Schema, SchemaKind};
+use crate::schema::{
+ DecimalSchema, InnerDecimalSchema, Name, Namespace, Schema, SchemaKind,
UuidSchema,
+};
use crate::types;
+use crate::{AvroResult, Error};
use std::borrow::Borrow;
-use std::collections::{BTreeMap, HashMap, HashSet};
+use std::collections::{BTreeMap, HashMap};
use std::fmt::{Debug, Formatter};
+use strum::IntoDiscriminant;
/// A description of a Union schema
#[derive(Clone)]
pub struct UnionSchema {
/// The schemas that make up this union
pub(crate) schemas: Vec<Schema>,
- // Used to ensure uniqueness of schema inputs, and provide constant time
finding of the
- // schema index given a value.
- // **NOTE** that this approach does not work for named types, and will
have to be modified
- // to support that. A simple solution is to also keep a mapping of the
names used.
+ /// The indexes of unnamed types.
+ ///
+ /// Logical types have been reduced to their inner type.
variant_index: BTreeMap<SchemaKind, usize>,
+ /// The indexes of named types.
+ ///
+ /// The names themselves aren't saved as they aren't used.
+ named_index: Vec<usize>,
}
impl Debug for UnionSchema {
@@ -51,25 +57,16 @@ impl UnionSchema {
/// Will return an error if `schemas` has duplicate unnamed schemas or if
`schemas`
/// contains a union.
pub fn new(schemas: Vec<Schema>) -> AvroResult<Self> {
- let mut named_schemas: HashSet<&Name> = HashSet::default();
- let mut vindex = BTreeMap::new();
- for (i, schema) in schemas.iter().enumerate() {
- if let Schema::Union(_) = schema {
- return Err(Details::GetNestedUnion.into());
- } else if !schema.is_named() &&
vindex.insert(SchemaKind::from(schema), i).is_some() {
- return Err(Details::GetUnionDuplicate.into());
- } else if schema.is_named() {
- let name = schema.name().unwrap();
- if !named_schemas.insert(name) {
- return
Err(Details::GetUnionDuplicateNamedSchemas(name.to_string()).into());
- }
- vindex.insert(SchemaKind::from(schema), i);
- }
+ let mut builder = Self::builder();
+ for schema in schemas {
+ builder.variant(schema)?;
}
- Ok(UnionSchema {
- schemas,
- variant_index: vindex,
- })
+ Ok(builder.build())
+ }
+
+ /// Build a `UnionSchema` piece-by-piece.
+ pub fn builder() -> UnionSchemaBuilder {
+ UnionSchemaBuilder::new()
}
/// Returns a slice to all variants of this schema.
@@ -79,7 +76,7 @@ impl UnionSchema {
/// Returns true if the any of the variants of this `UnionSchema` is
`Null`.
pub fn is_nullable(&self) -> bool {
- self.schemas.iter().any(|x| matches!(x, Schema::Null))
+ self.variant_index.contains_key(&SchemaKind::Null)
}
/// Optionally returns a reference to the schema matched by this value, as
well as its position
@@ -93,45 +90,141 @@ impl UnionSchema {
known_schemata: Option<&HashMap<Name, S>>,
enclosing_namespace: &Namespace,
) -> Option<(usize, &Schema)> {
- let schema_kind = SchemaKind::from(value);
- if let Some(&i) = self.variant_index.get(&schema_kind) {
- // fast path
- Some((i, &self.schemas[i]))
- } else {
- // slow path (required for matching logical or named types)
-
- // first collect what schemas we already know
- let mut collected_names: HashMap<Name, &Schema> = known_schemata
- .map(|names| {
- names
- .iter()
- .map(|(name, schema)| (name.clone(), schema.borrow()))
- .collect()
+ let known_schemata_if_none = HashMap::new();
+ let known_schemata = known_schemata.unwrap_or(&known_schemata_if_none);
+ let ValueSchemaKind { unnamed, named } =
Self::value_to_base_schemakind(value);
+ // Unnamed schema types can be looked up directly using the
variant_index
+ let unnamed = unnamed
+ .and_then(|kind| self.variant_index.get(&kind).copied())
+ .map(|index| (index, &self.schemas[index]))
+ .and_then(|(index, schema)| {
+ let kind = schema.discriminant();
+ // Maps and arrays need to be checked if they actually match
the value
+ if kind == SchemaKind::Map || kind == SchemaKind::Array {
+ let namespace = if schema.namespace().is_some() {
+ &schema.namespace()
+ } else {
+ enclosing_namespace
+ };
+
+ // TODO: Do this without the clone
+ value
+ .clone()
+ .resolve_internal(schema, known_schemata, namespace,
&None)
+ .ok()
+ .map(|_| (index, schema))
+ } else {
+ Some((index, schema))
+ }
+ });
+ let named = named.and_then(|kind| {
+ // Every named type needs to be checked against a value until one
matches
+
+ self.named_index
+ .iter()
+ .copied()
+ .map(|i| (i, &self.schemas[i]))
+ .filter(|(_i, s)| {
+ let s_kind = schema_to_base_schemakind(s);
+ s_kind == kind || s_kind == SchemaKind::Ref
+ })
+ .find(|(_i, schema)| {
+ let namespace = if schema.namespace().is_some() {
+ &schema.namespace()
+ } else {
+ enclosing_namespace
+ };
+
+ // TODO: Do this without the clone
+ value
+ .clone()
+ .resolve_internal(schema, known_schemata, namespace,
&None)
+ .is_ok()
})
- .unwrap_or_default();
-
- self.schemas.iter().enumerate().find(|(_, schema)| {
- let resolved_schema = ResolvedSchema::new_with_known_schemata(
- vec![*schema],
- enclosing_namespace,
- &collected_names,
- )
- .expect("Schema didn't successfully parse");
- let resolved_names = resolved_schema.names_ref;
-
- // extend known schemas with just resolved names
- collected_names.extend(resolved_names);
- let namespace = &schema.namespace().or_else(||
enclosing_namespace.clone());
-
- value
- .clone()
- .resolve_internal(schema, &collected_names, namespace,
&None)
- .is_ok()
- })
+ });
+
+ match (unnamed, named) {
+ (Some((u_i, _)), Some((n_i, _))) if u_i < n_i => unnamed,
+ (Some(_), Some(_)) => named,
+ (Some(_), None) => unnamed,
+ (None, Some(_)) => named,
+ (None, None) => {
+ // Slow path, check if value can be promoted to any of the
types in the union
+ self.schemas.iter().enumerate().find(|(_i, schema)| {
+ let namespace = if schema.namespace().is_some() {
+ &schema.namespace()
+ } else {
+ enclosing_namespace
+ };
+
+ // TODO: Do this without the clone
+ value
+ .clone()
+ .resolve_internal(schema, known_schemata, namespace,
&None)
+ .is_ok()
+ })
+ }
+ }
+ }
+
+ /// Convert a value to a [`SchemaKind`] stripping logical types to their
base type.
+ fn value_to_base_schemakind(value: &types::Value) -> ValueSchemaKind {
+ let schemakind = SchemaKind::from(value);
+ match schemakind {
+ SchemaKind::Decimal => ValueSchemaKind {
+ unnamed: Some(SchemaKind::Bytes),
+ named: Some(SchemaKind::Fixed),
+ },
+ SchemaKind::BigDecimal => ValueSchemaKind {
+ unnamed: Some(SchemaKind::Bytes),
+ named: None,
+ },
+ SchemaKind::Uuid => ValueSchemaKind {
+ unnamed: Some(SchemaKind::String),
+ named: Some(SchemaKind::Fixed),
+ },
+ SchemaKind::Date | SchemaKind::TimeMillis => ValueSchemaKind {
+ unnamed: Some(SchemaKind::Int),
+ named: None,
+ },
+ SchemaKind::TimeMicros
+ | SchemaKind::TimestampMillis
+ | SchemaKind::TimestampMicros
+ | SchemaKind::TimestampNanos
+ | SchemaKind::LocalTimestampMillis
+ | SchemaKind::LocalTimestampMicros
+ | SchemaKind::LocalTimestampNanos => ValueSchemaKind {
+ unnamed: Some(SchemaKind::Long),
+ named: None,
+ },
+ SchemaKind::Duration => ValueSchemaKind {
+ unnamed: None,
+ named: Some(SchemaKind::Fixed),
+ },
+ SchemaKind::Record | SchemaKind::Enum | SchemaKind::Fixed =>
ValueSchemaKind {
+ unnamed: None,
+ named: Some(schemakind),
+ },
+ // When a `serde_json::Value` is converted to a `types::Value` a
object will always become a map
+ // so a `types::Value::Map` can also be a record.
+ SchemaKind::Map => ValueSchemaKind {
+ unnamed: Some(SchemaKind::Map),
+ named: Some(SchemaKind::Record),
+ },
+ _ => ValueSchemaKind {
+ unnamed: Some(schemakind),
+ named: None,
+ },
}
}
}
+/// The schema kinds matching a specific value.
+struct ValueSchemaKind {
+ unnamed: Option<SchemaKind>,
+ named: Option<SchemaKind>,
+}
+
// No need to compare variant_index, it is derivative of schemas.
impl PartialEq for UnionSchema {
fn eq(&self, other: &UnionSchema) -> bool {
@@ -139,11 +232,178 @@ impl PartialEq for UnionSchema {
}
}
+/// A builder for [`UnionSchema`]
+#[derive(Default, Debug)]
+pub struct UnionSchemaBuilder {
+ schemas: Vec<Schema>,
+ names: HashMap<Name, usize>,
+ variant_index: BTreeMap<SchemaKind, usize>,
+}
+
+impl UnionSchemaBuilder {
+ /// Create a builder.
+ ///
+ /// See also [`UnionSchema::builder`].
+ pub fn new() -> Self {
+ Self::default()
+ }
+
+ #[doc(hidden)]
+ /// This is not a public API, it should only be used by `avro_derive`
+ ///
+ /// Add a variant to this union, if it already exists ignore it.
+ ///
+ /// # Errors
+ /// Will return a [`Details::GetUnionDuplicateMap`] or
[`Details::GetUnionDuplicateArray`] if
+ /// duplicate maps or arrays are encountered with different subtypes.
+ pub fn variant_ignore_duplicates(&mut self, schema: Schema) -> Result<&mut
Self, Error> {
+ if let Some(name) = schema.name() {
+ if let Some(current) = self.names.get(name).copied() {
+ if self.schemas[current] != schema {
+ return
Err(Details::GetUnionDuplicateNamedSchemas(name.to_string()).into());
+ }
+ } else {
+ self.names.insert(name.clone(), self.schemas.len());
+ self.schemas.push(schema);
+ }
+ } else if let Schema::Map(_) = &schema {
+ if let Some(index) =
self.variant_index.get(&SchemaKind::Map).copied() {
+ if self.schemas[index] != schema {
+ return Err(
+
Details::GetUnionDuplicateMap(self.schemas[index].clone(), schema).into(),
+ );
+ }
+ } else {
+ self.variant_index
+ .insert(SchemaKind::Map, self.schemas.len());
+ self.schemas.push(schema);
+ }
+ } else if let Schema::Array(_) = &schema {
+ if let Some(index) =
self.variant_index.get(&SchemaKind::Array).copied() {
+ if self.schemas[index] != schema {
+ return Err(Details::GetUnionDuplicateArray(
+ self.schemas[index].clone(),
+ schema,
+ )
+ .into());
+ }
+ } else {
+ self.variant_index
+ .insert(SchemaKind::Array, self.schemas.len());
+ self.schemas.push(schema);
+ }
+ } else {
+ let discriminant = schema_to_base_schemakind(&schema);
+ if discriminant == SchemaKind::Union {
+ return Err(Details::GetNestedUnion.into());
+ }
+ if !self.variant_index.contains_key(&discriminant) {
+ self.variant_index.insert(discriminant, self.schemas.len());
+ self.schemas.push(schema);
+ }
+ }
+ Ok(self)
+ }
+
+ /// Add a variant to this union.
+ ///
+ /// # Errors
+ /// Will return a [`Details::GetUnionDuplicateNamedSchemas`] or
[`Details::GetUnionDuplicate`] if
+ /// duplicate names or schema kinds are found.
+ pub fn variant(&mut self, schema: Schema) -> Result<&mut Self, Error> {
+ if let Some(name) = schema.name() {
+ if self.names.contains_key(name) {
+ return
Err(Details::GetUnionDuplicateNamedSchemas(name.to_string()).into());
+ } else {
+ self.names.insert(name.clone(), self.schemas.len());
+ self.schemas.push(schema);
+ }
+ } else {
+ let discriminant = schema_to_base_schemakind(&schema);
+ if discriminant == SchemaKind::Union {
+ return Err(Details::GetNestedUnion.into());
+ }
+ if self.variant_index.contains_key(&discriminant) {
+ return Err(Details::GetUnionDuplicate(discriminant).into());
+ } else {
+ self.variant_index.insert(discriminant, self.schemas.len());
+ self.schemas.push(schema);
+ }
+ }
+ Ok(self)
+ }
+
+ /// Check if a schema already exists in this union.
+ pub fn contains(&self, schema: &Schema) -> bool {
+ if let Some(name) = schema.name() {
+ if let Some(current) = self.names.get(name).copied() {
+ &self.schemas[current] == schema
+ } else {
+ false
+ }
+ } else {
+ let discriminant = schema_to_base_schemakind(schema);
+ if let Some(index) =
self.variant_index.get(&discriminant).copied() {
+ &self.schemas[index] == schema
+ } else {
+ false
+ }
+ }
+ }
+
+ /// Create the `UnionSchema`.
+ pub fn build(mut self) -> UnionSchema {
+ self.schemas.shrink_to_fit();
+ let mut named_index: Vec<_> = self.names.into_values().collect();
+ named_index.sort();
+ UnionSchema {
+ variant_index: self.variant_index,
+ named_index,
+ schemas: self.schemas,
+ }
+ }
+}
+
+/// Get the [`SchemaKind`] of a [`Schema`] converting logical types to their
base type.
+fn schema_to_base_schemakind(schema: &Schema) -> SchemaKind {
+ let kind = schema.discriminant();
+ match kind {
+ SchemaKind::Date | SchemaKind::TimeMillis => SchemaKind::Int,
+ SchemaKind::TimeMicros
+ | SchemaKind::TimestampMillis
+ | SchemaKind::TimestampMicros
+ | SchemaKind::TimestampNanos
+ | SchemaKind::LocalTimestampMillis
+ | SchemaKind::LocalTimestampMicros
+ | SchemaKind::LocalTimestampNanos => SchemaKind::Long,
+ SchemaKind::Uuid => match schema {
+ Schema::Uuid(UuidSchema::Bytes) => SchemaKind::Bytes,
+ Schema::Uuid(UuidSchema::String) => SchemaKind::String,
+ Schema::Uuid(UuidSchema::Fixed(_)) => SchemaKind::Fixed,
+ _ => unreachable!(),
+ },
+ SchemaKind::Decimal => match schema {
+ Schema::Decimal(DecimalSchema {
+ inner: InnerDecimalSchema::Bytes,
+ ..
+ }) => SchemaKind::Bytes,
+ Schema::Decimal(DecimalSchema {
+ inner: InnerDecimalSchema::Fixed(_),
+ ..
+ }) => SchemaKind::Fixed,
+ _ => unreachable!(),
+ },
+ SchemaKind::Duration => SchemaKind::Fixed,
+ _ => kind,
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
use crate::error::{Details, Error};
use crate::schema::RecordSchema;
+ use crate::types::Value;
use apache_avro_test_helper::TestResult;
#[test]
@@ -174,4 +434,185 @@ mod tests {
Ok(())
}
+
+ #[test]
+ fn avro_rs_489_union_schema_builder_primitive_type() -> TestResult {
+ let mut builder = UnionSchema::builder();
+ builder.variant(Schema::Null)?;
+ assert!(builder.variant(Schema::Null).is_err());
+ builder.variant_ignore_duplicates(Schema::Null)?;
+ builder.variant(Schema::Int)?;
+ assert!(builder.variant(Schema::Int).is_err());
+ builder.variant_ignore_duplicates(Schema::Int)?;
+ builder.variant(Schema::Long)?;
+ assert!(builder.variant(Schema::Long).is_err());
+ builder.variant_ignore_duplicates(Schema::Long)?;
+
+ let union = builder.build();
+ assert_eq!(union.schemas, &[Schema::Null, Schema::Int, Schema::Long]);
+
+ Ok(())
+ }
+
+ #[test]
+ fn avro_rs_489_union_schema_builder_complex_types() -> TestResult {
+ let enum_abc = Schema::parse_str(
+ r#"{
+ "type": "enum",
+ "name": "ABC",
+ "symbols": ["A", "B", "C"]
+ }"#,
+ )?;
+ let enum_abc_with_extra_symbol = Schema::parse_str(
+ r#"{
+ "type": "enum",
+ "name": "ABC",
+ "symbols": ["A", "B", "C", "D"]
+ }"#,
+ )?;
+ let enum_def = Schema::parse_str(
+ r#"{
+ "type": "enum",
+ "name": "DEF",
+ "symbols": ["D", "E", "F"]
+ }"#,
+ )?;
+ let fixed_abc = Schema::parse_str(
+ r#"{
+ "type": "fixed",
+ "name": "ABC",
+ "size": 1
+ }"#,
+ )?;
+ let fixed_foo = Schema::parse_str(
+ r#"{
+ "type": "fixed",
+ "name": "Foo",
+ "size": 1
+ }"#,
+ )?;
+
+ let mut builder = UnionSchema::builder();
+ builder.variant(enum_abc.clone())?;
+ assert!(builder.variant(enum_abc.clone()).is_err());
+ builder.variant_ignore_duplicates(enum_abc.clone())?;
+ // Name is the same but different schemas, so should always fail
+ assert!(builder.variant(fixed_abc.clone()).is_err());
+ assert!(
+ builder
+ .variant_ignore_duplicates(fixed_abc.clone())
+ .is_err()
+ );
+ // Name and schema type are the same but symbols are different
+ assert!(builder.variant(enum_abc_with_extra_symbol.clone()).is_err());
+ assert!(
+ builder
+ .variant_ignore_duplicates(enum_abc_with_extra_symbol.clone())
+ .is_err()
+ );
+ builder.variant(enum_def.clone())?;
+ assert!(builder.variant(enum_def.clone()).is_err());
+ builder.variant_ignore_duplicates(enum_def.clone())?;
+ builder.variant(fixed_foo.clone())?;
+ assert!(builder.variant(fixed_foo.clone()).is_err());
+ builder.variant_ignore_duplicates(fixed_foo.clone())?;
+
+ let union = builder.build();
+ assert_eq!(union.variants(), &[enum_abc, enum_def, fixed_foo]);
+
+ Ok(())
+ }
+
+ #[test]
+ fn avro_rs_489_union_schema_builder_logical_types() -> TestResult {
+ let fixed_uuid = Schema::parse_str(
+ r#"{
+ "type": "fixed",
+ "name": "Uuid",
+ "size": 16
+ }"#,
+ )?;
+ let uuid = Schema::parse_str(
+ r#"{
+ "type": "fixed",
+ "logicalType": "uuid",
+ "name": "Uuid",
+ "size": 16
+ }"#,
+ )?;
+
+ let mut builder = UnionSchema::builder();
+
+ builder.variant(Schema::Date)?;
+ assert!(builder.variant(Schema::Date).is_err());
+ builder.variant_ignore_duplicates(Schema::Date)?;
+ assert!(builder.variant(Schema::Int).is_err());
+ builder.variant_ignore_duplicates(Schema::Int)?;
+ builder.variant(uuid.clone())?;
+ assert!(builder.variant(uuid.clone()).is_err());
+ builder.variant_ignore_duplicates(uuid.clone())?;
+ assert!(builder.variant(fixed_uuid.clone()).is_err());
+ assert!(
+ builder
+ .variant_ignore_duplicates(fixed_uuid.clone())
+ .is_err()
+ );
+
+ let union = builder.build();
+ assert_eq!(union.schemas, &[Schema::Date, uuid]);
+
+ Ok(())
+ }
+
+ #[test]
+ fn avro_rs_489_find_schema_with_known_schemata_wrong_map() -> TestResult {
+ let union = UnionSchema::new(vec![Schema::map(Schema::Int).build(),
Schema::Null])?;
+ let value = Value::Map(
+ [("key".to_string(), Value::String("value".to_string()))]
+ .into_iter()
+ .collect(),
+ );
+
+ assert!(
+ union
+ .find_schema_with_known_schemata(&value, None::<&HashMap<Name,
Schema>>, &None)
+ .is_none()
+ );
+
+ Ok(())
+ }
+
+ #[test]
+ fn avro_rs_489_find_schema_with_known_schemata_type_promotion() ->
TestResult {
+ let union = UnionSchema::new(vec![Schema::Long, Schema::Null])?;
+ let value = Value::Int(42);
+
+ assert_eq!(
+ union.find_schema_with_known_schemata(&value,
None::<&HashMap<Name, Schema>>, &None),
+ Some((0, &Schema::Long))
+ );
+
+ Ok(())
+ }
+
+ #[test]
+ fn avro_rs_489_find_schema_with_known_schemata_uuid_vs_fixed() ->
TestResult {
+ let uuid = Schema::parse_str(
+ r#"{
+ "type": "fixed",
+ "logicalType": "uuid",
+ "name": "Uuid",
+ "size": 16
+ }"#,
+ )?;
+ let union = UnionSchema::new(vec![uuid.clone(), Schema::Null])?;
+ let value = Value::Fixed(16, vec![0; 16]);
+
+ assert_eq!(
+ union.find_schema_with_known_schemata(&value,
None::<&HashMap<Name, Schema>>, &None),
+ Some((0, &uuid))
+ );
+
+ Ok(())
+ }
}
diff --git a/avro/src/serde/derive.rs b/avro/src/serde/derive.rs
index 7e3fb9e..e9b782a 100644
--- a/avro/src/serde/derive.rs
+++ b/avro/src/serde/derive.rs
@@ -952,4 +952,20 @@ mod tests {
Ok(())
}
+
+ #[test]
+ #[should_panic(
+ expected = "Option<T> must produce a valid (non-nested) union: Error {
details: Unions cannot contain duplicate types, found at least two Null }"
+ )]
+ fn avro_rs_489_some_unit() {
+ <Option<()>>::get_schema();
+ }
+
+ #[test]
+ #[should_panic(
+ expected = "Option<T> must produce a valid (non-nested) union: Error {
details: Unions may not directly contain a union }"
+ )]
+ fn avro_rs_489_option_option() {
+ <Option<Option<i32>>>::get_schema();
+ }
}
diff --git a/avro/src/types.rs b/avro/src/types.rs
index b3b2035..8a9bdf6 100644
--- a/avro/src/types.rs
+++ b/avro/src/types.rs
@@ -3140,15 +3140,10 @@ Field with name '"b"' is not a member of the map
items"#,
let main_schema = schemata.last().unwrap();
let other_schemata: Vec<&Schema> = schemata.iter().take(2).collect();
- let resolve_result = avro_value.resolve_schemata(main_schema,
other_schemata);
+ let resolve_result = avro_value.resolve_schemata(main_schema,
other_schemata)?;
assert!(
- resolve_result.is_ok(),
- "result of resolving with schemata should be ok, got:
{resolve_result:?}"
- );
-
- assert!(
- resolve_result?.validate_schemata(schemata.iter().collect()),
+ resolve_result.validate_schemata(schemata.iter().collect()),
"result of validation with schemata should be true"
);