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