qubit-json 0.12.3

Resource-aware infrastructure for lenient and strict JSON processing
Documentation
// =============================================================================
//    Copyright (c) 2026 Haixing Hu.
//
//    SPDX-License-Identifier: Apache-2.0
//
//    Licensed under the Apache License, Version 2.0.
// =============================================================================
//! Cross-API composition and compatibility contract tests.

use std::cell::Cell;
use std::convert::Infallible;
use std::io::Cursor;

use qubit_budget::json::JsonDecodeLimits;
use qubit_budget::json::JsonResource;
use qubit_budget::json::JsonValueBudget;
use qubit_budget::json::JsonValueLimits;
#[cfg(feature = "jcs")]
use qubit_json::decode::JcsDecoder;
use qubit_json::decode::JsonDecodeErrorKind;
use qubit_json::decode::JsonDecodeNumberModel;
use qubit_json::decode::JsonDecoder;
use qubit_json::decode::JsonMaterializationErrorReason;
use qubit_json::decode::JsonNumberPolicy;
use qubit_json::decode::JsonValidatedDecodeError;
use qubit_json::decode::JsonValidatedDecodeOptions;
use qubit_json::encode::JsonEncoder;
use qubit_json::encode::JsonIntegerSignedness;
use qubit_json::encode::JsonSerializationErrorKind;
use qubit_json::format::JsonPrettyOptions;
use qubit_json::format::format_json_tokens;
use qubit_json::value::JsonValueEncoder;
#[cfg(feature = "jcs")]
use qubit_json::value::SafeJsonValue;
use serde::Deserialize;
use serde::Deserializer;
use serde::de::DeserializeSeed;
use serde_json::Number;
use serde_json::Value;
use serde_json::from_str;
use serde_json::json;
use serde_json::to_vec;

struct SeedCallProbe<'a>(&'a Cell<bool>);

impl<'de> DeserializeSeed<'de> for SeedCallProbe<'_> {
    type Value = Value;

    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
    where
        D: Deserializer<'de>,
    {
        self.0.set(true);
        Deserialize::deserialize(deserializer)
    }
}

#[test]
fn test_unique_seed_keeps_admission_precedence_and_reuses_after_materialization_error() {
    let called = Cell::new(false);
    let mut decoder = JsonDecoder::unlimited();

    let syntax_error = decoder
        .decode_unique_seed_utf8(SeedCallProbe(&called), br#"{"x":0,"x":1,"tail":]"#)
        .unwrap_err();
    assert_eq!(syntax_error.kind(), JsonDecodeErrorKind::InvalidJson);
    assert_eq!(syntax_error.materialization_error_reason(), None);
    assert!(
        !called.get(),
        "seed must not run before full lexical admission"
    );

    let duplicate_error = decoder
        .decode_unique_seed_utf8(SeedCallProbe(&called), br#"{"x":0,"x":1,"tail":null}"#)
        .unwrap_err();
    assert_eq!(
        duplicate_error.materialization_error_reason(),
        Some(JsonMaterializationErrorReason::DuplicateKey)
    );
    assert!(
        !called.get(),
        "duplicate-key materialization failure must stop before seed invocation"
    );

    let limits = JsonDecodeLimits::<JsonResource, usize>::builder()
        .max_number_bytes(1)
        .build();
    let mut budget_decoder = JsonDecoder::with_limits(limits);
    let budget_error = budget_decoder
        .decode_unique_seed_utf8(SeedCallProbe(&called), br#"{"x":0,"x":1,"later":123}"#)
        .unwrap_err();
    assert_eq!(budget_error.kind(), JsonDecodeErrorKind::Budget);
    assert!(
        !called.get(),
        "later admission failure must precede seed invocation"
    );

    assert_eq!(
        decoder
            .decode_unique_seed_utf8(SeedCallProbe(&called), b"true")
            .unwrap(),
        Value::Bool(true)
    );
    assert!(
        called.get(),
        "a later successful decode must invoke the seed"
    );
}

#[test]
fn test_failed_policy_keeps_input_charge_and_next_document_commits_exact_values() {
    let limits = JsonDecodeLimits::<JsonResource, usize>::builder()
        .max_input_bytes(9)
        .max_nodes(2)
        .max_payload_bytes(2)
        .build();
    let mut decoder = JsonDecoder::with_limits(limits);
    let options = JsonValidatedDecodeOptions::builder().build();

    let failed = decoder
        .decode_unique_validated_str("[1]", &options, |_, _| Err("policy failure"))
        .unwrap_err();
    assert!(matches!(failed, JsonValidatedDecodeError::Policy { .. }));
    assert_eq!(decoder.session().input_budget().unwrap().used(), 3);
    assert_eq!(decoder.session().value_budget().used_nodes(), Some(0));
    assert_eq!(
        decoder.session().value_budget().used_payload_bytes(),
        Some(0)
    );

    let owner = decoder.decode_value_str("[\"é\"]").unwrap();
    assert_eq!(owner.as_value(), &json!(["é"]));
    assert_eq!(decoder.session().input_budget().unwrap().used(), 9);
    assert_eq!(decoder.session().value_budget().used_nodes(), Some(2));
    assert_eq!(
        decoder.session().value_budget().used_payload_bytes(),
        Some(2)
    );
}

#[test]
fn test_typed_fallback_keeps_duplicate_map_semantics() {
    let mut input = Cursor::new(br#"{"wide":18446744073709551616,"x":1,"x":2}"#.to_vec());
    let value: Value = JsonDecoder::unlimited()
        .decode_input_with_number_model(
            &mut input,
            JsonDecodeNumberModel::Integer64WithFiniteFallback,
        )
        .unwrap();
    assert_eq!(value["x"], json!(2));
    assert!(value["wide"].as_f64().unwrap().is_finite());
}

#[test]
fn test_typed_fallback_matches_active_serde_representation() {
    let source = "18446744073709551617";
    let expected: Value = from_str(source).unwrap();
    let mut input = Cursor::new(source.as_bytes());
    let actual: Value = JsonDecoder::unlimited()
        .decode_input_with_number_model(
            &mut input,
            JsonDecodeNumberModel::Integer64WithFiniteFallback,
        )
        .unwrap();
    assert_eq!(actual.to_string(), expected.to_string());
    assert_eq!(actual.is_f64(), expected.is_f64());

    let encoded = JsonEncoder::unlimited().to_vec(&actual);
    let expected_text = expected.to_string();
    if expected_text == source {
        let error = encoded.expect_err("the preserved wide integer is not strictly encodable");
        assert_eq!(
            error.serialization_error().unwrap().kind(),
            JsonSerializationErrorKind::IntegerOutOfRange {
                signedness: JsonIntegerSignedness::Unsigned,
            },
        );
    } else {
        let rounded = Value::Number(Number::from_f64(source.parse::<f64>().unwrap()).unwrap());
        assert_eq!(actual.to_string(), rounded.to_string());
        assert_eq!(encoded.unwrap(), to_vec(&rounded).unwrap());
    }
}

#[test]
fn test_validated_fallback_materializes_encodable_f64() {
    let source = "18446744073709551617";
    let options = JsonValidatedDecodeOptions::builder()
        .number_model(JsonDecodeNumberModel::Integer64WithFiniteFallback)
        .build();
    let actual = JsonDecoder::unlimited()
        .decode_unique_validated_str(source, &options, |_, _| {
            Ok::<_, Infallible>(JsonNumberPolicy::Preserve)
        })
        .unwrap();
    let expected = Value::Number(Number::from_f64(source.parse::<f64>().unwrap()).unwrap());
    assert!(actual.as_value().is_f64());
    assert_eq!(actual.as_value().to_string(), expected.to_string());
    assert_eq!(
        JsonEncoder::unlimited().to_vec(&actual).unwrap(),
        to_vec(&expected).unwrap(),
    );
}

#[test]
fn test_guarded_value_composes_with_encoder_and_token_formatter() {
    let source = json!({"ok": true, "items": [1, 2]});
    let limits = JsonValueLimits::<JsonResource, usize>::builder()
        .max_nodes(5)
        .build();
    let mut budget = JsonValueBudget::new(limits);
    let mut transaction = budget.transaction();
    let owner = JsonValueEncoder::new()
        .encode_in_transaction(&source, &mut transaction)
        .unwrap();
    transaction.commit().unwrap();
    assert_eq!(budget.used_nodes(), Some(5));

    let encoded = JsonEncoder::unlimited().to_vec(&owner).unwrap();
    let options = JsonPrettyOptions::builder().build().unwrap();
    let pretty = format_json_tokens(&encoded, &options, None).unwrap();
    let reparsed = JsonDecoder::unlimited().decode_value_utf8(&pretty).unwrap();
    assert_eq!(owner, reparsed);
}

#[test]
fn test_value_decoder_owner_composes_with_text_encoder_and_formatter() {
    let owner = JsonDecoder::unlimited()
        .decode_value_str(r#"{"ok":true,"items":[1,2]}"#)
        .unwrap();
    let encoded = JsonEncoder::unlimited().to_vec(&owner).unwrap();
    let options = JsonPrettyOptions::builder().build().unwrap();
    let pretty = format_json_tokens(&encoded, &options, None).unwrap();
    let reparsed = JsonDecoder::unlimited().decode_value_utf8(&pretty).unwrap();
    assert_eq!(owner, reparsed);
}

#[cfg(feature = "jcs")]
#[test]
fn test_ordinary_integer_jcs_binary64_and_pretty_tokens_remain_separate() {
    let ordinary = JsonDecoder::unlimited()
        .decode_value_str("9007199254740993")
        .unwrap();
    assert_eq!(ordinary.as_value().as_u64(), Some(9_007_199_254_740_993));

    let jcs = JcsDecoder::unlimited()
        .decode_str("9007199254740993")
        .unwrap();
    assert_eq!(jcs.as_f64(), Some(9_007_199_254_740_992.0));
    let jcs = SafeJsonValue::new(jcs);
    assert!(jcs.as_value().is_number());

    let options = JsonPrettyOptions::builder().build().unwrap();
    assert_eq!(
        format_json_tokens(br#"{"n":1e0}"#, &options, None).unwrap(),
        b"{\n  \"n\": 1e0\n}"
    );
}