config-easy 0.2.1

A small interactive settings menu for command-line Rust applications
Documentation
# config-easy

[![Crates.io](https://img.shields.io/crates/v/config-easy.svg)](https://crates.io/crates/config-easy)
[![Documentation](https://docs.rs/config-easy/badge.svg)](https://docs.rs/config-easy)
[![License](https://img.shields.io/crates/l/config-easy.svg)](https://gitlab.com/tw-libraries/rust/config-easy)
[![Rust Edition](https://img.shields.io/badge/edition-2024-blue.svg)](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