From 9592038cc61ac740a15af9efe63a379e571c46c5 Mon Sep 17 00:00:00 2001 From: Antonio Uccio Verardi Date: Fri, 10 Jul 2020 22:18:26 +0100 Subject: [PATCH] Move from failure to thiserror (#135) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Move from failure to thiserror Closes #115 This is still a WIP branch, with lots of TODOs and some things about thiserror I still can't wrap my head around. However, the heavy-lifting is done, the failure crate has been removed from the list of dependencies and compilation, tests, benchmarks and linting are all green. The two biggest things I have yet to figure out are: 1. How to deal with the errors manually defined in ser.rs and de.rs: they are publicly available and as soon as I touch anything I hit cryptic serde errors 2. How to make errors like TryFromIntError part of more abstract ones like ParseSchemaError, which could have a source error or just a String description. * Update tests/io.rs Co-authored-by: Joel Höner * Renaming errors + apply clippy consistently Rename AvroError to Error Removed redundant Error suffix from variants Introduce AvroResult shorthand alias with crate visibility Align clippy invocation in tests with the one in pre-commits * Stop stressing about generic errors and add a couple more sprecific ones * Centralize Ser and De errors into Error The trick was implementing the ser::Error and de::Error trait for crate::errors::Error and return Error::Ser and Error::De variants in the implementation of the custom() method. * SnappyCdcError as struct for consistency * Update CHANGELOG * Update CHANGELOG, README and add a Migration Guide page Co-authored-by: Joel Höner --- .pre-commit-config.yaml | 2 +- CHANGELOG.md | 16 ++- Cargo.toml | 2 +- README.md | 15 ++- README.tpl | 1 + examples/to_value.rs | 3 +- migration_guide.md | 79 +++++++++++++ src/codec.rs | 17 ++- src/de.rs | 84 +++++-------- src/decimal.rs | 23 ++-- src/decode.rs | 50 ++++---- src/errors.rs | 73 ++++++++++++ src/lib.rs | 27 +++-- src/reader.rs | 47 ++++---- src/schema.rs | 124 +++++++++---------- src/ser.rs | 43 ++----- src/types.rs | 255 +++++++++++++++++++--------------------- src/util.rs | 56 +++------ src/writer.rs | 50 +++----- tests/io.rs | 16 +-- tests/schema.rs | 22 ++-- 21 files changed, 525 insertions(+), 480 deletions(-) create mode 100644 migration_guide.md create mode 100644 src/errors.rs diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4aae4e89aa3..2d41ba2dab0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,7 @@ - id: rust-clippy name: Rust clippy description: Run cargo clippy on files included in the commit. clippy should be installed before-hand. - entry: cargo clippy --all-features --all -- + entry: cargo clippy --all-targets --all-features -- -Dclippy::all pass_filenames: false types: [file, rust] language: system diff --git a/CHANGELOG.md b/CHANGELOG.md index e025fd0ff4b..a985d23b998 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,13 +5,21 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## Unreleased +### Changed +- Introduce custom Error enum to replace all existing errors (backward-incompatible) (#135) +- Swapped failure for thiserror (backward-incompatible) (#135) +- Update digest crate and digest::Digest trait to 0.9 (backward-incompatible with digest::Digest 0.8) (#133) +- Replace some manual from_str implementations with strum (#136) + +## Deprecated +- Deprecate ToAvro in favor of From for Value implementations (#137) ## [0.10.0] - 2020-05-31 ### Changed -- Writer::into_inner() now calls flush() and returns a Result +- Writer::into_inner() now calls flush() and returns a Result (backward-incompatible) ### Added -- Add utilited for schema compatibility check +- Add utility for schema compatibility check ## [0.9.1] - 2020-05-02 ### Changed @@ -63,7 +71,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.6.0]- 2018-08-11 ### Added -- impl Send+Sync for Schema (non-backwards compatible) +- impl Send+Sync for Schema (backwards-incompatible) ## [0.5.0] - 2018-08-06 ### Added @@ -76,7 +84,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [0.4.1] - 2018-06-17 ### Changed -- Implememented clippy suggestions +- Implemented clippy suggestions ## [0.4.0] - 2018-06-17 ### Changed diff --git a/Cargo.toml b/Cargo.toml index c2f7bf25956..1d110cf25a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,6 @@ harness = false byteorder = "1.0.0" crc = { version = "1.3.0", optional = true } digest = "0.9" -failure = "0.1.5" libflate = "0.1" num-bigint = "0.2.6" rand = "0.4" @@ -44,6 +43,7 @@ serde = { version = "1.0", features = ["derive"] } snap = { version = "0.2.3", optional = true } strum = "0.18.0" strum_macros = "0.18.0" +thiserror = "1.0" typed-builder = "0.5.1" uuid = { version = "0.8.1", features = ["v4"] } zerocopy = "0.3.0" diff --git a/README.md b/README.md index 2be83ca4281..06a38a18f39 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,11 @@ version = "x.y" features = ["snappy"] ``` +## Upgrading to a newer minor version + +The library is still in beta, so there might be backward-incompatible changes between minor +versions. If you have troubles upgrading, check the [version upgrade guide](migration_guide.md). + ## Defining a schema An Avro data cannot exist without an Avro schema. Schemas **must** be used while writing and @@ -297,8 +302,7 @@ The following is an example of how to combine everything showed so far and it is quick reference of the library interface: ```rust -use avro_rs::{Codec, Reader, Schema, Writer, from_value, types::Record}; -use failure::Error; +use avro_rs::{Codec, Reader, Schema, Writer, from_value, types::Record, Error}; use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize)] @@ -363,10 +367,9 @@ Note that the on-disk representation is identical to the underlying primitive/co ```rust use avro_rs::{ types::Record, types::Value, Codec, Days, Decimal, Duration, Millis, Months, Reader, Schema, - Writer, + Writer, Error, }; use num_bigint::ToBigInt; -use failure::Error; fn main() -> Result<(), Error> { let raw_schema = r#" @@ -476,8 +479,7 @@ Note: Rabin fingerprinting is NOT SUPPORTED yet. An example of fingerprinting for the supported fingerprints: ```rust -use avro_rs::Schema; -use failure::Error; +use avro_rs::{Schema, Error}; use md5::Md5; use sha2::Sha256; @@ -572,4 +574,5 @@ Everyone is encouraged to contribute! You can contribute by forking the GitHub r All contributions will be licensed under [MIT License](https://github.com/flavray/avro-rs/blob/master/LICENSE). Please consider adding documentation, tests and a line for your change under the Unreleased section in the [CHANGELOG](https://github.com/flavray/avro-rs/blob/master/CHANGELOG.md). +If you introduce a backward-incompatible change, please consider adding instruction to migrate in the [Migration Guide](migration_guide.md) If you modify the crate documentation in `lib.rs`, run `make readme` to sync the README file. diff --git a/README.tpl b/README.tpl index 8e96744e4a2..99a43fb2abe 100644 --- a/README.tpl +++ b/README.tpl @@ -16,4 +16,5 @@ Everyone is encouraged to contribute! You can contribute by forking the GitHub r All contributions will be licensed under [MIT License](https://github.com/flavray/avro-rs/blob/master/LICENSE). Please consider adding documentation, tests and a line for your change under the Unreleased section in the [CHANGELOG](https://github.com/flavray/avro-rs/blob/master/CHANGELOG.md). +If you introduce a backward-incompatible change, please consider adding instruction to migrate in the [Migration Guide](migration_guide.md) If you modify the crate documentation in `lib.rs`, run `make readme` to sync the README file. diff --git a/examples/to_value.rs b/examples/to_value.rs index 1923031d9cc..19635f3bf34 100644 --- a/examples/to_value.rs +++ b/examples/to_value.rs @@ -1,5 +1,4 @@ -use avro_rs::to_value; -use failure::Error; +use avro_rs::{to_value, Error}; use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize)] diff --git a/migration_guide.md b/migration_guide.md new file mode 100644 index 00000000000..cf5e47fa28a --- /dev/null +++ b/migration_guide.md @@ -0,0 +1,79 @@ +# Migration Guide +## Unreleased +- A custom `Error` enum has been introduced to replace all existing errors and + the `failure` crate has been replaced by `thiserror`. + + This means that all public functions returning `Result` + will now return `Result` and that you can pattern match on + `Error` variants if you want to gather more information about the error. + + For example, code that used to be like this: + ```rust + match decoded { + Ok(msg) => Ok(msg.to_string()), + Err(ref e) => match e.downcast_ref::() { + Some(_) => Ok("default".to_string()), + None => Err(format!("Unexpected error: {}", e)), + }, + } + ``` + + now becomes: + ```rust + match decoded { + Ok(msg) => Ok(msg.to_string()), + Err(Error::SchemaResolution(_)) => Ok("default".to_string()), + Err(ref e) => Err(format!("Unexpected error: {}", e)), + } + ``` + + Please note that all instances of: + - `DecodeError` + - `ValidationError` + - `DeError` + - `SerError` + - `ParseSchemaError` + - `SchemaResolutionError` + + must be replaced by `Error`. + +- The `ToAvro` trait has been deprecated in favor of `From` for `Value` implementations. + + Code like the following: + ```rust + use crate::types::{Record, ToAvro, Value}; + + let expected: Value = record.avro(); + ``` + + should be updated to: + + ```rust + use crate::types::{Record, Value}; + + let expected: Value = record.into(); + ``` + + Using the `ToAvro` trait will result in a deprecation warning. The trait will + be removed in future versions. + +- The `digest` crate has been updated to version `0.9`. If you were using the + `digest::Digest` trait from version `0.8`, you must update to the one defined + in `0.9`. + +## 0.10.0 +- `Writer::into_inner()` now calls `flush()` and returns a `Result`. + + This means that code like + ```rust + writer.append_ser(test)?; + writer.flush()?; + let input = writer.into_inner(); + ``` + + can be simplified into + ```rust + writer.append_ser(test)?; + let input = writer.into_inner()?; + ``` + There is no harm in leaving old calls to `flush()` around. diff --git a/src/codec.rs b/src/codec.rs index ef84bbb737b..4a26670600c 100644 --- a/src/codec.rs +++ b/src/codec.rs @@ -1,6 +1,6 @@ //! Logic for all supported compression codecs in Avro. +use crate::errors::{AvroResult, Error}; use crate::types::Value; -use failure::Error; use libflate::deflate::{Decoder, Encoder}; use std::io::{Read, Write}; use strum_macros::{EnumString, IntoStaticStr}; @@ -30,12 +30,13 @@ impl From for Value { impl Codec { /// Compress a stream of bytes in-place. - pub fn compress(self, stream: &mut Vec) -> Result<(), Error> { + pub fn compress(self, stream: &mut Vec) -> AvroResult<()> { match self { Codec::Null => (), Codec::Deflate => { let mut encoder = Encoder::new(Vec::new()); encoder.write_all(stream)?; + // Deflate errors seem to just be io::Error *stream = encoder.finish().into_result()?; } #[cfg(feature = "snappy")] @@ -58,7 +59,7 @@ impl Codec { } /// Decompress a stream of bytes in-place. - pub fn decompress(self, stream: &mut Vec) -> Result<(), Error> { + pub fn decompress(self, stream: &mut Vec) -> AvroResult<()> { *stream = match self { Codec::Null => return Ok(()), Codec::Deflate => { @@ -69,7 +70,6 @@ impl Codec { } #[cfg(feature = "snappy")] Codec::Snappy => { - use crate::util::DecodeError; use byteorder::ByteOrder; let decompressed_size = snap::decompress_len(&stream[..stream.len() - 4])?; @@ -80,11 +80,10 @@ impl Codec { let actual_crc = crc::crc32::checksum_ieee(&decoded); if expected_crc != actual_crc { - return Err(DecodeError::new(format!( - "bad Snappy CRC32; expected {:x} but got {:x}", - expected_crc, actual_crc - )) - .into()); + return Err(Error::SnappyCrcError { + expected: expected_crc, + found: actual_crc, + }); } decoded } diff --git a/src/de.rs b/src/de.rs index b44af42f86a..0e1374e308f 100644 --- a/src/de.rs +++ b/src/de.rs @@ -3,40 +3,20 @@ use std::collections::{ hash_map::{Keys, Values}, HashMap, }; -use std::error; use std::fmt; use std::slice::Iter; use serde::{ - de::{self, DeserializeSeed, Error as SerdeError, Visitor}, + de::{self, DeserializeSeed, Visitor}, forward_to_deserialize_any, Deserialize, }; +use crate::errors::Error; use crate::types::Value; -/// Represents errors that could be encountered while deserializing data -#[derive(Clone, Debug, PartialEq)] -pub struct Error { - message: String, -} - impl de::Error for Error { fn custom(msg: T) -> Self { - Error { - message: msg.to_string(), - } - } -} - -impl fmt::Display for Error { - fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str(&self.to_string()) - } -} - -impl error::Error for Error { - fn description(&self) -> &str { - &self.message + Error::De(msg.to_string()) } } @@ -140,14 +120,14 @@ impl<'de> de::VariantAccess<'de> for EnumUnitDeserializer<'de> { where T: DeserializeSeed<'de>, { - Err(Error::custom("Unexpected Newtype variant")) + Err(de::Error::custom("Unexpected Newtype variant")) } fn tuple_variant(self, _len: usize, _visitor: V) -> Result where V: Visitor<'de>, { - Err(Error::custom("Unexpected tuple variant")) + Err(de::Error::custom("Unexpected tuple variant")) } fn struct_variant( @@ -158,7 +138,7 @@ impl<'de> de::VariantAccess<'de> for EnumUnitDeserializer<'de> { where V: Visitor<'de>, { - Err(Error::custom("Unexpected struct variant")) + Err(de::Error::custom("Unexpected struct variant")) } } @@ -171,7 +151,7 @@ impl<'de> de::EnumAccess<'de> for EnumDeserializer<'de> { V: DeserializeSeed<'de>, { self.input.first().map_or( - Err(Error::custom("A record must have a least one field")), + Err(de::Error::custom("A record must have a least one field")), |item| match (item.0.as_ref(), &item.1) { ("type", Value::String(x)) => Ok(( seed.deserialize(StringDeserializer { @@ -179,11 +159,11 @@ impl<'de> de::EnumAccess<'de> for EnumDeserializer<'de> { })?, self, )), - (field, Value::String(_)) => Err(Error::custom(format!( + (field, Value::String(_)) => Err(de::Error::custom(format!( "Expected first field named 'type': got '{}' instead", field ))), - (_, _) => Err(Error::custom( + (_, _) => Err(de::Error::custom( "Expected first field of type String for the type name".to_string(), )), }, @@ -203,7 +183,7 @@ impl<'de> de::VariantAccess<'de> for EnumDeserializer<'de> { T: DeserializeSeed<'de>, { self.input.get(1).map_or( - Err(Error::custom( + Err(de::Error::custom( "Expected a newtype variant, got nothing instead.", )), |item| seed.deserialize(&Deserializer::new(&item.1)), @@ -215,7 +195,7 @@ impl<'de> de::VariantAccess<'de> for EnumDeserializer<'de> { V: Visitor<'de>, { self.input.get(1).map_or( - Err(Error::custom( + Err(de::Error::custom( "Expected a tuple variant, got nothing instead.", )), |item| de::Deserializer::deserialize_seq(&Deserializer::new(&item.1), visitor), @@ -231,7 +211,7 @@ impl<'de> de::VariantAccess<'de> for EnumDeserializer<'de> { V: Visitor<'de>, { self.input.get(1).map_or( - Err(Error::custom("Expected a struct variant, got nothing")), + Err(de::Error::custom("Expected a struct variant, got nothing")), |item| { de::Deserializer::deserialize_struct( &Deserializer::new(&item.1), @@ -268,11 +248,11 @@ impl<'a, 'de> de::Deserializer<'de> for &'a Deserializer<'de> { Value::Long(i) => visitor.visit_i64(i), Value::Float(f) => visitor.visit_f32(f), Value::Double(d) => visitor.visit_f64(d), - _ => Err(Error::custom("Unsupported union")), + _ => Err(de::Error::custom("Unsupported union")), }, Value::Record(ref fields) => visitor.visit_map(StructDeserializer::new(fields)), Value::Array(ref fields) => visitor.visit_seq(SeqDeserializer::new(fields)), - value => Err(Error::custom(format!( + value => Err(de::Error::custom(format!( "incorrect value of type: {:?}", crate::schema::SchemaKind::from(value) ))), @@ -287,7 +267,7 @@ impl<'a, 'de> de::Deserializer<'de> for &'a Deserializer<'de> { where V: Visitor<'de>, { - Err(Error::custom("avro does not support char")) + Err(de::Error::custom("avro does not support char")) } fn deserialize_str(self, visitor: V) -> Result @@ -297,9 +277,9 @@ impl<'a, 'de> de::Deserializer<'de> for &'a Deserializer<'de> { match *self.input { Value::String(ref s) => visitor.visit_str(s), Value::Bytes(ref bytes) | Value::Fixed(_, ref bytes) => ::std::str::from_utf8(bytes) - .map_err(|e| Error::custom(e.to_string())) + .map_err(|e| de::Error::custom(e.to_string())) .and_then(|s| visitor.visit_str(s)), - _ => Err(Error::custom("not a string|bytes|fixed")), + _ => Err(de::Error::custom("not a string|bytes|fixed")), } } @@ -311,14 +291,14 @@ impl<'a, 'de> de::Deserializer<'de> for &'a Deserializer<'de> { Value::String(ref s) => visitor.visit_string(s.to_owned()), Value::Bytes(ref bytes) | Value::Fixed(_, ref bytes) => { String::from_utf8(bytes.to_owned()) - .map_err(|e| Error::custom(e.to_string())) + .map_err(|e| de::Error::custom(e.to_string())) .and_then(|s| visitor.visit_string(s)) } Value::Union(ref x) => match **x { Value::String(ref s) => visitor.visit_string(s.to_owned()), - _ => Err(Error::custom("not a string|bytes|fixed")), + _ => Err(de::Error::custom("not a string|bytes|fixed")), }, - _ => Err(Error::custom("not a string|bytes|fixed")), + _ => Err(de::Error::custom("not a string|bytes|fixed")), } } @@ -329,7 +309,7 @@ impl<'a, 'de> de::Deserializer<'de> for &'a Deserializer<'de> { match *self.input { Value::String(ref s) => visitor.visit_bytes(s.as_bytes()), Value::Bytes(ref bytes) | Value::Fixed(_, ref bytes) => visitor.visit_bytes(bytes), - _ => Err(Error::custom("not a string|bytes|fixed")), + _ => Err(de::Error::custom("not a string|bytes|fixed")), } } @@ -342,7 +322,7 @@ impl<'a, 'de> de::Deserializer<'de> for &'a Deserializer<'de> { Value::Bytes(ref bytes) | Value::Fixed(_, ref bytes) => { visitor.visit_byte_buf(bytes.to_owned()) } - _ => Err(Error::custom("not a string|bytes|fixed")), + _ => Err(de::Error::custom("not a string|bytes|fixed")), } } @@ -353,7 +333,7 @@ impl<'a, 'de> de::Deserializer<'de> for &'a Deserializer<'de> { match *self.input { Value::Union(ref inner) if inner.as_ref() == &Value::Null => visitor.visit_none(), Value::Union(ref inner) => visitor.visit_some(&Deserializer::new(inner)), - _ => Err(Error::custom("not a union")), + _ => Err(de::Error::custom("not a union")), } } @@ -363,7 +343,7 @@ impl<'a, 'de> de::Deserializer<'de> for &'a Deserializer<'de> { { match *self.input { Value::Null => visitor.visit_unit(), - _ => Err(Error::custom("not a null")), + _ => Err(de::Error::custom("not a null")), } } @@ -397,9 +377,9 @@ impl<'a, 'de> de::Deserializer<'de> for &'a Deserializer<'de> { Value::Array(ref items) => visitor.visit_seq(SeqDeserializer::new(items)), Value::Union(ref inner) => match **inner { Value::Array(ref items) => visitor.visit_seq(SeqDeserializer::new(items)), - _ => Err(Error::custom("not an array")), + _ => Err(de::Error::custom("not an array")), }, - _ => Err(Error::custom("not an array")), + _ => Err(de::Error::custom("not an array")), } } @@ -428,7 +408,7 @@ impl<'a, 'de> de::Deserializer<'de> for &'a Deserializer<'de> { { match *self.input { Value::Map(ref items) => visitor.visit_map(MapDeserializer::new(items)), - _ => Err(Error::custom("not a map")), + _ => Err(de::Error::custom("not a map")), } } @@ -445,9 +425,9 @@ impl<'a, 'de> de::Deserializer<'de> for &'a Deserializer<'de> { Value::Record(ref fields) => visitor.visit_map(StructDeserializer::new(fields)), Value::Union(ref inner) => match **inner { Value::Record(ref fields) => visitor.visit_map(StructDeserializer::new(fields)), - _ => Err(Error::custom("not a record")), + _ => Err(de::Error::custom("not a record")), }, - _ => Err(Error::custom("not a record")), + _ => Err(de::Error::custom("not a record")), } } @@ -465,7 +445,7 @@ impl<'a, 'de> de::Deserializer<'de> for &'a Deserializer<'de> { Value::Record(ref fields) => visitor.visit_enum(EnumDeserializer::new(&fields)), // This has to be a unit Enum Value::Enum(_index, ref field) => visitor.visit_enum(EnumUnitDeserializer::new(&field)), - _ => Err(Error::custom("not an enum")), + _ => Err(de::Error::custom("not an enum")), } } @@ -521,7 +501,7 @@ impl<'de> de::MapAccess<'de> for MapDeserializer<'de> { { match self.input_values.next() { Some(ref value) => seed.deserialize(&Deserializer::new(value)), - None => Err(Error::custom("should not happen - too many values")), + None => Err(de::Error::custom("should not happen - too many values")), } } } @@ -552,7 +532,7 @@ impl<'de> de::MapAccess<'de> for StructDeserializer<'de> { { match self.value.take() { Some(value) => seed.deserialize(&Deserializer::new(value)), - None => Err(Error::custom("should not happen - too many values")), + None => Err(de::Error::custom("should not happen - too many values")), } } } diff --git a/src/decimal.rs b/src/decimal.rs index aaa2403e1c1..c6572e0698d 100644 --- a/src/decimal.rs +++ b/src/decimal.rs @@ -1,4 +1,4 @@ -use failure::{Error, Fail}; +use crate::errors::{AvroResult, Error}; use num_bigint::BigInt; #[derive(Debug, Clone)] @@ -15,31 +15,26 @@ impl PartialEq for Decimal { } } -#[derive(Fail, Debug)] -#[fail(display = "Decimal sign extension error: {}", _0)] -pub struct SignExtendError(&'static str, usize, usize); - impl Decimal { pub(crate) fn len(&self) -> usize { self.len } - fn to_vec(&self) -> Result, Error> { + fn to_vec(&self) -> AvroResult> { self.to_sign_extended_bytes_with_len(self.len) } - pub(crate) fn to_sign_extended_bytes_with_len(&self, len: usize) -> Result, Error> { + pub(crate) fn to_sign_extended_bytes_with_len(&self, len: usize) -> AvroResult> { let sign_byte = 0xFF * u8::from(self.value.sign() == num_bigint::Sign::Minus); let mut decimal_bytes = vec![sign_byte; len]; let raw_bytes = self.value.to_signed_bytes_be(); let num_raw_bytes = raw_bytes.len(); - let start_byte_index = len.checked_sub(num_raw_bytes).ok_or_else(|| { - SignExtendError( - "number of bytes requested for sign extension {} is less than the number of bytes needed to decode {}", - len, - num_raw_bytes, - ) - })?; + let start_byte_index = len + .checked_sub(num_raw_bytes) + .ok_or_else(|| Error::SignExtend { + requested: len, + needed: num_raw_bytes, + })?; decimal_bytes[start_byte_index..].copy_from_slice(&raw_bytes); Ok(decimal_bytes) } diff --git a/src/decode.rs b/src/decode.rs index 5d12c187bc4..f8fbc28a000 100644 --- a/src/decode.rs +++ b/src/decode.rs @@ -2,32 +2,32 @@ use std::collections::HashMap; use std::io::Read; use std::str::FromStr; -use failure::Error; use uuid::Uuid; use crate::decimal::Decimal; use crate::duration::Duration; +use crate::errors::{AvroResult, Error}; use crate::schema::Schema; use crate::types::Value; -use crate::util::{safe_len, zag_i32, zag_i64, DecodeError}; +use crate::util::{safe_len, zag_i32, zag_i64}; #[inline] -fn decode_long(reader: &mut R) -> Result { +fn decode_long(reader: &mut R) -> AvroResult { zag_i64(reader).map(Value::Long) } #[inline] -fn decode_int(reader: &mut R) -> Result { +fn decode_int(reader: &mut R) -> AvroResult { zag_i32(reader).map(Value::Int) } #[inline] -fn decode_len(reader: &mut R) -> Result { +fn decode_len(reader: &mut R) -> AvroResult { zag_i64(reader).and_then(|len| safe_len(len as usize)) } /// Decode a `Value` from avro format given its `Schema`. -pub fn decode(schema: &Schema, reader: &mut R) -> Result { +pub fn decode(schema: &Schema, reader: &mut R) -> AvroResult { match *schema { Schema::Null => Ok(Value::Null), Schema::Boolean => { @@ -37,32 +37,34 @@ pub fn decode(schema: &Schema, reader: &mut R) -> Result match buf[0] { 0u8 => Ok(Value::Boolean(false)), 1u8 => Ok(Value::Boolean(true)), - _ => Err(DecodeError::new("not a bool").into()), + _ => Err(Error::Decode("not a bool".to_string())), } } Schema::Decimal { ref inner, .. } => match **inner { Schema::Fixed { .. } => match decode(inner, reader)? { Value::Fixed(_, bytes) => Ok(Value::Decimal(Decimal::from(bytes))), - _ => Err(DecodeError::new( - "not a fixed value, required for decimal with fixed schema", - ) - .into()), + _ => Err(Error::Decode( + "not a fixed value, required for decimal with fixed schema".to_string(), + )), }, Schema::Bytes => match decode(inner, reader)? { Value::Bytes(bytes) => Ok(Value::Decimal(Decimal::from(bytes))), - _ => Err(DecodeError::new( - "not a bytes value, required for decimal with bytes schema", - ) - .into()), + _ => Err(Error::Decode( + "not a bytes value, required for decimal with bytes schema".to_string(), + )), }, - _ => Err( - DecodeError::new("not a fixed or bytes type, required for decimal schema").into(), - ), + _ => Err(Error::Decode( + "not a fixed or bytes type, required for decimal schema".to_string(), + )), }, Schema::Uuid => Ok(Value::Uuid(Uuid::from_str( match decode(&Schema::String, reader)? { Value::String(ref s) => s, - _ => return Err(DecodeError::new("not a string type, required for uuid").into()), + _ => { + return Err(Error::Decode( + "not a string type, required for uuid".to_string(), + )) + } }, )?)), Schema::Int => decode_int(reader), @@ -100,7 +102,7 @@ pub fn decode(schema: &Schema, reader: &mut R) -> Result String::from_utf8(buf) .map(Value::String) - .map_err(|_| DecodeError::new("not a valid utf-8 string").into()) + .map_err(|_| Error::Decode("not a valid utf-8 string".to_string())) } Schema::Fixed { size, .. } => { let mut buf = vec![0u8; size as usize]; @@ -153,7 +155,7 @@ pub fn decode(schema: &Schema, reader: &mut R) -> Result let value = decode(inner, reader)?; items.insert(key, value); } else { - return Err(DecodeError::new("map key is not a string").into()); + return Err(Error::Decode("map key is not a string".to_string())); } } } @@ -165,7 +167,7 @@ pub fn decode(schema: &Schema, reader: &mut R) -> Result let variants = inner.variants(); let variant = variants .get(index as usize) - .ok_or_else(|| DecodeError::new("Union index out of bounds"))?; + .ok_or_else(|| Error::Decode("Union index out of bounds".to_string()))?; let value = decode(variant, reader)?; Ok(Value::Union(Box::new(value))) } @@ -184,10 +186,10 @@ pub fn decode(schema: &Schema, reader: &mut R) -> Result let symbol = symbols[index as usize].clone(); Ok(Value::Enum(index, symbol)) } else { - Err(DecodeError::new("enum symbol index out of bounds").into()) + Err(Error::Decode("enum symbol index out of bounds".to_string())) } } else { - Err(DecodeError::new("enum symbol not found").into()) + Err(Error::Decode("enum symbol not found".to_string())) } } } diff --git a/src/errors.rs b/src/errors.rs new file mode 100644 index 00000000000..98aaa315411 --- /dev/null +++ b/src/errors.rs @@ -0,0 +1,73 @@ +use thiserror::Error; + +pub(crate) type AvroResult = Result; + +#[non_exhaustive] +#[derive(Error, Debug)] +/// Error type returned from the library +pub enum Error { + /// All cases of `std::io::Error` + #[error(transparent)] + IO(#[from] std::io::Error), + + /// Error due to unrecognized coded + #[error("unrecognized codec: {0:?}")] + Codec(String), + + /// Errors happened while decoding Avro data (except for `std::io::Error`) + #[error("decoding error: {0}")] + Decode(String), + + /// Errors happened while parsing Avro schemas + #[error("failed to parse schema: {0}")] + Parse(String), + + /// Errors happened while performing schema resolution on Avro data + #[error("schema resolution error: {0}")] + SchemaResolution(String), + + /// Errors happened while validating Avro data + #[error("validation error: {0}")] + Validation(String), + + /// Errors that could be encountered while serializing data, implements `serde::ser::Error` + #[error("data serialization error: {0}")] + Ser(String), + + /// Errors that could be encountered while deserializing data, implements `serde::de::Error` + #[error("data deserialization error: {0}")] + De(String), + + /// Error happened trying to allocate too many bytes + #[error("unable to allocate {desired} bytes (maximum allowed: {maximum})")] + MemoryAllocation { desired: usize, maximum: usize }, + + /// All cases of `uuid::Error` + #[error(transparent)] + Uuid(#[from] uuid::Error), + + /// Error happening with decimal representation + #[error("number of bytes requested for decimal sign extension {requested} is less than the number of bytes needed to decode {needed}")] + SignExtend { requested: usize, needed: usize }, + + /// All cases of `std::num::TryFromIntError` + #[error(transparent)] + TryFromInt(#[from] std::num::TryFromIntError), + + /// All cases of `serde_json::Error` + #[error(transparent)] + JSON(#[from] serde_json::Error), + + /// All cases of `std::string::FromUtf8Error` + #[error(transparent)] + FromUtf8(#[from] std::string::FromUtf8Error), + + /// Error happening when there is a mismatch of the snappy CRC + #[error("bad Snappy CRC32; expected {expected:x} but got {found:x}")] + SnappyCrcError { expected: u32, found: u32 }, + + /// Errors coming from Snappy encoding and decoding + #[cfg(feature = "snappy")] + #[error(transparent)] + Snappy(#[from] snap::Error), +} diff --git a/src/lib.rs b/src/lib.rs index c7922f7bd82..d4f06540fbe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -49,6 +49,11 @@ //! features = ["snappy"] //! ``` //! +//! # Upgrading to a newer minor version +//! +//! The library is still in beta, so there might be backward-incompatible changes between minor +//! versions. If you have troubles upgrading, check the [version upgrade guide](migration_guide.md). +//! //! # Defining a schema //! //! An Avro data cannot exist without an Avro schema. Schemas **must** be used while writing and @@ -409,8 +414,7 @@ //! quick reference of the library interface: //! //! ``` -//! use avro_rs::{Codec, Reader, Schema, Writer, from_value, types::Record}; -//! use failure::Error; +//! use avro_rs::{Codec, Reader, Schema, Writer, from_value, types::Record, Error}; //! use serde::{Deserialize, Serialize}; //! //! #[derive(Debug, Deserialize, Serialize)] @@ -475,10 +479,9 @@ //! ```rust //! use avro_rs::{ //! types::Record, types::Value, Codec, Days, Decimal, Duration, Millis, Months, Reader, Schema, -//! Writer, +//! Writer, Error, //! }; //! use num_bigint::ToBigInt; -//! use failure::Error; //! //! fn main() -> Result<(), Error> { //! let raw_schema = r#" @@ -588,8 +591,7 @@ //! An example of fingerprinting for the supported fingerprints: //! //! ```rust -//! use avro_rs::Schema; -//! use failure::Error; +//! use avro_rs::{Schema, Error}; //! use md5::Md5; //! use sha2::Sha256; //! @@ -681,6 +683,7 @@ mod decimal; mod decode; mod duration; mod encode; +mod errors; mod reader; mod ser; mod util; @@ -691,15 +694,15 @@ pub mod schema_compatibility; pub mod types; pub use crate::codec::Codec; -pub use crate::de::{from_value, Error as DeError}; +pub use crate::de::from_value; pub use crate::decimal::Decimal; pub use crate::duration::{Days, Duration, Millis, Months}; +pub use crate::errors::Error; pub use crate::reader::{from_avro_datum, Reader}; -pub use crate::schema::{ParseSchemaError, Schema}; -pub use crate::ser::{to_value, Error as SerError}; -pub use crate::types::SchemaResolutionError; -pub use crate::util::{max_allocation_bytes, DecodeError}; -pub use crate::writer::{to_avro_datum, ValidationError, Writer}; +pub use crate::schema::Schema; +pub use crate::ser::to_value; +pub use crate::util::max_allocation_bytes; +pub use crate::writer::{to_avro_datum, Writer}; #[cfg(test)] mod tests { diff --git a/src/reader.rs b/src/reader.rs index ce34733cbae..cb84d797e97 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -2,14 +2,13 @@ use std::io::{ErrorKind, Read}; use std::str::{from_utf8, FromStr}; -use failure::Error; use serde_json::from_slice; use crate::decode::decode; -use crate::schema::ParseSchemaError; +use crate::errors::{AvroResult, Error}; use crate::schema::Schema; use crate::types::Value; -use crate::util::{self, DecodeError}; +use crate::util; use crate::Codec; // Internal Block reader. @@ -27,7 +26,7 @@ struct Block { } impl Block { - fn new(reader: R) -> Result, Error> { + fn new(reader: R) -> AvroResult> { let mut block = Block { reader, codec: Codec::Null, @@ -44,14 +43,14 @@ impl Block { /// Try to read the header and to set the writer `Schema`, the `Codec` and the marker based on /// its content. - fn read_header(&mut self) -> Result<(), Error> { + fn read_header(&mut self) -> AvroResult<()> { let meta_schema = Schema::Map(Box::new(Schema::Bytes)); let mut buf = [0u8; 4]; self.reader.read_exact(&mut buf)?; if buf != [b'O', b'b', b'j', 1u8] { - return Err(DecodeError::new("wrong magic in header").into()); + return Err(Error::Decode("wrong magic in header".to_string())); } if let Value::Map(meta) = decode(&meta_schema, &mut self.reader)? { @@ -69,7 +68,7 @@ impl Block { if let Some(schema) = schema { self.writer_schema = schema; } else { - return Err(ParseSchemaError::new("unable to parse schema").into()); + return Err(Error::Parse("unable to parse schema".to_string())); } if let Some(codec) = meta @@ -86,7 +85,7 @@ impl Block { self.codec = codec; } } else { - return Err(DecodeError::new("no metadata in header").into()); + return Err(Error::Decode("no metadata in header".to_string())); } let mut buf = [0u8; 16]; @@ -96,7 +95,7 @@ impl Block { Ok(()) } - fn fill_buf(&mut self, n: usize) -> Result<(), Error> { + fn fill_buf(&mut self, n: usize) -> AvroResult<()> { // The buffer needs to contain exactly `n` elements, otherwise codecs will potentially read // invalid bytes. // @@ -116,7 +115,7 @@ impl Block { /// Try to read a data block, also performing schema resolution for the objects contained in /// the block. The objects are stored in an internal buffer to the `Reader`. - fn read_block_next(&mut self) -> Result<(), Error> { + fn read_block_next(&mut self) -> AvroResult<()> { assert!(self.is_empty(), "Expected self to be empty!"); match util::read_long(&mut self.reader) { Ok(block_len) => { @@ -127,9 +126,9 @@ impl Block { self.reader.read_exact(&mut marker)?; if marker != self.marker { - return Err( - DecodeError::new("block marker does not match header marker").into(), - ); + return Err(Error::Decode( + "block marker does not match header marker".to_string(), + )); } // NOTE (JAB): This doesn't fit this Reader pattern very well. @@ -143,13 +142,15 @@ impl Block { return Ok(()); } Err(e) => { - if let ErrorKind::UnexpectedEof = e.downcast::<::std::io::Error>()?.kind() { - // to not return any error in case we only finished to read cleanly from the stream - return Ok(()); + if let Error::IO(ioe) = e { + if let ErrorKind::UnexpectedEof = ioe.kind() { + // to not return any error in case we only finished to read cleanly from the stream + return Ok(()); + } } } }; - Err(DecodeError::new("unable to read block").into()) + Err(Error::Decode("unable to read block".to_string())) } fn len(&self) -> usize { @@ -160,7 +161,7 @@ impl Block { self.len() == 0 } - fn read_next(&mut self, read_schema: Option<&Schema>) -> Result, Error> { + fn read_next(&mut self, read_schema: Option<&Schema>) -> AvroResult> { if self.is_empty() { self.read_block_next()?; if self.is_empty() { @@ -204,7 +205,7 @@ impl<'a, R: Read> Reader<'a, R> { /// No reader `Schema` will be set. /// /// **NOTE** The avro header is going to be read automatically upon creation of the `Reader`. - pub fn new(reader: R) -> Result, Error> { + pub fn new(reader: R) -> AvroResult> { let block = Block::new(reader)?; let reader = Reader { block, @@ -219,7 +220,7 @@ impl<'a, R: Read> Reader<'a, R> { /// to read from. /// /// **NOTE** The avro header is going to be read automatically upon creation of the `Reader`. - pub fn with_schema(schema: &'a Schema, reader: R) -> Result, Error> { + pub fn with_schema(schema: &'a Schema, reader: R) -> AvroResult> { let block = Block::new(reader)?; let mut reader = Reader { block, @@ -243,7 +244,7 @@ impl<'a, R: Read> Reader<'a, R> { } #[inline] - fn read_next(&mut self) -> Result, Error> { + fn read_next(&mut self) -> AvroResult> { let read_schema = if self.should_resolve_schema { self.reader_schema } else { @@ -255,7 +256,7 @@ impl<'a, R: Read> Reader<'a, R> { } impl<'a, R: Read> Iterator for Reader<'a, R> { - type Item = Result; + type Item = AvroResult; fn next(&mut self) -> Option { // to prevent keep on reading after the first error occurs @@ -284,7 +285,7 @@ pub fn from_avro_datum( writer_schema: &Schema, reader: &mut R, reader_schema: Option<&Schema>, -) -> Result { +) -> AvroResult { let value = decode(writer_schema, reader)?; match reader_schema { Some(ref schema) => value.resolve(schema), diff --git a/src/schema.rs b/src/schema.rs index 723826363ff..08c3252f83e 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -1,8 +1,8 @@ //! Logic for parsing and interacting with schemas in Avro format. +use crate::errors::{AvroResult, Error}; use crate::types; use crate::util::MapHelper; use digest::Digest; -use failure::{Error, Fail}; use serde::{ ser::{SerializeMap, SerializeSeq}, Deserialize, Serialize, Serializer, @@ -15,20 +15,6 @@ use std::fmt; use std::str::FromStr; use strum_macros::EnumString; -/// Describes errors happened while parsing Avro schemas. -#[derive(Fail, Debug)] -#[fail(display = "Failed to parse schema: {}", _0)] -pub struct ParseSchemaError(String); - -impl ParseSchemaError { - pub fn new(msg: S) -> ParseSchemaError - where - S: Into, - { - ParseSchemaError(msg.into()) - } -} - /// 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/spec.html#schema_fingerprints) @@ -218,10 +204,10 @@ impl Name { } /// Parse a `serde_json::Value` into a `Name`. - fn parse(complex: &Map) -> Result { + fn parse(complex: &Map) -> AvroResult { let name = complex .name() - .ok_or_else(|| ParseSchemaError::new("No `name` field"))?; + .ok_or_else(|| Error::Parse("No `name` field".to_string()))?; let namespace = complex.string("namespace"); @@ -297,10 +283,10 @@ pub enum RecordFieldOrder { impl RecordField { /// Parse a `serde_json::Value` into a `RecordField`. - fn parse(field: &Map, position: usize) -> Result { + fn parse(field: &Map, position: usize) -> AvroResult { let name = field .name() - .ok_or_else(|| ParseSchemaError::new("No `name` in record field"))?; + .ok_or_else(|| Error::Parse("No `name` in record field".to_string()))?; // TODO: "type" = "" let schema = Schema::parse_complex(field)?; @@ -335,17 +321,19 @@ pub struct UnionSchema { } impl UnionSchema { - pub(crate) fn new(schemas: Vec) -> Result { + pub(crate) fn new(schemas: Vec) -> AvroResult { let mut vindex = HashMap::new(); for (i, schema) in schemas.iter().enumerate() { if let Schema::Union(_) = schema { - return Err( - ParseSchemaError::new("Unions may not directly contain a union").into(), - ); + return Err(Error::Parse( + "Unions may not directly contain a union".to_string(), + )); } let kind = SchemaKind::from(schema); if vindex.insert(kind, i).is_some() { - return Err(ParseSchemaError::new("Unions cannot contain duplicate types").into()); + return Err(Error::Parse( + "Unions cannot contain duplicate types".to_string(), + )); } } Ok(UnionSchema { @@ -385,12 +373,12 @@ type DecimalMetadata = usize; pub(crate) type Precision = DecimalMetadata; pub(crate) type Scale = DecimalMetadata; -fn parse_json_integer_for_decimal(value: &serde_json::Number) -> Result { +fn parse_json_integer_for_decimal(value: &serde_json::Number) -> AvroResult { if value.is_u64() { Ok(value .as_u64() .ok_or_else(|| { - ParseSchemaError::new(format!( + Error::Parse(format!( "JSON value {} claims to be u64 but cannot be converted", value )) @@ -400,24 +388,23 @@ fn parse_json_integer_for_decimal(value: &serde_json::Number) -> Result Result { + pub fn parse_str(input: &str) -> AvroResult { // TODO: (#82) this should be a ParseSchemaError wrapping the JSON error let value = serde_json::from_str(input)?; Self::parse(&value) @@ -425,12 +412,14 @@ impl Schema { /// Create a `Schema` from a `serde_json::Value` representing a JSON Avro /// schema. - pub fn parse(value: &Value) -> Result { + pub fn parse(value: &Value) -> AvroResult { match *value { Value::String(ref t) => Schema::parse_primitive(t.as_str()), Value::Object(ref data) => Schema::parse_complex(data), Value::Array(ref data) => Schema::parse_union(data), - _ => Err(ParseSchemaError::new("Must be a JSON string, object or array").into()), + _ => Err(Error::Parse( + "Must be a JSON string, object or array".to_string(), + )), } } @@ -459,7 +448,7 @@ impl Schema { /// Parse a `serde_json::Value` representing a primitive Avro type into a /// `Schema`. - fn parse_primitive(primitive: &str) -> Result { + fn parse_primitive(primitive: &str) -> AvroResult { match primitive { "null" => Ok(Schema::Null), "boolean" => Ok(Schema::Boolean), @@ -469,27 +458,22 @@ impl Schema { "float" => Ok(Schema::Float), "bytes" => Ok(Schema::Bytes), "string" => Ok(Schema::String), - other => Err(ParseSchemaError::new(format!("Unknown type: {}", other)).into()), + other => Err(Error::Parse(format!("Unknown type: {}", other))), } } - fn parse_precision_and_scale( - complex: &Map, - ) -> Result<(Precision, Scale), Error> { + fn parse_precision_and_scale(complex: &Map) -> AvroResult<(Precision, Scale)> { fn get_decimal_integer( complex: &Map, key: &str, - ) -> Result { + ) -> AvroResult { match complex.get(key) { Some(&Value::Number(ref value)) => parse_json_integer_for_decimal(value), - None => { - Err(ParseSchemaError::new(format!("{} missing for decimal type", key)).into()) - } - precision => Err(ParseSchemaError::new(format!( + None => Err(Error::Parse(format!("{} missing for decimal type", key))), + precision => Err(Error::Parse(format!( "invalid JSON for {}: {:?}", key, precision, - )) - .into()), + ))), } } let precision = get_decimal_integer(complex, "precision")?; @@ -502,11 +486,11 @@ impl Schema { /// /// Avro supports "recursive" definition of types. /// e.g: {"type": {"type": "string"}} - fn parse_complex(complex: &Map) -> Result { + fn parse_complex(complex: &Map) -> AvroResult { fn logical_verify_type( complex: &Map, kinds: &[SchemaKind], - ) -> Result { + ) -> AvroResult { match complex.get("type") { Some(value) => { let ty = Schema::parse(value)?; @@ -516,16 +500,15 @@ impl Schema { { Ok(ty) } else { - Err(ParseSchemaError::new(format!( + Err(Error::Parse(format!( "Unexpected `type` ({}) variant for `logicalType`", value - )) - .into()) + ))) } } - None => { - Err(ParseSchemaError::new("No `type` field found for `logicalType`").into()) - } + None => Err(Error::Parse( + "No `type` field found for `logicalType`".to_string(), + )), } } match complex.get("logicalType") { @@ -579,7 +562,7 @@ impl Schema { // The spec says to ignore invalid logical types and just continue through to the // underlying type - It is unclear whether that applies to this case or not, where the // `logicalType` is not a string. - Some(_) => return Err(ParseSchemaError::new("logicalType must be a string").into()), + Some(_) => return Err(Error::Parse("logicalType must be a string".to_string())), _ => {} } match complex.get("type") { @@ -593,16 +576,17 @@ impl Schema { }, Some(&Value::Object(ref data)) => Schema::parse_complex(data), Some(&Value::Array(ref variants)) => Schema::parse_union(variants), - Some(unknown) => { - Err(ParseSchemaError::new(format!("Unknown complex type: {0:?}", unknown)).into()) - } - None => Err(ParseSchemaError::new("No `type` in complex type").into()), + Some(unknown) => Err(Error::Parse(format!( + "Unknown complex type: {0:?}", + unknown + ))), + None => Err(Error::Parse("No `type` in complex type".to_string())), } } /// Parse a `serde_json::Value` representing a Avro record type into a /// `Schema`. - fn parse_record(complex: &Map) -> Result { + fn parse_record(complex: &Map) -> AvroResult { let name = Name::parse(complex)?; let mut lookup = HashMap::new(); @@ -610,7 +594,7 @@ impl Schema { let fields: Vec = complex .get("fields") .and_then(|fields| fields.as_array()) - .ok_or_else(|| ParseSchemaError::new("No `fields` in record").into()) + .ok_or_else(|| Error::Parse("No `fields` in record".to_string())) .and_then(|fields| { fields .iter() @@ -634,19 +618,19 @@ impl Schema { /// Parse a `serde_json::Value` representing a Avro enum type into a /// `Schema`. - fn parse_enum(complex: &Map) -> Result { + fn parse_enum(complex: &Map) -> AvroResult { let name = Name::parse(complex)?; let symbols = complex .get("symbols") .and_then(|v| v.as_array()) - .ok_or_else(|| ParseSchemaError::new("No `symbols` field in enum")) + .ok_or_else(|| Error::Parse("No `symbols` field in enum".to_string())) .and_then(|symbols| { symbols .iter() .map(|symbol| symbol.as_str().map(|s| s.to_string())) .collect::>() - .ok_or_else(|| ParseSchemaError::new("Unable to parse `symbols` in enum")) + .ok_or_else(|| Error::Parse("Unable to parse `symbols` in enum".to_string())) })?; Ok(Schema::Enum { @@ -658,27 +642,27 @@ impl Schema { /// Parse a `serde_json::Value` representing a Avro array type into a /// `Schema`. - fn parse_array(complex: &Map) -> Result { + fn parse_array(complex: &Map) -> AvroResult { complex .get("items") - .ok_or_else(|| ParseSchemaError::new("No `items` in array").into()) + .ok_or_else(|| Error::Parse("No `items` in array".to_string())) .and_then(|items| Schema::parse(items)) .map(|schema| Schema::Array(Box::new(schema))) } /// Parse a `serde_json::Value` representing a Avro map type into a /// `Schema`. - fn parse_map(complex: &Map) -> Result { + fn parse_map(complex: &Map) -> AvroResult { complex .get("values") - .ok_or_else(|| ParseSchemaError::new("No `values` in map").into()) + .ok_or_else(|| Error::Parse("No `values` in map".to_string())) .and_then(|items| Schema::parse(items)) .map(|schema| Schema::Map(Box::new(schema))) } /// Parse a `serde_json::Value` representing a Avro union type into a /// `Schema`. - fn parse_union(items: &[Value]) -> Result { + fn parse_union(items: &[Value]) -> AvroResult { items .iter() .map(Schema::parse) @@ -688,13 +672,13 @@ impl Schema { /// Parse a `serde_json::Value` representing a Avro fixed type into a /// `Schema`. - fn parse_fixed(complex: &Map) -> Result { + fn parse_fixed(complex: &Map) -> AvroResult { let name = Name::parse(complex)?; let size = complex .get("size") .and_then(|v| v.as_i64()) - .ok_or_else(|| ParseSchemaError::new("No `size` in fixed"))?; + .ok_or_else(|| Error::Parse("No `size` in fixed".to_string()))?; Ok(Schema::Fixed { name, diff --git a/src/ser.rs b/src/ser.rs index fa25d31c930..41278e10e9c 100644 --- a/src/ser.rs +++ b/src/ser.rs @@ -1,16 +1,19 @@ //! Logic for serde-compatible serialization. use std::collections::HashMap; -use std::error; use std::fmt; use std::iter::once; -use serde::{ - ser::{self, Error as SerdeError}, - Serialize, -}; +use serde::{ser, Serialize}; +use crate::errors::Error; use crate::types::Value; +impl ser::Error for Error { + fn custom(msg: T) -> Self { + Error::Ser(msg.to_string()) + } +} + #[derive(Clone, Default)] pub struct Serializer {} @@ -39,32 +42,6 @@ pub struct StructVariantSerializer<'a> { fields: Vec<(String, Value)>, } -/// Represents errors that could be encountered while serializing data -#[derive(Clone, Debug, PartialEq)] -pub struct Error { - message: String, -} - -impl ser::Error for Error { - fn custom(msg: T) -> Self { - Error { - message: msg.to_string(), - } - } -} - -impl fmt::Display for Error { - fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { - formatter.write_str(&self.to_string()) - } -} - -impl error::Error for Error { - fn description(&self) -> &str { - &self.message - } -} - impl SeqSerializer { pub fn new(len: Option) -> SeqSerializer { let items = match len { @@ -170,7 +147,7 @@ impl<'b> ser::Serializer for &'b mut Serializer { if v <= i64::max_value() as u64 { self.serialize_i64(v as i64) } else { - Err(Error::custom("u64 is too large")) + Err(ser::Error::custom("u64 is too large")) } } @@ -410,7 +387,7 @@ impl ser::SerializeMap for MapSerializer { self.indices.insert(key, self.values.len()); Ok(()) } else { - Err(Error::custom("map key is not a string")) + Err(ser::Error::custom("map key is not a string")) } } diff --git a/src/types.rs b/src/types.rs index 18340c2c774..f2b7bc9d64b 100644 --- a/src/types.rs +++ b/src/types.rs @@ -5,28 +5,14 @@ use std::hash::BuildHasher; use std::str::FromStr; use std::u8; -use failure::{Error, Fail}; use serde_json::Value as JsonValue; use uuid::Uuid; use crate::decimal::Decimal; use crate::duration::Duration; +use crate::errors::{AvroResult, Error}; use crate::schema::{Precision, RecordField, Scale, Schema, SchemaKind, UnionSchema}; -/// Describes errors happened while performing schema resolution on Avro data. -#[derive(Fail, Debug)] -#[fail(display = "Schema resoulution error: {}", _0)] -pub struct SchemaResolutionError(pub String); - -impl SchemaResolutionError { - pub fn new(msg: S) -> SchemaResolutionError - where - S: Into, - { - SchemaResolutionError(msg.into()) - } -} - /// Compute the maximum decimal value precision of a byte array of length `len` could hold. fn max_prec_for_len(len: usize) -> Result { Ok((2.0_f64.powi(i32::try_from(8 * len - 1)?) - 1.0 as f64) @@ -333,7 +319,7 @@ impl Value { /// See [Schema Resolution](https://avro.apache.org/docs/current/spec.html#Schema+Resolution) /// in the Avro specification for the full set of rules of schema /// resolution. - pub fn resolve(mut self, schema: &Schema) -> Result { + pub fn resolve(mut self, schema: &Schema) -> AvroResult { // Check if this schema is a union, and if the reader schema is not. if SchemaKind::from(&self) == SchemaKind::Union && SchemaKind::from(schema) != SchemaKind::Union @@ -375,35 +361,36 @@ impl Value { } } - fn resolve_uuid(self) -> Result { + fn resolve_uuid(self) -> AvroResult { match self { uuid @ Value::Uuid(_) => Ok(uuid), Value::String(ref string) => Ok(Value::Uuid(Uuid::from_str(string)?)), - other => { - Err(SchemaResolutionError::new(format!("UUID expected, got {:?}", other)).into()) - } + other => Err(Error::SchemaResolution(format!( + "UUID expected, got {:?}", + other + ))), } } - fn resolve_duration(self) -> Result { + fn resolve_duration(self) -> AvroResult { match self { duration @ Value::Duration { .. } => Ok(duration), Value::Fixed(size, bytes) => { if size != 12 { - return Err(SchemaResolutionError::new(format!( + return Err(Error::SchemaResolution(format!( "Fixed bytes of size 12 expected, got Fixed of size {}", size - )) - .into()); + ))); } Ok(Value::Duration(Duration::from([ bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7], bytes[8], bytes[9], bytes[10], bytes[11], ]))) } - other => Err( - SchemaResolutionError::new(format!("Duration expected, got {:?}", other)).into(), - ), + other => Err(Error::SchemaResolution(format!( + "Duration expected, got {:?}", + other + ))), } } @@ -412,42 +399,38 @@ impl Value { precision: Precision, scale: Scale, inner: &Schema, - ) -> Result { + ) -> AvroResult { if scale > precision { - return Err(SchemaResolutionError::new(format!( + return Err(Error::SchemaResolution(format!( "Scale {} is greater than precision {}", scale, precision - )) - .into()); + ))); } match inner { &Schema::Fixed { size, .. } => { if max_prec_for_len(size)? < precision { - return Err(SchemaResolutionError::new(format!( + return Err(Error::SchemaResolution(format!( "Fixed size {} is not large enough to hold decimal values of precision {}", size, precision, - )) - .into()); + ))); } } Schema::Bytes => (), _ => { - return Err(SchemaResolutionError::new(format!( + return Err(Error::SchemaResolution(format!( "Underlying decimal type must be fixed or bytes, got {:?}", inner - )) - .into()) + ))) } }; match self { Value::Decimal(num) => { let num_bytes = num.len(); if max_prec_for_len(num_bytes)? > precision { - Err(SchemaResolutionError::new(format!( + Err(Error::SchemaResolution(format!( "Precision {} too small to hold decimal values with {} bytes", precision, num_bytes, - )) - .into()) + ))) } else { Ok(Value::Decimal(num)) } @@ -455,142 +438,145 @@ impl Value { } Value::Fixed(_, bytes) | Value::Bytes(bytes) => { if max_prec_for_len(bytes.len())? > precision { - Err(SchemaResolutionError::new(format!( + Err(Error::SchemaResolution(format!( "Precision {} too small to hold decimal values with {} bytes", precision, bytes.len(), - )) - .into()) + ))) } else { // precision and scale match, can we assume the underlying type can hold the data? Ok(Value::Decimal(Decimal::from(bytes))) } } - other => { - Err(SchemaResolutionError::new(format!("Decimal expected, got {:?}", other)).into()) - } + other => Err(Error::SchemaResolution(format!( + "Decimal expected, got {:?}", + other + ))), } } - fn resolve_date(self) -> Result { + fn resolve_date(self) -> AvroResult { match self { Value::Date(d) | Value::Int(d) => Ok(Value::Date(d)), - other => { - Err(SchemaResolutionError::new(format!("Date expected, got {:?}", other)).into()) - } + other => Err(Error::SchemaResolution(format!( + "Date expected, got {:?}", + other + ))), } } - fn resolve_time_millis(self) -> Result { + fn resolve_time_millis(self) -> AvroResult { match self { Value::TimeMillis(t) | Value::Int(t) => Ok(Value::TimeMillis(t)), - other => Err(SchemaResolutionError::new(format!( + other => Err(Error::SchemaResolution(format!( "TimeMillis expected, got {:?}", other - )) - .into()), + ))), } } - fn resolve_time_micros(self) -> Result { + fn resolve_time_micros(self) -> AvroResult { match self { Value::TimeMicros(t) | Value::Long(t) => Ok(Value::TimeMicros(t)), Value::Int(t) => Ok(Value::TimeMicros(i64::from(t))), - other => Err(SchemaResolutionError::new(format!( + other => Err(Error::SchemaResolution(format!( "TimeMicros expected, got {:?}", other - )) - .into()), + ))), } } - fn resolve_timestamp_millis(self) -> Result { + fn resolve_timestamp_millis(self) -> AvroResult { match self { Value::TimestampMillis(ts) | Value::Long(ts) => Ok(Value::TimestampMillis(ts)), Value::Int(ts) => Ok(Value::TimestampMillis(i64::from(ts))), - other => Err(SchemaResolutionError::new(format!( + other => Err(Error::SchemaResolution(format!( "TimestampMillis expected, got {:?}", other - )) - .into()), + ))), } } - fn resolve_timestamp_micros(self) -> Result { + fn resolve_timestamp_micros(self) -> AvroResult { match self { Value::TimestampMicros(ts) | Value::Long(ts) => Ok(Value::TimestampMicros(ts)), Value::Int(ts) => Ok(Value::TimestampMicros(i64::from(ts))), - other => Err(SchemaResolutionError::new(format!( + other => Err(Error::SchemaResolution(format!( "TimestampMicros expected, got {:?}", other - )) - .into()), + ))), } } - fn resolve_null(self) -> Result { + fn resolve_null(self) -> AvroResult { match self { Value::Null => Ok(Value::Null), - other => { - Err(SchemaResolutionError::new(format!("Null expected, got {:?}", other)).into()) - } + other => Err(Error::SchemaResolution(format!( + "Null expected, got {:?}", + other + ))), } } - fn resolve_boolean(self) -> Result { + fn resolve_boolean(self) -> AvroResult { match self { Value::Boolean(b) => Ok(Value::Boolean(b)), - other => { - Err(SchemaResolutionError::new(format!("Boolean expected, got {:?}", other)).into()) - } + other => Err(Error::SchemaResolution(format!( + "Boolean expected, got {:?}", + other + ))), } } - fn resolve_int(self) -> Result { + fn resolve_int(self) -> AvroResult { match self { Value::Int(n) => Ok(Value::Int(n)), Value::Long(n) => Ok(Value::Int(n as i32)), - other => { - Err(SchemaResolutionError::new(format!("Int expected, got {:?}", other)).into()) - } + other => Err(Error::SchemaResolution(format!( + "Int expected, got {:?}", + other + ))), } } - fn resolve_long(self) -> Result { + fn resolve_long(self) -> AvroResult { match self { Value::Int(n) => Ok(Value::Long(i64::from(n))), Value::Long(n) => Ok(Value::Long(n)), - other => { - Err(SchemaResolutionError::new(format!("Long expected, got {:?}", other)).into()) - } + other => Err(Error::SchemaResolution(format!( + "Long expected, got {:?}", + other + ))), } } - fn resolve_float(self) -> Result { + fn resolve_float(self) -> AvroResult { match self { Value::Int(n) => Ok(Value::Float(n as f32)), Value::Long(n) => Ok(Value::Float(n as f32)), Value::Float(x) => Ok(Value::Float(x)), Value::Double(x) => Ok(Value::Float(x as f32)), - other => { - Err(SchemaResolutionError::new(format!("Float expected, got {:?}", other)).into()) - } + other => Err(Error::SchemaResolution(format!( + "Float expected, got {:?}", + other + ))), } } - fn resolve_double(self) -> Result { + fn resolve_double(self) -> AvroResult { match self { Value::Int(n) => Ok(Value::Double(f64::from(n))), Value::Long(n) => Ok(Value::Double(n as f64)), Value::Float(x) => Ok(Value::Double(f64::from(x))), Value::Double(x) => Ok(Value::Double(x)), - other => { - Err(SchemaResolutionError::new(format!("Double expected, got {:?}", other)).into()) - } + other => Err(Error::SchemaResolution(format!( + "Double expected, got {:?}", + other + ))), } } - fn resolve_bytes(self) -> Result { + fn resolve_bytes(self) -> AvroResult { match self { Value::Bytes(bytes) => Ok(Value::Bytes(bytes)), Value::String(s) => Ok(Value::Bytes(s.into_bytes())), @@ -600,51 +586,52 @@ impl Value { .map(Value::try_u8) .collect::, _>>()?, )), - other => { - Err(SchemaResolutionError::new(format!("Bytes expected, got {:?}", other)).into()) - } + other => Err(Error::SchemaResolution(format!( + "Bytes expected, got {:?}", + other + ))), } } - fn resolve_string(self) -> Result { + fn resolve_string(self) -> AvroResult { match self { Value::String(s) => Ok(Value::String(s)), Value::Bytes(bytes) => Ok(Value::String(String::from_utf8(bytes)?)), - other => { - Err(SchemaResolutionError::new(format!("String expected, got {:?}", other)).into()) - } + other => Err(Error::SchemaResolution(format!( + "String expected, got {:?}", + other + ))), } } - fn resolve_fixed(self, size: usize) -> Result { + fn resolve_fixed(self, size: usize) -> AvroResult { match self { Value::Fixed(n, bytes) => { if n == size { Ok(Value::Fixed(n, bytes)) } else { - Err(SchemaResolutionError::new(format!( + Err(Error::SchemaResolution(format!( "Fixed size mismatch, {} expected, got {}", size, n - )) - .into()) + ))) } } - other => { - Err(SchemaResolutionError::new(format!("String expected, got {:?}", other)).into()) - } + other => Err(Error::SchemaResolution(format!( + "String expected, got {:?}", + other + ))), } } - fn resolve_enum(self, symbols: &[String]) -> Result { + fn resolve_enum(self, symbols: &[String]) -> AvroResult { let validate_symbol = |symbol: String, symbols: &[String]| { if let Some(index) = symbols.iter().position(|ref item| item == &&symbol) { Ok(Value::Enum(index as i32, symbol)) } else { - Err(SchemaResolutionError::new(format!( + Err(Error::SchemaResolution(format!( "Enum default {} is not among allowed symbols {:?}", symbol, symbols, - )) - .into()) + ))) } }; @@ -653,24 +640,22 @@ impl Value { if i >= 0 && i < symbols.len() as i32 { validate_symbol(s, symbols) } else { - Err(SchemaResolutionError::new(format!( + Err(Error::SchemaResolution(format!( "Enum value {} is out of bound {}", i, symbols.len() as i32 - )) - .into()) + ))) } } Value::String(s) => validate_symbol(s, symbols), - other => Err(SchemaResolutionError::new(format!( + other => Err(Error::SchemaResolution(format!( "Enum({:?}) expected, got {:?}", symbols, other - )) - .into()), + ))), } } - fn resolve_union(self, schema: &UnionSchema) -> Result { + fn resolve_union(self, schema: &UnionSchema) -> AvroResult { let v = match self { // Both are unions case. Value::Union(v) => *v, @@ -678,13 +663,13 @@ impl Value { v => v, }; // Find the first match in the reader schema. - let (_, inner) = schema - .find_schema(&v) - .ok_or_else(|| SchemaResolutionError::new("Could not find matching type in union"))?; + let (_, inner) = schema.find_schema(&v).ok_or_else(|| { + Error::SchemaResolution("Could not find matching type in union".to_string()) + })?; Ok(Value::Union(Box::new(v.resolve(inner)?))) } - fn resolve_array(self, schema: &Schema) -> Result { + fn resolve_array(self, schema: &Schema) -> AvroResult { match self { Value::Array(items) => Ok(Value::Array( items @@ -692,15 +677,14 @@ impl Value { .map(|item| item.resolve(schema)) .collect::>()?, )), - other => Err(SchemaResolutionError::new(format!( + other => Err(Error::SchemaResolution(format!( "Array({:?}) expected, got {:?}", schema, other - )) - .into()), + ))), } } - fn resolve_map(self, schema: &Schema) -> Result { + fn resolve_map(self, schema: &Schema) -> AvroResult { match self { Value::Map(items) => Ok(Value::Map( items @@ -708,22 +692,21 @@ impl Value { .map(|(key, value)| value.resolve(schema).map(|value| (key, value))) .collect::>()?, )), - other => Err(SchemaResolutionError::new(format!( + other => Err(Error::SchemaResolution(format!( "Map({:?}) expected, got {:?}", schema, other - )) - .into()), + ))), } } - fn resolve_record(self, fields: &[RecordField]) -> Result { + fn resolve_record(self, fields: &[RecordField]) -> AvroResult { let mut items = match self { Value::Map(items) => Ok(items), Value::Record(fields) => Ok(fields.into_iter().collect::>()), - other => Err(Error::from(SchemaResolutionError::new(format!( + other => Err(Error::SchemaResolution(format!( "Record({:?}) expected, got {:?}", fields, other - )))), + ))), }?; let new_fields = fields @@ -750,11 +733,10 @@ impl Value { _ => Value::from(value.clone()), }, None => { - return Err(SchemaResolutionError::new(format!( + return Err(Error::SchemaResolution(format!( "missing field {} in record", field.name - )) - .into()); + ))); } }, }; @@ -767,7 +749,7 @@ impl Value { Ok(Value::Record(new_fields)) } - fn try_u8(self) -> Result { + fn try_u8(self) -> AvroResult { let int = self.resolve(&Schema::Int)?; if let Value::Int(n) = int { if n >= 0 && n <= i32::from(u8::MAX) { @@ -775,7 +757,10 @@ impl Value { } } - Err(SchemaResolutionError::new(format!("Unable to convert to u8, got {:?}", int)).into()) + Err(Error::SchemaResolution(format!( + "Unable to convert to u8, got {:?}", + int + ))) } } diff --git a/src/util.rs b/src/util.rs index f1a1f42eb15..e32bcdedd7d 100644 --- a/src/util.rs +++ b/src/util.rs @@ -2,9 +2,10 @@ use std::i64; use std::io::Read; use std::sync::Once; -use failure::{Error, Fail}; use serde_json::{Map, Value}; +use crate::errors::{AvroResult, Error}; + /// Maximum number of bytes that can be allocated when decoding /// Avro-encoded values. This is a protection against ill-formed /// data, whose length field might be interpreted as enourmous. @@ -12,34 +13,6 @@ use serde_json::{Map, Value}; pub static mut MAX_ALLOCATION_BYTES: usize = 512 * 1024 * 1024; static MAX_ALLOCATION_BYTES_ONCE: Once = Once::new(); -/// Describes errors happened trying to allocate too many bytes -#[derive(Fail, Debug)] -#[fail(display = "Allocation error: {}", _0)] -pub struct AllocationError(String); - -impl AllocationError { - pub fn new(msg: S) -> AllocationError - where - S: Into, - { - AllocationError(msg.into()) - } -} - -/// Describes errors happened while decoding Avro data. -#[derive(Fail, Debug)] -#[fail(display = "Decoding error: {}", _0)] -pub struct DecodeError(String); - -impl DecodeError { - pub fn new(msg: S) -> DecodeError - where - S: Into, - { - DecodeError(msg.into()) - } -} - pub trait MapHelper { fn string(&self, key: &str) -> Option; @@ -60,7 +33,7 @@ impl MapHelper for Map { } } -pub fn read_long(reader: &mut R) -> Result { +pub fn read_long(reader: &mut R) -> AvroResult { zag_i64(reader) } @@ -72,16 +45,16 @@ pub fn zig_i64(n: i64, buffer: &mut Vec) { encode_variable(((n << 1) ^ (n >> 63)) as u64, buffer) } -pub fn zag_i32(reader: &mut R) -> Result { +pub fn zag_i32(reader: &mut R) -> AvroResult { let i = zag_i64(reader)?; if i < i64::from(i32::min_value()) || i > i64::from(i32::max_value()) { - Err(DecodeError::new("int out of range").into()) + Err(Error::Decode("int out of range".to_string())) } else { Ok(i as i32) } } -pub fn zag_i64(reader: &mut R) -> Result { +pub fn zag_i64(reader: &mut R) -> AvroResult { let z = decode_variable(reader)?; Ok(if z & 0x1 == 0 { (z >> 1) as i64 @@ -102,7 +75,7 @@ fn encode_variable(mut z: u64, buffer: &mut Vec) { } } -fn decode_variable(reader: &mut R) -> Result { +fn decode_variable(reader: &mut R) -> AvroResult { let mut i = 0u64; let mut buf = [0u8; 1]; @@ -110,7 +83,9 @@ fn decode_variable(reader: &mut R) -> Result { loop { if j > 9 { // if j * 7 > 64 - return Err(DecodeError::new("Overflow when decoding integer value").into()); + return Err(Error::Decode( + "Overflow when decoding integer value".to_string(), + )); } reader.read_exact(&mut buf[..])?; i |= (u64::from(buf[0] & 0x7F)) << (j * 7); @@ -140,17 +115,16 @@ pub fn max_allocation_bytes(num_bytes: usize) -> usize { } } -pub fn safe_len(len: usize) -> Result { +pub fn safe_len(len: usize) -> AvroResult { let max_bytes = max_allocation_bytes(512 * 1024 * 1024); if len <= max_bytes { Ok(len) } else { - Err(AllocationError::new(format!( - "Unable to allocate {} bytes (Maximum allowed: {})", - len, max_bytes - )) - .into()) + Err(Error::MemoryAllocation { + desired: len, + maximum: max_bytes, + }) } } diff --git a/src/writer.rs b/src/writer.rs index 67f147d61b1..ec02e6bac28 100644 --- a/src/writer.rs +++ b/src/writer.rs @@ -2,11 +2,11 @@ use std::collections::HashMap; use std::io::Write; -use failure::{Error, Fail}; use rand::random; use serde::Serialize; use crate::encode::{encode, encode_ref, encode_to_vec}; +use crate::errors::{AvroResult, Error}; use crate::schema::Schema; use crate::ser::Serializer; use crate::types::Value; @@ -15,20 +15,6 @@ use crate::Codec; const DEFAULT_BLOCK_SIZE: usize = 16000; const AVRO_OBJECT_HEADER: &[u8] = b"Obj\x01"; -/// Describes errors happened while validating Avro data. -#[derive(Fail, Debug)] -#[fail(display = "Validation error: {}", _0)] -pub struct ValidationError(String); - -impl ValidationError { - pub fn new(msg: S) -> ValidationError - where - S: Into, - { - ValidationError(msg.into()) - } -} - /// Main interface for writing Avro formatted values. #[derive(typed_builder::TypedBuilder)] pub struct Writer<'a, W> { @@ -81,7 +67,7 @@ impl<'a, W: Write> Writer<'a, W> { /// **NOTE** This function is not guaranteed to perform any actual write, since it relies on /// internal buffering for performance reasons. If you want to be sure the value has been /// written, then call [`flush`](struct.Writer.html#method.flush). - pub fn append>(&mut self, value: T) -> Result { + pub fn append>(&mut self, value: T) -> AvroResult { let n = if !self.has_header { let header = self.header()?; let n = self.append_bytes(header.as_ref())?; @@ -110,7 +96,7 @@ impl<'a, W: Write> Writer<'a, W> { /// **NOTE** This function is not guaranteed to perform any actual write, since it relies on /// internal buffering for performance reasons. If you want to be sure the value has been /// written, then call [`flush`](struct.Writer.html#method.flush). - pub fn append_value_ref(&mut self, value: &Value) -> Result { + pub fn append_value_ref(&mut self, value: &Value) -> AvroResult { let n = if !self.has_header { let header = self.header()?; let n = self.append_bytes(header.as_ref())?; @@ -140,7 +126,7 @@ impl<'a, W: Write> Writer<'a, W> { /// **NOTE** This function is not guaranteed to perform any actual write, since it relies on /// internal buffering for performance reasons. If you want to be sure the value has been /// written, then call [`flush`](struct.Writer.html#method.flush). - pub fn append_ser(&mut self, value: S) -> Result { + pub fn append_ser(&mut self, value: S) -> AvroResult { let avro_value = value.serialize(&mut self.serializer)?; self.append(avro_value) } @@ -152,7 +138,7 @@ impl<'a, W: Write> Writer<'a, W> { /// /// **NOTE** This function forces the written data to be flushed (an implicit /// call to [`flush`](struct.Writer.html#method.flush) is performed). - pub fn extend>(&mut self, values: I) -> Result + pub fn extend>(&mut self, values: I) -> AvroResult where I: IntoIterator, { @@ -187,7 +173,7 @@ impl<'a, W: Write> Writer<'a, W> { /// /// **NOTE** This function forces the written data to be flushed (an implicit /// call to [`flush`](struct.Writer.html#method.flush) is performed). - pub fn extend_ser(&mut self, values: I) -> Result + pub fn extend_ser(&mut self, values: I) -> AvroResult where I: IntoIterator, { @@ -221,7 +207,7 @@ impl<'a, W: Write> Writer<'a, W> { /// /// **NOTE** This function forces the written data to be flushed (an implicit /// call to [`flush`](struct.Writer.html#method.flush) is performed). - pub fn extend_from_slice(&mut self, values: &[Value]) -> Result { + pub fn extend_from_slice(&mut self, values: &[Value]) -> AvroResult { let mut num_bytes = 0; for value in values { num_bytes += self.append_value_ref(value)?; @@ -235,7 +221,7 @@ impl<'a, W: Write> Writer<'a, W> { /// has been written before releasing the `Writer`. /// /// Return the number of bytes written. - pub fn flush(&mut self) -> Result { + pub fn flush(&mut self) -> AvroResult { if self.num_values == 0 { return Ok(0); } @@ -260,30 +246,30 @@ impl<'a, W: Write> Writer<'a, W> { /// /// **NOTE** This function forces the written data to be flushed (an implicit /// call to [`flush`](struct.Writer.html#method.flush) is performed). - pub fn into_inner(mut self) -> Result { + pub fn into_inner(mut self) -> AvroResult { self.flush()?; Ok(self.writer) } /// Generate and append synchronization marker to the payload. - fn append_marker(&mut self) -> Result { + fn append_marker(&mut self) -> AvroResult { // using .writer.write directly to avoid mutable borrow of self // with ref borrowing of self.marker Ok(self.writer.write(&self.marker)?) } /// Append a raw Avro Value to the payload avoiding to encode it again. - fn append_raw(&mut self, value: &Value, schema: &Schema) -> Result { + fn append_raw(&mut self, value: &Value, schema: &Schema) -> AvroResult { self.append_bytes(encode_to_vec(&value, schema).as_ref()) } /// Append pure bytes to the payload. - fn append_bytes(&mut self, bytes: &[u8]) -> Result { + fn append_bytes(&mut self, bytes: &[u8]) -> AvroResult { Ok(self.writer.write(bytes)?) } /// Create an Avro header based on schema, codec and sync marker. - fn header(&self) -> Result, Error> { + fn header(&self) -> AvroResult> { let schema_bytes = serde_json::to_string(self.schema)?.into_bytes(); let mut metadata = HashMap::with_capacity(2); @@ -312,18 +298,18 @@ fn write_avro_datum>( schema: &Schema, value: T, buffer: &mut Vec, -) -> Result<(), Error> { +) -> AvroResult<()> { let avro = value.into(); if !avro.validate(schema) { - return Err(ValidationError::new("value does not match schema").into()); + return Err(Error::Validation("value does not match schema".to_string())); } encode(&avro, schema, buffer); Ok(()) } -fn write_value_ref(schema: &Schema, value: &Value, buffer: &mut Vec) -> Result<(), Error> { +fn write_value_ref(schema: &Schema, value: &Value, buffer: &mut Vec) -> AvroResult<()> { if !value.validate(schema) { - return Err(ValidationError::new("value does not match schema").into()); + return Err(Error::Validation("value does not match schema".to_string())); } encode_ref(value, schema, buffer); Ok(()) @@ -335,7 +321,7 @@ fn write_value_ref(schema: &Schema, value: &Value, buffer: &mut Vec) -> Resu /// **NOTE** This function has a quite small niche of usage and does NOT generate headers and sync /// markers; use [`Writer`](struct.Writer.html) to be fully Avro-compatible if you don't know what /// you are doing, instead. -pub fn to_avro_datum>(schema: &Schema, value: T) -> Result, Error> { +pub fn to_avro_datum>(schema: &Schema, value: T) -> AvroResult> { let mut buffer = Vec::new(); write_avro_datum(schema, value, &mut buffer)?; Ok(buffer) diff --git a/tests/io.rs b/tests/io.rs index 159d8514599..8b75fedd503 100644 --- a/tests/io.rs +++ b/tests/io.rs @@ -1,9 +1,7 @@ //! Port of https://github.com/apache/avro/blob/release-1.9.1/lang/py/test/test_io.py use std::io::Cursor; -use avro_rs::{ - from_avro_datum, to_avro_datum, types::Value, Schema, SchemaResolutionError, ValidationError, -}; +use avro_rs::{from_avro_datum, to_avro_datum, types::Value, Error, Schema}; use lazy_static::lazy_static; lazy_static! { @@ -220,10 +218,8 @@ fn test_no_default_value() -> Result<(), String> { ); match decoded { Ok(_) => Err(String::from("Expected SchemaResolutionError, got Ok")), - Err(ref e) => match e.downcast_ref::() { - Some(_) => Ok(()), - None => Err(format!("Expected SchemaResolutionError, got {}", e)), - }, + Err(Error::SchemaResolution(_)) => Ok(()), + Err(ref e) => Err(format!("Expected SchemaResolutionError, got {}", e)), } } @@ -307,9 +303,7 @@ fn test_type_exception() -> Result<(), String> { let encoded = to_avro_datum(&writer_schema, datum_to_write); match encoded { Ok(_) => Err(String::from("Expected ValidationError, got Ok")), - Err(ref e) => match e.downcast_ref::() { - Some(_) => Ok(()), - None => Err(format!("Expected ValidationError, got {}", e)), - }, + Err(Error::Validation(_)) => Ok(()), + Err(ref e) => Err(format!("Expected ValidationError, got {}", e)), } } diff --git a/tests/schema.rs b/tests/schema.rs index f22ecc9ba04..95d59bae78c 100644 --- a/tests/schema.rs +++ b/tests/schema.rs @@ -1,5 +1,6 @@ //! Port of https://github.com/apache/avro/blob/release-1.9.1/lang/py/test/test_schema.py use avro_rs::schema::Name; +use avro_rs::Error; use avro_rs::Schema; use lazy_static::lazy_static; @@ -729,16 +730,17 @@ fn test_root_error_is_not_swallowed_on_parse_error() -> Result<(), String> { let raw_schema = r#"/not/a/real/file"#; let error = Schema::parse_str(raw_schema).unwrap_err(); - // TODO: (#82) this should be a ParseSchemaError wrapping the JSON error - match error.downcast::() { - Ok(e) => { - assert!( - e.to_string().contains("expected value at line 1 column 1"), - e.to_string() - ); - Ok(()) - } - Err(e) => Err(format!("Expected serde_json::error::Error, got {:?}", e)), + if let Error::JSON(e) = error { + assert!( + e.to_string().contains("expected value at line 1 column 1"), + e.to_string() + ); + Ok(()) + } else { + Err(format!( + "Expected serde_json::error::Error, got {:?}", + error + )) } }