1use crate::errors::ParseError;
6use crate::parse::{PackageName, PackageVariant};
7use crate::{AbsolutePackageUrl, RepositoryUrl};
8
9#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
16pub struct UnpinnedAbsolutePackageUrl {
17 repo: RepositoryUrl,
18 name: PackageName,
19 variant: Option<PackageVariant>,
21}
22
23impl UnpinnedAbsolutePackageUrl {
24 pub fn new(repo: RepositoryUrl, name: PackageName, variant: Option<PackageVariant>) -> Self {
26 Self { repo, name, variant }
27 }
28
29 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 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 pub fn repository(&self) -> &RepositoryUrl {
46 &self.repo
47 }
48
49 pub fn name(&self) -> &PackageName {
51 &self.name
52 }
53
54 pub fn variant(&self) -> Option<&PackageVariant> {
56 self.variant.as_ref()
57 }
58
59 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 pub fn set_repository(&mut self, repository: RepositoryUrl) -> &mut Self {
69 self.repo = repository;
70 self
71 }
72
73 pub fn clear_variant(&mut self) -> &mut Self {
75 self.variant = None;
76 self
77 }
78}
79
80impl 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 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 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}