qubit_value/value_wire/value_wire_json_error.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
9//! Privacy-safe structured metadata for JSON wire decoding failures.
10
11use std::fmt;
12
13use serde_json::error::Category;
14
15/// Privacy-safe classification and source position for a JSON wire failure.
16///
17/// Obtain this metadata from [`ValueWireDecodeError::InvalidJson`] rather than
18/// constructing it directly. It contains only the Serde category and source
19/// coordinates; it does not retain the input or detailed error message. A
20/// coordinate of zero means that the source position is unavailable, as can
21/// happen for target-type failures after decoding through a unique-key tree.
22/// Direct conversion from a Serde error preserves its source coordinates.
23///
24/// [`ValueWireDecodeError::InvalidJson`]: crate::ValueWireDecodeError::InvalidJson
25///
26/// # Examples
27///
28/// ```
29/// use qubit_value::ValueWireDecodeError;
30/// use qubit_value::ValueWireJsonError;
31/// use serde_json::error::Category;
32///
33/// fn check_metadata(metadata: &ValueWireJsonError) {
34/// assert_eq!(metadata.classify(), Category::Data);
35/// assert!(metadata.is_data());
36/// assert!(metadata.line() > 0);
37/// assert!(metadata.column() > 0);
38/// }
39///
40/// let source = serde_json::from_str::<u64>(r#""not a number""#).unwrap_err();
41/// let error = ValueWireDecodeError::from(source);
42/// let ValueWireDecodeError::InvalidJson(metadata) = error else {
43/// panic!("direct Serde conversion must preserve safe metadata");
44/// };
45/// check_metadata(&metadata);
46/// ```
47#[must_use]
48#[derive(Debug)]
49pub struct ValueWireJsonError {
50 /// Serde's category for the JSON decoding failure.
51 category: Category,
52 /// One-based source line, or zero when unavailable.
53 line: usize,
54 /// One-based source column, or zero when unavailable.
55 column: usize,
56}
57
58impl ValueWireJsonError {
59 /// Constructs safe JSON error metadata.
60 ///
61 /// # Parameters
62 ///
63 /// - `category`: Serde's category for the failure.
64 /// - `line`: one-based source line, or zero when unavailable.
65 /// - `column`: one-based source column, or zero when unavailable.
66 ///
67 /// # Returns
68 ///
69 /// The metadata containing only the supplied category and coordinates.
70 #[inline]
71 pub(crate) fn new(category: Category, line: usize, column: usize) -> Self {
72 Self {
73 category,
74 line,
75 column,
76 }
77 }
78
79 /// Returns the Serde JSON failure category.
80 ///
81 /// # Returns
82 ///
83 /// The category associated with the failure.
84 #[must_use]
85 #[inline]
86 pub fn classify(&self) -> Category {
87 self.category
88 }
89
90 /// Returns the one-based input line, or zero when unavailable.
91 ///
92 /// # Returns
93 ///
94 /// The source line, or zero if no source position is available.
95 #[must_use]
96 #[inline]
97 pub fn line(&self) -> usize {
98 self.line
99 }
100
101 /// Returns the one-based input column, or zero when unavailable.
102 ///
103 /// # Returns
104 ///
105 /// The source column, or zero if no source position is available.
106 #[must_use]
107 #[inline]
108 pub fn column(&self) -> usize {
109 self.column
110 }
111
112 /// Returns whether the error is an I/O failure.
113 ///
114 /// # Returns
115 ///
116 /// `true` if the category is `Category::Io`; otherwise, `false`.
117 #[must_use]
118 #[inline]
119 pub fn is_io(&self) -> bool {
120 self.category == Category::Io
121 }
122
123 /// Returns whether the error is a JSON syntax failure.
124 ///
125 /// # Returns
126 ///
127 /// `true` if the category is `Category::Syntax`; otherwise, `false`.
128 #[must_use]
129 #[inline]
130 pub fn is_syntax(&self) -> bool {
131 self.category == Category::Syntax
132 }
133
134 /// Returns whether the JSON data has an incompatible type or value.
135 ///
136 /// # Returns
137 ///
138 /// `true` if the category is `Category::Data`; otherwise, `false`.
139 #[must_use]
140 #[inline]
141 pub fn is_data(&self) -> bool {
142 self.category == Category::Data
143 }
144
145 /// Returns whether the JSON input ended before decoding completed.
146 ///
147 /// # Returns
148 ///
149 /// `true` if the category is `Category::Eof`; otherwise, `false`.
150 #[must_use]
151 #[inline]
152 pub fn is_eof(&self) -> bool {
153 self.category == Category::Eof
154 }
155}
156
157impl fmt::Display for ValueWireJsonError {
158 /// Writes the failure category and coordinates without exposing input data.
159 ///
160 /// # Parameters
161 ///
162 /// - `formatter`: Destination for the privacy-safe diagnostic.
163 ///
164 /// # Returns
165 ///
166 /// Success after writing the diagnostic.
167 ///
168 /// # Errors
169 ///
170 /// Returns the destination's formatting error if writing fails.
171 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
172 write!(
173 formatter,
174 "JSON deserialization failed ({:?}) at line {}, column {}",
175 self.category, self.line, self.column
176 )
177 }
178}
179
180impl std::error::Error for ValueWireJsonError {}