wasmer_config/package/
named_package_ident.rs

1use std::{fmt::Write, str::FromStr};
2
3use semver::VersionReq;
4
5use super::{NamedPackageId, PackageParseError};
6
7#[derive(PartialEq, Eq, Clone, Debug, Hash)]
8pub enum Tag {
9    Named(String),
10    VersionReq(semver::VersionReq),
11}
12
13impl Tag {
14    pub fn as_named(&self) -> Option<&String> {
15        if let Self::Named(v) = self {
16            Some(v)
17        } else {
18            None
19        }
20    }
21
22    pub fn as_version_req(&self) -> Option<&semver::VersionReq> {
23        if let Self::VersionReq(v) = self {
24            Some(v)
25        } else {
26            None
27        }
28    }
29}
30
31impl std::fmt::Display for Tag {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        match self {
34            Tag::Named(n) => n.fmt(f),
35            Tag::VersionReq(v) => v.fmt(f),
36        }
37    }
38}
39
40impl std::str::FromStr for Tag {
41    type Err = PackageParseError;
42
43    fn from_str(s: &str) -> Result<Self, Self::Err> {
44        if s == "latest" {
45            Ok(Self::VersionReq(semver::VersionReq::STAR))
46        } else {
47            match semver::VersionReq::from_str(s) {
48                Ok(v) => Ok(Self::VersionReq(v)),
49                Err(_) => Ok(Self::Named(s.to_string())),
50            }
51        }
52    }
53}
54
55/// Parsed representation of a package identifier.
56///
57/// Format:
58/// [https?://<domain>/][namespace/]name[@version]
59#[derive(PartialEq, Eq, Clone, Debug, Hash)]
60pub struct NamedPackageIdent {
61    pub registry: Option<String>,
62    pub namespace: Option<String>,
63    pub name: String,
64    pub tag: Option<Tag>,
65}
66
67impl NamedPackageIdent {
68    pub fn try_from_full_name_and_version(
69        full_name: &str,
70        version: &str,
71    ) -> Result<Self, PackageParseError> {
72        let (namespace, name) = match full_name.split_once('/') {
73            Some((ns, name)) => (Some(ns.to_owned()), name.to_owned()),
74            None => (None, full_name.to_owned()),
75        };
76
77        let version = version
78            .parse::<VersionReq>()
79            .map_err(|e| PackageParseError::new(version, e.to_string()))?;
80
81        Ok(Self {
82            registry: None,
83            namespace,
84            name,
85            tag: Some(Tag::VersionReq(version)),
86        })
87    }
88
89    pub fn tag_str(&self) -> Option<String> {
90        self.tag.as_ref().map(|x| x.to_string())
91    }
92
93    /// Namespaced name.
94    ///
95    /// Eg: "namespace/name"
96    pub fn full_name(&self) -> String {
97        if let Some(ns) = &self.namespace {
98            format!("{}/{}", ns, self.name)
99        } else {
100            self.name.clone()
101        }
102    }
103
104    pub fn version_opt(&self) -> Option<&VersionReq> {
105        match &self.tag {
106            Some(Tag::VersionReq(v)) => Some(v),
107            Some(Tag::Named(_)) | None => None,
108        }
109    }
110
111    pub fn version_or_default(&self) -> VersionReq {
112        match &self.tag {
113            Some(Tag::VersionReq(v)) => v.clone(),
114            Some(Tag::Named(_)) | None => semver::VersionReq::STAR,
115        }
116    }
117
118    pub fn registry_url(&self) -> Result<Option<url::Url>, PackageParseError> {
119        let Some(reg) = &self.registry else {
120            return Ok(None);
121        };
122
123        let reg = if !reg.starts_with("http://") && !reg.starts_with("https://") {
124            format!("https://{reg}")
125        } else {
126            reg.clone()
127        };
128
129        url::Url::parse(&reg)
130            .map_err(|e| PackageParseError::new(reg, e.to_string()))
131            .map(Some)
132    }
133
134    /// Build the ident for a package.
135    ///
136    /// Format: [NAMESPACE/]NAME[@tag]
137    pub fn build_identifier(&self) -> String {
138        let mut ident = if let Some(ns) = &self.namespace {
139            format!("{}/{}", ns, self.name)
140        } else {
141            self.name.to_string()
142        };
143
144        if let Some(tag) = &self.tag {
145            ident.push('@');
146            // Writing to a string only fails on memory allocation errors.
147            write!(&mut ident, "{tag}").unwrap();
148        }
149        ident
150    }
151
152    pub fn build(&self) -> String {
153        let mut out = String::new();
154        if let Some(url) = &self.registry {
155            // NOTE: writing to a String can only fail on allocation errors.
156            write!(&mut out, "{url}").unwrap();
157
158            if !out.ends_with('/') {
159                out.push(':');
160            }
161        }
162        if let Some(ns) = &self.namespace {
163            out.push_str(ns);
164            out.push('/');
165        }
166        out.push_str(&self.name);
167        if let Some(tag) = &self.tag {
168            out.push('@');
169            // Writing to a string only fails on memory allocation errors.
170            write!(&mut out, "{tag}").unwrap();
171        }
172
173        out
174    }
175
176    /// Returns true if this ident matches the given package id.
177    ///
178    /// Semver constraints are matched against the package id's version.
179    pub fn matches_id(&self, id: &NamedPackageId) -> bool {
180        if self.full_name() == id.full_name {
181            if let Some(tag) = &self.tag {
182                match tag {
183                    Tag::Named(n) => n == &id.version.to_string(),
184                    Tag::VersionReq(v) => v.matches(&id.version),
185                }
186            } else {
187                true
188            }
189        } else {
190            false
191        }
192    }
193}
194
195impl From<NamedPackageId> for NamedPackageIdent {
196    fn from(value: NamedPackageId) -> Self {
197        let (namespace, name) = match value.full_name.split_once('/') {
198            Some((ns, name)) => (Some(ns.to_owned()), name.to_owned()),
199            None => (None, value.full_name),
200        };
201
202        Self {
203            registry: None,
204            namespace,
205            name,
206            tag: Some(Tag::VersionReq(semver::VersionReq {
207                comparators: vec![semver::Comparator {
208                    op: semver::Op::Exact,
209                    major: value.version.major,
210                    minor: Some(value.version.minor),
211                    patch: Some(value.version.patch),
212                    pre: value.version.pre,
213                }],
214            })),
215        }
216    }
217}
218
219impl std::str::FromStr for NamedPackageIdent {
220    type Err = PackageParseError;
221
222    fn from_str(value: &str) -> Result<Self, Self::Err> {
223        let (rest, tag_opt) = value
224            .trim()
225            .rsplit_once('@')
226            .map(|(x, y)| (x, if y.is_empty() { None } else { Some(y) }))
227            .unwrap_or((value, None));
228
229        let tag = if let Some(v) = tag_opt.filter(|x| !x.is_empty()) {
230            Some(Tag::from_str(v)?)
231        } else {
232            None
233        };
234
235        let (rest, name) = if let Some((r, n)) = rest.rsplit_once('/') {
236            (r, n)
237        } else {
238            ("", rest)
239        };
240
241        let name = name.trim();
242        if name.is_empty() {
243            return Err(PackageParseError::new(value, "package name is required"));
244        }
245
246        let (rest, namespace) = if rest.is_empty() {
247            ("", None)
248        } else {
249            let (rest, ns) = rest.rsplit_once(':').unwrap_or(("", rest));
250
251            let ns = ns.trim();
252
253            if ns.is_empty() {
254                return Err(PackageParseError::new(value, "namespace can not be empty"));
255            }
256            (rest, Some(ns.to_string()))
257        };
258
259        let rest = rest.trim();
260        let registry = if rest.is_empty() {
261            None
262        } else {
263            Some(rest.to_string())
264        };
265
266        Ok(Self {
267            registry,
268            namespace,
269            name: name.to_string(),
270            tag,
271        })
272    }
273}
274
275impl std::fmt::Display for NamedPackageIdent {
276    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
277        write!(f, "{}", self.build())
278    }
279}
280
281impl serde::Serialize for NamedPackageIdent {
282    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
283    where
284        S: serde::ser::Serializer,
285    {
286        self.to_string().serialize(serializer)
287    }
288}
289
290impl<'de> serde::Deserialize<'de> for NamedPackageIdent {
291    fn deserialize<D>(deserializer: D) -> Result<NamedPackageIdent, D::Error>
292    where
293        D: serde::de::Deserializer<'de>,
294    {
295        let s = String::deserialize(deserializer)?;
296        Self::from_str(&s).map_err(serde::de::Error::custom)
297    }
298}
299
300impl schemars::JsonSchema for NamedPackageIdent {
301    fn schema_name() -> String {
302        "NamedPackageIdent".to_string()
303    }
304
305    fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
306        String::json_schema(gen)
307    }
308}
309
310#[cfg(test)]
311mod tests {
312    use std::str::FromStr;
313
314    use crate::package::PackageParseError;
315
316    use super::*;
317
318    #[test]
319    fn test_parse_webc_ident() {
320        // Success cases.
321
322        assert_eq!(
323            NamedPackageIdent::from_str("ns/name").unwrap(),
324            NamedPackageIdent {
325                registry: None,
326                namespace: Some("ns".to_string()),
327                name: "name".to_string(),
328                tag: None,
329            }
330        );
331
332        assert_eq!(
333            NamedPackageIdent::from_str("ns/name@").unwrap(),
334            NamedPackageIdent {
335                registry: None,
336                namespace: Some("ns".to_string()),
337                name: "name".to_string(),
338                tag: None,
339            },
340            "empty tag should be parsed as None"
341        );
342
343        assert_eq!(
344            NamedPackageIdent::from_str("ns/name@tag").unwrap(),
345            NamedPackageIdent {
346                registry: None,
347                namespace: Some("ns".to_string()),
348                name: "name".to_string(),
349                tag: Some(Tag::Named("tag".to_string())),
350            }
351        );
352
353        assert_eq!(
354            NamedPackageIdent::from_str("reg.com:ns/name").unwrap(),
355            NamedPackageIdent {
356                registry: Some("reg.com".to_string()),
357                namespace: Some("ns".to_string()),
358                name: "name".to_string(),
359                tag: None,
360            }
361        );
362
363        assert_eq!(
364            NamedPackageIdent::from_str("reg.com:ns/name@tag").unwrap(),
365            NamedPackageIdent {
366                registry: Some("reg.com".to_string()),
367                namespace: Some("ns".to_string()),
368                name: "name".to_string(),
369                tag: Some(Tag::Named("tag".to_string())),
370            }
371        );
372
373        assert_eq!(
374            NamedPackageIdent::from_str("reg.com:ns/name").unwrap(),
375            NamedPackageIdent {
376                registry: Some("reg.com".to_string()),
377                namespace: Some("ns".to_string()),
378                name: "name".to_string(),
379                tag: None,
380            }
381        );
382
383        assert_eq!(
384            NamedPackageIdent::from_str("reg.com:ns/name@tag").unwrap(),
385            NamedPackageIdent {
386                registry: Some("reg.com".to_string()),
387                namespace: Some("ns".to_string()),
388                name: "name".to_string(),
389                tag: Some(Tag::Named("tag".to_string())),
390            }
391        );
392
393        assert_eq!(
394            NamedPackageIdent::from_str("reg.com:ns/name").unwrap(),
395            NamedPackageIdent {
396                registry: Some("reg.com".to_string()),
397                namespace: Some("ns".to_string()),
398                name: "name".to_string(),
399                tag: None,
400            }
401        );
402
403        assert_eq!(
404            NamedPackageIdent::from_str("reg.com:ns/name@tag").unwrap(),
405            NamedPackageIdent {
406                registry: Some("reg.com".to_string()),
407                namespace: Some("ns".to_string()),
408                name: "name".to_string(),
409                tag: Some(Tag::Named("tag".to_string())),
410            }
411        );
412
413        // Failure cases.
414
415        assert_eq!(
416            NamedPackageIdent::from_str("alpha").unwrap(),
417            NamedPackageIdent {
418                registry: None,
419                namespace: None,
420                name: "alpha".to_string(),
421                tag: None,
422            },
423        );
424
425        assert_eq!(
426            NamedPackageIdent::from_str(""),
427            Err(PackageParseError::new("", "package name is required"))
428        );
429    }
430
431    #[test]
432    fn test_serde_serialize_package_ident_with_repo() {
433        // Serialize
434        let ident = NamedPackageIdent {
435            registry: Some("wapm.io".to_string()),
436            namespace: Some("ns".to_string()),
437            name: "name".to_string(),
438            tag: None,
439        };
440
441        let raw = serde_json::to_string(&ident).unwrap();
442        assert_eq!(raw, "\"wapm.io:ns/name\"");
443
444        let ident2 = serde_json::from_str::<NamedPackageIdent>(&raw).unwrap();
445        assert_eq!(ident, ident2);
446    }
447
448    #[test]
449    fn test_serde_serialize_webc_str_ident_without_repo() {
450        // Serialize
451        let ident = NamedPackageIdent {
452            registry: None,
453            namespace: Some("ns".to_string()),
454            name: "name".to_string(),
455            tag: None,
456        };
457
458        let raw = serde_json::to_string(&ident).unwrap();
459        assert_eq!(raw, "\"ns/name\"");
460
461        let ident2 = serde_json::from_str::<NamedPackageIdent>(&raw).unwrap();
462        assert_eq!(ident, ident2);
463    }
464
465    #[test]
466    fn test_named_package_ident_matches_id() {
467        assert!(NamedPackageIdent::from_str("ns/name")
468            .unwrap()
469            .matches_id(&NamedPackageId::try_new("ns/name", "0.1.0").unwrap()));
470
471        assert!(NamedPackageIdent::from_str("ns/name")
472            .unwrap()
473            .matches_id(&NamedPackageId::try_new("ns/name", "1.0.1").unwrap()));
474
475        assert!(NamedPackageIdent::from_str("ns/name@1")
476            .unwrap()
477            .matches_id(&NamedPackageId::try_new("ns/name", "1.0.1").unwrap()));
478
479        assert!(!NamedPackageIdent::from_str("ns/name@2")
480            .unwrap()
481            .matches_id(&NamedPackageId::try_new("ns/name", "1.0.1").unwrap()));
482    }
483}