nornir-build-thing-skew/src/lib.rs
//! **What a reader DID with a recipe, in a shape two readers can be compared in.**
//!
//! See `tests/recipe_skew.rs` for the guard itself and the measurement it was
//! written from.
use std::collections::BTreeSet;
/// One reader's honest reading of one recipe.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Reading {
/// `CARGO_PKG_VERSION` of the reader.
pub reader: &'static str,
/// Every `build.*` key path the reader ignored. Empty is the law.
pub dropped: Vec<String>,
/// The refusal, when the reader could not load the file at all.
pub refused: Option<String>,
}
impl Reading {
/// Did this reader read every declared byte of the recipe?
pub fn is_faithful(&self) -> bool {
self.refused.is_none() && self.dropped.is_empty()
}
/// One line, for a failure message that has to name what happened.
pub fn line(&self) -> String {
match (&self.refused, self.dropped.is_empty()) {
(Some(e), _) => format!(
"{}: REFUSED — {}",
self.reader,
e.lines().next().unwrap_or("").trim()
),
(None, true) => format!("{}: read it in full", self.reader),
(None, false) => format!(
"{}: SILENTLY DROPPED {}\n {}",
self.reader,
self.dropped.len(),
self.dropped.join("\n ")
),
}
}
}
/// The reading, generated once per linked crate copy.
///
/// A macro and not a trait because the two crates are two unrelated types with
/// the same shape — there is no trait either of them implements, and there could
/// not be: the older one was published before the newer one existed.
#[macro_export]
macro_rules! reader {
($fn_name:ident, $krate:ident, $version:expr) => {
/// Read `text` with this crate copy and report what it did.
pub fn $fn_name(text: &str) -> $crate::Reading {
let mut seen = ::std::collections::BTreeSet::new();
// toml >= 0.9: building the deserializer parses; a parse error is
// this reader's error like any other.
let r: Result<$krate::variant::BuildConfig, _> = ::toml::Deserializer::parse(text)
.and_then(|de| {
::serde_ignored::deserialize(de, |p| {
seen.insert(p.to_string());
})
});
match r {
Ok(_) => $crate::Reading {
reader: $version,
dropped: $crate::recipe_paths(seen),
refused: None,
},
Err(e) => $crate::Reading {
reader: $version,
dropped: Vec::new(),
refused: Some(e.to_string()),
},
}
}
};
}
/// Only the `[build]` subtree is the recipe: `BuildConfig` also parses a
/// `nornir.toml` full of other root tables, and those are not this format's
/// business.
pub fn recipe_paths(seen: BTreeSet<String>) -> Vec<String> {
seen.into_iter()
.filter(|p| p == "build" || p.starts_with("build."))
.collect()
}