1use std::{borrow::Cow, 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() -> Cow<'static, str> {
150 Cow::Borrowed("PackageSource")
151 }
152
153 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
154 String::json_schema(generator)
155 }
156
157 fn inline_schema() -> bool {
158 false
159 }
160
161 fn schema_id() -> std::borrow::Cow<'static, str> {
162 Self::schema_name()
163 }
164}
165
166#[cfg(test)]
167mod tests {
168 use crate::package::Tag;
169
170 use super::*;
171
172 #[test]
173 fn test_parse_package_specifier() {
174 assert_eq!(
176 PackageSource::from_str("ns/name").unwrap(),
177 PackageSource::from(NamedPackageIdent {
178 registry: None,
179 namespace: Some("ns".to_string()),
180 name: "name".to_string(),
181 tag: None,
182 })
183 );
184
185 assert_eq!(
186 PackageSource::from_str("ns/name@").unwrap(),
187 PackageSource::from(NamedPackageIdent {
188 registry: None,
189 namespace: Some("ns".to_string()),
190 name: "name".to_string(),
191 tag: None,
192 }),
193 "empty tag should be parsed as None"
194 );
195
196 assert_eq!(
197 PackageSource::from_str("ns/name@tag").unwrap(),
198 PackageSource::from(NamedPackageIdent {
199 registry: None,
200 namespace: Some("ns".to_string()),
201 name: "name".to_string(),
202 tag: Some(Tag::Named("tag".to_string())),
203 })
204 );
205
206 assert_eq!(
207 PackageSource::from_str("reg.com:ns/name").unwrap(),
208 PackageSource::from(NamedPackageIdent {
209 registry: Some("reg.com".to_string()),
210 namespace: Some("ns".to_string()),
211 name: "name".to_string(),
212 tag: None,
213 })
214 );
215
216 assert_eq!(
217 PackageSource::from_str("reg.com:ns/name@tag").unwrap(),
218 PackageSource::from(NamedPackageIdent {
219 registry: Some("reg.com".to_string()),
220 namespace: Some("ns".to_string()),
221 name: "name".to_string(),
222 tag: Some(Tag::Named("tag".to_string())),
223 })
224 );
225
226 assert_eq!(
227 PackageSource::from_str("reg.com:ns/name").unwrap(),
228 PackageSource::from(NamedPackageIdent {
229 registry: Some("reg.com".to_string()),
230 namespace: Some("ns".to_string()),
231 name: "name".to_string(),
232 tag: None,
233 })
234 );
235
236 assert_eq!(
237 PackageSource::from_str("reg.com:ns/name@tag").unwrap(),
238 PackageSource::from(NamedPackageIdent {
239 registry: Some("reg.com".to_string()),
240 namespace: Some("ns".to_string()),
241 name: "name".to_string(),
242 tag: Some(Tag::Named("tag".to_string())),
243 })
244 );
245
246 assert_eq!(
247 PackageSource::from_str("reg.com:ns/name").unwrap(),
248 PackageSource::from(NamedPackageIdent {
249 registry: Some("reg.com".to_string()),
250 namespace: Some("ns".to_string()),
251 name: "name".to_string(),
252 tag: None,
253 })
254 );
255
256 assert_eq!(
257 PackageSource::from_str("reg.com:ns/name@tag").unwrap(),
258 PackageSource::from(NamedPackageIdent {
259 registry: Some("reg.com".to_string()),
260 namespace: Some("ns".to_string()),
261 name: "name".to_string(),
262 tag: Some(Tag::Named("tag".to_string())),
263 })
264 );
265
266 assert_eq!(
268 PackageSource::from_str("alpha"),
269 Ok(PackageSource::from(NamedPackageIdent {
270 registry: None,
271 namespace: None,
272 name: "alpha".to_string(),
273 tag: None,
274 }))
275 );
276
277 assert_eq!(
278 PackageSource::from_str(""),
279 Err(PackageParseError::new(
280 "",
281 "An empty string is not a valid package source"
282 ))
283 );
284 assert_eq!(
285 PackageSource::from_str("ns/name").unwrap(),
286 PackageSource::from(NamedPackageIdent {
287 registry: None,
288 namespace: Some("ns".to_string()),
289 name: "name".to_string(),
290 tag: None,
291 })
292 );
293
294 assert_eq!(
295 PackageSource::from_str(
296 "sha256:5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"
297 )
298 .unwrap(),
299 PackageSource::from(
300 PackageHash::from_str(
301 "sha256:5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03"
302 )
303 .unwrap()
304 )
305 );
306
307 let wants = vec![
308 "sha256:5891b5b522d5df086d0ff0b110fbd9d21bb4fc7163af34d08286a2e846f6be03",
309 "./dir",
310 "ns/name",
311 "ns/name@",
312 "ns/name@tag",
313 ];
314 for want in wants {
315 let spec = PackageSource::from_str(want).unwrap();
316 assert_eq!(spec, PackageSource::from_str(&spec.to_string()).unwrap());
317 }
318 }
319
320 #[test]
321 fn parse_package_sources() {
322 let inputs = [
323 (
324 "first",
325 PackageSource::from(NamedPackageIdent {
326 registry: None,
327 namespace: None,
328 name: "first".to_string(),
329 tag: None,
330 }),
331 ),
332 (
333 "namespace/package",
334 PackageSource::from(NamedPackageIdent {
335 registry: None,
336 namespace: Some("namespace".to_string()),
337 name: "package".to_string(),
338 tag: None,
339 }),
340 ),
341 (
342 "namespace/package@1.0.0",
343 PackageSource::from(NamedPackageIdent {
344 registry: None,
345 namespace: Some("namespace".to_string()),
346 name: "package".to_string(),
347 tag: Some(Tag::VersionReq("1.0.0".parse().unwrap())),
348 }),
349 ),
350 (
351 "namespace/package@latest",
352 PackageSource::from(NamedPackageIdent {
353 registry: None,
354 namespace: Some("namespace".to_string()),
355 name: "package".to_string(),
356 tag: Some(Tag::VersionReq(semver::VersionReq::STAR)),
357 }),
358 ),
359 (
360 "https://wapm/io/namespace/package@1.0.0",
361 PackageSource::Url("https://wapm/io/namespace/package@1.0.0".parse().unwrap()),
362 ),
363 (
364 "/path/to/some/file.webc",
365 PackageSource::Path("/path/to/some/file.webc".into()),
366 ),
367 ("./file.webc", PackageSource::Path("./file.webc".into())),
368 #[cfg(windows)]
369 (
370 r"C:\Path\to\some\file.webc",
371 PackageSource::Path(r"C:\Path\to\some\file.webc".into()),
372 ),
373 ];
374
375 for (index, (src, expected)) in inputs.into_iter().enumerate() {
376 eprintln!("testing pattern {}", index + 1);
377 let parsed = PackageSource::from_str(src).unwrap();
378 assert_eq!(parsed, expected);
379 }
380 }
381}