Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
72901b9
feat: serialize json request body
gregorydemay Feb 28, 2025
e989118
refactor: move to inside http
gregorydemay Feb 28, 2025
ee9b6bc
refactor: json feature activate http
gregorydemay Feb 28, 2025
5749ade
refactor: use client with JSON RPC request
gregorydemay Feb 28, 2025
a81555b
refactor: use client with JSON RPC response
gregorydemay Feb 28, 2025
1f60ae6
fix: tests (start)
gregorydemay Feb 28, 2025
3e3b396
clippy, formatting
gregorydemay Mar 3, 2025
1fecc50
filter response
gregorydemay Mar 4, 2025
ff500a1
Use HTTP conversion as filter response
gregorydemay Mar 4, 2025
fce09ef
rename filter to convert
gregorydemay Mar 4, 2025
839b9c4
convert request service
gregorydemay Mar 4, 2025
c46b457
rename generic parameters
gregorydemay Mar 4, 2025
0430d63
use converter for JSON RPC request
gregorydemay Mar 4, 2025
74f2f88
use converter for JSON RPC response
gregorydemay Mar 4, 2025
d63226d
use converter for HTTP request
gregorydemay Mar 4, 2025
dded3a4
use converter for HTTP response
gregorydemay Mar 4, 2025
3fef789
clean-up canhttp
gregorydemay Mar 4, 2025
c54ee98
use explicit error
gregorydemay Mar 4, 2025
afb732e
remove filter feature
gregorydemay Mar 4, 2025
911a4f1
Filter HTTP response
gregorydemay Mar 4, 2025
10d5676
Use response filter
gregorydemay Mar 4, 2025
ea3b788
fix json comparison
gregorydemay Mar 4, 2025
258ddbe
simplify call
gregorydemay Mar 4, 2025
955170f
added TODOs
gregorydemay Mar 4, 2025
3ba8602
fix docs
gregorydemay Mar 4, 2025
34ab9df
lint
gregorydemay Mar 4, 2025
c75550e
split convert module between request/response
gregorydemay Mar 4, 2025
bb8b111
Merge branch 'main' into gdemay/XC-287-json-rpc-layer
gregorydemay Mar 4, 2025
c608a04
doc for convert module
gregorydemay Mar 6, 2025
1cd3403
doc for http module
gregorydemay Mar 6, 2025
708c9fc
make JsonRequestConverter generic
gregorydemay Mar 6, 2025
98fdb2e
docs for JSON module
gregorydemay Mar 6, 2025
5bac12c
docs for client
gregorydemay Mar 6, 2025
55db8d9
use `all-features` flag
gregorydemay Mar 6, 2025
bb68f2c
linting
gregorydemay Mar 6, 2025
1a4e772
unit tests for JSON RPC layer
gregorydemay Mar 6, 2025
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:

- name: Cargo doc
run: |
cargo doc --workspace --no-deps
cargo doc --workspace --no-deps --all-features
env:
RUSTDOCFLAGS: "--deny warnings"

Expand Down Expand Up @@ -94,7 +94,7 @@ jobs:
- uses: Swatinem/rust-cache@v2

- name: Cargo test
run: cargo test --workspace -- --test-threads=2 --nocapture
run: cargo test --workspace --all-features -- --test-threads=2 --nocapture

e2e:
runs-on: ubuntu-latest
Expand Down
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 7 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ inherits = "release"

[dependencies]
candid = { workspace = true }
canhttp = { path = "canhttp" }
canhttp = { path = "canhttp", features = ["json"] }
ethnum = { workspace = true }
evm_rpc_types = { path = "evm_rpc_types" }
futures = { workspace = true }
Expand All @@ -36,9 +36,10 @@ minicbor = { workspace = true }
serde = { workspace = true }
serde_bytes = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
thousands = "0.2"
tower = {workspace = true}
tower-http = {workspace = true, features = ["set-header", "util"]}
tower = { workspace = true }
tower-http = { workspace = true, features = ["set-header", "util"] }
url = { workspace = true }
hex = "0.4"
ethers-core = "2.0"
Expand All @@ -59,9 +60,10 @@ rand = "0.8"
[workspace.dependencies]
assert_matches = "1.5.0"
candid = { version = "0.10.13" }
candid_parser = {version = "0.1.4"}
candid_parser = { version = "0.1.4" }
ethnum = { version = "1.5.0", features = ["serde"] }
futures = "0.3.31"
futures-util = "0.3.31"
getrandom = { version = "0.2", features = ["custom"] }
hex = "0.4.3"
http = "1.2.0"
Expand Down Expand Up @@ -90,4 +92,4 @@ thiserror = "2.0.11"
url = "2.5"

[workspace]
members = [ "canhttp", "e2e/rust", "evm_rpc_types"]
members = ["canhttp", "e2e/rust", "evm_rpc_types"]
8 changes: 6 additions & 2 deletions canhttp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ documentation = "https://docs.rs/canhttp"

[dependencies]
ic-cdk = { workspace = true }
futures-util = { workspace = true }
http = { workspace = true, optional = true }
num-traits = { workspace = true, optional = true }
pin-project = { workspace = true }
tower = { workspace = true, features = ["filter", "retry"] }
serde = { workspace = true, optional = true }
serde_json = { workspace = true, optional = true }
tower = { workspace = true, features = ["retry"] }
tower-layer = { workspace = true, optional = true }
thiserror = { workspace = true }

Expand All @@ -26,4 +29,5 @@ tokio = { workspace = true, features = ["full"] }

[features]
default = ["http"]
http = ["dep:http", "dep:num-traits", "dep:tower-layer"]
http = ["dep:http", "dep:num-traits", "dep:tower-layer"]
json = ["http", "dep:serde", "dep:serde_json"]
27 changes: 23 additions & 4 deletions canhttp/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use thiserror::Error;
use tower::{BoxError, Service};
use tower::{BoxError, Service, ServiceBuilder};

/// Thin wrapper around [`ic_cdk::api::management_canister::http_request::http_request`]
/// that implements the [`tower::Service`] trait. Its functionality can be extended by composing so-called
Expand All @@ -19,6 +19,22 @@ use tower::{BoxError, Service};
#[derive(Clone, Debug)]
pub struct Client;

impl Client {
/// Create a new client returning custom errors.
pub fn new_with_error<CustomError: From<IcError>>(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The idea is that the service uses the CustomError type, so that any additional layer that may produce errors need to be mapped to the service error type. This forces the user of the library to handle errors explicitly.

) -> impl Service<IcHttpRequestWithCycles, Response = IcHttpResponse, Error = CustomError> {
ServiceBuilder::new()
.map_err(CustomError::from)
.service(Client)
}

/// Creates a new client where error type is erased.
pub fn new_with_box_error(
) -> impl Service<IcHttpRequestWithCycles, Response = IcHttpResponse, Error = BoxError> {
Self::new_with_error::<BoxError>()
}
}

/// Error returned by the Internet Computer when making an HTTPs outcall.
#[derive(Error, Clone, Debug, PartialEq, Eq)]
#[error("Error from ICP: (code {code:?}, message {message})")]
Expand All @@ -42,7 +58,7 @@ impl IcError {

impl Service<IcHttpRequestWithCycles> for Client {
type Response = IcHttpResponse;
type Error = BoxError;
type Error = IcError;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;

fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Expand All @@ -58,14 +74,17 @@ impl Service<IcHttpRequestWithCycles> for Client {
.await
{
Ok((response,)) => Ok(response),
Err((code, message)) => Err(BoxError::from(IcError { code, message })),
Err((code, message)) => Err(IcError { code, message }),
}
})
}
}

#[derive(Clone, Debug, PartialEq, Eq)]
/// [`IcHttpRequest`] specifying how many cycles should be attached for the HTTPs outcall.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct IcHttpRequestWithCycles {
/// Request to be made.
pub request: IcHttpRequest,
/// Number of cycles to attach.
pub cycles: u128,
}
148 changes: 148 additions & 0 deletions canhttp/src/convert/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
//! Fallible conversion from one type to another that can be used as a tower middleware.
//!
//! # Examples
//!
//! ## To convert requests
//!
//! A converter can be used to convert request types:
//! * If the result of the conversion is [`Ok`], the converted type will be forwarded to the inner service.
//! * If the result of the conversion is [`Err`], the error will be returned and the inner service will *not* be called.
//!
//! When used to convert requests (with [`ConvertRequestLayer`], the functionality offered by [`Convert`] is similar to that of
//! [`Predicate`](https://docs.rs/tower/0.5.2/tower/filter/trait.Predicate.html) in that it can act as a *filter*. The main difference is that the error does not need to be boxed.
//!
//! ```rust
//! use std::convert::Infallible;
//! use canhttp::convert::{Convert, ConvertServiceBuilder};
//! use tower::{ServiceBuilder, Service, ServiceExt};
//!
//! async fn bare_bone_service(request: Vec<u8>) -> Result<Vec<u8>, Infallible> {
//! Ok(request)
//! }
//!
//! struct UsefulRequest(Vec<u8>);
//!
//! #[derive(Clone)]
//! struct UsefulRequestConverter;
//!
//! impl Convert<UsefulRequest> for UsefulRequestConverter {
//! type Output = Vec<u8>;
//! type Error = Infallible;
//!
//! fn try_convert(&mut self, input: UsefulRequest) -> Result<Self::Output, Self::Error> {
//! Ok(input.0)
//! }
//! }
//!
//! # #[tokio::main]
//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let mut service = ServiceBuilder::new()
//! .convert_request(UsefulRequestConverter)
//! .service_fn(bare_bone_service);
//!
//! let request = UsefulRequest(vec![42]);
//!
//! let response = service
//! .ready()
//! .await?
//! .call(request)
//! .await?;
//!
//! assert_eq!(response, vec![42_u8]);
//! # Ok(())
//! # }
//! ```
//!
//! ## To convert responses
//!
//! A converter can be used to convert response types:
//! ```rust
//! use std::convert::Infallible;
//! use canhttp::convert::{Convert, ConvertServiceBuilder};
//! use tower::{ServiceBuilder, Service, ServiceExt};
//!
//! async fn bare_bone_service(request: Vec<u8>) -> Result<Vec<u8>, Infallible> {
//! Ok(request)
//! }
//!
//! #[derive(Debug, PartialEq)]
//! struct UsefulResponse(Vec<u8>);
//!
//! #[derive(Clone)]
//! struct UsefulResponseConverter;
//!
//! impl Convert<Vec<u8>> for UsefulResponseConverter {
//! type Output = UsefulResponse;
//! type Error = Infallible;
//!
//! fn try_convert(&mut self, input: Vec<u8>) -> Result<Self::Output, Self::Error> {
//! Ok(UsefulResponse(input))
//! }
//! }
//!
//! # #[tokio::main]
//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let mut service = ServiceBuilder::new()
//! .convert_response(UsefulResponseConverter)
//! .service_fn(bare_bone_service);
//!
//! let request = vec![42];
//!
//! let response = service
//! .ready()
//! .await?
//! .call(request)
//! .await?;
//!
//! assert_eq!(response, UsefulResponse(vec![42_u8]));
//! # Ok(())
//! # }
//! ```

pub use request::{ConvertRequest, ConvertRequestLayer};
pub use response::{ConvertResponse, ConvertResponseLayer};

mod request;
mod response;

use tower::ServiceBuilder;
use tower_layer::Stack;

/// Fallible conversion from one type to another.
pub trait Convert<Input> {
/// Converted type if the conversion succeeds.
type Output;
/// Error type if the conversion fails
type Error;

/// Try to convert an instance of the input type to the output type.
/// The conversion may fail, in which case an error is returned.
fn try_convert(&mut self, response: Input) -> Result<Self::Output, Self::Error>;
}

/// Extension trait that adds methods to [`tower::ServiceBuilder`] for adding middleware
/// based on fallible conversion between types.
pub trait ConvertServiceBuilder<L> {
/// Convert the request type.
///
/// See the [module docs](crate::convert) for examples.
fn convert_request<C>(self, f: C) -> ServiceBuilder<Stack<ConvertRequestLayer<C>, L>>;

/// Convert the response type.
///
/// See the [module docs](crate::convert) for examples.
fn convert_response<C>(self, f: C) -> ServiceBuilder<Stack<ConvertResponseLayer<C>, L>>;
}

impl<L> ConvertServiceBuilder<L> for ServiceBuilder<L> {
fn convert_request<C>(self, converter: C) -> ServiceBuilder<Stack<ConvertRequestLayer<C>, L>> {
self.layer(ConvertRequestLayer::new(converter))
}

fn convert_response<C>(
self,
converter: C,
) -> ServiceBuilder<Stack<ConvertResponseLayer<C>, L>> {
self.layer(ConvertResponseLayer::new(converter))
}
}
63 changes: 63 additions & 0 deletions canhttp/src/convert/request.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
use crate::convert::Convert;
use futures_util::future;
use std::task::{Context, Poll};
use tower::Service;
use tower_layer::Layer;

/// Convert request of a service into another type, where the conversion may fail.
///
/// This [`Layer`] produces instances of the [`ConvertRequest`] service.
///
/// [`Layer`]: tower::Layer
#[derive(Debug, Clone)]
pub struct ConvertRequestLayer<C> {
converter: C,
}

impl<C> ConvertRequestLayer<C> {
/// Returns a new [`ConvertRequestLayer`]
pub fn new(converter: C) -> Self {
Self { converter }
}
}

/// Convert requests into another type and forward the converted type to the inner service
/// *only if* the conversion was successful.
#[derive(Debug, Clone)]
pub struct ConvertRequest<S, C> {
inner: S,
converter: C,
}

impl<S, C: Clone> Layer<S> for ConvertRequestLayer<C> {
type Service = ConvertRequest<S, C>;

fn layer(&self, inner: S) -> Self::Service {
Self::Service {
inner,
converter: self.converter.clone(),
}
}
}

impl<S, Converter, Request, NewRequest, Error> Service<NewRequest> for ConvertRequest<S, Converter>
where
Converter: Convert<NewRequest, Output = Request>,
S: Service<Request, Error = Error>,
Converter::Error: Into<Error>,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

We cannot use tower::filter::Predicate because the use of BoxError is hard-coded.

{
type Response = S::Response;
type Error = S::Error;
type Future = future::Either<S::Future, future::Ready<Result<S::Response, S::Error>>>;

fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}

fn call(&mut self, new_req: NewRequest) -> Self::Future {
match self.converter.try_convert(new_req) {
Ok(request) => future::Either::Left(self.inner.call(request)),
Err(err) => future::Either::Right(future::ready(Err(err.into()))),
}
}
}
Loading
Loading