Skip to main content

qubit_json/encode/
json_encoder.rs

1// =============================================================================
2//    Copyright (c) 2025 - 2026 Haixing Hu.
3//
4//    SPDX-License-Identifier: Apache-2.0
5//
6//    Licensed under the Apache License, Version 2.0.
7// =============================================================================
8//! Stateful strict JSON text encoding.
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::Serialize;
21use serde_json::Error as JsonError;
22use serde_json::Serializer as JsonSerializer;
23
24use super::JsonEncodeError;
25use super::output::JsonOutputAccounting;
26use super::output::JsonOutputBuffer;
27use super::output::JsonOutputWriter;
28use super::serializer::JsonEncodeContext;
29use super::serializer::JsonEncodeSerializer;
30
31/// Encodes strict JSON text while owning cumulative accounting state.
32///
33/// Signed and unsigned 64-bit integers are supported in full. Serde `i128`
34/// values are accepted only when they fit `i64`, or are non-negative and fit
35/// `u64`; `u128` values must fit `u64`. Wider integers return a serialization
36/// error instead of being truncated or converted to strings. Floating-point
37/// values must be finite.
38///
39/// Nested serialization errors remain failures of the complete operation even
40/// when an enclosing custom `Serialize` implementation ignores them. Arbitrary
41/// custom error text is not retained. Incremental output may already contain an
42/// accepted prefix when an error is reported.
43///
44/// # Type Parameters
45///
46/// * `'budget` - Lifetime of the caller-provided cumulative encode session.
47/// * `R` - Resource identity tracked by the encode session.
48/// * `Q` - Quantity representation used for resource accounting.
49///
50/// # Examples
51///
52/// ```
53/// use qubit_json::encode::JsonEncoder;
54///
55/// let mut encoder = JsonEncoder::unlimited();
56/// let bytes = encoder.to_vec(&serde_json::json!({"ok": true}))?;
57/// assert_eq!(bytes, br#"{"ok":true}"#);
58/// # Ok::<(), qubit_json::encode::JsonEncodeError<qubit_budget::json::JsonResource>>(())
59/// ```
60pub struct JsonEncoder<'budget, R = JsonResource, Q = usize>
61where
62    Q: ResourceQuantity,
63{
64    /// Session charged by each encode operation.
65    session: JsonEncodeSession<'budget, R, Q>,
66}
67
68impl<R, Q> JsonEncoder<'static, R, Q>
69where
70    R: Clone + Debug,
71    Q: ResourceQuantity,
72{
73    /// Creates an encoder with an owned session built from explicit limits.
74    ///
75    /// # Parameters
76    ///
77    /// * `limits` - Resource limits used to construct the cumulative session.
78    ///
79    /// # Returns
80    ///
81    /// An encoder whose cumulative accounting starts empty and is constrained
82    /// by `limits`.
83    #[inline(always)]
84    #[must_use]
85    pub fn with_limits(limits: JsonEncodeLimits<R, Q>) -> Self {
86        Self::new(JsonEncodeSession::from_limits(limits))
87    }
88}
89
90impl JsonEncoder<'static, JsonResource, usize> {
91    /// Creates an encoder with an explicitly unlimited standard session.
92    ///
93    /// # Returns
94    ///
95    /// An encoder with no configured output or encoded-value limits.
96    #[inline(always)]
97    #[must_use]
98    pub fn unlimited() -> Self {
99        Self::with_limits(JsonEncodeLimits::new())
100    }
101}
102
103impl<'budget, R, Q> JsonEncoder<'budget, R, Q>
104where
105    R: Clone + Debug,
106    Q: ResourceQuantity,
107{
108    /// Creates an encoder that owns a reusable cumulative session.
109    ///
110    /// # Parameters
111    ///
112    /// * `session` - Session that receives committed output accounting.
113    ///
114    /// # Returns
115    ///
116    /// An encoder that retains `session` until [`Self::into_session`] is called
117    /// or the encoder is dropped.
118    #[inline(always)]
119    #[must_use]
120    pub fn new(session: JsonEncodeSession<'budget, R, Q>) -> Self {
121        Self { session }
122    }
123
124    /// Returns the cumulative session for read-only inspection.
125    ///
126    /// The reference exposes cumulative accounting state. Bytes accepted by
127    /// an external destination remain charged even when the encode operation
128    /// fails; value-resource charges commit only after the complete operation
129    /// succeeds. The reference remains borrowed from this encoder.
130    ///
131    /// # Returns
132    ///
133    /// A shared reference to the cumulative encode session.
134    #[inline(always)]
135    #[must_use]
136    pub const fn session(&self) -> &JsonEncodeSession<'budget, R, Q> {
137        &self.session
138    }
139
140    /// Returns mutable access to the cumulative session.
141    ///
142    /// Changes made through the reference affect the limits and accounting
143    /// state used by subsequent encode operations.
144    ///
145    /// # Returns
146    ///
147    /// A mutable reference to the cumulative encode session.
148    #[inline(always)]
149    #[must_use]
150    pub const fn session_mut(&mut self) -> &mut JsonEncodeSession<'budget, R, Q> {
151        &mut self.session
152    }
153
154    /// Returns the cumulative session and consumes the encoder.
155    ///
156    /// No output is produced and no accounting is reset; ownership of the
157    /// accumulated state is transferred to the caller.
158    ///
159    /// # Returns
160    ///
161    /// The session previously owned by this encoder.
162    #[inline(always)]
163    #[must_use]
164    pub fn into_session(self) -> JsonEncodeSession<'budget, R, Q> {
165        self.session
166    }
167
168    /// Encodes `value` into compact JSON and commits only complete success.
169    ///
170    /// # Type Parameters
171    ///
172    /// * `T` - Source type serialized into JSON.
173    ///
174    /// # Parameters
175    ///
176    /// * `value` - Value to serialize.
177    ///
178    /// # Returns
179    ///
180    /// The compact JSON bytes on success.
181    ///
182    /// # Errors
183    ///
184    /// Returns [`JsonEncodeErrorKind::Budget`](super::JsonEncodeErrorKind::Budget) when accounting rejects the value
185    /// or output,
186    /// [`JsonEncodeErrorKind::InvalidRawJson`](super::JsonEncodeErrorKind::InvalidRawJson)
187    /// when a `RawValue` or private raw payload fails this encoder's strict
188    /// JSON admission, or a serialization error when Serde rejects `value`.
189    /// Inspect [`JsonEncodeError::syntax_error`] for the structured syntax
190    /// reason.
191    pub fn to_vec<T>(&mut self, value: &T) -> Result<Vec<u8>, JsonEncodeError<R, Q>>
192    where
193        T: Serialize + ?Sized,
194    {
195        let has_value_limits = self.session.value_budget().limits().has_limits();
196        let mut attempt = self.session.begin_value();
197        let bytes = Self::serialize_buffer(value, &mut attempt, has_value_limits)?;
198        attempt
199            .try_consume_output_bytes(bytes.len())
200            .map_err(JsonEncodeError::<R, Q>::budget)?;
201        attempt.commit().map_err(JsonEncodeError::<R, Q>::budget)?;
202        Ok(bytes)
203    }
204
205    /// Buffers a complete document before writing it to `writer`.
206    ///
207    /// Success means `writer` accepted the complete document and output/value
208    /// accounting was committed. This method does not flush the writer. If
209    /// the writer buffers output, call its [`Write::flush`] method after this
210    /// call succeeds. The writer is borrowed mutably for this call. A later
211    /// flush failure is outside this encoding transaction and does not roll
212    /// back accounting already committed by this method. A successful flush
213    /// only reports the writer's result; it does not confirm persistence to
214    /// disk or receipt by a remote peer.
215    ///
216    /// # Examples
217    ///
218    /// ```
219    /// use std::io::{BufWriter, Write};
220    /// use qubit_budget::json::{JsonEncodeLimits, JsonResource};
221    /// use qubit_json::encode::JsonEncoder;
222    /// use serde_json::json;
223    ///
224    /// let limits = JsonEncodeLimits::<JsonResource, usize>::builder()
225    ///     .max_output_bytes(64)
226    ///     .build();
227    /// let mut encoder = JsonEncoder::with_limits(limits);
228    /// let mut writer = BufWriter::new(Vec::new());
229    /// encoder.write_buffered(&mut writer, &json!({"ok": true}))?;
230    /// writer.flush()?;
231    /// let bytes = writer.into_inner().expect("flush already succeeded");
232    /// assert_eq!(bytes, br#"{"ok":true}"#);
233    /// # Ok::<(), Box<dyn std::error::Error>>(())
234    /// ```
235    ///
236    /// # Type Parameters
237    ///
238    /// * `O` - Destination output type.
239    /// * `T` - Source type serialized into JSON.
240    ///
241    /// # Parameters
242    ///
243    /// * `writer` - Destination receiving the complete JSON document.
244    /// * `value` - Value to serialize.
245    ///
246    /// # Returns
247    ///
248    /// `Ok(())` after the complete document is written and accounting is
249    /// committed.
250    ///
251    /// # Errors
252    ///
253    /// Returns [`JsonEncodeErrorKind::Budget`](super::JsonEncodeErrorKind::Budget) when accounting rejects the value
254    /// or output,
255    /// [`JsonEncodeErrorKind::InvalidRawJson`](super::JsonEncodeErrorKind::InvalidRawJson)
256    /// when a `RawValue` or private raw payload fails this encoder's strict
257    /// JSON admission, a serialization error when Serde rejects `value`, or
258    /// a writer error when writing fails. Inspect
259    /// [`JsonEncodeError::syntax_error`] for the structured syntax reason.
260    pub fn write_buffered<O, T>(
261        &mut self,
262        writer: &mut O,
263        value: &T,
264    ) -> Result<(), JsonEncodeError<R, Q>>
265    where
266        O: ByteOutput + ?Sized,
267        T: Serialize + ?Sized,
268    {
269        let has_value_limits = self.session.value_budget().limits().has_limits();
270        let mut attempt = self.session.begin_value();
271        let bytes = Self::serialize_buffer(value, &mut attempt, has_value_limits)?;
272        attempt
273            .check_output_bytes(bytes.len())
274            .map_err(JsonEncodeError::<R, Q>::budget)?;
275        Self::write_buffer(writer, &bytes, &mut attempt)?;
276        attempt.commit().map_err(JsonEncodeError::<R, Q>::budget)?;
277        Ok(())
278    }
279
280    /// Streams `value` directly to `writer`, retaining accepted prefixes.
281    ///
282    /// After serialization succeeds, this method flushes `writer` before
283    /// committing value-resource accounting. A flush failure is reported as a
284    /// writer error, and bytes already accepted remain charged to the output
285    /// budget while value-resource accounting is not committed. A successful
286    /// flush only reports the writer's result; it does not confirm persistence
287    /// to disk or receipt by a remote peer. The writer is borrowed mutably for
288    /// this call. Do not rely on `Drop` to flush buffered output.
289    ///
290    /// # Type Parameters
291    ///
292    /// * `O` - Destination output type.
293    /// * `T` - Source type serialized into JSON.
294    ///
295    /// # Parameters
296    ///
297    /// * `writer` - Destination receiving streamed JSON bytes.
298    /// * `value` - Value to serialize.
299    ///
300    /// # Returns
301    ///
302    /// `Ok(())` after serialization and output accounting complete.
303    ///
304    /// # Errors
305    ///
306    /// Returns [`JsonEncodeErrorKind::Budget`](super::JsonEncodeErrorKind::Budget)
307    /// when accounting rejects value resources or output bytes,
308    /// [`JsonEncodeErrorKind::InvalidRawJson`](super::JsonEncodeErrorKind::InvalidRawJson) when a
309    /// `RawValue` or private raw payload fails this encoder's strict JSON
310    /// admission, a serialization error when Serde rejects `value`, or a
311    /// writer error when writing or flushing fails. Inspect
312    /// [`JsonEncodeError::syntax_error`] for the structured syntax reason.
313    /// Accepted output prefixes remain written and charged when a later
314    /// operation fails; value-resource charges commit only after the complete
315    /// operation succeeds.
316    pub fn write_incremental<O, T>(
317        &mut self,
318        writer: &mut O,
319        value: &T,
320    ) -> Result<(), JsonEncodeError<R, Q>>
321    where
322        O: ByteOutput + ?Sized,
323        T: Serialize + ?Sized,
324    {
325        let has_value_limits = self.session.value_budget().limits().has_limits();
326        let mut attempt = self.session.begin_value();
327        let result = {
328            let (output_budget, transaction) = attempt.split_mut();
329            let accounting = RefCell::new(JsonOutputAccounting::new(output_budget));
330            let mut output = JsonOutputWriter::new(writer, &accounting);
331            let result = {
332                let mut inner = JsonSerializer::new(&mut output);
333                let context = RefCell::new(JsonEncodeContext {
334                    transaction,
335                    output: &accounting,
336                    has_value_limits,
337                });
338                if has_value_limits {
339                    value.serialize(JsonEncodeSerializer::<_, R, Q, true>::new(
340                        &mut inner, &context,
341                    ))
342                } else {
343                    value.serialize(JsonEncodeSerializer::<_, R, Q, false>::new(
344                        &mut inner, &context,
345                    ))
346                }
347            };
348            if result.is_ok() {
349                let _ = Write::flush(&mut output);
350            }
351            output.into_result(result)
352        };
353        result?;
354        attempt.commit().map_err(JsonEncodeError::<R, Q>::budget)?;
355        Ok(())
356    }
357
358    /// Serializes one value into an output-bounded byte buffer.
359    ///
360    /// # Type Parameters
361    ///
362    /// * `T` - Source type serialized into JSON.
363    ///
364    /// # Parameters
365    ///
366    /// * `value` - Value to serialize.
367    /// * `attempt` - Encode attempt receiving staged value and output charges.
368    /// * `has_value_limits` - Whether serialized values have active limits.
369    ///
370    /// # Returns
371    ///
372    /// The serialized JSON bytes after all applicable output checks pass.
373    ///
374    /// # Errors
375    ///
376    /// Returns a budget error when value or output accounting rejects the
377    /// serialization, an invalid-raw-JSON error for malformed private raw
378    /// values, or a serialization error from Serde.
379    fn serialize_buffer<T>(
380        value: &T,
381        attempt: &mut JsonEncodeAttempt<'_, R, Q>,
382        has_value_limits: bool,
383    ) -> Result<Vec<u8>, JsonEncodeError<R, Q>>
384    where
385        T: Serialize + ?Sized,
386    {
387        let (output_budget, transaction) = attempt.split_mut();
388        if output_budget.is_none() {
389            let accounting = RefCell::new(JsonOutputAccounting::new(None));
390            let mut bytes = Vec::new();
391            let result = {
392                let mut inner = JsonSerializer::new(&mut bytes);
393                let context = RefCell::new(JsonEncodeContext {
394                    transaction,
395                    output: &accounting,
396                    has_value_limits,
397                });
398                if has_value_limits {
399                    value.serialize(JsonEncodeSerializer::<_, R, Q, true>::new(
400                        &mut inner, &context,
401                    ))
402                } else {
403                    value.serialize(JsonEncodeSerializer::<_, R, Q, false>::new(
404                        &mut inner, &context,
405                    ))
406                }
407            };
408            if let Some(error) = accounting.borrow_mut().take_violation() {
409                return Err(JsonEncodeError::<R, Q>::budget(error));
410            }
411            if let Some(error) = accounting.borrow_mut().take_syntax_error() {
412                return Err(JsonEncodeError::<R, Q>::invalid_raw_json(error));
413            }
414            if let Some(error) = accounting.borrow_mut().take_serialization_error() {
415                return Err(JsonEncodeError::<R, Q>::serialization(error));
416            }
417            if result.is_err() {
418                let error = accounting.borrow_mut().take_serialization_error_or_custom();
419                return Err(JsonEncodeError::<R, Q>::serialization(error));
420            }
421            return Ok(bytes);
422        }
423        let accounting = RefCell::new(JsonOutputAccounting::new(output_budget));
424        let mut output = JsonOutputBuffer::new(&accounting);
425        let result = {
426            let mut inner = JsonSerializer::new(&mut output);
427            let context = RefCell::new(JsonEncodeContext {
428                transaction,
429                output: &accounting,
430                has_value_limits,
431            });
432            if has_value_limits {
433                value.serialize(JsonEncodeSerializer::<_, R, Q, true>::new(
434                    &mut inner, &context,
435                ))
436            } else {
437                value.serialize(JsonEncodeSerializer::<_, R, Q, false>::new(
438                    &mut inner, &context,
439                ))
440            }
441        };
442        if result.is_ok() {
443            let _ = Write::flush(&mut output);
444        }
445        output.into_result(result)
446    }
447
448    /// Writes buffered bytes and charges each accepted prefix.
449    ///
450    /// # Type Parameters
451    ///
452    /// * `O` - Destination output type.
453    ///
454    /// # Parameters
455    ///
456    /// * `writer` - Destination receiving the buffered JSON bytes.
457    /// * `bytes` - Complete serialized document to write.
458    /// * `attempt` - Encode attempt receiving output charges.
459    ///
460    /// # Returns
461    ///
462    /// Returns `Ok(())` after all bytes are written and accounted for.
463    ///
464    /// # Errors
465    ///
466    /// Returns a budget error if output accounting rejects a written prefix,
467    /// or a writer error if the destination cannot accept the bytes.
468    fn write_buffer<O>(
469        writer: &mut O,
470        bytes: &[u8],
471        attempt: &mut JsonEncodeAttempt<'_, R, Q>,
472    ) -> Result<(), JsonEncodeError<R, Q>>
473    where
474        O: ByteOutput + ?Sized,
475    {
476        let (output_budget, _) = attempt.split_mut();
477        let accounting = RefCell::new(JsonOutputAccounting::new(output_budget));
478        let mut output = JsonOutputWriter::new(writer, &accounting);
479        let result = output.write_all(bytes).map_err(JsonError::io);
480        output.into_result(result)
481    }
482}