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