atproto_identity/
url.rs

1//! URL construction utilities for HTTP endpoints.
2//!
3//! Build well-formed HTTP request URLs with parameter encoding
4//! and query string generation.
5
6/// A single query parameter as a key-value pair.
7pub type QueryParam<'a> = (&'a str, &'a str);
8/// A collection of query parameters.
9pub type QueryParams<'a> = Vec<QueryParam<'a>>;
10
11/// Builds a query string from a collection of query parameters.
12///
13/// # Arguments
14///
15/// * `query` - Collection of key-value pairs to build into a query string
16///
17/// # Returns
18///
19/// A formatted query string with URL-encoded parameters
20pub fn build_querystring(query: QueryParams) -> String {
21    query.iter().fold(String::new(), |acc, &tuple| {
22        acc + tuple.0 + "=" + tuple.1 + "&"
23    })
24}
25
26/// Builder for constructing URLs with host, path, and query parameters.
27pub struct URLBuilder {
28    host: String,
29    path: String,
30    params: Vec<(String, String)>,
31}
32
33/// Convenience function to build a URL with optional parameters.
34///
35/// # Arguments
36///
37/// * `host` - The hostname (will be prefixed with https:// if needed)
38/// * `path` - The URL path
39/// * `params` - Vector of optional key-value pairs for query parameters
40///
41/// # Returns
42///
43/// A fully constructed URL string
44pub fn build_url(host: &str, path: &str, params: Vec<Option<(&str, &str)>>) -> String {
45    let mut url_builder = URLBuilder::new(host);
46    url_builder.path(path);
47
48    for (key, value) in params.iter().filter_map(|x| *x) {
49        url_builder.param(key, value);
50    }
51
52    url_builder.build()
53}
54
55impl URLBuilder {
56    /// Creates a new URLBuilder with the specified host.
57    ///
58    /// # Arguments
59    ///
60    /// * `host` - The hostname (will be prefixed with https:// if needed and trailing slash removed)
61    ///
62    /// # Returns
63    ///
64    /// A new URLBuilder instance
65    pub fn new(host: &str) -> URLBuilder {
66        let host = if host.starts_with("https://") {
67            host.to_string()
68        } else {
69            format!("https://{}", host)
70        };
71
72        let host = if let Some(trimmed) = host.strip_suffix('/') {
73            trimmed.to_string()
74        } else {
75            host
76        };
77
78        URLBuilder {
79            host: host.to_string(),
80            params: vec![],
81            path: "/".to_string(),
82        }
83    }
84
85    /// Adds a query parameter to the URL.
86    ///
87    /// # Arguments
88    ///
89    /// * `key` - The parameter key
90    /// * `value` - The parameter value (will be URL-encoded)
91    ///
92    /// # Returns
93    ///
94    /// A mutable reference to self for method chaining
95    pub fn param(&mut self, key: &str, value: &str) -> &mut Self {
96        self.params
97            .push((key.to_owned(), urlencoding::encode(value).to_string()));
98        self
99    }
100
101    /// Sets the URL path.
102    ///
103    /// # Arguments
104    ///
105    /// * `path` - The URL path
106    ///
107    /// # Returns
108    ///
109    /// A mutable reference to self for method chaining
110    pub fn path(&mut self, path: &str) -> &mut Self {
111        path.clone_into(&mut self.path);
112        self
113    }
114
115    /// Constructs the final URL string.
116    ///
117    /// # Returns
118    ///
119    /// The complete URL with host, path, and query parameters
120    pub fn build(self) -> String {
121        let mut url_params = String::new();
122
123        if !self.params.is_empty() {
124            url_params.push('?');
125
126            let qs_args = self.params.iter().map(|(k, v)| (&**k, &**v)).collect();
127            url_params.push_str(build_querystring(qs_args).as_str());
128        }
129
130        format!("{}{}{}", self.host, self.path, url_params)
131    }
132}