fuchsia_url/
unpinned_absolute_package_url.rs

1// Copyright 2022 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::errors::ParseError;
6use crate::parse::{PackageName, PackageVariant};
7use crate::{AbsolutePackageUrl, RepositoryUrl};
8
9/// A URL locating a Fuchsia package. Cannot have a hash.
10/// Has the form "fuchsia-pkg://<repository>/<name>[/variant]" where:
11///   * "repository" is a valid hostname
12///   * "name" is a valid package name
13///   * "variant" is an optional valid package variant
14/// https://fuchsia.dev/fuchsia-src/concepts/packages/package_url
15#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub struct UnpinnedAbsolutePackageUrl {
17    repo: RepositoryUrl,
18    name: PackageName,
19    // TODO(https://fxbug.dev/335388895): Remove variant concept
20    variant: Option<PackageVariant>,
21}
22
23impl UnpinnedAbsolutePackageUrl {
24    /// Create an UnpinnedAbsolutePackageUrl from its component parts.
25    pub fn new(repo: RepositoryUrl, name: PackageName, variant: Option<PackageVariant>) -> Self {
26        Self { repo, name, variant }
27    }
28
29    /// Create an UnpinnedAbsolutePackageUrl from a RepositoryUrl and a &str `path` that will
30    /// be validated.
31    pub fn new_with_path(repo: RepositoryUrl, path: &str) -> Result<Self, ParseError> {
32        let (name, variant) = crate::parse_path_to_name_and_variant(path)?;
33        Ok(Self::new(repo, name, variant))
34    }
35
36    /// Parse a "fuchsia-pkg://" URL that locates an unpinned (no hash query parameter) package.
37    pub fn parse(url: &str) -> Result<Self, ParseError> {
38        match AbsolutePackageUrl::parse(url)? {
39            AbsolutePackageUrl::Unpinned(unpinned) => Ok(unpinned),
40            AbsolutePackageUrl::Pinned(_) => Err(ParseError::CannotContainHash),
41        }
42    }
43
44    /// The Repository URL behind this URL (this URL without the path).
45    pub fn repository(&self) -> &RepositoryUrl {
46        &self.repo
47    }
48
49    /// The package name.
50    pub fn name(&self) -> &PackageName {
51        &self.name
52    }
53
54    /// The optional package variant.
55    pub fn variant(&self) -> Option<&PackageVariant> {
56        self.variant.as_ref()
57    }
58
59    /// The path ("/name[/variant]").
60    pub fn path(&self) -> String {
61        match &self.variant {
62            Some(variant) => format!("/{}/{}", self.name, variant),
63            None => format!("/{}", self.name),
64        }
65    }
66
67    /// Change the repository to `repository`.
68    pub fn set_repository(&mut self, repository: RepositoryUrl) -> &mut Self {
69        self.repo = repository;
70        self
71    }
72
73    /// Clear the variant if there is one.
74    pub fn clear_variant(&mut self) -> &mut Self {
75        self.variant = None;
76        self
77    }
78}
79
80// UnpinnedAbsolutePackageUrl does not maintain any invariants on its `repo` field in addition to
81// those already maintained by RepositoryUrl so this is safe.
82impl std::ops::Deref for UnpinnedAbsolutePackageUrl {
83    type Target = RepositoryUrl;
84
85    fn deref(&self) -> &Self::Target {
86        &self.repo
87    }
88}
89
90impl std::str::FromStr for UnpinnedAbsolutePackageUrl {
91    type Err = ParseError;
92
93    fn from_str(url: &str) -> Result<Self, Self::Err> {
94        Self::parse(url)
95    }
96}
97
98impl std::convert::TryFrom<&str> for UnpinnedAbsolutePackageUrl {
99    type Error = ParseError;
100
101    fn try_from(value: &str) -> Result<Self, Self::Error> {
102        Self::parse(value)
103    }
104}
105
106impl std::fmt::Display for UnpinnedAbsolutePackageUrl {
107    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108        let () = write!(f, "{}/{}", self.repo, self.name)?;
109        if let Some(variant) = &self.variant {
110            let () = write!(f, "/{}", variant)?;
111        }
112        Ok(())
113    }
114}
115
116impl serde::Serialize for UnpinnedAbsolutePackageUrl {
117    fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
118        self.to_string().serialize(ser)
119    }
120}
121
122impl<'de> serde::Deserialize<'de> for UnpinnedAbsolutePackageUrl {
123    fn deserialize<D>(de: D) -> Result<Self, D::Error>
124    where
125        D: serde::Deserializer<'de>,
126    {
127        let url = String::deserialize(de)?;
128        Ok(Self::parse(&url).map_err(|err| serde::de::Error::custom(err))?)
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use crate::errors::PackagePathSegmentError;
136    use assert_matches::assert_matches;
137    use std::convert::TryFrom as _;
138
139    #[test]
140    fn new_with_path_err() {
141        for (path, err) in [
142            ("", ParseError::PathMustHaveLeadingSlash),
143            ("/", ParseError::MissingName),
144            ("//", ParseError::InvalidName(PackagePathSegmentError::Empty)),
145            ("/name/variant/other", ParseError::ExtraPathSegments),
146        ] {
147            assert_matches!(
148                UnpinnedAbsolutePackageUrl::new_with_path(
149                    "fuchsia-pkg://example.org".parse().unwrap(),
150                    path.into(),
151                ),
152                Err(e) if e == err,
153                "the path {:?}", path
154            );
155        }
156    }
157
158    #[test]
159    fn new_with_path_ok() {
160        let repo = "fuchsia-pkg://example.org".parse::<RepositoryUrl>().unwrap();
161        let url = UnpinnedAbsolutePackageUrl::new_with_path(repo.clone(), "/name".into()).unwrap();
162        assert_eq!(url.name().as_ref(), "name");
163        assert_eq!(url.variant(), None);
164
165        let url = UnpinnedAbsolutePackageUrl::new_with_path(repo.clone(), "/name/variant".into())
166            .unwrap();
167        assert_eq!(url.name().as_ref(), "name");
168        assert_eq!(url.variant().map(|v| v.as_ref()), Some("variant"));
169    }
170
171    #[test]
172    fn parse_err() {
173        for (url, err) in [
174            ("fuchsia-boot://example.org/name", ParseError::InvalidScheme),
175            ("fuchsia-pkg://", ParseError::MissingHost),
176            ("fuchsia-pkg://exaMple.org", ParseError::InvalidHost),
177            ("fuchsia-pkg://example.org/", ParseError::MissingName),
178            (
179                "fuchsia-pkg://example.org//",
180                ParseError::InvalidPathSegment(PackagePathSegmentError::Empty),
181            ),
182            ("fuchsia-pkg://example.org/name/variant/extra", ParseError::ExtraPathSegments),
183            ("fuchsia-pkg://example.org/name#resource", ParseError::CannotContainResource),
184            ("fuchsia-pkg://example.org/name?hash=0000000000000000000000000000000000000000000000000000000000000000", ParseError::CannotContainHash)
185        ] {
186            assert_matches!(
187                UnpinnedAbsolutePackageUrl::parse(url),
188                Err(e) if e == err,
189                "the url {:?}", url
190            );
191            assert_matches!(
192                url.parse::<UnpinnedAbsolutePackageUrl>(),
193                Err(e) if e == err,
194                "the url {:?}", url
195            );
196            assert_matches!(
197                UnpinnedAbsolutePackageUrl::try_from(url),
198                Err(e) if e == err,
199                "the url {:?}", url
200            );
201            assert_matches!(
202                serde_json::from_str::<UnpinnedAbsolutePackageUrl>(url),
203                Err(_),
204                "the url {:?}",
205                url
206            );
207        }
208    }
209
210    #[test]
211    fn parse_ok() {
212        for (url, host, name, variant, path) in [
213            ("fuchsia-pkg://example.org/name", "example.org", "name", None, "/name"),
214            (
215                "fuchsia-pkg://example.org/name/variant",
216                "example.org",
217                "name",
218                Some("variant"),
219                "/name/variant",
220            ),
221        ] {
222            let json_url = format!("\"{url}\"");
223
224            // Creation
225            let name = name.parse::<crate::PackageName>().unwrap();
226            let variant = variant.map(|v| v.parse::<crate::PackageVariant>().unwrap());
227            let validate = |parsed: &UnpinnedAbsolutePackageUrl| {
228                assert_eq!(parsed.host(), host);
229                assert_eq!(parsed.name(), &name);
230                assert_eq!(parsed.variant(), variant.as_ref());
231                assert_eq!(parsed.path(), path);
232            };
233            validate(&UnpinnedAbsolutePackageUrl::parse(url).unwrap());
234            validate(&url.parse::<UnpinnedAbsolutePackageUrl>().unwrap());
235            validate(&UnpinnedAbsolutePackageUrl::try_from(url).unwrap());
236            validate(&serde_json::from_str::<UnpinnedAbsolutePackageUrl>(&json_url).unwrap());
237
238            // Stringification
239            assert_eq!(
240                UnpinnedAbsolutePackageUrl::parse(url).unwrap().to_string(),
241                url,
242                "the url {:?}",
243                url
244            );
245            assert_eq!(
246                serde_json::to_string(&UnpinnedAbsolutePackageUrl::parse(url).unwrap()).unwrap(),
247                json_url,
248                "the url {:?}",
249                url
250            );
251        }
252    }
253
254    #[test]
255    fn set_repository() {
256        let mut url = UnpinnedAbsolutePackageUrl::parse("fuchsia-pkg://example.org/name").unwrap();
257
258        url.set_repository("fuchsia-pkg://example.com".parse().unwrap());
259
260        assert_eq!(url.host(), "example.com");
261    }
262
263    #[test]
264    fn clear_variant() {
265        let mut url =
266            UnpinnedAbsolutePackageUrl::parse("fuchsia-pkg://example.org/name/variant").unwrap();
267        url.clear_variant();
268        assert_eq!(url.variant(), None);
269
270        let mut url = UnpinnedAbsolutePackageUrl::parse("fuchsia-pkg://example.org/name").unwrap();
271        url.clear_variant();
272        assert_eq!(url.variant(), None);
273    }
274}