1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
//! Compute the worst-case **node count** GitHub's GraphQL API attributes to a
//! query — offline, from the query text and its page-size variable bindings
//! alone, before the query is ever sent.
//!
//! # `nodeCount`, not `cost`
//!
//! GitHub meters two different numbers against two different limits, and this
//! crate is about one of them.
//!
//! * **`nodeCount`** — the maximum number of nodes **one query may return**,
//! limited **per query** at [`NODE_LIMIT`]. That is what [`node_count`]
//! computes.
//! * **`cost`** — the rate-limit **points** a call spends, metered **per hour**
//! across everything one credential does.
//!
//! Neither the code nor these docs computes or claims anything about `cost`.
//!
//! # The rules, and where they come from
//!
//! GitHub publishes them at
//! <https://docs.github.com/en/graphql/overview/rate-limits-and-node-limits-for-the-graphql-api>:
//! node counts **multiply** down a nested path, **sum** across sibling paths,
//! every connection supplies a `first` or a `last` inside `1..=100`, and the
//! limit one query may not reach is 500,000. Check this crate against that page
//! rather than against our confidence.
//!
//! # No schema, by design
//!
//! [`node_count`] is handed document text and nothing else. It reaches no
//! network, reads no credential, and consults no GraphQL schema — so it cannot
//! know which fields are connections by type. Instead:
//!
//! * a field carrying a `first:` or a `last:` argument **is** a connection, and
//! multiplies the count beneath it;
//! * every other field contributes no multiplier and no nodes of its own.
//!
//! That is what makes this crate runnable in a fork pull request with no secret,
//! which is the whole reason a consumer can put it in a gate. The cost is one
//! blind spot, stated plainly: **a connection that supplies neither `first` nor
//! `last` is invalid to GitHub and invisible here.** Such a field is counted as
//! an ordinary field, so the answer is an undercount rather than an error. A
//! field supplying *both* is also invalid to GitHub; the larger of the two is
//! used, so the answer stays a worst case.
//!
//! # Example
//!
//! ```
//! use github_graphql_node_count::{node_count, NodeCountError, Variables, NODE_LIMIT};
//!
//! let document = r#"
//! query($repos: Int!) {
//! viewer {
//! repositories(first: $repos) {
//! edges { node { name issues(first: 10) { edges { node { title } } } } }
//! }
//! }
//! }
//! "#;
//! let variables = Variables::from([("repos".to_string(), 50)]);
//!
//! // 50 repositories + 50 x 10 issues.
//! assert_eq!(node_count(document, &variables)?, 550);
//! assert!(node_count(document, &variables)? < NODE_LIMIT);
//!
//! // A page size GitHub would reject comes back as an error, not a number.
//! let over = Variables::from([("repos".to_string(), 500)]);
//! assert!(matches!(
//! node_count(document, &over),
//! Err(NodeCountError::PageSizeOutOfRange { .. })
//! ));
//! # Ok::<(), NodeCountError>(())
//! ```
pub use ;
/// GitHub's published limit on the number of nodes one query may return.
///
/// > Individual calls cannot request more than 500,000 total nodes.
///
/// — [Rate limits and node limits for the GraphQL API](https://docs.github.com/en/graphql/overview/rate-limits-and-node-limits-for-the-graphql-api).
///
/// This is a **per-query** ceiling on `nodeCount`. It is not the hourly `cost`
/// budget, which this crate says nothing about.
pub const NODE_LIMIT: u64 = 500_000;
/// The integer bound each page-size variable a document names is given, keyed by
/// variable name **without** the leading `$`.
///
/// A variable a document declares but no `first:`/`last:` argument references
/// need not appear. A variable a `first:`/`last:` *does* reference must, or
/// [`node_count`] returns [`NodeCountError::UnboundVariable`] — a declared
/// default value is not consulted.
///
/// A `u32` rather than a validated page-size newtype: this alias is a contract
/// with the repositories that call this crate, restated verbatim in their own
/// builds, so it is not ours to narrow. The `1..=100` range GitHub requires is
/// enforced where the value is *used*, and a binding outside it comes back as
/// [`NodeCountError::PageSizeOutOfRange`] rather than being silently counted.
// llmlint: ignore[invalid_states_unrepresentable] the type of this alias is a frozen
// cross-repository contract (see the paragraph above); narrowing it to a newtype would break
// the consumer written against it, so the range is validated at the one point of use instead.
pub type Variables = BTreeMap;
/// The worst-case number of nodes the one operation in `document` may return,
/// under `variables`, computed by GitHub's published rules.
///
/// The document must hold exactly one operation. Fragment definitions beside it
/// are resolved, so the natural consumer shape — one shared fragment
/// concatenated onto each of several operations, giving several
/// single-operation documents — is counted correctly, once per document.
///
/// # Errors
///
/// Returns [`NodeCountError`] rather than panicking, returning zero, or
/// returning a wrong count, when:
///
/// * the text does not parse as GraphQL ([`NodeCountError::Parse`]);
/// * the document declares no operation ([`NodeCountError::NoOperation`]);
/// * it declares more than one, which this signature cannot disambiguate
/// ([`NodeCountError::MultipleOperations`]);
/// * a `first:`/`last:` names a variable `variables` does not bind
/// ([`NodeCountError::UnboundVariable`]);
/// * a `first:`/`last:` value falls outside `1..=100`
/// ([`NodeCountError::PageSizeOutOfRange`]) or is not an integer at all
/// ([`NodeCountError::PageSizeNotAnInteger`]);
/// * a spread names a fragment the document does not define
/// ([`NodeCountError::UndefinedFragment`]), or the spreads form a cycle
/// ([`NodeCountError::FragmentCycle`]);
/// * the count grows past `u64` ([`NodeCountError::Overflow`]), which only a
/// document far above [`NODE_LIMIT`] can do.