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>(&mut self, writer: &mut O, value: &T) -> Result<(), JsonEncodeError<R, Q>>
261    where
262        O: ByteOutput + ?Sized,
263        T: Serialize + ?Sized,
264    {
265        let has_value_limits = self.session.value_budget().limits().has_limits();
266        let mut attempt = self.session.begin_value();
267        let bytes = Self::serialize_buffer(value, &mut attempt, has_value_limits)?;
268        attempt
269            .check_output_bytes(bytes.len())
270            .map_err(JsonEncodeError::<R, Q>::budget)?;
271        Self::write_buffer(writer, &bytes, &mut attempt)?;
272        attempt.commit().map_err(JsonEncodeError::<R, Q>::budget)?;
273        Ok(())
274    }
275
276    /// Streams `value` directly to `writer`, retaining accepted prefixes.
277    ///
278    /// After serialization succeeds, this method flushes `writer` before
279    /// committing value-resource accounting. A flush failure is reported as a
280    /// writer error, and bytes already accepted remain charged to the output
281    /// budget while value-resource accounting is not committed. A successful
282    /// flush only reports the writer's result; it does not confirm persistence
283    /// to disk or receipt by a remote peer. The writer is borrowed mutably for
284    /// this call. Do not rely on `Drop` to flush buffered output.
285    ///
286    /// # Type Parameters
287    ///
288    /// * `O` - Destination output type.
289    /// * `T` - Source type serialized into JSON.
290    ///
291    /// # Parameters
292    ///
293    /// * `writer` - Destination receiving streamed JSON bytes.
294    /// * `value` - Value to serialize.
295    ///
296    /// # Returns
297    ///
298    /// `Ok(())` after serialization and output accounting complete.
299    ///
300    /// # Errors
301    ///
302    /// Returns [`JsonEncodeErrorKind::Budget`](super::JsonEncodeErrorKind::Budget)
303    /// when accounting rejects value resources or output bytes,
304    /// [`JsonEncodeErrorKind::InvalidRawJson`](super::JsonEncodeErrorKind::InvalidRawJson) when a
305    /// `RawValue` or private raw payload fails this encoder's strict JSON
306    /// admission, a serialization error when Serde rejects `value`, or a
307    /// writer error when writing or flushing fails. Inspect
308    /// [`JsonEncodeError::syntax_error`] for the structured syntax reason.
309    /// Accepted output prefixes remain written and charged when a later
310    /// operation fails; value-resource charges commit only after the complete
311    /// operation succeeds.
312    pub fn write_incremental<O, T>(&mut self, writer: &mut O, value: &T) -> Result<(), JsonEncodeError<R, Q>>
313    where
314        O: ByteOutput + ?Sized,
315        T: Serialize + ?Sized,
316    {
317        let has_value_limits = self.session.value_budget().limits().has_limits();
318        let mut attempt = self.session.begin_value();
319        let result = {
320            let (output_budget, transaction) = attempt.split_mut();
321            let accounting = RefCell::new(JsonOutputAccounting::new(output_budget));
322            let mut output = JsonOutputWriter::new(writer, &accounting);
323            let result = {
324                let mut inner = JsonSerializer::new(&mut output);
325                let context = RefCell::new(JsonEncodeContext {
326                    transaction,
327                    output: &accounting,
328                    has_value_limits,
329                });
330                if has_value_limits {
331                    value.serialize(JsonEncodeSerializer::<_, R, Q, true>::new(&mut inner, &context))
332                } else {
333                    value.serialize(JsonEncodeSerializer::<_, R, Q, false>::new(&mut inner, &context))
334                }
335            };
336            if result.is_ok() {
337                let _ = Write::flush(&mut output);
338            }
339            output.into_result(result)
340        };
341        result?;
342        attempt.commit().map_err(JsonEncodeError::<R, Q>::budget)?;
343        Ok(())
344    }
345
346    /// Serializes one value into an output-bounded byte buffer.
347    ///
348    /// # Type Parameters
349    ///
350    /// * `T` - Source type serialized into JSON.
351    ///
352    /// # Parameters
353    ///
354    /// * `value` - Value to serialize.
355    /// * `attempt` - Encode attempt receiving staged value and output charges.
356    /// * `has_value_limits` - Whether serialized values have active limits.
357    ///
358    /// # Returns
359    ///
360    /// The serialized JSON bytes after all applicable output checks pass.
361    ///
362    /// # Errors
363    ///
364    /// Returns a budget error when value or output accounting rejects the
365    /// serialization, an invalid-raw-JSON error for malformed private raw
366    /// values, or a serialization error from Serde.
367    fn serialize_buffer<T>(
368        value: &T,
369        attempt: &mut JsonEncodeAttempt<'_, R, Q>,
370        has_value_limits: bool,
371    ) -> Result<Vec<u8>, JsonEncodeError<R, Q>>
372    where
373        T: Serialize + ?Sized,
374    {
375        let (output_budget, transaction) = attempt.split_mut();
376        if output_budget.is_none() {
377            let accounting = RefCell::new(JsonOutputAccounting::new(None));
378            let mut bytes = Vec::new();
379            let result = {
380                let mut inner = JsonSerializer::new(&mut bytes);
381                let context = RefCell::new(JsonEncodeContext {
382                    transaction,
383                    output: &accounting,
384                    has_value_limits,
385                });
386                if has_value_limits {
387                    value.serialize(JsonEncodeSerializer::<_, R, Q, true>::new(&mut inner, &context))
388                } else {
389                    value.serialize(JsonEncodeSerializer::<_, R, Q, false>::new(&mut inner, &context))
390                }
391            };
392            if let Some(error) = accounting.borrow_mut().take_violation() {
393                return Err(JsonEncodeError::<R, Q>::budget(error));
394            }
395            if let Some(error) = accounting.borrow_mut().take_syntax_error() {
396                return Err(JsonEncodeError::<R, Q>::invalid_raw_json(error));
397            }
398            if let Some(error) = accounting.borrow_mut().take_serialization_error() {
399                return Err(JsonEncodeError::<R, Q>::serialization(error));
400            }
401            if result.is_err() {
402                let error = accounting.borrow_mut().take_serialization_error_or_custom();
403                return Err(JsonEncodeError::<R, Q>::serialization(error));
404            }
405            return Ok(bytes);
406        }
407        let accounting = RefCell::new(JsonOutputAccounting::new(output_budget));
408        let mut output = JsonOutputBuffer::new(&accounting);
409        let result = {
410            let mut inner = JsonSerializer::new(&mut output);
411            let context = RefCell::new(JsonEncodeContext {
412                transaction,
413                output: &accounting,
414                has_value_limits,
415            });
416            if has_value_limits {
417                value.serialize(JsonEncodeSerializer::<_, R, Q, true>::new(&mut inner, &context))
418            } else {
419                value.serialize(JsonEncodeSerializer::<_, R, Q, false>::new(&mut inner, &context))
420            }
421        };
422        if result.is_ok() {
423            let _ = Write::flush(&mut output);
424        }
425        output.into_result(result)
426    }
427
428    /// Writes buffered bytes and charges each accepted prefix.
429    ///
430    /// # Type Parameters
431    ///
432    /// * `O` - Destination output type.
433    ///
434    /// # Parameters
435    ///
436    /// * `writer` - Destination receiving the buffered JSON bytes.
437    /// * `bytes` - Complete serialized document to write.
438    /// * `attempt` - Encode attempt receiving output charges.
439    ///
440    /// # Returns
441    ///
442    /// Returns `Ok(())` after all bytes are written and accounted for.
443    ///
444    /// # Errors
445    ///
446    /// Returns a budget error if output accounting rejects a written prefix,
447    /// or a writer error if the destination cannot accept the bytes.
448    fn write_buffer<O>(
449        writer: &mut O,
450        bytes: &[u8],
451        attempt: &mut JsonEncodeAttempt<'_, R, Q>,
452    ) -> Result<(), JsonEncodeError<R, Q>>
453    where
454        O: ByteOutput + ?Sized,
455    {
456        let (output_budget, _) = attempt.split_mut();
457        let accounting = RefCell::new(JsonOutputAccounting::new(output_budget));
458        let mut output = JsonOutputWriter::new(writer, &accounting);
459        let result = output.write_all(bytes).map_err(JsonError::io);
460        output.into_result(result)
461    }
462}