bouillon 0.2.1

A thin, opinionated wrapper around soup that provides an easy, fluent API for sending HTTP requests.
Documentation
use soup::glib::{self};

#[derive(Debug)]
pub(crate) struct UriBuilder {
    flags: glib::UriFlags,
    scheme: glib::GString,
    userinfo: Option<glib::GString>,
    host: Option<glib::GString>,
    port: i32,
    path: glib::GString,
    query: Option<glib::GStringBuilder>,
    fragment: Option<glib::GString>,
}

impl Default for UriBuilder {
    fn default() -> Self {
        Self {
            flags: glib::UriFlags::ENCODED,
            scheme: Default::default(),
            userinfo: Default::default(),
            host: Default::default(),
            port: Default::default(),
            path: Default::default(),
            query: Default::default(),
            fragment: Default::default(),
        }
    }
}

impl From<&glib::Uri> for UriBuilder {
    fn from(value: &glib::Uri) -> Self {
        if value.flags().contains(glib::UriFlags::ENCODED) {
            Self {
                flags: value.flags(),
                scheme: value.scheme(),
                userinfo: value.userinfo(),
                host: value.host(),
                port: value.port(),
                path: value.path(),
                query: value.query().map(glib::GStringBuilder::new),
                fragment: value.fragment(),
            }
        } else {
            // We can only handle URIs that do not decode their parts,
            // so if that's not the case, we split the URI again.
            let flags = value.flags() | glib::UriFlags::ENCODED;
            let (scheme, userinfo, host, port, path, query, fragment) =
                glib::Uri::split(&value.to_str(), flags).expect("valid URI to be splittable");
            Self {
                flags,
                scheme: scheme.expect("valid URI to have a scheme"),
                userinfo,
                host,
                port,
                path,
                query: query.map(glib::GStringBuilder::new),
                fragment,
            }
        }
    }
}

impl UriBuilder {
    #[cfg(feature = "query")]
    /// Appends a already encoded params to the query.
    pub(crate) fn append_query(mut self, params: &str) -> Self {
        if params.is_empty() {
            return self;
        }
        let query = self.query.get_or_insert_default();
        if !query.is_empty() {
            query.append("&");
        }
        query.append(params);
        self
    }

    pub(crate) fn build(self) -> glib::Uri {
        glib::Uri::build(
            self.flags | glib::UriFlags::ENCODED,
            &self.scheme,
            self.userinfo.as_deref(),
            self.host.as_deref(),
            self.port,
            &self.path,
            self.query.as_deref(),
            self.fragment.as_deref(),
        )
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use soup::glib::{Uri, UriFlags};

    #[test]
    fn returns_original_url_when_encoded_set() {
        const URI: &str = "https://user:pw@example.com/hello%20world?foo=bar%20baz#frag%20ment";
        let uri = Uri::parse(URI, UriFlags::ENCODED).unwrap();
        assert_eq!(URI, uri.to_str()); // sanity check
        let actual = UriBuilder::from(&uri).build();
        assert_uri_equal(&uri, &actual);
    }

    #[test]
    fn returns_original_url_when_encoded_not_set() {
        const URI: &str = "https://user:pw@example.com:80/hello%20world?foo=bar%20baz#frag%20ment";
        let uri = Uri::parse(URI, UriFlags::NONE).unwrap();
        assert_eq!(URI, uri.to_str()); // sanity check
        let actual = UriBuilder::from(&uri).build();
        assert_uri_equal(&uri, &actual);
    }

    #[cfg(feature = "query")]
    #[test]
    fn does_nothing_if_params_are_empty() {
        const URIS: &[&str] = &[
            "https://example.com",
            "https://example.com?",
            "https://example.com?foo=bar",
            "https://example.com#foo=bar",
        ];
        for uri_str in URIS {
            let uri = Uri::parse(uri_str, UriFlags::ENCODED).unwrap();
            assert_eq!(uri_str, &uri.to_str()); // sanity check
            let actual = UriBuilder::from(&uri).append_query("").build();
            assert_uri_equal(&uri, &actual);
        }
    }

    #[cfg(feature = "query")]
    #[test]
    fn appends_if_no_query_present() {
        const URI: &str = "https://example.com";
        const EXPECTED_URI: &str = "https://example.com?foo=bar";
        let uri = Uri::parse(URI, UriFlags::NONE).unwrap();
        assert_eq!(URI, uri.to_str()); // sanity check
        let actual = UriBuilder::from(&uri).append_query("foo=bar").build();
        assert_eq!(EXPECTED_URI, actual.to_str());
    }

    #[cfg(feature = "query")]
    #[test]
    fn appends_if_empty_query_present() {
        const URI: &str = "https://example.com?";
        const EXPECTED_URI: &str = "https://example.com?foo=bar";
        let uri = Uri::parse(URI, UriFlags::NONE).unwrap();
        assert_eq!(URI, uri.to_str()); // sanity check
        let actual = UriBuilder::from(&uri).append_query("foo=bar").build();
        assert_eq!(EXPECTED_URI, actual.to_str());
    }

    #[cfg(feature = "query")]
    #[test]
    fn appends_if_query_present() {
        const URI: &str = "https://example.com?foo";
        const EXPECTED_URI: &str = "https://example.com?foo&foo=bar";
        let uri = Uri::parse(URI, UriFlags::NONE).unwrap();
        assert_eq!(URI, uri.to_str()); // sanity check
        let actual = UriBuilder::from(&uri).append_query("foo=bar").build();
        assert_eq!(EXPECTED_URI, actual.to_str());
    }

    fn assert_uri_equal(left: &Uri, right: &Uri) {
        assert_eq!(left.to_str(), right.to_str());
    }
}