Skip to content

Commit

Permalink
sui-graphql-client: rework errors (#58)
Browse files Browse the repository at this point in the history
Implement a custom error type instead of relying on `anyhow` for the graphql client.
  • Loading branch information
stefan-mysten authored Nov 14, 2024
1 parent 98ecfa1 commit ff5ccfe
Show file tree
Hide file tree
Showing 9 changed files with 402 additions and 249 deletions.
3 changes: 2 additions & 1 deletion crates/sui-graphql-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ description = "Sui GraphQL RPC Client for the Sui Blockchain"
anyhow = "1.0.71"
async-stream = "0.3.3"
async-trait = "0.1.61"
base64ct = { version = "1.6.0", features = ["alloc"] }
base64ct = { version = "1.6.0", features = ["alloc", "std"] }
bcs = "0.1.4"
chrono = "0.4.26"
cynic = "3.7.3"
Expand All @@ -23,6 +23,7 @@ serde_json = {version = "1.0.95"}
sui-types = { package = "sui-sdk-types", path = "../sui-sdk-types", features = ["serde"] }
tracing = "0.1.37"
tokio = "1.36.0"
url = "2.5.3"

[dev-dependencies]
sui-types = { package = "sui-sdk-types", path = "../sui-sdk-types", features = ["serde", "rand", "hash"] }
Expand Down
179 changes: 179 additions & 0 deletions crates/sui-graphql-client/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
// Copyright (c) Mysten Labs, Inc.
// SPDX-License-Identifier: Apache-2.0

use std::num::ParseIntError;
use std::num::TryFromIntError;

use cynic::GraphQlError;

use sui_types::types::AddressParseError;
use sui_types::types::DigestParseError;
use sui_types::types::TypeParseError;

type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;

pub type Result<T, E = Error> = std::result::Result<T, E>;

/// General error type for the client. It is used to wrap all the possible errors that can occur.
#[derive(Debug)]
pub struct Error {
inner: Box<InnerError>,
}

/// Error type for the client. It is split into multiple fields to allow for more granular error
/// handling. The `source` field is used to store the original error.
#[derive(Debug)]
struct InnerError {
/// Error kind.
kind: Kind,
/// Errors returned by the GraphQL server.
query_errors: Option<Vec<GraphQlError>>,
/// The original error.
source: Option<BoxError>,
}

#[derive(Debug)]
#[non_exhaustive]
pub enum Kind {
Deserialization,
Parse,
Query,
Other,
}

impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.inner.source.as_deref().map(|e| e as _)
}
}

impl Error {
// Public accessors

/// Returns the kind of error.
pub fn kind(&self) -> &Kind {
&self.inner.kind
}

/// Original GraphQL query errors.
pub fn graphql_errors(&self) -> Option<&[GraphQlError]> {
self.inner.query_errors.as_deref()
}

// Private constructors

/// Convert the given error into a generic error.
pub(crate) fn from_error<E: Into<BoxError>>(kind: Kind, error: E) -> Self {
Self {
inner: Box::new(InnerError {
kind,
source: Some(error.into()),
query_errors: None,
}),
}
}

/// Special constructor for queries that expect to return data but it's none.
pub(crate) fn empty_response_error() -> Self {
Self {
inner: Box::new(InnerError {
kind: Kind::Query,
source: Some("Expected a non-empty response data from query".into()),
query_errors: None,
}),
}
}

/// Create a Query kind of error with the original graphql errors.
pub(crate) fn graphql_error(errors: Vec<GraphQlError>) -> Self {
Self {
inner: Box::new(InnerError {
kind: Kind::Query,
source: None,
query_errors: Some(errors),
}),
}
}
}

impl std::fmt::Display for Kind {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Kind::Deserialization => write!(f, "Deserialization error:"),
Kind::Parse => write!(f, "Parse error:"),
Kind::Query => write!(f, "Query error:"),
Kind::Other => write!(f, "Error:"),
}
}
}

impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.inner.kind)?;

if let Some(source) = &self.inner.source {
writeln!(f, " {}", source)?;
}
Ok(())
}
}

impl From<bcs::Error> for Error {
fn from(error: bcs::Error) -> Self {
Self::from_error(Kind::Deserialization, error)
}
}

impl From<reqwest::Error> for Error {
fn from(error: reqwest::Error) -> Self {
Self::from_error(Kind::Other, error)
}
}

impl From<url::ParseError> for Error {
fn from(error: url::ParseError) -> Self {
Self::from_error(Kind::Parse, error)
}
}

impl From<ParseIntError> for Error {
fn from(error: ParseIntError) -> Self {
Self::from_error(Kind::Parse, error)
}
}

impl From<AddressParseError> for Error {
fn from(error: AddressParseError) -> Self {
Self::from_error(Kind::Parse, error)
}
}

impl From<base64ct::Error> for Error {
fn from(error: base64ct::Error) -> Self {
Self::from_error(Kind::Parse, error)
}
}

impl From<chrono::ParseError> for Error {
fn from(error: chrono::ParseError) -> Self {
Self::from_error(Kind::Parse, error)
}
}

impl From<DigestParseError> for Error {
fn from(error: DigestParseError) -> Self {
Self::from_error(Kind::Parse, error)
}
}

impl From<TryFromIntError> for Error {
fn from(error: TryFromIntError) -> Self {
Self::from_error(Kind::Parse, error)
}
}

impl From<TypeParseError> for Error {
fn from(error: TypeParseError) -> Self {
Self::from_error(Kind::Parse, error)
}
}
Loading

0 comments on commit ff5ccfe

Please sign in to comment.