Skip to content

Opening a struct your own way

Every struct with a place of its own implements two traits. Schema opens it exactly as declared: the fields read, their rules run, the place claimed. Open is how the rest of the program opens it, and unless the struct says otherwise it is Schema and nothing more. Code that opens structs it does not name asks for Open:

fn opened<S: Open>(store: &Store) -> S {
S::new_with(store)
}

open = manual is for a struct whose opening is more than its declaration - a value worked out from the machine it runs on, a check against something the application knows. The macro then writes no Open, and the struct’s author does, starting from Schema:

#[amethystate(prefix = "agent", open = manual)]
pub struct AgentSettings {
#[amestate(default = 0u32)]
pub workers: u32,
}
impl Open for AgentSettings {
fn try_new_with(store: &Store) -> Result<Self, OpenStruct> {
let machine = store.context().require::<Machine>();
let settings = <Self as Schema>::open(store)?;
if settings.workers().get() == 0 {
settings.workers().set(machine.cores);
}
Ok(settings)
}
}

The author writes try_new_with, the door that answers with a Result; new_with, which panics with what it answered, comes with the trait. What the struct needs comes from store.context(), the same values a declared rule is handed, and require panics naming a value nobody gave - a store built without it is built wrong the same way on every run.

A field’s rule sees one value and none of its siblings, and the struct is the place its fields are kept rather than a value with a rule of its own. So an invariant between fields goes here. The hand-written Open sees every field at once, after each has been through its own rule, and puts them right the way a rule would:

#[amethystate(prefix = "window", open = manual)]
pub struct Window {
#[amestate(default = 400u32)]
pub min: u32,
#[amestate(default = 1600u32)]
pub max: u32,
}
impl Open for Window {
fn try_new_with(store: &Store) -> Result<Self, OpenStruct> {
let window = <Self as Schema>::open(store)?;
if window.min().get() > window.max().get() {
window.max().set(window.min().get());
}
Ok(window)
}
}

It runs once, where the struct is opened. A write to min afterwards goes through min’s own rule and through nothing that knows about max; two fields written from two threads are a limit the Durability page sets out.

The hand-written Open is the only way in. new_with and try_new_with on the struct, load_with and try_load_with on a persistent one and AmeStateSlice all go through it, so no caller opens the struct past it by accident. That is also why the impl itself starts from <Self as Schema>::open and never from Self::try_new_with: the second is this very Open, and would call itself. And a struct that says open = manual has no new() or load() over the global store: reaching for the global store is a compile error rather than something a review has to catch.

open belongs to a struct with a place of its own. A nested struct is opened by the struct holding it, so the macro refuses open on one.