Skip to content

Commit ee610a3

Browse files
authored
Add convenience APIs on operations (#905)
* `OperationMap::len` and `OperationMap::is_empty`, making it more like a collection type * `Operation::all_fields` and `Operation::root_fields` iterators
1 parent b9f39e8 commit ee610a3

File tree

3 files changed

+165
-37
lines changed

3 files changed

+165
-37
lines changed

crates/apollo-compiler/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,14 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
3232

3333
- **Port MaxIntrospectionDepthRule from graphql-js - [SimonSapin] in [pull/904].**
3434
This limits list nesting in introspection query which can cause very large responses.
35+
- **Add convenience APIs on operations - [SimonSapin] in [pull/905]**
36+
* `OperationMap::len` and `OperationMap::is_empty`, making it more like a collection type
37+
* `Operation::all_fields` and `Operation::root_fields` iterators
3538

3639
[SimonSapin]: https://github.com/SimonSapin
3740
[pull/898]: https://github.com/apollographql/apollo-rs/pull/898
3841
[pull/904]: https://github.com/apollographql/apollo-rs/pull/904
42+
[pull/905]: https://github.com/apollographql/apollo-rs/pull/905
3943

4044

4145
# [1.0.0-beta.20](https://crates.io/crates/apollo-compiler/1.0.0-beta.20) - 2024-07-30

crates/apollo-compiler/src/executable/mod.rs

Lines changed: 107 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
//! which can contain operations and fragments.
33
44
use crate::ast;
5-
use crate::collections::HashSet;
65
use crate::collections::IndexMap;
76
use crate::coordinate::FieldArgumentCoordinate;
87
use crate::coordinate::TypeAttributeCoordinate;
@@ -350,6 +349,14 @@ impl OperationMap {
350349
map
351350
}
352351

352+
pub fn is_empty(&self) -> bool {
353+
self.anonymous.is_none() && self.named.is_empty()
354+
}
355+
356+
pub fn len(&self) -> usize {
357+
self.anonymous.is_some() as usize + self.named.len()
358+
}
359+
353360
/// Returns an iterator of operations, both anonymous and named
354361
pub fn iter(&self) -> impl Iterator<Item = &'_ Node<Operation>> {
355362
self.anonymous
@@ -444,44 +451,107 @@ impl Operation {
444451
/// Return whether this operation is a query that only selects introspection meta-fields:
445452
/// `__type`, `__schema`, and `__typename`
446453
pub fn is_introspection(&self, document: &ExecutableDocument) -> bool {
447-
fn is_introspection_impl<'a>(
448-
document: &'a ExecutableDocument,
449-
seen_fragments: &mut HashSet<&'a Name>,
450-
set: &'a SelectionSet,
451-
) -> bool {
452-
set.selections.iter().all(|selection| match selection {
453-
Selection::Field(field) => {
454-
matches!(field.name.as_str(), "__type" | "__schema" | "__typename")
455-
}
456-
Selection::FragmentSpread(spread) => {
457-
document
458-
.fragments
459-
.get(&spread.fragment_name)
460-
.is_some_and(|fragment| {
461-
let new = seen_fragments.insert(&spread.fragment_name);
462-
if new {
463-
is_introspection_impl(
464-
document,
465-
seen_fragments,
466-
&fragment.selection_set,
467-
)
468-
} else {
469-
// This isn't the first time we've seen this spread.
470-
// We trust that the first visit will find all
471-
// relevant fields and stop the recursion (without
472-
// affecting the overall `all` result).
473-
true
474-
}
475-
})
476-
}
477-
Selection::InlineFragment(inline) => {
478-
is_introspection_impl(document, seen_fragments, &inline.selection_set)
454+
self.is_query()
455+
&& self
456+
.root_fields(document)
457+
.all(|field| matches!(field.name.as_str(), "__type" | "__schema" | "__typename"))
458+
}
459+
460+
/// Returns an iterator of field selections that are at the root of the response.
461+
/// That is, inline fragments and fragment spreads at the root are traversed,
462+
/// but field sub-selections are not.
463+
///
464+
/// See also [`all_fields`][Self::all_fields].
465+
///
466+
/// `document` is used to look up fragment definitions.
467+
///
468+
/// This does **not** perform [field merging] nor fragment spreads de-duplication,
469+
/// so multiple items in this iterator may have the same response key,
470+
/// point to the same field definition, or even be the same field selection.
471+
///
472+
/// [field merging]: https://spec.graphql.org/draft/#sec-Field-Selection-Merging
473+
pub fn root_fields<'doc>(
474+
&'doc self,
475+
document: &'doc ExecutableDocument,
476+
) -> impl Iterator<Item = &'doc Node<Field>> {
477+
let mut stack = vec![self.selection_set.selections.iter()];
478+
std::iter::from_fn(move || {
479+
while let Some(selection_set_iter) = stack.last_mut() {
480+
match selection_set_iter.next() {
481+
Some(Selection::Field(field)) => {
482+
// Yield one item from the `root_fields()` iterator
483+
// but ignore its sub-selections in `field.selection_set`
484+
return Some(field);
485+
}
486+
Some(Selection::InlineFragment(inline)) => {
487+
stack.push(inline.selection_set.selections.iter())
488+
}
489+
Some(Selection::FragmentSpread(spread)) => {
490+
if let Some(def) = document.fragments.get(&spread.fragment_name) {
491+
stack.push(def.selection_set.selections.iter())
492+
} else {
493+
// Undefined fragments are silently ignored.
494+
// They should never happen in a valid document.
495+
}
496+
}
497+
None => {
498+
// Remove an empty iterator from the stack
499+
// and continue with the parent selection set
500+
stack.pop();
501+
}
479502
}
480-
})
481-
}
503+
}
504+
None
505+
})
506+
}
482507

483-
self.operation_type == OperationType::Query
484-
&& is_introspection_impl(document, &mut HashSet::default(), &self.selection_set)
508+
/// Returns an iterator of all field selections in this operation.
509+
///
510+
/// See also [`root_fields`][Self::root_fields].
511+
///
512+
/// `document` is used to look up fragment definitions.
513+
///
514+
/// This does **not** perform [field merging] nor fragment spreads de-duplication,
515+
/// so multiple items in this iterator may have the same response key,
516+
/// point to the same field definition, or even be the same field selection.
517+
///
518+
/// [field merging]: https://spec.graphql.org/draft/#sec-Field-Selection-Merging
519+
pub fn all_fields<'doc>(
520+
&'doc self,
521+
document: &'doc ExecutableDocument,
522+
) -> impl Iterator<Item = &'doc Node<Field>> {
523+
let mut stack = vec![self.selection_set.selections.iter()];
524+
std::iter::from_fn(move || {
525+
while let Some(selection_set_iter) = stack.last_mut() {
526+
match selection_set_iter.next() {
527+
Some(Selection::Field(field)) => {
528+
if !field.selection_set.is_empty() {
529+
// Will be considered for the next call
530+
stack.push(field.selection_set.selections.iter())
531+
}
532+
// Yield one item from the `all_fields()` iterator
533+
return Some(field);
534+
}
535+
Some(Selection::InlineFragment(inline)) => {
536+
stack.push(inline.selection_set.selections.iter())
537+
}
538+
Some(Selection::FragmentSpread(spread)) => {
539+
if let Some(def) = document.fragments.get(&spread.fragment_name) {
540+
stack.push(def.selection_set.selections.iter())
541+
} else {
542+
// Undefined fragments are silently ignored.
543+
// They should never happen in a valid document.
544+
}
545+
}
546+
None => {
547+
// Remove an empty iterator from the stack
548+
// and continue with the parent selection set
549+
stack.pop();
550+
}
551+
}
552+
}
553+
None
554+
})
485555
}
486556

487557
serialize_method!();

crates/apollo-compiler/tests/executable.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,3 +233,57 @@ fn is_introspection_repeated_fragment() {
233233
.is_introspection(&query_doc_direct)
234234
);
235235
}
236+
237+
#[test]
238+
fn iter_root_fields() {
239+
let schema = r#"
240+
type Query {
241+
f1: T
242+
f2: Int
243+
f3: Int
244+
}
245+
type T {
246+
inner: String
247+
}
248+
"#;
249+
let doc = r#"
250+
{ f1 { inner } ... { f2 } ... F ... F }
251+
fragment F on Query { f3 }
252+
"#;
253+
let schema = Schema::parse_and_validate(schema, "").unwrap();
254+
let doc = ExecutableDocument::parse_and_validate(&schema, doc, "").unwrap();
255+
let op = doc.operations.get(None).unwrap();
256+
assert_eq!(
257+
op.root_fields(&doc)
258+
.map(|f| f.name.as_str())
259+
.collect::<Vec<_>>(),
260+
["f1", "f2", "f3", "f3"]
261+
);
262+
}
263+
264+
#[test]
265+
fn iter_all_fields() {
266+
let schema = r#"
267+
type Query {
268+
f1: T
269+
f2: Int
270+
f3: Int
271+
}
272+
type T {
273+
inner: String
274+
}
275+
"#;
276+
let doc = r#"
277+
{ f1 { inner } ... { f2 } ... F ... F }
278+
fragment F on Query { f3 }
279+
"#;
280+
let schema = Schema::parse_and_validate(schema, "").unwrap();
281+
let doc = ExecutableDocument::parse_and_validate(&schema, doc, "").unwrap();
282+
let op = doc.operations.get(None).unwrap();
283+
assert_eq!(
284+
op.all_fields(&doc)
285+
.map(|f| f.name.as_str())
286+
.collect::<Vec<_>>(),
287+
["f1", "inner", "f2", "f3", "f3"]
288+
);
289+
}

0 commit comments

Comments
 (0)