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