wasmer_deploy_schema/schema/
webc.rs1use serde::{Deserialize, Serialize};
2
3#[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 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 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 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#[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 #[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 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 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 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 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}