A generic, framework-agnostic Rust library for managing application settings with backup/restore, sub-settings, and credential management.
Built with modern Rust best practices — Comprehensive test coverage (143 tests), CI-enforced quality gates (fmt, clippy, cargo-deny), and production-ready error handling.
| Feature | Description |
|---|---|
| Settings Management | Load/save with rich schema metadata for UI rendering |
| Sub-Settings | Per-entity configs (e.g., one JSON per remote) |
| Schema Migration | Lazy migration for transparent data upgrades |
| Backup & Restore | Encrypted ZIP backups with AES-256 |
| Secret Settings | Auto-routes secrets to OS keychain |
| External Configs | Include external files/commands in backups |
| Env Var Overrides | Override settings via environment variables (Docker/K8s) |
| Atomic Writes | Crash-safe file writes (temp file + rename) |
| Cross-Platform | Pure Rust - Windows, macOS, Linux, Android |
[dependencies]
rcman = "0.1"| Feature | Description | Default? |
|---|---|---|
json |
JSON storage | ✅ |
backup |
Backup/restore (zip) | ✅ |
derive |
#[derive(SettingsSchema)] macro |
❌ |
keychain |
OS keychain support | ❌ |
encrypted-file |
AES-256 encrypted file | ❌ |
full |
All features | ❌ |
Examples:
# Default (settings + backup)
rcman = "0.1"
# Minimal (just settings, no backup)
rcman = { version = "0.1", default-features = false, features = ["json"] }
# With OS keychain support
rcman = { version = "0.1", features = ["keychain"] }
# Everything
rcman = { version = "0.1", features = ["full"] }use rcman::{SettingsManager, SubSettingsConfig};
// Initialize with fluent builder API
let manager = SettingsManager::builder("my-app", "1.0.0")
.config_dir("~/.config/my-app")
.with_credentials() // Enable automatic secret storage
.with_env_prefix("MYAPP") // Enable env var overrides (MYAPP_UI_THEME=dark)
.with_sub_settings(SubSettingsConfig::new("remotes")) // Per-entity config
.with_migrator(|mut value| {
// Transparent schema upgrades (runs once on first load)
if let Some(obj) = value.as_object_mut() {
// Example: rename old field to new field
if let Some(ui) = obj.get_mut("ui").and_then(|v| v.as_object_mut()) {
if let Some(color) = ui.remove("color") {
ui.insert("theme".to_string(), color);
}
}
}
value
})
.build()?;Define settings using the clean builder API:
use rcman::{settings, SettingsSchema, SettingMetadata, opt};
#[derive(Default, Serialize, Deserialize)]
struct AppSettings {
dark_mode: bool,
language: String,
api_key: String,
}
impl SettingsSchema for AppSettings {
fn get_metadata() -> HashMap<String, SettingMetadata> {
settings! {
// Toggle setting
"ui.dark_mode" => SettingMetadata::toggle("Dark Mode", false)
.category("appearance")
.order(1),
// Select with options
"ui.language" => SettingMetadata::select("Language", "en", vec![
opt("en", "English"),
opt("tr", "Turkish"),
opt("de", "German"),
]),
// Number with range
"ui.font_size" => SettingMetadata::number("Font Size", 14.0)
.min(8.0).max(32.0).step(1.0),
// Secret (auto-stored in keychain!)
"api.key" => SettingMetadata::password("API Key", "")
.secret(),
// List of strings
"network.allowed_ips" => SettingMetadata::list("Allowed IPs", vec!["127.0.0.1".to_string()])
.description("IP addresses allowed to connect")
.category("network"),
}
}
}| Constructor | Description |
|---|---|
text(label, default) |
Text input |
password(label, default) |
Password input |
number(label, default) |
Number input |
toggle(label, default) |
Boolean toggle |
select(label, default, options) |
Dropdown |
color(label, default) |
Color picker |
path(label, default) |
Directory path |
file(label, default) |
File path |
list(label, default) |
List of strings |
info(label, default) |
Read-only display |
.description() .min() .max() .step() .placeholder() .category() .order() .requires_restart() .advanced() .disabled() .secret() .pattern() .pattern_error()
Instead of implementing SettingsSchema manually, use the derive macro:
rcman = { version = "0.1", features = ["derive"] }use rcman::DeriveSettingsSchema;
use serde::{Deserialize, Serialize};
#[derive(Default, Serialize, Deserialize, DeriveSettingsSchema)]
#[schema(category = "general")]
struct GeneralSettings {
#[setting(label = "Enable Tray", description = "Show tray icon")]
tray_enabled: bool,
#[setting(label = "Port", min = 1024, max = 65535)]
port: u16,
#[setting(label = "Theme", options(("light", "Light"), ("dark", "Dark")))]
theme: String,
}Available field attributes:
label,description,categorymin,max,step(for numbers)options((...))(for selects)secret,advanced,requires_restart,skip
Per-entity configuration files (e.g., one config per "remote"):
use rcman::{SettingsManager, SubSettingsConfig};
use serde_json::json;
// Register sub-settings via builder
let manager = SettingsManager::builder("my-app", "1.0.0")
.with_sub_settings(SubSettingsConfig::new("remotes")) // Multi-file mode
.with_sub_settings(SubSettingsConfig::new("backends").single_file()) // Single-file mode
.build()?;
// Access sub-settings
let remotes = manager.sub_settings("remotes")?;
// CRUD operations
remotes.set("gdrive", &json!({"type": "drive"}))?;
let gdrive_config = remotes.get::<serde_json::Value>("gdrive")?;
let all_remotes = remotes.list()?;
remotes.delete("onedrive")?;Storage Modes:
| Mode | Files Created | Use Case |
|---|---|---|
| Multi-file (default) | remotes/gdrive.json, remotes/s3.json |
Large configs, many entities |
| Single-file | backends.json |
Small collections, simpler file structure |
Automatically upgrade old data formats when loading settings:
use rcman::{SettingsManager, SubSettingsConfig};
use serde_json::json;
// Main settings migration
let manager = SettingsManager::builder("my-app", "2.0.0")
.with_migrator(|mut value| {
// Upgrade v1 -> v2: rename "color" to "theme"
if let Some(obj) = value.as_object_mut() {
if let Some(ui) = obj.get_mut("ui").and_then(|v| v.as_object_mut()) {
if let Some(color) = ui.remove("color") {
ui.insert("theme".to_string(), color);
}
}
}
value
})
.build()?;
// Sub-settings migration (per-entry for multi-file mode)
let remotes_config = SubSettingsConfig::new("remotes")
.with_migrator(|mut value| {
// Add version field to each remote
if let Some(obj) = value.as_object_mut() {
if !obj.contains_key("version") {
obj.insert("version".into(), json!(2));
}
}
value
});
// Sub-settings migration (whole-file for single-file mode)
let backends_config = SubSettingsConfig::new("backends")
.single_file()
.with_migrator(|mut value| {
// Migrate all backends at once
if let Some(obj) = value.as_object_mut() {
for (_name, backend) in obj.iter_mut() {
if let Some(b) = backend.as_object_mut() {
b.insert("migrated".into(), json!(true));
}
}
}
value
});How it works:
- Migrator runs automatically on first load after app update
- If data changes, it's immediately written back to disk
- Subsequent loads skip migration (no performance impact)
- Multi-file mode: Migrator runs per-entry (each remote.json)
- Single-file mode: Migrator runs on whole file (all entries at once)
Settings marked with .secret() are automatically stored in the OS keychain:
// In schema
"api.key" => SettingMetadata::password("API Key", "")
.secret(),
// Usage - automatically routes to keychain!
manager.save_setting::<MySettings>("api", "key", json!("sk-123"))?;
// → Stored in OS keychain, NOT in settings.jsonBackends:
- macOS: Keychain
- Windows: Credential Manager
- Linux: Secret Service (via libsecret)
- Fallback: Encrypted file with Argon2id + AES-256-GCM
Create, analyze, and restore encrypted backups using the builder pattern:
use rcman::{BackupOptions, RestoreOptions};
// Create full backup with builder pattern
let backup_path = manager.backup()
.create(BackupOptions::new()
.output_dir("./backups")
.password("backup_password")
.note("Weekly backup")
.filename_suffix("full")) // Custom filename: app_timestamp_full.rcman
?;
// Create partial backup (only specific sub-settings)
let remotes_backup = manager.backup()
.create(BackupOptions::new()
.output_dir("./backups")
.export_type(ExportType::SettingsOnly)
.include_settings(false) // Don't include main settings
.include_sub_settings("remotes") // Only backup remotes
.filename_suffix("remotes")) // Creates: app_timestamp_remotes.rcman
?;
// Analyze a backup before restoring (inspect contents, check encryption)
let analysis = manager.backup().analyze(&backup_path)?;
println!("Encrypted: {}", analysis.requires_password);
println!("Valid: {}", analysis.is_valid);
println!("Created by app v{}", analysis.manifest.app_version);
if !analysis.warnings.is_empty() {
println!("Warnings: {:?}", analysis.warnings);
}
// Restore with builder pattern
manager.backup()
.restore(RestoreOptions::from_path(&backup_path)
.password("backup_password")
.overwrite(true))
?;When you save a setting that equals its default, rcman removes it from storage:
- Regular settings: Removed from JSON file
- Secret settings: Removed from keychain
This keeps files minimal and allows changing defaults in code to auto-apply to users.
# Save non-default value (stored)
manager.save_setting::<S>("ui", "theme", json!("dark"))?;
// Save default value (removed from storage)
manager.save_setting::<S>("ui", "theme", json!("light"))?; // "light" is default
// Or use reset_setting() to explicitly reset
manager.reset_setting::<S>("ui", "theme")?;Override settings via environment variables for Docker/Kubernetes deployments:
// Enable with prefix
let config = SettingsConfig::builder("my-app", "1.0.0")
.with_env_prefix("MYAPP")
.build();Format: {PREFIX}_{CATEGORY}_{KEY} (all uppercase)
| Setting Key | Environment Variable |
|---|---|
ui.theme |
MYAPP_UI_THEME=dark |
core.port |
MYAPP_CORE_PORT=9090 |
general.debug |
MYAPP_GENERAL_DEBUG=true |
Priority: Env Var > Stored Value > Default
Type Parsing:
true/false→ boolean- Numbers → i64/f64
- JSON → parsed as JSON
- Everything else → string
UI Detection:
let settings = manager.load_settings::<MySettings>()?;
for (key, meta) in settings {
if meta.env_override {
println!("🔒 {} is overridden by env var", key);
}
}Note: Secret settings (stored in keychain) are NOT affected by env var overrides by default. To enable, use
.env_overrides_secrets(true):SettingsConfig::builder("my-app", "1.0.0") .with_env_prefix("MYAPP") .env_overrides_secrets(true) // Allow MYAPP_API_KEY to override keychain .build()
rcman/
├── config/
│ ├── types.rs # SettingsConfig + Builder
│ └── schema.rs # SettingMetadata + Builder, settings! macro
├── credentials/
│ ├── keychain.rs # OS keychain backend
│ ├── encrypted.rs # AES-256-GCM file backend
│ └── memory.rs # Testing backend
├── backup/
│ ├── operations.rs # Create backups
│ ├── restore.rs # Restore backups
│ └── archive.rs # Zip utilities
├── manager.rs # Main SettingsManager
├── storage.rs # StorageBackend trait
└── sub_settings.rs # Per-entity configs
rcman is designed for efficiency:
- In-Memory Caching: Settings are cached after first load, eliminating redundant disk I/O
- Defaults Cache: Default values are cached to avoid repeated schema lookups
- Sync I/O: Simple, blocking file operations using
std::fs(no runtime overhead) - Smart Writes: Only writes to disk when values actually change
- Zero-Copy Reads: Uses
RwLockfor concurrent read access without cloning
Benchmarks (typical desktop app with 50 settings):
- First load: ~2ms (disk read + parse)
- Cached load: ~50μs (memory access)
- Save setting: ~1-3ms (validation + disk write)
All operations return typed errors:
use rcman::{Error, Result};
match manager.save_setting::<MySettings>("ui", "theme", json!("dark")) {
Ok(()) => println!("Saved!"),
Err(Error::InvalidSettingValue { reason, .. }) => println!("Invalid: {}", reason),
Err(e) => println!("Error: {}", e),
}This project follows modern Rust library best practices. See CONTRIBUTING.md for development guidelines.
# Run all checks (CI equivalent)
just fmt clippy test deny
# Individual commands
just fmt # Format code
just clippy # Run linter
just test # Run tests
just docs # Build docs
just deny # Check dependencies- MSRV: Rust 1.70+
- Code Quality:
clippy -D warningsenforced in CI - Test Coverage: 143 tests (120 passing + 13 performance + 10 environment)
- Documentation: Comprehensive doctests and API docs
- Dependencies: Audited via
cargo-deny(licenses, advisories, duplicates)
git config core.hooksPath .githooks
chmod +x .githooks/pre-commitMIT