Skip to main content

fuchsia_url/
fuchsia_pkg_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::{FuchsiaPkgAbsolutePackageUrl, 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 FuchsiaPkgUnpinnedAbsolutePackageUrl {
17    repo: RepositoryUrl,
18    name: PackageName,
19    // TODO(https://fxbug.dev/335388895): Remove variant concept
20    variant: Option<PackageVariant>,
21}
22
23impl FuchsiaPkgUnpinnedAbsolutePackageUrl {
24    /// Create an FuchsiaPkgUnpinnedAbsolutePackageUrl 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 FuchsiaPkgUnpinnedAbsolutePackageUrl from a RepositoryUrl and a &str `path` that
30    /// will 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 FuchsiaPkgAbsolutePackageUrl::parse(url)? {
39            FuchsiaPkgAbsolutePackageUrl::Unpinned(unpinned) => Ok(unpinned),
40            FuchsiaPkgAbsolutePackageUrl::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 => self.name.to_string(),
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// FuchsiaPkgUnpinnedAbsolutePackageUrl does not maintain any invariants on its `repo` field in
81// addition to those already maintained by RepositoryUrl so this is safe.
82impl std::ops::Deref for FuchsiaPkgUnpinnedAbsolutePackageUrl {
83    type Target = RepositoryUrl;
84
85    fn deref(&self) -> &Self::Target {
86        &self.repo
87    }
88}
89
90impl std::str::FromStr for FuchsiaPkgUnpinnedAbsolutePackageUrl {
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 FuchsiaPkgUnpinnedAbsolutePackageUrl {
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 FuchsiaPkgUnpinnedAbsolutePackageUrl {
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 FuchsiaPkgUnpinnedAbsolutePackageUrl {
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 FuchsiaPkgUnpinnedAbsolutePackageUrl {
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::MissingName),
143            ("/", ParseError::InvalidName(PackagePathSegmentError::Empty)),
144            ("name/variant/other", ParseError::ExtraPathSegments),
145        ] {
146            assert_matches!(
147                FuchsiaPkgUnpinnedAbsolutePackageUrl::new_with_path(
148                    "fuchsia-pkg://example.org".parse().unwrap(),
149                    path.into(),
150                ),
151                Err(e) if e == err,
152                "the path {:?}", path
153            );
154        }
155    }
156
157    #[test]
158    fn new_with_path_ok() {
159        let repo = "fuchsia-pkg://example.org".parse::<RepositoryUrl>().unwrap();
160        let url = FuchsiaPkgUnpinnedAbsolutePackageUrl::new_with_path(repo.clone(), "name".into())
161            .unwrap();
162        assert_eq!(url.name().as_ref(), "name");
163        assert_eq!(url.variant(), None);
164
165        let url = FuchsiaPkgUnpinnedAbsolutePackageUrl::new_with_path(
166            repo.clone(),
167            "name/variant".into(),
168        )
169        .unwrap();
170        assert_eq!(url.name().as_ref(), "name");
171        assert_eq!(url.variant().map(|v| v.as_ref()), Some("variant"));
172    }
173
174    #[test]
175    fn parse_err() {
176        for (url, err) in [
177            ("fuchsia-boot://example.org/name", ParseError::InvalidScheme),
178            ("fuchsia-pkg://", ParseError::MissingHost),
179            ("fuchsia-pkg://exaMple.org", ParseError::InvalidHost),
180            ("fuchsia-pkg://example.org/", ParseError::MissingName),
181            (
182                "fuchsia-pkg://example.org//",
183                ParseError::InvalidPathSegment(PackagePathSegmentError::Empty),
184            ),
185            ("fuchsia-pkg://example.org/name/variant/extra", ParseError::ExtraPathSegments),
186            ("fuchsia-pkg://example.org/name#resource", ParseError::CannotContainResource),
187            (
188                "fuchsia-pkg://example.org/name?hash=0000000000000000000000000000000000000000000000000000000000000000",
189                ParseError::CannotContainHash,
190            ),
191        ] {
192            assert_matches!(
193                FuchsiaPkgUnpinnedAbsolutePackageUrl::parse(url),
194                Err(e) if e == err,
195                "the url {:?}", url
196            );
197            assert_matches!(
198                url.parse::<FuchsiaPkgUnpinnedAbsolutePackageUrl>(),
199                Err(e) if e == err,
200                "the url {:?}", url
201            );
202            assert_matches!(
203                FuchsiaPkgUnpinnedAbsolutePackageUrl::try_from(url),
204                Err(e) if e == err,
205                "the url {:?}", url
206            );
207            assert_matches!(
208                serde_json::from_str::<FuchsiaPkgUnpinnedAbsolutePackageUrl>(url),
209                Err(_),
210                "the url {:?}",
211                url
212            );
213        }
214    }
215
216    #[test]
217    fn parse_ok() {
218        for (url, host, name, variant, path) in [
219            ("fuchsia-pkg://example.org/name", "example.org", "name", None, "name"),
220            (
221                "fuchsia-pkg://example.org/name/variant",
222                "example.org",
223                "name",
224                Some("variant"),
225                "name/variant",
226            ),
227        ] {
228            let json_url = format!("\"{url}\"");
229
230            // Creation
231            let name = name.parse::<crate::PackageName>().unwrap();
232            let variant = variant.map(|v| v.parse::<crate::PackageVariant>().unwrap());
233            let validate = |parsed: &FuchsiaPkgUnpinnedAbsolutePackageUrl| {
234                assert_eq!(parsed.host(), host);
235                assert_eq!(parsed.name(), &name);
236                assert_eq!(parsed.variant(), variant.as_ref());
237                assert_eq!(parsed.path(), path);
238            };
239            validate(&FuchsiaPkgUnpinnedAbsolutePackageUrl::parse(url).unwrap());
240            validate(&url.parse::<FuchsiaPkgUnpinnedAbsolutePackageUrl>().unwrap());
241            validate(&FuchsiaPkgUnpinnedAbsolutePackageUrl::try_from(url).unwrap());
242            validate(
243                &serde_json::from_str::<FuchsiaPkgUnpinnedAbsolutePackageUrl>(&json_url).unwrap(),
244            );
245
246            // Stringification
247            assert_eq!(
248                FuchsiaPkgUnpinnedAbsolutePackageUrl::parse(url).unwrap().to_string(),
249                url,
250                "the url {:?}",
251                url
252            );
253            assert_eq!(
254                serde_json::to_string(&FuchsiaPkgUnpinnedAbsolutePackageUrl::parse(url).unwrap())
255                    .unwrap(),
256                json_url,
257                "the url {:?}",
258                url
259            );
260        }
261    }
262
263    #[test]
264    fn set_repository() {
265        let mut url =
266            FuchsiaPkgUnpinnedAbsolutePackageUrl::parse("fuchsia-pkg://example.org/name").unwrap();
267
268        url.set_repository("fuchsia-pkg://example.com".parse().unwrap());
269
270        assert_eq!(url.host(), "example.com");
271    }
272
273    #[test]
274    fn clear_variant() {
275        let mut url =
276            FuchsiaPkgUnpinnedAbsolutePackageUrl::parse("fuchsia-pkg://example.org/name/variant")
277                .unwrap();
278        url.clear_variant();
279        assert_eq!(url.variant(), None);
280
281        let mut url =
282            FuchsiaPkgUnpinnedAbsolutePackageUrl::parse("fuchsia-pkg://example.org/name").unwrap();
283        url.clear_variant();
284        assert_eq!(url.variant(), None);
285    }
286}