use crate::cli::Format;
use crate::exit::ExitCode;
use crate::output::{CopyChange, CopyReport, Streams, emit, write_outcome};
use super::address::Address;
use super::confirm::Go;
use super::plan::Plan;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ReportState {
Preview,
DryRun,
Written,
}
impl ReportState {
pub(crate) fn preview(dry_run: bool) -> Self {
if dry_run { Self::DryRun } else { Self::Preview }
}
}
pub(crate) fn report_for(
from: &Address,
to: &Address,
plan: &Plan,
state: ReportState,
) -> CopyReport {
CopyReport {
from: from.sheep.clone(),
to: to.sheep.clone(),
path: from.path.dotted(),
written: state == ReportState::Written,
dry_run: state == ReportState::DryRun,
pending: Vec::new(),
changes: plan
.changes
.iter()
.map(|change| CopyChange {
key: change.key.clone(),
change: change.kind.as_str(),
old: change.old.clone(),
new: change.new.clone(),
})
.collect(),
}
}
#[must_use]
pub(crate) fn shows_changes_first(fmt: Format, go: Go) -> bool {
fmt == Format::Table || go == Go::Ask
}
pub(crate) fn print_changes(
streams: &mut Streams<'_>,
report: &CopyReport,
) -> Result<(), ExitCode> {
let writer: &mut dyn std::io::Write = if streams.fmt == Format::Table {
&mut *streams.out
} else {
&mut *streams.err
};
emit(writer, Format::Table, "copy", report.clone(), streams.style)
.map_err(|_| ExitCode::Failure)
}
pub(crate) fn finish(streams: &mut Streams<'_>, report: &CopyReport) -> ExitCode {
if streams.fmt != Format::Json {
return ExitCode::Success;
}
write_outcome(emit(
&mut *streams.out,
Format::Json,
"copy",
report.clone(),
streams.style,
))
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
use crate::commands::copy::address::ConfigPath;
use crate::commands::copy::confirm::{Answer, confirm};
use crate::commands::copy::plan::{Change, ChangeKind};
use crate::output::Render;
fn sides(dog: &str) -> (Address, Address) {
let path = ConfigPath::Dog {
dog: dog.to_string(),
keys: Vec::new(),
};
(
Address {
sheep: "web".to_string(),
path: path.clone(),
},
Address {
sheep: "api".to_string(),
path,
},
)
}
#[test]
fn a_report_prints_its_changes_and_its_verdict() {
let (from, to) = sides("kelpie");
let plan = Plan {
changes: vec![Change {
key: "rulings.decide".to_string(),
kind: ChangeKind::Changed,
old: Some(json!("issue")),
new: Some(json!("panel")),
}],
write: None,
};
let report = report_for(&from, &to, &plan, ReportState::Written);
let json = serde_json::to_value(&report).unwrap();
assert_eq!(json["from"], "web");
assert_eq!(json["to"], "api");
assert_eq!(json["path"], "dogs.kelpie");
assert_eq!(json["written"], true);
assert_eq!(json["dry_run"], false);
assert_eq!(json["changes"][0]["key"], "rulings.decide");
assert_eq!(json["changes"][0]["change"], "changed");
assert_eq!(json["changes"][0]["old"], "issue");
assert_eq!(json["changes"][0]["new"], "panel");
assert_eq!(
report.rows(),
vec![vec![
"rulings.decide".to_string(),
"changed".to_string(),
"issue".to_string(),
"panel".to_string(),
]]
);
}
#[test]
fn the_json_form_shows_the_change_list_before_it_asks() {
assert!(shows_changes_first(Format::Json, Go::Ask));
assert!(shows_changes_first(Format::Table, Go::Write));
assert!(shows_changes_first(Format::Table, Go::Refuse));
assert!(
!shows_changes_first(Format::Json, Go::Write),
"--yes writes without asking, so a script gets no table on stderr"
);
assert!(
!shows_changes_first(Format::Json, Go::DryRun),
"the envelope is a dry run's output already"
);
assert!(!shows_changes_first(Format::Json, Go::Nothing));
let mut out = Vec::new();
let mut err = Vec::new();
let mut streams = Streams::bare(&mut out, &mut err, Format::Json);
let plan = Plan {
changes: vec![
Change {
key: "rulings.decide".to_string(),
kind: ChangeKind::Changed,
old: Some(json!("issue")),
new: Some(json!("panel")),
},
Change {
key: "restart_delay".to_string(),
kind: ChangeKind::Added,
old: None,
new: Some(json!(2)),
},
],
write: None,
};
let (from, to) = sides("kelpie");
let report = report_for(&from, &to, &plan, ReportState::Preview);
print_changes(&mut streams, &report).expect("the preview writes");
let mut reader = std::io::Cursor::new(b"n\n".as_slice());
let answer = confirm(&mut streams, "web", "api", "dogs.kelpie", 2, &mut reader);
assert!(matches!(answer, Answer::No));
let shown = String::from_utf8(err).unwrap();
let list = shown
.find("KEY")
.expect("the change list printed on stderr");
let question = shown.find("[y/N]").expect("the question, after the list");
assert!(
list < question,
"the operator sees both changes before being asked to approve them: {shown}"
);
assert!(
shown.contains("rulings.decide") && shown.contains("restart_delay"),
"{shown}"
);
assert!(
out.is_empty(),
"stdout stays the one envelope this run never reached"
);
}
#[test]
fn the_table_form_keeps_the_change_list_on_stdout() {
let mut out = Vec::new();
let mut err = Vec::new();
let mut streams = Streams::bare(&mut out, &mut err, Format::Table);
let plan = Plan {
changes: vec![Change {
key: "restart_delay".to_string(),
kind: ChangeKind::Added,
old: None,
new: Some(json!(2)),
}],
write: None,
};
let (from, to) = (
Address {
sheep: "web".to_string(),
path: ConfigPath::Field {
name: "restart_delay".to_string(),
},
},
Address {
sheep: "api".to_string(),
path: ConfigPath::Field {
name: "restart_delay".to_string(),
},
},
);
let report = report_for(&from, &to, &plan, ReportState::Preview);
print_changes(&mut streams, &report).expect("the table writes");
let shown = String::from_utf8(out).unwrap();
assert!(
shown.contains("KEY") && shown.contains("restart_delay"),
"{shown}"
);
assert!(
err.is_empty(),
"the table form's stderr stays for diagnostics"
);
}
}