Skip to main content

qubit_json/decode/
json_syntax_error_reason.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//! Stable reasons reported by strict JSON lexical admission.
9
10use std::fmt;
11
12use crate::lexical::JsonLexicalErrorReason;
13
14/// The concrete reason why a JSON document was rejected lexically.
15///
16/// This enum intentionally remains exhaustive so callers can classify every
17/// documented lexical rejection at compile time. New reasons require a
18/// breaking release rather than a `#[non_exhaustive]` change.
19///
20/// # Examples
21///
22/// ```
23/// use qubit_json::decode::JsonSyntaxErrorReason;
24///
25/// let reason = JsonSyntaxErrorReason::UnexpectedByte;
26/// assert_eq!(reason.to_string(), "unexpected byte");
27/// ```
28#[must_use]
29#[derive(Clone, Copy, Debug, Eq, PartialEq)]
30pub enum JsonSyntaxErrorReason {
31    /// The document ended before a complete token or container was found.
32    UnexpectedEnd,
33    /// A byte is not valid at the current JSON position.
34    ///
35    /// The byte itself is intentionally not retained, so default structured
36    /// diagnostics never expose input content.
37    UnexpectedByte,
38    /// An object key was not followed by a colon.
39    ExpectedColon,
40    /// An array value was not followed by a comma or closing bracket.
41    ExpectedCommaOrArrayEnd,
42    /// An object value was not followed by a comma or closing brace.
43    ExpectedCommaOrObjectEnd,
44    /// An object key was expected at the current position.
45    ExpectedObjectKey,
46    /// A string escape sequence is invalid.
47    InvalidEscape,
48    /// A Unicode escape does not contain four hexadecimal digits.
49    InvalidUnicodeEscape,
50    /// A Unicode surrogate pair is malformed.
51    UnpairedSurrogate,
52    /// The input contains invalid UTF-8.
53    InvalidUtf8,
54    /// A number does not follow JSON number grammar.
55    InvalidNumber,
56    /// An integer is outside the supported `i64`/`u64` range.
57    IntegerOutOfRange,
58    /// A number is outside the finite `f64` range for the selected number
59    /// model.
60    FloatOutOfRange,
61    /// Non-whitespace bytes follow the complete root value.
62    TrailingCharacters,
63    /// A nesting or position counter overflowed.
64    NestingOverflow,
65}
66
67impl From<JsonLexicalErrorReason> for JsonSyntaxErrorReason {
68    /// Maps a lexical rejection reason to its public diagnostic counterpart.
69    ///
70    /// The mapping is exhaustive so each internal lexical failure retains its
71    /// corresponding stable public reason.
72    ///
73    /// # Parameters
74    ///
75    /// * `reason` - Internal reason produced by strict lexical scanning.
76    ///
77    /// # Returns
78    ///
79    /// The public syntax reason corresponding to `reason`.
80    #[inline]
81    fn from(reason: JsonLexicalErrorReason) -> Self {
82        match reason {
83            JsonLexicalErrorReason::UnexpectedEnd => Self::UnexpectedEnd,
84            JsonLexicalErrorReason::UnexpectedByte => Self::UnexpectedByte,
85            JsonLexicalErrorReason::ExpectedColon => Self::ExpectedColon,
86            JsonLexicalErrorReason::ExpectedCommaOrArrayEnd => Self::ExpectedCommaOrArrayEnd,
87            JsonLexicalErrorReason::ExpectedCommaOrObjectEnd => Self::ExpectedCommaOrObjectEnd,
88            JsonLexicalErrorReason::ExpectedObjectKey => Self::ExpectedObjectKey,
89            JsonLexicalErrorReason::InvalidEscape => Self::InvalidEscape,
90            JsonLexicalErrorReason::InvalidUnicodeEscape => Self::InvalidUnicodeEscape,
91            JsonLexicalErrorReason::UnpairedSurrogate => Self::UnpairedSurrogate,
92            JsonLexicalErrorReason::InvalidUtf8 => Self::InvalidUtf8,
93            JsonLexicalErrorReason::InvalidNumber => Self::InvalidNumber,
94            JsonLexicalErrorReason::IntegerOutOfRange => Self::IntegerOutOfRange,
95            JsonLexicalErrorReason::FloatOutOfRange => Self::FloatOutOfRange,
96            JsonLexicalErrorReason::TrailingCharacters => Self::TrailingCharacters,
97            JsonLexicalErrorReason::NestingOverflow => Self::NestingOverflow,
98            JsonLexicalErrorReason::ConsumerStopped => Self::UnexpectedByte,
99            JsonLexicalErrorReason::EncodedStringLimit { .. } => Self::UnexpectedByte,
100        }
101    }
102}
103
104impl fmt::Display for JsonSyntaxErrorReason {
105    /// Formats this reason as stable, input-independent diagnostic text.
106    ///
107    /// # Parameters
108    ///
109    /// * `formatter` - Destination for the formatted reason.
110    ///
111    /// # Returns
112    ///
113    /// `Ok(())` when the reason is written successfully.
114    ///
115    /// # Errors
116    ///
117    /// Returns the formatter's error if writing the reason fails.
118    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
119        match self {
120            Self::UnexpectedEnd => formatter.write_str("unexpected end of input"),
121            Self::UnexpectedByte => formatter.write_str("unexpected byte"),
122            Self::ExpectedColon => formatter.write_str("expected ':'"),
123            Self::ExpectedCommaOrArrayEnd => formatter.write_str("expected ',' or ']' in array"),
124            Self::ExpectedCommaOrObjectEnd => formatter.write_str("expected ',' or '}' in object"),
125            Self::ExpectedObjectKey => formatter.write_str("expected object key"),
126            Self::InvalidEscape => formatter.write_str("invalid string escape"),
127            Self::InvalidUnicodeEscape => formatter.write_str("invalid Unicode escape"),
128            Self::UnpairedSurrogate => formatter.write_str("unpaired Unicode surrogate"),
129            Self::InvalidUtf8 => formatter.write_str("invalid UTF-8"),
130            Self::InvalidNumber => formatter.write_str("invalid JSON number"),
131            Self::IntegerOutOfRange => {
132                formatter.write_str("JSON integer is outside the supported 64-bit range")
133            }
134            Self::FloatOutOfRange => {
135                formatter.write_str("JSON number is outside the finite f64 range")
136            }
137            Self::TrailingCharacters => formatter.write_str("trailing characters"),
138            Self::NestingOverflow => formatter.write_str("JSON nesting overflow"),
139        }
140    }
141}