wasmer_config/package/
package_source.rs

1use std::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 [`WebcIdent`].
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() -> 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        // Parse as WebcIdent
167        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        // Failure cases.
259        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}