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}