App — the application context¶
App is the value every command handler receives. It holds the tool's identity, its
version, its assets, its configuration and its shutdown token. Every field is
reference-counted, so App::clone() is an O(1) refcount bump rather than a deep
copy — command handlers take it by value and fan-out code clones it freely.
Fields¶
| Field | Type | Visibility | Notes |
|---|---|---|---|
metadata |
Arc<ToolMetadata> |
public | Static tool identity. See Tool metadata. |
version |
Arc<VersionInfo> |
public | Build-time version. See Version information. |
assets |
Arc<Assets> |
public | Virtual filesystem overlay from rtb-assets: embedded defaults plus user overrides. |
shutdown |
CancellationToken |
public | Root cancellation token. Derive per-subsystem children with shutdown.child_token(). |
credentials_provider |
Option<Arc<dyn CredentialProvider>> |
public | None for tools that have not wired one. See Credentials. |
config |
ErasedConfig |
crate-private | Type-erased. Reach it through typed_config or config_as. |
typed_config_ops |
Option<Arc<TypedConfigOps>> |
crate-private | Present only when a typed config bundle was attached. |
trailing_args |
Arc<[OsString]> |
crate-private | Read with the trailing_args() method. |
credentials_provider being public and the App binding being mut is enough to
swap a provider in after construction — the crate's own credential tests do exactly
that.
What App does and does not derive¶
App derives Clone and nothing else. In particular it does not implement
Debug, so println!("{app:?}") will not compile and #[derive(Debug)] on a
struct that holds an App will not compile either. Print the pieces you need —
app.metadata.name, app.version.version — rather than the whole context.
App is Send + Sync + 'static; the crate's own test suite asserts those bounds so
they cannot regress silently.
Constructors¶
App::new — the production constructor¶
pub fn new<C>(
metadata: ToolMetadata,
version: VersionInfo,
config: Config<C>,
assets: Assets,
credentials_provider: Option<Arc<dyn CredentialProvider>>,
) -> Self
where
C: serde::de::DeserializeOwned + Send + Sync + 'static;
App::new is public. Nothing stops you calling it, and rtb-test-support calls it
internally. What it does not do is install the logging, miette hooks, panic
hooks, signal handlers and command registration that
rtb_cli::Application::builder().build() sets up — which is why production tools go
through the builder instead.
Three things it fixes for you, with no parameter to change them:
shutdownis a fresh rootCancellationToken. There is no way to pass an existing token in, so anAppcannot be made a child of an outer cancellation scope at construction time.typed_config_opsstartsNone, soconfig_schema()andconfig_value()both returnNoneuntilwith_typed_configis called.trailing_argsstarts empty.
Tools that have not typed their configuration yet pass Config::<()>::default();
C = () satisfies the bound.
App::for_testing — hidden, and superseded¶
Equivalent to App::new(metadata, version, Config::<()>::default(), Assets::default(), None).
It is #[doc(hidden)] but it is genuinely pub — it is not gated behind
cfg(test) and there is no Cargo feature controlling it, so any crate depending on
rtb-app can call it. New downstream tests should use
TestAppBuilder instead, which is the promoted path.
Builder-style methods¶
with_typed_config¶
Replaces the erased config storage and attaches the schema/render closures. Pass the
two together — an ErasedConfig holding a Config<A> paired with ops built for B
leaves config_value() returning None for the rest of the process's life, with no
error to tell you why. See Typed configuration.
with_trailing_args¶
Attaches the CLI tokens captured after a passthrough subcommand's name. It consumes
and returns self, so calling it on a clone leaves the shared App untouched —
which is exactly how rtb-cli uses it, giving only the dispatched command the args
while pre-run hooks see an empty list.
Accessors¶
trailing_args¶
The tokens after the matched subcommand name, for commands that set
Command::subcommand_passthrough to true. Empty for every
other command — an empty slice here means "not a passthrough command", not "the
user passed nothing".
credentials¶
Owned (name, credential) pairs from the wired provider. Returns an empty Vec when
credentials_provider is None, so a tool that has not declared any credentials
reports an empty set rather than failing.
typed_config and config_as¶
pub fn typed_config<C>(&self) -> Option<Arc<Config<C>>>
where C: serde::de::DeserializeOwned + Send + Sync + 'static;
#[track_caller]
pub fn config_as<C>(&self) -> Arc<Config<C>>
where C: serde::de::DeserializeOwned + Send + Sync + 'static;
Both recover the typed configuration by downcasting the erased storage. The returned
Arc<Config<C>> shares the same allocation as the one inside App, so
Arc::ptr_eq holds across App::clone() followed by typed_config() on both
copies. The downcast is a single Any round-trip — cheap enough to call at the top
of every command body without caching.
The difference is only in the failure mode:
| Situation | typed_config::<C>() |
config_as::<C>() |
|---|---|---|
C matches the wired type |
Some(Arc<Config<C>>) |
Arc<Config<C>> |
C does not match |
None |
panics |
| No typed config was ever wired | None (the stored Config<()> only matches C = ()) |
panics |
The panic message names the requested type, and #[track_caller] reports your call
site rather than a line inside rtb-app:
App::config_as::<mytool::Settings>() — no matching typed config wired
(did `Application::builder().config(...)` get called with the right type?)
Use config_as only from the crate that also wired the config. Anywhere else,
typed_config and a graceful fallback is the honest choice.
config_schema and config_value¶
pub fn config_schema(&self) -> Option<&serde_json::Value>;
pub fn config_value(&self) -> Option<serde_json::Value>;
The JSON Schema for the wired config type, and the merged config rendered as JSON.
These drive rtb-cli's config schema / validate / show / get without that crate
needing to know your C.
Both return None unless a typed-config bundle was attached. Passing a real
Config<MySettings> to App::new is not enough — App::new erases the config but
never builds the ops, because its bound is only DeserializeOwned while the ops need
Serialize + JsonSchema as well. The bundle arrives via with_typed_config, which
is what rtb_cli::Application::builder().config(...) and
TestAppBuilder::config(...) both call. So it is entirely possible to have
typed_config::<C>() return Some while config_schema() returns None; that
combination means the config was wired through the plain constructor.