Wire a typed configuration¶
Without this, config show prints a placeholder, config schema errors, and
config set writes whatever you give it unvalidated. Wiring a typed config turns
all five subcommands on.
Define the config type¶
Four traits are required: Serialize, DeserializeOwned, JsonSchema, and
Send + Sync + 'static.
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MyConfig {
/// Where the API lives.
pub endpoint: String,
/// Request timeout in seconds.
#[serde(default = "default_timeout")]
pub timeout_secs: u64,
}
fn default_timeout() -> u64 {
30
}
The JsonSchema bound is what makes config schema and the validation paths work.
It is not optional — a tool whose config shape cannot derive JsonSchema has to
stay on the untyped path.
Hand it to the builder¶
let config: rtb_config::Config<MyConfig> = /* built by rtb-config */;
Application::builder()
.metadata(metadata)
.version(rtb_app::version_info!())
.config(config)
.build()?
.run()
.await
Loading and layering the configuration is
rtb-config's job. .config(...) is the
step that makes the result visible to the config command and to your own command
handlers.
Read it from a command¶
async fn run(&self, app: App) -> miette::Result<()> {
let config = app
.typed_config::<MyConfig>()
.ok_or_else(|| miette::miette!("typed config not wired"))?;
// …
}
typed_config::<C>() returns None when nothing was wired, or when the type
requested is not the type that was.
What changes once it is wired¶
| Command | Untyped | Typed |
|---|---|---|
config show |
Placeholder comment lines | The merged value, as YAML |
config get |
Reads the user file | Reads the merged value, defaults and env overlays included |
config schema |
Fails | Prints the JSON Schema |
config validate |
Parse check only | Full schema validation |
config set |
Writes unvalidated | Validates the candidate first; rejects on failure |
The get change is the one that catches people out: untyped get shows what is in
the file, typed get shows what the tool actually sees. A value that comes from an
embedded default appears only in the typed reading.
Verify it¶
$ mytool config schema | head -5
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "MyConfig",
...
$ mytool config set /timeout_secs '"thirty"'
config set rejected: candidate value at `/timeout_secs` fails the wired schema: ...
If config schema still errors, .config(...) did not run — check it is on the
builder chain before .build().
Related¶
configreference — every subcommand, the file paths, the format rules.- rtb-config — building the
Config<C>this step consumes.