Tests and embedded callers can resolve the complete configuration without
mutating the process environment. Use ScanEnvSource for the explicit ability
to enumerate variables for the merge layer, and pass it to
CsvEnv::with_source. The derive-generated
OrthoConfig::load_from_iter_with_sources entry point accepts both the
discovery SharedEnvSource and merge SharedScanEnvSource, so
configuration-path selection, files, and the environment layer use the same
supplied values.
MapEnv implements both source traits. Create one Arc<MapEnv>, then coerce
cloned Arc values to SharedEnvSource and SharedScanEnvSource; the same
map can therefore drive discovery lookups and merge enumeration. The ordinary
load() and load_from_iter() methods continue to use ProcessEnv by default.
For an upgrade checklist covering this opt-in API, see the v0.10.0 migration guide.
For a directly composed environment layer, attach the scan source to CsvEnv
with with_source:
use ortho_config::{CsvEnv, MapEnv};
use std::sync::Arc;
let source = Arc::new(
MapEnv::new()
.with_var("APP_DATABASE__HOST", "localhost")
.with_var("APP_TAGS", "one,two"),
);
let provider = CsvEnv::prefixed("APP_")
.uppercase(true)
.split("__")
.with_source(source);
let _figment = ortho_config::figment::Figment::new().merge(provider);
The injected provider follows the same declarative rules as the process-backed
provider: prefix matching is case-insensitive and strips the matching prefix,
uppercase and lowercase transform names at their place in the builder
chain, and each split replaces its separator with a dotted key component. CSV
parsing is enabled by default; comma-containing scalar values become lists,
while values beginning with [, {, or a quote remain structured or quoted
values. Use csv(false) when a comma must remain part of a scalar value. The
source-aware path rejects map() and filter_map() providers because
arbitrary closures cannot be replayed against a ScanEnvSource.
For example, this complete load uses one map for the selector and merge layers:
use ortho_config::{
MapEnv, OrthoConfig, OrthoResult, SharedEnvSource, SharedScanEnvSource,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[derive(Debug, Deserialize, Serialize, OrthoConfig)]
#[ortho_config(prefix = "ACME_")]
struct Config {
#[ortho_config(default = 8080)]
port: u16,
}
fn main() -> OrthoResult<()> {
let environment = Arc::new(MapEnv::new().with_var("ACME_PORT", "9000"));
let discovery: SharedEnvSource = environment.clone();
let merge: SharedScanEnvSource = environment;
let config = Config::load_from_iter_with_sources(["acme"], discovery, merge)?;
println!("port={}", config.port);
Ok(())
}
The example prints port=9000. No process variable is read: the discovery
source handles named lookups, while the scan source supplies the merge layer.