bitcoindevkit / bitcoindevkit/rust-esplora-client

Refactor library so that it can work with any HTTP client crate

Open
#97 19 comments 1 reaction 1 assignee Claimed by @oleonardolima View on GitHub
enhancement
Dominant language
Rust
Stars
56
Forks
75
Avg merge
19h 8m
Merged PRs (30d)
4

Description

## The Problem

Right now `rust-esplora-client` is designed to only work with HTTP client crates that are baked in (i.e. `minreq` for blocking, `reqwest` for async). However, these HTTP client crates may not be preferred by the caller. The caller may also wish to use a different version of the HTTP client crate.

## Proposed Solution

Represent each endpoint call as a distinct type. Introduce a trait that is to be implemented for each endpoint call type. This trait will contain the *template* of how to form the HTTP request and parse the HTTP response body and status code.

```rust
pub trait HttpRequest {
type Output: for<'a> serde::de::Deserialize<'a>;

fn request_url_path(&self) -> String;
fn request_method(&self) -> &str;
fn request_body(&self) -> Vec;
fn parse_response(status: i32, body: &[u8]) -> Result>;
}

pub enum ResponseError {
Json(serde_json::Error),
HttpStatus { status: i32, body: Vec },
Client(E),
}
```

Examples of endpoint types...

```rust
pub struct GetTx {
pub txid: Txid,
}

impl HttpRequest for GetTx {
type Output = Option;

fn request_url_path(&self) -> String {
format!("/tx/{}/raw", self.txid)
}

fn request_method(&self) -> &str {
"GET"
}

fn request_body(&self) -> Vec {
Vec::with_capacity(0)
}

fn parse_response(status: i32, body: &[u8]) -> Result> {
match status {
200 => Ok(serde_json::from_slice(body).map_err(ResponseError::Json)?),
404 => Ok(None),
error_status => Err(ResponseError::HttpStatus {
status: error_status,
body: body.to_vec(),
}),
}
}
}

pub struct PostTx {
pub tx: Transaction,
}

impl HttpRequest for PostTx {
type Output = ();

fn request_url_path(&self) -> String {
"/tx".to_string()
}

fn request_method(&self) -> &str {
"POST"
}

fn request_body(&self) -> Vec {
bitcoin::consensus::encode::serialize(&self.tx)
.to_lower_hex_string()
.as_bytes()
.to_vec()
}

fn parse_response(status: i32, body: &[u8]) -> Result> {
match status {
200 => Ok(()),
error_status => Err(ResponseError::HttpStatus {
status: error_status,
body: body.to_vec(),
}),
}
}
}
```

Example for calling any endpoint with `minreq`:

```rust
pub fn call_with_minreq(
url_base: &str,
request: R,
) -> Result> {
let req = match request.request_method() {
"GET" => minreq::get(format!("{}{}", url_base, request.request_url_path())),
"POST" => minreq::post(format!("{}{}", url_base, request.request_url_path())),
unhandled_request_method => {
panic!("unexpected request method: {}", unhandled_request_method)
}
}
.with_body(request.request_body());

let resp = req.send().map_err(ResponseError::Client)?;
R::parse_response(resp.status_code, resp.as_bytes())
}
```

Example for calling any endpoint with `reqwest`:

```rust
pub async fn call_with_reqwest(
client: Client,
url_base: &str,
request: R,
) -> Result> {
let method =
reqwest::Method::from_str(request.request_method()).expect("request method must be valid");
let resp = client
.request(
method,
&format!("{}{}", url_base, request.request_url_path()),
)
.body(request.request_body().to_vec())
.send()
.await
.map_err(ResponseError::Client)?;
let status = resp.status().as_u16() as i32;
let body = resp.bytes().await.map_err(ResponseError::Client)?.to_vec();
R::parse_response(status, &body)
}
```

## In Conclusion

As can be seen, the code needed for a HTTP client crate to work with the trait is minimal.

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.