Skip to content

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

Open
@evanlinjin

Description

@evanlinjin

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

enhancementNew feature or request

Type

No type

Projects

Status

Discussion

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions