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}