Defining configuration structures

Version 0.6.0 Updated Nov 12, 2025

A configuration is represented by a plain Rust struct. To take advantage of OrthoConfig, derive the following traits:

  • serde::Deserialize and serde::Serialize – required for deserializing values and merging overrides.

  • The derive macro generates a hidden clap::Parser implementation, so manual clap annotations are not required in typical use. CLI customization is performed using ortho_config attributes such as cli_short, or cli_long.

  • OrthoConfig – provided by the library. This derive macro generates the code to load and merge configuration from multiple sources.

Optionally, the struct can include a #[ortho_config(prefix = "PREFIX")] attribute. The prefix sets a common string for environment variables and configuration file names. When the attribute omits a trailing underscore, ortho_config appends one automatically so environment variables consistently use <PREFIX>_. Trailing underscores are trimmed and the prefix is lower‑cased when used to form file names. For example, a prefix of APP results in environment variables like APP_PORT and file names such as .app.toml.

Field-level attributes

Field attributes modify how a field is sourced or merged:

Attribute Behaviour
default = expr Supplies a default value when no source provides one. The expression can be a literal or a function path.
cli_long = "name" Overrides the automatically generated long CLI flag (kebab-case).
cli_short = 'c' Adds a single-letter short flag for the field.
merge_strategy = "append" For Vec<T> fields, specifies that values from different sources should be concatenated. This is currently the only supported strategy and is the default for vector fields.

Unrecognized keys are ignored by the derive macro for forwards compatibility. Unknown keys will therefore silently do nothing. Developers who require stricter validation may add manual compile_error! guards.

Vector append buffers operate on raw JSON values, so element types only need to implement serde::Deserialize. Deriving serde::Serialize remains useful when applications serialize configuration back out (for example, to emit defaults), but it is no longer required merely to opt into the append strategy.

By default, each field receives a long flag derived from its name in kebab‑case and a short flag. The macro chooses the short flag using these rules:

  • Use the field's first ASCII alphanumeric character.
  • If that character is already taken or reserved, try its uppercase form.
  • If both are unavailable, no short flag is assigned; specify cli_short to resolve the collision.
Scenario Result
First letter free -p
Lowercase taken; uppercase free -P
Both cases taken none (set cli_short)
Explicit override via cli_short -r

Collisions are evaluated against short flags already assigned within the same parser, and reserved characters such as clap's -h and -V. A character is considered taken if it matches either set.

The macro does not scan other characters in the field name when deriving the short flag. Short flags must be single ASCII alphanumeric characters and may not use clap's global -h or -V options. Long flags must contain only ASCII alphanumeric characters or hyphens, must not start with -, cannot be named help or version, and the macro rejects underscores.

For example, when multiple fields begin with the same character, cli_short can disambiguate the final field:

#[derive(OrthoConfig)]
struct Options {
    port: u16,                         // -p
    path: String,                      // -P
    #[ortho_config(cli_short = 'r')]
    peer: String,                      // -r via override
}

Example configuration struct

The following example illustrates many of these features:

  use ortho_config::{OrthoConfig, OrthoError};
  use serde::{Deserialize, Serialize};

  #[derive(Debug, Clone, Deserialize, Serialize, OrthoConfig)]
  // env vars use APP_ (the macro adds the underscore automatically)
  #[ortho_config(prefix = "APP")]
  struct AppConfig {
      /// Logging verbosity
      log_level: String,

    /// Port to bind on – defaults to 8080 when unspecified
    #[ortho_config(default = 8080)]
    port: u16,

    /// Optional list of features. Values from files, environment and CLI are appended.
    #[ortho_config(merge_strategy = "append")]
    features: Vec<String>,

    /// Nested configuration for the database. A separate prefix is used to avoid ambiguity.
    #[serde(flatten)]
    database: DatabaseConfig,

    /// Enable verbose output; also available as -v via cli_short
    #[ortho_config(cli_short = 'v')]
    verbose: bool,
  }

#[derive(Debug, Clone, Deserialize, Serialize, OrthoConfig)]
#[ortho_config(prefix = "DB")]               // used in conjunction with APP_ prefix to form APP_DB_URL
struct DatabaseConfig {
    url: String,

    #[ortho_config(default = 5)]
    pool_size: Option<u32>,
}

fn main() -> Result<(), OrthoError> {
    // Parse CLI arguments and merge with defaults, file and environment
    let config = AppConfig::load()?;
    println!("Final config: {:#?}", config);
    Ok(())
}

clap attributes are not required in general; flags are derived from field names and ortho_config attributes. In this example, the AppConfig struct uses a prefix of APP. The DatabaseConfig struct declares a prefix DB, resulting in environment variables such as APP_DB_URL. The features field is a Vec<String> and accumulates values from multiple sources rather than overwriting them.

Customising configuration discovery

Configuration discovery can be tailored per struct using the discovery(...) attribute. The keys recognised today include:

  • app_name: directory name used under XDG and application data folders.
  • env_var: override for the environment variable consulted before discovery runs (defaults to <PREFIX>CONFIG_PATH).
  • config_file_name: primary filename searched in platform-specific configuration directories (defaults to config.toml).
  • dotfile_name: dotfile name consulted in the current working directory and the user's home directory.
  • project_file_name: filename searched within project roots (defaults to the dotfile name).
  • config_cli_long / config_cli_short: rename the CLI flag used to provide an explicit configuration path.
  • config_cli_visible: when true, the generated CLI flag appears in help output instead of remaining hidden.

Supplying only the keys you need lets you rename the CLI flag without altering file discovery, or vice versa. When the attribute is omitted, the defaults described in Config path override continue to apply.