Skip to content

Typed configuration storage

rtb_app::typed_config is the machinery that lets App hold a tool's typed configuration without App itself becoming generic over that type. Most tool authors never name these types directly — they call App::typed_config — but the module is public because rtb-cli and rtb-test-support both wire it.

use rtb_app::typed_config::{erase, ErasedConfig, TypedConfigOps};

ErasedConfig

pub type ErasedConfig = Arc<dyn Any + Send + Sync>;

A type alias, not a new type. It is Arc<dyn Any + …> specifically so that Arc::downcast recovers Arc<Config<C>> sharing the same backing allocation — no copy, no second refcount, and Arc::ptr_eq holds between the value inside App and the one handed back by typed_config.

erase

pub fn erase<C>(config: Config<C>) -> ErasedConfig
where C: serde::de::DeserializeOwned + Send + Sync + 'static;

Wraps a Config<C> for storage. App::new calls it, and so does TestAppBuilder::config.

TypedConfigOps

pub struct TypedConfigOps {
    pub schema: serde_json::Value,
    // plus a private render closure
}

impl TypedConfigOps {
    pub fn new<C>() -> Self
    where C: Serialize + DeserializeOwned + schemars::JsonSchema + Send + Sync + 'static;

    pub fn render(&self, erased: &ErasedConfig) -> Option<serde_json::Value>;
}

The type-erased view onto a wired config: the JSON Schema for C, plus a closure that renders the merged value as JSON. C is captured in the closure at construction time and is never named again, which is what lets rtb-cli drive config show / get / schema / validate without knowing the tool's config type.

TypedConfigOps implements Debug by hand — the schema is printed and the closure renders as <closure>.

The bounds differ, and that is why config_schema() can be None

Item Bounds on C
erase / App::new DeserializeOwned + Send + Sync + 'static
TypedConfigOps::new Serialize + DeserializeOwned + JsonSchema + Send + Sync + 'static

Storage needs only DeserializeOwned; producing a schema and rendering a value need JsonSchema and Serialize on top. So a config type can be stored on an App without being renderable, and that is the mechanical reason App::config_schema() and App::config_value() return Option rather than the value: App::new alone never builds the ops.

render returns None on a mismatch, silently

pub fn render(&self, erased: &ErasedConfig) -> Option<serde_json::Value>;

None means the erased value is not a Config<C> of the type captured when the ops were built. There is no error describing the mismatch. The public API prevents this pairing going wrong — with_typed_config takes both halves together — but if you call it yourself, a mismatched pair produces a permanently empty config show and no diagnostic anywhere.

Schema generation cannot fail loudly

TypedConfigOps::new generates the schema with schemars::SchemaGenerator::default() and serialises it with serde_json::to_value(...).unwrap_or(serde_json::Value::Null). A serialisation failure therefore leaves schema as JSON null rather than returning an error. In practice a derived JsonSchema always serialises, but a hand-written implementation that does not will surface as a null schema rather than a message.