Loadable Configuration


* A system where settings and configurations are complex and logic-driven.

* Where said system is too complex to configure via standard configuration file formats such as
  `JSON`, `TOML` or `YAML`.

* The system is complex enough to require a full programming language to configure.
  Essentially _configuration by code_.

* Yet the configuration must be flexible, late-bound and dynamically loadable, just like a
  configuration file.

* Leverage the loadable [modules] of Rhai.  The [`no_module`] feature must not be on.

* Expose the configuration API.  Use separate scripts to configure that API.
  Dynamically load scripts via the `import` statement.

* Leverage [function overloading] to simplify the API design.

* Since Rhai is _sand-boxed_, it cannot mutate the environment.  To modify the external
  configuration object via an API, it must be wrapped in a `RefCell` (or `RwLock`/`Mutex` for
  [`sync`]) and shared to the [`Engine`].

Implementation

Configuration type

#[derive(Debug, Clone, Default)]
struct Config {
    id: String,
    some_field: i64,
    some_list: Vec<String>,
    some_map: HashMap<String, bool>,
}

Make shared object

type SharedConfig = Rc<RefCell<Config>>;

let config = SharedConfig::default();

or in multi-threaded environments with the sync feature, use one of the following:

type SharedConfig = Arc<RwLock<Config>>;

type SharedConfig = Arc<Mutex<Config>>;

Register config API

The trick to building a Config API is to clone the shared configuration object and move it into each function registration via a closure.

Therefore, it is not possible to use a plugin module to achieve this, and each function must be registered one after another.

// Notice 'move' is used to move the shared configuration object into the closure.
let cfg = config.clone();
engine.register_fn("config_set_id", move |id: String| cfg.borrow_mut().id = id);

let cfg = config.clone();
engine.register_fn("config_get_id", move || cfg.borrow().id.clone());

let cfg = config.clone();
engine.register_fn("config_set", move |value: i64| cfg.borrow_mut().some_field = value);

// Remember Rhai functions can be overloaded when designing the API.

let cfg = config.clone();
engine.register_fn("config_add", move |value: String|
    cfg.borrow_mut().some_list.push(value)
);

let cfg = config.clone();
engine.register_fn("config_add", move |values: &mut Array|
    cfg.borrow_mut().some_list.extend(values.into_iter().map(|v| v.to_string()))
);

let cfg = config.clone();
engine.register_fn("config_add", move |key: String, value: bool|
    cfg.borrow_mut().some_map.insert(key, value)
);

let cfg = config.clone();
engine.register_fn("config_contains", move |value: String|
    cfg.borrow().some_list.contains(&value)
);

let cfg = config.clone();
engine.register_fn("config_is_set", move |value: String|
    cfg.borrow().some_map.get(&value).cloned().unwrap_or(false)
);

Configuration script

┌────────────────┐
│ my_config.rhai │
└────────────────┘

config_set_id("hello");

config_add("foo");          // add to list
config_add("bar", true);    // add to map

if config_contains("hey") || config_is_set("hey") {
    config_add("baz", false);   // add to map
}

Load the configuration

import "my_config";         // run configuration script without creating a module

let id = config_get_id();

id == "hello";

Consider a Custom Syntax

This is probably one of the few scenarios where a custom syntax can be recommended.

A properly-designed custom syntax can make the configuration file clean, simple to write, easy to understand and quick to modify.

For example, the above configuration example may be expressed by this custom syntax:

┌────────────────┐
│ my_config.rhai │
└────────────────┘

// Configure ID
id "hello";

// Add to list
list + "foo";

// Add to map
map "bar" => true;

if config contains "hey" || config is_set "hey" {
    map "baz" => false;
}

Notice that contains and is_set may also be implemented as a custom operator.