Skip to main content

amethystate_core/primitives/
error.rs

1use crate::failure::{Because, StorageError};
2use crate::path::{SmolStr, StorePath, StorePathError};
3use error_stack::Report;
4use std::fmt;
5
6/// Writes the two halves every write-error model spells the same way, for an
7/// enum carrying `TooDeep`, `WillNotEncode`, `Closed` and `Store`.
8///
9/// The four are shared because the decision is: a caller acts differently on a
10/// depth budget, on a codec, and on a store that has let go of its file, and
11/// everything else travels whole so the facts attached along the way survive.
12/// Only the outermost context is read, and it becomes a type rather than
13/// something to downcast.
14///
15/// A model's own variants sit beside these and are not touched - a path that
16/// would not parse, a place a declaration owns, a source that is gone. What
17/// this settles is that a new [`StorageError`] cannot be told apart in one
18/// model and not in the other.
19///
20/// The caller needs `Report`, `StorageError`, `StorePath` and `Because` in
21/// scope, which is what any such model already imports.
22#[macro_export]
23macro_rules! what_the_store_said {
24    ($ty:ident) => {
25        impl $ty {
26            /// What the store said, told apart where a caller would act on it
27            /// differently.
28            pub fn from_store(
29                at: &$crate::path::StorePath,
30                why: ::error_stack::Report<$crate::failure::StorageError>,
31            ) -> Self {
32                match *why.current_context() {
33                    $crate::failure::StorageError::Depth => Self::TooDeep {
34                        at: at.clone(),
35                        why: why.into(),
36                    },
37                    $crate::failure::StorageError::Codec => Self::WillNotEncode {
38                        at: at.clone(),
39                        why: why.into(),
40                    },
41                    $crate::failure::StorageError::Closed => Self::Closed { at: at.clone() },
42                    _ => Self::Store(why.into()),
43                }
44            }
45
46            /// The whole report as a string, facts and all, for a caller
47            /// handing this to `anyhow`: `.context(why.explain())`.
48            ///
49            /// The chain [`source`](std::error::Error::source) walks is the
50            /// readable half and is usually enough. This is the rest of it -
51            /// every frame, every attachment, laid out the way the report
52            /// renders itself - for a log record that has to carry everything.
53            pub fn explain(&self) -> String {
54                match self {
55                    Self::Store(why)
56                    | Self::TooDeep { why, .. }
57                    | Self::WillNotEncode { why, .. } => why.explain(),
58                    other => other.to_string(),
59                }
60            }
61        }
62    };
63}
64
65/// Everything a write through a reactive primitive can fail with, and nothing
66/// else.
67///
68/// Fields, cells and maps fail in the same ways, so they share one set; the
69/// per-primitive names below are aliases kept for readability at call sites.
70///
71/// Ordinary [`std::error::Error`], so a caller who does not want to look can
72/// `?` it into `anyhow`, `eyre` or a `Box<dyn Error>` and be done - and get the
73/// whole diagnosis out of it. [`Display`](fmt::Display) says what this write
74/// was and where; [`source`](std::error::Error::source) descends through the
75/// contexts under it, each carrying what was attached to it, so a
76/// `{:?}` of an `anyhow::Error` names the operation, the key, the file and the
77/// engine's own words without a `match` anywhere.
78///
79/// A caller who wants a fact as a type rather than as a sentence reads it back
80/// with [`facts::all`](crate::facts::all) over the report the variant carries -
81/// `facts::all::<StoreFile, _>(why)` for the file, `Key` for the key. That is
82/// the typed way in; the chain is the readable one.
83///
84/// `{:?}` prints the same thing `anyhow` prints, because that is the shape a
85/// reader has learnt: `fn main() -> anyhow::Result<()>` renders with `Debug`,
86/// and so does every `tracing::error!("{err:?}")`. What the report holds beyond
87/// the chain - the frame each context was raised at - is in
88/// [`explain`](Self::explain), which is where a dump belongs.
89pub enum WriteValue {
90    /// An interceptor turned the change down, in its own words.
91    Intercepted { at: StorePath, said: SmolStr },
92
93    /// A rule declared on the field turned the value down, in its own words.
94    Refused { at: StorePath, said: SmolStr },
95
96    /// Interceptors wrote back into the value they guard, nested deeper than
97    /// a write may go, and the guard stopped them.
98    ///
99    /// Nothing turned the change down - a rule that refuses is
100    /// [`Intercepted`](Self::Intercepted). This is a loop in the interceptors
101    /// themselves.
102    Recursed { at: StorePath },
103
104    /// Nothing is stored where the write was aimed, and this write only
105    /// changes what is already there.
106    Absent { at: StorePath },
107
108    /// The name handed in cannot be a level, so nothing can sit under it.
109    NotAPath(StorePathError),
110
111    /// The path or the value nests deeper than this store reads back.
112    ///
113    /// `why` is kept whole because the numbers are the diagnosis: which budget
114    /// ran out, what it was, and how much the path had already spent are
115    /// attached to it.
116    TooDeep { at: StorePath, why: Because },
117
118    /// The value will not turn into what the store keeps, with what the codec
119    /// said kept whole.
120    WillNotEncode { at: StorePath, why: Because },
121
122    /// The store has let go of its file, so nothing lands.
123    ///
124    /// No report, because there is never one to keep: every
125    /// [`StorageError::Closed`] is minted fresh at the refusal - the store
126    /// checks whether it is closed and answers - so nothing has failed
127    /// underneath. What the report did carry is the store's own file, and that
128    /// is dropped here rather than lost from somewhere deeper.
129    Closed { at: StorePath },
130
131    /// The field or map this cell views has been dropped. `into_cell` is the
132    /// form that keeps it alive.
133    SourceGone,
134
135    /// The disk, in every sense: the file, the engine, the codec.
136    ///
137    /// Carries the report whole, so the facts attached along the way - the
138    /// key, the table, how many bytes - are still there for whoever wants
139    /// them.
140    Store(Because),
141}
142
143what_the_store_said!(WriteValue);
144
145impl WriteValue {
146    /// The same, for a backend whose failures are its own rather than the
147    /// store's - a client talking to one over a wire. `doing` is the operation
148    /// the wire was carrying, and what the transport said is the frame below.
149    pub fn from_backend<E>(at: &StorePath, doing: StorageError, why: Report<E>) -> Self
150    where
151        E: std::error::Error + Send + Sync + 'static,
152    {
153        Self::from_store(at, why.change_context(doing))
154    }
155
156    /// The refusal an interceptor gave.
157    pub fn intercepted(at: &StorePath, said: impl AsRef<str>) -> Self {
158        Self::Intercepted {
159            at: at.clone(),
160            said: SmolStr::new(said.as_ref()),
161        }
162    }
163
164    /// What running a change past its interceptors refused, as a write's
165    /// failure.
166    pub fn refused(at: &StorePath, refusal: crate::primitives::intercept::Refusal) -> Self {
167        use crate::primitives::intercept::Refusal;
168
169        match refusal {
170            Refusal::Said(said) => Self::intercepted(at, said),
171            Refusal::Recursed => Self::Recursed { at: at.clone() },
172            Refusal::Ruled(said) => Self::Refused {
173                at: at.clone(),
174                said: SmolStr::new(said),
175            },
176        }
177    }
178}
179
180impl fmt::Display for WriteValue {
181    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182        match self {
183            Self::Intercepted { at, said } => {
184                write!(f, "an interceptor turned down the write to {at}: {said}")
185            }
186            Self::Refused { at, said } => {
187                write!(f, "a declared rule turned down the write to {at}: {said}")
188            }
189            Self::Recursed { at } => write!(
190                f,
191                "interceptors wrote back into {at} deeper than a write may nest, so nothing was \
192                 written"
193            ),
194            Self::Absent { at } => write!(f, "nothing is stored at {at}"),
195            Self::NotAPath(why) => write!(f, "the write was given no path to land at: {why}"),
196            Self::TooDeep { at, .. } => write!(f, "{at} is deeper than this store reads back"),
197            Self::WillNotEncode { at, .. } => {
198                write!(f, "what was written to {at} will not encode")
199            }
200            Self::Closed { at } => {
201                write!(f, "the store was closed, so nothing was written to {at}")
202            }
203            Self::SourceGone => f.write_str(
204                "the value this cell views is gone: the field or map it came from was dropped",
205            ),
206            Self::Store(why) => write!(f, "{}", why.current_context()),
207        }
208    }
209}
210
211impl fmt::Debug for WriteValue {
212    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213        crate::failure::spelled(self, f)
214    }
215}
216
217impl std::error::Error for WriteValue {
218    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
219        match self {
220            Self::NotAPath(why) => Some(why),
221            Self::Store(why) | Self::TooDeep { why, .. } | Self::WillNotEncode { why, .. } => {
222                why.caused().map(|under| under as &dyn std::error::Error)
223            }
224            Self::Intercepted { .. }
225            | Self::Refused { .. }
226            | Self::Recursed { .. }
227            | Self::Absent { .. }
228            | Self::Closed { .. }
229            | Self::SourceGone => None,
230        }
231    }
232}
233
234impl From<StorePathError> for WriteValue {
235    fn from(why: StorePathError) -> Self {
236        Self::NotAPath(why)
237    }
238}
239
240impl From<Report<StorageError>> for WriteValue {
241    fn from(why: Report<StorageError>) -> Self {
242        Self::Store(why.into())
243    }
244}
245
246/// Back into a report, for the plumbing under the boundary.
247///
248/// The sets are what a caller outside the library meets; inside it, everything
249/// still travels as a report, and this is what lets a `?` cross from one to
250/// the other without a `map_err` at every line.
251impl From<WriteValue> for Report<StorageError> {
252    fn from(why: WriteValue) -> Self {
253        match why {
254            WriteValue::Store(report)
255            | WriteValue::TooDeep { why: report, .. }
256            | WriteValue::WillNotEncode { why: report, .. } => report.into_report(),
257            WriteValue::NotAPath(why) => Report::new(why).change_context(StorageError::Path),
258            WriteValue::Closed { at } => {
259                Report::new(StorageError::Closed).attach(crate::facts::Key(at))
260            }
261            WriteValue::Absent { at } => Report::new(StorageError::Read)
262                .attach(crate::facts::Key(at))
263                .attach("nothing is stored there"),
264            WriteValue::Intercepted { at, said } => Report::new(StorageError::Write)
265                .attach(crate::facts::Key(at))
266                .attach(format!("an interceptor turned it down: {said}")),
267            WriteValue::Refused { at, said } => Report::new(StorageError::Write)
268                .attach(crate::facts::Key(at))
269                .attach(format!("a declared rule turned it down: {said}")),
270            WriteValue::Recursed { at } => Report::new(StorageError::Write)
271                .attach(crate::facts::Key(at))
272                .attach("interceptors wrote back into it deeper than a write may nest"),
273            WriteValue::SourceGone => Report::new(StorageError::Write)
274                .attach("the field or map this cell viewed was dropped"),
275        }
276    }
277}
278
279pub type WriteResult<T> = Result<T, WriteValue>;
280
281pub type FieldError = WriteValue;
282pub type ReactiveMapError = WriteValue;
283
284pub type ReactiveFieldResult<T> = WriteResult<T>;
285pub type ReactiveMapResult<T> = WriteResult<T>;