Skip to content

v0.9.4 — API Maturity

Pre-release
Pre-release

Choose a tag to compare

@jamesgober jamesgober released this 19 May 14:04

config-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 inside enterprise.rs, ConfigManager, the re-exports in lib.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 behind Arc<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 Config single-threaded &Value return and lose the cached-multi-thread story EnterpriseConfig had, or
  • Switch to owned Value returns and break every existing Config user.

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 pass

All 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). Land Config::cache_stats(), the ConfigOptions::defaults field, and the Config::make_read_only() ergonomic helper. Verify the sub-50ns single-key-cached-get claim across 1-16 threads by committed criterion benchmark. Retire the comparison-baseline enterprise_benchmarks.rs once 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-fuzz per parser, one CPU-hour clean each.
  • 0.9.9 — Documentation + release candidate. 1.0.0-rc.1 cut.
  • 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


Full diff: v0.9.3...v0.9.4.
Changelog: CHANGELOG.md.