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#[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 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(®)
130 .map_err(|e| PackageParseError::new(reg, e.to_string()))
131 .map(Some)
132 }
133
134 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 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 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 write!(&mut out, "{tag}").unwrap();
171 }
172
173 out
174 }
175
176 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 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 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 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 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}