Read your tool's typed configuration¶
Get your config out of the App¶
App stores configuration type-erased, so you name the type on the way out:
async fn run(&self, app: App) -> miette::Result<()> {
let Some(config) = app.typed_config::<MySettings>() else {
return Err(miette::miette!("mytool: no typed configuration wired"));
};
let settings = config.get();
println!("talking to {}", settings.host);
Ok(())
}
typed_config::<C>() returns Option<Arc<Config<C>>>, and the returned Arc shares
the same allocation as the one inside the App — the downcast is a single Any
round-trip, cheap enough to call at the top of every command body without caching it
anywhere.
Choose between typed_config and config_as¶
| Use | When |
|---|---|
typed_config::<C>() |
Anywhere the config might not be wired, or C might not be the tool's type. Returns None. |
config_as::<C>() |
Only from the same crate that wired the config at startup. Panics. |
config_as panics with a message naming the type you asked for, and #[track_caller]
reports your line rather than one inside rtb-app:
App::config_as::<mytool::Settings>() — no matching typed config wired
(did `Application::builder().config(...)` get called with the right type?)
A library crate that might be used by more than one tool should always use
typed_config — the panic is only reasonable when you personally know the config was
wired.
Handle "no typed config" gracefully¶
A tool that has not typed its configuration yet has a Config<()> stored, so
typed_config::<C>() returns None for every C except (). Commands meant to
work in both situations should fall back rather than fail:
let host = app
.typed_config::<MySettings>()
.map(|c| c.get().host.clone())
.unwrap_or_else(|| "localhost".to_string());
Why config_schema() is None when typed_config() is Some¶
This combination catches people out. It means the config was stored, but the schema-and-render bundle was never attached.
App::new erases a Config<C> and nothing else. It cannot build the bundle, because
its bound on C is only DeserializeOwned while generating a schema needs
JsonSchema and rendering a value needs Serialize. The bundle arrives separately,
through App::with_typed_config — which is what
rtb_cli::Application::builder().config(...) and TestAppBuilder::config(...) both
call for you.
So:
How the App was built |
typed_config::<C>() |
config_schema() / config_value() |
|---|---|---|
Application::builder().config(cfg) |
Some |
Some |
TestAppBuilder::…config_value(c) |
Some |
Some |
App::new(…, cfg, …) directly |
Some |
None |
| nothing wired | None |
None |
If you are building an App by hand and config show comes back empty, that last
row is why.
Read the merged config without naming the type¶
Generic code — anything that has to work across tools — reads JSON instead:
if let Some(value) = app.config_value() {
println!("{}", serde_json::to_string_pretty(&value)?);
}
if let Some(schema) = app.config_schema() {
// schema is a &serde_json::Value
}
This is the path rtb-cli's config show / get / schema / validate takes, and it is
the reason C never has to appear in rtb-cli's signatures.