Skip to main content

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 {}