apache-avro 0.22.0

A library for working with Apache Avro in Rust
Documentation
// 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.

//! **[Apache Avro](https://avro.apache.org/)** is a data serialization system which provides rich
//! data structures and a compact, fast, binary data format. If you are not familiar with the data
//! format, please read [`documentation::primer`] first.
//!
//! There are two ways of working with Avro data in this crate:
//!
//! 1. Via the generic [`Value`](types::Value) type.
//! 2. Via types implementing [`AvroSchema`] and Serde's [`Serialize`] and [`Deserialize`].
//!
//! The first option is great for dealing with Avro data in a dynamic way. For example, when working
//! with unknown or rapidly changing schemas or when you don't want or need to map to Rust types. The
//! module documentation of [`documentation::dynamic`] explains how to work in this dynamic way.
//!
//! The second option is great when dealing with static schemas that should be decoded to and encoded
//! from Rust types. The module documentation of [`serde`] explains how to work in this static way.
//!
//! # Features
//!
//! - `derive`: enable support for deriving [`AvroSchema`]
//! - `snappy`: enable support for the Snappy codec
//! - `zstandard`: enable support for the Zstandard codec
//! - `bzip`: enable support for the Bzip2 codec
//! - `xz`: enable support for the Xz codec
//!
//! # MSRV
//!
//! The current MSRV is 1.88.0.
//!
//! The MSRV may be bumped in minor releases.
//!
// These are links because otherwise `cargo rdme` gets angry
//! [`Serialize`]: https://docs.rs/serde/latest/serde/trait.Serialize.html
//! [`Deserialize`]: https://docs.rs/serde/latest/serde/trait.Deserialize.html

// Enable using `fake_variadic` on docs.rs
#![cfg_attr(docsrs, feature(rustdoc_internals))]
#![cfg_attr(docsrs, allow(internal_features))]

mod bigdecimal;
mod bytes;
mod codec;
mod decimal;
mod decode;
mod duration;
mod encode;

#[cfg(doc)]
pub mod documentation;
pub mod error;
pub mod headers;
pub mod rabin;
pub mod reader;
pub mod schema;
pub mod schema_compatibility;
pub mod schema_equality;
pub mod serde;
pub mod types;
pub mod util;
pub mod validator;
pub mod writer;

#[expect(deprecated)]
pub use crate::{
    bigdecimal::BigDecimal,
    bytes::{
        serde_avro_bytes, serde_avro_bytes_opt, serde_avro_fixed, serde_avro_fixed_opt,
        serde_avro_slice, serde_avro_slice_opt,
    },
};
#[cfg(feature = "bzip")]
pub use codec::bzip::Bzip2Settings;
#[cfg(feature = "xz")]
pub use codec::xz::XzSettings;
#[cfg(feature = "zstandard")]
pub use codec::zstandard::ZstandardSettings;
pub use codec::{Codec, DeflateSettings};
pub use decimal::Decimal;
pub use duration::{Days, Duration, Millis, Months};
pub use error::Error;
#[expect(
    deprecated,
    reason = "Still need to export it until we remove it completely"
)]
pub use reader::{
    Reader,
    datum::{from_avro_datum, from_avro_datum_reader_schemata, from_avro_datum_schemata},
    read_marker,
    single_object::{GenericSingleObjectReader, SpecificSingleObjectReader},
};
pub use schema::Schema;
pub use serde::{AvroSchema, AvroSchemaComponent, from_value, to_value};
pub use uuid::Uuid;
#[expect(
    deprecated,
    reason = "Still need to export it until we remove it completely"
)]
pub use writer::{
    Clearable, Writer, WriterBuilder,
    datum::{to_avro_datum, to_avro_datum_schemata, write_avro_datum_ref},
    single_object::{GenericSingleObjectWriter, SpecificSingleObjectWriter},
};

#[cfg(feature = "derive")]
pub use apache_avro_derive::AvroSchema;

/// A convenience type alias for `Result`s with `Error`s.
pub type AvroResult<T> = Result<T, Error>;

/// Set the maximum number of bytes that can be allocated when decoding data.
///
/// This function only changes the setting once. On subsequent calls the value will stay the same
/// as the first time it is called. It is automatically called on first allocation and defaults to
/// [`util::DEFAULT_MAX_ALLOCATION_BYTES`].
///
/// # Returns
/// The configured maximum, which might be different from what the function was called with if the
/// value was already set before.
#[deprecated(
    since = "0.21.0",
    note = "Please use apache_avro::util::max_allocation_bytes"
)]
pub fn max_allocation_bytes(num_bytes: usize) -> usize {
    util::max_allocation_bytes(num_bytes)
}

/// Set whether the serializer and deserializer should indicate to types that the format is human-readable.
///
/// This function only changes the setting once. On subsequent calls the value will stay the same
/// as the first time it is called. It is automatically called on first allocation and defaults to
/// [`util::DEFAULT_SERDE_HUMAN_READABLE`].
///
/// *NOTE*: Changing this setting can change the output of [`from_value`] and the
/// accepted input of [`to_value`].
///
/// # Returns
/// The configured human-readable value, which might be different from what the function was called
/// with if the value was already set before.
#[deprecated(
    since = "0.21.0",
    note = "Please use apache_avro::util::set_serde_human_readable"
)]
pub fn set_serde_human_readable(human_readable: bool) -> bool {
    util::set_serde_human_readable(human_readable)
}

#[cfg(test)]
mod tests {
    use crate::{
        Codec, Reader, Schema, Writer,
        reader::datum::GenericDatumReader,
        types::{Record, Value},
    };
    use apache_avro_test_helper::TestResult;
    use pretty_assertions::assert_eq;

    //TODO: move where it fits better
    #[test]
    fn test_enum_default() {
        let writer_raw_schema = r#"
            {
                "type": "record",
                "name": "test",
                "fields": [
                    {"name": "a", "type": "long", "default": 42},
                    {"name": "b", "type": "string"}
                ]
            }
        "#;
        let reader_raw_schema = r#"
            {
                "type": "record",
                "name": "test",
                "fields": [
                    {"name": "a", "type": "long", "default": 42},
                    {"name": "b", "type": "string"},
                    {
                        "name": "c",
                        "type": {
                            "type": "enum",
                            "name": "suit",
                            "symbols": ["diamonds", "spades", "clubs", "hearts"]
                        },
                        "default": "spades"
                    }
                ]
            }
        "#;
        let writer_schema = Schema::parse_str(writer_raw_schema).unwrap();
        let reader_schema = Schema::parse_str(reader_raw_schema).unwrap();
        let mut writer = Writer::with_codec(&writer_schema, Vec::new(), Codec::Null).unwrap();
        let mut record = Record::new(writer.schema()).unwrap();
        record.put("a", 27i64);
        record.put("b", "foo");
        writer.append_value(record).unwrap();
        let input = writer.into_inner().unwrap();
        let mut reader = Reader::builder(&input[..])
            .reader_schema(&reader_schema)
            .build()
            .unwrap();
        assert_eq!(
            reader.next().unwrap().unwrap(),
            Value::Record(vec![
                ("a".to_string(), Value::Long(27)),
                ("b".to_string(), Value::String("foo".to_string())),
                ("c".to_string(), Value::Enum(1, "spades".to_string())),
            ])
        );
        assert!(reader.next().is_none());
    }

    //TODO: move where it fits better
    #[test]
    fn test_enum_string_value() {
        let raw_schema = r#"
            {
                "type": "record",
                "name": "test",
                "fields": [
                    {"name": "a", "type": "long", "default": 42},
                    {"name": "b", "type": "string"},
                    {
                        "name": "c",
                        "type": {
                            "type": "enum",
                            "name": "suit",
                            "symbols": ["diamonds", "spades", "clubs", "hearts"]
                        },
                        "default": "spades"
                    }
                ]
            }
        "#;
        let schema = Schema::parse_str(raw_schema).unwrap();
        let mut writer = Writer::with_codec(&schema, Vec::new(), Codec::Null).unwrap();
        let mut record = Record::new(writer.schema()).unwrap();
        record.put("a", 27i64);
        record.put("b", "foo");
        record.put("c", "clubs");
        writer.append_value(record).unwrap();
        let input = writer.into_inner().unwrap();
        let mut reader = Reader::builder(&input[..])
            .reader_schema(&schema)
            .build()
            .unwrap();
        assert_eq!(
            reader.next().unwrap().unwrap(),
            Value::Record(vec![
                ("a".to_string(), Value::Long(27)),
                ("b".to_string(), Value::String("foo".to_string())),
                ("c".to_string(), Value::Enum(2, "clubs".to_string())),
            ])
        );
        assert!(reader.next().is_none());
    }

    //TODO: move where it fits better
    #[test]
    fn test_enum_no_reader_schema() {
        let writer_raw_schema = r#"
            {
                "type": "record",
                "name": "test",
                "fields": [
                    {"name": "a", "type": "long", "default": 42},
                    {"name": "b", "type": "string"},
                    {
                        "name": "c",
                        "type": {
                            "type": "enum",
                            "name": "suit",
                            "symbols": ["diamonds", "spades", "clubs", "hearts"]
                        },
                        "default": "spades"
                    }
                ]
            }
        "#;
        let writer_schema = Schema::parse_str(writer_raw_schema).unwrap();
        let mut writer = Writer::with_codec(&writer_schema, Vec::new(), Codec::Null).unwrap();
        let mut record = Record::new(writer.schema()).unwrap();
        record.put("a", 27i64);
        record.put("b", "foo");
        record.put("c", "clubs");
        writer.append_value(record).unwrap();
        let input = writer.into_inner().unwrap();
        let mut reader = Reader::new(&input[..]).unwrap();
        assert_eq!(
            reader.next().unwrap().unwrap(),
            Value::Record(vec![
                ("a".to_string(), Value::Long(27)),
                ("b".to_string(), Value::String("foo".to_string())),
                ("c".to_string(), Value::Enum(2, "clubs".to_string())),
            ])
        );
    }

    #[test]
    fn test_illformed_length() -> TestResult {
        let raw_schema = r#"
            {
                "type": "record",
                "name": "test",
                "fields": [
                    {"name": "a", "type": "long", "default": 42},
                    {"name": "b", "type": "string"}
                ]
            }
        "#;

        let schema = Schema::parse_str(raw_schema).unwrap();

        // Would allocate 18446744073709551605 bytes
        let illformed: &[u8] = &[0x3e, 0x15, 0xff, 0x1f, 0x15, 0xff];

        let value = GenericDatumReader::builder(&schema)
            .build()?
            .read_value(&mut &*illformed);
        assert!(value.is_err());

        Ok(())
    }
}