diff --git a/async-graphql-derive/src/enum.rs b/async-graphql-derive/src/enum.rs index 8bb76139..2c2e15f1 100644 --- a/async-graphql-derive/src/enum.rs +++ b/async-graphql-derive/src/enum.rs @@ -118,7 +118,7 @@ pub fn generate(enum_args: &args::Enum, input: &DeriveInput) -> Result, _field: &#crate_name::Positioned<#crate_name::parser::types::Field>) -> #crate_name::Result<#crate_name::serde_json::Value> { - Ok(#crate_name::EnumType::to_value(self).into()) + Ok(#crate_name::EnumType::to_value(self).into_json().unwrap()) } } }; diff --git a/async-graphql-derive/src/input_object.rs b/async-graphql-derive/src/input_object.rs index f99da6fc..cc99f8bd 100644 --- a/async-graphql-derive/src/input_object.rs +++ b/async-graphql-derive/src/input_object.rs @@ -111,7 +111,10 @@ pub fn generate(object_args: &args::InputObject, input: &DeriveInput) -> Result< } put_fields.push(quote! { - map.insert(#name.to_string(), #crate_name::InputValueType::to_value(&self.#ident)); + map.insert( + #crate_name::parser::types::Name::new_unchecked(#name.to_owned()), + #crate_name::InputValueType::to_value(&self.#ident) + ); }); fields.push(ident); diff --git a/async-graphql-derive/src/scalar.rs b/async-graphql-derive/src/scalar.rs index a83c1f83..07683ff5 100644 --- a/async-graphql-derive/src/scalar.rs +++ b/async-graphql-derive/src/scalar.rs @@ -65,7 +65,7 @@ pub fn generate(scalar_args: &args::Scalar, item_impl: &mut ItemImpl) -> Result< _: &#crate_name::ContextSelectionSet<'_>, _field: &#crate_name::Positioned<#crate_name::parser::types::Field> ) -> #crate_name::Result<#crate_name::serde_json::Value> { - Ok(#crate_name::ScalarType::to_value(self).into()) + Ok(#crate_name::ScalarType::to_value(self).into_json().unwrap()) } } }; diff --git a/async-graphql-parser/src/graphql.pest b/async-graphql-parser/src/graphql.pest index a4d87de7..5fcc7fd3 100644 --- a/async-graphql-parser/src/graphql.pest +++ b/async-graphql-parser/src/graphql.pest @@ -2,28 +2,124 @@ WHITESPACE = _{ " " | "," | "\t" | "\u{feff}" | line_terminator } COMMENT = _{ "#" ~ (!line_terminator ~ ANY)* } line_terminator = @{ "\r\n" | "\r" | "\n" } -document = { SOI ~ definition+ ~ EOI } -definition = { operation_definition | fragment_definition } +// Executable // + +executable_document = { SOI ~ executable_definition+ ~ EOI } +executable_definition = { operation_definition | fragment_definition } operation_definition = { named_operation_definition | selection_set } named_operation_definition = { operation_type ~ name? ~ variable_definitions? ~ directives? ~ selection_set } -operation_type = { "query" | "mutation" | "subscription" } variable_definitions = { "(" ~ variable_definition* ~ ")" } variable_definition = { variable ~ ":" ~ type_ ~ default_value? } -variable = { "$" ~ name } -default_value = { "=" ~ value } + +selection_set = { "{" ~ selection+ ~ "}" } +selection = { field | inline_fragment | fragment_spread } +field = { alias? ~ name ~ arguments? ~ directives? ~ selection_set? } +alias = { name ~ ":" } +fragment_spread = { "..." ~ name ~ directives? } +inline_fragment = { "..." ~ type_condition? ~ directives? ~ selection_set } + +fragment_definition = { "fragment" ~ name ~ type_condition ~ directives? ~ selection_set } +type_condition = { "on" ~ name } + +// Service // + +service_document = { SOI ~ type_system_definition+ ~ EOI } +type_system_definition = { schema_definition | type_definition | directive_definition } + +schema_definition = { + "schema" ~ const_directives? ~ "{" ~ operation_type_definition+ ~ "}" + | extend ~ "schema" ~ (const_directives? ~ "{" ~ operation_type_definition+ ~ "}" | const_directives) +} +operation_type_definition = { operation_type ~ ":" ~ name } + +type_definition = { scalar_type | object_type | interface_type | union_type | enum_type | input_object_type } + +scalar_type = { + string? ~ "scalar" ~ name ~ const_directives? + | extend ~ "scalar" ~ name ~ const_directives +} + +object_type = { + string? ~ "type" ~ name ~ implements_interfaces? ~ const_directives? ~ fields_definition? + | extend ~ "type" ~ name ~ (implements_interfaces? ~ (const_directives? ~ fields_definition | const_directives) | implements_interfaces) +} +implements_interfaces = { "implements" ~ "&"? ~ name ~ ("&" ~ name)* } + +interface_type = { + string? ~ "interface" ~ name ~ const_directives? ~ fields_definition? + | extend ~ "interface" ~ name ~ (const_directives? ~ fields_definition | const_directives) +} + +fields_definition = { "{" ~ field_definition+ ~ "}" } +field_definition = { string? ~ name ~ arguments_definition? ~ ":" ~ type_ ~ const_directives? } + +union_type = { + string? ~ "union" ~ name ~ const_directives? ~ union_member_types? + | extend ~ "union" ~ name ~ (const_directives? ~ union_member_types | const_directives) +} +union_member_types = { "=" ~ "|"? ~ name ~ ("|" ~ name)* } + +enum_type = { + string? ~ "enum" ~ name ~ const_directives? ~ enum_values? + | extend ~ "enum" ~ name ~ (const_directives? ~ enum_values | const_directives) +} +enum_values = { "{" ~ enum_value_definition+ ~ "}" } +enum_value_definition = { string? ~ enum_value ~ const_directives? } + +input_object_type = { + string? ~ "input" ~ name ~ const_directives? ~ input_fields_definition? + | extend ~ "input" ~ name ~ (const_directives? ~ input_fields_definition | const_directives) +} +input_fields_definition = { "{" ~ input_value_definition+ ~ "}" } + +extend = { "extend" } + +directive_definition = { string? ~ "directive" ~ "@" ~ name ~ arguments_definition? ~ "on" ~ directive_locations } +directive_locations = { "|"? ~ directive_location ~ ("|" ~ directive_location)* } +directive_location = { + "QUERY" + | "MUTATION" + | "SUBSCRIPTION" + | "FIELD" + | "FRAGMENT_DEFINITION" + | "FRAGMENT_SPREAD" + | "INLINE_FRAGMENT" + | "SCHEMA" + | "SCALAR" + | "OBJECT" + | "FIELD_DEFINITION" + | "ARGUMENT_DEFINITION" + | "INTERFACE" + | "UNION" + | "ENUM" + | "ENUM_VALUE" + | "INPUT_OBJECT" + | "INPUT_FIELD_DEFINITION" +} + +arguments_definition = { "(" ~ input_value_definition+ ~ ")" } + +input_value_definition = { string? ~ name ~ ":" ~ type_ ~ default_value? ~ const_directives? } + +// Common // + +operation_type = { "query" | "mutation" | "subscription" } + +default_value = { "=" ~ const_value } type_ = @{ (name | "[" ~ type_ ~ "]") ~ "!"? } -// float must come before int; other than that order does not matter -value = { variable | float | int | string | boolean | null | enum_ | list | object } +const_value = { number | string | boolean | null | enum_value | const_list | const_object } +value = { variable | number | string | boolean | null | enum_value | list | object } -float = @{ int ~ ((fractional ~ exponent) | fractional | exponent) } +variable = { "$" ~ name } + +number = @{ float | int } +float = { int ~ ((fractional ~ exponent) | fractional | exponent) } fractional = { "." ~ ASCII_DIGIT+ } exponent = { ("E" | "e") ~ ("+" | "-")? ~ ASCII_DIGIT+ } - -int = @{ "-"? ~ ("0" | (ASCII_NONZERO_DIGIT ~ ASCII_DIGIT*)) } -boolean = { "true" | "false" } +int = { "-"? ~ ("0" | (ASCII_NONZERO_DIGIT ~ ASCII_DIGIT*)) } string = ${ ("\"\"\"" ~ block_string_content ~ "\"\"\"") | ("\"" ~ string_content ~ "\"") } block_string_content = @{ block_string_character* } @@ -43,27 +139,29 @@ string_character = { // slightly and disallow non-scalar value code points at the parsing level. unicode_scalar_value_hex = { !(^"d" ~ ('8'..'9' | 'a'..'f' | 'A'..'F')) ~ ASCII_HEX_DIGIT{4} } +boolean = { "true" | "false" } + null = { "null" } -enum_ = ${ !(boolean | null) ~ name } +enum_value = ${ !(boolean | null) ~ name } -list = { "[" ~ value* ~ "]" } +const_list = { "[" ~ const_value* ~ "]" } +list = { "[" ~ value* ~ "]" } -name_value = { name ~ ":" ~ value } -object = { "{" ~ name_value* ~ "}" } +const_object = { "{" ~ const_object_field* ~ "}" } +object = { "{" ~ object_field* ~ "}" } +const_object_field = { name ~ ":" ~ const_value } +object_field = { name ~ ":" ~ value } -selection_set = { "{" ~ selection+ ~ "}" } -selection = { field | inline_fragment | fragment_spread } -field = { alias? ~ name ~ arguments? ~ directives? ~ selection_set? } -alias = { name ~ ":" } -arguments = { "(" ~ name_value* ~ ")" } -fragment_spread = { "..." ~ name ~ directives? } -inline_fragment = { "..." ~ type_condition? ~ directives? ~ selection_set } -fragment_definition = { "fragment" ~ name ~ type_condition ~ directives? ~ selection_set } -type_condition = { "on" ~ name } +const_directives = { const_directive+ } +directives = { directive+ } +const_directive = { "@" ~ name ~ const_arguments? } +directive = { "@" ~ name ~ arguments? } -directives = { directive* } -directive = { "@" ~ name ~ arguments? } +const_arguments = { "(" ~ const_argument+ ~ ")" } +arguments = { "(" ~ argument+ ~ ")" } +const_argument = { name ~ ":" ~ const_value } +argument = { name ~ ":" ~ value } name = @{ (ASCII_ALPHA | "_") ~ (ASCII_ALPHA | ASCII_DIGIT | "_")* } diff --git a/async-graphql-parser/src/lib.rs b/async-graphql-parser/src/lib.rs index 4ac8aeb3..23db8667 100644 --- a/async-graphql-parser/src/lib.rs +++ b/async-graphql-parser/src/lib.rs @@ -9,14 +9,13 @@ use pest::error::LineColLocation; use pest::RuleType; use std::fmt; -pub use parser::parse_query; +pub use parse::{parse_query, parse_schema}; pub use pos::{Pos, Positioned}; pub mod types; -mod parser; +mod parse; mod pos; -mod utils; /// Parser error. #[derive(Debug, PartialEq)] @@ -27,6 +26,16 @@ pub struct Error { pub message: String, } +impl Error { + /// Create a new error with the given position and message. + pub fn new(message: impl Into, pos: Pos) -> Self { + Self { + pos, + message: message.into(), + } + } +} + impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(&self.message) diff --git a/async-graphql-parser/src/parse/executable.rs b/async-graphql-parser/src/parse/executable.rs new file mode 100644 index 00000000..368daa8b --- /dev/null +++ b/async-graphql-parser/src/parse/executable.rs @@ -0,0 +1,312 @@ +use super::*; + +/// Parse a GraphQL query document. +/// +/// # Errors +/// +/// Fails if the query is not a valid GraphQL document. +pub fn parse_query>(input: T) -> Result { + let mut pc = PositionCalculator::new(input.as_ref()); + Ok(parse_executable_document( + exactly_one(GraphQLParser::parse(Rule::executable_document, input.as_ref())?), + &mut pc, + )?) +} + +fn parse_executable_document(pair: Pair, pc: &mut PositionCalculator) -> Result { + debug_assert_eq!(pair.as_rule(), Rule::executable_document); + + Ok(ExecutableDocument { + definitions: pair + .into_inner() + .filter(|pair| pair.as_rule() != Rule::EOI) + .map(|pair| parse_executable_definition(pair, pc)) + .collect::>()?, + }) +} + +fn parse_executable_definition(pair: Pair, pc: &mut PositionCalculator) -> Result { + debug_assert_eq!(pair.as_rule(), Rule::executable_definition); + + let pair = exactly_one(pair.into_inner()); + Ok(match pair.as_rule() { + Rule::operation_definition => ExecutableDefinition::Operation(parse_operation_definition(pair, pc)?), + Rule::fragment_definition => ExecutableDefinition::Fragment(parse_fragment_definition(pair, pc)?), + _ => unreachable!(), + }) +} + +fn parse_operation_definition( + pair: Pair, + pc: &mut PositionCalculator, +) -> Result> { + debug_assert_eq!(pair.as_rule(), Rule::operation_definition); + + let pos = pc.step(&pair); + let pair = exactly_one(pair.into_inner()); + Ok(Positioned::new( + match pair.as_rule() { + Rule::named_operation_definition => parse_named_operation_definition(pair, pc)?, + Rule::selection_set => OperationDefinition { + ty: OperationType::Query, + name: None, + variable_definitions: Vec::new(), + directives: Vec::new(), + selection_set: parse_selection_set(pair, pc)?, + }, + _ => unreachable!(), + }, + pos, + )) +} + +fn parse_named_operation_definition( + pair: Pair, + pc: &mut PositionCalculator, +) -> Result { + debug_assert_eq!(pair.as_rule(), Rule::named_operation_definition); + + let mut pairs = pair.into_inner(); + + let ty = parse_operation_type(pairs.next().unwrap(), pc)?; + let name = parse_if_rule(&mut pairs, Rule::name, |pair| parse_name(pair, pc))?; + let variable_definitions = parse_if_rule(&mut pairs, Rule::variable_definitions, |pair| parse_variable_definitions(pair, pc))?; + let directives = parse_opt_directives(&mut pairs, pc)?; + let selection_set = parse_selection_set(pairs.next().unwrap(), pc)?; + + debug_assert_eq!(pairs.next(), None); + + Ok(OperationDefinition { + ty: ty.node, + name, + variable_definitions: variable_definitions.unwrap_or_default(), + directives, + selection_set, + }) +} + +fn parse_variable_definitions( + pair: Pair, + pc: &mut PositionCalculator, +) -> Result>> { + debug_assert_eq!(pair.as_rule(), Rule::variable_definitions); + + pair.into_inner() + .map(|pair| parse_variable_definition(pair, pc)) + .collect() +} + +fn parse_variable_definition( + pair: Pair, + pc: &mut PositionCalculator, +) -> Result> { + debug_assert_eq!(pair.as_rule(), Rule::variable_definition); + + let pos = pc.step(&pair); + let mut pairs = pair.into_inner(); + + let variable = parse_variable(pairs.next().unwrap(), pc)?; + let var_type = parse_type(pairs.next().unwrap(), pc)?; + let default_value = parse_if_rule(&mut pairs, Rule::default_value, |pair| parse_default_value(pair, pc))?; + + debug_assert_eq!(pairs.next(), None); + + Ok(Positioned::new( + VariableDefinition { + name: variable, + var_type, + default_value, + }, + pos, + )) +} + +fn parse_selection_set(pair: Pair, pc: &mut PositionCalculator) -> Result> { + debug_assert_eq!(pair.as_rule(), Rule::selection_set); + + let pos = pc.step(&pair); + + Ok(Positioned::new( + SelectionSet { + items: pair + .into_inner() + .map(|pair| parse_selection(pair, pc)) + .collect::>()?, + }, + pos, + )) +} + +fn parse_selection(pair: Pair, pc: &mut PositionCalculator) -> Result> { + debug_assert_eq!(pair.as_rule(), Rule::selection); + + let pos = pc.step(&pair); + let pair = exactly_one(pair.into_inner()); + + Ok(Positioned::new( + match pair.as_rule() { + Rule::field => Selection::Field(parse_field(pair, pc)?), + Rule::fragment_spread => Selection::FragmentSpread(parse_fragment_spread(pair, pc)?), + Rule::inline_fragment => Selection::InlineFragment(parse_inline_fragment(pair, pc)?), + _ => unreachable!(), + }, + pos, + )) +} + +fn parse_field(pair: Pair, pc: &mut PositionCalculator) -> Result> { + debug_assert_eq!(pair.as_rule(), Rule::field); + + let pos = pc.step(&pair); + let mut pairs = pair.into_inner(); + + let alias = parse_if_rule(&mut pairs, Rule::alias, |pair| parse_alias(pair, pc))?; + let name = parse_name(pairs.next().unwrap(), pc)?; + let arguments = parse_if_rule(&mut pairs, Rule::arguments, |pair| parse_arguments(pair, pc))?; + let directives = parse_opt_directives(&mut pairs, pc)?; + let selection_set = + parse_if_rule(&mut pairs, Rule::selection_set, |pair| parse_selection_set(pair, pc))?; + + debug_assert_eq!(pairs.next(), None); + + Ok(Positioned::new( + Field { + alias, + name, + arguments: arguments.unwrap_or_default(), + directives, + selection_set: selection_set.unwrap_or_default(), + }, + pos, + )) +} + +fn parse_alias(pair: Pair, pc: &mut PositionCalculator) -> Result> { + debug_assert_eq!(pair.as_rule(), Rule::alias); + parse_name(exactly_one(pair.into_inner()), pc) +} + + +fn parse_fragment_spread( + pair: Pair, + pc: &mut PositionCalculator, +) -> Result> { + debug_assert_eq!(pair.as_rule(), Rule::fragment_spread); + + let pos = pc.step(&pair); + let mut pairs = pair.into_inner(); + + let fragment_name = parse_name(pairs.next().unwrap(), pc)?; + let directives = parse_opt_directives(&mut pairs, pc)?; + + debug_assert_eq!(pairs.next(), None); + + Ok(Positioned::new( + FragmentSpread { + fragment_name, + directives, + }, + pos, + )) +} + +fn parse_inline_fragment( + pair: Pair, + pc: &mut PositionCalculator, +) -> Result> { + debug_assert_eq!(pair.as_rule(), Rule::inline_fragment); + + let pos = pc.step(&pair); + let mut pairs = pair.into_inner(); + + let type_condition = + parse_if_rule(&mut pairs, Rule::type_condition, |pair| parse_type_condition(pair, pc))?; + let directives = parse_opt_directives(&mut pairs, pc)?; + let selection_set = parse_selection_set(pairs.next().unwrap(), pc)?; + + debug_assert_eq!(pairs.next(), None); + + Ok(Positioned::new( + InlineFragment { + type_condition, + directives, + selection_set, + }, + pos, + )) +} + +fn parse_fragment_definition( + pair: Pair, + pc: &mut PositionCalculator, +) -> Result> { + debug_assert_eq!(pair.as_rule(), Rule::fragment_definition); + + let pos = pc.step(&pair); + let mut pairs = pair.into_inner(); + + let name = parse_name(pairs.next().unwrap(), pc)?; + let type_condition = parse_type_condition(pairs.next().unwrap(), pc)?; + let directives = parse_opt_directives(&mut pairs, pc)?; + let selection_set = parse_selection_set(pairs.next().unwrap(), pc)?; + + debug_assert_eq!(pairs.next(), None); + + Ok(Positioned::new( + FragmentDefinition { + name, + type_condition, + directives, + selection_set, + }, + pos, + )) +} + +fn parse_type_condition( + pair: Pair, + pc: &mut PositionCalculator, +) -> Result> { + debug_assert_eq!(pair.as_rule(), Rule::type_condition); + + let pos = pc.step(&pair); + Ok(Positioned::new( + TypeCondition { + on: parse_name(exactly_one(pair.into_inner()), pc)?, + }, + pos, + )) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + #[test] + fn test_parser() { + for entry in fs::read_dir("tests/executables").unwrap() { + if let Ok(entry) = entry { + GraphQLParser::parse(Rule::executable_document, &fs::read_to_string(entry.path()).unwrap()) + .unwrap(); + } + } + } + + #[test] + fn test_parser_ast() { + for entry in fs::read_dir("tests/executables").unwrap() { + if let Ok(entry) = entry { + parse_query(fs::read_to_string(entry.path()).unwrap()).unwrap(); + } + } + } + + #[test] + fn test_parse_overflowing_int() { + let query_ok = format!("mutation {{ add(big: {}) }} ", std::i32::MAX); + let query_overflow = format!("mutation {{ add(big: {}0000) }} ", std::i32::MAX); + assert!(parse_query(query_ok).is_ok()); + assert!(parse_query(query_overflow).is_ok()); + } +} diff --git a/async-graphql-parser/src/parse/mod.rs b/async-graphql-parser/src/parse/mod.rs new file mode 100644 index 00000000..f8a1b965 --- /dev/null +++ b/async-graphql-parser/src/parse/mod.rs @@ -0,0 +1,269 @@ +//! Parsing module. +//! +//! This module's structure mirrors `types`. + +use crate::pos::{Positioned, PositionCalculator}; +use crate::types::*; +use crate::{Result, Error}; +use pest::iterators::{Pair, Pairs}; +use pest::Parser; +use pest_derive::Parser; +use utils::*; + +mod executable; +mod service; +mod utils; + +pub use executable::parse_query; +pub use service::parse_schema; + +#[derive(Parser)] +#[grammar = "graphql.pest"] +struct GraphQLParser; + +fn parse_operation_type( + pair: Pair, + pc: &mut PositionCalculator, +) -> Result> { + debug_assert_eq!(pair.as_rule(), Rule::operation_type); + + let pos = pc.step(&pair); + + Ok(Positioned::new( + match pair.as_str() { + "query" => OperationType::Query, + "mutation" => OperationType::Mutation, + "subscription" => OperationType::Subscription, + _ => unreachable!(), + }, + pos, + )) +} + +fn parse_default_value(pair: Pair, pc: &mut PositionCalculator) -> Result> { + debug_assert_eq!(pair.as_rule(), Rule::default_value); + + parse_const_value(exactly_one(pair.into_inner()), pc) +} + +fn parse_type(pair: Pair, pc: &mut PositionCalculator) -> Result> { + debug_assert_eq!(pair.as_rule(), Rule::type_); + + Ok(Positioned::new(Type::new(pair.as_str()).unwrap(), pc.step(&pair))) +} + +fn parse_const_value(pair: Pair, pc: &mut PositionCalculator) -> Result> { + debug_assert_eq!(pair.as_rule(), Rule::const_value); + + let pos = pc.step(&pair); + let pair = exactly_one(pair.into_inner()); + + Ok(Positioned::new( + match pair.as_rule() { + Rule::number => ConstValue::Number(parse_number(pair, pc)?.node), + Rule::string => ConstValue::String(parse_string(pair, pc)?.node), + Rule::boolean => ConstValue::Boolean(parse_boolean(pair, pc)?.node), + Rule::null => ConstValue::Null, + Rule::enum_value => ConstValue::Enum(parse_enum_value(pair, pc)?.node), + Rule::const_list => ConstValue::List( + pair.into_inner() + .map(|pair| Ok(parse_const_value(pair, pc)?.node)) + .collect::>()?, + ), + Rule::const_object => ConstValue::Object( + pair.into_inner() + .map(|pair| { + debug_assert_eq!(pair.as_rule(), Rule::const_object_field); + + let mut pairs = pair.into_inner(); + + let name = parse_name(pairs.next().unwrap(), pc)?; + let value = parse_const_value(pairs.next().unwrap(), pc)?; + + debug_assert_eq!(pairs.next(), None); + + Ok((name.node, value.node)) + }) + .collect::>()?, + ), + _ => unreachable!(), + }, + pos, + )) +} +fn parse_value(pair: Pair, pc: &mut PositionCalculator) -> Result> { + debug_assert_eq!(pair.as_rule(), Rule::value); + + let pos = pc.step(&pair); + let pair = exactly_one(pair.into_inner()); + + Ok(Positioned::new( + match pair.as_rule() { + Rule::variable => Value::Variable(parse_variable(pair, pc)?.node), + Rule::number => Value::Number(parse_number(pair, pc)?.node), + Rule::string => Value::String(parse_string(pair, pc)?.node), + Rule::boolean => Value::Boolean(parse_boolean(pair, pc)?.node), + Rule::null => Value::Null, + Rule::enum_value => Value::Enum(parse_enum_value(pair, pc)?.node), + Rule::list => Value::List( + pair.into_inner() + .map(|pair| Ok(parse_value(pair, pc)?.node)) + .collect::>()?, + ), + Rule::object => Value::Object( + pair.into_inner() + .map(|pair| { + debug_assert_eq!(pair.as_rule(), Rule::object_field); + let mut pairs = pair.into_inner(); + + let name = parse_name(pairs.next().unwrap(), pc)?; + let value = parse_value(pairs.next().unwrap(), pc)?; + + debug_assert_eq!(pairs.next(), None); + + Ok((name.node, value.node)) + }) + .collect::>()?, + ), + _ => unreachable!(), + }, + pos, + )) +} + +fn parse_variable(pair: Pair, pc: &mut PositionCalculator) -> Result> { + debug_assert_eq!(pair.as_rule(), Rule::variable); + parse_name(exactly_one(pair.into_inner()), pc) +} +fn parse_number(pair: Pair, pc: &mut PositionCalculator) -> Result> { + debug_assert_eq!(pair.as_rule(), Rule::number); + let pos = pc.step(&pair); + Ok(Positioned::new(pair.as_str().parse().expect("failed to parse number"), pos)) +} +fn parse_string(pair: Pair, pc: &mut PositionCalculator) -> Result> { + debug_assert_eq!(pair.as_rule(), Rule::string); + let pos = pc.step(&pair); + let pair = exactly_one(pair.into_inner()); + Ok(Positioned::new(match pair.as_rule() { + Rule::block_string_content => block_string_value(pair.as_str()), + Rule::string_content => string_value(pair.as_str()), + _ => unreachable!(), + }, pos)) +} +fn parse_boolean(pair: Pair, pc: &mut PositionCalculator) -> Result> { + debug_assert_eq!(pair.as_rule(), Rule::boolean); + let pos = pc.step(&pair); + Ok(Positioned::new(match pair.as_str() { + "true" => true, + "false" => false, + _ => unreachable!(), + }, pos)) +} +fn parse_enum_value(pair: Pair, pc: &mut PositionCalculator) -> Result> { + debug_assert_eq!(pair.as_rule(), Rule::enum_value); + parse_name(exactly_one(pair.into_inner()), pc) +} + +fn parse_opt_const_directives<'a>(pairs: &mut Pairs<'a, Rule>, pc: &mut PositionCalculator) -> Result>> { + Ok(parse_if_rule(pairs, Rule::const_directives, |pair| parse_const_directives(pair, pc))?.unwrap_or_default()) +} +fn parse_opt_directives<'a>(pairs: &mut Pairs<'a, Rule>, pc: &mut PositionCalculator) -> Result>> { + Ok(parse_if_rule(pairs, Rule::directives, |pair| parse_directives(pair, pc))?.unwrap_or_default()) +} +fn parse_const_directives(pair: Pair, pc: &mut PositionCalculator) -> Result>> { + debug_assert_eq!(pair.as_rule(), Rule::const_directives); + + pair.into_inner() + .map(|pair| parse_const_directive(pair, pc)) + .collect() +} +fn parse_directives(pair: Pair, pc: &mut PositionCalculator) -> Result>> { + debug_assert_eq!(pair.as_rule(), Rule::directives); + + pair.into_inner() + .map(|pair| parse_directive(pair, pc)) + .collect() +} + +fn parse_const_directive(pair: Pair, pc: &mut PositionCalculator) -> Result> { + debug_assert_eq!(pair.as_rule(), Rule::const_directive); + + let pos = pc.step(&pair); + let mut pairs = pair.into_inner(); + + let name = parse_name(pairs.next().unwrap(), pc)?; + let arguments = parse_if_rule(&mut pairs, Rule::const_arguments, |pair| parse_const_arguments(pair, pc))?; + + debug_assert_eq!(pairs.next(), None); + + Ok(Positioned::new( + ConstDirective { + name, + arguments: arguments.unwrap_or_default(), + }, + pos, + )) +} +fn parse_directive(pair: Pair, pc: &mut PositionCalculator) -> Result> { + debug_assert_eq!(pair.as_rule(), Rule::directive); + + let pos = pc.step(&pair); + let mut pairs = pair.into_inner(); + + let name = parse_name(pairs.next().unwrap(), pc)?; + let arguments = parse_if_rule(&mut pairs, Rule::arguments, |pair| parse_arguments(pair, pc))?; + + debug_assert_eq!(pairs.next(), None); + + Ok(Positioned::new( + Directive { + name, + arguments: arguments.unwrap_or_default(), + }, + pos, + )) +} + +fn parse_const_arguments( + pair: Pair, + pc: &mut PositionCalculator, +) -> Result, Positioned)>> { + debug_assert_eq!(pair.as_rule(), Rule::const_arguments); + pair.into_inner() + .map(|pair| { + debug_assert_eq!(pair.as_rule(), Rule::const_argument); + let mut pairs = pair.into_inner(); + + let name = parse_name(pairs.next().unwrap(), pc)?; + let value = parse_const_value(pairs.next().unwrap(), pc)?; + + debug_assert_eq!(pairs.next(), None); + + Ok((name, value)) + }) + .collect() +} +fn parse_arguments( + pair: Pair, + pc: &mut PositionCalculator, +) -> Result, Positioned)>> { + debug_assert_eq!(pair.as_rule(), Rule::arguments); + pair.into_inner() + .map(|pair| { + debug_assert_eq!(pair.as_rule(), Rule::argument); + let mut pairs = pair.into_inner(); + + let name = parse_name(pairs.next().unwrap(), pc)?; + let value = parse_value(pairs.next().unwrap(), pc)?; + + debug_assert_eq!(pairs.next(), None); + + Ok((name, value)) + }) + .collect() +} + +fn parse_name(pair: Pair, pc: &mut PositionCalculator) -> Result> { + debug_assert_eq!(pair.as_rule(), Rule::name); + Ok(Positioned::new(Name::new_unchecked(pair.as_str().to_owned()), pc.step(&pair))) +} diff --git a/async-graphql-parser/src/parse/service.rs b/async-graphql-parser/src/parse/service.rs new file mode 100644 index 00000000..263ff308 --- /dev/null +++ b/async-graphql-parser/src/parse/service.rs @@ -0,0 +1,331 @@ +use super::*; + +/// Parse a GraphQL schema document. +/// +/// # Errors +/// +/// Fails if the schema is not a valid GraphQL document. +pub fn parse_schema>(input: T) -> Result { + let mut pc = PositionCalculator::new(input.as_ref()); + Ok(parse_service_document( + exactly_one(GraphQLParser::parse(Rule::service_document, input.as_ref())?), + &mut pc, + )?) +} + +fn parse_service_document(pair: Pair, pc: &mut PositionCalculator) -> Result { + debug_assert_eq!(pair.as_rule(), Rule::service_document); + + Ok(ServiceDocument { + definitions: pair + .into_inner() + .filter(|pair| pair.as_rule() != Rule::EOI) + .map(|pair| parse_type_system_definition(pair, pc)) + .collect::>()?, + }) +} + +fn parse_type_system_definition(pair: Pair, pc: &mut PositionCalculator) -> Result { + debug_assert_eq!(pair.as_rule(), Rule::type_system_definition); + + let pair = exactly_one(pair.into_inner()); + Ok(match pair.as_rule() { + Rule::schema_definition => TypeSystemDefinition::Schema(parse_schema_definition(pair, pc)?), + Rule::type_definition => TypeSystemDefinition::Type(parse_type_definition(pair, pc)?), + Rule::directive_definition => TypeSystemDefinition::Directive(parse_directive_definition(pair, pc)?), + _ => unreachable!(), + }) +} + +fn parse_schema_definition(pair: Pair, pc: &mut PositionCalculator) -> Result> { + debug_assert_eq!(pair.as_rule(), Rule::schema_definition); + + let pos = pc.step(&pair); + let mut pairs = pair.into_inner(); + + let extend = next_if_rule(&mut pairs, Rule::extend).is_some(); + let directives = parse_opt_const_directives(&mut pairs, pc)?; + + let mut query = None; + let mut mutation = None; + let mut subscription = None; + + for pair in pairs { + debug_assert_eq!(pair.as_rule(), Rule::operation_type_definition); + + let mut pairs = pair.into_inner(); + + let operation_type = parse_operation_type(pairs.next().unwrap(), pc)?; + let name = parse_name(pairs.next().unwrap(), pc)?; + + match operation_type.node { + OperationType::Query => { + if query.is_some() { + return Err(operation_type.error_here("multiple query roots")); + } + query = Some(name); + } + OperationType::Mutation => { + if mutation.is_some() { + return Err(operation_type.error_here("multiple mutation roots")); + } + mutation = Some(name); + } + OperationType::Subscription => { + if subscription.is_some() { + return Err(operation_type.error_here("multiple subscription roots")); + } + subscription = Some(name); + } + } + + debug_assert_eq!(pairs.next(), None); + } + + if !extend && query.is_none() { + return Err(Error::new("missing query root", pos)); + } + + Ok(Positioned::new(SchemaDefinition { + extend, + directives, + query, + mutation, + subscription, + }, pos)) +} + +fn parse_type_definition(pair: Pair, pc: &mut PositionCalculator) -> Result> { + debug_assert_eq!(pair.as_rule(), Rule::type_definition); + + let pos = pc.step(&pair); + let pair = exactly_one(pair.into_inner()); + let rule = pair.as_rule(); + let mut pairs = pair.into_inner(); + + let description = parse_if_rule(&mut pairs, Rule::string, |pair| parse_string(pair, pc))?; + let extend = next_if_rule(&mut pairs, Rule::extend).is_some(); + let name = parse_name(pairs.next().unwrap(), pc)?; + + let (directives, kind) = match rule { + Rule::scalar_type => { + let directives = parse_opt_const_directives(&mut pairs, pc)?; + (directives, TypeKind::Scalar) + } + Rule::object_type => { + let implements = parse_if_rule(&mut pairs, Rule::implements_interfaces, |pair| { + debug_assert_eq!(pair.as_rule(), Rule::implements_interfaces); + + pair.into_inner() + .map(|pair| parse_name(pair, pc)) + .collect::>() + })?; + + let directives = parse_opt_const_directives(&mut pairs, pc)?; + + let fields = parse_if_rule(&mut pairs, Rule::fields_definition, |pair| parse_fields_definition(pair, pc))?.unwrap_or_default(); + + (directives, TypeKind::Object(ObjectType { + implements: implements.unwrap_or_default(), + fields, + })) + } + Rule::interface_type => { + let directives = parse_opt_const_directives(&mut pairs, pc)?; + let fields = parse_if_rule(&mut pairs, Rule::fields_definition, |pair| parse_fields_definition(pair, pc))?.unwrap_or_default(); + (directives, TypeKind::Interface(InterfaceType { fields })) + } + Rule::union_type => { + let directives = parse_opt_const_directives(&mut pairs, pc)?; + let members = parse_if_rule(&mut pairs, Rule::union_member_types, |pair| { + debug_assert_eq!(pair.as_rule(), Rule::union_member_types); + + pair.into_inner() + .map(|pair| parse_name(pair, pc)) + .collect() + })?.unwrap_or_default(); + (directives, TypeKind::Union(UnionType { members })) + } + Rule::enum_type => { + let directives = parse_opt_const_directives(&mut pairs, pc)?; + let values = parse_if_rule(&mut pairs, Rule::enum_values, |pair| { + debug_assert_eq!(pair.as_rule(), Rule::enum_values); + + pair.into_inner() + .map(|pair| { + debug_assert_eq!(pair.as_rule(), Rule::enum_value_definition); + + let pos = pc.step(&pair); + let mut pairs = pair.into_inner(); + + let description = parse_if_rule(&mut pairs, Rule::string, |pair| parse_string(pair, pc))?; + let value = parse_enum_value(pairs.next().unwrap(), pc)?; + let directives = parse_opt_const_directives(&mut pairs, pc)?; + + debug_assert_eq!(pairs.next(), None); + + Ok(Positioned::new(EnumValueDefinition { + description, + value, + directives, + }, pos)) + }) + .collect() + })?.unwrap_or_default(); + (directives, TypeKind::Enum(EnumType { values })) + } + Rule::input_object_type => { + let directives = parse_opt_const_directives(&mut pairs, pc)?; + let fields = parse_if_rule(&mut pairs, Rule::input_fields_definition, |pair| { + debug_assert_eq!(pair.as_rule(), Rule::input_fields_definition); + + pair + .into_inner() + .map(|pair| parse_input_value_definition(pair, pc)) + .collect() + })?.unwrap_or_default(); + + (directives, TypeKind::InputObject(InputObjectType { fields })) + } + _ => unreachable!(), + }; + + debug_assert_eq!(pairs.next(), None); + + Ok(Positioned::new(TypeDefinition { + extend, + description, + name, + directives, + kind, + }, pos)) +} + +fn parse_fields_definition(pair: Pair, pc: &mut PositionCalculator) -> Result>> { + debug_assert_eq!(pair.as_rule(), Rule::fields_definition); + + pair.into_inner().map(|pair| parse_field_definition(pair, pc)).collect() +} + +fn parse_field_definition(pair: Pair, pc: &mut PositionCalculator) -> Result> { + debug_assert_eq!(pair.as_rule(), Rule::field_definition); + + let pos = pc.step(&pair); + let mut pairs = pair.into_inner(); + + let description = parse_if_rule(&mut pairs, Rule::string, |pair| parse_string(pair, pc))?; + let name = parse_name(pairs.next().unwrap(), pc)?; + let arguments = parse_if_rule(&mut pairs, Rule::arguments_definition, |pair| parse_arguments_definition(pair, pc))?.unwrap_or_default(); + let ty = parse_type(pairs.next().unwrap(), pc)?; + let directives = parse_opt_const_directives(&mut pairs, pc)?; + + debug_assert_eq!(pairs.next(), None); + + Ok(Positioned::new(FieldDefinition { description, name, arguments, ty, directives }, pos)) +} + +fn parse_directive_definition(pair: Pair, pc: &mut PositionCalculator) -> Result> { + debug_assert_eq!(pair.as_rule(), Rule::directive_definition); + + let pos = pc.step(&pair); + let mut pairs = pair.into_inner(); + + let description = parse_if_rule(&mut pairs, Rule::string, |pair| parse_string(pair, pc))?; + let name = parse_name(pairs.next().unwrap(), pc)?; + let arguments = parse_if_rule(&mut pairs, Rule::arguments_definition, |pair| { + debug_assert_eq!(pair.as_rule(), Rule::arguments_definition); + pair + .into_inner() + .map(|pair| parse_input_value_definition(pair, pc)) + .collect() + })?.unwrap_or_default(); + let locations = { + let pair = pairs.next().unwrap(); + debug_assert_eq!(pair.as_rule(), Rule::directive_locations); + pair + .into_inner() + .map(|pair| { + let pos = pc.step(&pair); + debug_assert_eq!(pair.as_rule(), Rule::directive_location); + Positioned::new(match pair.as_str() { + "QUERY" => DirectiveLocation::Query, + "MUTATION" => DirectiveLocation::Mutation, + "SUBSCRIPTION" => DirectiveLocation::Subscription, + "FIELD" => DirectiveLocation::Field, + "FRAGMENT_DEFINITION" => DirectiveLocation::FragmentDefinition, + "FRAGMENT_SPREAD" => DirectiveLocation::FragmentSpread, + "INLINE_FRAGMENT" => DirectiveLocation::InlineFragment, + "SCHEMA" => DirectiveLocation::Schema, + "SCALAR" => DirectiveLocation::Scalar, + "OBJECT" => DirectiveLocation::Object, + "FIELD_DEFINITION" => DirectiveLocation::FieldDefinition, + "ARGUMENT_DEFINITION" => DirectiveLocation::ArgumentDefinition, + "INTERFACE" => DirectiveLocation::Interface, + "UNION" => DirectiveLocation::Union, + "ENUM" => DirectiveLocation::Enum, + "ENUM_VALUE" => DirectiveLocation::EnumValue, + "INPUT_OBJECT" => DirectiveLocation::InputObject, + "INPUT_FIELD_DEFINITION" => DirectiveLocation::InputFieldDefinition, + _ => unreachable!(), + }, pos) + }) + .collect() + }; + + debug_assert_eq!(pairs.next(), None); + + Ok(Positioned::new(DirectiveDefinition { + description, + name, + arguments, + locations, + }, pos)) +} + +fn parse_arguments_definition(pair: Pair, pc: &mut PositionCalculator) -> Result>> { + debug_assert_eq!(pair.as_rule(), Rule::arguments_definition); + + pair.into_inner() + .map(|pair| parse_input_value_definition(pair, pc)) + .collect() +} + +fn parse_input_value_definition(pair: Pair, pc: &mut PositionCalculator) -> Result> { + debug_assert_eq!(pair.as_rule(), Rule::input_value_definition); + + let pos = pc.step(&pair); + let mut pairs = pair.into_inner(); + + let description = parse_if_rule(&mut pairs, Rule::string, |pair| parse_string(pair, pc))?; + let name = parse_name(pairs.next().unwrap(), pc)?; + let ty = parse_type(pairs.next().unwrap(), pc)?; + let default_value = parse_if_rule(&mut pairs, Rule::default_value, |pair| parse_default_value(pair, pc))?; + let directives = parse_opt_const_directives(&mut pairs, pc)?; + + Ok(Positioned::new(InputValueDefinition { description, name, ty, default_value, directives }, pos)) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + #[test] + fn test_parser() { + for entry in fs::read_dir("tests/services").unwrap() { + if let Ok(entry) = entry { + GraphQLParser::parse(Rule::service_document, &fs::read_to_string(entry.path()).unwrap()) + .unwrap(); + } + } + } + + #[test] + fn test_parser_ast() { + for entry in fs::read_dir("tests/services").unwrap() { + if let Ok(entry) = entry { + parse_schema(fs::read_to_string(entry.path()).unwrap()).unwrap(); + } + } + } +} diff --git a/async-graphql-parser/src/utils.rs b/async-graphql-parser/src/parse/utils.rs similarity index 68% rename from async-graphql-parser/src/utils.rs rename to async-graphql-parser/src/parse/utils.rs index 6db3443c..827c9ba4 100644 --- a/async-graphql-parser/src/utils.rs +++ b/async-graphql-parser/src/parse/utils.rs @@ -1,53 +1,27 @@ -use crate::Pos; -use pest::iterators::Pair; -use pest::RuleType; -use std::str::Chars; +use super::Rule; +use crate::Result; +use pest::iterators::{Pair, Pairs}; -pub struct PositionCalculator<'a> { - input: Chars<'a>, - pos: usize, - line: usize, - column: usize, -} - -impl<'a> PositionCalculator<'a> { - pub fn new(input: &'a str) -> PositionCalculator<'a> { - Self { - input: input.chars(), - pos: 0, - line: 1, - column: 1, - } - } - - pub fn step(&mut self, pair: &Pair) -> Pos { - let pos = pair.as_span().start(); - debug_assert!(pos >= self.pos); - for _ in 0..pos - self.pos { - match self.input.next() { - Some('\r') => { - self.column = 1; - } - Some('\n') => { - self.line += 1; - self.column = 1; - } - Some(_) => { - self.column += 1; - } - None => break, - } - } - self.pos = pos; - Pos { - line: self.line, - column: self.column, - } +pub(super) fn next_if_rule<'a>(pairs: &mut Pairs<'a, Rule>, rule: Rule) -> Option> { + if pairs.peek().map_or(false, |pair| pair.as_rule() == rule) { + Some(pairs.next().unwrap()) + } else { + None } } +pub(super) fn parse_if_rule<'a, T>(pairs: &mut Pairs<'a, Rule>, rule: Rule, f: impl FnOnce(Pair) -> Result) -> Result> { + next_if_rule(pairs, rule).map(f).transpose() +} -// See https://spec.graphql.org/June2018/#BlockStringValue() -pub(crate) fn block_string_value(raw: &str) -> String { +pub(super) fn exactly_one(iter: impl IntoIterator) -> T { + let mut iter = iter.into_iter(); + let res = iter.next().unwrap(); + debug_assert!(matches!(iter.next(), None)); + res +} + + +pub(super) fn block_string_value(raw: &str) -> String { // Split the string by either \r\n, \r or \n let lines: Vec<_> = raw .split("\r\n") @@ -111,7 +85,7 @@ fn test_block_string_value() { ); } -pub(crate) fn string_value(s: &str) -> String { +pub(super) fn string_value(s: &str) -> String { let mut chars = s.chars(); std::iter::from_fn(|| { diff --git a/async-graphql-parser/src/parser.rs b/async-graphql-parser/src/parser.rs deleted file mode 100644 index c43a34fc..00000000 --- a/async-graphql-parser/src/parser.rs +++ /dev/null @@ -1,482 +0,0 @@ -use crate::pos::Positioned; -use crate::types::*; -use crate::utils::{block_string_value, string_value, PositionCalculator}; -use crate::Result; -use pest::iterators::{Pair, Pairs}; -use pest::Parser; -use pest_derive::Parser; - -#[derive(Parser)] -#[grammar = "graphql.pest"] -struct GraphQLParser; - -/// Parse a GraphQL query. -/// -/// # Errors -/// -/// Fails if the query is not a valid GraphQL document. -pub fn parse_query>(input: T) -> Result { - let mut pc = PositionCalculator::new(input.as_ref()); - Ok(parse_document( - exactly_one(GraphQLParser::parse(Rule::document, input.as_ref())?), - &mut pc, - )) -} - -fn parse_document(pair: Pair, pc: &mut PositionCalculator) -> Document { - debug_assert_eq!(pair.as_rule(), Rule::document); - - Document { - definitions: pair - .into_inner() - .filter(|pair| pair.as_rule() != Rule::EOI) - .map(|pair| parse_definition(pair, pc)) - .collect(), - } -} - -fn parse_definition(pair: Pair, pc: &mut PositionCalculator) -> Definition { - debug_assert_eq!(pair.as_rule(), Rule::definition); - - let pair = exactly_one(pair.into_inner()); - match pair.as_rule() { - Rule::operation_definition => Definition::Operation(parse_operation_definition(pair, pc)), - Rule::fragment_definition => Definition::Fragment(parse_fragment_definition(pair, pc)), - _ => unreachable!(), - } -} - -fn parse_operation_definition( - pair: Pair, - pc: &mut PositionCalculator, -) -> Positioned { - debug_assert_eq!(pair.as_rule(), Rule::operation_definition); - - let pos = pc.step(&pair); - let pair = exactly_one(pair.into_inner()); - Positioned::new( - match pair.as_rule() { - Rule::named_operation_definition => parse_named_operation_definition(pair, pc), - Rule::selection_set => OperationDefinition { - ty: OperationType::Query, - name: None, - variable_definitions: Vec::new(), - directives: Vec::new(), - selection_set: parse_selection_set(pair, pc), - }, - _ => unreachable!(), - }, - pos, - ) -} - -fn parse_named_operation_definition( - pair: Pair, - pc: &mut PositionCalculator, -) -> OperationDefinition { - debug_assert_eq!(pair.as_rule(), Rule::named_operation_definition); - - let mut pairs = pair.into_inner(); - - let ty = parse_operation_type(&pairs.next().unwrap(), pc); - let name = next_if_rule(&mut pairs, Rule::name).map(|pair| parse_name(&pair, pc)); - let variable_definitions = next_if_rule(&mut pairs, Rule::variable_definitions) - .map(|pair| parse_variable_definitions(pair, pc)); - let directives = - next_if_rule(&mut pairs, Rule::directives).map(|pair| parse_directives(pair, pc)); - let selection_set = parse_selection_set(pairs.next().unwrap(), pc); - - debug_assert_eq!(pairs.next(), None); - - OperationDefinition { - ty: ty.node, - name, - variable_definitions: variable_definitions.unwrap_or_default(), - directives: directives.unwrap_or_default(), - selection_set, - } -} - -fn parse_operation_type( - pair: &Pair, - pc: &mut PositionCalculator, -) -> Positioned { - debug_assert_eq!(pair.as_rule(), Rule::operation_type); - - let pos = pc.step(&pair); - - Positioned::new( - match pair.as_str() { - "query" => OperationType::Query, - "mutation" => OperationType::Mutation, - "subscription" => OperationType::Subscription, - _ => unreachable!(), - }, - pos, - ) -} - -fn parse_variable_definitions( - pair: Pair, - pc: &mut PositionCalculator, -) -> Vec> { - debug_assert_eq!(pair.as_rule(), Rule::variable_definitions); - - pair.into_inner() - .map(|pair| parse_variable_definition(pair, pc)) - .collect() -} - -fn parse_variable_definition( - pair: Pair, - pc: &mut PositionCalculator, -) -> Positioned { - debug_assert_eq!(pair.as_rule(), Rule::variable_definition); - - let pos = pc.step(&pair); - let mut pairs = pair.into_inner(); - - let variable = parse_variable(pairs.next().unwrap(), pc); - let var_type = parse_type(&pairs.next().unwrap(), pc); - let default_value = pairs.next().map(|pair| parse_default_value(pair, pc)); - - debug_assert_eq!(pairs.peek(), None); - - Positioned::new( - VariableDefinition { - name: variable, - var_type, - default_value, - }, - pos, - ) -} - -fn parse_variable(pair: Pair, pc: &mut PositionCalculator) -> Positioned { - debug_assert_eq!(pair.as_rule(), Rule::variable); - - parse_name(&exactly_one(pair.into_inner()), pc) -} - -fn parse_default_value(pair: Pair, pc: &mut PositionCalculator) -> Positioned { - debug_assert_eq!(pair.as_rule(), Rule::default_value); - - parse_value(exactly_one(pair.into_inner()), pc) -} - -fn parse_type(pair: &Pair, pc: &mut PositionCalculator) -> Positioned { - debug_assert_eq!(pair.as_rule(), Rule::type_); - - Positioned::new(Type::new(pair.as_str()), pc.step(&pair)) -} - -fn parse_value(pair: Pair, pc: &mut PositionCalculator) -> Positioned { - debug_assert_eq!(pair.as_rule(), Rule::value); - - let pos = pc.step(&pair); - let pair = exactly_one(pair.into_inner()); - - Positioned::new( - match pair.as_rule() { - Rule::variable => Value::Variable(parse_variable(pair, pc).node), - Rule::float | Rule::int => { - Value::Number(pair.as_str().parse().expect("failed to parse number")) - } - Rule::string => Value::String({ - let pair = exactly_one(pair.into_inner()); - match pair.as_rule() { - Rule::block_string_content => block_string_value(pair.as_str()), - Rule::string_content => string_value(pair.as_str()), - _ => unreachable!(), - } - }), - Rule::boolean => Value::Boolean(match pair.as_str() { - "true" => true, - "false" => false, - _ => unreachable!(), - }), - Rule::null => Value::Null, - Rule::enum_ => Value::Enum(parse_name(&exactly_one(pair.into_inner()), pc).node), - Rule::list => Value::List( - pair.into_inner() - .map(|pair| parse_value(pair, pc).node) - .collect(), - ), - Rule::object => Value::Object( - pair.into_inner() - .map(|pair| { - let (name, value) = parse_name_value(pair, pc); - (name.node, value.node) - }) - .collect(), - ), - _ => unreachable!(), - }, - pos, - ) -} - -fn parse_name_value( - pair: Pair, - pc: &mut PositionCalculator, -) -> (Positioned, Positioned) { - debug_assert_eq!(pair.as_rule(), Rule::name_value); - - let mut pairs = pair.into_inner(); - - let name = parse_name(&pairs.next().unwrap(), pc); - let value = parse_value(pairs.next().unwrap(), pc); - - debug_assert_eq!(pairs.next(), None); - - (name, value) -} - -fn parse_selection_set(pair: Pair, pc: &mut PositionCalculator) -> Positioned { - debug_assert_eq!(pair.as_rule(), Rule::selection_set); - - let pos = pc.step(&pair); - - Positioned::new( - SelectionSet { - items: pair - .into_inner() - .map(|pair| parse_selection(pair, pc)) - .collect(), - }, - pos, - ) -} - -fn parse_selection(pair: Pair, pc: &mut PositionCalculator) -> Positioned { - debug_assert_eq!(pair.as_rule(), Rule::selection); - - let pos = pc.step(&pair); - let pair = exactly_one(pair.into_inner()); - - Positioned::new( - match pair.as_rule() { - Rule::field => Selection::Field(parse_field(pair, pc)), - Rule::fragment_spread => Selection::FragmentSpread(parse_fragment_spread(pair, pc)), - Rule::inline_fragment => Selection::InlineFragment(parse_inline_fragment(pair, pc)), - _ => unreachable!(), - }, - pos, - ) -} - -fn parse_field(pair: Pair, pc: &mut PositionCalculator) -> Positioned { - debug_assert_eq!(pair.as_rule(), Rule::field); - - let pos = pc.step(&pair); - let mut pairs = pair.into_inner(); - - let alias = next_if_rule(&mut pairs, Rule::alias).map(|pair| parse_alias(pair, pc)); - let name = parse_name(&pairs.next().unwrap(), pc); - let arguments = next_if_rule(&mut pairs, Rule::arguments).map(|pair| parse_arguments(pair, pc)); - let directives = - next_if_rule(&mut pairs, Rule::directives).map(|pair| parse_directives(pair, pc)); - let selection_set = - next_if_rule(&mut pairs, Rule::selection_set).map(|pair| parse_selection_set(pair, pc)); - - Positioned::new( - Field { - alias, - name, - arguments: arguments.unwrap_or_default(), - directives: directives.unwrap_or_default(), - selection_set: selection_set.unwrap_or_default(), - }, - pos, - ) -} - -fn parse_alias(pair: Pair, pc: &mut PositionCalculator) -> Positioned { - debug_assert_eq!(pair.as_rule(), Rule::alias); - parse_name(&exactly_one(pair.into_inner()), pc) -} - -fn parse_arguments( - pair: Pair, - pc: &mut PositionCalculator, -) -> Vec<(Positioned, Positioned)> { - debug_assert_eq!(pair.as_rule(), Rule::arguments); - pair.into_inner() - .map(|pair| parse_name_value(pair, pc)) - .collect() -} - -fn parse_fragment_spread( - pair: Pair, - pc: &mut PositionCalculator, -) -> Positioned { - debug_assert_eq!(pair.as_rule(), Rule::fragment_spread); - - let pos = pc.step(&pair); - let mut pairs = pair.into_inner(); - - let fragment_name = parse_name(&pairs.next().unwrap(), pc); - let directives = pairs.next().map(|pair| parse_directives(pair, pc)); - - debug_assert_eq!(pairs.peek(), None); - - Positioned::new( - FragmentSpread { - fragment_name, - directives: directives.unwrap_or_default(), - }, - pos, - ) -} - -fn parse_inline_fragment( - pair: Pair, - pc: &mut PositionCalculator, -) -> Positioned { - debug_assert_eq!(pair.as_rule(), Rule::inline_fragment); - - let pos = pc.step(&pair); - let mut pairs = pair.into_inner(); - - let type_condition = - next_if_rule(&mut pairs, Rule::type_condition).map(|pair| parse_type_condition(pair, pc)); - let directives = - next_if_rule(&mut pairs, Rule::directives).map(|pair| parse_directives(pair, pc)); - let selection_set = parse_selection_set(pairs.next().unwrap(), pc); - - debug_assert_eq!(pairs.next(), None); - - Positioned::new( - InlineFragment { - type_condition, - directives: directives.unwrap_or_default(), - selection_set, - }, - pos, - ) -} - -fn parse_fragment_definition( - pair: Pair, - pc: &mut PositionCalculator, -) -> Positioned { - debug_assert_eq!(pair.as_rule(), Rule::fragment_definition); - - let pos = pc.step(&pair); - let mut pairs = pair.into_inner(); - - let name = parse_name(&pairs.next().unwrap(), pc); - let type_condition = parse_type_condition(pairs.next().unwrap(), pc); - let directives = - next_if_rule(&mut pairs, Rule::directives).map(|pair| parse_directives(pair, pc)); - let selection_set = parse_selection_set(pairs.next().unwrap(), pc); - - debug_assert_eq!(pairs.next(), None); - - Positioned::new( - FragmentDefinition { - name, - type_condition, - directives: directives.unwrap_or_default(), - selection_set, - }, - pos, - ) -} - -fn parse_type_condition( - pair: Pair, - pc: &mut PositionCalculator, -) -> Positioned { - debug_assert_eq!(pair.as_rule(), Rule::type_condition); - - let pos = pc.step(&pair); - Positioned::new( - TypeCondition { - on: parse_name(&exactly_one(pair.into_inner()), pc), - }, - pos, - ) -} - -fn parse_directives(pair: Pair, pc: &mut PositionCalculator) -> Vec> { - debug_assert_eq!(pair.as_rule(), Rule::directives); - - pair.into_inner() - .map(|pair| parse_directive(pair, pc)) - .collect() -} - -fn parse_directive(pair: Pair, pc: &mut PositionCalculator) -> Positioned { - debug_assert_eq!(pair.as_rule(), Rule::directive); - - let pos = pc.step(&pair); - let mut pairs = pair.into_inner(); - - let name = parse_name(&pairs.next().unwrap(), pc); - let arguments = pairs.next().map(|pair| parse_arguments(pair, pc)); - - debug_assert_eq!(pairs.peek(), None); - - Positioned::new( - Directive { - name, - arguments: arguments.unwrap_or_default(), - }, - pos, - ) -} - -fn parse_name(pair: &Pair, pc: &mut PositionCalculator) -> Positioned { - debug_assert_eq!(pair.as_rule(), Rule::name); - Positioned::new(pair.as_str().to_owned(), pc.step(&pair)) -} - -// Parser helper functions - -fn next_if_rule<'a>(pairs: &mut Pairs<'a, Rule>, rule: Rule) -> Option> { - if pairs.peek().map_or(false, |pair| pair.as_rule() == rule) { - Some(pairs.next().unwrap()) - } else { - None - } -} -fn exactly_one(iter: impl IntoIterator) -> T { - let mut iter = iter.into_iter(); - let res = iter.next().unwrap(); - debug_assert!(matches!(iter.next(), None)); - res -} - -#[cfg(test)] -mod tests { - use super::*; - use std::fs; - - #[test] - fn test_parser() { - for entry in fs::read_dir("tests/queries").unwrap() { - if let Ok(entry) = entry { - GraphQLParser::parse(Rule::document, &fs::read_to_string(entry.path()).unwrap()) - .unwrap(); - } - } - } - - #[test] - fn test_parser_ast() { - for entry in fs::read_dir("tests/queries").unwrap() { - if let Ok(entry) = entry { - parse_query(fs::read_to_string(entry.path()).unwrap()).unwrap(); - } - } - } - - #[test] - fn test_parse_overflowing_int() { - let query_ok = format!("mutation {{ add(big: {}) }} ", std::i32::MAX); - let query_overflow = format!("mutation {{ add(big: {}0000) }} ", std::i32::MAX); - assert!(parse_query(query_ok).is_ok()); - assert!(parse_query(query_overflow).is_ok()); - } -} diff --git a/async-graphql-parser/src/pos.rs b/async-graphql-parser/src/pos.rs index 8afcfa5c..16224d51 100644 --- a/async-graphql-parser/src/pos.rs +++ b/async-graphql-parser/src/pos.rs @@ -3,6 +3,10 @@ use std::borrow::{Borrow, BorrowMut}; use std::cmp::Ordering; use std::fmt; use std::hash::{Hash, Hasher}; +use std::str::Chars; +use pest::RuleType; +use pest::iterators::Pair; +use crate::Error; /// Original position of an element in source code. #[derive(PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Default, Hash, Serialize)] @@ -35,6 +39,39 @@ pub struct Positioned { pub node: T, } +impl Positioned { + /// Create a new positioned node from the node and its position. + #[must_use] + pub const fn new(node: T, pos: Pos) -> Positioned { + Positioned { node, pos } + } + + /// Get the inner node. + /// + /// This is most useful in callback chains where `Positioned::into_inner` is easier to read than + /// `|positioned| positioned.node`. + #[inline] + pub fn into_inner(self) -> T { + self.node + } + + /// Create a new positioned node with the same position as this one. + #[must_use] + pub fn position_node(&self, other: U) -> Positioned { + Positioned::new(other, self.pos) + } + + /// Map the inner value of this positioned node. + #[must_use] + pub fn map(self, f: impl FnOnce(T) -> U) -> Positioned { + Positioned::new(f(self.node), self.pos) + } + + pub(crate) fn error_here(&self, message: impl Into) -> Error { + Error::new(message, self.pos) + } +} + impl fmt::Display for Positioned { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.node.fmt(f) @@ -74,16 +111,45 @@ impl BorrowMut for Positioned { } } -impl Positioned { - /// Create a new positioned node from the node and its position. - #[must_use] - pub const fn new(node: T, pos: Pos) -> Positioned { - Positioned { node, pos } +pub(crate) struct PositionCalculator<'a> { + input: Chars<'a>, + pos: usize, + line: usize, + column: usize, +} + +impl<'a> PositionCalculator<'a> { + pub(crate) fn new(input: &'a str) -> PositionCalculator<'a> { + Self { + input: input.chars(), + pos: 0, + line: 1, + column: 1, + } } - /// Get the inner node. - #[inline] - pub fn into_inner(self) -> T { - self.node + pub(crate) fn step(&mut self, pair: &Pair) -> Pos { + let pos = pair.as_span().start(); + debug_assert!(pos >= self.pos); + for _ in 0..pos - self.pos { + match self.input.next() { + Some('\r') => { + self.column = 1; + } + Some('\n') => { + self.line += 1; + self.column = 1; + } + Some(_) => { + self.column += 1; + } + None => break, + } + } + self.pos = pos; + Pos { + line: self.line, + column: self.column, + } } } diff --git a/async-graphql-parser/src/types.rs b/async-graphql-parser/src/types.rs deleted file mode 100644 index 1edb8fed..00000000 --- a/async-graphql-parser/src/types.rs +++ /dev/null @@ -1,551 +0,0 @@ -//! GraphQL types. - -use crate::pos::{Pos, Positioned}; -use serde::{Serialize, Serializer}; -use std::collections::{BTreeMap, HashMap}; -use std::fmt::{self, Formatter, Write}; -use std::fs::File; - -/// A complete GraphQL file or request string. -/// -/// [Reference](https://spec.graphql.org/June2018/#Document). -#[derive(Debug, Clone)] -pub struct Document { - /// The definitions of the document. - pub definitions: Vec, -} - -impl Document { - /// Convert the document into an [`ExecutableDocument`](struct.ExecutableDocument). Will return - /// `None` if there is no operation in the document. - /// - /// The `operation_name` parameter, if set, makes sure that the main operation of the document, - /// if named, will have that name. - #[must_use] - pub fn into_executable(self, operation_name: Option<&str>) -> Option { - let mut operation = None; - let mut fragments = HashMap::new(); - - for definition in self.definitions { - match definition { - Definition::Operation(op) - if operation_name - .zip(op.node.name.as_ref()) - .map_or(false, |(required_name, op_name)| { - required_name != op_name.node - }) => {} - Definition::Operation(op) => { - operation.get_or_insert(op); - } - Definition::Fragment(fragment) => { - fragments.insert(fragment.node.name.node.clone(), fragment); - } - } - } - operation.map(|operation| ExecutableDocument { - operation, - fragments, - }) - } -} - -/// An executable document. This is a [`Document`](struct.Document.html) with at least one -/// operation, and any number of fragments. -#[derive(Debug, Clone)] -pub struct ExecutableDocument { - /// The main operation of the document. - pub operation: Positioned, - /// The fragments of the document. - pub fragments: HashMap>, -} - -/// An executable definition in a query; a query, mutation, subscription or fragment definition. -/// -/// [Reference](https://spec.graphql.org/June2018/#ExecutableDefinition). -#[derive(Debug, Clone)] -pub enum Definition { - /// The definition of an operation. - Operation(Positioned), - /// The definition of a fragment. - Fragment(Positioned), -} - -impl Definition { - /// Get the position of the definition. - #[must_use] - pub fn pos(&self) -> Pos { - match self { - Self::Operation(op) => op.pos, - Self::Fragment(frag) => frag.pos, - } - } - - /// Get a reference to the directives of the definition. - #[must_use] - pub fn directives(&self) -> &Vec> { - match self { - Self::Operation(op) => &op.node.directives, - Self::Fragment(frag) => &frag.node.directives, - } - } - /// Get a mutable reference to the directives of the definition. - #[must_use] - pub fn directives_mut(&mut self) -> &mut Vec> { - match self { - Self::Operation(op) => &mut op.node.directives, - Self::Fragment(frag) => &mut frag.node.directives, - } - } -} - -/// A GraphQL operation, such as `mutation($content:String!) { makePost(content: $content) { id } }`. -/// -/// [Reference](https://spec.graphql.org/June2018/#OperationDefinition). -#[derive(Debug, Clone)] -pub struct OperationDefinition { - /// The type of operation. - pub ty: OperationType, - /// The name of the operation. - pub name: Option>, - /// The variable definitions. - pub variable_definitions: Vec>, - /// The operation's directives. - pub directives: Vec>, - /// The operation's selection set. - pub selection_set: Positioned, -} - -/// The type of an operation; `query`, `mutation` or `subscription`. -/// -/// [Reference](https://spec.graphql.org/June2018/#OperationType). -#[derive(Debug, PartialEq, Eq, Copy, Clone)] -pub enum OperationType { - /// A query. - Query, - /// A mutation. - Mutation, - /// A subscription. - Subscription, -} - -impl fmt::Display for OperationType { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - f.write_str(match self { - Self::Query => "query", - Self::Mutation => "mutation", - Self::Subscription => "subscription", - }) - } -} - -/// A variable definition inside a list of variable definitions, for example `$name:String!`. -/// -/// [Reference](https://spec.graphql.org/June2018/#VariableDefinition). -#[derive(Debug, Clone)] -pub struct VariableDefinition { - /// The name of the variable, without the preceding `$`. - pub name: Positioned, - /// The type of the variable. - pub var_type: Positioned, - /// The optional default value of the variable. - pub default_value: Option>, -} - -impl VariableDefinition { - /// Get the default value of the variable; this is `default_value` if it is present, - /// `Value::Null` if it is nullable and `None` otherwise. - #[must_use] - pub fn default_value(&self) -> Option<&Value> { - self.default_value - .as_ref() - .map(|value| &value.node) - .or_else(|| { - if self.var_type.node.nullable { - Some(&Value::Null) - } else { - None - } - }) - } -} - -/// A GraphQL type, for example `String` or `[String!]!`. -/// -/// [Reference](https://spec.graphql.org/June2018/#Type). -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct Type { - /// The base type. - pub base: BaseType, - /// Whether the type is nullable. - pub nullable: bool, -} - -impl Type { - /// Create a type from the type string. - #[must_use] - pub fn new(ty: &str) -> Self { - let (nullable, ty) = if let Some(rest) = ty.strip_suffix('!') { - (false, rest) - } else { - (true, ty) - }; - - Self { - base: if let Some(ty) = ty.strip_prefix('[').and_then(|ty| ty.strip_suffix(']')) { - BaseType::List(Box::new(Self::new(ty))) - } else { - BaseType::Named(ty.to_owned()) - }, - nullable, - } - } -} - -impl fmt::Display for Type { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.base.fmt(f)?; - if !self.nullable { - f.write_char('!')?; - } - Ok(()) - } -} - -/// A GraphQL base type, for example `String` or `[String!]`. This does not include whether the -/// type is nullable; for that see [Type](struct.Type.html). -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum BaseType { - /// A named type, such as `String`. - Named(String), - /// A list type, such as `[String]`. - List(Box), -} - -impl fmt::Display for BaseType { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Named(name) => f.write_str(name), - Self::List(ty) => write!(f, "[{}]", ty), - } - } -} - -/// A GraphQL value, for example `1`, `$name` or `"Hello World!"`. -/// -/// [Reference](https://spec.graphql.org/June2018/#Value). -#[derive(Clone, Debug, Serialize, PartialEq, Eq)] -#[serde(untagged)] -pub enum Value { - /// `null`. - Null, - /// A variable, without the `$`. - #[serde(serialize_with = "serialize_variable")] - Variable(String), - /// A number. - Number(serde_json::Number), - /// A string. - String(String), - /// A boolean. - Boolean(bool), - /// An enum. These are typically in `SCREAMING_SNAKE_CASE`. - Enum(String), - /// A list of values. - List(Vec), - /// An object. This is a map of keys to values. - Object(BTreeMap), - /// An uploaded file. - #[serde(serialize_with = "serialize_upload")] - Upload(UploadValue), -} -fn serialize_variable(name: &str, serializer: S) -> Result { - serializer.serialize_str(&format!("${}", name)) -} -fn serialize_upload(_: &UploadValue, serializer: S) -> Result { - serializer.serialize_unit() -} - -impl Default for Value { - fn default() -> Self { - Value::Null - } -} - -impl fmt::Display for Value { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Value::Variable(name) => write!(f, "${}", name), - Value::Number(num) => write!(f, "{}", *num), - Value::String(val) => write_quoted(val, f), - Value::Boolean(true) => f.write_str("true"), - Value::Boolean(false) => f.write_str("false"), - Value::Null | Value::Upload(_) => f.write_str("null"), - Value::Enum(name) => f.write_str(name), - Value::List(items) => { - f.write_char('[')?; - for (i, item) in items.iter().enumerate() { - if i != 0 { - f.write_str(", ")?; - } - item.fmt(f)?; - } - f.write_char(']') - } - Value::Object(items) => { - f.write_str("{")?; - for (i, (name, value)) in items.iter().enumerate() { - write!(f, "{}{}: {}", if i == 0 { "" } else { ", " }, name, value)?; - } - f.write_str("}") - } - } - } -} - -fn write_quoted(s: &str, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_char('"')?; - for c in s.chars() { - match c { - '\r' => f.write_str("\\r"), - '\n' => f.write_str("\\n"), - '\t' => f.write_str("\\t"), - '"' => f.write_str("\\\""), - '\\' => f.write_str("\\\\"), - c if c.is_control() => write!(f, "\\u{:04}", c as u32), - c => f.write_char(c), - }? - } - f.write_char('"') -} - -impl From for serde_json::Value { - fn from(value: Value) -> Self { - match value { - Value::Null | Value::Upload(_) => serde_json::Value::Null, - Value::Variable(name) => name.into(), - Value::Number(n) => serde_json::Value::Number(n), - Value::String(s) => s.into(), - Value::Boolean(v) => v.into(), - Value::Enum(e) => e.into(), - Value::List(values) => values - .into_iter() - .map(Into::into) - .collect::>() - .into(), - Value::Object(obj) => serde_json::Value::Object( - obj.into_iter() - .map(|(name, value)| (name, value.into())) - .collect(), - ), - } - } -} - -impl From for Value { - fn from(value: serde_json::Value) -> Self { - match value { - serde_json::Value::Null => Value::Null, - serde_json::Value::Bool(n) => Value::Boolean(n), - serde_json::Value::Number(n) => Value::Number(n), - serde_json::Value::String(s) => Value::String(s), - serde_json::Value::Array(ls) => Value::List(ls.into_iter().map(Into::into).collect()), - serde_json::Value::Object(obj) => Value::Object( - obj.into_iter() - .map(|(name, value)| (name, value.into())) - .collect(), - ), - } - } -} - -/// A file upload value. -pub struct UploadValue { - /// The name of the file. - pub filename: String, - /// The content type of the file. - pub content_type: Option, - /// The file data. - pub content: File, -} - -impl UploadValue { - /// Attempt to clone the upload value. This type's `Clone` implementation simply calls this and - /// panics on failure. - /// - /// # Errors - /// - /// Fails if cloning the inner `File` fails. - pub fn try_clone(&self) -> std::io::Result { - Ok(Self { - filename: self.filename.clone(), - content_type: self.content_type.clone(), - content: self.content.try_clone()?, - }) - } -} - -impl Clone for UploadValue { - fn clone(&self) -> Self { - self.try_clone().unwrap() - } -} - -impl fmt::Debug for UploadValue { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "Upload({})", self.filename) - } -} - -impl PartialEq for UploadValue { - fn eq(&self, other: &Self) -> bool { - self.filename == other.filename - } -} -impl Eq for UploadValue {} - -/// A set of fields to be selected, for example `{ name age }`. -/// -/// [Reference](https://spec.graphql.org/June2018/#SelectionSet). -#[derive(Debug, Default, Clone)] -pub struct SelectionSet { - /// The fields to be selected. - pub items: Vec>, -} - -/// A part of an object to be selected; a single field, a fragment spread or an inline fragment. -/// -/// [Reference](https://spec.graphql.org/June2018/#Selection). -#[derive(Debug, Clone)] -pub enum Selection { - /// Select a single field, such as `name` or `weightKilos: weight(unit: KILOGRAMS)`. - Field(Positioned), - /// Select using a fragment. - FragmentSpread(Positioned), - /// Select using an inline fragment. - InlineFragment(Positioned), -} - -impl Selection { - /// Get a reference to the directives of the selection. - #[must_use] - pub fn directives(&self) -> &Vec> { - match self { - Self::Field(field) => &field.node.directives, - Self::FragmentSpread(spread) => &spread.node.directives, - Self::InlineFragment(fragment) => &fragment.node.directives, - } - } - /// Get a mutable reference to the directives of the selection. - #[must_use] - pub fn directives_mut(&mut self) -> &mut Vec> { - match self { - Self::Field(field) => &mut field.node.directives, - Self::FragmentSpread(spread) => &mut spread.node.directives, - Self::InlineFragment(fragment) => &mut fragment.node.directives, - } - } -} - -/// A field being selected on an object, such as `name` or `weightKilos: weight(unit: KILOGRAMS)`. -/// -/// [Reference](https://spec.graphql.org/June2018/#Field). -#[derive(Debug, Clone)] -pub struct Field { - /// The optional field alias. - pub alias: Option>, - /// The name of the field. - pub name: Positioned, - /// The arguments to the field, empty if no arguments are provided. - pub arguments: Vec<(Positioned, Positioned)>, - /// The directives in the field selector. - pub directives: Vec>, - /// The subfields being selected in this field, if it is an object. Empty if no fields are - /// being selected. - pub selection_set: Positioned, -} - -impl Field { - /// Get the response key of the field. This is the alias if present and the name otherwise. - #[must_use] - pub fn response_key(&self) -> &Positioned { - self.alias.as_ref().unwrap_or(&self.name) - } - - /// Get the value of the argument with the specified name. - #[must_use] - pub fn get_argument(&self, name: &str) -> Option<&Positioned> { - self.arguments - .iter() - .find(|item| item.0.node == name) - .map(|item| &item.1) - } -} - -/// A fragment selector, such as `... userFields`. -/// -/// [Reference](https://spec.graphql.org/June2018/#FragmentSpread). -#[derive(Debug, Clone)] -pub struct FragmentSpread { - /// The name of the fragment being selected. - pub fragment_name: Positioned, - /// The directives in the fragment selector. - pub directives: Vec>, -} - -/// An inline fragment selector, such as `... on User { name }`. -/// -/// [Reference](https://spec.graphql.org/June2018/#InlineFragment). -#[derive(Debug, Clone)] -pub struct InlineFragment { - /// The type condition. - pub type_condition: Option>, - /// The directives in the inline fragment. - pub directives: Vec>, - /// The selected fields of the fragment. - pub selection_set: Positioned, -} - -/// The definition of a fragment, such as `fragment userFields on User { name age }`. -/// -/// [Reference](https://spec.graphql.org/June2018/#FragmentDefinition). -#[derive(Debug, Clone)] -pub struct FragmentDefinition { - /// The name of the fragment. Any name is allowed except `on`. - pub name: Positioned, - /// The type this fragment operates on. - pub type_condition: Positioned, - /// Directives in the fragment. - pub directives: Vec>, - /// The fragment's selection set. - pub selection_set: Positioned, -} - -/// A type a fragment can apply to (`on` followed by the type). -/// -/// [Reference](https://spec.graphql.org/June2018/#TypeCondition). -#[derive(Debug, Clone)] -pub struct TypeCondition { - /// The type this fragment applies to. - pub on: Positioned, -} - -/// A GraphQL directive, such as `@deprecated(reason: "Use the other field")`. -/// -/// [Reference](https://spec.graphql.org/June2018/#Directive). -#[derive(Debug, Clone)] -pub struct Directive { - /// The name of the directive. - pub name: Positioned, - /// The arguments to the directive. - pub arguments: Vec<(Positioned, Positioned)>, -} - -impl Directive { - /// Get the argument with the given name. - #[must_use] - pub fn get_argument(&self, name: &str) -> Option<&Positioned> { - self.arguments - .iter() - .find(|item| item.0.node == name) - .map(|item| &item.1) - } -} diff --git a/async-graphql-parser/src/types/executable.rs b/async-graphql-parser/src/types/executable.rs new file mode 100644 index 00000000..82e72cf1 --- /dev/null +++ b/async-graphql-parser/src/types/executable.rs @@ -0,0 +1,269 @@ +//! Executable document-related GraphQL types. + +use super::*; + +/// An executable GraphQL file or request string. +/// +/// [Reference](https://spec.graphql.org/June2018/#ExecutableDocument). +#[derive(Debug, Clone)] +pub struct ExecutableDocument { + /// The definitions of the document. + pub definitions: Vec, +} + +impl ExecutableDocument { + /// Convert the document into an [`ExecutableDocumentData`](struct.ExecutableDocumentData.html). + /// Will return `None` if there is no operation in the document. + /// + /// The `operation_name` parameter, if set, makes sure that the main operation of the document, + /// if named, will have that name. + #[must_use] + pub fn into_data(self, operation_name: Option<&str>) -> Option { + let mut operation = None; + let mut fragments = HashMap::new(); + + for definition in self.definitions { + match definition { + ExecutableDefinition::Operation(op) + if operation_name + .zip(op.node.name.as_ref()) + .map_or(false, |(required_name, op_name)| { + required_name != op_name.node + }) => {} + ExecutableDefinition::Operation(op) => { + operation.get_or_insert(op); + } + ExecutableDefinition::Fragment(fragment) => { + fragments.insert(fragment.node.name.node.clone(), fragment); + } + } + } + operation.map(|operation| ExecutableDocumentData { + operation, + fragments, + }) + } +} + +/// The data of an executable document. This is a [`ExecutableDocument`](struct.ExecutableDocument.html) with at least +/// one operation, and any number of fragments. +#[derive(Debug, Clone)] +pub struct ExecutableDocumentData { + /// The main operation of the document. + pub operation: Positioned, + /// The fragments of the document. + pub fragments: HashMap>, +} + +/// An executable definition in a query; a query, mutation, subscription or fragment definition. +/// +/// [Reference](https://spec.graphql.org/June2018/#ExecutableDefinition). +#[derive(Debug, Clone)] +pub enum ExecutableDefinition { + /// The definition of an operation. + Operation(Positioned), + /// The definition of a fragment. + Fragment(Positioned), +} + +impl ExecutableDefinition { + /// Get the position of the definition. + #[must_use] + pub fn pos(&self) -> Pos { + match self { + Self::Operation(op) => op.pos, + Self::Fragment(frag) => frag.pos, + } + } + + /// Get a reference to the directives of the definition. + #[must_use] + pub fn directives(&self) -> &Vec> { + match self { + Self::Operation(op) => &op.node.directives, + Self::Fragment(frag) => &frag.node.directives, + } + } + /// Get a mutable reference to the directives of the definition. + #[must_use] + pub fn directives_mut(&mut self) -> &mut Vec> { + match self { + Self::Operation(op) => &mut op.node.directives, + Self::Fragment(frag) => &mut frag.node.directives, + } + } +} + +/// A GraphQL operation, such as `mutation($content:String!) { makePost(content: $content) { id } }`. +/// +/// [Reference](https://spec.graphql.org/June2018/#OperationDefinition). +#[derive(Debug, Clone)] +pub struct OperationDefinition { + /// The type of operation. + pub ty: OperationType, + /// The name of the operation. + pub name: Option>, + /// The variable definitions. + pub variable_definitions: Vec>, + /// The operation's directives. + pub directives: Vec>, + /// The operation's selection set. + pub selection_set: Positioned, +} + +/// A variable definition inside a list of variable definitions, for example `$name:String!`. +/// +/// [Reference](https://spec.graphql.org/June2018/#VariableDefinition). +#[derive(Debug, Clone)] +pub struct VariableDefinition { + /// The name of the variable, without the preceding `$`. + pub name: Positioned, + /// The type of the variable. + pub var_type: Positioned, + /// The optional default value of the variable. + pub default_value: Option>, +} + +impl VariableDefinition { + /// Get the default value of the variable; this is `default_value` if it is present, + /// `Value::Null` if it is nullable and `None` otherwise. + #[must_use] + pub fn default_value(&self) -> Option<&ConstValue> { + self.default_value + .as_ref() + .map(|value| &value.node) + .or_else(|| { + if self.var_type.node.nullable { + Some(&ConstValue::Null) + } else { + None + } + }) + } +} + +/// A set of fields to be selected, for example `{ name age }`. +/// +/// [Reference](https://spec.graphql.org/June2018/#SelectionSet). +#[derive(Debug, Default, Clone)] +pub struct SelectionSet { + /// The fields to be selected. + pub items: Vec>, +} + +/// A part of an object to be selected; a single field, a fragment spread or an inline fragment. +/// +/// [Reference](https://spec.graphql.org/June2018/#Selection). +#[derive(Debug, Clone)] +pub enum Selection { + /// Select a single field, such as `name` or `weightKilos: weight(unit: KILOGRAMS)`. + Field(Positioned), + /// Select using a fragment. + FragmentSpread(Positioned), + /// Select using an inline fragment. + InlineFragment(Positioned), +} + +impl Selection { + /// Get a reference to the directives of the selection. + #[must_use] + pub fn directives(&self) -> &Vec> { + match self { + Self::Field(field) => &field.node.directives, + Self::FragmentSpread(spread) => &spread.node.directives, + Self::InlineFragment(fragment) => &fragment.node.directives, + } + } + /// Get a mutable reference to the directives of the selection. + #[must_use] + pub fn directives_mut(&mut self) -> &mut Vec> { + match self { + Self::Field(field) => &mut field.node.directives, + Self::FragmentSpread(spread) => &mut spread.node.directives, + Self::InlineFragment(fragment) => &mut fragment.node.directives, + } + } +} + +/// A field being selected on an object, such as `name` or `weightKilos: weight(unit: KILOGRAMS)`. +/// +/// [Reference](https://spec.graphql.org/June2018/#Field). +#[derive(Debug, Clone)] +pub struct Field { + /// The optional field alias. + pub alias: Option>, + /// The name of the field. + pub name: Positioned, + /// The arguments to the field, empty if no arguments are provided. + pub arguments: Vec<(Positioned, Positioned)>, + /// The directives in the field selector. + pub directives: Vec>, + /// The subfields being selected in this field, if it is an object. Empty if no fields are + /// being selected. + pub selection_set: Positioned, +} + +impl Field { + /// Get the response key of the field. This is the alias if present and the name otherwise. + #[must_use] + pub fn response_key(&self) -> &Positioned { + self.alias.as_ref().unwrap_or(&self.name) + } + + /// Get the value of the argument with the specified name. + #[must_use] + pub fn get_argument(&self, name: &str) -> Option<&Positioned> { + self.arguments + .iter() + .find(|item| item.0.node == name) + .map(|item| &item.1) + } +} + +/// A fragment selector, such as `... userFields`. +/// +/// [Reference](https://spec.graphql.org/June2018/#FragmentSpread). +#[derive(Debug, Clone)] +pub struct FragmentSpread { + /// The name of the fragment being selected. + pub fragment_name: Positioned, + /// The directives in the fragment selector. + pub directives: Vec>, +} + +/// An inline fragment selector, such as `... on User { name }`. +/// +/// [Reference](https://spec.graphql.org/June2018/#InlineFragment). +#[derive(Debug, Clone)] +pub struct InlineFragment { + /// The type condition. + pub type_condition: Option>, + /// The directives in the inline fragment. + pub directives: Vec>, + /// The selected fields of the fragment. + pub selection_set: Positioned, +} + +/// The definition of a fragment, such as `fragment userFields on User { name age }`. +/// +/// [Reference](https://spec.graphql.org/June2018/#FragmentDefinition). +#[derive(Debug, Clone)] +pub struct FragmentDefinition { + /// The name of the fragment. Any name is allowed except `on`. + pub name: Positioned, + /// The type this fragment operates on. + pub type_condition: Positioned, + /// Directives in the fragment. + pub directives: Vec>, + /// The fragment's selection set. + pub selection_set: Positioned, +} + +/// A type a fragment can apply to (`on` followed by the type). +/// +/// [Reference](https://spec.graphql.org/June2018/#TypeCondition). +#[derive(Debug, Clone)] +pub struct TypeCondition { + /// The type this fragment applies to. + pub on: Positioned, +} diff --git a/async-graphql-parser/src/types/mod.rs b/async-graphql-parser/src/types/mod.rs new file mode 100644 index 00000000..096c4165 --- /dev/null +++ b/async-graphql-parser/src/types/mod.rs @@ -0,0 +1,611 @@ +//! GraphQL types. +//! +//! The two root types are [`ExecutableDocument`](struct.ExecutableDocument.html) and +//! [`ServiceDocument`](struct.ServiceDocument.html), representing an executable GraphQL query and a +//! GraphQL service respectively. +//! +//! This follows the [June 2018 edition of the GraphQL spec](https://spec.graphql.org/June2018/). + +use crate::pos::{Pos, Positioned}; +use serde::{Serialize, Deserialize}; +use serde::ser::{Serializer, Error as _}; +use serde::de::{Deserializer, Error as _, Unexpected}; +use std::collections::{BTreeMap, HashMap}; +use std::fmt::{self, Display, Formatter, Write}; +use std::fs::File; +use std::convert::{TryFrom, TryInto}; +use std::borrow::Borrow; +use std::ops::Deref; + +pub use executable::*; +pub use service::*; +pub use serde_json::Number; + +mod executable; +mod service; + +/// The type of an operation; `query`, `mutation` or `subscription`. +/// +/// [Reference](https://spec.graphql.org/June2018/#OperationType). +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +pub enum OperationType { + /// A query. + Query, + /// A mutation. + Mutation, + /// A subscription. + Subscription, +} + +impl Display for OperationType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(match self { + Self::Query => "query", + Self::Mutation => "mutation", + Self::Subscription => "subscription", + }) + } +} + +/// A GraphQL type, for example `String` or `[String!]!`. +/// +/// [Reference](https://spec.graphql.org/June2018/#Type). +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Type { + /// The base type. + pub base: BaseType, + /// Whether the type is nullable. + pub nullable: bool, +} + +impl Type { + /// Create a type from the type string. + #[must_use] + pub fn new(ty: &str) -> Option { + let (nullable, ty) = if let Some(rest) = ty.strip_suffix('!') { + (false, rest) + } else { + (true, ty) + }; + + Some(Self { + base: if let Some(ty) = ty.strip_prefix('[') { + BaseType::List(Box::new(Self::new(ty.strip_suffix(']')?)?)) + } else { + BaseType::Named(Name::new(ty.to_owned()).ok()?) + }, + nullable, + }) + } +} + +impl Display for Type { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + self.base.fmt(f)?; + if !self.nullable { + f.write_char('!')?; + } + Ok(()) + } +} + +/// A GraphQL base type, for example `String` or `[String!]`. This does not include whether the +/// type is nullable; for that see [Type](struct.Type.html). +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum BaseType { + /// A named type, such as `String`. + Named(Name), + /// A list type, such as `[String]`. + List(Box), +} + +impl Display for BaseType { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::Named(name) => f.write_str(name), + Self::List(ty) => write!(f, "[{}]", ty), + } + } +} + +/// A resolved GraphQL value, for example `1` or `"Hello World!"`. +/// +/// It can be serialized and deserialized. Enums will be converted to strings. Attempting to +/// serialize `Upload` will fail, and `Enum` and `Upload` cannot be deserialized. +/// +/// [Reference](https://spec.graphql.org/June2018/#Value). +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum ConstValue { + /// `null`. + Null, + /// A number. + Number(Number), + /// A string. + String(String), + /// A boolean. + Boolean(bool), + /// An enum. These are typically in `SCREAMING_SNAKE_CASE`. + #[serde(skip_deserializing)] + Enum(Name), + /// A list of values. + List(Vec), + /// An object. This is a map of keys to values. + Object(BTreeMap), + /// An uploaded file. + #[serde(serialize_with = "fail_serialize_upload", skip_deserializing)] + Upload(UploadValue), +} + +impl ConstValue { + /// Convert this `ConstValue` into a `Value`. + #[must_use] + pub fn into_value(self) -> Value { + match self { + Self::Null => Value::Null, + Self::Number(num) => Value::Number(num), + Self::String(s) => Value::String(s), + Self::Boolean(b) => Value::Boolean(b), + Self::Enum(v) => Value::Enum(v), + Self::List(items) => Value::List(items.into_iter().map(ConstValue::into_value).collect()), + Self::Object(map) => Value::Object(map.into_iter().map(|(key, value)| (key, value.into_value())).collect()), + Self::Upload(upload) => Value::Upload(upload), + } + } + + /// Attempt to convert the value into JSON. This is equivalent to the `TryFrom` implementation. + /// + /// # Errors + /// + /// Fails if serialization fails (see enum docs for more info). + pub fn into_json(self) -> serde_json::Result { + self.try_into() + } + + /// Attempt to convert JSON into a value. This is equivalent to the `TryFrom` implementation. + /// + /// # Errors + /// + /// Fails if deserialization fails (see enum docs for more info). + pub fn from_json(json: serde_json::Value) -> serde_json::Result { + json.try_into() + } +} + +impl Default for ConstValue { + fn default() -> Self { + Self::Null + } +} + +impl Display for ConstValue { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::Number(num) => write!(f, "{}", *num), + Self::String(val) => write_quoted(val, f), + Self::Boolean(true) => f.write_str("true"), + Self::Boolean(false) => f.write_str("false"), + Self::Null | Self::Upload(_) => f.write_str("null"), + Self::Enum(name) => f.write_str(name), + Self::List(items) => write_list(items, f), + Self::Object(map) => write_object(map, f), + } + } +} + +impl TryFrom for ConstValue { + type Error = serde_json::Error; + fn try_from(value: serde_json::Value) -> Result { + Self::deserialize(value) + } +} +impl TryFrom for serde_json::Value { + type Error = serde_json::Error; + fn try_from(value: ConstValue) -> Result { + serde_json::to_value(value) + } +} + +/// A GraphQL value, for example `1`, `$name` or `"Hello World!"`. This is +/// [`ConstValue`](enum.ConstValue.html) with variables. +/// +/// It can be serialized and deserialized. Enums will be converted to strings. Attempting to +/// serialize `Upload` or `Variable` will fail, and `Enum`, `Upload` and `Variable` cannot be +/// deserialized. +/// +/// [Reference](https://spec.graphql.org/June2018/#Value). +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(untagged)] +pub enum Value { + /// A variable, without the `$`. + #[serde(serialize_with = "fail_serialize_variable", skip_deserializing)] + Variable(Name), + /// `null`. + Null, + /// A number. + Number(Number), + /// A string. + String(String), + /// A boolean. + Boolean(bool), + /// An enum. These are typically in `SCREAMING_SNAKE_CASE`. + #[serde(skip_deserializing)] + Enum(Name), + /// A list of values. + List(Vec), + /// An object. This is a map of keys to values. + Object(BTreeMap), + /// An uploaded file. + #[serde(serialize_with = "fail_serialize_upload", skip_deserializing)] + Upload(UploadValue), +} + +impl Value { + /// Attempt to convert the value into a const value by using a function to get a variable. + pub fn into_const_with(self, mut f: impl FnMut(Name) -> Result) -> Result { + self.into_const_with_mut(&mut f) + } + + fn into_const_with_mut(self, f: &mut impl FnMut(Name) -> Result) -> Result { + Ok(match self { + Self::Variable(name) => f(name)?, + Self::Null => ConstValue::Null, + Self::Number(num) => ConstValue::Number(num), + Self::String(s) => ConstValue::String(s), + Self::Boolean(b) => ConstValue::Boolean(b), + Self::Enum(v) => ConstValue::Enum(v), + Self::List(items) => ConstValue::List(items.into_iter().map(|value| value.into_const_with_mut(f)).collect::>()?), + Self::Object(map) => ConstValue::Object(map.into_iter().map(|(key, value)| Ok((key, value.into_const_with_mut(f)?))).collect::>()?), + Self::Upload(upload) => ConstValue::Upload(upload), + }) + } + + /// Attempt to convert the value into a const value. + /// + /// Will fail if the value contains variables. + #[must_use] + pub fn into_const(self) -> Option { + self.into_const_with(|_| Err(())).ok() + } + + /// Attempt to convert the value into JSON. This is equivalent to the `TryFrom` implementation. + /// + /// # Errors + /// + /// Fails if serialization fails (see enum docs for more info). + pub fn into_json(self) -> serde_json::Result { + self.try_into() + } + + /// Attempt to convert JSON into a value. This is equivalent to the `TryFrom` implementation. + /// + /// # Errors + /// + /// Fails if deserialization fails (see enum docs for more info). + pub fn from_json(json: serde_json::Value) -> serde_json::Result { + json.try_into() + } +} + +impl Default for Value { + fn default() -> Self { + Self::Null + } +} + +impl Display for Value { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Self::Variable(name) => write!(f, "${}", name), + Self::Number(num) => write!(f, "{}", *num), + Self::String(val) => write_quoted(val, f), + Self::Boolean(true) => f.write_str("true"), + Self::Boolean(false) => f.write_str("false"), + Self::Null | Self::Upload(_) => f.write_str("null"), + Self::Enum(name) => f.write_str(name), + Self::List(items) => write_list(items, f), + Self::Object(map) => write_object(map, f), + } + } +} + +impl From for Value { + fn from(value: ConstValue) -> Self { + value.into_value() + } +} + +impl TryFrom for Value { + type Error = serde_json::Error; + fn try_from(value: serde_json::Value) -> Result { + Self::deserialize(value) + } +} +impl TryFrom for serde_json::Value { + type Error = serde_json::Error; + fn try_from(value: Value) -> Result { + serde_json::to_value(value) + } +} + +fn fail_serialize_variable(_: &str, _: S) -> Result { + Err(S::Error::custom("cannot serialize variable")) +} +fn fail_serialize_upload(_: &UploadValue, _: S) -> Result { + Err(S::Error::custom("cannot serialize uploaded file")) +} + +fn write_quoted(s: &str, f: &mut Formatter<'_>) -> fmt::Result { + f.write_char('"')?; + for c in s.chars() { + match c { + '\r' => f.write_str("\\r"), + '\n' => f.write_str("\\n"), + '\t' => f.write_str("\\t"), + '"' => f.write_str("\\\""), + '\\' => f.write_str("\\\\"), + c if c.is_control() => write!(f, "\\u{:04}", c as u32), + c => f.write_char(c), + }? + } + f.write_char('"') +} + fn write_list(list: impl IntoIterator, f: &mut Formatter<'_>) -> fmt::Result { + f.write_char('[')?; + for item in list { + item.fmt(f)?; + f.write_char(',')?; + } + f.write_char(']') +} + fn write_object(object: impl IntoIterator, f: &mut Formatter<'_>) -> fmt::Result { + f.write_char('{')?; + for (name, value) in object { + write!(f, "{}: {},", name, value)?; + } + f.write_char('}') +} + +/// A file upload value. +pub struct UploadValue { + /// The name of the file. + pub filename: String, + /// The content type of the file. + pub content_type: Option, + /// The file data. + pub content: File, +} + +impl UploadValue { + /// Attempt to clone the upload value. This type's `Clone` implementation simply calls this and + /// panics on failure. + /// + /// # Errors + /// + /// Fails if cloning the inner `File` fails. + pub fn try_clone(&self) -> std::io::Result { + Ok(Self { + filename: self.filename.clone(), + content_type: self.content_type.clone(), + content: self.content.try_clone()?, + }) + } +} + +impl Clone for UploadValue { + fn clone(&self) -> Self { + self.try_clone().unwrap() + } +} + +impl fmt::Debug for UploadValue { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "Upload({})", self.filename) + } +} + +impl PartialEq for UploadValue { + fn eq(&self, other: &Self) -> bool { + self.filename == other.filename + } +} +impl Eq for UploadValue {} + +/// A const GraphQL directive, such as `@deprecated(reason: "Use the other field)`. This differs +/// from [`Directive`](struct.Directive.html) in that it uses [`ConstValue`](enum.ConstValue.html) +/// instead of [`Value`](enum.Value.html). +/// +/// [Reference](https://spec.graphql.org/June2018/#Directive). +#[derive(Debug, Clone)] +pub struct ConstDirective { + /// The name of the directive. + pub name: Positioned, + /// The arguments to the directive. + pub arguments: Vec<(Positioned, Positioned)>, +} + +impl ConstDirective { + /// Convert this `ConstDirective` into a `Directive`. + #[must_use] + pub fn into_directive(self) -> Directive { + Directive { + name: self.name, + arguments: self.arguments.into_iter().map(|(name, value)| (name, value.map(ConstValue::into_value))).collect(), + } + } + + /// Get the argument with the given name. + #[must_use] + pub fn get_argument(&self, name: &str) -> Option<&Positioned> { + self.arguments + .iter() + .find(|item| item.0.node == name) + .map(|item| &item.1) + } +} + +/// A GraphQL directive, such as `@deprecated(reason: "Use the other field")`. +/// +/// [Reference](https://spec.graphql.org/June2018/#Directive). +#[derive(Debug, Clone)] +pub struct Directive { + /// The name of the directive. + pub name: Positioned, + /// The arguments to the directive. + pub arguments: Vec<(Positioned, Positioned)>, +} + +impl Directive { + /// Attempt to convert this `Directive` into a `ConstDirective`. + #[must_use] + pub fn into_const(self) -> Option { + Some(ConstDirective { + name: self.name, + arguments: self.arguments.into_iter().map(|(name, value)| Some((name, Positioned::new(value.node.into_const()?, value.pos)))).collect::>()?, + }) + } + + /// Get the argument with the given name. + #[must_use] + pub fn get_argument(&self, name: &str) -> Option<&Positioned> { + self.arguments + .iter() + .find(|item| item.0.node == name) + .map(|item| &item.1) + } +} + +/// A GraphQL name. This is a newtype wrapper around a string with the addition guarantee that it +/// is a valid GraphQL name (follows the regex `[_A-Za-z][_0-9A-Za-z]*`). +/// +/// [Reference](https://spec.graphql.org/June2018/#Name). +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)] +#[serde(transparent)] +pub struct Name(String); + +impl Name { + /// Check whether the name is valid (follows the regex `[_A-Za-z][_0-9A-Za-z]*`). + #[must_use] + pub fn is_valid(name: &str) -> bool { + let mut bytes = name.bytes(); + bytes.next().map_or(false, |c| c.is_ascii_alphabetic() || c == b'_') + && bytes.all(|c| c.is_ascii_alphanumeric() || c == b'_') + } + + /// Create a new name without checking whether it is valid or not. This will always check in + /// debug mode. + /// + /// This function is not `unsafe` because an invalid name does not cause UB, but care should be + /// taken to make sure it is a valid name. + #[must_use] + pub fn new_unchecked(name: String) -> Self { + debug_assert!(Self::is_valid(&name)); + Self(name) + } + + /// Create a new name, checking whether it is valid. Returns ownership of the string if it + /// fails. + /// + /// # Errors + /// + /// Fails if the name is not a valid name. + pub fn new(name: String) -> Result { + if Self::is_valid(&name) { + Ok(Self(name)) + } else { + Err(name) + } + } + + /// Get the name as a string. + #[must_use] + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Convert the name to a `String`. + #[must_use] + pub fn into_string(self) -> String { + self.0 + } +} + +impl AsRef for Name { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl Borrow for Name { + fn borrow(&self) -> &str { + &self.0 + } +} + +impl From for String { + fn from(name: Name) -> Self { + name.0 + } +} + +impl Deref for Name { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Display for Name { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + Display::fmt(&self.0, f) + } +} + +impl PartialEq for Name { + fn eq(&self, other: &String) -> bool { + self.0 == *other + } +} +impl PartialEq for Name { + fn eq(&self, other: &str) -> bool { + self.0 == other + } +} +impl PartialEq for String { + fn eq(&self, other: &Name) -> bool { + other == self + } +} +impl PartialEq for str { + fn eq(&self, other: &Name) -> bool { + other == self + } +} +impl<'a> PartialEq<&'a str> for Name { + fn eq(&self, other: &&'a str) -> bool { + self == *other + } +} +impl<'a> PartialEq for &'a str { + fn eq(&self, other: &Name) -> bool { + other == self + } +} + +impl<'de> Deserialize<'de> for Name { + fn deserialize>(deserializer: D) -> Result { + Self::new(String::deserialize(deserializer)?).map_err(|s| D::Error::invalid_value(Unexpected::Str(&s), &"a GraphQL name")) + } +} + +#[cfg(test)] +#[test] +fn test_valid_names() { + assert!(Name::is_valid("valid_name")); + assert!(Name::is_valid("numbers123_456_789abc")); + assert!(Name::is_valid("MiXeD_CaSe")); + assert!(Name::is_valid("_")); + assert!(!Name::is_valid("invalid name")); + assert!(!Name::is_valid("123and_text")); +} diff --git a/async-graphql-parser/src/types/service.rs b/async-graphql-parser/src/types/service.rs new file mode 100644 index 00000000..852b01d5 --- /dev/null +++ b/async-graphql-parser/src/types/service.rs @@ -0,0 +1,233 @@ +//! Service-related GraphQL types. + +use super::*; + +/// An GraphQL file or request string defining a GraphQL service. +/// +/// [Reference](https://spec.graphql.org/June2018/#Document). +#[derive(Debug, Clone)] +pub struct ServiceDocument { + /// The definitions of this document. + pub definitions: Vec, +} + +/// A definition concerning the type system of a GraphQL service. +/// +/// [Reference](https://spec.graphql.org/June2018/#TypeSystemDefinition). This enum also covers +/// [extensions](https://spec.graphql.org/June2018/#TypeSystemExtension). +#[derive(Debug, Clone)] +pub enum TypeSystemDefinition { + /// The definition of the schema of the service. + Schema(Positioned), + /// The definition of a type in the service. + Type(Positioned), + /// The definition of a directive in the service. + Directive(Positioned), +} + +/// The definition of the schema in a GraphQL service. +/// +/// [Reference](https://spec.graphql.org/June2018/#SchemaDefinition). This also covers +/// [extensions](https://spec.graphql.org/June2018/#SchemaExtension). +#[derive(Debug, Clone)] +pub struct SchemaDefinition { + /// Whether the schema is an extension of another schema. + pub extend: bool, + /// The directives of the schema definition. + pub directives: Vec>, + /// The query root. This is always `Some` when the schema is not extended. + pub query: Option>, + /// The mutation root, if present. + pub mutation: Option>, + /// The subscription root, if present. + pub subscription: Option>, +} + +/// The definition of a type in a GraphQL service. +/// +/// [Reference](https://spec.graphql.org/June2018/#TypeDefinition). This also covers +/// [extensions](https://spec.graphql.org/June2018/#TypeExtension). +#[derive(Debug, Clone)] +pub struct TypeDefinition { + /// Whether the type is an extension of another type. + pub extend: bool, + /// The description of the type, if present. This is never present on an extension type. + pub description: Option>, + /// The name of the type. + pub name: Positioned, + /// The directives of type definition. + pub directives: Vec>, + /// Which kind of type is being defined; scalar, object, enum, etc. + pub kind: TypeKind, +} + +/// A kind of type; scalar, object, enum, etc. +#[derive(Debug, Clone)] +pub enum TypeKind { + /// A scalar type. + Scalar, + /// An object type. + Object(ObjectType), + /// An interface type. + Interface(InterfaceType), + /// A union type. + Union(UnionType), + /// An enum type. + Enum(EnumType), + /// An input object type. + InputObject(InputObjectType), +} + +/// The definition of an object type. +/// +/// [Reference](https://spec.graphql.org/June2018/#ObjectType). +#[derive(Debug, Clone)] +pub struct ObjectType { + /// The interfaces implemented by the object. + pub implements: Vec>, + /// The fields of the object type. + pub fields: Vec>, +} + +/// The definition of a field inside an object or interface. +/// +/// [Reference](https://spec.graphql.org/June2018/#FieldDefinition). +#[derive(Debug, Clone)] +pub struct FieldDefinition { + /// The description of the field. + pub description: Option>, + /// The name of the field. + pub name: Positioned, + /// The arguments of the field. + pub arguments: Vec>, + /// The type of the field. + pub ty: Positioned, + /// The directives of the field. + pub directives: Vec>, +} + +/// The definition of an interface type. +/// +/// [Reference](https://spec.graphql.org/June2018/#InterfaceType). +#[derive(Debug, Clone)] +pub struct InterfaceType { + /// The fields of the interface type. + pub fields: Vec>, +} + +/// The definition of a union type. +/// +/// [Reference](https://spec.graphql.org/June2018/#UnionType). +#[derive(Debug, Clone)] +pub struct UnionType { + /// The member types of the union. + pub members: Vec>, +} + +/// The definition of an enum. +/// +/// [Reference](https://spec.graphql.org/June2018/#EnumType). +#[derive(Debug, Clone)] +pub struct EnumType { + /// The possible values of the enum. + pub values: Vec>, +} + +/// The definition of a value inside an enum. +/// +/// [Reference](https://spec.graphql.org/June2018/#EnumValueDefinition). +#[derive(Debug, Clone)] +pub struct EnumValueDefinition { + /// The description of the argument. + pub description: Option>, + /// The value name. + pub value: Positioned, + /// The directives of the enum value. + pub directives: Vec>, +} + +/// The definition of an input object. +/// +/// [Reference](https://spec.graphql.org/June2018/#InputObjectType). +#[derive(Debug, Clone)] +pub struct InputObjectType { + /// The fields of the input object. + pub fields: Vec>, +} + +/// The definition of an input value inside the arguments of a field. +/// +/// [Reference](https://spec.graphql.org/June2018/#InputValueDefinition). +#[derive(Debug, Clone)] +pub struct InputValueDefinition { + /// The description of the argument. + pub description: Option>, + /// The name of the argument. + pub name: Positioned, + /// The type of the argument. + pub ty: Positioned, + /// The default value of the argument, if there is one. + pub default_value: Option>, + /// The directives of the input value. + pub directives: Vec>, +} + +/// The definition of a directive in a service. +/// +/// [Reference](https://spec.graphql.org/June2018/#DirectiveDefinition). +#[derive(Debug, Clone)] +pub struct DirectiveDefinition { + /// The description of the directive. + pub description: Option>, + /// The name of the directive. + pub name: Positioned, + /// The arguments of the directive. + pub arguments: Vec>, + /// The locations the directive applies to. + pub locations: Vec>, +} + +/// Where a directive can apply to. +/// +/// [Reference](https://spec.graphql.org/June2018/#DirectiveLocation). +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum DirectiveLocation { + /// A [query](enum.OperationType.html#variant.Query) [operation](struct.OperationDefinition.html). + Query, + /// A [mutation](enum.OperationType.html#variant.Mutation) [operation](struct.OperationDefinition.html). + Mutation, + /// A [subscription](enum.OperationType.html#variant.Subscription) [operation](struct.OperationDefinition.html). + Subscription, + /// A [field](struct.Field.html). + Field, + /// A [fragment definition](struct.FragmentDefinition.html). + FragmentDefinition, + /// A [fragment spread](struct.FragmentSpread.html). + FragmentSpread, + /// An [inline fragment](struct.InlineFragment.html). + InlineFragment, + /// A [schema](struct.Schema.html). + Schema, + /// A [scalar](enum.TypeKind.html#variant.Scalar). + Scalar, + /// An [object](struct.ObjectType.html). + Object, + /// A [field definition](struct.FieldDefinition.html). + FieldDefinition, + /// An [input value definition](struct.InputFieldDefinition.html) as the arguments of a field + /// but not an input object. + ArgumentDefinition, + /// An [interface](struct.InterfaceType.html). + Interface, + /// A [union](struct.UnionType.html). + Union, + /// An [enum](struct.EnumType.html). + Enum, + /// A [value on an enum](struct.EnumValueDefinition.html). + EnumValue, + /// An [input object](struct.InputObjectType.html). + InputObject, + /// An [input value definition](struct.InputValueDefinition.html) on an input object but not a + /// field. + InputFieldDefinition, +} diff --git a/async-graphql-parser/tests/queries/directive_args.graphql b/async-graphql-parser/tests/executables/directive_args.graphql similarity index 100% rename from async-graphql-parser/tests/queries/directive_args.graphql rename to async-graphql-parser/tests/executables/directive_args.graphql diff --git a/async-graphql-parser/tests/queries/fragment.graphql b/async-graphql-parser/tests/executables/fragment.graphql similarity index 100% rename from async-graphql-parser/tests/queries/fragment.graphql rename to async-graphql-parser/tests/executables/fragment.graphql diff --git a/async-graphql-parser/tests/queries/fragment_spread.graphql b/async-graphql-parser/tests/executables/fragment_spread.graphql similarity index 100% rename from async-graphql-parser/tests/queries/fragment_spread.graphql rename to async-graphql-parser/tests/executables/fragment_spread.graphql diff --git a/async-graphql-parser/tests/queries/inline_fragment.graphql b/async-graphql-parser/tests/executables/inline_fragment.graphql similarity index 100% rename from async-graphql-parser/tests/queries/inline_fragment.graphql rename to async-graphql-parser/tests/executables/inline_fragment.graphql diff --git a/async-graphql-parser/tests/queries/inline_fragment_dir.graphql b/async-graphql-parser/tests/executables/inline_fragment_dir.graphql similarity index 100% rename from async-graphql-parser/tests/queries/inline_fragment_dir.graphql rename to async-graphql-parser/tests/executables/inline_fragment_dir.graphql diff --git a/async-graphql-parser/tests/queries/kitchen-sink.graphql b/async-graphql-parser/tests/executables/kitchen-sink.graphql similarity index 100% rename from async-graphql-parser/tests/queries/kitchen-sink.graphql rename to async-graphql-parser/tests/executables/kitchen-sink.graphql diff --git a/async-graphql-parser/tests/queries/kitchen-sink_canonical.graphql b/async-graphql-parser/tests/executables/kitchen-sink_canonical.graphql similarity index 100% rename from async-graphql-parser/tests/queries/kitchen-sink_canonical.graphql rename to async-graphql-parser/tests/executables/kitchen-sink_canonical.graphql diff --git a/async-graphql-parser/tests/queries/minimal.graphql b/async-graphql-parser/tests/executables/minimal.graphql similarity index 100% rename from async-graphql-parser/tests/queries/minimal.graphql rename to async-graphql-parser/tests/executables/minimal.graphql diff --git a/async-graphql-parser/tests/queries/minimal_mutation.graphql b/async-graphql-parser/tests/executables/minimal_mutation.graphql similarity index 100% rename from async-graphql-parser/tests/queries/minimal_mutation.graphql rename to async-graphql-parser/tests/executables/minimal_mutation.graphql diff --git a/async-graphql-parser/tests/queries/minimal_query.graphql b/async-graphql-parser/tests/executables/minimal_query.graphql similarity index 100% rename from async-graphql-parser/tests/queries/minimal_query.graphql rename to async-graphql-parser/tests/executables/minimal_query.graphql diff --git a/async-graphql-parser/tests/queries/multiline_string.graphql b/async-graphql-parser/tests/executables/multiline_string.graphql similarity index 100% rename from async-graphql-parser/tests/queries/multiline_string.graphql rename to async-graphql-parser/tests/executables/multiline_string.graphql diff --git a/async-graphql-parser/tests/queries/mutation_directive.graphql b/async-graphql-parser/tests/executables/mutation_directive.graphql similarity index 100% rename from async-graphql-parser/tests/queries/mutation_directive.graphql rename to async-graphql-parser/tests/executables/mutation_directive.graphql diff --git a/async-graphql-parser/tests/queries/named_query.graphql b/async-graphql-parser/tests/executables/named_query.graphql similarity index 100% rename from async-graphql-parser/tests/queries/named_query.graphql rename to async-graphql-parser/tests/executables/named_query.graphql diff --git a/async-graphql-parser/tests/queries/nested_selection.graphql b/async-graphql-parser/tests/executables/nested_selection.graphql similarity index 100% rename from async-graphql-parser/tests/queries/nested_selection.graphql rename to async-graphql-parser/tests/executables/nested_selection.graphql diff --git a/async-graphql-parser/tests/queries/query_aliases.graphql b/async-graphql-parser/tests/executables/query_aliases.graphql similarity index 100% rename from async-graphql-parser/tests/queries/query_aliases.graphql rename to async-graphql-parser/tests/executables/query_aliases.graphql diff --git a/async-graphql-parser/tests/queries/query_arguments.graphql b/async-graphql-parser/tests/executables/query_arguments.graphql similarity index 100% rename from async-graphql-parser/tests/queries/query_arguments.graphql rename to async-graphql-parser/tests/executables/query_arguments.graphql diff --git a/async-graphql-parser/tests/queries/query_directive.graphql b/async-graphql-parser/tests/executables/query_directive.graphql similarity index 100% rename from async-graphql-parser/tests/queries/query_directive.graphql rename to async-graphql-parser/tests/executables/query_directive.graphql diff --git a/async-graphql-parser/tests/queries/query_list_argument.graphql b/async-graphql-parser/tests/executables/query_list_argument.graphql similarity index 100% rename from async-graphql-parser/tests/queries/query_list_argument.graphql rename to async-graphql-parser/tests/executables/query_list_argument.graphql diff --git a/async-graphql-parser/tests/queries/query_object_argument.graphql b/async-graphql-parser/tests/executables/query_object_argument.graphql similarity index 100% rename from async-graphql-parser/tests/queries/query_object_argument.graphql rename to async-graphql-parser/tests/executables/query_object_argument.graphql diff --git a/async-graphql-parser/tests/queries/query_var_default_float.graphql b/async-graphql-parser/tests/executables/query_var_default_float.graphql similarity index 100% rename from async-graphql-parser/tests/queries/query_var_default_float.graphql rename to async-graphql-parser/tests/executables/query_var_default_float.graphql diff --git a/async-graphql-parser/tests/queries/query_var_default_list.graphql b/async-graphql-parser/tests/executables/query_var_default_list.graphql similarity index 100% rename from async-graphql-parser/tests/queries/query_var_default_list.graphql rename to async-graphql-parser/tests/executables/query_var_default_list.graphql diff --git a/async-graphql-parser/tests/queries/query_var_default_object.graphql b/async-graphql-parser/tests/executables/query_var_default_object.graphql similarity index 100% rename from async-graphql-parser/tests/queries/query_var_default_object.graphql rename to async-graphql-parser/tests/executables/query_var_default_object.graphql diff --git a/async-graphql-parser/tests/queries/query_var_default_string.graphql b/async-graphql-parser/tests/executables/query_var_default_string.graphql similarity index 100% rename from async-graphql-parser/tests/queries/query_var_default_string.graphql rename to async-graphql-parser/tests/executables/query_var_default_string.graphql diff --git a/async-graphql-parser/tests/queries/query_var_defaults.graphql b/async-graphql-parser/tests/executables/query_var_defaults.graphql similarity index 100% rename from async-graphql-parser/tests/queries/query_var_defaults.graphql rename to async-graphql-parser/tests/executables/query_var_defaults.graphql diff --git a/async-graphql-parser/tests/queries/query_vars.graphql b/async-graphql-parser/tests/executables/query_vars.graphql similarity index 100% rename from async-graphql-parser/tests/queries/query_vars.graphql rename to async-graphql-parser/tests/executables/query_vars.graphql diff --git a/async-graphql-parser/tests/queries/string_literal.graphql b/async-graphql-parser/tests/executables/string_literal.graphql similarity index 100% rename from async-graphql-parser/tests/queries/string_literal.graphql rename to async-graphql-parser/tests/executables/string_literal.graphql diff --git a/async-graphql-parser/tests/queries/subscription_directive.graphql b/async-graphql-parser/tests/executables/subscription_directive.graphql similarity index 100% rename from async-graphql-parser/tests/queries/subscription_directive.graphql rename to async-graphql-parser/tests/executables/subscription_directive.graphql diff --git a/async-graphql-parser/tests/schemas/implements.graphql b/async-graphql-parser/tests/schemas/implements.graphql deleted file mode 100644 index 473bc7aa..00000000 --- a/async-graphql-parser/tests/schemas/implements.graphql +++ /dev/null @@ -1,7 +0,0 @@ -type Type1 implements IOne - -type Type1 implements IOne & ITwo - -interface Type1 implements IOne - -interface Type1 implements IOne & ITwo diff --git a/async-graphql-parser/tests/schemas/implements_amp.graphql b/async-graphql-parser/tests/schemas/implements_amp.graphql deleted file mode 100644 index afff22d7..00000000 --- a/async-graphql-parser/tests/schemas/implements_amp.graphql +++ /dev/null @@ -1,5 +0,0 @@ -type Type1 implements & IOne & ITwo -type Type2 implements & IOne - -interface Type1 implements & IOne & ITwo -interface Type2 implements & IOne diff --git a/async-graphql-parser/tests/schemas/implements_amp_canonical.graphql b/async-graphql-parser/tests/schemas/implements_amp_canonical.graphql deleted file mode 100644 index 6b287d41..00000000 --- a/async-graphql-parser/tests/schemas/implements_amp_canonical.graphql +++ /dev/null @@ -1,7 +0,0 @@ -type Type1 implements IOne & ITwo - -type Type2 implements IOne - -interface Type1 implements IOne & ITwo - -interface Type2 implements IOne diff --git a/async-graphql-parser/tests/schemas/directive.graphql b/async-graphql-parser/tests/services/directive.graphql similarity index 100% rename from async-graphql-parser/tests/schemas/directive.graphql rename to async-graphql-parser/tests/services/directive.graphql diff --git a/async-graphql-parser/tests/schemas/directive_descriptions.graphql b/async-graphql-parser/tests/services/directive_descriptions.graphql similarity index 100% rename from async-graphql-parser/tests/schemas/directive_descriptions.graphql rename to async-graphql-parser/tests/services/directive_descriptions.graphql diff --git a/async-graphql-parser/tests/schemas/directive_descriptions_canonical.graphql b/async-graphql-parser/tests/services/directive_descriptions_canonical.graphql similarity index 100% rename from async-graphql-parser/tests/schemas/directive_descriptions_canonical.graphql rename to async-graphql-parser/tests/services/directive_descriptions_canonical.graphql diff --git a/async-graphql-parser/tests/schemas/empty_union.graphql b/async-graphql-parser/tests/services/empty_union.graphql similarity index 100% rename from async-graphql-parser/tests/schemas/empty_union.graphql rename to async-graphql-parser/tests/services/empty_union.graphql diff --git a/async-graphql-parser/tests/schemas/enum.graphql b/async-graphql-parser/tests/services/enum.graphql similarity index 100% rename from async-graphql-parser/tests/schemas/enum.graphql rename to async-graphql-parser/tests/services/enum.graphql diff --git a/async-graphql-parser/tests/schemas/extend_enum.graphql b/async-graphql-parser/tests/services/extend_enum.graphql similarity index 100% rename from async-graphql-parser/tests/schemas/extend_enum.graphql rename to async-graphql-parser/tests/services/extend_enum.graphql diff --git a/async-graphql-parser/tests/schemas/extend_input.graphql b/async-graphql-parser/tests/services/extend_input.graphql similarity index 100% rename from async-graphql-parser/tests/schemas/extend_input.graphql rename to async-graphql-parser/tests/services/extend_input.graphql diff --git a/async-graphql-parser/tests/schemas/extend_input_canonical.graphql b/async-graphql-parser/tests/services/extend_input_canonical.graphql similarity index 100% rename from async-graphql-parser/tests/schemas/extend_input_canonical.graphql rename to async-graphql-parser/tests/services/extend_input_canonical.graphql diff --git a/async-graphql-parser/tests/schemas/extend_interface.graphql b/async-graphql-parser/tests/services/extend_interface.graphql similarity index 100% rename from async-graphql-parser/tests/schemas/extend_interface.graphql rename to async-graphql-parser/tests/services/extend_interface.graphql diff --git a/async-graphql-parser/tests/schemas/extend_object.graphql b/async-graphql-parser/tests/services/extend_object.graphql similarity index 100% rename from async-graphql-parser/tests/schemas/extend_object.graphql rename to async-graphql-parser/tests/services/extend_object.graphql diff --git a/async-graphql-parser/tests/schemas/extend_scalar.graphql b/async-graphql-parser/tests/services/extend_scalar.graphql similarity index 100% rename from async-graphql-parser/tests/schemas/extend_scalar.graphql rename to async-graphql-parser/tests/services/extend_scalar.graphql diff --git a/async-graphql-parser/tests/services/implements.graphql b/async-graphql-parser/tests/services/implements.graphql new file mode 100644 index 00000000..a84b001a --- /dev/null +++ b/async-graphql-parser/tests/services/implements.graphql @@ -0,0 +1,3 @@ +type Type1 implements IOne + +type Type1 implements IOne & ITwo diff --git a/async-graphql-parser/tests/services/implements_amp.graphql b/async-graphql-parser/tests/services/implements_amp.graphql new file mode 100644 index 00000000..e4ff2d38 --- /dev/null +++ b/async-graphql-parser/tests/services/implements_amp.graphql @@ -0,0 +1,2 @@ +type Type1 implements & IOne & ITwo +type Type2 implements & IOne diff --git a/async-graphql-parser/tests/services/implements_amp_canonical.graphql b/async-graphql-parser/tests/services/implements_amp_canonical.graphql new file mode 100644 index 00000000..f066126b --- /dev/null +++ b/async-graphql-parser/tests/services/implements_amp_canonical.graphql @@ -0,0 +1,3 @@ +type Type1 implements IOne & ITwo + +type Type2 implements IOne diff --git a/async-graphql-parser/tests/schemas/input_type.graphql b/async-graphql-parser/tests/services/input_type.graphql similarity index 100% rename from async-graphql-parser/tests/schemas/input_type.graphql rename to async-graphql-parser/tests/services/input_type.graphql diff --git a/async-graphql-parser/tests/schemas/interface.graphql b/async-graphql-parser/tests/services/interface.graphql similarity index 100% rename from async-graphql-parser/tests/schemas/interface.graphql rename to async-graphql-parser/tests/services/interface.graphql diff --git a/async-graphql-parser/tests/schemas/kitchen-sink.graphql b/async-graphql-parser/tests/services/kitchen-sink.graphql similarity index 100% rename from async-graphql-parser/tests/schemas/kitchen-sink.graphql rename to async-graphql-parser/tests/services/kitchen-sink.graphql diff --git a/async-graphql-parser/tests/schemas/kitchen-sink_canonical.graphql b/async-graphql-parser/tests/services/kitchen-sink_canonical.graphql similarity index 100% rename from async-graphql-parser/tests/schemas/kitchen-sink_canonical.graphql rename to async-graphql-parser/tests/services/kitchen-sink_canonical.graphql diff --git a/async-graphql-parser/tests/schemas/minimal.graphql b/async-graphql-parser/tests/services/minimal.graphql similarity index 100% rename from async-graphql-parser/tests/schemas/minimal.graphql rename to async-graphql-parser/tests/services/minimal.graphql diff --git a/async-graphql-parser/tests/schemas/minimal_type.graphql b/async-graphql-parser/tests/services/minimal_type.graphql similarity index 100% rename from async-graphql-parser/tests/schemas/minimal_type.graphql rename to async-graphql-parser/tests/services/minimal_type.graphql diff --git a/async-graphql-parser/tests/schemas/scalar_type.graphql b/async-graphql-parser/tests/services/scalar_type.graphql similarity index 100% rename from async-graphql-parser/tests/schemas/scalar_type.graphql rename to async-graphql-parser/tests/services/scalar_type.graphql diff --git a/async-graphql-parser/tests/schemas/simple_object.graphql b/async-graphql-parser/tests/services/simple_object.graphql similarity index 100% rename from async-graphql-parser/tests/schemas/simple_object.graphql rename to async-graphql-parser/tests/services/simple_object.graphql diff --git a/async-graphql-parser/tests/schemas/union.graphql b/async-graphql-parser/tests/services/union.graphql similarity index 100% rename from async-graphql-parser/tests/schemas/union.graphql rename to async-graphql-parser/tests/services/union.graphql diff --git a/async-graphql-parser/tests/schemas/union_extension.graphql b/async-graphql-parser/tests/services/union_extension.graphql similarity index 100% rename from async-graphql-parser/tests/schemas/union_extension.graphql rename to async-graphql-parser/tests/services/union_extension.graphql diff --git a/src/context.rs b/src/context.rs index b0fde8a3..e3684a5d 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,8 +1,8 @@ use crate::base::Type; use crate::extensions::Extensions; -use crate::parser::types::{Directive, ExecutableDocument, Field, SelectionSet, Value}; +use crate::parser::types::{Directive, ExecutableDocumentData, Field, SelectionSet, Value as InputValue, Name}; use crate::schema::SchemaEnv; -use crate::{FieldResult, InputValueType, Lookahead, Pos, Positioned, QueryError, Result}; +use crate::{FieldResult, InputValueType, Lookahead, Pos, Positioned, QueryError, Result, Value}; use fnv::FnvHashMap; use serde::ser::SerializeSeq; use serde::{Serialize, Serializer}; @@ -16,7 +16,7 @@ use std::sync::Arc; /// Variables of a query. #[derive(Debug, Clone, Default, Serialize)] -pub struct Variables(pub BTreeMap); +pub struct Variables(pub BTreeMap); impl Display for Variables { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { @@ -30,13 +30,15 @@ impl Display for Variables { impl Variables { /// Parse variables from JSON object. - // TODO: Is this supposed to be able to return an error? - pub fn parse_from_json(value: serde_json::Value) -> Result { - Ok(if let Value::Object(obj) = value.into() { + /// + /// If the value is not a map, or the keys of map are not valid GraphQL names, then an empty + /// `Variables` instance will be returned. + pub fn parse_from_json(value: serde_json::Value) -> Self { + if let Ok(Value::Object(obj)) = Value::from_json(value) { Self(obj) } else { Default::default() - }) + } } pub(crate) fn variable_path(&mut self, path: &str) -> Option<&mut Value> { @@ -212,7 +214,7 @@ impl<'a, T> Deref for ContextBase<'a, T> { pub struct QueryEnvInner { pub extensions: spin::Mutex, pub variables: Variables, - pub document: ExecutableDocument, + pub document: ExecutableDocumentData, pub ctx_data: Arc, } @@ -233,7 +235,7 @@ impl QueryEnv { pub fn new( extensions: spin::Mutex, variables: Variables, - document: ExecutableDocument, + document: ExecutableDocumentData, ctx_data: Arc, ) -> QueryEnv { QueryEnv(Arc::new(QueryEnvInner { @@ -364,27 +366,9 @@ impl<'a, T> ContextBase<'a, T> { }) } - fn resolved_input_value(&self, mut value: Positioned) -> Result { - self.resolve_input_value(&mut value.node, value.pos)?; - Ok(value.node) - } - - fn resolve_input_value(&self, value: &mut Value, pos: Pos) -> Result<()> { - match value { - Value::Variable(var_name) => *value = self.var_value(&var_name, pos)?, - Value::List(ls) => { - for ls_value in ls { - self.resolve_input_value(ls_value, pos)?; - } - } - Value::Object(obj) => { - for obj_value in obj.values_mut() { - self.resolve_input_value(obj_value, pos)?; - } - } - _ => (), - } - Ok(()) + fn resolve_input_value(&self, value: Positioned) -> Result { + let pos = value.pos; + value.node.into_const_with(|name| self.var_value(&name, pos)) } #[doc(hidden)] @@ -403,10 +387,7 @@ impl<'a, T> ContextBase<'a, T> { _ => continue, }; - let Positioned { - node: mut condition_input, - pos, - } = directive + let condition_input = directive .node .get_argument("if") .ok_or_else(|| { @@ -419,10 +400,10 @@ impl<'a, T> ContextBase<'a, T> { })? .clone(); - self.resolve_input_value(&mut condition_input, pos)?; + let pos = condition_input.pos; + let condition_input = self.resolve_input_value(condition_input)?; - if include - != ::parse(Some(condition_input)) + if include != ::parse(Some(condition_input)) .map_err(|e| e.into_error(pos, bool::qualified_type_name()))? { return Ok(true); @@ -474,7 +455,7 @@ impl<'a> ContextBase<'a, &'a Positioned> { } } let (pos, value) = match value { - Some(value) => (value.pos, Some(self.resolved_input_value(value)?)), + Some(value) => (value.pos, Some(self.resolve_input_value(value)?)), None => (Pos::default(), None), }; InputValueType::parse(value).map_err(|e| e.into_error(pos, T::qualified_type_name())) diff --git a/src/extensions/logger.rs b/src/extensions/logger.rs index db7bd8b8..ed07ae70 100644 --- a/src/extensions/logger.rs +++ b/src/extensions/logger.rs @@ -1,5 +1,5 @@ use crate::extensions::{Extension, ResolveInfo}; -use crate::parser::types::{Definition, Document, OperationType, Selection}; +use crate::parser::types::{ExecutableDefinition, ExecutableDocument, OperationType, Selection}; use crate::{Error, Variables}; use itertools::Itertools; use log::{error, info, trace}; @@ -31,12 +31,12 @@ impl Extension for Logger { self.variables = variables.clone(); } - fn parse_end(&mut self, document: &Document) { + fn parse_end(&mut self, document: &ExecutableDocument) { let is_schema = document .definitions .iter() .filter_map(|definition| match definition { - Definition::Operation(operation) if operation.node.ty == OperationType::Query => Some(operation), + ExecutableDefinition::Operation(operation) if operation.node.ty == OperationType::Query => Some(operation), _ => None, }) .any(|operation| operation.node.selection_set.node.items.iter().any(|selection| matches!(&selection.node, Selection::Field(field) if field.node.name.node == "__schema"))); diff --git a/src/extensions/mod.rs b/src/extensions/mod.rs index 300124a9..c86ad51b 100644 --- a/src/extensions/mod.rs +++ b/src/extensions/mod.rs @@ -10,7 +10,7 @@ use crate::{Result, Variables}; pub use self::apollo_tracing::ApolloTracing; pub use self::logger::Logger; pub use self::tracing::Tracing; -use crate::parser::types::Document; +use crate::parser::types::ExecutableDocument; use crate::Error; use serde_json::Value; @@ -47,7 +47,7 @@ pub trait Extension: Sync + Send + 'static { fn parse_start(&mut self, query_source: &str, variables: &Variables) {} /// Called at the end of the parse. - fn parse_end(&mut self, document: &Document) {} + fn parse_end(&mut self, document: &ExecutableDocument) {} /// Called at the begin of the validation. fn validation_start(&mut self) {} @@ -96,7 +96,7 @@ impl Extension for Extensions { .for_each(|e| e.parse_start(query_source, variables)); } - fn parse_end(&mut self, document: &Document) { + fn parse_end(&mut self, document: &ExecutableDocument) { self.0.iter_mut().for_each(|e| e.parse_end(document)); } diff --git a/src/http/mod.rs b/src/http/mod.rs index 6e521e23..2991bfa3 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -45,9 +45,7 @@ impl IntoQueryBuilder for GQLRequest { builder = builder.operation_name(operation_name); } if let Some(variables) = self.variables { - if let Ok(variables) = Variables::parse_from_json(variables) { - builder = builder.variables(variables); - } + builder = builder.variables(Variables::parse_from_json(variables)) } Ok(builder) } diff --git a/src/lib.rs b/src/lib.rs index 2dd7c7a5..a11ca4e8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -141,7 +141,7 @@ pub use error::{ ParseRequestError, QueryError, ResultExt, RuleError, }; pub use look_ahead::Lookahead; -pub use parser::{types::Value, Pos, Positioned}; +pub use parser::{types::ConstValue as Value, Pos, Positioned}; pub use query::{IntoQueryBuilder, IntoQueryBuilderOpts, QueryBuilder, QueryResponse}; pub use registry::CacheControl; pub use scalars::{Any, Json, OutputJson, ID}; diff --git a/src/look_ahead.rs b/src/look_ahead.rs index e28531d5..34790b40 100644 --- a/src/look_ahead.rs +++ b/src/look_ahead.rs @@ -1,8 +1,8 @@ -use crate::parser::types::{ExecutableDocument, Field, Selection, SelectionSet}; +use crate::parser::types::{ExecutableDocumentData, Field, Selection, SelectionSet}; /// A selection performed by a query pub struct Lookahead<'a> { - pub(crate) document: &'a ExecutableDocument, + pub(crate) document: &'a ExecutableDocumentData, pub(crate) field: Option<&'a Field>, } @@ -25,7 +25,7 @@ impl<'a> Lookahead<'a> { } fn find<'a>( - document: &'a ExecutableDocument, + document: &'a ExecutableDocumentData, selection_set: &'a SelectionSet, name: &str, ) -> Option<&'a Field> { diff --git a/src/mutation_resolver.rs b/src/mutation_resolver.rs index 674c9378..53ff5135 100644 --- a/src/mutation_resolver.rs +++ b/src/mutation_resolver.rs @@ -52,7 +52,7 @@ fn do_resolve<'a, T: ObjectType + Send + Sync>( if let Some(MetaType::Object { fields, .. }) = ctx.schema_env.registry.types.get(T::type_name().as_ref()) { - if !fields.contains_key(&field.node.name.node) { + if !fields.contains_key(field.node.name.node.as_str()) { continue; } } @@ -79,7 +79,7 @@ fn do_resolve<'a, T: ObjectType + Send + Sync>( pos: field.pos, path: None, err: QueryError::FieldNotFound { - field_name: field.node.name.node.clone(), + field_name: field.node.name.node.clone().into_string(), object: T::type_name().to_string(), }, }); @@ -96,7 +96,7 @@ fn do_resolve<'a, T: ObjectType + Send + Sync>( .resolve_field(&ctx_field) .await .log_error(&ctx.query_env.extensions)?; - values.insert(field_name, value); + values.insert(field_name.into_string(), value); ctx_field .query_env @@ -126,7 +126,7 @@ fn do_resolve<'a, T: ObjectType + Send + Sync>( pos: fragment_spread.pos, path: None, err: QueryError::UnknownFragment { - name: fragment_spread.node.fragment_name.node.clone(), + name: fragment_spread.node.fragment_name.node.clone().into_string(), }, }); } diff --git a/src/query.rs b/src/query.rs index 97c0fd31..304cf6d0 100644 --- a/src/query.rs +++ b/src/query.rs @@ -2,11 +2,11 @@ use crate::context::{Data, ResolveId}; use crate::error::ParseRequestError; use crate::extensions::{BoxExtension, ErrorLogger, Extension}; use crate::mutation_resolver::do_mutation_resolve; -use crate::parser::types::{OperationType, UploadValue, Value}; +use crate::parser::types::{OperationType, UploadValue}; use crate::registry::CacheControl; use crate::{ do_resolve, ContextBase, Error, ObjectType, Pos, QueryEnv, QueryError, Result, Schema, - SubscriptionType, Variables, + SubscriptionType, Variables,Value }; use std::any::Any; use std::fs::File; @@ -141,7 +141,7 @@ impl QueryBuilder { // execute let inc_resolve_id = AtomicUsize::default(); - let document = match document.into_executable(self.operation_name.as_deref()) { + let document = match document.into_data(self.operation_name.as_deref()) { Some(document) => document, None => { return if let Some(operation_name) = self.operation_name { diff --git a/src/resolver.rs b/src/resolver.rs index 465e121e..268b33ab 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -54,7 +54,7 @@ pub fn collect_fields<'a, T: ObjectType + Send + Sync>( if field.node.name.node == "__typename" { // Get the typename let ctx_field = ctx.with_field(field); - let field_name = ctx_field.item.node.response_key().node.clone(); + let field_name = ctx_field.item.node.response_key().node.clone().into_string(); futures.push(Box::pin( future::ok::( root.introspection_type_name().to_string().into(), @@ -78,7 +78,7 @@ pub fn collect_fields<'a, T: ObjectType + Send + Sync>( let ctx = ctx.clone(); async move { let ctx_field = ctx.with_field(field); - let field_name = ctx_field.item.node.response_key().node.clone(); + let field_name = ctx_field.item.node.response_key().node.clone().into_string(); let resolve_info = ResolveInfo { resolve_id: ctx_field.resolve_id, @@ -98,7 +98,7 @@ pub fn collect_fields<'a, T: ObjectType + Send + Sync>( pos: field.pos, path: None, err: QueryError::FieldNotFound { - field_name: field.node.name.node.to_owned(), + field_name: field.node.name.node.clone().into_string(), object: T::type_name().to_string(), }, }); diff --git a/src/scalars/any.rs b/src/scalars/any.rs index 3900da5b..96c5c91e 100644 --- a/src/scalars/any.rs +++ b/src/scalars/any.rs @@ -26,15 +26,12 @@ impl ScalarType for Any { impl Any { /// Parse this `Any` value to T by `serde_json`. - pub fn parse_value(&self) -> std::result::Result { - serde_json::from_value(self.to_value().into()) + pub fn parse_value(&self) -> serde_json::Result { + serde_json::from_value(self.to_value().into_json()?) } } -impl From for Any -where - T: Into, -{ +impl> From for Any { fn from(value: T) -> Any { Any(value.into()) } diff --git a/src/scalars/json.rs b/src/scalars/json.rs index 9c5824c8..d75d3bb6 100644 --- a/src/scalars/json.rs +++ b/src/scalars/json.rs @@ -11,7 +11,10 @@ use std::borrow::Cow; use std::ops::{Deref, DerefMut}; /// A scalar that can represent any JSON value. +/// +/// If the inner type cannot be serialized as JSON (e.g. it has non-string keys) it will be `null`. #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Hash, Default)] +#[serde(transparent)] pub struct Json(pub T); impl Deref for Json { @@ -28,8 +31,8 @@ impl DerefMut for Json { } } -impl From for Json { - fn from(value: serde_json::Value) -> Self { +impl From for Json { + fn from(value: T) -> Self { Self(value) } } @@ -38,13 +41,14 @@ impl From for Json { #[Scalar(internal, name = "JSON")] impl ScalarType for Json { fn parse(value: Value) -> InputValueResult { - Ok(serde_json::from_value(value.into()).map(Json)?) + Ok(serde_json::from_value(value.into_json()?)?) } fn to_value(&self) -> Value { serde_json::to_value(&self.0) - .unwrap_or_else(|_| serde_json::Value::Null) - .into() + .ok() + .and_then(|json| Value::from_json(json).ok()) + .unwrap_or_else(|| Value::Null) } } @@ -66,8 +70,8 @@ impl DerefMut for OutputJson { } } -impl From for OutputJson { - fn from(value: serde_json::Value) -> Self { +impl From for OutputJson { + fn from(value: T) -> Self { Self(value) } } diff --git a/src/schema.rs b/src/schema.rs index 3198c4c2..a83bb105 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -2,7 +2,7 @@ use crate::context::Data; use crate::extensions::{BoxExtension, ErrorLogger, Extension, Extensions}; use crate::model::__DirectiveLocation; use crate::parser::parse_query; -use crate::parser::types::{Document, OperationType}; +use crate::parser::types::{ExecutableDocument, OperationType}; use crate::query::QueryBuilder; use crate::registry::{MetaDirective, MetaInputValue, Registry}; use crate::subscription::{create_connection, create_subscription_stream, ConnectionTransport}; @@ -12,7 +12,6 @@ use crate::{ CacheControl, Error, ObjectType, Pos, QueryEnv, QueryError, QueryResponse, Result, SubscriptionType, Type, Variables, ID, }; -use bytes::Bytes; use futures::channel::mpsc; use futures::Stream; use indexmap::map::IndexMap; @@ -318,7 +317,7 @@ where source: &str, variables: &Variables, query_extensions: &[Box BoxExtension + Send + Sync>], - ) -> Result<(Document, CacheControl, spin::Mutex)> { + ) -> Result<(ExecutableDocument, CacheControl, spin::Mutex)> { // create extension instances let extensions = spin::Mutex::new(Extensions( self.0 @@ -377,7 +376,7 @@ where ) -> Result> + Send> { let (document, _, extensions) = self.prepare_query(source, &variables, &Vec::new())?; - let document = match document.into_executable(operation_name) { + let document = match document.into_data(operation_name) { Some(document) => document, None => { return if let Some(name) = operation_name { diff --git a/src/subscription/subscription_type.rs b/src/subscription/subscription_type.rs index fc2a204a..2b71deca 100644 --- a/src/subscription/subscription_type.rs +++ b/src/subscription/subscription_type.rs @@ -76,7 +76,7 @@ where .as_ref() .map(|v| &v.node) { - if name.node == Subscription::type_name() { + if name.node.as_str() == Subscription::type_name() { create_subscription_stream( schema, environment.clone(), diff --git a/src/subscription/ws_transport.rs b/src/subscription/ws_transport.rs index 3164768d..5233645a 100644 --- a/src/subscription/ws_transport.rs +++ b/src/subscription/ws_transport.rs @@ -84,11 +84,7 @@ impl ConnectionTransport for WebSocketTransport { "start" => { if let (Some(id), Some(payload)) = (msg.id, msg.payload) { if let Ok(request) = serde_json::from_value::(payload) { - let variables = request - .variables - .map(|value| Variables::parse_from_json(value).ok()) - .flatten() - .unwrap_or_default(); + let variables = Variables::parse_from_json(request.variables.unwrap_or_default()); match schema .create_subscription_stream( &request.query, diff --git a/src/types/enum.rs b/src/types/enum.rs index 732a4697..3fedb84d 100644 --- a/src/types/enum.rs +++ b/src/types/enum.rs @@ -1,3 +1,4 @@ +use crate::parser::types::Name; use crate::{InputValueError, InputValueResult, Type, Value}; #[allow(missing_docs)] @@ -35,7 +36,7 @@ pub trait EnumType: Type + Sized + Eq + Send + Copy + Sized + 'static { let items = Self::items(); for item in items { if item.value == *self { - return Value::Enum(item.name.into()); + return Value::Enum(Name::new_unchecked(item.name.to_owned())); } } unreachable!() diff --git a/src/validation/mod.rs b/src/validation/mod.rs index 421cc7a1..17a3ee07 100644 --- a/src/validation/mod.rs +++ b/src/validation/mod.rs @@ -8,7 +8,7 @@ mod utils; mod visitor; mod visitors; -use crate::parser::types::Document; +use crate::parser::types::ExecutableDocument; use crate::registry::Registry; use crate::{CacheControl, Error, Result, Variables}; use visitor::{visit, VisitorContext, VisitorNil}; @@ -31,7 +31,7 @@ pub enum ValidationMode { pub fn check_rules( registry: &Registry, - doc: &Document, + doc: &ExecutableDocument, variables: Option<&Variables>, mode: ValidationMode, ) -> Result { diff --git a/src/validation/rules/arguments_of_correct_type.rs b/src/validation/rules/arguments_of_correct_type.rs index c5fc2558..d5217223 100644 --- a/src/validation/rules/arguments_of_correct_type.rs +++ b/src/validation/rules/arguments_of_correct_type.rs @@ -1,9 +1,9 @@ use crate::context::QueryPathNode; -use crate::parser::types::{Directive, Field}; +use crate::parser::types::{Directive, Field, Value, Name}; use crate::registry::MetaInputValue; use crate::validation::utils::is_valid_input_value; use crate::validation::visitor::{Visitor, VisitorContext}; -use crate::{Positioned, QueryPathSegment, Value}; +use crate::{Positioned, QueryPathSegment}; use indexmap::map::IndexMap; #[derive(Default)] @@ -20,7 +20,7 @@ impl<'a> Visitor<'a> for ArgumentsOfCorrectType<'a> { self.current_args = ctx .registry .directives - .get(&directive.node.name.node) + .get(directive.node.name.node.as_str()) .map(|d| &d.args); } @@ -35,22 +35,17 @@ impl<'a> Visitor<'a> for ArgumentsOfCorrectType<'a> { fn enter_argument( &mut self, ctx: &mut VisitorContext<'a>, - name: &'a Positioned, + name: &'a Positioned, value: &'a Positioned, ) { if let Some(arg) = self .current_args - .and_then(|args| args.get(&*name.node).map(|input| input)) + .and_then(|args| args.get(name.node.as_str()).map(|input| input)) { - if let Some(validator) = &arg.validator { - let value = match &value.node { - Value::Variable(var_name) => ctx - .variables - .and_then(|variables| variables.0.get(var_name)), - _ => Some(&value.node), - }; + let value = value.node.clone().into_const_with(|var_name| ctx.variables.and_then(|variables| variables.0.get(&var_name)).map(Clone::clone).ok_or(())).ok(); - if let Some(value) = value { + if let Some(validator) = &arg.validator { + if let Some(value) = &value { if let Err(reason) = validator.is_valid(value) { ctx.report_error( vec![name.pos], @@ -61,16 +56,16 @@ impl<'a> Visitor<'a> for ArgumentsOfCorrectType<'a> { } } - if let Some(reason) = is_valid_input_value( + if let Some(reason) = value.and_then(|value| is_valid_input_value( ctx.registry, ctx.variables, &arg.ty, - &value.node, + &value, QueryPathNode { parent: None, segment: QueryPathSegment::Name(arg.name), }, - ) { + )) { ctx.report_error( vec![name.pos], format!("Invalid value for argument {}", reason), diff --git a/src/validation/rules/fields_on_correct_type.rs b/src/validation/rules/fields_on_correct_type.rs index bf95b026..c0b73fb5 100644 --- a/src/validation/rules/fields_on_correct_type.rs +++ b/src/validation/rules/fields_on_correct_type.rs @@ -19,7 +19,7 @@ impl<'a> Visitor<'a> for FieldsOnCorrectType { if parent_type .fields() - .and_then(|fields| fields.get(&field.node.name.node)) + .and_then(|fields| fields.get(field.node.name.node.as_str())) .is_none() && !field .node diff --git a/src/validation/rules/known_argument_names.rs b/src/validation/rules/known_argument_names.rs index e7d1b265..243e225e 100644 --- a/src/validation/rules/known_argument_names.rs +++ b/src/validation/rules/known_argument_names.rs @@ -1,8 +1,8 @@ -use crate::parser::types::{Directive, Field}; +use crate::parser::types::{Directive, Field, Value, Name}; use crate::registry::MetaInputValue; use crate::validation::suggestion::make_suggestion; use crate::validation::visitor::{Visitor, VisitorContext}; -use crate::{Positioned, Value}; +use crate::Positioned; use indexmap::map::IndexMap; enum ArgsType<'a> { @@ -41,7 +41,7 @@ impl<'a> Visitor<'a> for KnownArgumentNames<'a> { self.current_args = ctx .registry .directives - .get(&directive.node.name.node) + .get(directive.node.name.node.as_str()) .map(|d| (&d.args, ArgsType::Directive(&directive.node.name.node))); } @@ -56,11 +56,11 @@ impl<'a> Visitor<'a> for KnownArgumentNames<'a> { fn enter_argument( &mut self, ctx: &mut VisitorContext<'a>, - name: &'a Positioned, + name: &'a Positioned, _value: &'a Positioned, ) { if let Some((args, arg_type)) = &self.current_args { - if !args.contains_key(&*name.node) { + if !args.contains_key(name.node.as_str()) { match arg_type { ArgsType::Field { field_name, @@ -73,7 +73,7 @@ impl<'a> Visitor<'a> for KnownArgumentNames<'a> { name, field_name, type_name, - self.get_suggestion(&name.node) + self.get_suggestion(name.node.as_str()) ), ); } @@ -84,7 +84,7 @@ impl<'a> Visitor<'a> for KnownArgumentNames<'a> { "Unknown argument \"{}\" on directive \"{}\".{}", name, directive_name, - self.get_suggestion(&name.node) + self.get_suggestion(name.node.as_str()) ), ); } diff --git a/src/validation/rules/known_directives.rs b/src/validation/rules/known_directives.rs index 2a98774a..5a8d87da 100644 --- a/src/validation/rules/known_directives.rs +++ b/src/validation/rules/known_directives.rs @@ -55,7 +55,7 @@ impl<'a> Visitor<'a> for KnownDirectives { ctx: &mut VisitorContext<'a>, directive: &'a Positioned, ) { - if let Some(schema_directive) = ctx.registry.directives.get(&directive.node.name.node) { + if let Some(schema_directive) = ctx.registry.directives.get(directive.node.name.node.as_str()) { if let Some(current_location) = self.location_stack.last() { if !schema_directive.locations.contains(current_location) { ctx.report_error( diff --git a/src/validation/rules/lone_anonymous_operation.rs b/src/validation/rules/lone_anonymous_operation.rs index 084d304f..dfd95ef4 100644 --- a/src/validation/rules/lone_anonymous_operation.rs +++ b/src/validation/rules/lone_anonymous_operation.rs @@ -1,4 +1,4 @@ -use crate::parser::types::{Definition, Document, OperationDefinition}; +use crate::parser::types::{ExecutableDefinition, ExecutableDocument, OperationDefinition}; use crate::validation::visitor::{Visitor, VisitorContext}; use crate::Positioned; @@ -8,11 +8,11 @@ pub struct LoneAnonymousOperation { } impl<'a> Visitor<'a> for LoneAnonymousOperation { - fn enter_document(&mut self, _ctx: &mut VisitorContext<'a>, doc: &'a Document) { + fn enter_document(&mut self, _ctx: &mut VisitorContext<'a>, doc: &'a ExecutableDocument) { self.operation_count = Some( doc.definitions .iter() - .filter(|d| matches!(&d, Definition::Operation(_))) + .filter(|d| matches!(&d, ExecutableDefinition::Operation(_))) .count(), ); } diff --git a/src/validation/rules/no_fragment_cycles.rs b/src/validation/rules/no_fragment_cycles.rs index b7487b01..9f84883f 100644 --- a/src/validation/rules/no_fragment_cycles.rs +++ b/src/validation/rules/no_fragment_cycles.rs @@ -1,5 +1,5 @@ use crate::error::RuleError; -use crate::parser::types::{Document, FragmentDefinition, FragmentSpread}; +use crate::parser::types::{ExecutableDocument, FragmentDefinition, FragmentSpread}; use crate::validation::visitor::{Visitor, VisitorContext}; use crate::{Pos, Positioned}; use std::collections::{HashMap, HashSet}; @@ -54,7 +54,7 @@ pub struct NoFragmentCycles<'a> { } impl<'a> Visitor<'a> for NoFragmentCycles<'a> { - fn exit_document(&mut self, ctx: &mut VisitorContext<'a>, _doc: &'a Document) { + fn exit_document(&mut self, ctx: &mut VisitorContext<'a>, _doc: &'a ExecutableDocument) { let mut detector = CycleDetector { visited: HashSet::new(), spreads: &self.spreads, diff --git a/src/validation/rules/no_undefined_variables.rs b/src/validation/rules/no_undefined_variables.rs index d70c0f7d..714813ca 100644 --- a/src/validation/rules/no_undefined_variables.rs +++ b/src/validation/rules/no_undefined_variables.rs @@ -1,9 +1,9 @@ use crate::parser::types::{ - Document, FragmentDefinition, FragmentSpread, OperationDefinition, VariableDefinition, + ExecutableDocument, FragmentDefinition, FragmentSpread, OperationDefinition, VariableDefinition, Value, Name }; use crate::validation::utils::{referenced_variables, Scope}; use crate::validation::visitor::{Visitor, VisitorContext}; -use crate::{Pos, Positioned, Value}; +use crate::{Pos, Positioned}; use std::collections::{HashMap, HashSet}; #[derive(Default)] @@ -45,7 +45,7 @@ impl<'a> NoUndefinedVariables<'a> { } impl<'a> Visitor<'a> for NoUndefinedVariables<'a> { - fn exit_document(&mut self, ctx: &mut VisitorContext<'a>, _doc: &'a Document) { + fn exit_document(&mut self, ctx: &mut VisitorContext<'a>, _doc: &'a ExecutableDocument) { for (op_name, &(ref def_pos, ref def_vars)) in &self.defined_variables { let mut unused = Vec::new(); let mut visited = HashSet::new(); @@ -110,7 +110,7 @@ impl<'a> Visitor<'a> for NoUndefinedVariables<'a> { fn enter_argument( &mut self, _ctx: &mut VisitorContext<'a>, - name: &'a Positioned, + name: &'a Positioned, value: &'a Positioned, ) { if let Some(ref scope) = self.current_scope { diff --git a/src/validation/rules/no_unused_fragments.rs b/src/validation/rules/no_unused_fragments.rs index 8c7e1415..7a9a314f 100644 --- a/src/validation/rules/no_unused_fragments.rs +++ b/src/validation/rules/no_unused_fragments.rs @@ -1,5 +1,5 @@ use crate::parser::types::{ - Definition, Document, FragmentDefinition, FragmentSpread, OperationDefinition, + ExecutableDefinition, ExecutableDocument, FragmentDefinition, FragmentSpread, OperationDefinition, }; use crate::validation::utils::Scope; use crate::validation::visitor::{Visitor, VisitorContext}; @@ -32,11 +32,11 @@ impl<'a> NoUnusedFragments<'a> { } impl<'a> Visitor<'a> for NoUnusedFragments<'a> { - fn exit_document(&mut self, ctx: &mut VisitorContext<'a>, doc: &'a Document) { + fn exit_document(&mut self, ctx: &mut VisitorContext<'a>, doc: &'a ExecutableDocument) { let mut reachable = HashSet::new(); for def in &doc.definitions { - if let Definition::Operation(operation_definition) = def { + if let ExecutableDefinition::Operation(operation_definition) = def { self.find_reachable_fragments( &Scope::Operation( operation_definition diff --git a/src/validation/rules/no_unused_variables.rs b/src/validation/rules/no_unused_variables.rs index e7d68a43..67b65164 100644 --- a/src/validation/rules/no_unused_variables.rs +++ b/src/validation/rules/no_unused_variables.rs @@ -1,9 +1,9 @@ use crate::parser::types::{ - Document, FragmentDefinition, FragmentSpread, OperationDefinition, VariableDefinition, + ExecutableDocument, FragmentDefinition, FragmentSpread, OperationDefinition, VariableDefinition, Value, Name }; use crate::validation::utils::{referenced_variables, Scope}; use crate::validation::visitor::{Visitor, VisitorContext}; -use crate::{Pos, Positioned, Value}; +use crate::{Pos, Positioned}; use std::collections::{HashMap, HashSet}; #[derive(Default)] @@ -45,7 +45,7 @@ impl<'a> NoUnusedVariables<'a> { } impl<'a> Visitor<'a> for NoUnusedVariables<'a> { - fn exit_document(&mut self, ctx: &mut VisitorContext<'a>, _doc: &'a Document) { + fn exit_document(&mut self, ctx: &mut VisitorContext<'a>, _doc: &'a ExecutableDocument) { for (op_name, def_vars) in &self.defined_variables { let mut used = HashSet::new(); let mut visited = HashSet::new(); @@ -109,7 +109,7 @@ impl<'a> Visitor<'a> for NoUnusedVariables<'a> { fn enter_argument( &mut self, _ctx: &mut VisitorContext<'a>, - _name: &'a Positioned, + _name: &'a Positioned, value: &'a Positioned, ) { if let Some(ref scope) = self.current_scope { diff --git a/src/validation/rules/possible_fragment_spreads.rs b/src/validation/rules/possible_fragment_spreads.rs index 4d1d9027..53d6823a 100644 --- a/src/validation/rules/possible_fragment_spreads.rs +++ b/src/validation/rules/possible_fragment_spreads.rs @@ -1,4 +1,4 @@ -use crate::parser::types::{Definition, Document, FragmentSpread, InlineFragment, TypeCondition}; +use crate::parser::types::{ExecutableDefinition, ExecutableDocument, FragmentSpread, InlineFragment, TypeCondition}; use crate::validation::visitor::{Visitor, VisitorContext}; use crate::Positioned; use std::collections::HashMap; @@ -9,9 +9,9 @@ pub struct PossibleFragmentSpreads<'a> { } impl<'a> Visitor<'a> for PossibleFragmentSpreads<'a> { - fn enter_document(&mut self, _ctx: &mut VisitorContext<'a>, doc: &'a Document) { + fn enter_document(&mut self, _ctx: &mut VisitorContext<'a>, doc: &'a ExecutableDocument) { for d in &doc.definitions { - if let Definition::Fragment(fragment) = &d { + if let ExecutableDefinition::Fragment(fragment) = &d { let TypeCondition { on: type_name } = &fragment.node.type_condition.node; self.fragment_types .insert(&fragment.node.name.node, &type_name.node); @@ -56,7 +56,7 @@ impl<'a> Visitor<'a> for PossibleFragmentSpreads<'a> { .as_ref() .map(|c| &c.node) { - if let Some(on_type) = ctx.registry.types.get(&fragment_type.node) { + if let Some(on_type) = ctx.registry.types.get(fragment_type.node.as_str()) { if !parent_type.type_overlap(&on_type) { ctx.report_error( vec![inline_fragment.pos], diff --git a/src/validation/rules/provided_non_null_arguments.rs b/src/validation/rules/provided_non_null_arguments.rs index d92f04e1..3696b873 100644 --- a/src/validation/rules/provided_non_null_arguments.rs +++ b/src/validation/rules/provided_non_null_arguments.rs @@ -12,7 +12,7 @@ impl<'a> Visitor<'a> for ProvidedNonNullArguments { ctx: &mut VisitorContext<'a>, directive: &'a Positioned, ) { - if let Some(schema_directive) = ctx.registry.directives.get(&directive.node.name.node) { + if let Some(schema_directive) = ctx.registry.directives.get(directive.node.name.node.as_str()) { for arg in schema_directive.args.values() { if MetaTypeName::create(&arg.ty).is_non_null() && arg.default_value.is_none() diff --git a/src/validation/rules/unique_argument_names.rs b/src/validation/rules/unique_argument_names.rs index b169ac6a..dc68ee22 100644 --- a/src/validation/rules/unique_argument_names.rs +++ b/src/validation/rules/unique_argument_names.rs @@ -1,6 +1,6 @@ -use crate::parser::types::{Directive, Field}; +use crate::parser::types::{Directive, Field, Value, Name}; use crate::validation::visitor::{Visitor, VisitorContext}; -use crate::{Positioned, Value}; +use crate::Positioned; use std::collections::HashSet; #[derive(Default)] @@ -20,10 +20,10 @@ impl<'a> Visitor<'a> for UniqueArgumentNames<'a> { fn enter_argument( &mut self, ctx: &mut VisitorContext<'a>, - name: &'a Positioned, + name: &'a Positioned, _value: &'a Positioned, ) { - if !self.names.insert(&name.node) { + if !self.names.insert(name.node.as_str()) { ctx.report_error( vec![name.pos], format!("There can only be one argument named \"{}\"", name), diff --git a/src/validation/rules/variables_in_allowed_position.rs b/src/validation/rules/variables_in_allowed_position.rs index 1ce06146..9b40cea0 100644 --- a/src/validation/rules/variables_in_allowed_position.rs +++ b/src/validation/rules/variables_in_allowed_position.rs @@ -1,10 +1,10 @@ use crate::parser::types::{ - Document, FragmentDefinition, FragmentSpread, OperationDefinition, VariableDefinition, + ExecutableDocument, FragmentDefinition, FragmentSpread, OperationDefinition, VariableDefinition, Value, }; use crate::registry::MetaTypeName; use crate::validation::utils::Scope; use crate::validation::visitor::{Visitor, VisitorContext}; -use crate::{Pos, Positioned, Value}; +use crate::{Pos, Positioned}; use std::collections::{HashMap, HashSet}; #[derive(Default)] @@ -62,7 +62,7 @@ impl<'a> VariableInAllowedPosition<'a> { } impl<'a> Visitor<'a> for VariableInAllowedPosition<'a> { - fn exit_document(&mut self, ctx: &mut VisitorContext<'a>, _doc: &'a Document) { + fn exit_document(&mut self, ctx: &mut VisitorContext<'a>, _doc: &'a ExecutableDocument) { for (op_scope, var_defs) in &self.variable_defs { self.collect_incorrect_usages(op_scope, var_defs, ctx, &mut HashSet::new()); } diff --git a/src/validation/test_harness.rs b/src/validation/test_harness.rs index 4a8b6cd5..a643cccc 100644 --- a/src/validation/test_harness.rs +++ b/src/validation/test_harness.rs @@ -2,7 +2,7 @@ #![allow(dead_code)] #![allow(unreachable_code)] -use crate::parser::types::Document; +use crate::parser::types::ExecutableDocument; use crate::validation::visitor::{visit, Visitor, VisitorContext}; use crate::*; use once_cell::sync::Lazy; @@ -333,7 +333,7 @@ impl MutationRoot { static TEST_HARNESS: Lazy> = Lazy::new(|| Schema::new(QueryRoot, MutationRoot, EmptySubscription)); -pub fn validate<'a, V, F>(doc: &'a Document, factory: F) -> Result<()> +pub fn validate<'a, V, F>(doc: &'a ExecutableDocument, factory: F) -> Result<()> where V: Visitor<'a> + 'a, F: Fn() -> V, @@ -349,7 +349,7 @@ where Ok(()) } -pub(crate) fn expect_passes_rule_<'a, V, F>(doc: &'a Document, factory: F) +pub(crate) fn expect_passes_rule_<'a, V, F>(doc: &'a ExecutableDocument, factory: F) where V: Visitor<'a> + 'a, F: Fn() -> V, @@ -374,7 +374,7 @@ macro_rules! expect_passes_rule { }; } -pub(crate) fn expect_fails_rule_<'a, V, F>(doc: &'a Document, factory: F) +pub(crate) fn expect_fails_rule_<'a, V, F>(doc: &'a ExecutableDocument, factory: F) where V: Visitor<'a> + 'a, F: Fn() -> V, diff --git a/src/validation/utils.rs b/src/validation/utils.rs index 83d1c312..75c2d92a 100644 --- a/src/validation/utils.rs +++ b/src/validation/utils.rs @@ -1,5 +1,6 @@ +use crate::parser::types::{Value, ConstValue}; use crate::context::QueryPathNode; -use crate::{registry, QueryPathSegment, Value, Variables}; +use crate::{registry, QueryPathSegment, Variables}; use std::collections::HashSet; #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -37,25 +38,23 @@ pub fn is_valid_input_value( registry: ®istry::Registry, variables: Option<&Variables>, type_name: &str, - value: &Value, + value: &ConstValue, path_node: QueryPathNode, ) -> Option { - if let Value::Variable(_) = value { - return None; - } - match registry::MetaTypeName::create(type_name) { registry::MetaTypeName::NonNull(type_name) => match value { - Value::Null => Some(valid_error( + ConstValue::Null => Some(valid_error( &path_node, format!("expected type \"{}\"", type_name), )), _ => is_valid_input_value(registry, variables, type_name, value, path_node), }, registry::MetaTypeName::List(type_name) => match value { - Value::List(elems) => { - for (idx, elem) in elems.iter().enumerate() { - if let Some(reason) = is_valid_input_value( + ConstValue::List(elems) => { + elems + .iter() + .enumerate() + .find_map(|(idx, elem)| is_valid_input_value( registry, variables, type_name, @@ -63,133 +62,107 @@ pub fn is_valid_input_value( QueryPathNode { parent: Some(&path_node), segment: QueryPathSegment::Index(idx), - }, - ) { - return Some(reason); - } - } - None + } + )) } _ => is_valid_input_value(registry, variables, type_name, value, path_node), }, registry::MetaTypeName::Named(type_name) => { - if let Value::Null = value { + if let ConstValue::Null = value { return None; } - if let Some(ty) = registry.types.get(type_name) { - match ty { - registry::MetaType::Scalar { is_valid, .. } => { - let value = match value { - Value::Variable(var_name) => { - variables.and_then(|variables| variables.0.get(var_name)) - } - _ => Some(value), - }; - if let Some(value) = value { - if !is_valid(value) { - Some(valid_error( - &path_node, - format!("expected type \"{}\"", type_name), - )) - } else { - None - } + match registry.types.get(type_name).unwrap() { + registry::MetaType::Scalar { is_valid, .. } => { + if is_valid(&value) { + None + } else { + Some(valid_error( + &path_node, + format!("expected type \"{}\"", type_name), + )) + } + } + registry::MetaType::Enum { enum_values, name: enum_name, .. } => match value { + ConstValue::Enum(name) => { + if !enum_values.contains_key(name.as_str()) { + Some(valid_error( + &path_node, + format!( + "enumeration type \"{}\" does not contain the value \"{}\"", + enum_name, + name + ), + )) } else { None } } - registry::MetaType::Enum { enum_values, .. } => match value { - Value::Enum(name) => { - if !enum_values.contains_key(name.as_str()) { - Some(valid_error( - &path_node, - format!( - "enumeration type \"{}\" does not contain the value \"{}\"", - ty.name(), - name - ), - )) - } else { - None - } - } - _ => Some(valid_error( - &path_node, - format!("expected type \"{}\"", type_name), - )), - }, - registry::MetaType::InputObject { input_fields, .. } => match value { - Value::Object(values) => { - let mut input_names = values - .keys() - .map(|name| name.as_ref()) - .collect::>(); + _ => Some(valid_error( + &path_node, + format!("expected type \"{}\"", type_name), + )), + }, + registry::MetaType::InputObject { input_fields, name: object_name, .. } => match value { + ConstValue::Object(values) => { + let mut input_names = values + .keys() + .map(|name| name.as_ref()) + .collect::>(); - for field in input_fields.values() { - input_names.remove(field.name); - if let Some(value) = values.get(field.name) { - if let Some(validator) = &field.validator { - let value = match value { - Value::Variable(var_name) => variables - .and_then(|variables| variables.0.get(var_name)), - _ => Some(value), - }; - - if let Some(value) = value { - if let Err(reason) = validator.is_valid(value) { - return Some(valid_error( - &QueryPathNode { - parent: Some(&path_node), - segment: QueryPathSegment::Name(field.name), - }, - reason, - )); - } - } - } - - if let Some(reason) = is_valid_input_value( - registry, - variables, - &field.ty, - value, - QueryPathNode { - parent: Some(&path_node), - segment: QueryPathSegment::Name(field.name), - }, - ) { - return Some(reason); - } - } else if registry::MetaTypeName::create(&field.ty).is_non_null() - && field.default_value.is_none() - { - return Some(valid_error( - &path_node, - format!( - "field \"{}\" of type \"{}\" is required but not provided", - field.name, - ty.name(), - ), + for field in input_fields.values() { + input_names.remove(field.name); + if let Some(value) = values.get(field.name) { + if let Some(validator) = &field.validator { + if let Err(reason) = validator.is_valid(value) { + return Some(valid_error( + &QueryPathNode { + parent: Some(&path_node), + segment: QueryPathSegment::Name(field.name), + }, + reason, )); + } } - } - if let Some(name) = input_names.iter().next() { + if let Some(reason) = is_valid_input_value( + registry, + variables, + &field.ty, + value, + QueryPathNode { + parent: Some(&path_node), + segment: QueryPathSegment::Name(field.name), + }, + ) { + return Some(reason); + } + } else if registry::MetaTypeName::create(&field.ty).is_non_null() + && field.default_value.is_none() + { return Some(valid_error( - &path_node, - format!("unknown field \"{}\" of type \"{}\"", name, ty.name()), - )); + &path_node, + format!( + "field \"{}\" of type \"{}\" is required but not provided", + field.name, + object_name, + ), + )); } - - None } - _ => None, - }, + + if let Some(name) = input_names.iter().next() { + return Some(valid_error( + &path_node, + format!("unknown field \"{}\" of type \"{}\"", name, object_name) + )); + } + + None + } _ => None, - } - } else { - unreachable!() + }, + _ => None, } } } diff --git a/src/validation/visitor.rs b/src/validation/visitor.rs index 2af7eebe..5ba0c745 100644 --- a/src/validation/visitor.rs +++ b/src/validation/visitor.rs @@ -1,10 +1,10 @@ use crate::error::RuleError; use crate::parser::types::{ - Definition, Directive, Document, Field, FragmentDefinition, FragmentSpread, InlineFragment, - OperationDefinition, OperationType, Selection, SelectionSet, TypeCondition, VariableDefinition, + ExecutableDefinition, Directive, ExecutableDocument, Field, FragmentDefinition, FragmentSpread, InlineFragment, + OperationDefinition, OperationType, Selection, SelectionSet, TypeCondition, VariableDefinition, Value, Name }; use crate::registry::{self, MetaType, MetaTypeName}; -use crate::{Pos, Positioned, Value, Variables}; +use crate::{Pos, Positioned, Variables}; use std::collections::HashMap; pub struct VisitorContext<'a> { @@ -19,7 +19,7 @@ pub struct VisitorContext<'a> { impl<'a> VisitorContext<'a> { pub fn new( registry: &'a registry::Registry, - doc: &'a Document, + doc: &'a ExecutableDocument, variables: Option<&'a Variables>, ) -> Self { Self { @@ -32,7 +32,7 @@ impl<'a> VisitorContext<'a> { .definitions .iter() .filter_map(|d| match &d { - Definition::Fragment(fragment) => Some((&*fragment.node.name.node, fragment)), + ExecutableDefinition::Fragment(fragment) => Some((&*fragment.node.name.node, fragment)), _ => None, }) .collect(), @@ -95,8 +95,8 @@ impl<'a> VisitorContext<'a> { } pub trait Visitor<'a> { - fn enter_document(&mut self, _ctx: &mut VisitorContext<'a>, _doc: &'a Document) {} - fn exit_document(&mut self, _ctx: &mut VisitorContext<'a>, _doc: &'a Document) {} + fn enter_document(&mut self, _ctx: &mut VisitorContext<'a>, _doc: &'a ExecutableDocument) {} + fn exit_document(&mut self, _ctx: &mut VisitorContext<'a>, _doc: &'a ExecutableDocument) {} fn enter_operation_definition( &mut self, @@ -153,14 +153,14 @@ pub trait Visitor<'a> { fn enter_argument( &mut self, _ctx: &mut VisitorContext<'a>, - _name: &'a Positioned, + _name: &'a Positioned, _value: &'a Positioned, ) { } fn exit_argument( &mut self, _ctx: &mut VisitorContext<'a>, - _name: &'a Positioned, + _name: &'a Positioned, _value: &'a Positioned, ) { } @@ -261,12 +261,12 @@ where A: Visitor<'a> + 'a, B: Visitor<'a> + 'a, { - fn enter_document(&mut self, ctx: &mut VisitorContext<'a>, doc: &'a Document) { + fn enter_document(&mut self, ctx: &mut VisitorContext<'a>, doc: &'a ExecutableDocument) { self.0.enter_document(ctx, doc); self.1.enter_document(ctx, doc); } - fn exit_document(&mut self, ctx: &mut VisitorContext<'a>, doc: &'a Document) { + fn exit_document(&mut self, ctx: &mut VisitorContext<'a>, doc: &'a ExecutableDocument) { self.0.exit_document(ctx, doc); self.1.exit_document(ctx, doc); } @@ -346,7 +346,7 @@ where fn enter_argument( &mut self, ctx: &mut VisitorContext<'a>, - name: &'a Positioned, + name: &'a Positioned, value: &'a Positioned, ) { self.0.enter_argument(ctx, name, value); @@ -356,7 +356,7 @@ where fn exit_argument( &mut self, ctx: &mut VisitorContext<'a>, - name: &'a Positioned, + name: &'a Positioned, value: &'a Positioned, ) { self.0.exit_argument(ctx, name, value); @@ -446,7 +446,7 @@ where } } -pub fn visit<'a, V: Visitor<'a>>(v: &mut V, ctx: &mut VisitorContext<'a>, doc: &'a Document) { +pub fn visit<'a, V: Visitor<'a>>(v: &mut V, ctx: &mut VisitorContext<'a>, doc: &'a ExecutableDocument) { v.enter_document(ctx, doc); visit_definitions(v, ctx, doc); v.exit_document(ctx, doc); @@ -455,16 +455,16 @@ pub fn visit<'a, V: Visitor<'a>>(v: &mut V, ctx: &mut VisitorContext<'a>, doc: & fn visit_definitions<'a, V: Visitor<'a>>( v: &mut V, ctx: &mut VisitorContext<'a>, - doc: &'a Document, + doc: &'a ExecutableDocument, ) { for d in &doc.definitions { match d { - Definition::Operation(operation) => { + ExecutableDefinition::Operation(operation) => { visit_operation_definition(v, ctx, operation); } - Definition::Fragment(fragment) => { + ExecutableDefinition::Fragment(fragment) => { let TypeCondition { on: name } = &fragment.node.type_condition.node; - ctx.with_type(ctx.registry.types.get(&name.node), |ctx| { + ctx.with_type(ctx.registry.types.get(name.node.as_str()), |ctx| { visit_fragment_definition(v, ctx, fragment) }); } @@ -544,7 +544,7 @@ fn visit_selection<'a, V: Visitor<'a>>( .as_ref() .map(|c| &c.node) { - ctx.with_type(ctx.registry.types.get(&name.node), |ctx| { + ctx.with_type(ctx.registry.types.get(name.node.as_str()), |ctx| { visit_inline_fragment(v, ctx, inline_fragment) }); } @@ -615,7 +615,7 @@ fn visit_input_value<'a, V: Visitor<'a>>( { if let MetaType::InputObject { input_fields, .. } = ty { for (item_key, item_value) in values { - if let Some(input_value) = input_fields.get(item_key) { + if let Some(input_value) = input_fields.get(item_key.as_str()) { visit_input_value( v, ctx, @@ -655,7 +655,7 @@ fn visit_directives<'a, V: Visitor<'a>>( for d in directives { v.enter_directive(ctx, d); - let schema_directive = ctx.registry.directives.get(&d.node.name.node); + let schema_directive = ctx.registry.directives.get(d.node.name.node.as_str()); for (name, value) in &d.node.arguments { v.enter_argument(ctx, name, value); diff --git a/tests/input_object.rs b/tests/input_object.rs index 5d3baf92..1d698de8 100644 --- a/tests/input_object.rs +++ b/tests/input_object.rs @@ -128,12 +128,11 @@ pub async fn test_inputobject_flatten_recursive() { assert_eq!( MyInputObject::parse(Some( - serde_json::json!({ + Value::from_json(serde_json::json!({ "a": 10, "b": 20, "c": 30, - }) - .into() + })).unwrap() )) .unwrap(), MyInputObject { @@ -154,12 +153,11 @@ pub async fn test_inputobject_flatten_recursive() { c: 30, } .to_value(), - serde_json::json!({ + Value::from_json(serde_json::json!({ "a": 10, "b": 20, "c": 30, - }) - .into() + })).unwrap() ); struct Query; @@ -263,12 +261,11 @@ pub async fn test_inputobject_flatten_multiple() { assert_eq!( ABC::parse(Some( - serde_json::json!({ + Value::from_json(serde_json::json!({ "a": 10, "b": 20, "c": 30, - }) - .into() + })).unwrap() )) .unwrap(), ABC { @@ -285,11 +282,10 @@ pub async fn test_inputobject_flatten_multiple() { c: C { c: 30 } } .to_value(), - serde_json::json!({ + Value::from_json(serde_json::json!({ "a": 10, "b": 20, "c": 30, - }) - .into() + })).unwrap() ); } diff --git a/tests/input_validators.rs b/tests/input_validators.rs index bf0ed132..0db11f70 100644 --- a/tests/input_validators.rs +++ b/tests/input_validators.rs @@ -2,6 +2,7 @@ use async_graphql::validators::{ Email, IntEqual, IntGreaterThan, IntLessThan, IntNonZero, IntRange, ListMaxLength, ListMinLength, StringMaxLength, StringMinLength, MAC, }; +use async_graphql_parser::types::Name; use async_graphql::*; #[async_std::test] @@ -1627,7 +1628,7 @@ pub async fn test_input_validator_variable() { let mut variables = Variables::default(); variables .0 - .insert("id".to_string(), Value::String(case.to_string())); + .insert(Name::new("id".to_owned()).unwrap(), Value::String(case.to_string())); let field_query = "query($id: String!) {fieldParameter(id: $id)}"; let object_query = "query($id: String!) {inputObject(input: {id: $id})}"; diff --git a/tests/maybe_undefined.rs b/tests/maybe_undefined.rs index db160e17..93cc5282 100644 --- a/tests/maybe_undefined.rs +++ b/tests/maybe_undefined.rs @@ -37,7 +37,7 @@ pub async fn test_maybe_undefined_type() { { v1:value1(input: 99) v2:value1(input: null) - v3:value1() + v3:value1 v4:value2(input: { value: 99} ) v5:value2(input: { value: null} ) v6:value2(input: {} ) diff --git a/tests/subscription_websocket.rs b/tests/subscription_websocket.rs index c3c30b0f..79d45f40 100644 --- a/tests/subscription_websocket.rs +++ b/tests/subscription_websocket.rs @@ -25,8 +25,7 @@ pub async fn test_subscription_ws_transport() { "type": "connection_init", "payload": { "token": "123456" } })) - .unwrap() - .into(), + .unwrap(), ) .await .unwrap(); @@ -46,8 +45,7 @@ pub async fn test_subscription_ws_transport() { "query": "subscription { values }" }, })) - .unwrap() - .into(), + .unwrap(), ) .await .unwrap(); @@ -104,8 +102,7 @@ pub async fn test_subscription_ws_transport_with_token() { "type": "connection_init", "payload": { "token": "123456" } })) - .unwrap() - .into(), + .unwrap(), ) .await .unwrap(); @@ -125,8 +122,7 @@ pub async fn test_subscription_ws_transport_with_token() { "query": "subscription { values }" }, })) - .unwrap() - .into(), + .unwrap(), ) .await .unwrap(); @@ -183,8 +179,7 @@ pub async fn test_subscription_ws_transport_error() { serde_json::to_vec(&serde_json::json!({ "type": "connection_init" })) - .unwrap() - .into(), + .unwrap(), ) .await .unwrap(); @@ -204,8 +199,7 @@ pub async fn test_subscription_ws_transport_error() { "query": "subscription { events { value } }" }, })) - .unwrap() - .into(), + .unwrap(), ) .await .unwrap(); @@ -253,8 +247,7 @@ pub async fn test_query_over_websocket() { serde_json::to_vec(&serde_json::json!({ "type": "connection_init", })) - .unwrap() - .into(), + .unwrap(), ) .await .unwrap(); @@ -274,8 +267,7 @@ pub async fn test_query_over_websocket() { "query": "query { value }" }, })) - .unwrap() - .into(), + .unwrap(), ) .await .unwrap(); diff --git a/tests/variables.rs b/tests/variables.rs index 600076cb..80387333 100644 --- a/tests/variables.rs +++ b/tests/variables.rs @@ -29,7 +29,6 @@ pub async fn test_variables() { "intVal": 10, "intListVal": [1, 2, 3, 4, 5], })) - .unwrap(), ); let resp = query.execute(&schema).await.unwrap(); assert_eq!( @@ -90,7 +89,7 @@ pub async fn test_variable_no_value() { } "#, ) - .variables(Variables::parse_from_json(serde_json::json!({})).unwrap()); + .variables(Variables::parse_from_json(serde_json::json!({}))); let resp = query.execute(&schema).await.unwrap(); assert_eq!( resp.data, @@ -123,7 +122,6 @@ pub async fn test_variable_null() { Variables::parse_from_json(serde_json::json!({ "intVal": null, })) - .unwrap(), ); let resp = query.execute(&schema).await.unwrap(); assert_eq!( @@ -176,7 +174,6 @@ pub async fn test_variable_in_input_object() { Variables::parse_from_json(serde_json::json!({ "value": 10, })) - .unwrap(), ) .execute(&schema) .await @@ -200,7 +197,6 @@ pub async fn test_variable_in_input_object() { Variables::parse_from_json(serde_json::json!({ "value": 3, })) - .unwrap(), ) .execute(&schema) .await @@ -224,7 +220,6 @@ pub async fn test_variable_in_input_object() { Variables::parse_from_json(serde_json::json!({ "value": 10, })) - .unwrap(), ) .execute(&schema) .await