5 releases
Uses new Rust 2024
| new 0.6.0 | Nov 8, 2025 |
|---|---|
| 0.5.0 | Oct 9, 2025 |
| 0.5.0-beta2 | Sep 18, 2025 |
| 0.5.0-beta1 | Aug 29, 2025 |
| 0.5.0-alpha2 | Aug 25, 2025 |
#350 in Configuration
427 downloads per month
Used in pg-embed-setup-unpriv
175KB
3K
SLoC
OrthoConfig
OrthoConfig is a Rust configuration management library designed for
simplicity and power, inspired by the flexible configuration mechanisms found
in tools like esbuild. This enables an application to seamlessly load
configuration from command-line arguments, environment variables, and
configuration files, all with a clear order of precedence and minimal
boilerplate.
The core principle is orthographic option naming: a single field in a Rust
configuration struct can be set through idiomatic naming conventions from
various sources (e.g., --my-option for CLI, MY_APP_MY_OPTION for
environment variables, my_option in a TOML file) without requiring extensive
manual aliasing.
Core Features
- Layered Configuration: Sources configuration from multiple places with a
well-defined precedence:
- Command-Line Arguments (Highest)
- Environment Variables
- Configuration File (e.g.,
config.toml) - Application-Defined Defaults (Lowest)
- Orthographic Option Naming: Automatically maps diverse external naming conventions (kebab-case, UPPER_SNAKE_CASE, etc.) to a Rust struct's snake_case fields.
- Type-Safe Deserialization: Uses
serdeto deserialize configuration into strongly typed Rust structs. - Easy to Use: A simple
#[derive(OrthoConfig)]macro enables a quick start. - Customizable: Field-level attributes allow fine-grained control over naming, defaults, and merging behavior.
- Config discovery attributes: Use
#[ortho_config(discovery(...))]to rename the generated config override flag, adjust environment variables, and customise the filenames searched for configuration files without bespoke glue code. - Nested Configuration: Naturally supports nested structs for organized configuration.
- Sensible Defaults: Aims for intuitive behavior out-of-the-box.
Quick Start
- Add
OrthoConfigto the projectCargo.toml:
[dependencies]
ortho_config = "0.6.0" # Replace with the latest version
serde = { version = "1.0", features = ["derive"] }
ortho_config re-exports its parsing dependencies, so applications can import
figment, uncased, xdg (on Unix-like and Redox targets), and the optional
format parsers (figment_json5, json5, serde_saphyr, toml) without
declaring them directly.
- Define the configuration struct:
use ortho_config::{OrthoConfig, OrthoResult};
use serde::{Deserialize, Serialize}; // Required for OrthoConfig derive
#[derive(Debug, Clone, Deserialize, Serialize, OrthoConfig)]
#[ortho_config(prefix = "DB")] // Nested prefix: e.g., APP_DB_URL
struct DatabaseConfig {
// Automatically maps to:
// CLI: --database-url <value> (if clap flattens) or via file/env
// Env: APP_DB_URL=<value>
// File: [database] url = <value>
url: String,
#[ortho_config(default = 5)]
pool_size: Option<u32>, // Optional value, defaults to `Some(5)`
}
#[derive(Debug, Deserialize, Serialize, OrthoConfig)]
#[ortho_config(prefix = "APP")] // Prefix for environment variables (e.g., APP_LOG_LEVEL)
struct AppConfig {
log_level: String,
// Automatically maps to:
// CLI: --port <value>
// Env: APP_PORT=<value>
// File: port = <value>
#[ortho_config(default = 8080)]
port: u16,
#[ortho_config(merge_strategy = "append")] // Default for Vec<T> is append
features: Vec<String>,
// Nested configuration
database: DatabaseConfig,
#[ortho_config(cli_short = 'v')] // Enable a short flag: -v
verbose: bool, // Defaults to false if not specified
}
fn main() -> OrthoResult<()> {
let config = AppConfig::load()?; // Load configuration
println!("Loaded configuration: {:#?}", config);
if config.verbose {
println!("Verbose mode enabled!");
}
println!("Log level: {}", config.log_level);
println!("Listening on port: {}", config.port);
println!("Enabled features: {:?}", config.features);
println!("Database URL: {}", config.database.url);
println!("Database pool size: {:?}", config.database.pool_size);
Ok(())
}
- Running the application:
- With CLI arguments:
cargo run -- --log-level debug --port 3000 -v --features extra_cli_feature - With environment variables:
APP_LOG_LEVEL=warn APP_PORT=4000APP_DB_URL="postgres://localhost/mydb"APP_FEATURES="env_feat1,env_feat2" cargo run - With a
.app.tomlfile (assuming#[ortho_config(prefix = "APP_")]; adjust for the chosen prefix):
- With a
.app.tomlfile (assuming#[ortho_config(prefix = "APP_")]; adjust for your prefix):
# .app.toml
log_level = "file_level"
port = 5000
features = ["file_feat_a", "file_feat_b"]
[database]
url = "mysql://localhost/prod_db"
pool_size = 10
Configuration Sources and Precedence
OrthoConfig loads configuration from the following sources, with later sources overriding earlier ones:
- Application-Defined Defaults: Specified using
#[ortho_config(default =…)]orOption<T>fields (which default toNone). - Configuration File: Resolved in this order:
--config-pathCLI option (renameable through thediscovery(...)attribute)[PREFIX]CONFIG_PATHenvironment variable.<prefix>.tomlin the current directory.<prefix>.tomlin the user's home directory (where<prefix>comes from#[ortho_config(prefix = "…")]and defaults toconfig). JSON5 and YAML support are feature gated.
- Environment Variables: Variables prefixed with the string specified in
#[ortho_config(prefix = "...")](e.g.,APP_). Nested struct fields are typically accessed using double underscores (e.g.,APP_DATABASE__URLifprefix = "APP"onAppConfigand no prefix onDatabaseConfig, orAPP_DB_URLwith#onDatabaseConfig). - Command-Line Arguments: Parsed using
clapconventions. Long flags are derived from field names (e.g.,my_fieldbecomes--my-field).
File Format Support
TOML parsing is enabled by default. Enable the json5 and yaml features to
support additional formats:
[dependencies]
ortho_config = { version = "0.6.0", features = ["json5", "yaml"] }
When the yaml feature is enabled, configuration files are parsed with
serde-saphyr configured for YAML 1.2 semantics. Options::strict_booleans
keeps legacy literals such as yes or on as plain strings, and duplicate
mapping keys raise errors instead of being silently overwritten.
Error interop helpers
OrthoConfig includes small extensions to simplify error conversions:
OrthoResultExt::into_ortho()maps external errors intoOrthoResult<T>.OrthoMergeExt::into_ortho_merge()mapsfigment::ErrorintoOrthoError::MergewithinOrthoResult<T>.ResultIntoFigment::to_figment()convertsOrthoResult<T>intoResult<T, figment::Error>for integrations that prefer Figment’s type.
These keep examples and adapters concise while maintaining explicit semantics.
To return multiple failures at once, OrthoError::aggregate builds an
aggregate error from either owned or shared errors. When the collection might
be empty, OrthoError::try_aggregate returns Option<OrthoError>:
use ortho_config::OrthoError;
let agg = OrthoError::aggregate(vec![
OrthoError::validation("port", "must be positive"), // or explicit variant
OrthoError::gathering_arc(figment::Error::from("boom")),
]);
assert!(
OrthoError::try_aggregate(std::iter::empty::<OrthoError>()).is_none()
);
The file loader selects the parser based on the extension
(.toml, .json, .json5, .yaml, .yml). When the json5 feature is
active, both .json and .json5 files are parsed using the JSON5 format.
Standard JSON is valid JSON5, so existing .json files continue to work.
Without this feature enabled, attempting to load a .json or .json5 file
will result in an error. When the yaml feature is enabled, .yaml and .yml
files are also discovered and parsed. Without this feature, those extensions
are ignored during path discovery.
JSON5 extends JSON with conveniences such as comments, trailing commas, single-quoted strings, and unquoted keys.
Orthographic Naming
A key goal of OrthoConfig is to make configuration natural from any source. A
field like max_connections: u32 in a Rust struct will, by default, be
configurable via:
- CLI:
--max-connections <value> - Environment (assuming
#[ortho_config(prefix = "MYAPP")]):MYAPP_MAX_CONNECTIONS=<value> - TOML file:
max_connections = <value> - JSON5 file:
max_connectionsormaxConnections(configurable)
You can customize these mappings using #[ortho_config(…)] attributes.
Field Attributes #[ortho_config(…)]
Customize behaviour for each field:
#[ortho_config(default =…)]: Sets a default value. Can be a literal (e.g.,"debug",123,true) or a path to a function (e.g.,default = "my_default_fn").#[ortho_config(cli_long = "custom-name")]: Specifies a custom long CLI flag (e.g.,--custom-name).#[ortho_config(cli_short = 'c')]: Specifies a short CLI flag (e.g.,-c).#: Specifies a custom environment variable suffix (appended to the struct-level prefix).#[ortho_config(file_key = "customKey")]: Specifies a custom key name for configuration files.#[ortho_config(merge_strategy = "append")]: ForVec<T>fields, defines how values from different sources are combined. Defaults to"append".#[ortho_config(flatten)]: Similar toserde(flatten), useful for inlining fields from a nested struct into the parent's namespace for CLI or environment variables.
Subcommand Configuration
Applications using clap subcommands can keep per-command defaults in a
dedicated cmds namespace. The helper load_and_merge_subcommand_for or the
SubcmdConfigMerge trait reads these values from configuration files and
environment variables using the struct’s prefix() value. When no prefix is
set, environment variables use no prefix, whilst file discovery still defaults
to .config.toml. These values are then merged beneath the CLI arguments.
use clap::{Args, Parser};
use serde::Deserialize;
use ortho_config::OrthoConfig;
use ortho_config::SubcmdConfigMerge;
#[derive(Debug, Deserialize, Args, OrthoConfig)]
#[ortho_config(prefix = "APP_")]
pub struct AddUserArgs {
username: Option<String>,
admin: Option<bool>,
}
#[derive(Parser)]
struct Cli {
#[command(flatten)]
args: AddUserArgs,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let cli = Cli::parse();
// Reads `[cmds.add-user]` sections and `APP_CMDS_ADD_USER_*` variables
// then merges with CLI values
let args = cli.args.load_and_merge()?;
println!("Final args: {args:?}");
Ok(())
}
Configuration file example:
[cmds.add-user]
username = "file_user"
admin = true
Environment variables override file values using the pattern
<PREFIX>CMDS_<SUBCOMMAND>_:
APP_CMDS_ADD_USER_USERNAME=env_user
APP_CMDS_ADD_USER_ADMIN=false
Dispatching Subcommands
Subcommands can be executed with defaults applied using
clap-dispatch:
use clap::{Args, Parser};
use clap_dispatch::clap_dispatch;
use serde::Deserialize;
use ortho_config::{load_and_merge_subcommand_for, OrthoConfig};
#[derive(Debug, Deserialize, Args, OrthoConfig)]
#[ortho_config(prefix = "APP_")]
pub struct AddUserArgs {
username: Option<String>,
admin: Option<bool>,
}
#[derive(Debug, Deserialize, Args, OrthoConfig)]
pub struct ListItemsArgs {
category: Option<String>,
all: Option<bool>,
}
trait Run {
fn run(&self, db_url: &str) -> Result<(), String>;
}
impl Run for AddUserArgs { /* application logic here */ }
impl Run for ListItemsArgs { /* application logic here */ }
#[derive(Parser)]
#[command(name = "registry-ctl")]
#[clap_dispatch(fn run(self, db_url: &str) -> Result<(), String>)]
enum Commands {
AddUser(AddUserArgs),
ListItems(ListItemsArgs),
}
fn main() -> Result<(), String> {
let cli = Commands::parse();
let db_url = "postgres://user:pass@localhost/registry";
// merge per-command defaults
let cmd = match cli {
Commands::AddUser(args) => {
Commands::AddUser(load_and_merge_subcommand_for::<AddUserArgs>(&args)?)
}
Commands::ListItems(args) => {
Commands::ListItems(load_and_merge_subcommand_for::<ListItemsArgs>(&args)?)
}
};
cmd.run(db_url)
}
Why OrthoConfig?
- Reduced Boilerplate: Define the configuration schema once and let OrthoConfig handle multi-source loading and mapping.
- Developer Ergonomics: Intuitive mapping from external sources to Rust code.
- Flexibility: Users of the application can configure it in the way that best suits their environment (CLI for quick overrides, env vars for CI/CD, files for persistent settings).
- Clear Precedence: Predictable configuration resolution.
Migrating from 0.5 to 0.6
Version v0.6.0 streamlines dependency management, discovery, and YAML parsing. For a full walkthrough see the v0.6.0 migration guide; the highlights are:
- Update every
ortho_configandortho_config_macrosdependency to0.6.0. Feature flags now flow from the runtime crate to the macros, so you can drop duplicated feature declarations on the derive crate. - Use the crates re-exported via
ortho_config::figment(and friends) instead of keeping direct dependencies on Figment,uncased, orxdg. - Prefer the
#[ortho_config(discovery(...))]attribute to configure search paths declaratively and bubble up errors fromConfigDiscovery::load_first, which now returnsErrwhenever every candidate failed to load. - Switch to the new
SaphyrYamlprovider (behind the existingyamlfeature) wherever Figment's YAML provider was used to benefit from YAML 1.2 semantics and duplicate-key validation.
Migrating from 0.4 to 0.5
Version v0.5.0 introduces a small API refinement:
- In v0.5.0 the helper
load_subcommand_config_forwas removed. Useload_and_merge_subcommand_forto load defaults and merge them with CLI arguments. - Types deriving
OrthoConfigexpose an associatedprefix()function. Use this if you need the configured prefix directly.
Update the Cargo.toml to depend on ortho_config = "0.5.0" and adjust code
to call load_and_merge_subcommand_for instead of manually merging defaults.
Version management
- The
scripts/bump_version.pyhelper keeps the workspace and member crates in version sync. - It requires
uvon thePATHas the shebang usesuvfor dependency resolution. - Run it with the desired semantic version:
./scripts/bump_version.py 1.2.3
Publish checks
Run make publish-check before releasing to execute the lading publish
pre-flight validations with the repository's helper scripts on the PATH. The
target is parameterised via PUBLISH_CHECK_FLAGS, which defaults to
--allow-dirty for local development convenience. Continuous integration
should invoke make publish-check PUBLISH_CHECK_FLAGS= so the command runs
without the permissive flag.
Contributing
Contributions are welcome! Please feel free to submit issues, fork the repository, and send pull requests.
License
OrthoConfig is distributed under the terms of both the ISC license.
See LICENSE for details.
Dependencies
~5–18MB
~224K SLoC