easy_prefs 3.0.0

The simplest to use API we could think of to persist prefs to disk. Basically wrap a macro around a struct (see syntax), then data is saved when you write to it. Performant, testable, thread safe, easy to migrate, and careful to not corrupt your data.
Documentation
# easy_prefs

A simple, safe, and performant preferences library for Rust applications that makes storing and retrieving settings as easy as reading and writing struct fields.

This macro-based library lets you define your preferences—including default values and custom storage keys—and persist them to disk using TOML. It emphasizes data safety by using atomic writes and enforces a single-instance rule to prevent race conditions.

**Now with WebAssembly support!** Use the same API in browser extensions, web apps, and native applications. When compiled to WASM, preferences are stored in localStorage instead of the file system.

*Created by Ever Accountable – an app dedicated to helping people overcome compulsive porn use and become their best selves. More info at [everaccountable.com](https://everaccountable.com).*

## Quick Start

### 1. Add Dependencies

In your `Cargo.toml`, add:

```toml
[dependencies]
easy_prefs = "3.0"  # Use the latest version
serde = { version = "1.0", features = ["derive"] }
```

*(The library re-exports `paste`, `toml`, and `once_cell` so you don’t need to add them separately.)*

### 2. Define Your Preferences

Create a preferences struct with default values and customizable storage keys:

```rust
use easy_prefs::easy_prefs;

easy_prefs! {
    pub struct AppPreferences {
        /// Boolean preference with default `true`, stored as "notifications"
        pub notifications: bool = true => "notifications",
        /// String preference with default "guest", stored as "username"
        pub username: String = "guest".to_string() => "username",
    },
    "app-preferences"  // This defines the filename (app-preferences.toml)
}
```

### 3. Load and Use Preferences

```rust
fn main() {
    // Load preferences - always succeeds by using defaults if needed
    // In debug builds: panics on errors to catch issues early  
    // In release builds: logs errors and returns defaults
    let mut prefs = AppPreferences::load("/path/to/config/dir");

    println!("Notifications: {}", prefs.get_notifications());

    // Update a value (this write is blocking).
    prefs.save_notifications(false).expect("Save failed");

    // Batch updates using an edit guard (auto-saves on drop).
    {
        let mut guard = prefs.edit();
        guard.set_notifications(true);
        guard.set_username("Abe Lincoln".to_string());
    }
}
```

## WebAssembly Support

easy_prefs works seamlessly in WebAssembly environments like Safari extensions and web applications. When compiled to WASM, it automatically uses localStorage instead of the file system.

### Enabling WASM Support

Easy_prefs automatically detects WASM targets, no special features needed:

```toml
[dependencies]
easy_prefs = "3.0"
```

### Building for WASM

```bash
cargo build --target wasm32-unknown-unknown --features wasm
```

### Usage in Safari Extensions

```rust
use easy_prefs::easy_prefs;
use wasm_bindgen::prelude::*;

easy_prefs! {
    pub struct ExtensionSettings {
        pub enabled: bool = true => "enabled",
        pub api_key: String = String::new() => "api_key",
    },
    "safari-extension"
}

#[wasm_bindgen]
pub fn init_extension() -> Result<(), JsValue> {
    // The "directory" parameter becomes the localStorage key prefix
    // Using load_with_error() for explicit error handling in WASM
    let mut settings = ExtensionSettings::load_with_error("com.mycompany.extension")
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    
    // Use the same API as native
    settings.save_enabled(true)
        .map_err(|e| JsValue::from_str(&e.to_string()))?;
    Ok(())
}
```

### Storage Locations

- **Native platforms**: Files stored in the specified directory (directory will be created if it doesn't exist)
- **WASM/Browser**: Data stored in localStorage with keys prefixed by your app ID (slashes and dots in the app ID are replaced with underscores)

## Detailed Information

### Error Handling

The library provides two methods for loading preferences:

- **`load()`** - Simple API that always succeeds:
  - In release builds: Returns defaults on errors (logs them)
  - In debug/test builds: Panics on errors to catch issues early
  - Always panics if another instance is already loaded
  - When returning defaults due to errors, storage is still properly configured for future saves
  - Use this for the simplest API where the app should continue even if preferences can't be loaded

- **`load_with_error()`** - Returns `Result<Self, LoadError>`:
  - For explicit error handling when needed
  - Returns `LoadError` enum with these variants:
    - **InstanceAlreadyLoaded:** Only one instance can be loaded at a time
    - **DeserializationError:** Errors while parsing TOML data (includes location info)
    - **StorageError:** General storage operation failures (wraps std::io::Error)

Example:
```rust
// Simple approach - always succeeds
let prefs = AppPreferences::load("./config");

// Explicit error handling
match AppPreferences::load_with_error("./config") {
    Ok(prefs) => { /* use prefs */ },
    Err(LoadError::InstanceAlreadyLoaded) => { /* handle conflict */ },
    Err(e) => { /* handle other errors */ }
}
```


### Use Across Threads

Use `Arc<Mutex<>>` to share the preferences struct between threads.
The single-instance constraint prevents loading the same preferences from multiple locations simultaneously - attempting to do so will panic (with `load()`) or return an error (with `load_with_error()`).

### Atomic Writes

To ensure data integrity, writes are atomic on all platforms:

**Native platforms:**
- Data is first written to a temporary file
- The temporary file is atomically renamed to the final file
- This ensures the preferences file is never left in a partially written state

**WASM/Browser environments:**
- localStorage provides atomic writes by specification
- The `setItem()` method either fully succeeds or leaves the old data untouched
- If the browser crashes or runs out of storage, your existing data remains intact

### Testing with `load_testing()`

For unit tests, use `load_testing()`, which:
- Creates a temporary file (cleaned up after the test).
- Bypasses the single-instance constraint, making testing simpler.

### Migration from Version 2.x

**Breaking Changes in Version 3.0:**

- `load_default()` has been removed. It bypassed the single-instance constraint which could lead to data corruption.
- `load()` now always succeeds instead of returning a `Result`. In release builds it returns defaults on errors, in debug builds it panics to catch issues early.
- For explicit error handling, use the new `load_with_error()` method which returns `Result<Self, LoadError>`.

**Migration Guide:**

```rust
// Old (v2.x):
let prefs = AppPreferences::load(dir).unwrap_or_else(|e| {
    log::error!("Failed to load: {}", e);
    AppPreferences::load_default(dir)
});

// New (v3.0) - Simple approach:
let prefs = AppPreferences::load(dir);  // Always succeeds

// New (v3.0) - With explicit error handling:
let prefs = match AppPreferences::load_with_error(dir) {
    Ok(p) => p,
    Err(e) => {
        log::error!("Failed to load: {}", e);
        return; // Handle error appropriately
    }
};
```

### Edit Guards and Debug Checks

When batching updates with an edit guard:
- A warning (active only in debug builds) ensures the guard isn’t held for more than 1 second to prevent blocking.
- This safety check helps catch long-held locks during development.

### Utility Methods

- **get_preferences_file_path():**  
  Returns the full path of the preferences file as a string, useful for debugging.

- **load():**  
  Loads preferences, always succeeding by using defaults if needed. Panics in debug mode on errors to catch issues early.

- **load_with_error():**  
  Loads preferences with explicit error handling, returning `Result<Self, LoadError>`.

- **load_testing():**  
  Creates a temporary instance for unit testing, bypassing the single-instance constraint.

### Customizable Storage Keys

The macro’s syntax (`=> "field_name"`) lets you define a stored key that differs from the struct field name. This is helpful when renaming fields or preserving legacy data formats.

### Dependencies & Serialization

The macro requires [Serde](https://serde.rs) for serialization/deserialization and re-exports helpful crates like `paste`, `toml`, `once_cell`, and `web_time` to manage lazy statics, code generation, and cross-platform time handling.

## Limitations

- **Not for Large Data:**  
  All data is kept in memory and the entire file is rewritten on every save. Use a full database if you need to handle large datasets.
- **Blocking Writes:**  
  File writes happen on the calling thread, so be mindful of performance in critical sections.

## License

MIT OR Apache-2.0

```
Copyright (c) 2023 Ever Accountable

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
  
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
  
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
```