Skip to main content

nu_command/semver/
value.rs

1use nu_protocol::{
2    ShellError, Span, Value,
3    ast::{Comparison, Operator},
4    casing::Casing,
5};
6use serde::{Deserialize, Serialize};
7use std::any::Any;
8use std::cmp::Ordering;
9use std::ops::Deref;
10
11#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
12pub struct SemverValue {
13    pub version: semver::Version,
14}
15
16#[typetag::serde]
17impl nu_protocol::CustomValue for SemverValue {
18    fn clone_value(&self, span: Span) -> Value {
19        Value::custom(Box::new(self.clone()), span)
20    }
21
22    fn type_name(&self) -> String {
23        "semver".to_string()
24    }
25
26    fn to_base_value(&self, span: Span) -> Result<Value, ShellError> {
27        Ok(Value::string(self.version.to_string(), span))
28    }
29
30    fn as_any(&self) -> &dyn Any {
31        self
32    }
33
34    fn as_mut_any(&mut self) -> &mut dyn Any {
35        self
36    }
37
38    fn partial_cmp(&self, other: &Value) -> Option<Ordering> {
39        match other {
40            Value::Custom { val, .. } if val.type_name() == self.type_name() => {
41                let other_version =
42                    val.to_base_value(other.span())
43                        .ok()
44                        .and_then(|value| match value {
45                            Value::String { val, .. } => semver::Version::parse(&val).ok(),
46                            _ => None,
47                        });
48
49                other_version.and_then(|other_version| self.version.partial_cmp(&other_version))
50            }
51            Value::String { val, .. } => semver::Version::parse(val)
52                .ok()
53                .and_then(|other_version| self.version.partial_cmp(&other_version)),
54            _ => None,
55        }
56    }
57
58    fn follow_path_string(
59        &self,
60        self_span: Span,
61        column_name: String,
62        path_span: Span,
63        _optional: bool,
64        casing: Casing,
65    ) -> Result<Value, ShellError> {
66        let col = match casing {
67            Casing::Sensitive => column_name,
68            Casing::Insensitive => column_name.to_lowercase(),
69        };
70
71        match col.as_str() {
72            "major" => Ok(Value::int(self.version.major as i64, path_span)),
73            "minor" => Ok(Value::int(self.version.minor as i64, path_span)),
74            "patch" => Ok(Value::int(self.version.patch as i64, path_span)),
75            "pre" => Ok(Value::string(self.version.pre.to_string(), path_span)),
76            "build" => Ok(Value::string(self.version.build.to_string(), path_span)),
77            _ => Err(ShellError::CantFindColumn {
78                col_name: col,
79                span: Some(path_span),
80                src_span: self_span,
81            }),
82        }
83    }
84
85    fn operation(
86        &self,
87        lhs_span: Span,
88        operator: Operator,
89        op: Span,
90        right: &Value,
91    ) -> Result<Value, ShellError> {
92        match operator {
93            Operator::Comparison(Comparison::In) => {
94                if let Value::Custom { val, .. } = right
95                    && let Some(range) = val
96                        .as_any()
97                        .downcast_ref::<super::range::SemverRangeValue>()
98                {
99                    return Ok(Value::bool(range.requirement.matches(&self.version), op));
100                }
101                Err(ShellError::OperatorIncompatibleTypes {
102                    op: operator,
103                    lhs: nu_protocol::Type::Custom("semver".into()),
104                    rhs: right.get_type(),
105                    op_span: op,
106                    lhs_span,
107                    rhs_span: right.span(),
108                    help: Some("expected a semver-range on the right side"),
109                })
110            }
111            _ => Err(ShellError::OperatorUnsupportedType {
112                op: operator,
113                unsupported: nu_protocol::Type::Custom(self.type_name().into()),
114                op_span: op,
115                unsupported_span: lhs_span,
116                help: None,
117            }),
118        }
119    }
120}
121
122impl SemverValue {
123    pub fn new(version: semver::Version) -> Self {
124        Self { version }
125    }
126
127    pub fn bump_major(&self) -> Self {
128        Self {
129            version: semver::Version {
130                major: self.version.major + 1,
131                minor: 0,
132                patch: 0,
133                pre: semver::Prerelease::EMPTY,
134                build: semver::BuildMetadata::EMPTY,
135            },
136        }
137    }
138
139    pub fn bump_minor(&self) -> Self {
140        Self {
141            version: semver::Version {
142                major: self.version.major,
143                minor: self.version.minor + 1,
144                patch: 0,
145                pre: semver::Prerelease::EMPTY,
146                build: semver::BuildMetadata::EMPTY,
147            },
148        }
149    }
150
151    pub fn bump_patch(&self) -> Self {
152        Self {
153            version: semver::Version {
154                major: self.version.major,
155                minor: self.version.minor,
156                patch: self.version.patch + 1,
157                pre: semver::Prerelease::EMPTY,
158                build: semver::BuildMetadata::EMPTY,
159            },
160        }
161    }
162
163    pub fn bump_prerelease(&self, tag: &str) -> Result<Self, ShellError> {
164        let current_pre = self.version.pre.as_str();
165
166        let new_pre = if current_pre.is_empty() {
167            format!("{}.1", tag)
168        } else if current_pre.starts_with(tag) {
169            if let Some(dot_pos) = current_pre.rfind('.') {
170                let suffix = &current_pre[dot_pos + 1..];
171                if let Ok(num) = suffix.parse::<u64>() {
172                    format!("{}.{}", tag, num + 1)
173                } else {
174                    format!("{}.1", tag)
175                }
176            } else {
177                format!("{}.1", tag)
178            }
179        } else {
180            format!("{}.0", tag)
181        };
182
183        let pre = semver::Prerelease::new(&new_pre).map_err(|e| {
184            ShellError::Generic(nu_protocol::shell_error::generic::GenericError::new(
185                "Invalid prerelease",
186                e.to_string(),
187                Span::unknown(),
188            ))
189        })?;
190
191        Ok(Self {
192            version: semver::Version {
193                major: self.version.major,
194                minor: self.version.minor,
195                patch: self.version.patch,
196                pre,
197                build: self.version.build.clone(),
198            },
199        })
200    }
201
202    pub fn bump_release(&self) -> Self {
203        Self {
204            version: semver::Version {
205                major: self.version.major,
206                minor: self.version.minor,
207                patch: self.version.patch,
208                pre: semver::Prerelease::EMPTY,
209                build: semver::BuildMetadata::EMPTY,
210            },
211        }
212    }
213
214    pub fn set_build_metadata(&self, metadata: &str) -> Result<Self, ShellError> {
215        let build = semver::BuildMetadata::new(metadata).map_err(|e| {
216            ShellError::Generic(nu_protocol::shell_error::generic::GenericError::new(
217                "Invalid build metadata",
218                e.to_string(),
219                Span::unknown(),
220            ))
221        })?;
222
223        Ok(Self {
224            version: semver::Version {
225                major: self.version.major,
226                minor: self.version.minor,
227                patch: self.version.patch,
228                pre: self.version.pre.clone(),
229                build,
230            },
231        })
232    }
233
234    /// For use by tests and examples only.
235    pub fn test_value(s: &str) -> Value {
236        Value::test_custom_value(Box::new(Self {
237            version: s
238                .parse::<semver::Version>()
239                .unwrap_or_else(|_| semver::Version::new(0, 0, 0)),
240        }))
241    }
242}
243
244impl<'a> TryFrom<&'a Value> for SemverValue {
245    type Error = ShellError;
246
247    fn try_from(value: &'a Value) -> Result<Self, Self::Error> {
248        let span = value.span();
249
250        match value {
251            Value::String { val, .. } => {
252                semver::Version::parse(val)
253                    .map(SemverValue::new)
254                    .map_err(|e| ShellError::IncorrectValue {
255                        msg: format!("Value is not a valid semver version: {e}"),
256                        val_span: span,
257                        call_span: span,
258                    })
259            }
260            Value::Custom { val, .. } => {
261                if let Some(semver) = val.as_any().downcast_ref::<Self>() {
262                    Ok(semver.clone())
263                } else {
264                    Err(ShellError::CantConvert {
265                        to_type: "semver".into(),
266                        from_type: val.type_name(),
267                        span,
268                        help: None,
269                    })
270                }
271            }
272            x => Err(ShellError::CantConvert {
273                to_type: "semver".into(),
274                from_type: x.get_type().to_string(),
275                span,
276                help: None,
277            }),
278        }
279    }
280}
281
282impl Deref for SemverValue {
283    type Target = semver::Version;
284
285    fn deref(&self) -> &Self::Target {
286        &self.version
287    }
288}
289#[cfg(test)]
290mod tests {
291    use super::*;
292    use nu_protocol::CustomValue;
293
294    #[test]
295    fn semver_custom_values_compare_equal_when_versions_match() {
296        let expected = Value::custom(
297            Box::new(SemverValue::new(semver::Version::parse("1.2.3").unwrap())),
298            Span::test_data(),
299        );
300        let got = Value::custom(
301            Box::new(SemverValue::new(semver::Version::parse("1.2.3").unwrap())),
302            Span::test_data(),
303        );
304
305        assert_eq!(expected.partial_cmp(&got), Some(Ordering::Equal));
306        assert_eq!(expected, got);
307    }
308
309    #[test]
310    fn semver_bump_example_result_compares_equal_through_tester() -> nu_test_support::Result {
311        let mut tester = nu_test_support::test();
312        let got: Value = tester.run("'1.2.3' | into semver | semver bump major")?;
313        let expected = SemverValue::test_value("2.0.0");
314
315        assert_eq!(got.partial_cmp(&expected), Some(Ordering::Equal));
316        assert_eq!(got, expected);
317        Ok(())
318    }
319
320    fn parse_version(s: &str) -> semver::Version {
321        semver::Version::parse(s).unwrap()
322    }
323
324    #[test]
325    fn test_new() {
326        let version = parse_version("1.2.3");
327        let semver_val = SemverValue::new(version.clone());
328        assert_eq!(semver_val.version, version);
329    }
330
331    #[test]
332    fn test_bump_major() {
333        let semver_val = SemverValue::new(parse_version("1.2.3"));
334        let bumped = semver_val.bump_major();
335        assert_eq!(bumped.version.to_string(), "2.0.0");
336
337        // Test with prerelease and build metadata
338        let semver_val = SemverValue::new(parse_version("1.2.3-alpha.1+build.2"));
339        let bumped = semver_val.bump_major();
340        assert_eq!(bumped.version.to_string(), "2.0.0");
341    }
342
343    #[test]
344    fn test_bump_minor() {
345        let semver_val = SemverValue::new(parse_version("1.2.3"));
346        let bumped = semver_val.bump_minor();
347        assert_eq!(bumped.version.to_string(), "1.3.0");
348
349        // Test with prerelease
350        let semver_val = SemverValue::new(parse_version("1.2.3-beta"));
351        let bumped = semver_val.bump_minor();
352        assert_eq!(bumped.version.to_string(), "1.3.0");
353    }
354
355    #[test]
356    fn test_bump_patch() {
357        let semver_val = SemverValue::new(parse_version("1.2.3"));
358        let bumped = semver_val.bump_patch();
359        assert_eq!(bumped.version.to_string(), "1.2.4");
360
361        // Test with build metadata
362        let semver_val = SemverValue::new(parse_version("1.2.3+build"));
363        let bumped = semver_val.bump_patch();
364        assert_eq!(bumped.version.to_string(), "1.2.4");
365    }
366
367    #[test]
368    fn test_bump_prerelease_empty() {
369        let semver_val = SemverValue::new(parse_version("1.2.3"));
370        let bumped = semver_val.bump_prerelease("alpha").unwrap();
371        assert_eq!(bumped.version.to_string(), "1.2.3-alpha.1");
372    }
373
374    #[test]
375    fn test_bump_prerelease_same_tag() {
376        let semver_val = SemverValue::new(parse_version("1.2.3-alpha.0"));
377        let bumped = semver_val.bump_prerelease("alpha").unwrap();
378        assert_eq!(bumped.version.to_string(), "1.2.3-alpha.1");
379
380        let semver_val = SemverValue::new(parse_version("1.2.3-alpha.5"));
381        let bumped = semver_val.bump_prerelease("alpha").unwrap();
382        assert_eq!(bumped.version.to_string(), "1.2.3-alpha.6");
383    }
384
385    #[test]
386    fn test_bump_prerelease_different_tag() {
387        let semver_val = SemverValue::new(parse_version("1.2.3-alpha.1"));
388        let bumped = semver_val.bump_prerelease("beta").unwrap();
389        assert_eq!(bumped.version.to_string(), "1.2.3-beta.0");
390    }
391
392    #[test]
393    fn test_bump_prerelease_no_number() {
394        let semver_val = SemverValue::new(parse_version("1.2.3-alpha"));
395        let bumped = semver_val.bump_prerelease("alpha").unwrap();
396        assert_eq!(bumped.version.to_string(), "1.2.3-alpha.1");
397    }
398
399    #[test]
400    fn test_bump_release() {
401        let semver_val = SemverValue::new(parse_version("1.2.3-alpha.1+build.2"));
402        let bumped = semver_val.bump_release();
403        assert_eq!(bumped.version.to_string(), "1.2.3");
404
405        let semver_val = SemverValue::new(parse_version("1.2.3"));
406        let bumped = semver_val.bump_release();
407        assert_eq!(bumped.version.to_string(), "1.2.3");
408    }
409
410    #[test]
411    fn test_partial_cmp() {
412        let v1 = SemverValue::new(parse_version("1.0.0"));
413        let v2 = SemverValue::new(parse_version("2.0.0"));
414        let v3 = SemverValue::new(parse_version("1.0.0"));
415
416        let val2 = Value::custom(Box::new(v2.clone()), Span::test_data());
417        let val1 = Value::custom(Box::new(v1.clone()), Span::test_data());
418        let val3 = Value::custom(Box::new(v3.clone()), Span::test_data());
419
420        assert_eq!(CustomValue::partial_cmp(&v1, &val2), Some(Ordering::Less));
421        assert_eq!(
422            CustomValue::partial_cmp(&v2, &val1),
423            Some(Ordering::Greater)
424        );
425        assert_eq!(CustomValue::partial_cmp(&v1, &val3), Some(Ordering::Equal));
426
427        // Test with semver string input
428        let string_val = Value::string("1.0.0", Span::test_data());
429        assert_eq!(
430            CustomValue::partial_cmp(&v1, &string_val),
431            Some(Ordering::Equal)
432        );
433
434        // Test with non-semver string input
435        let invalid_string_val = Value::string("not-a-version", Span::test_data());
436        assert_eq!(CustomValue::partial_cmp(&v1, &invalid_string_val), None);
437    }
438
439    #[test]
440    fn test_value_equality_for_semver_custom_values() {
441        let expected = SemverValue::test_value("2.0.0");
442        let actual = Value::custom(
443            Box::new(SemverValue::new(parse_version("2.0.0"))),
444            Span::test_data(),
445        );
446
447        assert_eq!(expected, actual);
448    }
449
450    #[test]
451    fn test_operation_in() {
452        use crate::semver::range::SemverRangeValue;
453
454        let version = SemverValue::new(parse_version("1.2.3"));
455        let range = SemverRangeValue::new(semver::VersionReq::parse(">=1.0.0").unwrap());
456
457        let range_val = Value::custom(Box::new(range), Span::test_data());
458
459        let result = version
460            .operation(
461                Span::test_data(),
462                Operator::Comparison(Comparison::In),
463                Span::test_data(),
464                &range_val,
465            )
466            .unwrap();
467
468        assert!(matches!(result, Value::Bool { val: true, .. }));
469
470        // Test with non-matching range
471        let range = SemverRangeValue::new(semver::VersionReq::parse(">=2.0.0").unwrap());
472        let range_val = Value::custom(Box::new(range), Span::test_data());
473
474        let result = version
475            .operation(
476                Span::test_data(),
477                Operator::Comparison(Comparison::In),
478                Span::test_data(),
479                &range_val,
480            )
481            .unwrap();
482
483        assert!(matches!(result, Value::Bool { val: false, .. }));
484    }
485
486    #[test]
487    fn test_operation_unsupported() {
488        let version = SemverValue::new(parse_version("1.2.3"));
489        let other = Value::int(42, Span::test_data());
490
491        let result = version.operation(
492            Span::test_data(),
493            Operator::Math(nu_protocol::ast::Math::Add),
494            Span::test_data(),
495            &other,
496        );
497
498        assert!(result.is_err());
499    }
500
501    #[test]
502    fn test_custom_value_trait() {
503        let version = SemverValue::new(parse_version("1.2.3"));
504
505        // Test type_name
506        assert_eq!(version.type_name(), "semver");
507
508        // Test to_base_value
509        let base = version.to_base_value(Span::test_data()).unwrap();
510        assert!(matches!(base, Value::String { val, .. } if val == "1.2.3"));
511
512        // Test clone_value
513        let cloned = version.clone_value(Span::test_data());
514        assert!(matches!(cloned, Value::Custom { .. }));
515
516        // Test as_any
517        let any = version.as_any();
518        assert!(any.downcast_ref::<SemverValue>().is_some());
519    }
520}