use std::collections::BTreeSet;
use std::io::{BufRead, IsTerminal, Write};
use caexfer::{AssumptionKind, ConversionReport, Error, Omission, Result, Stage};
use super::args::Args;
pub(super) fn flag_for(notice: &Omission) -> &'static str {
match notice.stage {
Stage::Source | Stage::Destination => "--accept-omissions",
Stage::Assumption => match notice.assumption {
Some(AssumptionKind::BasicFrame) => "--accept-basic-frame",
Some(AssumptionKind::ZeroRotations) => "--accept-zero-rotations",
Some(AssumptionKind::SyntheticZero) => "--accept-synthetic-zero",
None => "--accept-all",
},
}
}
fn accepted(args: &Args, flag: &str) -> bool {
args.accept_all
|| match flag {
"--accept-omissions" => args.accept_omissions,
"--accept-basic-frame" => args.accept_basic_frame,
"--accept-zero-rotations" => args.accept_zero_rotations,
"--accept-synthetic-zero" => args.accept_synthetic_zero,
_ => false,
}
}
fn confirm_with(
args: &Args,
report: &ConversionReport,
companion: Option<&[Omission]>,
interactive: bool,
input: &mut impl BufRead,
warning: &mut impl Write,
) -> Result<()> {
let mut notices: Vec<(&Omission, bool)> =
report.omissions.iter().map(|item| (item, false)).collect();
if let Some(companion) = companion {
for item in companion {
if !notices.iter().any(|(previous, _)| *previous == item) {
notices.push((item, true));
}
}
}
let pending: Vec<_> = notices
.into_iter()
.filter(|(item, _)| !accepted(args, flag_for(item)))
.collect();
if pending.is_empty() {
return Ok(());
}
let flags: BTreeSet<_> = pending.iter().map(|(item, _)| flag_for(item)).collect();
let reasons = pending
.iter()
.enumerate()
.map(|(index, (item, companion))| {
format!(
"{}. {}{} [{}]",
index + 1,
if *companion { "Companion: " } else { "" },
item.detail,
flag_for(item)
)
})
.collect::<Vec<_>>()
.join("\n");
let flags = flags.into_iter().collect::<Vec<_>>().join(", ");
let guidance =
format!("use {flags} (or --accept-all) to accept these changes without a prompt");
if !args.json {
writeln!(warning, "Conversion limitations:\n{reasons}\n{guidance}")?;
}
if !interactive || args.json {
return Err(Error::new(
"E_USAGE",
if args.json {
format!("conversion needs confirmation:\n{reasons}\n{guidance}")
} else {
guidance
},
));
}
write!(warning, "Continue with these changes? [y/N] ")?;
warning.flush()?;
let mut answer = String::new();
input.read_line(&mut answer)?;
if matches!(answer.trim().to_ascii_lowercase().as_str(), "y" | "yes") {
Ok(())
} else {
Err(Error::new(
"E_CANCELLED",
"conversion declined; no output installed",
))
}
}
pub(super) fn confirm(
args: &Args,
report: &ConversionReport,
companion: Option<&[Omission]>,
) -> Result<()> {
confirm_with(
args,
report,
companion,
std::io::stdin().is_terminal(),
&mut std::io::stdin().lock(),
&mut std::io::stderr().lock(),
)
}
#[cfg(test)]
mod tests {
use super::*;
use caexfer::{AssumptionKind, Omission};
#[test]
fn interactive_answer_accepts_enumerated_changes() {
let mut report = ConversionReport::default();
report.omissions.push(Omission::assumed(
AssumptionKind::ZeroRotations,
"rotations filled with zero",
));
let mut warning = Vec::new();
confirm_with(
&Args::default(),
&report,
None,
true,
&mut std::io::Cursor::new(b"yes\n"),
&mut warning,
)
.unwrap();
let warning = String::from_utf8(warning).unwrap();
assert!(warning.contains("1. rotations filled with zero [--accept-zero-rotations]"));
assert!(warning.contains("Continue with these changes?"));
}
#[test]
fn unattended_conversion_requires_specific_flag() {
let mut report = ConversionReport::default();
report.omissions.push(Omission {
stage: Stage::Source,
assumption: None,
detail: "materials omitted".into(),
});
let error = confirm_with(
&Args::default(),
&report,
None,
false,
&mut std::io::Cursor::new(Vec::<u8>::new()),
&mut Vec::new(),
)
.unwrap_err();
assert!(error.message.contains("--accept-omissions"));
}
#[test]
fn declined_prompt_returns_cancelled() {
let report = ConversionReport {
omissions: vec![Omission::assumed(
AssumptionKind::SyntheticZero,
"synthetic results",
)],
..ConversionReport::default()
};
let error = confirm_with(
&Args::default(),
&report,
None,
true,
&mut std::io::Cursor::new(b"no\n"),
&mut Vec::new(),
)
.unwrap_err();
assert_eq!(error.code, "E_CANCELLED");
}
#[test]
fn empty_report_needs_no_confirmation() {
confirm_with(
&Args::default(),
&ConversionReport::default(),
None,
false,
&mut std::io::Cursor::new(Vec::<u8>::new()),
&mut Vec::new(),
)
.unwrap();
}
#[test]
fn unclassified_assumption_requires_accept_all() {
let notice = Omission {
stage: Stage::Assumption,
assumption: None,
detail: "unclassified inference".into(),
};
assert_eq!(flag_for(¬ice), "--accept-all");
let report = ConversionReport {
omissions: vec![notice],
..ConversionReport::default()
};
let error = confirm_with(
&Args::default(),
&report,
None,
false,
&mut std::io::Cursor::new(Vec::<u8>::new()),
&mut Vec::new(),
)
.unwrap_err();
assert!(error.message.contains("--accept-all"));
confirm_with(
&Args {
accept_all: true,
..Args::default()
},
&report,
None,
false,
&mut std::io::Cursor::new(Vec::<u8>::new()),
&mut Vec::new(),
)
.unwrap();
}
}