use clap::ValueEnum;
use core::fmt;
use noyalib::to_string as to_yaml_string;
use serde::Serialize;
use std::process::{ExitCode, Termination};
use thiserror::Error;
use crate::results::{
BoundaryVersionResult, ComparisonStatement, FilterTestResult, FlatVersionsList, GenerateResult,
OrderedVersionMap, SelectResult, ValidateResult, VersionExplanation, VersionMutationResult,
};
#[derive(Error, Debug)]
pub(crate) enum ApplicationError {
#[error("Invalid input (expected {expected:?}, got {found:?}")]
InvalidArgument { expected: String, found: String },
#[error("Failed to prepare output in this format {err:?}")]
OutputFormatError { err: String },
#[error("Failed a requirement {err:?}")]
FailedRequirementError { err: String },
}
#[derive(ValueEnum, Clone, Debug)]
pub(crate) enum OutputFormat {
Text,
Yaml,
Json,
}
pub(crate) enum ExitOutcome {
Normal(SubcommandResult),
AlwaysSuccessful(SubcommandResult),
}
impl ExitOutcome {
pub(crate) const fn new(output: SubcommandResult, hard_success: bool) -> Self {
if hard_success {
Self::AlwaysSuccessful(output)
} else {
Self::Normal(output)
}
}
}
impl Termination for ExitOutcome {
fn report(self) -> ExitCode {
match self {
Self::Normal(result) => result.report(),
Self::AlwaysSuccessful(_result) => ExitCode::SUCCESS,
}
}
}
macro_rules! subcommand_result {
(
enum $name:ident {
$($variant:ident($ty:ty)),* $(,)?
}
) => {
#[derive(Serialize)]
#[serde(untagged)]
pub(crate) enum $name {
$($variant($ty),)*
}
$(impl From<$ty> for $name {
fn from(value: $ty) -> Self {
Self::$variant(value)
}
})*
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
$(Self::$variant(inner) => write!(f, "{inner}"),)*
}
}
}
};
}
subcommand_result! {
enum SubcommandResult {
ComparisonStatement(ComparisonStatement),
OrderedVersionMap(OrderedVersionMap),
VersionExplanation(VersionExplanation),
FlatVersionsList(FlatVersionsList),
GenerateResult(GenerateResult),
FilterTestResult(FilterTestResult),
ValidateResult(ValidateResult),
VersionMutation(VersionMutationResult),
SelectResult(SelectResult),
BoundaryVersionResult(BoundaryVersionResult),
}
}
impl Termination for SubcommandResult {
fn report(self) -> ExitCode {
match self {
Self::ComparisonStatement(s) => s.report(),
Self::FilterTestResult(s) => s.report(),
Self::ValidateResult(s) => s.report(),
Self::SelectResult(s) => s.report(),
Self::OrderedVersionMap(_)
| Self::VersionExplanation(_)
| Self::FlatVersionsList(_)
| Self::GenerateResult(_)
| Self::VersionMutation(_)
| Self::BoundaryVersionResult(_) => ExitCode::SUCCESS,
}
}
}
fn serialize_yaml(result: &SubcommandResult) -> Result<String, ApplicationError> {
to_yaml_string(result).map_err(|e| ApplicationError::OutputFormatError { err: e.to_string() })
}
pub(crate) fn emit(
result: &SubcommandResult,
format: OutputFormat,
) -> Result<(), ApplicationError> {
match format {
OutputFormat::Text => print!("{result}"),
OutputFormat::Yaml => {
println!("---");
let yaml = serialize_yaml(result)?;
print!("{yaml}");
}
OutputFormat::Json => {
let json = serde_json::to_string(result)
.map_err(|e| ApplicationError::OutputFormatError { err: e.to_string() })?;
println!("{json}");
}
}
Ok(())
}
#[cfg(test)]
mod yaml_structure_tests {
use super::{SubcommandResult, serialize_yaml};
use crate::results::{
OrderedVersionMap, SelectResult, SemverComponent, ValidateResult, VersionExplanation,
VersionMutationResult,
};
use semver::Version;
fn parse_yaml_value(result: &SubcommandResult) -> serde_json::Value {
let yaml = serialize_yaml(result).unwrap();
let cfg = noyalib::ParserConfig::new().lossless_u64_integers(true);
noyalib::from_str_with_config(&yaml, &cfg).unwrap()
}
#[test]
fn validate_result_yaml_structure() {
let result =
SubcommandResult::ValidateResult(ValidateResult::validate("1.2.3".into(), false));
let doc = parse_yaml_value(&result);
assert_eq!(doc.get("valid").and_then(|v| v.as_bool()), Some(true));
}
#[test]
fn version_mutation_result_yaml_structure() {
let result = SubcommandResult::VersionMutation(VersionMutationResult {
mutated_version: Version::parse("2.1.1").unwrap(),
});
let doc = parse_yaml_value(&result);
assert_eq!(
doc.get("mutated_version").and_then(|v| v.as_str()),
Some("2.1.1")
);
}
#[test]
fn ordered_version_map_yaml_structure() {
let mut versions = vec![
Version::parse("1.0.0").unwrap(),
Version::parse("2.0.0").unwrap(),
];
let map = OrderedVersionMap::new(&mut versions, &None, false, false, false);
let result = SubcommandResult::OrderedVersionMap(map);
let doc = parse_yaml_value(&result);
let versions = doc
.get("versions")
.and_then(|v| v.as_object())
.expect("versions map");
assert_eq!(versions.len(), 2);
assert!(versions.contains_key("1.0.0"));
assert!(versions.contains_key("2.0.0"));
}
#[test]
fn version_explanation_yaml_structure() {
let version = Version::parse("1.2.3-rc.0+build.1").unwrap();
let result = SubcommandResult::VersionExplanation(VersionExplanation::from(&version));
let doc = parse_yaml_value(&result);
assert_eq!(doc.get("major").and_then(|v| v.as_u64()), Some(1));
assert_eq!(doc.get("minor").and_then(|v| v.as_u64()), Some(2));
assert_eq!(doc.get("patch").and_then(|v| v.as_u64()), Some(3));
assert_eq!(
doc.get("prerelease_string").and_then(|v| v.as_str()),
Some("rc.0")
);
assert!(doc.get("prerelease").and_then(|v| v.as_array()).is_some());
assert_eq!(
doc.get("build_metadata_string").and_then(|v| v.as_str()),
Some("build.1")
);
assert!(
doc.get("build-metadata")
.and_then(|v| v.as_array())
.is_some()
);
}
#[test]
fn version_explanation_yaml_preserves_u64_above_i64_max() {
const MAJOR: u64 = i64::MAX as u64 + 1;
let version = Version::parse(&format!("{MAJOR}.0.0")).unwrap();
let result = SubcommandResult::VersionExplanation(VersionExplanation::from(&version));
let yaml = serialize_yaml(&result).unwrap();
assert!(
yaml.contains(&format!("major: {MAJOR}")),
"expected plain integer scalar in YAML output, got:\n{yaml}"
);
assert!(
!yaml.contains(&format!("major: \"{MAJOR}\""))
&& !yaml.contains(&format!("major: '{MAJOR}'")),
"large semver components must not be emitted as YAML strings:\n{yaml}"
);
let doc = parse_yaml_value(&result);
assert_eq!(doc.get("major").and_then(|v| v.as_u64()), Some(MAJOR));
}
#[test]
fn select_result_untagged_yaml_structure() {
let inner = SelectResult::select("1.2.3", SemverComponent::Major, false, false).unwrap();
let result = SubcommandResult::SelectResult(inner);
let doc = parse_yaml_value(&result);
assert_eq!(doc.get("value").and_then(|v| v.as_str()), Some("1"));
}
}