1use std::{borrow::Cow, 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)]
61pub struct NamedPackageIdent {
62 pub registry: Option<String>,
63 pub namespace: Option<String>,
64 pub name: String,
65 pub tag: Option<Tag>,
66}
67
68impl NamedPackageIdent {
69 pub fn try_from_full_name_and_version(
70 full_name: &str,
71 version: &str,
72 ) -> Result<Self, PackageParseError> {
73 let (namespace, name) = match full_name.split_once('/') {
74 Some((ns, name)) => (Some(ns.to_owned()), name.to_owned()),
75 None => (None, full_name.to_owned()),
76 };
77
78 let version = version
79 .parse::<VersionReq>()
80 .map_err(|e| PackageParseError::new(version, e.to_string()))?;
81
82 Ok(Self {
83 registry: None,
84 namespace,
85 name,
86 tag: Some(Tag::VersionReq(version)),
87 })
88 }
89
90 pub fn tag_str(&self) -> Option<String> {
91 self.tag.as_ref().map(|x| x.to_string())
92 }
93
94 pub fn full_name(&self) -> String {
98 if let Some(ns) = &self.namespace {
99 format!("{}/{}", ns, self.name)
100 } else {
101 self.name.clone()
102 }
103 }
104
105 pub fn version_opt(&self) -> Option<&VersionReq> {
106 match &self.tag {
107 Some(Tag::VersionReq(v)) => Some(v),
108 Some(Tag::Named(_)) | None => None,
109 }
110 }
111
112 pub fn version_or_default(&self) -> VersionReq {
113 match &self.tag {
114 Some(Tag::VersionReq(v)) => v.clone(),
115 Some(Tag::Named(_)) | None => semver::VersionReq::STAR,
116 }
117 }
118
119 pub fn registry_url(&self) -> Result<Option<url::Url>, PackageParseError> {
120 let Some(reg) = &self.registry else {
121 return Ok(None);
122 };
123
124 let reg = if !reg.starts_with("http://") && !reg.starts_with("https://") {
125 format!("https://{reg}")
126 } else {
127 reg.clone()
128 };
129
130 url::Url::parse(®)
131 .map_err(|e| PackageParseError::new(reg, e.to_string()))
132 .map(Some)
133 }
134
135 pub fn build_identifier(&self) -> String {
140 let mut ident = if let Some(ns) = &self.namespace {
141 format!("{}/{}", ns, self.name)
142 } else {
143 self.name.to_string()
144 };
145
146 if let Some(tag) = &self.tag {
147 ident.push('@');
148 write!(&mut ident, "{tag}").unwrap();
150 }
151 ident
152 }
153
154 pub fn build(&self) -> String {
155 let mut out = String::new();
156 if let Some(url) = &self.registry {
157 write!(&mut out, "{url}").unwrap();
159
160 if !out.ends_with('/') {
161 out.push(':');
162 }
163 }
164 if let Some(ns) = &self.namespace {
165 out.push_str(ns);
166 out.push('/');
167 }
168 out.push_str(&self.name);
169 if let Some(tag) = &self.tag {
170 out.push('@');
171 write!(&mut out, "{tag}").unwrap();
173 }
174
175 out
176 }
177
178 pub fn matches_id(&self, id: &NamedPackageId) -> bool {
182 if self.full_name() == id.full_name {
183 if let Some(tag) = &self.tag {
184 match tag {
185 Tag::Named(n) => n == &id.version.to_string(),
186 Tag::VersionReq(v) => v.matches(&id.version),
187 }
188 } else {
189 true
190 }
191 } else {
192 false
193 }
194 }
195}
196
197impl From<NamedPackageId> for NamedPackageIdent {
198 fn from(value: NamedPackageId) -> Self {
199 let (namespace, name) = match value.full_name.split_once('/') {
200 Some((ns, name)) => (Some(ns.to_owned()), name.to_owned()),
201 None => (None, value.full_name),
202 };
203
204 Self {
205 registry: None,
206 namespace,
207 name,
208 tag: Some(Tag::VersionReq(semver::VersionReq {
209 comparators: vec![semver::Comparator {
210 op: semver::Op::Exact,
211 major: value.version.major,
212 minor: Some(value.version.minor),
213 patch: Some(value.version.patch),
214 pre: value.version.pre,
215 }],
216 })),
217 }
218 }
219}
220
221impl std::str::FromStr for NamedPackageIdent {
222 type Err = PackageParseError;
223
224 fn from_str(value: &str) -> Result<Self, Self::Err> {
225 let (rest, tag_opt) = value
226 .trim()
227 .rsplit_once('@')
228 .map(|(x, y)| (x, if y.is_empty() { None } else { Some(y) }))
229 .unwrap_or((value, None));
230
231 let tag = if let Some(v) = tag_opt.filter(|x| !x.is_empty()) {
232 Some(Tag::from_str(v)?)
233 } else {
234 None
235 };
236
237 let (rest, name) = if let Some((r, n)) = rest.rsplit_once('/') {
238 (r, n)
239 } else {
240 ("", rest)
241 };
242
243 let name = name.trim();
244 if name.is_empty() {
245 return Err(PackageParseError::new(value, "package name is required"));
246 }
247
248 let (rest, namespace) = if rest.is_empty() {
249 ("", None)
250 } else {
251 let (rest, ns) = rest.rsplit_once(':').unwrap_or(("", rest));
252
253 let ns = ns.trim();
254
255 if ns.is_empty() {
256 return Err(PackageParseError::new(value, "namespace can not be empty"));
257 }
258 (rest, Some(ns.to_string()))
259 };
260
261 let rest = rest.trim();
262 let registry = if rest.is_empty() {
263 None
264 } else {
265 Some(rest.to_string())
266 };
267
268 Ok(Self {
269 registry,
270 namespace,
271 name: name.to_string(),
272 tag,
273 })
274 }
275}
276
277impl std::fmt::Display for NamedPackageIdent {
278 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
279 write!(f, "{}", self.build())
280 }
281}
282
283impl serde::Serialize for NamedPackageIdent {
284 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
285 where
286 S: serde::ser::Serializer,
287 {
288 self.to_string().serialize(serializer)
289 }
290}
291
292impl<'de> serde::Deserialize<'de> for NamedPackageIdent {
293 fn deserialize<D>(deserializer: D) -> Result<NamedPackageIdent, D::Error>
294 where
295 D: serde::de::Deserializer<'de>,
296 {
297 let s = String::deserialize(deserializer)?;
298 Self::from_str(&s).map_err(serde::de::Error::custom)
299 }
300}
301
302impl schemars::JsonSchema for NamedPackageIdent {
303 fn schema_name() -> Cow<'static, str> {
304 Cow::Borrowed("NamedPackageIdent")
305 }
306
307 fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
308 String::json_schema(generator)
309 }
310
311 fn inline_schema() -> bool {
312 false
313 }
314
315 fn schema_id() -> Cow<'static, str> {
316 Self::schema_name()
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use std::str::FromStr;
323
324 use crate::package::PackageParseError;
325
326 use super::*;
327
328 #[test]
329 fn test_parse_webc_ident() {
330 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 );
341
342 assert_eq!(
343 NamedPackageIdent::from_str("ns/name@").unwrap(),
344 NamedPackageIdent {
345 registry: None,
346 namespace: Some("ns".to_string()),
347 name: "name".to_string(),
348 tag: None,
349 },
350 "empty tag should be parsed as None"
351 );
352
353 assert_eq!(
354 NamedPackageIdent::from_str("ns/name@tag").unwrap(),
355 NamedPackageIdent {
356 registry: None,
357 namespace: Some("ns".to_string()),
358 name: "name".to_string(),
359 tag: Some(Tag::Named("tag".to_string())),
360 }
361 );
362
363 assert_eq!(
364 NamedPackageIdent::from_str("reg.com:ns/name").unwrap(),
365 NamedPackageIdent {
366 registry: Some("reg.com".to_string()),
367 namespace: Some("ns".to_string()),
368 name: "name".to_string(),
369 tag: None,
370 }
371 );
372
373 assert_eq!(
374 NamedPackageIdent::from_str("reg.com:ns/name@tag").unwrap(),
375 NamedPackageIdent {
376 registry: Some("reg.com".to_string()),
377 namespace: Some("ns".to_string()),
378 name: "name".to_string(),
379 tag: Some(Tag::Named("tag".to_string())),
380 }
381 );
382
383 assert_eq!(
384 NamedPackageIdent::from_str("reg.com:ns/name").unwrap(),
385 NamedPackageIdent {
386 registry: Some("reg.com".to_string()),
387 namespace: Some("ns".to_string()),
388 name: "name".to_string(),
389 tag: None,
390 }
391 );
392
393 assert_eq!(
394 NamedPackageIdent::from_str("reg.com:ns/name@tag").unwrap(),
395 NamedPackageIdent {
396 registry: Some("reg.com".to_string()),
397 namespace: Some("ns".to_string()),
398 name: "name".to_string(),
399 tag: Some(Tag::Named("tag".to_string())),
400 }
401 );
402
403 assert_eq!(
404 NamedPackageIdent::from_str("reg.com:ns/name").unwrap(),
405 NamedPackageIdent {
406 registry: Some("reg.com".to_string()),
407 namespace: Some("ns".to_string()),
408 name: "name".to_string(),
409 tag: None,
410 }
411 );
412
413 assert_eq!(
414 NamedPackageIdent::from_str("reg.com:ns/name@tag").unwrap(),
415 NamedPackageIdent {
416 registry: Some("reg.com".to_string()),
417 namespace: Some("ns".to_string()),
418 name: "name".to_string(),
419 tag: Some(Tag::Named("tag".to_string())),
420 }
421 );
422
423 assert_eq!(
426 NamedPackageIdent::from_str("alpha").unwrap(),
427 NamedPackageIdent {
428 registry: None,
429 namespace: None,
430 name: "alpha".to_string(),
431 tag: None,
432 },
433 );
434
435 assert_eq!(
436 NamedPackageIdent::from_str(""),
437 Err(PackageParseError::new("", "package name is required"))
438 );
439 }
440
441 #[test]
442 fn test_serde_serialize_package_ident_with_repo() {
443 let ident = NamedPackageIdent {
445 registry: Some("wapm.io".to_string()),
446 namespace: Some("ns".to_string()),
447 name: "name".to_string(),
448 tag: None,
449 };
450
451 let raw = serde_json::to_string(&ident).unwrap();
452 assert_eq!(raw, "\"wapm.io:ns/name\"");
453
454 let ident2 = serde_json::from_str::<NamedPackageIdent>(&raw).unwrap();
455 assert_eq!(ident, ident2);
456 }
457
458 #[test]
459 fn test_serde_serialize_webc_str_ident_without_repo() {
460 let ident = NamedPackageIdent {
462 registry: None,
463 namespace: Some("ns".to_string()),
464 name: "name".to_string(),
465 tag: None,
466 };
467
468 let raw = serde_json::to_string(&ident).unwrap();
469 assert_eq!(raw, "\"ns/name\"");
470
471 let ident2 = serde_json::from_str::<NamedPackageIdent>(&raw).unwrap();
472 assert_eq!(ident, ident2);
473 }
474
475 #[test]
476 fn test_named_package_ident_matches_id() {
477 assert!(
478 NamedPackageIdent::from_str("ns/name")
479 .unwrap()
480 .matches_id(&NamedPackageId::try_new("ns/name", "0.1.0").unwrap())
481 );
482
483 assert!(
484 NamedPackageIdent::from_str("ns/name")
485 .unwrap()
486 .matches_id(&NamedPackageId::try_new("ns/name", "1.0.1").unwrap())
487 );
488
489 assert!(
490 NamedPackageIdent::from_str("ns/name@1")
491 .unwrap()
492 .matches_id(&NamedPackageId::try_new("ns/name", "1.0.1").unwrap())
493 );
494
495 assert!(
496 !NamedPackageIdent::from_str("ns/name@2")
497 .unwrap()
498 .matches_id(&NamedPackageId::try_new("ns/name", "1.0.1").unwrap())
499 );
500 }
501}