OpenLogi Config TOML Validation and Recovery: A Deep Dive into Schema Integrity Mechanisms
OpenLogi enforces strict validation through deny_unknown_fields, schema version checks, and automatic migrations, with atomic saves and clear error propagation ensuring configuration integrity across upgrades.
The AprilNEA/OpenLogi repository implements a robust configuration system in Rust that balances strict schema enforcement with graceful recovery from older formats. Every TOML configuration file undergoes multiple validation layers before the application accepts it, while built-in migration paths preserve user data across schema evolution.
How Strict Validation Prevents Configuration Drift
OpenLogi's validation strategy operates at deserialization, version comparison, and structural levels to reject invalid configurations immediately.
Reject Unknown Fields with deny_unknown_fields
The Config struct in crates/openlogi-core/src/config.rs uses Serde's deny_unknown_fields attribute to eliminate silent configuration errors:
// From crates/openlogi-core/src/config.rs, lines 98-99
#[serde(deny_unknown_fields)]
pub struct Config {
// ...
}
This causes any TOML key not explicitly defined in the schema to trigger a deserialization error. Users receive immediate feedback about typos or deprecated options rather than having settings silently ignored.
Schema Version Enforcement
Every configuration carries a schema_version: u32 field validated against the current codebase's SCHEMA_VERSION constant (set to 7 as of the source analysis):
// From crates/openlogi-core/src/config.rs, lines 94-95
pub const SCHEMA_VERSION: u32 = 7;
The ConfigFile::load_from_path implementation applies a hard rule: newer versions are rejected, while older versions trigger automatic migration. A version of zero or any value exceeding SCHEMA_VERSION causes an explicit error, preventing data loss from version mismatches.
Automatic Recovery Through Schema Migrations
When ConfigFile::load_from_path encounters schema versions 1 through 6, it executes a pipeline of migration functions that transform legacy structures into the current format.
Migration Functions in config.rs
| Migration | Version Range | Purpose |
|---|---|---|
migrate_thumbwheel_native_direction |
Pre-v7 | Updates thumb-wheel defaults introduced in v7 (lines 24-33) |
migrate_transport_scoped_keys |
v4 | Rewrites direct: prefixed keys to stable identity keys (lines 38-57) |
migrate_owner_locked_gestures |
Pre-v4 | Resolves "owner-locked" gesture data structures (lines 59-86) |
Each migration runs in-place on the parsed TOML value before final struct deserialization. After successful migration, the file is rewritten atomically to persist the updated schema.
// Loading with automatic migration handling
let cfg = ConfigFile::load_from_path(
openlogi_core::paths::config_dir().join("config.toml")
)?;
// If the loaded file was version 4, migrate_transport_scoped_keys
// transforms it to version 7 before returning
println!("Loaded and migrated to version {}", cfg.schema_version);
Atomic Persistence and Ephemeral Mode
Atomic Save Operations
The configuration system guarantees durability through atomic file writes. The save_atomic implementation (in crates/openlogi-core/src/config/file.rs) ensures that partial writes never corrupt the on-disk state.
Ephemeral Configurations for Testing
The Config struct includes an internal ephemeral flag marked with #[serde(skip)], making it invisible to TOML serialization:
// From crates/openlogi-core/src/config.rs, lines 152-162
#[serde(skip)]
ephemeral: bool,
Creating an ephemeral configuration disables all disk writes:
// Ephemeral mode: all changes stay in memory
let mut test_cfg = Config::ephemeral();
test_cfg.set_device_custom_name("receiver:abc123:slot:2", Some("Test Device".into()));
test_cfg.save_atomic()?; // No-op: disk is never touched
This mechanism allows test suites to manipulate full configurations without risking developer data.
Error Propagation to the User Interface
When the GUI requests a configuration reload from the background agent, the IPC layer transforms low-level parsing failures into actionable diagnostics.
The ConfigReloadError type in crates/openlogi-ipc/src/ipc.rs (lines 72-79) carries both the file path and, when available, the precise TOML source location:
// Handling reload feedback in GUI code
match agent.reload_config().await {
Ok(_) => println!("Config reloaded successfully"),
Err(e) => eprintln!("Failed to reload config: {}", e),
// e.g.: "Failed to reload config: unknown field `gestre` at line 12 column 5"
}
This prevents the agent from entering undefined states and gives users exact line numbers for fixing syntax or schema violations.
Key Implementation Files
| File Path | Responsibility |
|---|---|
crates/openlogi-core/src/config.rs |
Config struct, schema version constant, migration functions |
crates/openlogi-core/src/config/file.rs |
ConfigFile trait with load_from_path and save_atomic |
crates/openlogi-ipc/src/ipc.rs |
ConfigReloadError for cross-process error reporting |
docs/config.example.toml |
Reference configuration following the validated schema |
Summary
deny_unknown_fieldsenforces schema exactness by rejecting unexpected TOML keys at parse time- Schema version checking blocks forward-incompatible files while triggering automatic migration for older versions
- Migration pipeline transforms legacy configurations through specialized functions for each version gap
- Atomic saves and ephemeral mode guarantee data durability and enable safe testing
- Rich error propagation surfaces precise failure locations to users through the IPC boundary
Frequently Asked Questions
What happens if I manually add an unknown key to config.toml?
OpenLogi will refuse to load the configuration. The deny_unknown_fields attribute causes Serde to return a deserialization error with the unknown key name, which propagates through the system as a clear error message pointing to the offending line.
Can I use a configuration from a newer OpenLogi version with an older binary?
No. The version check explicitly rejects configurations where schema_version exceeds the binary's SCHEMA_VERSION constant. This prevents silent data loss from settings that the older binary cannot interpret.
How does migration preserve my custom settings?
Migration functions transform only the structural elements that changed between versions, leaving user-defined values intact. For example, migrate_transport_scoped_keys rewrites key prefixes but preserves the associated gesture mappings. The migrated file is then atomically rewritten to disk with the updated schema_version.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →