1use super::value::SemverValue;
2use nu_engine::command_prelude::*;
3use nu_protocol::{Parameter, shell_error::generic::GenericError};
4
5#[derive(Clone)]
6pub struct SemverBump;
7
8impl Command for SemverBump {
9 fn name(&self) -> &str {
10 "semver bump"
11 }
12
13 fn signature(&self) -> Signature {
14 Signature::build("semver bump")
15 .input_output_types(vec![
16 (Type::Custom("semver".into()), Type::Custom("semver".into())),
17 (Type::String, Type::Custom("semver".into())),
18 ])
19 .switch(
20 "ignore-errors",
21 "If the input is not a valid semver version, return the original input unchanged",
22 Some('i'),
23 )
24 .switch(
25 "preserve-build-metadata",
26 "Preserve the existing build metadata from the input version",
27 Some('p'),
28 )
29 .switch(
30 "loose",
31 "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 when parsing string input; the prefix is preserved on the result",
32 Some('l'),
33 )
34 .named(
35 "build-metadata",
36 SyntaxShape::String,
37 "Additionally set the build metadata. Takes precedence over --preserve-build-metadata",
38 Some('b'),
39 )
40 .param(Parameter::Required(
41 PositionalArg::new("level", SyntaxShape::String)
42 .desc("The level to bump: major, minor, patch, alpha, beta, rc, release.")
43 .completion(Completion::new_list(&[
44 "major", "minor", "patch", "alpha", "beta", "rc", "release",
45 ])),
46 ))
47 .category(Category::Filters)
48 }
49
50 fn description(&self) -> &str {
51 "Bump a semantic version to the next level."
52 }
53
54 fn search_terms(&self) -> Vec<&str> {
55 vec!["version", "increment", "major", "minor", "patch"]
56 }
57
58 fn examples(&self) -> Vec<Example<'static>> {
59 vec![
60 Example {
61 description: "Bump major version",
62 example: "'1.2.3' | into semver | semver bump major",
63 result: Some(SemverValue::test_value("2.0.0")),
64 },
65 Example {
66 description: "Bump minor version",
67 example: "'1.2.3' | into semver | semver bump minor",
68 result: Some(SemverValue::test_value("1.3.0")),
69 },
70 Example {
71 description: "Bump patch version",
72 example: "'1.2.3' | into semver | semver bump patch",
73 result: Some(SemverValue::test_value("1.2.4")),
74 },
75 Example {
76 description: "Bump patch version with string input",
77 example: "'1.2.3' | semver bump patch",
78 result: Some(SemverValue::test_value("1.2.4")),
79 },
80 Example {
81 description: "Add alpha prerelease",
82 example: "'1.2.3' | into semver | semver bump alpha",
83 result: Some(SemverValue::test_value("1.2.3-alpha.1")),
84 },
85 Example {
86 description: "Remove prerelease",
87 example: "'1.2.3-alpha' | into semver | semver bump release",
88 result: Some(SemverValue::test_value("1.2.3")),
89 },
90 Example {
91 description: "Bump with preserved build metadata",
92 example: "'1.2.3+build.5' | into semver | semver bump patch --preserve-build-metadata",
93 result: Some(SemverValue::test_value("1.2.4+build.5")),
94 },
95 Example {
96 description: "Bump a loosely-prefixed version string",
99 example: "'v1.2.3' | semver bump patch --loose | to text",
100 result: Some(Value::test_string("v1.2.4")),
101 },
102 Example {
103 description: "Bump after converting with --loose (prefix is preserved)",
106 example: "'v1.2.3' | into semver --loose | semver bump major | to text",
107 result: Some(Value::test_string("v2.0.0")),
108 },
109 ]
110 }
111
112 fn run(
113 &self,
114 engine_state: &EngineState,
115 stack: &mut Stack,
116 call: &Call,
117 input: PipelineData,
118 ) -> Result<PipelineData, ShellError> {
119 let level: String = call.req(engine_state, stack, 0)?;
120 let ignore_errors = call.has_flag(engine_state, stack, "ignore-errors")?;
121 let build_metadata: Option<String> =
122 call.get_flag(engine_state, stack, "build-metadata")?;
123 let preserve_build_metadata =
124 call.has_flag(engine_state, stack, "preserve-build-metadata")?;
125 let loose = call.has_flag(engine_state, stack, "loose")?;
126 let head = call.head;
127
128 input.map(
129 move |value| {
130 bump_value_with_options(
131 &value,
132 &level,
133 head,
134 ignore_errors,
135 build_metadata.as_deref(),
136 preserve_build_metadata,
137 loose,
138 )
139 .unwrap_or_else(|err| Value::error(err, head))
140 },
141 engine_state.signals(),
142 )
143 }
144}
145
146fn bump_value_with_options(
147 input: &Value,
148 level: &str,
149 head: Span,
150 ignore_errors: bool,
151 build_metadata: Option<&str>,
152 preserve_build_metadata: bool,
153 loose: bool,
154) -> Result<Value, ShellError> {
155 let semver_val = match SemverValue::try_from_value(input, loose) {
156 Ok(semver) => semver,
157 Err(err) => {
158 if ignore_errors {
159 return Ok(input.clone());
160 }
161 return Err(err);
162 }
163 };
164
165 let original_build = semver_val.version.build.clone();
166
167 let result = match level {
168 "major" => semver_val.bump_major(),
169 "minor" => semver_val.bump_minor(),
170 "patch" => semver_val.bump_patch(),
171 "alpha" | "beta" | "rc" => semver_val.bump_prerelease(level)?,
172 "release" => semver_val.bump_release(),
173 _ => {
174 return Err(ShellError::Generic(
175 GenericError::new(
176 "Invalid bump level",
177 format!("'{}' is not a valid bump level", level),
178 head,
179 )
180 .with_help("valid levels: major, minor, patch, alpha, beta, rc, release"),
181 ));
182 }
183 };
184
185 let result = match (build_metadata, preserve_build_metadata) {
186 (Some(metadata), _) => result.set_build_metadata(metadata)?,
187 (None, true) => SemverValue::with_prefix(
188 semver::Version {
189 build: original_build,
190 ..result.version
191 },
192 result.prefix,
193 ),
194 (None, false) => result,
195 };
196
197 Ok(Value::custom(Box::new(result), head))
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203
204 fn create_semver_value(version: &str) -> Value {
205 let semver = SemverValue::new(semver::Version::parse(version).unwrap());
206 Value::custom(Box::new(semver), Span::test_data())
207 }
208
209 fn get_semver_from_value(value: &Value) -> String {
210 match value {
211 Value::Custom { val, .. } => {
212 let semver = val.as_any().downcast_ref::<SemverValue>().unwrap();
213 semver.display()
214 }
215 _ => panic!("Expected Custom value"),
216 }
217 }
218
219 fn bump(
220 input: &Value,
221 level: &str,
222 ignore_errors: bool,
223 build_metadata: Option<&str>,
224 preserve_build_metadata: bool,
225 loose: bool,
226 ) -> Result<Value, ShellError> {
227 bump_value_with_options(
228 input,
229 level,
230 Span::test_data(),
231 ignore_errors,
232 build_metadata,
233 preserve_build_metadata,
234 loose,
235 )
236 }
237
238 #[test]
239 fn test_bump_major() {
240 let input = create_semver_value("1.2.3");
241 let result = bump(&input, "major", false, None, false, false).unwrap();
242 assert_eq!(get_semver_from_value(&result), "2.0.0");
243 }
244
245 #[test]
246 fn test_bump_minor() {
247 let input = create_semver_value("1.2.3");
248 let result = bump(&input, "minor", false, None, false, false).unwrap();
249 assert_eq!(get_semver_from_value(&result), "1.3.0");
250 }
251
252 #[test]
253 fn test_bump_patch() {
254 let input = create_semver_value("1.2.3");
255 let result = bump(&input, "patch", false, None, false, false).unwrap();
256 assert_eq!(get_semver_from_value(&result), "1.2.4");
257 }
258
259 #[test]
260 fn test_bump_alpha() {
261 let input = create_semver_value("1.2.3");
262 let result = bump(&input, "alpha", false, None, false, false).unwrap();
263 assert_eq!(get_semver_from_value(&result), "1.2.3-alpha.1");
264 }
265
266 #[test]
267 fn test_bump_beta() {
268 let input = create_semver_value("1.2.3");
269 let result = bump(&input, "beta", false, None, false, false).unwrap();
270 assert_eq!(get_semver_from_value(&result), "1.2.3-beta.1");
271 }
272
273 #[test]
274 fn test_bump_rc() {
275 let input = create_semver_value("1.2.3");
276 let result = bump(&input, "rc", false, None, false, false).unwrap();
277 assert_eq!(get_semver_from_value(&result), "1.2.3-rc.1");
278 }
279
280 #[test]
281 fn test_bump_release() {
282 let input = create_semver_value("1.2.3-alpha.1");
283 let result = bump(&input, "release", false, None, false, false).unwrap();
284 assert_eq!(get_semver_from_value(&result), "1.2.3");
285 }
286
287 #[test]
288 fn test_bump_invalid_level() {
289 let input = create_semver_value("1.2.3");
290 let result = bump(&input, "invalid", false, None, false, false);
291 assert!(result.is_err());
292 }
293
294 #[test]
295 fn test_bump_string_input_is_supported() {
296 let input = Value::string("1.2.3", Span::test_data());
297 let result = bump(&input, "major", false, None, false, false).unwrap();
298 assert_eq!(get_semver_from_value(&result), "2.0.0");
299 }
300
301 #[test]
302 fn test_bump_string_input_with_build_metadata() {
303 let input = Value::string("1.2.3", Span::test_data());
304 let result = bump(&input, "minor", false, Some("build"), false, false).unwrap();
305 assert_eq!(get_semver_from_value(&result), "1.3.0+build");
306 }
307
308 #[test]
309 fn test_bump_ignore_errors_for_invalid_input() {
310 let input = Value::string("not-a-version", Span::test_data());
311 let result = bump(&input, "major", true, None, false, false).unwrap();
312 assert!(matches!(result, Value::String { .. }));
313 }
314
315 #[test]
316 fn test_bump_wrong_custom_value() {
317 let input = Value::int(42, Span::test_data());
318 let result = bump(&input, "major", false, None, false, false);
319 assert!(result.is_err());
320 }
321
322 #[test]
323 fn test_bump_with_prerelease() {
324 let input = create_semver_value("1.2.3-alpha.1");
325 let result = bump(&input, "major", false, None, false, false).unwrap();
326 assert_eq!(get_semver_from_value(&result), "2.0.0");
327 }
328
329 #[test]
330 fn test_bump_with_build_metadata() {
331 let input = create_semver_value("1.2.3+build.1");
332 let result = bump(&input, "minor", false, None, false, false).unwrap();
333 assert_eq!(get_semver_from_value(&result), "1.3.0");
334 }
335
336 #[test]
337 fn test_bump_preserve_build_metadata() {
338 let input = create_semver_value("1.2.3+build.5");
339 let result = bump(&input, "patch", false, None, true, false).unwrap();
340 assert_eq!(get_semver_from_value(&result), "1.2.4+build.5");
341 }
342
343 #[test]
344 fn test_bump_preserve_build_metadata_major() {
345 let input = create_semver_value("1.2.3+build.1");
346 let result = bump(&input, "major", false, None, true, false).unwrap();
347 assert_eq!(get_semver_from_value(&result), "2.0.0+build.1");
348 }
349
350 #[test]
351 fn test_bump_build_metadata_takes_precedence() {
352 let input = create_semver_value("1.2.3+build.1");
353 let result = bump(&input, "patch", false, Some("override"), true, false).unwrap();
354 assert_eq!(get_semver_from_value(&result), "1.2.4+override");
355 }
356
357 #[test]
358 fn test_bump_loose_string_preserves_prefix() {
359 let input = Value::string("v1.2.3", Span::test_data());
360 let result = bump(&input, "major", false, None, false, true).unwrap();
361 assert_eq!(get_semver_from_value(&result), "v2.0.0");
362
363 let input = Value::string("v.1.2.3", Span::test_data());
364 let result = bump(&input, "patch", false, None, false, true).unwrap();
365 assert_eq!(get_semver_from_value(&result), "v.1.2.4");
366
367 let input = Value::string("v:1.2.3", Span::test_data());
368 let result = bump(&input, "minor", false, None, false, true).unwrap();
369 assert_eq!(get_semver_from_value(&result), "v:1.3.0");
370
371 let input = Value::string("v-1.2.3", Span::test_data());
372 let result = bump(&input, "major", false, None, false, true).unwrap();
373 assert_eq!(get_semver_from_value(&result), "v-2.0.0");
374
375 let input = Value::string("v_1.2.3", Span::test_data());
376 let result = bump(&input, "patch", false, None, false, true).unwrap();
377 assert_eq!(get_semver_from_value(&result), "v_1.2.4");
378 }
379
380 #[test]
381 fn test_bump_loose_required_for_prefixed_string() {
382 let input = Value::string("v1.2.3", Span::test_data());
383 let result = bump(&input, "major", false, None, false, false);
384 assert!(result.is_err());
385 }
386
387 #[test]
388 fn test_bump_preserves_prefix_from_semver_value() {
389 let input = Value::custom(
390 Box::new(SemverValue::parse("v1.2.3", true).unwrap()),
391 Span::test_data(),
392 );
393 let result = bump(&input, "major", false, None, false, false).unwrap();
395 assert_eq!(get_semver_from_value(&result), "v2.0.0");
396 }
397}
398
399#[cfg(test)]
400mod test {
401 use super::*;
402
403 #[test]
404 fn test_examples() -> nu_test_support::Result {
405 nu_test_support::test().examples(SemverBump)
406 }
407}