Skip to content

Write config back and export a schema

The mutable feature adds two associated items to Config<C>: write, which puts the current value on disk, and schema, which emits a JSON Schema for the config struct. They exist to back config set, config schema and config validate style subcommands.

Enable the feature and the derives

rtb-config = { version = "0.6", features = ["mutable"] }

Both items need more from C than the rest of the crate does:

use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Debug, Default, Deserialize, Serialize, JsonSchema)]
struct MyConfig {
    host: String,
    port: u16,
}

Without Serialize and JsonSchema the methods are not callable and the compiler says the method `write` exists for struct `Config<MyConfig>`, but its trait bounds were not satisfied, with a note naming Serialize and JsonSchema as the unsatisfied bounds. The fix is usually a missing derive.

Write the current configuration to a file

cfg.write(std::path::Path::new("/etc/mytool/config.yaml"))?;

Missing parent directories are created. The format follows the extension:

Extension Format
.yml, .yaml YAML
.toml TOML
.json JSON
anything else, or none YAML

Write YAML or JSON if you intend to read the file back. ConfigBuilder::user_file parses every file as YAML, so a .toml file this method wrote cannot be loaded again by this crate.

Understand what gets written

write serialises the merged value, exactly what get() returns, not the file layer on its own. Three consequences to design around:

  • Environment overrides are baked in. If MYTOOL_PORT=9999 is set when you call write, port: 9999 is now in the file permanently, as if a human had typed it.
  • Embedded defaults become explicit. Every key gets written out, so a later release that changes a default no longer affects this user.
  • The existing file is replaced. Comments, key order and any hand-formatting in the user's file are gone.

For a config set command that edits one key, this is usually the wrong tool: it turns a one-key edit into a full rewrite of everything the process happened to be running with. Editing the user's file directly keeps the rest of it intact.

The write is also not atomic: a crash part-way leaves a truncated file. If that matters, serialise to a temporary file in the same directory and rename it into place.

Emit a JSON Schema for the config struct

let schema = Config::<MyConfig>::schema();
println!("{}", serde_json::to_string_pretty(&schema)?);

schema() is an associated function, so it needs no loaded config and can run before anything is read. The result is a serde_json::Value with the usual top-level keys ($schema, title, type and properties), generated fresh on each call. There is no caching; the call is cheap enough for CLI use.

This is what to wire to a config schema subcommand, and what to hand to an editor for YAML completion against your tool's config file.

Validate a value before writing it

Nothing in the crate does this for you. schema() produces the schema; validating a candidate value against it is the caller's job, using a JSON Schema validator of your choice.

ConfigError::Schema exists for exactly that step, so a validation failure in your own code has a home in the same error type. No code path inside rtb-config ever returns it.

Next