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)]
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() -> String {
304 "NamedPackageIdent".to_string()
305 }
306
307 fn json_schema(r#gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
308 String::json_schema(r#gen)
309 }
310}
311
312#[cfg(test)]
313mod tests {
314 use std::str::FromStr;
315
316 use crate::package::PackageParseError;
317
318 use super::*;
319
320 #[test]
321 fn test_parse_webc_ident() {
322 assert_eq!(
325 NamedPackageIdent::from_str("ns/name").unwrap(),
326 NamedPackageIdent {
327 registry: None,
328 namespace: Some("ns".to_string()),
329 name: "name".to_string(),
330 tag: None,
331 }
332 );
333
334 assert_eq!(
335 NamedPackageIdent::from_str("ns/name@").unwrap(),
336 NamedPackageIdent {
337 registry: None,
338 namespace: Some("ns".to_string()),
339 name: "name".to_string(),
340 tag: None,
341 },
342 "empty tag should be parsed as None"
343 );
344
345 assert_eq!(
346 NamedPackageIdent::from_str("ns/name@tag").unwrap(),
347 NamedPackageIdent {
348 registry: None,
349 namespace: Some("ns".to_string()),
350 name: "name".to_string(),
351 tag: Some(Tag::Named("tag".to_string())),
352 }
353 );
354
355 assert_eq!(
356 NamedPackageIdent::from_str("reg.com:ns/name").unwrap(),
357 NamedPackageIdent {
358 registry: Some("reg.com".to_string()),
359 namespace: Some("ns".to_string()),
360 name: "name".to_string(),
361 tag: None,
362 }
363 );
364
365 assert_eq!(
366 NamedPackageIdent::from_str("reg.com:ns/name@tag").unwrap(),
367 NamedPackageIdent {
368 registry: Some("reg.com".to_string()),
369 namespace: Some("ns".to_string()),
370 name: "name".to_string(),
371 tag: Some(Tag::Named("tag".to_string())),
372 }
373 );
374
375 assert_eq!(
376 NamedPackageIdent::from_str("reg.com:ns/name").unwrap(),
377 NamedPackageIdent {
378 registry: Some("reg.com".to_string()),
379 namespace: Some("ns".to_string()),
380 name: "name".to_string(),
381 tag: None,
382 }
383 );
384
385 assert_eq!(
386 NamedPackageIdent::from_str("reg.com:ns/name@tag").unwrap(),
387 NamedPackageIdent {
388 registry: Some("reg.com".to_string()),
389 namespace: Some("ns".to_string()),
390 name: "name".to_string(),
391 tag: Some(Tag::Named("tag".to_string())),
392 }
393 );
394
395 assert_eq!(
396 NamedPackageIdent::from_str("reg.com:ns/name").unwrap(),
397 NamedPackageIdent {
398 registry: Some("reg.com".to_string()),
399 namespace: Some("ns".to_string()),
400 name: "name".to_string(),
401 tag: None,
402 }
403 );
404
405 assert_eq!(
406 NamedPackageIdent::from_str("reg.com:ns/name@tag").unwrap(),
407 NamedPackageIdent {
408 registry: Some("reg.com".to_string()),
409 namespace: Some("ns".to_string()),
410 name: "name".to_string(),
411 tag: Some(Tag::Named("tag".to_string())),
412 }
413 );
414
415 assert_eq!(
418 NamedPackageIdent::from_str("alpha").unwrap(),
419 NamedPackageIdent {
420 registry: None,
421 namespace: None,
422 name: "alpha".to_string(),
423 tag: None,
424 },
425 );
426
427 assert_eq!(
428 NamedPackageIdent::from_str(""),
429 Err(PackageParseError::new("", "package name is required"))
430 );
431 }
432
433 #[test]
434 fn test_serde_serialize_package_ident_with_repo() {
435 let ident = NamedPackageIdent {
437 registry: Some("wapm.io".to_string()),
438 namespace: Some("ns".to_string()),
439 name: "name".to_string(),
440 tag: None,
441 };
442
443 let raw = serde_json::to_string(&ident).unwrap();
444 assert_eq!(raw, "\"wapm.io:ns/name\"");
445
446 let ident2 = serde_json::from_str::<NamedPackageIdent>(&raw).unwrap();
447 assert_eq!(ident, ident2);
448 }
449
450 #[test]
451 fn test_serde_serialize_webc_str_ident_without_repo() {
452 let ident = NamedPackageIdent {
454 registry: None,
455 namespace: Some("ns".to_string()),
456 name: "name".to_string(),
457 tag: None,
458 };
459
460 let raw = serde_json::to_string(&ident).unwrap();
461 assert_eq!(raw, "\"ns/name\"");
462
463 let ident2 = serde_json::from_str::<NamedPackageIdent>(&raw).unwrap();
464 assert_eq!(ident, ident2);
465 }
466
467 #[test]
468 fn test_named_package_ident_matches_id() {
469 assert!(
470 NamedPackageIdent::from_str("ns/name")
471 .unwrap()
472 .matches_id(&NamedPackageId::try_new("ns/name", "0.1.0").unwrap())
473 );
474
475 assert!(
476 NamedPackageIdent::from_str("ns/name")
477 .unwrap()
478 .matches_id(&NamedPackageId::try_new("ns/name", "1.0.1").unwrap())
479 );
480
481 assert!(
482 NamedPackageIdent::from_str("ns/name@1")
483 .unwrap()
484 .matches_id(&NamedPackageId::try_new("ns/name", "1.0.1").unwrap())
485 );
486
487 assert!(
488 !NamedPackageIdent::from_str("ns/name@2")
489 .unwrap()
490 .matches_id(&NamedPackageId::try_new("ns/name", "1.0.1").unwrap())
491 );
492 }
493}