1use crate::errors::ParseError;
6use crate::{RelativePackageUrl, Resource, UrlParts};
7
8#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub struct RelativeComponentUrl {
15 package: RelativePackageUrl,
16 resource: Resource,
17}
18
19impl RelativeComponentUrl {
20 pub(crate) fn from_parts(parts: UrlParts) -> Result<Self, ParseError> {
21 let UrlParts { scheme, host, path, hash, resource } = parts;
22 let package =
23 RelativePackageUrl::from_parts(UrlParts { scheme, host, path, hash, resource: None })?;
24 let resource = resource.ok_or(ParseError::MissingResource)?;
25 Ok(Self { package, resource })
26 }
27
28 pub fn parse(url: &str) -> Result<Self, ParseError> {
30 let relative_component_url = Self::from_parts(UrlParts::parse(url)?)?;
31 let () = crate::validate_inverse_relative_url(url)?;
32 Ok(relative_component_url)
33 }
34
35 pub fn package_url(&self) -> &RelativePackageUrl {
37 &self.package
38 }
39
40 pub fn resource(&self) -> &Resource {
42 &self.resource
43 }
44
45 pub(crate) fn into_package_and_resource(self) -> (RelativePackageUrl, Resource) {
46 let Self { package, resource } = self;
47 (package, resource)
48 }
49}
50
51impl std::str::FromStr for RelativeComponentUrl {
52 type Err = ParseError;
53
54 fn from_str(url: &str) -> Result<Self, Self::Err> {
55 Self::parse(url)
56 }
57}
58
59impl std::convert::TryFrom<&str> for RelativeComponentUrl {
60 type Error = ParseError;
61
62 fn try_from(value: &str) -> Result<Self, Self::Error> {
63 Self::parse(value)
64 }
65}
66
67impl std::fmt::Display for RelativeComponentUrl {
68 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69 write!(f, "{}#{}", self.package, self.resource.percent_encode())
70 }
71}
72
73impl serde::Serialize for RelativeComponentUrl {
74 fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
75 self.to_string().serialize(ser)
76 }
77}
78
79impl<'de> serde::Deserialize<'de> for RelativeComponentUrl {
80 fn deserialize<D>(de: D) -> Result<Self, D::Error>
81 where
82 D: serde::Deserializer<'de>,
83 {
84 let url = String::deserialize(de)?;
85 Ok(Self::parse(&url).map_err(|err| serde::de::Error::custom(err))?)
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92 use crate::errors::PackagePathSegmentError;
93 use crate::resource::ResourcePathError;
94 use assert_matches::assert_matches;
95 use std::convert::TryFrom as _;
96
97 #[test]
98 fn parse_err() {
99 for (url, err) in [
100 ("fuchsia-pkg://example.org/name#resource", ParseError::CannotContainScheme),
101 ("fuchsia-pkg:///name#resource", ParseError::CannotContainScheme),
102 ("fuchsia-pkg://name#resource", ParseError::CannotContainScheme),
103 ("//example.org/name#resource", ParseError::HostMustBeEmpty),
104 (
105 "///name#resource",
106 ParseError::InvalidRelativePath(
107 "///name#resource".to_string(),
108 Some("name#resource".to_string()),
109 ),
110 ),
111 (
112 "nAme#resource",
113 ParseError::InvalidPathSegment(PackagePathSegmentError::InvalidCharacter {
114 character: 'A',
115 }),
116 ),
117 ("name", ParseError::MissingResource),
118 ("name#", ParseError::MissingResource),
119 ("#resource", ParseError::MissingName),
120 (".#resource", ParseError::MissingName),
121 ("..#resource", ParseError::MissingName),
122 (
123 "name#resource/",
124 ParseError::InvalidResourcePath(ResourcePathError::PathEndsWithSlash),
125 ),
126 (
127 "/name#resource",
128 ParseError::InvalidRelativePath(
129 "/name#resource".to_string(),
130 Some("name#resource".to_string()),
131 ),
132 ),
133 ("name#..", ParseError::InvalidResourcePath(ResourcePathError::NameIsDotDot)),
134 (
135 "name#resource%00",
136 ParseError::InvalidResourcePath(ResourcePathError::NameContainsNull),
137 ),
138 ("extra/segment#resource", ParseError::RelativePathCannotSpecifyVariant),
139 ("too/many/segments#resource", ParseError::ExtraPathSegments),
140 ] {
141 assert_matches!(
142 RelativeComponentUrl::parse(url),
143 Err(e) if e == err,
144 "the url {:?}; expected = {:?}",
145 url, err
146 );
147 assert_matches!(
148 url.parse::<RelativeComponentUrl>(),
149 Err(e) if e == err,
150 "the url {:?}; expected = {:?}",
151 url, err
152 );
153 assert_matches!(
154 RelativeComponentUrl::try_from(url),
155 Err(e) if e == err,
156 "the url {:?}; expected = {:?}",
157 url, err
158 );
159 assert_matches!(
160 serde_json::from_str::<RelativeComponentUrl>(url),
161 Err(_),
162 "the url {:?}",
163 url
164 );
165 }
166 }
167
168 #[test]
169 fn parse_ok() {
170 for (url, package, resource) in
171 [("name#resource", "name", "resource"), ("name#reso%09urce", "name", "reso\turce")]
172 {
173 let normalized_url = url.trim_start_matches('/');
174 let json_url = format!("\"{url}\"");
175 let normalized_json_url = format!("\"{normalized_url}\"");
176
177 let validate = |parsed: &RelativeComponentUrl| {
179 assert_eq!(parsed.package_url().as_ref(), package);
180 assert_eq!(parsed.resource().as_ref(), resource);
181 };
182 validate(&RelativeComponentUrl::parse(url).unwrap());
183 validate(&url.parse::<RelativeComponentUrl>().unwrap());
184 validate(&RelativeComponentUrl::try_from(url).unwrap());
185 validate(&serde_json::from_str::<RelativeComponentUrl>(&json_url).unwrap());
186
187 assert_eq!(RelativeComponentUrl::parse(url).unwrap().to_string(), normalized_url);
189 assert_eq!(
190 serde_json::to_string(&RelativeComponentUrl::parse(url).unwrap()).unwrap(),
191 normalized_json_url,
192 );
193 }
194 }
195}