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.
// =============================================================================
//! Incremental accounting deserialization into a JSON value tree.

use std::fmt::Debug;

use qubit_budget::ResourceQuantity;
use qubit_budget::json::JsonValueTransaction;
use serde::Deserializer;
use serde::de::DeserializeSeed;
use serde_json::Value;

use super::internal::JsonValueVisitor;

/// Serde seed that constructs a [`Value`] while accounting decoded resources.
///
/// Unlike lexical JSON admission, this seed observes values after a Serde
/// deserializer has decoded them. For a number, it charges `NumberBytes` using
/// the compact display length of the constructed [`serde_json::Number`], not
/// the original token length. It cannot inspect original number lexemes or
/// enforce text-level integer and floating-point range rules. Use
/// `JsonDecoder` when decoding JSON text requires those guarantees. This seed
/// remains suitable for decoded-value budget enforcement inside a type's
/// ordinary [`serde::Deserialize`] implementation, where the original input
/// bytes are unavailable.
///
/// Duplicate object keys use last-key-wins behavior by default. Call
/// [`Self::reject_duplicate_keys`] to reject duplicates throughout the value;
/// container and key budget checks still take precedence, and a duplicate's
/// value is not requested from the deserializer.
///
/// This seed stages measurements incrementally in the caller's transaction; it
/// does not commit or roll back that transaction. Any deserialization failure
/// may leave partial staged charges, and non-budget errors do not necessarily
/// poison the transaction. Drop the entire transaction after an error and start
/// a new transaction before retrying. This also discards earlier work staged in
/// that transaction.
///
/// Successful seed deserialization consumes one value, not necessarily a
/// complete document. Complete the deserializer's end-of-input check and any
/// required application validation before committing. If either fails, drop the
/// transaction.
///
/// Do not pass this seed to [`crate::decode::JsonDecoder::decode_seed_str`] or
/// [`crate::decode::JsonDecoder::decode_seed_utf8`] or
/// [`crate::decode::JsonDecoder::decode_unique_seed_utf8`] when its transaction
/// and the decoder represent the same logical decoded-value budget.
/// `JsonDecoder` already accounts the complete value during lexical admission,
/// so the seed would charge that value a second time. In that pipeline, use a
/// seed that performs only domain deserialization and domain-specific checks.
///
/// # Type Parameters
///
/// * `'transaction` - Lifetime of the mutable accounting transaction borrow.
/// * `'budget` - Lifetime of the budget state owned by that transaction.
/// * `R` - Resource identity tracked by the value transaction.
/// * `Q` - Quantity representation used for resource accounting.
///
/// # Examples
///
/// ```
/// use qubit_budget::json::{JsonResource, JsonValueBudget, JsonValueLimits};
/// use qubit_json::value::AccountingJsonValueSeed;
/// use serde::de::DeserializeSeed;
///
/// let mut budget = JsonValueBudget::new(JsonValueLimits::<JsonResource, usize>::default());
/// let mut transaction = budget.transaction();
/// let mut deserializer = serde_json::Deserializer::from_str(r#"{"ok":true}"#);
/// let value = AccountingJsonValueSeed::new(&mut transaction)
///     .reject_duplicate_keys()
///     .deserialize(&mut deserializer)?;
/// deserializer.end()?;
/// assert_eq!(value["ok"], true);
/// transaction.commit()?;
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
pub struct AccountingJsonValueSeed<'transaction, 'budget, R, Q = usize>
where
    Q: ResourceQuantity,
{
    /// Transaction receiving the decoded value's staged resource charges.
    transaction: &'transaction mut JsonValueTransaction<'budget, R, Q>,

    /// Whether duplicate decoded object keys are rejected before their values.
    reject_duplicate_keys: bool,
}

impl<'transaction, 'budget, R, Q> AccountingJsonValueSeed<'transaction, 'budget, R, Q>
where
    Q: ResourceQuantity,
{
    /// Creates a root seed using the supplied decoded-value transaction.
    ///
    /// # Parameters
    ///
    /// * `transaction` - Transaction receiving decoded JSON resource charges.
    ///   It must not duplicate decoded-value accounting already performed by an
    ///   outer [`crate::decode::JsonDecoder`].
    ///
    /// # Returns
    ///
    /// A seed that constructs one accounted [`Value`] tree.
    #[inline(always)]
    #[must_use]
    pub fn new(transaction: &'transaction mut JsonValueTransaction<'budget, R, Q>) -> Self {
        Self {
            transaction,
            reject_duplicate_keys: false,
        }
    }

    /// Rejects duplicate decoded object keys throughout the constructed value.
    ///
    /// Keys are compared after decoding, without Unicode normalization.
    /// Existing container and key budget checks run before duplicate
    /// detection. A duplicate is rejected before its value is requested
    /// from the deserializer.
    ///
    /// # Returns
    ///
    /// This seed configured to reject duplicate object keys. After any error,
    /// discard the entire transaction; this method does not change commit
    /// rules.
    #[must_use]
    #[inline]
    pub fn reject_duplicate_keys(mut self) -> Self {
        self.reject_duplicate_keys = true;
        self
    }
}

impl<'de, R, Q> DeserializeSeed<'de> for AccountingJsonValueSeed<'_, '_, R, Q>
where
    R: Clone + Debug,
    Q: ResourceQuantity,
{
    /// JSON value tree produced by deserializing with this accounting seed.
    type Value = Value;

    /// Builds one value through the accounting visitor and stages its resource
    /// charges.
    ///
    /// # Parameters
    ///
    /// * `deserializer` - Serde input that supplies the value to decode.
    ///
    /// # Type Parameters
    ///
    /// * `D` - Deserializer that supplies the input and determines its error
    ///   type.
    ///
    /// # Returns
    ///
    /// The decoded [`Value`]. Its resource charges remain staged in the
    /// transaction until the caller commits that transaction.
    ///
    /// # Errors
    ///
    /// Returns the deserializer's error when the input cannot be represented
    /// as a JSON value or when the transaction rejects a resource charge. An
    /// error may leave partial charges staged, and a non-budget error does not
    /// necessarily poison the transaction. The caller must drop the entire
    /// transaction after an error, including when the deserializer's later
    /// end-of-input check or application validation fails; this seed does not
    /// roll back or commit caller-owned transaction state.
    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
    where
        D: Deserializer<'de>,
    {
        deserializer.deserialize_any(JsonValueVisitor::new(
            self.transaction,
            1,
            self.reject_duplicate_keys,
        ))
    }
}