Initial import
This commit is contained in:
commit
94d689fa82
76 changed files with 16729 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
target
|
||||
Cargo.lock
|
15
.travis.yml
Normal file
15
.travis.yml
Normal 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
20
Cargo.toml
Normal 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
25
LICENSE
Normal 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
143
README.md
Normal 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
56
examples/server.rs
Normal 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
445
src/ast.rs
Normal 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()
|
||||
}
|
||||
}
|
323
src/integrations/iron_handlers.rs
Normal file
323
src/integrations/iron_handlers.rs
Normal 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
1
src/integrations/mod.rs
Normal file
|
@ -0,0 +1 @@
|
|||
#[cfg(feature="iron-handlers")] pub mod iron_handlers;
|
340
src/lib.rs
Normal file
340
src/lib.rs
Normal 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
156
src/macros/args.rs
Normal 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
99
src/macros/enums.rs
Normal 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
55
src/macros/field.rs
Normal 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
317
src/macros/interface.rs
Normal 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
6
src/macros/mod.rs
Normal 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
379
src/macros/object.rs
Normal 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
109
src/macros/scalar.rs
Normal 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
339
src/parser/document.rs
Normal 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
522
src/parser/lexer.rs
Normal 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
16
src/parser/mod.rs
Normal 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
179
src/parser/parser.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
123
src/parser/tests/document.rs
Normal file
123
src/parser/tests/document.rs
Normal 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
536
src/parser/tests/lexer.rs
Normal 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
3
src/parser/tests/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
mod document;
|
||||
mod lexer;
|
||||
mod value;
|
134
src/parser/tests/value.rs
Normal file
134
src/parser/tests/value.rs
Normal 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
177
src/parser/utils.rs
Normal 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
71
src/parser/value.rs
Normal 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
631
src/schema/meta.rs
Normal 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
3
src/schema/mod.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod model;
|
||||
pub mod schema;
|
||||
pub mod meta;
|
340
src/schema/model.rs
Normal file
340
src/schema/model.rs
Normal 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
251
src/schema/schema.rs
Normal 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
125
src/tests/bench.rs
Normal 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));
|
||||
}
|
166
src/tests/introspection_tests.rs
Normal file
166
src/tests/introspection_tests.rs
Normal 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
7
src/tests/mod.rs
Normal 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
223
src/tests/model.rs
Normal 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
364
src/tests/query_tests.rs
Normal 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
105
src/tests/schema.rs
Normal 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
404
src/types/base.rs
Normal 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
108
src/types/containers.rs
Normal 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
8
src/types/mod.rs
Normal 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
73
src/types/pointers.rs
Normal 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
121
src/types/scalars.rs
Normal 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
421
src/types/schema.rs
Normal 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
67
src/types/utilities.rs
Normal 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
174
src/validation/context.rs
Normal 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
21
src/validation/mod.rs
Normal 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};
|
160
src/validation/multi_visitor.rs
Normal file
160
src/validation/multi_visitor.rs
Normal 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));
|
||||
}
|
||||
}
|
913
src/validation/rules/arguments_of_correct_type.rs
Normal file
913
src/validation/rules/arguments_of_correct_type.rs
Normal 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),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
154
src/validation/rules/default_values_of_correct_type.rs
Normal file
154
src/validation/rules/default_values_of_correct_type.rs
Normal 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),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
272
src/validation/rules/fields_on_correct_type.rs
Normal file
272
src/validation/rules/fields_on_correct_type.rs
Normal 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
|
||||
}
|
||||
}
|
||||
"#);
|
||||
}
|
||||
|
||||
}
|
173
src/validation/rules/fragments_on_composite_types.rs
Normal file
173
src/validation/rules/fragments_on_composite_types.rs
Normal 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),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
228
src/validation/rules/known_argument_names.rs
Normal file
228
src/validation/rules/known_argument_names.rs
Normal 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),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
216
src/validation/rules/known_directives.rs
Normal file
216
src/validation/rules/known_directives.rs
Normal 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 == ¤t_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),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
88
src/validation/rules/known_fragment_names.rs
Normal file
88
src/validation/rules/known_fragment_names.rs
Normal 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),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
87
src/validation/rules/known_type_names.rs
Normal file
87
src/validation/rules/known_type_names.rs
Normal 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),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
125
src/validation/rules/lone_anonymous_operation.rs
Normal file
125
src/validation/rules/lone_anonymous_operation.rs
Normal 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),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
59
src/validation/rules/mod.rs
Normal file
59
src/validation/rules/mod.rs
Normal 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);
|
||||
}
|
333
src/validation/rules/no_fragment_cycles.rs
Normal file
333
src/validation/rules/no_fragment_cycles.rs
Normal 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(¤t_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)
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
488
src/validation/rules/no_undefined_variables.rs
Normal file
488
src/validation/rules/no_undefined_variables.rs
Normal 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),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
249
src/validation/rules/no_unused_fragments.rs
Normal file
249
src/validation/rules/no_unused_fragments.rs
Normal 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),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
351
src/validation/rules/no_unused_variables.rs
Normal file
351
src/validation/rules/no_unused_variables.rs
Normal 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),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
1743
src/validation/rules/overlapping_fields_can_be_merged.rs
Normal file
1743
src/validation/rules/overlapping_fields_can_be_merged.rs
Normal file
File diff suppressed because it is too large
Load diff
314
src/validation/rules/possible_fragment_spreads.rs
Normal file
314
src/validation/rules/possible_fragment_spreads.rs
Normal 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),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
280
src/validation/rules/provided_non_null_arguments.rs
Normal file
280
src/validation/rules/provided_non_null_arguments.rs
Normal 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),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
168
src/validation/rules/scalar_leafs.rs
Normal file
168
src/validation/rules/scalar_leafs.rs
Normal 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),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
201
src/validation/rules/unique_argument_names.rs
Normal file
201
src/validation/rules/unique_argument_names.rs
Normal 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),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
149
src/validation/rules/unique_fragment_names.rs
Normal file
149
src/validation/rules/unique_fragment_names.rs
Normal 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)
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
131
src/validation/rules/unique_input_field_names.rs
Normal file
131
src/validation/rules/unique_input_field_names.rs
Normal 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),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
145
src/validation/rules/unique_operation_names.rs
Normal file
145
src/validation/rules/unique_operation_names.rs
Normal 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),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
81
src/validation/rules/unique_variable_names.rs
Normal file
81
src/validation/rules/unique_variable_names.rs
Normal 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),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
62
src/validation/rules/variables_are_input_types.rs
Normal file
62
src/validation/rules/variables_are_input_types.rs
Normal 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),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
429
src/validation/rules/variables_in_allowed_position.rs
Normal file
429
src/validation/rules/variables_in_allowed_position.rs
Normal 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),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
479
src/validation/test_harness.rs
Normal file
479
src/validation/test_harness.rs
Normal 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
65
src/validation/traits.rs
Normal 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
260
src/validation/visitor.rs
Normal 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
123
src/value.rs
Normal 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()),
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue