From 752f6adde8c782105d9715f22aa6ef6040fbc2e8 Mon Sep 17 00:00:00 2001 From: Sunli Date: Fri, 15 May 2020 11:42:01 +0800 Subject: [PATCH 1/2] Define AST types for schema --- async-graphql-parser/src/error.rs | 32 +++ async-graphql-parser/src/lib.rs | 9 +- async-graphql-parser/src/query.pest | 1 - async-graphql-parser/src/query_parser.rs | 187 +----------------- async-graphql-parser/src/schema.pest | 31 +++ async-graphql-parser/src/schema.rs | 149 ++++++++++++++ async-graphql-parser/src/utils.rs | 153 ++++++++++++++ .../tests/schemas/directive.graphql | 1 + .../schemas/directive_descriptions.graphql | 19 ++ .../directive_descriptions_canonical.graphql | 13 ++ .../tests/schemas/empty_union.graphql | 1 + .../tests/schemas/enum.graphql | 4 + .../tests/schemas/extend_enum.graphql | 3 + .../tests/schemas/extend_input.graphql | 3 + .../schemas/extend_input_canonical.graphql | 3 + .../tests/schemas/extend_interface.graphql | 3 + .../tests/schemas/extend_object.graphql | 3 + .../tests/schemas/extend_scalar.graphql | 1 + .../tests/schemas/implements.graphql | 3 + .../tests/schemas/implements_amp.graphql | 2 + .../schemas/implements_amp_canonical.graphql | 3 + .../tests/schemas/input_type.graphql | 4 + .../tests/schemas/interface.graphql | 3 + .../tests/schemas/kitchen-sink.graphql | 117 +++++++++++ .../schemas/kitchen-sink_canonical.graphql | 106 ++++++++++ .../tests/schemas/minimal.graphql | 3 + .../tests/schemas/minimal_type.graphql | 1 + .../tests/schemas/scalar_type.graphql | 2 + .../tests/schemas/simple_object.graphql | 3 + .../tests/schemas/union.graphql | 1 + .../tests/schemas/union_extension.graphql | 1 + 31 files changed, 683 insertions(+), 182 deletions(-) create mode 100644 async-graphql-parser/src/error.rs create mode 100644 async-graphql-parser/src/schema.pest create mode 100644 async-graphql-parser/src/schema.rs create mode 100644 async-graphql-parser/src/utils.rs create mode 100644 async-graphql-parser/tests/schemas/directive.graphql create mode 100644 async-graphql-parser/tests/schemas/directive_descriptions.graphql create mode 100644 async-graphql-parser/tests/schemas/directive_descriptions_canonical.graphql create mode 100644 async-graphql-parser/tests/schemas/empty_union.graphql create mode 100644 async-graphql-parser/tests/schemas/enum.graphql create mode 100644 async-graphql-parser/tests/schemas/extend_enum.graphql create mode 100644 async-graphql-parser/tests/schemas/extend_input.graphql create mode 100644 async-graphql-parser/tests/schemas/extend_input_canonical.graphql create mode 100644 async-graphql-parser/tests/schemas/extend_interface.graphql create mode 100644 async-graphql-parser/tests/schemas/extend_object.graphql create mode 100644 async-graphql-parser/tests/schemas/extend_scalar.graphql create mode 100644 async-graphql-parser/tests/schemas/implements.graphql create mode 100644 async-graphql-parser/tests/schemas/implements_amp.graphql create mode 100644 async-graphql-parser/tests/schemas/implements_amp_canonical.graphql create mode 100644 async-graphql-parser/tests/schemas/input_type.graphql create mode 100644 async-graphql-parser/tests/schemas/interface.graphql create mode 100644 async-graphql-parser/tests/schemas/kitchen-sink.graphql create mode 100644 async-graphql-parser/tests/schemas/kitchen-sink_canonical.graphql create mode 100644 async-graphql-parser/tests/schemas/minimal.graphql create mode 100644 async-graphql-parser/tests/schemas/minimal_type.graphql create mode 100644 async-graphql-parser/tests/schemas/scalar_type.graphql create mode 100644 async-graphql-parser/tests/schemas/simple_object.graphql create mode 100644 async-graphql-parser/tests/schemas/union.graphql create mode 100644 async-graphql-parser/tests/schemas/union_extension.graphql diff --git a/async-graphql-parser/src/error.rs b/async-graphql-parser/src/error.rs new file mode 100644 index 00000000..a7d65418 --- /dev/null +++ b/async-graphql-parser/src/error.rs @@ -0,0 +1,32 @@ +use crate::Pos; +use pest::error::LineColLocation; +use pest::RuleType; +use std::fmt; + +/// Parser error +#[derive(Error, Debug, PartialEq)] +pub struct Error { + pub pos: Pos, + pub message: String, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.message) + } +} + +impl From> for Error { + fn from(err: pest::error::Error) -> Self { + Error { + pos: { + let (line, column) = match err.line_col { + LineColLocation::Pos((line, column)) => (line, column), + LineColLocation::Span((line, column), _) => (line, column), + }; + Pos { line, column } + }, + message: err.to_string(), + } + } +} diff --git a/async-graphql-parser/src/lib.rs b/async-graphql-parser/src/lib.rs index 95871538..ecba8985 100644 --- a/async-graphql-parser/src/lib.rs +++ b/async-graphql-parser/src/lib.rs @@ -3,11 +3,16 @@ extern crate pest_derive; #[macro_use] extern crate thiserror; -mod pos; pub mod query; +pub mod schema; + +mod error; +mod pos; mod query_parser; +mod utils; mod value; +pub use error::Error; pub use pos::{Pos, Positioned}; -pub use query_parser::{parse_query, parse_value, Error, ParsedValue, Result}; +pub use query_parser::{parse_query, parse_value, ParsedValue, Result}; pub use value::{UploadValue, Value}; diff --git a/async-graphql-parser/src/query.pest b/async-graphql-parser/src/query.pest index ed92e17e..1a444f04 100644 --- a/async-graphql-parser/src/query.pest +++ b/async-graphql-parser/src/query.pest @@ -29,7 +29,6 @@ nonnull_type = { (list_type | name) ~ "!" } type_ = { nonnull_type | list_type | name } // query - arguments = { "(" ~ pair* ~ ")" } directive = { "@" ~ name ~ arguments? } directives = { directive* } diff --git a/async-graphql-parser/src/query_parser.rs b/async-graphql-parser/src/query_parser.rs index 6e988261..bcf3ac8d 100644 --- a/async-graphql-parser/src/query_parser.rs +++ b/async-graphql-parser/src/query_parser.rs @@ -1,102 +1,22 @@ use crate::pos::Positioned; use crate::query::*; +use crate::utils::{to_static_str, unquote_string, PositionCalculator}; use crate::value::Value; -use crate::Pos; -use arrayvec::ArrayVec; -use pest::error::LineColLocation; +use crate::{Error, Pos}; use pest::iterators::Pair; use pest::Parser; use std::borrow::Cow; use std::collections::BTreeMap; use std::fmt; -use std::iter::Peekable; use std::ops::Deref; -use std::str::Chars; #[derive(Parser)] #[grammar = "query.pest"] struct QueryParser; -/// Parser error -#[derive(Error, Debug, PartialEq)] -pub struct Error { - pub pos: Pos, - pub message: String, -} - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}", self.message) - } -} - -impl From> for Error { - fn from(err: pest::error::Error) -> Self { - Error { - pos: { - let (line, column) = match err.line_col { - LineColLocation::Pos((line, column)) => (line, column), - LineColLocation::Span((line, column), _) => (line, column), - }; - Pos { line, column } - }, - message: err.to_string(), - } - } -} - /// Parser result pub type Result = std::result::Result; -pub(crate) struct PositionCalculator<'a> { - input: Peekable>, - pos: usize, - line: usize, - column: usize, -} - -impl<'a> PositionCalculator<'a> { - fn new(input: &'a str) -> PositionCalculator<'a> { - Self { - input: input.chars().peekable(), - 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') => { - if let Some(&'\n') = self.input.peek() { - self.input.next(); - self.line += 1; - self.column = 1; - } else { - 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, - } - } -} - /// Parse a GraphQL query. pub fn parse_query>(input: T) -> Result { let source = input.into(); @@ -134,6 +54,12 @@ pub struct ParsedValue { value: Value, } +impl fmt::Debug for ParsedValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.value) + } +} + impl Deref for ParsedValue { type Target = Value; @@ -647,103 +573,6 @@ fn parse_fragment_definition( )) } -#[inline] -fn to_static_str(s: &str) -> &'static str { - unsafe { (s as *const str).as_ref().unwrap() } -} - -fn unquote_string(s: &'static str, pos: Pos) -> Result> { - debug_assert!(s.starts_with('"') && s.ends_with('"')); - let s = &s[1..s.len() - 1]; - - if !s.contains('\\') { - return Ok(Cow::Borrowed(to_static_str(s))); - } - - let mut chars = s.chars(); - let mut res = String::with_capacity(s.len()); - let mut temp_code_point = ArrayVec::<[u8; 4]>::new(); - - while let Some(c) = chars.next() { - match c { - '\\' => { - match chars.next().expect("slash cant be at the end") { - c @ '"' | c @ '\\' | c @ '/' => res.push(c), - 'b' => res.push('\u{0010}'), - 'f' => res.push('\u{000C}'), - 'n' => res.push('\n'), - 'r' => res.push('\r'), - 't' => res.push('\t'), - 'u' => { - temp_code_point.clear(); - for _ in 0..4 { - match chars.next() { - Some(inner_c) if inner_c.is_digit(16) => { - temp_code_point.push(inner_c as u8) - } - Some(inner_c) => { - return Err(Error { - pos, - message: format!( - "{} is not a valid unicode code point", - inner_c - ), - }); - } - None => { - return Err(Error { - pos, - message: format!( - "{} must have 4 characters after it", - unsafe { - std::str::from_utf8_unchecked( - temp_code_point.as_slice(), - ) - } - ), - }); - } - } - } - - // convert our hex string into a u32, then convert that into a char - match u32::from_str_radix( - unsafe { std::str::from_utf8_unchecked(temp_code_point.as_slice()) }, - 16, - ) - .map(std::char::from_u32) - { - Ok(Some(unicode_char)) => res.push(unicode_char), - _ => { - return Err(Error { - pos, - message: format!( - "{} is not a valid unicode code point", - unsafe { - std::str::from_utf8_unchecked( - temp_code_point.as_slice(), - ) - } - ), - }); - } - } - } - c => { - return Err(Error { - pos, - message: format!("bad escaped char {:?}", c), - }); - } - } - } - c => res.push(c), - } - } - - Ok(Cow::Owned(res)) -} - #[cfg(test)] mod tests { use super::*; diff --git a/async-graphql-parser/src/schema.pest b/async-graphql-parser/src/schema.pest new file mode 100644 index 00000000..e8268490 --- /dev/null +++ b/async-graphql-parser/src/schema.pest @@ -0,0 +1,31 @@ +WHITESPACE = _{ " " | "," | "\t" | "\u{feff}" | NEWLINE } +COMMENT = _{ "#" ~ (!"\n" ~ ANY)* } + +// value +int = @{ "-"? ~ ("0" | (ASCII_NONZERO_DIGIT ~ ASCII_DIGIT*)) } +float = @{ "-"? ~ int ~ "." ~ ASCII_DIGIT+ ~ exp? } +exp = @{ ("E" | "e") ~ ("+" | "-")? ~ ASCII_DIGIT+ } + +string = @{ "\"" ~ string_inner ~ "\"" } +string_inner = @{ (!("\"" | "\\") ~ ANY)* ~ (escape ~ string_inner)? } +escape = @{ "\\" ~ ("\"" | "\\" | "/" | "b" | "f" | "n" | "r" | "t" | unicode) } +unicode = @{ "u" ~ ASCII_HEX_DIGIT{4} } + +boolean = { "true" | "false" } +null = { "null" } +name = @{ (ASCII_ALPHA | "_") ~ (ASCII_ALPHA | ASCII_DIGIT | "_")* } +variable = { "$" ~ name } + +array = { "[" ~ value* ~ "]" } + +pair = { name ~ ":" ~ value } +object = { "{" ~ pair* ~ "}" } + +value = { object | array | variable | float | int | string | null | boolean | name } + +// type +list_type = { "[" ~ type_ ~ "]" } +nonnull_type = { (list_type | name) ~ "!" } +type_ = { nonnull_type | list_type | name } + +// type system diff --git a/async-graphql-parser/src/schema.rs b/async-graphql-parser/src/schema.rs new file mode 100644 index 00000000..a54c6df1 --- /dev/null +++ b/async-graphql-parser/src/schema.rs @@ -0,0 +1,149 @@ +use crate::pos::Positioned; +use crate::ParsedValue; + +#[derive(Debug, PartialEq)] +pub enum Type { + Named(String), + List(Box), + NonNull(Box), +} + +#[derive(Debug)] +pub struct Document { + pub definitions: Vec>, +} + +#[derive(Debug)] +pub enum Definition { + SchemaDefinition(Positioned), + TypeDefinition { + extend: bool, + description: Positioned, + definition: Positioned, + }, + DirectiveDefinition(Positioned), +} + +#[derive(Debug)] +pub struct SchemaDefinition { + pub directives: Vec>, + pub query: Option>, + pub mutation: Option>, + pub subscription: Option>, +} + +#[derive(Debug)] +pub enum TypeDefinition { + Scalar(Positioned), + Object(Positioned), + Interface(Positioned), + Union(Positioned), + Enum(Positioned), + InputObject(Positioned), +} + +#[derive(Debug)] +pub struct ScalarType { + pub name: Positioned, + pub directives: Vec>, +} + +#[derive(Debug)] +pub struct ObjectType { + pub name: Positioned, + pub implements_interfaces: Vec>, + pub directives: Vec>, + pub fields: Vec>, +} + +#[derive(Debug)] +pub struct Field { + pub description: Option>, + pub name: Positioned, + pub arguments: Vec>, + pub ty: Positioned, + pub directives: Vec>, +} + +#[derive(Debug)] +pub struct InputValue { + pub description: Option>, + pub name: Positioned, + pub ty: Positioned, + pub default_value: Option, + pub directives: Vec>, +} + +#[derive(Debug)] +pub struct InterfaceType { + pub name: Positioned, + pub directives: Vec>, + pub fields: Vec>, +} + +#[derive(Debug)] +pub struct UnionType { + pub name: Positioned, + pub directives: Vec>, + pub types: Vec>, +} + +#[derive(Debug)] +pub struct EnumType { + pub name: Positioned, + pub directives: Vec>, + pub values: Vec>, +} + +#[derive(Debug)] +pub struct EnumValue { + pub description: Option>, + pub name: Positioned, + pub directives: Vec>, +} + +#[derive(Debug)] +pub struct InputObjectType { + pub name: Positioned, + pub directives: Vec>, + pub fields: Vec>, +} + +#[derive(Debug)] +pub enum DirectiveLocation { + // executable + Query, + Mutation, + Subscription, + Field, + FragmentDefinition, + FragmentSpread, + InlineFragment, + + // type_system + Schema, + Scalar, + Object, + FieldDefinition, + ArgumentDefinition, + Interface, + Union, + Enum, + EnumValue, + InputObject, + InputFieldDefinition, +} + +#[derive(Debug)] +pub struct DirectiveDefinition { + pub description: Option>, + pub name: Positioned, + pub arguments: Vec>, + pub locations: Vec>, +} + +#[derive(Debug)] +pub struct Directive { + pub name: Positioned, + pub arguments: Vec<(Positioned, Positioned)>, +} diff --git a/async-graphql-parser/src/utils.rs b/async-graphql-parser/src/utils.rs new file mode 100644 index 00000000..7a34e457 --- /dev/null +++ b/async-graphql-parser/src/utils.rs @@ -0,0 +1,153 @@ +use crate::{Error, Pos, Result}; +use arrayvec::ArrayVec; +use pest::iterators::Pair; +use pest::RuleType; +use std::borrow::Cow; +use std::iter::Peekable; +use std::str::Chars; + +pub struct PositionCalculator<'a> { + input: Peekable>, + pos: usize, + line: usize, + column: usize, +} + +impl<'a> PositionCalculator<'a> { + pub fn new(input: &'a str) -> PositionCalculator<'a> { + Self { + input: input.chars().peekable(), + 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') => { + if let Some(&'\n') = self.input.peek() { + self.input.next(); + self.line += 1; + self.column = 1; + } else { + 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, + } + } +} + +#[inline] +pub fn to_static_str(s: &str) -> &'static str { + unsafe { (s as *const str).as_ref().unwrap() } +} + +pub fn unquote_string(s: &'static str, pos: Pos) -> Result> { + debug_assert!(s.starts_with('"') && s.ends_with('"')); + let s = &s[1..s.len() - 1]; + + if !s.contains('\\') { + return Ok(Cow::Borrowed(to_static_str(s))); + } + + let mut chars = s.chars(); + let mut res = String::with_capacity(s.len()); + let mut temp_code_point = ArrayVec::<[u8; 4]>::new(); + + while let Some(c) = chars.next() { + match c { + '\\' => { + match chars.next().expect("slash cant be at the end") { + c @ '"' | c @ '\\' | c @ '/' => res.push(c), + 'b' => res.push('\u{0010}'), + 'f' => res.push('\u{000C}'), + 'n' => res.push('\n'), + 'r' => res.push('\r'), + 't' => res.push('\t'), + 'u' => { + temp_code_point.clear(); + for _ in 0..4 { + match chars.next() { + Some(inner_c) if inner_c.is_digit(16) => { + temp_code_point.push(inner_c as u8) + } + Some(inner_c) => { + return Err(Error { + pos, + message: format!( + "{} is not a valid unicode code point", + inner_c + ), + }); + } + None => { + return Err(Error { + pos, + message: format!( + "{} must have 4 characters after it", + unsafe { + std::str::from_utf8_unchecked( + temp_code_point.as_slice(), + ) + } + ), + }); + } + } + } + + // convert our hex string into a u32, then convert that into a char + match u32::from_str_radix( + unsafe { std::str::from_utf8_unchecked(temp_code_point.as_slice()) }, + 16, + ) + .map(std::char::from_u32) + { + Ok(Some(unicode_char)) => res.push(unicode_char), + _ => { + return Err(Error { + pos, + message: format!( + "{} is not a valid unicode code point", + unsafe { + std::str::from_utf8_unchecked( + temp_code_point.as_slice(), + ) + } + ), + }); + } + } + } + c => { + return Err(Error { + pos, + message: format!("bad escaped char {:?}", c), + }); + } + } + } + c => res.push(c), + } + } + + Ok(Cow::Owned(res)) +} diff --git a/async-graphql-parser/tests/schemas/directive.graphql b/async-graphql-parser/tests/schemas/directive.graphql new file mode 100644 index 00000000..96019ccc --- /dev/null +++ b/async-graphql-parser/tests/schemas/directive.graphql @@ -0,0 +1 @@ +directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT diff --git a/async-graphql-parser/tests/schemas/directive_descriptions.graphql b/async-graphql-parser/tests/schemas/directive_descriptions.graphql new file mode 100644 index 00000000..b17eaacd --- /dev/null +++ b/async-graphql-parser/tests/schemas/directive_descriptions.graphql @@ -0,0 +1,19 @@ +""" +Directs the executor to include this field or fragment only when the `if` argument is true. +""" +directive @include( + """ + Included when true. + """ + if: Boolean! +) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +""" +Directs the executor to skip this field or fragment when the `if` argument is true. +""" +directive @skip( + """ + Skipped when true. + """ + if: Boolean! +) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT diff --git a/async-graphql-parser/tests/schemas/directive_descriptions_canonical.graphql b/async-graphql-parser/tests/schemas/directive_descriptions_canonical.graphql new file mode 100644 index 00000000..6bc4d733 --- /dev/null +++ b/async-graphql-parser/tests/schemas/directive_descriptions_canonical.graphql @@ -0,0 +1,13 @@ +""" + Directs the executor to include this field or fragment only when the `if` argument is true. +""" +directive @include(""" + Included when true. +""" if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +""" + Directs the executor to skip this field or fragment when the `if` argument is true. +""" +directive @skip(""" + Skipped when true. +""" if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT diff --git a/async-graphql-parser/tests/schemas/empty_union.graphql b/async-graphql-parser/tests/schemas/empty_union.graphql new file mode 100644 index 00000000..cba4f2f4 --- /dev/null +++ b/async-graphql-parser/tests/schemas/empty_union.graphql @@ -0,0 +1 @@ +union UndefinedUnion diff --git a/async-graphql-parser/tests/schemas/enum.graphql b/async-graphql-parser/tests/schemas/enum.graphql new file mode 100644 index 00000000..78072603 --- /dev/null +++ b/async-graphql-parser/tests/schemas/enum.graphql @@ -0,0 +1,4 @@ +enum Site { + DESKTOP + MOBILE +} diff --git a/async-graphql-parser/tests/schemas/extend_enum.graphql b/async-graphql-parser/tests/schemas/extend_enum.graphql new file mode 100644 index 00000000..12ce6561 --- /dev/null +++ b/async-graphql-parser/tests/schemas/extend_enum.graphql @@ -0,0 +1,3 @@ +extend enum Site { + VR +} diff --git a/async-graphql-parser/tests/schemas/extend_input.graphql b/async-graphql-parser/tests/schemas/extend_input.graphql new file mode 100644 index 00000000..64ddf3ed --- /dev/null +++ b/async-graphql-parser/tests/schemas/extend_input.graphql @@ -0,0 +1,3 @@ +extend input InputType { + other: Float = 1.23e4 +} diff --git a/async-graphql-parser/tests/schemas/extend_input_canonical.graphql b/async-graphql-parser/tests/schemas/extend_input_canonical.graphql new file mode 100644 index 00000000..df70f320 --- /dev/null +++ b/async-graphql-parser/tests/schemas/extend_input_canonical.graphql @@ -0,0 +1,3 @@ +extend input InputType { + other: Float = 12300 +} diff --git a/async-graphql-parser/tests/schemas/extend_interface.graphql b/async-graphql-parser/tests/schemas/extend_interface.graphql new file mode 100644 index 00000000..5536dc77 --- /dev/null +++ b/async-graphql-parser/tests/schemas/extend_interface.graphql @@ -0,0 +1,3 @@ +extend interface Bar { + two(argument: InputType!): Type +} diff --git a/async-graphql-parser/tests/schemas/extend_object.graphql b/async-graphql-parser/tests/schemas/extend_object.graphql new file mode 100644 index 00000000..5e6920a3 --- /dev/null +++ b/async-graphql-parser/tests/schemas/extend_object.graphql @@ -0,0 +1,3 @@ +extend type Foo { + seven(argument: [String]): Type +} diff --git a/async-graphql-parser/tests/schemas/extend_scalar.graphql b/async-graphql-parser/tests/schemas/extend_scalar.graphql new file mode 100644 index 00000000..a6dc49cf --- /dev/null +++ b/async-graphql-parser/tests/schemas/extend_scalar.graphql @@ -0,0 +1 @@ +extend scalar CustomScalar @onScalar diff --git a/async-graphql-parser/tests/schemas/implements.graphql b/async-graphql-parser/tests/schemas/implements.graphql new file mode 100644 index 00000000..a84b001a --- /dev/null +++ b/async-graphql-parser/tests/schemas/implements.graphql @@ -0,0 +1,3 @@ +type Type1 implements IOne + +type Type1 implements IOne & ITwo diff --git a/async-graphql-parser/tests/schemas/implements_amp.graphql b/async-graphql-parser/tests/schemas/implements_amp.graphql new file mode 100644 index 00000000..e4ff2d38 --- /dev/null +++ b/async-graphql-parser/tests/schemas/implements_amp.graphql @@ -0,0 +1,2 @@ +type Type1 implements & IOne & ITwo +type 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 new file mode 100644 index 00000000..f066126b --- /dev/null +++ b/async-graphql-parser/tests/schemas/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/schemas/input_type.graphql new file mode 100644 index 00000000..db7634ba --- /dev/null +++ b/async-graphql-parser/tests/schemas/input_type.graphql @@ -0,0 +1,4 @@ +input InputType { + key: String! + answer: Int = 42 +} diff --git a/async-graphql-parser/tests/schemas/interface.graphql b/async-graphql-parser/tests/schemas/interface.graphql new file mode 100644 index 00000000..d167a481 --- /dev/null +++ b/async-graphql-parser/tests/schemas/interface.graphql @@ -0,0 +1,3 @@ +interface Bar { + one: Type +} diff --git a/async-graphql-parser/tests/schemas/kitchen-sink.graphql b/async-graphql-parser/tests/schemas/kitchen-sink.graphql new file mode 100644 index 00000000..f94f47c8 --- /dev/null +++ b/async-graphql-parser/tests/schemas/kitchen-sink.graphql @@ -0,0 +1,117 @@ +# Copyright (c) 2015-present, Facebook, Inc. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +schema { + query: QueryType + mutation: MutationType +} + +""" +This is a description +of the `Foo` type. +""" +type Foo implements Bar & Baz { + one: Type + two(argument: InputType!): Type + three(argument: InputType, other: String): Int + four(argument: String = "string"): String + five(argument: [String] = ["string", "string"]): String + six(argument: InputType = {key: "value"}): Type + seven(argument: Int = null): Type +} + +type AnnotatedObject @onObject(arg: "value") { + annotatedField(arg: Type = "default" @onArg): Type @onField +} + +type UndefinedType + +extend type Foo { + seven(argument: [String]): Type +} + +extend type Foo @onType + +interface Bar { + one: Type + four(argument: String = "string"): String +} + +interface AnnotatedInterface @onInterface { + annotatedField(arg: Type @onArg): Type @onField +} + +interface UndefinedInterface + +extend interface Bar { + two(argument: InputType!): Type +} + +extend interface Bar @onInterface + +union Feed = Story | Article | Advert + +union AnnotatedUnion @onUnion = A | B + +union AnnotatedUnionTwo @onUnion = | A | B + +union UndefinedUnion + +extend union Feed = Photo | Video + +extend union Feed @onUnion + +scalar CustomScalar + +scalar AnnotatedScalar @onScalar + +extend scalar CustomScalar @onScalar + +enum Site { + DESKTOP + MOBILE +} + +enum AnnotatedEnum @onEnum { + ANNOTATED_VALUE @onEnumValue + OTHER_VALUE +} + +enum UndefinedEnum + +extend enum Site { + VR +} + +extend enum Site @onEnum + +input InputType { + key: String! + answer: Int = 42 +} + +input AnnotatedInput @onInputObject { + annotatedField: Type @onField +} + +input UndefinedInput + +extend input InputType { + other: Float = 1.23e4 +} + +extend input InputType @onInputObject + +directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +directive @include(if: Boolean!) + on FIELD + | FRAGMENT_SPREAD + | INLINE_FRAGMENT + +directive @include2(if: Boolean!) on + | FIELD + | FRAGMENT_SPREAD + | INLINE_FRAGMENT diff --git a/async-graphql-parser/tests/schemas/kitchen-sink_canonical.graphql b/async-graphql-parser/tests/schemas/kitchen-sink_canonical.graphql new file mode 100644 index 00000000..7bdb4fad --- /dev/null +++ b/async-graphql-parser/tests/schemas/kitchen-sink_canonical.graphql @@ -0,0 +1,106 @@ +schema { + query: QueryType + mutation: MutationType +} + +""" + This is a description + of the `Foo` type. +""" +type Foo implements Bar & Baz { + one: Type + two(argument: InputType!): Type + three(argument: InputType, other: String): Int + four(argument: String = "string"): String + five(argument: [String] = ["string", "string"]): String + six(argument: InputType = {key: "value"}): Type + seven(argument: Int = null): Type +} + +type AnnotatedObject @onObject(arg: "value") { + annotatedField(arg: Type = "default" @onArg): Type @onField +} + +type UndefinedType + +extend type Foo { + seven(argument: [String]): Type +} + +extend type Foo @onType + +interface Bar { + one: Type + four(argument: String = "string"): String +} + +interface AnnotatedInterface @onInterface { + annotatedField(arg: Type @onArg): Type @onField +} + +interface UndefinedInterface + +extend interface Bar { + two(argument: InputType!): Type +} + +extend interface Bar @onInterface + +union Feed = Story | Article | Advert + +union AnnotatedUnion @onUnion = A | B + +union AnnotatedUnionTwo @onUnion = A | B + +union UndefinedUnion + +extend union Feed = Photo | Video + +extend union Feed @onUnion + +scalar CustomScalar + +scalar AnnotatedScalar @onScalar + +extend scalar CustomScalar @onScalar + +enum Site { + DESKTOP + MOBILE +} + +enum AnnotatedEnum @onEnum { + ANNOTATED_VALUE @onEnumValue + OTHER_VALUE +} + +enum UndefinedEnum + +extend enum Site { + VR +} + +extend enum Site @onEnum + +input InputType { + key: String! + answer: Int = 42 +} + +input AnnotatedInput @onInputObject { + annotatedField: Type @onField +} + +input UndefinedInput + +extend input InputType { + other: Float = 12300 +} + +extend input InputType @onInputObject + +directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT + +directive @include2(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT diff --git a/async-graphql-parser/tests/schemas/minimal.graphql b/async-graphql-parser/tests/schemas/minimal.graphql new file mode 100644 index 00000000..614a1f13 --- /dev/null +++ b/async-graphql-parser/tests/schemas/minimal.graphql @@ -0,0 +1,3 @@ +schema { + query: Query +} diff --git a/async-graphql-parser/tests/schemas/minimal_type.graphql b/async-graphql-parser/tests/schemas/minimal_type.graphql new file mode 100644 index 00000000..c2df5588 --- /dev/null +++ b/async-graphql-parser/tests/schemas/minimal_type.graphql @@ -0,0 +1 @@ +type UndefinedType diff --git a/async-graphql-parser/tests/schemas/scalar_type.graphql b/async-graphql-parser/tests/schemas/scalar_type.graphql new file mode 100644 index 00000000..43955310 --- /dev/null +++ b/async-graphql-parser/tests/schemas/scalar_type.graphql @@ -0,0 +1,2 @@ +"This is the best scalar type" +scalar BestType @perfectness(value: 100500) diff --git a/async-graphql-parser/tests/schemas/simple_object.graphql b/async-graphql-parser/tests/schemas/simple_object.graphql new file mode 100644 index 00000000..a454e38e --- /dev/null +++ b/async-graphql-parser/tests/schemas/simple_object.graphql @@ -0,0 +1,3 @@ +type Foo { + bar: Type +} diff --git a/async-graphql-parser/tests/schemas/union.graphql b/async-graphql-parser/tests/schemas/union.graphql new file mode 100644 index 00000000..ddadd4a8 --- /dev/null +++ b/async-graphql-parser/tests/schemas/union.graphql @@ -0,0 +1 @@ +union Feed = Story | Article | Advert diff --git a/async-graphql-parser/tests/schemas/union_extension.graphql b/async-graphql-parser/tests/schemas/union_extension.graphql new file mode 100644 index 00000000..a18d4648 --- /dev/null +++ b/async-graphql-parser/tests/schemas/union_extension.graphql @@ -0,0 +1 @@ +extend union Feed = Photo | Video From 6c7fb0dff7fc57eafb7a1acc19e9caaace2262ba Mon Sep 17 00:00:00 2001 From: sunli Date: Fri, 15 May 2020 22:27:38 +0800 Subject: [PATCH 2/2] Schema parser has been implemented. --- async-graphql-parser/src/error.rs | 3 + async-graphql-parser/src/lib.rs | 6 +- async-graphql-parser/src/query.pest | 7 +- async-graphql-parser/src/query_parser.rs | 23 +- async-graphql-parser/src/schema.pest | 49 +- async-graphql-parser/src/schema.rs | 40 +- async-graphql-parser/src/schema_parser.rs | 761 ++++++++++++++++++ async-graphql-parser/src/utils.rs | 11 +- .../tests/queries/multiline_string.graphql | 7 + feature-comparison.md | 4 +- 10 files changed, 871 insertions(+), 40 deletions(-) create mode 100644 async-graphql-parser/src/schema_parser.rs create mode 100644 async-graphql-parser/tests/queries/multiline_string.graphql diff --git a/async-graphql-parser/src/error.rs b/async-graphql-parser/src/error.rs index a7d65418..158e8bf0 100644 --- a/async-graphql-parser/src/error.rs +++ b/async-graphql-parser/src/error.rs @@ -30,3 +30,6 @@ impl From> for Error { } } } + +/// Parser result +pub type Result = std::result::Result; diff --git a/async-graphql-parser/src/lib.rs b/async-graphql-parser/src/lib.rs index ecba8985..359f6085 100644 --- a/async-graphql-parser/src/lib.rs +++ b/async-graphql-parser/src/lib.rs @@ -9,10 +9,12 @@ pub mod schema; mod error; mod pos; mod query_parser; +mod schema_parser; mod utils; mod value; -pub use error::Error; +pub use error::{Error, Result}; pub use pos::{Pos, Positioned}; -pub use query_parser::{parse_query, parse_value, ParsedValue, Result}; +pub use query_parser::{parse_query, parse_value, ParsedValue}; +pub use schema_parser::parse_schema; pub use value::{UploadValue, Value}; diff --git a/async-graphql-parser/src/query.pest b/async-graphql-parser/src/query.pest index 1a444f04..16fa6962 100644 --- a/async-graphql-parser/src/query.pest +++ b/async-graphql-parser/src/query.pest @@ -6,8 +6,11 @@ int = @{ "-"? ~ ("0" | (ASCII_NONZERO_DIGIT ~ ASCII_DIGIT*)) } float = @{ "-"? ~ int ~ "." ~ ASCII_DIGIT+ ~ exp? } exp = @{ ("E" | "e") ~ ("+" | "-")? ~ ASCII_DIGIT+ } -string = @{ "\"" ~ string_inner ~ "\"" } -string_inner = @{ (!("\"" | "\\") ~ ANY)* ~ (escape ~ string_inner)? } +singleline_string = @{ "\"" ~ singleline_inner ~ "\"" } +singleline_inner = @{ (!("\"" | "\\") ~ ANY)* ~ (escape ~ singleline_inner)? } +multiline_inner = @{ (!("\"\"\"" | "\\") ~ ANY)* ~ (escape ~ multiline_inner)? } +multiline_string = @{ "\"\"\"" ~ multiline_inner ~ "\"\"\"" } +string = @{ multiline_string | singleline_string } escape = @{ "\\" ~ ("\"" | "\\" | "/" | "b" | "f" | "n" | "r" | "t" | unicode) } unicode = @{ "u" ~ ASCII_HEX_DIGIT{4} } diff --git a/async-graphql-parser/src/query_parser.rs b/async-graphql-parser/src/query_parser.rs index bcf3ac8d..5168af7f 100644 --- a/async-graphql-parser/src/query_parser.rs +++ b/async-graphql-parser/src/query_parser.rs @@ -2,7 +2,7 @@ use crate::pos::Positioned; use crate::query::*; use crate::utils::{to_static_str, unquote_string, PositionCalculator}; use crate::value::Value; -use crate::{Error, Pos}; +use crate::Result; use pest::iterators::Pair; use pest::Parser; use std::borrow::Cow; @@ -14,9 +14,6 @@ use std::ops::Deref; #[grammar = "query.pest"] struct QueryParser; -/// Parser result -pub type Result = std::result::Result; - /// Parse a GraphQL query. pub fn parse_query>(input: T) -> Result { let source = input.into(); @@ -291,14 +288,8 @@ fn parse_value2(pair: Pair, pc: &mut PositionCalculator) -> Result Rule::float => Value::Float(pair.as_str().parse().unwrap()), Rule::int => Value::Int(pair.as_str().parse().unwrap()), Rule::string => Value::String({ - let start_pos = pair.as_span().start_pos().line_col(); - unquote_string( - to_static_str(pair.as_str()), - Pos { - line: start_pos.0, - column: start_pos.1, - }, - )? + let pos = pc.step(&pair); + unquote_string(pair.as_str(), pos)? }), Rule::name => Value::Enum(to_static_str(pair.as_str())), Rule::boolean => Value::Boolean(match pair.as_str() { @@ -331,9 +322,7 @@ fn parse_object_value(pair: Pair, pc: &mut PositionCalculator) -> Result { - map.extend(std::iter::once(parse_object_pair(pair, pc)?)); - } + Rule::pair => map.extend(std::iter::once(parse_object_pair(pair, pc)?)), _ => unreachable!(), } } @@ -344,9 +333,7 @@ fn parse_array_value(pair: Pair, pc: &mut PositionCalculator) -> Result { - array.push(parse_value2(pair, pc)?); - } + Rule::value => array.push(parse_value2(pair, pc)?), _ => unreachable!(), } } diff --git a/async-graphql-parser/src/schema.pest b/async-graphql-parser/src/schema.pest index e8268490..16b2d0f7 100644 --- a/async-graphql-parser/src/schema.pest +++ b/async-graphql-parser/src/schema.pest @@ -6,22 +6,24 @@ int = @{ "-"? ~ ("0" | (ASCII_NONZERO_DIGIT ~ ASCII_DIGIT*)) } float = @{ "-"? ~ int ~ "." ~ ASCII_DIGIT+ ~ exp? } exp = @{ ("E" | "e") ~ ("+" | "-")? ~ ASCII_DIGIT+ } -string = @{ "\"" ~ string_inner ~ "\"" } -string_inner = @{ (!("\"" | "\\") ~ ANY)* ~ (escape ~ string_inner)? } +singleline_string = @{ "\"" ~ singleline_inner ~ "\"" } +singleline_inner = @{ (!("\"" | "\\") ~ ANY)* ~ (escape ~ singleline_inner)? } +multiline_inner = @{ (!("\"\"\"" | "\\") ~ ANY)* ~ (escape ~ multiline_inner)? } +multiline_string = @{ "\"\"\"" ~ multiline_inner ~ "\"\"\"" } +string = @{ multiline_string | singleline_string } escape = @{ "\\" ~ ("\"" | "\\" | "/" | "b" | "f" | "n" | "r" | "t" | unicode) } unicode = @{ "u" ~ ASCII_HEX_DIGIT{4} } boolean = { "true" | "false" } null = { "null" } name = @{ (ASCII_ALPHA | "_") ~ (ASCII_ALPHA | ASCII_DIGIT | "_")* } -variable = { "$" ~ name } array = { "[" ~ value* ~ "]" } pair = { name ~ ":" ~ value } object = { "{" ~ pair* ~ "}" } -value = { object | array | variable | float | int | string | null | boolean | name } +value = { object | array | float | int | string | null | boolean | name } // type list_type = { "[" ~ type_ ~ "]" } @@ -29,3 +31,42 @@ nonnull_type = { (list_type | name) ~ "!" } type_ = { nonnull_type | list_type | name } // type system +arguments = { "(" ~ pair* ~ ")" } +directive = { "@" ~ name ~ arguments? } +directives = { directive* } +schema_definition = { extend? ~ "schema" ~ directives? ~ "{" ~ root_operation_type_definition* ~ "}" } +operation_type = @{ "query" | "mutation" | "subscription" } +root_operation_type_definition = { operation_type ~ ":" ~ name } +extend = { "extend" } +sclar_type_definition = { string? ~ extend? ~ "scalar" ~ name ~ directives? } +implements_interfaces = { "implements" ~ "&"? ~ name ~ ("&" ~ name)* } +default_value = { "=" ~ value } +input_value_definition = { string? ~ name ~ ":" ~ type_ ~ default_value? ~ directives? } +arguments_definition = { "(" ~ input_value_definition* ~ ")" } +field_definition = { string? ~ name ~ arguments_definition? ~ ":" ~ type_ ~ directives? } +fields_definition = { "{" ~ field_definition* ~ "}" } +object_type_definition = { string? ~ extend? ~ "type" ~ name ~ implements_interfaces? ~ directives? ~ fields_definition? } +interface_type_definition = { string? ~ extend? ~ "interface" ~ name ~ directives? ~ fields_definition? } +union_member_types = { "=" ~ "|"? ~ name ~ ("|" ~ name)* } +union_type_definition = { string? ~ extend? ~ "union" ~ name ~ directives? ~ union_member_types? } +enum_value_definition = { string? ~ name ~ directives? } +enum_values_definition = { "{" ~ enum_value_definition* ~ "}" } +enum_type_definition = { string? ~ extend? ~ "enum" ~ name ~ directives? ~ enum_values_definition? } +input_fields_definition = { "{" ~ input_value_definition* ~ "}" } +input_type_definition = { string? ~ extend? ~ "input" ~ name ~ directives? ~ input_fields_definition? } +executable_directive_location = @{ "QUERY" | "MUTATION" | "SUBSCRIPTION" | "FIELD" | "FRAGMENT_DEFINITION" | "FRAGMENT_SPREAD" | "INLINE_FRAGMENT" } +type_system_directive_location = @{ "SCHEMA" | "SCALAR" | "OBJECT" | "FIELD_DEFINITION" | "ARGUMENT_DEFINITION" | "INTERFACE" | "UNION" | "ENUM" | "ENUM_VALUE" | "INPUT_OBJECT" | "INPUT_FIELD_DEFINITION"} +directive_location = @{ executable_directive_location | type_system_directive_location } +directive_locations = { "|"? ~ directive_location ~ ("|" ~ directive_location)* } +directive_definition = { string? ~ "directive" ~ "@" ~ name ~ arguments_definition ~ "on" ~ directive_locations } +definition = { + schema_definition | + sclar_type_definition | + object_type_definition | + interface_type_definition | + union_type_definition | + enum_type_definition | + input_type_definition | + directive_definition +} +document = { SOI ~ definition+ ~ EOI} diff --git a/async-graphql-parser/src/schema.rs b/async-graphql-parser/src/schema.rs index a54c6df1..c595d195 100644 --- a/async-graphql-parser/src/schema.rs +++ b/async-graphql-parser/src/schema.rs @@ -1,5 +1,18 @@ use crate::pos::Positioned; -use crate::ParsedValue; +use std::collections::BTreeMap; + +#[derive(Clone, Debug)] +#[allow(missing_docs)] +pub enum Value { + Null, + Int(i64), + Float(f64), + String(String), + Boolean(bool), + Enum(String), + List(Vec), + Object(BTreeMap), +} #[derive(Debug, PartialEq)] pub enum Type { @@ -16,16 +29,13 @@ pub struct Document { #[derive(Debug)] pub enum Definition { SchemaDefinition(Positioned), - TypeDefinition { - extend: bool, - description: Positioned, - definition: Positioned, - }, + TypeDefinition(Positioned), DirectiveDefinition(Positioned), } #[derive(Debug)] pub struct SchemaDefinition { + pub extend: bool, pub directives: Vec>, pub query: Option>, pub mutation: Option>, @@ -44,12 +54,16 @@ pub enum TypeDefinition { #[derive(Debug)] pub struct ScalarType { + pub extend: bool, + pub description: Option>, pub name: Positioned, pub directives: Vec>, } #[derive(Debug)] pub struct ObjectType { + pub extend: bool, + pub description: Option>, pub name: Positioned, pub implements_interfaces: Vec>, pub directives: Vec>, @@ -70,12 +84,14 @@ pub struct InputValue { pub description: Option>, pub name: Positioned, pub ty: Positioned, - pub default_value: Option, + pub default_value: Option>, pub directives: Vec>, } #[derive(Debug)] pub struct InterfaceType { + pub extend: bool, + pub description: Option>, pub name: Positioned, pub directives: Vec>, pub fields: Vec>, @@ -83,13 +99,17 @@ pub struct InterfaceType { #[derive(Debug)] pub struct UnionType { + pub extend: bool, + pub description: Option>, pub name: Positioned, pub directives: Vec>, - pub types: Vec>, + pub members: Vec>, } #[derive(Debug)] pub struct EnumType { + pub extend: bool, + pub description: Option>, pub name: Positioned, pub directives: Vec>, pub values: Vec>, @@ -104,6 +124,8 @@ pub struct EnumValue { #[derive(Debug)] pub struct InputObjectType { + pub extend: bool, + pub description: Option>, pub name: Positioned, pub directives: Vec>, pub fields: Vec>, @@ -145,5 +167,5 @@ pub struct DirectiveDefinition { #[derive(Debug)] pub struct Directive { pub name: Positioned, - pub arguments: Vec<(Positioned, Positioned)>, + pub arguments: Vec<(Positioned, Positioned)>, } diff --git a/async-graphql-parser/src/schema_parser.rs b/async-graphql-parser/src/schema_parser.rs new file mode 100644 index 00000000..ca70eb8d --- /dev/null +++ b/async-graphql-parser/src/schema_parser.rs @@ -0,0 +1,761 @@ +use crate::schema::*; +use crate::utils::{unquote_string, PositionCalculator}; +use crate::{Positioned, Result}; +use pest::iterators::Pair; +use pest::Parser; +use std::collections::BTreeMap; + +#[derive(Parser)] +#[grammar = "schema.pest"] +struct SchemaParser; + +/// Parse a GraphQL schema. +pub fn parse_schema>(input: T) -> Result { + let document_pair: Pair = SchemaParser::parse(Rule::document, input.as_ref())? + .next() + .unwrap(); + let mut definitions = Vec::new(); + let mut pc = PositionCalculator::new(input.as_ref()); + + for pair in document_pair.into_inner() { + match pair.as_rule() { + Rule::definition => { + for pair in pair.into_inner() { + match pair.as_rule() { + Rule::schema_definition => { + definitions.push( + parse_schema_definition(pair, &mut pc)? + .pack(Definition::SchemaDefinition), + ); + } + Rule::sclar_type_definition => definitions.push( + parse_scalar_type(pair, &mut pc)? + .pack(TypeDefinition::Scalar) + .pack(Definition::TypeDefinition), + ), + Rule::object_type_definition => definitions.push( + parse_object_type(pair, &mut pc)? + .pack(TypeDefinition::Object) + .pack(Definition::TypeDefinition), + ), + Rule::interface_type_definition => definitions.push( + parse_interface_type(pair, &mut pc)? + .pack(TypeDefinition::Interface) + .pack(Definition::TypeDefinition), + ), + Rule::union_type_definition => definitions.push( + parse_union_type(pair, &mut pc)? + .pack(TypeDefinition::Union) + .pack(Definition::TypeDefinition), + ), + Rule::enum_type_definition => definitions.push( + parse_enum_type(pair, &mut pc)? + .pack(TypeDefinition::Enum) + .pack(Definition::TypeDefinition), + ), + Rule::input_type_definition => definitions.push( + parse_input_type(pair, &mut pc)? + .pack(TypeDefinition::InputObject) + .pack(Definition::TypeDefinition), + ), + Rule::directive_definition => definitions.push( + parse_directive_definition(pair, &mut pc)? + .pack(Definition::DirectiveDefinition), + ), + _ => unreachable!(), + } + } + } + Rule::EOI => {} + _ => unreachable!(), + } + } + + Ok(Document { definitions }) +} + +fn parse_schema_definition( + pair: Pair, + pc: &mut PositionCalculator, +) -> Result> { + let pos = pc.step(&pair); + let mut extend = false; + let mut directives = None; + let mut query = None; + let mut mutation = None; + let mut subscription = None; + + for pair in pair.into_inner() { + match pair.as_rule() { + Rule::extend => extend = true, + Rule::directives => directives = Some(parse_directives(pair, pc)?), + Rule::root_operation_type_definition => { + let mut op_name = None; + let mut ty_name = None; + for pair in pair.into_inner() { + match pair.as_rule() { + Rule::operation_type => op_name = Some(pair.as_str().to_string()), + Rule::name => { + ty_name = + Some(Positioned::new(pair.as_str().to_string(), pc.step(&pair))) + } + _ => unreachable!(), + } + } + match op_name.as_deref() { + Some("query") => query = ty_name, + Some("mutation") => mutation = ty_name, + Some("subscription") => subscription = ty_name, + _ => unreachable!(), + } + } + _ => unreachable!(), + } + } + + Ok(Positioned::new( + SchemaDefinition { + extend, + directives: directives.unwrap_or_default(), + query, + mutation, + subscription, + }, + pos, + )) +} + +fn parse_directive(pair: Pair, pc: &mut PositionCalculator) -> Result> { + let pos = pc.step(&pair); + let mut name = None; + let mut arguments = None; + for pair in pair.into_inner() { + match pair.as_rule() { + Rule::name => { + let pos = pc.step(&pair); + name = Some(Positioned::new(pair.as_str().to_string(), pos)) + } + Rule::arguments => arguments = Some(parse_arguments(pair, pc)?), + _ => unreachable!(), + } + } + Ok(Positioned::new( + Directive { + name: name.unwrap(), + arguments: arguments.unwrap_or_default(), + }, + pos, + )) +} + +fn parse_directives( + pair: Pair, + pc: &mut PositionCalculator, +) -> Result>> { + let mut directives = Vec::new(); + for pair in pair.into_inner() { + match pair.as_rule() { + Rule::directive => directives.push(parse_directive(pair, pc)?), + _ => unreachable!(), + } + } + Ok(directives) +} + +fn parse_arguments( + pair: Pair, + pc: &mut PositionCalculator, +) -> Result, Positioned)>> { + let mut arguments = Vec::new(); + for pair in pair.into_inner() { + match pair.as_rule() { + Rule::pair => arguments.extend(std::iter::once(parse_pair(pair, pc)?)), + _ => unreachable!(), + } + } + Ok(arguments) +} + +fn parse_pair( + pair: Pair, + pc: &mut PositionCalculator, +) -> Result<(Positioned, Positioned)> { + let mut name = None; + let mut value = None; + for pair in pair.into_inner() { + match pair.as_rule() { + Rule::name => name = Some(Positioned::new(pair.as_str().to_string(), pc.step(&pair))), + Rule::value => { + value = { + let pos = pc.step(&pair); + Some(Positioned::new(parse_value(pair, pc)?, pos)) + } + } + _ => unreachable!(), + } + } + Ok((name.unwrap(), value.unwrap())) +} + +fn parse_value(pair: Pair, pc: &mut PositionCalculator) -> Result { + let pair = pair.into_inner().next().unwrap(); + Ok(match pair.as_rule() { + Rule::object => parse_object_value(pair, pc)?, + Rule::array => parse_array_value(pair, pc)?, + Rule::float => Value::Float(pair.as_str().parse().unwrap()), + Rule::int => Value::Int(pair.as_str().parse().unwrap()), + Rule::string => Value::String({ + let pos = pc.step(&pair); + unquote_string(pair.as_str(), pos).map(|s| s.to_string())? + }), + Rule::name => Value::Enum(pair.to_string()), + Rule::boolean => Value::Boolean(match pair.as_str() { + "true" => true, + "false" => false, + _ => unreachable!(), + }), + Rule::null => Value::Null, + _ => unreachable!(), + }) +} + +fn parse_object_pair(pair: Pair, pc: &mut PositionCalculator) -> Result<(String, Value)> { + let mut name = None; + let mut value = None; + for pair in pair.into_inner() { + match pair.as_rule() { + Rule::name => name = Some(pair.as_str().to_string()), + Rule::value => value = Some(parse_value(pair, pc)?), + _ => unreachable!(), + } + } + Ok((name.unwrap(), value.unwrap())) +} + +fn parse_object_value(pair: Pair, pc: &mut PositionCalculator) -> Result { + let mut map = BTreeMap::new(); + for pair in pair.into_inner() { + match pair.as_rule() { + Rule::pair => map.extend(std::iter::once(parse_object_pair(pair, pc)?)), + _ => unreachable!(), + } + } + Ok(Value::Object(map)) +} + +fn parse_array_value(pair: Pair, pc: &mut PositionCalculator) -> Result { + let mut array = Vec::new(); + for pair in pair.into_inner() { + match pair.as_rule() { + Rule::value => array.push(parse_value(pair, pc)?), + _ => unreachable!(), + } + } + Ok(Value::List(array)) +} + +fn parse_scalar_type( + pair: Pair, + pc: &mut PositionCalculator, +) -> Result> { + let pos = pc.step(&pair); + let mut description = None; + let mut extend = false; + let mut name = None; + let mut directives = None; + + for pair in pair.into_inner() { + match pair.as_rule() { + Rule::string => { + description = Some(Positioned::new(pair.as_str().to_string(), pc.step(&pair))) + } + Rule::extend => extend = true, + Rule::name => name = Some(Positioned::new(pair.as_str().to_string(), pc.step(&pair))), + Rule::directives => directives = Some(parse_directives(pair, pc)?), + _ => unreachable!(), + } + } + + Ok(Positioned::new( + ScalarType { + extend, + description, + name: name.unwrap(), + directives: directives.unwrap_or_default(), + }, + pos, + )) +} + +fn parse_implements_interfaces( + pair: Pair, + pc: &mut PositionCalculator, +) -> Result>> { + let mut interfaces = Vec::new(); + for pair in pair.into_inner() { + match pair.as_rule() { + Rule::name => { + interfaces.push(Positioned::new(pair.as_str().to_string(), pc.step(&pair))) + } + _ => unreachable!(), + } + } + Ok(interfaces) +} + +fn parse_type(pair: Pair, pc: &mut PositionCalculator) -> Result { + let pair = pair.into_inner().next().unwrap(); + match pair.as_rule() { + Rule::nonnull_type => Ok(Type::NonNull(Box::new(parse_type(pair, pc)?))), + Rule::list_type => Ok(Type::List(Box::new(parse_type(pair, pc)?))), + Rule::name => Ok(Type::Named(pair.as_str().to_string())), + Rule::type_ => parse_type(pair, pc), + _ => unreachable!(), + } +} + +fn parse_input_value_definition( + pair: Pair, + pc: &mut PositionCalculator, +) -> Result> { + let pos = pc.step(&pair); + let mut description = None; + let mut name = None; + let mut type_ = None; + let mut default_value = None; + let mut directives = None; + + for pair in pair.into_inner() { + match pair.as_rule() { + Rule::string => { + description = Some(Positioned::new(pair.as_str().to_string(), pc.step(&pair))) + } + Rule::name => name = Some(Positioned::new(pair.as_str().to_string(), pc.step(&pair))), + Rule::type_ => { + let pos = pc.step(&pair); + type_ = Some(Positioned::new(parse_type(pair, pc)?, pos)); + } + Rule::default_value => { + let pos = pc.step(&pair); + default_value = Some(Positioned::new( + parse_value(pair.into_inner().next().unwrap(), pc)?, + pos, + )); + } + Rule::directives => directives = Some(parse_directives(pair, pc)?), + _ => unreachable!(), + } + } + + Ok(Positioned::new( + InputValue { + description, + name: name.unwrap(), + default_value, + ty: type_.unwrap(), + directives: directives.unwrap_or_default(), + }, + pos, + )) +} + +fn parse_arguments_definition( + pair: Pair, + pc: &mut PositionCalculator, +) -> Result>> { + let mut arguments = Vec::new(); + for pair in pair.into_inner() { + match pair.as_rule() { + Rule::input_value_definition => arguments.push(parse_input_value_definition(pair, pc)?), + _ => unreachable!(), + } + } + Ok(arguments) +} + +fn parse_field_definition( + pair: Pair, + pc: &mut PositionCalculator, +) -> Result> { + let pos = pc.step(&pair); + let mut description = None; + let mut name = None; + let mut arguments = None; + let mut type_ = None; + let mut directives = None; + + for pair in pair.into_inner() { + match pair.as_rule() { + Rule::string => { + description = Some(Positioned::new(pair.as_str().to_string(), pc.step(&pair))) + } + Rule::name => name = Some(Positioned::new(pair.as_str().to_string(), pc.step(&pair))), + Rule::arguments_definition => arguments = Some(parse_arguments_definition(pair, pc)?), + Rule::type_ => { + let pos = pc.step(&pair); + type_ = Some(Positioned::new(parse_type(pair, pc)?, pos)); + } + Rule::directives => directives = Some(parse_directives(pair, pc)?), + _ => unreachable!(), + } + } + + Ok(Positioned::new( + Field { + description, + name: name.unwrap(), + arguments: arguments.unwrap_or_default(), + ty: type_.unwrap(), + directives: directives.unwrap_or_default(), + }, + pos, + )) +} + +fn parse_fields_definition( + pair: Pair, + pc: &mut PositionCalculator, +) -> Result>> { + let mut fields = Vec::new(); + for pair in pair.into_inner() { + match pair.as_rule() { + Rule::field_definition => fields.push(parse_field_definition(pair, pc)?), + _ => unreachable!(), + } + } + Ok(fields) +} + +fn parse_object_type( + pair: Pair, + pc: &mut PositionCalculator, +) -> Result> { + let pos = pc.step(&pair); + let mut description = None; + let mut extend = false; + let mut name = None; + let mut implements_interfaces = None; + let mut directives = None; + let mut fields = None; + + for pair in pair.into_inner() { + match pair.as_rule() { + Rule::string => { + description = Some(Positioned::new(pair.as_str().to_string(), pc.step(&pair))) + } + Rule::extend => extend = true, + Rule::name => name = Some(Positioned::new(pair.as_str().to_string(), pc.step(&pair))), + Rule::implements_interfaces => { + implements_interfaces = Some(parse_implements_interfaces(pair, pc)?) + } + Rule::directives => directives = Some(parse_directives(pair, pc)?), + Rule::fields_definition => fields = Some(parse_fields_definition(pair, pc)?), + _ => unreachable!(), + } + } + + Ok(Positioned::new( + ObjectType { + extend, + description, + name: name.unwrap(), + implements_interfaces: implements_interfaces.unwrap_or_default(), + directives: directives.unwrap_or_default(), + fields: fields.unwrap_or_default(), + }, + pos, + )) +} + +fn parse_interface_type( + pair: Pair, + pc: &mut PositionCalculator, +) -> Result> { + let pos = pc.step(&pair); + let mut description = None; + let mut extend = false; + let mut name = None; + let mut directives = None; + let mut fields = None; + + for pair in pair.into_inner() { + match pair.as_rule() { + Rule::string => { + description = Some(Positioned::new(pair.as_str().to_string(), pc.step(&pair))) + } + Rule::extend => extend = true, + Rule::name => name = Some(Positioned::new(pair.as_str().to_string(), pc.step(&pair))), + Rule::directives => directives = Some(parse_directives(pair, pc)?), + Rule::fields_definition => fields = Some(parse_fields_definition(pair, pc)?), + _ => unreachable!(), + } + } + + Ok(Positioned::new( + InterfaceType { + extend, + description, + name: name.unwrap(), + directives: directives.unwrap_or_default(), + fields: fields.unwrap_or_default(), + }, + pos, + )) +} + +fn parse_union_members( + pair: Pair, + pc: &mut PositionCalculator, +) -> Result>> { + let mut members = Vec::new(); + for pair in pair.into_inner() { + match pair.as_rule() { + Rule::name => members.push(Positioned::new(pair.as_str().to_string(), pc.step(&pair))), + _ => unreachable!(), + } + } + Ok(members) +} + +fn parse_union_type( + pair: Pair, + pc: &mut PositionCalculator, +) -> Result> { + let pos = pc.step(&pair); + let mut description = None; + let mut extend = false; + let mut name = None; + let mut directives = None; + let mut members = None; + + for pair in pair.into_inner() { + match pair.as_rule() { + Rule::string => { + description = Some(Positioned::new(pair.as_str().to_string(), pc.step(&pair))) + } + Rule::extend => extend = true, + Rule::name => name = Some(Positioned::new(pair.as_str().to_string(), pc.step(&pair))), + Rule::directives => directives = Some(parse_directives(pair, pc)?), + Rule::union_member_types => members = Some(parse_union_members(pair, pc)?), + _ => unreachable!(), + } + } + + Ok(Positioned::new( + UnionType { + extend, + description, + name: name.unwrap(), + directives: directives.unwrap_or_default(), + members: members.unwrap_or_default(), + }, + pos, + )) +} + +fn parse_enum_value( + pair: Pair, + pc: &mut PositionCalculator, +) -> Result> { + let pos = pc.step(&pair); + let mut description = None; + let mut name = None; + let mut directives = None; + for pair in pair.into_inner() { + match pair.as_rule() { + Rule::string => { + description = Some(Positioned::new(pair.as_str().to_string(), pc.step(&pair))) + } + Rule::name => name = Some(Positioned::new(pair.as_str().to_string(), pc.step(&pair))), + Rule::directives => directives = Some(parse_directives(pair, pc)?), + _ => unreachable!(), + } + } + Ok(Positioned::new( + EnumValue { + description, + name: name.unwrap(), + directives: directives.unwrap_or_default(), + }, + pos, + )) +} + +fn parse_enum_values( + pair: Pair, + pc: &mut PositionCalculator, +) -> Result>> { + let mut values = Vec::new(); + for pair in pair.into_inner() { + match pair.as_rule() { + Rule::enum_value_definition => values.push(parse_enum_value(pair, pc)?), + _ => unreachable!(), + } + } + Ok(values) +} + +fn parse_enum_type(pair: Pair, pc: &mut PositionCalculator) -> Result> { + let pos = pc.step(&pair); + let mut description = None; + let mut extend = false; + let mut name = None; + let mut directives = None; + let mut values = None; + + for pair in pair.into_inner() { + match pair.as_rule() { + Rule::string => { + description = Some(Positioned::new(pair.as_str().to_string(), pc.step(&pair))) + } + Rule::extend => extend = true, + Rule::name => name = Some(Positioned::new(pair.as_str().to_string(), pc.step(&pair))), + Rule::directives => directives = Some(parse_directives(pair, pc)?), + Rule::enum_values_definition => values = Some(parse_enum_values(pair, pc)?), + _ => unreachable!(), + } + } + + Ok(Positioned::new( + EnumType { + extend, + description, + name: name.unwrap(), + directives: directives.unwrap_or_default(), + values: values.unwrap_or_default(), + }, + pos, + )) +} + +fn parse_input_type( + pair: Pair, + pc: &mut PositionCalculator, +) -> Result> { + let pos = pc.step(&pair); + let mut description = None; + let mut extend = false; + let mut name = None; + let mut directives = None; + let mut fields = None; + + for pair in pair.into_inner() { + match pair.as_rule() { + Rule::string => { + description = Some(Positioned::new(pair.as_str().to_string(), pc.step(&pair))) + } + Rule::extend => extend = true, + Rule::name => name = Some(Positioned::new(pair.as_str().to_string(), pc.step(&pair))), + Rule::directives => directives = Some(parse_directives(pair, pc)?), + Rule::input_fields_definition => fields = Some(parse_arguments_definition(pair, pc)?), + _ => unreachable!(), + } + } + + Ok(Positioned::new( + InputObjectType { + extend, + description, + name: name.unwrap(), + directives: directives.unwrap_or_default(), + fields: fields.unwrap_or_default(), + }, + pos, + )) +} + +fn parse_directive_locations( + pair: Pair, + pc: &mut PositionCalculator, +) -> Result>> { + let mut locations = Vec::new(); + for pair in pair.into_inner() { + match pair.as_rule() { + Rule::directive_location => { + let pos = pc.step(&pair); + let loc = 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!(), + }; + locations.push(Positioned::new(loc, pos)); + } + _ => unreachable!(), + } + } + Ok(locations) +} + +fn parse_directive_definition( + pair: Pair, + pc: &mut PositionCalculator, +) -> Result> { + let pos = pc.step(&pair); + let mut description = None; + let mut name = None; + let mut arguments = None; + let mut locations = None; + + for pair in pair.into_inner() { + match pair.as_rule() { + Rule::string => { + description = Some(Positioned::new(pair.as_str().to_string(), pc.step(&pair))) + } + Rule::name => name = Some(Positioned::new(pair.as_str().to_string(), pc.step(&pair))), + Rule::arguments_definition => arguments = Some(parse_arguments_definition(pair, pc)?), + Rule::directive_locations => locations = Some(parse_directive_locations(pair, pc)?), + _ => unreachable!(), + } + } + + Ok(Positioned::new( + DirectiveDefinition { + description, + name: name.unwrap(), + arguments: arguments.unwrap(), + locations: locations.unwrap(), + }, + pos, + )) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + #[test] + fn test_parser() { + for entry in fs::read_dir("tests/schemas").unwrap() { + if let Ok(entry) = entry { + SchemaParser::parse(Rule::document, &fs::read_to_string(entry.path()).unwrap()) + .unwrap(); + } + } + } + + #[test] + fn test_parser_ast() { + for entry in fs::read_dir("tests/schemas").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/utils.rs index 7a34e457..a57e0f36 100644 --- a/async-graphql-parser/src/utils.rs +++ b/async-graphql-parser/src/utils.rs @@ -60,9 +60,14 @@ pub fn to_static_str(s: &str) -> &'static str { unsafe { (s as *const str).as_ref().unwrap() } } -pub fn unquote_string(s: &'static str, pos: Pos) -> Result> { - debug_assert!(s.starts_with('"') && s.ends_with('"')); - let s = &s[1..s.len() - 1]; +pub fn unquote_string(s: &str, pos: Pos) -> Result> { + let s = if s.starts_with(r#"""""#) { + &s[3..s.len() - 3] + } else if s.starts_with('"') { + &s[1..s.len() - 1] + } else { + unreachable!() + }; if !s.contains('\\') { return Ok(Cow::Borrowed(to_static_str(s))); diff --git a/async-graphql-parser/tests/queries/multiline_string.graphql b/async-graphql-parser/tests/queries/multiline_string.graphql new file mode 100644 index 00000000..85b3fc2f --- /dev/null +++ b/async-graphql-parser/tests/queries/multiline_string.graphql @@ -0,0 +1,7 @@ +{ + a(s: """ + a + b + c + """) +} \ No newline at end of file diff --git a/feature-comparison.md b/feature-comparison.md index 3879c45d..7d1719dc 100644 --- a/feature-comparison.md +++ b/feature-comparison.md @@ -8,10 +8,10 @@ Comparing Features of Other Rust GraphQL Implementations |----------------|---------------|-----------------| | async/await | 👍 | ⛔️ | | Rustfmt friendly(No DSL) | 👍 | ⛔️ | -| Boilerplate | less | some | +| Boilerplate | Less | Some | +| Type Safety | 👍 | 👍 | | Query | 👍 | 👍 | | Mutation | 👍 | 👍 | -| Type Safety | 👍 | 👍 | | Interfaces | 👍 | 👍 | | Union | 👍 | 👍 | | Dataloading | 👍 | 👍 |