use anyhow::anyhow;
use anyhow::Error;
use graphql_tools::parser::minify_query_document;
use graphql_tools::parser::schema::InputObjectType;
use moka::sync::Cache;
use std::cmp::Ordering;
use std::collections::HashMap;
use std::collections::HashSet;
use graphql_tools::ast::{
visit_document, OperationTransformer, OperationVisitor, OperationVisitorContext, Transformed,
TransformedValue,
};
use graphql_tools::parser::parse_query;
use graphql_tools::parser::query::{
Definition, Directive, Document, Field, FragmentDefinition, Number, OperationDefinition,
Selection, SelectionSet, Text, Type, Value, VariableDefinition,
};
use graphql_tools::parser::schema::{Document as SchemaDocument, TypeDefinition};
struct SchemaCoordinatesContext<'a> {
pub schema_coordinates: HashSet<String>,
pub used_input_fields: HashSet<&'a str>,
pub input_values_provided: HashMap<String, usize>,
pub used_variables: HashSet<&'a str>,
pub variables_with_defaults: HashSet<&'a str>,
error: Option<Error>,
}
impl SchemaCoordinatesContext<'_> {
fn is_corrupted(&self) -> bool {
self.error.is_some()
}
}
pub fn collect_schema_coordinates(
document: &Document<'static, String>,
schema: &SchemaDocument<'static, String>,
) -> Result<HashSet<String>, Error> {
let mut ctx = SchemaCoordinatesContext {
schema_coordinates: HashSet::new(),
used_input_fields: HashSet::new(),
input_values_provided: HashMap::new(),
used_variables: HashSet::new(),
variables_with_defaults: HashSet::new(),
error: None,
};
let mut visit_context = OperationVisitorContext::new(document, schema);
let mut visitor = SchemaCoordinatesVisitor {
visited_input_object_types: HashSet::new(),
};
visit_document(&mut visitor, document, &mut visit_context, &mut ctx);
if let Some(error) = ctx.error {
Err(error)
} else {
for type_name in ctx.used_input_fields {
visitor.collect_nested_input_type(schema, type_name, &mut ctx.schema_coordinates);
}
Ok(ctx.schema_coordinates)
}
}
fn is_builtin_scalar(type_name: &str) -> bool {
matches!(type_name, "String" | "Int" | "Float" | "Boolean" | "ID")
}
fn mark_as_used(ctx: &mut SchemaCoordinatesContext, id: &str) {
if let Some(count) = ctx.input_values_provided.get_mut(id) {
if *count > 0 {
*count -= 1;
ctx.schema_coordinates.insert(format!("{}!", id));
}
}
ctx.schema_coordinates.insert(id.to_string());
}
fn count_input_value_provided(ctx: &mut SchemaCoordinatesContext, id: &str) {
let counter = ctx.input_values_provided.entry(id.to_string()).or_insert(0);
*counter += 1;
}
fn value_exists(v: &Value<String>) -> bool {
!matches!(v, Value::Null)
}
struct SchemaCoordinatesVisitor<'a> {
visited_input_object_types: HashSet<&'a str>,
}
impl<'a> SchemaCoordinatesVisitor<'a> {
fn process_default_value(
info: &OperationVisitorContext<'a>,
ctx: &mut SchemaCoordinatesContext,
type_name: &str,
value: &Value<String>,
) {
match value {
Value::Object(obj) => {
if let Some(TypeDefinition::InputObject(input_obj)) =
info.schema.type_by_name(type_name)
{
for (field_name, field_value) in obj {
if let Some(field_def) =
input_obj.fields.iter().find(|f| &f.name == field_name)
{
let coordinate = format!("{}.{}", type_name, field_name);
ctx.schema_coordinates.insert(format!("{}!", coordinate));
ctx.schema_coordinates.insert(coordinate);
let field_type_name = Self::resolve_type_name(&field_def.value_type);
Self::process_default_value(info, ctx, field_type_name, field_value);
}
}
}
}
Value::List(values) => {
for val in values {
Self::process_default_value(info, ctx, type_name, val);
}
}
Value::Enum(enum_value) => {
let enum_coordinate = format!("{}.{}", type_name, enum_value);
ctx.schema_coordinates.insert(enum_coordinate);
}
_ => {
}
}
}
fn resolve_type_name(t: &'a Type<String>) -> &'a str {
match t {
Type::NamedType(value) => value.as_str(),
Type::ListType(t) => Self::resolve_type_name(t),
Type::NonNullType(t) => Self::resolve_type_name(t),
}
}
fn resolve_references(
&self,
schema: &'a SchemaDocument<'static, String>,
type_name: &'a str,
) -> Option<Vec<&'a str>> {
let mut visited_types = Vec::new();
Self::_resolve_references(schema, type_name, &mut visited_types);
Some(visited_types)
}
fn _resolve_references(
schema: &'a SchemaDocument<'static, String>,
type_name: &'a str,
visited_types: &mut Vec<&'a str>,
) {
if visited_types.contains(&type_name) {
return;
}
visited_types.push(type_name);
let named_type = schema.type_by_name(type_name);
if let Some(TypeDefinition::InputObject(input_type)) = named_type {
for field in &input_type.fields {
let field_type = Self::resolve_type_name(&field.value_type);
Self::_resolve_references(schema, field_type, visited_types);
}
}
}
fn collect_nested_input_type(
&mut self,
schema: &'a SchemaDocument<'static, String>,
input_type_name: &'a str,
coordinates: &mut HashSet<String>,
) {
if let Some(input_type_def) = schema.type_by_name(input_type_name) {
match input_type_def {
TypeDefinition::Scalar(scalar_def) => {
coordinates.insert(scalar_def.name.clone());
}
TypeDefinition::InputObject(nested_input_type) => {
self.collect_nested_input_fields(schema, nested_input_type, coordinates);
}
TypeDefinition::Enum(enum_type) => {
for value in &enum_type.values {
coordinates.insert(format!("{}.{}", enum_type.name, value.name));
}
}
_ => {}
}
} else if is_builtin_scalar(input_type_name) {
coordinates.insert(input_type_name.to_string());
}
}
fn collect_nested_input_fields(
&mut self,
schema: &'a SchemaDocument<'static, String>,
input_type: &'a InputObjectType<'static, String>,
coordinates: &mut HashSet<String>,
) {
if self
.visited_input_object_types
.contains(&input_type.name.as_str())
{
return;
}
self.visited_input_object_types
.insert(input_type.name.as_str());
for field in &input_type.fields {
let field_coordinate = format!("{}.{}", input_type.name, field.name);
coordinates.insert(field_coordinate);
let field_type_name = field.value_type.inner_type();
self.collect_nested_input_type(schema, field_type_name, coordinates);
}
}
}
impl<'a> OperationVisitor<'a, SchemaCoordinatesContext<'a>> for SchemaCoordinatesVisitor<'a> {
fn enter_variable_value(
&mut self,
_info: &mut OperationVisitorContext<'a>,
ctx: &mut SchemaCoordinatesContext<'a>,
name: &'a str,
) {
ctx.used_variables.insert(name);
}
fn enter_field(
&mut self,
info: &mut OperationVisitorContext<'a>,
ctx: &mut SchemaCoordinatesContext,
field: &Field<'static, String>,
) {
if ctx.is_corrupted() {
return;
}
let field_name = field.name.to_string();
if let Some(parent_type) = info.current_parent_type() {
let parent_name = parent_type.name();
ctx.schema_coordinates
.insert(format!("{}.{}", parent_name, field_name));
if let Some(field_def) = parent_type.field_by_name(&field_name) {
let field_output_type = info.schema.type_by_name(field_def.field_type.inner_type());
if let Some(TypeDefinition::Enum(enum_type)) = field_output_type {
for value in &enum_type.values {
ctx.schema_coordinates.insert(format!(
"{}.{}",
enum_type.name.as_str(),
value.name
));
}
}
}
} else {
ctx.error = Some(anyhow!(
"Unable to find parent type of '{}' field",
field.name
))
}
}
fn enter_variable_definition(
&mut self,
info: &mut OperationVisitorContext<'a>,
ctx: &mut SchemaCoordinatesContext<'a>,
var: &'a graphql_tools::static_graphql::query::VariableDefinition,
) {
if ctx.is_corrupted() {
return;
}
if var.default_value.is_some() {
ctx.variables_with_defaults.insert(var.name.as_str());
}
let type_name = Self::resolve_type_name(&var.var_type);
if let Some(inner_types) = self.resolve_references(info.schema, type_name) {
for inner_type in inner_types {
ctx.used_input_fields.insert(inner_type);
}
}
ctx.used_input_fields.insert(type_name);
if let Some(default_value) = &var.default_value {
Self::process_default_value(info, ctx, type_name, default_value);
}
}
fn enter_argument(
&mut self,
info: &mut OperationVisitorContext<'a>,
ctx: &mut SchemaCoordinatesContext<'a>,
arg: &(String, Value<'static, String>),
) {
if ctx.is_corrupted() {
return;
}
if info.current_parent_type().is_none() {
ctx.error = Some(anyhow!(
"Unable to find parent type of '{}' argument",
arg.0.clone()
));
return;
}
let parent_type = info.current_parent_type().unwrap();
let type_name = parent_type.name();
let field = info.current_field();
if let Some(field) = field {
let field_name = field.name.clone();
let (arg_name, arg_value) = arg;
let coordinate = format!("{type_name}.{field_name}.{arg_name}");
let has_value = match arg_value {
Value::Null => false,
Value::Variable(var_name) => {
ctx.variables_with_defaults.contains(var_name.as_str())
}
_ => true,
};
if has_value {
count_input_value_provided(ctx, &coordinate);
}
mark_as_used(ctx, &coordinate);
if let Some(field_def) = parent_type.field_by_name(&field_name) {
if let Some(arg_def) = field_def.arguments.iter().find(|a| &a.name == arg_name) {
let arg_type_name = Self::resolve_type_name(&arg_def.value_type);
match arg_value {
Value::Enum(value) => {
let value_str: String = value.to_string();
ctx.schema_coordinates
.insert(format!("{arg_type_name}.{value_str}").to_string());
}
Value::List(_) => {
}
Value::Object(_) => {
if let Some(TypeDefinition::Scalar(_)) =
info.schema.type_by_name(arg_type_name)
{
ctx.schema_coordinates.insert(arg_type_name.to_string());
}
}
Value::Variable(_) => {
}
_ => {
if is_builtin_scalar(arg_type_name) {
ctx.schema_coordinates.insert(arg_type_name.to_string());
} else if let Some(TypeDefinition::Scalar(_)) =
info.schema.type_by_name(arg_type_name)
{
ctx.schema_coordinates.insert(arg_type_name.to_string());
}
}
}
}
}
}
}
fn enter_list_value(
&mut self,
info: &mut OperationVisitorContext<'a>,
ctx: &mut SchemaCoordinatesContext,
values: &Vec<Value<'static, String>>,
) {
if ctx.is_corrupted() {
return;
}
if let Some(input_type) = info.current_input_type() {
let coordinate = input_type.name().to_string();
for value in values {
match value {
Value::Enum(value) => {
let value_str = value.to_string();
ctx.schema_coordinates
.insert(format!("{}.{}", coordinate, value_str));
}
Value::Object(_) => {
}
Value::List(_) => {
}
Value::Variable(_) => {
}
_ => {
if is_builtin_scalar(&coordinate) {
ctx.schema_coordinates.insert(coordinate.clone());
} else if let Some(TypeDefinition::Scalar(_)) =
info.schema.type_by_name(&coordinate)
{
ctx.schema_coordinates.insert(coordinate.clone());
}
}
}
}
}
}
fn enter_object_value(
&mut self,
info: &mut OperationVisitorContext<'a>,
ctx: &mut SchemaCoordinatesContext,
object_value: &[(String, graphql_tools::static_graphql::query::Value)],
) {
if let Some(TypeDefinition::InputObject(input_object_def)) = info.current_input_type() {
object_value.iter().for_each(|(name, value)| {
if let Some(field) = input_object_def
.fields
.iter()
.find(|field| field.name.eq(name))
{
let coordinate = format!("{}.{}", input_object_def.name, field.name);
let has_value = match value {
Value::Variable(var_name) => {
ctx.variables_with_defaults.contains(var_name.as_str())
}
_ => value_exists(value),
};
ctx.schema_coordinates.insert(coordinate.clone());
if has_value {
ctx.schema_coordinates.insert(format!("{coordinate}!"));
}
mark_as_used(ctx, &coordinate);
let field_type_name = field.value_type.inner_type();
match value {
Value::Enum(value) => {
let value_str = value.to_string();
ctx.schema_coordinates
.insert(format!("{field_type_name}.{value_str}").to_string());
}
Value::List(_) => {
}
Value::Object(_) => {
if let Some(TypeDefinition::Scalar(_)) =
info.schema.type_by_name(field_type_name)
{
ctx.schema_coordinates.insert(field_type_name.to_string());
}
}
Value::Variable(_) => {
if is_builtin_scalar(field_type_name) {
ctx.schema_coordinates.insert(field_type_name.to_string());
} else if let Some(TypeDefinition::Scalar(_)) =
info.schema.type_by_name(field_type_name)
{
ctx.schema_coordinates.insert(field_type_name.to_string());
}
}
Value::Null => {
if let Some(TypeDefinition::InputObject(nested_input_obj)) =
info.schema.type_by_name(field_type_name)
{
self.collect_nested_input_fields(
info.schema,
nested_input_obj,
&mut ctx.schema_coordinates,
);
}
}
_ => {
if is_builtin_scalar(field_type_name) {
ctx.schema_coordinates.insert(field_type_name.to_string());
} else if let Some(TypeDefinition::Scalar(_)) =
info.schema.type_by_name(field_type_name)
{
ctx.schema_coordinates.insert(field_type_name.to_string());
}
}
}
}
});
}
}
}
struct StripLiteralsTransformer {}
impl<'a, T: Text<'a> + Clone> OperationTransformer<'a, T> for StripLiteralsTransformer {
fn transform_value(&mut self, node: &Value<'a, T>) -> TransformedValue<Value<'a, T>> {
match node {
Value::Float(_) => TransformedValue::Replace(Value::Float(0.0)),
Value::Int(_) => TransformedValue::Replace(Value::Int(Number::from(0))),
Value::String(_) => TransformedValue::Replace(Value::String(String::from(""))),
Value::Variable(_) => TransformedValue::Keep,
Value::Boolean(_) => TransformedValue::Keep,
Value::Null => TransformedValue::Keep,
Value::Enum(_) => TransformedValue::Keep,
Value::List(val) => {
let items: Vec<Value<'a, T>> = val
.iter()
.map(|item| self.transform_value(item).replace_or_else(|| item.clone()))
.collect();
TransformedValue::Replace(Value::List(items))
}
Value::Object(fields) => {
let fields: Vec<(T::Value, Value<'a, T>)> = fields
.iter()
.map(|field| {
let (name, value) = field;
let new_value = self
.transform_value(value)
.replace_or_else(|| value.clone());
(name.clone(), new_value)
})
.collect();
TransformedValue::Replace(Value::Object(fields))
}
}
}
fn transform_field(
&mut self,
field: &graphql_tools::parser::query::Field<'a, T>,
) -> Transformed<graphql_tools::parser::query::Selection<'a, T>> {
let selection_set = self.transform_selection_set(&field.selection_set);
let arguments = self.transform_arguments(&field.arguments);
let directives = self.transform_directives(&field.directives);
Transformed::Replace(Selection::Field(Field {
arguments: arguments.replace_or_else(|| field.arguments.clone()),
directives: directives.replace_or_else(|| field.directives.clone()),
selection_set: SelectionSet {
items: selection_set.replace_or_else(|| field.selection_set.items.clone()),
span: field.selection_set.span,
},
position: field.position,
alias: None,
name: field.name.clone(),
}))
}
}
#[derive(Hash, Eq, PartialEq, Clone, Copy)]
pub struct PointerAddress(usize);
impl PointerAddress {
pub fn new<T>(ptr: &T) -> Self {
let ptr_address: usize = unsafe { std::mem::transmute(ptr) };
Self(ptr_address)
}
}
type Seen<'s, T> = HashMap<PointerAddress, Transformed<Selection<'s, T>>>;
pub struct SortSelectionsTransform<'s, T: Text<'s> + Clone> {
seen: Seen<'s, T>,
}
impl<'s, T: Text<'s> + Clone> Default for SortSelectionsTransform<'s, T> {
fn default() -> Self {
Self::new()
}
}
impl<'s, T: Text<'s> + Clone> SortSelectionsTransform<'s, T> {
pub fn new() -> Self {
Self {
seen: Default::default(),
}
}
}
impl<'s, T: Text<'s> + Clone> OperationTransformer<'s, T> for SortSelectionsTransform<'s, T> {
fn transform_document(
&mut self,
document: &Document<'s, T>,
) -> TransformedValue<Document<'s, T>> {
let mut next_definitions = self
.transform_list(&document.definitions, Self::transform_definition)
.replace_or_else(|| document.definitions.to_vec());
next_definitions.sort_unstable_by(|a, b| self.compare_definitions(a, b));
TransformedValue::Replace(Document {
definitions: next_definitions,
})
}
fn transform_selection_set(
&mut self,
selections: &SelectionSet<'s, T>,
) -> TransformedValue<Vec<Selection<'s, T>>> {
let mut next_selections = self
.transform_list(&selections.items, Self::transform_selection)
.replace_or_else(|| selections.items.to_vec());
next_selections.sort_unstable_by(|a, b| self.compare_selections(a, b));
TransformedValue::Replace(next_selections)
}
fn transform_directives(
&mut self,
directives: &[Directive<'s, T>],
) -> TransformedValue<Vec<Directive<'s, T>>> {
let mut next_directives = self
.transform_list(directives, Self::transform_directive)
.replace_or_else(|| directives.to_vec());
next_directives.sort_unstable_by(|a, b| self.compare_directives(a, b));
TransformedValue::Replace(next_directives)
}
fn transform_arguments(
&mut self,
arguments: &[(T::Value, Value<'s, T>)],
) -> TransformedValue<Vec<(T::Value, Value<'s, T>)>> {
let mut next_arguments = self
.transform_list(arguments, Self::transform_argument)
.replace_or_else(|| arguments.to_vec());
next_arguments.sort_unstable_by(|a, b| self.compare_arguments(a, b));
TransformedValue::Replace(next_arguments)
}
fn transform_variable_definitions(
&mut self,
variable_definitions: &Vec<VariableDefinition<'s, T>>,
) -> TransformedValue<Vec<VariableDefinition<'s, T>>> {
let mut next_variable_definitions = self
.transform_list(variable_definitions, Self::transform_variable_definition)
.replace_or_else(|| variable_definitions.to_vec());
next_variable_definitions.sort_unstable_by(|a, b| self.compare_variable_definitions(a, b));
TransformedValue::Replace(next_variable_definitions)
}
fn transform_fragment(
&mut self,
fragment: &FragmentDefinition<'s, T>,
) -> Transformed<FragmentDefinition<'s, T>> {
let mut directives = fragment.directives.clone();
directives.sort_unstable_by_key(|var| var.name.clone());
let selections = self.transform_selection_set(&fragment.selection_set);
Transformed::Replace(FragmentDefinition {
selection_set: SelectionSet {
items: selections.replace_or_else(|| fragment.selection_set.items.clone()),
span: fragment.selection_set.span,
},
directives,
name: fragment.name.clone(),
position: fragment.position,
type_condition: fragment.type_condition.clone(),
})
}
fn transform_selection(
&mut self,
selection: &Selection<'s, T>,
) -> Transformed<Selection<'s, T>> {
match selection {
Selection::InlineFragment(selection) => {
let key = PointerAddress::new(selection);
if let Some(prev) = self.seen.get(&key) {
return prev.clone();
}
let transformed = self.transform_inline_fragment(selection);
self.seen.insert(key, transformed.clone());
transformed
}
Selection::Field(field) => {
let key = PointerAddress::new(field);
if let Some(prev) = self.seen.get(&key) {
return prev.clone();
}
let transformed = self.transform_field(field);
self.seen.insert(key, transformed.clone());
transformed
}
Selection::FragmentSpread(_) => Transformed::Keep,
}
}
}
impl<'s, T: Text<'s> + Clone> SortSelectionsTransform<'s, T> {
fn compare_definitions(&self, a: &Definition<'s, T>, b: &Definition<'s, T>) -> Ordering {
match (a, b) {
(Definition::Operation(_), Definition::Operation(_)) => Ordering::Equal,
(Definition::Fragment(a), Definition::Fragment(b)) => a.name.cmp(&b.name),
_ => definition_kind_ordering(a).cmp(&definition_kind_ordering(b)),
}
}
fn compare_selections(&self, a: &Selection<'s, T>, b: &Selection<'s, T>) -> Ordering {
match (a, b) {
(Selection::Field(a), Selection::Field(b)) => a.name.cmp(&b.name),
(Selection::FragmentSpread(a), Selection::FragmentSpread(b)) => {
a.fragment_name.cmp(&b.fragment_name)
}
_ => {
let a_ordering = selection_kind_ordering(a);
let b_ordering = selection_kind_ordering(b);
a_ordering.cmp(&b_ordering)
}
}
}
fn compare_directives(&self, a: &Directive<'s, T>, b: &Directive<'s, T>) -> Ordering {
a.name.cmp(&b.name)
}
fn compare_arguments(
&self,
a: &(T::Value, Value<'s, T>),
b: &(T::Value, Value<'s, T>),
) -> Ordering {
a.0.cmp(&b.0)
}
fn compare_variable_definitions(
&self,
a: &VariableDefinition<'s, T>,
b: &VariableDefinition<'s, T>,
) -> Ordering {
a.name.cmp(&b.name)
}
}
fn selection_kind_ordering<'s, T: Text<'s>>(selection: &Selection<'s, T>) -> u8 {
match selection {
Selection::FragmentSpread(_) => 1,
Selection::InlineFragment(_) => 2,
Selection::Field(_) => 3,
}
}
fn definition_kind_ordering<'a, T: Text<'a>>(definition: &Definition<'a, T>) -> u8 {
match definition {
Definition::Operation(_) => 1,
Definition::Fragment(_) => 2,
}
}
pub fn normalize_operation<'a>(operation_document: &Document<'a, String>) -> Document<'a, String> {
let mut strip_literals_transformer = StripLiteralsTransformer {};
let normalized = strip_literals_transformer
.transform_document(operation_document)
.replace_or_else(|| operation_document.clone());
SortSelectionsTransform::new()
.transform_document(&normalized)
.replace_or_else(|| normalized.clone())
}
#[derive(Clone)]
pub struct ProcessedOperation {
pub operation: String,
pub hash: String,
pub coordinates: Vec<String>,
}
pub struct OperationProcessor {
cache: Cache<String, Option<ProcessedOperation>>,
}
impl Default for OperationProcessor {
fn default() -> Self {
Self::new()
}
}
impl OperationProcessor {
pub fn new() -> OperationProcessor {
OperationProcessor {
cache: Cache::new(1000),
}
}
pub fn process(
&self,
query: &str,
schema: &SchemaDocument<'static, String>,
) -> Result<Option<ProcessedOperation>, String> {
if self.cache.contains_key(query) {
let entry = self
.cache
.get(query)
.expect("Unable to acquire Cache in OperationProcessor.process");
Ok(entry.clone())
} else {
let result = self.transform(query, schema)?;
self.cache.insert(query.to_string(), result.clone());
Ok(result)
}
}
fn transform(
&self,
operation: &str,
schema: &SchemaDocument<'static, String>,
) -> Result<Option<ProcessedOperation>, String> {
let parsed = parse_query(operation)
.map_err(|e| e.to_string())?
.into_static();
let is_introspection = parsed.definitions.iter().find(|def| match def {
Definition::Operation(OperationDefinition::Query(query)) => query
.selection_set
.items
.iter()
.any(|selection| match selection {
Selection::Field(field) => field.name == "__schema" || field.name == "__type",
_ => false,
}),
_ => false,
});
if is_introspection.is_some() {
return Ok(None);
}
let schema_coordinates_result =
collect_schema_coordinates(&parsed, schema).map_err(|e| e.to_string())?;
let schema_coordinates: Vec<String> = Vec::from_iter(schema_coordinates_result);
let normalized = normalize_operation(&parsed);
let printed = minify_query_document(&normalized);
let hash = format!("{:x}", md5::compute(printed.clone()));
Ok(Some(ProcessedOperation {
operation: printed,
hash,
coordinates: schema_coordinates,
}))
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use graphql_tools::parser::parse_query;
use graphql_tools::parser::parse_schema;
use super::collect_schema_coordinates;
const SCHEMA_SDL: &str = "
type Query {
project(selector: ProjectSelectorInput!): Project
projectsByType(type: ProjectType!): [Project!]!
projectsByTypes(types: [ ProjectType!]!): [Project!]!
projects(filter: FilterInput, and: [FilterInput!]): [Project!]!
projectsByMetadata(metadata: JSON): [Project!]!
}
type Mutation {
deleteProject(selector: ProjectSelectorInput!): DeleteProjectPayload!
}
input ProjectSelectorInput {
organization: ID!
project: ID!
}
input FilterInput {
type: ProjectType
pagination: PaginationInput
order: [ProjectOrderByInput!]
metadata: JSON
}
input PaginationInput {
limit: Int
offset: Int
}
input ProjectOrderByInput {
field: String!
direction: OrderDirection
}
enum OrderDirection {
ASC
DESC
}
type ProjectSelector {
organization: ID!
project: ID!
}
type DeleteProjectPayload {
selector: ProjectSelector!
deletedProject: Project!
}
type Project {
id: ID!
cleanId: ID!
name: String!
type: ProjectType!
buildUrl: String
validationUrl: String
}
enum ProjectType {
FEDERATION
STITCHING
SINGLE
}
scalar JSON
";
#[test]
fn basic_test() {
let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
let document = parse_query::<String>(
"
mutation deleteProjectOperation($selector: ProjectSelectorInput!) {
deleteProject(selector: $selector) {
selector {
organization
project
}
deletedProject {
...ProjectFields
}
}
}
fragment ProjectFields on Project {
id
cleanId
name
type
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"Mutation.deleteProject",
"Mutation.deleteProject.selector",
"DeleteProjectPayload.selector",
"ProjectSelector.organization",
"ProjectSelector.project",
"DeleteProjectPayload.deletedProject",
"ID",
"Project.id",
"Project.cleanId",
"Project.name",
"Project.type",
"ProjectType.FEDERATION",
"ProjectType.STITCHING",
"ProjectType.SINGLE",
"ProjectSelectorInput.organization",
"ProjectSelectorInput.project",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn entire_input() {
let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
let document = parse_query::<String>(
"
query projects($filter: FilterInput) {
projects(filter: $filter) {
name
}
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"Query.projects",
"Query.projects.filter",
"Project.name",
"FilterInput.type",
"ProjectType.FEDERATION",
"ProjectType.STITCHING",
"ProjectType.SINGLE",
"FilterInput.pagination",
"PaginationInput.limit",
"Int",
"PaginationInput.offset",
"FilterInput.metadata",
"FilterInput.order",
"ProjectOrderByInput.field",
"String",
"ProjectOrderByInput.direction",
"OrderDirection.ASC",
"OrderDirection.DESC",
"JSON",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn entire_input_list() {
let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
let document = parse_query::<String>(
"
query projects($filter: FilterInput) {
projects(and: $filter) {
name
}
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"Query.projects",
"Query.projects.and",
"Project.name",
"FilterInput.type",
"ProjectType.FEDERATION",
"ProjectType.STITCHING",
"ProjectType.SINGLE",
"FilterInput.pagination",
"FilterInput.metadata",
"PaginationInput.limit",
"Int",
"PaginationInput.offset",
"FilterInput.order",
"ProjectOrderByInput.field",
"String",
"ProjectOrderByInput.direction",
"OrderDirection.ASC",
"OrderDirection.DESC",
"JSON",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn entire_input_and_enum_value() {
let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
let document = parse_query::<String>(
"
query getProjects($pagination: PaginationInput) {
projects(and: { pagination: $pagination, type: FEDERATION }) {
name
}
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"Query.projects",
"Query.projects.and",
"Query.projects.and!",
"Project.name",
"PaginationInput.limit",
"Int",
"PaginationInput.offset",
"FilterInput.pagination",
"FilterInput.type",
"FilterInput.type!",
"ProjectType.FEDERATION",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn enum_value_list() {
let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
let document = parse_query::<String>(
"
query getProjects {
projectsByTypes(types: [FEDERATION, STITCHING]) {
name
}
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"Query.projectsByTypes",
"Query.projectsByTypes.types",
"Query.projectsByTypes.types!",
"Project.name",
"ProjectType.FEDERATION",
"ProjectType.STITCHING",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn enums_and_scalars_input() {
let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
let document = parse_query::<String>(
"
query getProjects($limit: Int!, $type: ProjectType!) {
projects(filter: { pagination: { limit: $limit }, type: $type }) {
id
}
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"Query.projects",
"Query.projects.filter",
"Query.projects.filter!",
"Project.id",
"Int",
"ProjectType.FEDERATION",
"ProjectType.STITCHING",
"ProjectType.SINGLE",
"FilterInput.pagination",
"FilterInput.pagination!",
"FilterInput.type",
"PaginationInput.limit",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn hard_coded_scalars_input() {
let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
let document = parse_query::<String>(
"
{
projects(filter: { pagination: { limit: 20 } }) {
id
}
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"Query.projects",
"Query.projects.filter",
"Query.projects.filter!",
"Project.id",
"FilterInput.pagination",
"FilterInput.pagination!",
"Int",
"PaginationInput.limit",
"PaginationInput.limit!",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn enum_values_object_field() {
let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
let document = parse_query::<String>(
"
query getProjects($limit: Int!) {
projects(filter: { pagination: { limit: $limit }, type: FEDERATION }) {
id
}
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"Query.projects",
"Query.projects.filter",
"Query.projects.filter!",
"Project.id",
"Int",
"FilterInput.pagination",
"FilterInput.pagination!",
"FilterInput.type",
"FilterInput.type!",
"PaginationInput.limit",
"ProjectType.FEDERATION",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn enum_list_inline() {
let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
let document = parse_query::<String>(
"
query getProjects {
projectsByTypes(types: [FEDERATION]) {
id
}
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"Query.projectsByTypes",
"Query.projectsByTypes.types",
"Query.projectsByTypes.types!",
"Project.id",
"ProjectType.FEDERATION",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn enum_list_variable() {
let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
let document_inline = parse_query::<String>(
"
query getProjects($types: [ProjectType!]!) {
projectsByTypes(types: $types) {
id
}
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document_inline, &schema).unwrap();
let expected = vec![
"Query.projectsByTypes",
"Query.projectsByTypes.types",
"Project.id",
"ProjectType.FEDERATION",
"ProjectType.STITCHING",
"ProjectType.SINGLE",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn enum_values_argument() {
let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
let document = parse_query::<String>(
"
query getProjects {
projectsByType(type: FEDERATION) {
id
}
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"Query.projectsByType",
"Query.projectsByType.type",
"Query.projectsByType.type!",
"Project.id",
"ProjectType.FEDERATION",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn arguments() {
let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
let document = parse_query::<String>(
"
query getProjects($limit: Int!, $type: ProjectType!) {
projects(filter: { pagination: { limit: $limit }, type: $type }) {
id
}
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"Query.projects",
"Query.projects.filter",
"Query.projects.filter!",
"Project.id",
"Int",
"ProjectType.FEDERATION",
"ProjectType.STITCHING",
"ProjectType.SINGLE",
"FilterInput.pagination",
"FilterInput.pagination!",
"FilterInput.type",
"PaginationInput.limit",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn skips_argument_directives() {
let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
let document = parse_query::<String>(
"
query getProjects($limit: Int!, $type: ProjectType!, $includeName: Boolean!) {
projects(filter: { pagination: { limit: $limit }, type: $type }) {
id
...NestedFragment
}
}
fragment NestedFragment on Project {
...IncludeNameFragment @include(if: $includeName)
}
fragment IncludeNameFragment on Project {
name
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"Query.projects",
"Query.projects.filter",
"Query.projects.filter!",
"Project.id",
"Project.name",
"Int",
"ProjectType.FEDERATION",
"ProjectType.STITCHING",
"ProjectType.SINGLE",
"Boolean",
"FilterInput.pagination",
"FilterInput.pagination!",
"FilterInput.type",
"PaginationInput.limit",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn used_only_input_fields() {
let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
let document = parse_query::<String>(
"
query getProjects($limit: Int!, $type: ProjectType!) {
projects(filter: {
pagination: { limit: $limit },
type: $type
}) {
id
}
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"Query.projects",
"Query.projects.filter",
"Query.projects.filter!",
"Project.id",
"Int",
"ProjectType.FEDERATION",
"ProjectType.STITCHING",
"ProjectType.SINGLE",
"FilterInput.pagination",
"FilterInput.pagination!",
"FilterInput.type",
"PaginationInput.limit",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn input_object_mixed() {
let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
let document = parse_query::<String>(
"
query getProjects($pagination: PaginationInput!, $type: ProjectType!) {
projects(filter: { pagination: $pagination, type: $type }) {
id
}
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"Query.projects",
"Query.projects.filter",
"Query.projects.filter!",
"Project.id",
"PaginationInput.limit",
"Int",
"PaginationInput.offset",
"ProjectType.FEDERATION",
"ProjectType.STITCHING",
"ProjectType.SINGLE",
"FilterInput.pagination",
"FilterInput.type",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn custom_scalar_as_argument_inlined() {
let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
let document = parse_query::<String>(
"
query getProjects {
projectsByMetadata(metadata: { key: { value: \"value\" } }) {
name
}
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"Query.projectsByMetadata",
"Query.projectsByMetadata.metadata",
"Query.projectsByMetadata.metadata!",
"Project.name",
"JSON",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn custom_scalar_as_argument_variable() {
let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
let document = parse_query::<String>(
"
query getProjects($metadata: JSON) {
projectsByMetadata(metadata: $metadata) {
name
}
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"Query.projectsByMetadata",
"Query.projectsByMetadata.metadata",
"Project.name",
"JSON",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn custom_scalar_as_argument_variable_with_default() {
let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
let document = parse_query::<String>(
"
query getProjects($metadata: JSON = { key: { value: \"value\" } }) {
projectsByMetadata(metadata: $metadata) {
name
}
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"Query.projectsByMetadata",
"Query.projectsByMetadata.metadata",
"Query.projectsByMetadata.metadata!",
"Project.name",
"JSON",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn custom_scalar_as_input_field_inlined() {
let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
let document = parse_query::<String>(
"
query getProjects {
projects(filter: { metadata: { key: \"value\" } }) {
name
}
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"Query.projects",
"Query.projects.filter",
"Query.projects.filter!",
"FilterInput.metadata",
"FilterInput.metadata!",
"Project.name",
"JSON",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn custom_scalar_as_input_field_variable() {
let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
let document = parse_query::<String>(
"
query getProjects($metadata: JSON) {
projects(filter: { metadata: $metadata }) {
name
}
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"Query.projects",
"Query.projects.filter",
"Query.projects.filter!",
"FilterInput.metadata",
"Project.name",
"JSON",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn custom_scalar_as_input_field_variable_with_default() {
let schema = parse_schema::<String>(SCHEMA_SDL).unwrap();
let document = parse_query::<String>(
"
query getProjects($metadata: JSON = { key: { value: \"value\" } }) {
projects(filter: { metadata: $metadata }) {
name
}
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"Query.projects",
"Query.projects.filter",
"Query.projects.filter!",
"FilterInput.metadata",
"FilterInput.metadata!",
"Project.name",
"JSON",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn primitive_field_with_arg_schema_coor() {
let schema = parse_schema::<String>(
"type Query {
hello(message: String): String
}",
)
.unwrap();
let document = parse_query::<String>(
"
query {
hello(message: \"world\")
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"Query.hello",
"Query.hello.message!",
"Query.hello.message",
"String",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn unused_variable_as_nullable_argument() {
let schema = parse_schema::<String>(
"
type Query {
random(a: String): String
}
",
)
.unwrap();
let document = parse_query::<String>(
"
query Foo($a: String) {
random(a: $a)
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec!["Query.random", "Query.random.a", "String"]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn unused_nullable_input_field() {
let schema = parse_schema::<String>(
"
type Query {
random(a: A): String
}
input A {
b: B
}
input B {
c: C
}
input C {
d: String
}
",
)
.unwrap();
let document = parse_query::<String>(
"
query Foo {
random(a: { b: null })
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"Query.random",
"Query.random.a",
"Query.random.a!",
"A.b",
"B.c",
"C.d",
"String",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn required_variable_as_input_field() {
let schema = parse_schema::<String>(
"
type Query {
random(a: A): String
}
input A {
b: String
}
",
)
.unwrap();
let document = parse_query::<String>(
"
query Foo($b:String! = \"b\") {
random(a: { b: $b })
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"Query.random",
"Query.random.a",
"Query.random.a!",
"A.b",
"A.b!",
"String",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn undefined_variable_as_input_field() {
let schema = parse_schema::<String>(
"
type Query {
random(a: A): String
}
input A {
b: String
}
",
)
.unwrap();
let document = parse_query::<String>(
"
query Foo($b: String!) {
random(a: { b: $b })
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"Query.random",
"Query.random.a",
"Query.random.a!",
"A.b",
"String",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn deeply_nested_variables() {
let schema = parse_schema::<String>(
"
type Query {
random(a: A): String
}
input A {
b: B
}
input B {
c: C
}
input C {
d: String
}
",
)
.unwrap();
let document = parse_query::<String>(
"
query Random($a: A = { b: { c: { d: \"D\" } } }) {
random(a: $a)
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"Query.random",
"Query.random.a",
"Query.random.a!",
"A.b",
"A.b!",
"B.c",
"B.c!",
"C.d",
"C.d!",
"String",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn aliased_field() {
let schema = parse_schema::<String>(
"
type Query {
random(a: String): String
}
input C {
d: String
}
",
)
.unwrap();
let document = parse_query::<String>(
"
query Random($a: String= \"B\" ) {
foo: random(a: $a )
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"Query.random",
"Query.random.a",
"Query.random.a!",
"String",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn multiple_fields_with_mixed_nullability() {
let schema = parse_schema::<String>(
"
type Query {
random(a: String): String
}
input C {
d: String
}
",
)
.unwrap();
let document = parse_query::<String>(
"
query Random($a: String = null) {
nullable: random(a: $a)
nonnullable: random(a: \"B\")
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"Query.random",
"Query.random.a",
"Query.random.a!",
"String",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn nonnull_and_default_arguments() {
let schema = parse_schema::<String>(
"
type Query {
user(id: ID!, name: String): User
}
type User {
id: ID!
name: String
}
",
)
.unwrap();
let document = parse_query::<String>(
"
query($id: ID! = \"123\") {
user(id: $id) { name }
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"User.name",
"Query.user",
"ID",
"Query.user.id!",
"Query.user.id",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn default_nullable_arguments() {
let schema = parse_schema::<String>(
"
type Query {
user(id: ID!, name: String): User
}
type User {
id: ID!
name: String
}
",
)
.unwrap();
let document = parse_query::<String>(
"
query($name: String = \"John\") {
user(id: \"fixed\", name: $name) { id }
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"User.id",
"Query.user",
"ID",
"Query.user.id!",
"Query.user.id",
"Query.user.name!",
"Query.user.name",
"String",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn non_null_no_default_arguments() {
let schema = parse_schema::<String>(
"
type Query {
user(id: ID!, name: String): User
}
type User {
id: ID!
name: String
}
",
)
.unwrap();
let document = parse_query::<String>(
"
query($id: ID!) {
user(id: $id) { name }
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec!["User.name", "Query.user", "ID", "Query.user.id"]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn fixed_arguments() {
let schema = parse_schema::<String>(
"
type Query {
user(id: ID!, name: String): User
}
type User {
id: ID!
name: String
}
",
)
.unwrap();
let document = parse_query::<String>(
"
query($name: String) {
user(id: \"fixed\", name: $name) { id }
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"User.id",
"Query.user",
"ID",
"Query.user.id!",
"Query.user.id",
"Query.user.name",
"String",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn recursive_fragments() {
let schema = parse_schema::<String>(
"
type Query {
user(id: ID!): User
}
type User {
id: ID!
friends: [User!]!
}
",
)
.unwrap();
let document = parse_query::<String>(
"
query UserQuery($id: ID!) {
user(id: $id) {
...UserFragment
}
}
fragment UserFragment on User {
id
friends {
...UserFragment
}
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"Query.user",
"Query.user.id",
"User.id",
"User.friends",
"ID",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn recursive_input_types() {
let schema = parse_schema::<String>(
"
type Query {
node(id: ID!): Node
}
type Mutation {
createNode(input: NodeInput!): Node
}
input NodeInput {
name: String!
parent: NodeInput
}
type Node {
id: ID!
name: String!
parent: Node
}
",
)
.unwrap();
let document = parse_query::<String>(
"
mutation CreateNode($input: NodeInput!) {
createNode(input: $input) {
id
name
}
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"Mutation.createNode",
"Mutation.createNode.input",
"Node.id",
"Node.name",
"NodeInput.name",
"NodeInput.parent",
"String",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
#[test]
fn recursive_null_input() {
let schema = parse_schema::<String>(
"
type Query {
someField(input: RecursiveInput!): SomeType
}
input RecursiveInput {
field: String
nested: RecursiveInput
}
type SomeType {
id: ID!
}
",
)
.unwrap();
let document = parse_query::<String>(
"
query MyQuery {
someField(input: { nested: null }) { id }
}
",
)
.unwrap();
let schema_coordinates = collect_schema_coordinates(&document, &schema).unwrap();
let expected = vec![
"Query.someField",
"Query.someField.input",
"Query.someField.input!",
"SomeType.id",
"RecursiveInput.field",
"RecursiveInput.nested",
"String",
]
.into_iter()
.map(|s| s.to_string())
.collect::<HashSet<String>>();
let extra: Vec<&String> = schema_coordinates.difference(&expected).collect();
let missing: Vec<&String> = expected.difference(&schema_coordinates).collect();
assert_eq!(extra.len(), 0, "Extra: {:?}", extra);
assert_eq!(missing.len(), 0, "Missing: {:?}", missing);
}
}