use crate::semver::value::SemverValue;
use nu_engine::command_prelude::*;
use nu_protocol::shell_error::generic::GenericError;
#[derive(Clone)]
pub struct IntoSemver;
impl Command for IntoSemver {
fn name(&self) -> &str {
"into semver"
}
fn signature(&self) -> Signature {
Signature::build("into semver")
.input_output_types(vec![
(Type::String, Type::Custom("semver".into())),
(Type::Custom("semver".into()), Type::Custom("semver".into())),
(Type::record(), Type::Custom("semver".into())),
])
.category(Category::Conversions)
}
fn description(&self) -> &str {
"Convert a value to a semantic version."
}
fn search_terms(&self) -> Vec<&str> {
vec!["version", "convert", "semantic"]
}
fn run(
&self,
engine_state: &EngineState,
_stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let head = call.head;
input.map(
move |value| into_semver(&value, head),
engine_state.signals(),
)
}
fn examples(&self) -> Vec<Example<'static>> {
vec![
Example {
description: "Convert a string to a semver value",
example: "'1.2.3' | into semver",
result: None,
},
Example {
description: "Convert a string with prerelease",
example: "'1.2.3-alpha.1+build.2' | into semver",
result: None,
},
Example {
description: "Convert a record to a semver value",
example: "{major: 1, minor: 2, patch: 3} | into semver",
result: None,
},
]
}
}
fn into_semver(input: &Value, head: Span) -> Value {
match input {
Value::Custom { val, .. } if val.type_name() == "semver" => input.clone(),
Value::String { val, .. } => match semver::Version::parse(val) {
Ok(version) => Value::custom(Box::new(SemverValue::new(version)), head),
Err(_) => Value::error(
ShellError::Generic(
GenericError::new(
format!("Cannot convert \"{val}\" to a semver"),
"the given string is not a valid semver version",
head,
)
.with_help("expected format: major.minor.patch (e.g. 1.2.3)"),
),
head,
),
},
Value::Record { val, .. } => parse_record_to_semver(val, head),
_ => Value::error(
ShellError::Generic(GenericError::new(
format!("Cannot convert {} to semver", input.get_type()),
"expected a string, record, or semver value",
head,
)),
head,
),
}
}
fn parse_record_to_semver(record: &nu_protocol::Record, head: Span) -> Value {
let major = record.get("major").and_then(|v| v.as_int().ok());
let minor = record.get("minor").and_then(|v| v.as_int().ok());
let patch = record.get("patch").and_then(|v| v.as_int().ok());
let major = match major {
Some(v) if v >= 0 => v as u64,
_ => {
return Value::error(
ShellError::Generic(
GenericError::new(
"Cannot convert record to semver",
"missing or invalid 'major' field",
head,
)
.with_help("expected a non-negative integer"),
),
head,
);
}
};
let minor = match minor {
Some(v) if v >= 0 => v as u64,
_ => {
return Value::error(
ShellError::Generic(
GenericError::new(
"Cannot convert record to semver",
"missing or invalid 'minor' field",
head,
)
.with_help("expected a non-negative integer"),
),
head,
);
}
};
let patch = match patch {
Some(v) if v >= 0 => v as u64,
_ => {
return Value::error(
ShellError::Generic(
GenericError::new(
"Cannot convert record to semver",
"missing or invalid 'patch' field",
head,
)
.with_help("expected a non-negative integer"),
),
head,
);
}
};
let pre = record
.get("pre")
.and_then(|v| v.as_str().ok())
.unwrap_or("");
let build = record
.get("build")
.and_then(|v| v.as_str().ok())
.unwrap_or("");
let pre = match semver::Prerelease::new(pre) {
Ok(p) => p,
Err(e) => {
return Value::error(
ShellError::Generic(GenericError::new(
"Cannot convert record to semver",
format!("invalid prerelease: {e}"),
head,
)),
head,
);
}
};
let build = match semver::BuildMetadata::new(build) {
Ok(b) => b,
Err(e) => {
return Value::error(
ShellError::Generic(GenericError::new(
"Cannot convert record to semver",
format!("invalid build metadata: {e}"),
head,
)),
head,
);
}
};
let version = semver::Version {
major,
minor,
patch,
pre,
build,
};
Value::custom(Box::new(SemverValue::new(version)), head)
}
#[cfg(test)]
mod tests {
use super::*;
use nu_protocol::record;
fn get_custom_value(value: &Value) -> &SemverValue {
match value {
Value::Custom { val, .. } => val.as_any().downcast_ref::<SemverValue>().unwrap(),
_ => panic!("Expected Custom value"),
}
}
#[test]
fn test_into_semver_from_string() {
let value = Value::string("1.2.3", Span::test_data());
let result = into_semver(&value, Span::test_data());
assert!(matches!(result, Value::Custom { .. }));
let semver_val = get_custom_value(&result);
assert_eq!(semver_val.version.to_string(), "1.2.3");
}
#[test]
fn test_into_semver_from_string_with_prerelease() {
let value = Value::string("1.2.3-alpha.1+build.2", Span::test_data());
let result = into_semver(&value, Span::test_data());
let semver_val = get_custom_value(&result);
assert_eq!(semver_val.version.to_string(), "1.2.3-alpha.1+build.2");
}
#[test]
fn test_into_semver_from_invalid_string() {
let value = Value::string("not-a-version", Span::test_data());
let result = into_semver(&value, Span::test_data());
assert!(matches!(result, Value::Error { .. }));
}
#[test]
fn test_into_semver_from_semver() {
let original = SemverValue::new(semver::Version::parse("1.2.3").unwrap());
let value = Value::custom(Box::new(original), Span::test_data());
let result = into_semver(&value, Span::test_data());
let semver_val = get_custom_value(&result);
assert_eq!(semver_val.version.to_string(), "1.2.3");
}
#[test]
fn test_into_semver_from_record_basic() {
let record = record! {
"major" => Value::int(1, Span::test_data()),
"minor" => Value::int(2, Span::test_data()),
"patch" => Value::int(3, Span::test_data()),
};
let value = Value::record(record, Span::test_data());
let result = into_semver(&value, Span::test_data());
let semver_val = get_custom_value(&result);
assert_eq!(semver_val.version.to_string(), "1.2.3");
}
#[test]
fn test_into_semver_from_record_with_prerelease() {
let record = record! {
"major" => Value::int(1, Span::test_data()),
"minor" => Value::int(2, Span::test_data()),
"patch" => Value::int(3, Span::test_data()),
"pre" => Value::string("alpha.1", Span::test_data()),
};
let value = Value::record(record, Span::test_data());
let result = into_semver(&value, Span::test_data());
let semver_val = get_custom_value(&result);
assert_eq!(semver_val.version.to_string(), "1.2.3-alpha.1");
}
#[test]
fn test_into_semver_from_record_with_build() {
let record = record! {
"major" => Value::int(1, Span::test_data()),
"minor" => Value::int(2, Span::test_data()),
"patch" => Value::int(3, Span::test_data()),
"build" => Value::string("build.2", Span::test_data()),
};
let value = Value::record(record, Span::test_data());
let result = into_semver(&value, Span::test_data());
let semver_val = get_custom_value(&result);
assert_eq!(semver_val.version.to_string(), "1.2.3+build.2");
}
#[test]
fn test_into_semver_from_record_with_both() {
let record = record! {
"major" => Value::int(1, Span::test_data()),
"minor" => Value::int(2, Span::test_data()),
"patch" => Value::int(3, Span::test_data()),
"pre" => Value::string("alpha", Span::test_data()),
"build" => Value::string("build", Span::test_data()),
};
let value = Value::record(record, Span::test_data());
let result = into_semver(&value, Span::test_data());
let semver_val = get_custom_value(&result);
assert_eq!(semver_val.version.to_string(), "1.2.3-alpha+build");
}
#[test]
fn test_into_semver_from_record_missing_major() {
let record = record! {
"minor" => Value::int(2, Span::test_data()),
"patch" => Value::int(3, Span::test_data()),
};
let value = Value::record(record, Span::test_data());
let result = into_semver(&value, Span::test_data());
assert!(matches!(result, Value::Error { .. }));
}
#[test]
fn test_into_semver_from_record_negative_major() {
let record = record! {
"major" => Value::int(-1, Span::test_data()),
"minor" => Value::int(2, Span::test_data()),
"patch" => Value::int(3, Span::test_data()),
};
let value = Value::record(record, Span::test_data());
let result = into_semver(&value, Span::test_data());
assert!(matches!(result, Value::Error { .. }));
}
#[test]
fn test_into_semver_from_unsupported_type() {
let value = Value::int(42, Span::test_data());
let result = into_semver(&value, Span::test_data());
assert!(matches!(result, Value::Error { .. }));
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_examples() -> nu_test_support::Result {
nu_test_support::test().examples(IntoSemver)
}
}