1use crate::semver::value::SemverValue;
2use nu_cmd_base::input_handler::{CmdArgument, operate};
3use nu_engine::command_prelude::*;
4use nu_protocol::shell_error::generic::GenericError;
5
6#[derive(Clone)]
7pub struct IntoSemver;
8
9impl Command for IntoSemver {
10 fn name(&self) -> &str {
11 "into semver"
12 }
13
14 fn signature(&self) -> Signature {
15 Signature::build("into semver")
16 .input_output_types(vec![
17 (Type::String, Type::custom("semver")),
18 (Type::custom("semver"), Type::custom("semver")),
19 (Type::record(), Type::custom("semver")),
20 (Type::record(), Type::record()),
21 (Type::table(), Type::table()),
22 (Type::list(Type::String), Type::list(Type::custom("semver"))),
23 (
24 Type::list(Type::custom("semver")),
25 Type::list(Type::custom("semver")),
26 ),
27 (Type::table(), Type::list(Type::custom("semver"))),
28 (Type::Any, Type::custom("semver")),
30 ])
31 .switch(
32 "loose",
33 "Allow common non-strict prefixes such as v1.2.3, v.1.2.3, v:1.2.3, v-1.2.3, or v_1.2.3",
34 Some('l'),
35 )
36 .rest(
37 "rest",
38 SyntaxShape::CellPath,
39 "For a data structure input, convert data at the given cell paths.",
40 )
41 .category(Category::Conversions)
42 }
43
44 fn description(&self) -> &str {
45 "Convert a value (string, record, or semver) to a semantic version."
46 }
47
48 fn extra_description(&self) -> &str {
49 "From a record: major/minor/patch are required; pre, build, and prefix are optional. \
50 prefix is display-only metadata (e.g. \"v\"); re-parsing from text with --loose only \
51 accepts recognized loose prefixes (v/V with optional . : - _)."
52 }
53
54 fn search_terms(&self) -> Vec<&str> {
55 vec!["version", "convert", "semantic"]
56 }
57
58 fn run(
59 &self,
60 engine_state: &EngineState,
61 stack: &mut Stack,
62 call: &Call,
63 input: PipelineData,
64 ) -> Result<PipelineData, ShellError> {
65 let head = call.head;
66 let cell_paths = call.rest(engine_state, stack, 0)?;
67 let loose = call.has_flag(engine_state, stack, "loose")?;
68 let args = Arguments {
69 cell_paths: (!cell_paths.is_empty()).then_some(cell_paths),
70 loose,
71 };
72 operate(into_semver, args, input, head, engine_state.signals())
73 }
74
75 fn examples(&self) -> Vec<Example<'static>> {
76 vec![
77 Example {
78 description: "Convert a string to a semver value",
79 example: "'1.2.3' | into semver",
80 result: None,
81 },
82 Example {
83 description: "Convert a string with prerelease",
84 example: "'1.2.3-alpha.1+build.2' | into semver",
85 result: None,
86 },
87 Example {
88 description: "Convert a record to a semver value",
89 example: "{major: 1, minor: 2, patch: 3} | into semver",
90 result: None,
91 },
92 Example {
93 description: "Parse a version with a common leading v prefix",
94 example: "'v1.2.3' | into semver --loose",
95 result: None,
96 },
97 Example {
98 description: "Parse versions like v.1.2.3, v:1.2.3, v-1.2.3, or v_1.2.3",
99 example: "['v.1.2.3' 'v:2.0.0' 'v-3.0.0' 'v_4.0.0'] | into semver --loose",
100 result: None,
101 },
102 ]
103 }
104}
105
106struct Arguments {
107 cell_paths: Option<Vec<CellPath>>,
108 loose: bool,
109}
110
111impl CmdArgument for Arguments {
112 fn take_cell_paths(&mut self) -> Option<Vec<CellPath>> {
113 self.cell_paths.take()
114 }
115}
116
117fn into_semver(input: &Value, args: &Arguments, head: Span) -> Value {
118 match input {
119 Value::Custom { val, .. } if val.type_name() == "semver" => input.clone(),
120 Value::String { val, .. } => match SemverValue::parse(val, args.loose) {
121 Ok(version) => Value::custom(Box::new(version), head),
122 Err(_) => {
123 let help = if args.loose {
124 "expected format: major.minor.patch (e.g. 1.2.3), optionally with a v/V, v., v:, v-, or v_ prefix"
125 } else {
126 "expected format: major.minor.patch (e.g. 1.2.3); use --loose for prefixes like v1.2.3"
127 };
128 Value::error(
129 ShellError::Generic(
130 GenericError::new(
131 format!("Cannot convert \"{val}\" to a semver"),
132 "the given string is not a valid semver version",
133 head,
134 )
135 .with_help(help),
136 ),
137 head,
138 )
139 }
140 },
141 Value::Record { val, .. } => parse_record_to_semver(val, head),
142 _ => Value::error(
143 ShellError::Generic(GenericError::new(
144 format!("Cannot convert {} to semver", input.get_type()),
145 "expected a string, record, or semver value",
146 head,
147 )),
148 head,
149 ),
150 }
151}
152
153fn parse_record_to_semver(record: &nu_protocol::Record, head: Span) -> Value {
154 let major = record.get("major").and_then(|v| v.as_int().ok());
155 let minor = record.get("minor").and_then(|v| v.as_int().ok());
156 let patch = record.get("patch").and_then(|v| v.as_int().ok());
157
158 let major = match major {
159 Some(v) if v >= 0 => v as u64,
160 _ => {
161 return Value::error(
162 ShellError::Generic(
163 GenericError::new(
164 "Cannot convert record to semver",
165 "missing or invalid 'major' field",
166 head,
167 )
168 .with_help("expected a non-negative integer"),
169 ),
170 head,
171 );
172 }
173 };
174
175 let minor = match minor {
176 Some(v) if v >= 0 => v as u64,
177 _ => {
178 return Value::error(
179 ShellError::Generic(
180 GenericError::new(
181 "Cannot convert record to semver",
182 "missing or invalid 'minor' field",
183 head,
184 )
185 .with_help("expected a non-negative integer"),
186 ),
187 head,
188 );
189 }
190 };
191
192 let patch = match patch {
193 Some(v) if v >= 0 => v as u64,
194 _ => {
195 return Value::error(
196 ShellError::Generic(
197 GenericError::new(
198 "Cannot convert record to semver",
199 "missing or invalid 'patch' field",
200 head,
201 )
202 .with_help("expected a non-negative integer"),
203 ),
204 head,
205 );
206 }
207 };
208
209 let pre = record
210 .get("pre")
211 .and_then(|v| v.as_str().ok())
212 .unwrap_or("");
213
214 let build = record
215 .get("build")
216 .and_then(|v| v.as_str().ok())
217 .unwrap_or("");
218
219 let prefix = record
222 .get("prefix")
223 .and_then(|v| v.as_str().ok())
224 .unwrap_or("")
225 .to_string();
226
227 let pre = match semver::Prerelease::new(pre) {
228 Ok(p) => p,
229 Err(e) => {
230 return Value::error(
231 ShellError::Generic(GenericError::new(
232 "Cannot convert record to semver",
233 format!("invalid prerelease: {e}"),
234 head,
235 )),
236 head,
237 );
238 }
239 };
240
241 let build = match semver::BuildMetadata::new(build) {
242 Ok(b) => b,
243 Err(e) => {
244 return Value::error(
245 ShellError::Generic(GenericError::new(
246 "Cannot convert record to semver",
247 format!("invalid build metadata: {e}"),
248 head,
249 )),
250 head,
251 );
252 }
253 };
254
255 let version = semver::Version {
256 major,
257 minor,
258 patch,
259 pre,
260 build,
261 };
262
263 Value::custom(Box::new(SemverValue::with_prefix(version, prefix)), head)
264}
265
266#[cfg(test)]
267mod tests {
268 use super::*;
269 use nu_protocol::record;
270 use nu_test_support::Result;
271 use nu_test_support::prelude::*;
272
273 fn args(loose: bool) -> Arguments {
274 Arguments {
275 cell_paths: None,
276 loose,
277 }
278 }
279
280 fn get_custom_value(value: &Value) -> &SemverValue {
281 match value {
282 Value::Custom { val, .. } => val.as_any().downcast_ref::<SemverValue>().unwrap(),
283 _ => panic!("Expected Custom value"),
284 }
285 }
286
287 #[test]
288 fn test_into_semver_from_string() {
289 let value = Value::string("1.2.3", Span::test_data());
290 let result = into_semver(&value, &args(false), Span::test_data());
291
292 assert!(matches!(result, Value::Custom { .. }));
293 let semver_val = get_custom_value(&result);
294 assert_eq!(semver_val.version.to_string(), "1.2.3");
295 assert!(semver_val.prefix.is_empty());
296 }
297
298 #[test]
299 fn test_into_semver_from_string_with_prerelease() {
300 let value = Value::string("1.2.3-alpha.1+build.2", Span::test_data());
301 let result = into_semver(&value, &args(false), Span::test_data());
302
303 let semver_val = get_custom_value(&result);
304 assert_eq!(semver_val.version.to_string(), "1.2.3-alpha.1+build.2");
305 }
306
307 #[test]
308 fn test_into_semver_from_invalid_string() {
309 let value = Value::string("not-a-version", Span::test_data());
310 let result = into_semver(&value, &args(false), Span::test_data());
311
312 assert!(matches!(result, Value::Error { .. }));
313 }
314
315 #[test]
316 fn test_into_semver_loose_prefixes() {
317 for (input, prefix, version) in [
318 ("v1.2.3", "v", "1.2.3"),
319 ("V1.2.3", "V", "1.2.3"),
320 ("v.1.2.3", "v.", "1.2.3"),
321 ("v:1.2.3", "v:", "1.2.3"),
322 ("v-1.2.3", "v-", "1.2.3"),
323 ("v_1.2.3", "v_", "1.2.3"),
324 ("v1.2.3-alpha.1+build", "v", "1.2.3-alpha.1+build"),
325 ] {
326 let value = Value::string(input, Span::test_data());
327 let result = into_semver(&value, &args(true), Span::test_data());
328 let semver_val = get_custom_value(&result);
329 assert_eq!(semver_val.prefix, prefix, "input={input}");
330 assert_eq!(semver_val.version.to_string(), version, "input={input}");
331 assert_eq!(semver_val.display(), input);
332 }
333 }
334
335 #[test]
336 fn test_into_semver_loose_required_for_prefix() {
337 let value = Value::string("v1.2.3", Span::test_data());
338 let result = into_semver(&value, &args(false), Span::test_data());
339 assert!(matches!(result, Value::Error { .. }));
340 }
341
342 #[test]
343 fn test_into_semver_from_semver() {
344 let original = SemverValue::new(semver::Version::parse("1.2.3").unwrap());
345 let value = Value::custom(Box::new(original), Span::test_data());
346 let result = into_semver(&value, &args(false), Span::test_data());
347
348 let semver_val = get_custom_value(&result);
350 assert_eq!(semver_val.version.to_string(), "1.2.3");
351 }
352
353 #[test]
354 fn test_into_semver_from_record_basic() {
355 let record = record! {
356 "major" => Value::int(1, Span::test_data()),
357 "minor" => Value::int(2, Span::test_data()),
358 "patch" => Value::int(3, Span::test_data()),
359 };
360 let value = Value::record(record, Span::test_data());
361 let result = into_semver(&value, &args(false), Span::test_data());
362
363 let semver_val = get_custom_value(&result);
364 assert_eq!(semver_val.version.to_string(), "1.2.3");
365 }
366
367 #[test]
368 fn test_into_semver_from_record_with_prerelease() {
369 let record = record! {
370 "major" => Value::int(1, Span::test_data()),
371 "minor" => Value::int(2, Span::test_data()),
372 "patch" => Value::int(3, Span::test_data()),
373 "pre" => Value::string("alpha.1", Span::test_data()),
374 };
375 let value = Value::record(record, Span::test_data());
376 let result = into_semver(&value, &args(false), Span::test_data());
377
378 let semver_val = get_custom_value(&result);
379 assert_eq!(semver_val.version.to_string(), "1.2.3-alpha.1");
380 }
381
382 #[test]
383 fn test_into_semver_from_record_with_build() {
384 let record = record! {
385 "major" => Value::int(1, Span::test_data()),
386 "minor" => Value::int(2, Span::test_data()),
387 "patch" => Value::int(3, Span::test_data()),
388 "build" => Value::string("build.2", Span::test_data()),
389 };
390 let value = Value::record(record, Span::test_data());
391 let result = into_semver(&value, &args(false), Span::test_data());
392
393 let semver_val = get_custom_value(&result);
394 assert_eq!(semver_val.version.to_string(), "1.2.3+build.2");
395 }
396
397 #[test]
398 fn test_into_semver_from_record_with_both() {
399 let record = record! {
400 "major" => Value::int(1, Span::test_data()),
401 "minor" => Value::int(2, Span::test_data()),
402 "patch" => Value::int(3, Span::test_data()),
403 "pre" => Value::string("alpha", Span::test_data()),
404 "build" => Value::string("build", Span::test_data()),
405 };
406 let value = Value::record(record, Span::test_data());
407 let result = into_semver(&value, &args(false), Span::test_data());
408
409 let semver_val = get_custom_value(&result);
410 assert_eq!(semver_val.version.to_string(), "1.2.3-alpha+build");
411 }
412
413 #[test]
414 fn test_into_semver_from_record_with_prefix() {
415 let record = record! {
416 "major" => Value::int(1, Span::test_data()),
417 "minor" => Value::int(2, Span::test_data()),
418 "patch" => Value::int(3, Span::test_data()),
419 "prefix" => Value::string("v", Span::test_data()),
420 };
421 let value = Value::record(record, Span::test_data());
422 let result = into_semver(&value, &args(false), Span::test_data());
423
424 let semver_val = get_custom_value(&result);
425 assert_eq!(semver_val.prefix, "v");
426 assert_eq!(semver_val.version.to_string(), "1.2.3");
427 assert_eq!(semver_val.display(), "v1.2.3");
428 }
429
430 #[test]
431 fn test_into_semver_record_round_trip_preserves_prefix() -> Result {
432 test()
433 .run("'v1.2.3' | into semver --loose | into record | into semver | to text")
434 .expect_value_eq("v1.2.3")
435 }
436
437 #[test]
438 fn test_into_semver_from_record_missing_major() {
439 let record = record! {
440 "minor" => Value::int(2, Span::test_data()),
441 "patch" => Value::int(3, Span::test_data()),
442 };
443 let value = Value::record(record, Span::test_data());
444 let result = into_semver(&value, &args(false), Span::test_data());
445
446 assert!(matches!(result, Value::Error { .. }));
447 }
448
449 #[test]
450 fn test_into_semver_from_record_negative_major() {
451 let record = record! {
452 "major" => Value::int(-1, Span::test_data()),
453 "minor" => Value::int(2, Span::test_data()),
454 "patch" => Value::int(3, Span::test_data()),
455 };
456 let value = Value::record(record, Span::test_data());
457 let result = into_semver(&value, &args(false), Span::test_data());
458
459 assert!(matches!(result, Value::Error { .. }));
460 }
461
462 #[test]
463 fn test_into_semver_from_unsupported_type() {
464 let value = Value::int(42, Span::test_data());
465 let result = into_semver(&value, &args(false), Span::test_data());
466
467 assert!(matches!(result, Value::Error { .. }));
468 }
469
470 #[test]
471 fn test_into_semver_from_list_of_records() -> Result {
472 let value = Value::test_list(vec![
473 Value::test_record(record! {
474 "major" => Value::test_int(1),
475 "minor" => Value::test_int(2),
476 "patch" => Value::test_int(3),
477 }),
478 Value::test_record(record! {
479 "major" => Value::test_int(0),
480 "minor" => Value::test_int(1),
481 "patch" => Value::test_int(6),
482 }),
483 ]);
484
485 test()
486 .run_with_data("into semver", value)
487 .expect_value_eq(vec!["1.2.3", "0.1.6"])
488 }
489
490 #[test]
491 fn test_into_semver_from_list_of_strings() -> Result {
492 let value = Value::test_list(vec![
493 Value::test_string("3.1.0"),
494 Value::test_string("0.10.5"),
495 ]);
496 test()
497 .run_with_data("into semver", value)
498 .expect_value_eq(vec!["3.1.0", "0.10.5"])
499 }
500
501 #[test]
502 fn test_into_semver_loose_list() -> Result {
503 test()
504 .run("['v1.0.0' 'v.2.0.0' 'v:3.0.0'] | into semver --loose")
505 .expect_value_eq(vec!["v1.0.0", "v.2.0.0", "v:3.0.0"])
506 }
507
508 #[test]
509 fn test_into_semver_at_cell_paths() -> Result {
510 let cell_a = Value::test_record(record! {
511 "major" => Value::test_int(0),
512 "minor" => Value::test_int(10),
513 "patch" => Value::test_int(2),
514 });
515 let value = Value::test_record(record! {
516 "a" => cell_a,
517 "b" => Value::test_string("will not error"),
518 "c" => Value::test_string("5.3.0"),
519 });
520
521 test()
522 .run_with_data("into semver a c | values", value)
523 .expect_value_eq(vec!["0.10.2", "will not error", "5.3.0"])
524 }
525
526 #[test]
527 fn semver_comparison_can_be_or_ed_with_bool() -> Result {
528 test()
529 .run("true or (('1.0.0' | into semver) < ('2.0.0' | into semver))")
530 .expect_value_eq(true)
531 }
532
533 #[test]
534 fn semver_comparison_can_be_and_ed_with_bool() -> Result {
535 test()
536 .run("false and (('1.0.0' | into semver) < ('2.0.0' | into semver))")
537 .expect_value_eq(false)
538 }
539
540 #[test]
541 fn semver_comparison_can_be_bound_with_let() -> Result {
542 test()
543 .run("let x = ('1.0.0' | into semver) < ('2.0.0' | into semver); $x")
544 .expect_value_eq(true)
545 }
546
547 #[test]
548 fn semver_in_range_can_be_or_ed_with_bool() -> Result {
549 test()
550 .run("true or (('1.2.3' | into semver) in ('>=1.0.0' | into semver-range))")
551 .expect_value_eq(true)
552 }
553}
554
555#[cfg(test)]
556mod test {
557 use super::*;
558
559 #[test]
560 fn test_examples() -> nu_test_support::Result {
561 nu_test_support::test().examples(IntoSemver)
562 }
563}