flawless-http 1.0.0-beta.3

HTTP client for https://flawless.dev.
Documentation
use std::time::Duration;

use flawless::idempotent::Idempotence;

use crate::transport::flawless::{Flawless, Idempotent};
use crate::{response::Response, transport::HTTPTransport, Config, Error, IntoBody};

/// An HTTP request.
#[derive(Debug, Clone)]
pub struct Request<T>
where
    T: HTTPTransport,
{
    config: Config,
    transport: T,
    method: String,
    url: String,
    headers: Vec<(String, String)>,
    body: Vec<u8>,
}

impl<T: HTTPTransport> Request<T> {
    /// Create a new request over a provided transport layer.
    pub fn new<M, U>(transport: T, method: M, url: U) -> Request<T>
    where
        M: Into<String>,
        U: Into<String>,
    {
        Request {
            config: Config::default(),
            transport,
            method: method.into(),
            url: url.into(),
            headers: Vec::new(),
            body: Vec::new(),
        }
    }

    /// Perform the request.
    pub fn send(self) -> Result<Response, Error> {
        self.transport.send(self.config, self.method, self.url, self.headers, self.body)
    }

    //// Sets the duration of time a request should wait on a response before returning a timeout error.
    ///
    /// Default: 120s
    ///
    /// Timeout errors are of kind [`ErrorKind::Io`](crate::ErrorKind::Io). Call [`Error::message()`] for
    /// more information.
    ///
    /// ```no_run
    /// use std::time::Duration;
    /// use flawless_http::{get, ErrorKind};
    ///
    /// let response = get("https://httpbin.org/delay/2")
    ///                  .timeout(Duration::from_secs(1))
    ///                  .send();
    /// assert!(response.is_err());
    /// assert_eq!(response.err().unwrap().kind(), ErrorKind::Io);
    /// ```
    pub fn timeout(mut self, duration: Duration) -> Self {
        self.config.set_timeout(duration);
        self
    }

    /// Sets the size limit of a response body.
    ///
    /// Default: 10_000_000 (~10Mb)
    ///
    /// In case a response returns a bigger body, it will be truncated.
    ///
    /// ```no_run
    /// use flawless_http::get;
    ///
    /// let response = get("https://httpbin.org/get")
    ///                  .response_body_limit(100)
    ///                  .send();
    /// assert!(response.is_ok());
    /// assert_eq!(response.unwrap().text().unwrap().len(), 100);
    /// ```
    pub fn response_body_limit(mut self, limit: u64) -> Self {
        self.config.set_response_body_limit(limit);
        self
    }

    /// Gets header by name if it exists, otherwise returns `None`.
    ///
    /// If multiple headers exist with the same name, will return the first one. HTTP header names are NOT
    /// case-sensitive and case will be ignored during lookup.
    pub fn header<N: AsRef<str>>(&self, name: N) -> Option<&String> {
        let name_l = name.as_ref().to_lowercase();
        for header in self.headers.iter() {
            let header_name_l = header.0.to_lowercase();
            if name_l == header_name_l {
                return Some(&header.1);
            }
        }

        None
    }

    /// Sets a request header.
    ///
    /// If the header already exists, it will be overwritten. HTTP header names are NOT case-sensitive and
    /// case will be ignored during comparison. dsfsd sdfsdffsdf sdfsd
    /// ```no_run
    /// use serde_json::Value;
    /// use flawless_http::get;
    ///
    /// let response = get("https://httpbin.org/get")
    ///                  .set_header("Accept", "text/plain")
    ///                  .send();
    /// let json: Value = response.unwrap().json().unwrap();
    /// let accept_header = json["headers"]["Accept"].as_str();
    /// assert_eq!(accept_header, Some("text/plain"));
    /// ```
    pub fn set_header<N, V>(mut self, name: N, value: V) -> Self
    where
        N: Into<String>,
        V: Into<String>,
    {
        // Attempt to override existing header.
        let name: String = name.into();
        for header in self.headers.iter_mut() {
            if header.0.to_lowercase() == name.to_lowercase() {
                header.1 = value.into();
                return self;
            }
        }
        // If no header was overwritten, push it at the end of the array.
        self.headers.push((name, value.into()));
        self
    }

    /// Sets the body of a request.
    ///
    /// Depending on the argument, additional request headers might be set. If headers are already manually
    /// set by the user, this call should not override them.
    ///
    /// # Text arguments
    ///
    /// In case a `String` or `&str` type is given to `body`, the `"Content-Length"` header is going to be set
    /// to the byte length of the string.
    ///
    /// ```no_run
    /// use flawless_http::post;
    ///
    /// let response = post("https://httpbin.org/post")
    ///                  .body("Hello world!")
    ///                  .send();
    /// assert!(response.is_ok());
    /// ```
    ///
    /// # Form arguments
    ///
    /// In case a slice of tuples of strings is passed in (`&[(&str, &str)]`), `body` will assume a for is
    /// being submitted and URL encode it. It will set the header `"Content-Type"` to
    /// `"application/x-www-form-urlencoded"`, and `"Content-Length"` to the size of the encoded content.
    ///
    /// ```no_run
    /// use flawless_http::post;
    ///
    /// let response = post("https://httpbin.org/post")
    ///                  .body([
    ///                     ("Hello", "world!"),
    ///                     ("second", "argument"),
    ///                   ].as_ref())
    ///                  .send();
    /// assert!(response.is_ok());
    /// ```
    ///
    /// # JSON arguments
    ///
    /// In case of a [`serde_json::Value`] type, `body` will assume that the content is of type JSON and set
    /// the header `"Content-Type"` to `"application/json"`. It will also set `"Content-Length"` to the size
    /// of the serialized JSON.
    ///
    /// ```no_run
    /// use flawless_http::post;
    /// use serde_json::json;
    ///
    /// let response = post("https://httpbin.org/post")
    ///                  .body(json!({
    ///                     "Hello": "world!",
    ///                     "second": "argument",
    ///                   }))
    ///                  .send();
    /// assert!(response.is_ok());
    /// ```
    pub fn body<B: IntoBody<T>>(self, body: B) -> Self {
        let (mut request, body) = body.into(self);
        request.body = body;
        request
    }
}

impl Idempotence for Request<Flawless> {
    type Idempotent = Request<Idempotent>;

    /// Mark the request as idempotent.
    ///
    /// Idempotent requests will automatically be retried in case of timeouts or errors.
    fn idempotent(self) -> Self::Idempotent {
        let mut config = self.config;
        config.set_idempotent(true);
        Request {
            config,
            transport: Idempotent,
            method: self.method,
            url: self.url,
            headers: self.headers,
            body: self.body,
        }
    }
}

/// Create a GET request.
///
/// ```no_run
/// use flawless_http::get;
///
/// let response = get("https://httpbin.org/get").send();
/// assert!(response.is_ok());
/// ```
pub fn get<U: Into<String>>(url: U) -> Request<Flawless> {
    Request::new(Flawless, "GET", url)
}

/// Create a POST request.
///
/// ```no_run
/// use flawless_http::post;
///
/// let response = post("https://httpbin.org/post").send();
/// assert!(response.is_ok());
/// ```
pub fn post<U: Into<String>>(url: U) -> Request<Flawless> {
    Request::new(Flawless, "POST", url)
}

/// Create a DELETE request.
///
/// ```no_run
/// use flawless_http::delete;
///
/// let response = delete("https://httpbin.org/delete").send();
/// assert!(response.is_ok());
/// ```
pub fn delete<U: Into<String>>(url: U) -> Request<Flawless> {
    Request::new(Flawless, "DELETE", url)
}

/// Create a PUT request.
///
/// ```no_run
/// use flawless_http::put;
///
/// let response = put("https://httpbin.org/put").send();
/// assert!(response.is_ok());
/// ```
pub fn put<U: Into<String>>(url: U) -> Request<Flawless> {
    Request::new(Flawless, "PUT", url)
}

/// Create a HEAD request.
///
/// ```no_run
/// use flawless_http::head;
///
/// let response = head("https://httpbin.org/get").send();
/// assert!(response.is_ok());
/// ```
pub fn head<U: Into<String>>(url: U) -> Request<Flawless> {
    Request::new(Flawless, "HEAD", url)
}

/// Create a PATCH request.
///
/// ```no_run
/// use flawless_http::patch;
///
/// let response = patch("https://httpbin.org/patch").send();
/// assert!(response.is_ok());
/// ```
pub fn patch<U: Into<String>>(url: U) -> Request<Flawless> {
    Request::new(Flawless, "PATCH", url)
}