1use super::parse;
2use nu_protocol::{
3 CustomValue, ShellError, Span, Value,
4 ast::{Comparison, Operator},
5 casing::Casing,
6};
7use serde::{Deserialize, Serialize};
8use std::any::Any;
9use std::cmp::Ordering;
10use std::ops::Deref;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct SemverValue {
21 pub version: semver::Version,
22 #[serde(default)]
24 pub prefix: String,
25}
26
27impl PartialEq for SemverValue {
29 fn eq(&self, other: &Self) -> bool {
30 self.version == other.version
31 }
32}
33
34impl Eq for SemverValue {}
35
36impl PartialOrd for SemverValue {
37 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
38 Some(self.cmp(other))
39 }
40}
41
42impl Ord for SemverValue {
43 fn cmp(&self, other: &Self) -> Ordering {
44 self.version.cmp(&other.version)
45 }
46}
47
48#[typetag::serde]
49impl nu_protocol::CustomValue for SemverValue {
50 fn clone_value(&self, span: Span) -> Value {
51 Value::custom(Box::new(self.clone()), span)
52 }
53
54 fn type_name(&self) -> String {
55 "semver".to_string()
56 }
57
58 fn to_base_value(&self, span: Span) -> Result<Value, ShellError> {
59 Ok(Value::string(self.display(), span))
60 }
61
62 fn as_any(&self) -> &dyn Any {
63 self
64 }
65
66 fn as_mut_any(&mut self) -> &mut dyn Any {
67 self
68 }
69
70 fn partial_cmp(&self, other: &Value) -> Option<Ordering> {
71 match other {
72 Value::Custom { val, .. } => {
73 if let Some(other) = val.as_any().downcast_ref::<SemverValue>() {
78 return self.version.partial_cmp(&other.version);
79 }
80 if val.type_name() == self.type_name() {
81 return val
82 .to_base_value(other.span())
83 .ok()
84 .and_then(|value| match value {
85 Value::String { val, .. } => {
88 parse::parse_version(&val, true).ok().map(|(v, _)| v)
89 }
90 _ => None,
91 })
92 .and_then(|other_version| self.version.partial_cmp(&other_version));
93 }
94 None
95 }
96 Value::String { val, .. } => parse::parse_version(val, true)
97 .ok()
98 .and_then(|(other_version, _)| self.version.partial_cmp(&other_version)),
99 _ => None,
100 }
101 }
102
103 fn follow_path_string(
104 &self,
105 self_span: Span,
106 column_name: String,
107 path_span: Span,
108 _optional: bool,
109 casing: Casing,
110 ) -> Result<Value, ShellError> {
111 let col = match casing {
112 Casing::Sensitive => column_name,
113 Casing::Insensitive => column_name.to_lowercase(),
114 };
115
116 match col.as_str() {
117 "major" => Ok(Value::int(self.version.major as i64, path_span)),
118 "minor" => Ok(Value::int(self.version.minor as i64, path_span)),
119 "patch" => Ok(Value::int(self.version.patch as i64, path_span)),
120 "pre" => Ok(Value::string(self.version.pre.to_string(), path_span)),
121 "build" => Ok(Value::string(self.version.build.to_string(), path_span)),
122 "prefix" => Ok(Value::string(self.prefix.clone(), path_span)),
123 _ => Err(ShellError::CantFindColumn {
124 col_name: col,
125 span: Some(path_span),
126 src_span: self_span,
127 }),
128 }
129 }
130
131 fn operation(
132 &self,
133 lhs_span: Span,
134 operator: Operator,
135 op: Span,
136 right: &Value,
137 ) -> Result<Value, ShellError> {
138 match operator {
139 Operator::Comparison(Comparison::In) => {
140 if let Value::Custom { val, .. } = right
141 && let Some(range) = val
142 .as_any()
143 .downcast_ref::<super::range::SemverRangeValue>()
144 {
145 return Ok(Value::bool(range.requirement.matches(&self.version), op));
146 }
147 Err(ShellError::OperatorIncompatibleTypes {
148 op: operator,
149 lhs: nu_protocol::Type::Custom("semver".into()),
150 rhs: right.get_type(),
151 op_span: op,
152 lhs_span,
153 rhs_span: right.span(),
154 help: Some("expected a semver-range on the right side"),
155 })
156 }
157 Operator::Comparison(
158 comparison @ (Comparison::Equal
159 | Comparison::NotEqual
160 | Comparison::LessThan
161 | Comparison::LessThanOrEqual
162 | Comparison::GreaterThan
163 | Comparison::GreaterThanOrEqual),
164 ) => match CustomValue::partial_cmp(self, right) {
165 Some(ordering) => {
166 let result = match comparison {
167 Comparison::Equal => ordering == Ordering::Equal,
168 Comparison::NotEqual => ordering != Ordering::Equal,
169 Comparison::LessThan => ordering == Ordering::Less,
170 Comparison::LessThanOrEqual => {
171 matches!(ordering, Ordering::Less | Ordering::Equal)
172 }
173 Comparison::GreaterThan => ordering == Ordering::Greater,
174 Comparison::GreaterThanOrEqual => {
175 matches!(ordering, Ordering::Greater | Ordering::Equal)
176 }
177 _ => unreachable!("matched only equality/ordering comparisons above"),
178 };
179 Ok(Value::bool(result, op))
180 }
181 None => Err(ShellError::OperatorIncompatibleTypes {
182 op: operator,
183 lhs: nu_protocol::Type::Custom("semver".into()),
184 rhs: right.get_type(),
185 op_span: op,
186 lhs_span,
187 rhs_span: right.span(),
188 help: Some("expected another semver or a valid version string"),
189 }),
190 },
191 _ => Err(ShellError::OperatorUnsupportedType {
192 op: operator,
193 unsupported: nu_protocol::Type::Custom(self.type_name().into()),
194 op_span: op,
195 unsupported_span: lhs_span,
196 help: None,
197 }),
198 }
199 }
200}
201
202impl SemverValue {
203 pub fn new(version: semver::Version) -> Self {
204 Self {
205 version,
206 prefix: String::new(),
207 }
208 }
209
210 pub fn with_prefix(version: semver::Version, prefix: impl Into<String>) -> Self {
211 Self {
212 version,
213 prefix: prefix.into(),
214 }
215 }
216
217 pub fn display(&self) -> String {
219 if self.prefix.is_empty() {
220 self.version.to_string()
221 } else {
222 format!("{}{}", self.prefix, self.version)
223 }
224 }
225
226 pub fn parse(s: &str, loose: bool) -> Result<Self, semver::Error> {
228 let (version, prefix) = parse::parse_version(s, loose)?;
229 Ok(Self::with_prefix(version, prefix))
230 }
231
232 pub fn bump_major(&self) -> Self {
233 Self::with_prefix(
234 semver::Version {
235 major: self.version.major + 1,
236 minor: 0,
237 patch: 0,
238 pre: semver::Prerelease::EMPTY,
239 build: semver::BuildMetadata::EMPTY,
240 },
241 self.prefix.clone(),
242 )
243 }
244
245 pub fn bump_minor(&self) -> Self {
246 Self::with_prefix(
247 semver::Version {
248 major: self.version.major,
249 minor: self.version.minor + 1,
250 patch: 0,
251 pre: semver::Prerelease::EMPTY,
252 build: semver::BuildMetadata::EMPTY,
253 },
254 self.prefix.clone(),
255 )
256 }
257
258 pub fn bump_patch(&self) -> Self {
259 Self::with_prefix(
260 semver::Version {
261 major: self.version.major,
262 minor: self.version.minor,
263 patch: self.version.patch + 1,
264 pre: semver::Prerelease::EMPTY,
265 build: semver::BuildMetadata::EMPTY,
266 },
267 self.prefix.clone(),
268 )
269 }
270
271 pub fn bump_prerelease(&self, tag: &str) -> Result<Self, ShellError> {
272 let current_pre = self.version.pre.as_str();
273
274 let new_pre = if current_pre.is_empty() {
275 format!("{}.1", tag)
276 } else if current_pre.starts_with(tag) {
277 if let Some(dot_pos) = current_pre.rfind('.') {
278 let suffix = ¤t_pre[dot_pos + 1..];
279 if let Ok(num) = suffix.parse::<u64>() {
280 format!("{}.{}", tag, num + 1)
281 } else {
282 format!("{}.1", tag)
283 }
284 } else {
285 format!("{}.1", tag)
286 }
287 } else {
288 format!("{}.0", tag)
289 };
290
291 let pre = semver::Prerelease::new(&new_pre).map_err(|e| {
292 ShellError::Generic(nu_protocol::shell_error::generic::GenericError::new(
293 "Invalid prerelease",
294 e.to_string(),
295 Span::unknown(),
296 ))
297 })?;
298
299 Ok(Self::with_prefix(
300 semver::Version {
301 major: self.version.major,
302 minor: self.version.minor,
303 patch: self.version.patch,
304 pre,
305 build: self.version.build.clone(),
306 },
307 self.prefix.clone(),
308 ))
309 }
310
311 pub fn bump_release(&self) -> Self {
312 Self::with_prefix(
313 semver::Version {
314 major: self.version.major,
315 minor: self.version.minor,
316 patch: self.version.patch,
317 pre: semver::Prerelease::EMPTY,
318 build: semver::BuildMetadata::EMPTY,
319 },
320 self.prefix.clone(),
321 )
322 }
323
324 pub fn set_build_metadata(&self, metadata: &str) -> Result<Self, ShellError> {
325 let build = semver::BuildMetadata::new(metadata).map_err(|e| {
326 ShellError::Generic(nu_protocol::shell_error::generic::GenericError::new(
327 "Invalid build metadata",
328 e.to_string(),
329 Span::unknown(),
330 ))
331 })?;
332
333 Ok(Self::with_prefix(
334 semver::Version {
335 major: self.version.major,
336 minor: self.version.minor,
337 patch: self.version.patch,
338 pre: self.version.pre.clone(),
339 build,
340 },
341 self.prefix.clone(),
342 ))
343 }
344
345 pub fn try_from_value(value: &Value, loose: bool) -> Result<Self, ShellError> {
349 let span = value.span();
350
351 match value {
352 Value::String { val, .. } => {
353 Self::parse(val, loose).map_err(|e| ShellError::IncorrectValue {
354 msg: format!("Value is not a valid semver version: {e}"),
355 val_span: span,
356 call_span: span,
357 })
358 }
359 Value::Custom { val, .. } => {
360 if let Some(semver) = val.as_any().downcast_ref::<Self>() {
361 Ok(semver.clone())
362 } else {
363 Err(ShellError::CantConvert {
364 to_type: "semver".into(),
365 from_type: val.type_name(),
366 span,
367 help: None,
368 })
369 }
370 }
371 x => Err(ShellError::CantConvert {
372 to_type: "semver".into(),
373 from_type: x.get_type().to_string(),
374 span,
375 help: None,
376 }),
377 }
378 }
379
380 pub fn test_value(s: &str) -> Value {
382 let semver =
383 Self::parse(s, true).unwrap_or_else(|_| Self::new(semver::Version::new(0, 0, 0)));
384 Value::test_custom_value(Box::new(semver))
385 }
386}
387
388impl<'a> TryFrom<&'a Value> for SemverValue {
389 type Error = ShellError;
390
391 fn try_from(value: &'a Value) -> Result<Self, Self::Error> {
392 Self::try_from_value(value, false)
393 }
394}
395
396impl Deref for SemverValue {
397 type Target = semver::Version;
398
399 fn deref(&self) -> &Self::Target {
400 &self.version
401 }
402}
403#[cfg(test)]
404mod tests {
405 use super::*;
406 use nu_protocol::CustomValue;
407
408 #[test]
409 fn semver_custom_values_compare_equal_when_versions_match() {
410 let expected = Value::custom(
411 Box::new(SemverValue::new(semver::Version::parse("1.2.3").unwrap())),
412 Span::test_data(),
413 );
414 let got = Value::custom(
415 Box::new(SemverValue::new(semver::Version::parse("1.2.3").unwrap())),
416 Span::test_data(),
417 );
418
419 assert_eq!(expected.partial_cmp(&got), Some(Ordering::Equal));
420 assert_eq!(expected, got);
421 }
422
423 #[test]
424 fn semver_bump_example_result_compares_equal_through_tester() -> nu_test_support::Result {
425 let mut tester = nu_test_support::test();
426 let got: Value = tester.run("'1.2.3' | into semver | semver bump major")?;
427 let expected = SemverValue::test_value("2.0.0");
428
429 assert_eq!(got.partial_cmp(&expected), Some(Ordering::Equal));
430 assert_eq!(got, expected);
431 Ok(())
432 }
433
434 fn parse_version(s: &str) -> semver::Version {
435 semver::Version::parse(s).unwrap()
436 }
437
438 #[test]
439 fn test_new() {
440 let version = parse_version("1.2.3");
441 let semver_val = SemverValue::new(version.clone());
442 assert_eq!(semver_val.version, version);
443 }
444
445 #[test]
446 fn test_bump_major() {
447 let semver_val = SemverValue::new(parse_version("1.2.3"));
448 let bumped = semver_val.bump_major();
449 assert_eq!(bumped.version.to_string(), "2.0.0");
450
451 let semver_val = SemverValue::new(parse_version("1.2.3-alpha.1+build.2"));
453 let bumped = semver_val.bump_major();
454 assert_eq!(bumped.version.to_string(), "2.0.0");
455 }
456
457 #[test]
458 fn test_bump_minor() {
459 let semver_val = SemverValue::new(parse_version("1.2.3"));
460 let bumped = semver_val.bump_minor();
461 assert_eq!(bumped.version.to_string(), "1.3.0");
462
463 let semver_val = SemverValue::new(parse_version("1.2.3-beta"));
465 let bumped = semver_val.bump_minor();
466 assert_eq!(bumped.version.to_string(), "1.3.0");
467 }
468
469 #[test]
470 fn test_bump_patch() {
471 let semver_val = SemverValue::new(parse_version("1.2.3"));
472 let bumped = semver_val.bump_patch();
473 assert_eq!(bumped.version.to_string(), "1.2.4");
474
475 let semver_val = SemverValue::new(parse_version("1.2.3+build"));
477 let bumped = semver_val.bump_patch();
478 assert_eq!(bumped.version.to_string(), "1.2.4");
479 }
480
481 #[test]
482 fn test_bump_prerelease_empty() {
483 let semver_val = SemverValue::new(parse_version("1.2.3"));
484 let bumped = semver_val.bump_prerelease("alpha").unwrap();
485 assert_eq!(bumped.version.to_string(), "1.2.3-alpha.1");
486 }
487
488 #[test]
489 fn test_bump_prerelease_same_tag() {
490 let semver_val = SemverValue::new(parse_version("1.2.3-alpha.0"));
491 let bumped = semver_val.bump_prerelease("alpha").unwrap();
492 assert_eq!(bumped.version.to_string(), "1.2.3-alpha.1");
493
494 let semver_val = SemverValue::new(parse_version("1.2.3-alpha.5"));
495 let bumped = semver_val.bump_prerelease("alpha").unwrap();
496 assert_eq!(bumped.version.to_string(), "1.2.3-alpha.6");
497 }
498
499 #[test]
500 fn test_bump_prerelease_different_tag() {
501 let semver_val = SemverValue::new(parse_version("1.2.3-alpha.1"));
502 let bumped = semver_val.bump_prerelease("beta").unwrap();
503 assert_eq!(bumped.version.to_string(), "1.2.3-beta.0");
504 }
505
506 #[test]
507 fn test_bump_prerelease_no_number() {
508 let semver_val = SemverValue::new(parse_version("1.2.3-alpha"));
509 let bumped = semver_val.bump_prerelease("alpha").unwrap();
510 assert_eq!(bumped.version.to_string(), "1.2.3-alpha.1");
511 }
512
513 #[test]
514 fn test_bump_release() {
515 let semver_val = SemverValue::new(parse_version("1.2.3-alpha.1+build.2"));
516 let bumped = semver_val.bump_release();
517 assert_eq!(bumped.version.to_string(), "1.2.3");
518
519 let semver_val = SemverValue::new(parse_version("1.2.3"));
520 let bumped = semver_val.bump_release();
521 assert_eq!(bumped.version.to_string(), "1.2.3");
522 }
523
524 #[test]
525 fn test_partial_cmp() {
526 let v1 = SemverValue::new(parse_version("1.0.0"));
527 let v2 = SemverValue::new(parse_version("2.0.0"));
528 let v3 = SemverValue::new(parse_version("1.0.0"));
529
530 let val2 = Value::custom(Box::new(v2.clone()), Span::test_data());
531 let val1 = Value::custom(Box::new(v1.clone()), Span::test_data());
532 let val3 = Value::custom(Box::new(v3.clone()), Span::test_data());
533
534 assert_eq!(CustomValue::partial_cmp(&v1, &val2), Some(Ordering::Less));
535 assert_eq!(
536 CustomValue::partial_cmp(&v2, &val1),
537 Some(Ordering::Greater)
538 );
539 assert_eq!(CustomValue::partial_cmp(&v1, &val3), Some(Ordering::Equal));
540
541 let string_val = Value::string("1.0.0", Span::test_data());
543 assert_eq!(
544 CustomValue::partial_cmp(&v1, &string_val),
545 Some(Ordering::Equal)
546 );
547
548 let invalid_string_val = Value::string("not-a-version", Span::test_data());
550 assert_eq!(CustomValue::partial_cmp(&v1, &invalid_string_val), None);
551 }
552
553 #[test]
554 fn test_value_equality_for_semver_custom_values() {
555 let expected = SemverValue::test_value("2.0.0");
556 let actual = Value::custom(
557 Box::new(SemverValue::new(parse_version("2.0.0"))),
558 Span::test_data(),
559 );
560
561 assert_eq!(expected, actual);
562 }
563
564 #[test]
565 fn test_operation_in() {
566 use crate::semver::range::SemverRangeValue;
567
568 let version = SemverValue::new(parse_version("1.2.3"));
569 let range = SemverRangeValue::new(semver::VersionReq::parse(">=1.0.0").unwrap());
570
571 let range_val = Value::custom(Box::new(range), Span::test_data());
572
573 let result = version
574 .operation(
575 Span::test_data(),
576 Operator::Comparison(Comparison::In),
577 Span::test_data(),
578 &range_val,
579 )
580 .unwrap();
581
582 assert!(matches!(result, Value::Bool { val: true, .. }));
583
584 let range = SemverRangeValue::new(semver::VersionReq::parse(">=2.0.0").unwrap());
586 let range_val = Value::custom(Box::new(range), Span::test_data());
587
588 let result = version
589 .operation(
590 Span::test_data(),
591 Operator::Comparison(Comparison::In),
592 Span::test_data(),
593 &range_val,
594 )
595 .unwrap();
596
597 assert!(matches!(result, Value::Bool { val: false, .. }));
598 }
599
600 #[test]
601 fn test_operation_unsupported() {
602 let version = SemverValue::new(parse_version("1.2.3"));
603 let other = Value::int(42, Span::test_data());
604
605 let result = version.operation(
606 Span::test_data(),
607 Operator::Math(nu_protocol::ast::Math::Add),
608 Span::test_data(),
609 &other,
610 );
611
612 assert!(result.is_err());
613 }
614
615 fn assert_bool_op(left: &SemverValue, comparison: Comparison, right: &Value, expected: bool) {
616 let result = left
617 .operation(
618 Span::test_data(),
619 Operator::Comparison(comparison),
620 Span::test_data(),
621 right,
622 )
623 .unwrap();
624 assert!(matches!(result, Value::Bool { val, .. } if val == expected));
625 }
626
627 #[test]
628 fn test_operation_comparisons() {
629 let v201 = SemverValue::new(parse_version("2.0.1"));
630 let v199 = SemverValue::new(parse_version("1.9.9"));
631 let v100 = SemverValue::new(parse_version("1.0.0"));
632 let v100_again = SemverValue::new(parse_version("1.0.0"));
633 let v200 = SemverValue::new(parse_version("2.0.0"));
634 let v100_alpha = SemverValue::new(parse_version("1.0.0-alpha"));
635
636 let val201 = Value::custom(Box::new(v201.clone()), Span::test_data());
637 let val199 = Value::custom(Box::new(v199.clone()), Span::test_data());
638 let val100 = Value::custom(Box::new(v100.clone()), Span::test_data());
639 let val100_again = Value::custom(Box::new(v100_again.clone()), Span::test_data());
640 let val200 = Value::custom(Box::new(v200.clone()), Span::test_data());
641 let val100_alpha = Value::custom(Box::new(v100_alpha.clone()), Span::test_data());
642
643 assert_bool_op(&v201, Comparison::LessThan, &val199, false);
645 assert_bool_op(&v201, Comparison::GreaterThan, &val199, true);
646 assert_bool_op(&v199, Comparison::LessThan, &val201, true);
647
648 assert_bool_op(&v100, Comparison::Equal, &val100_again, true);
650 assert_bool_op(&v100, Comparison::NotEqual, &val200, true);
651 assert_bool_op(&v100, Comparison::Equal, &val200, false);
652
653 assert_bool_op(&v100, Comparison::LessThanOrEqual, &val100_again, true);
655 assert_bool_op(&v100, Comparison::GreaterThanOrEqual, &val100_again, true);
656 assert_bool_op(&v100, Comparison::LessThanOrEqual, &val200, true);
657 assert_bool_op(&v200, Comparison::GreaterThanOrEqual, &val100, true);
658
659 assert_bool_op(&v100_alpha, Comparison::LessThan, &val100, true);
661 assert_bool_op(&v100, Comparison::GreaterThan, &val100_alpha, true);
662
663 let string_eq = Value::string("1.2.3", Span::test_data());
665 let v123 = SemverValue::new(parse_version("1.2.3"));
666 assert_bool_op(&v123, Comparison::Equal, &string_eq, true);
667 assert_bool_op(
668 &v123,
669 Comparison::LessThan,
670 &Value::string("2.0.0", Span::test_data()),
671 true,
672 );
673 assert_bool_op(
674 &v201,
675 Comparison::GreaterThan,
676 &Value::string("1.9.9", Span::test_data()),
677 true,
678 );
679
680 let int_rhs = Value::int(42, Span::test_data());
682 assert!(
683 v100.operation(
684 Span::test_data(),
685 Operator::Comparison(Comparison::Equal),
686 Span::test_data(),
687 &int_rhs,
688 )
689 .is_err()
690 );
691 let invalid_string = Value::string("not-a-version", Span::test_data());
692 assert!(
693 v100.operation(
694 Span::test_data(),
695 Operator::Comparison(Comparison::LessThan),
696 Span::test_data(),
697 &invalid_string,
698 )
699 .is_err()
700 );
701 }
702
703 #[test]
704 fn test_value_comparison_methods() {
705 let left = SemverValue::test_value("2.0.1");
707 let right = SemverValue::test_value("1.9.9");
708 let span = Span::test_data();
709
710 assert!(matches!(
711 left.gt(span, &right, span).unwrap(),
712 Value::Bool { val: true, .. }
713 ));
714 assert!(matches!(
715 left.lt(span, &right, span).unwrap(),
716 Value::Bool { val: false, .. }
717 ));
718 assert!(matches!(
719 left.eq(span, &left, span).unwrap(),
720 Value::Bool { val: true, .. }
721 ));
722 assert!(matches!(
723 left.ne(span, &right, span).unwrap(),
724 Value::Bool { val: true, .. }
725 ));
726 assert!(matches!(
727 left.gte(span, &right, span).unwrap(),
728 Value::Bool { val: true, .. }
729 ));
730 assert!(matches!(
731 right.lte(span, &left, span).unwrap(),
732 Value::Bool { val: true, .. }
733 ));
734 }
735
736 #[test]
737 fn test_custom_value_trait() {
738 let version = SemverValue::new(parse_version("1.2.3"));
739
740 assert_eq!(version.type_name(), "semver");
742
743 let base = version.to_base_value(Span::test_data()).unwrap();
745 assert!(matches!(base, Value::String { val, .. } if val == "1.2.3"));
746
747 let prefixed = SemverValue::parse("v1.2.3", true).unwrap();
748 let base = prefixed.to_base_value(Span::test_data()).unwrap();
749 assert!(matches!(base, Value::String { val, .. } if val == "v1.2.3"));
750
751 let cloned = version.clone_value(Span::test_data());
753 assert!(matches!(cloned, Value::Custom { .. }));
754
755 let any = version.as_any();
757 assert!(any.downcast_ref::<SemverValue>().is_some());
758 }
759
760 #[test]
761 fn test_follow_path_prefix() {
762 let plain = SemverValue::new(parse_version("1.2.3"));
763 let prefix = plain
764 .follow_path_string(
765 Span::test_data(),
766 "prefix".into(),
767 Span::test_data(),
768 false,
769 Casing::Sensitive,
770 )
771 .unwrap();
772 assert!(matches!(prefix, Value::String { val, .. } if val.is_empty()));
773
774 let prefixed = SemverValue::parse("v1.2.3", true).unwrap();
775 let prefix = prefixed
776 .follow_path_string(
777 Span::test_data(),
778 "prefix".into(),
779 Span::test_data(),
780 false,
781 Casing::Sensitive,
782 )
783 .unwrap();
784 assert!(matches!(prefix, Value::String { val, .. } if val == "v"));
785
786 let major = prefixed
787 .follow_path_string(
788 Span::test_data(),
789 "major".into(),
790 Span::test_data(),
791 false,
792 Casing::Sensitive,
793 )
794 .unwrap();
795 assert!(matches!(major, Value::Int { val: 1, .. }));
796 }
797}