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    /// # Type Parameters
186    ///
187    /// * `T` - Serde source type; it may be dynamically sized.
188    /// * `R` - Resource identity retained in measured budget failures.
189    /// * `Q` - Exact unsigned quantity type used by the budget.
190    ///
191    /// # Parameters
192    ///
193    /// * `value` - Borrowed Serde value to project.
194    /// * `transaction` - Caller-owned transaction receiving native value
195    ///   measurements.
196    /// * `max_private_text_bytes` - Inclusive UTF-8 byte limit for each private
197    ///   RawValue or Number payload.
198    ///
199    /// # Returns
200    ///
201    /// A protected JSON owner when serialization and measurement succeed.
202    ///
203    /// # Errors
204    ///
205    /// Returns the first budget or serialization failure, or
206    /// `PrivateTextLimitExceeded` when a private text payload exceeds the
207    /// configured byte limit. The limit error does not contain payload text.
208    pub fn encode_in_transaction_with_private_text_limit<T, R, Q>(
209        &self,
210        value: &T,
211        transaction: &mut JsonValueTransaction<'_, R, Q>,
212        max_private_text_bytes: usize,
213    ) -> Result<SafeJsonValue, JsonValueEncodeError<R, Q>>
214    where
215        T: Serialize + ?Sized,
216        R: Clone,
217        Q: ResourceQuantity,
218    {
219        self.encode_in_transaction_impl(value, transaction, Some(max_private_text_bytes))
220    }
221
222    /// Projects a value using the caller-owned transaction and an optional
223    /// per-payload private-text limit. A retained context failure takes
224    /// precedence over the final `Serialize` result, including an apparent
225    /// success after a swallowed error. This helper neither commits nor rolls
226    /// back the borrowed batch transaction. After an error or unwind, the
227    /// caller must drop the entire transaction rather than commit its staged
228    /// usage.
229    ///
230    /// # Type Parameters
231    ///
232    /// * `T` - Serde source type; it may be dynamically sized.
233    /// * `R` - Resource identity retained in measured budget failures.
234    /// * `Q` - Exact unsigned quantity type used by the budget.
235    ///
236    /// # Parameters
237    ///
238    /// * `value` - Borrowed Serde value to project.
239    /// * `transaction` - Caller-owned transaction receiving staged usage.
240    /// * `max_private_text_bytes` - Optional inclusive UTF-8 byte limit for
241    ///   each private RawValue or Number payload.
242    ///
243    /// # Returns
244    ///
245    /// A protected JSON value when serialization and measurement succeed.
246    ///
247    /// # Errors
248    ///
249    /// Returns the first retained budget or serialization failure, or a
250    /// private-text-limit failure when a payload exceeds the configured limit.
251    fn encode_in_transaction_impl<T, R, Q>(
252        &self,
253        value: &T,
254        transaction: &mut JsonValueTransaction<'_, R, Q>,
255        max_private_text_bytes: Option<usize>,
256    ) -> Result<SafeJsonValue, JsonValueEncodeError<R, Q>>
257    where
258        T: Serialize + ?Sized,
259        R: Clone,
260        Q: ResourceQuantity,
261    {
262        let context = std::cell::RefCell::new(JsonValueEncodeContext::new(transaction, max_private_text_bytes));
263        let result = value.serialize(JsonValueSerializer::new(&context, 1));
264        if let Err(error) = result.as_ref() {
265            let _ = context.borrow_mut().record_serialization_error(*error);
266        }
267        match context.into_inner().take_failure() {
268            Some(JsonValueEncodeFailure::Budget(error)) => Err(JsonValueEncodeError::Budget(error)),
269            Some(JsonValueEncodeFailure::Serialization(error)) => Err(JsonValueEncodeError::Serialization(error)),
270            Some(JsonValueEncodeFailure::PrivateTextLimitExceeded { maximum }) => {
271                Err(JsonValueEncodeError::PrivateTextLimitExceeded { maximum })
272            }
273            None => result.map_err(JsonValueEncodeError::Serialization),
274        }
275    }
276}