Application names do not need to bend around OrthoConfig's defaults. Put the discovery contract beside the struct when the public flag or filenames are part of the CLI design:
use ortho_config::{OrthoConfig, OrthoResult};
use serde::{Deserialize, Serialize};
#[derive(Debug, Deserialize, Serialize, OrthoConfig)]
#[ortho_config(
prefix = "ACME_",
discovery(
app_name = "acme-server",
config_file_name = "server.toml",
dotfile_name = ".acme-server.toml",
project_file_name = ".acme-server.toml",
config_cli_long = "config",
config_cli_short = 'c',
config_cli_visible = true
)
)]
struct Config {
#[ortho_config(default = 8080)]
port: u16,
}
fn main() -> OrthoResult<()> {
let config = Config::load()?;
println!("port={}", config.port);
Ok(())
}
This application accepts --config and -c, reads ACME_CONFIG_PATH, looks
for .acme-server.toml in project locations, and uses server.toml in
platform configuration directories. Keeping these choices in the derive also
exposes them through OrthoConfigDocs.
Handle every `load_first` outcome
ConfigDiscovery::load_first distinguishes three outcomes. Ok(Some(...))
contains the first successfully parsed candidate. Ok(None) means discovery
had no candidates to try. Err(...) means candidates existed but none loaded
successfully; surface or map that error rather than treating it as absence:
use ortho_config::{ConfigDiscovery, OrthoResult};
fn load_discovered_config(discovery: &ConfigDiscovery) -> OrthoResult<()> {
match discovery.load_first() {
Ok(Some(_config)) => {
// Merge or deserialize the discovered Figment value.
println!("discovery=loaded");
Ok(())
}
Ok(None) => {
// Continue with application defaults.
println!("discovery=absent");
Ok(())
}
Err(error) => Err(error),
}
}
fn main() -> OrthoResult<()> {
let discovery = ConfigDiscovery::builder("acme").build();
load_discovered_config(&discovery)
}