1use {
2 failure::Error,
3 indexmap::IndexSet,
4 std::{
5 fmt,
6 hash::{Hash, Hasher},
7 str::FromStr,
8 },
9};
10
11#[deprecated(since = "0.4.3")]
13pub fn join_all<I>(segments: I) -> Result<Uri, Error>
14where
15 I: IntoIterator,
16 I::Item: AsRef<Uri>,
17{
18 segments
19 .into_iter()
20 .fold(Ok(Uri::root()), |acc, uri| acc?.join(uri))
21}
22
23#[derive(Debug, Clone)]
25pub struct Uri(UriKind);
26
27#[derive(Debug, Clone, PartialEq)]
28enum UriKind {
29 Root,
30 Asterisk,
31 Segments(String, Option<CaptureNames>),
32}
33
34impl PartialEq for Uri {
35 fn eq(&self, other: &Self) -> bool {
36 match (&self.0, &other.0) {
37 (&UriKind::Root, &UriKind::Root) | (&UriKind::Asterisk, &UriKind::Asterisk) => true,
38 (&UriKind::Segments(ref s, ..), &UriKind::Segments(ref o, ..)) if s == o => true,
39 _ => false,
40 }
41 }
42}
43
44impl Eq for Uri {}
45
46impl Hash for Uri {
47 fn hash<H: Hasher>(&self, state: &mut H) {
48 self.as_str().hash(state)
49 }
50}
51
52impl FromStr for Uri {
53 type Err = Error;
54
55 fn from_str(s: &str) -> Result<Self, Self::Err> {
56 Self::parse(s)
57 }
58}
59
60impl Uri {
61 pub fn root() -> Self {
62 Uri(UriKind::Root)
63 }
64
65 pub fn asterisk() -> Self {
66 Uri(UriKind::Asterisk)
67 }
68
69 pub fn from_static(s: &'static str) -> Self {
70 s.parse().expect("invalid URI")
71 }
72
73 pub fn parse(mut s: &str) -> Result<Self, Error> {
74 if !s.is_ascii() {
75 failure::bail!("The URI is not ASCII");
76 }
77
78 if s.starts_with('*') {
79 if s.len() > 1 {
80 failure::bail!("the URI with wildcard parameter must start with '/'");
81 }
82 return Ok(Uri(UriKind::Asterisk));
83 }
84
85 if !s.starts_with('/') {
86 failure::bail!("the URI must start with '/'");
87 }
88
89 if s == "/" {
90 return Ok(Self::root());
91 }
92
93 let mut has_trailing_slash = false;
94 if s.ends_with('/') {
95 has_trailing_slash = true;
96 s = &s[..s.len() - 1];
97 }
98
99 let mut names: Option<CaptureNames> = None;
100 for segment in s[1..].split('/') {
101 if names.as_ref().map_or(false, |names| names.has_wildcard) {
102 failure::bail!("The wildcard parameter has already set.");
103 }
104 if segment.is_empty() {
105 failure::bail!("empty segment");
106 }
107 if segment
108 .get(1..)
109 .map_or(false, |s| s.bytes().any(|b| b == b':' || b == b'*'))
110 {
111 failure::bail!("invalid character in a segment");
112 }
113 match segment.as_bytes()[0] {
114 b':' | b'*' => {
115 names.get_or_insert_with(Default::default).push(segment)?;
116 }
117 _ => {}
118 }
119 }
120
121 if has_trailing_slash {
122 Ok(Self::segments(format!("{}/", s), names))
123 } else {
124 Ok(Self::segments(s, names))
125 }
126 }
127
128 fn segments(s: impl Into<String>, names: Option<CaptureNames>) -> Self {
129 Uri(UriKind::Segments(s.into(), names))
130 }
131
132 #[cfg(test)]
133 fn static_(s: impl Into<String>) -> Self {
134 Self::segments(s, None)
135 }
136
137 #[cfg(test)]
138 fn captured(s: impl Into<String>, names: CaptureNames) -> Self {
139 Uri(UriKind::Segments(s.into(), Some(names)))
140 }
141
142 pub fn as_str(&self) -> &str {
143 match self.0 {
144 UriKind::Root => "/",
145 UriKind::Asterisk => "*",
146 UriKind::Segments(ref s, ..) => s.as_str(),
147 }
148 }
149
150 pub fn is_asterisk(&self) -> bool {
151 match self.0 {
152 UriKind::Asterisk => true,
153 _ => false,
154 }
155 }
156
157 pub fn capture_names(&self) -> Option<&CaptureNames> {
158 match self.0 {
159 UriKind::Segments(_, Some(ref names)) => Some(names),
160 _ => None,
161 }
162 }
163
164 pub fn join(&self, other: impl AsRef<Self>) -> Result<Self, Error> {
165 match self.0.clone() {
166 UriKind::Root => Ok(other.as_ref().clone()),
167 UriKind::Asterisk => {
168 failure::bail!("the asterisk URI cannot be joined with other URI(s)")
169 }
170 UriKind::Segments(mut segment, mut names) => match other.as_ref().0 {
171 UriKind::Root => Ok(Self::segments(segment, names)),
172 UriKind::Asterisk => {
173 failure::bail!("the asterisk URI cannot be joined with other URI(s)")
174 }
175 UriKind::Segments(ref other_segment, ref other_names) => {
176 segment += if segment.ends_with('/') {
177 other_segment.trim_left_matches('/')
178 } else {
179 other_segment
180 };
181 match (&mut names, other_names) {
182 (&mut Some(ref mut names), &Some(ref other_names)) => {
183 names.extend(other_names.params.iter().cloned())?;
184 }
185 (ref mut names @ None, &Some(ref other_names)) => {
186 **names = Some(other_names.clone());
187 }
188 (_, &None) => {}
189 }
190 Ok(Self::segments(segment, names))
191 }
192 },
193 }
194 }
195}
196
197impl AsRef<Uri> for Uri {
198 fn as_ref(&self) -> &Self {
199 self
200 }
201}
202
203impl fmt::Display for Uri {
204 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205 f.write_str(self.as_str())
206 }
207}
208
209#[derive(Clone, Debug, Default, PartialEq)]
210pub struct CaptureNames {
211 params: IndexSet<String>,
212 has_wildcard: bool,
213}
214
215impl CaptureNames {
216 fn push(&mut self, segment: &str) -> Result<(), Error> {
217 if self.has_wildcard {
218 failure::bail!("The wildcard parameter has already set");
219 }
220
221 let (kind, name) = segment.split_at(1);
222 match kind {
223 ":" | "*" => {}
224 "" => failure::bail!("empty segment"),
225 c => failure::bail!("unknown parameter kind: '{}'", c),
226 }
227
228 if name.is_empty() {
229 failure::bail!("empty parameter name");
230 }
231
232 if !self.params.insert(name.into()) {
233 failure::bail!("the duplicated parameter name");
234 }
235
236 if kind == "*" {
237 self.has_wildcard = true;
238 }
239
240 Ok(())
241 }
242
243 fn extend<T>(&mut self, names: impl IntoIterator<Item = T>) -> Result<(), Error>
244 where
245 T: AsRef<str>,
246 {
247 for name in names {
248 self.push(name.as_ref())?;
249 }
250 Ok(())
251 }
252
253 pub fn position(&self, name: &str) -> Option<usize> {
254 Some(self.params.get_full(name)?.0)
255 }
256}
257
258#[cfg_attr(feature = "cargo-clippy", allow(non_ascii_literal))]
259#[cfg(test)]
260mod tests {
261 use super::*;
262
263 macro_rules! t {
264 (@case $name:ident, $input:expr, $expected:expr) => {
265 #[test]
266 fn $name() {
267 assert_eq!($input.ok().map(|uri: Uri| uri.0), Some($expected.0));
268 }
269 };
270 ($(
271 $name:ident ($input:expr, $expected:expr);
272 )*) => {$(
273 t!(@case $name, $input, $expected);
274 )*};
275 }
276
277 t! [
278 parse_uri_root(
279 "/".parse(),
280 Uri::root()
281 );
282 parse_uri_static(
283 "/path/to/lib".parse(),
284 Uri::static_("/path/to/lib")
285 );
286 parse_uri_static_has_trailing_slash(
287 "/path/to/lib/".parse(),
288 Uri::static_("/path/to/lib/")
289 );
290 parse_uri_has_wildcard_params(
291 "/api/v1/:param/*path".parse(),
292 Uri::captured(
293 "/api/v1/:param/*path",
294 CaptureNames {
295 params: indexset!["param".into(), "path".into()],
296 has_wildcard: true,
297 }
298 )
299 );
300 ];
301
302 #[test]
303 fn parse_uri_failcase_empty() {
304 assert!("".parse::<Uri>().is_err());
305 }
306
307 #[test]
308 fn parse_uri_failcase_without_prefix_root() {
309 assert!("foo/bar".parse::<Uri>().is_err());
310 }
311
312 #[test]
313 fn parse_uri_failcase_duplicated_slashes() {
314 assert!("//foo/bar/".parse::<Uri>().is_err());
315 assert!("/foo//bar/".parse::<Uri>().is_err());
316 assert!("/foo/bar//".parse::<Uri>().is_err());
317 }
318
319 #[test]
320 fn parse_uri_failcase_invalid_wildcard_specifier_pos() {
321 assert!("/pa:th".parse::<Uri>().is_err());
322 }
323
324 #[test]
325 fn parse_uri_failcase_non_ascii() {
326 assert!("/パス".parse::<Uri>().is_err());
328 }
329
330 #[test]
331 fn parse_uri_failcase_duplicated_param_name() {
332 assert!("/:id/:id".parse::<Uri>().is_err());
333 }
334
335 #[test]
336 fn parse_uri_failcase_after_wildcard_name() {
337 assert!("/path/to/*a/id".parse::<Uri>().is_err());
338 }
339
340 t! [
341 join_roots(
342 Uri::root().join(Uri::root()),
343 Uri::root()
344 );
345 join_root_and_static(
346 Uri::root().join(Uri::static_("/path/to")),
347 Uri::static_("/path/to")
348 );
349 join_trailing_slash_before_root_1(
350 Uri::static_("/path/to/").join(Uri::root()),
351 Uri::static_("/path/to/")
352 );
353 join_trailing_slash_before_root_2(
354 Uri::static_("/path/to").join(Uri::root()),
355 Uri::static_("/path/to")
356 );
357 join_trailing_slash_before_static_1(
358 Uri::static_("/path").join(Uri::static_("/to")),
359 Uri::static_("/path/to")
360 );
361 join_trailing_slash_before_static_2(
362 Uri::static_("/path/").join(Uri::static_("/to")),
363 Uri::static_("/path/to")
364 );
365 ];
366}