recallable 0.2.0

Traits (`Recallable`, `Recall`, `TryRecall`) and macros for defining Memento pattern types and their state restoration behaviors.
Documentation

Recallable

CI Crates.io Documentation License: MIT License: Apache-2.0

recallable is a no_std-friendly crate for Memento-pattern state recovery in Rust. It gives a type a companion memento type and a way to apply that memento to an already initialized value.

This is useful when your runtime struct contains a mix of:

  • durable state that should be restored or updated
  • runtime-only fields that must stay alive across recall

Typical examples are caches, handles, closures, connection ids, metrics state, or other non-persisted fields that should not be reconstructed from serialized input.

What It Provides

  • Recallable: declares a companion Memento type
  • Recall: applies that memento infallibly
  • TryRecall: applies it fallibly with validation
  • #[derive(Recallable)]: generates the companion memento type
  • #[derive(Recall)]: generates recall logic
  • #[recallable_model]: convenience attribute for the common model path; with serde enabled it also removes extra derive boilerplate

The crate intentionally does not force one universal memento shape for container-like field types. A field type can choose whole-value replacement, selective inner updates, zipped updates, or any other domain-specific behavior through its own Recallable::Memento and Recall::recall implementations.

Why Not Just Deserialize?

Normal Deserialize constructs a brand-new value. That is awkward when you already have a live runtime object with fields that should survive updates unchanged.

Recallable lets you deserialize or construct a memento and apply it to an existing value instead:

  • durable state changes
  • skipped runtime fields stay untouched
  • nested #[recallable] fields use their own recall behavior

Quickstart

With the default serde feature, the easiest path is #[recallable_model].

[dependencies]
recallable = "0.2"
serde_json = "1"
use recallable::{Recall, Recallable, recallable_model};

#[recallable_model]
#[derive(Clone, Debug, PartialEq, Eq)]
struct DashboardState {
    volume: u8,
    label: String,
    #[recallable(skip)]
    cache_key: String,
}

fn main() {
    let mut dashboard = DashboardState {
        volume: 10,
        label: "draft".to_string(),
        cache_key: "keep-me".to_string(),
    };

    let memento: <DashboardState as Recallable>::Memento =
        serde_json::from_str(r#"{"volume":75,"label":"live"}"#).unwrap();

    dashboard.recall(memento);

    assert_eq!(dashboard.volume, 75);
    assert_eq!(dashboard.label, "live");
    assert_eq!(dashboard.cache_key, "keep-me");
}

serde_json is used here because it is easy to read in documentation. For no_std + serde deployments, prefer a no_std-compatible format such as postcard.

#[recallable_model] injects:

  • #[derive(Recallable, Recall)]
  • #[derive(serde::Serialize)] when the default serde feature is enabled
  • #[serde(skip)] for fields marked #[recallable(skip)]

Features

  • serde (default): enables macro-generated serde support; generated mementos derive serde::Deserialize, and #[recallable_model] also adds the source-side serde behavior described above. This feature remains compatible with no_std as long as your serde stack is configured for no_std.
  • impl_from: generates From<Struct> for <Struct as Recallable>::Memento
  • full: convenience feature for serde + impl_from
  • default-features = false: disables recallable's default serde integration. It is useful for non-serde setups, but it is not what makes no_std possible.

Example dependency sets:

[dependencies]
# Readable std example
recallable = "0.1"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[dependencies]
# no_std + serde example
recallable = { version = "0.1", default-features = false, features = ["serde"] }
serde = { version = "1", default-features = false, features = ["derive"] }
postcard = { version = "1", default-features = false, features = ["heapless"] }
heapless = { version = "0.9.2", default-features = false }
[dependencies]
# In-memory snapshots
recallable = { version = "0.1", features = ["impl_from"] }

Two Common Workflows

Persistence and restore

This is the default path when state crosses process boundaries or is written to disk. It works in both std and no_std environments; only the serialization format and serde configuration differ.

  1. Serialize the source struct.
  2. Deserialize into <Type as Recallable>::Memento.
  3. Apply the memento with recall or try_recall.

This flow is especially convenient with #[recallable_model], because the source struct's serialized shape already matches the generated memento shape.

In-memory snapshots

Enable impl_from when you want an owned memento inside the same process for checkpoint/rollback, undo stacks, or test fixtures.

use recallable::{Recall, Recallable, recallable_model};

#[recallable_model]
#[derive(Clone, Debug, PartialEq)]
struct Counter {
    value: i32,
}

fn main() {
    let original = Counter { value: 42 };
    let memento: <Counter as Recallable>::Memento = original.clone().into();

    let mut target = Counter { value: 0 };
    target.recall(memento);

    assert_eq!(target, original);
}

Manual trait implementations

You do not need the macros to use the traits. Manual implementations work whether or not serde is enabled.

Disable default features only when you want recallable itself to stop enabling serde support by default:

[dependencies]
recallable = { version = "0.1", default-features = false }

Define the memento type and recall behavior manually:

use recallable::{Recall, Recallable};

struct EngineState {
    applied_ticks: u64,
    cached_checksum: u64,
}

struct EngineMemento {
    applied_ticks: u64,
}

impl Recallable for EngineState {
    type Memento = EngineMemento;
}

impl Recall for EngineState {
    fn recall(&mut self, memento: Self::Memento) {
        self.applied_ticks = memento.applied_ticks;
    }
}

Important Notes

  • #[recallable_model] must appear before the attributes it needs to inspect.
  • Direct #[derive(Recallable, Recall)] does not modify source serde behavior. If you need Serialize or #[serde(skip)], add them yourself.
  • There is intentionally no #[derive(TryRecall)]; fallible recall is where application-specific validation belongs.
  • Generated mementos are meant to be named through <Type as Recallable>::Memento.
  • Generated memento fields remain private.

Current Limitations

  • Derive macros currently support structs only: named, tuple, and unit structs
  • Borrowed state fields are rejected unless they are skipped
  • #[recallable] is path-only: it supports type parameters, path types, and associated types, but not tuple/reference/slice/function syntax directly
  • Serde attributes are not forwarded to the generated memento

Examples

Runnable examples live under recallable/examples/:

cargo run -p recallable --example basic_model
cargo run -p recallable --example nested_generic
cargo run -p recallable --example postcard_roundtrip
cargo run -p recallable --no-default-features --example manual_no_serde
cargo run -p recallable --no-default-features --features impl_from --example impl_from_roundtrip

More Documentation

License

Licensed under either MIT or Apache-2.0, at your option.