dothttp 0.10.0

dothttp is a text-based scriptable HTTP client. It is a fork for dot-http. It is a simple language that resembles the actual HTTP protocol but with additional features to make it practical for someone who builds and tests APIs.
Documentation
use std::{
    borrow::Cow,
    convert::{TryFrom, TryInto},
    str::FromStr,
};

use color_eyre::eyre::Context;
use http::Uri;
use reqwest::{header::HeaderMap, Client, RequestBuilder, Url};

use crate::{
    http::{ClientConfig, HttpClient, Method, Request, Response, Version},
    Result,
};

pub struct ReqwestHttpClient {
    client: Client,
}

impl Default for ReqwestHttpClient {
    fn default() -> Self {
        Self::create(ClientConfig::default())
    }
}

impl HttpClient for ReqwestHttpClient {
    fn create(config: ClientConfig) -> ReqwestHttpClient
    where
        Self: Sized,
    {
        let client = Client::builder()
            .danger_accept_invalid_certs(config.ssl_check)
            .build()
            .unwrap();

        ReqwestHttpClient { client }
    }

    async fn execute(&self, request: &Request) -> Result<Response> {
        let Request {
            method,
            target,
            headers,
            body,
        } = request;
        let mut request_builder = self
            .client
            .request(method.into(), get_request_target(target)?);
        request_builder = set_headers(headers, request_builder);
        if let Some(body) = body {
            request_builder = set_body(body, request_builder);
        }
        let response = request_builder.send().await?;

        map_reqwest_response(response).await
    }
}

fn get_request_target(target: &str) -> Result<Url> {
    let target = if target.starts_with("http://") || target.starts_with("https://") {
        Cow::Borrowed(target)
    } else {
        Cow::Owned(format!("http://{target}"))
    };

    let parsed = Uri::from_str(target.as_ref()).context("Invalid URI")?;

    let schema = parsed.scheme().map(|it| it.as_str()).unwrap_or("http");
    let authority = parsed
        .authority()
        .map(|it| it.as_str())
        .unwrap_or("0.0.0.0");
    let path = parsed.path_and_query().map(|it| it.as_str()).unwrap_or("/");

    let formatted = format!("{schema}://{authority}{path}");

    Ok(Url::from_str(&formatted).expect("to be correct"))
}

fn set_headers(
    headers: &[(String, String)],
    mut request_builder: RequestBuilder,
) -> RequestBuilder {
    for (key, value) in headers {
        request_builder = request_builder.header(key, value);
    }
    request_builder
}

impl From<&Method> for reqwest::Method {
    fn from(method: &Method) -> Self {
        match method {
            Method::Get => reqwest::Method::GET,
            Method::Post => reqwest::Method::POST,
            Method::Delete => reqwest::Method::DELETE,
            Method::Put => reqwest::Method::PUT,
            Method::Patch => reqwest::Method::PATCH,
            Method::Options => reqwest::Method::OPTIONS,
        }
    }
}

struct Headers(Vec<(String, String)>);

async fn map_reqwest_response(response: reqwest::Response) -> Result<Response> {
    let Headers(headers) = response.headers().try_into()?;
    Ok(Response {
        version: response.version().into(),
        status_code: response.status().as_u16(),
        status: response.status().to_string(),
        headers,
        body: match response.text().await? {
            body if !body.is_empty() => Some(body),
            _ => None,
        },
    })
}

impl TryFrom<&HeaderMap> for Headers {
    type Error = crate::Error;

    fn try_from(value: &HeaderMap) -> Result<Self> {
        let mut headers = vec![];
        for (header_name, header_value) in value.iter() {
            headers.push((header_name.to_string(), header_value.to_str()?.to_string()))
        }
        Ok(Headers(headers))
    }
}

impl From<reqwest::Version> for Version {
    fn from(value: reqwest::Version) -> Self {
        match value {
            reqwest::Version::HTTP_09 => Version::Http09,
            reqwest::Version::HTTP_10 => Version::Http10,
            reqwest::Version::HTTP_11 => Version::Http11,
            reqwest::Version::HTTP_2 => Version::Http2,
            reqwest::Version::HTTP_3 => Version::Http3,
            _ => unreachable!(),
        }
    }
}

fn set_body(body: &str, mut request_builder: RequestBuilder) -> RequestBuilder {
    let body = body.trim();
    request_builder = request_builder.body::<reqwest::Body>(body.to_string().into());
    request_builder
}