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}