qubit-value 0.13.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.
// =============================================================================

use std::io;
use std::io::Write;

use qubit_budget::BudgetError;
use qubit_budget::MeasuredBudgetError;
use qubit_budget::Observation;
use qubit_budget::QuantityConversionError;
use qubit_budget::QuantityMeasurement;
use qubit_budget::json::JsonResource;
use qubit_datatype::DataType;
use qubit_json::encode::JsonEncodeError;
use qubit_json::encode::JsonEncoder;
use qubit_json::encode::JsonIntegerSignedness;
use qubit_json::encode::JsonSerializationErrorKind;
use qubit_value::Value;
use qubit_value::ValueWireEncodeError;
use qubit_value::ValueWirePayloadV1;
use serde::Serialize;
use serde::Serializer;
use serde::ser::SerializeStruct;

/// Mimics a raw JSON payload with invalid text to exercise syntax validation.
struct InvalidRawValue;

impl Serialize for InvalidRawValue {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let token = concat!("$", "serde_json", ":", ":private::RawValue");
        let mut state = serializer.serialize_struct(token, 1)?;
        state.serialize_field(token, "[")?;
        state.end()
    }
}

/// Rejects every write to exercise encoder I/O error conversion.
struct RejectingWriter;

impl Write for RejectingWriter {
    fn write(&mut self, _buffer: &[u8]) -> io::Result<usize> {
        Err(io::Error::other("rejected"))
    }

    #[inline]
    fn flush(&mut self) -> io::Result<()> {
        Ok(())
    }
}

/// Verifies V1 construction rejects float values JSON cannot represent.
#[test]
fn test_value_wire_encode_error_rejects_non_finite_float() {
    let result = ValueWirePayloadV1::try_from(Value::Float64(f64::NAN));

    assert!(
        matches!(
            result,
            Err(ValueWireEncodeError::NonFiniteFloat {
                data_type: DataType::Float64,
            })
        ),
        "non-finite Float64 must preserve its data type in the wire error"
    );
}

/// Verifies wire conversion preserves the shared structured serialization
/// failure without introducing backend diagnostic text.
#[test]
fn test_value_wire_encode_error_preserves_serialization_kind() {
    let source = JsonEncoder::unlimited()
        .to_vec(&u128::MAX)
        .expect_err("wide integer must fail JSON serialization");
    let error = ValueWireEncodeError::from(source);

    assert!(
        matches!(
            error,
            ValueWireEncodeError::Json(source)
                if source.kind() == JsonSerializationErrorKind::IntegerOutOfRange {
                    signedness: JsonIntegerSignedness::Unsigned,
                }
        ),
        "wire conversion must preserve the unsigned integer range error kind"
    );
}

/// Verifies every non-budget owned encoder source keeps its wire-error shape.
#[test]
fn test_value_wire_encode_error_maps_owned_encoder_sources() {
    let invalid_raw: JsonEncodeError<JsonResource> = JsonEncoder::unlimited()
        .to_vec(&InvalidRawValue)
        .expect_err("invalid RawValue text must fail");
    let mut rejecting = RejectingWriter;
    let writer: JsonEncodeError<JsonResource> = JsonEncoder::unlimited()
        .write_buffered(&mut rejecting, &true)
        .expect_err("rejecting writer must fail");

    assert!(
        matches!(
            ValueWireEncodeError::from(invalid_raw),
            ValueWireEncodeError::Syntax(_)
        ),
        "invalid raw JSON must map to the syntax error variant"
    );
    assert!(
        matches!(
            ValueWireEncodeError::from(writer),
            ValueWireEncodeError::Io(_)
        ),
        "writer failure must map to the I/O error variant"
    );
}

#[test]
fn test_value_wire_encode_error_maps_measured_budget_failure_precisely() {
    let budget = MeasuredBudgetError::Budget(BudgetError::LimitExceeded {
        resource: JsonResource::OutputBytes,
        observed: Observation::Exact(2_usize),
        maximum: 1_usize,
    });
    let budget_error = ValueWireEncodeError::from(budget);
    assert!(
        matches!(
            budget_error,
            ValueWireEncodeError::Budget(BudgetError::LimitExceeded {
                resource: JsonResource::OutputBytes,
                observed: Observation::Exact(2),
                maximum: 1,
            })
        ),
        "budget conversion must preserve the resource, observation, and maximum"
    );
}

#[test]
fn test_value_wire_encode_error_maps_quantity_failure_precisely() {
    let source = QuantityConversionError::new(QuantityMeasurement::U64(u64::MAX), "usize");
    let quantity =
        MeasuredBudgetError::<JsonResource, usize>::quantity(JsonResource::NumberBytes, source);
    let quantity_error = ValueWireEncodeError::from(quantity);
    assert!(
        matches!(
            quantity_error,
            ValueWireEncodeError::Quantity {
                resource: JsonResource::NumberBytes,
                source: actual,
            } if actual == source
        ),
        "measured quantity conversion must preserve the resource and source"
    );
}

#[test]
fn test_value_wire_encode_error_maps_encoder_quantity_failure_precisely() {
    let source = QuantityConversionError::new(QuantityMeasurement::U64(u64::MAX), "usize");
    let measured =
        MeasuredBudgetError::<JsonResource, usize>::quantity(JsonResource::StringBytes, source);
    let encoder_error = JsonEncodeError::from(measured);

    let wire_error = ValueWireEncodeError::from(encoder_error);

    assert!(
        matches!(
            wire_error,
            ValueWireEncodeError::Quantity {
                resource: JsonResource::StringBytes,
                source: actual,
            } if actual == source
        ),
        "encoder quantity conversion must preserve the resource and source"
    );
}