xs-http 0.1.0

Instant HTTP API testing suite - type-safe alternative to Postman/curl
Documentation
use crate::error::Result;
use crate::response::Response;
use reqwest::blocking::Client;
use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
use serde::Serialize;
use std::collections::HashMap;
use std::time::Duration;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Method {
    Get,
    Post,
    Put,
    Delete,
    Patch,
    Head,
    Options,
}

impl Method {
    fn as_reqwest_method(&self) -> reqwest::Method {
        match self {
            Method::Get => reqwest::Method::GET,
            Method::Post => reqwest::Method::POST,
            Method::Put => reqwest::Method::PUT,
            Method::Delete => reqwest::Method::DELETE,
            Method::Patch => reqwest::Method::PATCH,
            Method::Head => reqwest::Method::HEAD,
            Method::Options => reqwest::Method::OPTIONS,
        }
    }
}

#[derive(Debug)]
pub struct Request {
    method: Method,
    url: String,
    headers: HeaderMap,
    body: Option<Vec<u8>>,
    query_params: HashMap<String, String>,
    timeout: Option<Duration>,
    follow_redirects: bool,
}

impl Request {
    pub fn new(method: Method, url: impl Into<String>) -> Self {
        Self {
            method,
            url: url.into(),
            headers: HeaderMap::new(),
            body: None,
            query_params: HashMap::new(),
            timeout: Some(Duration::from_secs(30)),
            follow_redirects: true,
        }
    }

    pub fn get(url: impl Into<String>) -> Self {
        Self::new(Method::Get, url)
    }

    pub fn post(url: impl Into<String>) -> Self {
        Self::new(Method::Post, url)
    }

    pub fn put(url: impl Into<String>) -> Self {
        Self::new(Method::Put, url)
    }

    pub fn delete(url: impl Into<String>) -> Self {
        Self::new(Method::Delete, url)
    }

    pub fn patch(url: impl Into<String>) -> Self {
        Self::new(Method::Patch, url)
    }

    pub fn head(url: impl Into<String>) -> Self {
        Self::new(Method::Head, url)
    }

    pub fn options(url: impl Into<String>) -> Self {
        Self::new(Method::Options, url)
    }

    pub fn header(mut self, key: impl AsRef<str>, value: impl AsRef<str>) -> Self {
        if let (Ok(name), Ok(val)) = (
            HeaderName::try_from(key.as_ref()),
            HeaderValue::try_from(value.as_ref()),
        ) {
            self.headers.insert(name, val);
        }
        self
    }

    pub fn headers(mut self, headers: Vec<(impl AsRef<str>, impl AsRef<str>)>) -> Self {
        for (key, value) in headers {
            self = self.header(key, value);
        }
        self
    }

    pub fn json<T: Serialize>(mut self, body: &T) -> Result<Self> {
        let json_string = serde_json::to_string(body)?;
        self.body = Some(json_string.into_bytes());
        self = self.header("Content-Type", "application/json");
        Ok(self)
    }

    pub fn body(mut self, body: impl Into<Vec<u8>>) -> Self {
        self.body = Some(body.into());
        self
    }

    pub fn text(self, text: impl Into<String>) -> Self {
        self.body(text.into().into_bytes())
            .header("Content-Type", "text/plain")
    }

    pub fn query(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
        self.query_params.insert(key.into(), value.into());
        self
    }

    pub fn timeout(mut self, duration: Duration) -> Self {
        self.timeout = Some(duration);
        self
    }

    pub fn no_timeout(mut self) -> Self {
        self.timeout = None;
        self
    }

    pub fn follow_redirects(mut self, follow: bool) -> Self {
        self.follow_redirects = follow;
        self
    }

    pub fn send(self) -> Result<Response> {
        let client = Client::builder()
            .redirect(if self.follow_redirects {
                reqwest::redirect::Policy::default()
            } else {
                reqwest::redirect::Policy::none()
            })
            .build()?;

        let mut url = url::Url::parse(&self.url)?;

        for (key, value) in self.query_params {
            url.query_pairs_mut().append_pair(&key, &value);
        }

        let mut request_builder = client
            .request(self.method.as_reqwest_method(), url)
            .headers(self.headers);

        if let Some(timeout) = self.timeout {
            request_builder = request_builder.timeout(timeout);
        }

        if let Some(body) = self.body {
            request_builder = request_builder.body(body);
        }

        let start = std::time::Instant::now();
        let response = request_builder.send()?;
        let duration = start.elapsed();

        Response::from_reqwest(response, duration)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_request_builder() {
        let req = Request::get("https://example.com")
            .header("Authorization", "Bearer token")
            .query("page", "1")
            .timeout(Duration::from_secs(10));

        assert_eq!(req.method, Method::Get);
        assert_eq!(req.url, "https://example.com");
        assert_eq!(req.timeout, Some(Duration::from_secs(10)));
    }

    #[test]
    fn test_json_body() {
        use serde_json::json;

        let body = json!({
            "name": "test",
            "value": 42
        });

        let req = Request::post("https://example.com").json(&body).unwrap();

        assert!(req.body.is_some());
        assert!(req.headers.contains_key("content-type"));
    }
}