Skip to main content

fuchsia_url/
path.rs

1// Copyright 2026 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::ParseError;
6
7/// One or more valid path segments separated by forward slashes.
8#[derive(Debug, PartialEq, Eq, Clone)]
9pub struct Path(String);
10
11impl TryFrom<String> for Path {
12    type Error = ParseError;
13    fn try_from(value: String) -> Result<Self, Self::Error> {
14        let () = validate_path(&value)?;
15        Ok(Self(value))
16    }
17}
18
19impl std::str::FromStr for Path {
20    type Err = ParseError;
21    fn from_str(s: &str) -> Result<Self, Self::Err> {
22        let () = validate_path(s)?;
23        Ok(Self(s.to_owned()))
24    }
25}
26
27impl std::fmt::Display for Path {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        self.0.fmt(f)
30    }
31}
32
33impl AsRef<str> for Path {
34    fn as_ref(&self) -> &str {
35        &self.0
36    }
37}
38
39impl std::ops::Deref for Path {
40    type Target = str;
41    fn deref(&self) -> &Self::Target {
42        self.0.as_str()
43    }
44}
45
46impl From<crate::PackageName> for Path {
47    fn from(other: crate::PackageName) -> Self {
48        // A package name is a valid `Path` segment.
49        debug_assert!(validate_path(other.as_ref()).is_ok());
50        Self(other.into())
51    }
52}
53
54// Succeeds if `path` is one or more valid path segments separated by forward slashes.
55fn validate_path(path: &str) -> Result<(), crate::ParseError> {
56    for s in path.split('/') {
57        let () = crate::parse::validate_package_path_segment(s)
58            .map_err(crate::ParseError::InvalidPathSegment)?;
59    }
60    Ok(())
61}
62
63// Validates that `path` is "name[/variant]" and returns the name and optional variant if so.
64pub(crate) fn parse_path_to_name_and_variant(
65    path: &str,
66) -> Result<(crate::PackageName, Option<crate::PackageVariant>), ParseError> {
67    if path.is_empty() {
68        return Err(ParseError::MissingName);
69    }
70    let mut iter = path.split('/').fuse();
71    let name = if let Some(s) = iter.next() {
72        s.parse().map_err(ParseError::InvalidName)?
73    } else {
74        return Err(ParseError::MissingName);
75    };
76    let variant = if let Some(s) = iter.next() {
77        Some(s.parse().map_err(ParseError::InvalidVariant)?)
78    } else {
79        None
80    };
81    if let Some(_) = iter.next() {
82        return Err(ParseError::ExtraPathSegments);
83    }
84    Ok((name, variant))
85}
86
87#[cfg(test)]
88mod test {
89    use super::*;
90    use assert_matches::assert_matches;
91
92    macro_rules! test_err {
93        (
94            $(
95                $test_name:ident => {
96                    path = $path:expr,
97                    err = $err:pat,
98                }
99            )+
100        ) => {
101            $(
102                #[test]
103                fn $test_name() {
104                    assert_matches!(
105                        validate_path($path),
106                        Err($err)
107                    );
108                }
109            )+
110        }
111    }
112
113    test_err! {
114        err_empty_path => {
115            path = "",
116            err = crate::ParseError::InvalidPathSegment(_),
117        }
118        err_leading_slash => {
119            path = "/leading-slash",
120            err = crate::ParseError::InvalidPathSegment(_),
121        }
122        err_trailing_slash => {
123            path = "name/",
124            err = crate::ParseError::InvalidPathSegment(_),
125        }
126        err_empty_segment => {
127            path = "name//trailing",
128            err = crate::ParseError::InvalidPathSegment(_),
129        }
130        err_invalid_segment => {
131            path = "name/#/trailing",
132            err = crate::ParseError::InvalidPathSegment(_),
133        }
134    }
135
136    #[test]
137    fn success() {
138        for path in ["name", "name/other", "name/other/more"] {
139            let () = validate_path(path).unwrap();
140        }
141    }
142}
143
144#[cfg(test)]
145mod test_parse_path_to_name_and_variant {
146    use super::*;
147    use assert_matches::assert_matches;
148
149    macro_rules! test_err {
150        (
151            $(
152                $test_name:ident => {
153                    path = $path:expr,
154                    err = $err:pat,
155                }
156            )+
157        ) => {
158            $(
159                #[test]
160                fn $test_name() {
161                    assert_matches!(
162                        parse_path_to_name_and_variant($path),
163                        Err($err)
164                    );
165                }
166            )+
167        }
168    }
169
170    test_err! {
171        err_no_name => {
172            path = "/",
173            err = ParseError::InvalidName(_),
174        }
175        err_leading_slash => {
176            path = "/name",
177            err = ParseError::InvalidName(_),
178        }
179        err_empty_variant => {
180            path = "name/",
181            err = ParseError::InvalidVariant(_),
182        }
183        err_trailing_slash => {
184            path = "name/variant/",
185            err = ParseError::ExtraPathSegments,
186        }
187        err_extra_segment => {
188            path = "name/variant/extra",
189            err = ParseError::ExtraPathSegments,
190        }
191        err_invalid_segment => {
192            path = "name/#",
193            err = ParseError::InvalidVariant(_),
194        }
195    }
196
197    #[test]
198    fn success() {
199        assert_eq!(
200            ("name".parse().unwrap(), None),
201            parse_path_to_name_and_variant("name").unwrap()
202        );
203        assert_eq!(
204            ("name".parse().unwrap(), Some("variant".parse().unwrap())),
205            parse_path_to_name_and_variant("name/variant").unwrap()
206        );
207    }
208}