Scriptable Event Handler with State
Map Style
A runnable example of this implementation is included.
See the Examples section for details.
I Hate this
! How Can I Get Rid of It?
You’re using Rust and you don’t want people to think you’re writing lowly JavaScript?
Taking inspiration from the JS Style, a slight modification of the
Main Style is to store all states inside an object map inside a custom Scope
.
Nevertheless, instead of writing this.variable_name
everywhere to access a state variable
(in the JS Style), you’d write state.variable_name
instead.
It is up to you to decide whether this is an improvement!
Handler Initialization
Notice that a variable definition filter is used to prevent shadowing of the states object map.
Implementation wise, this style follows closely the Main Style, but a single
object map is added to the custom Scope
which holds all state values.
Global constants can still be added to the custom Scope
as normal and used through the script.
Calls to the init
function no longer need to avoid rewinding the Scope
because state
variables are added as properties under the states object map.
impl Handler {
// Create a new 'Handler'.
pub fn new(path: impl Into<PathBuf>) -> Self {
let mut engine = Engine::new();
// Forbid shadowing of 'state' variable
engine.on_def_var(|_, info, _| Ok(info.name != "state"));
:
// Code omitted
:
// Use an object map to hold state
let mut states = Map::new();
// Default states can be added
states.insert("bool_state".into(), Dynamic::FALSE);
// Add the main states-holding object map and call it 'state'
scope.push("state", states);
// Just a simple 'call_fn' can do here because we're rewinding the 'Scope'
// In a real application you'd again be handling errors...
engine.call_fn(&mut scope, &ast, "init", ()).unwrap();
:
// Code omitted
:
Self { engine, scope, ast }
}
}
Handler Scripting Style
The stored state is kept in an object map in the custom Scope
.
In this example, that object map is named state
, but it can be any name.
User-defined functions in state
Because an object map is used to hold state values, it is even possible to add user-defined functions, leveraging the OOP support for object maps.
However, within these user-defined functions, the this
pointer binds to the object map.
Therefore, the variable-accessing syntax is different from the main body of the script.
fn do_action() {
// Access state: `state.xxx`
state.number = 42;
// Add OOP functions - you still need to use `this`...
state.log = |x| print(`State = ${this.value}, data = ${x}`);
}
Sample script
/// Initialize user-provided state.
/// State is stored inside an object map bound to 'state'.
fn init() {
// Add 'bool_state' as new state variable if one does not exist
if "bool_state" !in state {
state.bool_state = false;
}
// Add 'obj_state' as new state variable (overwrites any existing)
state.obj_state = new_state(0);
// Can also add OOP-style functions!
state.log = |x| print(`State = ${this.obj_state.value}, data = ${x}`);
}
/// 'start' event handler
fn start(data) {
// Can detect system-provided default states!
// Access state variables in 'state'
if state.bool_state {
throw "Already started!";
}
// New values can be added to the state
state.start_mode = data;
if state.obj_state.func1() || state.obj_state.func2() {
throw "Conditions not yet ready to start!";
}
state.bool_state = true;
state.obj_state.value = data;
// Constant 'MY_CONSTANT' in custom scope is also visible!
print(`MY_CONSTANT = ${MY_CONSTANT}`);
}
/// 'end' event handler
fn end(data) {
if !state.bool_state || "start_mode" !in state {
throw "Not yet started!";
}
if !state.obj_state.func1() && !state.obj_state.func2() {
throw "Conditions not yet ready to end!";
}
state.bool_state = false;
state.obj_state.value = data;
}
/// 'update' event handler
fn update(data) {
state.obj_state.value += process(data);
// Call user-defined function OOP-style!
state.log(data);
}