Skip to main content

nu_command/semver/
value.rs

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/// A semantic version value, optionally carrying a display prefix from loose parsing.
13///
14/// Equality and ordering compare only [`version`]; `prefix` is presentation metadata
15/// (e.g. `"v"` from `v1.2.3` parsed with `--loose`). That means `v1.0.0` and `1.0.0`
16/// compare equal, and command `example` `result:` values that use [`SemverValue`] only
17/// lock the version identity—not the display form. Prefer `display()` / `to text` when
18/// tests need to assert a preserved prefix.
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct SemverValue {
21    pub version: semver::Version,
22    /// Original loose prefix such as `"v"`, `"v."`, or `"v:"`. Empty when none.
23    #[serde(default)]
24    pub prefix: String,
25}
26
27/// Compares only the underlying SemVer version; `prefix` is ignored.
28impl 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                // Prefer a direct downcast when both values share the same crate instance.
74                // Fall back to type_name + base-value parse so comparison still works when
75                // TypeIds diverge (e.g. unit tests that pull nu-command in twice via
76                // nu-test-support).
77                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                            // Accept loose prefixes so cross-crate fallback still works
86                            // when display form is e.g. "v1.2.3".
87                            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    /// Display form including any loose prefix (e.g. `v1.2.3`).
218    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    /// Parse from a string. When `loose` is true, accepts prefixes like `v`, `v.`, `v:`.
227    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 = &current_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    /// Convert a pipeline value into a [`SemverValue`].
346    ///
347    /// When `loose` is true, string inputs may use prefixes like `v1.2.3`.
348    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    /// For use by tests and examples only.
381    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        // Test with prerelease and build metadata
452        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        // Test with prerelease
464        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        // Test with build metadata
476        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        // Test with semver string input
542        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        // Test with non-semver string input
549        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        // Test with non-matching range
585        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        // Discussion #18705 case: 2.0.1 is not less than 1.9.9
644        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        // Equality / inequality
649        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        // Inclusive bounds
654        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        // Prerelease is less than the corresponding release (semver spec)
660        assert_bool_op(&v100_alpha, Comparison::LessThan, &val100, true);
661        assert_bool_op(&v100, Comparison::GreaterThan, &val100_alpha, true);
662
663        // String RHS
664        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        // Incompatible RHS
681        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        // Exercise the Value::{lt,gt,eq,...} path that dispatches to operation for custom values.
706        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        // Test type_name
741        assert_eq!(version.type_name(), "semver");
742
743        // Test to_base_value
744        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        // Test clone_value
752        let cloned = version.clone_value(Span::test_data());
753        assert!(matches!(cloned, Value::Custom { .. }));
754
755        // Test as_any
756        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}