bitcoindevkit / bitcoindevkit/rust-esplora-client
Refactor library so that it can work with any HTTP client crate
- 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.