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}