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}