#settings #yaml #environment

spicex

A complete configuration solution for Rust applications, inspired by Viper

3 releases

0.1.2 Sep 14, 2025
0.1.1 Sep 10, 2025
0.1.0 Sep 9, 2025

#819 in Parser implementations

Download history 147/week @ 2025-09-04 219/week @ 2025-09-11 33/week @ 2025-09-18 8/week @ 2025-09-25 15/week @ 2025-10-02

98 downloads per month

MIT license

435KB
8K SLoC

SpiceX

A complete configuration solution for Rust applications, inspired by viper.

SpiceX is designed to work within an application and can handle all types of configuration needs and formats. It provides a unified interface for reading configuration from multiple sources with a clear precedence hierarchy.

Features

  • Multiple Configuration Sources - Files, environment variables, command line flags, defaults
  • Multiple File Formats - JSON, YAML, TOML, INI support
  • Precedence Hierarchy - Clear ordering of configuration sources
  • Nested Configuration - Dot notation access to nested values
  • Type Safety - Strong typing with automatic type conversion
  • Struct Deserialization - Deserialize configuration into Rust structs
  • File Watching - Automatic reloading when configuration files change
  • Environment Variables - Automatic mapping with prefix support
  • Command Line Flags - Integration with clap for CLI arguments
  • Default Values - Fallback values for missing configuration
  • Configuration Writing - Save configuration back to files

Quick Start

Add this to your Cargo.toml:

[dependencies]
spicex = "0.1.0"

# Optional: Enable CLI support
[dependencies.spicex]
version = "0.1.0"
features = ["cli"]

Basic Usage

use spicex::{Spice, ConfigValue};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut spice = Spice::new();

    // Set default values
    spice.set_default("database.host", ConfigValue::from("localhost"))?;
    spice.set_default("database.port", ConfigValue::from(5432i64))?;
    spice.set_default("debug", ConfigValue::from(false))?;

    // Configure file discovery
    spice.set_config_name("config");
    spice.add_config_path(".");
    spice.add_config_path("./configs");
    spice.add_config_path("/etc/myapp");

    // Try to read configuration file
    if let Err(e) = spice.read_in_config() {
        println!("No config file found, using defaults: {}", e);
    }

    // Set up environment variable support
    spice.set_env_prefix("MYAPP");
    spice.set_automatic_env(true);

    // Access configuration values
    let host = spice.get_string("database.host")?.unwrap_or_default();
    let port = spice.get_i64("database.port")?.unwrap_or(5432);
    let debug = spice.get_bool("debug")?.unwrap_or(false);

    println!("Database: {}:{}", host, port);
    println!("Debug mode: {}", debug);

    Ok(())
}

With Struct Deserialization

use spicex::{Spice, ConfigValue};
use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct DatabaseConfig {
    host: String,
    port: u16,
    #[serde(default)]
    ssl: bool,
}

#[derive(Deserialize, Debug)]
struct AppConfig {
    database: DatabaseConfig,
    debug: bool,
    #[serde(default)]
    log_level: String,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut spice = Spice::new();

    // Set defaults
    spice.set_default("database.host", ConfigValue::from("localhost"))?;
    spice.set_default("database.port", ConfigValue::from(5432i64))?;
    spice.set_default("debug", ConfigValue::from(false))?;
    spice.set_default("log_level", ConfigValue::from("info"))?;

    // Load configuration
    spice.set_config_name("config");
    spice.add_config_path(".");
    let _ = spice.read_in_config(); // Ignore errors, use defaults

    // Deserialize into struct
    let config: AppConfig = spice.unmarshal()?;
    println!("Configuration: {:#?}", config);

    Ok(())
}

Configuration Precedence

Spice uses the following precedence order (highest to lowest):

  1. Explicit calls - Values set via spice.set()
  2. Command line flags - CLI arguments (requires cli feature)
  3. Environment variables - System environment variables
  4. Configuration files - JSON, YAML, TOML, INI files
  5. Key/value stores - Remote configuration (future feature)
  6. Default values - Fallback values set via spice.set_default()

Configuration File Formats

JSON Example (config.json)

{
  "database": {
    "host": "localhost",
    "port": 5432,
    "ssl": true,
    "credentials": {
      "username": "admin",
      "password": "secret"
    }
  },
  "server": {
    "port": 8080,
    "host": "0.0.0.0"
  },
  "features": ["auth", "logging", "metrics"],
  "debug": false
}

YAML Example (config.yaml)

database:
  host: localhost
  port: 5432
  ssl: true
  credentials:
    username: admin
    password: secret

server:
  port: 8080
  host: 0.0.0.0

features:
  - auth
  - logging
  - metrics

debug: false

TOML Example (config.toml)

debug = false
features = ["auth", "logging", "metrics"]

[database]
host = "localhost"
port = 5432
ssl = true

[database.credentials]
username = "admin"
password = "secret"

[server]
port = 8080
host = "0.0.0.0"

INI Example (config.ini)

debug = false

[database]
host = localhost
port = 5432
ssl = true

[server]
port = 8080
host = 0.0.0.0

Environment Variables

Environment variables are automatically mapped to configuration keys:

# Set environment variables
export MYAPP_DATABASE_HOST=production-db
export MYAPP_DATABASE_PORT=5432
export MYAPP_DEBUG=true

# These become available as:
# database.host = "production-db"
# database.port = 5432
# debug = true
use spicex::Spice;

let mut spice = Spice::new();
spice.set_env_prefix("MYAPP");
spice.set_automatic_env(true);

// Access environment variables
let host = spice.get_string("database.host")?;
let debug = spice.get_bool("debug")?;

Command Line Flags

With the cli feature enabled, you can integrate with clap:

use spicex::Spice;
use clap::{Arg, Command};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let app = Command::new("myapp")
        .arg(Arg::new("host")
            .long("host")
            .value_name("HOST")
            .help("Database host"))
        .arg(Arg::new("port")
            .long("port")
            .value_name("PORT")
            .help("Database port"))
        .arg(Arg::new("debug")
            .long("debug")
            .action(clap::ArgAction::SetTrue)
            .help("Enable debug mode"));

    let matches = app.get_matches();

    let mut spice = Spice::new();
    spice.bind_flags(matches);

    // CLI flags now override other configuration sources
    let host = spice.get_string("host")?;
    let debug = spice.get_bool("debug")?;

    Ok(())
}

File Watching

Enable automatic reloading when configuration files change:

use spicex::Spice;
use std::sync::{Arc, Mutex};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut spice = Spice::new();
    spice.set_config_name("config");
    spice.read_in_config()?;

    // Enable file watching
    spice.watch_config()?;

    // Register callback for configuration changes
    let reload_count = Arc::new(Mutex::new(0));
    let count_clone = Arc::clone(&reload_count);

    spice.on_config_change(move || {
        let mut count = count_clone.lock().unwrap();
        *count += 1;
        println!("Configuration reloaded {} times", *count);
    })?;

    // Your application continues running...
    // Configuration will automatically reload when files change

    Ok(())
}

Writing Configuration

Save current configuration to files:

use spicex::{Spice, ConfigValue};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut spice = Spice::new();

    // Set some configuration
    spice.set("app.name", ConfigValue::from("My Application"))?;
    spice.set("app.version", ConfigValue::from("1.0.0"))?;
    spice.set("database.host", ConfigValue::from("localhost"))?;

    // Write to different formats
    spice.write_config("output.json")?;        // JSON format
    spice.write_config("output.yaml")?;        // YAML format
    spice.write_config_as("output.txt", "toml")?; // TOML in .txt file

    // Safe write (won't overwrite existing files)
    spice.safe_write_config("backup.json")?;

    Ok(())
}

Advanced Usage

Sub-configurations

Work with configuration subsections:

use spicex::{Spice, ConfigValue};
use std::collections::HashMap;

let mut spice = Spice::new();

// Set up nested configuration
let mut db_config = HashMap::new();
db_config.insert("host".to_string(), ConfigValue::from("localhost"));
db_config.insert("port".to_string(), ConfigValue::from(5432i64));
spice.set("database", ConfigValue::Object(db_config))?;

// Create sub-configuration for database settings
if let Some(db_viper) = spice.sub("database")? {
    // Access "host" directly instead of "database.host"
    let host = db_viper.get_string("host")?;
    let port = db_viper.get_i64("port")?;
}

Configuration Validation

Validate configuration during deserialization:

use spicex::{Spice, ConfigValue, ConfigError};
use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct ServerConfig {
    host: String,
    port: u16,
}

impl ServerConfig {
    fn validate(&self) -> Result<(), String> {
        if self.port < 1024 {
            return Err("Port must be >= 1024".to_string());
        }
        if self.host.is_empty() {
            return Err("Host cannot be empty".to_string());
        }
        Ok(())
    }
}

let mut spice = Spice::new();
spice.set("host", ConfigValue::from("localhost"))?;
spice.set("port", ConfigValue::from(8080i64))?;

let config: ServerConfig = spice.unmarshal_with_validation(|config: &ServerConfig| {
    config.validate().map_err(|e| ConfigError::invalid_value(e))
})?;

Error Handling

Spice provides detailed error information:

use spicex::{Spice, ConfigError};

let spice = Spice::new();

match spice.get_string("nonexistent.key") {
    Ok(Some(value)) => println!("Value: {}", value),
    Ok(None) => println!("Key not found"),
    Err(ConfigError::KeyNotFound { key }) => {
        println!("Key '{}' not found", key);
    }
    Err(ConfigError::TypeConversion { from, to }) => {
        println!("Cannot convert {} to {}", from, to);
    }
    Err(ConfigError::Parse { source_name, message }) => {
        println!("Parse error in {}: {}", source_name, message);
    }
    Err(e) => println!("Other error: {}", e),
}

Migration from Other Libraries

From config crate

// Old way (config crate)
use config::{Config, ConfigError, File};

let settings = Config::builder()
    .add_source(File::with_name("config"))
    .build()?;
let host: String = settings.get("database.host")?;

// New way (spice)
use spicex::Spice;

let mut spice = Spice::new();
spice.set_config_name("config");
spice.read_in_config()?;
let host = spice.get_string("database.host")?.unwrap_or_default();

From Environment Variables Only

// Old way (std::env)
use std::env;

let host = env::var("DATABASE_HOST").unwrap_or_else(|_| "localhost".to_string());
let port: u16 = env::var("DATABASE_PORT")
    .unwrap_or_else(|_| "5432".to_string())
    .parse()
    .unwrap_or(5432);

// New way (spice)
use spicex::{Spice, ConfigValue};

let mut spice = Spice::new();
spice.set_default("database.host", ConfigValue::from("localhost"))?;
spice.set_default("database.port", ConfigValue::from(5432i64))?;
spice.set_env_prefix("DATABASE");
spice.set_automatic_env(true);

let host = spice.get_string("host")?.unwrap_or_default();
let port = spice.get_i64("port")?.unwrap_or(5432) as u16;

Examples

The examples/ directory contains comprehensive examples:

Performance

Spice-rust is designed for performance:

  • Lazy Loading - Configuration sources are loaded on-demand
  • Caching - Values are cached after first access
  • Zero-Copy - Minimal allocations through strategic use of references
  • Efficient Parsing - Uses optimized parsers for each format

Run benchmarks with:

cargo run --example performance_benchmarks --release

Contributing

Contributions are welcome! Please see our Contributing Guide for details.

Development Setup

git clone https://github.com/myself659/spicex.git
cd spicex
cargo build
cargo test

Running Examples

# Basic usage
cargo run --example basic_usage

# With CLI support
cargo run --example cli_flag_usage --features cli -- --host localhost --port 8080

# File watching (requires a config file)
echo '{"debug": true}' > config.json
cargo run --example file_watching

License

at your option.

Acknowledgments

  • Inspired by viper for Go
  • Built with the excellent Rust ecosystem including serde, clap, notify, and more

Dependencies

~3–16MB
~147K SLoC