Skip to main content

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}