Skip to main content

amethystate_core/
facts.rs

1//! What a failure was about, as types rather than as sentences.
2//!
3//! Attached to a report with the [`Facts`] extension trait, and read back out
4//! of one with [`all`].
5
6use crate::path::StorePath;
7use error_stack::{Frame, Report, ResultExt};
8use std::fmt;
9use std::path::{Path, PathBuf};
10
11/// Every fact of type `T` the report carries, outermost first.
12///
13/// `Report::request_ref` would be the natural way and is nightly-only, so this
14/// walks the frames instead.
15pub fn all<T: Send + Sync + 'static, C>(report: &Report<C>) -> impl Iterator<Item = &T> {
16    report.frames().filter_map(Frame::downcast_ref::<T>)
17}
18
19macro_rules! fact {
20    ($(#[$meta:meta])* $name:ident($ty:ty) => $label:literal) => {
21        $(#[$meta])*
22        #[derive(Debug, Clone, PartialEq, Eq)]
23        pub struct $name(pub $ty);
24
25        impl fmt::Display for $name {
26            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27                write!(f, concat!($label, ": {}"), self.0)
28            }
29        }
30    };
31}
32
33/// The store's own file.
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct StoreFile(pub PathBuf);
36
37impl fmt::Display for StoreFile {
38    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39        write!(f, "store: {}", self.0.display())
40    }
41}
42
43/// The sidecar the text engines keep their bookkeeping in, which is a
44/// different file from the one holding the data.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct MetaFile(pub PathBuf);
47
48impl fmt::Display for MetaFile {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        write!(f, "meta file: {}", self.0.display())
51    }
52}
53
54fact!(
55    /// The key an operation was about.
56    Key(StorePath) => "key"
57);
58fact!(
59    /// The subtree an operation was about.
60    Prefix(StorePath) => "prefix"
61);
62fact!(
63    /// A key as it sits on disk, before anything has read it as a path - which
64    /// is the only state it is in when the reason for the failure is that it
65    /// cannot be read as one.
66    RawKey(String) => "stored key"
67);
68fact!(
69    /// The engine table it was in.
70    Table(String) => "table"
71);
72fact!(
73    /// Why a declared check would not have the value the store holds, in the
74    /// words the check itself gave.
75    ///
76    /// A value carrying this decoded perfectly well: what it failed is the
77    /// application's own rule about what the value may be.
78    Refused(String) => "refused"
79);
80fact!(
81    /// One level below a prefix, as it was scanned rather than as a caller
82    /// spelled it.
83    Entry(String) => "entry"
84);
85fact!(
86    /// The prefix a migration step was running over, which is what the keys in
87    /// the same report are relative to.
88    Migrating(String) => "migrating"
89);
90fact!(
91    /// A node in the bookkeeping sidecar, named by its kind and the prefix it
92    /// describes rather than by any key a caller wrote.
93    MetaNode(String) => "meta node"
94);
95fact!(
96    /// How large the value was.
97    ValueBytes(usize) => "value bytes"
98);
99fact!(
100    /// How far a scan had got when it failed.
101    Read(usize) => "read so far"
102);
103fact!(
104    /// How much the write buffer was holding.
105    Buffered(usize) => "buffered entries"
106);
107
108/// Attaches a fact to a failing result, lazily: nothing is built on the path
109/// that succeeds.
110pub trait Facts: ResultExt + Sized {
111    fn attach_store_file(self, file: &Path) -> Result<Self::Ok, Report<Self::Context>>;
112    fn attach_meta_file(self, file: &Path) -> Result<Self::Ok, Report<Self::Context>>;
113    fn attach_key(self, key: &StorePath) -> Result<Self::Ok, Report<Self::Context>>;
114    fn attach_prefix(self, prefix: &StorePath) -> Result<Self::Ok, Report<Self::Context>>;
115    fn attach_raw_key(self, key: &str) -> Result<Self::Ok, Report<Self::Context>>;
116    fn attach_table(self, table: &str) -> Result<Self::Ok, Report<Self::Context>>;
117    /// Takes anything that can spell itself, so a caller holding a path hands
118    /// the path over: the spelling is worked out inside the closure below,
119    /// which runs only where there is an error to attach it to.
120    fn attach_meta_node(self, node: impl fmt::Display) -> Result<Self::Ok, Report<Self::Context>>;
121    fn attach_entry(self, name: &str) -> Result<Self::Ok, Report<Self::Context>>;
122    fn attach_migrating(self, prefix: &StorePath) -> Result<Self::Ok, Report<Self::Context>>;
123    fn attach_value_bytes(self, len: usize) -> Result<Self::Ok, Report<Self::Context>>;
124    fn attach_read_so_far(self, count: usize) -> Result<Self::Ok, Report<Self::Context>>;
125    fn attach_buffered(self, count: usize) -> Result<Self::Ok, Report<Self::Context>>;
126}
127
128impl<R: ResultExt> Facts for R {
129    fn attach_store_file(self, file: &Path) -> Result<R::Ok, Report<R::Context>> {
130        self.attach_with(|| StoreFile(file.to_path_buf()))
131    }
132
133    fn attach_meta_file(self, file: &Path) -> Result<R::Ok, Report<R::Context>> {
134        self.attach_with(|| MetaFile(file.to_path_buf()))
135    }
136
137    fn attach_key(self, key: &StorePath) -> Result<R::Ok, Report<R::Context>> {
138        self.attach_with(|| Key(key.clone()))
139    }
140
141    fn attach_prefix(self, prefix: &StorePath) -> Result<R::Ok, Report<R::Context>> {
142        self.attach_with(|| Prefix(prefix.clone()))
143    }
144
145    fn attach_raw_key(self, key: &str) -> Result<R::Ok, Report<R::Context>> {
146        self.attach_with(|| RawKey(key.to_owned()))
147    }
148
149    fn attach_table(self, table: &str) -> Result<R::Ok, Report<R::Context>> {
150        self.attach_with(|| Table(table.to_owned()))
151    }
152
153    fn attach_meta_node(self, node: impl fmt::Display) -> Result<R::Ok, Report<R::Context>> {
154        self.attach_with(|| MetaNode(node.to_string()))
155    }
156
157    fn attach_entry(self, name: &str) -> Result<R::Ok, Report<R::Context>> {
158        self.attach_with(|| Entry(name.to_owned()))
159    }
160
161    fn attach_migrating(self, prefix: &StorePath) -> Result<R::Ok, Report<R::Context>> {
162        self.attach_with(|| Migrating(prefix.to_string()))
163    }
164
165    fn attach_value_bytes(self, len: usize) -> Result<R::Ok, Report<R::Context>> {
166        self.attach_with(|| ValueBytes(len))
167    }
168
169    fn attach_read_so_far(self, count: usize) -> Result<R::Ok, Report<R::Context>> {
170        self.attach_with(|| Read(count))
171    }
172
173    fn attach_buffered(self, count: usize) -> Result<R::Ok, Report<R::Context>> {
174        self.attach_with(|| Buffered(count))
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use crate::failure::StorageError;
182
183    fn failing() -> Result<(), Report<StorageError>> {
184        Err(Report::new(StorageError::Write))
185    }
186
187    #[test]
188    fn a_fact_reads_back_by_its_type() {
189        let path = StorePath::from_segments(["ui", "theme"]);
190        let report = failing().attach_key(&path).unwrap_err();
191
192        let found: Vec<&Key> = all::<Key, _>(&report).collect();
193        assert_eq!(found, vec![&Key(path)]);
194    }
195
196    #[test]
197    fn two_facts_of_different_types_do_not_collide() {
198        let key = StorePath::from_segments(["ui", "theme"]);
199        let prefix = StorePath::segment("ui");
200        let report = failing()
201            .attach_key(&key)
202            .attach_prefix(&prefix)
203            .unwrap_err();
204
205        assert_eq!(all::<Key, _>(&report).count(), 1);
206        assert_eq!(all::<Prefix, _>(&report).count(), 1);
207        assert_eq!(
208            all::<Prefix, _>(&report).next(),
209            Some(&Prefix(prefix)),
210            "a prefix must not be read back as a key"
211        );
212    }
213
214    #[test]
215    fn the_same_fact_attached_twice_is_visible_as_twice() {
216        let key = StorePath::from_segments(["ui", "theme"]);
217        let report = failing().attach_key(&key).attach_key(&key).unwrap_err();
218
219        assert_eq!(
220            all::<Key, _>(&report).count(),
221            2,
222            "a duplicate has to be countable before anything can collapse it"
223        );
224    }
225
226    #[test]
227    fn a_fact_prints_with_its_label() {
228        let report = failing()
229            .attach_key(&StorePath::from_segments(["ui", "theme"]))
230            .unwrap_err();
231
232        assert!(
233            format!("{report:?}").contains("key: ui.theme"),
234            "the label lives on the type, so it must reach the printed report"
235        );
236    }
237}