use crate::assert_diag_snapshot;
use facet::Facet;
use figue::{self as args, Driver, MockEnv, builder};
#[derive(Facet, Debug)]
struct Args {
#[facet(args::named, args::short = 'v')]
verbose: bool,
#[facet(args::config, args::env_prefix = "APP")]
config: ServerConfig,
}
#[derive(Facet, Debug)]
struct ServerConfig {
#[facet(default = "localhost")]
host: String,
#[facet(default = 8080)]
port: u16,
database: DatabaseConfig,
#[facet(default)]
tls: Option<TlsConfig>,
}
#[derive(Facet, Debug)]
struct DatabaseConfig {
url: String,
#[facet(default = 10)]
max_connections: u32,
#[facet(default = 30)]
timeout_secs: u64,
}
#[derive(Facet, Debug)]
struct TlsConfig {
cert_path: String,
key_path: String,
}
#[derive(Facet, Debug)]
struct ArgsWithFlattenedConfigRoot {
#[facet(args::config, args::env_prefix = "BEE_RUN")]
#[facet(flatten)]
run: FlattenedRunConfig,
}
#[derive(Facet, Debug)]
struct FlattenedRunConfig {
model: String,
#[facet(default)]
tag: Vec<String>,
#[facet(default)]
tui: bool,
}
#[test]
fn test_layered_all_sources() {
let config_json = r#"{
"config": {
"host": "0.0.0.0",
"port": 3000,
"database": {
"url": "postgres://localhost/mydb",
"max_connections": 20
}
}
}"#;
let env = MockEnv::from_pairs([("APP__PORT", "4000"), ("APP__DATABASE__TIMEOUT_SECS", "60")]);
let config = builder::<Args>()
.unwrap()
.cli(|cli| cli.args(["--verbose"]))
.env(|e| e.source(env))
.file(|f| f.content(config_json, "config.json"))
.build();
let driver = Driver::new(config);
let args = driver.run().unwrap();
assert!(args.verbose, "verbose should be true from CLI");
assert_eq!(args.config.host, "0.0.0.0", "host should come from file");
assert_eq!(args.config.port, 4000, "port should be overridden by env");
assert_eq!(
args.config.database.url, "postgres://localhost/mydb",
"database.url should come from file"
);
assert_eq!(
args.config.database.max_connections, 20,
"max_connections should come from file"
);
assert_eq!(
args.config.database.timeout_secs, 60,
"timeout_secs should be overridden by env"
);
assert!(args.config.tls.is_none(), "tls should be None (default)");
}
#[test]
fn test_flattened_config_root_merges_namespaced_sources_then_deserializes_flat() {
let config_json = r#"{
"run": {
"model": "file-model"
}
}"#;
let env = MockEnv::from_pairs([("BEE_RUN__MODEL", "env-model")]);
let config = builder::<ArgsWithFlattenedConfigRoot>()
.unwrap()
.cli(|cli| cli.args(["--tui", "--run.model", "cli-model"]))
.env(|e| e.source(env))
.file(|f| f.content(config_json, "config.json"))
.build();
let args = Driver::new(config).run().unwrap();
assert!(args.run.tui, "flattened CLI flag should populate run.tui");
assert!(
args.run.tag.is_empty(),
"explicit Vec default inside flattened config root should be preserved"
);
assert_eq!(
args.run.model, "cli-model",
"dotted CLI overrides should keep the config-root namespace and override env/file"
);
}
#[test]
fn test_layered_missing_required_field() {
let config_json = r#"{
"config": {
"host": "127.0.0.1",
"port": 5000,
"database": {
"max_connections": 5
}
}
}"#;
let env = MockEnv::from_pairs([("APP__DATABASE__TIMEOUT_SECS", "15")]);
let config = builder::<Args>()
.unwrap()
.cli(|cli| cli.args(["-v"]))
.env(|e| e.source(env))
.file(|f| f.content(config_json, "config.json"))
.build();
let driver = Driver::new(config);
let err = driver.run().unwrap_err();
assert_diag_snapshot!(err);
}
#[test]
fn test_layered_cli_overrides_all() {
let config_json = r#"{
"config": {
"host": "file-host",
"port": 1111,
"database": {
"url": "postgres://file/db",
"max_connections": 100,
"timeout_secs": 999
}
}
}"#;
let env = MockEnv::from_pairs([
("APP__HOST", "env-host"),
("APP__PORT", "2222"),
("APP__DATABASE__URL", "postgres://env/db"),
]);
let config = builder::<Args>()
.unwrap()
.cli(|cli| cli.args(["--config.host", "cli-host", "--config.port", "3333"]))
.env(|e| e.source(env))
.file(|f| f.content(config_json, "config.json"))
.build();
let driver = Driver::new(config);
let args = driver.run().unwrap();
assert_eq!(args.config.host, "cli-host", "host should come from CLI");
assert_eq!(args.config.port, 3333, "port should come from CLI");
assert_eq!(
args.config.database.url, "postgres://env/db",
"database.url should come from env"
);
assert_eq!(
args.config.database.max_connections, 100,
"max_connections should come from file"
);
assert_eq!(
args.config.database.timeout_secs, 999,
"timeout_secs should come from file"
);
}
#[derive(Facet, Debug)]
struct MultiConfigArgs {
#[facet(args::config, args::env_prefix = "BEE", rename = "cfg")]
cfg: PrimaryConfig,
#[facet(args::config, args::env_prefix = "BEE_EVAL", rename = "eval")]
eval: EvalConfig,
#[facet(args::subcommand)]
command: MultiConfigCommand,
}
#[derive(Facet, Debug)]
struct PrimaryConfig {
#[facet(default = "localhost")]
host: String,
#[facet(default = 8080)]
port: u16,
}
#[derive(Facet, Debug)]
struct EvalConfig {
dataset: String,
#[facet(default = 10)]
samples: u32,
#[facet(default)]
enabled: bool,
}
#[derive(Facet, Debug)]
#[repr(u8)]
enum MultiConfigCommand {
Run,
}
#[test]
fn test_layered_multiple_config_roots() {
let config_json = r#"{
"cfg": {
"host": "file-host",
"port": 3000
},
"eval": {
"dataset": "file-dataset",
"samples": 20
}
}"#;
let env = MockEnv::from_pairs([("BEE__PORT", "4000"), ("BEE_EVAL__SAMPLES", "30")]);
let config = builder::<MultiConfigArgs>()
.unwrap()
.cli(|cli| cli.args(["--cfg.host", "cli-host", "--eval.enabled", "true", "run"]))
.env(|e| e.source(env))
.file(|f| f.content(config_json, "config.json"))
.build();
let args = Driver::new(config).run().unwrap();
assert_eq!(args.cfg.host, "cli-host");
assert_eq!(args.cfg.port, 4000);
assert_eq!(args.eval.dataset, "file-dataset");
assert_eq!(args.eval.samples, 30);
assert!(args.eval.enabled);
assert!(matches!(args.command, MultiConfigCommand::Run));
}
#[test]
fn test_layered_multiple_config_roots_cli_file_targets_matching_root() {
use std::io::Write;
let mut cfg_file = tempfile::Builder::new().suffix(".json").tempfile().unwrap();
write!(
cfg_file,
r#"{{
"host": "file-host",
"port": 3000
}}"#
)
.unwrap();
let cfg_path = cfg_file.path().to_str().unwrap();
let config = builder::<MultiConfigArgs>()
.unwrap()
.cli(|cli| {
cli.args([
"--cfg",
cfg_path,
"--cfg.host",
"cli-host",
"--eval.dataset",
"cli-dataset",
"run",
])
})
.build();
let args = Driver::new(config).run().unwrap();
assert_eq!(args.cfg.host, "cli-host");
assert_eq!(args.cfg.port, 3000);
assert_eq!(args.eval.dataset, "cli-dataset");
assert_eq!(args.eval.samples, 10);
assert!(!args.eval.enabled);
assert!(matches!(args.command, MultiConfigCommand::Run));
}
#[derive(Facet, Debug)]
struct SimpleArgs {
#[facet(args::named, args::short = 'd')]
debug: bool,
#[facet(args::config, args::env_prefix = "MYAPP")]
settings: AppSettings,
}
#[derive(Facet, Debug)]
struct AppSettings {
name: String,
#[facet(default = "127.0.0.1")]
host: String,
#[facet(default = 8080)]
port: u16,
#[facet(default = 3)]
max_retries: u32,
#[facet(default = 5000)]
timeout_ms: u64,
#[facet(default)]
experimental: bool,
logging: LogConfig,
storage: StorageBackend,
}
#[derive(Facet, Debug)]
struct LogConfig {
#[facet(default = "info")]
level: String,
#[facet(default)]
format: LogFormat,
#[facet(default)]
file: Option<String>,
}
#[derive(Facet, Debug, Default)]
#[repr(u8)]
enum LogFormat {
#[default]
Plain,
Json,
Compact,
}
#[derive(Facet, Debug)]
#[facet(rename_all = "kebab-case")]
#[repr(u8)]
#[allow(dead_code)]
enum StorageBackend {
Local {
path: String,
},
S3 {
bucket: String,
#[facet(default = "us-east-1")]
region: String,
#[facet(default)]
endpoint: Option<String>,
},
Memory,
}
#[test]
fn test_layered_dump_shows_all_sources() {
let config_json = r#"{
"settings": {
"host": "0.0.0.0",
"port": 3000,
"max_retries": 5,
"logging": {
"level": "debug",
"format": "Json"
},
"storage": {
"s3": {
"region": "eu-west-1"
}
}
}
}"#;
let env = MockEnv::from_pairs([
("MYAPP__PORT", "4000"), ("MYAPP__TIMEOUT_MS", "10000"), ("MYAPP__LOGGING__FILE", "/var/log/app"), ]);
let config = builder::<SimpleArgs>()
.unwrap()
.cli(|cli| cli.args(["--debug", "--settings.experimental", "true"]))
.env(|e| e.source(env))
.file(|f| f.content(config_json, "app.json"))
.build();
let driver = Driver::new(config);
let err = driver.run().unwrap_err();
assert_diag_snapshot!(err);
}