qubit_json/encode/jcs_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//! Budget-aware encoder for JSON Canonicalization Scheme (JCS) output.
9
10use std::cell::RefCell;
11use std::fmt::Debug;
12use std::io::Write;
13
14use qubit_budget::ResourceQuantity;
15use qubit_budget::json::JsonEncodeAttempt;
16use qubit_budget::json::JsonEncodeLimits;
17use qubit_budget::json::JsonEncodeSession;
18use qubit_budget::json::JsonResource;
19use qubit_io::ByteOutput;
20use serde_json::Value;
21
22use super::JcsEncodeError;
23use super::JsonSerializationError;
24use super::JsonSerializationErrorKind;
25use super::internal::JcsRenderer;
26use super::internal::JcsRendererError;
27use super::output::JsonOutputAccounting;
28use super::output::JsonOutputBuffer;
29use super::output::JsonOutputWriter;
30use crate::value::traverse::JsonTreeReader;
31
32/// Encodes [`serde_json::Value`] values using RFC 8785 canonical JSON.
33///
34/// JCS uses an IEEE 754 binary64 number model. Integer values outside the
35/// exactly representable range can therefore be rounded (for example,
36/// `9007199254740993` becomes `9007199254740992`). Output limits measure the
37/// final canonical byte sequence. Auxiliary storage used while rendering is
38/// limited separately by `max_working_bytes`; this limit covers the renderer's
39/// traversal stack and active object member tables, but excludes the input
40/// [`Value`], buffered output bytes, and any admission stack used to check
41/// configured value limits before rendering. Configure `max_depth` as well
42/// when the admission traversal itself must have a depth bound. A working
43/// memory limit is a capacity bound for these renderer collections, not an
44/// exact heap-usage bound.
45///
46/// # Type Parameters
47///
48/// * `'budget` - Lifetime of the resource budget borrowed by the encoder
49/// session.
50/// * `R` - Resource identity type tracked by the session.
51/// * `Q` - Quantity type used for resource limits and accounting.
52///
53/// # Examples
54///
55/// ```
56/// use qubit_json::encode::JcsEncoder;
57///
58/// let mut encoder = JcsEncoder::unlimited();
59/// let bytes = encoder.to_vec(&serde_json::json!({"b": 2, "a": 1.0}))?;
60/// assert_eq!(bytes, br#"{"a":1,"b":2}"#);
61/// # Ok::<(), qubit_json::encode::JcsEncodeError<qubit_budget::json::JsonResource>>(())
62/// ```
63#[must_use]
64pub struct JcsEncoder<'budget, R = JsonResource, Q = usize>
65where
66 Q: ResourceQuantity,
67{
68 /// Cumulative accounting state shared by encode operations.
69 session: JsonEncodeSession<'budget, R, Q>,
70 /// Maximum auxiliary memory used while rendering a value.
71 max_working_bytes: usize,
72}
73
74impl<R, Q> JcsEncoder<'static, R, Q>
75where
76 R: Clone + Debug,
77 Q: ResourceQuantity,
78{
79 /// Creates an encoder with an owned session using `limits`.
80 ///
81 /// `max_working_bytes` caps the capacity of the renderer's traversal stack
82 /// and active object member tables.
83 ///
84 /// # Type Parameters
85 ///
86 /// * `R` - Resource identity type tracked by the session.
87 /// * `Q` - Quantity type used for resource limits and accounting.
88 ///
89 /// # Parameters
90 ///
91 /// * `limits` supplies cumulative value and output limits.
92 /// * `max_working_bytes` limits auxiliary traversal and member-table
93 /// memory.
94 ///
95 /// # Returns
96 ///
97 /// An encoder that owns a session initialized from `limits`.
98 pub fn with_limits(limits: JsonEncodeLimits<R, Q>, max_working_bytes: usize) -> Self {
99 Self::new(JsonEncodeSession::from_limits(limits), max_working_bytes)
100 }
101}
102
103impl JcsEncoder<'static, JsonResource, usize> {
104 /// Creates an encoder with effectively unlimited value, output, and
105 /// working memory limits.
106 ///
107 /// # Returns
108 ///
109 /// An encoder backed by a fresh session without configured resource limits.
110 pub fn unlimited() -> Self {
111 Self::with_limits(JsonEncodeLimits::new(), usize::MAX)
112 }
113}
114
115impl<'budget, R, Q> JcsEncoder<'budget, R, Q>
116where
117 R: Clone + Debug,
118 Q: ResourceQuantity,
119{
120 /// Creates an encoder that retains the supplied cumulative session.
121 ///
122 /// `max_working_bytes` caps the capacity of the renderer's traversal stack
123 /// and active object member tables.
124 ///
125 /// # Type Parameters
126 ///
127 /// * `'budget` - Lifetime of the resource budget borrowed by `session`.
128 /// * `R` - Resource identity type tracked by `session`.
129 /// * `Q` - Quantity type used for resource limits and accounting.
130 ///
131 /// # Parameters
132 ///
133 /// * `session` is the cumulative accounting session to retain.
134 /// * `max_working_bytes` limits auxiliary traversal and member-table
135 /// memory.
136 ///
137 /// # Returns
138 ///
139 /// An encoder using the supplied session and working-memory limit.
140 pub fn new(session: JsonEncodeSession<'budget, R, Q>, max_working_bytes: usize) -> Self {
141 Self {
142 session,
143 max_working_bytes,
144 }
145 }
146
147 /// Returns the maximum auxiliary working memory in bytes.
148 ///
149 /// # Returns
150 ///
151 /// The configured maximum working-memory capacity.
152 #[must_use]
153 #[inline]
154 pub const fn max_working_bytes(&self) -> usize {
155 self.max_working_bytes
156 }
157
158 /// Returns the cumulative accounting session.
159 ///
160 /// # Returns
161 ///
162 /// A shared reference to this encoder's session.
163 #[must_use]
164 #[inline]
165 pub const fn session(&self) -> &JsonEncodeSession<'budget, R, Q> {
166 &self.session
167 }
168
169 /// Returns mutable access to the cumulative accounting session.
170 ///
171 /// # Returns
172 ///
173 /// A mutable reference to this encoder's session.
174 #[must_use]
175 #[inline]
176 pub const fn session_mut(&mut self) -> &mut JsonEncodeSession<'budget, R, Q> {
177 &mut self.session
178 }
179
180 /// Consumes the encoder and returns its cumulative accounting session.
181 ///
182 /// # Returns
183 ///
184 /// The session containing the cumulative resource accounting state.
185 #[must_use]
186 pub fn into_session(self) -> JsonEncodeSession<'budget, R, Q> {
187 self.session
188 }
189
190 /// Encodes a value and commits its accounting only after full success.
191 ///
192 /// # Parameters
193 ///
194 /// * `value` is the JSON value to encode.
195 ///
196 /// # Returns
197 ///
198 /// The complete canonical JSON bytes on success.
199 ///
200 /// # Errors
201 ///
202 /// Returns `JcsEncodeError` if rendering, memory allocation, or resource
203 /// accounting fails. The value and output accounting is committed only on
204 /// success.
205 pub fn to_vec(&mut self, value: &Value) -> Result<Vec<u8>, JcsEncodeError<R, Q>> {
206 let mut attempt = self.session.begin_value();
207 let bytes = Self::serialize_buffer(value, &mut attempt, self.max_working_bytes)?;
208 attempt
209 .try_consume_output_bytes(bytes.len())
210 .map_err(JcsEncodeError::budget)?;
211 attempt.commit().map_err(JcsEncodeError::budget)?;
212 Ok(bytes)
213 }
214
215 /// Encodes a value as UTF-8 text, committing only after successful
216 /// encoding.
217 ///
218 /// # Parameters
219 ///
220 /// * `value` is the JSON value to encode.
221 ///
222 /// # Returns
223 ///
224 /// The complete canonical JSON document as a UTF-8 string.
225 ///
226 /// # Errors
227 ///
228 /// Returns `JcsEncodeError` if encoding or resource accounting fails.
229 ///
230 /// # Panics
231 ///
232 /// Panics only if the renderer produces bytes that are not valid UTF-8,
233 /// which violates the encoder's JSON output invariant.
234 pub fn to_string(&mut self, value: &Value) -> Result<String, JcsEncodeError<R, Q>> {
235 let bytes = self.to_vec(value)?;
236 Ok(String::from_utf8(bytes).expect("JCS output must be UTF-8"))
237 }
238
239 /// Buffers the canonical document before writing it to `output`.
240 ///
241 /// Output budget is checked against the complete document before external
242 /// I/O. If the destination fails, accepted bytes remain written and remain
243 /// charged, while the value transaction is not committed.
244 /// This method does not flush the destination. Success means that the
245 /// destination accepted the complete document and the encoding charges
246 /// were committed. The writer is borrowed mutably for this call; flush it
247 /// explicitly afterward. A later flush is outside this encoding transaction
248 /// and does not roll back committed charges if it fails. Do not rely on
249 /// `Drop` to report flush errors. A successful flush does not guarantee
250 /// durability or delivery to a remote peer.
251 ///
252 /// # Type Parameters
253 ///
254 /// * `O` - Output destination receiving the complete buffered document.
255 ///
256 /// # Parameters
257 ///
258 /// * `output` is borrowed mutably and receives the complete document.
259 /// * `value` is the JSON value to encode.
260 ///
261 /// # Returns
262 ///
263 /// `Ok(())` after the destination has accepted all bytes and accounting
264 /// is committed. The destination is not flushed.
265 ///
266 /// # Errors
267 ///
268 /// Returns `JcsEncodeError` if rendering, output budgeting, destination
269 /// I/O, or session accounting fails. A destination failure preserves
270 /// accepted output bytes and their output charges without committing the
271 /// value transaction. A later caller-initiated flush failure does not undo
272 /// the already committed charges.
273 ///
274 /// # Examples
275 ///
276 /// ```
277 /// use std::io::BufWriter;
278 /// use std::io::Write;
279 ///
280 /// use qubit_budget::json::JsonEncodeLimits;
281 /// use qubit_budget::json::JsonResource;
282 /// use qubit_json::encode::JcsEncoder;
283 /// use serde_json::json;
284 ///
285 /// let limits = JsonEncodeLimits::<JsonResource, usize>::builder()
286 /// .max_output_bytes(64)
287 /// .build();
288 /// let mut encoder = JcsEncoder::with_limits(limits, 16 * 1024);
289 /// let mut writer = BufWriter::new(Vec::new());
290 /// encoder.write_buffered(&mut writer, &json!({"b": 2, "a": 1}))?;
291 /// writer.flush()?;
292 /// let bytes = writer.into_inner().expect("flush already succeeded");
293 /// assert_eq!(bytes, br#"{"a":1,"b":2}"#);
294 /// # Ok::<(), Box<dyn std::error::Error>>(())
295 /// ```
296 pub fn write_buffered<O: ByteOutput + ?Sized>(
297 &mut self,
298 output: &mut O,
299 value: &Value,
300 ) -> Result<(), JcsEncodeError<R, Q>> {
301 let mut attempt = self.session.begin_value();
302 let bytes = Self::serialize_buffer(value, &mut attempt, self.max_working_bytes)?;
303 attempt
304 .check_output_bytes(bytes.len())
305 .map_err(JcsEncodeError::budget)?;
306 Self::write_buffer(output, &bytes, &mut attempt)?;
307 attempt.commit().map_err(JcsEncodeError::budget)
308 }
309
310 /// Writes canonical output directly to `output` as it is produced.
311 ///
312 /// Accepted bytes are charged immediately and remain written if rendering,
313 /// flushing, or destination I/O later fails. A failed operation does not
314 /// commit the value budget. On success, this method flushes the destination
315 /// before committing value accounting; a flush failure therefore retains
316 /// charges for accepted output bytes without committing the value budget.
317 /// The destination is borrowed mutably for this call.
318 ///
319 /// # Type Parameters
320 ///
321 /// * `O` - Output destination receiving bytes as they are rendered.
322 ///
323 /// # Parameters
324 ///
325 /// * `output` is borrowed mutably and receives bytes as they are rendered.
326 /// * `value` is the JSON value to encode.
327 ///
328 /// # Returns
329 ///
330 /// `Ok(())` after rendering, flushing, and accounting all succeed.
331 ///
332 /// # Errors
333 ///
334 /// Returns `JcsEncodeError` if rendering, working-memory allocation,
335 /// destination I/O, or session accounting fails. Accepted output bytes
336 /// remain written and charged, while failed operations do not commit the
337 /// value budget.
338 pub fn write_incremental<O: ByteOutput + ?Sized>(
339 &mut self,
340 output: &mut O,
341 value: &Value,
342 ) -> Result<(), JcsEncodeError<R, Q>> {
343 let mut attempt = self.session.begin_value();
344 JsonTreeReader::new(attempt.value_transaction_mut())
345 .account(value)
346 .map_err(JcsEncodeError::budget)?;
347 let result = {
348 let (output_budget, _) = attempt.split_mut();
349 let accounting = RefCell::new(JsonOutputAccounting::new(output_budget));
350 let mut writer = JsonOutputWriter::new(output, &accounting);
351 match JcsRenderer::render(value, &mut writer, self.max_working_bytes) {
352 Ok(()) => {
353 let result = Write::flush(&mut writer).map_err(serde_json::Error::io);
354 writer.into_result(result).map_err(JcsEncodeError::from_json_encode)
355 }
356 Err(JcsRendererError::WorkingMemory(error)) => Err(JcsEncodeError::working_memory(error)),
357 Err(JcsRendererError::Number(source)) => Err(JcsEncodeError::serialization(source)),
358 Err(JcsRendererError::Allocation) => Err(JcsEncodeError::serialization(JsonSerializationError::new(
359 JsonSerializationErrorKind::CustomSerialization,
360 ))),
361 Err(JcsRendererError::Serialize(error)) => {
362 writer.into_result(Err(error)).map_err(JcsEncodeError::from_json_encode)
363 }
364 }
365 };
366 result?;
367 attempt.commit().map_err(JcsEncodeError::budget)
368 }
369
370 /// Produces a complete canonical document in an output-bounded buffer.
371 ///
372 /// # Parameters
373 ///
374 /// * `value` - JSON value whose canonical representation is produced.
375 /// * `attempt` - In-progress value transaction that accounts the value.
376 /// * `max_working_bytes` - Maximum auxiliary memory used by the renderer.
377 ///
378 /// # Returns
379 ///
380 /// The complete canonical JSON bytes.
381 ///
382 /// # Errors
383 ///
384 /// Returns a JCS error if value accounting, rendering, output budgeting,
385 /// or working-memory allocation fails.
386 fn serialize_buffer(
387 value: &Value,
388 attempt: &mut JsonEncodeAttempt<'_, R, Q>,
389 max_working_bytes: usize,
390 ) -> Result<Vec<u8>, JcsEncodeError<R, Q>> {
391 JsonTreeReader::new(attempt.value_transaction_mut())
392 .account(value)
393 .map_err(JcsEncodeError::budget)?;
394 let (output_budget, _) = attempt.split_mut();
395 let accounting = RefCell::new(JsonOutputAccounting::new(output_budget));
396 let mut output = JsonOutputBuffer::new(&accounting);
397 match JcsRenderer::render(value, &mut output, max_working_bytes) {
398 Ok(()) => output.into_result(Ok(())).map_err(JcsEncodeError::from_json_encode),
399 Err(JcsRendererError::WorkingMemory(error)) => Err(JcsEncodeError::working_memory(error)),
400 Err(JcsRendererError::Number(source)) => Err(JcsEncodeError::serialization(source)),
401 Err(JcsRendererError::Allocation) => Err(JcsEncodeError::serialization(JsonSerializationError::new(
402 JsonSerializationErrorKind::CustomSerialization,
403 ))),
404 Err(JcsRendererError::Serialize(error)) => {
405 output.into_result(Err(error)).map_err(JcsEncodeError::from_json_encode)
406 }
407 }
408 }
409
410 /// Writes buffered bytes and charges destination-accepted prefixes.
411 ///
412 /// # Type Parameters
413 ///
414 /// * `O` - Output destination that accepts the buffered bytes.
415 ///
416 /// # Parameters
417 ///
418 /// * `output` - Destination receiving the buffered document.
419 /// * `bytes` - Complete document bytes to write.
420 /// * `attempt` - In-progress transaction whose output budget is charged.
421 ///
422 /// # Returns
423 ///
424 /// Returns `Ok(())` after all bytes are accepted by the destination.
425 ///
426 /// # Errors
427 ///
428 /// Returns a JCS error if destination writing or output accounting fails.
429 fn write_buffer<O: ByteOutput + ?Sized>(
430 output: &mut O,
431 bytes: &[u8],
432 attempt: &mut JsonEncodeAttempt<'_, R, Q>,
433 ) -> Result<(), JcsEncodeError<R, Q>> {
434 let (output_budget, _) = attempt.split_mut();
435 let accounting = RefCell::new(JsonOutputAccounting::new(output_budget));
436 let mut writer = JsonOutputWriter::new(output, &accounting);
437 let result = writer.write_all(bytes).map_err(serde_json::Error::io);
438 writer.into_result(result).map_err(JcsEncodeError::from_json_encode)
439 }
440}