|
2 | 2 | //! which can contain operations and fragments.
|
3 | 3 |
|
4 | 4 | use crate::ast;
|
5 |
| -use crate::collections::HashSet; |
6 | 5 | use crate::collections::IndexMap;
|
7 | 6 | use crate::coordinate::FieldArgumentCoordinate;
|
8 | 7 | use crate::coordinate::TypeAttributeCoordinate;
|
@@ -350,6 +349,14 @@ impl OperationMap {
|
350 | 349 | map
|
351 | 350 | }
|
352 | 351 |
|
| 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 | + |
353 | 360 | /// Returns an iterator of operations, both anonymous and named
|
354 | 361 | pub fn iter(&self) -> impl Iterator<Item = &'_ Node<Operation>> {
|
355 | 362 | self.anonymous
|
@@ -444,44 +451,107 @@ impl Operation {
|
444 | 451 | /// Return whether this operation is a query that only selects introspection meta-fields:
|
445 | 452 | /// `__type`, `__schema`, and `__typename`
|
446 | 453 | 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 | + } |
479 | 502 | }
|
480 |
| - }) |
481 |
| - } |
| 503 | + } |
| 504 | + None |
| 505 | + }) |
| 506 | + } |
482 | 507 |
|
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 | + }) |
485 | 555 | }
|
486 | 556 |
|
487 | 557 | serialize_method!();
|
|
0 commit comments