use crate::commands::deploy::approve::{
ApprovalAlreadyPosted, ApprovalDryRun, ApprovalGenerated, ApprovalPosted,
};
use crate::commands::{app, deploy, keys, login, operator, version};
use serde::Serialize;
use std::fmt::{self, Display, Formatter};
#[derive(Serialize)]
#[serde(tag = "reason", rename_all = "snake_case")]
#[cfg_attr(test, derive(strum::EnumIter))]
pub enum Outcome {
LoggedIn(login::LoggedIn),
OperatorCreated(operator::create::OperatorCreated),
ProfileDeleted(login::ProfileDeleted),
ManifestApprovalPosted(ApprovalPosted),
ManifestApprovalGenerated(ApprovalGenerated),
ManifestApprovalAlreadyPosted(ApprovalAlreadyPosted),
ManifestApprovalDryRun(ApprovalDryRun),
DeploymentRuntimeStatus(deploy::get_status::DeploymentRuntimeStatus),
ProvisioningDetails(deploy::provisioning_details::ProvisioningDetails),
ProvisioningShareCreated(deploy::provision::ProvisioningShareCreated),
QuorumKeySharePosted(deploy::post_share::QuorumKeySharePosted),
DeploymentStatus(deploy::status::DeploymentStatusReport),
DeploymentCreated(deploy::create::DeploymentCreated),
DeploymentConfigCreated(deploy::init::DeploymentConfigCreated),
DebugLogsFetched(deploy::debug_logs::DebugLogsFetched),
DeploymentDeleted(deploy::delete::DeploymentDeleted),
DeploymentRestored(deploy::restore::DeploymentRestored),
AppStatus(app::status::AppStatusReport),
AppsListed(app::list::AppsListed),
AppCreated(app::create::AppCreated),
AppConfigCreated(app::init::AppConfigCreated),
LiveDeploymentSet(app::set_live_deploy::LiveDeploymentSet),
AppDeleted(app::delete::AppDeleted),
QuorumKeyCreated(keys::create_quorum_key::QuorumKeyCreated),
QuorumKeyGenerated(keys::generate_local_quorum_key::QuorumKeyGenerated),
QuorumKeyConfigCreated(keys::init_local_quorum_key::QuorumKeyConfigCreated),
ReEncryptedShareGenerated(keys::re_encrypt_local_share::ReEncryptedShareGenerated),
Version(version::CliVersion),
}
impl Display for Outcome {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Outcome::LoggedIn(msg) => msg.fmt(f),
Outcome::OperatorCreated(msg) => msg.fmt(f),
Outcome::ProfileDeleted(msg) => msg.fmt(f),
Outcome::ManifestApprovalPosted(msg) => msg.fmt(f),
Outcome::ManifestApprovalGenerated(msg) => msg.fmt(f),
Outcome::ManifestApprovalAlreadyPosted(msg) => msg.fmt(f),
Outcome::ManifestApprovalDryRun(msg) => msg.fmt(f),
Outcome::DeploymentRuntimeStatus(msg) => msg.fmt(f),
Outcome::ProvisioningDetails(msg) => msg.fmt(f),
Outcome::ProvisioningShareCreated(msg) => msg.fmt(f),
Outcome::QuorumKeySharePosted(msg) => msg.fmt(f),
Outcome::DeploymentStatus(msg) => msg.fmt(f),
Outcome::DeploymentCreated(msg) => msg.fmt(f),
Outcome::DeploymentConfigCreated(msg) => msg.fmt(f),
Outcome::DebugLogsFetched(msg) => msg.fmt(f),
Outcome::DeploymentDeleted(msg) => msg.fmt(f),
Outcome::DeploymentRestored(msg) => msg.fmt(f),
Outcome::AppStatus(msg) => msg.fmt(f),
Outcome::AppsListed(msg) => msg.fmt(f),
Outcome::AppCreated(msg) => msg.fmt(f),
Outcome::AppConfigCreated(msg) => msg.fmt(f),
Outcome::LiveDeploymentSet(msg) => msg.fmt(f),
Outcome::AppDeleted(msg) => msg.fmt(f),
Outcome::QuorumKeyCreated(msg) => msg.fmt(f),
Outcome::QuorumKeyGenerated(msg) => msg.fmt(f),
Outcome::QuorumKeyConfigCreated(msg) => msg.fmt(f),
Outcome::ReEncryptedShareGenerated(msg) => msg.fmt(f),
Outcome::Version(msg) => msg.fmt(f),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use strum::IntoEnumIterator;
const NON_TERMINAL_REASONS: [&str; 3] =
["debug_log_line", "command_error", "missing_required_input"];
#[test]
fn terminal_reasons_do_not_collide_with_non_terminal_reasons() {
for outcome in Outcome::iter() {
let value = serde_json::to_value(&outcome)
.expect("every outcome payload must serialize as a JSON map");
let reason = value["reason"]
.as_str()
.expect("every serialized outcome must carry a `reason` tag");
assert!(
!NON_TERMINAL_REASONS.contains(&reason),
"terminal reason `{reason}` collides with a non-terminal reason"
);
}
}
#[test]
fn reasons_are_snake_case() {
let terminal_reasons = Outcome::iter().map(|outcome| {
serde_json::to_value(outcome)
.expect("every outcome payload must serialize as a JSON map")["reason"]
.as_str()
.expect("every serialized outcome must carry a `reason` tag")
.to_string()
});
for reason in terminal_reasons.chain(NON_TERMINAL_REASONS.map(String::from)) {
assert!(
!reason.is_empty()
&& reason.split('_').all(|word| {
!word.is_empty()
&& word
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
}),
"reason `{reason}` is not snake_case"
);
}
}
}