Open
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.
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<u8>;
fn parse_response<E>(status: i32, body: &[u8]) -> Result<Self::Output, ResponseError<E>>;
}
pub enum ResponseError<E> {
Json(serde_json::Error),
HttpStatus { status: i32, body: Vec<u8> },
Client(E),
}
Examples of endpoint types...
pub struct GetTx {
pub txid: Txid,
}
impl HttpRequest for GetTx {
type Output = Option<Transaction>;
fn request_url_path(&self) -> String {
format!("/tx/{}/raw", self.txid)
}
fn request_method(&self) -> &str {
"GET"
}
fn request_body(&self) -> Vec<u8> {
Vec::with_capacity(0)
}
fn parse_response<E>(status: i32, body: &[u8]) -> Result<Self::Output, ResponseError<E>> {
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<u8> {
bitcoin::consensus::encode::serialize(&self.tx)
.to_lower_hex_string()
.as_bytes()
.to_vec()
}
fn parse_response<E>(status: i32, body: &[u8]) -> Result<Self::Output, ResponseError<E>> {
match status {
200 => Ok(()),
error_status => Err(ResponseError::HttpStatus {
status: error_status,
body: body.to_vec(),
}),
}
}
}
Example for calling any endpoint with minreq
:
pub fn call_with_minreq<R: HttpRequest>(
url_base: &str,
request: R,
) -> Result<R::Output, ResponseError<minreq::Error>> {
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
:
pub async fn call_with_reqwest<R: HttpRequest>(
client: Client,
url_base: &str,
request: R,
) -> Result<R::Output, ResponseError<reqwest::Error>> {
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.
Metadata
Metadata
Assignees
Labels
Type
Projects
Status
Discussion