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 { category, line, column }
73 }
74
75 /// Returns the Serde JSON failure category.
76 ///
77 /// # Returns
78 ///
79 /// The category associated with the failure.
80 #[must_use]
81 #[inline]
82 pub fn classify(&self) -> Category {
83 self.category
84 }
85
86 /// Returns the one-based input line, or zero when unavailable.
87 ///
88 /// # Returns
89 ///
90 /// The source line, or zero if no source position is available.
91 #[must_use]
92 #[inline]
93 pub fn line(&self) -> usize {
94 self.line
95 }
96
97 /// Returns the one-based input column, or zero when unavailable.
98 ///
99 /// # Returns
100 ///
101 /// The source column, or zero if no source position is available.
102 #[must_use]
103 #[inline]
104 pub fn column(&self) -> usize {
105 self.column
106 }
107
108 /// Returns whether the error is an I/O failure.
109 ///
110 /// # Returns
111 ///
112 /// `true` if the category is `Category::Io`; otherwise, `false`.
113 #[must_use]
114 #[inline]
115 pub fn is_io(&self) -> bool {
116 self.category == Category::Io
117 }
118
119 /// Returns whether the error is a JSON syntax failure.
120 ///
121 /// # Returns
122 ///
123 /// `true` if the category is `Category::Syntax`; otherwise, `false`.
124 #[must_use]
125 #[inline]
126 pub fn is_syntax(&self) -> bool {
127 self.category == Category::Syntax
128 }
129
130 /// Returns whether the JSON data has an incompatible type or value.
131 ///
132 /// # Returns
133 ///
134 /// `true` if the category is `Category::Data`; otherwise, `false`.
135 #[must_use]
136 #[inline]
137 pub fn is_data(&self) -> bool {
138 self.category == Category::Data
139 }
140
141 /// Returns whether the JSON input ended before decoding completed.
142 ///
143 /// # Returns
144 ///
145 /// `true` if the category is `Category::Eof`; otherwise, `false`.
146 #[must_use]
147 #[inline]
148 pub fn is_eof(&self) -> bool {
149 self.category == Category::Eof
150 }
151}
152
153impl fmt::Display for ValueWireJsonError {
154 /// Writes the failure category and coordinates without exposing input data.
155 ///
156 /// # Parameters
157 ///
158 /// - `formatter`: Destination for the privacy-safe diagnostic.
159 ///
160 /// # Returns
161 ///
162 /// Success after writing the diagnostic.
163 ///
164 /// # Errors
165 ///
166 /// Returns the destination's formatting error if writing fails.
167 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
168 write!(
169 formatter,
170 "JSON deserialization failed ({:?}) at line {}, column {}",
171 self.category, self.line, self.column
172 )
173 }
174}
175
176impl std::error::Error for ValueWireJsonError {}