logo

Crate litter

source ¡ []
Expand description

litter makes your literals mutable with smart pointers into your source code.

These can be used for snapshot testing, or as a basic way of inlining state into scripts. This is only intended for use in code that’s being run through Cargo, as it relies on CARGO_ environment variables to locate the source code when mutations need to be written back. The implementation uses the #[track_caller] attribute (no macros).

To Do

  • implement naive unsyncronized writing
  • implement better writing
  • support external data(?)
  • Future work?! Almost nothing described above has actually been implemented yet!
  • Fallible alternatives instead of panicking, including for the case of writing values when the source files don’t exist (they can still be saved in-memory).
  • External data, not just inline.
  • While the value has only been read, we can hold on to a Weak Arc of its state and let it be freed. Once it’s been written to, we need to keep it in memory forever, so we leak a reference.

Basic Use

The litter::Litter type wraps a literal value with information about its location in your source code, allowing it to be mutated with changes reflected in the original script file. Literal types supported are integers (1, 2, 2_usize, -1i16), floats (1.5, 2e6f64), booleans (true, false), static strings ("hello", r##"world##"), static byte strings (b"one two \x12", br"hell\x00"). These are described by the litter::Literal trait.

.edit()ing a Literal or a Litter produces a LitterHandle. It implements Deref and DerefMut, exposing the inner value, as well as various other traits. If the inner value is modified, it will be written back to the file when the Litter is dropped.

Here’s a basic example, of a string that’s modified each time the script runs:

use litter::LiteralExt;

fn main() {
    let mut p = "and I say hello!".edit();
    *p += " hello!";
}
    let mut p = "and I say hello! hello!".edit();
    *p += " hello!";
    let mut p = "and I say hello! hello! hello!".edit();
    *p += " hello!";

Composition

The Litter constructors work using #[track_caller], not macros, so it’s possible to wrap them to create your own functions that modifying literals, with one important caveat: the literal in question must be the first literal that occurs as an argument in the function call. So f(x, "literal") works, but f(2, "literal") would not. For this reason we usually prefer to take the literal as the first argument to the function.

For example, we can use this to implement snapshot testing in the style of expect_test.

#[test]
fn test_the_ultimate_question() {
    assert_eq_u64(42, 6 * 9);
}

#[track_caller] // <- in order to look for literal at this function's call site instead
fn assert_eq_u64(expected: u64, actual: u64) {
    let expected = litter::new(expected);

    if expected != actual {
        if std::env::get("UPDATE_EXPECT").unwrap_or("0") != "0" {
            *expected = actual; // <- updated in memory, written to source at end of scope
        } else {
            panic!("\
                Expected {expected:?} but actual value was {actual:?}.\n\
                \n\
                To update the expected value, run this again with UPDATE_EXPECT=1.\
            ");
        }
    }
}

Running this test will initially fail with our panic message, but running it again with UPDATE_EXPECT=1 cargo test will pass and update our source code to reflect the correct value:

#[test]
fn test_the_ultimate_question() {
    assert_eq_u64(54, 6 * 9);
}

Although you don’t need to write this particular function yourself: a generic version is included at litter::assert_eq(literal, actual).

Serialization

If you enable the json, yaml, postcard, or toml features, LiteralExt for strings and bytes gains corresponding .edit_json(), .edit_yaml(), .edit_toml(), or .edit_postcard() methods that can be used to be used to inline non-primitive values that implement serde::{ Serialize, Deserialize }, as well as Debug, Clone, and Default. (If your type doesn’t implement Default, you may consider wrapping it in an Option<...>.)

As a special case for convenience, if the literal string is empty but deserialization fails, the type’s Default::default() value will be returned. Any other (de)serialization errors will cause the program to panic.

fn main() {
    // empty string interpreted as default, like "[]"
    let json_vec = "".edit_json::<Vec<usize>>();
    json_vec.push(json_vec.len());

    // empty byte string interpreted as default, like b"\x00"
    let postcard_vec = b"".edit_postcard::<Vec<usize>>();
    postcard_vec.push(postcard_vec.len());
}
    let json_vec = "[0]".edit_json::<Vec<usize>>();
    json_vec.push(json_vec.len());

    let postcard_vec = b"\x01\x00".edit_postcard::<Vec<usize>>();
    postcard_vec.postcard_vec(postcard_vec.len());
    let json_vec = "[0, 1]".edit_json::<Vec<usize>>();
    json_vec.push(json_vec.len());

    let postcard_vec = b"\x02\x00\x01".edit_postcard::<Vec<usize>>();
    postcard_vec.push(postcard_vec.len());

If no type is specified or can be inferred, the .edit_json(), .edit_yaml(), and .edit_toml() methods default to dynamic values (serde_json::Value, serde_yaml::Value, and toml::Value). However .edit_postcard() always requires a known type (it’s non-self-describing and can’t be handled dynamically).

For text formats, encoding to a string literal will be pretty/verbose, while encoding to a byte string literal will be use a compact representation. (This obviously isn’t relevant to binary-only formats like postcard and rkyv.)

Performance and Reliability

This is (clearly) intended for convenience, not performance. It should be fast enough for use in test snapshotting or dumping some state for a script, but you certainly wouldn’t want to use it for a high-throughput web server. Access to each literal is controlled by an RwLock which may block if used concurrently.

The filesystem is only accessed when a mutated value needs to be written back, so if a value is never then modified the filesystem won’t be accessed, and in that case the program can work fine on a different system without the source files available.

Logic errors can occur if multiple copies of your program are running concurrently and both try to modify the same file.

License

litter is Copyright Jeremy Banks, released under the familiar choice of MIT OR Apache-2.0.

litter copies heavily from the the expect-test library, which is also under MIT OR Apache-2.0 and is Copyright the rust-analyzer developers, including Aleksey Kladov and Dylan MacKenzie.

Crate Feature Flags

Serialization Formats

  • json — Support (de)serializing string/byte literals as JSON using serde via the .edit_json() methods.

  • postcard — Support (de)serializing byte literals as Postcard using serde via the .edit_postcard() methods.

  • toml — Support (de)serializing string/byte literals as TOML using serde via the .edit_toml() methods.

  • yaml — Support (de)serializing string/byte literals as YAML using serde via the .edit_yaml() methods.

Core Capabilities

  • write (enabled by default) — Support for writing literal mutations back to their source file.

  • mut (enabled by default) — Support for “mutating” literals.

  • panic (enabled by default) — Include features that may panic on error. (Disabling does not prevent all panics (yet?).)

Testing Tools

  • assertions (enabled by default) — Include assertion functions for snapshot testing.

Forbidding Features

These flags are obviously “non-additive” (contra the normal recommendation), so they should typically only be activated by the root binary, not libraries. These don’t disable the corresponding features directly; you’ll still need to use default-features = false. Rather, these flags will raise a compile errors if the corresponding features are enabled, to help catch cases where they may inadvertently be reenabled by other crates. These flags are ignored in --all-features builds.

  • forbid-panic — Forbids the use of the panic feature.

  • forbid-write — Forbids the use of the write feature.

  • forbid-mut — Forbids the use of the mut feature.

  • forbid-serde — Forbids the use of features that require serde.

Re-exports

pub use ::toke;

Structs

Enums

Traits

A type representing a literal value in the source code that can be edited by litter. Using this type with a value that is not actually a literal in your source code may result in logic errors or panics.

Functions

assert_eqassertions