use std::fmt;
use super::problem::{Code, FieldPath, Problem};
const MARK: char = '\u{1f}';
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct Mistake {
code: Code,
field: Option<&'static str>,
reason: String,
fix: Option<String>,
}
impl Mistake {
pub(crate) fn new(code: Code, reason: impl Into<String>) -> Mistake {
Mistake {
code,
field: None,
reason: reason.into(),
fix: None,
}
}
#[must_use]
pub(crate) fn at(mut self, field: &'static str) -> Mistake {
self.field = Some(field);
self
}
#[must_use]
pub(crate) fn fix(mut self, fix: impl Into<String>) -> Mistake {
self.fix = Some(fix.into());
self
}
pub(crate) fn decode(message: &str, path: &FieldPath) -> Option<Problem> {
let mut parts = message.split(MARK);
let reason = parts.next()?;
let code = parts.next().and_then(code)?;
let field = parts.next().unwrap_or_default();
let fix = parts.next().unwrap_or_default();
let at = if field.is_empty() { path.clone() } else { path.key(field) };
let problem = Problem::new(code, at, reason);
Some(if fix.is_empty() { problem } else { problem.fix(fix) })
}
}
fn code(id: &str) -> Option<Code> {
[
Code::UnknownField,
Code::RemovedField,
Code::MissingField,
Code::InvalidValue,
Code::Conflict,
Code::UnknownReference,
Code::SensitiveValue,
Code::Refused,
]
.into_iter()
.find(|c| c.id() == id)
}
impl fmt::Display for Mistake {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}{MARK}{}{MARK}{}{MARK}{}",
self.reason,
self.code.id(),
self.field.unwrap_or_default(),
self.fix.as_deref().unwrap_or_default()
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_mistake_reads_back_as_its_problem_at_its_field() {
let m = Mistake::new(Code::MissingField, "every fetch is pinned").at("url").fix("`sha256: <hex>`");
let at = FieldPath::new("machines.web.app.steps[2].file");
let p = Mistake::decode(&m.to_string(), &at).expect("a mistake");
assert_eq!((p.code, p.path.as_str()), (Code::MissingField, "machines.web.app.steps[2].file.url"));
assert_eq!((p.reason.as_str(), p.fix.as_deref()), ("every fetch is pinned", Some("`sha256: <hex>`")));
assert!(m.to_string().starts_with("every fetch is pinned"), "the reason shows first");
}
#[test]
fn a_plain_message_is_no_mistake() {
assert_eq!(Mistake::decode("invalid type: map, expected a string", &FieldPath::new("a")), None);
let bare = Mistake::decode(&Mistake::new(Code::Conflict, "one guard").to_string(), &FieldPath::new("a")).expect("a mistake");
assert_eq!((bare.path.as_str(), bare.fix), ("a", None));
}
}