Initial import

This commit is contained in:
Magnus Hallin 2016-09-11 18:41:29 +02:00
commit 94d689fa82
76 changed files with 16729 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
target
Cargo.lock

15
.travis.yml Normal file
View file

@ -0,0 +1,15 @@
language: rust
rust:
- stable
- beta
- nightly
matrix:
allow_failures:
- rust: nightly
script:
cargo build --verbose
cargo build --features iron-handlers --verbose
cargo test --verbose --features iron-handlers

20
Cargo.toml Normal file
View file

@ -0,0 +1,20 @@
[package]
name = "juniper"
version = "0.1.0"
authors = ["Magnus Hallin <mhallin@fastmail.com>"]
[features]
default = []
nightly = []
iron-handlers = ["iron"]
[dependencies]
rustc-serialize = "0.3.19"
iron = { version = "^0.4.0", optional = true }
[dev-dependencies]
iron = "^0.4.0"
router = "^0.2.0"
mount = "^0.2.1"
logger = "^0.1.0"
iron-test = "^0.4.0"

25
LICENSE Normal file
View file

@ -0,0 +1,25 @@
BSD 2-Clause License
Copyright (c) 2016, Magnus Hallin
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

143
README.md Normal file
View file

@ -0,0 +1,143 @@
# Juniper
> GraphQL server library for Rust
---
[GraphQL][graphql] is a data query language developed by Facebook intended to
serve mobile and web application frontends. Juniper makes it possible to write
GraphQL servers in Rust that are type-safe and blazingly fast.
Juniper does not include a web server - instead it provides building blocks to
make integration with existing servers straightforward. It optionally provides a
pre-built integration for the [Iron framework][iron].
## Installation
Add Juniper to your Cargo.toml:
```toml
[dependencies]
juniper = "0.5.0"
```
If you want the Iron integration enabled, you need to enable the `iron-handlers`
feature flag:
```toml
[dependencies]
juniper = { version = "0.5.0", features = ["iron-handlers"] }
```
## Building schemas
GraphQL turns the REST paradigm as it's usually implemented on its head: instead
of providing a fixed structure of all types and relations in the system, GraphQL
defines a _schema_ which your users can query. The schema defines all types,
fields, and relations available, while the query defines which fields and
relations a user is interested in.
Juniper expects you to already have the types you want to expose in GraphQL as
Rust data types. Other than that, it doesn't make any assumptions whether they
are stored in a database or just in memory. Exposing a type is a matter of
implementing the `GraphQLType` for your type. To make things a bit easier,
Juniper comes with a set of macros that help you do this, based on what kind of
type you want to expose. Let's look at how one could expose parts of the [Star
Wars Schema][swschema]:
```rust
#[macro_use] extern crate juniper;
use juniper::FieldResult;
enum Episode {
NewHope,
Empire,
Jedi,
}
struct Human {
id: String,
name: String,
appears_in: Vec<Episode>,
home_planet: String,
}
graphql_enum!(Episode {
Episode::NewHope => "NEW_HOPE",
Episode::Empire => "EMPIRE",
Episode::Jedi => "JEDI",
});
graphql_object!(Human: () as "Human" |&self| {
description: "A humanoid creature in the Star Wars universe"
// Field resolver methods look almost like ordinary methods. The macro picks
// up arguments and return types for the introspection schema, and verifies
// it during compilation.
field id() -> FieldResult<&String> {
Ok(&self.id)
}
field name() -> FieldResult<&String> {
Ok(&self.name)
}
field appears_in() -> FieldResult<&Vec<Episode>> {
Ok(&self.appears_in)
}
field home_planet() -> FieldResult<&String> {
Ok(&self.home_planet)
}
});
```
You can find the full example in [src/tests/schema.rs][test_schema_rs],
including polymorphism with traits and interfaces. For an example of the Iron
integration, see the [examples folder][examples].
## Features
Juniper supports the full GraphQL query language according to the
[specification][graphql_spec], including the introspective schema and all
validations. It does not, however, support the schema language.
As an exception to other GraphQL libraries for other languages, Juniper builds
non-null types by default. A field of type `Vec<Episode>` will be converted into
`[Episode!]!`. The corresponding Rust type for e.g. `[Episode]` would be
`Option<Vec<Option<Episode>>>`.
## API Stability
Juniper has not reached 1.0 yet, thus some API instability should be expected.
## 1.0 Roadmap
The road to 1.0 _focuses_ on two aspects: making sure the API hasn't got any
obvious dead-ends with respect to probable future features, and improving test
coverage for general execution. There are some chores that need to be completed
as well.
* [ ] Extensive execution testing
* [ ] Sending input objects and partial input objects in variables
* [ ] Sending enums in variables
* [ ] General input value type checking and validation
* [ ] Improve helper macros
* [ ] `graphql_union!` helper completely missing
* [ ] Add support for deprecating things
* [ ] Custom enum values and descriptions
* [ ] Improved syntax for fields that can't fail resolution - make
`FieldResult<T>` optional maybe?
* [ ] Investigate asynchronous execution - implementing it is not necessary, but
at least look at what API changes will be needed for us to hook into
[Tokio][tokio], for example.
* [ ] Larger examples to illustrate things like database access
[graphql]: http://graphql.org
[iron]: http://ironframework.io
[swschema]: http://graphql.org/docs/typesystem/
[graphql_spec]: http://facebook.github.io/graphql
[test_schema_rs]: src/tests/schema.rs
[tokio]: https://github.com/tokio-rs/tokio
[examples]: examples/

56
examples/server.rs Normal file
View file

@ -0,0 +1,56 @@
extern crate iron;
extern crate mount;
extern crate logger;
extern crate rustc_serialize;
#[macro_use] extern crate juniper;
use mount::Mount;
use logger::Logger;
use iron::prelude::*;
use juniper::FieldResult;
use juniper::iron_handlers::{GraphQLHandler, GraphiQLHandler};
fn context_factory(_: &mut Request) -> () {
()
}
fn main() {
let mut mount = Mount::new();
let graphql_endpoint = GraphQLHandler::new(context_factory, Query { }, Mutation { });
let graphiql_endpoint = GraphiQLHandler::new("/graphql");
mount.mount("/graphiql", graphiql_endpoint);
mount.mount("/graphql", graphql_endpoint);
let (logger_before, logger_after) = Logger::new(None);
let mut chain = Chain::new(mount);
chain.link_before(logger_before);
chain.link_after(logger_after);
let host = "localhost:8080";
println!("GraphQL server started on {}", host);
Iron::new(chain).http(host).unwrap();
}
struct Query {}
struct Mutation {}
graphql_object!(Query: () as "Query" |&self| {
field dummy() -> FieldResult<&str> {
Ok("Dummy field")
}
field error() -> FieldResult<&str> {
Err("Can't do it".to_owned())
}
});
graphql_object!(<CtxT> Mutation: CtxT as "Mutation" |&self| {
field print(value: String) -> FieldResult<String> {
println!("Printing text according to mutation");
println!("{}", value);
Ok(value)
}
});

445
src/ast.rs Normal file
View file

@ -0,0 +1,445 @@
use std::fmt;
use std::collections::HashMap;
use std::hash::Hash;
use std::vec;
use std::slice;
use rustc_serialize::json::{ToJson, Json};
use parser::Spanning;
/// A type literal in the syntax tree
///
/// This enum carries no semantic information and might refer to types that do
/// not exist.
#[derive(Clone, Eq, PartialEq, Debug)]
pub enum Type {
/// A nullable named type, e.g. `String`
Named(String),
/// A nullable list type, e.g. `[String]`
///
/// The list itself is what's nullable, the containing type might be non-null.
List(Box<Type>),
/// A non-null named type, e.g. `String!`
NonNullNamed(String),
/// A non-null list type, e.g. `[String]!`.
///
/// The list itself is what's non-null, the containing type might be null.
NonNullList(Box<Type>),
}
/// A JSON-like value that can be passed into the query execution, either
/// out-of-band, or in-band as default variable values. These are _not_ constant
/// and might contain variables.
///
/// Lists and objects variants are _spanned_, i.e. they contain a reference to
/// their position in the source file, if available.
#[derive(Clone, PartialEq, Debug)]
#[allow(missing_docs)]
pub enum InputValue {
Null,
Int(i64),
Float(f64),
String(String),
Boolean(bool),
Enum(String),
Variable(String),
List(Vec<Spanning<InputValue>>),
Object(Vec<(Spanning<String>, Spanning<InputValue>)>),
}
#[derive(Clone, PartialEq, Debug)]
pub struct VariableDefinition {
pub var_type: Spanning<Type>,
pub default_value: Option<Spanning<InputValue>>,
}
#[derive(Clone, PartialEq, Debug)]
pub struct Arguments {
pub items: Vec<(Spanning<String>, Spanning<InputValue>)>,
}
#[derive(Clone, PartialEq, Debug)]
pub struct VariableDefinitions {
pub items: Vec<(Spanning<String>, VariableDefinition)>,
}
#[derive(Clone, PartialEq, Debug)]
pub struct Field {
pub alias: Option<Spanning<String>>,
pub name: Spanning<String>,
pub arguments: Option<Spanning<Arguments>>,
pub directives: Option<Vec<Spanning<Directive>>>,
pub selection_set: Option<Vec<Selection>>,
}
#[derive(Clone, PartialEq, Debug)]
pub struct FragmentSpread {
pub name: Spanning<String>,
pub directives: Option<Vec<Spanning<Directive>>>,
}
#[derive(Clone, PartialEq, Debug)]
pub struct InlineFragment {
pub type_condition: Option<Spanning<String>>,
pub directives: Option<Vec<Spanning<Directive>>>,
pub selection_set: Vec<Selection>,
}
/// Entry in a GraphQL selection set
///
/// This enum represents one of the three variants of a selection that exists
/// in GraphQL: a field, a fragment spread, or an inline fragment. Each of the
/// variants references their location in the query source.
///
/// ```text
/// {
/// field(withArg: 123) { subField }
/// ...fragmentSpread
/// ...on User {
/// inlineFragmentField
/// }
/// }
/// ```
#[derive(Clone, PartialEq, Debug)]
#[allow(missing_docs)]
pub enum Selection {
Field(Spanning<Field>),
FragmentSpread(Spanning<FragmentSpread>),
InlineFragment(Spanning<InlineFragment>),
}
#[derive(Clone, PartialEq, Debug)]
pub struct Directive {
pub name: Spanning<String>,
pub arguments: Option<Spanning<Arguments>>,
}
#[derive(Clone, PartialEq, Debug)]
pub enum OperationType {
Query,
Mutation,
}
#[derive(Clone, PartialEq, Debug)]
pub struct Operation {
pub operation_type: OperationType,
pub name: Option<Spanning<String>>,
pub variable_definitions: Option<Spanning<VariableDefinitions>>,
pub directives: Option<Vec<Spanning<Directive>>>,
pub selection_set: Vec<Selection>,
}
#[derive(Clone, PartialEq, Debug)]
pub struct Fragment {
pub name: Spanning<String>,
pub type_condition: Spanning<String>,
pub directives: Option<Vec<Spanning<Directive>>>,
pub selection_set: Vec<Selection>,
}
#[derive(Clone, PartialEq, Debug)]
pub enum Definition {
Operation(Spanning<Operation>),
Fragment(Spanning<Fragment>),
}
pub type Document = Vec<Definition>;
/// Parse an unstructured input value into a Rust data type.
///
/// The conversion _can_ fail, and must in that case return None. Implemented
/// automatically by the convenience macros `graphql_enum!` and
/// `graphql_scalar!`. Must be implemented manually when manually exposing new
/// enums or scalars.
pub trait FromInputValue: Sized {
/// Performs the conversion.
fn from(v: &InputValue) -> Option<Self>;
}
/// Losslessly clones a Rust data type into an InputValue.
pub trait ToInputValue: Sized {
/// Performs the conversion.
fn to(&self) -> InputValue;
}
impl Type {
/// Get the name of a named type.
///
/// Only applies to named types; lists will return `None`.
pub fn name(&self) -> Option<&str> {
match *self {
Type::Named(ref n) | Type::NonNullNamed(ref n) => Some(n),
_ => None
}
}
/// Get the innermost name by unpacking lists
///
/// All type literals contain exactly one named type.
pub fn innermost_name(&self) -> &str {
match *self {
Type::Named(ref n) | Type::NonNullNamed(ref n) => n,
Type::List(ref l) | Type::NonNullList(ref l) => l.innermost_name(),
}
}
/// Determines if a type only can represent non-null values.
pub fn is_non_null(&self) -> bool {
match *self {
Type::NonNullNamed(_) | Type::NonNullList(_) => true,
_ => false,
}
}
}
impl fmt::Display for Type {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Type::Named(ref n) => write!(f, "{}", n),
Type::NonNullNamed(ref n) => write!(f, "{}!", n),
Type::List(ref t) => write!(f, "[{}]", t),
Type::NonNullList(ref t) => write!(f, "[{}]!", t),
}
}
}
impl InputValue {
/// Construct a null value.
pub fn null() -> InputValue { InputValue::Null }
/// Construct an integer value.
pub fn int(i: i64) -> InputValue { InputValue::Int(i) }
/// Construct a floating point value.
pub fn float(f: f64) -> InputValue { InputValue::Float(f) }
/// Construct a boolean value.
pub fn boolean(b: bool) -> InputValue { InputValue::Boolean(b) }
/// Construct a string value.
pub fn string<T: AsRef<str>>(s: T) -> InputValue {
InputValue::String(s.as_ref().to_owned())
}
/// Construct an enum value.
pub fn enum_value<T: AsRef<str>>(s: T) -> InputValue {
InputValue::Enum(s.as_ref().to_owned())
}
/// Construct a variable value.
pub fn variable<T: AsRef<str>>(v: T) -> InputValue {
InputValue::Variable(v.as_ref().to_owned())
}
/// Construct an unlocated list.
///
/// Convenience function to make each `InputValue` in the input vector
/// not contain any location information. Can be used from `ToInputValue`
/// implementations, where no source code position information is available.
pub fn list(l: Vec<InputValue>) -> InputValue {
InputValue::List(l.into_iter().map(|i| Spanning::unlocated(i)).collect())
}
/// Construct a located list.
pub fn parsed_list(l: Vec<Spanning<InputValue>>) -> InputValue {
InputValue::List(l)
}
/// Construct an unlocated object.
///
/// Similar to `InputValue::list`, it makes each key and value in the given
/// hash map not contain any location information.
pub fn object<K>(o: HashMap<K, InputValue>) -> InputValue
where K: AsRef<str> + Eq + Hash
{
InputValue::Object(
o.into_iter()
.map(|(k, v)|
(Spanning::unlocated(k.as_ref().to_owned()), Spanning::unlocated(v)))
.collect()
)
}
/// Construct a located object.
pub fn parsed_object(o: Vec<(Spanning<String>, Spanning<InputValue>)>) -> InputValue {
InputValue::Object(o)
}
/// Convert a `Json` structure into an `InputValue`.
///
/// This consumes the JSON instance.
///
/// Notes:
/// * No enums or variables will be produced by this method.
/// * All lists and objects will be unlocated
pub fn from_json(json: Json) -> InputValue {
match json {
Json::I64(i) => InputValue::int(i),
Json::U64(u) => InputValue::float(u as f64),
Json::F64(f) => InputValue::float(f),
Json::String(s) => InputValue::string(s),
Json::Boolean(b) => InputValue::boolean(b),
Json::Array(a) => InputValue::list(a.into_iter().map(InputValue::from_json).collect()),
Json::Object(o) => InputValue::object(o.into_iter().map(|(k,v)| (k, InputValue::from_json(v))).collect()),
Json::Null => InputValue::null(),
}
}
/// Resolve all variables to their values.
pub fn into_const(self, vars: &HashMap<String, InputValue>) -> InputValue {
match self {
InputValue::Variable(v) => vars[&v].clone(),
InputValue::List(l) => InputValue::List(
l.into_iter().map(|s| s.map(|v| v.into_const(vars))).collect()
),
InputValue::Object(o) => InputValue::Object(
o.into_iter().map(|(sk, sv)| (sk, sv.map(|v| v.into_const(vars)))).collect()
),
v => v,
}
}
/// Shorthand form of invoking `FromInputValue::from()`.
pub fn convert<T>(&self) -> Option<T> where T: FromInputValue {
<T as FromInputValue>::from(self)
}
/// Does the value represent null?
pub fn is_null(&self) -> bool {
match *self {
InputValue::Null => true,
_ => false,
}
}
/// Does the value represent a variable?
pub fn is_variable(&self) -> bool {
match *self {
InputValue::Variable(_) => true,
_ => false,
}
}
/// View the underlying enum value, if present.
pub fn as_enum_value(&self) -> Option<&str> {
match *self {
InputValue::Enum(ref e) => Some(e),
_ => None,
}
}
/// View the underlying string value, if present.
pub fn as_string_value(&self) -> Option<&str> {
match *self {
InputValue::String(ref s) => Some(s),
_ => None,
}
}
/// Convert the input value to an unlocated object value.
///
/// This constructs a new hashmap that contain references to the keys
/// and values in `self`.
pub fn to_object_value(&self) -> Option<HashMap<&str, &InputValue>> {
match *self {
InputValue::Object(ref o) => Some(
o.iter().map(|&(ref sk, ref sv)| (sk.item.as_str(), &sv.item)).collect()),
_ => None,
}
}
/// Convert the input value to an unlocated list value.
///
/// This constructs a new vector that contain references to the values
/// in `self`.
pub fn to_list_value(&self) -> Option<Vec<&InputValue>> {
match *self {
InputValue::List(ref l) => Some(l.iter().map(|s| &s.item).collect()),
_ => None,
}
}
/// Recursively find all variables
pub fn referenced_variables(&self) -> Vec<&str> {
match *self {
InputValue::Variable(ref name) => vec![name],
InputValue::List(ref l) => l.iter().flat_map(|v| v.item.referenced_variables()).collect(),
InputValue::Object(ref obj) => obj.iter().flat_map(|&(_, ref v)| v.item.referenced_variables()).collect(),
_ => vec![],
}
}
/// Compare equality with another `InputValue` ignoring any source position information.
pub fn unlocated_eq(&self, other: &InputValue) -> bool {
use InputValue::*;
match (self, other) {
(&Null, &Null) => true,
(&Int(i1), &Int(i2)) => i1 == i2,
(&Float(f1), &Float(f2)) => f1 == f2,
(&String(ref s1), &String(ref s2)) |
(&Enum(ref s1), &Enum(ref s2)) |
(&Variable(ref s1), &Variable(ref s2)) => s1 == s2,
(&Boolean(b1), &Boolean(b2)) => b1 == b2,
(&List(ref l1), &List(ref l2)) =>
l1.iter().zip(l2.iter()).all(|(ref v1, ref v2)| v1.item.unlocated_eq(&v2.item)),
(&Object(ref o1), &Object(ref o2)) =>
o1.len() == o2.len()
&& o1.iter()
.all(|&(ref sk1, ref sv1)| o2.iter().any(
|&(ref sk2, ref sv2)| sk1.item == sk2.item && sv1.item.unlocated_eq(&sv2.item))),
_ => false
}
}
}
impl ToJson for InputValue {
fn to_json(&self) -> Json {
match *self {
InputValue::Null | InputValue::Variable(_) => Json::Null,
InputValue::Int(i) => Json::I64(i),
InputValue::Float(f) => Json::F64(f),
InputValue::String(ref s) | InputValue::Enum(ref s) => Json::String(s.clone()),
InputValue::Boolean(b) => Json::Boolean(b),
InputValue::List(ref l) => Json::Array(l.iter().map(|x| x.item.to_json()).collect()),
InputValue::Object(ref o) => Json::Object(o.iter().map(|&(ref k, ref v)| (k.item.clone(), v.item.to_json())).collect()),
}
}
}
impl Arguments {
pub fn into_iter(self) -> vec::IntoIter<(Spanning<String>, Spanning<InputValue>)> {
self.items.into_iter()
}
pub fn iter(&self) -> slice::Iter<(Spanning<String>, Spanning<InputValue>)> {
self.items.iter()
}
pub fn iter_mut(&mut self) -> slice::IterMut<(Spanning<String>, Spanning<InputValue>)> {
self.items.iter_mut()
}
pub fn drain<'a>(&'a mut self) -> vec::Drain<'a, (Spanning<String>, Spanning<InputValue>)> {
self.items.drain(..)
}
pub fn len(&self) -> usize {
self.items.len()
}
pub fn get(&self, key: &str) -> Option<&Spanning<InputValue>> {
self.items
.iter()
.filter(|&&(ref k, _)| k.item == key)
.map(|&(_, ref v)| v)
.next()
}
}
impl VariableDefinitions {
pub fn iter(&self) -> slice::Iter<(Spanning<String>, VariableDefinition)> {
self.items.iter()
}
}

View file

@ -0,0 +1,323 @@
//! Optional handlers for the Iron framework. Requires the `iron-handlers` feature enabled.
use iron::prelude::*;
use iron::middleware::Handler;
use iron::mime::Mime;
use iron::status;
use iron::method;
use std::collections::{HashMap, BTreeMap};
use rustc_serialize::json::{ToJson, Json};
use ::{InputValue, GraphQLType, RootNode, execute};
/// Handler that executes GraphQL queries in the given schema
///
/// The handler responds to GET requests and POST requests only. In GET
/// requests, the query should be supplied in the `query` URL parameter, e.g.
/// `http://localhost:3000/graphql?query={hero{name}}`.
///
/// POST requests support both queries and variables. POST a JSON document to
/// this endpoint containing the field `"query"` and optionally `"variables"`.
/// The variables should be a JSON object containing the variable to value
/// mapping.
pub struct GraphQLHandler<CtxFactory, Query, Mutation, CtxT>
where CtxFactory: Fn(&mut Request) -> CtxT + Send + Sync + 'static,
CtxT: Send + Sync + 'static,
Query: GraphQLType<CtxT> + Send + Sync + 'static,
Mutation: GraphQLType<CtxT> + Send + Sync + 'static,
{
context_factory: CtxFactory,
root_node: RootNode<CtxT, Query, Mutation>,
}
/// Handler that renders GraphiQL - a graphical query editor interface
pub struct GraphiQLHandler {
graphql_url: String,
}
impl<CtxFactory, Query, Mutation, CtxT>
GraphQLHandler<CtxFactory, Query, Mutation, CtxT>
where CtxFactory: Fn(&mut Request) -> CtxT + Send + Sync + 'static,
CtxT: Send + Sync + 'static,
Query: GraphQLType<CtxT> + Send + Sync + 'static,
Mutation: GraphQLType<CtxT> + Send + Sync + 'static,
{
/// Build a new GraphQL handler
///
/// The context factory will receive the Iron request object and is
/// expected to construct a context object for the given schema. This can
/// be used to construct e.g. database connections or similar data that
/// the schema needs to execute the query.
pub fn new(context_factory: CtxFactory, query: Query, mutation: Mutation) -> Self {
GraphQLHandler {
context_factory: context_factory,
root_node: RootNode::new(query, mutation),
}
}
fn handle_get(&self, req: &mut Request) -> IronResult<Response> {
let url = req.url.clone().into_generic_url();
let mut query = None;
let variables = HashMap::new();
for (k, v) in url.query_pairs() {
if k == "query" {
query = Some(v.into_owned());
}
}
let query = iexpect!(query);
self.execute(req, &query, &variables)
}
fn handle_post(&self, req: &mut Request) -> IronResult<Response> {
let json_data = itry!(Json::from_reader(&mut req.body));
let json_obj = match json_data {
Json::Object(o) => o,
_ => return Ok(Response::with((status::BadRequest, "No JSON object was decoded"))),
};
let mut query = None;
let mut variables = HashMap::new();
for (k, v) in json_obj.into_iter() {
if k == "query" {
query = v.as_string().map(|s| s.to_owned());
}
else if k == "variables" {
variables = match InputValue::from_json(v).to_object_value() {
Some(o) => o.into_iter().map(|(k, v)| (k.to_owned(), v.clone())).collect(),
_ => HashMap::new(),
};
}
}
let query = iexpect!(query);
self.execute(req, &query, &variables)
}
fn execute(&self, req: &mut Request, query: &str, variables: &HashMap<String, InputValue>) -> IronResult<Response> {
let context = (self.context_factory)(req);
let result = execute(query, None, &self.root_node, variables, &context);
let content_type = "application/json".parse::<Mime>().unwrap();
match result {
Ok((result, errors)) => {
let mut map = BTreeMap::new();
map.insert("data".to_owned(), result.to_json());
if !errors.is_empty() {
map.insert("errors".to_owned(), errors.to_json());
}
let data = Json::Object(map);
let json = data.pretty();
Ok(Response::with((content_type, status::Ok, json.to_string())))
}
Err(err) => {
let data = err.to_json();
let json = data.pretty();
Ok(Response::with((content_type, status::BadRequest, json.to_string())))
}
}
}
}
impl GraphiQLHandler {
/// Build a new GraphiQL handler targeting the specified URL.
///
/// The provided URL should point to the URL of the attached `GraphQLHandler`. It can be
/// relative, so a common value could be `"/graphql"`.
pub fn new(graphql_url: &str) -> GraphiQLHandler {
GraphiQLHandler {
graphql_url: graphql_url.to_owned(),
}
}
}
impl<CtxFactory, Query, Mutation, CtxT>
Handler
for GraphQLHandler<CtxFactory, Query, Mutation, CtxT>
where CtxFactory: Fn(&mut Request) -> CtxT + Send + Sync + 'static,
CtxT: Send + Sync + 'static,
Query: GraphQLType<CtxT> + Send + Sync + 'static,
Mutation: GraphQLType<CtxT> + Send + Sync + 'static,
{
fn handle(&self, req: &mut Request) -> IronResult<Response> {
match req.method {
method::Get => self.handle_get(req),
method::Post => self.handle_post(req),
_ => Ok(Response::with((status::MethodNotAllowed)))
}
}
}
impl Handler for GraphiQLHandler {
fn handle(&self, _: &mut Request) -> IronResult<Response> {
let content_type = "text/html".parse::<Mime>().unwrap();
let stylesheet_source = r#"
<style>
html, body, #app {
height: 100%;
margin: 0;
overflow: hidden;
width: 100%;
}
</style>
"#;
let fetcher_source = r#"
<script>
function graphQLFetcher(params) {
return fetch(GRAPHQL_URL, {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: params.query,
variables: JSON.parse(params.variables || '{}')
}),
}).then(function (response) {
return response.text();
}).then(function (body) {
try {
return JSON.parse(body);
} catch (error) {
return body;
}
});
}
ReactDOM.render(
React.createElement(GraphiQL, {
fetcher: graphQLFetcher,
}),
document.querySelector('#app'));
</script>
"#;
let source = format!(r#"
<!DOCTYPE html>
<html>
<head>
<title>GraphQL</title>
{stylesheet_source}
<link rel="stylesheet" type="text/css" href="//cdnjs.cloudflare.com/ajax/libs/graphiql/0.7.3/graphiql.css">
</head>
<body>
<div id="app"></div>
<script src="//cdnjs.cloudflare.com/ajax/libs/fetch/1.0.0/fetch.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/react/15.2.1/react.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/react/15.2.1/react-dom.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/graphiql/0.7.3/graphiql.js"></script>
<script>var GRAPHQL_URL = '{graphql_url}';</script>
{fetcher_source}
</body>
</html>
"#,
graphql_url = self.graphql_url,
stylesheet_source = stylesheet_source,
fetcher_source = fetcher_source);
Ok(Response::with((content_type, status::Ok, source)))
}
}
#[cfg(test)]
mod tests {
use rustc_serialize::json::Json;
use iron::prelude::*;
use iron::status;
use iron::headers;
use iron_test::{request, response};
use iron::{Handler, Headers};
use ::tests::model::Database;
use super::GraphQLHandler;
fn context_factory(_: &mut Request) -> Database {
Database::new()
}
fn make_handler() -> Box<Handler> {
Box::new(GraphQLHandler::new(
context_factory,
Database::new(),
(),
))
}
fn unwrap_json_response(resp: Response) -> Json {
let result = response::extract_body_to_string(resp);
Json::from_str(&result).expect("Could not parse JSON object")
}
#[test]
fn test_simple_get() {
let response = request::get(
"http://localhost:3000/?query={hero{name}}",
Headers::new(),
&make_handler())
.expect("Unexpected IronError");
assert_eq!(response.status, Some(status::Ok));
assert_eq!(response.headers.get::<headers::ContentType>(),
Some(&headers::ContentType::json()));
let json = unwrap_json_response(response);
assert_eq!(
json,
Json::from_str(r#"{"data": {"hero": {"name": "R2-D2"}}}"#)
.expect("Invalid JSON constant in test"));
}
#[test]
fn test_simple_post() {
let response = request::post(
"http://localhost:3000/",
Headers::new(),
r#"{"query": "{hero{name}}"}"#,
&make_handler())
.expect("Unexpected IronError");
assert_eq!(response.status, Some(status::Ok));
assert_eq!(response.headers.get::<headers::ContentType>(),
Some(&headers::ContentType::json()));
let json = unwrap_json_response(response);
assert_eq!(
json,
Json::from_str(r#"{"data": {"hero": {"name": "R2-D2"}}}"#)
.expect("Invalid JSON constant in test"));
}
#[test]
fn test_unsupported_method() {
let response = request::options(
"http://localhost:3000/?query={hero{name}}",
Headers::new(),
&make_handler())
.expect("Unexpected IronError");
assert_eq!(response.status, Some(status::MethodNotAllowed));
}
}

1
src/integrations/mod.rs Normal file
View file

@ -0,0 +1 @@
#[cfg(feature="iron-handlers")] pub mod iron_handlers;

340
src/lib.rs Normal file
View file

@ -0,0 +1,340 @@
/*!
# GraphQL
[GraphQL][1] is a data query language developed by Facebook intended to serve
mobile and web application frontends. A server provides a schema, containing
types and fields that applications can query. Queries are hierarchical,
composable, and statically typed. Schemas are introspective, which lets clients
statically verify their queries against a server without actually executing
them.
This library provides data types and traits to expose Rust types in a GraphQL
schema, as well as an optional integration into the [Iron framework][2]. It
tries to keep the number of dynamic operations to a minimum, and give you as the
schema developer the control of the query execution path.
## Exposing data types
The `GraphQLType` trait is the primary interface towards application developers.
By deriving this trait, you can expose your types as either objects, enums,
interfaces, unions, or scalars.
However, due to the dynamic nature of GraphQL's type system, deriving this trait
manually is a bit tedious, especially in order to do it in a fully type safe
manner. To help with this task, this library provides a couple of macros; the
most common one being `graphql_object!`. Use this macro to expose your already
existing object types as GraphQL objects:
```rust
#[macro_use] extern crate juniper;
use juniper::FieldResult;
# use std::collections::HashMap;
struct User { id: String, name: String, friend_ids: Vec<String> }
struct QueryRoot;
struct Database { users: HashMap<String, User> }
// GraphQL objects can access a "context object" during execution. Use this
// object to provide e.g. database access to the field accessors.
//
// In this example, we use the Database struct as our context.
graphql_object!(User: Database as "User" |&self| {
// Expose a simple field as a GraphQL string.
// FieldResult<T> is an alias for Result<T, String> - simply return
// a string from this method and it will be correctly inserted into
// the execution response.
field id() -> FieldResult<&String> {
Ok(&self.id)
}
field name() -> FieldResult<&String> {
Ok(&self.name)
}
// Field accessors can optionally take an "executor" as their first
// argument. This object can help guide query execution and provides
// access to the context instance.
//
// In this example, the context is used to convert the friend_ids array
// into actual User objects.
field friends(&mut executor) -> FieldResult<Vec<&User>> {
Ok(self.friend_ids.iter()
.filter_map(|id| executor.context().users.get(id))
.collect())
}
});
// The context object is passed down to all referenced types - all your exposed
// types need to have the same context type.
graphql_object!(QueryRoot: Database as "Query" |&self| {
// Arguments work just like they do on functions.
field user(&mut executor, id: String) -> FieldResult<Option<&User>> {
Ok(executor.context().users.get(&id))
}
});
# fn main() { }
```
Adding per type, field, and argument documentation is possible directly from
this macro. For more in-depth information on how to expose fields and types, see
the [`graphql_object!`][3] macro.
## Integrating with Iron
The most obvious usecase is to expose the GraphQL schema over an HTTP endpoint.
To support this, the library provides an optional and customizable Iron handler.
For example, continuing from the schema created above:
```rust,no_run
extern crate iron;
# #[macro_use] extern crate juniper;
# use std::collections::HashMap;
use iron::prelude::*;
use juniper::iron_handlers::GraphQLHandler;
# use juniper::FieldResult;
#
# struct User { id: String, name: String, friend_ids: Vec<String> }
# struct QueryRoot;
# struct Database { users: HashMap<String, User> }
#
# graphql_object!(User: Database as "User" |&self| {
# field id() -> FieldResult<&String> {
# Ok(&self.id)
# }
#
# field name() -> FieldResult<&String> {
# Ok(&self.name)
# }
#
# field friends(&mut executor) -> FieldResult<Vec<&User>> {
# Ok(self.friend_ids.iter()
# .filter_map(|id| executor.context().users.get(id))
# .collect())
# }
# });
#
# graphql_object!(QueryRoot: Database as "Query" |&self| {
# field user(&mut executor, id: String) -> FieldResult<Option<&User>> {
# Ok(executor.context().users.get(&id))
# }
# });
// This function is executed for every request. Here, we would realistically
// provide a database connection or similar. For this example, we'll be
// creating the database from scratch.
fn context_factory(_: &mut Request) -> Database {
Database {
users: vec![
( "1000".to_owned(), User {
id: "1000".to_owned(), name: "Robin".to_owned(),
friend_ids: vec!["1001".to_owned()] } ),
( "1001".to_owned(), User {
id: "1001".to_owned(), name: "Max".to_owned(),
friend_ids: vec!["1000".to_owned()] } ),
].into_iter().collect()
}
}
fn main() {
// GraphQLHandler takes a context factory function, the root object,
// and the mutation object. If we don't have any mutations to expose, we
// can use the empty tuple () to indicate absence.
let graphql_endpoint = GraphQLHandler::new(context_factory, QueryRoot, ());
// Start serving the schema at the root on port 8080.
Iron::new(graphql_endpoint).http("localhost:8080").unwrap();
}
```
See the [`iron_handlers`][4] module and the [`GraphQLHandler`][5] documentation
for more information on what request methods are supported. There's also a
built-in [GraphiQL][6] handler included.
[1]: http://graphql.org
[2]: http://ironframework.io
[3]: macro.graphql_object!.html
[4]: iron_handlers/index.html
[5]: iron_handlers/struct.GraphQLHandler.html
[6]: https://github.com/graphql/graphiql
*/
#![cfg_attr(feature="nightly", feature(test))]
#![warn(missing_docs)]
extern crate rustc_serialize;
#[cfg(feature="nightly")] extern crate test;
#[cfg(feature="iron-handlers")] #[macro_use(itry, iexpect)] extern crate iron;
#[cfg(test)] extern crate iron_test;
#[macro_use] mod macros;
mod ast;
pub mod parser;
mod value;
mod types;
mod schema;
pub mod validation;
mod integrations;
#[cfg(test)]
mod tests;
use std::collections::HashMap;
use rustc_serialize::json::{ToJson, Json};
use parser::{parse_document_source, ParseError, Spanning, SourcePosition};
use types::execute_validated_query;
use validation::{RuleError, ValidatorContext, visit_all_rules};
pub use ast::{ToInputValue, FromInputValue, InputValue, Type, Selection};
pub use value::Value;
pub use types::base::{Arguments, GraphQLType, TypeKind};
pub use types::schema::{Executor, Registry, ExecutionResult, ExecutionError, FieldResult};
pub use types::scalars::ID;
pub use schema::model::RootNode;
pub use schema::meta;
#[cfg(feature="iron-handlers")] pub use integrations::iron_handlers;
/// An error that prevented query execution
#[derive(Debug, PartialEq)]
#[allow(missing_docs)]
pub enum GraphQLError<'a> {
ParseError(Spanning<ParseError<'a>>),
ValidationError(Vec<RuleError>),
}
/// Execute a query in a provided schema
pub fn execute<'a, CtxT, QueryT, MutationT>(
document_source: &'a str,
operation_name: Option<&str>,
root_node: &RootNode<CtxT, QueryT, MutationT>,
variables: &HashMap<String, InputValue>,
context: &CtxT,
)
-> Result<(Value, Vec<ExecutionError>), GraphQLError<'a>>
where QueryT: GraphQLType<CtxT>,
MutationT: GraphQLType<CtxT>,
{
let document = try!(parse_document_source(document_source));
{
let mut ctx = ValidatorContext::new(&root_node.schema, &document);
visit_all_rules(&mut ctx, &document);
let errors = ctx.into_errors();
if !errors.is_empty() {
return Err(GraphQLError::ValidationError(errors));
}
}
Ok(execute_validated_query(document, operation_name, root_node, variables, context))
}
impl<'a> From<Spanning<ParseError<'a>>> for GraphQLError<'a> {
fn from(f: Spanning<ParseError<'a>>) -> GraphQLError<'a> {
GraphQLError::ParseError(f)
}
}
impl<'a> ToJson for GraphQLError<'a> {
fn to_json(&self) -> Json {
let errs = match *self {
GraphQLError::ParseError(ref err) => parse_error_to_json(err),
GraphQLError::ValidationError(ref errs) => errs.to_json(),
};
Json::Object(vec![
("errors".to_owned(), errs),
].into_iter().collect())
}
}
fn parse_error_to_json(err: &Spanning<ParseError>) -> Json {
Json::Array(vec![
Json::Object(vec![
("message".to_owned(), format!("{}", err.item).to_json()),
("locations".to_owned(), vec![
Json::Object(vec![
("line".to_owned(), (err.start.line() + 1).to_json()),
("column".to_owned(), (err.start.column() + 1).to_json())
].into_iter().collect()),
].to_json()),
].into_iter().collect()),
])
}
impl ToJson for RuleError {
fn to_json(&self) -> Json {
Json::Object(vec![
("message".to_owned(), self.message().to_json()),
("locations".to_owned(), self.locations().to_json()),
].into_iter().collect())
}
}
impl ToJson for SourcePosition {
fn to_json(&self) -> Json {
Json::Object(vec![
("line".to_owned(), (self.line() + 1).to_json()),
("column".to_owned(), (self.column() + 1).to_json()),
].into_iter().collect())
}
}
impl ToJson for ExecutionError {
fn to_json(&self) -> Json {
Json::Object(vec![
("message".to_owned(), self.message().to_json()),
("locations".to_owned(), vec![self.location().clone()].to_json()),
("path".to_owned(), self.path().to_json()),
].into_iter().collect())
}
}
#[doc(hidden)]
pub fn to_snake_case(s: &str) -> String {
let mut dest = String::new();
for (i, part) in s.split('_').enumerate() {
if i > 0 && part.len() == 1 {
dest.push_str(&part.to_uppercase());
}
else if i > 0 && part.len() > 1 {
let first = part.chars().next().unwrap().to_uppercase().collect::<String>();
let second = &part[1..];
dest.push_str(&first);
dest.push_str(second);
}
else if i == 0 {
dest.push_str(part);
}
}
dest
}
#[test]
fn test_to_snake_case() {
assert_eq!(&to_snake_case("test")[..], "test");
assert_eq!(&to_snake_case("_test")[..], "Test");
assert_eq!(&to_snake_case("first_second")[..], "firstSecond");
assert_eq!(&to_snake_case("first_")[..], "first");
assert_eq!(&to_snake_case("a_b_c")[..], "aBC");
assert_eq!(&to_snake_case("a_bc")[..], "aBc");
assert_eq!(&to_snake_case("a_b")[..], "aB");
assert_eq!(&to_snake_case("a")[..], "a");
assert_eq!(&to_snake_case("")[..], "");
}

156
src/macros/args.rs Normal file
View file

@ -0,0 +1,156 @@
#[doc(hidden)]
#[macro_export]
macro_rules! __graphql__args {
// Internal type conversion
( @as_expr, $e:expr) => { $e };
( @as_pattern, $p:pat) => { $p };
(
@assign_arg_vars,
$args:ident, $executorvar:ident, &mut $exec:ident
) => {
let __graphql__args!(@as_pattern, $exec) = &mut $executorvar;
};
(
@assign_arg_vars,
$args:ident, $executorvar:ident, &mut $exec:ident, $($rest:tt)*
) => {
let __graphql__args!(@as_pattern, $exec) = &mut $executorvar;
__graphql__args!(@assign_arg_vars, $args, $executorvar, $($rest)*);
};
(
@assign_arg_vars,
$args:ident, $executorvar:ident,
$name:ident : Option<$ty:ty> as $desc:expr, $($rest:tt)*
) => {
let $name: Option<$ty> = $args
.get(&$crate::to_snake_case(stringify!($name)))
.unwrap_or(None);
__graphql__args!(@assign_arg_vars, $args, $executorvar, $($rest)*);
};
(
@assign_arg_vars,
$args:ident, $executorvar:ident,
$name:ident : Option<$ty:ty> as $desc:expr
) => {
let $name: Option<$ty> = $args
.get(&$crate::to_snake_case(stringify!($name)))
.unwrap_or(None);
};
(
@assign_arg_vars,
$args:ident, $executorvar:ident,
$name:ident $(= $default:tt)* : $ty:ty $(as $desc:tt)*, $($rest:tt)*
) => {
let $name: $ty = $args
.get(&$crate::to_snake_case(stringify!($name)))
.expect("Argument missing - validation must have failed");
__graphql__args!(@assign_arg_vars, $args, $executorvar, $($rest)*);
};
(
@assign_arg_vars,
$args:ident, $executorvar:ident,
$name:ident $(= $default:tt)* : $ty:ty $(as $desc:expr)*
) => {
let $name: $ty = $args
.get(&$crate::to_snake_case(stringify!($name)))
.expect("Argument missing - validation must have failed");
};
( @assign_arg_vars, $args:ident, $executorvar:ident, ) => {
();
};
(
@apply_args,
$reg:expr, $base:expr, ( &mut executor )
) => {
$base
};
(
@apply_args,
$reg:expr, $base:expr, ( &mut executor , $( $rest:tt )* )
) => {
__graphql__args!(
@apply_args,
$reg,
$base,
( $($rest)* ))
};
(
@apply_args,
$reg:expr, $base:expr, ( $name:ident = $default:tt : $t:ty )
) => {
$base.argument($reg.arg_with_default::<$t>(
&$crate::to_snake_case(stringify!($name)),
&__graphql__args!(@as_expr, $default)))
};
(
@apply_args,
$reg:expr, $base:expr, ( $name:ident = $default:tt : $t:ty , $( $rest:tt )* )
) => {
__graphql__args!(
@apply_args,
$reg,
$base.argument($reg.arg_with_default::<$t>(
&$crate::to_snake_case(stringify!($name)),
&__graphql__args!(@as_expr, $default))),
( $($rest)* ))
};
(
@apply_args,
$reg:expr, $base:expr, ( $name:ident : $t:ty )
) => {
$base.argument($reg.arg::<$t>(
&$crate::to_snake_case(stringify!($name))))
};
(
@apply_args,
$reg:expr, $base:expr, ( $name:ident : $t:ty , $( $rest:tt )* )
) => {
__graphql__args!(
@apply_args,
$reg,
$base.argument($reg.arg::<$t>(
&$crate::to_snake_case(stringify!($name)))),
( $($rest)* ))
};
(
@apply_args,
$reg:expr, $base:expr, ( $name:ident : $t:ty as $desc:expr )
) => {
$base.argument(
$reg.arg::<$t>(
&$crate::to_snake_case(stringify!($name)))
.description($desc))
};
(
@apply_args,
$reg:expr, $base:expr, ( $name:ident : $t:ty as $desc:expr , $( $rest:tt )* )
) => {
__graphql__args!(
@apply_args,
$reg,
$base.argument(
$reg.arg::<$t>(
&$crate::to_snake_case(stringify!($name)))
.description($desc)),
( $($rest)* ))
};
( @apply_args, $reg:expr, $base:expr, ( ) ) => {
$base
};
}

99
src/macros/enums.rs Normal file
View file

@ -0,0 +1,99 @@
/**
Expose simple enums
GraphQL enums are similar to enums classes C++ - more like grouped constants
with type safety than what Rust enums offer. This macro can be used to export
non-data carrying Rust enums to GraphQL:
```rust
# #[macro_use] extern crate juniper;
enum Color {
Red,
Green,
Blue
}
graphql_enum!(Color {
Color::Red => "RED",
Color::Green => "GREEN",
Color::Blue => "BLUE",
});
# fn main() { }
```
The macro expands to a `match` statement which will result in a compilation
error if not all enum variants are covered. It also creates an implementation
for `FromInputValue` and `ToInputValue`, making it usable in arguments and
default values.
If you want to expose the enum under a different name than the Rust type,
you can write `graphql_enum!(Color as "MyColor" { ...`.
*/
#[macro_export]
macro_rules! graphql_enum {
( @as_expr, $e:expr) => { $e };
( @as_pattern, $p:pat) => { $p };
// EnumName as "__ExportedNmae" { Enum::Value => "STRING_VALUE", }
// with no trailing comma
( $name:path as $outname:tt { $($eval:path => $ename:tt),* }) => {
impl<CtxT> $crate::GraphQLType<CtxT> for $name {
fn name() -> Option<&'static str> {
Some(graphql_enum!(@as_expr, $outname))
}
fn meta(registry: &mut $crate::Registry<CtxT>) -> $crate::meta::MetaType {
registry.build_enum_type::<$name>()(&[
$( $crate::meta::EnumValue::new(graphql_enum!(@as_expr, $ename)) ),*
])
.into_meta()
}
fn resolve(&self, _: Option<Vec<$crate::Selection>>, _: &mut $crate::Executor<CtxT>) -> $crate::Value {
match self {
$(
&graphql_enum!(@as_pattern, $eval) =>
$crate::Value::string(graphql_enum!(@as_expr, $ename)) ),*
}
}
}
impl $crate::FromInputValue for $name {
fn from(v: &$crate::InputValue) -> Option<$name> {
match v.as_enum_value() {
$(
Some(graphql_enum!(@as_pattern, $ename))
=> Some(graphql_enum!(@as_expr, $eval)), )*
_ => None,
}
}
}
impl $crate::ToInputValue for $name {
fn to(&self) -> $crate::InputValue {
match self {
$(
&graphql_enum!(@as_pattern, $eval) =>
$crate::InputValue::string(graphql_enum!(@as_expr, $ename)) ),*
}
}
}
};
// Same as above, *with* trailing comma
( $name:path as $outname:tt { $($eval:path => $ename:tt, )* }) => {
graphql_enum!($name as $outname { $( $eval => $ename ),* });
};
// Default named enum, without trailing comma
( $name:path { $($eval:path => $ename:tt),* }) => {
graphql_enum!($name as (stringify!($name)) { $( $eval => $ename ),* });
};
// Default named enum, with trailing comma
( $name:path { $($eval:path => $ename:tt, )* }) => {
graphql_enum!($name as (stringify!($name)) { $( $eval => $ename ),* });
};
}

55
src/macros/field.rs Normal file
View file

@ -0,0 +1,55 @@
#[doc(hidden)]
#[macro_export]
macro_rules! __graphql__build_field_matches {
(
$resolveargs:tt,
( $( $acc:tt )* ), field $name:ident $args:tt -> $t:ty as $desc:tt $body:block $( $rest:tt )*
) => {
__graphql__build_field_matches!(
$resolveargs,
(($name; $args; $t; $body) $( $acc )*),
$( $rest )*);
};
(
$resolveargs:tt,
( $( $acc:tt )* ), field $name:ident $args:tt -> $t:ty $body:block $( $rest:tt )*
) => {
__graphql__build_field_matches!(
$resolveargs,
(($name; $args; $t; $body) $( $acc )*),
$( $rest )*);
};
( $resolveargs:tt, $acc:tt, description : $value:tt $( $rest:tt )*) => {
__graphql__build_field_matches!($resolveargs, $acc, $( $rest )*);
};
( $resolveargs:tt, $acc:tt, interfaces : $value:tt $( $rest:tt )*) => {
__graphql__build_field_matches!($resolveargs, $acc, $( $rest )*);
};
( $resolveargs:tt, $acc:tt, instance_resolvers : | $execvar:pat | $resolvers:tt $( $rest:tt )*) => {
__graphql__build_field_matches!($resolveargs, $acc, $( $rest )*);
};
(
($outname:tt, $selfvar:ident, $fieldvar:ident, $argsvar:ident, $executorvar:ident),
( $( ( $name:ident; ( $($args:tt)* ); $t:ty; $body:block ) )* ),
) => {
$(
if $fieldvar == &$crate::to_snake_case(stringify!($name)) {
let result: $t = {
__graphql__args!(
@assign_arg_vars,
$argsvar, $executorvar, $($args)*
);
$body
};
return result.and_then(|r| $executorvar.resolve(&r))
}
)*
panic!("Field {} not found on type {}", $fieldvar, $outname);
};
}

317
src/macros/interface.rs Normal file
View file

@ -0,0 +1,317 @@
/**
Expose GraphQL interfaces
Mapping interfaces to GraphQL can be tricky: there is no direct counterpart to
GraphQL interfaces in Rust, and downcasting is not possible in the general case.
Many other GraphQL implementations in other languages use instance checks and
either dynamic typing or forced downcasts to support these features.
A GraphQL interface defines fields that the implementing types also need to
implement. A GraphQL interface also needs to be able to determine the concrete
type name as well as downcast the general type to the actual concrete type.
## Syntax
See the documentation for [`graphql_object!`][1] on the general item and type
syntax. `graphql_interface!` requires an additional `instance_resolvers` item,
and does _not_ support the `interfaces` item.
`instance_resolvers` is a list/lambda hybrid used to resolve the concrete
instance type of the interface. It starts with a context argument and continues
with an array of expressions, each resolving into an `Option<T>` of the possible
instances:
```rust,ignore
instance_resolvers: |&context| [
context.get_human(self.id()), // returns Option<Human>
context.get_droid(self.id()), // returns Option<Droid>
],
```
Each item in the array will be executed in order when the concrete type is
required.
## Example
A simplified extract from the StarWars schema example shows how to use the
shared context to implement downcasts.
```rust
# #[macro_use] extern crate juniper;
# use juniper::FieldResult;
# use std::collections::HashMap;
struct Human { id: String }
struct Droid { id: String }
struct Database {
humans: HashMap<String, Human>,
droids: HashMap<String, Droid>,
}
trait Character {
fn id(&self) -> &str;
}
impl Character for Human {
fn id(&self) -> &str { &self.id }
}
impl Character for Droid {
fn id(&self) -> &str { &self.id }
}
graphql_object!(Human: Database as "Human" |&self| {
field id() -> FieldResult<&str> { Ok(&self.id) }
});
graphql_object!(Droid: Database as "Droid" |&self| {
field id() -> FieldResult<&str> { Ok(&self.id) }
});
// You can introduce lifetimes or generic parameters by < > before the name.
graphql_interface!(<'a> &'a Character: Database as "Character" |&self| {
field id() -> FieldResult<&str> { Ok(self.id()) }
instance_resolvers: |&context| [
context.humans.get(self.id()),
context.droids.get(self.id()),
]
});
# fn main() { }
```
[1]: macro.graphql_object!.html
*/
#[macro_export]
macro_rules! graphql_interface {
( @as_item, $i:item) => { $i };
( @as_expr, $e:expr) => { $e };
(
@gather_meta,
$reg:expr, $acc:expr, $descr:expr,
field $name:ident $args:tt -> $t:ty as $desc:tt $body:block $( $rest:tt )*
) => {
$acc.push(__graphql__args!(
@apply_args,
$reg,
$reg.field_inside_result(
&$crate::to_snake_case(stringify!($name)),
Err("dummy".to_owned()) as $t)
.description($desc),
$args));
graphql_interface!(@gather_meta, $reg, $acc, $descr, $( $rest )*);
};
(
@gather_meta,
$reg:expr, $acc:expr, $descr:expr,
field $name:ident $args:tt -> $t:ty $body:block $( $rest:tt )*
) => {
$acc.push(__graphql__args!(
@apply_args,
$reg,
$reg.field_inside_result(
&$crate::to_snake_case(stringify!($name)),
Err("dummy".to_owned()) as $t),
$args));
graphql_interface!(@gather_meta, $reg, $acc, $descr, $( $rest )*);
};
(
@gather_meta,
$reg:expr, $acc:expr, $descr:expr,
description : $value:tt $( $rest:tt )*
) => {
$descr = Some(graphql_interface!(@as_expr, $value));
graphql_interface!(@gather_meta, $reg, $acc, $descr, $( $rest )*)
};
(
@gather_meta,
$reg:expr, $acc:expr, $descr:expr,
instance_resolvers: | $execvar:pat | $resolvers:tt $( $rest:tt )*
) => {
graphql_interface!(@gather_meta, $reg, $acc, $descr, $( $rest )*)
};
( @gather_meta, $reg:expr, $acc:expr, $descr:expr, $(,)* ) => {};
(
@resolve_into_type,
$buildargs:tt,
field $name:ident $args:tt -> $t:ty as $descr:tt $body:block $( $rest:tt )*
) => {
graphql_interface!(@resolve_into_type, $buildargs, $( $rest )*)
};
(
@resolve_into_type,
$buildargs:tt,
field $name:ident $args:tt -> $t:ty $body:block $( $rest:tt )*
) => {
graphql_interface!(@resolve_into_type, $buildargs, $( $rest )*)
};
(
@resolve_into_type,
$buildargs:tt, description : $value:tt $( $rest:tt )*
) => {
graphql_interface!(@resolve_into_type, $buildargs, $( $rest )*)
};
(
@resolve_into_type,
$buildargs:tt, interfaces : $value:tt $( $rest:tt )*
) => {
graphql_interface!(@resolve_into_type, $buildargs, $( $rest )*)
};
(
@concrete_type_name,
$buildargs:tt,
field $name:ident $args:tt -> $t:ty as $descr:tt $body:block $( $rest:tt )*
) => {
graphql_interface!(@concrete_type_name, $buildargs, $( $rest )*)
};
(
@concrete_type_name,
$buildargs:tt,
field $name:ident $args:tt -> $t:ty $body:block $( $rest:tt )*
) => {
graphql_interface!(@concrete_type_name, $buildargs, $( $rest )*)
};
(
@concrete_type_name,
$buildargs:tt, description : $value:tt $( $rest:tt )*
) => {
graphql_interface!(@concrete_type_name, $buildargs, $( $rest )*)
};
(
@concrete_type_name,
$buildargs:tt, interfaces : $value:tt $( $rest:tt )*
) => {
graphql_interface!(@concrete_type_name, $buildargs, $( $rest )*)
};
(
@concrete_type_name,
($outname:tt, $ctxtarg:ident, $ctxttype:ty),
instance_resolvers : | $ctxtvar:pat | [ $( $resolver:expr , )* ] $( $rest:tt )*
) => {
let $ctxtvar = &$ctxtarg;
fn inner_type_of<T>(_: T) -> String where T: $crate::GraphQLType<$ctxttype> {
T::name().unwrap().to_owned()
}
$(
if let Some(ref v) = $resolver {
return inner_type_of(v);
}
)*
panic!("Concrete type not handled by instance resolvers on {}", $outname);
};
( @concrete_type_name, $buildargs:tt, ) => {
()
};
(
@resolve_into_type,
($outname:tt, $typenamearg:ident, $execarg:ident, $ctxttype:ty),
instance_resolvers : | $ctxtvar:pat | [ $( $resolver:expr , )* ] $( $rest:tt )*
) => {
let $ctxtvar = &$execarg.context();
fn inner_type_of<T>(_: T) -> String where T: $crate::GraphQLType<$ctxttype> {
T::name().unwrap().to_owned()
}
$(
if let Some(ref v) = $resolver {
if inner_type_of(v) == $typenamearg {
return $execarg.resolve(v);
}
}
)*
panic!("Concrete type not handled by instance resolvers on {}", $outname);
};
( @resolve_into_type, $buildargs:tt, ) => {
()
};
(
( $($lifetime:tt),* ) $name:ty : $ctxt:ty as $outname:tt | &$mainself:ident | {
$( $items:tt )*
}
) => {
graphql_interface!(@as_item, impl<$($lifetime)*> $crate::GraphQLType<$ctxt> for $name {
fn name() -> Option<&'static str> {
Some($outname)
}
#[allow(unused_assignments)]
#[allow(unused_mut)]
fn meta(registry: &mut $crate::Registry<$ctxt>) -> $crate::meta::MetaType {
let mut fields = Vec::new();
let mut description = None;
graphql_interface!(@gather_meta, registry, fields, description, $($items)*);
let mut mt = registry.build_interface_type::<$name>()(&fields);
if let Some(description) = description {
mt = mt.description(description);
}
mt.into_meta()
}
#[allow(unused_variables)]
#[allow(unused_mut)]
fn resolve_field(&$mainself, field: &str, args: &$crate::Arguments, mut executor: &mut $crate::Executor<$ctxt>) -> $crate::ExecutionResult {
__graphql__build_field_matches!(($outname, $mainself, field, args, executor), (), $($items)*);
}
fn concrete_type_name(&$mainself, context: &$ctxt) -> String {
graphql_interface!(
@concrete_type_name,
($outname, context, $ctxt),
$($items)*);
}
fn resolve_into_type(&$mainself, type_name: &str, _: Option<Vec<$crate::Selection>>, executor: &mut $crate::Executor<$ctxt>) -> $crate::ExecutionResult {
graphql_interface!(
@resolve_into_type,
($outname, type_name, executor, $ctxt),
$($items)*);
}
});
};
(
<$($lifetime:tt),*> $name:ty : $ctxt:ty as $outname:tt | &$mainself:ident | {
$( $items:tt )*
}
) => {
graphql_interface!(
($($lifetime),*) $name : $ctxt as $outname | &$mainself | { $( $items )* });
};
(
$name:ty : $ctxt:ty as $outname:tt | &$mainself:ident | {
$( $items:tt )*
}
) => {
graphql_interface!(() $name : $ctxt as $outname | &$mainself | { $( $items )* });
};
}

6
src/macros/mod.rs Normal file
View file

@ -0,0 +1,6 @@
#[macro_use] mod enums;
#[macro_use] mod object;
#[macro_use] mod interface;
#[macro_use] mod scalar;
#[macro_use] mod args;
#[macro_use] mod field;

379
src/macros/object.rs Normal file
View file

@ -0,0 +1,379 @@
/**
Expose GraphQL objects
This is a short-hand macro that implements the `GraphQLType` trait for a given
type. By using this macro instead of implementing it manually, you gain type
safety and reduce repetitive declarations.
# Examples
The simplest case exposes fields on a struct:
```rust
# #[macro_use] extern crate juniper;
# use juniper::FieldResult;
struct User { id: String, name: String, group_ids: Vec<String> }
graphql_object!(User: () as "User" |&self| {
field id() -> FieldResult<&String> {
Ok(&self.id)
}
field name() -> FieldResult<&String> {
Ok(&self.name)
}
// Field and argument names will be converted from snake case to camel case,
// as is the common naming convention in GraphQL. The following field would
// be named "memberOfGroup", and the argument "groupId".
field member_of_group(group_id: String) -> FieldResult<bool> {
Ok(self.group_ids.iter().any(|gid| gid == &group_id))
}
});
# fn main() { }
```
## Documentation and descriptions
You can optionally add descriptions to the type itself, the fields, and field
arguments:
```rust
# #[macro_use] extern crate juniper;
# use juniper::FieldResult;
struct User { id: String, name: String, group_ids: Vec<String> }
graphql_object!(User: () as "User" |&self| {
description: "A user in the database"
field id() -> FieldResult<&String> as "The user's unique identifier" {
Ok(&self.id)
}
field name() -> FieldResult<&String> as "The user's name" {
Ok(&self.name)
}
field member_of_group(
group_id: String as "The group id you want to test membership against"
) -> FieldResult<bool> as "Test if a user is member of a group" {
Ok(self.group_ids.iter().any(|gid| gid == &group_id))
}
});
# fn main() { }
```
## Generics and lifetimes
You can expose generic or pointer types by prefixing the type with the necessary
generic parameters:
```rust
# #[macro_use] extern crate juniper;
# use juniper::FieldResult;
trait SomeTrait { fn id(&self) -> &str; }
graphql_object!(<'a> &'a SomeTrait: () as "SomeTrait" |&self| {
field id() -> FieldResult<&str> { Ok(self.id()) }
});
struct GenericType<T> { items: Vec<T> }
graphql_object!(<T> GenericType<T>: () as "GenericType" |&self| {
field count() -> FieldResult<i64> { Ok(self.items.len() as i64) }
});
struct SelfContained { name: String }
// If the type does not require access to a specific context, you can make it
// generic on the context type. This statically ensures that the fields only
// can access what's available from the type itself.
graphql_object!(<Context> SelfContained: Context as "SelfContained" |&self| {
field name() -> FieldResult<&String> { Ok(&self.name) }
});
# fn main() { }
```
## Implementing interfaces
You can use the `interfaces` item to implement interfaces:
```rust
# #[macro_use] extern crate juniper;
# use juniper::FieldResult;
trait Interface {
fn id(&self) -> &str;
fn as_implementor(&self) -> Option<Implementor>;
}
struct Implementor { id: String }
graphql_interface!(<'a> &'a Interface: () as "Interface" |&self| {
field id() -> FieldResult<&str> { Ok(self.id()) }
instance_resolvers: |&context| [
self.as_implementor(),
]
});
graphql_object!(Implementor: () as "Implementor" |&self| {
field id() -> FieldResult<&str> { Ok(&self.id) }
interfaces: [&Interface]
});
# fn main() { }
```
Note that the implementing type does not need to implement the trait on the Rust
side - only what's in the GraphQL schema matters. The GraphQL interface doesn't
even have to be backed by a trait!
## Emitting errors
`FieldResult<T>` is a simple type alias for `Result<T, String>`. In the end,
errors that fields emit are serialized into strings in the response. However,
the execution system will keep track of the source of all errors, and will
continue executing despite some fields failing.
```
# #[macro_use] extern crate juniper;
# use juniper::FieldResult;
struct User { id: String }
graphql_object!(User: () as "User" |&self| {
field id() -> FieldResult<&String> {
Ok(&self.id)
}
field name() -> FieldResult<&String> {
Err("Does not have a name".to_owned())
}
});
# fn main() { }
```
# Syntax
The top-most syntax of this macro defines which type to expose, the context
type, which lifetime parameters or generics to define, and which name to use in
the GraphQL schema. It takes one of the following two forms:
```text
ExposedType: ContextType as "ExposedName" |&self| { items... }
<Generics> ExposedType: ContextType as "ExposedName" |&self| { items... }
```
## Items
Each item within the brackets of the top level declaration has its own syntax.
The order of individual items does not matter. `graphql_object!` supports a
number of different items.
### Top-level description
```text
description: "Top level description"
```
Adds documentation to the type in the schema, usable by tools such as GraphiQL.
### Interfaces
```text
interfaces: [&Interface, ...]
```
Informs the schema that the type implements the specified interfaces. This needs
to be _GraphQL_ interfaces, not necessarily Rust traits. The Rust types do not
need to have any connection, only what's exposed in the schema matters.
### Fields
```text
field name(args...) -> FieldResult<Type> { }
field name(args...) -> FieldResult<Type> as "Field description" { }
```
Defines a field on the object. The name is converted to camel case, e.g.
`user_name` is exposed as `userName`. The `as "Field description"` adds the
string as documentation on the field.
### Field arguments
```text
&mut executor
arg_name: ArgType
arg_name = default_value: ArgType
arg_name: ArgType as "Argument description"
arg_name = default_value: ArgType as "Argument description"
```
Field arguments can take many forms. If the field needs access to the executor
or context, it can take an [Executor][1] instance by specifying `&mut executor`
as the first argument.
The other cases are similar to regular Rust arguments, with two additions:
argument documentation can be added by appending `as "Description"` after the
type, and a default value can be specified by appending `= value` after the
argument name.
Arguments are required (i.e. non-nullable) by default. If you specify _either_ a
default value, _or_ make the type into an `Option<>`, the argument becomes
optional. For example:
```text
arg_name: String -- required
arg_name: Option<String> -- optional, None if unspecified
arg_name = "default": String -- optional "default" if unspecified
```
[1]: struct.Executor.html
*/
#[macro_export]
macro_rules! graphql_object {
( @as_item, $i:item) => { $i };
( @as_expr, $e:expr) => { $e };
(
@gather_object_meta,
$reg:expr, $acc:expr, $descr:expr, $ifaces:expr,
field $name:ident $args:tt -> $t:ty as $desc:tt $body:block $( $rest:tt )*
) => {
$acc.push(__graphql__args!(
@apply_args,
$reg,
$reg.field_inside_result(
&$crate::to_snake_case(stringify!($name)),
Err("dummy".to_owned()) as $t)
.description($desc),
$args));
graphql_object!(@gather_object_meta, $reg, $acc, $descr, $ifaces, $( $rest )*);
};
(
@gather_object_meta,
$reg:expr, $acc:expr, $descr:expr, $ifaces:expr,
field $name:ident $args:tt -> $t:ty $body:block $( $rest:tt )*
) => {
$acc.push(__graphql__args!(
@apply_args,
$reg,
$reg.field_inside_result(
&$crate::to_snake_case(stringify!($name)),
Err("dummy".to_owned()) as $t),
$args));
graphql_object!(@gather_object_meta, $reg, $acc, $descr, $ifaces, $( $rest )*);
};
(
@gather_object_meta,
$reg:expr, $acc:expr, $descr:expr, $ifaces:expr,
description : $value:tt $( $rest:tt )*
) => {
$descr = Some(graphql_object!(@as_expr, $value));
graphql_object!(@gather_object_meta, $reg, $acc, $descr, $ifaces, $( $rest )*)
};
(
@gather_object_meta,
$reg:expr, $acc:expr, $descr:expr, $ifaces:expr,
interfaces : $value:tt $( $rest:tt )*
) => {
graphql_object!(@assign_interfaces, $reg, $ifaces, $value);
graphql_object!(@gather_object_meta, $reg, $acc, $descr, $ifaces, $( $rest )*)
};
(
@gather_object_meta,
$reg:expr, $acc:expr, $descr:expr, $ifaces:expr, $(,)*
) => {};
( @assign_interfaces, $reg:expr, $tgt:expr, [ $($t:ty,)* ] ) => {
$tgt = Some(vec![
$($reg.get_type::<$t>()),*
]);
};
( @assign_interfaces, $reg:expr, $tgt:expr, [ $($t:ty),* ] ) => {
$tgt = Some(vec![
$($reg.get_type::<$t>()),*
]);
};
(
( $($lifetime:tt)* );
$name:ty; $ctxt:ty; $outname:expr; $mainself:ident; $($items:tt)*
) => {
graphql_object!(@as_item, impl<$($lifetime)*> $crate::GraphQLType<$ctxt> for $name {
fn name() -> Option<&'static str> {
Some($outname)
}
#[allow(unused_assignments)]
#[allow(unused_mut)]
fn meta(registry: &mut $crate::Registry<$ctxt>) -> $crate::meta::MetaType {
let mut fields = Vec::new();
let mut description = None;
let mut interfaces: Option<Vec<$crate::Type>> = None;
graphql_object!(
@gather_object_meta,
registry, fields, description, interfaces, $($items)*
);
let mut mt = registry.build_object_type::<$name>()(&fields);
if let Some(description) = description {
mt = mt.description(description);
}
if let Some(interfaces) = interfaces {
mt = mt.interfaces(&interfaces);
}
mt.into_meta()
}
#[allow(unused_variables)]
#[allow(unused_mut)]
fn resolve_field(
&$mainself,
field: &str,
args: &$crate::Arguments,
mut executor: &mut $crate::Executor<$ctxt>
)
-> $crate::ExecutionResult
{
__graphql__build_field_matches!(
($outname, $mainself, field, args, executor),
(),
$($items)*);
}
});
};
(
<$( $lifetime:tt ),*> $name:ty : $ctxt:ty as $outname:tt | &$mainself:ident | {
$( $items:tt )*
}
) => {
graphql_object!(
( $($lifetime),* ); $name; $ctxt; $outname; $mainself; $( $items )*);
};
(
$name:ty : $ctxt:ty as $outname:tt | &$mainself:ident | {
$( $items:tt )*
}
) => {
graphql_object!(
( ); $name; $ctxt; $outname; $mainself; $( $items )*);
};
}

109
src/macros/scalar.rs Normal file
View file

@ -0,0 +1,109 @@
/**
Expose GraphQL scalars
The GraphQL language defines a number of built-in scalars: strings, numbers, and
booleans. This macro can be used either to define new types of scalars (e.g.
timestamps), or expose other types as one of the built-in scalars (e.g. bigints
as numbers or strings).
Since the preferred transport protocol for GraphQL responses is JSON, most
custom scalars will be transferred as strings. You therefore need to ensure that
the client library you are sending data to can parse the custom value into a
datatype appropriate for that platform.
```rust
# #[macro_use] extern crate juniper;
# use juniper::{Value, FieldResult};
struct UserID(String);
graphql_scalar!(UserID as "UserID" {
resolve(&self) -> Value {
Value::string(&self.0)
}
from_input_value(v: &InputValue) -> Option<UserID> {
v.as_string_value().map(|s| UserID(s.to_owned()))
}
});
# fn main() { }
```
In addition to implementing `GraphQLType` for the type in question,
`FromInputValue` and `ToInputValue` is also implemented. This makes the type
usable as arguments and default values.
`graphql_scalar!` supports generic and lifetime parameters similar to
`graphql_object!`.
*/
#[macro_export]
macro_rules! graphql_scalar {
(
@build_scalar_resolver,
resolve(&$selfvar:ident) -> Value $body:block $($rest:tt)*
) => {
fn resolve(&$selfvar, _: Option<Vec<$crate::Selection>>, _: &mut $crate::Executor<CtxT>) -> $crate::Value {
$body
}
};
(
@build_scalar_conv_impl,
$name:ty; [$($lifetime:tt),*];
resolve(&$selfvar:ident) -> Value $body:block $($rest:tt)*
) => {
impl<$($lifetime),*> $crate::ToInputValue for $name {
fn to(&$selfvar) -> $crate::InputValue {
$crate::ToInputValue::to(&$body)
}
}
graphql_scalar!(@build_scalar_conv_impl, $name; [$($lifetime),*]; $($rest)*);
};
(
@build_scalar_conv_impl,
$name:ty; [$($lifetime:tt),*];
from_input_value($arg:ident: &InputValue) -> $result:ty $body:block
$($rest:tt)*
) => {
impl<$($lifetime),*> $crate::FromInputValue for $name {
fn from($arg: &$crate::InputValue) -> $result {
$body
}
}
graphql_scalar!(@build_scalar_conv_impl, $name; [$($lifetime),*]; $($rest)*);
};
(
@build_scalar_conv_impl,
$name:ty; $($lifetime:tt),*;
) => {
};
(($($lifetime:tt),*) $name:ty as $outname:expr => { $( $items:tt )* }) => {
impl<$($lifetime,)* CtxT> $crate::GraphQLType<CtxT> for $name {
fn name() -> Option<&'static str> {
Some($outname)
}
fn meta(registry: &mut $crate::Registry<CtxT>) -> $crate::meta::MetaType {
registry.build_scalar_type::<Self>().into_meta()
}
graphql_scalar!(@build_scalar_resolver, $($items)*);
}
graphql_scalar!(@build_scalar_conv_impl, $name; [$($lifetime),*]; $($items)*);
};
(<$($lifetime:tt),*> $name:ty as $outname:tt { $( $items:tt )* }) => {
graphql_scalar!(($($lifetime),*) $name as $outname => { $( $items )* });
};
( $name:ty as $outname:tt { $( $items:tt )* }) => {
graphql_scalar!(() $name as $outname => { $( $items )* });
}
}

339
src/parser/document.rs Normal file
View file

@ -0,0 +1,339 @@
use ast::{Definition, Document, OperationType,
VariableDefinitions, VariableDefinition, InputValue,
Operation, Fragment, Selection, Directive, Field, Arguments,
FragmentSpread, InlineFragment, Type};
use parser::{Lexer, Parser, Spanning, UnlocatedParseResult, OptionParseResult, ParseResult, ParseError, Token};
use parser::value::parse_value_literal;
#[doc(hidden)]
pub fn parse_document_source(s: &str) -> UnlocatedParseResult<Document> {
let mut lexer = Lexer::new(s);
let mut parser = try!(Parser::new(&mut lexer).map_err(|s| s.map(ParseError::LexerError)));
parse_document(&mut parser)
}
fn parse_document<'a>(parser: &mut Parser<'a>) -> UnlocatedParseResult<'a, Document> {
let mut defs = Vec::new();
loop {
defs.push(try!(parse_definition(parser)));
if parser.peek().item == Token::EndOfFile {
return Ok(defs);
}
}
}
fn parse_definition<'a>(parser: &mut Parser<'a>) -> UnlocatedParseResult<'a, Definition> {
match parser.peek().item {
Token::CurlyOpen | Token::Name("query") | Token::Name("mutation") =>
Ok(Definition::Operation(try!(parse_operation_definition(parser)))),
Token::Name("fragment") =>
Ok(Definition::Fragment(try!(parse_fragment_definition(parser)))),
_ => Err(parser.next().map(ParseError::UnexpectedToken)),
}
}
fn parse_operation_definition<'a>(parser: &mut Parser<'a>) -> ParseResult<'a, Operation> {
if parser.peek().item == Token::CurlyOpen {
let selection_set = try!(parse_selection_set(parser));
Ok(Spanning::start_end(
&selection_set.start,
&selection_set.end,
Operation {
operation_type: OperationType::Query,
name: None,
variable_definitions: None,
directives: None,
selection_set: selection_set.item,
}))
}
else {
let start_pos = parser.peek().start.clone();
let operation_type = try!(parse_operation_type(parser));
let name = match parser.peek().item {
Token::Name(_) => Some(try!(parser.expect_name())),
_ => None
};
let variable_definitions = try!(parse_variable_definitions(parser));
let directives = try!(parse_directives(parser));
let selection_set = try!(parse_selection_set(parser));
Ok(Spanning::start_end(
&start_pos,
&selection_set.end,
Operation {
operation_type: operation_type.item,
name: name,
variable_definitions: variable_definitions,
directives: directives.map(|s| s.item),
selection_set: selection_set.item,
}))
}
}
fn parse_fragment_definition<'a>(parser: &mut Parser<'a>) -> ParseResult<'a, Fragment> {
let Spanning { start: start_pos, .. } = try!(parser.expect(&Token::Name("fragment")));
let name = match parser.expect_name() {
Ok(n) => if &n.item == "on" {
return Err(n.map(|_| ParseError::UnexpectedToken(Token::Name("on"))));
}
else {
n
},
Err(e) => return Err(e),
};
try!(parser.expect(&Token::Name("on")));
let type_cond = try!(parser.expect_name());
let directives = try!(parse_directives(parser));
let selection_set = try!(parse_selection_set(parser));
Ok(Spanning::start_end(
&start_pos,
&selection_set.end,
Fragment {
name: name,
type_condition: type_cond,
directives: directives.map(|s| s.item),
selection_set: selection_set.item,
}))
}
fn parse_optional_selection_set<'a>(parser: &mut Parser<'a>) -> OptionParseResult<'a, Vec<Selection>> {
if parser.peek().item == Token::CurlyOpen {
Ok(Some(try!(parse_selection_set(parser))))
}
else {
Ok(None)
}
}
fn parse_selection_set<'a>(parser: &mut Parser<'a>) -> ParseResult<'a, Vec<Selection>> {
parser.unlocated_delimited_nonempty_list(
&Token::CurlyOpen,
parse_selection,
&Token::CurlyClose)
}
fn parse_selection<'a>(parser: &mut Parser<'a>) -> UnlocatedParseResult<'a, Selection> {
match parser.peek().item {
Token::Ellipsis => parse_fragment(parser),
_ => parse_field(parser).map(Selection::Field),
}
}
fn parse_fragment<'a>(parser: &mut Parser<'a>) -> UnlocatedParseResult<'a, Selection> {
let Spanning { start: ref start_pos, .. } = try!(parser.expect(&Token::Ellipsis));
match parser.peek().item {
Token::Name("on") => {
parser.next();
let name = try!(parser.expect_name());
let directives = try!(parse_directives(parser));
let selection_set = try!(parse_selection_set(parser));
Ok(Selection::InlineFragment(
Spanning::start_end(
&start_pos.clone(),
&selection_set.end,
InlineFragment {
type_condition: Some(name),
directives: directives.map(|s| s.item),
selection_set: selection_set.item,
})))
},
Token::CurlyOpen => {
let selection_set = try!(parse_selection_set(parser));
Ok(Selection::InlineFragment(
Spanning::start_end(
&start_pos.clone(),
&selection_set.end,
InlineFragment {
type_condition: None,
directives: None,
selection_set: selection_set.item,
})))
},
Token::Name(_) => {
let frag_name = try!(parser.expect_name());
let directives = try!(parse_directives(parser));
Ok(Selection::FragmentSpread(
Spanning::start_end(
&start_pos.clone(),
&directives.as_ref().map_or(&frag_name.end, |s| &s.end).clone(),
FragmentSpread {
name: frag_name,
directives: directives.map(|s| s.item),
})))
},
_ => Err(parser.next().map(ParseError::UnexpectedToken)),
}
}
fn parse_field<'a>(parser: &mut Parser<'a>) -> ParseResult<'a, Field> {
let mut alias = Some(try!(parser.expect_name()));
let name = if try!(parser.skip(&Token::Colon)).is_some() {
try!(parser.expect_name())
}
else {
alias.take().unwrap()
};
let arguments = try!(parse_arguments(parser));
let directives = try!(parse_directives(parser));
let selection_set = try!(parse_optional_selection_set(parser));
Ok(Spanning::start_end(
&alias.as_ref().unwrap_or(&name).start.clone(),
&selection_set.as_ref().map(|s| &s.end)
.or_else(|| directives.as_ref().map(|s| &s.end))
.or_else(|| arguments.as_ref().map(|s| &s.end))
.unwrap_or(&name.end)
.clone(),
Field {
alias: alias,
name: name,
arguments: arguments,
directives: directives.map(|s| s.item),
selection_set: selection_set.map(|s| s.item),
}))
}
fn parse_arguments<'a>(parser: &mut Parser<'a>) -> OptionParseResult<'a, Arguments> {
if parser.peek().item != Token::ParenOpen {
Ok(None)
} else {
Ok(Some(try!(parser.delimited_nonempty_list(
&Token::ParenOpen,
parse_argument,
&Token::ParenClose
)).map(|args| Arguments { items: args.into_iter().map(|s| s.item).collect() })))
}
}
fn parse_argument<'a>(parser: &mut Parser<'a>) -> ParseResult<'a, (Spanning<String>, Spanning<InputValue>)> {
let name = try!(parser.expect_name());
try!(parser.expect(&Token::Colon));
let value = try!(parse_value_literal(parser, false));
Ok(Spanning::start_end(
&name.start.clone(),
&value.end.clone(),
(name, value)))
}
fn parse_operation_type<'a>(parser: &mut Parser<'a>) -> ParseResult<'a, OperationType> {
match parser.peek().item {
Token::Name("query") => Ok(parser.next().map(|_| OperationType::Query)),
Token::Name("mutation") => Ok(parser.next().map(|_| OperationType::Mutation)),
_ => Err(parser.next().map(ParseError::UnexpectedToken))
}
}
fn parse_variable_definitions<'a>(parser: &mut Parser<'a>) -> OptionParseResult<'a, VariableDefinitions> {
if parser.peek().item != Token::ParenOpen {
Ok(None)
}
else {
Ok(Some(try!(parser.delimited_nonempty_list(
&Token::ParenOpen,
parse_variable_definition,
&Token::ParenClose
)).map(|defs| VariableDefinitions { items: defs.into_iter().map(|s| s.item).collect() })))
}
}
fn parse_variable_definition<'a>(parser: &mut Parser<'a>) -> ParseResult<'a, (Spanning<String>, VariableDefinition)> {
let Spanning { start: start_pos, .. } = try!(parser.expect(&Token::Dollar));
let var_name = try!(parser.expect_name());
try!(parser.expect(&Token::Colon));
let var_type = try!(parse_type(parser));
let default_value = if try!(parser.skip(&Token::Equals)).is_some() {
Some(try!(parse_value_literal(parser, true)))
}
else {
None
};
Ok(Spanning::start_end(
&start_pos,
&default_value.as_ref().map_or(&var_type.end, |s| &s.end).clone(),
(
Spanning::start_end(
&start_pos,
&var_name.end,
var_name.item
),
VariableDefinition {
var_type: var_type,
default_value: default_value,
}
)))
}
fn parse_directives<'a>(parser: &mut Parser<'a>) -> OptionParseResult<'a, Vec<Spanning<Directive>>> {
if parser.peek().item != Token::At {
Ok(None)
}
else {
let mut items = Vec::new();
while parser.peek().item == Token::At {
items.push(try!(parse_directive(parser)));
}
Ok(Spanning::spanning(items))
}
}
fn parse_directive<'a>(parser: &mut Parser<'a>) -> ParseResult<'a, Directive> {
let Spanning { start: start_pos, .. } = try!(parser.expect(&Token::At));
let name = try!(parser.expect_name());
let arguments = try!(parse_arguments(parser));
Ok(Spanning::start_end(
&start_pos,
&arguments.as_ref().map_or(&name.end, |s| &s.end).clone(),
Directive {
name: name,
arguments: arguments,
}))
}
pub fn parse_type<'a>(parser: &mut Parser<'a>) -> ParseResult<'a, Type> {
let parsed_type = if let Some(Spanning { start: start_pos, ..}) = try!(parser.skip(&Token::BracketOpen)) {
let inner_type = try!(parse_type(parser));
let Spanning { end: end_pos, .. } = try!(parser.expect(&Token::BracketClose));
Spanning::start_end(
&start_pos,
&end_pos,
Type::List(Box::new(inner_type.item)))
}
else {
try!(parser.expect_name()).map(Type::Named)
};
Ok(match *parser.peek() {
Spanning { item: Token::ExclamationMark, .. } =>
try!(wrap_non_null(parser, parsed_type)),
_ => parsed_type
})
}
fn wrap_non_null<'a>(parser: &mut Parser<'a>, inner: Spanning<Type>) -> ParseResult<'a, Type> {
let Spanning { end: end_pos, .. } = try!(parser.expect(&Token::ExclamationMark));
let wrapped = match inner.item {
Type::Named(name) => Type::NonNullNamed(name),
Type::List(l) => Type::NonNullList(l),
t => t,
};
Ok(Spanning::start_end(&inner.start, &end_pos, wrapped))
}

522
src/parser/lexer.rs Normal file
View file

@ -0,0 +1,522 @@
use std::char;
use std::str::CharIndices;
use std::iter::{Iterator, Peekable};
use std::result::Result;
use std::fmt;
use parser::{SourcePosition, Spanning};
#[doc(hidden)]
#[derive(Debug)]
pub struct Lexer<'a> {
iterator: Peekable<CharIndices<'a>>,
source: &'a str,
length: usize,
position: SourcePosition,
has_reached_eof: bool,
}
/// A single token in the input source
#[derive(Debug, PartialEq)]
#[allow(missing_docs)]
pub enum Token<'a> {
Name(&'a str),
Int(i64),
Float(f64),
String(String),
ExclamationMark,
Dollar,
ParenOpen,
ParenClose,
BracketOpen,
BracketClose,
CurlyOpen,
CurlyClose,
Ellipsis,
Colon,
Equals,
At,
Pipe,
EndOfFile,
}
/// Error when tokenizing the input source
#[derive(Debug, PartialEq, Eq)]
pub enum LexerError {
/// An unknown character was found
///
/// Unknown characters are characters that do not occur anywhere in the
/// GraphQL language, such as `?` or `%`.
UnknownCharacter(char),
/// An unexpected character was found
///
/// Unexpected characters are characters that _do_ exist in the GraphQL
/// language, but is not expected at the current position in the document.
UnexpectedCharacter(char),
/// An unterminated string literal was found
///
/// Apart from forgetting the ending `"`, terminating a string within a
/// Unicode escape sequence or having a line break in the string also
/// causes this error.
UnterminatedString,
/// An unknown character in a string literal was found
///
/// This occurs when an invalid source character is found in a string
/// literal, such as ASCII control characters.
UnknownCharacterInString(char),
/// An unknown escape sequence in a string literal was found
///
/// Only a limited set of escape sequences are supported, this is emitted
/// when e.g. `"\l"` is parsed.
UnknownEscapeSequence(String),
/// The input source was unexpectedly terminated
///
/// Emitted when the current token requires a succeeding character, but
/// the source has reached EOF. Emitted when scanning e.g. `"1."`.
UnexpectedEndOfFile,
/// An invalid number literal was found
InvalidNumber,
}
pub type LexerResult<'a> = Result<Spanning<Token<'a>>, Spanning<LexerError>>;
impl<'a> Lexer<'a> {
#[doc(hidden)]
pub fn new(source: &'a str) -> Lexer<'a> {
Lexer {
iterator: source.char_indices().peekable(),
source: source,
length: source.len(),
position: SourcePosition::new_origin(),
has_reached_eof: false,
}
}
fn peek_char(&mut self) -> Option<(usize, char)> {
assert!(self.position.index() <= self.length);
assert!(!self.has_reached_eof);
self.iterator.peek().map(|&(idx, ch)| (idx, ch))
}
fn next_char(&mut self) -> Option<(usize, char)> {
assert!(self.position.index() <= self.length);
assert!(!self.has_reached_eof);
let next = self.iterator.next();
if let Some((_, ch)) = next {
if ch == '\n' {
self.position.advance_line();
}
else {
self.position.advance_col();
}
}
next
}
fn emit_single_char(&mut self, t: Token<'a>) -> Spanning<Token<'a>> {
assert!(self.position.index() <= self.length);
let start_pos = self.position.clone();
self.next_char()
.expect("Internal error in GraphQL lexer: emit_single_char reached EOF");
Spanning::single_width(&start_pos, t)
}
fn scan_over_whitespace(&mut self) {
while let Some((_, ch)) = self.peek_char() {
if ch == '\t' || ch == ' ' || ch == '\n' || ch == '\r' || ch == ',' {
self.next_char();
}
else if ch == '#' {
self.next_char();
while let Some((_, ch)) = self.peek_char() {
if is_source_char(ch) && (ch == '\n' || ch == '\r') {
self.next_char();
break;
}
else if is_source_char(ch) {
self.next_char();
}
else {
break;
}
}
}
else {
break;
}
}
}
fn scan_ellipsis(&mut self) -> LexerResult<'a> {
let start_pos = self.position.clone();
for _ in 0..3 {
let (_, ch) = try!(self.next_char().ok_or(Spanning::zero_width(&self.position, LexerError::UnexpectedEndOfFile)));
if ch != '.' {
return Err(Spanning::zero_width(&start_pos, LexerError::UnexpectedCharacter('.')));
}
}
Ok(Spanning::start_end(&start_pos, &self.position, Token::Ellipsis))
}
fn scan_name(&mut self) -> LexerResult<'a> {
let start_pos = self.position.clone();
let (start_idx, start_ch) = try!(self.next_char().ok_or(
Spanning::zero_width(&self.position, LexerError::UnexpectedEndOfFile)));
assert!(is_name_start(start_ch));
let mut end_idx = start_idx;
while let Some((idx, ch)) = self.peek_char() {
if is_name_cont(ch) {
self.next_char();
end_idx = idx;
}
else {
break;
}
}
Ok(Spanning::start_end(
&start_pos,
&self.position,
Token::Name(&self.source[start_idx..end_idx + 1])))
}
fn scan_string(&mut self) -> LexerResult<'a> {
let start_pos = self.position.clone();
let (_, start_ch) = try!(self.next_char().ok_or(
Spanning::zero_width(&self.position, LexerError::UnexpectedEndOfFile)));
assert!(start_ch == '"');
let mut acc = String::new();
while let Some((_, ch)) = self.peek_char() {
if ch == '"' {
self.next_char();
return Ok(Spanning::start_end(
&start_pos,
&self.position,
Token::String(acc)));
}
else if ch == '\\' {
self.next_char();
match self.peek_char() {
Some((_, '"')) => { self.next_char(); acc.push('"'); },
Some((_, '\\')) => { self.next_char(); acc.push('\\'); },
Some((_, '/')) => { self.next_char(); acc.push('/'); },
Some((_, 'b')) => { self.next_char(); acc.push('\u{0008}'); },
Some((_, 'f')) => { self.next_char(); acc.push('\u{000c}'); },
Some((_, 'n')) => { self.next_char(); acc.push('\n'); },
Some((_, 'r')) => { self.next_char(); acc.push('\r'); },
Some((_, 't')) => { self.next_char(); acc.push('\t'); },
Some((_, 'u')) => {
let start_pos = self.position.clone();
self.next_char();
acc.push(try!(self.scan_escaped_unicode(&start_pos)));
},
Some((_, ch)) => {
let mut s = String::from("\\");
s.push(ch);
return Err(Spanning::zero_width(
&self.position,
LexerError::UnknownEscapeSequence(s)));
},
None => {
return Err(Spanning::zero_width(
&self.position,
LexerError::UnterminatedString));
},
}
if let Some((_, ch)) = self.peek_char() {
if ch == 'n' {
}
}
else {
return Err(Spanning::zero_width(
&self.position,
LexerError::UnterminatedString));
}
}
else if ch == '\n' || ch == '\r' {
return Err(Spanning::zero_width(
&self.position,
LexerError::UnterminatedString));
}
else if !is_source_char(ch) {
return Err(Spanning::zero_width(
&self.position,
LexerError::UnknownCharacterInString(ch)));
}
else {
self.next_char();
acc.push(ch);
}
}
Err(Spanning::zero_width(&self.position, LexerError::UnterminatedString))
}
fn scan_escaped_unicode(&mut self, start_pos: &SourcePosition) -> Result<char, Spanning<LexerError>> {
let (start_idx, _) = try!(self.peek_char().ok_or(
Spanning::zero_width(&self.position, LexerError::UnterminatedString)));
let mut end_idx = start_idx;
let mut len = 0;
for _ in 0..4 {
let (idx, ch) = try!(self.next_char().ok_or(
Spanning::zero_width(&self.position, LexerError::UnterminatedString)));
if !ch.is_alphanumeric() {
break;
}
end_idx = idx;
len += 1;
}
let escape = &self.source[start_idx..end_idx+1];
if len != 4 {
return Err(Spanning::zero_width(
start_pos,
LexerError::UnknownEscapeSequence("\\u".to_owned() + escape)));
}
let code_point = try!(u32::from_str_radix(escape, 16).map_err(|_|
Spanning::zero_width(
start_pos,
LexerError::UnknownEscapeSequence("\\u".to_owned() + escape))));
char::from_u32(code_point).ok_or_else(||
Spanning::zero_width(
start_pos,
LexerError::UnknownEscapeSequence("\\u".to_owned() + escape)))
}
fn scan_number(&mut self) -> LexerResult<'a> {
let start_pos = self.position.clone();
let int_part = try!(self.scan_integer_part());
let mut frac_part = None;
let mut exp_part = None;
if let Some((_, '.')) = self.peek_char() {
self.next_char();
frac_part = Some(try!(self.scan_digits()));
}
if let Some((_, ch)) = self.peek_char() {
if ch == 'e' || ch == 'E' {
self.next_char();
let mut is_negative = false;
if let Some((_, ch)) = self.peek_char() {
if ch == '-' {
self.next_char();
is_negative = true;
}
else if ch == '+' {
self.next_char();
}
}
exp_part = Some(if is_negative { -1 } else { 1 } * try!(self.scan_digits()));
}
}
let mantissa = frac_part.map(|f| f as f64).map(|frac|
if frac > 0f64 {
frac / 10f64.powf(frac.log10().floor() + 1f64)
}
else {
0f64
}).map(|m| if int_part < 0 { -m } else { m });
let exp = exp_part.map(|e| e as f64).map(|e| 10f64.powf(e));
Ok(Spanning::start_end(
&start_pos,
&self.position,
match (mantissa, exp) {
(None, None) => Token::Int(int_part),
(None, Some(exp)) => Token::Float((int_part as f64) * exp),
(Some(mantissa), None) => Token::Float((int_part as f64) + mantissa),
(Some(mantissa), Some(exp)) => Token::Float(((int_part as f64) + mantissa) * exp),
}))
}
fn scan_integer_part(&mut self) -> Result<i64, Spanning<LexerError>> {
let is_negative = {
let (_, init_ch) = try!(self.peek_char().ok_or(
Spanning::zero_width(&self.position, LexerError::UnexpectedEndOfFile)));
if init_ch == '-' {
self.next_char();
true
}
else {
false
}
};
let (_, ch) = try!(self.peek_char().ok_or(
Spanning::zero_width(&self.position, LexerError::UnexpectedEndOfFile)));
if ch == '0' {
self.next_char();
match self.peek_char() {
Some((_, '0')) => Err(Spanning::zero_width(&self.position, LexerError::UnexpectedCharacter(ch))),
_ => Ok(0),
}
}
else {
Ok(try!(self.scan_digits()) * if is_negative { -1 } else { 1 })
}
}
fn scan_digits(&mut self) -> Result<i64, Spanning<LexerError>> {
let start_pos = self.position.clone();
let (start_idx, ch) = try!(self.peek_char().ok_or(
Spanning::zero_width(&self.position, LexerError::UnexpectedEndOfFile)));
let mut end_idx = start_idx;
if !ch.is_digit(10) {
return Err(Spanning::zero_width(&self.position, LexerError::UnexpectedCharacter(ch)));
}
while let Some((idx, ch)) = self.peek_char() {
if !ch.is_digit(10) {
break;
}
else {
self.next_char();
end_idx = idx;
}
}
i64::from_str_radix(&self.source[start_idx..end_idx+1], 10)
.map_err(|_| Spanning::zero_width(&start_pos, LexerError::InvalidNumber))
}
}
impl<'a> Iterator for Lexer<'a> {
type Item = LexerResult<'a>;
fn next(&mut self) -> Option<Self::Item> {
if self.has_reached_eof {
return None;
}
self.scan_over_whitespace();
let ch = self.iterator.peek().map(|&(_, ch)| ch);
Some(match ch {
Some('!') => Ok(self.emit_single_char(Token::ExclamationMark)),
Some('$') => Ok(self.emit_single_char(Token::Dollar)),
Some('(') => Ok(self.emit_single_char(Token::ParenOpen)),
Some(')') => Ok(self.emit_single_char(Token::ParenClose)),
Some('[') => Ok(self.emit_single_char(Token::BracketOpen)),
Some(']') => Ok(self.emit_single_char(Token::BracketClose)),
Some('{') => Ok(self.emit_single_char(Token::CurlyOpen)),
Some('}') => Ok(self.emit_single_char(Token::CurlyClose)),
Some(':') => Ok(self.emit_single_char(Token::Colon)),
Some('=') => Ok(self.emit_single_char(Token::Equals)),
Some('@') => Ok(self.emit_single_char(Token::At)),
Some('|') => Ok(self.emit_single_char(Token::Pipe)),
Some('.') => self.scan_ellipsis(),
Some('"') => self.scan_string(),
Some(ch) => {
if is_number_start(ch) {
self.scan_number()
}
else if is_name_start(ch) {
self.scan_name()
}
else {
Err(Spanning::zero_width(&self.position, LexerError::UnknownCharacter(ch)))
}
},
None => {
self.has_reached_eof = true;
Ok(Spanning::zero_width(
&self.position, Token::EndOfFile))
},
})
}
}
impl<'a> fmt::Display for Token<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Token::Name(ref name) => write!(f, "{}", name),
Token::Int(i) => write!(f, "{}", i),
Token::Float(v) => write!(f, "{}", v),
Token::String(ref s) => write!(f, "{}", s),
Token::ExclamationMark => write!(f, "!"),
Token::Dollar => write!(f, "$"),
Token::ParenOpen => write!(f, "("),
Token::ParenClose => write!(f, ")"),
Token::BracketOpen => write!(f, "["),
Token::BracketClose => write!(f, "]"),
Token::CurlyOpen => write!(f, "{{"),
Token::CurlyClose => write!(f, "}}"),
Token::Ellipsis => write!(f, "..."),
Token::Colon => write!(f, ":"),
Token::Equals => write!(f, "="),
Token::At => write!(f, "@"),
Token::Pipe => write!(f, "|"),
Token::EndOfFile => write!(f, "End of file"),
}
}
}
fn is_source_char(c: char) -> bool {
c == '\t' || c == '\n' || c == '\r' || c >= ' '
}
fn is_name_start(c: char) -> bool {
c == '_' || (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')
}
fn is_name_cont(c: char) -> bool {
is_name_start(c) || (c >= '0' && c <= '9')
}
fn is_number_start(c: char) -> bool {
c == '-' || (c >= '0' && c <= '9')
}
impl fmt::Display for LexerError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
LexerError::UnknownCharacter(c) => write!(f, "Unknown character \"{}\"", c),
LexerError::UnterminatedString => write!(f, "Unterminated string literal"),
LexerError::UnknownCharacterInString(c) => write!(f, "Unknown character \"{}\" in string literal", c),
LexerError::UnknownEscapeSequence(ref s) => write!(f, "Unknown escape sequence \"{}\" in string", s),
LexerError::UnexpectedCharacter(c) => write!(f, "Unexpected character \"{}\"", c),
LexerError::UnexpectedEndOfFile => write!(f, "Unexpected end of input"),
LexerError::InvalidNumber => write!(f, "Invalid number literal"),
}
}
}

16
src/parser/mod.rs Normal file
View file

@ -0,0 +1,16 @@
//! Query parser and language utilities
mod utils;
mod lexer;
mod parser;
mod value;
mod document;
#[cfg(test)]
mod tests;
pub use self::document::parse_document_source;
pub use self::parser::{Parser, ParseError, ParseResult, UnlocatedParseResult, OptionParseResult};
pub use self::lexer::{Token, Lexer, LexerError};
pub use self::utils::{Spanning, SourcePosition};

179
src/parser/parser.rs Normal file
View file

@ -0,0 +1,179 @@
use std::result::Result;
use std::fmt;
use parser::{Spanning, Token, LexerError, Lexer};
/// Error while parsing a GraphQL query
#[derive(Debug, PartialEq)]
pub enum ParseError<'a> {
/// An unexpected token occurred in the source
UnexpectedToken(Token<'a>),
/// The input source abruptly ended
UnexpectedEndOfFile,
/// An error during tokenization occurred
LexerError(LexerError),
}
#[doc(hidden)]
pub type ParseResult<'a, T> = Result<Spanning<T>, Spanning<ParseError<'a>>>;
#[doc(hidden)]
pub type UnlocatedParseResult<'a, T> = Result<T, Spanning<ParseError<'a>>>;
#[doc(hidden)]
pub type OptionParseResult<'a, T> = Result<Option<Spanning<T>>, Spanning<ParseError<'a>>>;
#[doc(hidden)]
#[derive(Debug)]
pub struct Parser<'a> {
tokens: Vec<Spanning<Token<'a>>>,
}
impl<'a> Parser<'a> {
#[doc(hidden)]
pub fn new(lexer: &mut Lexer<'a>) -> Result<Parser<'a>, Spanning<LexerError>> {
let mut tokens = Vec::new();
for res in lexer {
match res {
Ok(s) => tokens.push(s),
Err(e) => return Err(e),
}
}
Ok(Parser {
tokens: tokens,
})
}
#[doc(hidden)]
pub fn peek(&self) -> &Spanning<Token<'a>> {
&self.tokens[0]
}
#[doc(hidden)]
pub fn next(&mut self) -> Spanning<Token<'a>> {
if self.tokens.len() == 1 {
panic!("Can not parse over EOF marker");
}
self.tokens.remove(0)
}
#[doc(hidden)]
pub fn expect(&mut self, expected: &Token) -> ParseResult<'a, Token<'a>> {
if &self.peek().item != expected {
Err(self.next().map(ParseError::UnexpectedToken))
}
else {
Ok(self.next())
}
}
#[doc(hidden)]
pub fn skip(&mut self, expected: &Token) -> Result<Option<Spanning<Token<'a>>>, Spanning<ParseError<'a>>> {
if &self.peek().item == expected {
Ok(Some(self.next()))
}
else if self.peek().item == Token::EndOfFile {
Err(Spanning::zero_width(
&self.peek().start,
ParseError::UnexpectedEndOfFile))
}
else {
Ok(None)
}
}
#[doc(hidden)]
pub fn delimited_list<T, F>(&mut self, opening: &Token, parser: F, closing: &Token)
-> ParseResult<'a, Vec<Spanning<T>>>
where T: fmt::Debug, F: Fn(&mut Parser<'a>) -> ParseResult<'a, T>
{
let Spanning { start: start_pos, .. } = try!(self.expect(opening));
let mut items = Vec::new();
loop {
if let Some(Spanning { end: end_pos, .. }) = try!(self.skip(closing)) {
return Ok(Spanning::start_end(
&start_pos,
&end_pos,
items));
}
items.push(try!(parser(self)));
}
}
#[doc(hidden)]
pub fn delimited_nonempty_list<T, F>(&mut self, opening: &Token, parser: F, closing: &Token)
-> ParseResult<'a, Vec<Spanning<T>>>
where T: fmt::Debug, F: Fn(&mut Parser<'a>) -> ParseResult<'a, T>
{
let Spanning { start: start_pos, .. } = try!(self.expect(opening));
let mut items = Vec::new();
loop {
items.push(try!(parser(self)));
if let Some(Spanning { end: end_pos, .. }) = try!(self.skip(closing)) {
return Ok(Spanning::start_end(
&start_pos,
&end_pos,
items));
}
}
}
#[doc(hidden)]
pub fn unlocated_delimited_nonempty_list<T, F>(&mut self, opening: &Token, parser: F, closing: &Token)
-> ParseResult<'a, Vec<T>>
where T: fmt::Debug, F: Fn(&mut Parser<'a>) -> UnlocatedParseResult<'a, T>
{
let Spanning { start: start_pos, .. } = try!(self.expect(opening));
let mut items = Vec::new();
loop {
items.push(try!(parser(self)));
if let Some(Spanning { end: end_pos, .. }) = try!(self.skip(closing)) {
return Ok(Spanning::start_end(
&start_pos,
&end_pos,
items));
}
}
}
#[doc(hidden)]
pub fn expect_name(&mut self) -> ParseResult<'a, String> {
match *self.peek() {
Spanning { item: Token::Name(_), .. } =>
Ok(self.next().map(|token|
if let Token::Name(name) = token {
name.to_owned()
}
else {
panic!("Internal parse error in `expect_name`");
})),
Spanning { item: Token::EndOfFile, .. } =>
Err(Spanning::start_end(
&self.peek().start.clone(),
&self.peek().end.clone(),
ParseError::UnexpectedEndOfFile)),
_ => Err(self.next().map(ParseError::UnexpectedToken)),
}
}
}
impl<'a> fmt::Display for ParseError<'a> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
ParseError::UnexpectedToken(ref token) => write!(f, "Unexpected \"{}\"", token),
ParseError::UnexpectedEndOfFile => write!(f, "Unexpected end of input"),
ParseError::LexerError(ref err) => err.fmt(f),
}
}
}

View file

@ -0,0 +1,123 @@
use ast::{Definition, Operation, Document, OperationType, Field, Selection, InputValue, Arguments};
use parser::{Spanning, SourcePosition, ParseError, Token};
use parser::document::parse_document_source;
fn parse_document(s: &str) -> Document {
parse_document_source(s)
.expect(&format!("Parse error on input {:#?}", s))
}
fn parse_document_error<'a>(s: &'a str) -> Spanning<ParseError<'a>> {
match parse_document_source(s) {
Ok(doc) => panic!("*No* parse error on input {:#?} =>\n{:#?}", s, doc),
Err(err) => err,
}
}
#[test]
fn simple_ast() {
assert_eq!(
parse_document(r#"
{
node(id: 4) {
id
name
}
}
"#),
vec![
Definition::Operation(Spanning::start_end(
&SourcePosition::new(13, 1, 12),
&SourcePosition::new(124, 6, 13),
Operation {
operation_type: OperationType::Query,
name: None,
variable_definitions: None,
directives: None,
selection_set: vec![
Selection::Field(
Spanning::start_end(
&SourcePosition::new(31, 2, 16),
&SourcePosition::new(110, 5, 17),
Field {
alias: None,
name: Spanning::start_end(
&SourcePosition::new(31, 2, 16),
&SourcePosition::new(35, 2, 20),
"node".to_owned()),
arguments: Some(Spanning::start_end(
&SourcePosition::new(35, 2, 20),
&SourcePosition::new(42, 2, 27),
Arguments {
items: vec![
(
Spanning::start_end(
&SourcePosition::new(36, 2, 21),
&SourcePosition::new(38, 2, 23),
"id".to_owned()),
Spanning::start_end(
&SourcePosition::new(40, 2, 25),
&SourcePosition::new(41, 2, 26),
InputValue::int(4))
),
]
})),
directives: None,
selection_set: Some(vec![
Selection::Field(
Spanning::start_end(
&SourcePosition::new(65, 3, 20),
&SourcePosition::new(67, 3, 22),
Field {
alias: None,
name: Spanning::start_end(
&SourcePosition::new(65, 3, 20),
&SourcePosition::new(67, 3, 22),
"id".to_owned()),
arguments: None,
directives: None,
selection_set: None,
})),
Selection::Field(
Spanning::start_end(
&SourcePosition::new(88, 4, 20),
&SourcePosition::new(92, 4, 24),
Field {
alias: None,
name: Spanning::start_end(
&SourcePosition::new(88, 4, 20),
&SourcePosition::new(92, 4, 24),
"name".to_owned()),
arguments: None,
directives: None,
selection_set: None,
})),
]),
}))
]
}))
])
}
#[test]
fn errors() {
assert_eq!(
parse_document_error("{"),
Spanning::zero_width(
&SourcePosition::new(1, 0, 1),
ParseError::UnexpectedEndOfFile));
assert_eq!(
parse_document_error("{ ...MissingOn }\nfragment MissingOn Type"),
Spanning::start_end(
&SourcePosition::new(36, 1, 19),
&SourcePosition::new(40, 1, 23),
ParseError::UnexpectedToken(Token::Name("Type"))));
assert_eq!(
parse_document_error("{ ...on }"),
Spanning::start_end(
&SourcePosition::new(8, 0, 8),
&SourcePosition::new(9, 0, 9),
ParseError::UnexpectedToken(Token::CurlyClose)));
}

536
src/parser/tests/lexer.rs Normal file
View file

@ -0,0 +1,536 @@
use parser::{Lexer, SourcePosition, Spanning, Token, LexerError};
fn tokenize_to_vec<'a>(s: &'a str) -> Vec<Spanning<Token<'a>>> {
let mut tokens = Vec::new();
let mut lexer = Lexer::new(s);
loop {
match lexer.next() {
Some(Ok(t)) => {
let at_eof = t.item == Token::EndOfFile;
tokens.push(t);
if at_eof {
break;
}
},
Some(Err(e)) => panic!("Error in input stream: {:#?} for {:#?}", e, s),
None => panic!("EOF before EndOfFile token in {:#?}", s),
}
}
tokens
}
fn tokenize_single<'a>(s: &'a str) -> Spanning<Token<'a>> {
let mut tokens = tokenize_to_vec(s);
assert_eq!(tokens.len(), 2);
assert_eq!(tokens[1].item, Token::EndOfFile);
tokens.remove(0)
}
fn tokenize_error(s: &str) -> Spanning<LexerError> {
let mut lexer = Lexer::new(s);
loop {
match lexer.next() {
Some(Ok(t)) => {
if t.item == Token::EndOfFile {
panic!("Tokenizer did not return error for {:#?}", s);
}
},
Some(Err(e)) => {
return e;
},
None => panic!("Tokenizer did not return error for {:#?}", s),
}
}
}
#[test]
fn empty_source() {
assert_eq!(
tokenize_to_vec(""),
vec![
Spanning::zero_width(&SourcePosition::new_origin(), Token::EndOfFile),
]);
}
#[test]
fn disallow_control_codes() {
assert_eq!(
Lexer::new("\u{0007}").next(),
Some(Err(Spanning::zero_width(
&SourcePosition::new_origin(),
LexerError::UnknownCharacter('\u{0007}')))));
}
#[test]
fn skip_whitespace() {
assert_eq!(
tokenize_to_vec(r#"
foo
"#),
vec![
Spanning::start_end(
&SourcePosition::new(14, 2, 12),
&SourcePosition::new(17, 2, 15),
Token::Name("foo"),
),
Spanning::zero_width(
&SourcePosition::new(31, 4, 12),
Token::EndOfFile),
]);
}
#[test]
fn skip_comments() {
assert_eq!(
tokenize_to_vec(r#"
#comment
foo#comment
"#),
vec![
Spanning::start_end(
&SourcePosition::new(34, 2, 12),
&SourcePosition::new(37, 2, 15),
Token::Name("foo"),
),
Spanning::zero_width(
&SourcePosition::new(58, 3, 12),
Token::EndOfFile),
]);
}
#[test]
fn skip_commas() {
assert_eq!(
tokenize_to_vec(r#",,,foo,,,"#),
vec![
Spanning::start_end(
&SourcePosition::new(3, 0, 3),
&SourcePosition::new(6, 0, 6),
Token::Name("foo"),
),
Spanning::zero_width(
&SourcePosition::new(9, 0, 9),
Token::EndOfFile),
]);
}
#[test]
fn error_positions() {
assert_eq!(
Lexer::new(r#"
?
"#).next(),
Some(Err(Spanning::zero_width(
&SourcePosition::new(14, 2, 12),
LexerError::UnknownCharacter('?')))));
}
#[test]
fn strings() {
assert_eq!(
tokenize_single(r#""simple""#),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(8, 0, 8),
Token::String("simple".to_owned())));
assert_eq!(
tokenize_single(r#"" white space ""#),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(15, 0, 15),
Token::String(" white space ".to_owned())));
assert_eq!(
tokenize_single(r#""quote \"""#),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(10, 0, 10),
Token::String("quote \"".to_owned())));
assert_eq!(
tokenize_single(r#""escaped \n\r\b\t\f""#),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(20, 0, 20),
Token::String("escaped \n\r\u{0008}\t\u{000c}".to_owned())));
assert_eq!(
tokenize_single(r#""slashes \\ \/""#),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(15, 0, 15),
Token::String("slashes \\ /".to_owned())));
assert_eq!(
tokenize_single(r#""unicode \u1234\u5678\u90AB\uCDEF""#),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(34, 0, 34),
Token::String("unicode \u{1234}\u{5678}\u{90ab}\u{cdef}".to_owned())));
}
#[test]
fn string_errors() {
assert_eq!(
tokenize_error("\""),
Spanning::zero_width(
&SourcePosition::new(1, 0, 1),
LexerError::UnterminatedString));
assert_eq!(
tokenize_error("\"no end quote"),
Spanning::zero_width(
&SourcePosition::new(13, 0, 13),
LexerError::UnterminatedString));
assert_eq!(
tokenize_error("\"contains unescaped \u{0007} control char\""),
Spanning::zero_width(
&SourcePosition::new(20, 0, 20),
LexerError::UnknownCharacterInString('\u{0007}')));
assert_eq!(
tokenize_error("\"null-byte is not \u{0000} end of file\""),
Spanning::zero_width(
&SourcePosition::new(18, 0, 18),
LexerError::UnknownCharacterInString('\u{0000}')));
assert_eq!(
tokenize_error("\"multi\nline\""),
Spanning::zero_width(
&SourcePosition::new(6, 0, 6),
LexerError::UnterminatedString));
assert_eq!(
tokenize_error("\"multi\rline\""),
Spanning::zero_width(
&SourcePosition::new(6, 0, 6),
LexerError::UnterminatedString));
assert_eq!(
tokenize_error(r#""bad \z esc""#),
Spanning::zero_width(
&SourcePosition::new(6, 0, 6),
LexerError::UnknownEscapeSequence("\\z".to_owned())));
assert_eq!(
tokenize_error(r#""bad \x esc""#),
Spanning::zero_width(
&SourcePosition::new(6, 0, 6),
LexerError::UnknownEscapeSequence("\\x".to_owned())));
assert_eq!(
tokenize_error(r#""bad \u1 esc""#),
Spanning::zero_width(
&SourcePosition::new(6, 0, 6),
LexerError::UnknownEscapeSequence("\\u1".to_owned())));
assert_eq!(
tokenize_error(r#""bad \u0XX1 esc""#),
Spanning::zero_width(
&SourcePosition::new(6, 0, 6),
LexerError::UnknownEscapeSequence("\\u0XX1".to_owned())));
assert_eq!(
tokenize_error(r#""bad \uXXXX esc""#),
Spanning::zero_width(
&SourcePosition::new(6, 0, 6),
LexerError::UnknownEscapeSequence("\\uXXXX".to_owned())));
assert_eq!(
tokenize_error(r#""bad \uFXXX esc""#),
Spanning::zero_width(
&SourcePosition::new(6, 0, 6),
LexerError::UnknownEscapeSequence("\\uFXXX".to_owned())));
assert_eq!(
tokenize_error(r#""bad \uXXXF esc""#),
Spanning::zero_width(
&SourcePosition::new(6, 0, 6),
LexerError::UnknownEscapeSequence("\\uXXXF".to_owned())));
}
#[test]
fn numbers() {
assert_eq!(
tokenize_single("4"),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(1, 0, 1),
Token::Int(4)));
assert_eq!(
tokenize_single("4.123"),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(5, 0, 5),
Token::Float(4.123)));
assert_eq!(
tokenize_single("-4"),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(2, 0, 2),
Token::Int(-4)));
assert_eq!(
tokenize_single("9"),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(1, 0, 1),
Token::Int(9)));
assert_eq!(
tokenize_single("0"),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(1, 0, 1),
Token::Int(0)));
assert_eq!(
tokenize_single("-4.123"),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(6, 0, 6),
Token::Float(-4.123)));
assert_eq!(
tokenize_single("0.123"),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(5, 0, 5),
Token::Float(0.123)));
assert_eq!(
tokenize_single("123e4"),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(5, 0, 5),
Token::Float(123e4)));
assert_eq!(
tokenize_single("123E4"),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(5, 0, 5),
Token::Float(123e4)));
assert_eq!(
tokenize_single("123e-4"),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(6, 0, 6),
Token::Float(123e-4)));
assert_eq!(
tokenize_single("123e+4"),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(6, 0, 6),
Token::Float(123e4)));
assert_eq!(
tokenize_single("-1.123e4"),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(8, 0, 8),
Token::Float(-1.123e4)));
assert_eq!(
tokenize_single("-1.123E4"),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(8, 0, 8),
Token::Float(-1.123e4)));
assert_eq!(
tokenize_single("-1.123e-4"),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(9, 0, 9),
Token::Float(-1.123e-4)));
assert_eq!(
tokenize_single("-1.123e+4"),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(9, 0, 9),
Token::Float(-1.123e4)));
assert_eq!(
tokenize_single("-1.123e45"),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(9, 0, 9),
Token::Float(-1.123e45)));
}
#[test]
fn numbers_errors() {
assert_eq!(
tokenize_error("00"),
Spanning::zero_width(
&SourcePosition::new(1, 0, 1),
LexerError::UnexpectedCharacter('0')));
assert_eq!(
tokenize_error("+1"),
Spanning::zero_width(
&SourcePosition::new(0, 0, 0),
LexerError::UnknownCharacter('+')));
assert_eq!(
tokenize_error("1."),
Spanning::zero_width(
&SourcePosition::new(2, 0, 2),
LexerError::UnexpectedEndOfFile));
assert_eq!(
tokenize_error(".123"),
Spanning::zero_width(
&SourcePosition::new(0, 0, 0),
LexerError::UnexpectedCharacter('.')));
assert_eq!(
tokenize_error("1.A"),
Spanning::zero_width(
&SourcePosition::new(2, 0, 2),
LexerError::UnexpectedCharacter('A')));
assert_eq!(
tokenize_error("-A"),
Spanning::zero_width(
&SourcePosition::new(1, 0, 1),
LexerError::UnexpectedCharacter('A')));
assert_eq!(
tokenize_error("1.0e"),
Spanning::zero_width(
&SourcePosition::new(4, 0, 4),
LexerError::UnexpectedEndOfFile));
assert_eq!(
tokenize_error("1.0eA"),
Spanning::zero_width(
&SourcePosition::new(4, 0, 4),
LexerError::UnexpectedCharacter('A')));
}
#[test]
fn punctuation() {
assert_eq!(
tokenize_single("!"),
Spanning::single_width(
&SourcePosition::new(0, 0, 0),
Token::ExclamationMark));
assert_eq!(
tokenize_single("$"),
Spanning::single_width(
&SourcePosition::new(0, 0, 0),
Token::Dollar));
assert_eq!(
tokenize_single("("),
Spanning::single_width(
&SourcePosition::new(0, 0, 0),
Token::ParenOpen));
assert_eq!(
tokenize_single(")"),
Spanning::single_width(
&SourcePosition::new(0, 0, 0),
Token::ParenClose));
assert_eq!(
tokenize_single("..."),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(3, 0, 3),
Token::Ellipsis));
assert_eq!(
tokenize_single(":"),
Spanning::single_width(
&SourcePosition::new(0, 0, 0),
Token::Colon));
assert_eq!(
tokenize_single("="),
Spanning::single_width(
&SourcePosition::new(0, 0, 0),
Token::Equals));
assert_eq!(
tokenize_single("@"),
Spanning::single_width(
&SourcePosition::new(0, 0, 0),
Token::At));
assert_eq!(
tokenize_single("["),
Spanning::single_width(
&SourcePosition::new(0, 0, 0),
Token::BracketOpen));
assert_eq!(
tokenize_single("]"),
Spanning::single_width(
&SourcePosition::new(0, 0, 0),
Token::BracketClose));
assert_eq!(
tokenize_single("{"),
Spanning::single_width(
&SourcePosition::new(0, 0, 0),
Token::CurlyOpen));
assert_eq!(
tokenize_single("}"),
Spanning::single_width(
&SourcePosition::new(0, 0, 0),
Token::CurlyClose));
assert_eq!(
tokenize_single("|"),
Spanning::single_width(
&SourcePosition::new(0, 0, 0),
Token::Pipe));
}
#[test]
fn punctuation_error() {
assert_eq!(
tokenize_error(".."),
Spanning::zero_width(
&SourcePosition::new(2, 0, 2),
LexerError::UnexpectedEndOfFile));
assert_eq!(
tokenize_error("?"),
Spanning::zero_width(
&SourcePosition::new(0, 0, 0),
LexerError::UnknownCharacter('?')));
assert_eq!(
tokenize_error("\u{203b}"),
Spanning::zero_width(
&SourcePosition::new(0, 0, 0),
LexerError::UnknownCharacter('\u{203b}')));
assert_eq!(
tokenize_error("\u{200b}"),
Spanning::zero_width(
&SourcePosition::new(0, 0, 0),
LexerError::UnknownCharacter('\u{200b}')));
}

3
src/parser/tests/mod.rs Normal file
View file

@ -0,0 +1,3 @@
mod document;
mod lexer;
mod value;

134
src/parser/tests/value.rs Normal file
View file

@ -0,0 +1,134 @@
use std::collections::HashMap;
use ast::InputValue;
use parser::{Lexer, Spanning, SourcePosition, Parser};
use parser::value::parse_value_literal;
fn parse_value(s: &str) -> Spanning<InputValue> {
let mut lexer = Lexer::new(s);
let mut parser = Parser::new(&mut lexer)
.expect(&format!("Lexer error on input {:#?}", s));
parse_value_literal(&mut parser, false)
.expect(&format!("Parse error on input {:#?}", s))
}
#[test]
fn input_value_literals() {
assert_eq!(
parse_value("123"),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(3, 0, 3),
InputValue::int(123)));
assert_eq!(
parse_value("123.45"),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(6, 0, 6),
InputValue::float(123.45)));
assert_eq!(
parse_value("true"),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(4, 0, 4),
InputValue::boolean(true)));
assert_eq!(
parse_value("false"),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(5, 0, 5),
InputValue::boolean(false)));
assert_eq!(
parse_value(r#""test""#),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(6, 0, 6),
InputValue::string("test")));
assert_eq!(
parse_value("enum_value"),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(10, 0, 10),
InputValue::enum_value("enum_value")));
assert_eq!(
parse_value("$variable"),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(9, 0, 9),
InputValue::variable("variable")));
assert_eq!(
parse_value("[]"),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(2, 0, 2),
InputValue::list(vec![])));
assert_eq!(
parse_value("[1, [2, 3]]"),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(11, 0, 11),
InputValue::parsed_list(vec![
Spanning::start_end(
&SourcePosition::new(1, 0, 1),
&SourcePosition::new(2, 0, 2),
InputValue::int(1)),
Spanning::start_end(
&SourcePosition::new(4, 0, 4),
&SourcePosition::new(10, 0, 10),
InputValue::parsed_list(vec![
Spanning::start_end(
&SourcePosition::new(5, 0, 5),
&SourcePosition::new(6, 0, 6),
InputValue::int(2)),
Spanning::start_end(
&SourcePosition::new(8, 0, 8),
&SourcePosition::new(9, 0, 9),
InputValue::int(3)),
])),
])));
assert_eq!(
parse_value("{}"),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(2, 0, 2),
InputValue::object(HashMap::<String, InputValue>::new())));
assert_eq!(
parse_value(r#"{key: 123, other: {foo: "bar"}}"#),
Spanning::start_end(
&SourcePosition::new(0, 0, 0),
&SourcePosition::new(31, 0, 31),
InputValue::parsed_object(vec![
(
Spanning::start_end(
&SourcePosition::new(1, 0, 1),
&SourcePosition::new(4, 0, 4),
"key".to_owned()),
Spanning::start_end(
&SourcePosition::new(6, 0, 6),
&SourcePosition::new(9, 0, 9),
InputValue::int(123))
),
(
Spanning::start_end(
&SourcePosition::new(11, 0, 11),
&SourcePosition::new(16, 0, 16),
"other".to_owned()),
Spanning::start_end(
&SourcePosition::new(18, 0, 18),
&SourcePosition::new(30, 0, 30),
InputValue::parsed_object(vec![
(
Spanning::start_end(
&SourcePosition::new(19, 0, 19),
&SourcePosition::new(22, 0, 22),
"foo".to_owned()),
Spanning::start_end(
&SourcePosition::new(24, 0, 24),
&SourcePosition::new(29, 0, 29),
InputValue::string("bar"))
)
]))
)
])));
}

177
src/parser/utils.rs Normal file
View file

@ -0,0 +1,177 @@
use std::fmt;
use std::hash::{Hash, Hasher};
/// A reference to a line and column in an input source file
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SourcePosition {
index: usize,
line: usize,
col: usize,
}
/// Data structure used to wrap items with start and end markers in the input source
///
/// A "span" is a range of characters in the input source, starting at the
/// character pointed by the `start` field and ending just before the `end`
/// marker.
#[derive(Debug)]
pub struct Spanning<T: fmt::Debug> {
/// The wrapped item
pub item: T,
/// Start position of the item
pub start: SourcePosition,
/// End position of the item
///
/// This points to the first source position _after_ the wrapped item.
pub end: SourcePosition,
}
impl<T: fmt::Debug> Spanning<T> {
#[doc(hidden)]
pub fn zero_width(pos: &SourcePosition, item: T) -> Spanning<T> {
Spanning {
item: item,
start: pos.clone(),
end: pos.clone(),
}
}
#[doc(hidden)]
pub fn single_width(pos: &SourcePosition, item: T) -> Spanning<T> {
let mut end = pos.clone();
end.advance_col();
Spanning {
item: item,
start: pos.clone(),
end: end,
}
}
#[doc(hidden)]
pub fn start_end(start: &SourcePosition, end: &SourcePosition, item: T) -> Spanning<T> {
Spanning {
item: item,
start: start.clone(),
end: end.clone(),
}
}
#[doc(hidden)]
pub fn spanning(v: Vec<Spanning<T>>) -> Option<Spanning<Vec<Spanning<T>>>> {
if let (Some(start), Some(end)) = (v.first().map(|s| s.start.clone()), v.last().map(|s| s.end.clone())) {
Some(Spanning {
item: v,
start: start,
end: end,
})
}
else {
None
}
}
#[doc(hidden)]
pub fn unlocated(item: T) -> Spanning<T> {
Spanning {
item: item,
start: SourcePosition::new_origin(),
end: SourcePosition::new_origin(),
}
}
/// Modify the contents of the spanned item
pub fn map<O: fmt::Debug, F: Fn(T) -> O>(self, f: F) -> Spanning<O> {
Spanning {
item: f(self.item),
start: self.start.clone(),
end: self.end.clone(),
}
}
}
impl<T> Clone for Spanning<T> where T: Clone + fmt::Debug {
fn clone(&self) -> Self {
Spanning {
start: self.start.clone(),
end: self.end.clone(),
item: self.item.clone(),
}
}
}
impl<T> PartialEq for Spanning<T> where T: PartialEq + fmt::Debug {
fn eq(&self, other: &Self) -> bool {
self.start == other.start && self.end == other.end && self.item == other.item
}
}
impl<T> Eq for Spanning<T> where T: Eq + fmt::Debug {
}
impl<T> Hash for Spanning<T> where T: Hash + fmt::Debug {
fn hash<H: Hasher>(&self, state: &mut H) {
self.start.hash(state);
self.end.hash(state);
self.item.hash(state);
}
}
impl SourcePosition {
#[doc(hidden)]
pub fn new(index: usize, line: usize, col: usize) -> SourcePosition {
assert!(index >= line + col);
SourcePosition {
index: index,
line: line,
col: col,
}
}
#[doc(hidden)]
pub fn new_origin() -> SourcePosition {
SourcePosition {
index: 0,
line: 0,
col: 0,
}
}
#[doc(hidden)]
pub fn advance_col(&mut self) {
self.index += 1;
self.col += 1;
}
#[doc(hidden)]
pub fn advance_line(&mut self) {
self.index += 1;
self.line += 1;
self.col = 0;
}
/// The index of the character in the input source
///
/// Zero-based index. Take a substring of the original source starting at
/// this index to access the item pointed to by this `SourcePosition`.
pub fn index(&self) -> usize {
self.index
}
/// The line of the character in the input source
///
/// Zero-based index: the first line is line zero.
pub fn line(&self) -> usize {
self.line
}
/// The column of the character in the input source
///
/// Zero-based index: the first column is column zero.
pub fn column(&self) -> usize {
self.col
}
}

71
src/parser/value.rs Normal file
View file

@ -0,0 +1,71 @@
use ast::InputValue;
use parser::{Parser, ParseResult, ParseError, Token, Spanning};
pub fn parse_value_literal<'a>(parser: &mut Parser<'a>, is_const: bool) -> ParseResult<'a, InputValue> {
match *parser.peek() {
Spanning { item: Token::BracketOpen, .. } => parse_list_literal(parser, is_const),
Spanning { item: Token::CurlyOpen, .. } => parse_object_literal(parser, is_const),
Spanning { item: Token::Dollar, .. } if !is_const => parse_variable_literal(parser),
Spanning { item: Token::Int(i), .. } =>
Ok(parser.next().map(|_| InputValue::int(i))),
Spanning { item: Token::Float(f), .. } =>
Ok(parser.next().map(|_| InputValue::float(f))),
Spanning { item: Token::String(_), .. } =>
Ok(parser.next().map(|t|
if let Token::String(s) = t {
InputValue::string(s)
}
else {
panic!("Internal parser error");
})),
Spanning { item: Token::Name("true"), .. } =>
Ok(parser.next().map(|_| InputValue::boolean(true))),
Spanning { item: Token::Name("false"), .. } =>
Ok(parser.next().map(|_| InputValue::boolean(false))),
Spanning { item: Token::Name("null"), .. } =>
Err(parser.next().map(ParseError::UnexpectedToken)),
Spanning { item: Token::Name(name), .. } =>
Ok(parser.next().map(|_| InputValue::enum_value(name.to_owned()))),
_ => Err(parser.next().map(ParseError::UnexpectedToken)),
}
}
fn parse_list_literal<'a>(parser: &mut Parser<'a>, is_const: bool) -> ParseResult<'a, InputValue> {
Ok(try!(parser.delimited_list(
&Token::BracketOpen,
|p| parse_value_literal(p, is_const),
&Token::BracketClose
)).map(|items| InputValue::parsed_list(items)))
}
fn parse_object_literal<'a>(parser: &mut Parser<'a>, is_const: bool) -> ParseResult<'a, InputValue> {
Ok(try!(parser.delimited_list(
&Token::CurlyOpen,
|p| parse_object_field(p, is_const),
&Token::CurlyClose
)).map(|items| InputValue::parsed_object(items.into_iter().map(|s| s.item).collect())))
}
fn parse_object_field<'a>(parser: &mut Parser<'a>, is_const: bool) -> ParseResult<'a, (Spanning<String>, Spanning<InputValue>)> {
let key = try!(parser.expect_name());
try!(parser.expect(&Token::Colon));
let value = try!(parse_value_literal(parser, is_const));
Ok(Spanning::start_end(
&key.start.clone(),
&value.end.clone(),
(key, value)))
}
fn parse_variable_literal<'a>(parser: &mut Parser<'a>) -> ParseResult<'a, InputValue> {
let Spanning { start: start_pos, .. } = try!(parser.expect(&Token::Dollar));
let Spanning { item: name, end: end_pos, ..} = try!(parser.expect_name());
Ok(Spanning::start_end(
&start_pos,
&end_pos,
InputValue::variable(name)))
}

631
src/schema/meta.rs Normal file
View file

@ -0,0 +1,631 @@
//! Types used to describe a GraphQL schema
use std::fmt;
use ast::{InputValue, FromInputValue, Type};
use types::base::TypeKind;
/// Scalar type metadata
pub struct ScalarMeta {
#[doc(hidden)]
pub name: String,
#[doc(hidden)]
pub description: Option<String>,
#[doc(hidden)]
pub try_parse_fn: Box<Fn(&InputValue) -> bool + Send + Sync>,
}
/// List type metadata
#[derive(Debug)]
pub struct ListMeta {
#[doc(hidden)]
pub of_type: Type,
}
/// Nullable type metadata
#[derive(Debug)]
pub struct NullableMeta {
#[doc(hidden)]
pub of_type: Type,
}
/// Object type metadata
#[derive(Debug)]
pub struct ObjectMeta {
#[doc(hidden)]
pub name: String,
#[doc(hidden)]
pub description: Option<String>,
#[doc(hidden)]
pub fields: Vec<Field>,
#[doc(hidden)]
pub interface_names: Vec<String>,
}
/// Enum type metadata
pub struct EnumMeta {
#[doc(hidden)]
pub name: String,
#[doc(hidden)]
pub description: Option<String>,
#[doc(hidden)]
pub values: Vec<EnumValue>,
#[doc(hidden)]
pub try_parse_fn: Box<Fn(&InputValue) -> bool + Send + Sync>,
}
/// Interface type metadata
#[derive(Debug)]
pub struct InterfaceMeta {
#[doc(hidden)]
pub name: String,
#[doc(hidden)]
pub description: Option<String>,
#[doc(hidden)]
pub fields: Vec<Field>,
}
/// Union type metadata
#[derive(Debug)]
pub struct UnionMeta {
#[doc(hidden)]
pub name: String,
#[doc(hidden)]
pub description: Option<String>,
#[doc(hidden)]
pub of_type_names: Vec<String>,
}
/// Input object metadata
pub struct InputObjectMeta {
#[doc(hidden)]
pub name: String,
#[doc(hidden)]
pub description: Option<String>,
#[doc(hidden)]
pub input_fields: Vec<Argument>,
#[doc(hidden)]
pub try_parse_fn: Box<Fn(&InputValue) -> bool + Send + Sync>,
}
/// A placeholder for not-yet-registered types
///
/// After a type's `meta` method has been called but before it has returned, a placeholder type
/// is inserted into a registry to indicate existence.
#[derive(Debug)]
pub struct PlaceholderMeta {
#[doc(hidden)]
pub of_type: Type,
}
/// Generic type metadata
#[derive(Debug)]
pub enum MetaType {
#[doc(hidden)]
Scalar(ScalarMeta),
#[doc(hidden)]
List(ListMeta),
#[doc(hidden)]
Nullable(NullableMeta),
#[doc(hidden)]
Object(ObjectMeta),
#[doc(hidden)]
Enum(EnumMeta),
#[doc(hidden)]
Interface(InterfaceMeta),
#[doc(hidden)]
Union(UnionMeta),
#[doc(hidden)]
InputObject(InputObjectMeta),
#[doc(hidden)]
Placeholder(PlaceholderMeta),
}
/// Metadata for a field
#[derive(Debug, Clone)]
pub struct Field {
#[doc(hidden)]
pub name: String,
#[doc(hidden)]
pub description: Option<String>,
#[doc(hidden)]
pub arguments: Option<Vec<Argument>>,
#[doc(hidden)]
pub field_type: Type,
#[doc(hidden)]
pub deprecation_reason: Option<String>,
}
/// Metadata for an argument to a field
#[derive(Debug, Clone)]
pub struct Argument {
#[doc(hidden)]
pub name: String,
#[doc(hidden)]
pub description: Option<String>,
#[doc(hidden)]
pub arg_type: Type,
#[doc(hidden)]
pub default_value: Option<InputValue>,
}
/// Metadata for a single value in an enum
#[derive(Debug, Clone)]
pub struct EnumValue {
/// The name of the enum value
///
/// This is the string literal representation of the enum in responses.
pub name: String,
/// The optional description of the enum value.
///
/// Note: this is not the description of the enum itself; it's the
/// description of this enum _value_.
pub description: Option<String>,
/// The optional deprecation reason
///
/// If this is `Some`, the field will be considered `isDeprecated`.
pub deprecation_reason: Option<String>,
}
impl MetaType {
/// Access the name of the type, if applicable
///
/// Lists, non-null wrappers, and placeholders don't have names.
pub fn name(&self) -> Option<&str> {
match *self {
MetaType::Scalar(ScalarMeta { ref name, .. }) |
MetaType::Object(ObjectMeta { ref name, .. }) |
MetaType::Enum(EnumMeta { ref name, .. }) |
MetaType::Interface(InterfaceMeta { ref name, .. }) |
MetaType::Union(UnionMeta { ref name, .. }) |
MetaType::InputObject(InputObjectMeta { ref name, .. }) =>
Some(name),
_ => None,
}
}
/// Access the description of the type, if applicable
///
/// Lists, nullable wrappers, and placeholders don't have names.
pub fn description(&self) -> Option<&String> {
match *self {
MetaType::Scalar(ScalarMeta { ref description, .. }) |
MetaType::Object(ObjectMeta { ref description, .. }) |
MetaType::Enum(EnumMeta { ref description, .. }) |
MetaType::Interface(InterfaceMeta { ref description, .. }) |
MetaType::Union(UnionMeta { ref description, .. }) |
MetaType::InputObject(InputObjectMeta { ref description, .. }) =>
description.as_ref(),
_ => None,
}
}
/// Construct a `TypeKind` for a given type
///
/// # Panics
/// Panics if the type represents a placeholder or nullable type.
pub fn type_kind(&self) -> TypeKind {
match *self {
MetaType::Scalar(_) => TypeKind::Scalar,
MetaType::List(_) => TypeKind::List,
MetaType::Nullable(_) => panic!("Can't take type_kind of nullable meta type"),
MetaType::Object(_) => TypeKind::Object,
MetaType::Enum(_) => TypeKind::Enum,
MetaType::Interface(_) => TypeKind::Interface,
MetaType::Union(_) => TypeKind::Union,
MetaType::InputObject(_) => TypeKind::InputObject,
MetaType::Placeholder(_) => panic!("Can't take type_kind of placeholder meta type"),
}
}
/// Access a field's meta data given its name
///
/// Only objects and interfaces have fields. This method always returns `None` for other types.
pub fn field_by_name(&self, name: &str) -> Option<&Field> {
match *self {
MetaType::Object(ObjectMeta { ref fields, .. }) |
MetaType::Interface(InterfaceMeta { ref fields, .. }) =>
fields.iter().filter(|f| f.name == name).next(),
_ => None,
}
}
/// Access an input field's meta data given its name
///
/// Only input objects have input fields. This method always returns `None` for other types.
pub fn input_field_by_name(&self, name: &str) -> Option<&Argument> {
match *self {
MetaType::InputObject(InputObjectMeta { ref input_fields, .. }) =>
input_fields.iter().filter(|f| f.name == name).next(),
_ => None,
}
}
/// Construct a `Type` literal instance based on the metadata
pub fn as_type(&self) -> Type {
match *self {
MetaType::Scalar(ScalarMeta { ref name, .. }) |
MetaType::Object(ObjectMeta { ref name, .. }) |
MetaType::Enum(EnumMeta { ref name, .. }) |
MetaType::Interface(InterfaceMeta { ref name, .. }) |
MetaType::Union(UnionMeta { ref name, .. }) |
MetaType::InputObject(InputObjectMeta { ref name, .. }) =>
Type::NonNullNamed(name.to_owned()),
MetaType::List(ListMeta { ref of_type }) =>
Type::NonNullList(Box::new(of_type.clone())),
MetaType::Nullable(NullableMeta { ref of_type }) =>
match *of_type {
Type::NonNullNamed(ref inner) => Type::Named(inner.to_owned()),
Type::NonNullList(ref inner) => Type::List(inner.clone()),
ref t => t.clone(),
},
MetaType::Placeholder(PlaceholderMeta { ref of_type }) => of_type.clone(),
}
}
/// Access the input value parse function, if applicable
///
/// An input value parse function is a function that takes an `InputValue` instance and returns
/// `true` if it can be parsed as the provided type.
///
/// Only scalars, enums, and input objects have parse functions.
pub fn input_value_parse_fn(&self) -> Option<&Box<Fn(&InputValue) -> bool + Send + Sync>> {
match *self {
MetaType::Scalar(ScalarMeta { ref try_parse_fn, .. }) |
MetaType::Enum(EnumMeta { ref try_parse_fn, .. }) |
MetaType::InputObject(InputObjectMeta { ref try_parse_fn, .. }) =>
Some(try_parse_fn),
_ => None,
}
}
/// Returns true if the type is a composite type
///
/// Objects, interfaces, and unions are composite.
pub fn is_composite(&self) -> bool {
match *self {
MetaType::Object(_) |
MetaType::Interface(_) |
MetaType::Union(_) => true,
_ => false,
}
}
/// Returns true if the type can occur in leaf positions in queries
///
/// Only enums and scalars are leaf types.
pub fn is_leaf(&self) -> bool {
match *self {
MetaType::Enum(_) |
MetaType::Scalar(_) => true,
_ => false,
}
}
/// Returns true if the type is abstract
///
/// Only interfaces and unions are abstract types.
pub fn is_abstract(&self) -> bool {
match *self {
MetaType::Interface(_) |
MetaType::Union(_) => true,
_ => false,
}
}
/// Returns true if the type can be used in input positions, e.g. arguments or variables
///
/// Only scalars, enums, and input objects are input types.
pub fn is_input(&self) -> bool {
match *self {
MetaType::Scalar(_) |
MetaType::Enum(_) |
MetaType::InputObject(_) => true,
_ => false,
}
}
}
impl ScalarMeta {
/// Build a new scalar type metadata with the specified name
pub fn new<T: FromInputValue>(name: &str) -> ScalarMeta {
ScalarMeta {
name: name.to_owned(),
description: None,
try_parse_fn: Box::new(
|v: &InputValue| <T as FromInputValue>::from(v).is_some()),
}
}
/// Set the description for the given scalar type
///
/// If a description already was set prior to calling this method, it will be overwritten.
pub fn description(mut self, description: &str) -> ScalarMeta {
self.description = Some(description.to_owned());
self
}
/// Wrap the scalar in a generic meta type
pub fn into_meta(self) -> MetaType {
MetaType::Scalar(self)
}
}
impl ListMeta {
/// Build a new list type by wrapping the specified type
pub fn new(of_type: Type) -> ListMeta {
ListMeta {
of_type: of_type,
}
}
/// Wrap the list in a generic meta type
pub fn into_meta(self) -> MetaType {
MetaType::List(self)
}
}
impl NullableMeta {
/// Build a new nullable type by wrapping the specified type
pub fn new(of_type: Type) -> NullableMeta {
NullableMeta {
of_type: of_type,
}
}
/// Wrap the nullable type in a generic meta type
pub fn into_meta(self) -> MetaType {
MetaType::Nullable(self)
}
}
impl ObjectMeta {
/// Build a new object type with the specified name and fields
pub fn new(name: &str, fields: &[Field]) -> ObjectMeta {
ObjectMeta {
name: name.to_owned(),
description: None,
fields: fields.to_vec(),
interface_names: vec![],
}
}
/// Set the description for the object
///
/// If a description was provided prior to calling this method, it will be overwritten.
pub fn description(mut self, description: &str) -> ObjectMeta {
self.description = Some(description.to_owned());
self
}
/// Set the interfaces this type implements
///
/// If a list of interfaces already was provided prior to calling this method, they will be
/// overwritten.
pub fn interfaces(mut self, interfaces: &[Type]) -> ObjectMeta {
self.interface_names = interfaces.iter()
.map(|t| t.innermost_name().to_owned()).collect();
self
}
/// Wrap this object type in a generic meta type
pub fn into_meta(self) -> MetaType {
MetaType::Object(self)
}
}
impl EnumMeta {
/// Build a new enum type with the specified name and possible values
pub fn new<T: FromInputValue>(name: &str, values: &[EnumValue]) -> EnumMeta {
EnumMeta {
name: name.to_owned(),
description: None,
values: values.to_vec(),
try_parse_fn: Box::new(
|v: &InputValue| <T as FromInputValue>::from(v).is_some()),
}
}
/// Set the description of the type
///
/// If a description was provided prior to calling this method, it will be overwritten
pub fn description(mut self, description: &str) -> EnumMeta {
self.description = Some(description.to_owned());
self
}
/// Wrap this enum type in a generic meta type
pub fn into_meta(self) -> MetaType {
MetaType::Enum(self)
}
}
impl InterfaceMeta {
/// Build a new interface type with the specified name and fields
pub fn new(name: &str, fields: &[Field]) -> InterfaceMeta {
InterfaceMeta {
name: name.to_owned(),
description: None,
fields: fields.to_vec(),
}
}
/// Set the description of the type
///
/// If a description was provided prior to calling this method, it will be overwritten.
pub fn description(mut self, description: &str) -> InterfaceMeta {
self.description = Some(description.to_owned());
self
}
/// Wrap this interface type in a generic meta type
pub fn into_meta(self) -> MetaType {
MetaType::Interface(self)
}
}
impl UnionMeta {
/// Build a new union type with the specified name and possible types
pub fn new(name: &str, of_types: &[Type]) -> UnionMeta {
UnionMeta {
name: name.to_owned(),
description: None,
of_type_names: of_types.iter()
.map(|t| t.innermost_name().to_owned()).collect(),
}
}
/// Set the description of the type
///
/// If a description was provided prior to calling this method, it will be overwritten.
pub fn description(mut self, description: &str) -> UnionMeta {
self.description = Some(description.to_owned());
self
}
/// Wrap this union type in a generic meta type
pub fn into_meta(self) -> MetaType {
MetaType::Union(self)
}
}
impl InputObjectMeta {
/// Build a new input type with the specified name and input fields
pub fn new<T: FromInputValue>(name: &str, input_fields: &[Argument]) -> InputObjectMeta {
InputObjectMeta {
name: name.to_owned(),
description: None,
input_fields: input_fields.to_vec(),
try_parse_fn: Box::new(
|v: &InputValue| <T as FromInputValue>::from(v).is_some()),
}
}
/// Set the description of the type
///
/// If a description was provided prior to calling this method, it will be overwritten.
pub fn description(mut self, description: &str) -> InputObjectMeta {
self.description = Some(description.to_owned());
self
}
/// Wrap this union type in a generic meta type
pub fn into_meta(self) -> MetaType {
MetaType::InputObject(self)
}
}
impl Field {
/// Set the description of the field
///
/// This overwrites the description if any was previously set.
pub fn description(mut self, description: &str) -> Field {
self.description = Some(description.to_owned());
self
}
/// Add an argument to the field
///
/// Arguments are unordered and can't contain duplicates by name.
pub fn argument(mut self, argument: Argument) -> Field {
match self.arguments {
None => { self.arguments = Some(vec![argument]); }
Some(ref mut args) => { args.push(argument); }
};
self
}
/// Set the deprecation reason
///
/// This overwrites the deprecation reason if any was previously set.
pub fn deprecated(mut self, reason: &str) -> Field {
self.deprecation_reason = Some(reason.to_owned());
self
}
}
impl Argument {
#[doc(hidden)]
pub fn new(name: &str, arg_type: Type) -> Argument {
Argument {
name: name.to_owned(),
description: None,
arg_type: arg_type,
default_value: None
}
}
/// Set the description of the argument
///
/// This overwrites the description if any was previously set.
pub fn description(mut self, description: &str) -> Argument {
self.description = Some(description.to_owned());
self
}
/// Set the default value of the argument
///
/// This overwrites the description if any was previously set.
pub fn default_value(mut self, default_value: InputValue) -> Argument {
self.default_value = Some(default_value);
self
}
}
impl EnumValue {
/// Construct a new enum value with the provided name
pub fn new(name: &str) -> EnumValue {
EnumValue {
name: name.to_owned(),
description: None,
deprecation_reason: None,
}
}
/// Set the description of the enum value
///
/// This overwrites the description if any was previously set.
pub fn description(mut self, description: &str) -> EnumValue {
self.description = Some(description.to_owned());
self
}
/// Set the deprecation reason for the enum value
///
/// This overwrites the deprecation reason if any was previously set.
pub fn deprecated(mut self, reason: &str) -> EnumValue {
self.deprecation_reason = Some(reason.to_owned());
self
}
}
impl fmt::Debug for ScalarMeta {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.debug_struct("ScalarMeta")
.field("name", &self.name)
.field("description", &self.description)
.finish()
}
}
impl fmt::Debug for EnumMeta {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.debug_struct("EnumMeta")
.field("name", &self.name)
.field("description", &self.description)
.field("values", &self.values)
.finish()
}
}
impl fmt::Debug for InputObjectMeta {
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
fmt.debug_struct("InputObjectMeta")
.field("name", &self.name)
.field("description", &self.description)
.field("input_fields", &self.input_fields)
.finish()
}
}

3
src/schema/mod.rs Normal file
View file

@ -0,0 +1,3 @@
pub mod model;
pub mod schema;
pub mod meta;

340
src/schema/model.rs Normal file
View file

@ -0,0 +1,340 @@
use std::collections::HashMap;
use std::marker::PhantomData;
use std::fmt;
use types::base::{GraphQLType};
use types::schema::Registry;
use ast::Type;
use schema::meta::{MetaType, ObjectMeta, PlaceholderMeta, UnionMeta, InterfaceMeta, Argument};
/// Root query node of a schema
///
/// This brings the mutatino and query types together, and provides the
/// predefined metadata fields.
pub struct RootNode<InnerT, QueryT, MutationT=()> {
#[doc(hidden)]
pub query_type: QueryT,
#[doc(hidden)]
pub mutation_type: MutationT,
#[doc(hidden)]
pub schema: SchemaType,
phantom_wrapped: PhantomData<InnerT>,
}
/// Metadata for a schema
pub struct SchemaType {
types: HashMap<String, MetaType>,
query_type_name: String,
mutation_type_name: Option<String>,
directives: HashMap<String, DirectiveType>,
}
pub enum TypeType<'a> {
Concrete(&'a MetaType),
NonNull(Box<TypeType<'a>>),
List(Box<TypeType<'a>>),
}
pub struct DirectiveType {
pub name: String,
pub description: Option<String>,
pub locations: Vec<DirectiveLocation>,
pub arguments: Vec<Argument>,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum DirectiveLocation {
Query,
Mutation,
Field,
FragmentDefinition,
FragmentSpread,
InlineFragment,
}
impl<InnerT, QueryT, MutationT> RootNode<InnerT, QueryT, MutationT>
where QueryT: GraphQLType<InnerT>,
MutationT: GraphQLType<InnerT>,
{
/// Construct a new root node from query and mutation nodes
///
/// If the schema should not support mutations, you can pass in `()` to
/// remove the mutation type from the schema.
pub fn new(query_obj: QueryT, mutation_obj: MutationT) -> RootNode<InnerT, QueryT, MutationT> {
RootNode {
query_type: query_obj,
mutation_type: mutation_obj,
schema: SchemaType::new::<InnerT, QueryT, MutationT>(),
phantom_wrapped: PhantomData,
}
}
}
impl SchemaType {
pub fn new<CtxT, QueryT, MutationT>() -> SchemaType
where QueryT: GraphQLType<CtxT>,
MutationT: GraphQLType<CtxT>,
{
let mut types = HashMap::new();
let mut directives = HashMap::new();
let query_type_name: String;
let mutation_type_name: String;
{
let mut registry = Registry::<CtxT>::new(types);
query_type_name = registry.get_type::<QueryT>().innermost_name().to_owned();
mutation_type_name = registry.get_type::<MutationT>().innermost_name().to_owned();
types = registry.types;
}
{
let mut registry = Registry::<SchemaType>::new(types);
registry.get_type::<SchemaType>();
directives.insert(
"skip".to_owned(),
DirectiveType::new_skip(&mut registry));
directives.insert(
"include".to_owned(),
DirectiveType::new_include(&mut registry));
let mut meta_fields = vec![
registry.field::<SchemaType>("__schema"),
registry.field::<TypeType>("__type")
.argument(registry.arg::<String>("name")),
];
if let Some(root_type) = registry.types.get_mut(&query_type_name) {
if let &mut MetaType::Object(ObjectMeta { ref mut fields, .. }) = root_type {
fields.append(&mut meta_fields);
}
else {
panic!("Root type is not an object");
}
}
else {
panic!("Root type not found");
}
types = registry.types;
}
for meta_type in types.values() {
if let MetaType::Placeholder(PlaceholderMeta { ref of_type }) = *meta_type {
panic!("Type {:?} is still a placeholder type", of_type);
}
}
SchemaType {
types: types,
query_type_name: query_type_name,
mutation_type_name: if &mutation_type_name != "__Unit" { Some(mutation_type_name) } else { None },
directives: directives,
}
}
pub fn add_directive(&mut self, directive: DirectiveType) {
self.directives.insert(directive.name.clone(), directive);
}
pub fn type_by_name(&self, name: &str) -> Option<TypeType> {
self.types.get(name).map(|t| TypeType::Concrete(t))
}
pub fn concrete_type_by_name(&self, name: &str) -> Option<&MetaType> {
self.types.get(name)
}
pub fn query_type(&self) -> TypeType {
TypeType::Concrete(
self.types.get(&self.query_type_name)
.expect("Query type does not exist in schema"))
}
pub fn concrete_query_type(&self) -> &MetaType {
self.types.get(&self.query_type_name)
.expect("Query type does not exist in schema")
}
pub fn mutation_type(&self) -> Option<TypeType> {
if let Some(ref mutation_type_name) = self.mutation_type_name {
Some(self.type_by_name(mutation_type_name)
.expect("Mutation type does not exist in schema"))
}
else {
None
}
}
pub fn concrete_mutation_type(&self) -> Option<&MetaType> {
self.mutation_type_name.as_ref().map(|name|
self.concrete_type_by_name(name)
.expect("Mutation type does not exist in schema"))
}
pub fn type_list(&self) -> Vec<TypeType> {
self.types.values().map(|t| TypeType::Concrete(t)).collect()
}
pub fn concrete_type_list(&self) -> Vec<&MetaType> {
self.types.values().collect()
}
pub fn make_type(&self, t: &Type) -> TypeType {
match *t {
Type::NonNullNamed(ref n) =>
TypeType::NonNull(Box::new(
self.type_by_name(n).expect("Type not found in schema"))),
Type::NonNullList(ref inner) =>
TypeType::NonNull(Box::new(
TypeType::List(Box::new(self.make_type(inner))))),
Type::Named(ref n) => self.type_by_name(n).expect("Type not found in schema"),
Type::List(ref inner) =>
TypeType::List(Box::new(self.make_type(inner))),
}
}
pub fn directive_list(&self) -> Vec<&DirectiveType> {
self.directives.values().collect()
}
pub fn directive_by_name(&self, name: &str) -> Option<&DirectiveType> {
self.directives.get(name)
}
pub fn type_overlap(&self, t1: &MetaType, t2: &MetaType) -> bool {
if (t1 as *const MetaType) == (t2 as *const MetaType) {
return true;
}
match (t1.is_abstract(), t2.is_abstract()) {
(true, true) => self.possible_types(t1).iter().any(|t| self.is_possible_type(t2, t)),
(true, false) => self.is_possible_type(t1, t2),
(false, true) => self.is_possible_type(t2, t1),
(false, false) => false,
}
}
pub fn possible_types(&self, t: &MetaType) -> Vec<&MetaType> {
match *t {
MetaType::Union(UnionMeta { ref of_type_names, .. }) =>
of_type_names
.iter()
.flat_map(|t| self.concrete_type_by_name(t))
.collect(),
MetaType::Interface(InterfaceMeta { ref name, .. }) =>
self.concrete_type_list()
.into_iter()
.filter(|t| match **t {
MetaType::Object(ObjectMeta { ref interface_names, .. }) =>
interface_names.iter().any(|iname| iname == name),
_ => false
})
.collect(),
_ => panic!("Can't retrieve possible types from non-abstract meta type")
}
}
pub fn is_possible_type(&self, abstract_type: &MetaType, possible_type: &MetaType) -> bool {
self.possible_types(abstract_type)
.into_iter()
.any(|t| (t as *const MetaType) == (possible_type as *const MetaType))
}
pub fn is_subtype(&self, sub_type: &Type, super_type: &Type) -> bool {
use ast::Type::*;
if super_type == sub_type {
return true;
}
match (super_type, sub_type) {
(&NonNullNamed(ref super_name), &NonNullNamed(ref sub_name)) |
(&Named(ref super_name), &Named(ref sub_name)) |
(&Named(ref super_name), &NonNullNamed(ref sub_name)) =>
self.is_named_subtype(sub_name, super_name),
(&NonNullList(ref super_inner), &NonNullList(ref sub_inner)) |
(&List(ref super_inner), &List(ref sub_inner)) |
(&List(ref super_inner), &NonNullList(ref sub_inner)) =>
self.is_subtype(sub_inner, super_inner),
_ => false
}
}
pub fn is_named_subtype(&self, sub_type_name: &str, super_type_name: &str) -> bool {
if sub_type_name == super_type_name {
true
}
else if let (Some(sub_type), Some(super_type))
= (self.concrete_type_by_name(sub_type_name), self.concrete_type_by_name(super_type_name))
{
super_type.is_abstract() && self.is_possible_type(super_type, sub_type)
}
else {
false
}
}
}
impl<'a> TypeType<'a> {
pub fn to_concrete(&self) -> Option<&'a MetaType> {
match *self {
TypeType::Concrete(t) => Some(t),
_ => None
}
}
}
impl DirectiveType {
pub fn new(name: &str, locations: &[DirectiveLocation], arguments: &[Argument]) -> DirectiveType {
DirectiveType {
name: name.to_owned(),
description: None,
locations: locations.to_vec(),
arguments: arguments.to_vec(),
}
}
fn new_skip<CtxT>(registry: &mut Registry<CtxT>) -> DirectiveType {
Self::new(
"skip",
&[
DirectiveLocation::Field,
DirectiveLocation::FragmentSpread,
DirectiveLocation::InlineFragment,
],
&[
registry.arg::<bool>("if"),
])
}
fn new_include<CtxT>(registry: &mut Registry<CtxT>) -> DirectiveType {
Self::new(
"include",
&[
DirectiveLocation::Field,
DirectiveLocation::FragmentSpread,
DirectiveLocation::InlineFragment,
],
&[
registry.arg::<bool>("if"),
])
}
pub fn description(mut self, description: &str) -> DirectiveType {
self.description = Some(description.to_owned());
self
}
}
impl fmt::Display for DirectiveLocation {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.write_str(match *self {
DirectiveLocation::Query => "query",
DirectiveLocation::Mutation => "mutation",
DirectiveLocation::Field => "field",
DirectiveLocation::FragmentDefinition => "fragment definition",
DirectiveLocation::FragmentSpread => "fragment spread",
DirectiveLocation::InlineFragment => "inline fragment",
})
}
}

251
src/schema/schema.rs Normal file
View file

@ -0,0 +1,251 @@
use rustc_serialize::json::ToJson;
use types::base::{GraphQLType, Arguments, TypeKind};
use types::schema::{Executor, Registry, FieldResult, ExecutionResult};
use schema::meta::{MetaType, ObjectMeta, EnumMeta, InputObjectMeta, UnionMeta, InterfaceMeta,
Field, Argument, EnumValue};
use schema::model::{RootNode, SchemaType, TypeType, DirectiveType, DirectiveLocation};
impl<CtxT, QueryT, MutationT> GraphQLType<CtxT> for RootNode<CtxT, QueryT, MutationT>
where QueryT: GraphQLType<CtxT>,
MutationT: GraphQLType<CtxT>
{
fn name() -> Option<&'static str> {
QueryT::name()
}
fn meta(registry: &mut Registry<CtxT>) -> MetaType {
QueryT::meta(registry)
}
fn resolve_field(&self, field: &str, args: &Arguments, executor: &mut Executor<CtxT>) -> ExecutionResult {
match field {
"__schema" => executor.replaced_context(&self.schema).resolve(&self.schema),
"__type" => {
let type_name: String = args.get("name").unwrap();
executor.replaced_context(&self.schema).resolve(&self.schema.type_by_name(&type_name))
},
_=> self.query_type.resolve_field(field, args, executor),
}
}
}
graphql_object!(SchemaType: SchemaType as "__Schema" |&self| {
field types() -> FieldResult<Vec<TypeType>> {
Ok(self.type_list())
}
field query_type() -> FieldResult<TypeType> {
Ok(self.query_type())
}
field mutation_type() -> FieldResult<Option<TypeType>> {
Ok(self.mutation_type())
}
field directives() -> FieldResult<Vec<&DirectiveType>> {
Ok(self.directive_list())
}
});
graphql_object!(<'a> TypeType<'a>: SchemaType as "__Type" |&self| {
field name() -> FieldResult<Option<&str>> {
Ok(match *self {
TypeType::Concrete(t) => t.name(),
_ => None,
})
}
field description() -> FieldResult<Option<&String>> {
Ok(match *self {
TypeType::Concrete(t) => t.description(),
_ => None,
})
}
field kind() -> FieldResult<TypeKind> {
Ok(match *self {
TypeType::Concrete(t) => t.type_kind(),
TypeType::List(_) => TypeKind::List,
TypeType::NonNull(_) => TypeKind::NonNull,
})
}
field fields(include_deprecated = false: bool) -> FieldResult<Option<Vec<&Field>>> {
Ok(match *self {
TypeType::Concrete(&MetaType::Interface(InterfaceMeta { ref fields, .. })) |
TypeType::Concrete(&MetaType::Object(ObjectMeta { ref fields, .. })) =>
Some(fields
.iter()
.filter(|f| include_deprecated || f.deprecation_reason.is_none())
.collect()),
_ => None,
})
}
field of_type() -> FieldResult<Option<&TypeType>> {
Ok(match *self {
TypeType::Concrete(_) => None,
TypeType::List(ref l) | TypeType::NonNull(ref l) => Some(l),
})
}
field input_fields() -> FieldResult<Option<&Vec<Argument>>> {
Ok(match *self {
TypeType::Concrete(&MetaType::InputObject(InputObjectMeta { ref input_fields, .. })) =>
Some(input_fields),
_ => None,
})
}
field interfaces(&mut executor) -> FieldResult<Option<Vec<TypeType>>> {
Ok(match *self {
TypeType::Concrete(&MetaType::Object(ObjectMeta { ref interface_names, .. })) => {
let schema = executor.context();
Some(interface_names
.iter()
.filter_map(|n| schema.type_by_name(n))
.collect())
}
_ => None,
})
}
field possible_types(&mut executor) -> FieldResult<Option<Vec<TypeType>>> {
let schema = executor.context();
Ok(match *self {
TypeType::Concrete(&MetaType::Union(UnionMeta { ref of_type_names, .. })) => {
Some(of_type_names
.iter()
.filter_map(|tn| schema.type_by_name(tn))
.collect())
}
TypeType::Concrete(&MetaType::Interface(InterfaceMeta { name: ref iface_name, .. })) => {
Some(schema.concrete_type_list()
.iter()
.filter_map(|&ct|
if let &MetaType::Object(ObjectMeta { ref name, ref interface_names, .. }) = ct {
if interface_names.contains(iface_name) {
schema.type_by_name(name)
} else { None }
} else { None }
)
.collect())
}
_ => None,
})
}
field enum_values(include_deprecated = false: bool) -> FieldResult<Option<Vec<&EnumValue>>> {
Ok(match *self {
TypeType::Concrete(&MetaType::Enum(EnumMeta { ref values, .. })) =>
Some(values
.iter()
.filter(|f| include_deprecated || f.deprecation_reason.is_none())
.collect()),
_ => None,
})
}
});
graphql_object!(Field: SchemaType as "__Field" |&self| {
field name() -> FieldResult<&String> {
Ok(&self.name)
}
field description() -> FieldResult<&Option<String>> {
Ok(&self.description)
}
field args() -> FieldResult<Vec<&Argument>> {
Ok(self.arguments.as_ref().map_or_else(|| Vec::new(), |v| v.iter().collect()))
}
field type(&mut executor) -> FieldResult<TypeType> {
Ok(executor.context().make_type(&self.field_type))
}
field is_deprecated() -> FieldResult<bool> {
Ok(self.deprecation_reason.is_some())
}
field deprecation_reason() -> FieldResult<&Option<String>> {
Ok(&self.deprecation_reason)
}
});
graphql_object!(Argument: SchemaType as "__InputValue" |&self| {
field name() -> FieldResult<&String> {
Ok(&self.name)
}
field description() -> FieldResult<&Option<String>> {
Ok(&self.description)
}
field type(&mut executor) -> FieldResult<TypeType> {
Ok(executor.context().make_type(&self.arg_type))
}
field default_value() -> FieldResult<Option<String>> {
Ok(self.default_value.as_ref().map(|v| v.to_json().to_string()))
}
});
graphql_object!(EnumValue: SchemaType as "__EnumValue" |&self| {
field name() -> FieldResult<&String> {
Ok(&self.name)
}
field description() -> FieldResult<&Option<String>> {
Ok(&self.description)
}
field is_deprecated() -> FieldResult<bool> {
Ok(self.deprecation_reason.is_some())
}
field deprecation_reason() -> FieldResult<&Option<String>> {
Ok(&self.deprecation_reason)
}
});
graphql_enum!(TypeKind as "__TypeKind" {
TypeKind::Scalar => "SCALAR",
TypeKind::Object => "OBJECT",
TypeKind::Interface => "INTERFACE",
TypeKind::Union => "UNION",
TypeKind::Enum => "ENUM",
TypeKind::InputObject => "INPUT_OBJECT",
TypeKind::List => "LIST",
TypeKind::NonNull => "NON_NULL",
});
graphql_object!(DirectiveType: SchemaType as "__Directive" |&self| {
field name() -> FieldResult<&String> {
Ok(&self.name)
}
field description() -> FieldResult<&Option<String>> {
Ok(&self.description)
}
field locations() -> FieldResult<&Vec<DirectiveLocation>> {
Ok(&self.locations)
}
field args() -> FieldResult<&Vec<Argument>> {
Ok(&self.arguments)
}
});
graphql_enum!(DirectiveLocation as "__DirectiveLocation" {
DirectiveLocation::Query => "QUERY",
DirectiveLocation::Mutation => "MUTATION",
DirectiveLocation::Field => "FIELD",
DirectiveLocation::FragmentDefinition => "FRAGMENT_DEFINITION",
DirectiveLocation::FragmentSpread => "FRAGMENT_SPREAD",
DirectiveLocation::InlineFragment => "INLINE_FRAGMENT",
});

125
src/tests/bench.rs Normal file
View file

@ -0,0 +1,125 @@
use test::Bencher;
use std::collections::{HashMap};
use schema::model::RootNode;
use tests::model::Database;
#[bench]
fn query_type_name(b: &mut Bencher) {
let database = Database::new();
let schema = RootNode::new(&database, ());
let doc = r#"
query IntrospectionQueryTypeQuery {
__schema {
queryType {
name
}
}
}"#;
b.iter(|| ::execute(doc, None, &schema, &HashMap::new(), &database));
}
#[bench]
fn introspection_query(b: &mut Bencher) {
let database = Database::new();
let schema = RootNode::new(&database, ());
let doc = r#"
query IntrospectionQuery {
__schema {
queryType { name }
mutationType { name }
subscriptionType { name }
types {
...FullType
}
directives {
name
description
locations
args {
...InputValue
}
}
}
}
fragment FullType on __Type {
kind
name
description
fields(includeDeprecated: true) {
name
description
args {
...InputValue
}
type {
...TypeRef
}
isDeprecated
deprecationReason
}
inputFields {
...InputValue
}
interfaces {
...TypeRef
}
enumValues(includeDeprecated: true) {
name
description
isDeprecated
deprecationReason
}
possibleTypes {
...TypeRef
}
}
fragment InputValue on __InputValue {
name
description
type { ...TypeRef }
defaultValue
}
fragment TypeRef on __Type {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
ofType {
kind
name
}
}
}
}
}
}
}
}
"#;
b.iter(|| ::execute(doc, None, &schema, &HashMap::new(), &database));
}

View file

@ -0,0 +1,166 @@
use std::collections::{HashMap, HashSet};
use value::Value;
use schema::model::RootNode;
use tests::model::Database;
#[test]
fn test_query_type_name() {
let doc = r#"
query IntrospectionQueryTypeQuery {
__schema {
queryType {
name
}
}
}"#;
let database = Database::new();
let schema = RootNode::new(&database, ());
assert_eq!(
::execute(doc, None, &schema, &HashMap::new(), &database),
Ok((Value::object(vec![
("__schema", Value::object(vec![
("queryType", Value::object(vec![
("name", Value::string("Query")),
].into_iter().collect())),
].into_iter().collect())),
].into_iter().collect()),
vec![])));
}
#[test]
fn test_specific_type_name() {
let doc = r#"
query IntrospectionQueryTypeQuery {
__type(name: "Droid") {
name
}
}"#;
let database = Database::new();
let schema = RootNode::new(&database, ());
assert_eq!(
::execute(doc, None, &schema, &HashMap::new(), &database),
Ok((Value::object(vec![
("__type", Value::object(vec![
("name", Value::string("Droid")),
].into_iter().collect())),
].into_iter().collect()),
vec![])));
}
#[test]
fn test_specific_object_type_name_and_kind() {
let doc = r#"
query IntrospectionDroidKindQuery {
__type(name: "Droid") {
name
kind
}
}
"#;
let database = Database::new();
let schema = RootNode::new(&database, ());
assert_eq!(
::execute(doc, None, &schema, &HashMap::new(), &database),
Ok((Value::object(vec![
("__type", Value::object(vec![
("name", Value::string("Droid")),
("kind", Value::string("OBJECT")),
].into_iter().collect())),
].into_iter().collect()),
vec![])));
}
#[test]
fn test_specific_interface_type_name_and_kind() {
let doc = r#"
query IntrospectionDroidKindQuery {
__type(name: "Character") {
name
kind
}
}
"#;
let database = Database::new();
let schema = RootNode::new(&database, ());
assert_eq!(
::execute(doc, None, &schema, &HashMap::new(), &database),
Ok((Value::object(vec![
("__type", Value::object(vec![
("name", Value::string("Character")),
("kind", Value::string("INTERFACE")),
].into_iter().collect())),
].into_iter().collect()),
vec![])));
}
#[test]
fn test_documentation() {
let doc = r#"
query IntrospectionDroidDescriptionQuery {
__type(name: "Droid") {
name
description
}
}
"#;
let database = Database::new();
let schema = RootNode::new(&database, ());
assert_eq!(
::execute(doc, None, &schema, &HashMap::new(), &database),
Ok((
Value::object(vec![
("__type", Value::object(vec![
("name", Value::string("Droid")),
("description", Value::string("A mechanical creature in the Star Wars universe.")),
].into_iter().collect())),
].into_iter().collect()),
vec![])));
}
#[test]
fn test_possible_types() {
let doc = r#"
query IntrospectionDroidDescriptionQuery {
__type(name: "Character") {
possibleTypes {
name
}
}
}
"#;
let database = Database::new();
let schema = RootNode::new(&database, ());
let result = ::execute(doc, None, &schema, &HashMap::new(), &database);
println!("Result: {:#?}", result);
let (result, errors) = result.ok().expect("Query returned error");
assert_eq!(errors, vec![]);
let possible_types = result
.as_object_value().expect("execution result not an object")
.get("__type").expect("'__type' not present in result")
.as_object_value().expect("'__type' not an object")
.get("possibleTypes").expect("'possibleTypes' not present in '__type'")
.as_list_value().expect("'possibleTypes' not a list")
.iter().map(|t| t
.as_object_value().expect("possible type not an object")
.get("name").expect("'name' not present in type")
.as_string_value().expect("'name' not a string"))
.collect::<HashSet<_>>();
assert_eq!(
possible_types,
vec![
"Human",
"Droid",
].into_iter().collect());
}

7
src/tests/mod.rs Normal file
View file

@ -0,0 +1,7 @@
pub mod model;
mod schema;
pub mod query_tests;
pub mod introspection_tests;
#[cfg(feature="nightly")]
pub mod bench;

223
src/tests/model.rs Normal file
View file

@ -0,0 +1,223 @@
use std::collections::HashMap;
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
pub enum Episode {
NewHope,
Empire,
Jedi,
}
pub trait Character {
fn id(&self) -> &str;
fn name(&self) -> &str;
fn friend_ids(&self) -> &[String];
fn appears_in(&self) -> &[Episode];
fn secret_backstory(&self) -> &Option<String>;
fn as_character(&self) -> &Character;
}
pub trait Human: Character {
fn home_planet(&self) -> &Option<String>;
}
pub trait Droid: Character {
fn primary_function(&self) -> &Option<String>;
}
struct HumanData {
id: String,
name: String,
friend_ids: Vec<String>,
appears_in: Vec<Episode>,
secret_backstory: Option<String>,
home_planet: Option<String>,
}
struct DroidData {
id: String,
name: String,
friend_ids: Vec<String>,
appears_in: Vec<Episode>,
secret_backstory: Option<String>,
primary_function: Option<String>,
}
impl Character for HumanData {
fn id(&self) -> &str { &self.id }
fn name(&self) -> &str { &self.name }
fn friend_ids(&self) -> &[String] { &self.friend_ids }
fn appears_in(&self) -> &[Episode] { &self.appears_in }
fn secret_backstory(&self) -> &Option<String> { &self.secret_backstory }
fn as_character(&self) -> &Character { self }
}
impl Human for HumanData {
fn home_planet(&self) -> &Option<String> { &self.home_planet }
}
impl Character for DroidData {
fn id(&self) -> &str { &self.id }
fn name(&self) -> &str { &self.name }
fn friend_ids(&self) -> &[String] { &self.friend_ids }
fn appears_in(&self) -> &[Episode] { &self.appears_in }
fn secret_backstory(&self) -> &Option<String> { &self.secret_backstory }
fn as_character(&self) -> &Character { self }
}
impl Droid for DroidData {
fn primary_function(&self) -> &Option<String> { &self.primary_function }
}
pub struct Database {
humans: HashMap<String, HumanData>,
droids: HashMap<String, DroidData>,
}
impl HumanData {
pub fn new(
id: &str,
name: &str,
friend_ids: &[&str],
appears_in: &[Episode],
secret_backstory: Option<&str>,
home_planet: Option<&str>) -> HumanData
{
HumanData {
id: id.to_owned(),
name: name.to_owned(),
friend_ids: friend_ids.to_owned().into_iter().map(|f| f.to_owned()).collect(),
appears_in: appears_in.iter().cloned().collect(),
secret_backstory: secret_backstory.map(|b| b.to_owned()),
home_planet: home_planet.map(|p| p.to_owned()),
}
}
}
impl DroidData {
pub fn new(
id: &str,
name: &str,
friend_ids: &[&str],
appears_in: &[Episode],
secret_backstory: Option<&str>,
primary_function: Option<&str>) -> DroidData
{
DroidData {
id: id.to_owned(),
name: name.to_owned(),
friend_ids: friend_ids.to_owned().into_iter().map(|f| f.to_owned()).collect(),
appears_in: appears_in.iter().cloned().collect(),
secret_backstory: secret_backstory.map(|b| b.to_owned()),
primary_function: primary_function.map(|p| p.to_owned()),
}
}
}
impl Database {
pub fn new() -> Database {
let mut humans = HashMap::new();
let mut droids = HashMap::new();
humans.insert("1000".to_owned(), HumanData::new(
"1000",
"Luke Skywalker",
&["1002", "1003", "2000", "2001"],
&[Episode::NewHope, Episode::Empire, Episode::Jedi],
None,
Some("Tatooine"),
));
humans.insert("1001".to_owned(), HumanData::new(
"1001",
"Darth Vader",
&["1004"],
&[Episode::NewHope, Episode::Empire, Episode::Jedi],
None,
Some("Tatooine"),
));
humans.insert("1002".to_owned(), HumanData::new(
"1002",
"Han Solo",
&["1000", "1003", "2001"],
&[Episode::NewHope, Episode::Empire, Episode::Jedi],
None,
None,
));
humans.insert("1003".to_owned(), HumanData::new(
"1003",
"Leia Organa",
&["1000", "1002", "2000", "2001"],
&[Episode::NewHope, Episode::Empire, Episode::Jedi],
None,
Some("Alderaan"),
));
humans.insert("1004".to_owned(), HumanData::new(
"1004",
"Wilhuff Tarkin",
&["1001"],
&[Episode::NewHope],
None,
None,
));
droids.insert("2000".to_owned(), DroidData::new(
"2000",
"C-3PO",
&["1000", "1002", "1003", "2001"],
&[Episode::NewHope, Episode::Empire, Episode::Jedi],
None,
Some("Protocol"),
));
droids.insert("2001".to_owned(), DroidData::new(
"2001",
"R2-D2",
&["1000", "1002", "1003"],
&[Episode::NewHope, Episode::Empire, Episode::Jedi],
None,
Some("Astromech"),
));
Database {
humans: humans,
droids: droids,
}
}
pub fn get_hero(&self, episode: Option<Episode>) -> &Character {
if episode == Some(Episode::Empire) {
self.get_human("1000").unwrap().as_character()
} else {
self.get_droid("2001").unwrap().as_character()
}
}
pub fn get_human(&self, id: &str) -> Option<&Human> {
self.humans.get(id).map(|h| h as &Human)
}
pub fn get_droid(&self, id: &str) -> Option<&Droid> {
self.droids.get(id).map(|d| d as &Droid)
}
pub fn get_character(&self, id: &str) -> Option<&Character> {
if let Some(h) = self.humans.get(id) {
Some(h)
}
else if let Some(d) = self.droids.get(id) {
Some(d)
}
else {
None
}
}
pub fn get_friends(&self, c: &Character) -> Vec<&Character> {
c.friend_ids().iter()
.flat_map(|id| self.get_character(id))
.collect()
}
}

364
src/tests/query_tests.rs Normal file
View file

@ -0,0 +1,364 @@
use std::collections::HashMap;
use ast::InputValue;
use value::Value;
use schema::model::RootNode;
use tests::model::Database;
#[test]
fn test_hero_name() {
let doc = r#"
{
hero {
name
}
}"#;
let database = Database::new();
let schema = RootNode::new(&database, ());
assert_eq!(
::execute(doc, None, &schema, &HashMap::new(), &database),
Ok((Value::object(vec![
("hero", Value::object(vec![
("name", Value::string("R2-D2")),
].into_iter().collect())),
].into_iter().collect()),
vec![])));
}
#[test]
fn test_hero_name_and_friends() {
let doc = r#"
{
hero {
id
name
friends {
name
}
}
}"#;
let database = Database::new();
let schema = RootNode::new(&database, ());
assert_eq!(
::execute(doc, None, &schema, &HashMap::new(), &database),
Ok((Value::object(vec![
("hero", Value::object(vec![
("id", Value::string("2001")),
("name", Value::string("R2-D2")),
("friends", Value::list(vec![
Value::object(vec![
("name", Value::string("Luke Skywalker")),
].into_iter().collect()),
Value::object(vec![
("name", Value::string("Han Solo")),
].into_iter().collect()),
Value::object(vec![
("name", Value::string("Leia Organa")),
].into_iter().collect()),
])),
].into_iter().collect())),
].into_iter().collect()),
vec![])));
}
#[test]
fn test_hero_name_and_friends_and_friends_of_friends() {
let doc = r#"
{
hero {
id
name
friends {
name
appearsIn
friends {
name
}
}
}
}"#;
let database = Database::new();
let schema = RootNode::new(&database, ());
assert_eq!(
::execute(doc, None, &schema, &HashMap::new(), &database),
Ok((Value::object(vec![
("hero", Value::object(vec![
("id", Value::string("2001")),
("name", Value::string("R2-D2")),
("friends", Value::list(vec![
Value::object(vec![
("name", Value::string("Luke Skywalker")),
("appearsIn", Value::list(vec![
Value::string("NEW_HOPE"),
Value::string("EMPIRE"),
Value::string("JEDI"),
])),
("friends", Value::list(vec![
Value::object(vec![
("name", Value::string("Han Solo")),
].into_iter().collect()),
Value::object(vec![
("name", Value::string("Leia Organa")),
].into_iter().collect()),
Value::object(vec![
("name", Value::string("C-3PO")),
].into_iter().collect()),
Value::object(vec![
("name", Value::string("R2-D2")),
].into_iter().collect()),
])),
].into_iter().collect()),
Value::object(vec![
("name", Value::string("Han Solo")),
("appearsIn", Value::list(vec![
Value::string("NEW_HOPE"),
Value::string("EMPIRE"),
Value::string("JEDI"),
])),
("friends", Value::list(vec![
Value::object(vec![
("name", Value::string("Luke Skywalker")),
].into_iter().collect()),
Value::object(vec![
("name", Value::string("Leia Organa")),
].into_iter().collect()),
Value::object(vec![
("name", Value::string("R2-D2")),
].into_iter().collect()),
])),
].into_iter().collect()),
Value::object(vec![
("name", Value::string("Leia Organa")),
("appearsIn", Value::list(vec![
Value::string("NEW_HOPE"),
Value::string("EMPIRE"),
Value::string("JEDI"),
])),
("friends", Value::list(vec![
Value::object(vec![
("name", Value::string("Luke Skywalker")),
].into_iter().collect()),
Value::object(vec![
("name", Value::string("Han Solo")),
].into_iter().collect()),
Value::object(vec![
("name", Value::string("C-3PO")),
].into_iter().collect()),
Value::object(vec![
("name", Value::string("R2-D2")),
].into_iter().collect()),
])),
].into_iter().collect()),
])),
].into_iter().collect())),
].into_iter().collect()),
vec![])));
}
#[test]
fn test_query_name() {
let doc = r#"{ human(id: "1000") { name } }"#;
let database = Database::new();
let schema = RootNode::new(&database, ());
assert_eq!(
::execute(doc, None, &schema, &HashMap::new(), &database),
Ok((Value::object(vec![
("human", Value::object(vec![
("name", Value::string("Luke Skywalker")),
].into_iter().collect())),
].into_iter().collect()),
vec![])));
}
#[test]
fn test_query_alias_single() {
let doc = r#"{ luke: human(id: "1000") { name } }"#;
let database = Database::new();
let schema = RootNode::new(&database, ());
assert_eq!(
::execute(doc, None, &schema, &HashMap::new(), &database),
Ok((Value::object(vec![
("luke", Value::object(vec![
("name", Value::string("Luke Skywalker")),
].into_iter().collect())),
].into_iter().collect()),
vec![])));
}
#[test]
fn test_query_alias_multiple() {
let doc = r#"
{
luke: human(id: "1000") { name }
leia: human(id: "1003") { name }
}"#;
let database = Database::new();
let schema = RootNode::new(&database, ());
assert_eq!(
::execute(doc, None, &schema, &HashMap::new(), &database),
Ok((Value::object(vec![
("luke", Value::object(vec![
("name", Value::string("Luke Skywalker")),
].into_iter().collect())),
("leia", Value::object(vec![
("name", Value::string("Leia Organa")),
].into_iter().collect())),
].into_iter().collect()),
vec![])));
}
#[test]
fn test_query_alias_multiple_with_fragment() {
let doc = r#"
query UseFragment {
luke: human(id: "1000") { ...HumanFragment }
leia: human(id: "1003") { ...HumanFragment }
}
fragment HumanFragment on Human {
name
homePlanet
}"#;
let database = Database::new();
let schema = RootNode::new(&database, ());
assert_eq!(
::execute(doc, None, &schema, &HashMap::new(), &database),
Ok((Value::object(vec![
("luke", Value::object(vec![
("name", Value::string("Luke Skywalker")),
("homePlanet", Value::string("Tatooine")),
].into_iter().collect())),
("leia", Value::object(vec![
("name", Value::string("Leia Organa")),
("homePlanet", Value::string("Alderaan")),
].into_iter().collect())),
].into_iter().collect()),
vec![])));
}
#[test]
fn test_query_name_variable() {
let doc = r#"query FetchSomeIDQuery($someId: String!) { human(id: $someId) { name } }"#;
let database = Database::new();
let schema = RootNode::new(&database, ());
let vars = vec![
("someId".to_owned(), InputValue::string("1000")),
].into_iter().collect();
assert_eq!(
::execute(doc, None, &schema, &vars, &database),
Ok((Value::object(vec![
("human", Value::object(vec![
("name", Value::string("Luke Skywalker")),
].into_iter().collect())),
].into_iter().collect()),
vec![])));
}
#[test]
fn test_query_name_invalid_variable() {
let doc = r#"query FetchSomeIDQuery($someId: String!) { human(id: $someId) { name } }"#;
let database = Database::new();
let schema = RootNode::new(&database, ());
let vars = vec![
("someId".to_owned(), InputValue::string("some invalid id")),
].into_iter().collect();
assert_eq!(
::execute(doc, None, &schema, &vars, &database),
Ok((Value::object(vec![
("human", Value::null()),
].into_iter().collect()),
vec![])));
}
#[test]
fn test_query_friends_names() {
let doc = r#"{ human(id: "1000") { friends { name } } }"#;
let database = Database::new();
let schema = RootNode::new(&database, ());
assert_eq!(
::execute(doc, None, &schema, &HashMap::new(), &database),
Ok((Value::object(vec![
("human", Value::object(vec![
("friends", Value::list(vec![
Value::object(vec![
("name", Value::string("Han Solo")),
].into_iter().collect()),
Value::object(vec![
("name", Value::string("Leia Organa")),
].into_iter().collect()),
Value::object(vec![
("name", Value::string("C-3PO")),
].into_iter().collect()),
Value::object(vec![
("name", Value::string("R2-D2")),
].into_iter().collect()),
])),
].into_iter().collect())),
].into_iter().collect()),
vec![])));
}
#[test]
fn test_query_inline_fragments_droid() {
let doc = r#"
query InlineFragments {
hero {
name
__typename
...on Droid {
primaryFunction
}
}
}
"#;
let database = Database::new();
let schema = RootNode::new(&database, ());
assert_eq!(
::execute(doc, None, &schema, &HashMap::new(), &database),
Ok((Value::object(vec![
("hero", Value::object(vec![
("__typename", Value::string("Droid")),
("name", Value::string("R2-D2")),
("primaryFunction", Value::string("Astromech")),
].into_iter().collect())),
].into_iter().collect()),
vec![])));
}
#[test]
fn test_query_inline_fragments_human() {
let doc = r#"
query InlineFragments {
hero(episode: EMPIRE) {
name
__typename
}
}
"#;
let database = Database::new();
let schema = RootNode::new(&database, ());
assert_eq!(
::execute(doc, None, &schema, &HashMap::new(), &database),
Ok((Value::object(vec![
("hero", Value::object(vec![
("__typename", Value::string("Human")),
("name", Value::string("Luke Skywalker")),
].into_iter().collect())),
].into_iter().collect()),
vec![])));
}

105
src/tests/schema.rs Normal file
View file

@ -0,0 +1,105 @@
use types::schema::FieldResult;
use tests::model::{Character, Human, Droid, Database, Episode};
graphql_enum!(Episode {
Episode::NewHope => "NEW_HOPE",
Episode::Empire => "EMPIRE",
Episode::Jedi => "JEDI",
});
graphql_interface!(<'a> &'a Character: Database as "Character" |&self| {
description: "A character in the Star Wars Trilogy"
field id() -> FieldResult<&str> as "The id of the character" {
Ok(self.id())
}
field name() -> FieldResult<Option<&str>> as "The name of the character" {
Ok(Some(self.name()))
}
field friends(&mut executor) -> FieldResult<Vec<&Character>>
as "The friends of the character" {
Ok(executor.context().get_friends(self.as_character()))
}
field appears_in() -> FieldResult<&[Episode]> as "Which movies they appear in" {
Ok(self.appears_in())
}
instance_resolvers: |&context| [
context.get_human(&self.id()),
context.get_droid(&self.id()),
]
});
graphql_object!(<'a> &'a Human: Database as "Human" |&self| {
description: "A humanoid creature in the Star Wars universe."
interfaces: [&Character]
field id() -> FieldResult<&str> as "The id of the human"{
Ok(self.id())
}
field name() -> FieldResult<Option<&str>> as "The name of the human" {
Ok(Some(self.name()))
}
field friends(&mut executor) -> FieldResult<Vec<&Character>>
as "The friends of the human" {
Ok(executor.context().get_friends(self.as_character()))
}
field home_planet() -> FieldResult<&Option<String>> as "The home planet of the human" {
Ok(self.home_planet())
}
});
graphql_object!(<'a> &'a Droid: Database as "Droid" |&self| {
description: "A mechanical creature in the Star Wars universe."
interfaces: [&Character]
field id() -> FieldResult<&str> as "The id of the droid" {
Ok(self.id())
}
field name() -> FieldResult<Option<&str>> as "The name of the droid" {
Ok(Some(self.name()))
}
field friends(&mut executor) -> FieldResult<Vec<&Character>>
as "The friends of the droid" {
Ok(executor.context().get_friends(self.as_character()))
}
field primary_function() -> FieldResult<&Option<String>> as "The primary function of the droid" {
Ok(self.primary_function())
}
});
graphql_object!(Database: Database as "Query" |&self| {
description: "The root query object of the schema"
field human(
id: String as "id of the human"
) -> FieldResult<Option<&Human>> {
Ok(self.get_human(&id))
}
field droid(
id: String as "id of the droid"
) -> FieldResult<Option<&Droid>> {
Ok(self.get_droid(&id))
}
field hero(
episode: Option<Episode> as
"If omitted, returns the hero of the whole saga. If provided, returns \
the hero of that particular episode"
) -> FieldResult<Option<&Character>> {
Ok(Some(self.get_hero(episode).as_character()))
}
});

404
src/types/base.rs Normal file
View file

@ -0,0 +1,404 @@
use std::collections::HashMap;
use ast::{InputValue, Selection, Directive, FromInputValue};
use value::Value;
use schema::meta::{Argument, MetaType};
use types::schema::{Executor, Registry, ExecutionResult};
use parser::Spanning;
/// GraphQL type kind
///
/// The GraphQL specification defines a number of type kinds - the meta type
/// of a type.
#[derive(Clone, Eq, PartialEq, Debug)]
pub enum TypeKind {
/// ## Scalar types
///
/// Scalar types appear as the leaf nodes of GraphQL queries. Strings,
/// numbers, and booleans are the built in types, and while it's possible
/// to define your own, it's relatively uncommon.
Scalar,
/// ## Object types
///
/// The most common type to be implemented by users. Objects have fields
/// and can implement interfaces.
Object,
/// ## Interface types
///
/// Interface types are used to represent overlapping fields between
/// multiple types, and can be queried for their concrete type.
Interface,
/// ## Union types
///
/// Unions are similar to interfaces but can not contain any fields on
/// their own.
Union,
/// ## Enum types
///
/// Like scalars, enum types appear as the leaf nodes of GraphQL queries.
Enum,
/// ## Input objects
///
/// Represents complex values provided in queries _into_ the system.
InputObject,
/// ## List types
///
/// Represent lists of other types. This library provides implementations
/// for vectors and slices, but other Rust types can be extended to serve
/// as GraphQL lists.
List,
/// ## Non-null types
///
/// In GraphQL, nullable types are the default. By putting a `!` after a
/// type, it becomes non-nullable.
NonNull,
}
/// Field argument container
pub struct Arguments {
args: Option<HashMap<String, InputValue>>,
}
impl Arguments {
#[doc(hidden)]
pub fn new(mut args: Option<HashMap<String, InputValue>>, meta_args: &Option<Vec<Argument>>) -> Arguments {
if meta_args.is_some() && args.is_none() {
args = Some(HashMap::new());
}
if let (&mut Some(ref mut args), &Some(ref meta_args)) = (&mut args, meta_args) {
for arg in meta_args {
if !args.contains_key(&arg.name) {
if let Some(ref default_value) = arg.default_value {
args.insert(arg.name.clone(), default_value.clone());
}
}
}
}
Arguments {
args: args
}
}
/// Get and convert an argument into the desired type.
///
/// If the argument is found, or a default argument has been provided,
/// the `InputValue` will be converted into the type `T`.
///
/// Returns `Some` if the argument is present _and_ type conversion
/// succeeeds.
pub fn get<T>(&self, key: &str) -> Option<T> where T: FromInputValue {
match self.args {
Some(ref args) => match args.get(key) {
Some(v) => Some(v.convert().unwrap()),
None => None,
},
None => None,
}
}
}
/**
Primary trait used to expose Rust types in a GraphQL schema
All of the convenience macros ultimately expand into an implementation of
this trait for the given type. The macros remove duplicated definitions of
fields and arguments, and add type checks on all resolve functions
automatically. This can all be done manually.
`GraphQLType` provides _some_ convenience methods for you, in the form of
optional trait methods. The `name` and `meta` methods are mandatory, but
other than that, it depends on what type you're exposing:
* Scalars, enums, lists and non null wrappers only require `resolve`,
* Interfaces and objects require `resolve_field` _or_ `resolve` if you want
to implement custom resolution logic (probably not),
* Interfaces and unions require `resolve_into_type` and `concrete_type_name`.
* Input objects do not require anything
## Example
Manually deriving an object is straightforward but tedious. This is the
equivalent of the `User` object as shown in the example in the documentation
root:
```rust
use juniper::{GraphQLType, Registry, FieldResult,
Arguments, Executor, ExecutionResult};
use juniper::meta::MetaType;
# use std::collections::HashMap;
struct User { id: String, name: String, friend_ids: Vec<String> }
struct Database { users: HashMap<String, User> }
impl GraphQLType<Database> for User {
fn name() -> Option<&'static str> {
Some("User")
}
fn meta(registry: &mut Registry<Database>) -> MetaType {
// First, we need to define all fields and their types on this type.
//
// If need arguments, want to implement interfaces, or want to add
// documentation strings, we can do it here.
registry.build_object_type::<User>()(&[
registry.field::<&String>("id"),
registry.field::<&String>("name"),
registry.field::<Vec<&User>>("friends"),
])
.into_meta()
}
fn resolve_field(
&self,
field_name: &str,
args: &Arguments,
executor: &mut Executor<Database>
)
-> ExecutionResult
{
// Next, we need to match the queried field name. All arms of this
// match statement return `ExecutionResult`, which makes it hard to
// statically verify that the type you pass on to `executor.resolve`
// actually matches the one that you defined in `meta()` above.
let database = executor.context();
match field_name {
"id" => executor.resolve(&self.id),
"name" => executor.resolve(&self.name),
// You pass a vector of User objects to `executor.resolve`, and it
// will determine which fields of the sub-objects to actually
// resolve based on the query. The executor instance keeps track
// of its current position in the query.
"friends" => executor.resolve(
&self.friend_ids.iter()
.filter_map(|id| database.users.get(id))
.collect::<Vec<_>>()
),
// We can only reach this panic in two cases; either a mismatch
// between the defined schema in `meta()` above, or a validation
// in this library failed because of a bug.
//
// In either of those two cases, the only reasonable way out is
// to panic the thread.
_ => panic!("Field {} not found on type User", field_name),
}
}
}
```
*/
pub trait GraphQLType<CtxT>: Sized {
/// The name of the GraphQL type to expose.
///
/// This function will be called multiple times during schema construction.
/// It must _not_ perform any calculation and _always_ return the same
/// value.
fn name() -> Option<&'static str>;
/// The meta type representing this GraphQL type.
fn meta(registry: &mut Registry<CtxT>) -> MetaType;
/// Resolve the value of a single field on this type.
///
/// The arguments object contain all specified arguments, with default
/// values substituted for the ones not provided by the query.
///
/// The executor can be used to drive selections into sub-objects.
///
/// The default implementation panics through `unimplemented!()`.
#[allow(unused_variables)]
fn resolve_field(&self, field_name: &str, arguments: &Arguments, executor: &mut Executor<CtxT>)
-> ExecutionResult
{
unimplemented!()
}
/// Resolve this interface or union into a concrete type
///
/// Try to resolve the current type into the type name provided. If the
/// type matches, pass the instance along to `executor.resolve`.
///
/// The default implementation panics through `unimplemented()`.
#[allow(unused_variables)]
fn resolve_into_type(&self, type_name: &str, selection_set: Option<Vec<Selection>>, executor: &mut Executor<CtxT>) -> ExecutionResult {
unimplemented!();
}
/// Return the concrete type name for this instance/union.
///
/// The default implementation panics through `unimplemented()`.
#[allow(unused_variables)]
fn concrete_type_name(&self, context: &CtxT) -> String {
unimplemented!();
}
/// Resolve the provided selection set against the current object.
///
/// For non-object types, the selection set will be `None` and the value
/// of the object should simply be returned.
///
/// For objects, all fields in the selection set should be resolved.
///
/// The default implementation uses `resolve_field` to resolve all fields,
/// including those through fragment expansion, for object types. For
/// non-object types, this method panics through `unimplemented!()`.
fn resolve(&self, selection_set: Option<Vec<Selection>>, executor: &mut Executor<CtxT>) -> Value {
if let Some(selection_set) = selection_set {
let mut result = HashMap::new();
resolve_selection_set_into(self, selection_set, executor, &mut result);
Value::object(result)
}
else {
unimplemented!();
}
}
}
fn resolve_selection_set_into<T, CtxT>(
instance: &T,
selection_set: Vec<Selection>,
executor: &mut Executor<CtxT>,
result: &mut HashMap<String, Value>)
where T: GraphQLType<CtxT>
{
let meta_type = executor.schema()
.concrete_type_by_name(T::name().expect("Resolving named type's selection set"))
.expect("Type not found in schema");
for selection in selection_set {
match selection {
Selection::Field(Spanning { item: f, start: start_pos, .. }) => {
if is_excluded(
&match f.directives {
Some(sel) => Some(sel.iter().cloned().map(|s| s.item).collect()),
None => None,
},
executor.variables()) {
continue;
}
let response_name = &f.alias.as_ref().unwrap_or(&f.name).item;
if &f.name.item == "__typename" {
result.insert(
response_name.clone(),
Value::string(
instance.concrete_type_name(executor.context())));
continue;
}
let meta_field = meta_type.field_by_name(&f.name.item)
.expect(&format!("Field {} not found on type {:?}", f.name.item, meta_type.name()));
let exec_vars = executor.variables();
let mut sub_exec = executor.sub_executor(
Some(response_name.clone()),
start_pos.clone(),
f.selection_set);
let field_result = instance.resolve_field(
&f.name.item,
&Arguments::new(
f.arguments.map(|m|
m.item.into_iter().map(|(k, v)|
(k.item, v.item.into_const(exec_vars))).collect()),
&meta_field.arguments),
&mut sub_exec);
match field_result {
Ok(v) => { result.insert(response_name.clone(), v); }
Err(e) => { sub_exec.push_error(e, start_pos); }
}
},
Selection::FragmentSpread(Spanning { item: spread, .. }) => {
if is_excluded(
&match spread.directives {
Some(sel) => Some(sel.iter().cloned().map(|s| s.item).collect()),
None => None,
},
executor.variables()) {
continue;
}
let fragment = &executor.fragment_by_name(&spread.name.item)
.expect("Fragment could not be found");
resolve_selection_set_into(
instance, fragment.selection_set.clone(), executor, result);
},
Selection::InlineFragment(Spanning { item: fragment, start: start_pos, .. }) => {
if is_excluded(
&match fragment.directives {
Some(sel) => Some(sel.iter().cloned().map(|s| s.item).collect()),
None => None
},
executor.variables()) {
continue;
}
let mut sub_exec = executor.sub_executor(
None,
start_pos.clone(),
Some(fragment.selection_set.clone()));
if let Some(type_condition) = fragment.type_condition {
let sub_result = instance.resolve_into_type(
&type_condition.item,
Some(fragment.selection_set.clone()),
&mut sub_exec);
if let Ok(Value::Object(mut hash_map)) = sub_result {
for (k, v) in hash_map.drain() {
result.insert(k, v);
}
}
else if let Err(e) = sub_result {
sub_exec.push_error(e, start_pos);
}
}
else {
resolve_selection_set_into(
instance,
fragment.selection_set.clone(),
&mut sub_exec,
result);
}
},
}
}
}
fn is_excluded(directives: &Option<Vec<Directive>>, vars: &HashMap<String, InputValue>) -> bool {
if let Some(ref directives) = *directives {
for directive in directives {
let condition: bool = directive.arguments.iter()
.flat_map(|m| m.item.get("if"))
.flat_map(|v| v.item.clone().into_const(vars).convert())
.next().unwrap();
if directive.name.item == "skip" && condition {
return true
}
else if directive.name.item == "include" && !condition {
return true
}
}
false
}
else {
false
}
}

108
src/types/containers.rs Normal file
View file

@ -0,0 +1,108 @@
use ast::{InputValue, ToInputValue, FromInputValue, Selection};
use value::Value;
use schema::meta::MetaType;
use types::schema::{Executor, Registry};
use types::base::{GraphQLType};
impl<T, CtxT> GraphQLType<CtxT> for Option<T> where T: GraphQLType<CtxT> {
fn name() -> Option<&'static str> {
None
}
fn meta(registry: &mut Registry<CtxT>) -> MetaType {
registry.build_nullable_type::<T>().into_meta()
}
fn resolve(&self, _: Option<Vec<Selection>>, executor: &mut Executor<CtxT>) -> Value {
match *self {
Some(ref obj) => executor.resolve_into_value(obj),
None => Value::null(),
}
}
}
impl<T> FromInputValue for Option<T> where T: FromInputValue {
fn from(v: &InputValue) -> Option<Option<T>> {
match v {
&InputValue::Null => None,
v => match v.convert() {
Some(x) => Some(Some(x)),
None => None,
}
}
}
}
impl<T> ToInputValue for Option<T> where T: ToInputValue {
fn to(&self) -> InputValue {
match *self {
Some(ref v) => v.to(),
None => InputValue::null(),
}
}
}
impl<T, CtxT> GraphQLType<CtxT> for Vec<T> where T: GraphQLType<CtxT> {
fn name() -> Option<&'static str> {
None
}
fn meta(registry: &mut Registry<CtxT>) -> MetaType {
registry.build_list_type::<T>().into_meta()
}
fn resolve(&self, _: Option<Vec<Selection>>, executor: &mut Executor<CtxT>) -> Value {
Value::list(
self.iter().map(|e| executor.resolve_into_value(e)).collect()
)
}
}
impl<T> FromInputValue for Vec<T> where T: FromInputValue {
fn from(v: &InputValue) -> Option<Vec<T>> {
match *v {
InputValue::List(ref ls) => {
let v: Vec<_> = ls.iter().filter_map(|i| i.item.convert()).collect();
if v.len() == ls.len() {
Some(v)
}
else {
None
}
},
_ => None,
}
}
}
impl<T> ToInputValue for Vec<T> where T: ToInputValue {
fn to(&self) -> InputValue {
InputValue::list(self.iter().map(|v| v.to()).collect())
}
}
impl<'a, T, CtxT> GraphQLType<CtxT> for &'a [T] where T: GraphQLType<CtxT> {
fn name() -> Option<&'static str> {
None
}
fn meta(registry: &mut Registry<CtxT>) -> MetaType {
registry.build_list_type::<T>().into_meta()
}
fn resolve(&self, _: Option<Vec<Selection>>, executor: &mut Executor<CtxT>) -> Value {
Value::list(
self.iter().map(|e| executor.resolve_into_value(e)).collect()
)
}
}
impl<'a, T> ToInputValue for &'a [T] where T: ToInputValue {
fn to(&self) -> InputValue {
InputValue::list(self.iter().map(|v| v.to()).collect())
}
}

8
src/types/mod.rs Normal file
View file

@ -0,0 +1,8 @@
pub mod base;
pub mod schema;
pub mod scalars;
pub mod pointers;
pub mod containers;
pub mod utilities;
pub use self::schema::execute_validated_query;

73
src/types/pointers.rs Normal file
View file

@ -0,0 +1,73 @@
use ast::{Selection, InputValue, ToInputValue, FromInputValue};
use value::Value;
use schema::meta::MetaType;
use types::schema::{Executor, Registry, ExecutionResult};
use types::base::{Arguments, GraphQLType};
impl<T, CtxT> GraphQLType<CtxT> for Box<T> where T: GraphQLType<CtxT> {
fn name() -> Option<&'static str> {
T::name()
}
fn meta(registry: &mut Registry<CtxT>) -> MetaType {
T::meta(registry)
}
fn resolve_into_type(&self, name: &str, selection_set: Option<Vec<Selection>>, executor: &mut Executor<CtxT>) -> ExecutionResult {
(**self).resolve_into_type(name, selection_set, executor)
}
fn resolve_field(&self, field: &str, args: &Arguments, executor: &mut Executor<CtxT>) -> ExecutionResult
{
(**self).resolve_field(field, args, executor)
}
fn resolve(&self, selection_set: Option<Vec<Selection>>, executor: &mut Executor<CtxT>) -> Value {
(**self).resolve(selection_set, executor)
}
}
impl<T> FromInputValue for Box<T> where T: FromInputValue {
fn from(v: &InputValue) -> Option<Box<T>> {
match <T as FromInputValue>::from(v) {
Some(v) => Some(Box::new(v)),
None => None,
}
}
}
impl<T> ToInputValue for Box<T> where T: ToInputValue {
fn to(&self) -> InputValue {
(**self).to()
}
}
impl<'a, T, CtxT> GraphQLType<CtxT> for &'a T where T: GraphQLType<CtxT> {
fn name() -> Option<&'static str> {
T::name()
}
fn meta(registry: &mut Registry<CtxT>) -> MetaType {
T::meta(registry)
}
fn resolve_into_type(&self, name: &str, selection_set: Option<Vec<Selection>>, executor: &mut Executor<CtxT>) -> ExecutionResult {
(**self).resolve_into_type(name, selection_set, executor)
}
fn resolve_field(&self, field: &str, args: &Arguments, executor: &mut Executor<CtxT>) -> ExecutionResult
{
(**self).resolve_field(field, args, executor)
}
fn resolve(&self, selection_set: Option<Vec<Selection>>, executor: &mut Executor<CtxT>) -> Value {
(**self).resolve(selection_set, executor)
}
}
impl<'a, T> ToInputValue for &'a T where T: ToInputValue {
fn to(&self) -> InputValue {
(**self).to()
}
}

121
src/types/scalars.rs Normal file
View file

@ -0,0 +1,121 @@
use ast::{InputValue, Selection, FromInputValue, ToInputValue};
use value::Value;
use schema::meta::MetaType;
use types::schema::{Executor, Registry};
use types::base::GraphQLType;
/// An ID as defined by the GraphQL specification
///
/// Represented as a string, but can be converted _to_ from an integer as well.
pub struct ID(String);
graphql_scalar!(ID as "ID" {
resolve(&self) -> Value {
Value::string(&self.0)
}
from_input_value(v: &InputValue) -> Option<ID> {
match *v {
InputValue::String(ref s) => Some(ID(s.to_owned())),
InputValue::Int(i) => Some(ID(format!("{}", i))),
_ => None
}
}
});
graphql_scalar!(String as "String" {
resolve(&self) -> Value {
Value::string(self)
}
from_input_value(v: &InputValue) -> Option<String> {
match *v {
InputValue::String(ref s) => Some(s.clone()),
_ => None,
}
}
});
impl<'a, CtxT> GraphQLType<CtxT> for &'a str {
fn name() -> Option<&'static str> {
Some("String")
}
fn meta(registry: &mut Registry<CtxT>) -> MetaType {
registry.build_scalar_type::<String>().into_meta()
}
fn resolve(&self, _: Option<Vec<Selection>>, _: &mut Executor<CtxT>) -> Value {
Value::string(self)
}
}
impl<'a> ToInputValue for &'a str {
fn to(&self) -> InputValue {
InputValue::string(self)
}
}
graphql_scalar!(bool as "Boolean" {
resolve(&self) -> Value {
Value::boolean(*self)
}
from_input_value(v: &InputValue) -> Option<bool> {
match *v {
InputValue::Boolean(b) => Some(b),
_ => None,
}
}
});
graphql_scalar!(i64 as "Int" {
resolve(&self) -> Value {
Value::int(*self)
}
from_input_value(v: &InputValue) -> Option<i64> {
match *v {
InputValue::Int(i) => Some(i),
_ => None,
}
}
});
graphql_scalar!(f64 as "Float" {
resolve(&self) -> Value {
Value::float(*self)
}
from_input_value(v: &InputValue) -> Option<f64> {
match *v {
InputValue::Int(i) => Some(i as f64),
InputValue::Float(f) => Some(f),
_ => None,
}
}
});
impl<CtxT> GraphQLType<CtxT> for () {
fn name() -> Option<&'static str> {
Some("__Unit")
}
fn meta(registry: &mut Registry<CtxT>) -> MetaType {
registry.build_scalar_type::<Self>().into_meta()
}
}
impl FromInputValue for () {
fn from(_: &InputValue) -> Option<()> {
None
}
}

421
src/types/schema.rs Normal file
View file

@ -0,0 +1,421 @@
use std::collections::HashMap;
use std::marker::PhantomData;
use ast::{InputValue, ToInputValue, Document, Selection, Fragment, Definition, Type, FromInputValue, OperationType};
use value::Value;
use parser::SourcePosition;
use schema::meta::{MetaType, ScalarMeta, ListMeta, NullableMeta,
ObjectMeta, EnumMeta, InterfaceMeta, UnionMeta,
InputObjectMeta, PlaceholderMeta, Field, Argument,
EnumValue};
use schema::model::{RootNode, SchemaType};
use types::base::GraphQLType;
/// A type registry used to build schemas
///
/// The registry gathers metadata for all types in a schema. It provides
/// convenience methods to convert types implementing the `GraphQLType` trait
/// into `Type` instances and automatically registers them.
pub struct Registry<CtxT> {
/// Currently registered types
pub types: HashMap<String, MetaType>,
phantom: PhantomData<CtxT>,
}
#[derive(Clone)]
pub enum FieldPath<'a> {
Root(SourcePosition),
Field(String, SourcePosition, &'a FieldPath<'a>),
}
/// Query execution engine
///
/// The executor helps drive the query execution in a schema. It keeps track
/// of the current field stack, context, variables, and errors.
pub struct Executor<'a, CtxT> where CtxT: 'a {
fragments: &'a HashMap<String, Fragment>,
variables: &'a HashMap<String, InputValue>,
current_selection_set: Option<Vec<Selection>>,
schema: &'a SchemaType,
context: &'a CtxT,
errors: &'a mut Vec<ExecutionError>,
field_path: FieldPath<'a>,
}
/// Error type for errors that occur during query execution
///
/// All execution errors contain the source position in the query of the field
/// that failed to resolve. It also contains the field stack.
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq)]
pub struct ExecutionError {
location: SourcePosition,
path: Vec<String>,
message: String,
}
/// The result of resolving the value of a field of type `T`
pub type FieldResult<T> = Result<T, String>;
/// The result of resolving an unspecified field
pub type ExecutionResult = Result<Value, String>;
impl<'a, CtxT> Executor<'a, CtxT> {
/// Resolve a single arbitrary value into an `ExecutionResult`
pub fn resolve<T: GraphQLType<CtxT>>(&mut self, value: &T) -> ExecutionResult {
Ok(value.resolve(
match self.current_selection_set {
Some(ref sel) => Some(sel.clone()),
None => None,
},
self))
}
/// Resolve a single arbitrary value into a return value
///
/// If the field fails to resolve, `null` will be returned.
pub fn resolve_into_value<T: GraphQLType<CtxT>>(&mut self, value: &T) -> Value {
match self.resolve(value) {
Ok(v) => v,
Err(e) => {
let position = self.field_path.location().clone();
self.push_error(e, position);
Value::null()
},
}
}
/// Derive a new executor by replacing the context
///
/// This can be used to connect different types, e.g. from different Rust
/// libraries, that require different context types.
pub fn replaced_context<'b, NewCtxT>(&'b mut self, ctx: &'b NewCtxT) -> Executor<'b, NewCtxT> {
Executor {
fragments: self.fragments,
variables: self.variables,
current_selection_set: self.current_selection_set.clone(),
schema: self.schema,
context: ctx,
errors: self.errors,
field_path: self.field_path.clone(),
}
}
#[doc(hidden)]
pub fn sub_executor(
&mut self,
field_name: Option<String>,
location: SourcePosition,
selection_set: Option<Vec<Selection>>,
)
-> Executor<CtxT>
{
Executor {
fragments: self.fragments,
variables: self.variables,
current_selection_set: selection_set,
schema: self.schema,
context: self.context,
errors: self.errors,
field_path: match field_name {
Some(name) => FieldPath::Field(name, location, &self.field_path),
None => self.field_path.clone(),
},
}
}
/// Access the current context
///
/// You usually provide the context when calling the top-level `execute`
/// function, or using the context factory in the Iron integration.
pub fn context(&self) -> &'a CtxT {
self.context
}
/// The currently executing schema
pub fn schema(&self) -> &'a SchemaType {
self.schema
}
#[doc(hidden)]
pub fn variables(&self) -> &'a HashMap<String, InputValue> {
self.variables
}
#[doc(hidden)]
pub fn fragment_by_name(&self, name: &str) -> Option<&'a Fragment> {
self.fragments.get(name)
}
/// Add an error to the execution engine
pub fn push_error(&mut self, error: String, location: SourcePosition) {
let mut path = Vec::new();
self.field_path.construct_path(&mut path);
self.errors.push(ExecutionError {
location: location,
path: path,
message: error,
});
}
}
impl<'a> FieldPath<'a> {
fn construct_path(&self, acc: &mut Vec<String>) {
match *self {
FieldPath::Root(_) => (),
FieldPath::Field(ref name, _, ref parent) => {
parent.construct_path(acc);
acc.push(name.clone());
}
}
}
fn location(&self) -> &SourcePosition {
match *self {
FieldPath::Root(ref pos) |
FieldPath::Field(_, ref pos, _) => pos
}
}
}
impl ExecutionError {
/// The error message
pub fn message(&self) -> &str {
&self.message
}
/// The source location _in the query_ of the field that failed to resolve
pub fn location(&self) -> &SourcePosition {
&self.location
}
/// The path of fields leading to the field that generated this error
pub fn path(&self) -> &[String] {
&self.path
}
}
pub fn execute_validated_query<QueryT, MutationT, CtxT>(
document: Document,
operation_name: Option<&str>,
root_node: &RootNode<CtxT, QueryT, MutationT>,
variables: &HashMap<String, InputValue>,
context: &CtxT
)
-> (Value, Vec<ExecutionError>)
where QueryT: GraphQLType<CtxT>,
MutationT: GraphQLType<CtxT>,
{
let mut fragments = vec![];
let mut operation = None;
for def in document {
match def {
Definition::Operation(op) => {
if operation_name.is_none() && operation.is_some() {
panic!("Must provide operation name if query contains multiple operations");
}
let move_op = operation_name.is_none()
|| op.item.name.as_ref().map(|s| s.item.as_ref()) == operation_name;
if move_op {
operation = Some(op);
}
}
Definition::Fragment(f) => fragments.push(f),
};
}
let op = operation.expect("Could not find operation to execute");
let mut errors = Vec::new();
let value;
{
let mut executor = Executor {
fragments: &fragments.into_iter().map(|f| (f.item.name.item.clone(), f.item)).collect(),
variables: variables,
current_selection_set: Some(op.item.selection_set),
schema: &root_node.schema,
context: context,
errors: &mut errors,
field_path: FieldPath::Root(op.start),
};
value = match op.item.operation_type {
OperationType::Query => executor.resolve_into_value(&root_node),
OperationType::Mutation => executor.resolve_into_value(&root_node.mutation_type),
};
}
errors.sort();
(value, errors)
}
impl<CtxT> Registry<CtxT> {
/// Construct a new registry
pub fn new(types: HashMap<String, MetaType>) -> Registry<CtxT> {
Registry {
types: types,
phantom: PhantomData,
}
}
/// Get the `Type` instance for a given GraphQL type
///
/// If the registry hasn't seen a type with this name before, it will
/// construct its metadata and store it.
pub fn get_type<T>(&mut self) -> Type where T: GraphQLType<CtxT> {
if let Some(name) = T::name() {
if !self.types.contains_key(name) {
self.insert_placeholder(name, Type::NonNullNamed(name.to_owned()));
let meta = T::meta(self);
self.types.insert(name.to_owned(), meta);
}
self.types[name].as_type()
}
else {
T::meta(self).as_type()
}
}
/// Create a field with the provided name
pub fn field<T>(&mut self, name: &str) -> Field where T: GraphQLType<CtxT> {
Field {
name: name.to_owned(),
description: None,
arguments: None,
field_type: self.get_type::<T>(),
deprecation_reason: None,
}
}
#[doc(hidden)]
pub fn field_inside_result<T>(&mut self, name: &str, _: FieldResult<T>) -> Field where T: GraphQLType<CtxT> {
Field {
name: name.to_owned(),
description: None,
arguments: None,
field_type: self.get_type::<T>(),
deprecation_reason: None,
}
}
/// Create an argument with the provided name
pub fn arg<T>(&mut self, name: &str) -> Argument where T: GraphQLType<CtxT> + FromInputValue {
Argument::new(name, self.get_type::<T>())
}
/// Create an argument with a default value
///
/// When called with type `T`, the actual argument will be given the type
/// `Option<T>`.
pub fn arg_with_default<T>(
&mut self,
name: &str,
value: &T,
)
-> Argument
where T: GraphQLType<CtxT> + ToInputValue + FromInputValue
{
Argument::new(name, self.get_type::<Option<T>>())
.default_value(value.to())
}
fn insert_placeholder(&mut self, name: &str, of_type: Type) {
if !self.types.contains_key(name) {
self.types.insert(
name.to_owned(),
MetaType::Placeholder(PlaceholderMeta { of_type: of_type }));
}
}
/// Create a scalar meta type
///
/// This expects the type to implement `FromInputValue`.
pub fn build_scalar_type<T>(&mut self)
-> ScalarMeta
where T: FromInputValue + GraphQLType<CtxT>
{
let name = T::name().expect("Scalar types must be named. Implement name()");
ScalarMeta::new::<T>(name)
}
/// Create a list meta type
pub fn build_list_type<T: GraphQLType<CtxT>>(&mut self) -> ListMeta {
let of_type = self.get_type::<T>();
ListMeta::new(of_type)
}
/// Create a nullable meta type
pub fn build_nullable_type<T: GraphQLType<CtxT>>(&mut self) -> NullableMeta {
let of_type = self.get_type::<T>();
NullableMeta::new(of_type)
}
/// Create an object meta type builder
///
/// To prevent infinite recursion by enforcing ordering, this returns a
/// function that needs to be called with the list of fields on the object.
pub fn build_object_type<T>(&mut self)
-> Box<Fn(&[Field]) -> ObjectMeta>
where T: GraphQLType<CtxT>
{
let name = T::name().expect("Object types must be named. Implement name()");
let typename_field = self.field::<String>("__typename");
Box::new(move |fs: &[Field]| {
let mut v = fs.to_vec();
v.push(typename_field.clone());
ObjectMeta::new(name, &v)
})
}
/// Create an enum meta type
pub fn build_enum_type<T>(&mut self)
-> Box<Fn(&[EnumValue]) -> EnumMeta>
where T: FromInputValue + GraphQLType<CtxT>
{
let name = T::name().expect("Enum types must be named. Implement name()");
Box::new(move |values: &[EnumValue]| EnumMeta::new::<T>(name, values))
}
/// Create an interface meta type builder
pub fn build_interface_type<T>(&mut self)
-> Box<Fn(&[Field]) -> InterfaceMeta>
where T: GraphQLType<CtxT>
{
let name = T::name().expect("Interface types must be named. Implement name()");
let typename_field = self.field::<String>("__typename");
Box::new(move |fs: &[Field]| {
let mut v = fs.to_vec();
v.push(typename_field.clone());
InterfaceMeta::new(name, &v)
})
}
/// Create a union meta type builder
pub fn build_union_type<T>(&mut self)
-> Box<Fn(&[Type]) -> UnionMeta>
where T: GraphQLType<CtxT>
{
let name = T::name().expect("Union types must be named. Implement name()");
Box::new(move |ts: &[Type]| UnionMeta::new(name, ts))
}
/// Create an input object meta type builder
pub fn build_input_object_type<T>(&mut self)
-> Box<Fn(&[Argument]) -> InputObjectMeta>
where T: FromInputValue + GraphQLType<CtxT>
{
let name = T::name().expect("Input object types must be named. Implement name()");
Box::new(move |args: &[Argument]| InputObjectMeta::new::<T>(name, args))
}
}

67
src/types/utilities.rs Normal file
View file

@ -0,0 +1,67 @@
use std::collections::HashSet;
use ast::InputValue;
use schema::model::{SchemaType, TypeType};
use schema::meta::{MetaType, InputObjectMeta};
pub fn is_valid_literal_value(schema: &SchemaType, arg_type: &TypeType, arg_value: &InputValue) -> bool {
match *arg_type {
TypeType::NonNull(ref inner) => {
if arg_value.is_null() {
false
}
else {
is_valid_literal_value(schema, inner, arg_value)
}
}
TypeType::List(ref inner) => {
match *arg_value {
InputValue::List(ref items) => items.iter().all(|i| is_valid_literal_value(schema, inner, &i.item)),
ref v => is_valid_literal_value(schema, inner, v),
}
}
TypeType::Concrete(t) => {
match *arg_value {
ref v @ InputValue::Null |
ref v @ InputValue::Int(_) |
ref v @ InputValue::Float(_) |
ref v @ InputValue::String(_) |
ref v @ InputValue::Boolean(_) |
ref v @ InputValue::Enum(_) => {
if let Some(ref parse_fn) = t.input_value_parse_fn() {
parse_fn(&v)
} else {
false
}
},
InputValue::List(_) => false,
InputValue::Variable(_) => true,
InputValue::Object(ref obj) => {
if let &MetaType::InputObject(InputObjectMeta { ref input_fields, .. }) = t {
let mut remaining_required_fields = input_fields.iter()
.filter_map(|f| if f.arg_type.is_non_null() { Some(&f.name) } else { None })
.collect::<HashSet<_>>();
let all_types_ok = obj.iter().all(|&(ref key, ref value)| {
remaining_required_fields.remove(&key.item);
if let Some(ref arg_type) = input_fields.iter()
.filter(|f| f.name == key.item)
.map(|f| schema.make_type(&f.arg_type))
.next()
{
is_valid_literal_value(schema, arg_type, &value.item)
}
else {
false
}
});
all_types_ok && remaining_required_fields.is_empty()
}
else {
false
}
}
}
}
}
}

174
src/validation/context.rs Normal file
View file

@ -0,0 +1,174 @@
use std::collections::HashSet;
use ast::{Document, Definition, Type};
use schema::meta::MetaType;
use schema::model::SchemaType;
use parser::SourcePosition;
/// Query validation error
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct RuleError {
locations: Vec<SourcePosition>,
message: String,
}
#[doc(hidden)]
pub struct ValidatorContext<'a> {
pub schema: &'a SchemaType,
errors: Vec<RuleError>,
type_stack: Vec<Option<&'a MetaType>>,
type_literal_stack: Vec<Option<Type>>,
input_type_stack: Vec<Option<&'a MetaType>>,
input_type_literal_stack: Vec<Option<Type>>,
parent_type_stack: Vec<Option<&'a MetaType>>,
fragment_names: HashSet<String>,
}
impl RuleError {
#[doc(hidden)]
pub fn new(message: &str, locations: &[SourcePosition]) -> RuleError {
RuleError {
message: message.to_owned(),
locations: locations.to_vec(),
}
}
/// Access the message for a validation error
pub fn message(&self) -> &str {
&self.message
}
/// Access the positions of the validation error
///
/// All validation errors contain at least one source position, but some
/// validators supply extra context through multiple positions.
pub fn locations(&self) -> &[SourcePosition] {
&self.locations
}
}
impl<'a> ValidatorContext<'a> {
#[doc(hidden)]
pub fn new(schema: &'a SchemaType, document: &Document) -> ValidatorContext<'a> {
ValidatorContext {
errors: Vec::new(),
schema: schema,
type_stack: Vec::new(),
type_literal_stack: Vec::new(),
parent_type_stack: Vec::new(),
input_type_stack: Vec::new(),
input_type_literal_stack: Vec::new(),
fragment_names: document.iter()
.filter_map(|def| match *def {
Definition::Fragment(ref frag) => Some(frag.item.name.item.clone()),
_ => None,
})
.collect()
}
}
#[doc(hidden)]
pub fn append_errors(&mut self, mut errors: Vec<RuleError>) {
self.errors.append(&mut errors);
}
#[doc(hidden)]
pub fn report_error(&mut self, message: &str, locations: &[SourcePosition]) {
self.errors.push(RuleError::new(message, locations))
}
#[doc(hidden)]
pub fn into_errors(mut self) -> Vec<RuleError> {
self.errors.sort();
self.errors
}
#[doc(hidden)]
pub fn with_pushed_type<F, R>(&mut self, t: Option<&Type>, f: F)
-> R
where F: FnOnce(&mut ValidatorContext<'a>) -> R
{
if let Some(t) = t {
self.type_stack.push(self.schema.concrete_type_by_name(t.innermost_name()));
}
else {
self.type_stack.push(None);
}
self.type_literal_stack.push(t.map(|t| t.clone()));
let res = f(self);
self.type_literal_stack.pop();
self.type_stack.pop();
res
}
#[doc(hidden)]
pub fn with_pushed_parent_type<F, R>(&mut self, f: F)
-> R
where F: FnOnce(&mut ValidatorContext<'a>) -> R
{
self.parent_type_stack.push(*self.type_stack.last().unwrap_or(&None));
let res = f(self);
self.parent_type_stack.pop();
res
}
#[doc(hidden)]
pub fn with_pushed_input_type<F, R>(&mut self, t: Option<&Type>, f: F)
-> R
where F: FnOnce(&mut ValidatorContext<'a>) -> R
{
if let Some(t) = t {
self.input_type_stack.push(self.schema.concrete_type_by_name(t.innermost_name()));
}
else {
self.input_type_stack.push(None);
}
self.input_type_literal_stack.push(t.map(|t| t.clone()));
let res = f(self);
self.input_type_literal_stack.pop();
self.input_type_stack.pop();
res
}
#[doc(hidden)]
pub fn current_type(&self) -> Option<&'a MetaType> {
*self.type_stack.last().unwrap_or(&None)
}
#[doc(hidden)]
pub fn current_type_literal(&self) -> Option<&Type> {
match self.type_literal_stack.last() {
Some(&Some(ref t)) => Some(t),
_ => None
}
}
#[doc(hidden)]
pub fn parent_type(&self) -> Option<&'a MetaType> {
*self.parent_type_stack.last().unwrap_or(&None)
}
#[doc(hidden)]
pub fn current_input_type_literal(&self) -> Option<&Type> {
match self.input_type_literal_stack.last() {
Some(&Some(ref t)) => Some(t),
_ => None,
}
}
#[doc(hidden)]
pub fn is_known_fragment(&self, name: &str) -> bool {
self.fragment_names.contains(name)
}
}

21
src/validation/mod.rs Normal file
View file

@ -0,0 +1,21 @@
//! Query validation related methods and data structures
mod visitor;
mod traits;
mod context;
mod multi_visitor;
mod rules;
#[cfg(test)]
mod test_harness;
pub use self::traits::Visitor;
pub use self::visitor::visit;
pub use self::context::{RuleError, ValidatorContext};
pub use self::rules::visit_all_rules;
pub use self::multi_visitor::MultiVisitor;
#[cfg(test)]
pub use self::test_harness::{
expect_passes_rule, expect_fails_rule,
expect_passes_rule_with_schema, expect_fails_rule_with_schema};

View file

@ -0,0 +1,160 @@
use ast::{Document, Operation, Fragment, VariableDefinition, Selection,
Directive, InputValue, Field, FragmentSpread, InlineFragment};
use parser::Spanning;
use validation::{ValidatorContext, Visitor};
#[doc(hidden)]
pub struct MultiVisitor<'a> {
visitors: Vec<Box<Visitor<'a> + 'a>>
}
impl<'a> MultiVisitor<'a> {
#[doc(hidden)]
pub fn new(visitors: Vec<Box<Visitor<'a> + 'a>>) -> MultiVisitor<'a> {
MultiVisitor {
visitors: visitors
}
}
fn visit_all<F: FnMut(&mut Box<Visitor<'a> + 'a>) -> ()>(&mut self, mut f: F) {
for mut v in self.visitors.iter_mut() {
f(v);
}
}
}
impl<'a> Visitor<'a> for MultiVisitor<'a> {
fn enter_document(&mut self, ctx: &mut ValidatorContext<'a>, doc: &'a Document) {
self.visit_all(|v| v.enter_document(ctx, doc));
}
fn exit_document(&mut self, ctx: &mut ValidatorContext<'a>, doc: &'a Document) {
self.visit_all(|v| v.exit_document(ctx, doc));
}
fn enter_operation_definition(&mut self, ctx: &mut ValidatorContext<'a>, op: &'a Spanning<Operation>) {
self.visit_all(|v| v.enter_operation_definition(ctx, op));
}
fn exit_operation_definition(&mut self, ctx: &mut ValidatorContext<'a>, op: &'a Spanning<Operation>) {
self.visit_all(|v| v.exit_operation_definition(ctx, op));
}
fn enter_fragment_definition(&mut self, ctx: &mut ValidatorContext<'a>, f: &'a Spanning<Fragment>) {
self.visit_all(|v| v.enter_fragment_definition(ctx, f));
}
fn exit_fragment_definition(&mut self, ctx: &mut ValidatorContext<'a>, f: &'a Spanning<Fragment>) {
self.visit_all(|v| v.exit_fragment_definition(ctx, f));
}
fn enter_variable_definition(&mut self, ctx: &mut ValidatorContext<'a>, def: &'a (Spanning<String>, VariableDefinition)) {
self.visit_all(|v| v.enter_variable_definition(ctx, def));
}
fn exit_variable_definition(&mut self, ctx: &mut ValidatorContext<'a>, def: &'a (Spanning<String>, VariableDefinition)) {
self.visit_all(|v| v.exit_variable_definition(ctx, def));
}
fn enter_directive(&mut self, ctx: &mut ValidatorContext<'a>, d: &'a Spanning<Directive>) {
self.visit_all(|v| v.enter_directive(ctx, d));
}
fn exit_directive(&mut self, ctx: &mut ValidatorContext<'a>, d: &'a Spanning<Directive>) {
self.visit_all(|v| v.exit_directive(ctx, d));
}
fn enter_argument(&mut self, ctx: &mut ValidatorContext<'a>, arg: &'a (Spanning<String>, Spanning<InputValue>)) {
self.visit_all(|v| v.enter_argument(ctx, arg));
}
fn exit_argument(&mut self, ctx: &mut ValidatorContext<'a>, arg: &'a (Spanning<String>, Spanning<InputValue>)) {
self.visit_all(|v| v.exit_argument(ctx, arg));
}
fn enter_selection_set(&mut self, ctx: &mut ValidatorContext<'a>, s: &'a Vec<Selection>) {
self.visit_all(|v| v.enter_selection_set(ctx, s));
}
fn exit_selection_set(&mut self, ctx: &mut ValidatorContext<'a>, s: &'a Vec<Selection>) {
self.visit_all(|v| v.exit_selection_set(ctx, s));
}
fn enter_field(&mut self, ctx: &mut ValidatorContext<'a>, f: &'a Spanning<Field>) {
self.visit_all(|v| v.enter_field(ctx, f));
}
fn exit_field(&mut self, ctx: &mut ValidatorContext<'a>, f: &'a Spanning<Field>) {
self.visit_all(|v| v.exit_field(ctx, f));
}
fn enter_fragment_spread(&mut self, ctx: &mut ValidatorContext<'a>, s: &'a Spanning<FragmentSpread>) {
self.visit_all(|v| v.enter_fragment_spread(ctx, s));
}
fn exit_fragment_spread(&mut self, ctx: &mut ValidatorContext<'a>, s: &'a Spanning<FragmentSpread>) {
self.visit_all(|v| v.exit_fragment_spread(ctx, s));
}
fn enter_inline_fragment(&mut self, ctx: &mut ValidatorContext<'a>, f: &'a Spanning<InlineFragment>) {
self.visit_all(|v| v.enter_inline_fragment(ctx, f));
}
fn exit_inline_fragment(&mut self, ctx: &mut ValidatorContext<'a>, f: &'a Spanning<InlineFragment>) {
self.visit_all(|v| v.exit_inline_fragment(ctx, f));
}
fn enter_int_value(&mut self, ctx: &mut ValidatorContext<'a>, i: Spanning<i64>) {
self.visit_all(|v| v.enter_int_value(ctx, i.clone()));
}
fn exit_int_value(&mut self, ctx: &mut ValidatorContext<'a>, i: Spanning<i64>) {
self.visit_all(|v| v.exit_int_value(ctx, i.clone()));
}
fn enter_float_value(&mut self, ctx: &mut ValidatorContext<'a>, f: Spanning<f64>) {
self.visit_all(|v| v.enter_float_value(ctx, f.clone()));
}
fn exit_float_value(&mut self, ctx: &mut ValidatorContext<'a>, f: Spanning<f64>) {
self.visit_all(|v| v.exit_float_value(ctx, f.clone()));
}
fn enter_string_value(&mut self, ctx: &mut ValidatorContext<'a>, s: Spanning<&'a String>) {
self.visit_all(|v| v.enter_string_value(ctx, s.clone()));
}
fn exit_string_value(&mut self, ctx: &mut ValidatorContext<'a>, s: Spanning<&'a String>) {
self.visit_all(|v| v.exit_string_value(ctx, s.clone()));
}
fn enter_boolean_value(&mut self, ctx: &mut ValidatorContext<'a>, b: Spanning<bool>) {
self.visit_all(|v| v.enter_boolean_value(ctx, b.clone()));
}
fn exit_boolean_value(&mut self, ctx: &mut ValidatorContext<'a>, b: Spanning<bool>) {
self.visit_all(|v| v.exit_boolean_value(ctx, b.clone()));
}
fn enter_enum_value(&mut self, ctx: &mut ValidatorContext<'a>, s: Spanning<&'a String>) {
self.visit_all(|v| v.enter_enum_value(ctx, s.clone()));
}
fn exit_enum_value(&mut self, ctx: &mut ValidatorContext<'a>, s: Spanning<&'a String>) {
self.visit_all(|v| v.exit_enum_value(ctx, s.clone()));
}
fn enter_variable_value(&mut self, ctx: &mut ValidatorContext<'a>, s: Spanning<&'a String>) {
self.visit_all(|v| v.enter_variable_value(ctx, s.clone()));
}
fn exit_variable_value(&mut self, ctx: &mut ValidatorContext<'a>, s: Spanning<&'a String>) {
self.visit_all(|v| v.exit_variable_value(ctx, s.clone()));
}
fn enter_list_value(&mut self, ctx: &mut ValidatorContext<'a>, l: Spanning<&'a Vec<Spanning<InputValue>>>) {
self.visit_all(|v| v.enter_list_value(ctx, l.clone()));
}
fn exit_list_value(&mut self, ctx: &mut ValidatorContext<'a>, l: Spanning<&'a Vec<Spanning<InputValue>>>) {
self.visit_all(|v| v.exit_list_value(ctx, l.clone()));
}
fn enter_object_value(&mut self, ctx: &mut ValidatorContext<'a>, o: Spanning<&'a Vec<(Spanning<String>, Spanning<InputValue>)>>) {
self.visit_all(|v| v.enter_object_value(ctx, o.clone()));
}
fn exit_object_value(&mut self, ctx: &mut ValidatorContext<'a>, o: Spanning<&'a Vec<(Spanning<String>, Spanning<InputValue>)>>) {
self.visit_all(|v| v.exit_object_value(ctx, o.clone()));
}
fn enter_object_field(&mut self, ctx: &mut ValidatorContext<'a>, f: &'a (Spanning<String>, Spanning<InputValue>)) {
self.visit_all(|v| v.enter_object_field(ctx, f));
}
fn exit_object_field(&mut self, ctx: &mut ValidatorContext<'a>, f: &'a (Spanning<String>, Spanning<InputValue>)) {
self.visit_all(|v| v.exit_object_field(ctx, f));
}
}

View file

@ -0,0 +1,913 @@
use ast::{Field, InputValue, Directive};
use schema::meta::Argument;
use types::utilities::is_valid_literal_value;
use parser::Spanning;
use validation::{Visitor, ValidatorContext};
pub struct ArgumentsOfCorrectType<'a> {
current_args: Option<&'a Vec<Argument>>,
}
pub fn factory<'a>() -> ArgumentsOfCorrectType<'a> {
ArgumentsOfCorrectType {
current_args: None,
}
}
impl<'a> Visitor<'a> for ArgumentsOfCorrectType<'a> {
fn enter_directive(&mut self, ctx: &mut ValidatorContext<'a>, directive: &'a Spanning<Directive>) {
self.current_args = ctx.schema
.directive_by_name(&directive.item.name.item)
.map(|d| &d.arguments);
}
fn exit_directive(&mut self, _: &mut ValidatorContext<'a>, _: &'a Spanning<Directive>) {
self.current_args = None;
}
fn enter_field(&mut self, ctx: &mut ValidatorContext<'a>, field: &'a Spanning<Field>) {
self.current_args = ctx.parent_type()
.and_then(|t| t.field_by_name(&field.item.name.item))
.and_then(|f| f.arguments.as_ref());
}
fn exit_field(&mut self, _: &mut ValidatorContext<'a>, _: &'a Spanning<Field>) {
self.current_args = None;
}
fn enter_argument(&mut self, ctx: &mut ValidatorContext<'a>, &(ref arg_name, ref arg_value): &'a (Spanning<String>, Spanning<InputValue>)) {
if let Some(argument_meta) = self.current_args
.and_then(|args| args.iter().filter(|a| a.name == arg_name.item).next())
{
let meta_type = ctx.schema.make_type(&argument_meta.arg_type);
if !is_valid_literal_value(&ctx.schema, &meta_type, &arg_value.item) {
ctx.report_error(
&error_message(&arg_name.item, &format!("{}", argument_meta.arg_type)),
&[arg_value.start.clone()]);
}
}
}
}
fn error_message(arg_name: &str, type_name: &str) -> String {
format!(
"Invalid value for argument \"{}\", expected type \"{}\"",
arg_name, type_name)
}
#[cfg(test)]
mod tests {
use super::{error_message, factory};
use parser::SourcePosition;
use validation::{RuleError, expect_passes_rule, expect_fails_rule};
#[test]
fn good_int_value() {
expect_passes_rule(factory, r#"
{
complicatedArgs {
intArgField(intArg: 2)
}
}
"#);
}
#[test]
fn good_boolean_value() {
expect_passes_rule(factory, r#"
{
complicatedArgs {
booleanArgField(booleanArg: true)
}
}
"#);
}
#[test]
fn good_string_value() {
expect_passes_rule(factory, r#"
{
complicatedArgs {
stringArgField(stringArg: "foo")
}
}
"#);
}
#[test]
fn good_float_value() {
expect_passes_rule(factory, r#"
{
complicatedArgs {
floatArgField(floatArg: 1.1)
}
}
"#);
}
#[test]
fn int_into_float() {
expect_passes_rule(factory, r#"
{
complicatedArgs {
floatArgField(floatArg: 1)
}
}
"#);
}
#[test]
fn int_into_id() {
expect_passes_rule(factory, r#"
{
complicatedArgs {
idArgField(idArg: 1)
}
}
"#);
}
#[test]
fn string_into_id() {
expect_passes_rule(factory, r#"
{
complicatedArgs {
idArgField(idArg: "someIdString")
}
}
"#);
}
#[test]
fn good_enum_value() {
expect_passes_rule(factory, r#"
{
dog {
doesKnowCommand(dogCommand: SIT)
}
}
"#);
}
#[test]
fn int_into_string() {
expect_fails_rule(factory, r#"
{
complicatedArgs {
stringArgField(stringArg: 1)
}
}
"#,
&[
RuleError::new(&error_message("stringArg", "String"), &[
SourcePosition::new(89, 3, 42),
])
]);
}
#[test]
fn float_into_string() {
expect_fails_rule(factory, r#"
{
complicatedArgs {
stringArgField(stringArg: 1.0)
}
}
"#,
&[
RuleError::new(&error_message("stringArg", "String"), &[
SourcePosition::new(89, 3, 42),
])
]);
}
#[test]
fn boolean_into_string() {
expect_fails_rule(factory, r#"
{
complicatedArgs {
stringArgField(stringArg: true)
}
}
"#,
&[
RuleError::new(&error_message("stringArg", "String"), &[
SourcePosition::new(89, 3, 42),
])
]);
}
#[test]
fn unquoted_string_into_string() {
expect_fails_rule(factory, r#"
{
complicatedArgs {
stringArgField(stringArg: BAR)
}
}
"#,
&[
RuleError::new(&error_message("stringArg", "String"), &[
SourcePosition::new(89, 3, 42),
])
]);
}
#[test]
fn string_into_int() {
expect_fails_rule(factory, r#"
{
complicatedArgs {
intArgField(intArg: "3")
}
}
"#,
&[
RuleError::new(&error_message("intArg", "Int"), &[
SourcePosition::new(83, 3, 36),
])
]);
}
#[test]
fn unquoted_string_into_int() {
expect_fails_rule(factory, r#"
{
complicatedArgs {
intArgField(intArg: FOO)
}
}
"#,
&[
RuleError::new(&error_message("intArg", "Int"), &[
SourcePosition::new(83, 3, 36),
])
]);
}
#[test]
fn simple_float_into_int() {
expect_fails_rule(factory, r#"
{
complicatedArgs {
intArgField(intArg: 3.0)
}
}
"#,
&[
RuleError::new(&error_message("intArg", "Int"), &[
SourcePosition::new(83, 3, 36),
])
]);
}
#[test]
fn float_into_int() {
expect_fails_rule(factory, r#"
{
complicatedArgs {
intArgField(intArg: 3.333)
}
}
"#,
&[
RuleError::new(&error_message("intArg", "Int"), &[
SourcePosition::new(83, 3, 36),
])
]);
}
#[test]
fn string_into_float() {
expect_fails_rule(factory, r#"
{
complicatedArgs {
floatArgField(floatArg: "3.333")
}
}
"#,
&[
RuleError::new(&error_message("floatArg", "Float"), &[
SourcePosition::new(87, 3, 40),
])
]);
}
#[test]
fn boolean_into_float() {
expect_fails_rule(factory, r#"
{
complicatedArgs {
floatArgField(floatArg: true)
}
}
"#,
&[
RuleError::new(&error_message("floatArg", "Float"), &[
SourcePosition::new(87, 3, 40),
])
]);
}
#[test]
fn unquoted_into_float() {
expect_fails_rule(factory, r#"
{
complicatedArgs {
floatArgField(floatArg: FOO)
}
}
"#,
&[
RuleError::new(&error_message("floatArg", "Float"), &[
SourcePosition::new(87, 3, 40),
])
]);
}
#[test]
fn int_into_boolean() {
expect_fails_rule(factory, r#"
{
complicatedArgs {
booleanArgField(booleanArg: 2)
}
}
"#,
&[
RuleError::new(&error_message("booleanArg", "Boolean"), &[
SourcePosition::new(91, 3, 44),
])
]);
}
#[test]
fn float_into_boolean() {
expect_fails_rule(factory, r#"
{
complicatedArgs {
booleanArgField(booleanArg: 1.0)
}
}
"#,
&[
RuleError::new(&error_message("booleanArg", "Boolean"), &[
SourcePosition::new(91, 3, 44),
])
]);
}
#[test]
fn string_into_boolean() {
expect_fails_rule(factory, r#"
{
complicatedArgs {
booleanArgField(booleanArg: "true")
}
}
"#,
&[
RuleError::new(&error_message("booleanArg", "Boolean"), &[
SourcePosition::new(91, 3, 44),
])
]);
}
#[test]
fn unquoted_into_boolean() {
expect_fails_rule(factory, r#"
{
complicatedArgs {
booleanArgField(booleanArg: TRUE)
}
}
"#,
&[
RuleError::new(&error_message("booleanArg", "Boolean"), &[
SourcePosition::new(91, 3, 44),
])
]);
}
#[test]
fn float_into_id() {
expect_fails_rule(factory, r#"
{
complicatedArgs {
idArgField(idArg: 1.0)
}
}
"#,
&[
RuleError::new(&error_message("idArg", "ID"), &[
SourcePosition::new(81, 3, 34),
])
]);
}
#[test]
fn boolean_into_id() {
expect_fails_rule(factory, r#"
{
complicatedArgs {
idArgField(idArg: true)
}
}
"#,
&[
RuleError::new(&error_message("idArg", "ID"), &[
SourcePosition::new(81, 3, 34),
])
]);
}
#[test]
fn unquoted_into_id() {
expect_fails_rule(factory, r#"
{
complicatedArgs {
idArgField(idArg: SOMETHING)
}
}
"#,
&[
RuleError::new(&error_message("idArg", "ID"), &[
SourcePosition::new(81, 3, 34),
])
]);
}
#[test]
fn int_into_enum() {
expect_fails_rule(factory, r#"
{
dog {
doesKnowCommand(dogCommand: 2)
}
}
"#,
&[
RuleError::new(&error_message("dogCommand", "DogCommand"), &[
SourcePosition::new(79, 3, 44),
])
]);
}
#[test]
fn float_into_enum() {
expect_fails_rule(factory, r#"
{
dog {
doesKnowCommand(dogCommand: 1.0)
}
}
"#,
&[
RuleError::new(&error_message("dogCommand", "DogCommand"), &[
SourcePosition::new(79, 3, 44),
])
]);
}
#[test]
fn string_into_enum() {
expect_fails_rule(factory, r#"
{
dog {
doesKnowCommand(dogCommand: "SIT")
}
}
"#,
&[
RuleError::new(&error_message("dogCommand", "DogCommand"), &[
SourcePosition::new(79, 3, 44),
])
]);
}
#[test]
fn boolean_into_enum() {
expect_fails_rule(factory, r#"
{
dog {
doesKnowCommand(dogCommand: true)
}
}
"#,
&[
RuleError::new(&error_message("dogCommand", "DogCommand"), &[
SourcePosition::new(79, 3, 44),
])
]);
}
#[test]
fn unknown_enum_value_into_enum() {
expect_fails_rule(factory, r#"
{
dog {
doesKnowCommand(dogCommand: JUGGLE)
}
}
"#,
&[
RuleError::new(&error_message("dogCommand", "DogCommand"), &[
SourcePosition::new(79, 3, 44),
])
]);
}
#[test]
fn different_case_enum_value_into_enum() {
expect_fails_rule(factory, r#"
{
dog {
doesKnowCommand(dogCommand: sit)
}
}
"#,
&[
RuleError::new(&error_message("dogCommand", "DogCommand"), &[
SourcePosition::new(79, 3, 44),
])
]);
}
#[test]
fn good_list_value() {
expect_passes_rule(factory, r#"
{
complicatedArgs {
stringListArgField(stringListArg: ["one", "two"])
}
}
"#);
}
#[test]
fn empty_list_value() {
expect_passes_rule(factory, r#"
{
complicatedArgs {
stringListArgField(stringListArg: [])
}
}
"#);
}
#[test]
fn single_value_into_list() {
expect_passes_rule(factory, r#"
{
complicatedArgs {
stringListArgField(stringListArg: "one")
}
}
"#);
}
#[test]
fn incorrect_item_type() {
expect_fails_rule(factory, r#"
{
complicatedArgs {
stringListArgField(stringListArg: ["one", 2])
}
}
"#,
&[
RuleError::new(&error_message("stringListArg", "[String]"), &[
SourcePosition::new(97, 3, 50),
])
]);
}
#[test]
fn single_value_of_incorrect_type() {
expect_fails_rule(factory, r#"
{
complicatedArgs {
stringListArgField(stringListArg: 1)
}
}
"#,
&[
RuleError::new(&error_message("stringListArg", "[String]"), &[
SourcePosition::new(97, 3, 50),
])
]);
}
#[test]
fn arg_on_optional_arg() {
expect_passes_rule(factory, r#"
{
dog {
isHousetrained(atOtherHomes: true)
}
}
"#);
}
#[test]
fn no_arg_on_optional_arg() {
expect_passes_rule(factory, r#"
{
dog {
isHousetrained
}
}
"#);
}
#[test]
fn multiple_args() {
expect_passes_rule(factory, r#"
{
complicatedArgs {
multipleReqs(req1: 1, req2: 2)
}
}
"#);
}
#[test]
fn multiple_args_reverse_order() {
expect_passes_rule(factory, r#"
{
complicatedArgs {
multipleReqs(req2: 2, req1: 1)
}
}
"#);
}
#[test]
fn no_args_on_multiple_optional() {
expect_passes_rule(factory, r#"
{
complicatedArgs {
multipleOpts
}
}
"#);
}
#[test]
fn one_arg_on_multiple_optional() {
expect_passes_rule(factory, r#"
{
complicatedArgs {
multipleOpts(opt1: 1)
}
}
"#);
}
#[test]
fn second_arg_on_multiple_optional() {
expect_passes_rule(factory, r#"
{
complicatedArgs {
multipleOpts(opt2: 1)
}
}
"#);
}
#[test]
fn multiple_reqs_on_mixed_list() {
expect_passes_rule(factory, r#"
{
complicatedArgs {
multipleOptAndReq(req1: 3, req2: 4)
}
}
"#);
}
#[test]
fn multiple_reqs_and_one_opt_on_mixed_list() {
expect_passes_rule(factory, r#"
{
complicatedArgs {
multipleOptAndReq(req1: 3, req2: 4, opt1: 5)
}
}
"#);
}
#[test]
fn all_reqs_and_opts_on_mixed_list() {
expect_passes_rule(factory, r#"
{
complicatedArgs {
multipleOptAndReq(req1: 3, req2: 4, opt1: 5, opt2: 6)
}
}
"#);
}
#[test]
fn incorrect_value_type() {
expect_fails_rule(factory, r#"
{
complicatedArgs {
multipleReqs(req2: "two", req1: "one")
}
}
"#,
&[
RuleError::new(&error_message("req2", "Int!"), &[
SourcePosition::new(82, 3, 35),
]),
RuleError::new(&error_message("req1", "Int!"), &[
SourcePosition::new(95, 3, 48),
]),
]);
}
#[test]
fn incorrect_value_and_missing_argument() {
expect_fails_rule(factory, r#"
{
complicatedArgs {
multipleReqs(req1: "one")
}
}
"#,
&[
RuleError::new(&error_message("req1", "Int!"), &[
SourcePosition::new(82, 3, 35),
]),
]);
}
#[test]
fn optional_arg_despite_required_field_in_type() {
expect_passes_rule(factory, r#"
{
complicatedArgs {
complexArgField
}
}
"#);
}
#[test]
fn partial_object_only_required() {
expect_passes_rule(factory, r#"
{
complicatedArgs {
complexArgField(complexArg: { requiredField: true })
}
}
"#);
}
#[test]
fn partial_object_required_field_can_be_falsy() {
expect_passes_rule(factory, r#"
{
complicatedArgs {
complexArgField(complexArg: { requiredField: false })
}
}
"#);
}
#[test]
fn partial_object_including_required() {
expect_passes_rule(factory, r#"
{
complicatedArgs {
complexArgField(complexArg: { requiredField: true, intField: 4 })
}
}
"#);
}
#[test]
fn full_object() {
expect_passes_rule(factory, r#"
{
complicatedArgs {
complexArgField(complexArg: {
requiredField: true,
intField: 4,
stringField: "foo",
booleanField: false,
stringListField: ["one", "two"]
})
}
}
"#);
}
#[test]
fn full_object_with_fields_in_different_order() {
expect_passes_rule(factory, r#"
{
complicatedArgs {
complexArgField(complexArg: {
stringListField: ["one", "two"],
booleanField: false,
requiredField: true,
stringField: "foo",
intField: 4,
})
}
}
"#);
}
#[test]
fn partial_object_missing_required() {
expect_fails_rule(factory, r#"
{
complicatedArgs {
complexArgField(complexArg: { intField: 4 })
}
}
"#,
&[
RuleError::new(&error_message("complexArg", "ComplexInput"), &[
SourcePosition::new(91, 3, 44),
]),
]);
}
#[test]
fn partial_object_invalid_field_type() {
expect_fails_rule(factory, r#"
{
complicatedArgs {
complexArgField(complexArg: {
stringListField: ["one", 2],
requiredField: true,
})
}
}
"#,
&[
RuleError::new(&error_message("complexArg", "ComplexInput"), &[
SourcePosition::new(91, 3, 44),
]),
]);
}
#[test]
fn partial_object_unknown_field_arg() {
expect_fails_rule(factory, r#"
{
complicatedArgs {
complexArgField(complexArg: {
requiredField: true,
unknownField: "value"
})
}
}
"#,
&[
RuleError::new(&error_message("complexArg", "ComplexInput"), &[
SourcePosition::new(91, 3, 44),
]),
]);
}
#[test]
fn directive_with_valid_types() {
expect_passes_rule(factory, r#"
{
dog @include(if: true) {
name
}
human @skip(if: false) {
name
}
}
"#);
}
#[test]
fn directive_with_incorrect_types() {
expect_fails_rule(factory, r#"
{
dog @include(if: "yes") {
name @skip(if: ENUM)
}
}
"#,
&[
RuleError::new(&error_message("if", "Boolean!"), &[
SourcePosition::new(38, 2, 27),
]),
RuleError::new(&error_message("if", "Boolean!"), &[
SourcePosition::new(74, 3, 27),
]),
]);
}
}

View file

@ -0,0 +1,154 @@
use ast::VariableDefinition;
use types::utilities::is_valid_literal_value;
use parser::Spanning;
use validation::{Visitor, ValidatorContext};
pub struct DefaultValuesOfCorrectType {
}
pub fn factory() -> DefaultValuesOfCorrectType {
DefaultValuesOfCorrectType {
}
}
impl<'a> Visitor<'a> for DefaultValuesOfCorrectType {
fn enter_variable_definition(&mut self, ctx: &mut ValidatorContext<'a>, &(ref var_name, ref var_def): &'a (Spanning<String>, VariableDefinition)) {
if let Some(Spanning { item: ref var_value, ref start, .. }) = var_def.default_value {
if var_def.var_type.item.is_non_null() {
ctx.report_error(
&non_null_error_message(&var_name.item, &format!("{}", var_def.var_type.item)),
&[start.clone()])
}
else {
let meta_type = ctx.schema.make_type(&var_def.var_type.item);
if !is_valid_literal_value(&ctx.schema, &meta_type, var_value) {
ctx.report_error(
&type_error_message(&var_name.item, &format!("{}", var_def.var_type.item)),
&[start.clone()]);
}
}
}
}
}
fn type_error_message(arg_name: &str, type_name: &str) -> String {
format!(
"Invalid default value for argument \"{}\", expected type \"{}\"",
arg_name, type_name)
}
fn non_null_error_message(arg_name: &str, type_name: &str) -> String {
format!(
"Argument \"{}\" has type \"{}\" and is not nullable, so it't can't have a default value",
arg_name, type_name)
}
#[cfg(test)]
mod tests {
use super::{type_error_message, non_null_error_message, factory};
use parser::SourcePosition;
use validation::{RuleError, expect_passes_rule, expect_fails_rule};
#[test]
fn variables_with_no_default_values() {
expect_passes_rule(factory, r#"
query NullableValues($a: Int, $b: String, $c: ComplexInput) {
dog { name }
}
"#);
}
#[test]
fn required_variables_without_default_values() {
expect_passes_rule(factory, r#"
query RequiredValues($a: Int!, $b: String!) {
dog { name }
}
"#);
}
#[test]
fn variables_with_valid_default_values() {
expect_passes_rule(factory, r#"
query WithDefaultValues(
$a: Int = 1,
$b: String = "ok",
$c: ComplexInput = { requiredField: true, intField: 3 }
) {
dog { name }
}
"#);
}
#[test]
fn no_required_variables_with_default_values() {
expect_fails_rule(factory, r#"
query UnreachableDefaultValues($a: Int! = 3, $b: String! = "default") {
dog { name }
}
"#,
&[
RuleError::new(&non_null_error_message("a", "Int!"), &[
SourcePosition::new(53, 1, 52),
]),
RuleError::new(&non_null_error_message("b", "String!"), &[
SourcePosition::new(70, 1, 69),
]),
]);
}
#[test]
fn variables_with_invalid_default_values() {
expect_fails_rule(factory, r#"
query InvalidDefaultValues(
$a: Int = "one",
$b: String = 4,
$c: ComplexInput = "notverycomplex"
) {
dog { name }
}
"#,
&[
RuleError::new(&type_error_message("a", "Int"), &[
SourcePosition::new(61, 2, 22),
]),
RuleError::new(&type_error_message("b", "String"), &[
SourcePosition::new(93, 3, 25),
]),
RuleError::new(&type_error_message("c", "ComplexInput"), &[
SourcePosition::new(127, 4, 31),
]),
]);
}
#[test]
fn complex_variables_missing_required_field() {
expect_fails_rule(factory, r#"
query MissingRequiredField($a: ComplexInput = {intField: 3}) {
dog { name }
}
"#,
&[
RuleError::new(&type_error_message("a", "ComplexInput"), &[
SourcePosition::new(57, 1, 56),
]),
]);
}
#[test]
fn list_variables_with_invalid_item() {
expect_fails_rule(factory, r#"
query InvalidItem($a: [String] = ["one", 2]) {
dog { name }
}
"#,
&[
RuleError::new(&type_error_message("a", "[String]"), &[
SourcePosition::new(44, 1, 43),
]),
]);
}
}

View file

@ -0,0 +1,272 @@
use ast::Field;
use validation::{Visitor, ValidatorContext};
use parser::Spanning;
pub struct FieldsOnCorrectType {}
pub fn factory() -> FieldsOnCorrectType {
FieldsOnCorrectType {}
}
impl<'a> Visitor<'a> for FieldsOnCorrectType {
fn enter_field(&mut self, context: &mut ValidatorContext<'a>, field: &'a Spanning<Field>) {
{
if let Some(parent_type) = context.parent_type() {
let field_name = &field.item.name;
let type_name = parent_type.name().clone().unwrap_or("<unknown>");
if parent_type.field_by_name(&field_name.item).is_none() {
context.report_error(
&error_message(&field_name.item, &type_name),
&[field_name.start.clone()]);
}
}
}
}
}
fn error_message(field: &str, type_name: &str) -> String {
format!(r#"Unknown field "{}" on type "{}""#, field, type_name)
}
#[cfg(test)]
mod tests {
use super::{error_message, factory};
use parser::SourcePosition;
use validation::{RuleError, expect_passes_rule, expect_fails_rule};
#[test]
fn selection_on_object() {
expect_passes_rule(factory, r#"
fragment objectFieldSelection on Dog {
__typename
name
}
"#);
}
#[test]
fn aliased_selection_on_object() {
expect_passes_rule(factory, r#"
fragment aliasedObjectFieldSelection on Dog {
tn : __typename
otherName : name
}
"#);
}
#[test]
fn selection_on_interface() {
expect_passes_rule(factory, r#"
fragment interfaceFieldSelection on Pet {
__typename
name
}
"#);
}
#[test]
fn aliased_selection_on_interface() {
expect_passes_rule(factory, r#"
fragment interfaceFieldSelection on Pet {
otherName : name
}
"#);
}
#[test]
fn lying_alias_selection() {
expect_passes_rule(factory, r#"
fragment lyingAliasSelection on Dog {
name : nickname
}
"#);
}
#[test]
fn ignores_unknown_type() {
expect_passes_rule(factory, r#"
fragment unknownSelection on UnknownType {
unknownField
}
"#);
}
#[test]
fn nested_unknown_fields() {
expect_fails_rule(factory, r#"
fragment typeKnownAgain on Pet {
unknown_pet_field {
... on Cat {
unknown_cat_field
}
}
}
"#,
&[
RuleError::new(&error_message("unknown_pet_field", "Pet"), &[
SourcePosition::new(56, 2, 12)
]),
RuleError::new(&error_message("unknown_cat_field", "Cat"), &[
SourcePosition::new(119, 4, 16)
]),
]);
}
#[test]
fn unknown_field_on_fragment() {
expect_fails_rule(factory, r#"
fragment fieldNotDefined on Dog {
meowVolume
}
"#,
&[
RuleError::new(&error_message("meowVolume", "Dog"), &[
SourcePosition::new(57, 2, 12)
]),
]);
}
#[test]
fn ignores_deeply_unknown_field() {
expect_fails_rule(factory, r#"
fragment deepFieldNotDefined on Dog {
unknown_field {
deeper_unknown_field
}
}
"#,
&[
RuleError::new(&error_message("unknown_field", "Dog"), &[
SourcePosition::new(61, 2, 12)
]),
]);
}
#[test]
fn unknown_subfield() {
expect_fails_rule(factory, r#"
fragment subFieldNotDefined on Human {
pets {
unknown_field
}
}
"#,
&[
RuleError::new(&error_message("unknown_field", "Pet"), &[
SourcePosition::new(83, 3, 14)
]),
]);
}
#[test]
fn unknown_field_on_inline_fragment() {
expect_fails_rule(factory, r#"
fragment fieldNotDefined on Pet {
... on Dog {
meowVolume
}
}
"#,
&[
RuleError::new(&error_message("meowVolume", "Dog"), &[
SourcePosition::new(84, 3, 14)
]),
]);
}
#[test]
fn unknown_aliased_target() {
expect_fails_rule(factory, r#"
fragment aliasedFieldTargetNotDefined on Dog {
volume : mooVolume
}
"#,
&[
RuleError::new(&error_message("mooVolume", "Dog"), &[
SourcePosition::new(79, 2, 21)
]),
]);
}
#[test]
fn unknown_aliased_lying_field_target() {
expect_fails_rule(factory, r#"
fragment aliasedLyingFieldTargetNotDefined on Dog {
barkVolume : kawVolume
}
"#,
&[
RuleError::new(&error_message("kawVolume", "Dog"), &[
SourcePosition::new(88, 2, 25)
]),
]);
}
#[test]
fn not_defined_on_interface() {
expect_fails_rule(factory, r#"
fragment notDefinedOnInterface on Pet {
tailLength
}
"#,
&[
RuleError::new(&error_message("tailLength", "Pet"), &[
SourcePosition::new(63, 2, 12)
]),
]);
}
#[test]
fn defined_in_concrete_types_but_not_interface() {
expect_fails_rule(factory, r#"
fragment definedOnImplementorsButNotInterface on Pet {
nickname
}
"#,
&[
RuleError::new(&error_message("nickname", "Pet"), &[
SourcePosition::new(78, 2, 12)
]),
]);
}
#[test]
fn meta_field_on_union() {
expect_passes_rule(factory, r#"
fragment definedOnImplementorsButNotInterface on Pet {
__typename
}
"#);
}
#[test]
fn fields_on_union() {
expect_fails_rule(factory, r#"
fragment definedOnImplementorsQueriedOnUnion on CatOrDog {
name
}
"#,
&[
RuleError::new(&error_message("name", "CatOrDog"), &[
SourcePosition::new(82, 2, 12)
]),
]);
}
#[test]
fn valid_field_in_inline_fragment() {
expect_passes_rule(factory, r#"
fragment objectFieldSelection on Pet {
... on Dog {
name
}
... {
name
}
}
"#);
}
}

View file

@ -0,0 +1,173 @@
use ast::{Fragment, InlineFragment};
use parser::Spanning;
use validation::{Visitor, ValidatorContext};
pub struct FragmentsOnCompositeTypes {}
pub fn factory() -> FragmentsOnCompositeTypes {
FragmentsOnCompositeTypes {}
}
impl<'a> Visitor<'a> for FragmentsOnCompositeTypes {
fn enter_fragment_definition(&mut self, context: &mut ValidatorContext<'a>, f: &'a Spanning<Fragment>) {
{
if let Some(current_type) = context.current_type() {
if !current_type.is_composite() {
let type_name = current_type.name().clone().unwrap_or("<unknown>");
let type_cond = &f.item.type_condition;
context.report_error(
&error_message(
Some(&f.item.name.item.clone()),
&type_name),
&[type_cond.start.clone()]);
}
}
}
}
fn enter_inline_fragment(&mut self, context: &mut ValidatorContext<'a>, f: &'a Spanning<InlineFragment>) {
{
if let Some(ref type_cond) = f.item.type_condition {
let invalid_type_name = context.current_type().iter()
.filter(|&t| !t.is_composite())
.map(|t| t.name().clone().unwrap_or("<unknown>"))
.next();
if let Some(name) = invalid_type_name {
context.report_error(
&error_message(None, &name),
&[type_cond.start.clone()]);
}
}
}
}
}
fn error_message(fragment_name: Option<&str>, on_type: &str) -> String {
if let Some(name) = fragment_name {
format!(
r#"Fragment "{}" cannot condition non composite type "{}"#,
name, on_type)
}
else {
format!(
r#"Fragment cannot condition on non composite type "{}""#,
on_type)
}
}
#[cfg(test)]
mod tests {
use super::{error_message, factory};
use parser::SourcePosition;
use validation::{RuleError, expect_passes_rule, expect_fails_rule};
#[test]
fn on_object() {
expect_passes_rule(factory, r#"
fragment validFragment on Dog {
barks
}
"#);
}
#[test]
fn on_interface() {
expect_passes_rule(factory, r#"
fragment validFragment on Pet {
name
}
"#);
}
#[test]
fn on_object_inline() {
expect_passes_rule(factory, r#"
fragment validFragment on Pet {
... on Dog {
barks
}
}
"#);
}
#[test]
fn on_inline_without_type_cond() {
expect_passes_rule(factory, r#"
fragment validFragment on Pet {
... {
name
}
}
"#);
}
#[test]
fn on_union() {
expect_passes_rule(factory, r#"
fragment validFragment on CatOrDog {
__typename
}
"#);
}
#[test]
fn not_on_scalar() {
expect_fails_rule(factory, r#"
fragment scalarFragment on Boolean {
bad
}
"#,
&[
RuleError::new(&error_message(Some("scalarFragment"), "Boolean"), &[
SourcePosition::new(38, 1, 37),
]),
]);
}
#[test]
fn not_on_enum() {
expect_fails_rule(factory, r#"
fragment scalarFragment on FurColor {
bad
}
"#,
&[
RuleError::new(&error_message(Some("scalarFragment"), "FurColor"), &[
SourcePosition::new(38, 1, 37),
]),
]);
}
#[test]
fn not_on_input_object() {
expect_fails_rule(factory, r#"
fragment inputFragment on ComplexInput {
stringField
}
"#,
&[
RuleError::new(&error_message(Some("inputFragment"), "ComplexInput"), &[
SourcePosition::new(37, 1, 36),
]),
]);
}
#[test]
fn not_on_scalar_inline() {
expect_fails_rule(factory, r#"
fragment invalidFragment on Pet {
... on String {
barks
}
}
"#,
&[
RuleError::new(&error_message(None, "String"), &[
SourcePosition::new(64, 2, 19),
]),
]);
}
}

View file

@ -0,0 +1,228 @@
use ast::{Field, InputValue, Directive};
use schema::meta::Argument;
use parser::Spanning;
use validation::{ValidatorContext, Visitor};
#[derive(Debug)]
enum ArgumentPosition<'a> {
Directive(&'a str),
Field(&'a str, &'a str),
}
pub struct KnownArgumentNames<'a> {
current_args: Option<(ArgumentPosition<'a>, &'a Vec<Argument>)>,
}
pub fn factory<'a>() -> KnownArgumentNames<'a> {
KnownArgumentNames {
current_args: None,
}
}
impl<'a> Visitor<'a> for KnownArgumentNames<'a> {
fn enter_directive(&mut self, ctx: &mut ValidatorContext<'a>, directive: &'a Spanning<Directive>) {
self.current_args = ctx.schema
.directive_by_name(&directive.item.name.item)
.map(|d| (ArgumentPosition::Directive(&directive.item.name.item), &d.arguments));
}
fn exit_directive(&mut self, _: &mut ValidatorContext<'a>, _: &'a Spanning<Directive>) {
self.current_args = None;
}
fn enter_field(&mut self, ctx: &mut ValidatorContext<'a>, field: &'a Spanning<Field>) {
self.current_args = ctx.parent_type()
.and_then(|t| t.field_by_name(&field.item.name.item))
.and_then(|f| f.arguments.as_ref())
.map(|args| (
ArgumentPosition::Field(
&field.item.name.item,
&ctx.parent_type().expect("Parent type should exist")
.name().expect("Parent type should be named")),
args));
}
fn exit_field(&mut self, _: &mut ValidatorContext<'a>, _: &'a Spanning<Field>) {
self.current_args = None;
}
fn enter_argument(&mut self, ctx: &mut ValidatorContext<'a>, &(ref arg_name, _): &'a (Spanning<String>, Spanning<InputValue>)) {
if let Some((ref pos, args)) = self.current_args {
if args.iter().filter(|a| a.name == arg_name.item).next().is_none() {
let message = match *pos {
ArgumentPosition::Field(ref field_name, ref type_name) =>
field_error_message(&arg_name.item, field_name, type_name),
ArgumentPosition::Directive(ref directive_name) =>
directive_error_message(&arg_name.item, directive_name),
};
ctx.report_error(
&message,
&[arg_name.start.clone()]);
}
}
}
}
fn field_error_message(arg_name: &str, field_name: &str, type_name: &str) -> String {
format!(
r#"Unknown argument "{}" on field "{}" of type "{}""#,
arg_name, field_name, type_name)
}
fn directive_error_message(arg_name: &str, directive_name: &str) -> String {
format!(
r#"Unknown argument "{}" on directive "{}""#,
arg_name, directive_name)
}
#[cfg(test)]
mod tests {
use super::{field_error_message, directive_error_message, factory};
use parser::SourcePosition;
use validation::{RuleError, expect_passes_rule, expect_fails_rule};
#[test]
fn single_arg_is_known() {
expect_passes_rule(factory, r#"
fragment argOnRequiredArg on Dog {
doesKnowCommand(dogCommand: SIT)
}
"#);
}
#[test]
fn multiple_args_are_known() {
expect_passes_rule(factory, r#"
fragment multipleArgs on ComplicatedArgs {
multipleReqs(req1: 1, req2: 2)
}
"#);
}
#[test]
fn ignores_args_of_unknown_fields() {
expect_passes_rule(factory, r#"
fragment argOnUnknownField on Dog {
unknownField(unknownArg: SIT)
}
"#);
}
#[test]
fn multiple_args_in_reverse_order_are_known() {
expect_passes_rule(factory, r#"
fragment multipleArgsReverseOrder on ComplicatedArgs {
multipleReqs(req2: 2, req1: 1)
}
"#);
}
#[test]
fn no_args_on_optional_arg() {
expect_passes_rule(factory, r#"
fragment noArgOnOptionalArg on Dog {
isHousetrained
}
"#);
}
#[test]
fn args_are_known_deeply() {
expect_passes_rule(factory, r#"
{
dog {
doesKnowCommand(dogCommand: SIT)
}
human {
pet {
... on Dog {
doesKnowCommand(dogCommand: SIT)
}
}
}
}
"#);
}
#[test]
fn directive_args_are_known() {
expect_passes_rule(factory, r#"
{
dog @skip(if: true)
}
"#);
}
#[test]
fn undirective_args_are_invalid() {
expect_fails_rule(factory, r#"
{
dog @skip(unless: true)
}
"#,
&[
RuleError::new(&directive_error_message("unless", "skip"), &[
SourcePosition::new(35, 2, 22),
]),
]);
}
#[test]
fn invalid_arg_name() {
expect_fails_rule(factory, r#"
fragment invalidArgName on Dog {
doesKnowCommand(unknown: true)
}
"#,
&[
RuleError::new(&field_error_message("unknown", "doesKnowCommand", "Dog"), &[
SourcePosition::new(72, 2, 28),
]),
]);
}
#[test]
fn unknown_args_amongst_known_args() {
expect_fails_rule(factory, r#"
fragment oneGoodArgOneInvalidArg on Dog {
doesKnowCommand(whoknows: 1, dogCommand: SIT, unknown: true)
}
"#,
&[
RuleError::new(&field_error_message("whoknows", "doesKnowCommand", "Dog"), &[
SourcePosition::new(81, 2, 28),
]),
RuleError::new(&field_error_message("unknown", "doesKnowCommand", "Dog"), &[
SourcePosition::new(111, 2, 58),
]),
]);
}
#[test]
fn unknown_args_deeply() {
expect_fails_rule(factory, r#"
{
dog {
doesKnowCommand(unknown: true)
}
human {
pet {
... on Dog {
doesKnowCommand(unknown: true)
}
}
}
}
"#,
&[
RuleError::new(&field_error_message("unknown", "doesKnowCommand", "Dog"), &[
SourcePosition::new(61, 3, 30),
]),
RuleError::new(&field_error_message("unknown", "doesKnowCommand", "Dog"), &[
SourcePosition::new(193, 8, 34),
]),
]);
}
}

View file

@ -0,0 +1,216 @@
use ast::{Directive, Operation, OperationType, Fragment, FragmentSpread, Field, InlineFragment};
use validation::{ValidatorContext, Visitor};
use schema::model::DirectiveLocation;
use parser::Spanning;
pub struct KnownDirectives {
location_stack: Vec<DirectiveLocation>,
}
pub fn factory() -> KnownDirectives {
KnownDirectives {
location_stack: Vec::new(),
}
}
impl<'a> Visitor<'a> for KnownDirectives {
fn enter_operation_definition(&mut self, _: &mut ValidatorContext<'a>, op: &'a Spanning<Operation>) {
self.location_stack.push(match op.item.operation_type {
OperationType::Query => DirectiveLocation::Query,
OperationType::Mutation => DirectiveLocation::Mutation,
});
}
fn exit_operation_definition(&mut self, _: &mut ValidatorContext<'a>, _: &'a Spanning<Operation>) {
let top = self.location_stack.pop();
assert!(top == Some(DirectiveLocation::Query) || top == Some(DirectiveLocation::Mutation));
}
fn enter_field(&mut self, _: &mut ValidatorContext<'a>, _: &'a Spanning<Field>) {
self.location_stack.push(DirectiveLocation::Field);
}
fn exit_field(&mut self, _: &mut ValidatorContext<'a>, _: &'a Spanning<Field>) {
let top = self.location_stack.pop();
assert_eq!(top, Some(DirectiveLocation::Field));
}
fn enter_fragment_definition(&mut self, _: &mut ValidatorContext<'a>, _: &'a Spanning<Fragment>) {
self.location_stack.push(DirectiveLocation::FragmentDefinition);
}
fn exit_fragment_definition(&mut self, _: &mut ValidatorContext<'a>, _: &'a Spanning<Fragment>) {
let top = self.location_stack.pop();
assert_eq!(top, Some(DirectiveLocation::FragmentDefinition));
}
fn enter_fragment_spread(&mut self, _: &mut ValidatorContext<'a>, _: &'a Spanning<FragmentSpread>) {
self.location_stack.push(DirectiveLocation::FragmentSpread);
}
fn exit_fragment_spread(&mut self, _: &mut ValidatorContext<'a>, _: &'a Spanning<FragmentSpread>) {
let top = self.location_stack.pop();
assert_eq!(top, Some(DirectiveLocation::FragmentSpread));
}
fn enter_inline_fragment(&mut self, _: &mut ValidatorContext<'a>, _: &'a Spanning<InlineFragment>) {
self.location_stack.push(DirectiveLocation::InlineFragment);
}
fn exit_inline_fragment(&mut self, _: &mut ValidatorContext<'a>, _: &'a Spanning<InlineFragment>) {
let top = self.location_stack.pop();
assert_eq!(top, Some(DirectiveLocation::InlineFragment));
}
fn enter_directive(&mut self, ctx: &mut ValidatorContext<'a>, directive: &'a Spanning<Directive>) {
let directive_name = &directive.item.name.item;
if let Some(directive_type) = ctx.schema.directive_by_name(directive_name) {
if let Some(current_location) = self.location_stack.last() {
if directive_type.locations.iter().filter(|l| l == &current_location).next().is_none() {
ctx.report_error(
&misplaced_error_message(directive_name, current_location),
&[directive.start.clone()]);
}
}
}
else {
ctx.report_error(
&unknown_error_message(directive_name),
&[directive.start.clone()]);
}
}
}
fn unknown_error_message(directive_name: &str) -> String {
format!(r#"Unknown directive "{}""#, directive_name)
}
fn misplaced_error_message(directive_name: &str, location: &DirectiveLocation) -> String {
format!(r#"Directive "{}" may not be used on {}"#, directive_name, location)
}
#[cfg(test)]
mod tests {
use super::{unknown_error_message, misplaced_error_message, factory};
use parser::SourcePosition;
use validation::{RuleError, expect_passes_rule, expect_fails_rule};
use schema::model::DirectiveLocation;
#[test]
fn with_no_directives() {
expect_passes_rule(factory, r#"
query Foo {
name
...Frag
}
fragment Frag on Dog {
name
}
"#);
}
#[test]
fn with_known_directives() {
expect_passes_rule(factory, r#"
{
dog @include(if: true) {
name
}
human @skip(if: false) {
name
}
}
"#);
}
#[test]
fn with_unknown_directive() {
expect_fails_rule(factory, r#"
{
dog @unknown(directive: "value") {
name
}
}
"#,
&[
RuleError::new(&unknown_error_message("unknown"), &[
SourcePosition::new(29, 2, 16),
]),
]);
}
#[test]
fn with_many_unknown_directives() {
expect_fails_rule(factory, r#"
{
dog @unknown(directive: "value") {
name
}
human @unknown(directive: "value") {
name
pets @unknown(directive: "value") {
name
}
}
}
"#,
&[
RuleError::new(&unknown_error_message("unknown"), &[
SourcePosition::new(29, 2, 16),
]),
RuleError::new(&unknown_error_message("unknown"), &[
SourcePosition::new(111, 5, 18),
]),
RuleError::new(&unknown_error_message("unknown"), &[
SourcePosition::new(180, 7, 19),
]),
]);
}
#[test]
fn with_well_placed_directives() {
expect_passes_rule(factory, r#"
query Foo @onQuery {
name @include(if: true)
...Frag @include(if: true)
skippedField @skip(if: true)
...SkippedFrag @skip(if: true)
}
mutation Bar @onMutation {
someField
}
"#);
}
#[test]
fn with_misplaced_directives() {
expect_fails_rule(factory, r#"
query Foo @include(if: true) {
name @onQuery
...Frag @onQuery
}
mutation Bar @onQuery {
someField
}
"#,
&[
RuleError::new(&misplaced_error_message("include", &DirectiveLocation::Query), &[
SourcePosition::new(21, 1, 20),
]),
RuleError::new(&misplaced_error_message("onQuery", &DirectiveLocation::Field), &[
SourcePosition::new(59, 2, 17),
]),
RuleError::new(&misplaced_error_message("onQuery", &DirectiveLocation::FragmentSpread), &[
SourcePosition::new(88, 3, 20),
]),
RuleError::new(&misplaced_error_message("onQuery", &DirectiveLocation::Mutation), &[
SourcePosition::new(133, 6, 23),
]),
]);
}
}

View file

@ -0,0 +1,88 @@
use ast::FragmentSpread;
use validation::{ValidatorContext, Visitor};
use parser::Spanning;
pub struct KnownFragmentNames {}
pub fn factory() -> KnownFragmentNames {
KnownFragmentNames {}
}
impl<'a> Visitor<'a> for KnownFragmentNames {
fn enter_fragment_spread(&mut self, context: &mut ValidatorContext<'a>, spread: &'a Spanning<FragmentSpread>) {
let spread_name = &spread.item.name;
if !context.is_known_fragment(&spread_name.item) {
context.report_error(
&error_message(&spread_name.item),
&[spread_name.start.clone()]);
}
}
}
fn error_message(frag_name: &str) -> String {
format!(r#"Unknown fragment: "{}""#, frag_name)
}
#[cfg(test)]
mod tests {
use super::{error_message, factory};
use parser::SourcePosition;
use validation::{RuleError, expect_passes_rule, expect_fails_rule};
#[test]
fn known() {
expect_passes_rule(factory, r#"
{
human(id: 4) {
...HumanFields1
... on Human {
...HumanFields2
}
... {
name
}
}
}
fragment HumanFields1 on Human {
name
...HumanFields3
}
fragment HumanFields2 on Human {
name
}
fragment HumanFields3 on Human {
name
}
"#);
}
#[test]
fn unknown() {
expect_fails_rule(factory, r#"
{
human(id: 4) {
...UnknownFragment1
... on Human {
...UnknownFragment2
}
}
}
fragment HumanFields on Human {
name
...UnknownFragment3
}
"#,
&[
RuleError::new(&error_message("UnknownFragment1"), &[
SourcePosition::new(57, 3, 17),
]),
RuleError::new(&error_message("UnknownFragment2"), &[
SourcePosition::new(122, 5, 19),
]),
RuleError::new(&error_message("UnknownFragment3"), &[
SourcePosition::new(255, 11, 15),
]),
]);
}
}

View file

@ -0,0 +1,87 @@
use ast::{Fragment, InlineFragment, VariableDefinition};
use validation::{ValidatorContext, Visitor};
use parser::{SourcePosition, Spanning};
pub struct KnownTypeNames {}
pub fn factory() -> KnownTypeNames {
KnownTypeNames {}
}
impl<'a> Visitor<'a> for KnownTypeNames {
fn enter_inline_fragment(&mut self, ctx: &mut ValidatorContext<'a>, fragment: &'a Spanning<InlineFragment>) {
if let Some(ref type_cond) = fragment.item.type_condition {
validate_type(ctx, &type_cond.item, &type_cond.start);
}
}
fn enter_fragment_definition(&mut self, ctx: &mut ValidatorContext<'a>, fragment: &'a Spanning<Fragment>) {
let type_cond = &fragment.item.type_condition;
validate_type(ctx, &type_cond.item, &type_cond.start);
}
fn enter_variable_definition(&mut self, ctx: &mut ValidatorContext<'a>, &(_, ref var_def): &'a (Spanning<String>, VariableDefinition)) {
let type_name = var_def.var_type.item.innermost_name();
validate_type(ctx, &type_name, &var_def.var_type.start);
}
}
fn validate_type<'a>(ctx: &mut ValidatorContext<'a>, type_name: &str, location: &SourcePosition) {
if ctx.schema.type_by_name(type_name).is_none() {
ctx.report_error(
&error_message(type_name),
&[location.clone()]);
}
}
fn error_message(type_name: &str) -> String {
format!(r#"Unknown type "{}""#, type_name)
}
#[cfg(test)]
mod tests {
use super::{error_message, factory};
use parser::SourcePosition;
use validation::{RuleError, expect_passes_rule, expect_fails_rule};
#[test]
fn known_type_names_are_valid() {
expect_passes_rule(factory, r#"
query Foo($var: String, $required: [String!]!) {
user(id: 4) {
pets { ... on Pet { name }, ...PetFields, ... { name } }
}
}
fragment PetFields on Pet {
name
}
"#);
}
#[test]
fn unknown_type_names_are_invalid() {
expect_fails_rule(factory, r#"
query Foo($var: JumbledUpLetters) {
user(id: 4) {
name
pets { ... on Badger { name }, ...PetFields }
}
}
fragment PetFields on Peettt {
name
}
"#,
&[
RuleError::new(&error_message("JumbledUpLetters"), &[
SourcePosition::new(27, 1, 26),
]),
RuleError::new(&error_message("Badger"), &[
SourcePosition::new(120, 4, 28),
]),
RuleError::new(&error_message("Peettt"), &[
SourcePosition::new(210, 7, 32),
]),
]);
}
}

View file

@ -0,0 +1,125 @@
use ast::{Definition, Document, Operation};
use validation::{ValidatorContext, Visitor};
use parser::Spanning;
pub struct LoneAnonymousOperation {
operation_count: Option<usize>,
}
pub fn factory() -> LoneAnonymousOperation {
LoneAnonymousOperation {
operation_count: None,
}
}
impl<'a> Visitor<'a> for LoneAnonymousOperation {
fn enter_document(&mut self, _: &mut ValidatorContext<'a>, doc: &'a Document) {
self.operation_count = Some(doc
.iter()
.filter(|d| match **d {
Definition::Operation(_) => true,
Definition::Fragment(_) => false,
})
.count());
}
fn enter_operation_definition(&mut self, ctx: &mut ValidatorContext<'a>, op: &'a Spanning<Operation>) {
if let Some(operation_count) = self.operation_count {
if operation_count > 1 && op.item.name.is_none() {
ctx.report_error(error_message(), &[op.start.clone()]);
}
}
}
}
fn error_message() -> &'static str {
"This anonymous operation must be the only defined operation"
}
#[cfg(test)]
mod tests {
use super::{error_message, factory};
use parser::SourcePosition;
use validation::{RuleError, expect_passes_rule, expect_fails_rule};
#[test]
fn no_operations() {
expect_passes_rule(factory, r#"
fragment fragA on Type {
field
}
"#);
}
#[test]
fn one_anon_operation() {
expect_passes_rule(factory, r#"
{
field
}
"#);
}
#[test]
fn multiple_named_operations() {
expect_passes_rule(factory, r#"
query Foo {
field
}
query Bar {
field
}
"#);
}
#[test]
fn anon_operation_with_fragment() {
expect_passes_rule(factory, r#"
{
...Foo
}
fragment Foo on Type {
field
}
"#);
}
#[test]
fn multiple_anon_operations() {
expect_fails_rule(factory, r#"
{
fieldA
}
{
fieldB
}
"#,
&[
RuleError::new(error_message(), &[
SourcePosition::new(11, 1, 10),
]),
RuleError::new(error_message(), &[
SourcePosition::new(54, 4, 10),
]),
]);
}
#[test]
fn anon_operation_with_a_mutation() {
expect_fails_rule(factory, r#"
{
fieldA
}
mutation Foo {
fieldB
}
"#,
&[
RuleError::new(error_message(), &[
SourcePosition::new(11, 1, 10),
]),
]);
}
}

View file

@ -0,0 +1,59 @@
mod arguments_of_correct_type;
mod default_values_of_correct_type;
mod fields_on_correct_type;
mod fragments_on_composite_types;
mod known_argument_names;
mod known_directives;
mod known_fragment_names;
mod known_type_names;
mod lone_anonymous_operation;
mod no_fragment_cycles;
mod no_undefined_variables;
mod no_unused_fragments;
mod no_unused_variables;
mod overlapping_fields_can_be_merged;
mod possible_fragment_spreads;
mod provided_non_null_arguments;
mod scalar_leafs;
mod unique_argument_names;
mod unique_fragment_names;
mod unique_input_field_names;
mod unique_operation_names;
mod unique_variable_names;
mod variables_are_input_types;
mod variables_in_allowed_position;
use ast::Document;
use validation::{ValidatorContext, MultiVisitor, visit};
#[doc(hidden)]
pub fn visit_all_rules<'a>(ctx: &mut ValidatorContext<'a>, doc: &'a Document) {
let mut mv = MultiVisitor::new(vec![
Box::new(self::arguments_of_correct_type::factory()),
Box::new(self::default_values_of_correct_type::factory()),
Box::new(self::fields_on_correct_type::factory()),
Box::new(self::fragments_on_composite_types::factory()),
Box::new(self::known_argument_names::factory()),
Box::new(self::known_directives::factory()),
Box::new(self::known_fragment_names::factory()),
Box::new(self::known_type_names::factory()),
Box::new(self::lone_anonymous_operation::factory()),
Box::new(self::no_fragment_cycles::factory()),
Box::new(self::no_undefined_variables::factory()),
Box::new(self::no_unused_fragments::factory()),
Box::new(self::no_unused_variables::factory()),
Box::new(self::overlapping_fields_can_be_merged::factory()),
Box::new(self::possible_fragment_spreads::factory()),
Box::new(self::provided_non_null_arguments::factory()),
Box::new(self::scalar_leafs::factory()),
Box::new(self::unique_argument_names::factory()),
Box::new(self::unique_fragment_names::factory()),
Box::new(self::unique_input_field_names::factory()),
Box::new(self::unique_operation_names::factory()),
Box::new(self::unique_variable_names::factory()),
Box::new(self::variables_are_input_types::factory()),
Box::new(self::variables_in_allowed_position::factory()),
]);
visit(&mut mv, ctx, doc);
}

View file

@ -0,0 +1,333 @@
use std::collections::{HashMap, HashSet};
use ast::{Fragment, FragmentSpread, Document};
use validation::{ValidatorContext, Visitor, RuleError};
use parser::Spanning;
pub struct NoFragmentCycles<'a> {
current_fragment: Option<&'a str>,
spreads: HashMap<&'a str, Vec<Spanning<&'a str>>>,
fragment_order: Vec<&'a str>,
}
struct CycleDetector<'a> {
visited: HashSet<&'a str>,
spreads: &'a HashMap<&'a str, Vec<Spanning<&'a str>>>,
path_indices: HashMap<&'a str, usize>,
errors: Vec<RuleError>,
}
pub fn factory<'a>() -> NoFragmentCycles<'a> {
NoFragmentCycles {
current_fragment: None,
spreads: HashMap::new(),
fragment_order: Vec::new(),
}
}
impl<'a> Visitor<'a> for NoFragmentCycles<'a> {
fn exit_document(&mut self, ctx: &mut ValidatorContext<'a>, _: &'a Document) {
assert!(self.current_fragment.is_none());
let mut detector = CycleDetector {
visited: HashSet::new(),
spreads: &self.spreads,
path_indices: HashMap::new(),
errors: Vec::new(),
};
for frag in &self.fragment_order {
if !detector.visited.contains(frag) {
let mut path = Vec::new();
detector.detect_from(frag, &mut path);
}
}
ctx.append_errors(detector.errors);
}
fn enter_fragment_definition(&mut self, _: &mut ValidatorContext<'a>, fragment: &'a Spanning<Fragment>) {
assert!(self.current_fragment.is_none());
let fragment_name = &fragment.item.name.item;
self.current_fragment = Some(&fragment_name);
self.fragment_order.push(&fragment_name);
}
fn exit_fragment_definition(&mut self, _: &mut ValidatorContext<'a>, fragment: &'a Spanning<Fragment>) {
assert_eq!(Some(fragment.item.name.item.as_str()), self.current_fragment);
self.current_fragment = None;
}
fn enter_fragment_spread(&mut self, _: &mut ValidatorContext<'a>, spread: &'a Spanning<FragmentSpread>) {
if let Some(ref current_fragment) = self.current_fragment {
self.spreads
.entry(&current_fragment)
.or_insert_with(|| vec![])
.push(Spanning::start_end(
&spread.start.clone(),
&spread.end.clone(),
&spread.item.name.item));
}
}
}
impl<'a> CycleDetector<'a> {
fn detect_from(&mut self, from: &'a str, path: &mut Vec<&'a Spanning<&'a str>>) {
self.visited.insert(from);
if !self.spreads.contains_key(from) {
return;
}
self.path_indices.insert(from, path.len());
for node in &self.spreads[from] {
let name = &node.item;
let index = self.path_indices.get(name).map(|i| *i);
if let Some(index) = index {
let err_pos = if index < path.len() {
path[index]
} else {
node
};
self.errors.push(RuleError::new(
&error_message(name),
&[err_pos.start.clone()]));
}
else if !self.visited.contains(name) {
path.push(node);
self.detect_from(name, path);
path.pop();
}
}
self.path_indices.remove(from);
}
}
fn error_message(frag_name: &str) -> String {
format!(r#"Cannot spread fragment "{}""#, frag_name)
}
#[cfg(test)]
mod tests {
use super::{error_message, factory};
use parser::SourcePosition;
use validation::{RuleError, expect_passes_rule, expect_fails_rule};
#[test]
fn single_reference_is_valid() {
expect_passes_rule(factory, r#"
fragment fragA on Dog { ...fragB }
fragment fragB on Dog { name }
"#);
}
#[test]
fn spreading_twice_is_not_circular() {
expect_passes_rule(factory, r#"
fragment fragA on Dog { ...fragB, ...fragB }
fragment fragB on Dog { name }
"#);
}
#[test]
fn spreading_twice_indirectly_is_not_circular() {
expect_passes_rule(factory, r#"
fragment fragA on Dog { ...fragB, ...fragC }
fragment fragB on Dog { ...fragC }
fragment fragC on Dog { name }
"#);
}
#[test]
fn double_spread_within_abstract_types() {
expect_passes_rule(factory, r#"
fragment nameFragment on Pet {
... on Dog { name }
... on Cat { name }
}
fragment spreadsInAnon on Pet {
... on Dog { ...nameFragment }
... on Cat { ...nameFragment }
}
"#);
}
#[test]
fn does_not_false_positive_on_unknown_fragment() {
expect_passes_rule(factory, r#"
fragment nameFragment on Pet {
...UnknownFragment
}
"#);
}
#[test]
fn spreading_recursively_within_field_fails() {
expect_fails_rule(factory, r#"
fragment fragA on Human { relatives { ...fragA } },
"#,
&[
RuleError::new(&error_message("fragA"), &[
SourcePosition::new(49, 1, 48)
]),
]);
}
#[test]
fn no_spreading_itself_directly() {
expect_fails_rule(factory, r#"
fragment fragA on Dog { ...fragA }
"#,
&[
RuleError::new(&error_message("fragA"), &[
SourcePosition::new(35, 1, 34)
]),
]);
}
#[test]
fn no_spreading_itself_directly_within_inline_fragment() {
expect_fails_rule(factory, r#"
fragment fragA on Pet {
... on Dog {
...fragA
}
}
"#,
&[
RuleError::new(&error_message("fragA"), &[
SourcePosition::new(74, 3, 14)
]),
]);
}
#[test]
fn no_spreading_itself_indirectly() {
expect_fails_rule(factory, r#"
fragment fragA on Dog { ...fragB }
fragment fragB on Dog { ...fragA }
"#,
&[
RuleError::new(&error_message("fragA"), &[
SourcePosition::new(35, 1, 34)
]),
]);
}
#[test]
fn no_spreading_itself_indirectly_reports_opposite_order() {
expect_fails_rule(factory, r#"
fragment fragB on Dog { ...fragA }
fragment fragA on Dog { ...fragB }
"#,
&[
RuleError::new(&error_message("fragB"), &[
SourcePosition::new(35, 1, 34)
]),
]);
}
#[test]
fn no_spreading_itself_indirectly_within_inline_fragment() {
expect_fails_rule(factory, r#"
fragment fragA on Pet {
... on Dog {
...fragB
}
}
fragment fragB on Pet {
... on Dog {
...fragA
}
}
"#,
&[
RuleError::new(&error_message("fragA"), &[
SourcePosition::new(74, 3, 14)
]),
]);
}
#[test]
fn no_spreading_itself_deeply() {
expect_fails_rule(factory, r#"
fragment fragA on Dog { ...fragB }
fragment fragB on Dog { ...fragC }
fragment fragC on Dog { ...fragO }
fragment fragX on Dog { ...fragY }
fragment fragY on Dog { ...fragZ }
fragment fragZ on Dog { ...fragO }
fragment fragO on Dog { ...fragP }
fragment fragP on Dog { ...fragA, ...fragX }
"#,
&[
RuleError::new(&error_message("fragA"), &[
SourcePosition::new(35, 1, 34)
]),
RuleError::new(&error_message("fragO"), &[
SourcePosition::new(305, 7, 34)
]),
]);
}
#[test]
fn no_spreading_itself_deeply_two_paths() {
expect_fails_rule(factory, r#"
fragment fragA on Dog { ...fragB, ...fragC }
fragment fragB on Dog { ...fragA }
fragment fragC on Dog { ...fragA }
"#,
&[
RuleError::new(&error_message("fragA"), &[
SourcePosition::new(35, 1, 34)
]),
RuleError::new(&error_message("fragA"), &[
SourcePosition::new(45, 1, 44)
]),
]);
}
#[test]
fn no_spreading_itself_deeply_two_paths_alt_traversal_order() {
expect_fails_rule(factory, r#"
fragment fragA on Dog { ...fragC }
fragment fragB on Dog { ...fragC }
fragment fragC on Dog { ...fragA, ...fragB }
"#,
&[
RuleError::new(&error_message("fragA"), &[
SourcePosition::new(35, 1, 34)
]),
RuleError::new(&error_message("fragC"), &[
SourcePosition::new(135, 3, 44)
]),
]);
}
#[test]
fn no_spreading_itself_deeply_and_immediately() {
expect_fails_rule(factory, r#"
fragment fragA on Dog { ...fragB }
fragment fragB on Dog { ...fragB, ...fragC }
fragment fragC on Dog { ...fragA, ...fragB }
"#,
&[
RuleError::new(&error_message("fragA"), &[
SourcePosition::new(35, 1, 34)
]),
RuleError::new(&error_message("fragB"), &[
SourcePosition::new(80, 2, 34)
]),
RuleError::new(&error_message("fragB"), &[
SourcePosition::new(90, 2, 44)
]),
]);
}
}

View file

@ -0,0 +1,488 @@
use std::collections::{HashSet, HashMap};
use ast::{Document, Fragment, FragmentSpread, VariableDefinition, Operation, InputValue};
use validation::{ValidatorContext, Visitor, RuleError};
use parser::{SourcePosition, Spanning};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Scope<'a> {
Operation(Option<&'a str>),
Fragment(&'a str),
}
pub struct NoUndefinedVariables<'a> {
defined_variables: HashMap<Option<&'a str>, (SourcePosition, HashSet<&'a str>)>,
used_variables: HashMap<Scope<'a>, Vec<Spanning<&'a str>>>,
current_scope: Option<Scope<'a>>,
spreads: HashMap<Scope<'a>, Vec<&'a str>>,
}
pub fn factory<'a>() -> NoUndefinedVariables<'a> {
NoUndefinedVariables {
defined_variables: HashMap::new(),
used_variables: HashMap::new(),
current_scope: None,
spreads: HashMap::new(),
}
}
impl<'a> NoUndefinedVariables<'a> {
fn find_undef_vars(&'a self, scope: &Scope<'a>, defined: &HashSet<&'a str>, unused: &mut Vec<&'a Spanning<&'a str>>, visited: &mut HashSet<Scope<'a>>) {
if visited.contains(scope) {
return;
}
visited.insert(scope.clone());
if let Some(used_vars) = self.used_variables.get(scope) {
for var in used_vars {
if !defined.contains(&var.item) {
unused.push(var);
}
}
}
if let Some(spreads) = self.spreads.get(scope) {
for spread in spreads {
self.find_undef_vars(&Scope::Fragment(spread.clone()), defined, unused, visited);
}
}
}
}
impl<'a> Visitor<'a> for NoUndefinedVariables<'a> {
fn exit_document(&mut self, ctx: &mut ValidatorContext<'a>, _: &'a Document) {
for (op_name, &(ref pos, ref def_vars)) in &self.defined_variables {
let mut unused = Vec::new();
let mut visited = HashSet::new();
self.find_undef_vars(&Scope::Operation(op_name.clone()), &def_vars, &mut unused, &mut visited);
ctx.append_errors(unused
.into_iter()
.map(|var| RuleError::new(
&error_message(&var.item, op_name.clone()),
&[
var.start.clone(),
pos.clone()
]))
.collect());
}
}
fn enter_operation_definition(&mut self, _: &mut ValidatorContext<'a>, op: &'a Spanning<Operation>) {
let op_name = op.item.name.as_ref().map(|s| s.item.as_str());
self.current_scope = Some(Scope::Operation(op_name));
self.defined_variables.insert(op_name, (op.start.clone(), HashSet::new()));
}
fn enter_fragment_definition(&mut self, _: &mut ValidatorContext<'a>, f: &'a Spanning<Fragment>) {
self.current_scope = Some(Scope::Fragment(&f.item.name.item));
}
fn enter_fragment_spread(&mut self, _: &mut ValidatorContext<'a>, spread: &'a Spanning<FragmentSpread>) {
if let Some(ref scope) = self.current_scope {
self.spreads.entry(scope.clone())
.or_insert_with(|| Vec::new())
.push(&spread.item.name.item);
}
}
fn enter_variable_definition(&mut self, _: &mut ValidatorContext<'a>, &(ref var_name, _): &'a (Spanning<String>, VariableDefinition)) {
if let Some(Scope::Operation(ref name)) = self.current_scope {
if let Some(&mut (_, ref mut vars)) = self.defined_variables.get_mut(name) {
vars.insert(&var_name.item);
}
}
}
fn enter_argument(&mut self, _: &mut ValidatorContext<'a>, &(_, ref value): &'a (Spanning<String>, Spanning<InputValue>)) {
if let Some(ref scope) = self.current_scope {
self.used_variables
.entry(scope.clone())
.or_insert_with(|| Vec::new())
.append(&mut value.item
.referenced_variables()
.iter()
.map(|&var_name| Spanning::start_end(
&value.start.clone(),
&value.end.clone(),
var_name))
.collect());
}
}
}
fn error_message(var_name: &str, op_name: Option<&str>) -> String {
if let Some(op_name) = op_name {
format!(r#"Variable "${}" is not defined by operation "{}""#, var_name, op_name)
}
else {
format!(r#"Variable "${}" is not defined"#, var_name)
}
}
#[cfg(test)]
mod tests {
use super::{error_message, factory};
use parser::SourcePosition;
use validation::{RuleError, expect_passes_rule, expect_fails_rule};
#[test]
fn all_variables_defined() {
expect_passes_rule(factory, r#"
query Foo($a: String, $b: String, $c: String) {
field(a: $a, b: $b, c: $c)
}
"#);
}
#[test]
fn all_variables_deeply_defined() {
expect_passes_rule(factory, r#"
query Foo($a: String, $b: String, $c: String) {
field(a: $a) {
field(b: $b) {
field(c: $c)
}
}
}
"#);
}
#[test]
fn all_variables_deeply_defined_in_inline_fragments_defined() {
expect_passes_rule(factory, r#"
query Foo($a: String, $b: String, $c: String) {
... on Type {
field(a: $a) {
field(b: $b) {
... on Type {
field(c: $c)
}
}
}
}
}
"#);
}
#[test]
fn all_variables_in_fragments_deeply_defined() {
expect_passes_rule(factory, r#"
query Foo($a: String, $b: String, $c: String) {
...FragA
}
fragment FragA on Type {
field(a: $a) {
...FragB
}
}
fragment FragB on Type {
field(b: $b) {
...FragC
}
}
fragment FragC on Type {
field(c: $c)
}
"#);
}
#[test]
fn variable_within_single_fragment_defined_in_multiple_operations() {
expect_passes_rule(factory, r#"
query Foo($a: String) {
...FragA
}
query Bar($a: String) {
...FragA
}
fragment FragA on Type {
field(a: $a)
}
"#);
}
#[test]
fn variable_within_fragments_defined_in_operations() {
expect_passes_rule(factory, r#"
query Foo($a: String) {
...FragA
}
query Bar($b: String) {
...FragB
}
fragment FragA on Type {
field(a: $a)
}
fragment FragB on Type {
field(b: $b)
}
"#);
}
#[test]
fn variable_within_recursive_fragment_defined() {
expect_passes_rule(factory, r#"
query Foo($a: String) {
...FragA
}
fragment FragA on Type {
field(a: $a) {
...FragA
}
}
"#);
}
#[test]
fn variable_not_defined() {
expect_fails_rule(factory, r#"
query Foo($a: String, $b: String, $c: String) {
field(a: $a, b: $b, c: $c, d: $d)
}
"#,
&[
RuleError::new(&error_message("d", Some("Foo")), &[
SourcePosition::new(101, 2, 42),
SourcePosition::new(11, 1, 10),
]),
]);
}
#[test]
fn variable_not_defined_by_unnamed_query() {
expect_fails_rule(factory, r#"
{
field(a: $a)
}
"#,
&[
RuleError::new(&error_message("a", None), &[
SourcePosition::new(34, 2, 21),
SourcePosition::new(11, 1, 10),
]),
]);
}
#[test]
fn multiple_variables_not_defined() {
expect_fails_rule(factory, r#"
query Foo($b: String) {
field(a: $a, b: $b, c: $c)
}
"#,
&[
RuleError::new(&error_message("a", Some("Foo")), &[
SourcePosition::new(56, 2, 21),
SourcePosition::new(11, 1, 10),
]),
RuleError::new(&error_message("c", Some("Foo")), &[
SourcePosition::new(70, 2, 35),
SourcePosition::new(11, 1, 10),
]),
]);
}
#[test]
fn variable_in_fragment_not_defined_by_unnamed_query() {
expect_fails_rule(factory, r#"
{
...FragA
}
fragment FragA on Type {
field(a: $a)
}
"#,
&[
RuleError::new(&error_message("a", None), &[
SourcePosition::new(102, 5, 21),
SourcePosition::new(11, 1, 10),
]),
]);
}
#[test]
fn variable_in_fragment_not_defined_by_operation() {
expect_fails_rule(factory, r#"
query Foo($a: String, $b: String) {
...FragA
}
fragment FragA on Type {
field(a: $a) {
...FragB
}
}
fragment FragB on Type {
field(b: $b) {
...FragC
}
}
fragment FragC on Type {
field(c: $c)
}
"#,
&[
RuleError::new(&error_message("c", Some("Foo")), &[
SourcePosition::new(358, 15, 21),
SourcePosition::new(11, 1, 10),
]),
]);
}
#[test]
fn multiple_variables_in_fragments_not_defined() {
expect_fails_rule(factory, r#"
query Foo($b: String) {
...FragA
}
fragment FragA on Type {
field(a: $a) {
...FragB
}
}
fragment FragB on Type {
field(b: $b) {
...FragC
}
}
fragment FragC on Type {
field(c: $c)
}
"#,
&[
RuleError::new(&error_message("a", Some("Foo")), &[
SourcePosition::new(124, 5, 21),
SourcePosition::new(11, 1, 10),
]),
RuleError::new(&error_message("c", Some("Foo")), &[
SourcePosition::new(346, 15, 21),
SourcePosition::new(11, 1, 10),
]),
]);
}
#[test]
fn single_variable_in_fragment_not_defined_by_multiple_operations() {
expect_fails_rule(factory, r#"
query Foo($a: String) {
...FragAB
}
query Bar($a: String) {
...FragAB
}
fragment FragAB on Type {
field(a: $a, b: $b)
}
"#,
&[
RuleError::new(&error_message("b", Some("Foo")), &[
SourcePosition::new(201, 8, 28),
SourcePosition::new(11, 1, 10),
]),
RuleError::new(&error_message("b", Some("Bar")), &[
SourcePosition::new(201, 8, 28),
SourcePosition::new(79, 4, 10),
]),
]);
}
#[test]
fn variables_in_fragment_not_defined_by_multiple_operations() {
expect_fails_rule(factory, r#"
query Foo($b: String) {
...FragAB
}
query Bar($a: String) {
...FragAB
}
fragment FragAB on Type {
field(a: $a, b: $b)
}
"#,
&[
RuleError::new(&error_message("a", Some("Foo")), &[
SourcePosition::new(194, 8, 21),
SourcePosition::new(11, 1, 10),
]),
RuleError::new(&error_message("b", Some("Bar")), &[
SourcePosition::new(201, 8, 28),
SourcePosition::new(79, 4, 10),
]),
]);
}
#[test]
fn variable_in_fragment_used_by_other_operation() {
expect_fails_rule(factory, r#"
query Foo($b: String) {
...FragA
}
query Bar($a: String) {
...FragB
}
fragment FragA on Type {
field(a: $a)
}
fragment FragB on Type {
field(b: $b)
}
"#,
&[
RuleError::new(&error_message("a", Some("Foo")), &[
SourcePosition::new(191, 8, 21),
SourcePosition::new(11, 1, 10),
]),
RuleError::new(&error_message("b", Some("Bar")), &[
SourcePosition::new(263, 11, 21),
SourcePosition::new(78, 4, 10),
]),
]);
}
#[test]
fn multiple_undefined_variables_produce_multiple_errors() {
expect_fails_rule(factory, r#"
query Foo($b: String) {
...FragAB
}
query Bar($a: String) {
...FragAB
}
fragment FragAB on Type {
field1(a: $a, b: $b)
...FragC
field3(a: $a, b: $b)
}
fragment FragC on Type {
field2(c: $c)
}
"#,
&[
RuleError::new(&error_message("a", Some("Foo")), &[
SourcePosition::new(195, 8, 22),
SourcePosition::new(11, 1, 10),
]),
RuleError::new(&error_message("b", Some("Bar")), &[
SourcePosition::new(202, 8, 29),
SourcePosition::new(79, 4, 10),
]),
RuleError::new(&error_message("a", Some("Foo")), &[
SourcePosition::new(249, 10, 22),
SourcePosition::new(11, 1, 10),
]),
RuleError::new(&error_message("b", Some("Bar")), &[
SourcePosition::new(256, 10, 29),
SourcePosition::new(79, 4, 10),
]),
RuleError::new(&error_message("c", Some("Foo")), &[
SourcePosition::new(329, 13, 22),
SourcePosition::new(11, 1, 10),
]),
RuleError::new(&error_message("c", Some("Bar")), &[
SourcePosition::new(329, 13, 22),
SourcePosition::new(79, 4, 10),
]),
]);
}
}

View file

@ -0,0 +1,249 @@
use std::collections::{HashSet, HashMap};
use ast::{Document, Definition, Operation, Fragment, FragmentSpread};
use validation::{ValidatorContext, Visitor};
use parser::Spanning;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Scope<'a> {
Operation(Option<&'a str>),
Fragment(&'a str),
}
pub struct NoUnusedFragments<'a> {
spreads: HashMap<Scope<'a>, Vec<&'a str>>,
defined_fragments: HashSet<Spanning<&'a str>>,
current_scope: Option<Scope<'a>>,
}
pub fn factory<'a>() -> NoUnusedFragments<'a> {
NoUnusedFragments {
spreads: HashMap::new(),
defined_fragments: HashSet::new(),
current_scope: None,
}
}
impl<'a> NoUnusedFragments<'a> {
fn find_reachable_fragments(&self, from: &Scope<'a>, result: &mut HashSet<&'a str>) {
if let Scope::Fragment(ref name) = *from {
if result.contains(name) {
return;
}
else {
result.insert(name);
}
}
if let Some(spreads) = self.spreads.get(from) {
for spread in spreads {
self.find_reachable_fragments(&Scope::Fragment(spread.clone()), result)
}
}
}
}
impl<'a> Visitor<'a> for NoUnusedFragments<'a> {
fn exit_document(&mut self, ctx: &mut ValidatorContext<'a>, defs: &'a Document) {
let mut reachable = HashSet::new();
for def in defs {
if let Definition::Operation(Spanning { item: Operation { ref name, .. }, ..}) = *def {
let op_name = name.as_ref().map(|s| s.item.as_str());
self.find_reachable_fragments(&Scope::Operation(op_name), &mut reachable);
}
}
for fragment in &self.defined_fragments {
if !reachable.contains(&fragment.item) {
ctx.report_error(
&error_message(&fragment.item),
&[fragment.start.clone()]);
}
}
}
fn enter_operation_definition(&mut self, _: &mut ValidatorContext<'a>, op: &'a Spanning<Operation>) {
let op_name = op.item.name.as_ref().map(|s| s.item.as_ref());
self.current_scope = Some(Scope::Operation(op_name));
}
fn enter_fragment_definition(&mut self, _: &mut ValidatorContext<'a>, f: &'a Spanning<Fragment>) {
self.current_scope = Some(Scope::Fragment(&f.item.name.item));
self.defined_fragments.insert(Spanning::start_end(
&f.start,
&f.end,
&f.item.name.item));
}
fn enter_fragment_spread(&mut self, _: &mut ValidatorContext<'a>, spread: &'a Spanning<FragmentSpread>) {
if let Some(ref scope) = self.current_scope {
self.spreads.entry(scope.clone())
.or_insert_with(|| Vec::new())
.push(&spread.item.name.item);
}
}
}
fn error_message(frag_name: &str) -> String {
format!(r#"Fragment "{}" is never used"#, frag_name)
}
#[cfg(test)]
mod tests {
use super::{error_message, factory};
use parser::SourcePosition;
use validation::{RuleError, expect_passes_rule, expect_fails_rule};
#[test]
fn all_fragment_names_are_used() {
expect_passes_rule(factory, r#"
{
human(id: 4) {
...HumanFields1
... on Human {
...HumanFields2
}
}
}
fragment HumanFields1 on Human {
name
...HumanFields3
}
fragment HumanFields2 on Human {
name
}
fragment HumanFields3 on Human {
name
}
"#);
}
#[test]
fn all_fragment_names_are_used_by_multiple_operations() {
expect_passes_rule(factory, r#"
query Foo {
human(id: 4) {
...HumanFields1
}
}
query Bar {
human(id: 4) {
...HumanFields2
}
}
fragment HumanFields1 on Human {
name
...HumanFields3
}
fragment HumanFields2 on Human {
name
}
fragment HumanFields3 on Human {
name
}
"#);
}
#[test]
fn contains_unknown_fragments() {
expect_fails_rule(factory, r#"
query Foo {
human(id: 4) {
...HumanFields1
}
}
query Bar {
human(id: 4) {
...HumanFields2
}
}
fragment HumanFields1 on Human {
name
...HumanFields3
}
fragment HumanFields2 on Human {
name
}
fragment HumanFields3 on Human {
name
}
fragment Unused1 on Human {
name
}
fragment Unused2 on Human {
name
}
"#,
&[
RuleError::new(&error_message("Unused1"), &[
SourcePosition::new(465, 21, 10),
]),
RuleError::new(&error_message("Unused2"), &[
SourcePosition::new(532, 24, 10),
]),
]);
}
#[test]
fn contains_unknown_fragments_with_ref_cycle() {
expect_fails_rule(factory, r#"
query Foo {
human(id: 4) {
...HumanFields1
}
}
query Bar {
human(id: 4) {
...HumanFields2
}
}
fragment HumanFields1 on Human {
name
...HumanFields3
}
fragment HumanFields2 on Human {
name
}
fragment HumanFields3 on Human {
name
}
fragment Unused1 on Human {
name
...Unused2
}
fragment Unused2 on Human {
name
...Unused1
}
"#,
&[
RuleError::new(&error_message("Unused1"), &[
SourcePosition::new(465, 21, 10),
]),
RuleError::new(&error_message("Unused2"), &[
SourcePosition::new(555, 25, 10),
]),
]);
}
#[test]
fn contains_unknown_and_undef_fragments() {
expect_fails_rule(factory, r#"
query Foo {
human(id: 4) {
...bar
}
}
fragment foo on Human {
name
}
"#,
&[
RuleError::new(&error_message("foo"), &[
SourcePosition::new(107, 6, 10),
]),
]);
}
}

View file

@ -0,0 +1,351 @@
use std::collections::{HashSet, HashMap};
use ast::{Document, Fragment, FragmentSpread, VariableDefinition, Operation, InputValue};
use validation::{ValidatorContext, Visitor, RuleError};
use parser::Spanning;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Scope<'a> {
Operation(Option<&'a str>),
Fragment(&'a str),
}
pub struct NoUnusedVariables<'a> {
defined_variables: HashMap<Option<&'a str>, HashSet<&'a Spanning<String>>>,
used_variables: HashMap<Scope<'a>, Vec<&'a str>>,
current_scope: Option<Scope<'a>>,
spreads: HashMap<Scope<'a>, Vec<&'a str>>,
}
pub fn factory<'a>() -> NoUnusedVariables<'a> {
NoUnusedVariables {
defined_variables: HashMap::new(),
used_variables: HashMap::new(),
current_scope: None,
spreads: HashMap::new(),
}
}
impl<'a> NoUnusedVariables<'a> {
fn find_used_vars(&self, from: &Scope<'a>, defined: &HashSet<&'a str>, used: &mut HashSet<&'a str>, visited: &mut HashSet<Scope<'a>>) {
if visited.contains(from) {
return;
}
visited.insert(from.clone());
if let Some(used_vars) = self.used_variables.get(from) {
for var in used_vars {
if defined.contains(var) {
used.insert(var);
}
}
}
if let Some(spreads) = self.spreads.get(from) {
for spread in spreads {
self.find_used_vars(&Scope::Fragment(spread.clone()), defined, used, visited);
}
}
}
}
impl<'a> Visitor<'a> for NoUnusedVariables<'a> {
fn exit_document(&mut self, ctx: &mut ValidatorContext<'a>, _: &'a Document) {
for (op_name, def_vars) in &self.defined_variables {
let mut used = HashSet::new();
let mut visited = HashSet::new();
self.find_used_vars(
&Scope::Operation(op_name.clone()),
&def_vars.iter().map(|def| def.item.as_str()).collect(),
&mut used,
&mut visited);
ctx.append_errors(def_vars
.iter()
.filter(|var| !used.contains(var.item.as_str()))
.map(|var| RuleError::new(
&error_message(&var.item, op_name.clone()),
&[var.start.clone()]))
.collect());
}
}
fn enter_operation_definition(&mut self, _: &mut ValidatorContext<'a>, op: &'a Spanning<Operation>) {
let op_name = op.item.name.as_ref().map(|s| s.item.as_str());
self.current_scope = Some(Scope::Operation(op_name.clone()));
self.defined_variables.insert(op_name, HashSet::new());
}
fn enter_fragment_definition(&mut self, _: &mut ValidatorContext<'a>, f: &'a Spanning<Fragment>) {
self.current_scope = Some(Scope::Fragment(&f.item.name.item));
}
fn enter_fragment_spread(&mut self, _: &mut ValidatorContext<'a>, spread: &'a Spanning<FragmentSpread>) {
if let Some(ref scope) = self.current_scope {
self.spreads.entry(scope.clone())
.or_insert_with(|| Vec::new())
.push(&spread.item.name.item);
}
}
fn enter_variable_definition(&mut self, _: &mut ValidatorContext<'a>, &(ref var_name, _): &'a (Spanning<String>, VariableDefinition)) {
if let Some(Scope::Operation(ref name)) = self.current_scope {
if let Some(vars) = self.defined_variables.get_mut(name) {
vars.insert(var_name);
}
}
}
fn enter_argument(&mut self, _: &mut ValidatorContext<'a>, &(_, ref value): &'a (Spanning<String>, Spanning<InputValue>)) {
if let Some(ref scope) = self.current_scope {
self.used_variables
.entry(scope.clone())
.or_insert_with(|| Vec::new())
.append(&mut value.item.referenced_variables());
}
}
}
fn error_message(var_name: &str, op_name: Option<&str>) -> String {
if let Some(op_name) = op_name {
format!(r#"Variable "${}" is not defined by operation "{}""#, var_name, op_name)
}
else {
format!(r#"Variable "${}" is not defined"#, var_name)
}
}
#[cfg(test)]
mod tests {
use super::{error_message, factory};
use parser::SourcePosition;
use validation::{RuleError, expect_passes_rule, expect_fails_rule};
#[test]
fn uses_all_variables() {
expect_passes_rule(factory, r#"
query ($a: String, $b: String, $c: String) {
field(a: $a, b: $b, c: $c)
}
"#);
}
#[test]
fn uses_all_variables_deeply() {
expect_passes_rule(factory, r#"
query Foo($a: String, $b: String, $c: String) {
field(a: $a) {
field(b: $b) {
field(c: $c)
}
}
}
"#);
}
#[test]
fn uses_all_variables_deeply_in_inline_fragments() {
expect_passes_rule(factory, r#"
query Foo($a: String, $b: String, $c: String) {
... on Type {
field(a: $a) {
field(b: $b) {
... on Type {
field(c: $c)
}
}
}
}
}
"#);
}
#[test]
fn uses_all_variables_in_fragments() {
expect_passes_rule(factory, r#"
query Foo($a: String, $b: String, $c: String) {
...FragA
}
fragment FragA on Type {
field(a: $a) {
...FragB
}
}
fragment FragB on Type {
field(b: $b) {
...FragC
}
}
fragment FragC on Type {
field(c: $c)
}
"#);
}
#[test]
fn variable_used_by_fragment_in_multiple_operations() {
expect_passes_rule(factory, r#"
query Foo($a: String) {
...FragA
}
query Bar($b: String) {
...FragB
}
fragment FragA on Type {
field(a: $a)
}
fragment FragB on Type {
field(b: $b)
}
"#);
}
#[test]
fn variable_used_by_recursive_fragment() {
expect_passes_rule(factory, r#"
query Foo($a: String) {
...FragA
}
fragment FragA on Type {
field(a: $a) {
...FragA
}
}
"#);
}
#[test]
fn variable_not_used() {
expect_fails_rule(factory, r#"
query ($a: String, $b: String, $c: String) {
field(a: $a, b: $b)
}
"#,
&[
RuleError::new(&error_message("c", None), &[
SourcePosition::new(42, 1, 41),
]),
]);
}
#[test]
fn multiple_variables_not_used_1() {
expect_fails_rule(factory, r#"
query Foo($a: String, $b: String, $c: String) {
field(b: $b)
}
"#,
&[
RuleError::new(&error_message("a", Some("Foo")), &[
SourcePosition::new(21, 1, 20),
]),
RuleError::new(&error_message("c", Some("Foo")), &[
SourcePosition::new(45, 1, 44),
]),
]);
}
#[test]
fn variable_not_used_in_fragment() {
expect_fails_rule(factory, r#"
query Foo($a: String, $b: String, $c: String) {
...FragA
}
fragment FragA on Type {
field(a: $a) {
...FragB
}
}
fragment FragB on Type {
field(b: $b) {
...FragC
}
}
fragment FragC on Type {
field
}
"#,
&[
RuleError::new(&error_message("c", Some("Foo")), &[
SourcePosition::new(45, 1, 44),
]),
]);
}
#[test]
fn multiple_variables_not_used_2() {
expect_fails_rule(factory, r#"
query Foo($a: String, $b: String, $c: String) {
...FragA
}
fragment FragA on Type {
field {
...FragB
}
}
fragment FragB on Type {
field(b: $b) {
...FragC
}
}
fragment FragC on Type {
field
}
"#,
&[
RuleError::new(&error_message("a", Some("Foo")), &[
SourcePosition::new(21, 1, 20),
]),
RuleError::new(&error_message("c", Some("Foo")), &[
SourcePosition::new(45, 1, 44),
]),
]);
}
#[test]
fn variable_not_used_by_unreferenced_fragment() {
expect_fails_rule(factory, r#"
query Foo($b: String) {
...FragA
}
fragment FragA on Type {
field(a: $a)
}
fragment FragB on Type {
field(b: $b)
}
"#,
&[
RuleError::new(&error_message("b", Some("Foo")), &[
SourcePosition::new(21, 1, 20),
]),
]);
}
#[test]
fn variable_not_used_by_fragment_used_by_other_operation() {
expect_fails_rule(factory, r#"
query Foo($b: String) {
...FragA
}
query Bar($a: String) {
...FragB
}
fragment FragA on Type {
field(a: $a)
}
fragment FragB on Type {
field(b: $b)
}
"#,
&[
RuleError::new(&error_message("b", Some("Foo")), &[
SourcePosition::new(21, 1, 20),
]),
RuleError::new(&error_message("a", Some("Bar")), &[
SourcePosition::new(88, 4, 20),
]),
]);
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,314 @@
use std::collections::HashMap;
use ast::{Document, Definition, InlineFragment, FragmentSpread};
use validation::{ValidatorContext, Visitor};
use parser::Spanning;
use schema::meta::MetaType;
pub struct PossibleFragmentSpreads<'a> {
fragment_types: HashMap<&'a str, &'a MetaType>,
}
pub fn factory<'a>() -> PossibleFragmentSpreads<'a> {
PossibleFragmentSpreads {
fragment_types: HashMap::new(),
}
}
impl<'a> Visitor<'a> for PossibleFragmentSpreads<'a> {
fn enter_document(&mut self, ctx: &mut ValidatorContext<'a>, defs: &'a Document) {
for def in defs {
if let Definition::Fragment(Spanning { ref item, .. }) = *def {
if let Some(t) = ctx.schema.concrete_type_by_name(&item.type_condition.item) {
self.fragment_types.insert(&item.name.item, t);
}
}
}
}
fn enter_inline_fragment(&mut self, ctx: &mut ValidatorContext<'a>, frag: &'a Spanning<InlineFragment>) {
if let (Some(ref parent_type), Some(ref frag_type))
= (ctx.parent_type(), frag.item.type_condition.as_ref().and_then(|s| ctx.schema.concrete_type_by_name(&s.item)))
{
if !ctx.schema.type_overlap(parent_type, frag_type) {
ctx.report_error(
&error_message(
None,
parent_type.name().unwrap_or("<unknown>"),
frag_type.name().unwrap_or("<unknown>")),
&[frag.start.clone()]);
}
}
}
fn enter_fragment_spread(&mut self, ctx: &mut ValidatorContext<'a>, spread: &'a Spanning<FragmentSpread>) {
if let (Some(ref parent_type), Some(ref frag_type))
= (ctx.parent_type(), self.fragment_types.get(spread.item.name.item.as_str()))
{
if !ctx.schema.type_overlap(parent_type, frag_type) {
ctx.report_error(
&error_message(
Some(&spread.item.name.item),
parent_type.name().unwrap_or("<unknown>"),
frag_type.name().unwrap_or("<unknown>")),
&[spread.start.clone()]);
}
}
}
}
fn error_message(frag_name: Option<&str>, parent_type_name: &str, frag_type: &str) -> String {
if let Some(frag_name) = frag_name {
format!(
"Fragment \"{}\" cannot be spread here as objects of type \
\"{}\" can never be of type \"{}\"",
frag_name, parent_type_name, frag_type)
}
else {
format!(
"Fragment cannot be spread here as objects of type \"{}\" \
can never be of type \"{}\"",
parent_type_name, frag_type)
}
}
#[cfg(test)]
mod tests {
use super::{error_message, factory};
use parser::SourcePosition;
use validation::{RuleError, expect_passes_rule, expect_fails_rule};
#[test]
fn of_the_same_object() {
expect_passes_rule(factory, r#"
fragment objectWithinObject on Dog { ...dogFragment }
fragment dogFragment on Dog { barkVolume }
"#);
}
#[test]
fn of_the_same_object_with_inline_fragment() {
expect_passes_rule(factory, r#"
fragment objectWithinObjectAnon on Dog { ... on Dog { barkVolume } }
"#);
}
#[test]
fn object_into_an_implemented_interface() {
expect_passes_rule(factory, r#"
fragment objectWithinInterface on Pet { ...dogFragment }
fragment dogFragment on Dog { barkVolume }
"#);
}
#[test]
fn object_into_containing_union() {
expect_passes_rule(factory, r#"
fragment objectWithinUnion on CatOrDog { ...dogFragment }
fragment dogFragment on Dog { barkVolume }
"#);
}
#[test]
fn union_into_contained_object() {
expect_passes_rule(factory, r#"
fragment unionWithinObject on Dog { ...catOrDogFragment }
fragment catOrDogFragment on CatOrDog { __typename }
"#);
}
#[test]
fn union_into_overlapping_interface() {
expect_passes_rule(factory, r#"
fragment unionWithinInterface on Pet { ...catOrDogFragment }
fragment catOrDogFragment on CatOrDog { __typename }
"#);
}
#[test]
fn union_into_overlapping_union() {
expect_passes_rule(factory, r#"
fragment unionWithinUnion on DogOrHuman { ...catOrDogFragment }
fragment catOrDogFragment on CatOrDog { __typename }
"#);
}
#[test]
fn interface_into_implemented_object() {
expect_passes_rule(factory, r#"
fragment interfaceWithinObject on Dog { ...petFragment }
fragment petFragment on Pet { name }
"#);
}
#[test]
fn interface_into_overlapping_interface() {
expect_passes_rule(factory, r#"
fragment interfaceWithinInterface on Pet { ...beingFragment }
fragment beingFragment on Being { name }
"#);
}
#[test]
fn interface_into_overlapping_interface_in_inline_fragment() {
expect_passes_rule(factory, r#"
fragment interfaceWithinInterface on Pet { ... on Being { name } }
"#);
}
#[test]
fn interface_into_overlapping_union() {
expect_passes_rule(factory, r#"
fragment interfaceWithinUnion on CatOrDog { ...petFragment }
fragment petFragment on Pet { name }
"#);
}
#[test]
fn different_object_into_object() {
expect_fails_rule(factory, r#"
fragment invalidObjectWithinObject on Cat { ...dogFragment }
fragment dogFragment on Dog { barkVolume }
"#,
&[
RuleError::new(&error_message(Some("dogFragment"), "Cat", "Dog"), &[
SourcePosition::new(55, 1, 54),
]),
]);
}
#[test]
fn different_object_into_object_in_inline_fragment() {
expect_fails_rule(factory, r#"
fragment invalidObjectWithinObjectAnon on Cat {
... on Dog { barkVolume }
}
"#,
&[
RuleError::new(&error_message(None, "Cat", "Dog"), &[
SourcePosition::new(71, 2, 12),
]),
]);
}
#[test]
fn object_into_not_implementing_interface() {
expect_fails_rule(factory, r#"
fragment invalidObjectWithinInterface on Pet { ...humanFragment }
fragment humanFragment on Human { pets { name } }
"#,
&[
RuleError::new(&error_message(Some("humanFragment"), "Pet", "Human"), &[
SourcePosition::new(58, 1, 57),
]),
]);
}
#[test]
fn object_into_not_containing_union() {
expect_fails_rule(factory, r#"
fragment invalidObjectWithinUnion on CatOrDog { ...humanFragment }
fragment humanFragment on Human { pets { name } }
"#,
&[
RuleError::new(&error_message(Some("humanFragment"), "CatOrDog", "Human"), &[
SourcePosition::new(59, 1, 58),
]),
]);
}
#[test]
fn union_into_not_contained_object() {
expect_fails_rule(factory, r#"
fragment invalidUnionWithinObject on Human { ...catOrDogFragment }
fragment catOrDogFragment on CatOrDog { __typename }
"#,
&[
RuleError::new(&error_message(Some("catOrDogFragment"), "Human", "CatOrDog"), &[
SourcePosition::new(56, 1, 55),
]),
]);
}
#[test]
fn union_into_non_overlapping_interface() {
expect_fails_rule(factory, r#"
fragment invalidUnionWithinInterface on Pet { ...humanOrAlienFragment }
fragment humanOrAlienFragment on HumanOrAlien { __typename }
"#,
&[
RuleError::new(&error_message(Some("humanOrAlienFragment"), "Pet", "HumanOrAlien"), &[
SourcePosition::new(57, 1, 56),
]),
]);
}
#[test]
fn union_into_non_overlapping_union() {
expect_fails_rule(factory, r#"
fragment invalidUnionWithinUnion on CatOrDog { ...humanOrAlienFragment }
fragment humanOrAlienFragment on HumanOrAlien { __typename }
"#,
&[
RuleError::new(&error_message(Some("humanOrAlienFragment"), "CatOrDog", "HumanOrAlien"), &[
SourcePosition::new(58, 1, 57),
]),
]);
}
#[test]
fn interface_into_non_implementing_object() {
expect_fails_rule(factory, r#"
fragment invalidInterfaceWithinObject on Cat { ...intelligentFragment }
fragment intelligentFragment on Intelligent { iq }
"#,
&[
RuleError::new(&error_message(Some("intelligentFragment"), "Cat", "Intelligent"), &[
SourcePosition::new(58, 1, 57),
]),
]);
}
#[test]
fn interface_into_non_overlapping_interface() {
expect_fails_rule(factory, r#"
fragment invalidInterfaceWithinInterface on Pet {
...intelligentFragment
}
fragment intelligentFragment on Intelligent { iq }
"#,
&[
RuleError::new(&error_message(Some("intelligentFragment"), "Pet", "Intelligent"), &[
SourcePosition::new(73, 2, 12),
]),
]);
}
#[test]
fn interface_into_non_overlapping_interface_in_inline_fragment() {
expect_fails_rule(factory, r#"
fragment invalidInterfaceWithinInterfaceAnon on Pet {
...on Intelligent { iq }
}
"#,
&[
RuleError::new(&error_message(None, "Pet", "Intelligent"), &[
SourcePosition::new(77, 2, 12),
]),
]);
}
#[test]
fn interface_into_non_overlapping_union() {
expect_fails_rule(factory, r#"
fragment invalidInterfaceWithinUnion on HumanOrAlien { ...petFragment }
fragment petFragment on Pet { name }
"#,
&[
RuleError::new(&error_message(Some("petFragment"), "HumanOrAlien", "Pet"), &[
SourcePosition::new(66, 1, 65),
]),
]);
}
}

View file

@ -0,0 +1,280 @@
use ast::{Field, Directive};
use validation::{ValidatorContext, Visitor};
use parser::Spanning;
use schema::meta::{Field as FieldType};
use schema::model::DirectiveType;
pub struct ProvidedNonNullArguments {
}
pub fn factory() -> ProvidedNonNullArguments {
ProvidedNonNullArguments {}
}
impl<'a> Visitor<'a> for ProvidedNonNullArguments {
fn enter_field(&mut self, ctx: &mut ValidatorContext<'a>, field: &'a Spanning<Field>) {
let field_name = &field.item.name.item;
if let Some(&FieldType { arguments: Some(ref meta_args), ..}) = ctx.parent_type().and_then(|t| t.field_by_name(field_name)) {
for meta_arg in meta_args {
if meta_arg.arg_type.is_non_null()
&& field.item.arguments.as_ref().and_then(|args| args.item.get(&meta_arg.name)).is_none()
{
ctx.report_error(
&field_error_message(field_name, &meta_arg.name, &format!("{}", meta_arg.arg_type)),
&[field.start.clone()]);
}
}
}
}
fn enter_directive(&mut self, ctx: &mut ValidatorContext<'a>, directive: &'a Spanning<Directive>) {
let directive_name = &directive.item.name.item;
if let Some(&DirectiveType { arguments: ref meta_args, ..}) = ctx.schema.directive_by_name(directive_name) {
for meta_arg in meta_args {
if meta_arg.arg_type.is_non_null()
&& directive.item.arguments.as_ref().and_then(|args| args.item.get(&meta_arg.name)).is_none()
{
ctx.report_error(
&directive_error_message(directive_name, &meta_arg.name, &format!("{}", meta_arg.arg_type)),
&[directive.start.clone()]);
}
}
}
}
}
fn field_error_message(field_name: &str, arg_name: &str, type_name: &str) -> String {
format!(
r#"Field "{}" argument "{}" of type "{}" is required but not provided"#,
field_name, arg_name, type_name)
}
fn directive_error_message(directive_name: &str, arg_name: &str, type_name: &str) -> String {
format!(
r#"Directive "@{}" argument "{}" of type "{}" is required but not provided"#,
directive_name, arg_name, type_name)
}
#[cfg(test)]
mod tests {
use super::{field_error_message, directive_error_message, factory};
use parser::SourcePosition;
use validation::{RuleError, expect_passes_rule, expect_fails_rule};
#[test]
fn ignores_unknown_arguments() {
expect_passes_rule(factory, r#"
{
dog {
isHousetrained(unknownArgument: true)
}
}
"#);
}
#[test]
fn arg_on_optional_arg() {
expect_passes_rule(factory, r#"
{
dog {
isHousetrained(atOtherHomes: true)
}
}
"#);
}
#[test]
fn no_arg_on_optional_arg() {
expect_passes_rule(factory, r#"
{
dog {
isHousetrained
}
}
"#);
}
#[test]
fn multiple_args() {
expect_passes_rule(factory, r#"
{
complicatedArgs {
multipleReqs(req1: 1, req2: 2)
}
}
"#);
}
#[test]
fn multiple_args_reverse_order() {
expect_passes_rule(factory, r#"
{
complicatedArgs {
multipleReqs(req2: 2, req1: 1)
}
}
"#);
}
#[test]
fn no_args_on_multiple_optional() {
expect_passes_rule(factory, r#"
{
complicatedArgs {
multipleOpts
}
}
"#);
}
#[test]
fn one_arg_on_multiple_optional() {
expect_passes_rule(factory, r#"
{
complicatedArgs {
multipleOpts(opt1: 1)
}
}
"#);
}
#[test]
fn second_arg_on_multiple_optional() {
expect_passes_rule(factory, r#"
{
complicatedArgs {
multipleOpts(opt2: 1)
}
}
"#);
}
#[test]
fn muliple_reqs_on_mixed_list() {
expect_passes_rule(factory, r#"
{
complicatedArgs {
multipleOptAndReq(req1: 3, req2: 4)
}
}
"#);
}
#[test]
fn multiple_reqs_and_one_opt_on_mixed_list() {
expect_passes_rule(factory, r#"
{
complicatedArgs {
multipleOptAndReq(req1: 3, req2: 4, opt1: 5)
}
}
"#);
}
#[test]
fn all_reqs_on_opts_on_mixed_list() {
expect_passes_rule(factory, r#"
{
complicatedArgs {
multipleOptAndReq(req1: 3, req2: 4, opt1: 5, opt2: 6)
}
}
"#);
}
#[test]
fn missing_one_non_nullable_argument() {
expect_fails_rule(factory, r#"
{
complicatedArgs {
multipleReqs(req2: 2)
}
}
"#,
&[
RuleError::new(&field_error_message("multipleReqs", "req1", "Int!"), &[
SourcePosition::new(63, 3, 16),
]),
]);
}
#[test]
fn missing_multiple_non_nullable_arguments() {
expect_fails_rule(factory, r#"
{
complicatedArgs {
multipleReqs
}
}
"#,
&[
RuleError::new(&field_error_message("multipleReqs", "req1", "Int!"), &[
SourcePosition::new(63, 3, 16),
]),
RuleError::new(&field_error_message("multipleReqs", "req2", "Int!"), &[
SourcePosition::new(63, 3, 16),
]),
]);
}
#[test]
fn incorrect_value_and_missing_argument() {
expect_fails_rule(factory, r#"
{
complicatedArgs {
multipleReqs(req1: "one")
}
}
"#,
&[
RuleError::new(&field_error_message("multipleReqs", "req2", "Int!"), &[
SourcePosition::new(63, 3, 16),
]),
]);
}
#[test]
fn ignores_unknown_directives() {
expect_passes_rule(factory, r#"
{
dog @unknown
}
"#);
}
#[test]
fn with_directives_of_valid_types() {
expect_passes_rule(factory, r#"
{
dog @include(if: true) {
name
}
human @skip(if: false) {
name
}
}
"#);
}
#[test]
fn with_directive_with_missing_types() {
expect_fails_rule(factory, r#"
{
dog @include {
name @skip
}
}
"#,
&[
RuleError::new(&directive_error_message("include", "if", "Boolean!"), &[
SourcePosition::new(33, 2, 18),
]),
RuleError::new(&directive_error_message("skip", "if", "Boolean!"), &[
SourcePosition::new(65, 3, 21),
]),
]);
}
}

View file

@ -0,0 +1,168 @@
use ast::Field;
use validation::{ValidatorContext, Visitor, RuleError};
use parser::Spanning;
pub struct ScalarLeafs {}
pub fn factory() -> ScalarLeafs {
ScalarLeafs {}
}
impl<'a> Visitor<'a> for ScalarLeafs {
fn enter_field(&mut self, ctx: &mut ValidatorContext<'a>, field: &'a Spanning<Field>) {
let field_name = &field.item.name.item;
let error = if let (Some(field_type), Some(field_type_literal)) = (ctx.current_type(), ctx.current_type_literal()) {
match (field_type.is_leaf(), &field.item.selection_set) {
(true, &Some(_)) => Some(RuleError::new(
&no_allowed_error_message(field_name, &format!("{}", field_type_literal)),
&[field.start.clone()])),
(false, &None) => Some(RuleError::new(
&required_error_message(field_name, &format!("{}", field_type_literal)),
&[field.start.clone()])),
_ => None,
}
} else { None };
if let Some(error) = error {
ctx.append_errors(vec![error]);
}
}
}
fn no_allowed_error_message(field_name: &str, type_name: &str) -> String {
format!(
r#"Field "{}" must not have a selection since type {} has no subfields"#,
field_name, type_name)
}
fn required_error_message(field_name: &str, type_name: &str) -> String {
format!(
r#"Field "{}" of type "{}" must have a selection of subfields. Did you mean "{} {{ ... }}"?"#,
field_name, type_name, field_name)
}
#[cfg(test)]
mod tests {
use super::{no_allowed_error_message, required_error_message, factory};
use parser::SourcePosition;
use validation::{RuleError, expect_passes_rule, expect_fails_rule};
#[test]
fn valid_scalar_selection() {
expect_passes_rule(factory, r#"
fragment scalarSelection on Dog {
barks
}
"#);
}
#[test]
fn object_type_missing_selection() {
expect_fails_rule(factory, r#"
query directQueryOnObjectWithoutSubFields {
human
}
"#,
&[
RuleError::new(&required_error_message("human", "Human"), &[
SourcePosition::new(67, 2, 12),
]),
]);
}
#[test]
fn interface_type_missing_selection() {
expect_fails_rule(factory, r#"
{
human { pets }
}
"#,
&[
RuleError::new(&required_error_message("pets", "[Pet]"), &[
SourcePosition::new(33, 2, 20),
]),
]);
}
#[test]
fn valid_scalar_selection_with_args() {
expect_passes_rule(factory, r#"
fragment scalarSelectionWithArgs on Dog {
doesKnowCommand(dogCommand: SIT)
}
"#);
}
#[test]
fn scalar_selection_not_allowed_on_boolean() {
expect_fails_rule(factory, r#"
fragment scalarSelectionsNotAllowedOnBoolean on Dog {
barks { sinceWhen }
}
"#,
&[
RuleError::new(&no_allowed_error_message("barks", "Boolean"), &[
SourcePosition::new(77, 2, 12),
]),
]);
}
#[test]
fn scalar_selection_not_allowed_on_enum() {
expect_fails_rule(factory, r#"
fragment scalarSelectionsNotAllowedOnEnum on Cat {
furColor { inHexdec }
}
"#,
&[
RuleError::new(&no_allowed_error_message("furColor", "FurColor"), &[
SourcePosition::new(74, 2, 12),
]),
]);
}
#[test]
fn scalar_selection_not_allowed_with_args() {
expect_fails_rule(factory, r#"
fragment scalarSelectionsNotAllowedWithArgs on Dog {
doesKnowCommand(dogCommand: SIT) { sinceWhen }
}
"#,
&[
RuleError::new(&no_allowed_error_message("doesKnowCommand", "Boolean"), &[
SourcePosition::new(76, 2, 12),
]),
]);
}
#[test]
fn scalar_selection_not_allowed_with_directives() {
expect_fails_rule(factory, r#"
fragment scalarSelectionsNotAllowedWithDirectives on Dog {
name @include(if: true) { isAlsoHumanName }
}
"#,
&[
RuleError::new(&no_allowed_error_message("name", "String"), &[
SourcePosition::new(82, 2, 12),
]),
]);
}
#[test]
fn scalar_selection_not_allowed_with_directives_and_args() {
expect_fails_rule(factory, r#"
fragment scalarSelectionsNotAllowedWithDirectivesAndArgs on Dog {
doesKnowCommand(dogCommand: SIT) @include(if: true) { sinceWhen }
}
"#,
&[
RuleError::new(&no_allowed_error_message("doesKnowCommand", "Boolean"), &[
SourcePosition::new(89, 2, 12),
]),
]);
}
}

View file

@ -0,0 +1,201 @@
use std::collections::hash_map::{HashMap, Entry};
use ast::{Directive, Field, InputValue};
use validation::{ValidatorContext, Visitor};
use parser::{SourcePosition, Spanning};
pub struct UniqueArgumentNames<'a> {
known_names: HashMap<&'a str, SourcePosition>,
}
pub fn factory<'a>() -> UniqueArgumentNames<'a> {
UniqueArgumentNames {
known_names: HashMap::new(),
}
}
impl<'a> Visitor<'a> for UniqueArgumentNames<'a> {
fn enter_directive(&mut self, _: &mut ValidatorContext<'a>, _: &'a Spanning<Directive>) {
self.known_names = HashMap::new();
}
fn enter_field(&mut self, _: &mut ValidatorContext<'a>, _: &'a Spanning<Field>) {
self.known_names = HashMap::new();
}
fn enter_argument(&mut self, ctx: &mut ValidatorContext<'a>, &(ref arg_name, _): &'a (Spanning<String>, Spanning<InputValue>)) {
match self.known_names.entry(&arg_name.item) {
Entry::Occupied(e) => {
ctx.report_error(
&error_message(&arg_name.item),
&[e.get().clone(), arg_name.start.clone()]);
}
Entry::Vacant(e) => {
e.insert(arg_name.start.clone());
}
}
}
}
fn error_message(arg_name: &str) -> String {
format!("There can only be one argument named \"{}\"", arg_name)
}
#[cfg(test)]
mod tests {
use super::{error_message, factory};
use parser::SourcePosition;
use validation::{RuleError, expect_passes_rule, expect_fails_rule};
#[test]
fn no_arguments_on_field() {
expect_passes_rule(factory, r#"
{
field
}
"#);
}
#[test]
fn no_arguments_on_directive() {
expect_passes_rule(factory, r#"
{
field @directive
}
"#);
}
#[test]
fn argument_on_field() {
expect_passes_rule(factory, r#"
{
field(arg: "value")
}
"#);
}
#[test]
fn argument_on_directive() {
expect_passes_rule(factory, r#"
{
field @directive(arg: "value")
}
"#);
}
#[test]
fn same_argument_on_two_fields() {
expect_passes_rule(factory, r#"
{
one: field(arg: "value")
two: field(arg: "value")
}
"#);
}
#[test]
fn same_argument_on_field_and_directive() {
expect_passes_rule(factory, r#"
{
field(arg: "value") @directive(arg: "value")
}
"#);
}
#[test]
fn same_argument_on_two_directives() {
expect_passes_rule(factory, r#"
{
field @directive1(arg: "value") @directive2(arg: "value")
}
"#);
}
#[test]
fn multiple_field_arguments() {
expect_passes_rule(factory, r#"
{
field(arg1: "value", arg2: "value", arg3: "value")
}
"#);
}
#[test]
fn multiple_directive_arguments() {
expect_passes_rule(factory, r#"
{
field @directive(arg1: "value", arg2: "value", arg3: "value")
}
"#);
}
#[test]
fn duplicate_field_arguments() {
expect_fails_rule(factory, r#"
{
field(arg1: "value", arg1: "value")
}
"#,
&[
RuleError::new(&error_message("arg1"), &[
SourcePosition::new(31, 2, 18),
SourcePosition::new(46, 2, 33),
]),
]);
}
#[test]
fn many_duplicate_field_arguments() {
expect_fails_rule(factory, r#"
{
field(arg1: "value", arg1: "value", arg1: "value")
}
"#,
&[
RuleError::new(&error_message("arg1"), &[
SourcePosition::new(31, 2, 18),
SourcePosition::new(46, 2, 33),
]),
RuleError::new(&error_message("arg1"), &[
SourcePosition::new(31, 2, 18),
SourcePosition::new(61, 2, 48),
]),
]);
}
#[test]
fn duplicate_directive_arguments() {
expect_fails_rule(factory, r#"
{
field @directive(arg1: "value", arg1: "value")
}
"#,
&[
RuleError::new(&error_message("arg1"), &[
SourcePosition::new(42, 2, 29),
SourcePosition::new(57, 2, 44),
]),
]);
}
#[test]
fn many_duplicate_directive_arguments() {
expect_fails_rule(factory, r#"
{
field @directive(arg1: "value", arg1: "value", arg1: "value")
}
"#,
&[
RuleError::new(&error_message("arg1"), &[
SourcePosition::new(42, 2, 29),
SourcePosition::new(57, 2, 44),
]),
RuleError::new(&error_message("arg1"), &[
SourcePosition::new(42, 2, 29),
SourcePosition::new(72, 2, 59),
]),
]);
}
}

View file

@ -0,0 +1,149 @@
use std::collections::hash_map::{HashMap, Entry};
use ast::Fragment;
use parser::{SourcePosition, Spanning};
use validation::{ValidatorContext, Visitor};
pub struct UniqueFragmentNames<'a> {
names: HashMap<&'a str, SourcePosition>,
}
pub fn factory<'a>() -> UniqueFragmentNames<'a> {
UniqueFragmentNames {
names: HashMap::new(),
}
}
impl<'a> Visitor<'a> for UniqueFragmentNames<'a> {
fn enter_fragment_definition(&mut self, context: &mut ValidatorContext<'a>, f: &'a Spanning<Fragment>) {
match self.names.entry(&f.item.name.item) {
Entry::Occupied(e) => {
context.report_error(
&duplicate_message(&f.item.name.item),
&[e.get().clone(), f.item.name.start.clone()]);
}
Entry::Vacant(e) => {
e.insert(f.item.name.start.clone());
}
}
}
}
fn duplicate_message(frag_name: &str) -> String {
format!("There can only be one fragment named {}", frag_name)
}
#[cfg(test)]
mod tests {
use super::{duplicate_message, factory};
use parser::SourcePosition;
use validation::{RuleError, expect_passes_rule, expect_fails_rule};
#[test]
fn no_fragments() {
expect_passes_rule(factory, r#"
{
field
}
"#);
}
#[test]
fn one_fragment() {
expect_passes_rule(factory, r#"
{
...fragA
}
fragment fragA on Type {
field
}
"#);
}
#[test]
fn many_fragments() {
expect_passes_rule(factory, r#"
{
...fragA
...fragB
...fragC
}
fragment fragA on Type {
fieldA
}
fragment fragB on Type {
fieldB
}
fragment fragC on Type {
fieldC
}
"#);
}
#[test]
fn inline_fragments_always_unique() {
expect_passes_rule(factory, r#"
{
...on Type {
fieldA
}
...on Type {
fieldB
}
}
"#);
}
#[test]
fn fragment_and_operation_named_the_same() {
expect_passes_rule(factory, r#"
query Foo {
...Foo
}
fragment Foo on Type {
field
}
"#);
}
#[test]
fn fragments_named_the_same() {
expect_fails_rule(factory, r#"
{
...fragA
}
fragment fragA on Type {
fieldA
}
fragment fragA on Type {
fieldB
}
"#,
&[
RuleError::new(&duplicate_message("fragA"), &[
SourcePosition::new(65, 4, 19),
SourcePosition::new(131, 7, 19)
]),
]);
}
#[test]
fn fragments_named_the_same_no_reference() {
expect_fails_rule(factory, r#"
fragment fragA on Type {
fieldA
}
fragment fragA on Type {
fieldB
}
"#,
&[
RuleError::new(&duplicate_message("fragA"), &[
SourcePosition::new(20, 1, 19),
SourcePosition::new(86, 4, 19)
]),
]);
}
}

View file

@ -0,0 +1,131 @@
use std::collections::hash_map::{HashMap, Entry};
use ast::InputValue;
use validation::{ValidatorContext, Visitor};
use parser::{SourcePosition, Spanning};
pub struct UniqueInputFieldNames<'a> {
known_name_stack: Vec<HashMap<&'a str, SourcePosition>>,
}
pub fn factory<'a>() -> UniqueInputFieldNames<'a> {
UniqueInputFieldNames {
known_name_stack: Vec::new(),
}
}
impl<'a> Visitor<'a> for UniqueInputFieldNames<'a> {
fn enter_object_value(&mut self, _: &mut ValidatorContext<'a>, _: Spanning<&'a Vec<(Spanning<String>, Spanning<InputValue>)>>) {
self.known_name_stack.push(HashMap::new());
}
fn exit_object_value(&mut self, _: &mut ValidatorContext<'a>, _: Spanning<&'a Vec<(Spanning<String>, Spanning<InputValue>)>>) {
self.known_name_stack.pop();
}
fn enter_object_field(&mut self, ctx: &mut ValidatorContext<'a>, &(ref field_name, _): &'a (Spanning<String>, Spanning<InputValue>)) {
if let Some(ref mut known_names) = self.known_name_stack.last_mut() {
match known_names.entry(&field_name.item) {
Entry::Occupied(e) => {
ctx.report_error(
&error_message(&field_name.item),
&[e.get().clone(), field_name.start.clone()]);
}
Entry::Vacant(e) => {
e.insert(field_name.start.clone());
}
}
}
}
}
fn error_message(field_name: &str) -> String {
format!("There can only be one input field named \"{}\"", field_name)
}
#[cfg(test)]
mod tests {
use super::{error_message, factory};
use parser::SourcePosition;
use validation::{RuleError, expect_passes_rule, expect_fails_rule};
#[test]
fn input_object_with_fields() {
expect_passes_rule(factory, r#"
{
field(arg: { f: true })
}
"#);
}
#[test]
fn same_input_object_within_two_args() {
expect_passes_rule(factory, r#"
{
field(arg1: { f: true }, arg2: { f: true })
}
"#);
}
#[test]
fn multiple_input_object_fields() {
expect_passes_rule(factory, r#"
{
field(arg: { f1: "value", f2: "value", f3: "value" })
}
"#);
}
#[test]
fn allows_for_nested_input_objects_with_similar_fields() {
expect_passes_rule(factory, r#"
{
field(arg: {
deep: {
deep: {
id: 1
}
id: 1
}
id: 1
})
}
"#);
}
#[test]
fn duplicate_input_object_fields() {
expect_fails_rule(factory, r#"
{
field(arg: { f1: "value", f1: "value" })
}
"#,
&[
RuleError::new(&error_message("f1"), &[
SourcePosition::new(38, 2, 25),
SourcePosition::new(51, 2, 38),
]),
]);
}
#[test]
fn many_duplicate_input_object_fields() {
expect_fails_rule(factory, r#"
{
field(arg: { f1: "value", f1: "value", f1: "value" })
}
"#,
&[
RuleError::new(&error_message("f1"), &[
SourcePosition::new(38, 2, 25),
SourcePosition::new(51, 2, 38),
]),
RuleError::new(&error_message("f1"), &[
SourcePosition::new(38, 2, 25),
SourcePosition::new(64, 2, 51),
]),
]);
}
}

View file

@ -0,0 +1,145 @@
use std::collections::hash_map::{HashMap, Entry};
use ast::Operation;
use parser::{SourcePosition, Spanning};
use validation::{ValidatorContext, Visitor};
pub struct UniqueOperationNames<'a> {
names: HashMap<&'a str, SourcePosition>,
}
pub fn factory<'a>() -> UniqueOperationNames<'a> {
UniqueOperationNames {
names: HashMap::new(),
}
}
impl<'a> Visitor<'a> for UniqueOperationNames<'a> {
fn enter_operation_definition(&mut self, ctx: &mut ValidatorContext<'a>, op: &'a Spanning<Operation>) {
if let &Some(ref op_name) = &op.item.name {
match self.names.entry(&op_name.item) {
Entry::Occupied(e) => {
ctx.report_error(
&error_message(&op_name.item),
&[e.get().clone(), op.start.clone()]);
}
Entry::Vacant(e) => {
e.insert(op.start.clone());
}
}
}
}
}
fn error_message(op_name: &str) -> String {
format!("There can only be one operation named {}", op_name)
}
#[cfg(test)]
mod tests {
use super::{error_message, factory};
use parser::SourcePosition;
use validation::{RuleError, expect_passes_rule, expect_fails_rule};
#[test]
fn no_operations() {
expect_passes_rule(factory, r#"
fragment fragA on Type {
field
}
"#);
}
#[test]
fn one_anon_operation() {
expect_passes_rule(factory, r#"
{
field
}
"#);
}
#[test]
fn one_named_operation() {
expect_passes_rule(factory, r#"
query Foo {
field
}
"#);
}
#[test]
fn multiple_operations() {
expect_passes_rule(factory, r#"
query Foo {
field
}
query Bar {
field
}
"#);
}
#[test]
fn multiple_operations_of_different_types() {
expect_passes_rule(factory, r#"
query Foo {
field
}
mutation Bar {
field
}
"#);
}
#[test]
fn fragment_and_operation_named_the_same() {
expect_passes_rule(factory, r#"
query Foo {
...Foo
}
fragment Foo on Type {
field
}
"#);
}
#[test]
fn multiple_operations_of_same_name() {
expect_fails_rule(factory, r#"
query Foo {
fieldA
}
query Foo {
fieldB
}
"#,
&[
RuleError::new(&error_message("Foo"), &[
SourcePosition::new(11, 1, 10),
SourcePosition::new(64, 4, 10),
]),
]);
}
#[test]
fn multiple_ops_of_same_name_of_different_types() {
expect_fails_rule(factory, r#"
query Foo {
fieldA
}
mutation Foo {
fieldB
}
"#,
&[
RuleError::new(&error_message("Foo"), &[
SourcePosition::new(11, 1, 10),
SourcePosition::new(64, 4, 10),
]),
]);
}
}

View file

@ -0,0 +1,81 @@
use std::collections::hash_map::{HashMap, Entry};
use ast::{Operation, VariableDefinition};
use parser::{SourcePosition, Spanning};
use validation::{ValidatorContext, Visitor};
pub struct UniqueVariableNames<'a> {
names: HashMap<&'a str, SourcePosition>,
}
pub fn factory<'a>() -> UniqueVariableNames<'a> {
UniqueVariableNames {
names: HashMap::new(),
}
}
impl<'a> Visitor<'a> for UniqueVariableNames<'a> {
fn enter_operation_definition(&mut self, _: &mut ValidatorContext<'a>, _: &'a Spanning<Operation>) {
self.names = HashMap::new();
}
fn enter_variable_definition(&mut self, ctx: &mut ValidatorContext<'a>, &(ref var_name, _): &'a (Spanning<String>, VariableDefinition)) {
match self.names.entry(&var_name.item) {
Entry::Occupied(e) => {
ctx.report_error(
&error_message(&var_name.item),
&[e.get().clone(), var_name.start.clone()]);
}
Entry::Vacant(e) => {
e.insert(var_name.start.clone());
}
}
}
}
fn error_message(var_name: &str) -> String {
format!("There can only be one variable named {}", var_name)
}
#[cfg(test)]
mod tests {
use super::{error_message, factory};
use parser::SourcePosition;
use validation::{RuleError, expect_passes_rule, expect_fails_rule};
#[test]
fn unique_variable_names() {
expect_passes_rule(factory, r#"
query A($x: Int, $y: String) { __typename }
query B($x: String, $y: Int) { __typename }
"#);
}
#[test]
fn duplicate_variable_names() {
expect_fails_rule(factory, r#"
query A($x: Int, $x: Int, $x: String) { __typename }
query B($x: String, $x: Int) { __typename }
query C($x: Int, $x: Int) { __typename }
"#,
&[
RuleError::new(&error_message("x"), &[
SourcePosition::new(19, 1, 18),
SourcePosition::new(28, 1, 27),
]),
RuleError::new(&error_message("x"), &[
SourcePosition::new(19, 1, 18),
SourcePosition::new(37, 1, 36),
]),
RuleError::new(&error_message("x"), &[
SourcePosition::new(82, 2, 18),
SourcePosition::new(94, 2, 30),
]),
RuleError::new(&error_message("x"), &[
SourcePosition::new(136, 3, 18),
SourcePosition::new(145, 3, 27),
]),
]);
}
}

View file

@ -0,0 +1,62 @@
use ast::VariableDefinition;
use parser::Spanning;
use validation::{ValidatorContext, Visitor};
pub struct UniqueVariableNames {}
pub fn factory() -> UniqueVariableNames {
UniqueVariableNames {}
}
impl<'a> Visitor<'a> for UniqueVariableNames {
fn enter_variable_definition(&mut self, ctx: &mut ValidatorContext<'a>, &(ref var_name, ref var_def): &'a (Spanning<String>, VariableDefinition)) {
if let Some(var_type) = ctx.schema.concrete_type_by_name(var_def.var_type.item.innermost_name()) {
if !var_type.is_input() {
ctx.report_error(
&error_message(&var_name.item, &format!("{}", var_def.var_type.item)),
&[var_def.var_type.start.clone()]);
}
}
}
}
fn error_message(var_name: &str, type_name: &str) -> String {
format!("Variable \"{}\" cannot be of non-input type \"{}\"", var_name, type_name)
}
#[cfg(test)]
mod tests {
use super::{error_message, factory};
use parser::SourcePosition;
use validation::{RuleError, expect_passes_rule, expect_fails_rule};
#[test]
fn input_types_are_valid() {
expect_passes_rule(factory, r#"
query Foo($a: String, $b: [Boolean!]!, $c: ComplexInput) {
field(a: $a, b: $b, c: $c)
}
"#);
}
#[test]
fn output_types_are_invalid() {
expect_fails_rule(factory, r#"
query Foo($a: Dog, $b: [[CatOrDog!]]!, $c: Pet) {
field(a: $a, b: $b, c: $c)
}
"#,
&[
RuleError::new(&error_message("a", "Dog"), &[
SourcePosition::new(25, 1, 24),
]),
RuleError::new(&error_message("b", "[[CatOrDog!]]!"), &[
SourcePosition::new(34, 1, 33),
]),
RuleError::new(&error_message("c", "Pet"), &[
SourcePosition::new(54, 1, 53),
]),
]);
}
}

View file

@ -0,0 +1,429 @@
use std::collections::{HashSet, HashMap};
use ast::{Type, VariableDefinition, Document, Fragment, Operation, FragmentSpread};
use parser::Spanning;
use validation::{ValidatorContext, Visitor};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Scope<'a> {
Operation(Option<&'a str>),
Fragment(&'a str),
}
pub struct VariableInAllowedPosition<'a> {
spreads: HashMap<Scope<'a>, HashSet<&'a str>>,
variable_usages: HashMap<Scope<'a>, Vec<(Spanning<&'a String>, Type)>>,
variable_defs: HashMap<Scope<'a>, Vec<&'a (Spanning<String>, VariableDefinition)>>,
current_scope: Option<Scope<'a>>,
}
pub fn factory<'a>() -> VariableInAllowedPosition<'a> {
VariableInAllowedPosition {
spreads: HashMap::new(),
variable_usages: HashMap::new(),
variable_defs: HashMap::new(),
current_scope: None,
}
}
impl<'a> VariableInAllowedPosition<'a> {
fn collect_incorrect_usages(
&self,
from: &Scope<'a>,
var_defs: &Vec<&'a (Spanning<String>, VariableDefinition)>,
ctx: &mut ValidatorContext<'a>,
visited: &mut HashSet<Scope<'a>>,
)
{
if visited.contains(from) {
return;
}
visited.insert(from.clone());
if let Some(usages) = self.variable_usages.get(from) {
for &(ref var_name, ref var_type) in usages {
if let Some(&&(ref var_def_name, ref var_def)) = var_defs
.iter()
.filter(|&&&(ref n, _)| &n.item == var_name.item)
.next()
{
let expected_type = match (&var_def.default_value, &var_def.var_type.item) {
(&Some(_), &Type::List(ref inner)) => Type::NonNullList(inner.clone()),
(&Some(_), &Type::Named(ref inner)) => Type::NonNullNamed(inner.clone()),
(_, t) => t.clone(),
};
println!("Variable {} of type {} used in position expecting {}", var_name.item, expected_type, var_type);
if !ctx.schema.is_subtype(&expected_type, var_type) {
ctx.report_error(
&error_message(&var_name.item, &format!("{}", expected_type), &format!("{}", var_type)),
&[var_def_name.start.clone(), var_name.start.clone()]);
}
}
}
}
if let Some(spreads) = self.spreads.get(from) {
for spread in spreads {
self.collect_incorrect_usages(&Scope::Fragment(spread), var_defs, ctx, visited);
}
}
}
}
impl<'a> Visitor<'a> for VariableInAllowedPosition<'a> {
fn exit_document(&mut self, ctx: &mut ValidatorContext<'a>, _: &'a Document) {
for (op_scope, var_defs) in &self.variable_defs {
self.collect_incorrect_usages(&op_scope, var_defs, ctx, &mut HashSet::new());
}
}
fn enter_fragment_definition(&mut self, _: &mut ValidatorContext<'a>, fragment: &'a Spanning<Fragment>) {
self.current_scope = Some(Scope::Fragment(&fragment.item.name.item));
}
fn enter_operation_definition(&mut self, _: &mut ValidatorContext<'a>, op: &'a Spanning<Operation>) {
self.current_scope = Some(Scope::Operation(op.item.name.as_ref().map(|s| s.item.as_str())));
}
fn enter_fragment_spread(&mut self, _: &mut ValidatorContext<'a>, spread: &'a Spanning<FragmentSpread>) {
if let Some(ref scope) = self.current_scope {
self.spreads
.entry(scope.clone())
.or_insert_with(|| HashSet::new())
.insert(&spread.item.name.item);
}
}
fn enter_variable_definition(&mut self, _: &mut ValidatorContext<'a>, def: &'a (Spanning<String>, VariableDefinition)) {
if let Some(ref scope) = self.current_scope {
self.variable_defs
.entry(scope.clone())
.or_insert_with(|| Vec::new())
.push(def);
}
}
fn enter_variable_value(&mut self, ctx: &mut ValidatorContext<'a>, var_name: Spanning<&'a String>) {
if let (&Some(ref scope), Some(input_type)) = (&self.current_scope, ctx.current_input_type_literal()) {
self.variable_usages
.entry(scope.clone())
.or_insert_with(|| Vec::new())
.push((Spanning::start_end(&var_name.start, &var_name.end, &var_name.item), input_type.clone()));
}
}
}
fn error_message(var_name: &str, type_name: &str, expected_type_name: &str) -> String {
format!(
"Variable \"{}\" of type \"{}\" used in position expecting type \"{}\"",
var_name, type_name, expected_type_name)
}
#[cfg(test)]
mod tests {
use super::{error_message, factory};
use parser::SourcePosition;
use validation::{RuleError, expect_passes_rule, expect_fails_rule};
#[test]
fn boolean_into_boolean() {
expect_passes_rule(factory, r#"
query Query($booleanArg: Boolean)
{
complicatedArgs {
booleanArgField(booleanArg: $booleanArg)
}
}
"#);
}
#[test]
fn boolean_into_boolean_within_fragment() {
expect_passes_rule(factory, r#"
fragment booleanArgFrag on ComplicatedArgs {
booleanArgField(booleanArg: $booleanArg)
}
query Query($booleanArg: Boolean)
{
complicatedArgs {
...booleanArgFrag
}
}
"#);
expect_passes_rule(factory, r#"
query Query($booleanArg: Boolean)
{
complicatedArgs {
...booleanArgFrag
}
}
fragment booleanArgFrag on ComplicatedArgs {
booleanArgField(booleanArg: $booleanArg)
}
"#);
}
#[test]
fn non_null_boolean_into_boolean() {
expect_passes_rule(factory, r#"
query Query($nonNullBooleanArg: Boolean!)
{
complicatedArgs {
booleanArgField(booleanArg: $nonNullBooleanArg)
}
}
"#);
}
#[test]
fn non_null_boolean_into_boolean_within_fragment() {
expect_passes_rule(factory, r#"
fragment booleanArgFrag on ComplicatedArgs {
booleanArgField(booleanArg: $nonNullBooleanArg)
}
query Query($nonNullBooleanArg: Boolean!)
{
complicatedArgs {
...booleanArgFrag
}
}
"#);
}
#[test]
fn int_into_non_null_int_with_default() {
expect_passes_rule(factory, r#"
query Query($intArg: Int = 1)
{
complicatedArgs {
nonNullIntArgField(nonNullIntArg: $intArg)
}
}
"#);
}
#[test]
fn string_list_into_string_list() {
expect_passes_rule(factory, r#"
query Query($stringListVar: [String])
{
complicatedArgs {
stringListArgField(stringListArg: $stringListVar)
}
}
"#);
}
#[test]
fn non_null_string_list_into_string_list() {
expect_passes_rule(factory, r#"
query Query($stringListVar: [String!])
{
complicatedArgs {
stringListArgField(stringListArg: $stringListVar)
}
}
"#);
}
#[test]
fn string_into_string_list_in_item_position() {
expect_passes_rule(factory, r#"
query Query($stringVar: String)
{
complicatedArgs {
stringListArgField(stringListArg: [$stringVar])
}
}
"#);
}
#[test]
fn non_null_string_into_string_list_in_item_position() {
expect_passes_rule(factory, r#"
query Query($stringVar: String!)
{
complicatedArgs {
stringListArgField(stringListArg: [$stringVar])
}
}
"#);
}
#[test]
fn complex_input_into_complex_input() {
expect_passes_rule(factory, r#"
query Query($complexVar: ComplexInput)
{
complicatedArgs {
complexArgField(complexArg: $complexVar)
}
}
"#);
}
#[test]
fn complex_input_into_complex_input_in_field_position() {
expect_passes_rule(factory, r#"
query Query($boolVar: Boolean = false)
{
complicatedArgs {
complexArgField(complexArg: {requiredArg: $boolVar})
}
}
"#);
}
#[test]
fn non_null_boolean_into_non_null_boolean_in_directive() {
expect_passes_rule(factory, r#"
query Query($boolVar: Boolean!)
{
dog @include(if: $boolVar)
}
"#);
}
#[test]
fn boolean_in_non_null_in_directive_with_default() {
expect_passes_rule(factory, r#"
query Query($boolVar: Boolean = false)
{
dog @include(if: $boolVar)
}
"#);
}
#[test]
fn int_into_non_null_int() {
expect_fails_rule(factory, r#"
query Query($intArg: Int) {
complicatedArgs {
nonNullIntArgField(nonNullIntArg: $intArg)
}
}
"#,
&[
RuleError::new(&error_message("intArg", "Int", "Int!"), &[
SourcePosition::new(23, 1, 22),
SourcePosition::new(117, 3, 48),
]),
]);
}
#[test]
fn int_into_non_null_int_within_fragment() {
expect_fails_rule(factory, r#"
fragment nonNullIntArgFieldFrag on ComplicatedArgs {
nonNullIntArgField(nonNullIntArg: $intArg)
}
query Query($intArg: Int) {
complicatedArgs {
...nonNullIntArgFieldFrag
}
}
"#,
&[
RuleError::new(&error_message("intArg", "Int", "Int!"), &[
SourcePosition::new(154, 5, 22),
SourcePosition::new(110, 2, 46),
]),
]);
}
#[test]
fn int_into_non_null_int_within_nested_fragment() {
expect_fails_rule(factory, r#"
fragment outerFrag on ComplicatedArgs {
...nonNullIntArgFieldFrag
}
fragment nonNullIntArgFieldFrag on ComplicatedArgs {
nonNullIntArgField(nonNullIntArg: $intArg)
}
query Query($intArg: Int) {
complicatedArgs {
...outerFrag
}
}
"#,
&[
RuleError::new(&error_message("intArg", "Int", "Int!"), &[
SourcePosition::new(255, 9, 22),
SourcePosition::new(211, 6, 46),
]),
]);
}
#[test]
fn string_over_boolean() {
expect_fails_rule(factory, r#"
query Query($stringVar: String) {
complicatedArgs {
booleanArgField(booleanArg: $stringVar)
}
}
"#,
&[
RuleError::new(&error_message("stringVar", "String", "Boolean"), &[
SourcePosition::new(23, 1, 22),
SourcePosition::new(117, 3, 42),
]),
]);
}
#[test]
fn string_into_string_list() {
expect_fails_rule(factory, r#"
query Query($stringVar: String) {
complicatedArgs {
stringListArgField(stringListArg: $stringVar)
}
}
"#,
&[
RuleError::new(&error_message("stringVar", "String", "[String]"), &[
SourcePosition::new(23, 1, 22),
SourcePosition::new(123, 3, 48),
]),
]);
}
#[test]
fn boolean_into_non_null_boolean_in_directive() {
expect_fails_rule(factory, r#"
query Query($boolVar: Boolean) {
dog @include(if: $boolVar)
}
"#,
&[
RuleError::new(&error_message("boolVar", "Boolean", "Boolean!"), &[
SourcePosition::new(23, 1, 22),
SourcePosition::new(73, 2, 29),
]),
]);
}
#[test]
fn string_into_non_null_boolean_in_directive() {
expect_fails_rule(factory, r#"
query Query($stringVar: String) {
dog @include(if: $stringVar)
}
"#,
&[
RuleError::new(&error_message("stringVar", "String", "Boolean!"), &[
SourcePosition::new(23, 1, 22),
SourcePosition::new(74, 2, 29),
]),
]);
}
}

View file

@ -0,0 +1,479 @@
use parser::parse_document_source;
use ast::{FromInputValue, InputValue};
use types::base::GraphQLType;
use types::schema::Registry;
use types::scalars::ID;
use schema::model::{DirectiveType, DirectiveLocation, RootNode};
use schema::meta::{EnumValue, MetaType};
use validation::{Visitor, RuleError, ValidatorContext, MultiVisitor, visit};
struct Being;
struct Pet;
struct Canine;
struct Dog;
struct Cat;
struct Intelligent;
struct Human;
struct Alien;
struct DogOrHuman;
struct CatOrDog;
struct HumanOrAlien;
struct ComplicatedArgs;
struct QueryRoot;
#[derive(Debug)]
enum DogCommand {
Sit,
Heel,
Down,
}
#[derive(Debug)]
enum FurColor {
Brown,
Black,
Tan,
Spotted,
}
#[allow(dead_code)]
#[derive(Debug)]
struct ComplexInput {
required_field: bool,
int_field: Option<i64>,
string_field: Option<String>,
boolean_field: Option<bool>,
string_list_field: Option<Vec<Option<String>>>,
}
impl<CtxT> GraphQLType<CtxT> for Being {
fn name() -> Option<&'static str> {
Some("Being")
}
fn meta(registry: &mut Registry<CtxT>) -> MetaType {
registry.build_interface_type::<Self>()(&[
registry.field::<Option<String>>("name")
.argument(registry.arg::<Option<bool>>("surname")),
])
.into_meta()
}
}
impl<CtxT> GraphQLType<CtxT> for Pet {
fn name() -> Option<&'static str> {
Some("Pet")
}
fn meta(registry: &mut Registry<CtxT>) -> MetaType {
registry.build_interface_type::<Self>()(&[
registry.field::<Option<String>>("name")
.argument(registry.arg::<Option<bool>>("surname")),
])
.into_meta()
}
}
impl<CtxT> GraphQLType<CtxT> for Canine {
fn name() -> Option<&'static str> {
Some("Canine")
}
fn meta(registry: &mut Registry<CtxT>) -> MetaType {
registry.build_interface_type::<Self>()(&[
registry.field::<Option<String>>("name")
.argument(registry.arg::<Option<bool>>("surname")),
])
.into_meta()
}
}
impl<CtxT> GraphQLType<CtxT> for DogCommand {
fn name() -> Option<&'static str> {
Some("DogCommand")
}
fn meta(registry: &mut Registry<CtxT>) -> MetaType {
registry.build_enum_type::<Self>()(&[
EnumValue::new("SIT"),
EnumValue::new("HEEL"),
EnumValue::new("DOWN"),
])
.into_meta()
}
}
impl FromInputValue for DogCommand {
fn from(v: &InputValue) -> Option<DogCommand> {
match v.as_enum_value() {
Some("SIT") => Some(DogCommand::Sit),
Some("HEEL") => Some(DogCommand::Heel),
Some("DOWN") => Some(DogCommand::Down),
_ => None,
}
}
}
impl<CtxT> GraphQLType<CtxT> for Dog {
fn name() -> Option<&'static str> {
Some("Dog")
}
fn meta(registry: &mut Registry<CtxT>) -> MetaType {
registry.build_object_type::<Self>()(&[
registry.field::<Option<String>>("name")
.argument(registry.arg::<Option<bool>>("surname")),
registry.field::<Option<String>>("nickname"),
registry.field::<Option<i64>>("barkVolume"),
registry.field::<Option<bool>>("barks"),
registry.field::<Option<bool>>("doesKnowCommand")
.argument(registry.arg::<Option<DogCommand>>("dogCommand")),
registry.field::<Option<bool>>("isHousetrained")
.argument(registry.arg_with_default("atOtherHomes", &true)),
registry.field::<Option<bool>>("isAtLocation")
.argument(registry.arg::<Option<i64>>("x"))
.argument(registry.arg::<Option<i64>>("y")),
])
.interfaces(&[
registry.get_type::<Being>(),
registry.get_type::<Pet>(),
registry.get_type::<Canine>(),
])
.into_meta()
}
}
impl<CtxT> GraphQLType<CtxT> for FurColor {
fn name() -> Option<&'static str> {
Some("FurColor")
}
fn meta(registry: &mut Registry<CtxT>) -> MetaType {
registry.build_enum_type::<Self>()(&[
EnumValue::new("BROWN"),
EnumValue::new("BLACK"),
EnumValue::new("TAN"),
EnumValue::new("SPOTTED"),
])
.into_meta()
}
}
impl FromInputValue for FurColor {
fn from(v: &InputValue) -> Option<FurColor> {
match v.as_enum_value() {
Some("BROWN") => Some(FurColor::Brown),
Some("BLACK") => Some(FurColor::Black),
Some("TAN") => Some(FurColor::Tan),
Some("SPOTTED") => Some(FurColor::Spotted),
_ => None,
}
}
}
impl<CtxT> GraphQLType<CtxT> for Cat {
fn name() -> Option<&'static str> {
Some("Cat")
}
fn meta(registry: &mut Registry<CtxT>) -> MetaType {
registry.build_object_type::<Self>()(&[
registry.field::<Option<String>>("name")
.argument(registry.arg::<Option<bool>>("surname")),
registry.field::<Option<String>>("nickname"),
registry.field::<Option<bool>>("meows"),
registry.field::<Option<i64>>("meowVolume"),
registry.field::<Option<FurColor>>("furColor"),
])
.interfaces(&[
registry.get_type::<Being>(),
registry.get_type::<Pet>(),
])
.into_meta()
}
}
impl<CtxT> GraphQLType<CtxT> for CatOrDog {
fn name() -> Option<&'static str> {
Some("CatOrDog")
}
fn meta(registry: &mut Registry<CtxT>) -> MetaType {
registry.build_union_type::<Self>()(&[
registry.get_type::<Cat>(),
registry.get_type::<Dog>(),
])
.into_meta()
}
}
impl<CtxT> GraphQLType<CtxT> for Intelligent {
fn name() -> Option<&'static str> {
Some("Intelligent")
}
fn meta(registry: &mut Registry<CtxT>) -> MetaType {
registry.build_interface_type::<Self>()(&[
registry.field::<Option<i64>>("iq"),
])
.into_meta()
}
}
impl<CtxT> GraphQLType<CtxT> for Human {
fn name() -> Option<&'static str> {
Some("Human")
}
fn meta(registry: &mut Registry<CtxT>) -> MetaType {
registry.build_object_type::<Self>()(&[
registry.field::<Option<String>>("name")
.argument(registry.arg::<Option<bool>>("surname")),
registry.field::<Option<Vec<Option<Pet>>>>("pets"),
registry.field::<Option<Vec<Human>>>("relatives"),
registry.field::<Option<i64>>("iq"),
])
.interfaces(&[
registry.get_type::<Being>(),
registry.get_type::<Intelligent>(),
])
.into_meta()
}
}
impl<CtxT> GraphQLType<CtxT> for Alien {
fn name() -> Option<&'static str> {
Some("Alien")
}
fn meta(registry: &mut Registry<CtxT>) -> MetaType {
registry.build_object_type::<Self>()(&[
registry.field::<Option<String>>("name")
.argument(registry.arg::<Option<bool>>("surname")),
registry.field::<Option<i64>>("iq"),
registry.field::<Option<i64>>("numEyes"),
])
.interfaces(&[
registry.get_type::<Being>(),
registry.get_type::<Intelligent>(),
])
.into_meta()
}
}
impl<CtxT> GraphQLType<CtxT> for DogOrHuman {
fn name() -> Option<&'static str> {
Some("DogOrHuman")
}
fn meta(registry: &mut Registry<CtxT>) -> MetaType {
registry.build_union_type::<Self>()(&[
registry.get_type::<Dog>(),
registry.get_type::<Human>(),
])
.into_meta()
}
}
impl<CtxT> GraphQLType<CtxT> for HumanOrAlien {
fn name() -> Option<&'static str> {
Some("HumanOrAlien")
}
fn meta(registry: &mut Registry<CtxT>) -> MetaType {
registry.build_union_type::<Self>()(&[
registry.get_type::<Human>(),
registry.get_type::<Alien>(),
])
.into_meta()
}
}
impl<CtxT> GraphQLType<CtxT> for ComplexInput {
fn name() -> Option<&'static str> {
Some("ComplexInput")
}
fn meta(registry: &mut Registry<CtxT>) -> MetaType {
registry.build_input_object_type::<Self>()(&[
registry.arg::<bool>("requiredField"),
registry.arg::<Option<i64>>("intField"),
registry.arg::<Option<String>>("stringField"),
registry.arg::<Option<bool>>("booleanField"),
registry.arg::<Option<Vec<Option<String>>>>("stringListField"),
])
.into_meta()
}
}
impl FromInputValue for ComplexInput {
fn from(v: &InputValue) -> Option<ComplexInput> {
let obj = match v.to_object_value() {
Some(o) => o,
None => return None,
};
Some(ComplexInput {
required_field: match obj.get("requiredField").and_then(|v| v.convert()) {
Some(f) => f,
None => return None,
},
int_field: obj.get("intField").and_then(|v| v.convert()),
string_field: obj.get("stringField").and_then(|v| v.convert()),
boolean_field: obj.get("booleanField").and_then(|v| v.convert()),
string_list_field: obj.get("stringListField").and_then(|v| v.convert()),
})
}
}
impl<CtxT> GraphQLType<CtxT> for ComplicatedArgs {
fn name() -> Option<&'static str> {
Some("ComplicatedArgs")
}
fn meta(registry: &mut Registry<CtxT>) -> MetaType {
registry.build_object_type::<Self>()(&[
registry.field::<Option<String>>("intArgField")
.argument(registry.arg::<Option<i64>>("intArg")),
registry.field::<Option<String>>("nonNullIntArgField")
.argument(registry.arg::<i64>("nonNullIntArg")),
registry.field::<Option<String>>("stringArgField")
.argument(registry.arg::<Option<String>>("stringArg")),
registry.field::<Option<String>>("booleanArgField")
.argument(registry.arg::<Option<bool>>("booleanArg")),
registry.field::<Option<String>>("enumArgField")
.argument(registry.arg::<Option<FurColor>>("enumArg")),
registry.field::<Option<String>>("floatArgField")
.argument(registry.arg::<Option<f64>>("floatArg")),
registry.field::<Option<String>>("idArgField")
.argument(registry.arg::<Option<ID>>("idArg")),
registry.field::<Option<String>>("stringListArgField")
.argument(registry.arg::<Option<Vec<Option<String>>>>("stringListArg")),
registry.field::<Option<String>>("complexArgField")
.argument(registry.arg::<Option<ComplexInput>>("complexArg")),
registry.field::<Option<String>>("multipleReqs")
.argument(registry.arg::<i64>("req1"))
.argument(registry.arg::<i64>("req2")),
registry.field::<Option<String>>("multipleOpts")
.argument(registry.arg_with_default("opt1", &0i64))
.argument(registry.arg_with_default("opt2", &0i64)),
registry.field::<Option<String>>("multipleOptAndReq")
.argument(registry.arg::<i64>("req1"))
.argument(registry.arg::<i64>("req2"))
.argument(registry.arg_with_default("opt1", &0i64))
.argument(registry.arg_with_default("opt2", &0i64)),
])
.into_meta()
}
}
impl<CtxT> GraphQLType<CtxT> for QueryRoot {
fn name() -> Option<&'static str> {
Some("QueryRoot")
}
fn meta(registry: &mut Registry<CtxT>) -> MetaType {
registry.build_object_type::<Self>()(&[
registry.field::<Option<Human>>("human")
.argument(registry.arg::<Option<ID>>("id")),
registry.field::<Option<Alien>>("alien"),
registry.field::<Option<Dog>>("dog"),
registry.field::<Option<Cat>>("cat"),
registry.field::<Option<Pet>>("pet"),
registry.field::<Option<CatOrDog>>("catOrDog"),
registry.field::<Option<DogOrHuman>>("dorOrHuman"),
registry.field::<Option<HumanOrAlien>>("humanOrAlien"),
registry.field::<Option<ComplicatedArgs>>("complicatedArgs"),
])
.into_meta()
}
}
pub fn validate<'a, R, V, F>(r: R, q: &str, factory: F)
-> Vec<RuleError>
where R: GraphQLType<()>,
V: Visitor<'a> + 'a,
F: Fn() -> V
{
let mut root = RootNode::<(), R, ()>::new(r, ());
root.schema.add_directive(DirectiveType::new("onQuery", &[DirectiveLocation::Query], &[]));
root.schema.add_directive(DirectiveType::new("onMutation", &[DirectiveLocation::Mutation], &[]));
root.schema.add_directive(DirectiveType::new("onField", &[DirectiveLocation::Field], &[]));
root.schema.add_directive(DirectiveType::new("onFragmentDefinition", &[DirectiveLocation::FragmentDefinition], &[]));
root.schema.add_directive(DirectiveType::new("onFragmentSpread", &[DirectiveLocation::FragmentSpread], &[]));
root.schema.add_directive(DirectiveType::new("onInlineFragment", &[DirectiveLocation::InlineFragment], &[]));
let doc = parse_document_source(q)
.expect(&format!("Parse error on input {:#?}", q));
let mut ctx = ValidatorContext::new(
unsafe { ::std::mem::transmute(&root.schema) },
&doc);
let mut mv = MultiVisitor::new(vec![ Box::new(factory()) ]);
visit(&mut mv, &mut ctx, unsafe { ::std::mem::transmute(&doc) });
ctx.into_errors()
}
pub fn expect_passes_rule<'a, V, F>(factory: F, q: &str)
where V: Visitor<'a> + 'a,
F: Fn() -> V
{
expect_passes_rule_with_schema(QueryRoot, factory, q);
}
pub fn expect_passes_rule_with_schema<'a, R, V, F>(r: R, factory: F, q: &str)
where R: GraphQLType<()>,
V: Visitor<'a> + 'a,
F: Fn() -> V
{
let errs = validate(r, q, factory);
if !errs.is_empty() {
print_errors(&errs);
panic!("Expected rule to pass, but errors found");
}
}
pub fn expect_fails_rule<'a, V, F>(factory: F, q: &str, expected_errors: &[RuleError])
where V: Visitor<'a> + 'a,
F: Fn() -> V
{
expect_fails_rule_with_schema(QueryRoot, factory, q, expected_errors);
}
pub fn expect_fails_rule_with_schema<'a, R, V, F>(r: R, factory: F, q: &str, expected_errors: &[RuleError])
where R: GraphQLType<()>,
V: Visitor<'a> + 'a,
F: Fn() -> V
{
let errs = validate(r, q, factory);
if errs.is_empty() {
panic!("Expected rule to fail, but no errors were found");
}
else if errs != expected_errors {
println!("==> Expected errors:");
print_errors(expected_errors);
println!("\n==> Actual errors:");
print_errors(&errs);
panic!("Unexpected set of errors found");
}
}
fn print_errors(errs: &[RuleError]) {
for err in errs {
for p in err.locations() {
print!("[{:>3},{:>3},{:>3}] ", p.index(), p.line(), p.column());
}
println!("{}", err.message());
}
}

65
src/validation/traits.rs Normal file
View file

@ -0,0 +1,65 @@
use ast::{Document, Operation, Fragment, VariableDefinition, Selection,
Directive, InputValue, Field, FragmentSpread, InlineFragment};
use parser::Spanning;
use validation::ValidatorContext;
#[doc(hidden)]
pub trait Visitor<'a> {
fn enter_document(&mut self, _: &mut ValidatorContext<'a>, _: &'a Document) {}
fn exit_document(&mut self, _: &mut ValidatorContext<'a>, _: &'a Document) {}
fn enter_operation_definition(&mut self, _: &mut ValidatorContext<'a>, _: &'a Spanning<Operation>) {}
fn exit_operation_definition(&mut self, _: &mut ValidatorContext<'a>, _: &'a Spanning<Operation>) {}
fn enter_fragment_definition(&mut self, _: &mut ValidatorContext<'a>, _: &'a Spanning<Fragment>) {}
fn exit_fragment_definition(&mut self, _: &mut ValidatorContext<'a>, _: &'a Spanning<Fragment>) {}
fn enter_variable_definition(&mut self, _: &mut ValidatorContext<'a>, _: &'a (Spanning<String>, VariableDefinition)) {}
fn exit_variable_definition(&mut self, _: &mut ValidatorContext<'a>, _: &'a (Spanning<String>, VariableDefinition)) {}
fn enter_directive(&mut self, _: &mut ValidatorContext<'a>, _: &'a Spanning<Directive>) {}
fn exit_directive(&mut self, _: &mut ValidatorContext<'a>, _: &'a Spanning<Directive>) {}
fn enter_argument(&mut self, _: &mut ValidatorContext<'a>, _: &'a (Spanning<String>, Spanning<InputValue>)) {}
fn exit_argument(&mut self, _: &mut ValidatorContext<'a>, _: &'a (Spanning<String>, Spanning<InputValue>)) {}
fn enter_selection_set(&mut self, _: &mut ValidatorContext<'a>, _: &'a Vec<Selection>) {}
fn exit_selection_set(&mut self, _: &mut ValidatorContext<'a>, _: &'a Vec<Selection>) {}
fn enter_field(&mut self, _: &mut ValidatorContext<'a>, _: &'a Spanning<Field>) {}
fn exit_field(&mut self, _: &mut ValidatorContext<'a>, _: &'a Spanning<Field>) {}
fn enter_fragment_spread(&mut self, _: &mut ValidatorContext<'a>, _: &'a Spanning<FragmentSpread>) {}
fn exit_fragment_spread(&mut self, _: &mut ValidatorContext<'a>, _: &'a Spanning<FragmentSpread>) {}
fn enter_inline_fragment(&mut self, _: &mut ValidatorContext<'a>, _: &'a Spanning<InlineFragment>) {}
fn exit_inline_fragment(&mut self, _: &mut ValidatorContext<'a>, _: &'a Spanning<InlineFragment>) {}
fn enter_int_value(&mut self, _: &mut ValidatorContext<'a>, _: Spanning<i64>) {}
fn exit_int_value(&mut self, _: &mut ValidatorContext<'a>, _: Spanning<i64>) {}
fn enter_float_value(&mut self, _: &mut ValidatorContext<'a>, _: Spanning<f64>) {}
fn exit_float_value(&mut self, _: &mut ValidatorContext<'a>, _: Spanning<f64>) {}
fn enter_string_value(&mut self, _: &mut ValidatorContext<'a>, _: Spanning<&'a String>) {}
fn exit_string_value(&mut self, _: &mut ValidatorContext<'a>, _: Spanning<&'a String>) {}
fn enter_boolean_value(&mut self, _: &mut ValidatorContext<'a>, _: Spanning<bool>) {}
fn exit_boolean_value(&mut self, _: &mut ValidatorContext<'a>, _: Spanning<bool>) {}
fn enter_enum_value(&mut self, _: &mut ValidatorContext<'a>, _: Spanning<&'a String>) {}
fn exit_enum_value(&mut self, _: &mut ValidatorContext<'a>, _: Spanning<&'a String>) {}
fn enter_variable_value(&mut self, _: &mut ValidatorContext<'a>, _: Spanning<&'a String>) {}
fn exit_variable_value(&mut self, _: &mut ValidatorContext<'a>, _: Spanning<&'a String>) {}
fn enter_list_value(&mut self, _: &mut ValidatorContext<'a>, _: Spanning<&'a Vec<Spanning<InputValue>>>) {}
fn exit_list_value(&mut self, _: &mut ValidatorContext<'a>, _: Spanning<&'a Vec<Spanning<InputValue>>>) {}
fn enter_object_value(&mut self, _: &mut ValidatorContext<'a>, _: Spanning<&'a Vec<(Spanning<String>, Spanning<InputValue>)>>) {}
fn exit_object_value(&mut self, _: &mut ValidatorContext<'a>, _: Spanning<&'a Vec<(Spanning<String>, Spanning<InputValue>)>>) {}
fn enter_object_field(&mut self, _: &mut ValidatorContext<'a>, _: &'a (Spanning<String>, Spanning<InputValue>)) {}
fn exit_object_field(&mut self, _: &mut ValidatorContext<'a>, _: &'a (Spanning<String>, Spanning<InputValue>)) {}
}

260
src/validation/visitor.rs Normal file
View file

@ -0,0 +1,260 @@
use ast::{Definition, Document, Fragment, VariableDefinitions, Type, InputValue,
Directive, Arguments, Selection, Field, FragmentSpread, InlineFragment,
Operation, OperationType};
use schema::meta::Argument;
use parser::Spanning;
use validation::{Visitor, ValidatorContext};
#[doc(hidden)]
pub fn visit<'a, V: Visitor<'a>>(v: &mut V, ctx: &mut ValidatorContext<'a>, d: &'a Document) {
v.enter_document(ctx, d);
visit_definitions(v, ctx, d);
v.exit_document(ctx, d);
}
fn visit_definitions<'a, V: Visitor<'a>>(v: &mut V, ctx: &mut ValidatorContext<'a>, d: &'a Vec<Definition>) {
for def in d {
let def_type = match *def {
Definition::Fragment(Spanning {
item: Fragment { type_condition: Spanning { item: ref name, .. }, .. }, .. }) =>
Some(Type::NonNullNamed(name.to_owned())),
Definition::Operation(Spanning {
item: Operation { operation_type: OperationType::Query, .. }, .. }) =>
Some(Type::NonNullNamed(ctx.schema.concrete_query_type().name().unwrap().to_owned())),
Definition::Operation(Spanning {
item: Operation { operation_type: OperationType::Mutation, .. }, .. }) =>
ctx.schema.concrete_mutation_type()
.map(|t| Type::NonNullNamed(t.name().unwrap().to_owned())),
};
ctx.with_pushed_type(def_type.as_ref(), |ctx| {
enter_definition(v, ctx, def);
visit_definition(v, ctx, def);
exit_definition(v, ctx, def);
});
}
}
fn enter_definition<'a, V: Visitor<'a>>(v: &mut V, ctx: &mut ValidatorContext<'a>, def: &'a Definition) {
match *def {
Definition::Operation(ref op) => v.enter_operation_definition(ctx, op),
Definition::Fragment(ref f) => v.enter_fragment_definition(ctx, f),
}
}
fn exit_definition<'a, V: Visitor<'a>>(v: &mut V, ctx: &mut ValidatorContext<'a>, def: &'a Definition) {
match *def {
Definition::Operation(ref op) => v.exit_operation_definition(ctx, op),
Definition::Fragment(ref f) => v.exit_fragment_definition(ctx, f),
}
}
fn visit_definition<'a, V: Visitor<'a>>(v: &mut V, ctx: &mut ValidatorContext<'a>, def: &'a Definition) {
match *def {
Definition::Operation(ref op) => {
visit_variable_definitions(v, ctx, &op.item.variable_definitions);
visit_directives(v, ctx, &op.item.directives);
visit_selection_set(v, ctx, &op.item.selection_set);
},
Definition::Fragment(ref f) => {
visit_directives(v, ctx, &f.item.directives);
visit_selection_set(v, ctx, &f.item.selection_set);
},
}
}
fn visit_variable_definitions<'a, V: Visitor<'a>>(v: &mut V, ctx: &mut ValidatorContext<'a>, defs: &'a Option<Spanning<VariableDefinitions>>) {
if let Some(ref defs) = *defs {
for def in defs.item.iter() {
let var_type = def.1.var_type.item.clone();
ctx.with_pushed_input_type(Some(&var_type), |ctx| {
v.enter_variable_definition(ctx, def);
if let Some(ref default_value) = def.1.default_value {
visit_input_value(v, ctx, default_value);
}
v.exit_variable_definition(ctx, def);
})
}
}
}
fn visit_directives<'a, V: Visitor<'a>>(v: &mut V, ctx: &mut ValidatorContext<'a>, directives: &'a Option<Vec<Spanning<Directive>>>) {
if let Some(ref directives) = *directives {
for directive in directives {
let directive_arguments = ctx.schema.directive_by_name(&directive.item.name.item).map(|d| &d.arguments);
v.enter_directive(ctx, directive);
visit_arguments(v, ctx, &directive_arguments, &directive.item.arguments);
v.exit_directive(ctx, directive);
}
}
}
fn visit_arguments<'a, V: Visitor<'a>>(v: &mut V, ctx: &mut ValidatorContext<'a>, meta_args: &Option<&Vec<Argument>>, arguments: &'a Option<Spanning<Arguments>>) {
if let Some(ref arguments) = *arguments {
for argument in arguments.item.iter() {
let arg_type = meta_args
.and_then(|args| args.iter().filter(|a| a.name == argument.0.item).next())
.map(|a| &a.arg_type);
ctx.with_pushed_input_type(arg_type, |ctx| {
v.enter_argument(ctx, argument);
visit_input_value(v, ctx, &argument.1);
v.exit_argument(ctx, argument);
})
}
}
}
fn visit_selection_set<'a, V: Visitor<'a>>(v: &mut V, ctx: &mut ValidatorContext<'a>, selection_set: &'a Vec<Selection>) {
ctx.with_pushed_parent_type(|ctx| {
v.enter_selection_set(ctx, selection_set);
for selection in selection_set.iter() {
visit_selection(v, ctx, selection);
}
v.exit_selection_set(ctx, selection_set);
});
}
fn visit_selection<'a, V: Visitor<'a>>(v: &mut V, ctx: &mut ValidatorContext<'a>, selection: &'a Selection) {
match *selection {
Selection::Field(ref field) => visit_field(v, ctx, field),
Selection::FragmentSpread(ref spread) => visit_fragment_spread(v, ctx, spread),
Selection::InlineFragment(ref fragment) => visit_inline_fragment(v, ctx, fragment),
}
}
fn visit_field<'a, V: Visitor<'a>>(v: &mut V, ctx: &mut ValidatorContext<'a>, field: &'a Spanning<Field>) {
let meta_field = ctx.parent_type()
.and_then(|t| t.field_by_name(&field.item.name.item));
let field_type = meta_field.map(|f| &f.field_type);
let field_args = meta_field.and_then(|f| f.arguments.as_ref());
ctx.with_pushed_type(field_type, |ctx| {
v.enter_field(ctx, field);
visit_arguments(v, ctx, &field_args, &field.item.arguments);
visit_directives(v, ctx, &field.item.directives);
if let Some(ref selection_set) = field.item.selection_set {
visit_selection_set(v, ctx, selection_set);
}
v.exit_field(ctx, field);
});
}
fn visit_fragment_spread<'a, V: Visitor<'a>>(v: &mut V, ctx: &mut ValidatorContext<'a>, spread: &'a Spanning<FragmentSpread>) {
v.enter_fragment_spread(ctx, spread);
visit_directives(v, ctx, &spread.item.directives);
v.exit_fragment_spread(ctx, spread);
}
fn visit_inline_fragment<'a, V: Visitor<'a>>(v: &mut V, ctx: &mut ValidatorContext<'a>, fragment: &'a Spanning<InlineFragment>) {
let type_name = fragment.item.type_condition.clone().map(|s| s.item);
let mut visit_fn = move |ctx: &mut ValidatorContext<'a>| {
v.enter_inline_fragment(ctx, fragment);
visit_directives(v, ctx, &fragment.item.directives);
visit_selection_set(v, ctx, &fragment.item.selection_set);
v.exit_inline_fragment(ctx, fragment);
};
if let Some(type_name) = type_name {
ctx.with_pushed_type(Some(&Type::NonNullNamed(type_name)), visit_fn);
}
else {
visit_fn(ctx);
}
}
fn visit_input_value<'a, V: Visitor<'a>>(v: &mut V, ctx: &mut ValidatorContext<'a>, input_value: &'a Spanning<InputValue>) {
enter_input_value(v, ctx, input_value);
match input_value.item {
InputValue::Object(ref fields) => {
for field in fields {
let inner_type = ctx.current_input_type_literal()
.and_then(|t| match *t {
Type::NonNullNamed(ref name) | Type::Named(ref name) =>
ctx.schema.concrete_type_by_name(name),
_ => None,
})
.and_then(|ct| ct.input_field_by_name(&field.0.item))
.map(|f| &f.arg_type);
ctx.with_pushed_input_type(inner_type, |ctx| {
v.enter_object_field(ctx, field);
visit_input_value(v, ctx, &field.1);
v.exit_object_field(ctx, field);
})
}
}
InputValue::List(ref ls) => {
let inner_type = ctx.current_input_type_literal().and_then(|t| match *t {
Type::List(ref inner) | Type::NonNullList(ref inner) =>
Some(inner.as_ref().clone()),
_ => None,
});
ctx.with_pushed_input_type(inner_type.as_ref(), |ctx| {
for value in ls {
visit_input_value(v, ctx, value);
}
})
}
_ => (),
}
exit_input_value(v, ctx, input_value);
}
fn enter_input_value<'a, V: Visitor<'a>>(v: &mut V, ctx: &mut ValidatorContext<'a>, input_value: &'a Spanning<InputValue>) {
use InputValue::*;
let start = &input_value.start;
let end = &input_value.end;
match input_value.item {
Null => panic!("null values can't appear in the AST"),
Int(ref i) => v.enter_int_value(ctx, Spanning::start_end(start, end, *i)),
Float(ref f) => v.enter_float_value(ctx, Spanning::start_end(start, end, *f)),
String(ref s) => v.enter_string_value(ctx, Spanning::start_end(start, end, s)),
Boolean(ref b) => v.enter_boolean_value(ctx, Spanning::start_end(start, end, *b)),
Enum(ref s) => v.enter_enum_value(ctx, Spanning::start_end(start, end, s)),
Variable(ref s) => v.enter_variable_value(ctx, Spanning::start_end(start, end, s)),
List(ref l) => v.enter_list_value(ctx, Spanning::start_end(start, end, l)),
Object(ref o) => v.enter_object_value(ctx, Spanning::start_end(start, end, o)),
}
}
fn exit_input_value<'a, V: Visitor<'a>>(v: &mut V, ctx: &mut ValidatorContext<'a>, input_value: &'a Spanning<InputValue>) {
use InputValue::*;
let start = &input_value.start;
let end = &input_value.end;
match input_value.item {
Null => panic!("null values can't appear in the AST"),
Int(ref i) => v.exit_int_value(ctx, Spanning::start_end(start, end, *i)),
Float(ref f) => v.exit_float_value(ctx, Spanning::start_end(start, end, *f)),
String(ref s) => v.exit_string_value(ctx, Spanning::start_end(start, end, s)),
Boolean(ref b) => v.exit_boolean_value(ctx, Spanning::start_end(start, end, *b)),
Enum(ref s) => v.exit_enum_value(ctx, Spanning::start_end(start, end, s)),
Variable(ref s) => v.exit_variable_value(ctx, Spanning::start_end(start, end, s)),
List(ref l) => v.exit_list_value(ctx, Spanning::start_end(start, end, l)),
Object(ref o) => v.exit_object_value(ctx, Spanning::start_end(start, end, o)),
}
}

123
src/value.rs Normal file
View file

@ -0,0 +1,123 @@
use std::collections::HashMap;
use std::hash::Hash;
use rustc_serialize::json::{ToJson, Json};
use parser::Spanning;
use ast::{InputValue, ToInputValue};
/// Serializable value returned from query and field execution.
///
/// Used by the execution engine and resolvers to build up the response
/// structure. Similar to the `Json` type found in the serialize crate.
///
/// It is also similar to the `InputValue` type, but can not contain enum
/// values or variables. Also, lists and objects do not contain any location
/// information since they are generated by resolving fields and values rather
/// than parsing a source query.
#[derive(Debug, PartialEq)]
#[allow(missing_docs)]
pub enum Value {
Null,
Int(i64),
Float(f64),
String(String),
Boolean(bool),
List(Vec<Value>),
Object(HashMap<String, Value>),
}
impl Value {
// CONSTRUCTORS
/// Construct a null value.
pub fn null() -> Value { Value::Null }
/// Construct an integer value.
pub fn int(i: i64) -> Value { Value::Int(i) }
/// Construct a floating point value.
pub fn float(f: f64) -> Value { Value::Float(f) }
/// Construct a string value.
pub fn string<T: AsRef<str>>(s: T) -> Value { Value::String(s.as_ref().to_owned()) }
/// Construct a boolean value.
pub fn boolean(b: bool) -> Value { Value::Boolean(b) }
/// Construct a list value.
pub fn list(l: Vec<Value>) -> Value { Value::List(l) }
/// Construct an object value.
pub fn object<K>(o: HashMap<K, Value>) -> Value
where K: AsRef<str> + Eq + Hash
{
Value::Object(
o.into_iter().map(|(k, v)| (k.as_ref().to_owned(), v)).collect()
)
}
// DISCRIMINATORS
/// Does this value represent null?
pub fn is_null(&self) -> bool {
match *self {
Value::Null => true,
_ => false,
}
}
/// View the underlying object value, if present.
pub fn as_object_value(&self) -> Option<&HashMap<String, Value>> {
match *self {
Value::Object(ref o) => Some(o),
_ => None,
}
}
/// View the underlying list value, if present.
pub fn as_list_value(&self) -> Option<&Vec<Value>> {
match *self {
Value::List(ref l) => Some(l),
_ => None,
}
}
/// View the underlying string value, if present.
pub fn as_string_value(&self) -> Option<&str> {
match *self {
Value::String(ref s) => Some(s),
_ => None,
}
}
}
impl ToJson for Value {
fn to_json(&self) -> Json {
match *self {
Value::Null => Json::Null,
Value::Int(i) => Json::I64(i),
Value::Float(f) => Json::F64(f),
Value::String(ref s) => Json::String(s.clone()),
Value::Boolean(b) => Json::Boolean(b),
Value::List(ref l) => Json::Array(l.iter().map(|x| x.to_json()).collect()),
Value::Object(ref o) => Json::Object(o.iter().map(|(k,v)| (k.clone(), v.to_json())).collect()),
}
}
}
impl ToInputValue for Value {
fn to(&self) -> InputValue {
match *self {
Value::Null => InputValue::Null,
Value::Int(i) => InputValue::Int(i),
Value::Float(f) => InputValue::Float(f),
Value::String(ref s) => InputValue::String(s.clone()),
Value::Boolean(b) => InputValue::Boolean(b),
Value::List(ref l) => InputValue::List(l.iter().map(|x|
Spanning::unlocated(x.to())).collect()),
Value::Object(ref o) => InputValue::Object(o.iter().map(|(k,v)|
(Spanning::unlocated(k.clone()), Spanning::unlocated(v.to()))).collect()),
}
}
}