use clap::{Arg, ArgMatches, Command};
use crate::domain::model::status::{Status, StatusesConfig};
use crate::infra::driving::cli::errors::{die1, CliError};
use crate::infra::driving::cli::output::OutputFormat;
use crate::infra::driving::cli::render_structured;
use crate::infra::driving::cli::theme;
pub(crate) fn subcommand(noun: &'static str, id_help: &'static str) -> Command {
let article = super::indefinite(noun);
Command::new("status")
.about(format!("Read or change the status of {article}"))
.subcommand_required(true)
.arg_required_else_help(true)
.subcommand(
Command::new("show")
.about(format!("Print the current status of {article}"))
.arg(id_arg(id_help)),
)
.subcommand(
Command::new("update")
.about(format!(
"Transition {article} to a new status (records a status_changed event)",
))
.arg(id_arg(id_help))
.arg(
Arg::new("status")
.required(true)
.value_name("STATUS")
.help("New status (must be one of the configured values)"),
),
)
}
fn id_arg(help: &'static str) -> Arg {
Arg::new("id").required(true).value_name("ID").help(help)
}
pub(crate) fn required_status_arg(sub: &ArgMatches) -> &str {
crate::infra::driving::cli::helpers::required_str(sub, "status")
}
pub(crate) fn resolve_status_or_die(
statuses: &StatusesConfig,
raw: &str,
output_fmt: OutputFormat,
) -> Status {
statuses.resolve(raw).unwrap_or_else(|e| {
let known = statuses.status_names().collect::<Vec<_>>().join(", ");
die1(
CliError::new(format!("invalid status '{raw}': {e}"))
.kind("validation")
.hint(format!("Known statuses: {known}")),
output_fmt,
)
})
}
pub(crate) enum View {
NotFound,
Shown { status: String },
Changed { from: Status, to: Status },
NoOp,
}
pub(crate) fn render(
view: View,
display_id: &str,
canonical_id: &str,
noun: &str,
output_fmt: OutputFormat,
) {
match view {
View::NotFound => die1(
CliError::not_found(format!("{noun} '{display_id}'"), &format!("{noun} list")),
output_fmt,
),
view if output_fmt.is_structured() => {
render_structured_view(view, canonical_id, output_fmt)
}
View::Shown { status } => println!("{status}"),
View::Changed { from, to } => println!(
"{}",
theme::success(&format!("Updated {display_id}: {from} → {to}")),
),
View::NoOp => println!(
"{}",
theme::noop(&format!(
"No change: {noun} {display_id} already has that status",
)),
),
}
}
fn render_structured_view(view: View, canonical_id: &str, output_fmt: OutputFormat) {
#[derive(serde::Serialize)]
#[serde(untagged)]
enum StatusResult<'a> {
Show {
id: &'a str,
status: &'a str,
},
Change {
id: &'a str,
outcome: &'a str,
#[serde(skip_serializing_if = "Option::is_none")]
from: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
to: Option<String>,
},
}
let r = match &view {
View::Shown { status } => StatusResult::Show {
id: canonical_id,
status,
},
View::Changed { from, to } => StatusResult::Change {
id: canonical_id,
outcome: "status_changed",
from: Some(from.as_str().to_string()),
to: Some(to.as_str().to_string()),
},
View::NoOp => StatusResult::Change {
id: canonical_id,
outcome: "noop",
from: None,
to: None,
},
View::NotFound => unreachable!(),
};
render_structured(&r, output_fmt);
}