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}