wasmer_config/package/
package_source.rs

1use std::{borrow::Cow, str::FromStr};
2
3use super::{
4    NamedPackageId, NamedPackageIdent, PackageHash, PackageId, PackageIdent, PackageParseError,
5};
6
7/// Source location of a package.
8#[derive(PartialEq, Eq, Clone, Debug, Hash)]
9pub enum PackageSource {
10    /// An identifier in the format prescribed by the `WebcIdent` type.
11    Ident(PackageIdent),
12    /// An absolute or relative (dot-leading) path.
13    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        // Detect windows absolute paths
114        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        // Parse as WebcIdent
175        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        // Failure cases.
267        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}