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.commit().map_err(|_| {
132 JsonSerializationError::new(
133 crate::encode::JsonSerializationErrorKind::CustomSerialization,
134 )
135 })?;
136 Ok(owner.into_value())
137 }
138
139 /// Projects one value while staging native measurements in a caller-owned
140 /// transaction.
141 ///
142 /// The transaction remains borrowed and uncommitted. Repeated successful
143 /// calls may be committed together by the caller. If this method returns an
144 /// error or unwinds, the caller must drop the entire transaction rather
145 /// than committing its staged state.
146 ///
147 /// # Type Parameters
148 ///
149 /// * `T` - Serde source type; it may be dynamically sized.
150 /// * `R` - Resource identity retained in measured budget failures.
151 /// * `Q` - Exact unsigned quantity type used by the budget.
152 ///
153 /// # Parameters
154 ///
155 /// * `value` - Borrowed value to project.
156 /// * `transaction` - Caller-owned value transaction receiving native
157 /// measurements.
158 ///
159 /// # Returns
160 ///
161 /// A protected JSON owner when serialization and all measurements succeed.
162 ///
163 /// # Errors
164 ///
165 /// Returns the first measured budget error or serialization failure.
166 pub fn encode_in_transaction<T, R, Q>(
167 &self,
168 value: &T,
169 transaction: &mut JsonValueTransaction<'_, R, Q>,
170 ) -> Result<SafeJsonValue, JsonValueEncodeError<R, Q>>
171 where
172 T: Serialize + ?Sized,
173 R: Clone,
174 Q: ResourceQuantity,
175 {
176 self.encode_in_transaction_impl(value, transaction, None)
177 }
178
179 /// Projects one value while bounding each private RawValue or Number text
180 /// payload before it is copied or appended.
181 ///
182 /// The limit counts UTF-8 bytes in each payload independently and includes
183 /// the exact boundary. It does not limit ordinary strings or consume the
184 /// transaction's native value budget. If this method fails, drop the whole
185 /// transaction rather than committing staged state.
186 ///
187 /// # Type Parameters
188 ///
189 /// * `T` - Serde source type; it may be dynamically sized.
190 /// * `R` - Resource identity retained in measured budget failures.
191 /// * `Q` - Exact unsigned quantity type used by the budget.
192 ///
193 /// # Parameters
194 ///
195 /// * `value` - Borrowed Serde value to project.
196 /// * `transaction` - Caller-owned transaction receiving native value
197 /// measurements.
198 /// * `max_private_text_bytes` - Inclusive UTF-8 byte limit for each private
199 /// RawValue or Number payload.
200 ///
201 /// # Returns
202 ///
203 /// A protected JSON owner when serialization and measurement succeed.
204 ///
205 /// # Errors
206 ///
207 /// Returns the first budget or serialization failure, or
208 /// `PrivateTextLimitExceeded` when a private text payload exceeds the
209 /// configured byte limit. The limit error does not contain payload text.
210 pub fn encode_in_transaction_with_private_text_limit<T, R, Q>(
211 &self,
212 value: &T,
213 transaction: &mut JsonValueTransaction<'_, R, Q>,
214 max_private_text_bytes: usize,
215 ) -> Result<SafeJsonValue, JsonValueEncodeError<R, Q>>
216 where
217 T: Serialize + ?Sized,
218 R: Clone,
219 Q: ResourceQuantity,
220 {
221 self.encode_in_transaction_impl(value, transaction, Some(max_private_text_bytes))
222 }
223
224 /// Projects a value using the caller-owned transaction and an optional
225 /// per-payload private-text limit. A retained context failure takes
226 /// precedence over the final `Serialize` result, including an apparent
227 /// success after a swallowed error. This helper neither commits nor rolls
228 /// back the borrowed batch transaction. After an error or unwind, the
229 /// caller must drop the entire transaction rather than commit its staged
230 /// usage.
231 ///
232 /// # Type Parameters
233 ///
234 /// * `T` - Serde source type; it may be dynamically sized.
235 /// * `R` - Resource identity retained in measured budget failures.
236 /// * `Q` - Exact unsigned quantity type used by the budget.
237 ///
238 /// # Parameters
239 ///
240 /// * `value` - Borrowed Serde value to project.
241 /// * `transaction` - Caller-owned transaction receiving staged usage.
242 /// * `max_private_text_bytes` - Optional inclusive UTF-8 byte limit for
243 /// each private RawValue or Number payload.
244 ///
245 /// # Returns
246 ///
247 /// A protected JSON value when serialization and measurement succeed.
248 ///
249 /// # Errors
250 ///
251 /// Returns the first retained budget or serialization failure, or a
252 /// private-text-limit failure when a payload exceeds the configured limit.
253 fn encode_in_transaction_impl<T, R, Q>(
254 &self,
255 value: &T,
256 transaction: &mut JsonValueTransaction<'_, R, Q>,
257 max_private_text_bytes: Option<usize>,
258 ) -> Result<SafeJsonValue, JsonValueEncodeError<R, Q>>
259 where
260 T: Serialize + ?Sized,
261 R: Clone,
262 Q: ResourceQuantity,
263 {
264 let context = std::cell::RefCell::new(JsonValueEncodeContext::new(
265 transaction,
266 max_private_text_bytes,
267 ));
268 let result = value.serialize(JsonValueSerializer::new(&context, 1));
269 if let Err(error) = result.as_ref() {
270 let _ = context.borrow_mut().record_serialization_error(*error);
271 }
272 match context.into_inner().take_failure() {
273 Some(JsonValueEncodeFailure::Budget(error)) => Err(JsonValueEncodeError::Budget(error)),
274 Some(JsonValueEncodeFailure::Serialization(error)) => {
275 Err(JsonValueEncodeError::Serialization(error))
276 }
277 Some(JsonValueEncodeFailure::PrivateTextLimitExceeded { maximum }) => {
278 Err(JsonValueEncodeError::PrivateTextLimitExceeded { maximum })
279 }
280 None => result.map_err(JsonValueEncodeError::Serialization),
281 }
282 }
283}