Skip to main content

nu_command/semver/
bump.rs

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                // Assert via `to text` so the example locks the display prefix;
97                // SemverValue equality ignores prefix by design.
98                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                // Assert via `to text` so the example locks the display prefix;
104                // SemverValue equality ignores prefix by design.
105                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        // Prefix is already on the value; --loose is not required to keep it.
394        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}