1use crate::errors::ParseError;
6use crate::parse::{PackageName, PackageVariant};
7use crate::{PinnedAbsolutePackageUrl, RepositoryUrl, UnpinnedAbsolutePackageUrl, UrlParts};
8use fuchsia_hash::Hash;
9
10#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
18pub enum AbsolutePackageUrl {
19 Unpinned(UnpinnedAbsolutePackageUrl),
20 Pinned(PinnedAbsolutePackageUrl),
21}
22
23impl AbsolutePackageUrl {
24 pub(crate) fn from_parts(parts: UrlParts) -> Result<Self, ParseError> {
25 let UrlParts { scheme, host, path, hash, resource } = parts;
26 let repo = RepositoryUrl::new(
27 scheme.ok_or(ParseError::MissingScheme)?,
28 host.ok_or(ParseError::MissingHost)?,
29 )?;
30 if resource.is_some() {
31 return Err(ParseError::CannotContainResource);
32 }
33 Self::new_with_path(repo, &path, hash)
34 }
35
36 pub fn parse(url: &str) -> Result<Self, ParseError> {
38 Self::from_parts(UrlParts::parse(url)?)
39 }
40
41 pub fn from_url(url: &url::Url) -> Result<Self, ParseError> {
42 Self::from_parts(UrlParts::from_url(url)?)
43 }
44
45 pub fn new_with_path(
48 repo: RepositoryUrl,
49 path: &str,
50 hash: Option<Hash>,
51 ) -> Result<Self, ParseError> {
52 Ok(match hash {
53 None => Self::Unpinned(UnpinnedAbsolutePackageUrl::new_with_path(repo, path)?),
54 Some(hash) => Self::Pinned(PinnedAbsolutePackageUrl::new_with_path(repo, path, hash)?),
55 })
56 }
57
58 pub fn new(
60 repo: RepositoryUrl,
61 name: PackageName,
62 variant: Option<PackageVariant>,
63 hash: Option<Hash>,
64 ) -> Self {
65 match hash {
66 None => Self::Unpinned(UnpinnedAbsolutePackageUrl::new(repo, name, variant)),
67 Some(hash) => Self::Pinned(PinnedAbsolutePackageUrl::new(repo, name, variant, hash)),
68 }
69 }
70
71 pub fn hash(&self) -> Option<Hash> {
73 match self {
74 Self::Unpinned(_) => None,
75 Self::Pinned(pinned) => Some(pinned.hash()),
76 }
77 }
78
79 pub fn name(&self) -> &PackageName {
80 match self {
81 Self::Unpinned(unpinned) => &unpinned.name(),
82 Self::Pinned(pinned) => pinned.name(),
83 }
84 }
85
86 pub fn as_unpinned(&self) -> &UnpinnedAbsolutePackageUrl {
88 match self {
89 Self::Unpinned(unpinned) => &unpinned,
90 Self::Pinned(pinned) => pinned.as_unpinned(),
91 }
92 }
93
94 pub fn pinned(self) -> Option<PinnedAbsolutePackageUrl> {
96 match self {
97 Self::Unpinned(_) => None,
98 Self::Pinned(pinned) => Some(pinned),
99 }
100 }
101}
102
103impl std::ops::Deref for AbsolutePackageUrl {
106 type Target = UnpinnedAbsolutePackageUrl;
107
108 fn deref(&self) -> &Self::Target {
109 match self {
110 Self::Unpinned(unpinned) => &unpinned,
111 Self::Pinned(pinned) => &pinned,
112 }
113 }
114}
115
116impl std::ops::DerefMut for AbsolutePackageUrl {
119 fn deref_mut(&mut self) -> &mut Self::Target {
120 match self {
121 Self::Unpinned(unpinned) => unpinned,
122 Self::Pinned(pinned) => pinned,
123 }
124 }
125}
126
127impl std::str::FromStr for AbsolutePackageUrl {
128 type Err = ParseError;
129
130 fn from_str(url: &str) -> Result<Self, Self::Err> {
131 Self::parse(url)
132 }
133}
134
135impl std::convert::TryFrom<&str> for AbsolutePackageUrl {
136 type Error = ParseError;
137
138 fn try_from(value: &str) -> Result<Self, Self::Error> {
139 Self::parse(value)
140 }
141}
142
143impl std::convert::From<PinnedAbsolutePackageUrl> for AbsolutePackageUrl {
144 fn from(pinned: PinnedAbsolutePackageUrl) -> Self {
145 Self::Pinned(pinned)
146 }
147}
148
149impl std::convert::From<UnpinnedAbsolutePackageUrl> for AbsolutePackageUrl {
150 fn from(unpinned: UnpinnedAbsolutePackageUrl) -> Self {
151 Self::Unpinned(unpinned)
152 }
153}
154
155impl std::fmt::Display for AbsolutePackageUrl {
156 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157 match self {
158 Self::Unpinned(unpinned) => write!(f, "{}", unpinned),
159 Self::Pinned(pinned) => write!(f, "{}", pinned),
160 }
161 }
162}
163
164impl serde::Serialize for AbsolutePackageUrl {
165 fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
166 self.to_string().serialize(ser)
167 }
168}
169
170impl<'de> serde::Deserialize<'de> for AbsolutePackageUrl {
171 fn deserialize<D>(de: D) -> Result<Self, D::Error>
172 where
173 D: serde::Deserializer<'de>,
174 {
175 let url = String::deserialize(de)?;
176 Ok(Self::parse(&url).map_err(|err| serde::de::Error::custom(err))?)
177 }
178}
179
180#[cfg(test)]
181mod tests {
182 use super::*;
183 use crate::errors::PackagePathSegmentError;
184 use assert_matches::assert_matches;
185 use std::convert::TryFrom as _;
186
187 #[test]
188 fn parse_err() {
189 for (url, err) in [
190 ("example.org/name", ParseError::MissingScheme),
191 ("//example.org/name", ParseError::MissingScheme),
192 ("///name", ParseError::MissingScheme),
193 ("/name", ParseError::MissingScheme),
194 ("name", ParseError::MissingScheme),
195 ("fuchsia-boot://example.org/name", ParseError::InvalidScheme),
196 ("fuchsia-pkg://", ParseError::MissingHost),
197 ("fuchsia-pkg://exaMple.org", ParseError::InvalidHost),
198 ("fuchsia-pkg://example.org/", ParseError::MissingName),
199 (
200 "fuchsia-pkg://example.org//",
201 ParseError::InvalidPathSegment(PackagePathSegmentError::Empty),
202 ),
203 ("fuchsia-pkg://example.org/name/variant/extra", ParseError::ExtraPathSegments),
204 ("fuchsia-pkg://example.org/name#resource", ParseError::CannotContainResource),
205 ] {
206 assert_matches!(
207 AbsolutePackageUrl::parse(url),
208 Err(e) if e == err,
209 "the url {:?}", url
210 );
211 assert_matches!(
212 url.parse::<AbsolutePackageUrl>(),
213 Err(e) if e == err,
214 "the url {:?}", url
215 );
216 assert_matches!(
217 AbsolutePackageUrl::try_from(url),
218 Err(e) if e == err,
219 "the url {:?}", url
220 );
221 assert_matches!(
222 serde_json::from_str::<AbsolutePackageUrl>(url),
223 Err(_),
224 "the url {:?}",
225 url
226 );
227 }
228 }
229
230 #[test]
231 fn parse_ok() {
232 for (url, host, name, variant, hash) in [
233 ("fuchsia-pkg://example.org/name", "example.org", "name", None, None),
234 ("fuchsia-pkg://example.org/name/variant", "example.org", "name", Some("variant"), None),
235 (
236 "fuchsia-pkg://example.org/name?hash=0000000000000000000000000000000000000000000000000000000000000000", "example.org", "name", None, Some("0000000000000000000000000000000000000000000000000000000000000000")),
237 ("fuchsia-pkg://example.org/name/variant?hash=0000000000000000000000000000000000000000000000000000000000000000", "example.org", "name", Some("variant"), Some("0000000000000000000000000000000000000000000000000000000000000000")),
238 ] {
239 let json_url = format!("\"{url}\"");
240
241 let name = name.parse::<crate::PackageName>().unwrap();
243 let variant = variant.map(|v| v.parse::<crate::PackageVariant>().unwrap());
244 let hash = hash.map(|h| h.parse::<Hash>().unwrap());
245 let validate = |parsed: &AbsolutePackageUrl| {
246 assert_eq!(parsed.host(), host);
247 assert_eq!(parsed.name(), &name);
248 assert_eq!(parsed.variant(), variant.as_ref());
249 assert_eq!(parsed.hash(), hash);
250 };
251 validate(&AbsolutePackageUrl::parse(url).unwrap());
252 validate(&url.parse::<AbsolutePackageUrl>().unwrap());
253 validate(&AbsolutePackageUrl::try_from(url).unwrap());
254 validate(&serde_json::from_str::<AbsolutePackageUrl>(&json_url).unwrap());
255
256 assert_eq!(
258 AbsolutePackageUrl::parse(url).unwrap().to_string(),
259 url,
260 "the url {:?}",
261 url
262 );
263 assert_eq!(
264 serde_json::to_string(&AbsolutePackageUrl::parse(url).unwrap()).unwrap(),
265 json_url,
266 "the url {:?}",
267 url
268 );
269 }
270 }
271}