use anyhow::{Context, Result};
use atlassian_cli_output::{OutputFormat, OutputRenderer};
use serde::Serialize;
#[derive(Serialize)]
pub struct MutationResult {
pub success: bool,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
}
impl MutationResult {
pub fn new(message: impl Into<String>) -> Self {
Self {
success: true,
message: message.into(),
id: None,
}
}
pub fn with_id(message: impl Into<String>, id: impl Into<String>) -> Self {
Self {
success: true,
message: message.into(),
id: Some(id.into()),
}
}
}
pub fn render_success(
renderer: &OutputRenderer,
emoji_message: &str,
result: &MutationResult,
) -> Result<()> {
match renderer.format() {
OutputFormat::Table | OutputFormat::Markdown => {
println!("{emoji_message}");
Ok(())
}
OutputFormat::Quiet => {
if let Some(id) = &result.id {
println!("{id}");
}
Ok(())
}
_ => renderer.render(&result),
}
}
pub fn confirmation_matches(expected: &str, typed: &str) -> bool {
!expected.is_empty() && typed.trim() == expected
}
pub fn confirm_destructive(expected: &str, warning: &str) -> Result<()> {
confirm_destructive_on(
expected,
warning,
std::io::IsTerminal::is_terminal(&std::io::stdin()),
)
}
pub(crate) fn confirm_destructive_on(
expected: &str,
warning: &str,
stdin_is_terminal: bool,
) -> Result<()> {
use std::io::Write;
if !stdin_is_terminal {
anyhow::bail!(
"{warning}\nRefusing to continue: no terminal available to confirm. \
Pass --yes to skip this prompt in a script."
);
}
eprintln!("{warning}");
eprint!("Type '{expected}' to confirm: ");
std::io::stderr()
.flush()
.context("Failed to write confirmation prompt")?;
let mut line = String::new();
std::io::stdin()
.read_line(&mut line)
.context("Failed to read confirmation")?;
if !confirmation_matches(expected, &line) {
anyhow::bail!("Confirmation did not match '{expected}'. Nothing was changed.");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn without_a_terminal_it_refuses() {
let err = confirm_destructive_on("repo", "about to delete", false)
.expect_err("must refuse when nothing can answer");
let message = format!("{err:#}");
assert!(message.contains("Refusing to continue"), "{message}");
assert!(
message.contains("--yes"),
"must say how to proceed: {message}"
);
}
#[test]
fn confirmation_requires_the_exact_resource_name() {
assert!(confirmation_matches("my-repo", "my-repo"));
assert!(confirmation_matches("my-repo", " my-repo \n"));
assert!(!confirmation_matches("my-repo", "yes"));
assert!(!confirmation_matches("my-repo", "y"));
assert!(!confirmation_matches("my-repo", ""));
assert!(!confirmation_matches("my-repo", "My-Repo"));
assert!(!confirmation_matches("my-repo", "my-repo-2"));
}
#[test]
fn empty_expectation_never_confirms() {
assert!(!confirmation_matches("", ""));
assert!(!confirmation_matches("", "anything"));
}
#[test]
fn test_mutation_result_new() {
let result = MutationResult::new("Created issue");
assert!(result.success);
assert_eq!(result.message, "Created issue");
assert!(result.id.is_none());
}
#[test]
fn test_mutation_result_with_id() {
let result = MutationResult::with_id("Created issue", "PROJ-123");
assert!(result.success);
assert_eq!(result.message, "Created issue");
assert_eq!(result.id, Some("PROJ-123".to_string()));
}
#[test]
fn test_render_success_table() {
let renderer = OutputRenderer::new(OutputFormat::Table);
let result = MutationResult::with_id("Created", "123");
assert!(render_success(&renderer, "✅ Created", &result).is_ok());
}
#[test]
fn test_render_success_json() {
let renderer = OutputRenderer::new(OutputFormat::Json);
let result = MutationResult::with_id("Created", "123");
assert!(render_success(&renderer, "✅ Created", &result).is_ok());
}
#[test]
fn test_render_success_quiet() {
let renderer = OutputRenderer::new(OutputFormat::Quiet);
let result = MutationResult::with_id("Created", "123");
assert!(render_success(&renderer, "✅ Created", &result).is_ok());
}
#[test]
fn test_render_success_quiet_no_id() {
let renderer = OutputRenderer::new(OutputFormat::Quiet);
let result = MutationResult::new("Deleted");
assert!(render_success(&renderer, "✅ Deleted", &result).is_ok());
}
#[test]
fn test_render_success_markdown() {
let renderer = OutputRenderer::new(OutputFormat::Markdown);
let result = MutationResult::with_id("Created", "123");
assert!(render_success(&renderer, "✅ Created", &result).is_ok());
}
}