Skip to main content

qubit_json/value/
json_value_encoder.rs

1// =============================================================================
2//    Copyright (c) 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Strict projection from Serde values into materialized JSON values.
9
10use qubit_budget::ResourceQuantity;
11use qubit_budget::json::JsonResource;
12use qubit_budget::json::JsonValueBudget;
13use qubit_budget::json::JsonValueLimits;
14use qubit_budget::json::JsonValueTransaction;
15use serde::Serialize;
16use serde_json::Value;
17
18use self::internal::JsonValueEncodeContext;
19use self::internal::JsonValueEncodeFailure;
20use self::internal::JsonValueSerializer;
21use crate::encode::JsonSerializationError;
22use crate::value::JsonValueEncodeError;
23use crate::value::SafeJsonValue;
24
25mod internal;
26
27/// Projects serializable values into strict materialized JSON.
28///
29/// The encoder accepts the full signed and unsigned 64-bit JSON integer range,
30/// rejects wider numeric values and non-finite floats, validates map keys, and
31/// rejects duplicate object keys. It owns no resource budget; use
32/// [`crate::encode::JsonEncoder`] when encoded text and resource accounting are
33/// required. [`Self::encode_in_transaction`] stages native JSON measurements
34/// and returns a [`SafeJsonValue`] protected until the caller explicitly
35/// unwraps it. This construction-time protection does not extend to recursive
36/// cloning or destruction of a bare [`Value`], and taking ownership of a bare
37/// value does not grant budget admission.
38///
39/// # Examples
40///
41/// ```
42/// use qubit_json::value::JsonValueEncoder;
43/// use serde_json::json;
44///
45/// let encoder = JsonValueEncoder::new();
46/// let value = encoder.encode(&json!({"ok": true}))?;
47/// assert_eq!(value, json!({"ok": true}));
48/// # Ok::<(), qubit_json::encode::JsonSerializationError>(())
49/// ```
50#[derive(Debug, Clone, Copy, Default)]
51pub struct JsonValueEncoder {
52    /// Prevents external struct literals while reserving room for future
53    /// policy.
54    private: (),
55}
56
57impl JsonValueEncoder {
58    /// Creates a strict encoder with the default immutable policy.
59    ///
60    /// # Returns
61    ///
62    /// A reusable encoder that performs no resource accounting.
63    #[must_use]
64    #[inline(always)]
65    pub const fn new() -> Self {
66        Self { private: () }
67    }
68
69    /// Projects one serializable value into strict JSON.
70    ///
71    /// # Type Parameters
72    ///
73    /// * `T` - Source type traversed through its Serde representation.
74    ///
75    /// # Parameters
76    ///
77    /// * `value` - Borrowed value to project without taking ownership.
78    ///
79    /// # Returns
80    ///
81    /// The fully materialized, bare [`Value`]. Any protection used while
82    /// constructing it does not extend to recursive destruction or cloning of
83    /// this tree, and taking ownership of it does not mean it passed a caller's
84    /// resource budget. Use [`SafeJsonValue::new`] to immediately take over the
85    /// returned tree when protected ownership is needed, or use
86    /// [`Self::encode_in_transaction`] when budget admission is required. The
87    /// transaction-aware entry point still cannot guarantee protection around
88    /// every recursive action performed internally by an arbitrary third-party
89    /// Serde serializer.
90    ///
91    /// # Errors
92    ///
93    /// Returns a strict value error whose precise kind distinguishes number,
94    /// object-key, RawValue, capacity, serializer-contract, and opaque custom
95    /// failures without retaining arbitrary diagnostic text.
96    ///
97    /// # Examples
98    ///
99    /// ```
100    /// use qubit_json::value::JsonValueEncoder;
101    /// use qubit_json::value::SafeJsonValue;
102    /// use serde_json::value::RawValue;
103    ///
104    /// let text = format!("{}0{}", "[".repeat(129), "]".repeat(129));
105    /// let raw = RawValue::from_string(text)?;
106    /// let owner = SafeJsonValue::new(JsonValueEncoder::new().encode(&raw)?);
107    /// assert!(owner.as_value().is_array());
108    /// # Ok::<(), Box<dyn std::error::Error>>(())
109    /// ```
110    pub fn encode<T>(&self, value: &T) -> Result<Value, JsonSerializationError>
111    where
112        T: Serialize + ?Sized,
113    {
114        let () = self.private;
115        let mut budget = JsonValueBudget::new(JsonValueLimits::<JsonResource, usize>::new());
116        let mut transaction = budget.transaction();
117        let owner = match self.encode_in_transaction(value, &mut transaction) {
118            Ok(owner) => owner,
119            Err(JsonValueEncodeError::Serialization(error)) => return Err(error),
120            Err(JsonValueEncodeError::Budget(_)) => {
121                return Err(JsonSerializationError::new(
122                    crate::encode::JsonSerializationErrorKind::CustomSerialization,
123                ));
124            }
125            Err(JsonValueEncodeError::PrivateTextLimitExceeded { .. }) => {
126                return Err(JsonSerializationError::new(
127                    crate::encode::JsonSerializationErrorKind::CustomSerialization,
128                ));
129            }
130        };
131        transaction
132            .commit()
133            .map_err(|_| JsonSerializationError::new(crate::encode::JsonSerializationErrorKind::CustomSerialization))?;
134        Ok(owner.into_value())
135    }
136
137    /// Projects one value while staging native measurements in a caller-owned
138    /// transaction.
139    ///
140    /// The transaction remains borrowed and uncommitted. Repeated successful
141    /// calls may be committed together by the caller. If this method returns an
142    /// error or unwinds, the caller must drop the entire transaction rather
143    /// than committing its staged state.
144    ///
145    /// # Type Parameters
146    ///
147    /// * `T` - Serde source type; it may be dynamically sized.
148    /// * `R` - Resource identity retained in measured budget failures.
149    /// * `Q` - Exact unsigned quantity type used by the budget.
150    ///
151    /// # Parameters
152    ///
153    /// * `value` - Borrowed value to project.
154    /// * `transaction` - Caller-owned value transaction receiving native
155    ///   measurements.
156    ///
157    /// # Returns
158    ///
159    /// A protected JSON owner when serialization and all measurements succeed.
160    ///
161    /// # Errors
162    ///
163    /// Returns the first measured budget error or serialization failure.
164    pub fn encode_in_transaction<T, R, Q>(
165        &self,
166        value: &T,
167        transaction: &mut JsonValueTransaction<'_, R, Q>,
168    ) -> Result<SafeJsonValue, JsonValueEncodeError<R, Q>>
169    where
170        T: Serialize + ?Sized,
171        R: Clone,
172        Q: ResourceQuantity,
173    {
174        self.encode_in_transaction_impl(value, transaction, None)
175    }
176
177    /// Projects one value while bounding each private RawValue or Number text
178    /// payload before it is copied or appended.
179    ///
180    /// The limit counts UTF-8 bytes in each payload independently and includes
181    /// the exact boundary. It does not limit ordinary strings or consume the
182    /// transaction's native value budget. If this method fails, drop the whole
183    /// transaction rather than committing staged state.
184    ///
185    /// # Parameters
186    ///
187    /// * `value` - Borrowed Serde value to project.
188    /// * `transaction` - Caller-owned transaction receiving native value
189    ///   measurements.
190    /// * `max_private_text_bytes` - Inclusive UTF-8 byte limit for each private
191    ///   RawValue or Number payload.
192    ///
193    /// # Returns
194    ///
195    /// A protected JSON owner when serialization and measurement succeed.
196    ///
197    /// # Errors
198    ///
199    /// Returns the first budget or serialization failure, or
200    /// `PrivateTextLimitExceeded` when a private text payload exceeds the
201    /// configured byte limit. The limit error does not contain payload text.
202    pub fn encode_in_transaction_with_private_text_limit<T, R, Q>(
203        &self,
204        value: &T,
205        transaction: &mut JsonValueTransaction<'_, R, Q>,
206        max_private_text_bytes: usize,
207    ) -> Result<SafeJsonValue, JsonValueEncodeError<R, Q>>
208    where
209        T: Serialize + ?Sized,
210        R: Clone,
211        Q: ResourceQuantity,
212    {
213        self.encode_in_transaction_impl(value, transaction, Some(max_private_text_bytes))
214    }
215
216    /// Projects a value using the caller-owned transaction and an optional
217    /// per-payload private-text limit. A retained context failure takes
218    /// precedence over the final `Serialize` result, including an apparent
219    /// success after a swallowed error. This helper neither commits nor rolls
220    /// back the borrowed batch transaction. After an error or unwind, the
221    /// caller must drop the entire transaction rather than commit its staged
222    /// usage.
223    ///
224    /// # Parameters
225    ///
226    /// * `value` - Borrowed Serde value to project.
227    /// * `transaction` - Caller-owned transaction receiving staged usage.
228    /// * `max_private_text_bytes` - Optional inclusive UTF-8 byte limit for
229    ///   each private RawValue or Number payload.
230    ///
231    /// # Returns
232    ///
233    /// A protected JSON value when serialization and measurement succeed.
234    ///
235    /// # Errors
236    ///
237    /// Returns the first retained budget or serialization failure, or a
238    /// private-text-limit failure when a payload exceeds the configured limit.
239    fn encode_in_transaction_impl<T, R, Q>(
240        &self,
241        value: &T,
242        transaction: &mut JsonValueTransaction<'_, R, Q>,
243        max_private_text_bytes: Option<usize>,
244    ) -> Result<SafeJsonValue, JsonValueEncodeError<R, Q>>
245    where
246        T: Serialize + ?Sized,
247        R: Clone,
248        Q: ResourceQuantity,
249    {
250        let context = std::cell::RefCell::new(JsonValueEncodeContext::new(transaction, max_private_text_bytes));
251        let result = value.serialize(JsonValueSerializer::new(&context, 1));
252        if let Err(error) = result.as_ref() {
253            let _ = context.borrow_mut().record_serialization_error(*error);
254        }
255        match context.into_inner().take_failure() {
256            Some(JsonValueEncodeFailure::Budget(error)) => Err(JsonValueEncodeError::Budget(error)),
257            Some(JsonValueEncodeFailure::Serialization(error)) => Err(JsonValueEncodeError::Serialization(error)),
258            Some(JsonValueEncodeFailure::PrivateTextLimitExceeded { maximum }) => {
259                Err(JsonValueEncodeError::PrivateTextLimitExceeded { maximum })
260            }
261            None => result.map_err(JsonValueEncodeError::Serialization),
262        }
263    }
264}