qubit-value 0.12.2

Type-safe containers for single, multi-valued, and named runtime values
Documentation

Qubit Value

Rust CI Coverage Crates.io Rust License 中文文档

qubit-value gives Rust applications one type-safe boundary for values that are known only at runtime. It is useful when configuration, metadata, protocol fields, or user input can be a boolean, number, string, date, collection, or structured JSON value, but the application still needs explicit types, controlled conversion, and predictable errors.

The problem it solves

Without a shared runtime value model, each key-value subsystem tends to invent its own enum, conversion rules, unset semantics, and serialization format. That creates three recurring problems:

  • a missing value, an explicitly empty collection, and JSON null are easily confused;
  • a one-item collection can be accidentally treated as a scalar;
  • values crossing a process or storage boundary lose their runtime type, or accept conversions that were never intended.

Value stores one typed scalar, MultiValues stores one homogeneous collection, and ValueContainer preserves the explicit scalar-or-collection shape. Unset(DataType) retains the declared type without pretending that a concrete value exists.

Quick start: a small runtime configuration map

This is a small configuration-like map: each key stores a different Value, then the reader chooses strict access, explicit conversion, or a typed default. The snippet assumes it is inside a function that returns a compatible Result, so ? can propagate value errors.

use std::collections::HashMap;
use std::time::Duration;

use qubit_datatype::DataType;
use qubit_value::Value;

let config = HashMap::from([
    ("host".to_owned(), Value::new("localhost".to_owned())),
    ("port".to_owned(), Value::new("8080".to_owned())),
    ("debug".to_owned(), Value::new(false)),
    (
        "timeout".to_owned(),
        Value::new_unset(DataType::Duration),
    ),
]);

let host: String = config["host"].get()?;
assert_eq!(host, "localhost");

let port: u16 = config["port"].to()?;
assert_eq!(port, 8080);

let debug: bool = config["debug"].get()?;
assert!(!debug);

let timeout: Duration = config["timeout"].get_or(Duration::from_secs(30))?;
assert_eq!(timeout, Duration::from_secs(30));

If you need a complete, general-purpose configuration object instead of assembling a map yourself, use Config from rs-config. It builds on Value and adds higher-level capabilities such as property management, typed and multi-value reads, defaults, sections, conversion policies, interpolation, and pluggable file/environment configuration sources.

get() is a strict type read: it does not silently convert. to() uses the shared conversion rules from qubit-datatype; failed conversions remain errors. The converter feature is required for to() and to_or(); get_or() only supplies a fallback for an unset value and does not convert.

Use to_with when the boundary needs an explicit policy and limits. Every to_with call creates a fresh ConversionSession, so independent reads do not share cumulative consumption:

use qubit_datatype::ConversionLimits;
use qubit_datatype::ConversionPolicy;
use qubit_value::Value;

let policy = ConversionPolicy::env_friendly();
let limits = ConversionLimits::default();
let first = Value::new(" 8080 ").to_with::<u16>(&policy, &limits)?;
let second = Value::new(" 8081 ").to_with::<u16>(&policy, &limits)?;
assert_eq!((first, second), (8080, 8081));

Installation

Add the core crate and its type vocabulary to Cargo.toml:

[dependencies]
qubit-value = { version = "0.12", features = ["converter"] }
qubit-datatype = { version = "0.14", default-features = false, features = ["converter"] }

The quick-start example uses Value::to, so it enables converter. The default feature set is empty; enable only the families you use:

Feature Additional DataType or capability
converter Cross-type conversion APIs such as Value::to
chrono Date, Time, DateTime, and Instant
big-integer BigInteger backed by num_bigint::BigInt
big-decimal BigDecimal backed by bigdecimal::BigDecimal
big-number Compatibility alias for both big-number features
url Url backed by url::Url
json Json backed by serde_json::Value and bounded versioned JSON Wire encoding/decoding
natural-json Convenience alias for converter + json; their combination also enables Natural JSON
redact Policy-aware redacted views through qubit-redact
all converter, chrono, big-number, url, json, natural-json, and redact

What it provides

  • Value and MultiValues have typed constructors, typed getters, generic mutation, borrowed reads, and explicit unset state.
  • view() exposes borrowed runtime variants for adapters that inspect values without cloning strings, maps, JSON trees, or collection buffers.
  • ValueContainer::Scalar and ValueContainer::Collection preserve shape; a one-item collection remains a collection.
  • get_or/to_or and collection variants make fallback behavior explicit: unset values can use defaults, while concrete empty first-item reads, missing collection items, type mismatches, and ordinary conversion failures remain errors.
  • NamedValue and NamedMultiValues attach a key to a runtime value without changing the value's type semantics.
  • ValueWireV1 provides a versioned, type-preserving JSON representation with bounded to_json_vec() and to_json_writer() entry points; explicit JsonDecodeLimits and JsonEncodeLimits are accepted by the corresponding directional _with_limits methods. Decode uses a caller-configured JsonDecodeSession; encode uses JsonEncodeSession to enforce structure and output bytes online. Use Wire V1 when the receiver must reconstruct the exact DataType and shape.
  • Natural JSON helpers produce ordinary null, scalar, object, and array values when runtime type tags are not wanted.
  • The runtime vocabulary currently contains 25 DataType variants. Concrete feature-gated values require the corresponding feature, although their unset type declarations remain available.

This crate does not provide a complete configuration store, schema registry, file format, or distributed cache. It provides the typed value layer those systems can build on. Its Eq/Hash implementations are suitable for in-memory Rust collections, not persistent fingerprints or distributed-cache keys. The user guide documents the complete type table, JSON number contract, errors, resource limits, and feature compatibility.

Built on Value

Two sibling crates use this value model for key-value containers:

  • rs-config provides typed configuration properties, configuration-file and environment-oriented access, and policy-controlled reads. Use it when the key-value data describes application configuration.
  • rs-metadata provides typed metadata/property storage and filtering. Use it when values describe resources, records, or searchable application metadata.

Wire V1 or Natural JSON?

Choose Wire V1 when a receiver must distinguish Int32(42) from String("42"), preserve scalar versus collection shape, or retain Unset(DataType). A typical document is:

{"version":1,"value":{"scalar":{"int32":42}}}

Choose to_json_value() when the boundary is ordinary application JSON and the receiver only needs JSON semantics. Wire V1 is closed and versioned; Natural JSON intentionally omits runtime type tags. The user guide contains the full Wire workflow, borrowed payload examples, feature compatibility rules, and resource-limit handling.

The Wire DTOs implement Serialize, but deliberately do not implement generic Deserialize: a general Serde deserializer cannot enforce the raw-input and structural limits required at an untrusted boundary. Use the bounded ValueWireV1::decode_json_slice helpers for complete JSON documents. For an embedded value, pass ValueWireV1Seed::new() or ValueWirePayloadV1Seed::new() to the surrounding JsonDecoder::decode_seed_utf8 or next_value_seed call so the outer protocol owns one shared budget.

NamedValue and NamedMultiValues do implement generic Deserialize so they can be embedded in larger Serde documents. That implementation validates the V1 schema and payload shape but inherits resource accounting from the supplied deserializer. Use their bounded decode_json_slice helpers for complete, untrusted JSON input, or a resource-bounded outer decoder when they are nested.

Natural JSON cannot reconstruct DataType, unset state, or scalar-versus- collection shape. Wire V1 rejects non-finite floats and unsupported or malformed payloads instead of guessing. Use a fresh bounded session for each independent Wire operation; reuse a session only when several embedded values belong to the same outer request budget.

For conversion fallbacks, a policy-missing scalar may use the supplied default. A missing item inside a concrete collection never does, including item zero; an explicitly empty collection also remains an error for first-item reads.

Learn more

Testing

# Run tests with the default feature set
cargo test

# Run tests with all declared features
cargo test --all-features

# Project CI checks
./ci-check.sh

# Check code coverage
./coverage.sh

License

Copyright (c) 2025 - 2026. Haixing Hu. All rights reserved.

Licensed under the Apache License, Version 2.0. See LICENSE for the full license text.

Contributing

Contributions are welcome. Please follow the Rust API guidelines, keep public API documentation and tests current, and run ./align-ci.sh to format code and ./ci-check.sh to satisfy CI requirements before submitting a pull request.

Author

Haixing Hu - Qubit Co. Ltd.

Repository: https://github.com/qubit-ltd/rs-value