use std::io::Write;
use crate::cli::Session;
use crate::exit::ExitCode;
#[derive(Debug)]
pub struct Intent<'a> {
pub action: &'a str,
pub targets: &'a [String],
pub body: &'a serde_json::Value,
pub always_confirm: bool,
}
#[derive(Debug)]
pub enum Gate {
Proceed,
Stop(ExitCode),
}
#[must_use]
pub fn check(intent: &Intent<'_>, session: &Session) -> Gate {
let mut err = anstream::stderr();
if let Some(resolved) = &session.resolved {
session.announce(resolved);
}
if session.global.dry_run {
let _ = writeln!(err, "dry run: would {}", intent.action);
let body =
serde_json::to_string_pretty(intent.body).unwrap_or_else(|_| intent.body.to_string());
let _ = writeln!(err, "{body}");
return Gate::Stop(ExitCode::Success);
}
if intent.targets.len() > 1 && !session.global.yes {
let _ = writeln!(
err,
"refusing to {} across {} issues without --yes: {}",
intent.action,
intent.targets.len(),
intent.targets.join(", "),
);
return Gate::Stop(ExitCode::ConfirmationRequired);
}
if intent.always_confirm && !session.global.yes {
let _ = writeln!(
err,
"refusing to {} without --yes: this one cannot be undone",
intent.action,
);
return Gate::Stop(ExitCode::ConfirmationRequired);
}
Gate::Proceed
}
pub fn parse_assignment(raw: &str) -> Result<(String, serde_json::Value), String> {
if let Some((key, value)) = raw.split_once(":=")
&& !key.contains('=')
{
if key.is_empty() {
return Err(format!("empty field name in `{raw}`"));
}
return match serde_json::from_str(value) {
Ok(parsed) => Ok((key.to_owned(), parsed)),
Err(error) => Err(format!(
"`{raw}` says JSON with `:=` and is not valid JSON: {error}"
)),
};
}
let Some((key, value)) = raw.split_once('=') else {
return Err(format!("expected key=value, got `{raw}`"));
};
if key.is_empty() {
return Err(format!("empty field name in `{raw}`"));
}
let parsed =
serde_json::from_str(value).unwrap_or_else(|_| serde_json::Value::String(value.to_owned()));
Ok((key.to_owned(), parsed))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn numbers_and_booleans_keep_their_type() {
assert_eq!(
parse_assignment("storyPoints=3"),
Ok(("storyPoints".to_owned(), serde_json::json!(3)))
);
assert_eq!(
parse_assignment("flagged=true"),
Ok(("flagged".to_owned(), serde_json::json!(true)))
);
}
#[test]
fn plain_text_stays_a_string() {
assert_eq!(
parse_assignment("status=In Progress"),
Ok(("status".to_owned(), serde_json::json!("In Progress")))
);
}
#[test]
fn json_arrays_pass_through_as_arrays() {
assert_eq!(
parse_assignment(r#"tags=["a","b"]"#),
Ok(("tags".to_owned(), serde_json::json!(["a", "b"])))
);
}
#[test]
fn only_the_first_equals_separates() {
assert_eq!(
parse_assignment("summary=a=b"),
Ok(("summary".to_owned(), serde_json::json!("a=b")))
);
}
#[test]
fn a_json_assignment_is_not_guessed_at() {
assert_eq!(
parse_assignment(r#"summary:="3""#),
Ok(("summary".to_owned(), serde_json::json!("3"))),
"a number-shaped summary must be writable as a string"
);
assert_eq!(
parse_assignment("storyPoints:=3"),
Ok(("storyPoints".to_owned(), serde_json::json!(3)))
);
}
#[test]
fn invalid_json_after_the_colon_is_refused() {
assert!(parse_assignment("summary:=not json").is_err());
}
#[test]
fn only_a_leading_colon_equals_means_json() {
assert_eq!(
parse_assignment("summary=a:=b"),
Ok(("summary".to_owned(), serde_json::json!("a:=b")))
);
}
#[test]
fn a_pair_without_an_equals_is_rejected() {
assert!(parse_assignment("storyPoints").is_err());
assert!(parse_assignment("=3").is_err());
}
}