//! What commands print: a short labelled report or one JSON object, and //! errors with a stable code, an exit status and the next step. use serde_json::{Map, Value, json}; /// A finished command's answer. #[derive(Default)] pub struct Report { /// Label and value pairs, printed as aligned columns. lines: Vec<(String, String)>, /// Lines printed as they are (diffs, logs), before the labelled lines. raw: Vec, /// The JSON form of the same facts. pub json: Map, pub next: Vec, /// Exit status for answers that aren't failures but need attention /// (sync leaving conflicts, land waiting for someone). pub exit: i32, } impl Report { pub fn new() -> Self { Report::default() } pub fn line(&mut self, label: &str, value: impl Into) -> &mut Self { self.lines.push((label.to_string(), value.into())); self } /// A continuation line under the previous label. pub fn more(&mut self, value: impl Into) -> &mut Self { self.lines.push((String::new(), value.into())); self } pub fn raw(&mut self, text: impl Into) -> &mut Self { self.raw.push(text.into()); self } pub fn set(&mut self, key: &str, value: impl Into) -> &mut Self { self.json.insert(key.to_string(), value.into()); self } pub fn next(&mut self, step: impl Into) -> &mut Self { self.next.push(step.into()); self } pub fn print(&self, as_json: bool) { if as_json { let mut object = Map::new(); object.insert("ok".into(), Value::Bool(self.exit == 0 || self.exit == 3 || self.exit == 5)); for (k, v) in &self.json { object.insert(k.clone(), v.clone()); } object.insert("next".into(), json!(self.next)); println!("{}", Value::Object(object)); return; } for text in &self.raw { print!("{text}"); if !text.ends_with('\n') { println!(); } } print_lines(&self.lines); if let Some(text) = next_text(&self.next) { println!("next: {text}"); } } } fn print_lines(lines: &[(String, String)]) { let width = lines.iter().map(|(l, _)| l.chars().count()).max().unwrap_or(0).max(8) + 2; for (label, value) in lines { let mut first = true; for part in value.split('\n') { if first { println!("{label:width$}{part}"); first = false; } else { println!("{:width$}{part}", ""); } } } } fn next_text(next: &[String]) -> Option { match next.len() { 0 => None, 1 => Some(next[0].clone()), _ => Some(next.join(", then ")), } } /// A command that didn't do what was asked. Every failure says whether /// anything changed and what to do next. #[derive(Debug)] pub struct Fail { pub code: &'static str, pub exit: i32, pub message: String, /// Indented detail lines under the message (files, reasons). pub details: Vec, /// What did or didn't change ("main didn't change; your change is saved"). pub state: Option, pub next: Vec, pub json: Map, } impl Fail { pub fn new(code: &'static str, message: impl Into) -> Self { let exit = match code { "bad_usage" | "no_description" => 2, "conflict" => 3, "checks_failed" => 4, "waiting" => 5, "offline" => 6, "not_allowed" | "not_logged_in" => 7, "secret" => 8, _ => 1, }; Fail { code, exit, message: message.into(), details: Vec::new(), state: None, next: Vec::new(), json: Map::new(), } } pub fn detail(mut self, line: impl Into) -> Self { self.details.push(line.into()); self } pub fn state(mut self, text: impl Into) -> Self { self.state = Some(text.into()); self } pub fn next(mut self, step: impl Into) -> Self { self.next.push(step.into()); self } pub fn set(mut self, key: &str, value: impl Into) -> Self { self.json.insert(key.to_string(), value.into()); self } pub fn print(&self, as_json: bool) { if as_json { let mut object = Map::new(); object.insert("ok".into(), Value::Bool(false)); object.insert("error".into(), json!({ "code": self.code, "message": self.message })); for (k, v) in &self.json { object.insert(k.clone(), v.clone()); } if let Some(state) = &self.state { object.insert("state".into(), json!(state)); } object.insert("next".into(), json!(self.next)); println!("{}", Value::Object(object)); return; } eprintln!("error: {}", self.message); for line in &self.details { eprintln!(" {line}"); } if let Some(state) = &self.state { eprintln!("{state}"); } if let Some(text) = next_text(&self.next) { eprintln!("next: {text}"); } } } impl From for Fail { fn from(error: anyhow::Error) -> Self { if let Some(fail) = error.downcast_ref::() { return fail.0.clone_fail(); } let message = format!("{error:#}"); if is_network(&message) { return Fail::new("offline", format!("can't reach the host: {message}")) .state("nothing on the host changed; your work is saved here") .next("run it again when you're online"); } Fail::new("failed", message) } } impl From for Fail { fn from(error: vcs_core::Error) -> Self { Fail::from(anyhow::Error::from(error)) } } impl From for Fail { fn from(error: rusqlite::Error) -> Self { Fail::from(anyhow::Error::from(error)) } } impl From for Fail { fn from(error: std::io::Error) -> Self { Fail::from(anyhow::Error::from(error)) } } /// Carries a [`Fail`] through `anyhow` (for helpers that return anyhow errors). #[derive(Debug)] pub struct FailBox(pub Fail); impl std::fmt::Display for FailBox { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(&self.0.message) } } impl std::error::Error for FailBox {} impl Fail { fn clone_fail(&self) -> Fail { Fail { code: self.code, exit: self.exit, message: self.message.clone(), details: self.details.clone(), state: self.state.clone(), next: self.next.clone(), json: self.json.clone(), } } pub fn into_anyhow(self) -> anyhow::Error { anyhow::Error::new(FailBox(self)) } } pub fn is_network(message: &str) -> bool { let m = message.to_ascii_lowercase(); [ "failed to resolve address", "could not resolve", "failed to connect", "connection refused", "network is unreachable", "timed out", "dns error", "connection reset", "failed to send request", "io: connection", ] .iter() .any(|needle| m.contains(needle)) } pub type Out = Result;