Skip to content

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.

use rtb_app::app::App;

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:

  • shutdown is a fresh root CancellationToken. There is no way to pass an existing token in, so an App cannot be made a child of an outer cancellation scope at construction time.
  • typed_config_ops starts None, so config_schema() and config_value() both return None until with_typed_config is called.
  • trailing_args starts empty.

Tools that have not typed their configuration yet pass Config::<()>::default(); C = () satisfies the bound.

App::for_testing — hidden, and superseded

#[doc(hidden)]
pub fn for_testing(metadata: ToolMetadata, version: VersionInfo) -> Self;

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

pub fn with_typed_config(self, erased: ErasedConfig, ops: Arc<TypedConfigOps>) -> Self;

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

pub fn with_trailing_args(self, args: Vec<OsString>) -> Self;

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

pub fn trailing_args(&self) -> &[OsString];

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

pub fn credentials(&self) -> Vec<(String, CredentialRef)>;

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.