qubit-value 0.12.1

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 = "redact")]
use std::collections::HashMap;
#[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::AdmittedConversion;
#[cfg(feature = "converter")]
use qubit_datatype::DataConversionError;
#[cfg(feature = "converter")]
use qubit_datatype::DataConversionTarget;
#[cfg(feature = "converter")]
use qubit_datatype::DataType;
#[cfg(feature = "converter")]
use qubit_datatype::DataTypeOf;
#[cfg(feature = "redact")]
use qubit_redact::MaskPolicy;
#[cfg(feature = "redact")]
use qubit_redact::Redact;
#[cfg(feature = "redact")]
use qubit_redact::RedactionCompletion;
#[cfg(feature = "redact")]
use qubit_redact::RedactionPolicy;
#[cfg(feature = "redact")]
use qubit_redact::Redactor;
#[cfg(feature = "redact")]
use qubit_redact::Sensitivity;

#[cfg(feature = "redact")]
fn redacted_text<T: Redact>(value: &T, policy: &RedactionPolicy) -> String {
    Redactor::new(policy.clone())
        .redact_text(value)
        .into_complete_text()
        .expect("test output must be complete")
        .into_string()
}
#[cfg(any(
    feature = "converter",
    feature = "chrono",
    feature = "big-integer",
    feature = "big-decimal",
    feature = "url",
    feature = "json",
    feature = "redact",
))]
use qubit_value::MultiValues;
#[cfg(feature = "redact")]
use qubit_value::NamedValue;
#[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;
#[cfg(any(
    feature = "converter",
    feature = "chrono",
    feature = "big-integer",
    feature = "big-decimal",
    feature = "url",
    feature = "json"
))]
use qubit_value::ValueWirePayloadV1;
#[cfg(any(
    feature = "converter",
    feature = "chrono",
    feature = "big-integer",
    feature = "big-decimal",
    feature = "url",
    feature = "json",
))]
#[cfg(feature = "url")]
use url::Url;

#[cfg(any(
    feature = "converter",
    feature = "chrono",
    feature = "big-integer",
    feature = "big-decimal",
    feature = "url",
    feature = "json",
))]
fn assert_wire_serialization(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");
    #[cfg(feature = "json")]
    {
        let decoded = ValueWirePayloadV1::decode_json_slice(_encoded.as_bytes()).expect("deserialize wire payload");
        assert_eq!(decoded, wire);
    }
}

#[cfg(feature = "converter")]
#[test]
fn test_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_wire_serialization(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(input: AdmittedConversion<'_, '_, '_>) -> Result<Self, DataConversionError> {
        input.convert::<u16>().map(Self)
    }
}

/// Verifies every value shape accepts a downstream-owned target directly.
#[cfg(feature = "converter")]
#[test]
fn test_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 test_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_wire_serialization(scalar);
    assert_wire_serialization(collection);
}

#[cfg(feature = "big-integer")]
#[test]
fn test_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_wire_serialization(integer_value);
    assert_wire_serialization(integers);
}

#[cfg(feature = "big-decimal")]
#[test]
fn test_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_wire_serialization(decimal_value);
    assert_wire_serialization(decimals);
}

#[cfg(feature = "url")]
#[test]
fn test_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_wire_serialization(scalar);
    assert_wire_serialization(collection);
}

#[cfg(feature = "json")]
#[test]
fn test_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_wire_serialization(scalar);
    assert_wire_serialization(collection);
}

#[cfg(feature = "redact")]
#[test]
fn test_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()
        .fields(|fields| {
            fields.raise("api_key", Sensitivity::Secret);
        })
        .expect("the test builder input should be valid")
        .build()
        .expect("policy should build");

    let output = redacted_text(&value, &policy);

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

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

    let output = redacted_text(&value, &policy);

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

#[cfg(all(feature = "redact", feature = "json"))]
#[test]
fn test_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()
        .fields(|fields| {
            fields
                .raise("api_key", Sensitivity::Secret)
                .raise("token", Sensitivity::Secret);
        })
        .expect("the test builder input should be valid")
        .build()
        .expect("policy should build");

    let debug = redacted_text(&value, &policy);
    let display = debug.clone();

    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(feature = "redact")]
#[test]
fn test_redact_feature_stops_before_unadmitted_collection_elements() {
    let values = MultiValues::String(vec!["visible".to_owned(), "must-not-be-formatted".to_owned()]);
    let policy = RedactionPolicy::builder()
        .limits(|limits| {
            limits.max_nodes(64).max_collection_items(1).max_depth(8);
        })
        .expect("the test domain limits should build a policy")
        .build()
        .expect("the test domain limits should build a policy");

    let result = Redactor::new(policy).redact_text(&values);
    assert_eq!(result.summary().completion(), RedactionCompletion::Truncated);
    let output = result.text().as_str();

    assert!(output.contains("visible"), "{output}");
    assert!(!output.contains("must-not-be-formatted"), "{output}");
    assert!(output.contains("<truncated>"), "{output}");
}

#[cfg(all(feature = "converter", feature = "chrono"))]
#[test]
fn test_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 test_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 test_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 test_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 test_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})
    );
}

/// The compatibility combination and its alias expose the same bounded
/// projection.
#[cfg(all(feature = "converter", feature = "json"))]
#[test]
fn test_natural_json_feature_combination_preserves_shape() {
    let scalar = ValueContainer::from("one");
    let collection = ValueContainer::from(vec!["one".to_owned()]);
    assert_eq!(
        scalar.to_json_value().expect("scalar projection"),
        serde_json::json!("one")
    );
    assert_eq!(
        collection.to_json_value().expect("collection projection"),
        serde_json::json!(["one"])
    );
}