| Crates.io | ortho_config_macros |
| lib.rs | ortho_config_macros |
| version | 0.7.0 |
| created_at | 2025-08-25 23:19:08.489862+00 |
| updated_at | 2026-01-02 01:56:50.514221+00 |
| description | Procedural macros for ortho_config. |
| homepage | |
| repository | https://github.com/leynos/ortho-config |
| max_upload_size | |
| id | 1810284 |
| size | 205,204 |
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.
config.toml)serde to deserialize configuration into
strongly typed Rust structs.#[derive(OrthoConfig)] macro enables a quick
start.#[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.OrthoConfig to the project Cargo.toml:[dependencies]
ortho_config = "0.7.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.
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(())
}
cargo run -- --log-level debug --port 3000 -v --features extra_cli_featureAPP_LOG_LEVEL=warn APP_PORT=4000
APP_DB_URL="postgres://localhost/mydb"
APP_FEATURES="env_feat1,env_feat2" cargo run.app.toml file (assuming #[ortho_config(prefix = "APP_")];
adjust for the chosen prefix):.app.toml file (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
OrthoConfig loads configuration from the following sources, with later sources overriding earlier ones:
#[ortho_config(default =…)] or Option<T> fields (which default to
None).--config-path CLI option (renameable through the
discovery(...) attribute)[PREFIX]CONFIG_PATH environment variable.<prefix>.toml in the current directory.<prefix>.toml in the user's home directory
(where <prefix> comes from #[ortho_config(prefix = "…")] and defaults
to config). JSON5 and YAML support are feature gated.#[ortho_config(prefix = "...")] (e.g., APP_). Nested struct fields are
typically accessed using double underscores (e.g., APP_DATABASE__URL if
prefix = "APP" on AppConfig and no prefix on DatabaseConfig, or
APP_DB_URL with # on DatabaseConfig).clap conventions. Long flags are
derived from field names (e.g., my_field becomes --my-field).TOML parsing is enabled by default. Enable the json5 and yaml features to
support additional formats:
[dependencies]
ortho_config = { version = "0.7.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.
OrthoConfig includes small extensions to simplify error conversions:
OrthoResultExt::into_ortho() maps external errors into OrthoResult<T>.OrthoMergeExt::into_ortho_merge() maps figment::Error into
OrthoError::Merge within OrthoResult<T>.ResultIntoFigment::to_figment() converts OrthoResult<T> into
Result<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.
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:
--max-connections <value>#[ortho_config(prefix = "MYAPP")]):
MYAPP_MAX_CONNECTIONS=<value>max_connections = <value>max_connections or maxConnections (configurable)You can customize these mappings using #[ortho_config(…)] 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")]: For Vec<T> fields, defines how
values from different sources are combined. Defaults to "append".#[ortho_config(flatten)]: Similar to serde(flatten), useful for inlining
fields from a nested struct into the parent's namespace for CLI or
environment variables.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
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)
}
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:
ortho_config and ortho_config_macros dependency to 0.6.0.
Feature flags now flow from the runtime crate to the macros, so you can drop
duplicated feature declarations on the derive crate.ortho_config::figment (and friends) instead
of keeping direct dependencies on Figment, uncased, or xdg.#[ortho_config(discovery(...))] attribute to configure search
paths declaratively and bubble up errors from ConfigDiscovery::load_first,
which now returns Err whenever every candidate failed to load.SaphyrYaml provider (behind the existing yaml
feature) wherever Figment's YAML provider was used to benefit from YAML 1.2
semantics and duplicate-key validation.Version v0.5.0 introduces a small API refinement:
load_subcommand_config_for was removed. Use
load_and_merge_subcommand_for to load defaults
and merge them with CLI arguments.OrthoConfig expose an associated prefix() 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.
scripts/bump_version.py helper keeps the workspace and member crates in
version sync.uv on the PATH as the shebang
uses uv for dependency resolution../scripts/bump_version.py 1.2.3
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 now defaults to an
empty value, so the command enforces a clean working tree. Developers who want
the previous convenience may opt in explicitly:
PUBLISH_CHECK_FLAGS="--allow-dirty" make publish-check
Continuous integration should keep the strict default and call
make publish-check without additional flags.
Contributions are welcome! Please feel free to submit issues, fork the repository, and send pull requests.
OrthoConfig is distributed under the terms of both the ISC license.
See LICENSE for details.