github_graphql_node_count/error.rs
1//! The one error type [`node_count`](crate::node_count) returns.
2
3use std::fmt;
4
5use graphql_parser::Pos;
6
7/// Which argument a connection took its page size from.
8///
9/// GraphQL and GitHub define exactly these two, so the set is closed and a
10/// consumer may match it exhaustively.
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum PageSizeArgument {
13 /// `first:` — a forward page.
14 First,
15 /// `last:` — a backward page.
16 Last,
17}
18
19impl PageSizeArgument {
20 /// The argument name as a document spells it.
21 pub const fn as_str(self) -> &'static str {
22 match self {
23 Self::First => "first",
24 Self::Last => "last",
25 }
26 }
27}
28
29impl fmt::Display for PageSizeArgument {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 f.write_str(self.as_str())
32 }
33}
34
35/// Where in the document a problem was found, rendered as `line:column`.
36///
37/// `graphql-parser` reports a position for every field and fragment, so an error
38/// can point a reader at the text that caused it rather than at the whole
39/// document.
40// llmlint: ignore[invalid_states_unrepresentable] a 1-based line/column arrives from the
41// parser as a plain `usize` and is only ever rendered; narrowing it to `NonZeroUsize` would
42// buy an unrepresentable zero at the price of an unreachable fallback at the `From<Pos>`
43// boundary — trading a state nothing constructs for a branch no test can cover.
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
45pub struct Position {
46 /// 1-based line number.
47 pub line: usize,
48 /// 1-based column number.
49 pub column: usize,
50}
51
52impl From<Pos> for Position {
53 fn from(pos: Pos) -> Self {
54 Self {
55 line: pos.line,
56 column: pos.column,
57 }
58 }
59}
60
61impl fmt::Display for Position {
62 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
63 write!(f, "{}:{}", self.line, self.column)
64 }
65}
66
67/// Why a document's node count could not be computed.
68///
69/// Marked `#[non_exhaustive]`: a future release may add a variant without that
70/// being a breaking change, so match with a `_ =>` arm.
71///
72/// Every variant's [`Display`](fmt::Display) names the field or the document
73/// position at fault, so a consumer can report which part of its query is wrong
74/// without re-parsing the document itself.
75#[derive(Debug, Clone, PartialEq, Eq)]
76#[non_exhaustive]
77pub enum NodeCountError {
78 /// The text is not a GraphQL document.
79 Parse {
80 /// The parser's own message, which carries the position it stopped at.
81 message: String,
82 },
83 /// The document declares no operation, so there is nothing to count.
84 NoOperation,
85 /// The document declares more than one operation.
86 ///
87 /// [`node_count`](crate::node_count) counts *the* operation in a document and
88 /// takes no operation name, so it cannot say which one was meant. Split the
89 /// document and ask once per operation.
90 MultipleOperations {
91 /// The operations found, named in document order; an anonymous operation
92 /// appears as `<anonymous>`.
93 names: Vec<String>,
94 },
95 /// A `first:`/`last:` argument names a variable the caller did not bind.
96 UnboundVariable {
97 /// The field carrying the argument, as the document spells it.
98 field: String,
99 /// Which page-size argument it was.
100 argument: PageSizeArgument,
101 /// The variable name, without the leading `$`.
102 variable: String,
103 /// Where the field appears in the document.
104 position: Position,
105 },
106 /// A `first:`/`last:` page size falls outside GitHub's `1..=100`.
107 PageSizeOutOfRange {
108 /// The field carrying the argument, as the document spells it.
109 field: String,
110 /// Which page-size argument it was.
111 argument: PageSizeArgument,
112 /// The page size that was rejected.
113 value: i64,
114 /// Where the field appears in the document.
115 position: Position,
116 },
117 /// A `first:`/`last:` argument is neither an integer literal nor a variable.
118 PageSizeNotAnInteger {
119 /// The field carrying the argument, as the document spells it.
120 field: String,
121 /// Which page-size argument it was.
122 argument: PageSizeArgument,
123 /// The argument value as the document wrote it.
124 found: String,
125 /// Where the field appears in the document.
126 position: Position,
127 },
128 /// A spread names a fragment the document does not define.
129 UndefinedFragment {
130 /// The fragment name, without the leading `...`.
131 name: String,
132 /// Where the spread appears in the document.
133 position: Position,
134 },
135 /// A fragment spreads itself, directly or through other fragments.
136 ///
137 /// GraphQL forbids this; counting it would not terminate.
138 FragmentCycle {
139 /// The fragment the cycle was detected re-entering.
140 name: String,
141 /// Where the spread that closes the cycle appears.
142 position: Position,
143 },
144 /// The count grew past what a `u64` can express.
145 ///
146 /// Reachable only from a document nesting enough maximum-size pages that the
147 /// running multiplier overflows — far above [`NODE_LIMIT`](crate::NODE_LIMIT),
148 /// so such a document is over the limit whatever the exact number would be.
149 Overflow {
150 /// The field whose multiplication overflowed.
151 field: String,
152 /// Where that field appears in the document.
153 position: Position,
154 },
155}
156
157impl fmt::Display for NodeCountError {
158 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159 match self {
160 Self::Parse { message } => {
161 write!(f, "the document is not valid GraphQL: {message}")
162 }
163 Self::NoOperation => f.write_str("the document declares no operation to count"),
164 Self::MultipleOperations { names } => write!(
165 f,
166 "the document declares {} operations ({}); node_count counts one \
167 operation and cannot tell which was meant",
168 names.len(),
169 names.join(", "),
170 ),
171 Self::UnboundVariable {
172 field,
173 argument,
174 variable,
175 position,
176 } => write!(
177 f,
178 "at {position}, field `{field}` passes `{argument}: ${variable}`, \
179 but no binding for `{variable}` was supplied",
180 ),
181 Self::PageSizeOutOfRange {
182 field,
183 argument,
184 value,
185 position,
186 } => write!(
187 f,
188 "at {position}, field `{field}` passes `{argument}: {value}`, \
189 outside GitHub's page-size range 1..=100",
190 ),
191 Self::PageSizeNotAnInteger {
192 field,
193 argument,
194 found,
195 position,
196 } => write!(
197 f,
198 "at {position}, field `{field}` passes `{argument}: {found}`, \
199 which is neither an integer nor a variable",
200 ),
201 Self::UndefinedFragment { name, position } => write!(
202 f,
203 "at {position}, the spread `...{name}` names a fragment the document \
204 does not define",
205 ),
206 Self::FragmentCycle { name, position } => write!(
207 f,
208 "at {position}, the spread `...{name}` re-enters a fragment already \
209 being counted; fragment spreads must not form a cycle",
210 ),
211 Self::Overflow { field, position } => write!(
212 f,
213 "at {position}, the node count for field `{field}` grew past what a \
214 64-bit integer can express; the document is far above NODE_LIMIT",
215 ),
216 }
217 }
218}
219
220impl std::error::Error for NodeCountError {}