How OpenLogi Manages TOML Config Schema Versioning for Safe Migration
OpenLogi stores user preferences in a single TOML file and uses a numeric SCHEMA_VERSION constant combined with targeted migration functions to safely evolve the configuration format without data loss.
OpenLogi, an open-source input device manager written in Rust, persists all user settings in a central config.toml file. To ensure this file remains compatible across software updates, the project implements a robust TOML config schema versioning system that automatically migrates legacy formats while preventing data corruption from concurrent edits or obsolete fields.
Version Tracking with SCHEMA_VERSION
In crates/openlogi-core/src/config.rs, OpenLogi defines a constant that represents the current schema revision:
const SCHEMA_VERSION: u32 = 7;
This constant (currently set to 7 at line 94) is incremented whenever the persisted data shape or enum vocabulary changes. When the application loads a configuration file, it compares the file's recorded version against this constant to determine if migration is required.
Loading Pipeline and Version Detection
The entry point for configuration loading is Config::load_from_path. Before parsing the full configuration structure, the loader extracts a minimal ConfigHeader that contains the schema_version field stored in the TOML file.
If the header's version is older than SCHEMA_VERSION, the system invokes a series of targeted migration functions inside parse_config (lines 47-84 of crates/openlogi-core/src/config/file.rs). Each migration runs sequentially, transforming the in-memory representation step-by-step until it matches the latest schema.
Targeted Migration Functions
OpenLogi uses discrete migration functions that trigger based on specific version thresholds. These functions live in the loading pipeline and rewrite only the affected portions of the configuration:
- migrate_owner_locked_gestures: Triggered when
schema_version≤ 3. Rewrites the obsolete "gesture-owner" model into the modern per-button gesture map. - migrate_transport_scoped_keys: Triggered when
schema_version≤ 4. Renames transport-scopeddirect:keys to stable identity fragments while preserving route links. - migrate_thumbwheel_native_direction: Triggered when
schema_version≤ 6. Adjusts pre-v7 thumb-wheel defaults to the new normalized direction. - repair_duplicate_routes: Runs for all versions. Normalizes duplicate route entries after the preceding migration passes.
After all applicable migrations complete, the in-memory Config struct has its schema_version field updated to match SCHEMA_VERSION, ensuring the next save operation writes the file in the latest format.
Obsolete Field Protection
To prevent newer code from silently accepting outdated configuration keys, OpenLogi implements reject_obsolete_fields (lines 86-110 of crates/openlogi-core/src/config/file.rs). This function scans the raw TOML document for keys that were removed after specific schema versions, such as button_bindings, gesture_bindings, and gesture_owner. If any obsolete keys appear in a file claiming a newer schema version, the loader aborts with a descriptive error rather than ignoring the invalid data.
Conflict-Aware Saving and Atomic Backup
Writing configuration changes safely requires preventing simultaneous modification conflicts. The ConfigFile::save method (implemented in crates/openlogi-core/src/config/file.rs) implements this protection through several mechanisms:
Conflict Detection (lines 53-69): Before writing, the implementation compares the current on-disk contents with the snapshot captured when the file was loaded (self.source). If they differ, a conflict error is returned, forcing the caller to reload and preventing accidental overwrites of concurrent edits.
Atomic Writes with Backup (lines 71-90): When a migration occurs, migration_backup_path creates a single backup of the original version before the new content is written atomically. This guarantees users can recover the pre-migration file if the process is interrupted by a crash or power loss.
Practical Implementation Examples
The following Rust code demonstrates loading a configuration file with automatic migration:
use openlogi_core::config::{Config, ConfigFile};
// Load the config, automatically migrating older versions.
let cfg = Config::load_from_path(&path)?;
// Returns a Config with schema_version == SCHEMA_VERSION (7)
To save changes while ensuring no external modifications occurred:
// Load the file wrapper which tracks the source state
let mut file = ConfigFile::load_from_path(&path)?;
// ... modify cfg ...
file.save(&cfg)?; // Returns error if file changed on disk
For testing specific migrations manually:
let (mut cfg, mut file) = ConfigFile::load_from_path(&path)?;
// Manually trigger specific migration for v6 -> v7
cfg.migrate_thumbwheel_native_direction();
file.save(&cfg)?;
Summary
OpenLogi's approach to TOML config schema versioning provides a reliable foundation for long-term data integrity:
- Explicit version constants (
SCHEMA_VERSION) make schema changes discoverable and auditable. - Header-based detection enables early validation before full parsing.
- Targeted migration functions isolate transformation logic per schema revision.
- Obsolete field rejection prevents silent acceptance of invalid legacy data.
- Conflict-aware saving detects concurrent modifications to prevent data loss.
- Atomic backup strategy preserves original files during migration for disaster recovery.
Frequently Asked Questions
What happens if OpenLogi encounters a config file newer than the current SCHEMA_VERSION?
If the schema_version in the file header exceeds SCHEMA_VERSION, the loader treats this as an error condition. Since the application cannot safely parse formats it does not understand, it aborts the loading process and returns a descriptive error indicating that the configuration was created by a newer version of OpenLogi.
How does OpenLogi prevent data loss when multiple processes edit the config simultaneously?
The ConfigFile::save method implements optimistic concurrency control by comparing the current disk contents against the snapshot stored in self.source when the file was first loaded. If any differences are detected, the save operation returns a conflict error rather than overwriting the changes, forcing the caller to reload the updated configuration before attempting to save again.
Can users recover their original configuration if a migration corrupts the data?
Yes. When a migration is applied, OpenLogi creates a backup using migration_backup_path before writing the new format atomically. This backup preserves the pre-migration state exactly as it existed on disk, allowing users to manually restore the original file if the migrated version encounters issues or contains unexpected transformations.
Where are the migration functions located in the OpenLogi source code?
The migration orchestration and loading logic reside in crates/openlogi-core/src/config/file.rs, specifically within the parse_config function (lines 47-84). The individual migration methods such as migrate_owner_locked_gestures and migrate_thumbwheel_native_direction are defined on the Config struct in crates/openlogi-core/src/config.rs, while the version constant SCHEMA_VERSION is declared at line 94 of the same file.
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 →