v0.9.4 — API Maturity
Pre-releaseconfig-lib v0.9.4 — Deprecation of the dual Config / EnterpriseConfig surface
The third patch on the road to 1.0. v0.9.4 begins the architectural consolidation called for in Phase 0.9.4 of the roadmap. EnterpriseConfig, ConfigManager, and the enterprise::direct parsing helpers are all marked #[deprecated] and the unified Config API gains a ConfigOptions knobs struct for opt-out behavior (read_only is wired today; cache_enabled and cache_capacity are reserved for the v0.9.5 caching work). The deprecation is advisory only — every existing call-site continues to compile and run unchanged through the v0.9.x line.
The actual data-model merger — folding EnterpriseConfig's multi-tier cache into a Config::get that still returns Option<&Value> under concurrent reads — lands with v0.9.5's lock-free caching work. Doing the deprecation announcement and the cache rewrite as two separate releases is intentional: the caching architecture decides the borrow semantics of the unified Config::get, and shipping the API merger ahead of that design would either freeze the wrong return type or force a second migration. v0.9.4 sets users up for the v0.9.5 transition cleanly.
What is config-lib?
A multi-format configuration library for Rust. One API for TOML, JSON, INI, XML, HCL, NOML, Java Properties, and CONF — with hot reload, environment variable overrides, schema validation, audit logging, and cached reads. Designed to be the configuration layer a production database can depend on without wrapping.
What's new in 0.9.4
ConfigOptions — opt-out behavior knobs
The single biggest forward-looking addition is ConfigOptions, a #[non_exhaustive] struct that carries the small set of toggles that should not be enabled by default. v0.9.4 wires one of them (read_only) into the existing Config::set / remove / merge paths; the other two (cache_enabled, cache_capacity) are accepted today so that v0.9.5's caching work does not require a second API change at every call site.
use config_lib::{Config, ConfigOptions};
// Default options — caching on (v0.9.5), writes allowed.
let _cfg = Config::with_options(ConfigOptions::default());
// Read-only configuration for a hot path that must never be mutated.
let opts = ConfigOptions::new().read_only(true);
let mut locked = Config::with_options(opts);
assert!(locked.set("foo", "bar").is_err()); // rejected with Error::general(..)The struct uses the canonical #[non_exhaustive] + consuming-builder-methods pattern so new knobs can be added in v0.9.x MINOR releases without breaking SemVer. The default (ConfigOptions::default()) produces the canonical Hive-DB-tuned configuration; the builder methods are zero-cost type-state shifts.
EnterpriseConfig, ConfigManager, enterprise::direct::* deprecated
Every public item in the enterprise module now carries an explicit #[deprecated(since = "0.9.4", note = "...")] attribute. The notes point users at the corresponding unified-Config operation. The module-level rustdoc opens with a migration table so anyone discovering the deprecation gets a directly-actionable next step.
The deprecation is advisory only:
- Existing call-sites continue to compile.
- The behavior of every deprecated method is unchanged.
- The deprecation window runs through the entire v0.9.x line and into the v1.x SemVer-stable window. Per the roadmap's stability contract, deprecated items keep working for at least one full MINOR cycle (six months minimum) before removal in v2.0.
- The crate itself ships clean (
cargo clippy --all-targets --all-features -- -D warnings): internal references to the deprecated items insideenterprise.rs,ConfigManager, the re-exports inlib.rs, and the comparison benchmarks all carry scoped#[allow(deprecated)]annotations with REPS-AUDIT rationale documenting why the suppression is correct.
Migration guide (also in examples/enterprise_demo.rs)
Was (EnterpriseConfig) |
Use (unified Config + ConfigOptions) |
|---|---|
EnterpriseConfig::new() |
Config::new() |
EnterpriseConfig::from_string(s, fmt) |
Config::from_string(s, fmt) |
EnterpriseConfig::from_file(p) |
Config::from_file(p) |
cfg.get("k") (owned Value) |
cfg.get("k").cloned() (owned), or cfg.get("k") (borrowed &Value) |
cfg.set("k", v) |
cfg.set("k", v) |
cfg.exists("k") |
cfg.contains_key("k") |
cfg.keys() (Vec<String>) |
cfg.keys() (Result<Vec<&str>>) |
cfg.save() / cfg.save_to(p) |
cfg.save() / cfg.save_to_file(p) |
cfg.merge(other) |
cfg.merge(other) |
cfg.set_default(k, v) |
cfg.get(k).and_then(...).unwrap_or(v) (rich defaults table returns in v0.9.5) |
cfg.cache_stats() |
Lands on Config in v0.9.5. |
cfg.make_read_only() |
Config::with_options(ConfigOptions::new().read_only(true)) |
ConfigManager |
Retained; only its internal storage type changes in v0.9.5. |
enterprise::direct::parse_string |
config_lib::parse |
enterprise::direct::parse_file |
config_lib::parse_file |
examples/enterprise_demo.rs rewritten end-to-end
The headline example for the enterprise module is now a model migration. Five demos covering the original feature surface (set/get with nested keys, default-value lookup, read-only mode via ConfigOptions, file load, one-shot string parsing) all use Config directly. The file closes with the migration table reproduced inline so anyone reading the example as a tutorial sees the old → new translation in context.
$ cargo run --example enterprise_demo
Configuration Library Demo (v0.9.4 unified Config API)
======================================================
Demo 1: Config with set/get
host: localhost
port: 8080
…
Demo 3: Read-only configuration via ConfigOptions
set() on read-only config returned: true
…README leads with Config everywhere
The README's recommendations now lead with Config and ConfigOptions. The old Enterprise Caching section is now a Read-only mode and forward-compatible options section with ConfigOptions::new().read_only(true) as the modeled pattern, and a clear deprecation banner pointing readers at v0.9.5 for the caching landing. The Default Configuration Settings — Method 2 section was rewritten from the EnterpriseConfig::set_default pattern to the simpler get(k).and_then(..).unwrap_or(default) pattern. The troubleshooting tip about cached reads no longer recommends EnterpriseConfig for new code.
Why the data-model merger isn't in this release
The roadmap's original Phase 0.9.4 scope reads "Implement new unified Config combining the best of both". On closer reading the two surfaces are not source-compatible:
Config::get(&str) -> Option<&Value>(borrowed)EnterpriseConfig::get(&str) -> Option<Value>(owned clone, returned from behindArc<RwLock<BTreeMap>>)
Returning &Value requires the configuration data to live somewhere that yields a stable reference. The lock-free architecture that makes this safe under concurrent reads — ArcSwap<Arc<Value>> for whole-tree swap, DashMap or equivalent for resolved-key caching — is the explicit subject of Phase 0.9.5. Locking the unified Config::get return semantics ahead of that design would either:
- Bake in the current
Configsingle-threaded&Valuereturn and lose the cached-multi-thread storyEnterpriseConfighad, or - Switch to owned
Valuereturns and break every existingConfiguser.
Neither was acceptable. v0.9.4 therefore delivers everything that is safely shippable today (deprecation surface, ConfigOptions foundation, runnable migration model, exit-criteria-compliant README) and v0.9.5 will deliver the cache-backed unified Config::get that the architecture demands. The roadmap's Phase 0.9.4 section now explicitly notes this split, and Phase 0.9.5 is annotated as the merger landing point.
Breaking changes
None.
This release does not remove any public item, change any method signature, or alter any runtime behavior of code that does not opt into ConfigOptions::read_only. Adding read_only = true newly causes Config::set / remove / merge to return Err(Error::general("Configuration is read-only")) — but only on configurations explicitly constructed with that option. Default-constructed Config instances behave exactly as in v0.9.3.
Every existing call-site against EnterpriseConfig, ConfigManager, or enterprise::direct::* continues to compile. They emit deprecation warnings; the warnings are advisory and not promoted to errors anywhere in the crate's own CI gate.
Verification
cargo fmt --all -- --check # clean
cargo clippy --all-targets --all-features -- -D warnings # clean (zero warnings)
cargo test --all-features # 95 tests pass (63 unit + 14 integration + 11 validation + 7 doc)
cargo doc --no-deps --all-features # clean with RUSTDOCFLAGS="-D warnings"
cargo audit # zero vulnerabilities (one allowed rustls-pemfile unmaintained warning)
cargo run --example enterprise_demo # all five demos passAll green.
What's next
The remaining path to 1.0:
- 0.9.5 — Lock-free caching + Config/EnterpriseConfig data-model merger. Replace
Arc<RwLock<BTreeMap>>with a lock-free backend (DashMap or ArcSwap-of-HashMap, decided by criterion benchmark). LandConfig::cache_stats(), theConfigOptions::defaultsfield, and theConfig::make_read_only()ergonomic helper. Verify the sub-50ns single-key-cached-get claim across 1-16 threads by committedcriterionbenchmark. Retire the comparison-baselineenterprise_benchmarks.rsonce the new benches are committed. - 0.9.6 — Event-driven hot reload. Swap polling for
notify-backed file events on Linux/macOS/Windows. - 0.9.7 — Dependency hygiene + NOML/TOML opt-in. Move NOML/TOML out of default features. Delivers the deferred MSRV 1.75 commitment.
- 0.9.8 — Fuzz testing.
cargo-fuzzper parser, one CPU-hour clean each. - 0.9.9 — Documentation + release candidate.
1.0.0-rc.1cut. - 1.0.0 — Stable.
Installation
[dependencies]
config-lib = "0.9.4"
# With optional features
config-lib = { version = "0.9.4", features = ["json", "xml", "hcl", "validation"] }MSRV in 0.9.4: Rust 1.82 (lowering to 1.75 in 0.9.7 alongside NOML/TOML opt-in).
Documentation
- README
- API Reference
- Supported Formats
- Developer Guidelines
- Production Roadmap to 1.0
- Migration example (this release)
- CHANGELOG
Full diff: v0.9.3...v0.9.4.
Changelog: CHANGELOG.md.