bouillon 0.2.1

A thin, opinionated wrapper around soup that provides an easy, fluent API for sending HTTP requests.
Documentation
#[cfg(any(feature = "json", feature = "form", feature = "query"))]
use crate::Error;
use crate::Method;
use crate::Response;
use crate::Result;
use crate::body::Body;
#[cfg(any(feature = "json", feature = "form"))]
use crate::message_headers_ext::MessageHeadersExt as _;
use crate::session_ext::SessionExt;
use crate::uri_builder::UriBuilder;
use soup::glib;
use soup::glib::Uri;
use soup::{Message, Session};
use std::fmt::Display;

#[cfg(any(feature = "json", feature = "form"))]
const CONTENT_TYPE: &str = "Content-Type";

const AUTHORIZATION: &str = "Authorization";

/// An HTTP request. Use a [`RequestBuilder`] to build one.
#[derive(Debug, Clone)]
pub struct Request {
    message: Message,
    io_priority: glib::Priority,
}

impl Request {
    /// The underlying soup [`Message`] of this request.
    pub fn message(&self) -> &Message {
        &self.message
    }

    pub fn io_priority(&self) -> glib::Priority {
        self.io_priority
    }

    pub fn into_message(self) -> Message {
        self.message
    }
}

/// A builder for a [`Request`].
/// Use one of the methods on [`SessionExt`] to start a builder.
#[derive(Debug)]
pub struct RequestBuilder {
    message: Result<Message>,
    uri_builder: UriBuilder,
    session: Session,
    io_priority: glib::Priority,
}

impl RequestBuilder {
    pub(crate) fn new(session: Session, method: Method, uri: Result<Uri>) -> Self {
        match uri {
            Ok(uri) => Self {
                message: Ok(Message::from_uri(method.as_str(), &uri)),
                uri_builder: UriBuilder::from(&uri),
                session,
                io_priority: glib::Priority::DEFAULT,
            },
            Err(error) => Self {
                message: Err(error),
                uri_builder: UriBuilder::default(),
                session,
                io_priority: glib::Priority::DEFAULT,
            },
        }
    }

    /// Appends a header to the request.
    pub fn header(self, name: &str, value: &str) -> Self {
        if let Ok(message) = &self.message {
            let headers = message
                .request_headers()
                .expect("request headers to be set");
            headers.append(name, value);
        }
        self
    }

    /// Enable HTTP basic authentication.
    pub fn basic_auth<U, P>(self, username: U, password: Option<P>) -> Self
    where
        U: Display,
        P: Display,
    {
        let header_value = {
            let header_value = if let Some(password) = password {
                format!("{username}:{password}")
            } else {
                username.to_string()
            };
            glib::base64_encode(header_value.as_bytes())
        };
        self.header(AUTHORIZATION, &header_value)
    }

    /// Enable HTTP bearer authentication.
    pub fn bearer_auth<T>(self, token: T) -> Self
    where
        T: Display,
    {
        self.header(AUTHORIZATION, &format!("Bearer {token}"))
    }

    /// Set the request body.
    pub fn body<T>(self, body: T) -> Self
    where
        T: Into<Body>,
    {
        if let Ok(message) = &self.message {
            body.into().set_as_request_body(message);
        }
        self
    }

    /// Adds the given flags to the message's flags.
    pub fn flags(self, flags: soup::MessageFlags) -> Self {
        if let Ok(message) = &self.message {
            message.add_flags(flags);
        }
        self
    }

    /// Disables following redirect (3xx) responses.
    pub fn no_redirect(self) -> Self {
        self.flags(soup::MessageFlags::NO_REDIRECT)
    }

    /// Set a JSON as the request body.
    /// ### Errors
    /// [`Self::build`] will fail when the serialization fails.
    #[cfg(feature = "json")]
    pub fn json<T>(mut self, body: &T) -> Self
    where
        T: serde::Serialize + ?Sized,
    {
        if let Ok(message) = &self.message {
            match json::to_vec(body) {
                Err(error) => {
                    self.message = Err(Error::new(error));
                }
                Ok(bytes) => {
                    self.set_header_if_not_set(CONTENT_TYPE, "application/json");
                    Body::from(bytes).set_as_request_body(message);
                }
            };
        }
        self
    }

    /// Set a url-encoded form as the request body.
    /// ### Errors
    /// [`Self::build`] will fail when the serialization fails.
    #[cfg(feature = "form")]
    pub fn form<T>(mut self, body: &T) -> Self
    where
        T: serde::Serialize + ?Sized,
    {
        if let Ok(message) = &self.message {
            match serde_urlencoded::to_string(body) {
                Err(error) => {
                    self.message = Err(Error::new(error));
                }
                Ok(body) => {
                    self.set_header_if_not_set(CONTENT_TYPE, "application/x-www-form-urlencoded");
                    Body::from(body).set_as_request_body(message);
                }
            };
        }
        self
    }

    /// Appends query parameters to the URL.
    /// ### Errors
    /// [`Self::build`] will fail when the serialization fails.
    #[cfg(feature = "query")]
    pub fn query<T>(mut self, query: &T) -> Self
    where
        T: serde::Serialize + ?Sized,
    {
        if self.message.is_ok() {
            match serde_urlencoded::to_string(query) {
                Err(error) => {
                    self.message = Err(Error::new(error));
                }
                Ok(query) => {
                    self.uri_builder = self.uri_builder.append_query(&query);
                }
            };
        }
        self
    }

    /// Set [`glib::Priority`] with which the request will be sent.
    pub fn io_priority(mut self, priority: glib::Priority) -> Self {
        self.io_priority = priority;
        self
    }

    /// Builds and sends the request.
    pub async fn send(self) -> Result<Response> {
        let (session, request) = self.build_split();
        session.execute(request?).await
    }

    /// Builds a [`Request`] that can be modified and then sent using [`SessionExt::execute`].
    pub fn build(self) -> Result<Request> {
        let (_, message) = self.build_split();
        message
    }

    /// Builds a [`Request`] that can be modified and then sent using [`SessionExt::execute`]
    /// and also returns the soup [`Session`] associated with this builder.
    pub fn build_split(self) -> (Session, Result<Request>) {
        let message = self
            .message
            .inspect(|message| message.set_uri(&self.uri_builder.build()))
            .map(|message| Request {
                message,
                io_priority: self.io_priority,
            });
        (self.session, message)
    }

    #[cfg(any(feature = "json", feature = "form"))]
    fn set_header_if_not_set(&self, name: &str, value: &str) {
        if let Ok(message) = &self.message {
            let headers = message
                .request_headers()
                .expect("request headers to be set");
            if !headers.contains(name) {
                headers.append(name, value);
            }
        }
    }
}