use std::path::{Path, PathBuf};
use dynamic_config::{dynamic_config, Builder, ErrorKind};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Server {
host: String,
port: u16,
}
#[dynamic_config]
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Declared {
host: String,
port: u16,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let directory = PathBuf::from("target/example-document-shape");
std::fs::create_dir_all(&directory)?;
one_document_no_header(&directory)?;
a_key_the_struct_does_not_have(&directory)?;
half_the_struct_in_each_file(&directory)?;
a_field_nothing_supplies(&directory)?;
Ok(())
}
fn one_document_no_header(directory: &Path) -> Result<(), Box<dyn std::error::Error>> {
println!("── 1. a document with no section header ──\n");
let file = write(
directory,
"server.json",
r#"{"host": "0.0.0.0", "port": 8000}"#,
)?;
let refused = Builder::<Server>::new("server").file(&file).load();
println!("without `.whole_document()`:\n {}\n", refused.unwrap_err());
let server: Server = Builder::new("server")
.whole_document()
.file(&file)
.env("APP_")
.load()?;
println!("with `.whole_document()`:\n {server:?}");
println!(
"\nThe key still names everything around the document: `APP_SERVER_PORT`\n\
reaches `port`, the cache entry and the diagnostics are named after it.\n\
A configuration with nothing to call itself may pass `\"\"`, and then\n\
the environment layer is just the prefix — `APP_PORT`.\n"
);
Ok(())
}
fn a_key_the_struct_does_not_have(directory: &Path) -> Result<(), Box<dyn std::error::Error>> {
println!("── 2. a key the struct does not name ──\n");
let file = write(
directory,
"extra.json",
r#"{"server": {"host": "0.0.0.0", "port": 8000, "hsot": "typo", "owner": "team-a"}}"#,
)?;
let server: Server = Builder::new("server").file(&file).load()?;
println!("the load ignores it:\n {server:?}\n");
let report = Declared::builder("server").file(&file).check()?;
println!("`check()` names it:\n{report}");
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
#[allow(dead_code)]
struct Strict {
host: String,
port: u16,
}
let error = Builder::<Strict>::new("server")
.file(&file)
.load()
.expect_err("`hsot` is a field this struct does not have");
println!("with `#[serde(deny_unknown_fields)]` the same file is refused:\n {error}\n");
Ok(())
}
fn half_the_struct_in_each_file(directory: &Path) -> Result<(), Box<dyn std::error::Error>> {
println!("── 3. one struct, two files ──\n");
let base = write(directory, "base.json", r#"{"server": {"host": "0.0.0.0"}}"#)?;
let ports = write(directory, "ports.json", r#"{"server": {"port": 8000}}"#)?;
let server: Server = Builder::new("server").file(&base).file(&ports).load()?;
println!("half in each:\n {server:?}\n");
let over = write(directory, "over.json", r#"{"server": {"port": 443}}"#)?;
let server: Server = Builder::new("server")
.file(&base)
.file(&ports)
.file(&over)
.load()?;
println!("where they overlap, the later file wins:\n {server:?}");
println!(
"\nTables merge key by key; arrays are replaced whole, never appended.\n\
A file that is not there is skipped, which is what makes an optional\n\
`secrets.json` work.\n"
);
Ok(())
}
fn a_field_nothing_supplies(directory: &Path) -> Result<(), Box<dyn std::error::Error>> {
println!("── 4. a field no source supplies ──\n");
let file = write(
directory,
"incomplete.json",
r#"{"server": {"host": "0.0.0.0"}}"#,
)?;
let error = Builder::<Server>::new("server")
.file(&file)
.load()
.expect_err("nothing supplies `port`");
println!("the load fails, naming the field:");
println!(" kind: {:?}", error.kind());
println!(" path: {}", error.path());
println!(" {error}\n");
assert_eq!(error.kind(), ErrorKind::Missing);
#[derive(Debug, Deserialize)]
#[allow(dead_code)]
struct Tolerant {
host: String,
#[serde(default = "eight_thousand")]
port: u16,
tags: Option<Vec<String>>,
}
fn eight_thousand() -> u16 {
8000
}
let tolerant: Tolerant = Builder::new("server").file(&file).load()?;
println!("with a `#[serde(default)]` and an `Option`, the same file loads:\n {tolerant:?}");
println!(
"\nA value the *program* can compute — but a file need not state — is\n\
`set_default`, the layer below the files. A section no file mentions\n\
is not a separate error: it is these missing fields.\n"
);
Ok(())
}
fn write(directory: &Path, name: &str, text: &str) -> std::io::Result<String> {
let path = directory.join(name);
std::fs::write(&path, text)?;
Ok(path.display().to_string())
}