Get started / Rust
docuconf for Rust
Keep your #[derive(Deserialize)] config struct and add #[derive(Docuconf)]. Doc comments are the descriptions, the Rust type picks the contract type, and figment's files and profiles become the contract's defaults and profiles.
- Builds on
- figment + serde
- Package
- docuconf (crates.io)
- Requires
- Rust 1.89
- Status
- v0.1 alpha, not yet released · conformance 134 of 134
- Source
- docuconf-rust · the orders example
Not published yet
main branch. After the first release it will be cargo add docuconf serde --features serde/derive.The code below is the orders service, the example every SDK ships: six variables, one of them a secret, checked the same way in every language. Compare it across languages.
1.Install
The crate is not on crates.io yet. Add it from the main branch (Rust 1.89+), with serde:
cargo add docuconf --git https://github.com/Docuconf/docuconf-rust
cargo add serde --features derive2.Declare your configuration
A serde struct with #[derive(Docuconf)]. The first paragraph of each /// comment is the description and the rest its details; #[docuconf(...)] sets defaults and constraints; Secret<T> marks a secret and keeps it out of Debug.
/// The service's configuration. The first paragraph of each `///` comment
/// is the variable's description in the contract and the rest its details,
/// and the Rust type picks its contract type.
#[derive(Debug, Deserialize, Docuconf)]
struct Config {
/// HTTP listen port.
#[docuconf(default = 8080, min = 1)]
port: u16,
/// Minimum log level emitted.
#[docuconf(default = "info")]
log_level: LogLevel,
/// Postgres connection string for the orders database.
// `Secret` marks it `secret: true`; `schemes` makes it a `url`, and
// `max_length` caps it in characters.
#[docuconf(schemes("postgres"), max_length = 2048)]
database_url: Secret<String>,
/// Browser origins allowed to call the API.
#[docuconf(default = ["http://localhost:3000"], min_items = 1)]
allowed_origins: Vec<String>,
/// Time allowed to read and answer one request.
#[docuconf(default = "30s", min = "1s", max = "5m")]
#[serde(with = "docuconf::humantime_serde")]
request_timeout: Duration,
/// Threads serving requests.
///
/// Each worker answers one connection at a time, so this is also the
/// number of requests served at once. Raise it when requests queue up;
/// each worker holds a [`std::thread`] stack.
///
/// Keep it at or below the database pool size:
///
/// - one connection per worker;
/// - plus one for migrations.
#[docuconf(default = 4, min = 1, max = 64)]
worker_count: u8,
}3.Load it at boot
docuconf::load_or_exit() reads the environment and every file input and returns the struct, or prints every problem and exits 1. docuconf::load() returns an error listing them instead.
let config: Config = docuconf::load_or_exit();4.Run it, and see an error
Run the example with a valid environment:
DATABASE_URL=postgres://orders:pw@localhost:5432/orders cargo run -p ordersWith PORT=0 and no DATABASE_URL, the service refuses to start, lists every problem with its error code and exits 1. The same lines go to /dev/termination-log.
$ PORT=0 cargo run -q -p orders
docuconf: 2 configuration problems:
DATABASE_URL: is required but not set (missing_required)
PORT: 0 is below min 1 (out_of_range)5.Test your configuration
Loader::env replaces the process environment, so a test passes exactly the variables it wants. Put the tests next to the struct, in a #[cfg(test)] module.
use super::*;
use docuconf::Code;
// Load from an explicit environment: the process environment is not read or changed.
fn load(env: &[(&str, &str)]) -> Result<Config, docuconf::Error> {
docuconf::Loader::<Config>::new()
.env(env.iter().copied())
.termination_log(false)
.load()
}
#[test]
fn defaults() {
let config = load(&[("DATABASE_URL", "postgres://orders@db/orders")]).unwrap();
assert_eq!(config.port, 8080);
assert_eq!(config.worker_count, 4);
}
#[test]
fn rejects_bad_values() {
let err = load(&[("PORT", "70000")]).unwrap_err();
let found: Vec<_> = err.violations().iter().map(|v| (v.input.as_str(), v.code)).collect();
assert!(found.contains(&("PORT", Code::OutOfRange)), "{found:?}");
assert!(found.contains(&("DATABASE_URL", Code::MissingRequired)), "{found:?}");
}cargo test -q6.Export the contract
The app exports its own contract: docuconf::export_command handles an export [--check] [PATH] argument and exits, so CI can rerun it with --check and fail when the committed file differs.
cargo run -q -p orders -- export contract.cue// `orders export [--check] [PATH]` writes (or checks) the contract and
// exits; any other invocation carries on.
docuconf::export_command::<Config>(&Meta::new("orders-api").package("orders"));// Code generated by docuconf. DO NOT EDIT.
package orders
import "docuconf.dev/contract"
contract.#Contract & {
apiVersion: "docuconf.dev/v1alpha1"
kind: "ConfigContract"
metadata: {
name: "orders-api"
generator: {
language: "rust"
sdk: "docuconf"
version: "0.1.0"
}
}
vars: {
ALLOWED_ORIGINS: {
type: "list"
description: "Browser origins allowed to call the API"
default: ["http://localhost:3000"]
items: "string"
encoding: "json"
minItems: 1
}
DATABASE_URL: {
type: "url"
description: "Postgres connection string for the orders database"
required: true
secret: true
schemes: ["postgres"]
maxLength: 2048
}
LOG_LEVEL: {
type: "enum"
description: "Minimum log level emitted"
default: "info"
values: ["debug", "info", "warn", "error"]
}
PORT: {
type: "int"
description: "HTTP listen port"
default: 8080
min: 1
max: 65535
}
REQUEST_TIMEOUT: {
type: "duration"
description: "Time allowed to read and answer one request"
default: "30s"
encoding: "go"
min: "1s"
max: "5m"
}
WORKER_COUNT: {
type: "int"
description: "Threads serving requests"
details: "Each worker answers one connection at a time, so this is also the\nnumber of requests served at once. Raise it when requests queue up;\neach worker holds a `std::thread` stack.\n\nKeep it at or below the database pool size:\n\n- one connection per worker;\n- plus one for migrations."
default: 4
min: 1
max: 64
}
}
}The platform then checks each environment's values against contract.cue before it deploys, with docuconf vet, the Helm chart or CUE. See How it works.
7.axum, Rocket and actix-web
Call docuconf::load() in main before you build the router, and share the struct as axum state or an actix Data. Rocket already uses figment: give docuconf's Loader your figment with .figment(...) and .profiles(...), so the contract carries the same files and profiles. Do not add figment's own Env provider alongside it: it trims values and guesses types. See the README.
Next
- The docuconf-rust README: every type, file input and option.
- The orders example, with its smoke test.
- The same service in every language, side by side.
- What every SDK supports, and this one's known gaps: No reload: watch. No JKS keystores.
Every snippet on this page is compiled or run against docuconf-rust's main branch in CI. Facts checked 2026-10-08.