use crate::path::StorePath;
use error_stack::{Frame, Report, ResultExt};
use std::fmt;
use std::path::{Path, PathBuf};
pub fn all<T: Send + Sync + 'static, C>(report: &Report<C>) -> impl Iterator<Item = &T> {
report.frames().filter_map(Frame::downcast_ref::<T>)
}
macro_rules! fact {
($(#[$meta:meta])* $name:ident($ty:ty) => $label:literal) => {
$(#[$meta])*
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct $name(pub $ty);
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, concat!($label, ": {}"), self.0)
}
}
};
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StoreFile(pub PathBuf);
impl fmt::Display for StoreFile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "store: {}", self.0.display())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MetaFile(pub PathBuf);
impl fmt::Display for MetaFile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "meta file: {}", self.0.display())
}
}
fact!(
Key(StorePath) => "key"
);
fact!(
Prefix(StorePath) => "prefix"
);
fact!(
RawKey(String) => "stored key"
);
fact!(
Table(String) => "table"
);
fact!(
Entry(String) => "entry"
);
fact!(
Migrating(String) => "migrating"
);
fact!(
MetaNode(String) => "meta node"
);
fact!(
ValueBytes(usize) => "value bytes"
);
fact!(
Read(usize) => "read so far"
);
fact!(
Buffered(usize) => "buffered entries"
);
pub trait Facts: ResultExt + Sized {
fn attach_store_file(self, file: &Path) -> Result<Self::Ok, Report<Self::Context>>;
fn attach_meta_file(self, file: &Path) -> Result<Self::Ok, Report<Self::Context>>;
fn attach_key(self, key: &StorePath) -> Result<Self::Ok, Report<Self::Context>>;
fn attach_prefix(self, prefix: &StorePath) -> Result<Self::Ok, Report<Self::Context>>;
fn attach_raw_key(self, key: &str) -> Result<Self::Ok, Report<Self::Context>>;
fn attach_table(self, table: &str) -> Result<Self::Ok, Report<Self::Context>>;
fn attach_meta_node(self, node: impl fmt::Display) -> Result<Self::Ok, Report<Self::Context>>;
fn attach_entry(self, name: &str) -> Result<Self::Ok, Report<Self::Context>>;
fn attach_migrating(self, prefix: &StorePath) -> Result<Self::Ok, Report<Self::Context>>;
fn attach_value_bytes(self, len: usize) -> Result<Self::Ok, Report<Self::Context>>;
fn attach_read_so_far(self, count: usize) -> Result<Self::Ok, Report<Self::Context>>;
fn attach_buffered(self, count: usize) -> Result<Self::Ok, Report<Self::Context>>;
}
impl<R: ResultExt> Facts for R {
fn attach_store_file(self, file: &Path) -> Result<R::Ok, Report<R::Context>> {
self.attach_with(|| StoreFile(file.to_path_buf()))
}
fn attach_meta_file(self, file: &Path) -> Result<R::Ok, Report<R::Context>> {
self.attach_with(|| MetaFile(file.to_path_buf()))
}
fn attach_key(self, key: &StorePath) -> Result<R::Ok, Report<R::Context>> {
self.attach_with(|| Key(key.clone()))
}
fn attach_prefix(self, prefix: &StorePath) -> Result<R::Ok, Report<R::Context>> {
self.attach_with(|| Prefix(prefix.clone()))
}
fn attach_raw_key(self, key: &str) -> Result<R::Ok, Report<R::Context>> {
self.attach_with(|| RawKey(key.to_owned()))
}
fn attach_table(self, table: &str) -> Result<R::Ok, Report<R::Context>> {
self.attach_with(|| Table(table.to_owned()))
}
fn attach_meta_node(self, node: impl fmt::Display) -> Result<R::Ok, Report<R::Context>> {
self.attach_with(|| MetaNode(node.to_string()))
}
fn attach_entry(self, name: &str) -> Result<R::Ok, Report<R::Context>> {
self.attach_with(|| Entry(name.to_owned()))
}
fn attach_migrating(self, prefix: &StorePath) -> Result<R::Ok, Report<R::Context>> {
self.attach_with(|| Migrating(prefix.to_string()))
}
fn attach_value_bytes(self, len: usize) -> Result<R::Ok, Report<R::Context>> {
self.attach_with(|| ValueBytes(len))
}
fn attach_read_so_far(self, count: usize) -> Result<R::Ok, Report<R::Context>> {
self.attach_with(|| Read(count))
}
fn attach_buffered(self, count: usize) -> Result<R::Ok, Report<R::Context>> {
self.attach_with(|| Buffered(count))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::failure::StorageError;
fn failing() -> Result<(), Report<StorageError>> {
Err(Report::new(StorageError::Write))
}
#[test]
fn a_fact_reads_back_by_its_type() {
let path = StorePath::from_segments(["ui", "theme"]);
let report = failing().attach_key(&path).unwrap_err();
let found: Vec<&Key> = all::<Key, _>(&report).collect();
assert_eq!(found, vec![&Key(path)]);
}
#[test]
fn two_facts_of_different_types_do_not_collide() {
let key = StorePath::from_segments(["ui", "theme"]);
let prefix = StorePath::segment("ui");
let report = failing()
.attach_key(&key)
.attach_prefix(&prefix)
.unwrap_err();
assert_eq!(all::<Key, _>(&report).count(), 1);
assert_eq!(all::<Prefix, _>(&report).count(), 1);
assert_eq!(
all::<Prefix, _>(&report).next(),
Some(&Prefix(prefix)),
"a prefix must not be read back as a key"
);
}
#[test]
fn the_same_fact_attached_twice_is_visible_as_twice() {
let key = StorePath::from_segments(["ui", "theme"]);
let report = failing().attach_key(&key).attach_key(&key).unwrap_err();
assert_eq!(
all::<Key, _>(&report).count(),
2,
"a duplicate has to be countable before anything can collapse it"
);
}
#[test]
fn a_fact_prints_with_its_label() {
let report = failing()
.attach_key(&StorePath::from_segments(["ui", "theme"]))
.unwrap_err();
assert!(
format!("{report:?}").contains("key: ui.theme"),
"the label lives on the type, so it must reach the printed report"
);
}
}