Snec

Crates.io Docs.rs Build Status

Configuration system with compile-time field lookup and modification notifications.

Overview

Snec is a configuration system focused on compile-time guarantees and a way of notifying a running system that a configurable value changed. Most of its power is implemented via macros, which is why those are exported by default.

While no built-in serialization support is provided, the architecture by itself is serialization-agnostic — using Serde and Snec for the same config table structure will work just fine.

Snec's architecture consists of those key components: - Config table — the structure which contains the configuration data for the program. Config tables implement the [Get] trait to access its fields, which allows them to hand out [Handle]s to its fields. Handles ensure that the assigned receiver gets notified when the field changes, unless it's explicitly prompted to perform a silent modification. - Entry — an uninhabited type (type with no possible values) implementing the [Entry] trait, representing an identifier for a field inside of a config table. - Receiver — type implementing the [Receiver] trait which will receive notifications whenever a entry in a config table it's interested in is modified.

Basic example

```rust use snec::{ConfigTable, Entry, GetExt as _}; use std::time::{SystemTime, Duration};

[derive(ConfigTable)]

struct MyConfigTable { #[snec] when: SystemTime, #[snec] who: String, #[snec] inwhichcountry: String, }

let mut configtable = MyConfigTable { when: SystemTime::UNIXEPOCH + Duration::fromsecs(566200800), who: "Jeremy".tostring(), inwhichcountry: "USA".tostring(), }; // To access the fields of our config table, we need to use the gethandle method from // the GetExt trait (which is a nicer way to use the Get trait). The entries part is // a module generated by the #[derive(ConfigTable)]. In most cases, it's desirable // to reexport the contents of the module in a public module with a different name and // some documentation, or simply in the containing module if you want the entry // identifiers to be in the same module as the config table. let mut handle = configtable.gethandle::(); // After we got the handle, we can use it to get a // mutable reference to the field and modify it: { let mut inwhichcountry = handle.modify(); *inwhichcountry = "Britain".tostring(); } // The reason why we put that in a scope and why we had to do this entire two-step process // is because otherwise we'd implicitly avoid notifying any receivers, which is something // that we'll look into in the next example. Since we don't have any, it won't really // hurt if we did this as well: { let inwhichcountry = handle.modifysilently(); *inwhichcountry = "Australia".to_string(); } Using receivers: rust use snec::{ConfigTable, Receiver, Entry, GetExt as _}; use std::time::{SystemTime, Duration};

[derive(ConfigTable)]

[snec(receiver = "MyReceiver::new -> MyReceiver")]

struct MyConfigTable { #[snec] whichyear: i64, #[snec(entry("snec::EmptyReceiver::new -> snec::EmptyReceiver"))] why: String, #[snec] randomintegerthati_like: u128, }

struct MyReceiver; impl MyReceiver { fn new() -> Self { // Since the #[derive(ConfigTable)] can only call functions (no arguments, return // value implements Receiver) to get a receiver, we have to do this. In a future // version, in-place closures might become supported. MyReceiver } } impl Receiver for MyReceiver { fn receive(&mut self, newvalue: &u128) { println!("My integer has been changed to {}!!", newvalue) } } impl Receiver for MyReceiver { fn receive(&mut self, newvalue: &i64) { println!("Resceduled to {}", newvalue) } }

let mut configtable = MyConfigTable { whichyear: 1987, why: "Accident".tostring(), randomintegerthatilike: 687800, }; // Now we have receivers which will immediately react to any changes in the values: let mut handle = configtable.gethandle::(); { let mut whichyear = handle.modify(); *whichyear = 1983; } // When the scope ends, the which_year guard is dropped and the receiver is informed. ```