From 52cde139b110f4e96d752d7eab9ebc7f06ca139f Mon Sep 17 00:00:00 2001 From: Simon Sapin Date: Thu, 23 Nov 2023 09:16:52 +0100 Subject: [PATCH] Add schema introspection --- crates/apollo-compiler/CHANGELOG.md | 20 +- .../src/executable/filtering.rs | 253 ++++ crates/apollo-compiler/src/executable/mod.rs | 15 + .../apollo-compiler/src/execution/engine.rs | 378 ++++++ .../src/execution/input_coercion.rs | 258 +++- .../src/execution/introspection.rs | 589 ++++++++ crates/apollo-compiler/src/execution/mod.rs | 7 + .../apollo-compiler/src/execution/resolver.rs | 215 +++ .../apollo-compiler/src/execution/response.rs | 27 + .../src/execution/result_coercion.rs | 210 +++ .../src/introspection/resolver.rs | 454 +++++++ .../introspect_full_schema.graphql | 98 ++ .../introspection/response_full.json | 1179 +++++++++++++++++ crates/apollo-compiler/tests/introspection.rs | 138 ++ crates/apollo-compiler/tests/main.rs | 1 + 15 files changed, 3836 insertions(+), 6 deletions(-) create mode 100644 crates/apollo-compiler/src/executable/filtering.rs create mode 100644 crates/apollo-compiler/src/execution/engine.rs create mode 100644 crates/apollo-compiler/src/execution/introspection.rs create mode 100644 crates/apollo-compiler/src/execution/resolver.rs create mode 100644 crates/apollo-compiler/src/execution/result_coercion.rs create mode 100644 crates/apollo-compiler/src/introspection/resolver.rs create mode 100644 crates/apollo-compiler/test_data/introspection/introspect_full_schema.graphql create mode 100644 crates/apollo-compiler/test_data/introspection/response_full.json create mode 100644 crates/apollo-compiler/tests/introspection.rs diff --git a/crates/apollo-compiler/CHANGELOG.md b/crates/apollo-compiler/CHANGELOG.md index 06e98a0d4..dc2bffb28 100644 --- a/crates/apollo-compiler/CHANGELOG.md +++ b/crates/apollo-compiler/CHANGELOG.md @@ -104,22 +104,32 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## Features -- **Add `parse_and_validate` constructors for `Schema` and `ExecutableDocument` - [SimonSapin], - [pull/752]:** - when mutating isn’t needed after parsing, - this returns an immutable `Valid<_>` value in one step. -- **Add execution-related and introspection functionality - [SimonSapin], +- **Add [schema introspection] support and execution-related APIs - [SimonSapin], [pull/FIXME]:** + - Add `apollo_compiler::execution::SchemaIntrospection` + providing full execution for the [schema introspection] parts of an operation + and separating the rest to be executed separately. + In order to support all kinds of introspection queries this actually includes + a full execution engine where users provide objects with resolvable fields. + At this time this engine is not exposed in the public API. + If you’re interested in it [let us know] about your use case! - Add data structure in `apollo_compiler::execution` for a GraphQL response, its data, and errors. All (de)serializable with `serde`. - Add [`coerce_variable_values()`] in that same module. - Make `Name` and `NodeStr` (de)serializable with `serde`. + - Add `ExecutableDocument::insert_operation` convenience method. +- **Add `parse_and_validate` constructors for `Schema` and `ExecutableDocument` - [SimonSapin], + [pull/752]:** + when mutating isn’t needed after parsing, + this returns an immutable `Valid<_>` value in one step. [SimonSapin]: https://github.com/SimonSapin [issue/709]: https://github.com/apollographql/apollo-rs/issues/709 [issue/751]: https://github.com/apollographql/apollo-rs/issues/751 [pull/752]: https://github.com/apollographql/apollo-rs/pull/752 [`coerce_variable_values()`]: https://spec.graphql.org/October2021/#sec-Coercing-Variable-Values +[schema introspection]: https://spec.graphql.org/October2021/#sec-Schema-Introspection +[let us know]: https://github.com/apollographql/apollo-rs/issues/new?assignees=&labels=triage&projects=&template=feature_request.md # [1.0.0-beta.7](https://crates.io/crates/apollo-compiler/1.0.0-beta.7) - 2023-11-17 diff --git a/crates/apollo-compiler/src/executable/filtering.rs b/crates/apollo-compiler/src/executable/filtering.rs new file mode 100644 index 000000000..16b83a274 --- /dev/null +++ b/crates/apollo-compiler/src/executable/filtering.rs @@ -0,0 +1,253 @@ +use crate::ast; +use crate::executable::Field; +use crate::executable::Fragment; +use crate::executable::FragmentSpread; +use crate::executable::InlineFragment; +use crate::executable::Operation; +use crate::executable::Selection; +use crate::executable::SelectionSet; +use crate::execution::RequestError; +use crate::schema; +use crate::schema::Name; +use crate::validation::Valid; +use crate::ExecutableDocument; +use crate::Node; +use crate::Schema; +use indexmap::IndexMap; +use std::collections::HashSet; + +type FragmentMap = IndexMap>; + +pub(crate) struct FilteredDocumentBuilder<'doc, Predicate> +where + Predicate: FnMut(&Selection) -> bool, +{ + document: &'doc Valid, + remove_selection: Predicate, + new_fragments: FragmentMap, + + /// The contents of these fragments was filtered to nothing. + /// Corresonding fragment spreads should be removed. + emptied_fragments: HashSet<&'doc Name>, + + /// Avoid infinite recursion + fragments_being_processed: HashSet<&'doc Name>, + + /// Remove unused variables to satisfy the _All Variables Used_ validation rule. + /// This feels like busy work. How important is it to produce a fully valid document? + /// + variables_used: HashSet<&'doc Name>, +} + +impl<'doc, Predicate> FilteredDocumentBuilder<'doc, Predicate> +where + Predicate: FnMut(&Selection) -> bool, +{ + /// Return a document with exactly one operation, + /// which is `operation` filtered according to `remove_selection`. + /// + /// If a non-empty selection set becomes empty, its parent is removed. + /// Returns `None` if there is nothing left. + /// + /// The returned document also contains fragments needed by the remaining selections. + /// Fragment definitions are filtered too. + pub(crate) fn single_operation( + schema: &Valid, + document: &'doc Valid, + operation: &'doc Operation, + remove_selection: Predicate, + ) -> Result>, RequestError> { + let mut builder = Self { + document, + remove_selection, + new_fragments: FragmentMap::new(), + emptied_fragments: HashSet::new(), + fragments_being_processed: HashSet::new(), + variables_used: HashSet::new(), + }; + let Some(new_operation) = builder.filter_operation(operation)? else { + return Ok(None); + }; + let mut new_document = ExecutableDocument { + sources: document.sources.clone(), + anonymous_operation: None, + named_operations: IndexMap::new(), + fragments: builder.new_fragments, + }; + new_document.insert_operation(new_operation); + let valid = if cfg!(debug_assertions) { + new_document + .validate(schema) + .expect("filtering a valid document should result in a valid document") + } else { + Valid::assume_valid(new_document) + }; + Ok(Some(valid)) + } + + fn filter_operation( + &mut self, + operation: &'doc Operation, + ) -> Result, RequestError> { + self.variables_used.clear(); + for var in &operation.variables { + if let Some(default) = &var.default_value { + self.variables_in_value(default) + } + } + for directive in &operation.directives { + for arg in &directive.arguments { + self.variables_in_value(&arg.value) + } + } + let Some(selection_set) = self.filter_selection_set(&operation.selection_set)? else { + return Ok(None); + }; + Ok(Some(Operation { + operation_type: operation.operation_type, + name: operation.name.clone(), + variables: operation + .variables + .iter() + .filter(|var| self.variables_used.contains(&var.name)) + .cloned() + .collect(), + directives: operation.directives.clone(), + selection_set, + })) + } + + fn filter_selection_set( + &mut self, + selection_set: &'doc SelectionSet, + ) -> Result, RequestError> { + let selections = selection_set + .selections + .iter() + .filter_map(|selection| self.filter_selection(selection).transpose()) + .collect::, _>>()?; + if !selections.is_empty() { + Ok(Some(SelectionSet { + ty: selection_set.ty.clone(), + selections, + })) + } else { + Ok(None) + } + } + + fn filter_selection( + &mut self, + selection: &'doc Selection, + ) -> Result, RequestError> { + if (self.remove_selection)(selection) { + return Ok(None); + } + let new_selection = match selection { + Selection::Field(field) => { + let selection_set = if field.selection_set.selections.is_empty() { + // Keep a leaf field as-is + field.selection_set.clone() + } else { + // `?` removes a non-leaf field if its sub-selections becomes empty + let Some(set) = self.filter_selection_set(&field.selection_set)? else { + return Ok(None); + }; + set + }; + for arg in &field.arguments { + self.variables_in_value(&arg.value) + } + Selection::Field(field.same_location(Field { + definition: field.definition.clone(), + alias: field.alias.clone(), + name: field.name.clone(), + arguments: field.arguments.clone(), + directives: field.directives.clone(), + selection_set, + })) + } + Selection::InlineFragment(inline_fragment) => { + let Some(selection_set) = + self.filter_selection_set(&inline_fragment.selection_set)? + else { + return Ok(None); + }; + Selection::InlineFragment(inline_fragment.same_location(InlineFragment { + type_condition: inline_fragment.type_condition.clone(), + directives: inline_fragment.directives.clone(), + selection_set, + })) + } + Selection::FragmentSpread(fragment_spread) => { + let name = &fragment_spread.fragment_name; + if self.emptied_fragments.contains(name) { + return Ok(None); + } + if self.fragments_being_processed.contains(name) { + return Err(RequestError::new("fragment spread cycle").validation_bug()); + } + if !self.new_fragments.contains_key(name) { + let fragment_def = + self.document.fragments.get(name).ok_or_else(|| { + RequestError::new("undefined fragment").validation_bug() + })?; + + let Some(selection_set) = + self.filter_selection_set(&fragment_def.selection_set)? + else { + self.emptied_fragments.insert(name); + return Ok(None); + }; + for directive in &fragment_def.directives { + for arg in &directive.arguments { + self.variables_in_value(&arg.value) + } + } + self.new_fragments.insert( + fragment_def.name.clone(), + fragment_def.same_location(Fragment { + name: fragment_def.name.clone(), + directives: fragment_def.directives.clone(), + selection_set, + }), + ); + } + Selection::FragmentSpread(fragment_spread.same_location(FragmentSpread { + fragment_name: name.clone(), + directives: fragment_spread.directives.clone(), + })) + } + }; + for directive in selection.directives() { + for arg in &directive.arguments { + self.variables_in_value(&arg.value) + } + } + Ok(Some(new_selection)) + } + + fn variables_in_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.variables_in_value(value) + } + } + schema::Value::Object(object) => { + for (_name, value) in object { + self.variables_in_value(value) + } + } + schema::Value::Null + | schema::Value::Enum(_) + | schema::Value::String(_) + | schema::Value::Float(_) + | schema::Value::Int(_) + | schema::Value::Boolean(_) => {} + } + } +} diff --git a/crates/apollo-compiler/src/executable/mod.rs b/crates/apollo-compiler/src/executable/mod.rs index 6ef358dee..9a361b37f 100644 --- a/crates/apollo-compiler/src/executable/mod.rs +++ b/crates/apollo-compiler/src/executable/mod.rs @@ -11,6 +11,7 @@ use indexmap::IndexMap; use std::collections::HashSet; use std::path::Path; +pub(crate) mod filtering; pub(crate) mod from_ast; mod serialize; pub(crate) mod validation; @@ -288,6 +289,20 @@ impl ExecutableDocument { .ok_or_else(|| RequestError::new("multiple operations but no `operationName`")) } + /// Insert the given operation in either `named_operations` or `anonymous_operation` + /// as appropriate, and return the old operation (if any) with that name (or lack thereof). + pub fn insert_operation( + &mut self, + operation: impl Into>, + ) -> Option> { + let operation = operation.into(); + if let Some(name) = &operation.name { + self.named_operations.insert(name.clone(), operation) + } else { + self.anonymous_operation.replace(operation) + } + } + serialize_method!(); } diff --git a/crates/apollo-compiler/src/execution/engine.rs b/crates/apollo-compiler/src/execution/engine.rs new file mode 100644 index 000000000..c1e755501 --- /dev/null +++ b/crates/apollo-compiler/src/execution/engine.rs @@ -0,0 +1,378 @@ +use crate::ast::Name; +use crate::ast::Value; +use crate::executable::Field; +use crate::executable::Operation; +use crate::executable::OperationType; +use crate::executable::Selection; +use crate::execution::input_coercion::coerce_argument_values; +use crate::execution::resolver::ObjectValue; +use crate::execution::resolver::ResolverError; +use crate::execution::result_coercion::complete_value; +use crate::execution::GraphQLError; +use crate::execution::GraphQLLocation; +use crate::execution::JsonMap; +use crate::execution::JsonValue; +use crate::execution::PathElement; +use crate::execution::RequestError; +use crate::execution::Response; +use crate::node::NodeLocation; +use crate::schema::ExtendedType; +use crate::schema::FieldDefinition; +use crate::schema::ObjectType; +use crate::schema::Type; +use crate::validation::Valid; +use crate::ExecutableDocument; +use crate::Schema; +use crate::SourceMap; +use indexmap::IndexMap; +use std::collections::HashSet; + +/// +#[derive(Debug, Copy, Clone)] +pub(crate) enum ExecutionMode { + /// Allowed to resolve fields in any order, including in parellel + Normal, + Sequential, +} + +/// Return in `Err` when a field error occurred at some non-nullable place +/// +/// +pub(crate) struct PropagateNull; + +/// Linked-list version of `Vec`, taking advantage of the call stack +pub(crate) type LinkedPath<'a> = Option<&'a LinkedPathElement<'a>>; + +pub(crate) struct LinkedPathElement<'a> { + pub(crate) element: PathElement, + pub(crate) next: LinkedPath<'a>, +} + +/// * +/// * +pub(crate) fn execute_query_or_mutation( + schema: &Valid, + document: &Valid, + variable_values: &Valid, + initial_value: &ObjectValue<'_>, + operation: &Operation, +) -> Result { + let object_type_name = operation.object_type(); + let object_type_def = schema.get_object(object_type_name).ok_or_else(|| { + RequestError::new(format!( + "Root operation type {object_type_name} is undefined or not an object type." + )) + .validation_bug() + })?; + let mut errors = Vec::new(); + let path = None; // root: empty path + let mode = if operation.operation_type == OperationType::Mutation { + ExecutionMode::Sequential + } else { + ExecutionMode::Normal + }; + let data = execute_selection_set( + schema, + document, + variable_values, + &mut errors, + path, + mode, + object_type_name, + object_type_def, + initial_value, + &operation.selection_set.selections, + ) + .ok(); + Ok(Response { + data: data.into(), + errors, + extensions: Default::default(), + }) +} + +/// +#[allow(clippy::too_many_arguments)] // yes it’s not a nice API but it’s internal +pub(crate) fn execute_selection_set<'a>( + schema: &Valid, + document: &'a Valid, + variable_values: &Valid, + errors: &mut Vec, + path: LinkedPath<'_>, + mode: ExecutionMode, + object_type_name: &str, + object_type: &ObjectType, + object_value: &ObjectValue<'_>, + selections: impl IntoIterator, +) -> Result { + let mut grouped_field_set = IndexMap::new(); + collect_fields( + schema, + document, + variable_values, + object_type_name, + object_type, + selections, + &mut HashSet::new(), + &mut grouped_field_set, + ); + + match mode { + ExecutionMode::Normal => {} + ExecutionMode::Sequential => { + // If we want parallelism, use `futures::future::join_all` (async) + // or Rayon’s `par_iter` (sync) here. + } + } + + let mut response_map = JsonMap::with_capacity(grouped_field_set.len()); + for (&response_key, fields) in &grouped_field_set { + // Indexing should not panic: `collect_fields` only creates a `Vec` to push to it + let field_name = &fields[0].name; + let Ok(field_def) = schema.type_field(object_type_name, field_name) else { + // TODO: Return a `validation_bug`` field error here? + // The spec specifically has a “If fieldType is defined” condition, + // but it being undefined would make the request invalid, right? + continue; + }; + let value = if field_name == "__typename" { + JsonValue::from(object_type_name) + } else { + let field_path = LinkedPathElement { + element: PathElement::Field(response_key.clone()), + next: path, + }; + execute_field( + schema, + document, + variable_values, + errors, + Some(&field_path), + mode, + object_value, + field_def, + fields, + )? + }; + response_map.insert(response_key.as_str(), value); + } + Ok(response_map) +} + +/// +#[allow(clippy::too_many_arguments)] // yes it’s not a nice API but it’s internal +fn collect_fields<'a>( + schema: &Schema, + document: &'a ExecutableDocument, + variable_values: &Valid, + object_type_name: &str, + object_type: &ObjectType, + selections: impl IntoIterator, + visited_fragments: &mut HashSet<&'a Name>, + grouped_fields: &mut IndexMap<&'a Name, Vec<&'a Field>>, +) { + for selection in selections { + if eval_if_arg(selection, "skip", variable_values).unwrap_or(false) + || !eval_if_arg(selection, "include", variable_values).unwrap_or(true) + { + continue; + } + match selection { + Selection::Field(field) => grouped_fields + .entry(field.response_key()) + .or_default() + .push(field.as_ref()), + Selection::FragmentSpread(spread) => { + let new = visited_fragments.insert(&spread.fragment_name); + if !new { + continue; + } + let Some(fragment) = document.fragments.get(&spread.fragment_name) else { + continue; + }; + if !does_fragment_type_apply( + schema, + object_type_name, + object_type, + fragment.type_condition(), + ) { + continue; + } + collect_fields( + schema, + document, + variable_values, + object_type_name, + object_type, + &fragment.selection_set.selections, + visited_fragments, + grouped_fields, + ) + } + Selection::InlineFragment(inline) => { + if let Some(condition) = &inline.type_condition { + if !does_fragment_type_apply(schema, object_type_name, object_type, condition) { + continue; + } + } + collect_fields( + schema, + document, + variable_values, + object_type_name, + object_type, + &inline.selection_set.selections, + visited_fragments, + grouped_fields, + ) + } + } + } +} + +/// +fn does_fragment_type_apply( + schema: &Schema, + object_type_name: &str, + object_type: &ObjectType, + fragment_type: &Name, +) -> bool { + match schema.types.get(fragment_type) { + Some(ExtendedType::Object(_)) => fragment_type == object_type_name, + Some(ExtendedType::Interface(_)) => { + object_type.implements_interfaces.contains(fragment_type) + } + Some(ExtendedType::Union(def)) => def.members.contains(object_type_name), + // Undefined or not an output type: validation should have caught this + _ => false, + } +} + +fn eval_if_arg( + selection: &Selection, + directive_name: &str, + variable_values: &Valid, +) -> Option { + match selection + .directives() + .get(directive_name)? + .argument_by_name("if")? + .as_ref() + { + Value::Boolean(value) => Some(*value), + Value::Variable(var) => variable_values.get(var.as_str())?.as_bool(), + _ => None, + } +} + +/// +#[allow(clippy::too_many_arguments)] // yes it’s not a nice API but it’s internal +fn execute_field( + schema: &Valid, + document: &Valid, + variable_values: &Valid, + errors: &mut Vec, + path: LinkedPath<'_>, + mode: ExecutionMode, + object_value: &ObjectValue<'_>, + field_def: &FieldDefinition, + fields: &[&Field], +) -> Result { + let field = fields[0]; + let argument_values = match coerce_argument_values( + schema, + document, + variable_values, + errors, + path, + field_def, + field, + ) { + Ok(argument_values) => argument_values, + Err(PropagateNull) => return try_nullify(&field_def.ty, Err(PropagateNull)), + }; + let resolved_result = object_value.resolve_field(&field.name, &argument_values); + let completed_result = match resolved_result { + Ok(resolved) => complete_value( + schema, + document, + variable_values, + errors, + path, + mode, + field.ty(), + resolved, + fields, + ), + Err(ResolverError { message }) => { + errors.push(field_error( + format!("resolver error: {message}"), + path, + field.name.location(), + &document.sources, + )); + Err(PropagateNull) + } + }; + try_nullify(&field_def.ty, completed_result) +} + +/// Try to insert a propagated null if possible, or keep propagating it. +/// +/// +pub(crate) fn try_nullify( + ty: &Type, + result: Result, +) -> Result { + match result { + Ok(json) => Ok(json), + Err(PropagateNull) => { + if ty.is_non_null() { + Err(PropagateNull) + } else { + Ok(JsonValue::Null) + } + } + } +} + +pub(crate) fn path_to_vec(mut link: LinkedPath<'_>) -> Vec { + let mut path = Vec::new(); + while let Some(node) = link { + path.push(node.element.clone()); + link = node.next; + } + path.reverse(); + path +} + +pub(crate) fn field_error( + message: impl ToString, + path: LinkedPath<'_>, + location: Option, + sources: &SourceMap, +) -> GraphQLError { + GraphQLError { + message: message.to_string(), + path: path_to_vec(path), + locations: GraphQLLocation::from_node(sources, location) + .into_iter() + .collect(), + extensions: Default::default(), + } +} + +impl RequestError { + pub(crate) fn into_field_error( + self, + path: LinkedPath<'_>, + location: Option, + sources: &SourceMap, + ) -> GraphQLError { + let Self(mut err) = self; + err.path = path_to_vec(path); + err.locations + .extend(GraphQLLocation::from_node(sources, location)); + err + } +} diff --git a/crates/apollo-compiler/src/execution/input_coercion.rs b/crates/apollo-compiler/src/execution/input_coercion.rs index a07a25d2e..5a09a3399 100644 --- a/crates/apollo-compiler/src/execution/input_coercion.rs +++ b/crates/apollo-compiler/src/execution/input_coercion.rs @@ -1,13 +1,20 @@ use crate::ast::Type; use crate::ast::Value; -use crate::executable::Operation; +use crate::executable::Field; +use crate::execution::engine::field_error; +use crate::execution::engine::LinkedPath; +use crate::execution::engine::PropagateNull; +use crate::execution::GraphQLError; use crate::execution::JsonMap; use crate::execution::JsonValue; use crate::execution::RequestError; use crate::schema::ExtendedType; +use crate::schema::FieldDefinition; use crate::validation::Valid; use crate::ExecutableDocument; +use crate::Node; use crate::Schema; +use std::collections::HashMap; macro_rules! request_error { ($($arg: tt)+) => { @@ -248,3 +255,252 @@ fn graphql_value_to_json( .collect(), } } + +/// +pub(crate) fn coerce_argument_values( + schema: &Schema, + document: &Valid, + variable_values: &Valid, + errors: &mut Vec, + path: LinkedPath<'_>, + field_def: &FieldDefinition, + field: &Field, +) -> Result { + let mut coerced_values = JsonMap::new(); + for arg_def in &field_def.arguments { + let arg_name = &arg_def.name; + if let Some(arg) = field.arguments.iter().find(|arg| arg.name == *arg_name) { + if let Value::Variable(var_name) = arg.value.as_ref() { + if let Some(var_value) = variable_values.get(var_name.as_str()) { + if var_value.is_null() && arg_def.ty.is_non_null() { + errors.push(field_error( + format!("null value for non-nullable argument {arg_name}"), + path, + arg_def.location(), + &document.sources, + )); + return Err(PropagateNull); + } else { + coerced_values.insert(arg_name.as_str(), var_value.clone()); + continue; + } + } + } else if arg.value.is_null() && arg_def.ty.is_non_null() { + errors.push(field_error( + format!("null value for non-nullable argument {arg_name}"), + path, + arg_def.location(), + &document.sources, + )); + return Err(PropagateNull); + } else { + let coerced_value = coerce_argument_value( + schema, + document, + variable_values, + errors, + path, + "argument", + "", + "", + arg_name, + &arg_def.ty, + &arg.value, + )?; + coerced_values.insert(arg_name.as_str(), coerced_value); + continue; + } + } + if let Some(default) = &arg_def.default_value { + let value = + graphql_value_to_json("argument", "", "", arg_name, default).map_err(|err| { + errors.push(err.into_field_error(path, arg_def.location(), &document.sources)); + PropagateNull + })?; + coerced_values.insert(arg_def.name.as_str(), value); + continue; + } + if arg_def.ty.is_non_null() { + errors.push(field_error( + format!("missing value for required argument {arg_name}"), + path, + arg_def.location(), + &document.sources, + )); + return Err(PropagateNull); + } + } + Ok(coerced_values) +} + +#[allow(clippy::too_many_arguments)] // yes it’s not a nice API but it’s internal +fn coerce_argument_value( + schema: &Schema, + document: &Valid, + variable_values: &Valid, + errors: &mut Vec, + path: LinkedPath<'_>, + kind: &str, + parent: &str, + sep: &str, + name: &str, + ty: &Type, + value: &Node, +) -> Result { + if value.is_null() { + if ty.is_non_null() { + errors.push(field_error( + format!("null value for non-null {kind} {parent}{sep}{name}"), + path, + value.location(), + &document.sources, + )); + return Err(PropagateNull); + } else { + return Ok(JsonValue::Null); + } + } + if let Some(var_name) = value.as_variable() { + if let Some(var_value) = variable_values.get(var_name.as_str()) { + if var_value.is_null() && ty.is_non_null() { + errors.push(field_error( + format!("null variable value for non-null {kind} {parent}{sep}{name}"), + path, + value.location(), + &document.sources, + )); + return Err(PropagateNull); + } else { + return Ok(var_value.clone()); + } + } else if ty.is_non_null() { + errors.push(field_error( + format!("missing variable for non-null {kind} {parent}{sep}{name}"), + path, + value.location(), + &document.sources, + )); + return Err(PropagateNull); + } else { + return Ok(JsonValue::Null); + } + } + let ty_name = match ty { + Type::List(inner_ty) | Type::NonNullList(inner_ty) => { + // https://spec.graphql.org/October2021/#sec-List.Input-Coercion + return value + .as_list() + // If not an array, treat the value as an array of size one: + .unwrap_or(std::slice::from_ref(value)) + .iter() + .map(|item| { + coerce_argument_value( + schema, + document, + variable_values, + errors, + path, + kind, + parent, + sep, + name, + inner_ty, + item, + ) + }) + .collect(); + } + Type::Named(ty_name) | Type::NonNullNamed(ty_name) => ty_name, + }; + let Some(ty_def) = schema.types.get(ty_name) else { + errors.push( + field_error( + format!("Undefined type {ty_name} for {kind} {parent}{sep}{name}"), + path, + value.location(), + &document.sources, + ) + .validation_bug(), + ); + return Err(PropagateNull); + }; + match ty_def { + ExtendedType::InputObject(ty_def) => { + // https://spec.graphql.org/October2021/#sec-Input-Objects.Input-Coercion + if let Some(object) = value.as_object() { + if let Some((key, _value)) = object + .iter() + .find(|(key, _value)| !ty_def.fields.contains_key(key)) + { + errors.push(field_error( + format!("Input object has key {key} not in type {ty_name}",), + path, + value.location(), + &document.sources, + )); + return Err(PropagateNull); + } + // `map` converts `&(k, v)` to `(&k, &v)` + let object: HashMap<_, _> = object.iter().map(|(k, v)| (k, v)).collect(); + let mut coerced_object = JsonMap::new(); + for (field_name, field_def) in &ty_def.fields { + if let Some(field_value) = object.get(field_name) { + let coerced_value = coerce_argument_value( + schema, + document, + variable_values, + errors, + path, + "input field", + ty_name, + ".", + field_name, + &field_def.ty, + field_value, + )?; + coerced_object.insert(field_name.as_str(), coerced_value); + } else if let Some(default) = &field_def.default_value { + let default = + graphql_value_to_json("input field", ty_name, ".", field_name, default) + .map_err(|err| { + errors.push(err.into_field_error( + path, + value.location(), + &document.sources, + )); + PropagateNull + })?; + coerced_object.insert(field_name.as_str(), default); + } else if field_def.ty.is_non_null() { + errors.push(field_error( + format!( + "Missing value for non-null input object field {ty_name}.{field_name}" + ), + path, + value.location(), + &document.sources, + )); + return Err(PropagateNull); + } else { + // Field not required + } + } + return Ok(coerced_object.into()); + } + } + _ => { + // For scalar and enums, rely and validation and just convert between Rust types + return graphql_value_to_json(kind, parent, sep, name, value).map_err(|err| { + errors.push(err.into_field_error(path, value.location(), &document.sources)); + PropagateNull + }); + } + } + errors.push(field_error( + format!("Could not coerce {kind} {parent}{sep}{name}: {value} to type {ty_name}"), + path, + value.location(), + &document.sources, + )); + Err(PropagateNull) +} diff --git a/crates/apollo-compiler/src/execution/introspection.rs b/crates/apollo-compiler/src/execution/introspection.rs new file mode 100644 index 000000000..a10c6d7f9 --- /dev/null +++ b/crates/apollo-compiler/src/execution/introspection.rs @@ -0,0 +1,589 @@ +use crate::executable::filtering::FilteredDocumentBuilder; +use crate::executable::Field; +use crate::executable::OperationType; +use crate::execution::engine::execute_query_or_mutation; +use crate::execution::resolver::ResolvedValue; +use crate::execution::JsonMap; +use crate::execution::RequestError; +use crate::execution::Response; +use crate::schema; +use crate::schema::Name; +use crate::validation::Valid; +use crate::ExecutableDocument; +use crate::Node; +use crate::Schema; +use std::borrow::Cow; +use std::collections::HashMap; +use std::collections::HashSet; +use std::sync::OnceLock; + +/// Execution for [schema introspection] +/// +/// [schema introspection]: https://spec.graphql.org/October2021/#sec-Schema-Introspection +#[derive(Debug)] +pub enum SchemaIntrospection { + /// The selected operation does *not* use [schema introspection] fields. + /// It should be executed unchanged. + /// + /// [schema introspection]: https://spec.graphql.org/October2021/#sec-Schema-Introspection + None, + + /// The selected operation *only* uses [schema introspection] fields. + /// This provides the full response, there is nothing else to execute. + /// + /// [schema introspection]: https://spec.graphql.org/October2021/#sec-Schema-Introspection + Only(Response), + + /// The selected operation uses *both* [schema introspection] fields and other fields. + /// + /// [schema introspection]: https://spec.graphql.org/October2021/#sec-Schema-Introspection + Both { + /// The response for schema introspection parts of the original operation. + introspection_response: Response, + + /// Contains exactly one operation with schema introspection fields removed, + /// and the fragment definitions it needs. + /// The operation definition name is preserved, + /// so either `None` or the original `Option<&str>` name request can be passed + /// to [`ExecutableDocument::get_operation`] to obtain the one operation. + /// + /// This operation should be executed separately, + /// and its response merged with `introspection_response` using [`Response::merge`]. + filtered_operation: Valid, + }, +} + +impl SchemaIntrospection { + /// Execute the [schema introspection] parts of an operation + /// and return the rest to be executed separately. + /// + /// [schema introspection]: https://spec.graphql.org/October2021/#sec-Schema-Introspection + pub fn execute( + schema: &Valid, + document: &Valid, + operation_name: Option<&str>, + variable_values: &Valid, + ) -> Result { + let operation = document.get_operation(operation_name)?; + if operation.operation_type != OperationType::Query { + return Ok(Self::None); + } + + fn is_schema_introspection_meta_field(field: &Node) -> bool { + field.name == "__schema" || field.name == "__type" + } + + let Some(introspection_document) = + FilteredDocumentBuilder::single_operation(schema, document, operation, |sel| { + // Remove fields… + sel.as_field().is_some_and(|field| { + // except __schema and __type meta-fields, + // and fields of the schema introspection schema + !is_schema_introspection_meta_field(field) && !field.definition.is_built_in() + }) + })? + else { + return Ok(Self::None); + }; + let non_introspection_document = + FilteredDocumentBuilder::single_operation(schema, document, operation, |sel| { + // Remove __schema and __type + sel.as_field() + .is_some_and(is_schema_introspection_meta_field) + })?; + let implementers_map = &OnceLock::new(); + let initial_value = &IntrospectionRootResolver(SchemaWithCache { + schema, + implementers_map, + }); + let introspection_response = execute_query_or_mutation( + schema, + &introspection_document, + variable_values, + initial_value, + operation, + )?; + if let Some(filtered_operation) = non_introspection_document { + Ok(Self::Both { + introspection_response, + filtered_operation, + }) + } else { + Ok(Self::Only(introspection_response)) + } + } + + /// Execute the [schema introspection] parts of an operation + /// and wrap a callback to execute the rest (if any). + /// + /// This calls [`execute`][Self::execute] + /// and handles each [`SchemaIntrospection`] enum variant as suggested by their documentation. + /// + /// [schema introspection]: https://spec.graphql.org/October2021/#sec-Schema-Introspection + pub fn execute_with( + schema: &Valid, + document: &Valid, + operation_name: Option<&str>, + variable_values: &Valid, + execute_non_introspection_parts: impl FnOnce( + &Valid, + ) -> Result, + ) -> Result { + match Self::execute(schema, document, operation_name, variable_values)? { + Self::Only(response) => Ok(response), + Self::None => execute_non_introspection_parts(document), + Self::Both { + filtered_operation, + introspection_response, + } => { + Ok(execute_non_introspection_parts(&filtered_operation)? + .merge(introspection_response)) + } + } + } +} + +#[derive(Clone, Copy)] +struct SchemaWithCache<'a> { + schema: &'a Schema, + implementers_map: &'a OnceLock>>, +} + +impl<'a> SchemaWithCache<'a> { + fn implementers_of(&self, interface_name: &str) -> impl Iterator { + self.implementers_map + .get_or_init(|| self.schema.implementers_map()) + .get(interface_name) + .into_iter() + .flatten() + } +} + +impl<'a> std::ops::Deref for SchemaWithCache<'a> { + type Target = &'a Schema; + + fn deref(&self) -> &Self::Target { + &self.schema + } +} + +struct IntrospectionRootResolver<'a>(SchemaWithCache<'a>); + +struct TypeDefResolver<'a> { + schema: SchemaWithCache<'a>, + name: &'a str, + def: &'a schema::ExtendedType, +} + +/// Only used for non-null and list types. `TypeDef` is used for everything else. +struct TypeResolver<'a> { + schema: SchemaWithCache<'a>, + ty: Cow<'a, schema::Type>, +} + +struct DirectiveResolver<'a> { + schema: SchemaWithCache<'a>, + def: &'a schema::DirectiveDefinition, +} + +struct FieldResolver<'a> { + schema: SchemaWithCache<'a>, + def: &'a schema::FieldDefinition, +} + +struct EnumValueResolver<'a> { + def: &'a schema::EnumValueDefinition, +} + +struct InputValueResolver<'a> { + schema: SchemaWithCache<'a>, + def: &'a schema::InputValueDefinition, +} + +fn type_def(schema: SchemaWithCache<'_>, name: impl AsRef) -> ResolvedValue<'_> { + ResolvedValue::opt_object( + schema + .types + .get_key_value(name.as_ref()) + .map(|(name, def)| TypeDefResolver { schema, name, def }), + ) +} + +fn type_def_opt<'a>( + schema: SchemaWithCache<'a>, + name: &Option>, +) -> ResolvedValue<'a> { + if let Some(name) = name.as_ref() { + type_def(schema, name) + } else { + ResolvedValue::null() + } +} + +fn ty<'a>(schema: SchemaWithCache<'a>, ty: &'a schema::Type) -> ResolvedValue<'a> { + if let schema::Type::Named(name) = ty { + type_def(schema, name) + } else { + ResolvedValue::object(TypeResolver { + schema, + ty: Cow::Borrowed(ty), + }) + } +} + +fn deprecation_reason(opt_directive: Option<&Node>) -> ResolvedValue<'_> { + ResolvedValue::leaf( + opt_directive + .and_then(|directive| directive.argument_by_name("reason")) + .and_then(|arg| arg.as_str()), + ) +} + +impl_resolver! { + for IntrospectionRootResolver<'_>: + + __typename = unreachable!(); + + fn __schema(&self_) { + Ok(ResolvedValue::object(self_.0)) + } + + fn __type(&self_, args) { + let name = args["name"].as_str().unwrap(); + Ok(type_def(self_.0, name)) + } +} + +impl_resolver! { + for SchemaWithCache<'_>: + + __typename = "__Schema"; + + fn description(&self_) { + Ok(ResolvedValue::leaf(self_.schema_definition.description.as_deref())) + } + + fn types(&self_) { + Ok(ResolvedValue::list(self_.types.iter().map(|(name, def)| { + ResolvedValue::object(TypeDefResolver { schema: *self_, name, def }) + }))) + } + + fn directives(&self_) { + Ok(ResolvedValue::list(self_.directive_definitions.values().map(|def| { + ResolvedValue::object(DirectiveResolver { schema: *self_, def }) + }))) + } + + fn queryType(&self_) { + Ok(type_def_opt(*self_, &self_.schema_definition.query)) + } + + fn mutationType(&self_) { + Ok(type_def_opt(*self_, &self_.schema_definition.mutation)) + } + + fn subscriptionType(&self_) { + Ok(type_def_opt(*self_, &self_.schema_definition.subscription)) + } +} + +impl_resolver! { + for TypeDefResolver<'_>: + + __typename = "__Type"; + + fn kind(&self_) { + Ok(ResolvedValue::leaf(match self_.def { + schema::ExtendedType::Scalar(_) => "SCALAR", + schema::ExtendedType::Object(_) => "OBJECT", + schema::ExtendedType::Interface(_) => "INTERFACE", + schema::ExtendedType::Union(_) => "UNION", + schema::ExtendedType::Enum(_) => "ENUM", + schema::ExtendedType::InputObject(_) => "INPUT_OBJECT", + })) + } + + fn name(&self_) { + Ok(ResolvedValue::leaf(self_.name)) + } + + fn description(&self_) { + Ok(ResolvedValue::leaf(self_.def.description().map(|desc| desc.as_str()))) + } + + fn fields(&self_, args) { + let fields = match self_.def { + schema::ExtendedType::Object(def) => &def.fields, + schema::ExtendedType::Interface(def) => &def.fields, + schema::ExtendedType::Scalar(_) | + schema::ExtendedType::Union(_) | + schema::ExtendedType::Enum(_) | + schema::ExtendedType::InputObject(_) => return Ok(ResolvedValue::null()), + }; + let include_deprecated = args["includeDeprecated"].as_bool().unwrap(); + Ok(ResolvedValue::list(fields + .values() + .filter(move |def| { + include_deprecated || def.directives.get("deprecated").is_none() + }) + .map(|def| { + ResolvedValue::object(FieldResolver { schema: self_.schema, def }) + }) + )) + } + + fn interfaces(&self_) { + let implements_interfaces = match self_.def { + schema::ExtendedType::Object(def) => &def.implements_interfaces, + schema::ExtendedType::Interface(def) => &def.implements_interfaces, + schema::ExtendedType::Scalar(_) | + schema::ExtendedType::Union(_) | + schema::ExtendedType::Enum(_) | + schema::ExtendedType::InputObject(_) => return Ok(ResolvedValue::null()), + }; + Ok(ResolvedValue::list(implements_interfaces.iter().filter_map(|name| { + self_.schema.types.get(&name.name).map(|def| { + ResolvedValue::object(TypeDefResolver { schema: self_.schema, name, def }) + }) + }))) + } + + fn possibleTypes(&self_) { + macro_rules! types { + ($names: expr) => { + Ok(ResolvedValue::list($names.filter_map(move |name| { + self_.schema.types.get(name).map(move |def| { + ResolvedValue::object(TypeDefResolver { schema: self_.schema, name, def }) + }) + }))) + } + } + match self_.def { + schema::ExtendedType::Interface(_) => types!(self_.schema.implementers_of(self_.name)), + schema::ExtendedType::Union(def) => types!(def.members.iter().map(|c| &c.name)), + schema::ExtendedType::Object(_) | + schema::ExtendedType::Scalar(_) | + schema::ExtendedType::Enum(_) | + schema::ExtendedType::InputObject(_) => Ok(ResolvedValue::null()), + } + } + + fn enumValues(&self_, args) { + let schema::ExtendedType::Enum(def) = self_.def else { + return Ok(ResolvedValue::null()); + }; + let include_deprecated = args["includeDeprecated"].as_bool().unwrap(); + Ok(ResolvedValue::list(def + .values + .values() + .filter(move |def| { + include_deprecated || def.directives.get("deprecated").is_none() + }) + .map(|def| { + ResolvedValue::object(EnumValueResolver { def }) + }) + )) + } + + fn inputFields(&self_, args) { + let schema::ExtendedType::InputObject(def) = self_.def else { + return Ok(ResolvedValue::null()); + }; + let include_deprecated = args["includeDeprecated"].as_bool().unwrap(); + Ok(ResolvedValue::list(def + .fields + .values() + .filter(move |def| { + include_deprecated || def.directives.get("deprecated").is_none() + }) + .map(|def| { + ResolvedValue::object(InputValueResolver { schema: self_.schema, def }) + }) + )) + } + + fn ofType() { + Ok(ResolvedValue::null()) + } + + fn specifiedByURL(&self_) { + let schema::ExtendedType::Scalar(def) = self_.def else { + return Ok(ResolvedValue::null()) + }; + Ok(ResolvedValue::leaf(def + .directives.get("specifiedBy") + .and_then(|dir| dir.argument_by_name("url")) + .and_then(|arg| arg.as_str()) + )) + } +} + +// Only used for non-null and list types +impl_resolver! { + for TypeResolver<'_>: + + __typename = "__Type"; + + fn kind(&self_) { + Ok(ResolvedValue::leaf(match &*self_.ty { + schema::Type::Named(_) => unreachable!(), + schema::Type::List(_) => "LIST", + schema::Type::NonNullNamed(_) | + schema::Type::NonNullList(_) => "NON_NULL", + })) + } + + fn ofType(&self_) { + Ok(match &*self_.ty { + schema::Type::Named(_) => unreachable!(), + schema::Type::List(inner) => ty(self_.schema, inner), + schema::Type::NonNullNamed(inner) => type_def(self_.schema, inner), + schema::Type::NonNullList(inner) => ResolvedValue::object(Self { + schema: self_.schema, + ty: Cow::Owned(schema::Type::List(inner.clone())) + }), + }) + } + + fn name() { Ok(ResolvedValue::null()) } + fn description() { Ok(ResolvedValue::null()) } + fn fields() { Ok(ResolvedValue::null()) } + fn interfaces() { Ok(ResolvedValue::null()) } + fn possibleTypes() { Ok(ResolvedValue::null()) } + fn enumValues() { Ok(ResolvedValue::null()) } + fn inputFields() { Ok(ResolvedValue::null()) } + fn specifiedBy() { Ok(ResolvedValue::null()) } +} + +impl_resolver! { + for DirectiveResolver<'_>: + + __typename = "__Directive"; + + fn name(&self_) { + Ok(ResolvedValue::leaf(self_.def.name.as_str())) + } + + fn description(&self_) { + Ok(ResolvedValue::leaf(self_.def.description.as_deref())) + } + + fn args(&self_, args) { + let include_deprecated = args["includeDeprecated"].as_bool().unwrap(); + Ok(ResolvedValue::list(self_ + .def + .arguments + .iter() + .filter(move |def| { + include_deprecated || def.directives.get("deprecated").is_none() + }) + .map(|def| { + ResolvedValue::object(InputValueResolver { schema: self_.schema, def }) + }) + )) + } + + fn locations(&self_) { + Ok(ResolvedValue::list(self_.def.locations.iter().map(|loc| { + ResolvedValue::leaf(loc.name()) + }))) + } + + fn isRepeatable(&self_) { + Ok(ResolvedValue::leaf(self_.def.repeatable)) + } +} + +impl_resolver! { + for FieldResolver<'_>: + + __typename = "__Field"; + + fn name(&self_) { + Ok(ResolvedValue::leaf(self_.def.name.as_str())) + } + + fn description(&self_) { + Ok(ResolvedValue::leaf(self_.def.description.as_deref())) + } + + fn args(&self_, args) { + let include_deprecated = args["includeDeprecated"].as_bool().unwrap(); + Ok(ResolvedValue::list(self_ + .def + .arguments + .iter() + .filter(move |def| { + include_deprecated || def.directives.get("deprecated").is_none() + }) + .map(|def| { + ResolvedValue::object(InputValueResolver { schema: self_.schema, def }) + }) + )) + } + + fn type(&self_) { + Ok(ty(self_.schema, &self_.def.ty)) + } + + fn isDeprecated(&self_) { + Ok(ResolvedValue::leaf(self_.def.directives.get("deprecated").is_some())) + } + + fn deprecationReason(&self_) { + Ok(deprecation_reason(self_.def.directives.get("deprecated"))) + } +} + +impl_resolver! { + for EnumValueResolver<'_>: + + __typename = "__EnumValue"; + + fn name(&self_) { + Ok(ResolvedValue::leaf(self_.def.value.as_str())) + } + + fn description(&self_) { + Ok(ResolvedValue::leaf(self_.def.description.as_deref())) + } + + fn isDeprecated(&self_) { + Ok(ResolvedValue::leaf(self_.def.directives.get("deprecated").is_some())) + } + + fn deprecationReason(&self_) { + Ok(deprecation_reason(self_.def.directives.get("deprecated"))) + } +} + +impl_resolver! { + for InputValueResolver<'_>: + + __typename = "__InputValue"; + + fn name(&self_) { + Ok(ResolvedValue::leaf(self_.def.name.as_str())) + } + + fn description(&self_) { + Ok(ResolvedValue::leaf(self_.def.description.as_deref())) + } + + fn type(&self_) { + Ok(ty(self_.schema, &self_.def.ty)) + } + + fn defaultValue(&self_) { + Ok(ResolvedValue::leaf(self_.def.default_value.as_ref().map(|val| val.to_string()))) + } + + fn isDeprecated(&self_) { + Ok(ResolvedValue::leaf(self_.def.directives.get("deprecated").is_some())) + } + + fn deprecationReason(&self_) { + Ok(deprecation_reason(self_.def.directives.get("deprecated"))) + } +} diff --git a/crates/apollo-compiler/src/execution/mod.rs b/crates/apollo-compiler/src/execution/mod.rs index c4b005689..9478ea1c8 100644 --- a/crates/apollo-compiler/src/execution/mod.rs +++ b/crates/apollo-compiler/src/execution/mod.rs @@ -4,16 +4,23 @@ //! [execution]: https://spec.graphql.org/October2021/#sec-Execution //! [response]: https://spec.graphql.org/October2021/#sec-Response +pub(crate) mod engine; mod input_coercion; +#[macro_use] +pub(crate) mod resolver; +mod introspection; mod response; +mod result_coercion; pub use self::input_coercion::coerce_variable_values; +pub use self::introspection::SchemaIntrospection; pub use self::response::GraphQLError; pub use self::response::GraphQLLocation; pub use self::response::PathElement; pub use self::response::RequestError; pub use self::response::Response; pub use self::response::ResponseData; +pub use self::response::EXTENSION_SUSPECTED_VALIDATION_BUG; /// Re-export of the version of the `serde_json_bytes` crate used for [`JsonValue`] and [`JsonMap`] pub use serde_json_bytes; diff --git a/crates/apollo-compiler/src/execution/resolver.rs b/crates/apollo-compiler/src/execution/resolver.rs new file mode 100644 index 000000000..b081e5989 --- /dev/null +++ b/crates/apollo-compiler/src/execution/resolver.rs @@ -0,0 +1,215 @@ +use crate::execution::JsonMap; +use serde_json_bytes::Value as JsonValue; + +/// A GraphQL object whose fields can be resolved during execution +pub(crate) type ObjectValue<'a> = dyn Resolver + 'a; + +/// Abstraction for implementing field resolvers. Used through [`ObjectValue`]. +/// +/// Use the [`impl_resolver!`][crate::impl_resolver] macro to implement this trait +/// with reduced boilerplate +pub(crate) trait Resolver { + /// Returns the name of the concrete object type this resolver represents + /// + /// That name expected to be that of an object type defined in the schema. + /// This is called when the schema indicates an abstract (interface or union) type. + fn type_name(&self) -> &'static str; + + /// Resolves a field of this object with the given arguments + /// + /// The resolved is expected to match the type of the corresponding field definition + /// in the schema. + fn resolve_field<'a>( + &'a self, + field_name: &'a str, + arguments: &'a JsonMap, + ) -> Result, ResolverError>; +} + +pub(crate) struct ResolverError { + pub(crate) message: String, +} + +/// Implements the [`Resolver`] trait with reduced boilerplate +/// +/// Define: +/// +/// * The implementing Rust type +/// * The __typename string +/// * One pseudo-method per field. Types are omitted in the signature for brevity. +/// - Takes two optional arguments: `&self` (which must be spelled something else because macros) +/// and `args: `[`&JsonMap`][crate::JsonMap] for the field arguments. +/// Field arguments are coerced according to their definition in the schema. +/// - Returns `Result`, `Err` it turned into a field error +macro_rules! impl_resolver { + ( + for $ty: ty: + __typename = $type_name: expr; + $( + fn $field_name: ident( + $( &$self_: ident $(, $( $args: ident $(,)? )? )? )? + ) $block: block + )* + + ) => { + impl $crate::execution::resolver::Resolver for $ty { + fn type_name(&self) -> &'static str { + $type_name + } + + fn resolve_field<'a>( + &'a self, + field_name: &'a str, + arguments: &'a $crate::execution::JsonMap, + ) -> Result< + $crate::execution::resolver::ResolvedValue<'_>, + crate::execution::resolver::ResolverError + > { + let _allow_unused = arguments; + match field_name { + $( + stringify!($field_name) => { + $( + let $self_ = self; + $($( + let $args = arguments; + )?)? + )? + return $block + }, + )* + _ => Err(crate::execution::resolver::ResolverError { + message: format!("unexpected field name: {field_name}") + }), + } + } + } + }; +} + +/// The value of a resolved field +pub(crate) enum ResolvedValue<'a> { + /// * JSON null represents GraphQL null + /// * A GraphQL enum value is represented as a JSON string + /// * GraphQL built-in scalars are coerced according to their respective *Result Coercion* spec + /// * For custom scalars, any JSON value is passed through as-is (including array or object) + Leaf(JsonValue), + + /// Expected where the GraphQL type is an object, interface, or union type + Object(Box>), + + /// Expected for GraphQL list types + List(Box> + 'a>), +} + +impl<'a> ResolvedValue<'a> { + /// Construct a null leaf resolved value + pub(crate) fn null() -> Self { + Self::Leaf(JsonValue::Null) + } + + /// Construct a leaf resolved value from something that is convertible to JSON + pub(crate) fn leaf(json: impl Into) -> Self { + Self::Leaf(json.into()) + } + + /// Construct an object resolved value from the resolver for that object + pub(crate) fn object(resolver: impl Resolver + 'a) -> Self { + Self::Object(Box::new(resolver)) + } + + /// Construct an object resolved value or null, from an optional resolver + pub(crate) fn opt_object(opt_resolver: Option) -> Self { + match opt_resolver { + Some(resolver) => Self::Object(Box::new(resolver)), + None => Self::null(), + } + } + + /// Construct a list resolved value from an iterator + pub(crate) fn list(iter: I) -> Self + where + I: IntoIterator, + I::IntoIter: 'a, + { + Self::List(Box::new(iter.into_iter())) + } +} + +#[cfg(test)] +mod tests { + use crate::executable::OperationType; + use crate::execution::coerce_variable_values; + use crate::execution::engine::execute_query_or_mutation; + use crate::execution::resolver::ResolvedValue; + use crate::execution::JsonMap; + use crate::execution::RequestError; + use crate::execution::Response; + use crate::execution::SchemaIntrospection; + use crate::validation::Valid; + use crate::ExecutableDocument; + use crate::Schema; + + struct QueryResolver { + world: String, + } + + impl_resolver! { + for &'_ QueryResolver: + + __typename = "Query"; + + fn null() { + Ok(ResolvedValue::null()) + } + + fn hello(&self_) { + Ok(ResolvedValue::list([ + ResolvedValue::leaf(format!("Hello {}!", self_.world)), + ResolvedValue::leaf(format!("Hello {}!", self_.world)), + ])) + } + + fn echo(&_self, args) { + Ok(ResolvedValue::leaf(args["value"].clone())) + } + + fn myself_again(&self_) { + Ok(ResolvedValue::object(*self_)) + } + } + + /// + #[allow(unused)] + fn execute_request( + schema: &Valid, + document: &Valid, + operation_name: Option<&str>, + variable_values: &JsonMap, + ) -> Result { + let variable_values = + coerce_variable_values(schema, document, operation_name, variable_values)?; + SchemaIntrospection::execute_with( + schema, + document, + operation_name, + &variable_values, + |filtered_document| { + let operation = filtered_document.get_operation(operation_name)?; + let initial_value = match operation.operation_type { + OperationType::Query => QueryResolver { + world: "World".into(), + }, + _ => unimplemented!(), + }; + execute_query_or_mutation( + schema, + filtered_document, + &variable_values, + &&initial_value, + operation, + ) + }, + ) + } +} diff --git a/crates/apollo-compiler/src/execution/response.rs b/crates/apollo-compiler/src/execution/response.rs index 4172ce807..b842af282 100644 --- a/crates/apollo-compiler/src/execution/response.rs +++ b/crates/apollo-compiler/src/execution/response.rs @@ -24,6 +24,7 @@ pub struct Response { #[serde(default = "ResponseData::absent")] pub data: ResponseData, + /// Reserved for any additional information #[serde(skip_serializing_if = "JsonMap::is_empty")] #[serde(default)] pub extensions: JsonMap, @@ -83,6 +84,7 @@ pub struct GraphQLError { #[serde(default)] pub path: Vec, + /// Reserved for any additional information #[serde(skip_serializing_if = "JsonMap::is_empty")] #[serde(default)] pub extensions: JsonMap, @@ -110,6 +112,31 @@ pub enum PathElement { ListIndex(usize), } +impl Response { + /// Merge two responses into one, such as to handle + /// [`SchemaIntrospection::Both`][crate::execution::SchemaIntrospection::Both]. + pub fn merge(mut self, mut other: Self) -> Self { + match (&mut self.data, other.data) { + (ResponseData::Absent, _) | (_, ResponseData::Absent) => { + // If either side is a request error (absent data), return a request error + self.data = ResponseData::Absent + } + (ResponseData::Null, _) | (_, ResponseData::Null) => { + // Otherwise if either side propagated null from a field error + // to the root of the response, return null data. + self.data = ResponseData::Null + } + (ResponseData::Object(self_data), ResponseData::Object(other_data)) => { + // Merge two objects/maps + self_data.extend(other_data) + } + } + self.errors.append(&mut other.errors); + self.extensions.extend(other.extensions); + self + } +} + impl GraphQLError { /// Call for errors that should not happen with a valid schema and document. /// See [`EXTENSION_SUSPECTED_VALIDATION_BUG`]. diff --git a/crates/apollo-compiler/src/execution/result_coercion.rs b/crates/apollo-compiler/src/execution/result_coercion.rs new file mode 100644 index 000000000..c7a95a78c --- /dev/null +++ b/crates/apollo-compiler/src/execution/result_coercion.rs @@ -0,0 +1,210 @@ +use crate::executable::Field; +use crate::execution::engine::execute_selection_set; +use crate::execution::engine::field_error; +use crate::execution::engine::try_nullify; +use crate::execution::engine::ExecutionMode; +use crate::execution::engine::LinkedPath; +use crate::execution::engine::LinkedPathElement; +use crate::execution::engine::PropagateNull; +use crate::execution::resolver::ResolvedValue; +use crate::execution::response::PathElement; +use crate::execution::GraphQLError; +use crate::execution::JsonMap; +use crate::execution::JsonValue; +use crate::schema::ExtendedType; +use crate::schema::Type; +use crate::validation::Valid; +use crate::ExecutableDocument; +use crate::Schema; + +/// +/// +/// Returns `Err` for a field error being propagated upwards to find a nullable place +#[allow(clippy::too_many_arguments)] // yes it’s not a nice API but it’s internal +pub(crate) fn complete_value<'a, 'b>( + schema: &'a Valid, + document: &'a Valid, + variable_values: &'a Valid, + errors: &'b mut Vec, + path: LinkedPath<'b>, + mode: ExecutionMode, + ty: &'a Type, + resolved: ResolvedValue<'a>, + fields: &'a [&'a Field], +) -> Result { + let new_field_error = + |message| field_error(message, path, fields[0].name.location(), &document.sources); + macro_rules! field_error { + ($($arg: tt)+) => { + { + errors.push(new_field_error(format!($($arg)+))); + return Err(PropagateNull); + } + }; + } + if let ResolvedValue::Leaf(JsonValue::Null) = resolved { + if ty.is_non_null() { + field_error!("Non-null type {ty} resolved to null") + } else { + return Ok(JsonValue::Null); + } + } + if let ResolvedValue::List(iter) = resolved { + match ty { + Type::Named(_) | Type::NonNullNamed(_) => { + field_error!("Non-list type {ty} resolved to a list") + } + Type::List(inner_ty) | Type::NonNullList(inner_ty) => { + let mut completed_list = Vec::with_capacity(iter.size_hint().0); + for (index, inner_resolved) in iter.enumerate() { + let inner_path = LinkedPathElement { + element: PathElement::ListIndex(index), + next: path, + }; + let inner_result = complete_value( + schema, + document, + variable_values, + errors, + Some(&inner_path), + mode, + inner_ty, + inner_resolved, + fields, + ); + // On field error, try to nullify that item + match try_nullify(inner_ty, inner_result) { + Ok(inner_value) => completed_list.push(inner_value), + // If the item is non-null, try to nullify the list + Err(PropagateNull) => return try_nullify(ty, Err(PropagateNull)), + } + } + return Ok(completed_list.into()); + } + } + } + let ty_name = match ty { + Type::List(_) | Type::NonNullList(_) => { + field_error!("List type {ty} resolved to an object") + } + Type::Named(name) | Type::NonNullNamed(name) => name, + }; + let Some(ty_def) = schema.types.get(ty_name) else { + errors.push(new_field_error(format!("Undefined type {ty_name}")).validation_bug()); + return Err(PropagateNull); + }; + if let ExtendedType::InputObject(_) = ty_def { + errors.push( + new_field_error(format!("Field with input object type {ty_name}")).validation_bug(), + ); + return Err(PropagateNull); + } + let resolved_obj = match resolved { + ResolvedValue::List(_) => unreachable!(), // early return above + ResolvedValue::Leaf(json_value) => { + match ty_def { + ExtendedType::InputObject(_) => unreachable!(), // early return above + ExtendedType::Object(_) | ExtendedType::Interface(_) | ExtendedType::Union(_) => { + field_error!( + "Resolver returned a leaf value \ + but expected an object for type {ty_name}" + ) + } + ExtendedType::Enum(enum_def) => { + // https://spec.graphql.org/October2021/#sec-Enums.Result-Coercion + if !json_value + .as_str() + .is_some_and(|str| enum_def.values.contains_key(str)) + { + field_error!("Resolver returned {json_value}, expected enum {ty_name}") + } + } + ExtendedType::Scalar(_) => match ty_name.as_str() { + "Int" => { + // https://spec.graphql.org/October2021/#sec-Int.Result-Coercion + // > GraphQL services may coerce non-integer internal values to integers + // > when reasonable without losing information + // + // We choose not to, to keep with Rust’s strong typing + if let Some(int) = json_value.as_i64() { + if i32::try_from(int).is_err() { + field_error!("Resolver returned {json_value} which overflows Int") + } + } else { + field_error!("Resolver returned {json_value}, expected Int") + } + } + "Float" => { + // https://spec.graphql.org/October2021/#sec-Float.Result-Coercion + if !json_value.is_f64() { + field_error!("Resolver returned {json_value}, expected Float") + } + } + "String" => { + // https://spec.graphql.org/October2021/#sec-String.Result-Coercion + if !json_value.is_string() { + field_error!("Resolver returned {json_value}, expected String") + } + } + "Boolean" => { + // https://spec.graphql.org/October2021/#sec-Boolean.Result-Coercion + if !json_value.is_boolean() { + field_error!("Resolver returned {json_value}, expected Boolean") + } + } + "ID" => { + // https://spec.graphql.org/October2021/#sec-ID.Result-Coercion + if !(json_value.is_string() || json_value.is_i64()) { + field_error!("Resolver returned {json_value}, expected ID") + } + } + _ => { + // Custom scalar: accept any JSON value (including an array or object, + // despite this being a "leaf" as far as GraphQL resolution is concerned) + } + }, + }; + return Ok(json_value); + } + ResolvedValue::Object(resolved_obj) => resolved_obj, + }; + let (object_type_name, object_type) = match ty_def { + ExtendedType::InputObject(_) => unreachable!(), // early return above + ExtendedType::Enum(_) | ExtendedType::Scalar(_) => { + field_error!( + "Resolver returned a an object of type {}, expected {ty_name}", + resolved_obj.type_name() + ) + } + ExtendedType::Interface(_) | ExtendedType::Union(_) => { + let object_type_name = resolved_obj.type_name(); + if let Some(def) = schema.get_object(object_type_name) { + (object_type_name, def) + } else { + field_error!( + "Resolver returned an object of type {object_type_name} \ + not defined in the schema" + ) + } + } + ExtendedType::Object(def) => { + debug_assert_eq!(ty_name, resolved_obj.type_name()); + (ty_name.as_str(), def) + } + }; + execute_selection_set( + schema, + document, + variable_values, + errors, + path, + mode, + object_type_name, + object_type, + &*resolved_obj, + fields + .iter() + .flat_map(|field| &field.selection_set.selections), + ) + .map(JsonValue::Object) +} diff --git a/crates/apollo-compiler/src/introspection/resolver.rs b/crates/apollo-compiler/src/introspection/resolver.rs new file mode 100644 index 000000000..f00aa20d5 --- /dev/null +++ b/crates/apollo-compiler/src/introspection/resolver.rs @@ -0,0 +1,454 @@ +use crate::execution::resolver::ResolvedValue; +use crate::schema; +use crate::schema::Name; +use crate::Node; +use crate::Schema; +use std::borrow::Cow; +use std::collections::HashMap; +use std::collections::HashSet; +use std::sync::OnceLock; + +#[derive(Clone, Copy)] +pub(super) struct SchemaWithCache<'a> { + pub(super) schema: &'a Schema, + pub(super) implementers_map: &'a OnceLock>>, +} + +impl<'a> SchemaWithCache<'a> { + fn implementers_of(&self, interface_name: &str) -> impl Iterator { + self.implementers_map + .get_or_init(|| self.schema.implementers_map()) + .get(interface_name) + .into_iter() + .flatten() + } +} + +impl<'a> std::ops::Deref for SchemaWithCache<'a> { + type Target = &'a Schema; + + fn deref(&self) -> &Self::Target { + &self.schema + } +} + +pub(super) struct IntrospectionRootResolver<'a>(pub(super) SchemaWithCache<'a>); + +struct TypeDefResolver<'a> { + schema: SchemaWithCache<'a>, + name: &'a str, + def: &'a schema::ExtendedType, +} + +/// Only used for non-null and list types. `TypeDef` is used for everything else. +struct TypeResolver<'a> { + schema: SchemaWithCache<'a>, + ty: Cow<'a, schema::Type>, +} + +struct DirectiveResolver<'a> { + schema: SchemaWithCache<'a>, + def: &'a schema::DirectiveDefinition, +} + +struct FieldResolver<'a> { + schema: SchemaWithCache<'a>, + def: &'a schema::FieldDefinition, +} + +struct EnumValueResolver<'a> { + def: &'a schema::EnumValueDefinition, +} + +struct InputValueResolver<'a> { + schema: SchemaWithCache<'a>, + def: &'a schema::InputValueDefinition, +} + +fn type_def(schema: SchemaWithCache<'_>, name: impl AsRef) -> ResolvedValue<'_> { + ResolvedValue::opt_object( + schema + .types + .get_key_value(name.as_ref()) + .map(|(name, def)| TypeDefResolver { schema, name, def }), + ) +} + +fn type_def_opt<'a>( + schema: SchemaWithCache<'a>, + name: &Option>, +) -> ResolvedValue<'a> { + if let Some(name) = name.as_ref() { + type_def(schema, name) + } else { + ResolvedValue::null() + } +} + +fn ty<'a>(schema: SchemaWithCache<'a>, ty: &'a schema::Type) -> ResolvedValue<'a> { + if let schema::Type::Named(name) = ty { + type_def(schema, name) + } else { + ResolvedValue::object(TypeResolver { + schema, + ty: Cow::Borrowed(ty), + }) + } +} + +fn deprecation_reason(opt_directive: Option<&Node>) -> ResolvedValue<'_> { + ResolvedValue::leaf( + opt_directive + .and_then(|directive| directive.argument_by_name("reason")) + .and_then(|arg| arg.as_str()), + ) +} + +impl_resolver! { + for IntrospectionRootResolver<'_>: + + __typename = unreachable!(); + + fn __schema(&self_) { + Ok(ResolvedValue::object(self_.0)) + } + + fn __type(&self_, args) { + let name = args["name"].as_str().unwrap(); + Ok(type_def(self_.0, name)) + } +} + +impl_resolver! { + for SchemaWithCache<'_>: + + __typename = "__Schema"; + + fn description(&self_) { + Ok(ResolvedValue::leaf(self_.schema_definition.description.as_deref())) + } + + fn types(&self_) { + Ok(ResolvedValue::list(self_.types.iter().map(|(name, def)| { + ResolvedValue::object(TypeDefResolver { schema: *self_, name, def }) + }))) + } + + fn directives(&self_) { + Ok(ResolvedValue::list(self_.directive_definitions.values().map(|def| { + ResolvedValue::object(DirectiveResolver { schema: *self_, def }) + }))) + } + + fn queryType(&self_) { + Ok(type_def_opt(*self_, &self_.schema_definition.query)) + } + + fn mutationType(&self_) { + Ok(type_def_opt(*self_, &self_.schema_definition.mutation)) + } + + fn subscriptionType(&self_) { + Ok(type_def_opt(*self_, &self_.schema_definition.subscription)) + } +} + +impl_resolver! { + for TypeDefResolver<'_>: + + __typename = "__Type"; + + fn kind(&self_) { + Ok(ResolvedValue::leaf(match self_.def { + schema::ExtendedType::Scalar(_) => "SCALAR", + schema::ExtendedType::Object(_) => "OBJECT", + schema::ExtendedType::Interface(_) => "INTERFACE", + schema::ExtendedType::Union(_) => "UNION", + schema::ExtendedType::Enum(_) => "ENUM", + schema::ExtendedType::InputObject(_) => "INPUT_OBJECT", + })) + } + + fn name(&self_) { + Ok(ResolvedValue::leaf(self_.name)) + } + + fn description(&self_) { + Ok(ResolvedValue::leaf(self_.def.description().map(|desc| desc.as_str()))) + } + + fn fields(&self_, args) { + let fields = match self_.def { + schema::ExtendedType::Object(def) => &def.fields, + schema::ExtendedType::Interface(def) => &def.fields, + schema::ExtendedType::Scalar(_) | + schema::ExtendedType::Union(_) | + schema::ExtendedType::Enum(_) | + schema::ExtendedType::InputObject(_) => return Ok(ResolvedValue::null()), + }; + let include_deprecated = args["includeDeprecated"].as_bool().unwrap(); + Ok(ResolvedValue::list(fields + .values() + .filter(move |def| { + include_deprecated || def.directives.get("deprecated").is_none() + }) + .map(|def| { + ResolvedValue::object(FieldResolver { schema: self_.schema, def }) + }) + )) + } + + fn interfaces(&self_) { + let implements_interfaces = match self_.def { + schema::ExtendedType::Object(def) => &def.implements_interfaces, + schema::ExtendedType::Interface(def) => &def.implements_interfaces, + schema::ExtendedType::Scalar(_) | + schema::ExtendedType::Union(_) | + schema::ExtendedType::Enum(_) | + schema::ExtendedType::InputObject(_) => return Ok(ResolvedValue::null()), + }; + Ok(ResolvedValue::list(implements_interfaces.iter().filter_map(|name| { + self_.schema.types.get(&name.name).map(|def| { + ResolvedValue::object(TypeDefResolver { schema: self_.schema, name, def }) + }) + }))) + } + + fn possibleTypes(&self_) { + macro_rules! types { + ($names: expr) => { + Ok(ResolvedValue::list($names.filter_map(move |name| { + self_.schema.types.get(name).map(move |def| { + ResolvedValue::object(TypeDefResolver { schema: self_.schema, name, def }) + }) + }))) + } + } + match self_.def { + schema::ExtendedType::Interface(_) => types!(self_.schema.implementers_of(self_.name)), + schema::ExtendedType::Union(def) => types!(def.members.iter().map(|c| &c.name)), + schema::ExtendedType::Object(_) | + schema::ExtendedType::Scalar(_) | + schema::ExtendedType::Enum(_) | + schema::ExtendedType::InputObject(_) => Ok(ResolvedValue::null()), + } + } + + fn enumValues(&self_, args) { + let schema::ExtendedType::Enum(def) = self_.def else { + return Ok(ResolvedValue::null()); + }; + let include_deprecated = args["includeDeprecated"].as_bool().unwrap(); + Ok(ResolvedValue::list(def + .values + .values() + .filter(move |def| { + include_deprecated || def.directives.get("deprecated").is_none() + }) + .map(|def| { + ResolvedValue::object(EnumValueResolver { def }) + }) + )) + } + + fn inputFields(&self_, args) { + let schema::ExtendedType::InputObject(def) = self_.def else { + return Ok(ResolvedValue::null()); + }; + let include_deprecated = args["includeDeprecated"].as_bool().unwrap(); + Ok(ResolvedValue::list(def + .fields + .values() + .filter(move |def| { + include_deprecated || def.directives.get("deprecated").is_none() + }) + .map(|def| { + ResolvedValue::object(InputValueResolver { schema: self_.schema, def }) + }) + )) + } + + fn ofType() { + Ok(ResolvedValue::null()) + } + + fn specifiedByURL(&self_) { + let schema::ExtendedType::Scalar(def) = self_.def else { + return Ok(ResolvedValue::null()) + }; + Ok(ResolvedValue::leaf(def + .directives.get("specifiedBy") + .and_then(|dir| dir.argument_by_name("url")) + .and_then(|arg| arg.as_str()) + )) + } +} + +// Only used for non-null and list types +impl_resolver! { + for TypeResolver<'_>: + + __typename = "__Type"; + + fn kind(&self_) { + Ok(ResolvedValue::leaf(match &*self_.ty { + schema::Type::Named(_) => unreachable!(), + schema::Type::List(_) => "LIST", + schema::Type::NonNullNamed(_) | + schema::Type::NonNullList(_) => "NON_NULL", + })) + } + + fn ofType(&self_) { + Ok(match &*self_.ty { + schema::Type::Named(_) => unreachable!(), + schema::Type::List(inner) => ty(self_.schema, inner), + schema::Type::NonNullNamed(inner) => type_def(self_.schema, inner), + schema::Type::NonNullList(inner) => ResolvedValue::object(Self { + schema: self_.schema, + ty: Cow::Owned(schema::Type::List(inner.clone())) + }), + }) + } + + fn name() { Ok(ResolvedValue::null()) } + fn description() { Ok(ResolvedValue::null()) } + fn fields() { Ok(ResolvedValue::null()) } + fn interfaces() { Ok(ResolvedValue::null()) } + fn possibleTypes() { Ok(ResolvedValue::null()) } + fn enumValues() { Ok(ResolvedValue::null()) } + fn inputFields() { Ok(ResolvedValue::null()) } + fn specifiedBy() { Ok(ResolvedValue::null()) } +} + +impl_resolver! { + for DirectiveResolver<'_>: + + __typename = "__Directive"; + + fn name(&self_) { + Ok(ResolvedValue::leaf(self_.def.name.as_str())) + } + + fn description(&self_) { + Ok(ResolvedValue::leaf(self_.def.description.as_deref())) + } + + fn args(&self_, args) { + let include_deprecated = args["includeDeprecated"].as_bool().unwrap(); + Ok(ResolvedValue::list(self_ + .def + .arguments + .iter() + .filter(move |def| { + include_deprecated || def.directives.get("deprecated").is_none() + }) + .map(|def| { + ResolvedValue::object(InputValueResolver { schema: self_.schema, def }) + }) + )) + } + + fn locations(&self_) { + Ok(ResolvedValue::list(self_.def.locations.iter().map(|loc| { + ResolvedValue::leaf(loc.name()) + }))) + } + + fn isRepeatable(&self_) { + Ok(ResolvedValue::leaf(self_.def.repeatable)) + } +} + +impl_resolver! { + for FieldResolver<'_>: + + __typename = "__Field"; + + fn name(&self_) { + Ok(ResolvedValue::leaf(self_.def.name.as_str())) + } + + fn description(&self_) { + Ok(ResolvedValue::leaf(self_.def.description.as_deref())) + } + + fn args(&self_, args) { + let include_deprecated = args["includeDeprecated"].as_bool().unwrap(); + Ok(ResolvedValue::list(self_ + .def + .arguments + .iter() + .filter(move |def| { + include_deprecated || def.directives.get("deprecated").is_none() + }) + .map(|def| { + ResolvedValue::object(InputValueResolver { schema: self_.schema, def }) + }) + )) + } + + fn type(&self_) { + Ok(ty(self_.schema, &self_.def.ty)) + } + + fn isDeprecated(&self_) { + Ok(ResolvedValue::leaf(self_.def.directives.get("deprecated").is_some())) + } + + fn deprecationReason(&self_) { + Ok(deprecation_reason(self_.def.directives.get("deprecated"))) + } +} + +impl_resolver! { + for EnumValueResolver<'_>: + + __typename = "__EnumValue"; + + fn name(&self_) { + Ok(ResolvedValue::leaf(self_.def.value.as_str())) + } + + fn description(&self_) { + Ok(ResolvedValue::leaf(self_.def.description.as_deref())) + } + + fn isDeprecated(&self_) { + Ok(ResolvedValue::leaf(self_.def.directives.get("deprecated").is_some())) + } + + fn deprecationReason(&self_) { + Ok(deprecation_reason(self_.def.directives.get("deprecated"))) + } +} + +impl_resolver! { + for InputValueResolver<'_>: + + __typename = "__InputValue"; + + fn name(&self_) { + Ok(ResolvedValue::leaf(self_.def.name.as_str())) + } + + fn description(&self_) { + Ok(ResolvedValue::leaf(self_.def.description.as_deref())) + } + + fn type(&self_) { + Ok(ty(self_.schema, &self_.def.ty)) + } + + fn defaultValue(&self_) { + Ok(ResolvedValue::leaf(self_.def.default_value.as_ref().map(|val| val.to_string()))) + } + + fn isDeprecated(&self_) { + Ok(ResolvedValue::leaf(self_.def.directives.get("deprecated").is_some())) + } + + fn deprecationReason(&self_) { + Ok(deprecation_reason(self_.def.directives.get("deprecated"))) + } +} diff --git a/crates/apollo-compiler/test_data/introspection/introspect_full_schema.graphql b/crates/apollo-compiler/test_data/introspection/introspect_full_schema.graphql new file mode 100644 index 000000000..a0a3100c4 --- /dev/null +++ b/crates/apollo-compiler/test_data/introspection/introspect_full_schema.graphql @@ -0,0 +1,98 @@ +query IntrospectionQuery { + __schema { + queryType { + name + } + mutationType { + name + } + subscriptionType { + name + } + types { + ...FullType + } + directives { + name + description + locations + args(includeDeprecated: true) { + ...InputValue + } + } + } +} +fragment FullType on __Type { + kind + name + description + fields(includeDeprecated: true) { + name + description + args(includeDeprecated: true) { + ...InputValue + } + type { + ...TypeRef + } + isDeprecated + deprecationReason + } + inputFields(includeDeprecated: true) { + ...InputValue + } + interfaces { + ...TypeRef + } + enumValues(includeDeprecated: true) { + name + description + isDeprecated + deprecationReason + } + possibleTypes { + ...TypeRef + } +} +fragment InputValue on __InputValue { + name + description + type { + ...TypeRef + } + defaultValue + isDeprecated + deprecationReason +} +fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } + } + } + } +} diff --git a/crates/apollo-compiler/test_data/introspection/response_full.json b/crates/apollo-compiler/test_data/introspection/response_full.json new file mode 100644 index 000000000..c18017a4f --- /dev/null +++ b/crates/apollo-compiler/test_data/introspection/response_full.json @@ -0,0 +1,1179 @@ +{ + "data": { + "__schema": { + "queryType": { + "name": "Query" + }, + "mutationType": null, + "subscriptionType": null, + "types": [ + { + "kind": "OBJECT", + "name": "__Schema", + "description": null, + "fields": [ + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "types", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "queryType", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "mutationType", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "subscriptionType", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "directives", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Directive", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Type", + "description": null, + "fields": [ + { + "name": "kind", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__TypeKind", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "fields", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Field", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "interfaces", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "possibleTypes", + "description": null, + "args": [], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "enumValues", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__EnumValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "inputFields", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ofType", + "description": null, + "args": [], + "type": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "specifiedByURL", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__TypeKind", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "SCALAR", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "LIST", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "NON_NULL", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Field", + "description": null, + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__InputValue", + "description": null, + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "type", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__Type", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "defaultValue", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__EnumValue", + "description": null, + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isDeprecated", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "deprecationReason", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "__Directive", + "description": null, + "fields": [ + { + "name": "name", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "description", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "locations", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "ENUM", + "name": "__DirectiveLocation", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "args", + "description": null, + "args": [ + { + "name": "includeDeprecated", + "description": null, + "type": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + }, + "defaultValue": "false", + "isDeprecated": false, + "deprecationReason": null + } + ], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "LIST", + "name": null, + "ofType": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "OBJECT", + "name": "__InputValue", + "ofType": null + } + } + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "isRepeatable", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "ENUM", + "name": "__DirectiveLocation", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": [ + { + "name": "QUERY", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "MUTATION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SUBSCRIPTION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_DEFINITION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FRAGMENT_SPREAD", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INLINE_FRAGMENT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "VARIABLE_DEFINITION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCHEMA", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "SCALAR", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "OBJECT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "FIELD_DEFINITION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ARGUMENT_DEFINITION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INTERFACE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "UNION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "ENUM_VALUE", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_OBJECT", + "description": null, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "INPUT_FIELD_DEFINITION", + "description": null, + "isDeprecated": false, + "deprecationReason": null + } + ], + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Int", + "description": "The `Int` scalar type represents non-fractional signed whole numeric values. Int\ncan represent values between -(2^31) and 2^31 - 1.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Float", + "description": "The `Float` scalar type represents signed double-precision fractional values as\nspecified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point).", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "String", + "description": "The `String` scalar type represents textual data, represented as UTF-8 character\nsequences. The String type is most often used by GraphQL to represent free-form\nhuman-readable text.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "Boolean", + "description": "The `Boolean` scalar type represents `true` or `false`.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "SCALAR", + "name": "ID", + "description": "The `ID` scalar type represents a unique identifier, often used to refetch an\nobject or as key for a cache. The ID type appears in a JSON response as a\nString; however, it is not intended to be human-readable. When expected as an\ninput type, any string (such as `\\\"4\\\"`) or integer (such as `4`) input value\nwill be accepted as an ID.", + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", + "name": "Query", + "description": null, + "fields": [ + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "int", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + } + }, + "isDeprecated": true, + "deprecationReason": "…" + }, + { + "name": "url", + "description": null, + "args": [], + "type": { + "kind": "SCALAR", + "name": "Url", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + { + "kind": "INTERFACE", + "name": "I", + "ofType": null + } + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "INTERFACE", + "name": "I", + "description": null, + "fields": [ + { + "name": "id", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "ID", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [], + "enumValues": null, + "possibleTypes": [ + { + "kind": "OBJECT", + "name": "Query", + "ofType": null + } + ] + }, + { + "kind": "SCALAR", + "name": "Url", + "description": null, + "fields": null, + "inputFields": null, + "interfaces": null, + "enumValues": null, + "possibleTypes": null + } + ], + "directives": [ + { + "name": "skip", + "description": "Directs the executor to skip this field or fragment when the `if` argument is true.", + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Skipped when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ] + }, + { + "name": "include", + "description": "Directs the executor to include this field or fragment only when the `if` argument is true.", + "locations": [ + "FIELD", + "FRAGMENT_SPREAD", + "INLINE_FRAGMENT" + ], + "args": [ + { + "name": "if", + "description": "Included when true.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "Boolean", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ] + }, + { + "name": "deprecated", + "description": "Marks an element of a GraphQL schema as no longer supported.", + "locations": [ + "FIELD_DEFINITION", + "ARGUMENT_DEFINITION", + "INPUT_FIELD_DEFINITION", + "ENUM_VALUE" + ], + "args": [ + { + "name": "reason", + "description": "Explains why this element was deprecated, usually also including a\nsuggestion for how to access supported similar data. Formatted using\nthe Markdown syntax, as specified by\n[CommonMark](https://commonmark.org/).", + "type": { + "kind": "SCALAR", + "name": "String", + "ofType": null + }, + "defaultValue": "\"No longer supported\"", + "isDeprecated": false, + "deprecationReason": null + } + ] + }, + { + "name": "specifiedBy", + "description": "Exposes a URL that specifies the behaviour of this scalar.", + "locations": [ + "SCALAR" + ], + "args": [ + { + "name": "url", + "description": "The URL that specifies the behaviour of this scalar.", + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "defaultValue": null, + "isDeprecated": false, + "deprecationReason": null + } + ] + } + ] + } + } +} \ No newline at end of file diff --git a/crates/apollo-compiler/tests/introspection.rs b/crates/apollo-compiler/tests/introspection.rs new file mode 100644 index 000000000..7b864e462 --- /dev/null +++ b/crates/apollo-compiler/tests/introspection.rs @@ -0,0 +1,138 @@ +use apollo_compiler::execution::coerce_variable_values; +use apollo_compiler::execution::JsonMap; +use apollo_compiler::execution::Response; +use apollo_compiler::execution::SchemaIntrospection; +use apollo_compiler::ExecutableDocument; +use apollo_compiler::Schema; +use expect_test::expect; +use expect_test::expect_file; + +#[test] +fn test() { + let schema = r#" + type Query implements I { + id: ID! + int: Int! @deprecated(reason: "…") + url: Url + } + + interface I { + id: ID! + } + + scalar Url @specifiedBy(url: "https://url.spec.whatwg.org/") + "#; + let schema = Schema::parse_and_validate(schema, "schema.graphql").unwrap(); + + let introspect = |query, variables: JsonMap| { + let document = + ExecutableDocument::parse_and_validate(&schema, query, "query.graphql").unwrap(); + let variables = coerce_variable_values(&schema, &document, None, &variables).unwrap(); + let response = SchemaIntrospection::execute_with( + &schema, + &document, + None, + &variables, + |non_introspection_document| { + Ok(Response { + errors: Default::default(), + data: apollo_compiler::execution::ResponseData::Object(Default::default()), + extensions: [( + "NON_INTROSPECTION".into(), + non_introspection_document + .serialize() + .no_indent() + .to_string() + .into(), + )] + .into_iter() + .collect(), + }) + }, + ) + .unwrap(); + serde_json::to_string_pretty(&response).unwrap() + }; + + let query = r#" + query WithVarible($verbose: Boolean!) { + I: __type(name: "I") { + possibleTypes { + name + fields @skip(if: $verbose) { + name + } + verboseFields: fields(includeDeprecated: true) @include(if: $verbose) { + name + deprecationReason + } + } + } + Url: __type(name: "Url") @include(if: $verbose) { + specifiedByURL + } + } + "#; + let expected = expect!([r#" + { + "data": { + "I": { + "possibleTypes": [ + { + "name": "Query", + "fields": [ + { + "name": "id" + }, + { + "name": "url" + } + ] + } + ] + } + } + }"#]); + let variables = [("verbose".into(), false.into())].into_iter().collect(); + let response = introspect(query, variables); + expected.assert_eq(&response); + + let variables = [("verbose".into(), true.into())].into_iter().collect(); + let response = introspect(query, variables); + let expected = expect!([r#" + { + "data": { + "I": { + "possibleTypes": [ + { + "name": "Query", + "verboseFields": [ + { + "name": "id", + "deprecationReason": null + }, + { + "name": "int", + "deprecationReason": "…" + }, + { + "name": "url", + "deprecationReason": null + } + ] + } + ] + }, + "Url": { + "specifiedByURL": "https://url.spec.whatwg.org/" + } + } + }"#]); + expected.assert_eq(&response); + + let response = introspect( + include_str!("../test_data/introspection/introspect_full_schema.graphql"), + Default::default(), + ); + expect_file!("../test_data/introspection/response_full.json").assert_eq(&response); +} diff --git a/crates/apollo-compiler/tests/main.rs b/crates/apollo-compiler/tests/main.rs index b5060e10b..88032e717 100644 --- a/crates/apollo-compiler/tests/main.rs +++ b/crates/apollo-compiler/tests/main.rs @@ -2,6 +2,7 @@ mod executable; mod extensions; mod field_set; mod field_type; +mod introspection; mod merge_schemas; /// Formerly in src/lib.rs mod misc;