use crate::ast;
use crate::collections::HashSet;
use crate::collections::IndexMap;
use crate::executable::Field;
use crate::executable::Fragment;
use crate::executable::FragmentSpread;
use crate::executable::InlineFragment;
use crate::executable::Operation;
use crate::executable::OperationType;
use crate::executable::Selection;
use crate::executable::SelectionSet;
use crate::execution::introspection_max_depth::DeeplyNestedIntrospectionListError;
use crate::execution::GraphQLError;
use crate::execution::Response;
use crate::execution::SchemaIntrospectionQuery;
use crate::parser::SourceMap;
use crate::parser::SourceSpan;
use crate::schema;
use crate::schema::Name;
use crate::validation::SuspectedValidationBug;
use crate::validation::Valid;
use crate::ExecutableDocument;
use crate::Node;
use crate::Schema;
use indexmap::map::Entry;
#[derive(Clone, Debug)]
pub enum SchemaIntrospectionSplit {
None,
Only(SchemaIntrospectionQuery),
Both {
introspection_query: SchemaIntrospectionQuery,
filtered_document: Valid<ExecutableDocument>,
},
}
#[derive(Debug)]
pub enum SchemaIntrospectionError {
SuspectedValidationBug(SuspectedValidationBug),
DeeplyNestedIntrospectionList(DeeplyNestedIntrospectionListError),
Unsupported {
message: String,
location: Option<SourceSpan>,
},
}
impl SchemaIntrospectionSplit {
pub fn split(
schema: &Valid<Schema>,
document: &Valid<ExecutableDocument>,
operation: &Node<Operation>,
) -> Result<Self, SchemaIntrospectionError> {
if operation.operation_type != OperationType::Query {
check_non_query(document, operation)?;
return Ok(Self::None);
}
let mut fragments_info = IndexMap::with_hasher(Default::default());
let operation_field_kinds =
collect_field_kinds(document, &mut fragments_info, &operation.selection_set)?;
if operation_field_kinds.schema_introspection.is_none() {
Ok(Self::None)
} else if !operation_field_kinds.has_other_fields {
let new_operation = operation.clone();
let fragments = fragments_info
.keys()
.map(|&key| (key.clone(), document.fragments[key].clone()))
.collect();
let introspection_document =
make_single_operation_document(schema, document, new_operation, fragments);
Ok(Self::Only(
SchemaIntrospectionQuery::assume_only_intropsection_fields(introspection_document)?,
))
} else {
let mut fragments_done = HashSet::with_hasher(Default::default());
let mut new_documents = Split {
introspection: DocumentBuilder::new(document, operation),
other: DocumentBuilder::new(document, operation),
};
let operation_selection_set = split_selection_set(
&mut fragments_done,
&mut new_documents,
&operation.selection_set,
);
Ok(Self::Both {
introspection_query: SchemaIntrospectionQuery::assume_only_intropsection_fields(
new_documents.introspection.build(
schema,
document,
operation_selection_set.introspection,
),
)?,
filtered_document: new_documents.other.build(
schema,
document,
operation_selection_set.other,
),
})
}
}
}
fn field_is_schema_introspection(field: &Field) -> bool {
field.name == "__schema" || field.name == "__type"
}
fn check_non_query<'doc>(
document: &'doc Valid<ExecutableDocument>,
operation: &'doc Operation,
) -> Result<(), SchemaIntrospectionError> {
fn check_selection_set<'doc>(
fragments_visited: &mut HashSet<&'doc Name>,
fragments_to_visit: &mut HashSet<&'doc Name>,
selection_set: &'doc SelectionSet,
) -> Result<(), &'doc Node<Field>> {
for selection in &selection_set.selections {
match selection {
Selection::Field(field) => {
if field_is_schema_introspection(field) {
return Err(field);
}
check_selection_set(
fragments_visited,
fragments_to_visit,
&field.selection_set,
)?
}
Selection::InlineFragment(inline_fragment) => check_selection_set(
fragments_visited,
fragments_to_visit,
&inline_fragment.selection_set,
)?,
Selection::FragmentSpread(fragment_spread) => {
let name = &fragment_spread.fragment_name;
if !fragments_visited.contains(name) {
fragments_to_visit.insert(name);
}
}
}
}
Ok(())
}
let unsupported = |field: &Node<Field>| SchemaIntrospectionError::Unsupported {
message: format!(
"Schema introspection field {} is not supported in a {} operation",
field.name, operation.operation_type,
),
location: field.location(),
};
let mut fragments_visited = HashSet::with_hasher(Default::default());
let mut fragments_to_visit = HashSet::with_hasher(Default::default());
check_selection_set(
&mut fragments_visited,
&mut fragments_to_visit,
&operation.selection_set,
)
.map_err(unsupported)?;
while let Some(name) = fragments_to_visit.iter().next().copied() {
let fragment_def = get_fragment(document, name)?;
check_selection_set(
&mut fragments_visited,
&mut fragments_to_visit,
&fragment_def.selection_set,
)
.map_err(unsupported)?;
fragments_to_visit.remove(name);
fragments_visited.insert(name);
}
Ok(())
}
type FragmentMap = IndexMap<Name, Node<Fragment>>;
fn make_single_operation_document(
schema: &Valid<Schema>,
document: &Valid<ExecutableDocument>,
new_operation: Node<Operation>,
fragments: FragmentMap,
) -> Valid<ExecutableDocument> {
let mut new_document = ExecutableDocument {
sources: document.sources.clone(),
operations: Default::default(),
fragments,
};
new_document.operations.insert(new_operation);
new_document
.validate(schema)
.expect("filtering a valid document should result in a valid document")
}
pub(crate) fn get_fragment<'doc>(
document: &'doc Valid<ExecutableDocument>,
name: &Name,
) -> Result<&'doc Node<Fragment>, SchemaIntrospectionError> {
document.fragments.get(name).ok_or_else(|| {
SuspectedValidationBug {
message: format!("undefined fragment {name}"),
location: name.location(),
}
.into()
})
}
#[derive(Clone, Copy, Default)]
struct TopLevelFieldKinds<'doc> {
schema_introspection: Option<&'doc Node<Field>>,
has_other_fields: bool,
}
impl std::ops::BitOrAssign for TopLevelFieldKinds<'_> {
fn bitor_assign(&mut self, rhs: Self) {
if self.schema_introspection.is_none() {
self.schema_introspection = rhs.schema_introspection
}
self.has_other_fields |= rhs.has_other_fields
}
}
enum Computation<T> {
Ongoing,
Done(T),
}
fn collect_field_kinds<'doc>(
document: &'doc Valid<ExecutableDocument>,
fragments: &mut IndexMap<&'doc Name, Computation<TopLevelFieldKinds<'doc>>>,
selection_set: &'doc SelectionSet,
) -> Result<TopLevelFieldKinds<'doc>, SchemaIntrospectionError> {
let mut top_level_field_kinds = TopLevelFieldKinds::default();
for selection in &selection_set.selections {
match selection {
Selection::Field(field) => {
let nested_field_kinds =
collect_field_kinds(document, fragments, &field.selection_set)?;
if field_is_schema_introspection(field) {
top_level_field_kinds
.schema_introspection
.get_or_insert(field);
} else {
if let Some(schema_introspection_field) =
nested_field_kinds.schema_introspection
{
return Err(SchemaIntrospectionError::Unsupported {
message: format!(
"Schema introspection field {} is not supported \
nested in other fields",
schema_introspection_field.name
),
location: schema_introspection_field.location(),
});
}
top_level_field_kinds.has_other_fields = true;
}
}
Selection::InlineFragment(inline_fragment) => {
top_level_field_kinds |=
collect_field_kinds(document, fragments, &inline_fragment.selection_set)?;
}
Selection::FragmentSpread(fragment_spread) => {
let fragment_def = get_fragment(document, &fragment_spread.fragment_name)?;
let name = &fragment_def.name; match fragments.entry(name) {
Entry::Occupied(entry) => match entry.get() {
Computation::Ongoing => {
return Err(SuspectedValidationBug {
message: "fragment cycle".to_owned(),
location: name.location(),
}
.into());
}
Computation::Done(fragment_field_kinds) => {
top_level_field_kinds |= *fragment_field_kinds
}
},
Entry::Vacant(entry) => {
entry.insert(Computation::Ongoing);
let fragment_field_kinds =
collect_field_kinds(document, fragments, &fragment_def.selection_set)?;
fragments.insert(name, Computation::Done(fragment_field_kinds));
top_level_field_kinds |= fragment_field_kinds
}
}
}
}
}
Ok(top_level_field_kinds)
}
#[derive(Default)]
struct Split<T> {
introspection: T,
other: T,
}
struct DocumentBuilder<'doc> {
original_document: &'doc Valid<ExecutableDocument>,
original_operation: &'doc Node<Operation>,
variables_used: HashSet<&'doc Name>,
new_fragments: FragmentMap,
}
fn split_selection_set<'doc>(
fragments_done: &mut HashSet<&'doc Name>,
new_documents: &mut Split<DocumentBuilder<'doc>>,
selection_set: &'doc SelectionSet,
) -> Split<SelectionSet> {
let mut new_selection_sets = Split {
introspection: SelectionSet::new(selection_set.ty.clone()),
other: SelectionSet::new(selection_set.ty.clone()),
};
for selection in &selection_set.selections {
match selection {
Selection::Field(field) => {
if field_is_schema_introspection(field) {
new_selection_sets.introspection.push(field.clone());
new_documents.introspection.visit_field(field);
} else {
new_selection_sets.other.push(field.clone());
new_documents.other.visit_field(field);
}
}
Selection::InlineFragment(inline_fragment) => {
let if_non_empty = |doc: &mut DocumentBuilder<'doc>,
parent: &mut SelectionSet,
nested: SelectionSet| {
if !nested.selections.is_empty() {
doc.visit_directives(&inline_fragment.directives);
parent.push(inline_fragment.same_location(InlineFragment {
type_condition: inline_fragment.type_condition.clone(),
directives: inline_fragment.directives.clone(),
selection_set: nested,
}))
}
};
let nested = split_selection_set(
fragments_done,
new_documents,
&inline_fragment.selection_set,
);
if_non_empty(
&mut new_documents.introspection,
&mut new_selection_sets.introspection,
nested.introspection,
);
if_non_empty(
&mut new_documents.other,
&mut new_selection_sets.other,
nested.other,
);
}
Selection::FragmentSpread(fragment_spread) => {
let name = &fragment_spread.fragment_name;
let new = fragments_done.insert(name);
if new {
let document = &new_documents.introspection.original_document;
let fragment_def = &document.fragments[name];
let if_non_empty = |doc: &mut DocumentBuilder<'doc>, nested: SelectionSet| {
if !nested.selections.is_empty() {
doc.visit_directives(&fragment_def.directives);
doc.new_fragments.insert(
fragment_def.name.clone(),
fragment_def.same_location(Fragment {
name: fragment_def.name.clone(),
directives: fragment_def.directives.clone(),
selection_set: nested,
}),
);
}
};
let nested = split_selection_set(
fragments_done,
new_documents,
&fragment_def.selection_set,
);
if_non_empty(&mut new_documents.introspection, nested.introspection);
if_non_empty(&mut new_documents.other, nested.other);
}
let if_defined = |doc: &mut DocumentBuilder<'doc>, parent: &mut SelectionSet| {
if doc.new_fragments.contains_key(name) {
doc.visit_directives(&fragment_spread.directives);
parent.push(fragment_spread.same_location(FragmentSpread {
fragment_name: fragment_spread.fragment_name.clone(),
directives: fragment_spread.directives.clone(),
}))
}
};
if_defined(
&mut new_documents.introspection,
&mut new_selection_sets.introspection,
);
if_defined(&mut new_documents.other, &mut new_selection_sets.other);
}
}
}
new_selection_sets
}
impl<'doc> DocumentBuilder<'doc> {
fn new(
original_document: &'doc Valid<ExecutableDocument>,
original_operation: &'doc Node<Operation>,
) -> Self {
Self {
original_document,
original_operation,
variables_used: Default::default(),
new_fragments: Default::default(),
}
}
fn visit_selection_set(&mut self, selection_set: &'doc SelectionSet) {
for selection in &selection_set.selections {
match selection {
Selection::Field(field) => self.visit_field(field),
Selection::InlineFragment(inline_fragment) => {
self.visit_directives(&inline_fragment.directives);
self.visit_selection_set(&inline_fragment.selection_set);
}
Selection::FragmentSpread(fragment_spread) => {
self.visit_directives(&fragment_spread.directives);
let name = &fragment_spread.fragment_name;
if let Entry::Vacant(entry) = self.new_fragments.entry(name.clone()) {
let fragment_def = &self.original_document.fragments[name];
entry.insert(fragment_def.clone());
self.visit_directives(&fragment_def.directives);
self.visit_selection_set(&fragment_def.selection_set);
};
}
}
}
}
fn visit_field(&mut self, field: &'doc Field) {
for arg in &field.arguments {
self.visit_value(&arg.value)
}
self.visit_directives(&field.directives);
self.visit_selection_set(&field.selection_set);
}
fn visit_directives(&mut self, directives: &'doc ast::DirectiveList) {
for directive in directives {
for arg in &directive.arguments {
self.visit_value(&arg.value)
}
}
}
fn visit_value(&mut self, value: &'doc ast::Value) {
match value {
schema::Value::Variable(name) => {
self.variables_used.insert(name);
}
schema::Value::List(list) => {
for value in list {
self.visit_value(value)
}
}
schema::Value::Object(object) => {
for (_name, value) in object {
self.visit_value(value)
}
}
schema::Value::Null
| schema::Value::Enum(_)
| schema::Value::String(_)
| schema::Value::Float(_)
| schema::Value::Int(_)
| schema::Value::Boolean(_) => {}
}
}
fn build(
mut self,
schema: &Valid<Schema>,
document: &Valid<ExecutableDocument>,
operation_selection_set: SelectionSet,
) -> Valid<ExecutableDocument> {
for directive in &self.original_operation.directives {
for arg in &directive.arguments {
self.visit_value(&arg.value)
}
}
let new_operation = self.original_operation.same_location(Operation {
operation_type: self.original_operation.operation_type,
name: self.original_operation.name.clone(),
variables: self
.original_operation
.variables
.iter()
.filter(|var| self.variables_used.contains(&var.name))
.cloned()
.collect(),
directives: self.original_operation.directives.clone(),
selection_set: operation_selection_set,
});
make_single_operation_document(schema, document, new_operation, self.new_fragments)
}
}
impl From<SuspectedValidationBug> for SchemaIntrospectionError {
fn from(value: SuspectedValidationBug) -> Self {
Self::SuspectedValidationBug(value)
}
}
impl SchemaIntrospectionError {
pub fn into_graphql_error(self, sources: &SourceMap) -> GraphQLError {
match self {
Self::SuspectedValidationBug(s) => s.into_graphql_error(sources),
Self::DeeplyNestedIntrospectionList(e) => {
GraphQLError::new("Maximum introspection depth exceeded", e.location, sources)
}
Self::Unsupported { message, location } => {
GraphQLError::new(message, location, sources)
}
}
}
pub fn into_response(self, sources: &SourceMap) -> Response {
Response::from_request_error(self.into_graphql_error(sources))
}
}