Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: RpcError and RpcResult and TransportError and TransportResult #28

Merged
merged 4 commits into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions crates/json-rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@ exclude.workspace = true
alloy-primitives.workspace = true
serde.workspace = true
serde_json = { workspace = true, features = ["raw_value"] }
thiserror.workspace = true
12 changes: 12 additions & 0 deletions crates/json-rpc/src/common.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::fmt::Display;

use serde::{de::Visitor, Deserialize, Serialize};

/// A JSON-RPC 2.0 ID object. This may be a number, a string, or null.
Expand Down Expand Up @@ -31,6 +33,16 @@ pub enum Id {
None,
}

impl Display for Id {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Id::Number(n) => write!(f, "{}", n),
Id::String(s) => write!(f, "{}", s),
Id::None => write!(f, "null"),
}
}
}

impl Serialize for Id {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Expand Down
95 changes: 95 additions & 0 deletions crates/json-rpc/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
use serde_json::value::RawValue;

use crate::{ErrorPayload, RpcReturn};

/// An RPC error.
#[derive(thiserror::Error, Debug)]
pub enum RpcError<E, ErrResp = Box<RawValue>> {
/// Server returned an error response.
#[error("Server returned an error response: {0}")]
ErrorResp(ErrorPayload<ErrResp>),

/// JSON serialization error.
#[error("Serialization error: {0}")]
SerError(
/// The underlying serde_json error.
// To avoid accidentally confusing ser and deser errors, we do not use
// the `#[from]` tag.
#[source]
serde_json::Error,
),
/// JSON deserialization error.
#[error("Deserialization error: {err}")]
DeserError {
/// The underlying serde_json error.
// To avoid accidentally confusing ser and deser errors, we do not use
// the `#[from]` tag.
#[source]
err: serde_json::Error,
/// For deser errors, the text that failed to deserialize.
text: String,
},

/// Transport error.
///
/// This variant is used when the error occurs during communication.
#[error("Error during transport: {0}")]
Transport(
/// The underlying transport error.
#[from]
E,
),
}

impl<E, ErrResp> RpcError<E, ErrResp>
where
ErrResp: RpcReturn,
{
/// Instantiate a new `TransportError` from an error response.
pub const fn err_resp(err: ErrorPayload<ErrResp>) -> Self {
Self::ErrorResp(err)
}
}

impl<E, ErrResp> RpcError<E, ErrResp> {
/// Instantiate a new `TransportError` from a [`serde_json::Error`]. This
/// should be called when the error occurs during serialization.
pub const fn ser_err(err: serde_json::Error) -> Self {
Self::SerError(err)
}

/// Instantiate a new `TransportError` from a [`serde_json::Error`] and the
/// text. This should be called when the error occurs during
/// deserialization.
pub fn deser_err(err: serde_json::Error, text: impl AsRef<str>) -> Self {
Self::DeserError { err, text: text.as_ref().to_owned() }
}

/// Check if the error is a serialization error.
pub const fn is_ser_error(&self) -> bool {
matches!(self, Self::SerError(_))
}

/// Check if the error is a deserialization error.
pub const fn is_deser_error(&self) -> bool {
matches!(self, Self::DeserError { .. })
}

/// Check if the error is a transport error.
pub const fn is_transport_error(&self) -> bool {
matches!(self, Self::Transport(_))
}

/// Check if the error is an error response.
pub const fn is_error_resp(&self) -> bool {
matches!(self, Self::ErrorResp(_))
}

/// Fallible conversion to an error response.
pub const fn as_error_resp(&self) -> Option<&ErrorPayload<ErrResp>> {
match self {
Self::ErrorResp(err) => Some(err),
_ => None,
}
}
}
48 changes: 40 additions & 8 deletions crates/json-rpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,34 @@
//!
//! [`alloy-transports`]: https://docs.rs/alloy-transports/latest/alloy-transports
//!
//! ## Usage
//!
//! This crate models the JSON-RPC 2.0 protocol data-types. It is intended to
//! be used to build JSON-RPC clients or servers. Most users will not need to
//! import this crate.
//!
//! This crate provides the following low-level data types:
//!
//! - [`Request`] - A JSON-RPC request.
//! - [`Response`] - A JSON-RPC response.
//! - [`ErrorPayload`] - A JSON-RPC error response payload, including code and message.
//! - [`ResponsePayload`] - The payload of a JSON-RPC response, either a success payload, or an
//! [`ErrorPayload`].
//!
//! For client-side Rust ergonomics, we want to map responses to [`Result`]s.
//! To that end, we provide the following types:
//!
//! - [`RpcError`] - An error that can occur during JSON-RPC communication. This type aggregates
//! errors that are common to all transports, such as (de)serialization, error responses, and
//! includes a generic transport error.
//! - [`RpcResult`] - A result modeling an Rpc outcome as `Result<T,
//! RpcError<E>>`.
Comment on lines +34 to +35
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
//! - [`RpcResult`] - A result modeling an Rpc outcome as `Result<T,
//! RpcError<E>>`.
//! - [`RpcResult`] - A result modeling an Rpc outcome as `Result<T, RpcError<E>>`.

//!
//! We recommend that transport implementors use [`RpcResult`] as the return
//! type for their transport methods, parameterized by their transport error
//! type. This will allow them to return either a successful response or an
//! error.
//!
//! ## Note On (De)Serialization
//!
//! [`Request`], [`Response`], and similar types are generic over the
Expand All @@ -35,9 +63,8 @@
//!
//! In general, partially deserialized responses can be further deserialized.
//! E.g. an [`BorrowedRpcResult`] may have success responses deserialized
//! with [`RpcResult::deserialize_success::<U>`], which will transform it to an
//! [`RpcResult<U>`]. Or the caller may use [`RpcResult::try_success_as::<U>`]
//! to attempt to deserialize without transforming the [`RpcResult`].
//! with [`crate::try_deserialize_ok::<U>`], which will transform it to an
//! [`RpcResult<U>`].

#![doc(
html_logo_url = "https://raw.github.com/alloy-rs/core/main/assets/alloy.jpg",
Expand All @@ -55,7 +82,11 @@
#![deny(unused_must_use, rust_2018_idioms)]
#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]

use serde::{de::DeserializeOwned, Serialize};
mod common;
pub use common::Id;

mod error;
pub use error::RpcError;

mod notification;
pub use notification::{EthNotification, PubSubItem};
Expand All @@ -72,11 +103,12 @@ pub use response::{
ResponsePayload,
};

mod common;
pub use common::Id;

mod result;
pub use result::{BorrowedRpcResult, RpcResult};
pub use result::{
transform_response, transform_result, try_deserialize_ok, BorrowedRpcResult, RpcResult,
};

use serde::{de::DeserializeOwned, Serialize};

/// An object that can be used as a JSON-RPC parameter.
///
Expand Down
12 changes: 12 additions & 0 deletions crates/json-rpc/src/response/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ pub struct ErrorPayload<ErrData = Box<RawValue>> {
pub data: Option<ErrData>,
}

impl<ErrData> std::fmt::Display for ErrorPayload<ErrData> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"ErrorPayload code {}, message: \"{}\", contains payload: {}",
self.code,
self.message,
self.data.is_some()
)
}
}

/// A [`ErrorPayload`] that has been partially deserialized, borrowing its
/// contents from the deserializer. This is used primarily for intermediate
/// deserialization. Most users will not require it.
Expand Down
Loading