Skip to main content

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