# config-easy
[](https://crates.io/crates/config-easy)
[](https://docs.rs/config-easy)
[](https://gitlab.com/tw-libraries/rust/config-easy)
[](https://doc.rust-lang.org/edition-guide/rust-2024/)
`config-easy` is a small Rust crate for displaying and editing key/value settings in command-line applications.
The core crate is storage-agnostic. It can be used with any settings backend by implementing `SettingsStore`. A built-in `rusqlite` adapter is available behind the optional `rusqlite` feature.
Applications remain responsible for schema creation, default values, typed configuration structs, CLI parsing, and app-specific behavior.
## Install
For custom storage or to avoid database dependencies:
```toml
[dependencies]
config-easy = "0.2"
```
For the built-in SQLite adapter:
```toml
[dependencies]
config-easy = { version = "0.2", features = ["rusqlite"] }
rusqlite = "0.40"
```
Default features are empty, so `config-easy` does not pull in `rusqlite` unless requested.
## SQLite Usage
The SQLite adapter expects a table with key/value text columns. The default schema is:
```sql
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
```
Basic usage:
```rust
let connection = rusqlite::Connection::open("app.db")?;
config_easy::sqlite::run(&connection)?;
```
This uses:
- table: `settings`
- key column: `key`
- value column: `value`
- order by: `key`
Builder usage:
```rust
config_easy::sqlite::builder(&connection)
.table("settings")
.key_column("key")
.value_column("value")
.order_by("key")
.run()?;
```
Table and column names are validated as simple SQL identifiers before being interpolated into SQL. Values are always bound as SQL parameters.
Valid identifiers are non-empty, start with an ASCII letter or `_`, and contain only ASCII letters, numbers, and `_`.
## Secret Values
```rust
config_easy::sqlite::builder(&connection)
.secret_keys(["my_api_key"])
.run()?;
```
Non-empty secret values are displayed as `********`. Empty secret values are displayed as empty. The underlying value is not changed unless the user selects that setting and enters a new value.
## Validation
```rust
config_easy::sqlite::builder(&connection)
.validator(|key, value| {
if key == "log_level" && !["debug", "info", "warn", "error"].contains(&value) {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"expected one of debug, info, warn, error",
)
.into());
}
Ok(())
})
.run()?;
```
Validation runs before a setting is updated.
Input values are trimmed before validation and saving.
## Custom Actions
Custom actions are closures with no arguments. Capture app state directly when needed:
```rust
config_easy::sqlite::builder(&connection)
.action("r", "reset delta link", || {
connection.execute("DELETE FROM sync_state WHERE key = 'delta_link'", [])?;
println!("Delta link reset.");
Ok(())
})
.run()?;
```
Custom actions are displayed in the prompt and run when their key is selected. Action keys cannot be empty, numeric, duplicated, `q`, or `quit`.
## Avoiding rusqlite Conflicts
If your application already uses a different `rusqlite` version, do not enable the `rusqlite` feature. Implement `SettingsStore` in your application and use the generic builder:
```rust
use config_easy::{SettingRow, SettingsQuery, SettingsStore};
struct AppSettingsStore<'a> {
conn: &'a rusqlite::Connection,
}
impl SettingsStore for AppSettingsStore<'_> {
fn load_settings(
&self,
query: &SettingsQuery<'_>,
) -> Result<Vec<SettingRow>, Box<dyn std::error::Error + Send + Sync>> {
let sql = format!(
"SELECT {}, {} FROM {} ORDER BY {}",
query.key_column, query.value_column, query.table, query.order_by
);
let mut statement = self.conn.prepare(&sql)?;
let rows = statement.query_map([], |row| {
Ok(SettingRow {
key: row.get(0)?,
value: row.get(1)?,
})
})?;
Ok(rows.collect::<Result<Vec<_>, _>>()?)
}
fn update_setting(
&self,
query: &SettingsQuery<'_>,
key: &str,
value: &str,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let sql = format!(
"UPDATE {} SET {} = ?1 WHERE {} = ?2",
query.table, query.value_column, query.key_column
);
self.conn.execute(&sql, rusqlite::params![value, key])?;
Ok(())
}
}
config_easy::builder(AppSettingsStore { conn: &connection }).run()?;
```
When writing your own SQL-backed store, validate any table or column names before interpolating them into SQL. Bind setting values as parameters.
## Examples
- `examples/custom_store.rs` shows the generic `SettingsStore` API without any database dependency.
- `examples/sqlite_basic.rs` shows the built-in SQLite adapter with the default table layout.
- `examples/sqlite_builder.rs` shows custom table names, secret keys, validation, and custom actions with SQLite.
Run examples with:
```sh
cargo run --example custom_store
cargo run --example sqlite_basic --features rusqlite
cargo run --example sqlite_builder --features rusqlite
```
The example binaries are compile-focused and do not open the interactive menu.
## Migrating from 0.1 to 0.2
`0.2` moves SQLite support behind an optional feature and makes the core menu storage-agnostic.
Update dependencies:
```toml
config-easy = { version = "0.2", features = ["rusqlite"] }
rusqlite = "0.40"
```
Update function paths:
```rust
// 0.1
config_easy::run(&connection)?;
config_easy::builder(&connection).run()?;
// 0.2
config_easy::sqlite::run(&connection)?;
config_easy::sqlite::builder(&connection).run()?;
```
Update custom actions:
```rust
// 0.1
config_easy::builder(&connection)
.action("r", "reset", |conn| {
conn.execute("DELETE FROM sync_state", [])?;
Ok(())
});
// 0.2
config_easy::sqlite::builder(&connection)
.action("r", "reset", || {
connection.execute("DELETE FROM sync_state", [])?;
Ok(())
});
```
## Deliberately Not Included
- Creating the settings table
- Seeding default settings
- Parsing values into app-specific types
- CLI argument parsing
- Encryption or secure secret storage
- App-specific maintenance logic