This is an automated email from the ASF dual-hosted git repository.
fokko pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/iceberg-rust.git
The following commit(s) were added to refs/heads/main by this push:
new 3604a8f feat: Implement error handling (#13)
3604a8f is described below
commit 3604a8f4ec80759f61ec1fa5b26164b811fdde9f
Author: Xuanwo <[email protected]>
AuthorDate: Fri Jul 28 19:37:12 2023 +0800
feat: Implement error handling (#13)
* feat: Implement error handling
Signed-off-by: Xuanwo <[email protected]>
* format code
Signed-off-by: Xuanwo <[email protected]>
---------
Signed-off-by: Xuanwo <[email protected]>
---
Cargo.toml | 5 +-
src/error.rs | 325 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--
src/lib.rs | 9 +-
src/spec/mod.rs | 2 +
4 files changed, 329 insertions(+), 12 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
index 8cdf28e..496cba4 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -32,4 +32,7 @@ serde = "^1.0"
serde_bytes = "0.11.8"
serde_json = "^1.0"
serde_derive = "^1.0"
-thiserror = "1.0.44"
+anyhow = "1.0.72"
+
+[dev-dependencies]
+pretty_assertions = "1.4.0"
diff --git a/src/error.rs b/src/error.rs
index fbaa6fd..87549a9 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -15,14 +15,319 @@
// specific language governing permissions and limitations
// under the License.
-use thiserror::Error;
-
-#[derive(Error, Debug)]
-pub enum IcebergError {
- #[error("The type `{0}` cannot be stored as bytes.")]
- ValueByteConversion(String),
- #[error("Failed to convert slice to array")]
- TryFromSlice(#[from] std::array::TryFromSliceError),
- #[error("Failed to convert u8 to string")]
- Utf8(#[from] std::str::Utf8Error),
+use std::backtrace::{Backtrace, BacktraceStatus};
+use std::fmt;
+use std::fmt::Debug;
+use std::fmt::Display;
+use std::fmt::Formatter;
+
+/// Result that is a wrapper of `Result<T, iceberg::Error>`
+pub type Result<T> = std::result::Result<T, Error>;
+
+/// ErrorKind is all kinds of Error of iceberg.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+#[non_exhaustive]
+pub enum ErrorKind {
+ /// Iceberg don't know what happened here, and no actions other than
+ /// just returning it back. For example, iceberg returns an internal
+ /// service error.
+ Unexpected,
+
+ /// Iceberg data is invalid.
+ ///
+ /// This error is returned when we try to read a table from iceberg but
+ /// failed to parse it's metadata or data file correctly.
+ ///
+ /// The table could be invalid or corrupted.
+ DataInvalid,
+ /// Iceberg feature is not supported.
+ ///
+ /// This error is returned when given iceberg feature is not supported.
+ FeatureUnsupported,
+}
+
+impl ErrorKind {
+ /// Convert self into static str.
+ pub fn into_static(self) -> &'static str {
+ self.into()
+ }
+}
+
+impl From<ErrorKind> for &'static str {
+ fn from(v: ErrorKind) -> &'static str {
+ match v {
+ ErrorKind::Unexpected => "Unexpected",
+ ErrorKind::DataInvalid => "DataInvalid",
+ ErrorKind::FeatureUnsupported => "FeatureUnsupported",
+ }
+ }
+}
+
+impl Display for ErrorKind {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ write!(f, "{}", self.into_static())
+ }
+}
+
+/// Error is the error struct returned by all iceberg functions.
+///
+/// ## Display
+///
+/// Error can be displayed in two ways:
+///
+/// - Via `Display`: like `err.to_string()` or `format!("{err}")`
+///
+/// Error will be printed in a single line:
+///
+/// ```shell
+/// Unexpected, context: { path: /path/to/file, called: send_async } =>
something wrong happened, source: networking error"
+/// ```
+///
+/// - Via `Debug`: like `format!("{err:?}")`
+///
+/// Error will be printed in multi lines with more details and backtraces (if
captured):
+///
+/// ```shell
+/// Unexpected => something wrong happened
+///
+/// Context:
+/// path: /path/to/file
+/// called: send_async
+///
+/// Source: networking error
+///
+/// Backtrace:
+/// 0: iceberg::error::Error::new
+/// at ./src/error.rs:197:24
+/// 1: iceberg::error::tests::generate_error
+/// at ./src/error.rs:241:9
+/// 2: iceberg::error::tests::test_error_debug_with_backtrace::{{closure}}
+/// at ./src/error.rs:305:41
+/// ...
+/// ```
+pub struct Error {
+ kind: ErrorKind,
+ message: String,
+
+ context: Vec<(&'static str, String)>,
+
+ source: Option<anyhow::Error>,
+ backtrace: Backtrace,
+}
+
+impl Display for Error {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ write!(f, "{}", self.kind)?;
+
+ if !self.context.is_empty() {
+ write!(f, ", context: {{ ")?;
+ write!(
+ f,
+ "{}",
+ self.context
+ .iter()
+ .map(|(k, v)| format!("{k}: {v}"))
+ .collect::<Vec<_>>()
+ .join(", ")
+ )?;
+ write!(f, " }}")?;
+ }
+
+ if !self.message.is_empty() {
+ write!(f, " => {}", self.message)?;
+ }
+
+ if let Some(source) = &self.source {
+ write!(f, ", source: {source}")?;
+ }
+
+ Ok(())
+ }
+}
+
+impl Debug for Error {
+ fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+ // If alternate has been specified, we will print like Debug.
+ if f.alternate() {
+ let mut de = f.debug_struct("Error");
+ de.field("kind", &self.kind);
+ de.field("message", &self.message);
+ de.field("context", &self.context);
+ de.field("source", &self.source);
+ de.field("backtrace", &self.backtrace);
+ return de.finish();
+ }
+
+ write!(f, "{}", self.kind)?;
+ if !self.message.is_empty() {
+ write!(f, " => {}", self.message)?;
+ }
+ writeln!(f)?;
+
+ if !self.context.is_empty() {
+ writeln!(f)?;
+ writeln!(f, "Context:")?;
+ for (k, v) in self.context.iter() {
+ writeln!(f, " {k}: {v}")?;
+ }
+ }
+ if let Some(source) = &self.source {
+ writeln!(f)?;
+ writeln!(f, "Source: {source:#}")?;
+ }
+
+ if self.backtrace.status() == BacktraceStatus::Captured {
+ writeln!(f)?;
+ writeln!(f, "Backtrace:")?;
+ writeln!(f, "{}", self.backtrace)?;
+ }
+
+ Ok(())
+ }
+}
+
+impl std::error::Error for Error {
+ fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+ self.source.as_ref().map(|v| v.as_ref())
+ }
+}
+
+impl Error {
+ /// Create a new Error with error kind and message.
+ pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
+ Self {
+ kind,
+ message: message.into(),
+ context: Vec::default(),
+
+ source: None,
+ // `Backtrace::capture()` will check if backtrace has been enabled
+ // internally. It's zero cost if backtrace is disabled.
+ backtrace: Backtrace::capture(),
+ }
+ }
+
+ /// Add more context in error.
+ pub fn with_context(mut self, key: &'static str, value: impl Into<String>)
-> Self {
+ self.context.push((key, value.into()));
+ self
+ }
+
+ /// Set source for error.
+ ///
+ /// # Notes
+ ///
+ /// If the source has been set, we will raise a panic here.
+ pub fn with_source(mut self, src: impl Into<anyhow::Error>) -> Self {
+ debug_assert!(self.source.is_none(), "the source error has been set");
+
+ self.source = Some(src.into());
+ self
+ }
+
+ /// Set the backtrace for error.
+ ///
+ /// This function is served as testing purpose and not intended to be
called
+ /// by users.
+ #[cfg(test)]
+ fn with_backtrace(mut self, backtrace: Backtrace) -> Self {
+ self.backtrace = backtrace;
+ self
+ }
+
+ /// Return error's kind.
+ ///
+ /// Users can use this method to check error's kind and take actions.
+ pub fn kind(&self) -> ErrorKind {
+ self.kind
+ }
+}
+
+impl From<std::str::Utf8Error> for Error {
+ fn from(v: std::str::Utf8Error) -> Self {
+ Self::new(ErrorKind::Unexpected, "handling invalid utf-8
characters").with_source(v)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use anyhow::anyhow;
+ use pretty_assertions::assert_eq;
+
+ use super::*;
+
+ fn generate_error_with_backtrace_disabled() -> Error {
+ Error::new(
+ ErrorKind::Unexpected,
+ "something wrong happened".to_string(),
+ )
+ .with_context("path", "/path/to/file".to_string())
+ .with_context("called", "send_async".to_string())
+ .with_source(anyhow!("networking error"))
+ .with_backtrace(Backtrace::disabled())
+ }
+
+ fn generate_error_with_backtrace_enabled() -> Error {
+ Error::new(
+ ErrorKind::Unexpected,
+ "something wrong happened".to_string(),
+ )
+ .with_context("path", "/path/to/file".to_string())
+ .with_context("called", "send_async".to_string())
+ .with_source(anyhow!("networking error"))
+ .with_backtrace(Backtrace::force_capture())
+ }
+
+ #[test]
+ fn test_error_display_without_backtrace() {
+ let s = format!("{}", generate_error_with_backtrace_disabled());
+ assert_eq!(
+ s,
+ r#"Unexpected, context: { path: /path/to/file, called: send_async
} => something wrong happened, source: networking error"#
+ )
+ }
+
+ #[test]
+ fn test_error_display_with_backtrace() {
+ let s = format!("{}", generate_error_with_backtrace_enabled());
+ assert_eq!(
+ s,
+ r#"Unexpected, context: { path: /path/to/file, called: send_async
} => something wrong happened, source: networking error"#
+ )
+ }
+
+ #[test]
+ fn test_error_debug_without_backtrace() {
+ let s = format!("{:?}", generate_error_with_backtrace_disabled());
+ assert_eq!(
+ s,
+ r#"Unexpected => something wrong happened
+
+Context:
+ path: /path/to/file
+ called: send_async
+
+Source: networking error
+"#
+ )
+ }
+
+ /// Backtrace contains build information, so we just assert the header of
error content.
+ #[test]
+ fn test_error_debug_with_backtrace() {
+ let s = format!("{:?}", generate_error_with_backtrace_enabled());
+
+ let expected = r#"Unexpected => something wrong happened
+
+Context:
+ path: /path/to/file
+ called: send_async
+
+Source: networking error
+
+Backtrace:
+ 0: iceberg::error::tests::generate_error_with_backtrace_enabled
+"#;
+ assert_eq!(&s[..expected.len()], expected,)
+ }
}
diff --git a/src/lib.rs b/src/lib.rs
index 99a35ac..fb9db5b 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -16,5 +16,12 @@
// under the License.
//! Native Rust implementation of Apache Iceberg
-pub mod error;
+
+#![deny(missing_docs)]
+
+mod error;
+pub use error::Error;
+pub use error::ErrorKind;
+pub use error::Result;
+
pub mod spec;
diff --git a/src/spec/mod.rs b/src/spec/mod.rs
index ff69ae2..bae9e2a 100644
--- a/src/spec/mod.rs
+++ b/src/spec/mod.rs
@@ -15,4 +15,6 @@
// specific language governing permissions and limitations
// under the License.
+//! Spec for Iceberg.
+
pub mod datatypes;