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 = ¤t_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 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 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 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 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 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 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 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 assert_eq!(version.type_name(), "semver");
507
508 let base = version.to_base_value(Span::test_data()).unwrap();
510 assert!(matches!(base, Value::String { val, .. } if val == "1.2.3"));
511
512 let cloned = version.clone_value(Span::test_data());
514 assert!(matches!(cloned, Value::Custom { .. }));
515
516 let any = version.as_any();
518 assert!(any.downcast_ref::<SemverValue>().is_some());
519 }
520}