Skip to main content

urly/
builder.rs

1use ntex_bytes::ByteString;
2
3use crate::authority::Authority;
4use crate::error::{ErrorKind, InvalidUrl, InvalidUrlParts};
5use crate::{parse, path::PathAndQuery, query::Fragment, scheme::Scheme, url::Url};
6
7/// URL builder.
8///
9/// Components are given literally (host, user info, query pairs) or
10/// percent-encoded (path, query, fragment) and are normalized by
11/// [`Builder::build`]. A builder created from a [`Url`] modifies that URL.
12///
13/// ```
14/// use urly::Url;
15///
16/// let url = Url::builder()
17///     .scheme("https")
18///     .userinfo("user", Some("p@ss"))
19///     .host("example.com")
20///     .port(8443)
21///     .path("/search")
22///     .query_pair("q", "a b")
23///     .fragment("top")
24///     .build()
25///     .unwrap();
26/// assert_eq!(url, "https://user:p%[email protected]:8443/search?q=a+b#top");
27///
28/// let url = urly::Builder::from(url).port(443).fragment("").build().unwrap();
29/// assert_eq!(url, "https://user:p%[email protected]:443/search?q=a+b#");
30/// ```
31#[derive(Debug, Default, Clone)]
32pub struct Builder {
33    base: Option<Url>,
34    scheme: Option<String>,
35    authority: Option<String>,
36    host: Option<String>,
37    userinfo: Option<(String, Option<String>)>,
38    port: Option<u16>,
39    path: Option<String>,
40    query: Option<String>,
41    pairs: Vec<(String, String)>,
42    fragment: Option<String>,
43}
44
45impl Builder {
46    /// Creates an empty builder.
47    pub fn new() -> Self {
48        Self::default()
49    }
50
51    /// Sets the scheme.
52    #[must_use]
53    pub fn scheme(mut self, scheme: &str) -> Self {
54        self.scheme = Some(scheme.to_string());
55        self
56    }
57
58    /// Sets the raw authority: `[userinfo "@"] host [":" port]`.
59    ///
60    /// Host, user info and port set separately are applied afterwards.
61    #[must_use]
62    pub fn authority(mut self, authority: &str) -> Self {
63        self.authority = Some(authority.to_string());
64        self
65    }
66
67    /// Sets the literal host, see [`Url::set_host`].
68    #[must_use]
69    pub fn host(mut self, host: &str) -> Self {
70        self.host = Some(host.to_string());
71        self
72    }
73
74    /// Sets the literal user name and password.
75    #[must_use]
76    pub fn userinfo(mut self, username: &str, password: Option<&str>) -> Self {
77        self.userinfo = Some((username.to_string(), password.map(str::to_string)));
78        self
79    }
80
81    /// Sets the port.
82    #[must_use]
83    pub fn port(mut self, port: u16) -> Self {
84        self.port = Some(port);
85        self
86    }
87
88    /// Sets the percent-encoded path.
89    #[must_use]
90    pub fn path(mut self, path: &str) -> Self {
91        self.path = Some(path.to_string());
92        self
93    }
94
95    /// Sets the percent-encoded query.
96    #[must_use]
97    pub fn query(mut self, query: &str) -> Self {
98        self.query = Some(query.to_string());
99        self
100    }
101
102    /// Appends a literal key-value pair to the query.
103    #[must_use]
104    pub fn query_pair(mut self, key: &str, value: &str) -> Self {
105        self.pairs.push((key.to_string(), value.to_string()));
106        self
107    }
108
109    /// Appends literal key-value pairs to the query.
110    #[must_use]
111    pub fn query_pairs<I, K, V>(mut self, pairs: I) -> Self
112    where
113        I: IntoIterator<Item = (K, V)>,
114        K: AsRef<str>,
115        V: AsRef<str>,
116    {
117        self.pairs.extend(
118            pairs
119                .into_iter()
120                .map(|(k, v)| (k.as_ref().to_string(), v.as_ref().to_string())),
121        );
122        self
123    }
124
125    /// Sets the percent-encoded fragment.
126    #[must_use]
127    pub fn fragment(mut self, fragment: &str) -> Self {
128        self.fragment = Some(fragment.to_string());
129        self
130    }
131
132    /// Builds the URL.
133    pub fn build(self) -> Result<Url, InvalidUrl> {
134        let mut url = self.base.unwrap_or_else(Url::empty);
135        if let Some(scheme) = self.scheme {
136            url.set_scheme(&scheme)?;
137        }
138        if let Some(authority) = self.authority {
139            url.set_authority(Some(&authority))?;
140        }
141        if let Some(host) = self.host {
142            url.set_host(&host)?;
143        }
144        if let Some((user, password)) = self.userinfo {
145            url.set_userinfo(Some(&user), password.as_deref())?;
146        }
147        if self.port.is_some() {
148            url.set_port(self.port)?;
149        }
150        if let Some(path) = self.path {
151            url.set_path(&path);
152        }
153        if let Some(query) = self.query {
154            url.set_query(Some(&query));
155        }
156        if !self.pairs.is_empty() {
157            url.extend_query_pairs(self.pairs);
158        }
159        if let Some(fragment) = self.fragment {
160            url.set_fragment(Some(&fragment));
161        }
162        if url.as_str().is_empty() {
163            Err(InvalidUrl::new(ErrorKind::Empty))
164        } else {
165            Ok(url)
166        }
167    }
168}
169
170impl From<Url> for Builder {
171    fn from(url: Url) -> Self {
172        Builder {
173            base: Some(url),
174            ..Default::default()
175        }
176    }
177}
178
179/// Percent-encoded URL parts.
180///
181/// Created by [`Url::into_parts`]; [`Url::from_parts`] validates and joins
182/// the parts back.
183#[derive(Debug, Default, Clone)]
184pub struct Parts {
185    /// Scheme without the `:`.
186    pub scheme: Option<ByteString>,
187    /// Authority without the leading `//`.
188    pub authority: Option<ByteString>,
189    /// Path and query.
190    pub path_and_query: ByteString,
191    /// Fragment without the `#`.
192    pub fragment: Option<ByteString>,
193}
194
195impl Url {
196    /// Creates a URL from strictly validated parts.
197    ///
198    /// ```
199    /// use urly::{Parts, Url};
200    ///
201    /// let url = Url::from_static("http://example.com/a?b#c");
202    /// let mut parts = url.into_parts();
203    /// assert_eq!(parts.path_and_query, "/a?b");
204    /// parts.fragment = None;
205    /// assert_eq!(Url::from_parts(parts).unwrap(), "http://example.com/a?b");
206    ///
207    /// let parts = Parts { path_and_query: "a b".into(), ..Parts::default() };
208    /// assert!(Url::from_parts(parts).is_err());
209    /// ```
210    pub fn from_parts(parts: Parts) -> Result<Url, InvalidUrlParts> {
211        let invalid_path = || InvalidUrlParts::from(InvalidUrl::new(ErrorKind::InvalidPath));
212
213        let pq = PathAndQuery::new(&parts.path_and_query)?;
214        let path = pq.path().as_str();
215        if let Some(scheme) = &parts.scheme {
216            Scheme::new(scheme)?;
217        }
218        if let Some(authority) = &parts.authority {
219            Authority::new(authority)?;
220            if !path.is_empty() && !path.starts_with('/') {
221                return Err(invalid_path());
222            }
223        } else if parts.scheme.is_none() && path.split('/').next().unwrap_or("").contains(':') {
224            return Err(invalid_path());
225        }
226        if let Some(fragment) = &parts.fragment {
227            Fragment::new(fragment)?;
228        }
229
230        let mut s = String::new();
231        if let Some(scheme) = &parts.scheme {
232            s.push_str(scheme);
233            s.push(':');
234        }
235        if let Some(authority) = &parts.authority {
236            s.push_str("//");
237            s.push_str(authority);
238        } else if path.starts_with("//") {
239            s.push_str("/.");
240        }
241        s.push_str(&parts.path_and_query);
242        if let Some(fragment) = &parts.fragment {
243            s.push('#');
244            s.push_str(fragment);
245        }
246        if s.is_empty() {
247            return Err(InvalidUrl::new(ErrorKind::Empty).into());
248        }
249        Ok(parse::parse(&s, None)?)
250    }
251}