nu-command 0.114.0

Nushell's built-in commands
Documentation
use super::value::SemverValue;
use nu_engine::command_prelude::*;
use nu_protocol::{Parameter, shell_error::generic::GenericError};

#[derive(Clone)]
pub struct SemverBump;

impl Command for SemverBump {
    fn name(&self) -> &str {
        "semver bump"
    }

    fn signature(&self) -> Signature {
        Signature::build("semver bump")
            .input_output_types(vec![
                (Type::Custom("semver".into()), Type::Custom("semver".into())),
                (Type::String, Type::Custom("semver".into())),
            ])
            .switch(
                "ignore-errors",
                "If the input is not a valid semver version, return the original input unchanged",
                Some('i'),
            )
            .switch(
                "preserve-build-metadata",
                "Preserve the existing build metadata from the input version",
                Some('p'),
            )
            .named(
                "build-metadata",
                SyntaxShape::String,
                "Additionally set the build metadata. Takes precedence over --preserve-build-metadata",
                Some('b'),
            )
            .param(Parameter::Required(
                PositionalArg::new("level", SyntaxShape::String)
                    .desc("The level to bump: major, minor, patch, alpha, beta, rc, release.")
                    .completion(Completion::new_list(&[
                        "major", "minor", "patch", "alpha", "beta", "rc", "release",
                    ])),
            ))
            .category(Category::Filters)
    }

    fn description(&self) -> &str {
        "Bump a semantic version to the next level."
    }

    fn search_terms(&self) -> Vec<&str> {
        vec!["version", "increment", "major", "minor", "patch"]
    }

    fn examples(&self) -> Vec<Example<'static>> {
        vec![
            Example {
                description: "Bump major version",
                example: "'1.2.3' | into semver | semver bump major",
                result: Some(SemverValue::test_value("2.0.0")),
            },
            Example {
                description: "Bump minor version",
                example: "'1.2.3' | into semver | semver bump minor",
                result: Some(SemverValue::test_value("1.3.0")),
            },
            Example {
                description: "Bump patch version",
                example: "'1.2.3' | into semver | semver bump patch",
                result: Some(SemverValue::test_value("1.2.4")),
            },
            Example {
                description: "Bump patch version with string input",
                example: "'1.2.3' | semver bump patch",
                result: Some(SemverValue::test_value("1.2.4")),
            },
            Example {
                description: "Add alpha prerelease",
                example: "'1.2.3' | into semver | semver bump alpha",
                result: Some(SemverValue::test_value("1.2.3-alpha.1")),
            },
            Example {
                description: "Remove prerelease",
                example: "'1.2.3-alpha' | into semver | semver bump release",
                result: Some(SemverValue::test_value("1.2.3")),
            },
            Example {
                description: "Bump with preserved build metadata",
                example: "'1.2.3+build.5' | into semver | semver bump patch --preserve-build-metadata",
                result: Some(SemverValue::test_value("1.2.4+build.5")),
            },
        ]
    }

    fn run(
        &self,
        engine_state: &EngineState,
        stack: &mut Stack,
        call: &Call,
        input: PipelineData,
    ) -> Result<PipelineData, ShellError> {
        let level: String = call.req(engine_state, stack, 0)?;
        let ignore_errors = call.has_flag(engine_state, stack, "ignore-errors")?;
        let build_metadata: Option<String> =
            call.get_flag(engine_state, stack, "build-metadata")?;
        let preserve_build_metadata =
            call.has_flag(engine_state, stack, "preserve-build-metadata")?;
        let head = call.head;

        input.map(
            move |value| {
                bump_value_with_options(
                    &value,
                    &level,
                    head,
                    ignore_errors,
                    build_metadata.as_deref(),
                    preserve_build_metadata,
                )
                .unwrap_or_else(|err| Value::error(err, head))
            },
            engine_state.signals(),
        )
    }
}

fn bump_value_with_options(
    input: &Value,
    level: &str,
    head: Span,
    ignore_errors: bool,
    build_metadata: Option<&str>,
    preserve_build_metadata: bool,
) -> Result<Value, ShellError> {
    let semver_val = match SemverValue::try_from(input) {
        Ok(semver) => semver,
        Err(err) => {
            if ignore_errors {
                return Ok(input.clone());
            }
            return Err(err);
        }
    };

    let original_build = semver_val.version.build.clone();

    let result = match level {
        "major" => semver_val.bump_major(),
        "minor" => semver_val.bump_minor(),
        "patch" => semver_val.bump_patch(),
        "alpha" | "beta" | "rc" => semver_val.bump_prerelease(level)?,
        "release" => semver_val.bump_release(),
        _ => {
            return Err(ShellError::Generic(
                GenericError::new(
                    "Invalid bump level",
                    format!("'{}' is not a valid bump level", level),
                    head,
                )
                .with_help("valid levels: major, minor, patch, alpha, beta, rc, release"),
            ));
        }
    };

    let result = match (build_metadata, preserve_build_metadata) {
        (Some(metadata), _) => result.set_build_metadata(metadata)?,
        (None, true) => SemverValue {
            version: semver::Version {
                build: original_build,
                ..result.version
            },
        },
        (None, false) => result,
    };

    Ok(Value::custom(Box::new(result), head))
}

#[cfg(test)]
mod tests {
    use super::*;

    fn create_semver_value(version: &str) -> Value {
        let semver = SemverValue::new(semver::Version::parse(version).unwrap());
        Value::custom(Box::new(semver), Span::test_data())
    }

    fn get_semver_from_value(value: &Value) -> String {
        match value {
            Value::Custom { val, .. } => {
                let semver = val.as_any().downcast_ref::<SemverValue>().unwrap();
                semver.version.to_string()
            }
            _ => panic!("Expected Custom value"),
        }
    }

    #[test]
    fn test_bump_major() {
        let input = create_semver_value("1.2.3");
        let result =
            bump_value_with_options(&input, "major", Span::test_data(), false, None, false)
                .unwrap();
        assert_eq!(get_semver_from_value(&result), "2.0.0");
    }

    #[test]
    fn test_bump_minor() {
        let input = create_semver_value("1.2.3");
        let result =
            bump_value_with_options(&input, "minor", Span::test_data(), false, None, false)
                .unwrap();
        assert_eq!(get_semver_from_value(&result), "1.3.0");
    }

    #[test]
    fn test_bump_patch() {
        let input = create_semver_value("1.2.3");
        let result =
            bump_value_with_options(&input, "patch", Span::test_data(), false, None, false)
                .unwrap();
        assert_eq!(get_semver_from_value(&result), "1.2.4");
    }

    #[test]
    fn test_bump_alpha() {
        let input = create_semver_value("1.2.3");
        let result =
            bump_value_with_options(&input, "alpha", Span::test_data(), false, None, false)
                .unwrap();
        assert_eq!(get_semver_from_value(&result), "1.2.3-alpha.1");
    }

    #[test]
    fn test_bump_beta() {
        let input = create_semver_value("1.2.3");
        let result =
            bump_value_with_options(&input, "beta", Span::test_data(), false, None, false).unwrap();
        assert_eq!(get_semver_from_value(&result), "1.2.3-beta.1");
    }

    #[test]
    fn test_bump_rc() {
        let input = create_semver_value("1.2.3");
        let result =
            bump_value_with_options(&input, "rc", Span::test_data(), false, None, false).unwrap();
        assert_eq!(get_semver_from_value(&result), "1.2.3-rc.1");
    }

    #[test]
    fn test_bump_release() {
        let input = create_semver_value("1.2.3-alpha.1");
        let result =
            bump_value_with_options(&input, "release", Span::test_data(), false, None, false)
                .unwrap();
        assert_eq!(get_semver_from_value(&result), "1.2.3");
    }

    #[test]
    fn test_bump_invalid_level() {
        let input = create_semver_value("1.2.3");
        let result =
            bump_value_with_options(&input, "invalid", Span::test_data(), false, None, false);
        assert!(result.is_err());
    }

    #[test]
    fn test_bump_string_input_is_supported() {
        let input = Value::string("1.2.3", Span::test_data());
        let result =
            bump_value_with_options(&input, "major", Span::test_data(), false, None, false)
                .unwrap();
        assert_eq!(get_semver_from_value(&result), "2.0.0");
    }

    #[test]
    fn test_bump_string_input_with_build_metadata() {
        let input = Value::string("1.2.3", Span::test_data());
        let result = bump_value_with_options(
            &input,
            "minor",
            Span::test_data(),
            false,
            Some("build"),
            false,
        )
        .unwrap();
        assert_eq!(get_semver_from_value(&result), "1.3.0+build");
    }

    #[test]
    fn test_bump_ignore_errors_for_invalid_input() {
        let input = Value::string("not-a-version", Span::test_data());
        let result =
            bump_value_with_options(&input, "major", Span::test_data(), true, None, false).unwrap();
        assert!(matches!(result, Value::String { .. }));
    }

    #[test]
    fn test_bump_wrong_custom_value() {
        let input = Value::int(42, Span::test_data());
        let result =
            bump_value_with_options(&input, "major", Span::test_data(), false, None, false);
        assert!(result.is_err());
    }

    #[test]
    fn test_bump_with_prerelease() {
        let input = create_semver_value("1.2.3-alpha.1");
        let result =
            bump_value_with_options(&input, "major", Span::test_data(), false, None, false)
                .unwrap();
        assert_eq!(get_semver_from_value(&result), "2.0.0");
    }

    #[test]
    fn test_bump_with_build_metadata() {
        let input = create_semver_value("1.2.3+build.1");
        let result =
            bump_value_with_options(&input, "minor", Span::test_data(), false, None, false)
                .unwrap();
        assert_eq!(get_semver_from_value(&result), "1.3.0");
    }

    #[test]
    fn test_bump_preserve_build_metadata() {
        let input = create_semver_value("1.2.3+build.5");
        let result =
            bump_value_with_options(&input, "patch", Span::test_data(), false, None, true).unwrap();
        assert_eq!(get_semver_from_value(&result), "1.2.4+build.5");
    }

    #[test]
    fn test_bump_preserve_build_metadata_major() {
        let input = create_semver_value("1.2.3+build.1");
        let result =
            bump_value_with_options(&input, "major", Span::test_data(), false, None, true).unwrap();
        assert_eq!(get_semver_from_value(&result), "2.0.0+build.1");
    }

    #[test]
    fn test_bump_build_metadata_takes_precedence() {
        let input = create_semver_value("1.2.3+build.1");
        let result = bump_value_with_options(
            &input,
            "patch",
            Span::test_data(),
            false,
            Some("override"),
            true,
        )
        .unwrap();
        assert_eq!(get_semver_from_value(&result), "1.2.4+override");
    }
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_examples() -> nu_test_support::Result {
        nu_test_support::test().examples(SemverBump)
    }
}