1use std::str::FromStr;
2
3use super::{
4 NamedPackageId, NamedPackageIdent, PackageHash, PackageId, PackageIdent, PackageParseError,
5};
6
7#[derive(PartialEq, Eq, Clone, Debug, Hash)]
9pub enum PackageSource {
10 Ident(PackageIdent),
12 Path(String),
14 Url(url::Url),
15}
16
17impl PackageSource {
18 pub fn as_ident(&self) -> Option<&PackageIdent> {
19 if let Self::Ident(v) = self {
20 Some(v)
21 } else {
22 None
23 }
24 }
25
26 pub fn as_hash(&self) -> Option<&PackageHash> {
27 self.as_ident().and_then(|x| x.as_hash())
28 }
29
30 pub fn as_named(&self) -> Option<&NamedPackageIdent> {
31 self.as_ident().and_then(|x| x.as_named())
32 }
33
34 pub fn as_path(&self) -> Option<&String> {
35 if let Self::Path(v) = self {
36 Some(v)
37 } else {
38 None
39 }
40 }
41
42 pub fn as_url(&self) -> Option<&url::Url> {
43 if let Self::Url(v) = self {
44 Some(v)
45 } else {
46 None
47 }
48 }
49}
50
51impl From<PackageIdent> for PackageSource {
52 fn from(id: PackageIdent) -> Self {
53 Self::Ident(id)
54 }
55}
56
57impl From<NamedPackageIdent> for PackageSource {
58 fn from(value: NamedPackageIdent) -> Self {
59 Self::Ident(PackageIdent::Named(value))
60 }
61}
62
63impl From<NamedPackageId> for PackageSource {
64 fn from(value: NamedPackageId) -> Self {
65 Self::Ident(PackageIdent::Named(NamedPackageIdent::from(value)))
66 }
67}
68
69impl From<PackageHash> for PackageSource {
70 fn from(value: PackageHash) -> Self {
71 Self::Ident(PackageIdent::Hash(value))
72 }
73}
74
75impl From<PackageId> for PackageSource {
76 fn from(value: PackageId) -> Self {
77 match value {
78 PackageId::Hash(hash) => Self::from(hash),
79 PackageId::Named(named) => Self::Ident(PackageIdent::Named(named.into())),
80 }
81 }
82}
83
84impl std::fmt::Display for PackageSource {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 match self {
87 Self::Ident(id) => id.fmt(f),
88 Self::Path(path) => path.fmt(f),
89 Self::Url(url) => url.fmt(f),
90 }
91 }
92}
93
94impl std::str::FromStr for PackageSource {
95 type Err = PackageParseError;
96
97 fn from_str(value: &str) -> Result<Self, Self::Err> {
98 let Some(first_char) = value.chars().next() else {
99 return Err(PackageParseError::new(
100 value,
101 "An empty string is not a valid package source",
102 ));
103 };
104
105 if value.contains("://") {
106 let url = value
107 .parse::<url::Url>()
108 .map_err(|e| PackageParseError::new(value, e.to_string()))?;
109 return Ok(Self::Url(url));
110 }
111
112 #[cfg(windows)]
113 if value.contains('\\') {
115 return Ok(Self::Path(value.to_string()));
116 }
117
118 match first_char {
119 '.' | '/' => Ok(Self::Path(value.to_string())),
120 _ => PackageIdent::from_str(value).map(Self::Ident),
121 }
122 }
123}
124
125impl serde::Serialize for PackageSource {
126 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
127 where
128 S: serde::Serializer,
129 {
130 match self {
131 Self::Ident(id) => id.serialize(serializer),
132 Self::Path(path) => path.serialize(serializer),
133 Self::Url(url) => url.serialize(serializer),
134 }
135 }
136}
137
138impl<'de> serde::Deserialize<'de> for PackageSource {
139 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
140 where
141 D: serde::Deserializer<'de>,
142 {
143 let s = String::deserialize(deserializer)?;
144 PackageSource::from_str(&s).map_err(|e| serde::de::Error::custom(e.to_string()))
145 }
146}
147
148impl schemars::JsonSchema for PackageSource {
149 fn schema_name() -> String {
150 "PackageSource".to_string()
151 }
152
153 fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
154 String::json_schema(gen)
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use crate::package::Tag;
161
162 use super::*;
163
164 #[test]
165 fn test_parse_package_specifier() {
166 assert_eq!(
168 PackageSource::from_str("ns/name").unwrap(),
169 PackageSource::from(NamedPackageIdent {
170 registry: None,
171 namespace: Some("ns".to_string()),
172 name: "name".to_string(),
173 tag: None,
174 })
175 );
176
177 assert_eq!(
178 PackageSource::from_str("ns/name@").unwrap(),
179 PackageSource::from(NamedPackageIdent {
180 registry: None,
181 namespace: Some("ns".to_string()),
182 name: "name".to_string(),
183 tag: None,
184 }),
185 "empty tag should be parsed as None"
186 );
187
188 assert_eq!(
189 PackageSource::from_str("ns/name@tag").unwrap(),
190 PackageSource::from(NamedPackageIdent {
191 registry: None,
192 namespace: Some("ns".to_string()),
193 name: "name".to_string(),
194 tag: Some(Tag::Named("tag".to_string())),
195 })
196 );
197
198 assert_eq!(
199 PackageSource::from_str("reg.com:ns/name").unwrap(),
200 PackageSource::from(NamedPackageIdent {
201 registry: Some("reg.com".to_string()),
202 namespace: Some("ns".to_string()),
203 name: "name".to_string(),
204 tag: None,
205 })
206 );
207
208 assert_eq!(
209 PackageSource::from_str("reg.com:ns/name@tag").unwrap(),
210 PackageSource::from(NamedPackageIdent {
211 registry: Some("reg.com".to_string()),
212 namespace: Some("ns".to_string()),
213 name: "name".to_string(),
214 tag: Some(Tag::Named("tag".to_string())),
215 })
216 );
217
218 assert_eq!(
219 PackageSource::from_str("reg.com:ns/name").unwrap(),
220 PackageSource::from(NamedPackageIdent {
221 registry: Some("reg.com".to_string()),
222 namespace: Some("ns".to_string()),
223 name: "name".to_string(),
224 tag: None,
225 })
226 );
227
228 assert_eq!(
229 PackageSource::from_str("reg.com:ns/name@tag").unwrap(),
230 PackageSource::from(NamedPackageIdent {
231 registry: Some("reg.com".to_string()),
232 namespace: Some("ns".to_string()),
233 name: "name".to_string(),
234 tag: Some(Tag::Named("tag".to_string())),
235 })
236 );
237
238 assert_eq!(
239 PackageSource::from_str("reg.com:ns/name").unwrap(),
240 PackageSource::from(NamedPackageIdent {
241 registry: Some("reg.com".to_string()),
242 namespace: Some("ns".to_string()),
243 name: "name".to_string(),
244 tag: None,
245 })
246 );
247
248 assert_eq!(
249 PackageSource::from_str("reg.com:ns/name@tag").unwrap(),
250 PackageSource::from(NamedPackageIdent {
251 registry: Some("reg.com".to_string()),
252 namespace: Some("ns".to_string()),
253 name: "name".to_string(),
254 tag: Some(Tag::Named("tag".to_string())),
255 })
256 );
257
258 assert_eq!(
260 PackageSource::from_str("alpha"),
261 Ok(PackageSource::from(NamedPackageIdent {
262 registry: None,
263 namespace: None,
264 name: "alpha".to_string(),
265 tag: None,
266 }))
267 );
268
269 assert_eq!(
270 PackageSource::from_str(""),
271 Err(PackageParseError::new(
272 "",
273 "An empty string is not a valid package source"
274 ))
275 );
276 assert_eq!(
277 PackageSource::from_str("ns/name").unwrap(),
278 PackageSource::from(NamedPackageIdent {
279 registry: None,
280 namespace: Some("ns".to_string()),
281 name: "name".to_string(),
282 tag: None,
283 })
284 );
285
286 assert_eq!(
287 PackageSource::from_str(
288 "sha256:5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"
289 )
290 .unwrap(),
291 PackageSource::from(
292 PackageHash::from_str(
293 "sha256:5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"
294 )
295 .unwrap()
296 )
297 );
298
299 let wants = vec![
300 "sha256:5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03",
301 "./dir",
302 "ns/name",
303 "ns/name@",
304 "ns/name@tag",
305 ];
306 for want in wants {
307 let spec = PackageSource::from_str(want).unwrap();
308 assert_eq!(spec, PackageSource::from_str(&spec.to_string()).unwrap());
309 }
310 }
311
312 #[test]
313 fn parse_package_sources() {
314 let inputs = [
315 (
316 "first",
317 PackageSource::from(NamedPackageIdent {
318 registry: None,
319 namespace: None,
320 name: "first".to_string(),
321 tag: None,
322 }),
323 ),
324 (
325 "namespace/package",
326 PackageSource::from(NamedPackageIdent {
327 registry: None,
328 namespace: Some("namespace".to_string()),
329 name: "package".to_string(),
330 tag: None,
331 }),
332 ),
333 (
334 "namespace/package@1.0.0",
335 PackageSource::from(NamedPackageIdent {
336 registry: None,
337 namespace: Some("namespace".to_string()),
338 name: "package".to_string(),
339 tag: Some(Tag::VersionReq("1.0.0".parse().unwrap())),
340 }),
341 ),
342 (
343 "namespace/package@latest",
344 PackageSource::from(NamedPackageIdent {
345 registry: None,
346 namespace: Some("namespace".to_string()),
347 name: "package".to_string(),
348 tag: Some(Tag::VersionReq(semver::VersionReq::STAR)),
349 }),
350 ),
351 (
352 "https://wapm/io/namespace/package@1.0.0",
353 PackageSource::Url("https://wapm/io/namespace/package@1.0.0".parse().unwrap()),
354 ),
355 (
356 "/path/to/some/file.webc",
357 PackageSource::Path("/path/to/some/file.webc".into()),
358 ),
359 ("./file.webc", PackageSource::Path("./file.webc".into())),
360 #[cfg(windows)]
361 (
362 r"C:\Path\to\some\file.webc",
363 PackageSource::Path(r"C:\Path\to\some\file.webc".into()),
364 ),
365 ];
366
367 for (index, (src, expected)) in inputs.into_iter().enumerate() {
368 eprintln!("testing pattern {}", index + 1);
369 let parsed = PackageSource::from_str(src).unwrap();
370 assert_eq!(parsed, expected);
371 }
372 }
373}