qubit-value 0.10.0

Type-safe containers for single, multi-valued, and named runtime values
Documentation
// =============================================================================
//    Copyright (c) 2025 - 2026 Haixing Hu.
//
//    SPDX-License-Identifier: Apache-2.0
//
//    Licensed under the Apache License, Version 2.0.
// =============================================================================

#[cfg(feature = "big-decimal")]
use std::str::FromStr;

#[cfg(feature = "big-decimal")]
use bigdecimal::BigDecimal;
#[cfg(feature = "chrono")]
use chrono::NaiveDate;
#[cfg(feature = "big-integer")]
use num_bigint::BigInt;
#[cfg(feature = "converter")]
use qubit_datatype::{
    DataConversionError,
    DataConversionOptions,
    DataConversionTarget,
    DataConverter,
    DataType,
    DataTypeOf,
};
#[cfg(any(
    feature = "converter",
    feature = "chrono",
    feature = "big-integer",
    feature = "big-decimal",
    feature = "url",
    feature = "json",
))]
use qubit_value::MultiValues;
#[cfg(any(
    feature = "converter",
    feature = "chrono",
    feature = "big-integer",
    feature = "big-decimal",
    feature = "url",
    feature = "json",
    feature = "redact",
))]
use qubit_value::Value;
#[cfg(any(
    feature = "converter",
    feature = "chrono",
    feature = "big-integer",
    feature = "big-decimal",
    feature = "url",
    feature = "json"
))]
use qubit_value::{
    ValueContainer,
    ValueWirePayloadV1,
};
#[cfg(any(
    feature = "converter",
    feature = "chrono",
    feature = "big-integer",
    feature = "big-decimal",
    feature = "url",
    feature = "json",
))]
#[cfg(feature = "url")]
use url::Url;

#[cfg(feature = "redact")]
use std::collections::HashMap;

#[cfg(feature = "redact")]
use qubit_redact::{
    MaskPolicy,
    Redact as _,
    RedactionPolicy,
    Sensitivity,
};

#[cfg(any(
    feature = "converter",
    feature = "chrono",
    feature = "big-integer",
    feature = "big-decimal",
    feature = "url",
    feature = "json",
))]
fn assert_json_round_trip(value: impl Into<ValueContainer>) {
    let wire = ValueWirePayloadV1::try_from(value.into())
        .expect("construct wire payload");
    let encoded = serde_json::to_string(&wire).expect("serialize wire payload");
    let decoded: ValueWirePayloadV1 =
        serde_json::from_str(&encoded).expect("deserialize wire payload");
    assert_eq!(decoded, wire);
}

#[cfg(feature = "converter")]
#[test]
fn converter_feature_converts_core_values() {
    let scalar = ValueContainer::from(42_i32);
    let collection = ValueContainer::from(vec![43_i32, 44]);

    assert_eq!(scalar.to_first::<i64>().expect("convert scalar"), 42);
    assert_eq!(
        collection.to_list::<i64>().expect("convert collection"),
        vec![43, 44]
    );
    assert_json_round_trip(collection);
}

/// A downstream conversion target used to validate rs-value's public bounds.
#[cfg(feature = "converter")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Port(u16);

#[cfg(feature = "converter")]
impl DataTypeOf for Port {
    const DATA_TYPE: DataType = DataType::UInt16;
}

#[cfg(feature = "converter")]
impl DataConversionTarget for Port {
    fn convert_from(
        source: &DataConverter<'_>,
        options: &DataConversionOptions,
    ) -> Result<Self, DataConversionError> {
        u16::convert_from(source, options).map(Self)
    }
}

/// Verifies every value shape accepts a downstream-owned target directly.
#[cfg(feature = "converter")]
#[test]
fn converter_feature_accepts_target_side_extension() {
    assert_eq!(Value::from("8080").to::<Port>().unwrap(), Port(8080));
    assert_eq!(
        MultiValues::from(vec!["8080", "8081"])
            .to_list::<Port>()
            .unwrap(),
        vec![Port(8080), Port(8081)]
    );
    assert_eq!(
        ValueContainer::from("8082").to_first::<Port>().unwrap(),
        Port(8082)
    );
}

#[cfg(feature = "chrono")]
#[test]
fn chrono_feature_preserves_values_and_wire_payloads() {
    let date = NaiveDate::from_ymd_opt(2026, 7, 15).expect("valid date");
    let scalar = Value::Date(date);
    let collection = MultiValues::Date(vec![date]);

    assert_eq!(scalar.get::<NaiveDate>().expect("read date"), date);
    assert_eq!(collection.get_dates().expect("read dates"), &[date]);
    assert_json_round_trip(scalar);
    assert_json_round_trip(collection);
}

#[cfg(feature = "big-integer")]
#[test]
fn big_integer_feature_preserves_values_and_wire_payloads() {
    let integer = BigInt::from(123_456_789_i64);
    let integer_value = Value::BigInteger(integer.clone());
    let integers = MultiValues::BigInteger(vec![integer.clone()]);

    assert_eq!(
        integer_value.get::<BigInt>().expect("read big integer"),
        integer
    );
    assert_eq!(
        integers.get_bigintegers().expect("read big integers"),
        &[integer]
    );
    assert_json_round_trip(integer_value);
    assert_json_round_trip(integers);
}

#[cfg(feature = "big-decimal")]
#[test]
fn big_decimal_feature_preserves_values_and_wire_payloads() {
    let decimal = BigDecimal::from_str("123.4500").expect("valid decimal");
    let decimal_value = Value::BigDecimal(decimal.clone());
    let decimals = MultiValues::BigDecimal(vec![decimal.clone()]);

    assert_eq!(
        decimal_value.get::<BigDecimal>().expect("read big decimal"),
        decimal
    );
    assert_eq!(
        decimals.get_bigdecimals().expect("read big decimals"),
        &[decimal]
    );
    assert_json_round_trip(decimal_value);
    assert_json_round_trip(decimals);
}

#[cfg(feature = "url")]
#[test]
fn url_feature_preserves_values_and_wire_payloads() {
    let url = Url::parse("https://example.com/path?q=1").expect("valid URL");
    let scalar = Value::new(url.clone());
    let collection = MultiValues::Url(vec![url.clone()]);

    assert_eq!(scalar.get::<Url>().expect("read URL"), url);
    assert_eq!(collection.get_urls().expect("read URLs"), &[url]);
    assert_json_round_trip(scalar);
    assert_json_round_trip(collection);
}

#[cfg(feature = "json")]
#[test]
fn json_feature_preserves_values_and_wire_payloads() {
    let json = serde_json::json!({"nested": [true, 42]});
    let scalar = Value::Json(json.clone());
    let collection = MultiValues::Json(vec![json.clone()]);

    assert_eq!(
        scalar.get::<serde_json::Value>().expect("read JSON value"),
        json
    );
    assert_eq!(collection.get_jsons().expect("read JSON values"), &[json]);
    assert_json_round_trip(scalar);
    assert_json_round_trip(collection);
}

#[cfg(feature = "redact")]
#[test]
fn redact_feature_masks_sensitive_string_map_entries() {
    let value = Value::StringMap(HashMap::from([
        ("api_key".to_owned(), "raw-secret".to_owned()),
        ("label".to_owned(), "visible".to_owned()),
    ]));
    let policy = RedactionPolicy::builder()
        .raise("api_key", Sensitivity::Secret)
        .expect("the test builder input should be valid")
        .build()
        .expect("policy should build");

    let output = format!("{:?}", value.redacted_with(&policy));

    assert!(!output.contains("raw-secret"));
    assert!(output.contains("visible"));
}

#[cfg(feature = "redact")]
#[test]
fn redact_feature_masks_sensitive_named_non_strings_as_opaque_values() {
    let value =
        qubit_value::NamedValue::new("secret_number", Value::Int32(12345));
    let policy = RedactionPolicy::builder()
        .raise("secret_number", Sensitivity::Low)
        .expect("the test builder input should be valid")
        .mask(
            Sensitivity::Low,
            MaskPolicy::preserve_edges(1, 1, "OPAQUE", 0),
        )
        .expect("the test mask policy should be valid")
        .build()
        .expect("policy should build");

    let output = format!("{:?}", value.redacted_with(&policy));

    assert!(!output.contains("12345"), "{output}");
    assert!(output.contains("OPAQUE"), "{output}");
}

#[cfg(all(feature = "redact", feature = "json"))]
#[test]
fn redact_feature_recursively_masks_sensitive_json_object_entries() {
    let value = Value::Json(serde_json::json!({
        "profile": {
            "api_key": "nested-secret",
            "label": "visible"
        },
        "items": [
            { "token": "array-secret" },
            "unkeyed-value"
        ]
    }));
    let policy = RedactionPolicy::builder()
        .raise("api_key", Sensitivity::Secret)
        .expect("the test builder input should be valid")
        .raise("token", Sensitivity::Secret)
        .expect("the test builder input should be valid")
        .build()
        .expect("policy should build");

    let debug = format!("{:#?}", value.redacted_with(&policy));
    let display = format!("{}", value.redacted_with(&policy));

    assert!(!debug.contains("nested-secret"));
    assert!(!debug.contains("array-secret"));
    assert!(debug.contains("visible"));
    assert!(debug.contains("unkeyed-value"));
    assert!(!display.contains("nested-secret"));
    assert!(!display.contains("array-secret"));
    assert!(!display.contains('\n'));
}

#[cfg(all(feature = "converter", feature = "chrono"))]
#[test]
fn converter_chrono_features_convert_text_to_date() {
    let expected = NaiveDate::from_ymd_opt(2026, 7, 15).expect("valid date");
    assert_eq!(
        Value::from("2026-07-15")
            .to::<NaiveDate>()
            .expect("convert text to date"),
        expected
    );
}

#[cfg(all(feature = "converter", feature = "big-integer"))]
#[test]
fn converter_big_integer_features_convert_text_to_big_integer() {
    assert_eq!(
        Value::from("123456789")
            .to::<BigInt>()
            .expect("convert text to big integer"),
        BigInt::from(123_456_789_i64)
    );
}

#[cfg(all(feature = "converter", feature = "big-decimal"))]
#[test]
fn converter_big_decimal_features_convert_text_to_big_decimal() {
    assert_eq!(
        Value::from("123.4500")
            .to::<BigDecimal>()
            .expect("convert text to big decimal"),
        BigDecimal::from_str("123.4500").expect("valid decimal")
    );
}

#[cfg(all(feature = "converter", feature = "url"))]
#[test]
fn converter_url_features_convert_text_to_url() {
    let expected = Url::parse("https://example.com/path").expect("valid URL");
    assert_eq!(
        Value::from("https://example.com/path")
            .to::<Url>()
            .expect("convert text to URL"),
        expected
    );
}

#[cfg(all(feature = "converter", feature = "json"))]
#[test]
fn converter_json_features_convert_text_to_json() {
    assert_eq!(
        Value::from(r#"{"answer":42}"#)
            .to::<serde_json::Value>()
            .expect("convert text to JSON"),
        serde_json::json!({"answer": 42})
    );
}