amethystate 0.23.2

A state manager for Rust applications
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
//! Why a declared struct would not open.
//!
//! Everything a constructor can fail with and nothing else, so a `match` over
//! it needs no arm for the failures that cannot happen there - a flush, a scan,
//! a migration. Adding a way for a constructor to fail is then a compile error
//! at every caller that was handling the rest.
//!
//! Ordinary [`std::error::Error`], so a caller who does not want to look can
//! `?` it into `anyhow`, `eyre` or a `Box<dyn Error>` and be done.

use crate::store::ReadValue;
use crate::store::StorageError;
use crate::store::places::Taken;
use crate::store::rule::Invalid;
use amethystate_core::failure::{Because, spelled};
use amethystate_core::path::{StorePath, StorePathError};
use error_stack::Report;
use std::fmt;
use std::sync::Arc;

/// What stopped a struct from being built.
pub enum OpenStruct {
    /// A declared rule read the stored value and turned it down, in the
    /// rule's own words.
    ///
    /// The value decoded perfectly well: what it failed is the application's
    /// own rule. Reached only where the field or its struct said
    /// [`OnUnreadable::Refuse`](crate::store::OnUnreadable::Refuse) - the other
    /// policies keep the default and record a
    /// [`Disagreement`](crate::observability::Disagreement) instead.
    Refused { at: StorePath, said: Arc<str> },

    /// What is stored will not read back as the field's declared type.
    ///
    /// `why` is kept whole because what the codec choked on - the type asked
    /// for, the bytes it found, how many of them - is attached to it.
    WillNotRead { at: StorePath, why: Because },

    /// Another struct already owns that place, so this one would write over it.
    ///
    /// [`Taken`] names both sides at both paths: a collision is only
    /// diagnosable with all four, and neither declaration mentions the other.
    /// Boxed because it is four times the size of every other variant and this
    /// one is the rarest.
    Taken(Box<Taken>),

    /// The levels handed in do not make a path. Only reachable through
    /// [`field_with_path`](crate::store::field_with_path) and its kin, which
    /// take a caller's own levels; a struct written with `#[amethystate]`
    /// carries a path the macro checked while it compiled.
    NotAPath(StorePathError),

    /// The disk, in every sense: the file, the engine, the codec.
    ///
    /// Carries the report whole, so the facts attached along the way - the
    /// key, the table, how many bytes - are still there for whoever wants
    /// them.
    Store(Because),

    /// A hand-written [`Open`] turned the struct down, in its own words.
    ///
    /// What `?` makes of an [`Invalid`] there - a value
    /// [`RuleContext::require`](crate::store::RuleContext::require) found
    /// missing, or a verdict of the constructor's own.
    Declined(Invalid),
}

/// A struct opened exactly as its declaration says: its fields read, its rules
/// run, its place claimed.
///
/// Generated for every struct with a place of its own. It is what [`Open`]
/// does unless the struct says `open = manual`, and what a hand-written
/// [`Open`] calls for the struct before doing its own part.
pub trait Schema: Sized {
    fn open(store: &crate::Store) -> Result<Self, OpenStruct>;
}

/// How a struct with a place of its own is opened over a store.
///
/// Generated over [`Schema`] for every such struct, so code that opens
/// structs it does not name asks for `S: Open`. A struct declared with
/// `open = manual` is given no impl and takes one written by hand, which reads
/// what it needs from [`Store::context`](crate::Store::context). Its inherent
/// `new_with` - a persistent struct's `load_with` - and
/// [`AmeStateSlice::load_slice`](crate::AmeStateSlice::load_slice) go through
/// that impl, and it has no `new()` or `load()` over the global store.
#[diagnostic::on_unimplemented(
    message = "`{Self}` is opened by hand, and nothing implements `Open` for it",
    label = "`open = manual` leaves `Open` to the struct's author",
    note = "write `impl Open for {Self}`, starting from `<Self as Schema>::open(store)`"
)]
pub trait Open: Sized {
    fn new_with(store: &crate::Store) -> Result<Self, OpenStruct>;
}

impl OpenStruct {
    /// The whole report as a string, facts and all.
    pub fn explain(&self) -> String {
        match self {
            Self::Store(why) | Self::WillNotRead { why, .. } => why.explain(),
            other => other.to_string(),
        }
    }
}

impl fmt::Display for OpenStruct {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Refused { at, said } => {
                write!(f, "a declared rule refused what is stored at {at}: {said}")
            }
            Self::WillNotRead { at, .. } => {
                write!(f, "what is stored at {at} will not read back")
            }
            Self::Taken(taken) => write!(f, "{taken}"),
            Self::NotAPath(why) => write!(f, "the field was given no path to sit at: {why}"),
            Self::Store(why) => write!(f, "{}", why.current_context()),
            Self::Declined(said) => {
                write!(
                    f,
                    "the struct's own constructor declined to open it: {said}"
                )
            }
        }
    }
}

impl fmt::Debug for OpenStruct {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        spelled(self, f)
    }
}

impl std::error::Error for OpenStruct {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Self::NotAPath(why) => Some(why),
            Self::Store(why) | Self::WillNotRead { why, .. } => {
                why.caused().map(|under| under as &dyn std::error::Error)
            }
            Self::Refused { .. } | Self::Taken(_) | Self::Declined(_) => None,
        }
    }
}

impl From<Invalid> for OpenStruct {
    fn from(said: Invalid) -> Self {
        Self::Declined(said)
    }
}

impl From<Box<Taken>> for OpenStruct {
    fn from(taken: Box<Taken>) -> Self {
        Self::Taken(taken)
    }
}

impl From<StorePathError> for OpenStruct {
    fn from(why: StorePathError) -> Self {
        Self::NotAPath(why)
    }
}

impl From<Report<StorageError>> for OpenStruct {
    fn from(why: Report<StorageError>) -> Self {
        Self::Store(why.into())
    }
}

/// A read that would not answer while the struct was being built, which is
/// most of what building one does.
impl From<ReadValue> for OpenStruct {
    fn from(why: ReadValue) -> Self {
        match why {
            ReadValue::NotAPath(why) => Self::NotAPath(why),
            ReadValue::WillNotRead { at, why } => Self::WillNotRead { at, why },
            ReadValue::Store(why) => Self::Store(why),
            ReadValue::Closed { at } => Self::Store(
                Report::new(StorageError::Closed)
                    .attach(amethystate_core::facts::Key(at))
                    .into(),
            ),
        }
    }
}

/// A write that would not land while the struct was being built - seeding a
/// map's declared defaults is the one that reaches here.
impl From<amethystate_core::primitives::error::WriteValue> for OpenStruct {
    fn from(why: amethystate_core::primitives::error::WriteValue) -> Self {
        Self::Store(Report::<StorageError>::from(why).into())
    }
}

/// Back into a report, for the plumbing under the boundary.
impl From<OpenStruct> for Report<StorageError> {
    fn from(why: OpenStruct) -> Self {
        match why {
            OpenStruct::Store(report) | OpenStruct::WillNotRead { why: report, .. } => {
                report.into_report()
            }
            OpenStruct::NotAPath(why) => Report::new(why).change_context(StorageError::Path),
            OpenStruct::Refused { at, said } => Report::new(StorageError::Read)
                .attach(amethystate_core::facts::Key(at))
                .attach(amethystate_core::facts::Refused(said.to_string())),
            OpenStruct::Taken(taken) => crate::store::places::refused(&taken),
            OpenStruct::Declined(said) => Report::new(StorageError::Read)
                .attach(amethystate_core::facts::Refused(said.reason().to_string())),
        }
    }
}

/// Why the store itself would not open.
pub enum OpenStore {
    /// The file or the directory it sits in cannot be used: it is missing, it
    /// is not writable, or something else holds it.
    WouldNotOpen { why: Because },

    /// The store opened, and bringing what was stored up to the declared
    /// schema did not finish. The store was closed again.
    ///
    /// `report` is what the pass did, where it ran: every prefix, and each
    /// failure whole - a [`MigrationError`](crate::migration::MigrationError)
    /// is read off it with `downcast_ref`. `why` is the first failure, told
    /// as text. A pass that could not run at all has no report, and `why` is
    /// the reason.
    Migrating {
        why: Because,
        report: Option<crate::MigrationReport>,
    },

    /// The disk, in every sense.
    Store(Because),
}

/// What opening does when the store's own files will not read.
///
/// A file that is not a database, a document that will not parse, bytes some
/// other program wrote there. Not about a directory that cannot be created, a
/// file something else holds, one this process may not read, or a SQLite
/// database another application marked as its own or a newer SQLite wrote:
/// those are refused whatever this says, because each is somebody's data that
/// reads fine where it belongs.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WillNotOpen {
    /// The open fails and the files are left exactly as they are, for a person
    /// to look at.
    #[default]
    Refuse,

    /// The files are taken away and an empty store is opened in their place.
    ///
    /// For a store whose contents can be rebuilt - a cache, an index, anything
    /// derived - where a start is worth more than what was in it. Everything
    /// [`StoreLayout::names`](crate::store::StoreLayout::names) names goes, the
    /// rewrite copies included, since a copy of what would not read is not a
    /// recovery; a store in the page's storage loses every key under its
    /// prefix, and no other store's. It is said at `warn` before anything is
    /// removed.
    ///
    /// Nothing here is undone afterwards: what is gone is gone, and an open
    /// that fails a second time fails with what it said the second time.
    StartFresh,
}

/// What a save does when the file it is about to replace will not read.
///
/// A text store's file is meant to be edited, so it can be left half-typed -
/// and a save that meets one cannot lay its own paths over a document it cannot
/// parse. The three answers below are what a store can do instead, and which is
/// right depends on whose the file is: a settings file a person keeps in their
/// editor, or one only the application was ever going to touch.
///
/// Neither redb nor SQLite asks. Their file is a database rather than a
/// document: nothing outside the engine writes it, and one that will not read is
/// answered at the open by [`WillNotOpen`] instead.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WhenItWillNotRead {
    /// Leave the file alone and let the save come round again, for as long as
    /// this window; once it runs out, set the file aside and write.
    ///
    /// The default, because a file that will not read is usually an editor
    /// mid-keystroke and is a document again a moment later. Nothing is decided
    /// while it might still fix itself: the save is refused, the debouncer
    /// tries again at its retry interval, and what the store holds waits in
    /// memory. Only a file that stays broken for the whole window is treated as
    /// broken rather than busy.
    ///
    /// The window is measured from the first save that met it, not from each
    /// attempt.
    TryAgainFor(std::time::Duration),

    /// The unreadable file is moved aside, under a name that says what it is,
    /// and the save goes ahead at once.
    ///
    /// Nothing is lost and the application keeps running: what a person typed
    /// is still on disk under `<name>.unreadable`. A second one replaces the
    /// first, since two copies of a file that will not read are worth no more
    /// than one.
    SetAside,

    /// The save is refused and the file is left exactly as it is.
    ///
    /// For a file whose contents are somebody's work rather than the
    /// application's: nothing on disk moves until it parses again, and what the
    /// store holds stays in memory, reported through the usual flush failure.
    /// The cost is that writes pile up unsaved for as long as the file stays
    /// broken.
    Refuse,

    /// The save writes its document whole and what was in the file is gone,
    /// with a line in the log and nothing else.
    ///
    /// For a file the application owns outright, where an unreadable one is a
    /// fault to be flattened rather than somebody's half-finished edit.
    Overwrite,
}

impl Default for WhenItWillNotRead {
    /// Long enough for an editor to finish writing and short enough that a file
    /// somebody actually broke is not held against the store all day.
    fn default() -> Self {
        Self::TryAgainFor(std::time::Duration::from_secs(5))
    }
}

impl OpenStore {
    /// What the store said, told apart where a caller would act on it
    /// differently.
    pub fn from_store(why: Report<StorageError>) -> Self {
        match *why.current_context() {
            StorageError::Open => Self::WouldNotOpen { why: why.into() },
            StorageError::Migrate => Self::Migrating {
                why: why.into(),
                report: None,
            },
            _ => Self::Store(why.into()),
        }
    }

    /// A pass that ran and left a failure behind.
    pub(crate) fn migrating(report: crate::MigrationReport) -> Self {
        let why = report
            .failures()
            .next()
            .map(told)
            .unwrap_or_else(|| Report::new(StorageError::Migrate));

        Self::Migrating {
            why: why.into(),
            report: Some(report),
        }
    }

    /// The whole report as a string, facts and all.
    pub fn explain(&self) -> String {
        self.why().explain()
    }

    fn why(&self) -> &Because {
        let (Self::WouldNotOpen { why } | Self::Migrating { why, .. } | Self::Store(why)) = self;
        why
    }
}

/// A failure as text, under the context a refused migration carries.
fn told(failed: &Report<StorageError>) -> Report<StorageError> {
    use error_stack::{AttachmentKind, FrameKind};

    failed.frames().fold(
        Report::new(StorageError::Migrate),
        |told, frame| match frame.kind() {
            FrameKind::Context(context) => told.attach(context.to_string()),
            FrameKind::Attachment(AttachmentKind::Printable(said)) => told.attach(said.to_string()),
            FrameKind::Attachment(_) => told,
        },
    )
}

impl fmt::Display for OpenStore {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::WouldNotOpen { .. } => f.write_str("the store would not open"),
            Self::Migrating { .. } => f.write_str(
                "the store opened, and the data would not come up to the declared schema",
            ),
            Self::Store(why) => write!(f, "{}", why.current_context()),
        }
    }
}

impl fmt::Debug for OpenStore {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        spelled(self, f)
    }
}

impl std::error::Error for OpenStore {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        self.why()
            .caused()
            .map(|under| under as &dyn std::error::Error)
    }
}

impl From<Report<StorageError>> for OpenStore {
    fn from(why: Report<StorageError>) -> Self {
        Self::from_store(why)
    }
}

impl From<OpenStore> for Report<StorageError> {
    fn from(why: OpenStore) -> Self {
        let (OpenStore::WouldNotOpen { why }
        | OpenStore::Migrating { why, .. }
        | OpenStore::Store(why)) = why;
        why.into_report()
    }
}