Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
fc51c3c
WIP: Add initial AST nodes for schema language
tailhook Feb 10, 2018
9014a76
WIP: factored out common types for schema
tailhook Feb 11, 2018
a96063e
Implement as_str()/from_str() for DirectiveLocation
tailhook Feb 11, 2018
354746b
WIP: Schema definition is parsed
tailhook Feb 11, 2018
96518eb
WIP: first roundtrip test for schema, rename query tests
tailhook Feb 11, 2018
99975b2
Implement scalar types support
tailhook Feb 11, 2018
2e7dace
Add non-working yet kitchen sink example, implement zero-field object…
tailhook Feb 11, 2018
b70b29f
WIP: Refactors grammar to produce nicer errors
tailhook Feb 11, 2018
7a9e636
WIP: implement `implements_interfaces` for types
tailhook Feb 11, 2018
d2cb1f8
WIP: add support for non-empty object types
tailhook Feb 11, 2018
ba18ce7
WIP: object type extensions support and printing field arguments
tailhook Feb 11, 2018
46198b7
WIP: Add grammar for interface
tailhook Feb 11, 2018
7f73c90
WIP: add support for extending interfaces
tailhook Feb 11, 2018
77ac386
WIP: implements unions support
tailhook Feb 11, 2018
6423a8c
WIP: undefined (empty) unions support
tailhook Feb 11, 2018
35303a1
WIP: Implement union type extension
tailhook Feb 11, 2018
c856a2a
WIP: Implement union type extension
tailhook Feb 11, 2018
fb6a2af
WIP: add enum support
tailhook Feb 11, 2018
137d851
Implement enum extensions
tailhook Feb 11, 2018
5b02ab7
WIP: implement input object type
tailhook Feb 11, 2018
1f64296
Allow floats with unsigned exponent
tailhook Feb 11, 2018
d981317
WIP: Implement input extension
tailhook Feb 11, 2018
1831c80
WIP: implement directives support
tailhook Feb 11, 2018
0122cc5
WIP: Smalle fixes to make kitchen-sink example work
tailhook Feb 11, 2018
0a330ce
Improve docs and eliminate warnings
tailhook Feb 11, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@ GraphQL Parser
[Github](https://github.com/tailhook/graphql-parser) |
[Crate](https://crates.io/crates/graphql-parser)

A parser, formatter and AST for graphql query language for rust.
A parser, formatter and AST for graphql query and schema definition language
for rust.

Current this library supports full graphql syntax, and the following
extensions:
Supported extensions:

1. Subscriptions
2. Block (triple quoted) strings

Schema definition language (also often called IDL) is on the to do list.

License
=======
Expand Down
256 changes: 256 additions & 0 deletions src/common.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
use std::collections::BTreeMap;

use combine::{parser, ParseResult, Parser};
use combine::easy::Error;
use combine::error::StreamError;
use combine::combinator::{many, many1, optional, position, choice};

use tokenizer::{Kind as T, Token, TokenStream};
use helpers::{punct, ident, kind, name};
use position::Pos;


/// An alias for string, used where graphql expects a name
pub type Name = String;

#[derive(Debug, Clone, PartialEq)]
pub struct Directive {
pub position: Pos,
pub name: Name,
pub arguments: Vec<(Name, Value)>,
}

/// This represents integer number
///
/// But since there is no definition on limit of number in spec
/// (only in implemetation), we do a trick similar to the one
/// in `serde_json`: encapsulate value in new-type, allowing type
/// to be extended later.
#[derive(Debug, Clone, PartialEq)]
// we use i64 as a reference implementation: graphql-js thinks even 32bit
// integers is enough. We might consider lift this limit later though
pub struct Number(pub(crate) i64);

#[derive(Debug, Clone, PartialEq)]
pub enum Value {
Variable(Name),
Int(Number),
Float(f64),
String(String),
Boolean(bool),
Null,
Enum(Name),
List(Vec<Value>),
Object(BTreeMap<Name, Value>),
}

#[derive(Debug, Clone, PartialEq)]
pub enum Type {
NamedType(Name),
ListType(Box<Type>),
NonNullType(Box<Type>),
}

impl Number {
/// Returns a number as i64 if it fits the type
pub fn as_i64(&self) -> Option<i64> {
Some(self.0)
}
}


pub fn directives<'a>(input: &mut TokenStream<'a>)
-> ParseResult<Vec<Directive>, TokenStream<'a>>
{
many(position()
.skip(punct("@"))
.and(name())
.and(parser(arguments))
.map(|((position, name), arguments)| {
Directive { position, name, arguments }
}))
.parse_stream(input)
}

pub fn arguments<'a>(input: &mut TokenStream<'a>)
-> ParseResult<Vec<(String, Value)>, TokenStream<'a>>
{
optional(
punct("(")
.with(many1(name()
.skip(punct(":"))
.and(parser(value))))
.skip(punct(")")))
.map(|opt| {
opt.unwrap_or_else(Vec::new)
})
.parse_stream(input)
}

pub fn int_value<'a>(input: &mut TokenStream<'a>)
-> ParseResult<Value, TokenStream<'a>>
{
kind(T::IntValue).and_then(|tok| tok.value.parse())
.map(Number).map(Value::Int)
.parse_stream(input)
}

pub fn float_value<'a>(input: &mut TokenStream<'a>)
-> ParseResult<Value, TokenStream<'a>>
{
kind(T::FloatValue).and_then(|tok| tok.value.parse())
.map(Value::Float)
.parse_stream(input)
}

fn unquote_block_string(src: &str) -> Result<String, Error<Token, Token>> {
debug_assert!(src.starts_with("\"\"\"") && src.ends_with("\"\"\""));
let indent = src[3..src.len()-3].lines().skip(1)
.filter_map(|line| {
let trimmed = line.trim_left().len();
if trimmed > 0 {
Some(line.len() - trimmed)
} else {
None // skip whitespace-only lines
}
})
.min().unwrap_or(0);
let mut result = String::with_capacity(src.len()-6);
let mut lines = src[3..src.len()-3].lines();
if let Some(first) = lines.next() {
let stripped = first.trim();
if !stripped.is_empty() {
result.push_str(stripped);
result.push('\n');
}
}
let mut last_line = 0;
for line in lines {
last_line = result.len();
if line.len() > indent {
result.push_str(&line[indent..].replace(r#"\""""#, r#"""""#));
}
result.push('\n');
}
if result[last_line..].trim().is_empty() {
result.truncate(last_line);
}

Ok(result)
}

fn unquote_string(s: &str) -> Result<String, Error<Token, Token>> {
let mut res = String::with_capacity(s.len());
debug_assert!(s.starts_with('"') && s.ends_with('"'));
let mut chars = s[1..s.len()-1].chars();
while let Some(c) = chars.next() {
match c {
'\\' => {
match chars.next().expect("slash cant be and 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' => {
unimplemented!();
}
c => {
return Err(Error::unexpected_message(
format_args!("bad escaped char {:?}", c)));
}
}
}
c => res.push(c),
}
}

Ok(res)
}

pub fn string<'a>(input: &mut TokenStream<'a>)
-> ParseResult<String, TokenStream<'a>>
{
choice((
kind(T::StringValue).and_then(|tok| unquote_string(tok.value)),
kind(T::BlockString).and_then(|tok| unquote_block_string(tok.value)),
)).parse_stream(input)
}

pub fn string_value<'a>(input: &mut TokenStream<'a>)
-> ParseResult<Value, TokenStream<'a>>
{
kind(T::StringValue).and_then(|tok| unquote_string(tok.value))
.map(Value::String)
.parse_stream(input)
}

pub fn block_string_value<'a>(input: &mut TokenStream<'a>)
-> ParseResult<Value, TokenStream<'a>>
{
kind(T::BlockString).and_then(|tok| unquote_block_string(tok.value))
.map(Value::String)
.parse_stream(input)
}

pub fn plain_value<'a>(input: &mut TokenStream<'a>)
-> ParseResult<Value, TokenStream<'a>>
{
ident("true").map(|_| Value::Boolean(true))
.or(ident("false").map(|_| Value::Boolean(false)))
.or(ident("null").map(|_| Value::Null))
.or(name().map(Value::Enum))
.or(parser(int_value))
.or(parser(float_value))
.or(parser(string_value))
.or(parser(block_string_value))
.parse_stream(input)
}

pub fn value<'a>(input: &mut TokenStream<'a>)
-> ParseResult<Value, TokenStream<'a>>
{
parser(plain_value)
.or(punct("$").with(name()).map(Value::Variable))
.or(punct("[").with(many(parser(value))).skip(punct("]"))
.map(Value::List))
.or(punct("{")
.with(many(name().skip(punct(":")).and(parser(value))))
.skip(punct("}"))
.map(Value::Object))
.parse_stream(input)
}

pub fn default_value<'a>(input: &mut TokenStream<'a>)
-> ParseResult<Value, TokenStream<'a>>
{
parser(plain_value)
.or(punct("[").with(many(parser(default_value))).skip(punct("]"))
.map(Value::List))
.or(punct("{")
.with(many(name().skip(punct(":")).and(parser(default_value))))
.skip(punct("}"))
.map(Value::Object))
.parse_stream(input)
}

pub fn parse_type<'a>(input: &mut TokenStream<'a>)
-> ParseResult<Type, TokenStream<'a>>
{
name().map(Type::NamedType)
.or(punct("[")
.with(parser(parse_type))
.skip(punct("]"))
.map(Box::new)
.map(Type::ListType))
.and(optional(punct("!")).map(|v| v.is_some()))
.map(|(typ, strict)|
if strict {
Type::NonNullType(Box::new(typ))
} else {
typ
}
)
.parse_stream(input)
}
9 changes: 9 additions & 0 deletions src/format.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
//! Formatting graphql
use std::default::Default;

use common::Directive;


#[derive(Debug, PartialEq)]
pub(crate) struct Formatter<'a> {
Expand Down Expand Up @@ -127,3 +129,10 @@ impl<'a> Formatter<'a> {
}
}
}

pub(crate) fn format_directives(dirs: &[Directive], f: &mut Formatter) {
for dir in dirs {
f.write(" ");
dir.display(f);
}
}
57 changes: 56 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
//!
//! 1. Subscriptions
//! 2. Block (triple quoted) strings
//! 3. Schema definition language a/k/a IDL (which is still in RFC)
//!
//!
//! Example: Parse and Format Query
Expand All @@ -39,18 +40,72 @@
//! # }
//! ```
//!
//! Example: Parse and Format Schema
//! --------------------------------
//!
//! ```rust
//! # extern crate failure;
//! # extern crate graphql_parser;
//! use graphql_parser::parse_schema;
//!
//! # fn parse() -> Result<(), failure::Error> {
//! let ast = parse_schema(r#"
//! schema {
//! query: Query
//! }
//! type Query {
//! users: [User!]!,
//! }
//! """
//! Example user object
//!
//! This is just a demo comment.
//! """
//! type User {
//! name: String!,
//! }
//! "#)?;
//! // Format canonical representation
//! assert_eq!(format!("{}", ast), "\
//! schema {
//! query: Query
//! }
//!
//! type Query {
//! users: [User!]!
//! }
//!
//! \"\"\"
//! Example user object
//!
//! This is just a demo comment.
//! \"\"\"
//! type User {
//! name: String!
//! }
//! ");
//! # Ok(())
//! # }
//! # fn main() {
//! # parse().unwrap()
//! # }
//! ```
//!
#![warn(missing_debug_implementations)]

extern crate combine;
#[macro_use] extern crate failure;
#[cfg(test)] #[macro_use] extern crate pretty_assertions;


mod common;
mod format;
mod position;
mod tokenizer;
mod helpers;
mod query;
pub mod query;
pub mod schema;

pub use query::{parse_query, QueryParseError};
pub use schema::{parse_schema, SchemaParseError};
pub use position::Pos;
Loading