wasmer_deploy_schema/schema/
webc.rs

1use serde::{Deserialize, Serialize};
2
3/// Parsed representation of a WebC package source.
4#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug, schemars::JsonSchema)]
5pub struct WebcIdent {
6    #[serde(skip_serializing_if = "Option::is_none")]
7    pub repository: Option<url::Url>,
8    pub namespace: String,
9    pub name: String,
10    #[serde(skip_serializing_if = "Option::is_none")]
11    pub tag: Option<String>,
12}
13
14impl WebcIdent {
15    /// Build the ident for a package.
16    ///
17    /// Format: NAMESPACE/NAME[@version|hash]
18    ///
19    /// If prefer_hash is true, the ident will use the signature hash instead of
20    /// the version if both are available.
21    pub fn build_identifier(&self) -> String {
22        let mut ident = format!("{}/{}", self.namespace, self.name);
23
24        if let Some(tag) = &self.tag {
25            ident.push('@');
26            ident.push_str(tag);
27        }
28        ident
29    }
30
31    /// The the url where the webc package can be downloaded.
32    ///
33    /// NOTE: returns [`Option::None`] if [`Self::repository`] is not set.
34    ///
35    /// Private packages will also require an auth token for downloading.
36    pub fn build_download_url(&self) -> Option<url::Url> {
37        let mut url = self.repository.as_ref()?.clone();
38        let ident = self.build_identifier();
39        let original_path = url.path().strip_suffix('/').unwrap_or(url.path());
40        let final_path = format!("{original_path}/{ident}");
41
42        url.set_path(&final_path);
43        Some(url)
44    }
45
46    /// The the url where the webc package can be downloaded.
47    ///
48    /// Private packages will also require an auth token for downloading.
49    pub fn build_download_url_with_default_registry(&self, default_reg: &url::Url) -> url::Url {
50        let mut url = self
51            .repository
52            .as_ref()
53            .cloned()
54            .unwrap_or_else(|| default_reg.clone());
55        let ident = self.build_identifier();
56        let original_path = url.path().strip_suffix('/').unwrap_or(url.path());
57        let final_path = format!("{original_path}/{ident}");
58
59        url.set_path(&final_path);
60        url
61    }
62
63    pub fn parse(value: &str) -> Result<Self, WebcParseError> {
64        let (rest, tag_opt) = value
65            .trim()
66            .rsplit_once('@')
67            .map(|(x, y)| (x, if y.is_empty() { None } else { Some(y) }))
68            .unwrap_or((value, None));
69
70        let mut parts = rest.rsplit('/');
71
72        let name = parts
73            .next()
74            .map(|x| x.trim())
75            .filter(|x| !x.is_empty())
76            .ok_or_else(|| WebcParseError::new(value, "package name is required"))?;
77
78        let namespace = parts
79            .next()
80            .map(|x| x.trim())
81            .filter(|x| !x.is_empty())
82            .ok_or_else(|| WebcParseError::new(value, "package namespace is required"))?;
83
84        let rest = parts.rev().collect::<Vec<_>>().join("/");
85        let repository = if rest.is_empty() {
86            None
87        } else {
88            let registry = rest.trim();
89            let full_registry =
90                if registry.starts_with("http://") || registry.starts_with("https://") {
91                    registry.to_string()
92                } else {
93                    format!("https://{}", registry)
94                };
95
96            let registry_url = url::Url::parse(&full_registry)
97                .map_err(|e| WebcParseError::new(value, format!("invalid registry url: {}", e)))?;
98            Some(registry_url)
99        };
100
101        Ok(Self {
102            repository,
103            namespace: namespace.to_string(),
104            name: name.to_string(),
105            tag: tag_opt.map(|x| x.to_string()),
106        })
107    }
108}
109
110impl std::fmt::Display for WebcIdent {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        if let Some(url) = self.build_download_url() {
113            write!(f, "{}", url)
114        } else {
115            write!(f, "{}", self.build_identifier())
116        }
117    }
118}
119
120/// Wrapper around [`WebcPackageIdentifierV1`].
121///
122/// The inner value is serialized and deserialized as a [`String`].
123#[derive(Clone, Debug, PartialEq, Eq)]
124pub struct StringWebcIdent(pub WebcIdent);
125
126impl StringWebcIdent {
127    pub fn parse(value: &str) -> Result<Self, WebcParseError> {
128        Ok(Self(WebcIdent::parse(value)?))
129    }
130}
131
132impl std::fmt::Display for StringWebcIdent {
133    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134        self.0.fmt(f)
135    }
136}
137
138impl schemars::JsonSchema for StringWebcIdent {
139    fn schema_name() -> String {
140        "StringWebcPackageIdent".to_string()
141    }
142
143    fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
144        schemars::schema::Schema::Object(schemars::schema::SchemaObject {
145            instance_type: Some(schemars::schema::InstanceType::String.into()),
146            ..Default::default()
147        })
148    }
149}
150
151impl From<StringWebcIdent> for WebcIdent {
152    fn from(x: StringWebcIdent) -> Self {
153        x.0
154    }
155}
156
157impl From<WebcIdent> for StringWebcIdent {
158    fn from(x: WebcIdent) -> Self {
159        Self(x)
160    }
161}
162
163impl serde::Serialize for StringWebcIdent {
164    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
165    where
166        S: serde::Serializer,
167    {
168        let val = self.0.to_string();
169        serializer.serialize_str(&val)
170    }
171}
172
173impl<'de> serde::Deserialize<'de> for StringWebcIdent {
174    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
175    where
176        D: serde::Deserializer<'de>,
177    {
178        let s = String::deserialize(deserializer)?;
179        let ident = WebcIdent::parse(&s).map_err(|e| serde::de::Error::custom(e.to_string()))?;
180        Ok(Self(ident))
181    }
182}
183
184impl std::str::FromStr for StringWebcIdent {
185    type Err = WebcParseError;
186
187    fn from_str(s: &str) -> Result<Self, Self::Err> {
188        Self::parse(s)
189    }
190}
191
192#[derive(PartialEq, Eq, Debug)]
193pub struct WebcParseError {
194    value: String,
195    message: String,
196}
197
198impl WebcParseError {
199    fn new(value: impl Into<String>, message: impl Into<String>) -> Self {
200        Self {
201            value: value.into(),
202            message: message.into(),
203        }
204    }
205}
206
207impl std::fmt::Display for WebcParseError {
208    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
209        write!(
210            f,
211            "could not parse webc package specifier '{}': {}",
212            self.value, self.message
213        )
214    }
215}
216
217impl std::error::Error for WebcParseError {}
218
219#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug, schemars::JsonSchema)]
220pub struct WebcPackagePathV1 {
221    /// Volume the file is contained in.
222    /// (webc packages can have multiple file volumes)
223    #[serde(skip_serializing_if = "Option::is_none")]
224    pub volume: Option<String>,
225    pub path: String,
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn test_parse_webc_ident() {
234        // Success cases.
235
236        assert_eq!(
237            WebcIdent::parse("ns/name").unwrap(),
238            WebcIdent {
239                repository: None,
240                namespace: "ns".to_string(),
241                name: "name".to_string(),
242                tag: None,
243            }
244        );
245
246        assert_eq!(
247            WebcIdent::parse("ns/name@").unwrap(),
248            WebcIdent {
249                repository: None,
250                namespace: "ns".to_string(),
251                name: "name".to_string(),
252                tag: None,
253            },
254            "empty tag should be parsed as None"
255        );
256
257        assert_eq!(
258            WebcIdent::parse("ns/name@tag").unwrap(),
259            WebcIdent {
260                repository: None,
261                namespace: "ns".to_string(),
262                name: "name".to_string(),
263                tag: Some("tag".to_string()),
264            }
265        );
266
267        assert_eq!(
268            WebcIdent::parse("reg.com/ns/name").unwrap(),
269            WebcIdent {
270                repository: Some(url::Url::parse("https://reg.com").unwrap()),
271                namespace: "ns".to_string(),
272                name: "name".to_string(),
273                tag: None,
274            }
275        );
276
277        assert_eq!(
278            WebcIdent::parse("reg.com/ns/name@tag").unwrap(),
279            WebcIdent {
280                repository: Some(url::Url::parse("https://reg.com").unwrap()),
281                namespace: "ns".to_string(),
282                name: "name".to_string(),
283                tag: Some("tag".to_string()),
284            }
285        );
286
287        assert_eq!(
288            WebcIdent::parse("https://reg.com/ns/name").unwrap(),
289            WebcIdent {
290                repository: Some(url::Url::parse("https://reg.com").unwrap()),
291                namespace: "ns".to_string(),
292                name: "name".to_string(),
293                tag: None,
294            }
295        );
296
297        assert_eq!(
298            WebcIdent::parse("https://reg.com/ns/name@tag").unwrap(),
299            WebcIdent {
300                repository: Some(url::Url::parse("https://reg.com").unwrap()),
301                namespace: "ns".to_string(),
302                name: "name".to_string(),
303                tag: Some("tag".to_string()),
304            }
305        );
306
307        assert_eq!(
308            WebcIdent::parse("http://reg.com/ns/name").unwrap(),
309            WebcIdent {
310                repository: Some(url::Url::parse("http://reg.com").unwrap()),
311                namespace: "ns".to_string(),
312                name: "name".to_string(),
313                tag: None,
314            }
315        );
316
317        assert_eq!(
318            WebcIdent::parse("http://reg.com/ns/name@tag").unwrap(),
319            WebcIdent {
320                repository: Some(url::Url::parse("http://reg.com").unwrap()),
321                namespace: "ns".to_string(),
322                name: "name".to_string(),
323                tag: Some("tag".to_string()),
324            }
325        );
326
327        // Failure cases.
328
329        assert_eq!(
330            WebcIdent::parse("alpha"),
331            Err(WebcParseError::new(
332                "alpha",
333                "package namespace is required"
334            ))
335        );
336
337        assert_eq!(
338            WebcIdent::parse(""),
339            Err(WebcParseError::new("", "package name is required"))
340        );
341    }
342
343    #[test]
344    fn test_serde_serialize_webc_str_ident_with_repo() {
345        // Serialize
346        let ident = StringWebcIdent(WebcIdent {
347            repository: Some(url::Url::parse("https://wapm.io").unwrap()),
348            namespace: "ns".to_string(),
349            name: "name".to_string(),
350            tag: None,
351        });
352
353        let raw = serde_json::to_string(&ident).unwrap();
354        assert_eq!(raw, "\"https://wapm.io/ns/name\"");
355
356        let ident2 = serde_json::from_str::<StringWebcIdent>(&raw).unwrap();
357        assert_eq!(ident, ident2);
358    }
359
360    #[test]
361    fn test_serde_serialize_webc_str_ident_without_repo() {
362        // Serialize
363        let ident = StringWebcIdent(WebcIdent {
364            repository: None,
365            namespace: "ns".to_string(),
366            name: "name".to_string(),
367            tag: None,
368        });
369
370        let raw = serde_json::to_string(&ident).unwrap();
371        assert_eq!(raw, "\"ns/name\"");
372
373        let ident2 = serde_json::from_str::<StringWebcIdent>(&raw).unwrap();
374        assert_eq!(ident, ident2);
375    }
376}