atproto-client 0.11.1

HTTP client for AT Protocol services with OAuth and identity integration
Documentation
//! URL construction utilities for HTTP endpoints.
//!
//! Build well-formed HTTP request URLs with parameter encoding
//! and query string generation.

/// A single query parameter as a key-value pair.
pub type QueryParam<'a> = (&'a str, &'a str);
/// A collection of query parameters.
pub type QueryParams<'a> = Vec<QueryParam<'a>>;

/// Builds a query string from a collection of query parameters.
///
/// # Arguments
///
/// * `query` - Collection of key-value pairs to build into a query string
///
/// # Returns
///
/// A formatted query string with URL-encoded parameters
pub fn build_querystring(query: QueryParams) -> String {
    query.iter().fold(String::new(), |acc, &tuple| {
        acc + tuple.0 + "=" + tuple.1 + "&"
    })
}

/// Builder for constructing URLs with host, path, and query parameters.
pub struct URLBuilder {
    host: String,
    path: String,
    params: Vec<(String, String)>,
}

/// Convenience function to build a URL with optional parameters.
///
/// # Arguments
///
/// * `host` - The hostname (will be prefixed with https:// if needed)
/// * `path` - The URL path
/// * `params` - Vector of optional key-value pairs for query parameters
///
/// # Returns
///
/// A fully constructed URL string
pub fn build_url(host: &str, path: &str, params: Vec<Option<(&str, &str)>>) -> String {
    let mut url_builder = URLBuilder::new(host);
    url_builder.path(path);

    for (key, value) in params.iter().filter_map(|x| *x) {
        url_builder.param(key, value);
    }

    url_builder.build()
}

impl URLBuilder {
    /// Creates a new URLBuilder with the specified host.
    ///
    /// # Arguments
    ///
    /// * `host` - The hostname (will be prefixed with https:// if needed and trailing slash removed)
    ///
    /// # Returns
    ///
    /// A new URLBuilder instance
    pub fn new(host: &str) -> URLBuilder {
        let host = if host.starts_with("https://") {
            host.to_string()
        } else {
            format!("https://{}", host)
        };

        let host = if let Some(trimmed) = host.strip_suffix('/') {
            trimmed.to_string()
        } else {
            host
        };

        URLBuilder {
            host: host.to_string(),
            params: vec![],
            path: "/".to_string(),
        }
    }

    /// Adds a query parameter to the URL.
    ///
    /// # Arguments
    ///
    /// * `key` - The parameter key
    /// * `value` - The parameter value (will be URL-encoded)
    ///
    /// # Returns
    ///
    /// A mutable reference to self for method chaining
    pub fn param(&mut self, key: &str, value: &str) -> &mut Self {
        self.params
            .push((key.to_owned(), urlencoding::encode(value).to_string()));
        self
    }

    /// Sets the URL path.
    ///
    /// # Arguments
    ///
    /// * `path` - The URL path
    ///
    /// # Returns
    ///
    /// A mutable reference to self for method chaining
    pub fn path(&mut self, path: &str) -> &mut Self {
        path.clone_into(&mut self.path);
        self
    }

    /// Constructs the final URL string.
    ///
    /// # Returns
    ///
    /// The complete URL with host, path, and query parameters
    pub fn build(self) -> String {
        let mut url_params = String::new();

        if !self.params.is_empty() {
            url_params.push('?');

            let qs_args = self.params.iter().map(|(k, v)| (&**k, &**v)).collect();
            url_params.push_str(build_querystring(qs_args).as_str());
        }

        format!("{}{}{}", self.host, self.path, url_params)
    }
}