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 file_stem(&self) -> Option<&str> {
72 std::path::Path::new(self.resource.as_ref()).file_stem()?.to_str()
73 }
74
75 pub fn package_url(&self) -> &FuchsiaPkgAbsolutePackageUrl {
77 &self.package
78 }
79
80 pub fn into_package_and_resource(self) -> (FuchsiaPkgAbsolutePackageUrl, Resource) {
82 let Self { package, resource } = self;
83 (package, resource)
84 }
85}
86
87impl std::ops::Deref for FuchsiaPkgAbsoluteComponentUrl {
90 type Target = FuchsiaPkgAbsolutePackageUrl;
91
92 fn deref(&self) -> &Self::Target {
93 &self.package
94 }
95}
96
97impl std::ops::DerefMut for FuchsiaPkgAbsoluteComponentUrl {
100 fn deref_mut(&mut self) -> &mut Self::Target {
101 &mut self.package
102 }
103}
104
105impl std::str::FromStr for FuchsiaPkgAbsoluteComponentUrl {
106 type Err = ParseError;
107
108 fn from_str(url: &str) -> Result<Self, Self::Err> {
109 Self::parse(url)
110 }
111}
112
113impl std::convert::TryFrom<&str> for FuchsiaPkgAbsoluteComponentUrl {
114 type Error = ParseError;
115
116 fn try_from(value: &str) -> Result<Self, Self::Error> {
117 Self::parse(value)
118 }
119}
120
121impl std::fmt::Display for FuchsiaPkgAbsoluteComponentUrl {
122 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123 write!(f, "{}#{}", self.package, self.resource.percent_encode())
124 }
125}
126
127impl serde::Serialize for FuchsiaPkgAbsoluteComponentUrl {
128 fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
129 self.to_string().serialize(ser)
130 }
131}
132
133impl<'de> serde::Deserialize<'de> for FuchsiaPkgAbsoluteComponentUrl {
134 fn deserialize<D>(de: D) -> Result<Self, D::Error>
135 where
136 D: serde::Deserializer<'de>,
137 {
138 let url = String::deserialize(de)?;
139 Ok(Self::parse(&url).map_err(|err| serde::de::Error::custom(err))?)
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146 use crate::ResourcePathError;
147 use crate::errors::PackagePathSegmentError;
148 use assert_matches::assert_matches;
149 use std::convert::TryFrom as _;
150
151 #[test]
152 fn parse_err() {
153 for (url, err) in [
154 ("example.org/name#resource", ParseError::MissingScheme),
155 ("//example.org/name#resource", ParseError::MissingScheme),
156 ("///name#resource", ParseError::MissingScheme),
157 ("/name#resource", ParseError::MissingScheme),
158 ("name#resource", ParseError::MissingScheme),
159 ("fuchsia-boot://example.org/name#resource", ParseError::InvalidScheme),
160 ("fuchsia-pkg:///name#resource", ParseError::MissingHost),
161 ("fuchsia-pkg://exaMple.org/name#resource", ParseError::InvalidHost),
162 ("fuchsia-pkg://example.org#resource", ParseError::MissingName),
163 (
164 "fuchsia-pkg://example.org//#resource",
165 ParseError::InvalidPathSegment(PackagePathSegmentError::Empty),
166 ),
167 (
168 "fuchsia-pkg://example.org/name/variant/extra#resource",
169 ParseError::ExtraPathSegments,
170 ),
171 ("fuchsia-pkg://example.org/name#", ParseError::MissingResource),
172 (
173 "fuchsia-pkg://example.org/name#/",
174 ParseError::InvalidResourcePath(ResourcePathError::PathStartsWithSlash),
175 ),
176 (
177 "fuchsia-pkg://example.org/name#resource/",
178 ParseError::InvalidResourcePath(ResourcePathError::PathEndsWithSlash),
179 ),
180 ] {
181 assert_matches!(
182 FuchsiaPkgAbsoluteComponentUrl::parse(url),
183 Err(e) if e == err,
184 "the url {:?}", url
185 );
186 assert_matches!(
187 url.parse::<FuchsiaPkgAbsoluteComponentUrl>(),
188 Err(e) if e == err,
189 "the url {:?}", url
190 );
191 assert_matches!(
192 FuchsiaPkgAbsoluteComponentUrl::try_from(url),
193 Err(e) if e == err,
194 "the url {:?}", url
195 );
196 assert_matches!(
197 serde_json::from_str::<FuchsiaPkgAbsoluteComponentUrl>(url),
198 Err(_),
199 "the url {:?}",
200 url
201 );
202 }
203 }
204
205 #[test]
206 fn parse_ok() {
207 for (url, variant, hash, resource) in [
208 ("fuchsia-pkg://example.org/name#resource", None, None, "resource"),
209 ("fuchsia-pkg://example.org/name/variant#resource", Some("variant"), None, "resource"),
210 (
211 "fuchsia-pkg://example.org/name?hash=0000000000000000000000000000000000000000000000000000000000000000#resource",
212 None,
213 Some("0000000000000000000000000000000000000000000000000000000000000000"),
214 "resource",
215 ),
216 ("fuchsia-pkg://example.org/name#%E2%98%BA", None, None, "☺"),
217 ] {
218 let json_url = format!("\"{url}\"");
219 let host = "example.org";
220 let name = "name";
221
222 let name = name.parse::<crate::PackageName>().unwrap();
224 let variant = variant.map(|v| v.parse::<crate::PackageVariant>().unwrap());
225 let hash = hash.map(|h| h.parse::<Hash>().unwrap());
226 let resource = resource.parse::<crate::Resource>().unwrap();
227 let validate = |parsed: &FuchsiaPkgAbsoluteComponentUrl| {
228 assert_eq!(parsed.host(), host);
229 assert_eq!(parsed.name(), &name);
230 assert_eq!(parsed.variant(), variant.as_ref());
231 assert_eq!(parsed.hash(), hash);
232 assert_eq!(parsed.resource(), &resource);
233 };
234 validate(&FuchsiaPkgAbsoluteComponentUrl::parse(url).unwrap());
235 validate(&url.parse::<FuchsiaPkgAbsoluteComponentUrl>().unwrap());
236 validate(&FuchsiaPkgAbsoluteComponentUrl::try_from(url).unwrap());
237 validate(&serde_json::from_str::<FuchsiaPkgAbsoluteComponentUrl>(&json_url).unwrap());
238
239 assert_eq!(
241 FuchsiaPkgAbsoluteComponentUrl::parse(url).unwrap().to_string(),
242 url,
243 "the url {:?}",
244 url
245 );
246 assert_eq!(
247 serde_json::to_string(&FuchsiaPkgAbsoluteComponentUrl::parse(url).unwrap())
248 .unwrap(),
249 json_url,
250 "the url {:?}",
251 url
252 );
253 }
254 }
255
256 #[test]
257 fn from_package_url_and_resource_err() {
260 for (resource, err) in [
261 ("", ParseError::InvalidResourcePath(ResourcePathError::PathIsEmpty)),
262 ("/", ParseError::InvalidResourcePath(ResourcePathError::PathStartsWithSlash)),
263 ] {
264 let package =
265 "fuchsia-pkg://example.org/name".parse::<FuchsiaPkgAbsolutePackageUrl>().unwrap();
266 assert_eq!(
267 FuchsiaPkgAbsoluteComponentUrl::from_package_url_and_resource(
268 package,
269 resource.into()
270 ),
271 Err(err),
272 "the resource {:?}",
273 resource
274 );
275 }
276 }
277
278 #[test]
279 fn from_package_url_and_resource_ok() {
280 let package =
281 "fuchsia-pkg://example.org/name".parse::<FuchsiaPkgAbsolutePackageUrl>().unwrap();
282
283 let component = FuchsiaPkgAbsoluteComponentUrl::from_package_url_and_resource(
284 package.clone(),
285 "resource".into(),
286 )
287 .unwrap();
288 assert_eq!(component.resource().as_ref(), "resource");
289
290 let component = FuchsiaPkgAbsoluteComponentUrl::from_package_url_and_resource(
291 package.clone(),
292 "☺".into(),
293 )
294 .unwrap();
295 assert_eq!(component.resource().as_ref(), "☺");
296 }
297
298 #[test]
299 fn file_stem() {
300 for (url, expected) in [
301 ("fuchsia-pkg://example.org/name#meta/comp.cm", Some("comp")),
302 ("fuchsia-pkg://example.org/name#meta/my.comp.cm", Some("my.comp")),
303 ("fuchsia-pkg://example.org/name#resource", Some("resource")),
304 ] {
305 let parsed = FuchsiaPkgAbsoluteComponentUrl::parse(url).unwrap();
306 assert_eq!(parsed.file_stem(), expected, "the url {:?}", url);
307 }
308 }
309}