amethystate 0.23.0

A state manager for Rust applications
Documentation
//! What a value has to be for this store to hand it back, decided while the
//! codec writes it.
//!
//! Everything caught here shares a shape: the codec takes the value, reports
//! success, and the next read cannot make sense of what it wrote. No error
//! anywhere and the file is gone, which is the worst form a defect has.
//!
//! Depth is the first. Every codec reads less deeply than it writes -
//! `serde_json` stops at 128 on the way in and has no limit on the way out,
//! `ron` stops at 64, and `rmp_serde` has no limit at all, so the stack runs
//! out around three thousand and kills the process on every later start
//! because the value is already committed. A non-finite float is the second:
//! JSON has no spelling for one, so `null` is written and fails to decode. An
//! integer past `i64` is the third, on toml, whose one integer type is signed
//! and 64 bits wide. A `Some(None)` is the fourth: the outer `Some` has
//! nothing of its own to write, so it reaches the file as one null and comes
//! back `None`. An enum is the fifth, on ron, whose document type has no
//! variant to hold the name.
//!
//! All of them have to be learned from the write itself. By the time a value
//! reaches a store it is a `&dyn erased_serde::Serialize`, and building it out
//! to inspect it is the dangerous act - on redb it is what overflows the
//! stack. Serde is a push protocol and the store is on the receiving end, so
//! [`Counting`] watches what goes past during the codec's own pass and
//! [`Noticed`] holds what it saw. [`Screening`] decides which of it is a
//! refusal, because only it knows what the store promised.

mod counting;

pub use counting::{Counted, Counting, Noticed};

use crate::store::builder::Backend;
use crate::store::config::WriteLimits;
use crate::store::facts::Key;
use crate::store::{CodecFormat, StorageError, StorageResult};
use amethystate_core::path::StorePath;
use error_stack::Report;

/// What one store will carry, worked out once when it opens.
///
/// Each answer is the running codec's, narrowed by whatever the store promised
/// to stay readable on. `key_depth` is the exception: the store's own cap on
/// paths, which is a setting rather than a fact about a format.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Screening {
    pub ceiling: usize,
    pub key_depth: Option<usize>,

    /// Whether a `NaN` or an infinity survives on the running engine and on
    /// every engine this store promised to stay readable on.
    pub non_finite_floats: bool,

    /// The same for an enum of any shape.
    pub enums: bool,

    /// The same for `Some(None)`, which only ron tells apart from `None`.
    pub nested_options: bool,

    /// The same for an integer past `i64`, which toml has no room for.
    pub wide_integers: bool,

    /// The same for a `u128` or an `i128` of any value, which toml and ron
    /// have no type for.
    pub integers_of_128_bits: bool,

    /// The same for an integer neither an `i64` nor a `u64` holds, which json
    /// reads back as neither.
    pub integers_past_64_bits: bool,
}

impl Screening {
    /// What a store running on `engine` under `limits` will carry.
    pub fn resolve(limits: &WriteLimits, engine: Backend) -> Self {
        Self {
            ceiling: limits.ceiling(engine),
            key_depth: limits.key_depth,
            non_finite_floats: limits.non_finite_floats(engine),
            enums: limits.enums(engine),
            nested_options: limits.a_nested_option(engine),
            wide_integers: limits.an_integer_past_i64(engine),
            integers_of_128_bits: limits.a_128_bit_integer(engine),
            integers_past_64_bits: limits.an_integer_past_64_bits(engine),
        }
    }

    /// The same for an engine known only by the codec it runs, which is how a
    /// text store knows itself - it is generic over the document, not over the
    /// backend that chose it.
    pub fn for_codec(limits: &WriteLimits, codec: CodecFormat) -> Self {
        Self::resolve(limits, Backend::writing(codec))
    }

    /// Whether a path is within the store's own cap on how deep a key may go.
    pub fn check_path(&self, path: &StorePath) -> StorageResult<()> {
        let levels = path.len();

        if let Some(cap) = self.key_depth
            && levels > cap
        {
            return Err(Report::new(StorageError::Depth)
                .attach(Key(path.clone()))
                .attach(format!("levels: {levels}, and the limit is {cap}"))
                .attach("set by: limits(|l| l.key_depth(..))")
                .attach(format!(
                    "what is stored here spends the same budget - this store reads {} levels in all",
                    self.ceiling
                )));
        }

        Ok(())
    }

    /// What a value at `path` has left to spend, to be carried through the
    /// codec's own pass.
    ///
    /// The path is counted with the value because the budget can be shared:
    /// where a schema declares the path, a text engine spells its levels as the
    /// document's, so a shallow value at a deep path is exactly as unreadable
    /// as a deep value at a shallow one.
    ///
    /// The other two layouts pay the same charge without owing it. A flat
    /// engine keeps the path as one key - `&str` on redb, `TEXT` on sqlite -
    /// and so does the plane a text engine writes an undeclared path into:
    /// `a_deep_undeclared_path_is_one_key_and_reads_back` puts 639 levels in a
    /// json document that opens one brace. Charging them anyway costs a handful
    /// of levels out of 512 and 127, refuses a little more than it must, and
    /// saves the store a second rule and the reader a second thing to know.
    pub fn for_value(&self, path: &StorePath) -> Noticed {
        Noticed::new(self.ceiling.saturating_sub(path.len()))
    }

    /// Says what went wrong, once a codec's error turns out to have been the
    /// count's.
    ///
    /// A `Serializer` may only return its own error type, so the refusal
    /// reaches the caller wearing the codec's clothes and cannot be recognised
    /// by its type - [`Noticed::overflowed`] is how the caller asks whether it
    /// was this.
    pub fn too_deep(&self, path: &StorePath) -> Report<StorageError> {
        let levels = path.len();
        let left = self.ceiling.saturating_sub(levels);

        Report::new(StorageError::Codec)
            .attach(Key(path.clone()))
            .attach(format!(
                "the path spends {levels} levels and the value goes past the {left} that are left"
            ))
            .attach(format!("this store reads at most {} levels", self.ceiling))
            .attach(
                "a value deeper than the reader accepts is written without complaint and \
                 cannot be read back",
            )
    }

    /// Asks the enum question again in the form a reader's format writes the
    /// value, for an engine whose own codec is not one.
    ///
    /// A type can write itself differently for a reader - an `IpAddr` is a
    /// string to json and ron and a variant to msgpack - and the engine that
    /// refuses enums is read by people. So where the running codec wrote a
    /// variant and this store promised an engine that refuses one, the value
    /// is passed through once more as a reader's format sees it, and what that
    /// pass saw is the answer. A pass that cannot finish leaves the first
    /// answer standing.
    pub fn settle_the_enum_for_a_reader(
        &self,
        seen: &Noticed,
        value: &dyn erased_serde::Serialize,
        path: &StorePath,
    ) {
        if self.enums || !seen.saw_an_enum() {
            return;
        }

        let readable = self.for_value(path);
        let mut sink = serde_json::Serializer::new(std::io::sink());

        if serde::Serialize::serialize(&readable.count(value), &mut sink).is_ok() {
            seen.take_the_enum_from(&readable);
        }
    }

    /// Whether a pass that has finished wrote something this store cannot read
    /// back, or cannot promise elsewhere.
    ///
    /// Asked after a *successful* write rather than after a failed one: a codec
    /// with no spelling for a `NaN` writes `null` and reports success, so there
    /// is no error to inspect. That is the whole reason the value has to be
    /// refused here - left alone it lands as `null`, the write says `Ok`, and
    /// the field goes on reporting the number it held before while the file
    /// holds nothing of the sort.
    pub fn refused(&self, seen: &Noticed, path: &StorePath) -> Option<Report<StorageError>> {
        if !self.non_finite_floats && seen.saw_a_non_finite_float() {
            return Some(
                Report::new(StorageError::Codec)
                    .attach(Key(path.clone()))
                    .attach("a NaN or an infinity, which this store cannot read back")
                    .attach(
                        "JSON has no spelling for either, so the codec writes `null` and \
                         decoding it as a float fails - on json, and on sqlite, which encodes \
                         with the same JSON",
                    ),
            );
        }

        if !self.integers_of_128_bits && seen.saw_a_128_bit_integer() {
            return Some(
                Report::new(StorageError::Codec)
                    .attach(Key(path.clone()))
                    .attach("a `u128` or an `i128`, which this store cannot read back")
                    .attach(
                        "toml and ron have no 128-bit integer type, so they refuse the type \
                         whatever it holds - `42u128` as much as `u128::MAX`",
                    ),
            );
        }

        if !self.integers_past_64_bits && seen.saw_an_integer_past_64_bits() {
            return Some(
                Report::new(StorageError::Codec)
                    .attach(Key(path.clone()))
                    .attach("an integer that neither an `i64` nor a `u64` holds")
                    .attach(
                        "a json number is read back into an `i64`, a `u64` or an `f64`, so one \
                         past both integer types comes back as none of them",
                    ),
            );
        }

        if !self.wide_integers && seen.saw_an_integer_past_i64() {
            return Some(
                Report::new(StorageError::Codec)
                    .attach(Key(path.clone()))
                    .attach("an integer that does not fit in an `i64`")
                    .attach(
                        "TOML has one integer type and it is signed and 64 bits wide, so a \
                         `u64` past its top has nowhere to go",
                    ),
            );
        }

        if !self.nested_options && seen.saw_a_collapsing_option() {
            return Some(
                Report::new(StorageError::Codec)
                    .attach(Key(path.clone()))
                    .attach("a `Some` holding nothing, which reads back as nothing at all")
                    .attach(
                        "the outer `Some` has nothing of its own to write, so `Some(None)` and \
                         `None` reach the file as one null - `c0` under msgpack, `null` under \
                         either JSON - and both come back `None`. ron is the one engine that \
                         spells the `Option` out and keeps them apart",
                    ),
            );
        }

        if !self.enums && seen.saw_an_enum() {
            return Some(
                Report::new(StorageError::Codec)
                    .attach(Key(path.clone()))
                    .attach("an enum, which this store cannot read back")
                    .attach(
                        "ron writes one as `On(3)` and parses it back into a `ron::value::Value`, \
                         which has no variant to put it in - the name is dropped there and the \
                         next read is handed a sequence. `Value` not supporting enums is \
                         listed in https://github.com/ron-rs/ron/issues/122",
                    ),
            );
        }

        None
    }
}