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;

Reply via email to