bouillon 0.2.1

A thin, opinionated wrapper around soup that provides an easy, fluent API for sending HTTP requests.
Documentation
use crate::utils::{is_client_error, is_server_error};
use crate::{Error, Result};
use soup::gio::prelude::{MemoryOutputStreamExt, OutputStreamExt};
use soup::{Message, MessageHeaders, Status, gio, glib};

/// An HTTP response including its body.
#[derive(Debug)]
pub struct Response {
    message: Message,
    body: gio::InputStream,
    io_priority: glib::Priority,
}

impl Response {
    pub(crate) fn new(
        message: Message,
        body: gio::InputStream,
        io_priority: glib::Priority,
    ) -> Self {
        Self {
            message,
            body,
            io_priority,
        }
    }
}

impl Response {
    /// The HTTP status.
    pub fn status(&self) -> Status {
        self.message.status()
    }

    /// The HTTP status code.
    pub fn status_code(&self) -> u32 {
        self.message.status_code()
    }

    /// The final URL of this response.
    pub fn uri(&self) -> glib::Uri {
        self.message.uri().expect("URI to be set")
    }

    /// The response headers.
    pub fn headers(&self) -> MessageHeaders {
        self.message
            .response_headers()
            .expect("response headers to be set")
    }

    /// The remote address used to get this response.
    pub fn remote_addr(&self) -> Option<gio::SocketAddress> {
        self.message.remote_address()
    }

    /// Read the response body as bytes.
    pub async fn bytes(self) -> Result<glib::Bytes> {
        let output_stream = gio::MemoryOutputStream::new_resizable();
        let flags =
            gio::OutputStreamSpliceFlags::CLOSE_SOURCE | gio::OutputStreamSpliceFlags::CLOSE_TARGET;
        output_stream
            .splice_future(&self.body, flags, self.io_priority)
            .await
            .map_err(|error| Error::new_with_uri(error, self.message.uri()))?;
        Ok(output_stream.steal_as_bytes())
    }

    /// Read the response body as UTF-8.
    /// Any invalid UTF-8 sequences will be replaced with U+FFFD REPLACEMENT CHARACTER, which looks like this: �
    pub async fn text(self) -> Result<String> {
        let bytes = self.bytes().await?;
        let text = String::from_utf8_lossy(&bytes);
        Ok(text.into_owned())
    }

    /// Try to deserialize the response body as JSON.
    #[cfg(feature = "json")]
    pub async fn json<T>(self) -> Result<T>
    where
        T: serde::de::DeserializeOwned,
    {
        let uri = self.message.uri();
        let bytes = self.bytes().await?;
        json::from_slice(&bytes).map_err(|error| Error::new_with_uri(error, uri))
    }

    /// Turn a response into an error if the server returned an error.
    pub fn error_for_status(self) -> Result<Self> {
        let status_code = self.message.status_code();
        if is_client_error(status_code) || is_server_error(status_code) {
            Err(Error::new_status(self.message()))
        } else {
            Ok(self)
        }
    }

    /// Turn a reference to a response into an error if the server returned an error.
    pub fn error_for_status_ref(&self) -> Result<&Self> {
        let status_code = self.message.status_code();
        if is_client_error(status_code) || is_server_error(status_code) {
            Err(Error::new_status(self.message()))
        } else {
            Ok(self)
        }
    }

    /// Returns the underlying soup [`Message`] of this response.
    pub fn message(&self) -> &Message {
        &self.message
    }

    pub fn split(self) -> (Message, gio::InputStream) {
        (self.message, self.body)
    }
}