Merge pull request #114 from graphql-rust/v09

0.9 release
This commit is contained in:
theduke 2017-12-03 15:32:13 +01:00 committed by GitHub
commit b46951717d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 1668 additions and 1027 deletions

View file

@ -8,10 +8,8 @@ rust:
- nightly
# Prevent accidentally breaking older Rust versions
- 1.17.0
- 1.18.0
- 1.19.0
- 1.20.0
- 1.21.0
env:
global:

View file

@ -3,4 +3,6 @@ members = [
"juniper",
"juniper_codegen",
"juniper_tests",
"juniper_iron",
"juniper_rocket",
]

4
Makefile.toml Normal file
View file

@ -0,0 +1,4 @@
[tasks.init]
condition = { channels = ["beta", "stable"] }
env = { "CARGO_MAKE_WORKSPACE_SKIP_MEMBERS" = "juniper_rocket;juniper_tests" }

180
README.md
View file

@ -4,7 +4,7 @@
> GraphQL server library for Rust
[![Build Status](https://travis-ci.org/graphql-rust/juniper.svg?branch=master)](https://travis-ci.org/graphql-rust/juniper)
[![Build status](https://ci.appveyor.com/api/projects/status/vsrwmsh9wobxugbs?svg=true)](https://ci.appveyor.com/project/theduke/juniper/branch/master)
[![Build status](https://ci.appveyor.com/api/projects/status/xav6tor6biu617uu?svg=true)](https://ci.appveyor.com/project/theduke/juniper)
[![codecov](https://codecov.io/gh/graphql-rust/juniper/branch/master/graph/badge.svg)](https://codecov.io/gh/graphql-rust/juniper)
[![Crates.io](https://img.shields.io/crates/v/juniper.svg?maxAge=2592000)](https://crates.io/crates/juniper)
[![Gitter chat](https://badges.gitter.im/juniper-graphql/gitter.png)](https://gitter.im/juniper-graphql)
@ -13,159 +13,87 @@
---
[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.
serve mobile and web application frontends.
*Juniper* makes it possible to write GraphQL servers in Rust that are
type-safe and blazingly fast. We also try to make declaring and resolving
GraphQL schemas as convenient as possible as Rust will allow.
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][iron] and [Rocket] frameworks.
pre-built integration for the [Iron][iron] and [Rocket] frameworks, including
embedded [Graphiql][graphiql] for easy debugging.
* [Cargo crate](https://crates.io/crates/juniper)
* [API Documentation](https://docs.rs/juniper)
* [API Reference][docsrs]
* [Book][book]: Guides and Examples
## Installation
Add Juniper to your Cargo.toml:
## Getting Started
```toml
[dependencies]
juniper = { git = "https://github.com/graphql-rust/juniper" }
```
The best place to get started is the [Juniper Book][book], which contains
guides with plenty of examples, covering all features of Juniper. (very much WIP)
If you want Iron integration, you need to depend on the `juniper_iron` crate.
feature flag:
To get started quickly and get a feel for Juniper, check out the
[Quickstart][book_quickstart] section.
```toml
[dependencies]
juniper = { git = "https://github.com/graphql-rust/juniper" }
juniper_iron = { git = "https://github.com/graphql-rust/juniper_iron" }
For specific information about macros, types and the Juniper api, the
[API Reference][docsrs] is the best place to look.
```
You can also check out [src/tests/schema.rs][test_schema_rs] to see a complex
schema including polymorphism with traits and interfaces.
For an example of web framework integration,
see the [rocket][rocket_examples] and [iron][iron_examples] examples folders.
If you want Rocket integration, you need to depend on the `juniper_rocket` crate.
**Note**: Until 0.9 is released, you will need to use a Git dependency to the current master branch.
```toml
[dependencies]
juniper = { git = "https://github.com/graphql-rust/juniper" }
juniper_rocket = { git = "https://github.com/graphql-rust/juniper_rocket" }
```
## 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: () |&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 framework
integration, see the [rocket][rocket_examples] and [iron][iron_examples] examples folders.
## 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.
[specification][graphql_spec], including interfaces, unions, schema
introspection, and 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>>>`.
## Integrations
### Data types
Juniper has automatic integration with some very common Rust crates to make
building schemas a breeze. The types from these crates will be usable in
your Schemas automatically.
* [uuid][uuid]
* [url][url]
* [chrono][chrono]
### Web Frameworks
* [rocket][rocket]
* [iron][iron]
## API Stability
Juniper has not reached 1.0 yet, thus some API instability should be expected.
## 1.0 Roadmap
> Version 0.8.1 probably be re-released as 1.0 to indicate API stability.
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.
* [X] Extensive execution testing
* [X] Sending input objects and partial input objects in variables
* [X] Sending enums in variables
* [X] General input value type checking and validation
* [X] Improve helper macros
* [X] `graphql_union!` helper completely missing
* [X] `graphql_input_object!` helper completely missing
* [X] Add support for deprecating things
* [X] Custom enum values and descriptions
* [X] Improved syntax for fields that can't fail resolution - make
`FieldResult<T>` optional maybe?
* [X] 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.
* [X] Larger examples to illustrate things like database access
[graphql]: http://graphql.org
[graphiql]: https://github.com/graphql/graphiql
[iron]: http://ironframework.io
[swschema]: http://graphql.org/docs/typesystem/
[graphql_spec]: http://facebook.github.io/graphql
[test_schema_rs]: juniper/src/tests/schema.rs
[test_schema_rs]: https://github.com/graphql-rust/juniper/blob/master/src/tests/schema.rs
[tokio]: https://github.com/tokio-rs/tokio
[rocket_examples]: https://github.com/graphql-rust/juniper_rocket/tree/master/examples
[iron_examples]: https://github.com/graphql-rust/juniper_iron/tree/master/examples
[rocket_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_rocket/examples
[iron_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_iron/examples
[Rocket]: https://rocket.rs
[book]: https://graphql-rust.github.io
[book_quickstart]: https://graphql-rust.github.io/quickstart.html
[docsrs]: https://docs.rs/juniper
[uuid]: https://crates.io/crates/uuid
[url]: https://crates.io/crates/url
[chrono]: https://crates.io/crates/chrono

View file

@ -5,28 +5,16 @@
environment:
matrix:
# Stable channel
- TARGET: i686-pc-windows-gnu
CHANNEL: stable
- TARGET: i686-pc-windows-msvc
CHANNEL: stable
- TARGET: x86_64-pc-windows-gnu
CHANNEL: stable
- TARGET: x86_64-pc-windows-msvc
CHANNEL: stable
# Beta channel
- TARGET: i686-pc-windows-gnu
CHANNEL: beta
- TARGET: i686-pc-windows-msvc
CHANNEL: beta
- TARGET: x86_64-pc-windows-gnu
CHANNEL: beta
- TARGET: x86_64-pc-windows-msvc
CHANNEL: beta
# Nightly channel
- TARGET: i686-pc-windows-gnu
CHANNEL: nightly
- TARGET: i686-pc-windows-msvc
CHANNEL: nightly
- TARGET: x86_64-pc-windows-gnu
CHANNEL: nightly
- TARGET: x86_64-pc-windows-msvc
@ -49,4 +37,9 @@ install:
build: false
test_script:
- cargo make ci-flow
- cd juniper && cargo build --verbose && cargo test --verbose && cd ..
- cd juniper_codegen && cargo build --verbose && cargo test --verbose && cd ..
- cd juniper_tests && cargo build --verbose && cargo test --verbose && cd ..
- cd juniper_iron && cargo build --verbose && cargo test --verbose && cd ..
- IF NOT %TARGET% == %TARGET:msvc=% ( IF %CHANNEL% == "nightly" ( cd juniper_rocket && cargo test --verbose && cargo build --verbose && cd .. ) )

89
changelog/0.9.0.md Normal file
View file

@ -0,0 +1,89 @@
# [0.9.0] 2017-12-03
## Changes
This is the first release in a long time.
Quite a few changes have accumulated since `0.8.1`, including multiple breaking
changes.
### Custom derive & macros
Juniper has gained custom derive implementations for input objects, objects and
enums.
* `#[derive(GraphQLInputObject)]`
* `#[derive(GraphQLEnum)]`
* `#[derive(GraphQLObject)]`
The `graphql_enum!` and `graphql_input_object!` macros did not provide any more
benefits, so they have been removed!
All functionality is now covered by custom derive.
Check the [docs](https://graphql-rust.github.io) to find out more.
### Web framework integrations - Iron & Rocket
The iron and rocket integrations were removed from the main crate, and are now
available via the [juniper_iron](https://crates.io/crates/juniper_iron) and
[juniper_rocket](https://crates.io/crates/juniper_rocket) crates.
### FieldError - Custom data
The `FieldError` type now supports custom data with the `Value` type from
serde_json. Use this to populate the `data` field in returned errors.
### Dynamic Schemas
Juniper has gained support for dynamic schemas, thanks to @srijs.
That also means the type of `RootNode` has changed to include a lifetime.
The repository was restructured to a multi crate workspace to enable several new
features like custom_derive and an extracted parser.
#[#66](https://github.com/graphql-rust/juniper/pull/66)
### Data Type Integrations
Integrations with multiple popular crates was added to make working with them
easier.
* uuid
* url
* chrono
### Field Order
To better comply with the specification, order of requested fields is
now preserved.
[#82](https://github.com/graphql-rust/juniper/issues/82
### From/ToInputValue
The `::from` and `::to` methods in `From/ToInputValue` were renamed to
`from/to_input_value()` to not conflict with other methods.
[#90](https://github.com/graphql-rust/juniper/pull/90)
### Other changes
* Several small performance improvements
* Use [fnv](https://github.com/servo/rust-fnv) hash map for better performance
## Contributors
A big shoutout to the many contributors for this version, sorted alphabetically.
* Cameron Eldridge <cameldridge+git@gmail.com>
* Christian Legnitto <christian@legnitto.com>
* Jacob Haslehurst <jacob@haslehurst.net>
* Jane Keibler <wanderingelf14@gmail.com>
* Magnus Hallin <mhallin@fastmail.com>
* rushmorem <rushmore@webenchanter.com>
* Rushmore Mushambi <rushmore@webenchanter.com>
* Sagie Gur-Ari <sagiegurari@gmail.com>
* Sam Rijs <srijs@airpost.net>
* Stanko Krtalić <stanko.krtalic@gmail.com>
* theduke <chris@theduke.at>
* thomas-jeepe <penguinSoccer@outlook.com>

View file

@ -1,22 +1,6 @@
Change log
==========
## [Unreleased]
The repository was restructured to a multi crate workspace to enable several new features like custom_derive and an extracted parser.
### New features
* New juniper_codegen crate which provides custom derives:
* `#[derive(GraphQLInputObject)]`
* `#[derive(GraphQLEnum)]`
* `#[derive(GraphQLObject)]`
## Breaking changes
* To better comply with the specification, order of requested fields is
now preserved.
([#82](https://github.com/graphql-rust/juniper/issues/82)
## [0.8.1] 2017-06-15

View file

@ -1,6 +1,6 @@
[package]
name = "juniper"
version = "0.8.1"
version = "0.9.0"
authors = ["Magnus Hallin <mhallin@fastmail.com>"]
description = "GraphQL server library"
license = "BSD-2-Clause"
@ -22,18 +22,25 @@ path = "benches/bench.rs"
[features]
nightly = []
expose-test-schema = []
default = ["chrono", "url", "uuid"]
default = [
"chrono",
"url",
"uuid"
]
[dependencies]
juniper_codegen = { version = "0.9.0", path = "../juniper_codegen" }
fnv = "1.0.3"
chrono = { version = "^0.4.0", optional = true }
ordermap = { version = "^0.2.11", features = ["serde-1"] }
serde = { version = "^1.0.8" }
serde_derive = {version="^1.0.8" }
serde_json = { version="^1.0.2", optional = true }
url = { version = "^1.5.1", optional = true }
ordermap = { version = "0.2.11", features = ["serde-1"] }
serde = { version = "1.0.8" }
serde_derive = {version="1.0.2" }
chrono = { version = "0.4.0", optional = true }
serde_json = { version="1.0.2", optional = true }
url = { version = "1.5.1", optional = true }
uuid = { version = "0.5.1", optional = true }
[dev-dependencies]
bencher = "^0.1.2"
serde_json = { version = "^1.0.2" }
bencher = "0.1.2"
serde_json = { version = "1.0.2" }

View file

@ -150,9 +150,9 @@ pub type Document<'a> = Vec<Definition<'a>>;
/// 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.
/// automatically by the convenience macro `graphql_scalar!` or by deriving GraphQLEnum.
///
/// Must be implemented manually when manually exposing new enums or scalars.
pub trait FromInputValue: Sized {
/// Performs the conversion.
fn from_input_value(v: &InputValue) -> Option<Self>;

View file

@ -9,7 +9,8 @@ use validation::RuleError;
use parser::SourcePosition;
use types::scalars::EmptyMutation;
#[derive(Debug)]
#[derive(GraphQLEnum, Debug)]
#[graphql(_internal)]
enum Color {
Red,
Green,
@ -17,12 +18,6 @@ enum Color {
}
struct TestType;
graphql_enum!(Color {
Color::Red => "RED",
Color::Green => "GREEN",
Color::Blue => "BLUE",
});
graphql_object!(TestType: () |&self| {
field to_string(color: Color) -> String {
format!("Color::{:?}", color)

View file

@ -5,34 +5,6 @@ use value::Value;
use schema::model::RootNode;
use types::scalars::EmptyMutation;
enum DefaultName {
Foo,
Bar,
}
enum Named {
Foo,
Bar,
}
enum NoTrailingComma {
Foo,
Bar,
}
enum EnumDescription {
Foo,
Bar,
}
enum EnumValueDescription {
Foo,
Bar,
}
enum EnumDeprecation {
Foo,
Bar,
}
struct Root;
/*
Syntax to validate:
@ -45,37 +17,53 @@ Syntax to validate:
*/
graphql_enum!(DefaultName {
DefaultName::Foo => "FOO",
DefaultName::Bar => "BAR",
});
#[derive(GraphQLEnum)]
#[graphql(_internal)]
enum DefaultName {
Foo,
Bar,
}
graphql_enum!(Named as "ANamedEnum" {
Named::Foo => "FOO",
Named::Bar => "BAR",
});
#[derive(GraphQLEnum)]
#[graphql(name = "ANamedEnum", _internal)]
enum Named {
Foo,
Bar,
}
graphql_enum!(NoTrailingComma {
NoTrailingComma::Foo => "FOO",
NoTrailingComma::Bar => "BAR"
});
#[derive(GraphQLEnum)]
#[graphql(_internal)]
enum NoTrailingComma {
Foo,
Bar,
}
graphql_enum!(EnumDescription {
description: "A description of the enum itself"
#[derive(GraphQLEnum)]
#[graphql(description = "A description of the enum itself", _internal)]
enum EnumDescription {
Foo,
Bar,
}
EnumDescription::Foo => "FOO",
EnumDescription::Bar => "BAR",
});
#[derive(GraphQLEnum)]
#[graphql(_internal)]
enum EnumValueDescription {
#[graphql(description = "The FOO value")]
Foo,
#[graphql(description = "The BAR value")]
Bar,
}
graphql_enum!(EnumValueDescription {
EnumValueDescription::Foo => "FOO" as "The FOO value",
EnumValueDescription::Bar => "BAR" as "The BAR value",
});
#[derive(GraphQLEnum)]
#[graphql(_internal)]
enum EnumDeprecation {
#[graphql(deprecated = "Please don't use FOO any more")]
Foo,
#[graphql(description = "The BAR value", deprecated = "Please don't use BAR any more")]
Bar,
}
graphql_enum!(EnumDeprecation {
EnumDeprecation::Foo => "FOO" deprecated "Please don't use FOO any more",
EnumDeprecation::Bar => "BAR" as "The BAR value" deprecated "Please don't use BAR any more",
});
struct Root;
graphql_object!(Root: () |&self| {
field default_name() -> DefaultName { DefaultName::Foo }

View file

@ -8,82 +8,81 @@ use types::scalars::EmptyMutation;
struct Root;
graphql_input_object!(
struct DefaultName {
#[derive(GraphQLInputObject)]
#[graphql(_internal)]
struct DefaultName {
field_one: String,
field_two: String,
}
);
}
graphql_input_object!(
struct NoTrailingComma {
#[derive(GraphQLInputObject)]
#[graphql(_internal)]
struct NoTrailingComma {
field_one: String,
field_two: String
}
);
}
graphql_input_object!(
#[derive(Debug)]
struct Derive {
#[derive(GraphQLInputObject, Debug)]
#[graphql(_internal)]
struct Derive {
field_one: String,
}
);
}
graphql_input_object!(
struct Named as "ANamedInputObject" {
#[derive(GraphQLInputObject, Debug)]
#[graphql(name = "ANamedInputObject", _internal)]
struct Named {
field_one: String,
}
);
}
graphql_input_object!(
description: "Description for the input object"
struct Description {
#[derive(GraphQLInputObject, Debug)]
#[graphql(description = "Description for the input object", _internal)]
struct Description {
field_one: String,
}
);
}
graphql_input_object!(
pub struct Public {
#[derive(GraphQLInputObject, Debug)]
#[graphql(_internal)]
pub struct Public {
field_one: String,
}
);
}
graphql_input_object!(
description: "Description for the input object"
pub struct PublicWithDescription {
#[derive(GraphQLInputObject, Debug)]
#[graphql(description = "Description for the input object", _internal)]
pub struct PublicWithDescription {
field_one: String,
}
);
}
graphql_input_object!(
description: "Description for the input object"
pub struct NamedPublicWithDescription as "APublicNamedInputObjectWithDescription" {
#[derive(GraphQLInputObject, Debug)]
#[graphql(name = "APublicNamedInputObjectWithDescription",
description = "Description for the input object",
_internal)]
pub struct NamedPublicWithDescription {
field_one: String,
}
);
}
graphql_input_object!(
pub struct NamedPublic as "APublicNamedInputObject" {
#[derive(GraphQLInputObject, Debug)]
#[graphql(name = "APublicNamedInputObject", _internal)]
pub struct NamedPublic {
field_one: String,
}
);
}
graphql_input_object!(
struct FieldDescription {
field_one: String as "The first field",
field_two: String as "The second field",
}
);
#[derive(GraphQLInputObject, Debug)]
#[graphql(_internal)]
struct FieldDescription {
#[graphql(description = "The first field")]
field_one: String,
#[graphql(description = "The second field")]
field_two: String,
}
graphql_input_object!(
struct FieldWithDefaults {
field_one = 123: i32,
field_two = 456: i32 as "The second field",
}
);
#[derive(GraphQLInputObject, Debug)]
#[graphql(_internal)]
struct FieldWithDefaults {
#[graphql(default = "123")]
field_one: i32,
#[graphql(default = "456", description = "The second field")]
field_two: i32,
}
graphql_object!(Root: () |&self| {
field test_field(

View file

@ -1,8 +1,17 @@
mod input_object;
mod enums;
// This asserts that the input objects defined public actually became public
#[allow(unused_imports)]
use self::input_object::{NamedPublic, NamedPublicWithDescription};
use executor::Variables;
use value::Value;
use schema::model::RootNode;
use types::scalars::EmptyMutation;
#[derive(GraphQLEnum)]
#[graphql(name = "SampleEnum", _internal)]
enum Sample {
One,
Two,
@ -24,11 +33,6 @@ graphql_scalar!(Scalar as "SampleScalar" {
}
});
graphql_enum!(Sample as "SampleEnum" {
Sample::One => "ONE",
Sample::Two => "TWO",
});
graphql_interface!(Interface: () as "SampleInterface" |&self| {
description: "A sample interface"

View file

@ -30,39 +30,35 @@ graphql_scalar!(TestComplexScalar {
}
});
graphql_input_object!(
#[derive(Debug)]
struct TestInputObject {
#[derive(GraphQLInputObject, Debug)]
#[graphql(_internal)]
struct TestInputObject {
a: Option<String>,
b: Option<Vec<Option<String>>>,
c: String,
d: Option<TestComplexScalar>,
}
);
}
graphql_input_object!(
#[derive(Debug)]
struct TestNestedInputObject {
#[derive(GraphQLInputObject, Debug)]
#[graphql(_internal)]
struct TestNestedInputObject {
na: TestInputObject,
nb: String,
}
);
}
graphql_input_object!(
#[derive(Debug)]
struct ExampleInputObject {
#[derive(GraphQLInputObject, Debug)]
#[graphql(_internal)]
struct ExampleInputObject {
a: Option<String>,
b: i32,
}
);
}
graphql_input_object!(
#[derive(Debug)]
struct InputWithDefaults {
a = 123: i32,
}
);
#[derive(GraphQLInputObject, Debug)]
#[graphql(_internal)]
struct InputWithDefaults {
#[graphql(default = "123")]
a: i32,
}
graphql_object!(TestType: () |&self| {
field field_with_object_input(input: Option<TestInputObject>) -> String {

View file

@ -2,117 +2,90 @@
# 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.
[GraphQL][graphql] is a data query language developed by Facebook intended to
serve mobile and web application frontends.
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][Iron] and
[Rocket]. 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.
*Juniper* makes it possible to write GraphQL servers in Rust that are
type-safe and blazingly fast. We also try to make declaring and resolving
GraphQL schemas as convenient as possible as Rust will allow.
Juniper only depends on `serde` and `serde_derive` by default, making it
lightweight and easy to drop into any project.
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][iron] and [Rocket] frameworks, including
embedded [Graphiql][graphiql] for easy debugging.
## Exposing data types
* [Cargo crate](https://crates.io/crates/juniper)
* [API Reference][docsrs]
* [Book][book]: Guides and Examples
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:
## Getting Started
```rust
#[macro_use] extern crate juniper;
# use std::collections::HashMap;
use juniper::{Context, FieldResult};
The best place to get started is the [Juniper Book][book], which contains
guides with plenty of examples, covering all features of Juniper.
struct User { id: String, name: String, friend_ids: Vec<String> }
struct QueryRoot;
struct Database { users: HashMap<String, User> }
To get started quickly and get a feel for Juniper, check out the
[Quickstart][book_quickstart] section.
impl Context for Database {}
For specific information about macros, types and the Juniper api, the
[API Reference][docsrs] is the best place to look.
// GraphQL objects can access a "context object" during execution. Use this
// object to provide e.g. database access to the field accessors. This object
// must implement the `Context` trait. If you don't need a context, use the
// empty tuple `()` to indicate this.
//
// In this example, we use the Database struct as our context.
graphql_object!(User: Database |&self| {
You can also check out [src/tests/schema.rs][test_schema_rs] to see a complex
schema including polymorphism with traits and interfaces.
For an example of web framework integration,
see the [rocket][rocket_examples] and [iron][iron_examples] examples folders.
// Expose a simple field as a GraphQL string.
field id() -> &String {
&self.id
}
field name() -> &String {
&self.name
}
## Features
// FieldResult<T> is an alias for Result<T, FieldError>, which can be
// converted to from anything that implements std::fmt::Display - simply
// return an error with a string using the ? operator from this method and
// it will be correctly inserted into the execution response.
field secret() -> FieldResult<&String> {
Err("Can't touch this".to_owned())?
}
Juniper supports the full GraphQL query language according to the
[specification][graphql_spec], including interfaces, unions, schema
introspection, and validations.
It does not, however, support the schema language.
// 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(&executor) -> Vec<&User> {
self.friend_ids.iter()
.filter_map(|id| executor.context().users.get(id))
.collect()
}
});
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>>>`.
// 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 |&self| {
## Integrations
// Arguments work just like they do on functions.
field user(&executor, id: String) -> Option<&User> {
executor.context().users.get(&id)
}
});
### Data types
# fn main() { }
```
Juniper has automatic integration with some very common Rust crates to make
building schemas a breeze. The types from these crates will be usable in
your Schemas automatically.
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.
* [uuid][uuid]
* [url][url]
* [chrono][chrono]
### Built-in object type integrations
### Web Frameworks
Juniper has [built-in integrations][object_integrations] for converting existing object types to
GraphQL objects for popular crates.
* [rocket][rocket]
* [iron][iron]
## Integrating with web servers
The most obvious usecase is to expose the GraphQL schema over an HTTP endpoint.
To support this, Juniper offers additional crates that integrate with popular web frameworks.
## API Stability
* [juniper_iron][juniper_iron]: Handlers for [Iron][Iron]
* [juniper_rocket][juniper_rocket]: Handlers for [Rocket][Rocket]
Juniper has not reached 1.0 yet, thus some API instability should be expected.
[1]: http://graphql.org
[3]: macro.graphql_object!.html
[Iron]: http://ironframework.io
[graphql]: http://graphql.org
[graphiql]: https://github.com/graphql/graphiql
[iron]: http://ironframework.io
[graphql_spec]: http://facebook.github.io/graphql
[test_schema_rs]: https://github.com/graphql-rust/juniper/blob/master/juniper/src/tests/schema.rs
[tokio]: https://github.com/tokio-rs/tokio
[rocket_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_rocket/examples
[iron_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_iron/examples
[Rocket]: https://rocket.rs
[object_integrations]: integrations/index.html
[book]: https://graphql-rust.github.io/
[book_quickstart]: https://graphql-rust.github.io/quickstart.html
[docsrs]: https://docs.rs/juniper
[uuid]: https://crates.io/crates/uuid
[url]: https://crates.io/crates/url
[chrono]: https://crates.io/crates/chrono
*/
#![warn(missing_docs)]
@ -136,7 +109,13 @@ extern crate url;
#[cfg(any(test, feature = "uuid"))]
extern crate uuid;
use std::borrow::Cow;
// Depend on juniper_codegen and re-export everything in it.
// This allows users to just depend on juniper and get the derive functionality automatically.
#[allow(unused_imports)]
#[macro_use]
extern crate juniper_codegen;
#[doc(hidden)]
pub use juniper_codegen::*;
#[macro_use]
mod value;
@ -147,6 +126,7 @@ pub mod parser;
mod types;
mod schema;
mod validation;
mod util;
mod executor;
// This needs to be public until docs have support for private modules:
// https://github.com/rust-lang/cargo/issues/1520
@ -164,6 +144,9 @@ pub mod tests;
#[cfg(test)]
mod executor_tests;
// Needs to be public because macros use it.
pub use util::to_camel_case;
use parser::{parse_document_source, ParseError, Spanning};
use validation::{validate_input_values, visit_all_rules, ValidatorContext};
use executor::execute_validated_query;
@ -232,40 +215,4 @@ impl<'a> From<Spanning<ParseError<'a>>> for GraphQLError<'a> {
}
}
#[doc(hidden)]
pub fn to_camel_case<'a>(s: &'a str) -> Cow<'a, str> {
let mut dest = Cow::Borrowed(s);
for (i, part) in s.split('_').enumerate() {
if i > 0 && part.len() == 1 {
dest += Cow::Owned(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 += Cow::Owned(first);
dest += second;
} else if i == 0 {
dest = Cow::Borrowed(part);
}
}
dest
}
#[test]
fn test_to_camel_case() {
assert_eq!(&to_camel_case("test")[..], "test");
assert_eq!(&to_camel_case("_test")[..], "Test");
assert_eq!(&to_camel_case("first_second")[..], "firstSecond");
assert_eq!(&to_camel_case("first_")[..], "first");
assert_eq!(&to_camel_case("a_b_c")[..], "aBC");
assert_eq!(&to_camel_case("a_bc")[..], "aBc");
assert_eq!(&to_camel_case("a_b")[..], "aB");
assert_eq!(&to_camel_case("a")[..], "a");
assert_eq!(&to_camel_case("")[..], "");
}

View file

@ -1,193 +0,0 @@
/**
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,
Orange,
Green,
Blue,
Black,
}
graphql_enum!(Color {
Color::Red => "RED" as "The color red",
Color::Orange => "ORANGE",
Color::Green => "GREEN",
Color::Blue => "BLUE",
Color::Black => "BLACK" deprecated "Superseded by ORANGE",
});
# 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 };
( @as_path, $p:path) => { $p };
// Calls $val.$func($arg) if $arg is not None
( @maybe_apply, None, $func:ident, $val:expr ) => { $val };
( @maybe_apply, $arg:tt, $func:ident, $val:expr ) => { $val.$func($arg) };
// Each of the @parse match arms accumulates data up to a call to @generate.
//
// ( $name, $outname, $descr ): the name of the Rust enum, the name of the
// GraphQL enum (as a string), and the description of the enum (as a string or None)
//
// [ ( $eval, $ename, $edescr, $edepr ) , ] the value of the Rust enum,
// the value of the GraphQL enum (as a string), the description of the enum
// value (as a string or None), and the deprecation reason of the enum value
// (as a string or None).
(
@generate,
( $name:path, $outname:tt, $descr:tt ),
[ $( ( $eval:tt, $ename:tt, $edescr:tt, $edepr:tt ) , )* ]
) => {
impl $crate::GraphQLType for $name {
type Context = ();
type TypeInfo = ();
fn name(_: &()) -> Option<&str> {
Some(graphql_enum!(@as_expr, $outname))
}
fn meta<'r>(info: &(), registry: &mut $crate::Registry<'r>) -> $crate::meta::MetaType<'r> {
graphql_enum!(
@maybe_apply, $descr, description,
registry.build_enum_type::<$name>(info, &[
$(
graphql_enum!(
@maybe_apply,
$edepr, deprecated,
graphql_enum!(
@maybe_apply,
$edescr, description,
$crate::meta::EnumValue::new(graphql_enum!(@as_expr, $ename))))
),*
]))
.into_meta()
}
fn resolve(&self, _: &(), _: Option<&[$crate::Selection]>, _: &$crate::Executor<Self::Context>) -> $crate::Value {
match *self {
$(
graphql_enum!(@as_pattern, $eval) =>
$crate::Value::string(graphql_enum!(@as_expr, $ename)) ),*
}
}
}
impl $crate::FromInputValue for $name {
fn from_input_value(v: &$crate::InputValue) -> Option<$name> {
match v.as_enum_value().or_else(|| v.as_string_value()) {
$(
Some(graphql_enum!(@as_pattern, $ename))
=> Some(graphql_enum!(@as_expr, $eval)), )*
_ => None,
}
}
}
impl $crate::ToInputValue for $name {
fn to_input_value(&self) -> $crate::InputValue {
match *self {
$(
graphql_enum!(@as_pattern, $eval) =>
$crate::InputValue::string(graphql_enum!(@as_expr, $ename)) ),*
}
}
}
};
// No more items to parse
( @parse, $meta:tt, $acc:tt, ) => {
graphql_enum!( @generate, $meta, $acc );
};
// Remove extraneous commas
( @parse, $meta:tt, $acc:tt, , $($rest:tt)* ) => {
graphql_enum!( @parse, $meta, $acc, $($rest)* );
};
// description: <description>
(
@parse,
( $name:tt, $outname:tt, $_ignore:tt ),
$acc:tt,
description: $descr:tt $($items:tt)*
) => {
graphql_enum!( @parse, ( $name, $outname, $descr ), $acc, $($items)* );
};
// RustEnumValue => "GraphQL enum value" deprecated <reason>
(
@parse,
$meta:tt,
[ $($acc:tt ,)* ],
$eval:path => $ename:tt deprecated $depr:tt $($rest:tt)*
) => {
graphql_enum!( @parse, $meta, [ $($acc ,)* ( $eval, $ename, None, $depr ), ], $($rest)* );
};
// RustEnumValue => "GraphQL enum value" as <description> deprecated <reason>
(
@parse,
$meta:tt,
[ $($acc:tt ,)* ],
$eval:path => $ename:tt as $descr:tt deprecated $depr:tt $($rest:tt)*
) => {
graphql_enum!( @parse, $meta, [ $($acc ,)* ( $eval, $ename, $descr, $depr ), ], $($rest)* );
};
// RustEnumValue => "GraphQL enum value" as <description>
(
@parse,
$meta:tt,
[ $($acc:tt ,)* ],
$eval:path => $ename:tt as $descr:tt $($rest:tt)*
) => {
graphql_enum!( @parse, $meta, [ $($acc ,)* ( $eval, $ename, $descr, None ), ], $($rest)* );
};
// RustEnumValue => "GraphQL enum value"
(
@parse,
$meta:tt,
[ $($acc:tt ,)* ],
$eval:path => $ename:tt $($rest:tt)*
) => {
graphql_enum!( @parse, $meta, [ $($acc ,)* ( $eval , $ename , None , None ), ], $($rest)* );
};
// Entry point:
// RustEnumName as "GraphQLEnumName" { ... }
(
$name:path as $outname:tt { $($items:tt)* }
) => {
graphql_enum!( @parse, ( $name, $outname, None ), [ ], $($items)* );
};
// Entry point
// RustEnumName { ... }
(
$name:path { $($items:tt)* }
) => {
graphql_enum!( @parse, ( $name, (stringify!($name)), None ), [ ], $($items)* );
};
}

View file

@ -1,283 +0,0 @@
/**
Create an input object
Input objects are used as data carriers for complex input values to
fields and mutations. Unlike the other helper macros,
`graphql_input_object!` actually *creates* the struct you define. It
does not add anything to the struct definition itself - what you type
is what will be generated:
```rust
# #[macro_use] extern crate juniper;
#
graphql_input_object!(
description: "Coordinates for the user"
struct Coordinates {
longitude: f64 as "The X coordinate, from -180 to +180",
latitude: f64 as "The Y coordinate, from -90 to +90",
}
);
# fn main() { }
```
This macro creates the struct as specified and implements
`FromInputValue` to automatically parse values provided from variables
and arguments.
If you want to expose the struct under a different name than the Rust
type, you can write `struct Coordinates as "MyCoordinates" { ...`.
You can specify *default values* for input object fields; the syntax
is similar to argument default values:
```rust
# #[macro_use] extern crate juniper;
#
graphql_input_object!(
struct SampleObject {
foo = 123: i32 as "A sample field, defaults to 123 if omitted"
}
);
# fn main() { }
```
*/
#[macro_export]
macro_rules! graphql_input_object {
// Calls $val.$func($arg) if $arg is not None
( @maybe_apply, None, $func:ident, $val:expr ) => { $val };
( @maybe_apply, $arg:tt, $func:ident, $val:expr ) => { $val.$func($arg) };
// Calls $val.description($descr) when $descr is not empty
( @apply_description, , $val:expr ) => { $val };
( @apply_description, $descr:tt , $val:expr ) => { $val.description($descr) };
// Generate the FromInputValue::from method body, provided a
// HashMap<&str, &InputValue> in $var
(
@generate_from_input_value,
$name:tt, $var:tt,
( $($field_name:ident $(= $default:tt)* : $field_type:ty $(as $descr:tt)* $(,)* ),* )
) => {
Some($name {
$( $field_name: {
let n = $crate::to_camel_case(stringify!($field_name));
let v: Option<&&$crate::InputValue> = $var.get(&n[..]);
match v {
$( Some(&&$crate::InputValue::Null) | None if true => $default, )*
Some(v) => $crate::FromInputValue::from_input_value(v).unwrap(),
_ => $crate::FromInputValue::from_input_value(&$crate::InputValue::null()).unwrap()
}
} ),*
})
};
// Generate the ToInputValue::To method body, provided self in $self
(
@generate_to_input_value,
$name:tt, $selfvar:tt,
( $($field_name:ident $(= $default:tt)* : $field_type:ty $(as $descr:tt)* $(,)* ),* )
) => {
$crate::InputValue::object(vec![
$(
($crate::to_camel_case(stringify!($field_name)), $selfvar.$field_name.to_input_value())
),*
].into_iter().collect())
};
// Generate the struct declaration, including (Rust) meta attributes
(
@generate_struct_fields,
( $($meta:tt)* ), ( $($pubmod:tt)* ), $name:tt,
( $($field_name:ident $(= $default:tt)* : $field_type:ty $(as $descr:tt)* $(,)* ),* )
) => {
$($meta)* $($pubmod)* struct $name {
$( $field_name: $field_type, )*
}
};
// Generate single field meta for field with default value
(
@generate_single_meta_field,
$reg:tt,
( $field_name:ident = $default:tt : $field_type:ty $(as $descr:tt)* )
) => {
graphql_input_object!(
@apply_description,
$($descr)*,
$reg.arg_with_default::<$field_type>(
&$crate::to_camel_case(stringify!($field_name)),
&$default, &()))
};
// Generate single field meta for field without default value
(
@generate_single_meta_field,
$reg:tt,
( $field_name:ident : $field_type:ty $(as $descr:tt)* )
) => {
graphql_input_object!(
@apply_description,
$($descr)*,
$reg.arg::<$field_type>(
&$crate::to_camel_case(stringify!($field_name)), &()))
};
// Generate the input field meta list, i.e. &[Argument] for
(
@generate_meta_fields,
$reg:tt,
( $($field_name:ident $(= $default:tt)* : $field_type:ty $(as $descr:tt)* $(,)* ),* )
) => {
&[
$(
graphql_input_object!(
@generate_single_meta_field,
$reg,
( $field_name $(= $default)* : $field_type $(as $descr)* )
)
),*
]
};
// #[...] struct $name { ... }
// struct $name { ... }
(
@parse,
( $_ignore1:tt, $_ignore2:tt, $_ignore3:tt, $_ignore4:tt, $_ignore5:tt, $descr:tt ),
$(#[$meta:meta])* struct $name:ident { $($fields:tt)* } $($rest:tt)*
) => {
graphql_input_object!(
@parse,
( ( $(#[$meta])* ), ( ), $name, (stringify!($name)), ($($fields)*), $descr ),
$($rest)*
);
};
// #[...] pub struct $name { ... }
// pub struct $name { ... }
(
@parse,
( $_ignore1:tt, $_ignore2:tt, $_ignore3:tt, $_ignore4:tt, $_ignore5:tt, $descr:tt ),
$(#[$meta:meta])* pub struct $name:ident { $($fields:tt)* } $($rest:tt)*
) => {
graphql_input_object!(
@parse,
( ( $(#[$meta])* ), ( pub ), $name, (stringify!($name)), ($($fields)*), $descr ),
$($rest)*
);
};
// #[...] struct $name as "GraphQLName" { ... }
// struct $name as "GraphQLName" { ... }
(
@parse,
( $_ignore1:tt, $_ignore2:tt, $_ignore3:tt, $_ignore4:tt, $_ignore5:tt, $descr:tt ),
$(#[$meta:meta])* struct $name:ident as $outname:tt { $($fields:tt)* } $($rest:tt)*
) => {
graphql_input_object!(
@parse,
( ( $($meta)* ), ( ), $name, $outname, ($($fields)*), $descr ),
$($rest)*
);
};
// #[...] pub struct $name as "GraphQLName" { ... }
// pub struct $name as "GraphQLName" { ... }
(
@parse,
( $_ignore1:tt, $_ignore2:tt, $_ignore3:tt, $_ignore4:tt, $_ignore5:tt, $descr:tt ),
$(#[$meta:meta])* pub struct $name:ident as $outname:tt { $($fields:tt)* } $($rest:tt)*
) => {
graphql_input_object!(
@parse,
( ( $($meta)* ), ( pub ), $name, $outname, ($($fields)*), $descr ),
$($rest)*
);
};
// description: <description>
(
@parse,
( $meta:tt, $pubmod:tt, $name:tt, $outname:tt, $fields:tt, $_ignore:tt ),
description: $descr:tt $($rest:tt)*
) => {
graphql_input_object!(
@parse,
( $meta, $pubmod, $name, $outname, $fields, $descr ),
$($rest)*
);
};
// No more data to parse, generate the struct and impls
(
@parse,
( $meta:tt, $pubmod:tt, $name:tt, $outname:tt, $fields:tt, $descr:tt ),
) => {
graphql_input_object!(@generate_struct_fields, $meta, $pubmod, $name, $fields);
impl $crate::FromInputValue for $name {
fn from_input_value(value: &$crate::InputValue) -> Option<$name> {
if let Some(obj) = value.to_object_value() {
graphql_input_object!(@generate_from_input_value, $name, obj, $fields)
}
else {
None
}
}
}
impl $crate::ToInputValue for $name {
fn to_input_value(&self) -> $crate::InputValue {
graphql_input_object!(@generate_to_input_value, $name, self, $fields)
}
}
impl $crate::GraphQLType for $name {
type Context = ();
type TypeInfo = ();
fn name(_: &()) -> Option<&str> {
Some($outname)
}
fn meta<'r>(_: &(), registry: &mut $crate::Registry<'r>) -> $crate::meta::MetaType<'r> {
let fields = graphql_input_object!(@generate_meta_fields, registry, $fields);
graphql_input_object!(
@maybe_apply, $descr, description,
registry.build_input_object_type::<$name>(&(), fields)).into_meta()
}
}
};
// Entry point: parse calls starting with a struct declaration
( $(#[$meta:meta])* struct $($items:tt)* ) => {
graphql_input_object!(
@parse,
( ( ), ( ), None, None, None, None ),
$(#[$meta])* struct $($items)*
);
};
// Entry point: parse calls starting with a public struct declaration
( $(#[$meta:meta])* pub struct $($items:tt)* ) => {
graphql_input_object!(
@parse,
( ( ), ( ), None, None, None, None ),
$(#[$meta])* pub struct $($items)*
);
};
// Entry point: parse calls starting with the description
( description: $($items:tt)* ) => {
graphql_input_object!(
@parse,
( ( ), ( ), None, None, None, None ),
description: $($items)*
);
};
}

View file

@ -1,6 +1,4 @@
#[macro_use]
mod enums;
#[macro_use]
mod object;
#[macro_use]
mod interface;
@ -11,8 +9,6 @@ mod args;
#[macro_use]
mod field;
#[macro_use]
mod input_object;
#[macro_use]
mod union;
#[cfg(test)]

View file

@ -19,12 +19,11 @@ Syntax to validate:
*/
graphql_input_object!(
#[derive(Debug)]
struct Point {
#[derive(GraphQLInputObject)]
#[graphql(_internal)]
struct Point {
x: i32,
}
);
}
graphql_object!(Root: () |&self| {
field simple() -> i32 { 0 }

View file

@ -1,7 +1,4 @@
mod enums;
mod scalar;
#[allow(dead_code)]
mod input_object;
mod args;
mod field;
mod object;
@ -9,6 +6,4 @@ mod interface;
mod union;
// This asserts that the input objects defined public actually became public
#[allow(unused_imports)]
use self::input_object::{NamedPublic, NamedPublicWithDescription};

View file

@ -49,13 +49,17 @@ pub struct DirectiveType<'a> {
pub arguments: Vec<Argument<'a>>,
}
#[derive(Clone, PartialEq, Eq, Debug)]
#[derive(GraphQLEnum, Clone, PartialEq, Eq, Debug)]
#[graphql(name = "__DirectiveLocation", _internal)]
pub enum DirectiveLocation {
Query,
Mutation,
Field,
#[graphql(name = "FRAGMENT_DEFINITION")]
FragmentDefinition,
#[graphql(name = "FRAGMENT_SPREAD")]
FragmentSpread,
#[graphql(name = "INLINE_SPREAD")]
InlineFragment,
}

View file

@ -232,17 +232,6 @@ graphql_object!(EnumValue: () as "__EnumValue" |&self| {
}
});
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!(<'a> DirectiveType<'a>: SchemaType<'a> as "__Directive" |&self| {
field name() -> &String {
@ -282,11 +271,3 @@ graphql_object!(<'a> DirectiveType<'a>: SchemaType<'a> as "__Directive" |&self|
}
});
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",
});

View file

@ -2,8 +2,10 @@
use std::collections::HashMap;
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
#[derive(GraphQLEnum, Copy, Clone, Eq, PartialEq, Debug)]
#[graphql(_internal)]
pub enum Episode {
#[graphql(name = "NEW_HOPE")]
NewHope,
Empire,
Jedi,

View file

@ -3,12 +3,6 @@ use executor::Context;
impl Context for Database {}
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"

View file

@ -13,7 +13,9 @@ use parser::Spanning;
///
/// The GraphQL specification defines a number of type kinds - the meta type
/// of a type.
#[derive(Clone, Eq, PartialEq, Debug)]
#[derive(GraphQLEnum, Clone, Eq, PartialEq, Debug)]
// Note: _internal flag needed to make derive work in juniper crate itself.
#[graphql(name = "__TypeKind", _internal)]
pub enum TypeKind {
/// ## Scalar types
///
@ -48,6 +50,7 @@ pub enum TypeKind {
/// ## Input objects
///
/// Represents complex values provided in queries _into_ the system.
#[graphql(name = "INPUT_OBJECT")]
InputObject,
/// ## List types
@ -61,6 +64,7 @@ pub enum TypeKind {
///
/// In GraphQL, nullable types are the default. By putting a `!` after a
/// type, it becomes non-nullable.
#[graphql(name = "NON_NULL")]
NonNull,
}

42
juniper/src/util.rs Normal file
View file

@ -0,0 +1,42 @@
use std::borrow::Cow;
/// Convert string to camel case.
///
/// Note: needs to be public because several macros use it.
#[doc(hidden)]
pub fn to_camel_case<'a>(s: &'a str) -> Cow<'a, str> {
let mut dest = Cow::Borrowed(s);
for (i, part) in s.split('_').enumerate() {
if i > 0 && part.len() == 1 {
dest += Cow::Owned(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 += Cow::Owned(first);
dest += second;
} else if i == 0 {
dest = Cow::Borrowed(part);
}
}
dest
}
#[test]
fn test_to_camel_case() {
assert_eq!(&to_camel_case("test")[..], "test");
assert_eq!(&to_camel_case("_test")[..], "Test");
assert_eq!(&to_camel_case("first_second")[..], "firstSecond");
assert_eq!(&to_camel_case("first_")[..], "first");
assert_eq!(&to_camel_case("a_b_c")[..], "aBC");
assert_eq!(&to_camel_case("a_bc")[..], "aBc");
assert_eq!(&to_camel_case("a_b")[..], "aB");
assert_eq!(&to_camel_case("a")[..], "a");
assert_eq!(&to_camel_case("")[..], "");
}

View file

@ -1,6 +1,6 @@
[package]
name = "juniper_codegen"
version = "0.8.1"
version = "0.9.0"
authors = ["Magnus Hallin <mhallin@fastmail.com>"]
description = "Internal custom derive trait for Juniper GraphQL"
license = "BSD-2-Clause"
@ -11,8 +11,8 @@ repository = "https://github.com/graphql-rust/juniper"
proc-macro = true
[dependencies]
syn = "0.11.11"
quote = "0.3.15"
syn = "0.11"
quote = "0.3"
[badges]
travis-ci = { repository = "graphql-rust/juniper" }

View file

@ -9,11 +9,17 @@ use util::*;
struct EnumAttrs {
name: Option<String>,
description: Option<String>,
internal: bool,
}
impl EnumAttrs {
fn from_input(input: &DeriveInput) -> EnumAttrs {
let mut res = EnumAttrs::default();
let mut res = EnumAttrs{
name: None,
description: None,
/// Flag to specify whether the calling crate is the "juniper" crate itself.
internal: false,
};
// Check attributes for name and description.
if let Some(items) = get_graphl_attr(&input.attrs) {
@ -26,6 +32,15 @@ impl EnumAttrs {
res.description = Some(val);
continue;
}
match item {
&NestedMetaItem::MetaItem(MetaItem::Word(ref ident)) => {
if ident == "_internal" {
res.internal = true;
continue;
}
},
_ => {},
}
panic!(format!(
"Unknown attribute for #[derive(GraphQLEnum)]: {:?}",
item
@ -109,7 +124,7 @@ pub fn impl_enum(ast: &syn::DeriveInput) -> Tokens {
// Build value.
let name = var_attrs
.name
.unwrap_or(variant.ident.as_ref().to_uppercase());
.unwrap_or(::util::to_upper_snake_case(variant.ident.as_ref()));
let descr = match var_attrs.description {
Some(s) => quote!{ Some(#s.to_string()) },
None => quote!{ None },
@ -119,7 +134,7 @@ pub fn impl_enum(ast: &syn::DeriveInput) -> Tokens {
None => quote!{ None },
};
let value = quote!{
::juniper::meta::EnumValue{
_juniper::meta::EnumValue{
name: #name.to_string(),
description: #descr,
deprecation_reason: #depr,
@ -129,7 +144,7 @@ pub fn impl_enum(ast: &syn::DeriveInput) -> Tokens {
// Build resolve match clause.
let resolve = quote!{
&#ident::#var_ident => ::juniper::Value::String(#name.to_string()),
&#ident::#var_ident => _juniper::Value::String(#name.to_string()),
};
resolves.push(resolve);
@ -142,13 +157,13 @@ pub fn impl_enum(ast: &syn::DeriveInput) -> Tokens {
// Buil to_input clause.
let to_input = quote!{
&#ident::#var_ident =>
::juniper::InputValue::string(#name.to_string()),
_juniper::InputValue::string(#name.to_string()),
};
to_inputs.push(to_input);
}
quote! {
impl ::juniper::GraphQLType for #ident {
let body = quote! {
impl _juniper::GraphQLType for #ident {
type Context = ();
type TypeInfo = ();
@ -156,7 +171,7 @@ pub fn impl_enum(ast: &syn::DeriveInput) -> Tokens {
Some(#name)
}
fn meta<'r>(_: &(), registry: &mut ::juniper::Registry<'r>) -> ::juniper::meta::MetaType<'r> {
fn meta<'r>(_: &(), registry: &mut _juniper::Registry<'r>) -> _juniper::meta::MetaType<'r> {
let meta = registry.build_enum_type::<#ident>(&(), &[
#(#values)*
]);
@ -164,15 +179,15 @@ pub fn impl_enum(ast: &syn::DeriveInput) -> Tokens {
meta.into_meta()
}
fn resolve(&self, _: &(), _: Option<&[::juniper::Selection]>, _: &::juniper::Executor<Self::Context>) -> ::juniper::Value {
fn resolve(&self, _: &(), _: Option<&[_juniper::Selection]>, _: &_juniper::Executor<Self::Context>) -> _juniper::Value {
match self {
#(#resolves)*
}
}
}
impl ::juniper::FromInputValue for #ident {
fn from_input_value(v: &::juniper::InputValue) -> Option<#ident> {
impl _juniper::FromInputValue for #ident {
fn from_input_value(v: &_juniper::InputValue) -> Option<#ident> {
match v.as_enum_value().or_else(|| v.as_string_value()) {
#(#from_inputs)*
_ => None,
@ -180,12 +195,49 @@ pub fn impl_enum(ast: &syn::DeriveInput) -> Tokens {
}
}
impl ::juniper::ToInputValue for #ident {
fn to_input_value(&self) -> ::juniper::InputValue {
impl _juniper::ToInputValue for #ident {
fn to_input_value(&self) -> _juniper::InputValue {
match self {
#(#to_inputs)*
}
}
}
};
let dummy_const = Ident::new(format!("_IMPL_GRAPHQLENUM_FOR_{}", ident));
// This ugly hack makes it possible to use the derive inside juniper itself.
// FIXME: Figure out a better way to do this!
let crate_reference = if attrs.internal {
quote! {
#[doc(hidden)]
mod _juniper {
pub use ::{
InputValue,
Value,
ToInputValue,
FromInputValue,
Executor,
Selection,
Registry,
GraphQLType,
meta
};
}
}
} else {
quote! {
extern crate juniper as _juniper;
}
};
let generated = quote! {
#[allow(non_upper_case_globals, unused_attributes, unused_qualifications)]
#[doc(hidden)]
const #dummy_const : () = {
#crate_reference
#body
};
};
generated
}

View file

@ -9,6 +9,7 @@ use util::*;
struct ObjAttrs {
name: Option<String>,
description: Option<String>,
internal: bool,
}
impl ObjAttrs {
@ -26,6 +27,15 @@ impl ObjAttrs {
res.description = Some(val);
continue;
}
match item {
&NestedMetaItem::MetaItem(MetaItem::Word(ref ident)) => {
if ident == "_internal" {
res.internal = true;
continue;
}
},
_ => {},
}
panic!(format!(
"Unknown attribute for #[derive(GraphQLInputObject)]: {:?}",
item
@ -40,7 +50,8 @@ impl ObjAttrs {
struct ObjFieldAttrs {
name: Option<String>,
description: Option<String>,
default: Option<String>,
default: bool,
default_expr: Option<String>,
}
impl ObjFieldAttrs {
@ -59,9 +70,18 @@ impl ObjFieldAttrs {
continue;
}
if let Some(val) = keyed_item_value(item, "default", true) {
res.default = Some(val);
res.default_expr = Some(val);
continue;
}
match item {
&NestedMetaItem::MetaItem(MetaItem::Word(ref ident)) => {
if ident == "default" {
res.default = true;
continue;
}
}
_ => {},
}
panic!(format!(
"Unknown attribute for #[derive(GraphQLInputObject)]: {:?}",
item
@ -122,14 +142,20 @@ pub fn impl_input_object(ast: &syn::DeriveInput) -> Tokens {
None => quote!{ let field = field; },
};
let default = match field_attrs.default {
let default = {
if field_attrs.default {
Some(quote! { Default::default() } )
} else {
match field_attrs.default_expr {
Some(ref def) => match syn::parse_token_trees(def) {
Ok(t) => Some(quote!{ #(#t)* }),
Ok(t) => Some(quote! { #(#t)* }),
Err(_) => {
panic!("#graphql(default = ?) must be a valid Rust expression inside a string");
}
},
None => None,
}
}
};
let create_meta_field = match default {
@ -158,7 +184,7 @@ pub fn impl_input_object(ast: &syn::DeriveInput) -> Tokens {
let from_input_default = match default {
Some(ref def) => {
quote!{
Some(&&::juniper::InputValue::Null) | None if true => #def,
Some(&&_juniper::InputValue::Null) | None if true => #def,
}
}
None => quote!{},
@ -169,8 +195,11 @@ pub fn impl_input_object(ast: &syn::DeriveInput) -> Tokens {
// TODO: investigate the unwraps here, they seem dangerous!
match obj.get(#name) {
#from_input_default
Some(v) => ::juniper::FromInputValue::from_input_value(v).unwrap(),
_ => ::juniper::FromInputValue::from_input_value(&::juniper::InputValue::null()).unwrap()
Some(v) => _juniper::FromInputValue::from_input_value(v).unwrap(),
_ => {
_juniper::FromInputValue::from_input_value(&_juniper::InputValue::null())
.unwrap()
},
}
},
};
@ -183,8 +212,8 @@ pub fn impl_input_object(ast: &syn::DeriveInput) -> Tokens {
to_inputs.push(to_input);
}
quote! {
impl ::juniper::GraphQLType for #ident {
let body = quote! {
impl _juniper::GraphQLType for #ident {
type Context = ();
type TypeInfo = ();
@ -192,7 +221,7 @@ pub fn impl_input_object(ast: &syn::DeriveInput) -> Tokens {
Some(#name)
}
fn meta<'r>(_: &(), registry: &mut ::juniper::Registry<'r>) -> ::juniper::meta::MetaType<'r> {
fn meta<'r>(_: &(), registry: &mut _juniper::Registry<'r>) -> _juniper::meta::MetaType<'r> {
let fields = &[
#(#meta_fields)*
];
@ -202,8 +231,8 @@ pub fn impl_input_object(ast: &syn::DeriveInput) -> Tokens {
}
}
impl ::juniper::FromInputValue for #ident {
fn from_input_value(value: &::juniper::InputValue) -> Option<#ident> {
impl _juniper::FromInputValue for #ident {
fn from_input_value(value: &_juniper::InputValue) -> Option<#ident> {
if let Some(obj) = value.to_object_value() {
let item = #ident {
#(#from_inputs)*
@ -216,12 +245,46 @@ pub fn impl_input_object(ast: &syn::DeriveInput) -> Tokens {
}
}
impl ::juniper::ToInputValue for #ident {
fn to_input_value(&self) -> ::juniper::InputValue {
::juniper::InputValue::object(vec![
impl _juniper::ToInputValue for #ident {
fn to_input_value(&self) -> _juniper::InputValue {
_juniper::InputValue::object(vec![
#(#to_inputs)*
].into_iter().collect())
}
}
};
let dummy_const = Ident::new(format!("_IMPL_GRAPHQLINPUTOBJECT_FOR_{}", ident));
// This ugly hack makes it possible to use the derive inside juniper itself.
// FIXME: Figure out a better way to do this!
let crate_reference = if attrs.internal {
quote! {
#[doc(hidden)]
mod _juniper {
pub use ::{
InputValue,
FromInputValue,
GraphQLType,
Registry,
meta,
ToInputValue
};
}
}
} else {
quote! {
extern crate juniper as _juniper;
}
};
let generated = quote! {
#[allow(non_upper_case_globals, unused_attributes, unused_qualifications)]
#[doc(hidden)]
const #dummy_const : () = {
#crate_reference
#body
};
};
generated
}

View file

@ -1,3 +1,9 @@
//! This crate supplies custom derive implementations for the
//! [juniper](https://github.com/graphql-rust/juniper) crate.
//!
//! You should not depend on juniper_codegen directly.
//! You only need the `juniper` crate.
#![recursion_limit = "1024"]
extern crate proc_macro;

View file

@ -80,3 +80,38 @@ fn test_to_camel_case() {
assert_eq!(&to_camel_case("a")[..], "a");
assert_eq!(&to_camel_case("")[..], "");
}
pub(crate) fn to_upper_snake_case(s: &str) -> String {
let mut last_lower = false;
let mut upper = String::new();
for c in s.chars() {
if c == '_' {
last_lower = false;
}
else if c.is_lowercase() {
last_lower = true;
} else if c.is_uppercase() {
if last_lower {
upper.push('_');
}
last_lower = false;
}
for u in c.to_uppercase() {
upper.push(u);
}
}
upper
}
#[test]
fn test_to_upper_snake_case() {
assert_eq!(to_upper_snake_case("abc"), "ABC");
assert_eq!(to_upper_snake_case("a_bc"), "A_BC");
assert_eq!(to_upper_snake_case("ABC"), "ABC");
assert_eq!(to_upper_snake_case("A_BC"), "A_BC");
assert_eq!(to_upper_snake_case("SomeInput"), "SOME_INPUT");
assert_eq!(to_upper_snake_case("someInput"), "SOME_INPUT");
assert_eq!(to_upper_snake_case("someINpuT"), "SOME_INPU_T");
assert_eq!(to_upper_snake_case("some_INpuT"), "SOME_INPU_T");
}

2
juniper_iron/.gitignore vendored Normal file
View file

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

View file

31
juniper_iron/Cargo.toml Normal file
View file

@ -0,0 +1,31 @@
[package]
name = "juniper_iron"
version = "0.1.0"
authors = ["Magnus Hallin <mhallin@fastmail.com>"]
description = "Iron integration for juniper"
license = "BSD-2-Clause"
documentation = "https://docs.rs/juniper_iron"
repository = "https://github.com/graphql-rust/juniper"
[dependencies]
serde = { version = "1.0.2" }
serde_json = { version = "1.0.2" }
juniper = { version = "0.9.0", path = "../juniper" }
urlencoded = { version = "0.5.0" }
iron = "0.5.1"
[dev-dependencies]
iron-test = "^0.5.0"
router = "^0.5.0"
mount = "^0.3.0"
logger = "^0.3.0"
[dev-dependencies.juniper]
version = "0.9.0"
features = ["expose-test-schema", "serde_json"]
path = "../juniper"
[badges]
travis-ci = { repository = "graphql-rust/juniper_iron" }
appveyor = { repository = "graphql-rust/juniper_iron" }

25
juniper_iron/LICENSE Normal file
View file

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

31
juniper_iron/README.md Normal file
View file

@ -0,0 +1,31 @@
# juniper_iron
This repository contains the [Iron][Iron] web framework integration for
[Juniper][Juniper], a [GraphQL][GraphQL] implementation for Rust.
For documentation, including guides and examples, check out [Juniper][Juniper].
A basic usage example can also be found in the [Api documentation][documentation].
## Examples
Check [examples/iron_server.rs][example] for example code of a working Iron server with GraphQL handlers.
## Links
* [Juniper][Juniper]
* [Api Reference][documetation]
* [Iron framework][Iron]
## License
This project is under the BSD-2 license.
Check the LICENSE file for details.
[Iron]: https://github.com/iron/iron
[Juniper]: https://github.com/graphql-rust/juniper
[GraphQL]: http://graphql.org
[documentation]: https://docs.rs/juniper_iron
[example]: https://github.com/graphql-rust/juniper/blob/master/juniper_iron/examples/iron_server.rs

View file

@ -0,0 +1,43 @@
extern crate iron;
extern crate mount;
extern crate logger;
extern crate serde;
extern crate juniper;
extern crate juniper_iron;
use std::env;
use mount::Mount;
use logger::Logger;
use iron::prelude::*;
use juniper::EmptyMutation;
use juniper_iron::{GraphQLHandler, GraphiQLHandler};
use juniper::tests::model::Database;
fn context_factory(_: &mut Request) -> Database {
Database::new()
}
fn main() {
let mut mount = Mount::new();
let graphql_endpoint = GraphQLHandler::new(
context_factory,
Database::new(),
EmptyMutation::<Database>::new(),
);
let graphiql_endpoint = GraphiQLHandler::new("/graphql");
mount.mount("/", 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 = env::var("LISTEN").unwrap_or("0.0.0.0:8080".to_owned());
println!("GraphQL server started on {}", host);
Iron::new(chain).http(host.as_str()).unwrap();
}

405
juniper_iron/src/lib.rs Normal file
View file

@ -0,0 +1,405 @@
/*!
# juniper_iron
This repository contains the [Iron][Iron] web framework integration for
[Juniper][Juniper], a [GraphQL][GraphQL] implementation for Rust.
For documentation, including guides and examples, check out [Juniper][Juniper].
A basic usage example can also be found in the [Api documentation][documentation].
## Links
* [Juniper][Juniper]
* [Api Reference][documentation]
* [Iron framework][Iron]
## Integrating with Iron
For example, continuing from the schema created above and using Iron to expose
the schema on an HTTP endpoint supporting both GET and POST requests:
```rust,no_run
extern crate iron;
# #[macro_use] extern crate juniper;
# extern crate juniper_iron;
# use std::collections::HashMap;
use iron::prelude::*;
use juniper_iron::GraphQLHandler;
use juniper::{Context, EmptyMutation};
# 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 |&self| {
# field id() -> FieldResult<&String> {
# Ok(&self.id)
# }
#
# field name() -> FieldResult<&String> {
# Ok(&self.name)
# }
#
# field friends(&executor) -> FieldResult<Vec<&User>> {
# Ok(self.friend_ids.iter()
# .filter_map(|id| executor.context().users.get(id))
# .collect())
# }
# });
#
# graphql_object!(QueryRoot: Database |&self| {
# field user(&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()
}
}
impl Context for Database {}
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, EmptyMutation::<Database>::new());
// Start serving the schema at the root on port 8080.
Iron::new(graphql_endpoint).http("localhost:8080").unwrap();
}
```
See the the [`GraphQLHandler`][3] documentation for more information on what request methods are
supported.
[3]: ./struct.GraphQLHandler.html
[Iron]: https://github.com/iron/iron
[Juniper]: https://github.com/graphql-rust/juniper
[GraphQL]: http://graphql.org
[documentation]: https://docs.rs/juniper_iron
*/
extern crate serde_json;
extern crate juniper;
extern crate urlencoded;
#[macro_use]
extern crate iron;
#[cfg(test)]
extern crate iron_test;
use iron::prelude::*;
use iron::middleware::Handler;
use iron::mime::Mime;
use iron::status;
use iron::method;
use urlencoded::{UrlDecodingError, UrlEncodedQuery};
use std::io::Read;
use std::error::Error;
use std::fmt;
use serde_json::error::Error as SerdeError;
use juniper::{GraphQLType, InputValue, RootNode};
use juniper::http;
/// 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<'a, CtxFactory, Query, Mutation, CtxT>
where
CtxFactory: Fn(&mut Request) -> CtxT + Send + Sync + 'static,
CtxT: 'static,
Query: GraphQLType<Context = CtxT> + Send + Sync + 'static,
Mutation: GraphQLType<Context = CtxT> + Send + Sync + 'static,
{
context_factory: CtxFactory,
root_node: RootNode<'a, Query, Mutation>,
}
/// Handler that renders GraphiQL - a graphical query editor interface
pub struct GraphiQLHandler {
graphql_url: String,
}
fn get_single_value<T>(mut values: Vec<T>) -> IronResult<T> {
if values.len() == 1 {
Ok(values.remove(0))
} else {
Err(
GraphQLIronError::InvalidData("Duplicate URL query parameter").into(),
)
}
}
fn parse_url_param(params: Option<Vec<String>>) -> IronResult<Option<String>> {
if let Some(values) = params {
get_single_value(values).map(Some)
} else {
Ok(None)
}
}
fn parse_variable_param(params: Option<Vec<String>>) -> IronResult<Option<InputValue>> {
if let Some(values) = params {
Ok(serde_json::from_str::<InputValue>(
get_single_value(values)?.as_ref(),
).map(Some)
.map_err(GraphQLIronError::Serde)?)
} else {
Ok(None)
}
}
impl<'a, CtxFactory, Query, Mutation, CtxT> GraphQLHandler<'a, CtxFactory, Query, Mutation, CtxT>
where
CtxFactory: Fn(&mut Request) -> CtxT + Send + Sync + 'static,
CtxT: 'static,
Query: GraphQLType<Context = CtxT, TypeInfo=()> + Send + Sync + 'static,
Mutation: GraphQLType<Context = CtxT, TypeInfo=()> + 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<http::GraphQLRequest> {
let url_query_string = req.get_mut::<UrlEncodedQuery>()
.map_err(|e| GraphQLIronError::Url(e))?;
let input_query = parse_url_param(url_query_string.remove("query"))?
.ok_or_else(|| GraphQLIronError::InvalidData("No query provided"))?;
let operation_name = parse_url_param(url_query_string.remove("operationName"))?;
let variables = parse_variable_param(url_query_string.remove("variables"))?;
Ok(http::GraphQLRequest::new(
input_query,
operation_name,
variables,
))
}
fn handle_post(&self, req: &mut Request) -> IronResult<http::GraphQLRequest> {
let mut request_payload = String::new();
itry!(req.body.read_to_string(&mut request_payload));
Ok(serde_json::from_str::<http::GraphQLRequest>(
request_payload.as_str(),
).map_err(|err| GraphQLIronError::Serde(err))?)
}
fn execute(&self, context: &CtxT, request: http::GraphQLRequest) -> IronResult<Response> {
let response = request.execute(&self.root_node, context);
let content_type = "application/json".parse::<Mime>().unwrap();
let json = serde_json::to_string_pretty(&response).unwrap();
let status = if response.is_ok() {
status::Ok
} else {
status::BadRequest
};
Ok(Response::with((content_type, status, json)))
}
}
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<'a, CtxFactory, Query, Mutation, CtxT> Handler
for GraphQLHandler<'a, CtxFactory, Query, Mutation, CtxT>
where
CtxFactory: Fn(&mut Request) -> CtxT + Send + Sync + 'static,
CtxT: 'static,
Query: GraphQLType<Context = CtxT, TypeInfo=()> + Send + Sync + 'static,
Mutation: GraphQLType<Context = CtxT, TypeInfo=()> + Send + Sync + 'static,
'a: 'static,
{
fn handle(&self, mut req: &mut Request) -> IronResult<Response> {
let context = (self.context_factory)(req);
let graphql_request = match req.method {
method::Get => self.handle_get(&mut req)?,
method::Post => self.handle_post(&mut req)?,
_ => return Ok(Response::with((status::MethodNotAllowed))),
};
self.execute(&context, graphql_request)
}
}
impl Handler for GraphiQLHandler {
fn handle(&self, _: &mut Request) -> IronResult<Response> {
let content_type = "text/html".parse::<Mime>().unwrap();
Ok(Response::with((
content_type,
status::Ok,
juniper::graphiql::graphiql_source(&self.graphql_url),
)))
}
}
#[derive(Debug)]
enum GraphQLIronError {
Serde(SerdeError),
Url(UrlDecodingError),
InvalidData(&'static str),
}
impl fmt::Display for GraphQLIronError {
fn fmt(&self, mut f: &mut fmt::Formatter) -> fmt::Result {
match *self {
GraphQLIronError::Serde(ref err) => fmt::Display::fmt(err, &mut f),
GraphQLIronError::Url(ref err) => fmt::Display::fmt(err, &mut f),
GraphQLIronError::InvalidData(ref err) => fmt::Display::fmt(err, &mut f),
}
}
}
impl Error for GraphQLIronError {
fn description(&self) -> &str {
match *self {
GraphQLIronError::Serde(ref err) => err.description(),
GraphQLIronError::Url(ref err) => err.description(),
GraphQLIronError::InvalidData(ref err) => err,
}
}
fn cause(&self) -> Option<&Error> {
match *self {
GraphQLIronError::Serde(ref err) => Some(err),
GraphQLIronError::Url(ref err) => Some(err),
GraphQLIronError::InvalidData(_) => None,
}
}
}
impl From<GraphQLIronError> for IronError {
fn from(err: GraphQLIronError) -> IronError {
let message = format!("{}", err);
IronError::new(err, (status::BadRequest, message))
}
}
#[cfg(test)]
mod tests {
use iron::prelude::*;
use iron_test::{request, response};
use iron::{Handler, Headers};
use juniper::tests::model::Database;
use juniper::http::tests as http_tests;
use juniper::EmptyMutation;
use super::GraphQLHandler;
struct TestIronIntegration;
impl http_tests::HTTPIntegration for TestIronIntegration {
fn get(&self, url: &str) -> http_tests::TestResponse {
make_test_response(request::get(
&("http://localhost:3000".to_owned() + url),
Headers::new(),
&make_handler(),
))
}
fn post(&self, url: &str, body: &str) -> http_tests::TestResponse {
make_test_response(request::post(
&("http://localhost:3000".to_owned() + url),
Headers::new(),
body,
&make_handler(),
))
}
}
#[test]
fn test_iron_integration() {
let integration = TestIronIntegration;
http_tests::run_http_test_suite(&integration);
}
fn context_factory(_: &mut Request) -> Database {
Database::new()
}
fn make_test_response(response: IronResult<Response>) -> http_tests::TestResponse {
let response = response.expect("Error response from GraphQL handler");
let status_code = response
.status
.expect("No status code returned from handler")
.to_u16() as i32;
let content_type = String::from_utf8(
response
.headers
.get_raw("content-type")
.expect("No content type header from handler")[0]
.clone(),
).expect("Content-type header invalid UTF-8");
let body = response::extract_body_to_string(response);
http_tests::TestResponse {
status_code: status_code,
body: Some(body),
content_type: content_type,
}
}
fn make_handler() -> Box<Handler> {
Box::new(GraphQLHandler::new(
context_factory,
Database::new(),
EmptyMutation::<Database>::new(),
))
}
}

2
juniper_rocket/.gitignore vendored Normal file
View file

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

27
juniper_rocket/Cargo.toml Normal file
View file

@ -0,0 +1,27 @@
[package]
name = "juniper_rocket"
version = "0.1.0"
authors = ["Magnus Hallin <mhallin@fastmail.com>"]
description = "Juniper GraphQL integration with Rocket"
license = "BSD-2-Clause"
documentation = "https://docs.rs/juniper_rocket"
repository = "https://github.com/graphql-rust/juniper"
[dependencies]
serde = { version = "1.0.2" }
serde_derive = {version="1.0.2" }
serde_json = { version = "1.0.2" }
juniper = { version = "0.9.0" , path = "../juniper"}
rocket = { version = "0.3.0" }
rocket_codegen = { version = "0.3.0" }
[badges]
travis-ci = { repository = "mhallin/juniper" }
appveyor = { repository = "mhallin/juniper" }
[dev-dependencies.juniper]
version = "0.9.0"
features = ["expose-test-schema", "serde_json"]
path = "../juniper"

25
juniper_rocket/LICENSE Normal file
View file

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

35
juniper_rocket/README.md Normal file
View file

@ -0,0 +1,35 @@
# juniper_rocket
This repository contains the [Rocket][Rocket] web server integration for
[Juniper][Juniper], a [GraphQL][GraphQL] implementation for Rust.
## Documentation
For documentation, including guides and examples, check out [Juniper][Juniper].
A basic usage example can also be found in the [Api documentation][documentation].
## Examples
Check [examples/rocket_server.rs][example] for example code of a working Rocket
server with GraphQL handlers.
## Links
* [Juniper][Juniper]
* [Api Reference][documetation]
* [Rocket][Iron]
## License
This project is under the BSD-2 license.
Check the LICENSE file for details.
[Rocket]: https://rocket.rs
[Juniper]: https://github.com/graphql-rust/juniper
[GraphQL]: http://graphql.org
[documentation]: https://docs.rs/juniper_rocket
[example]: https://github.com/graphql-rust/juniper_rocket/blob/master/examples/rocket_server.rs

View file

@ -0,0 +1,51 @@
#![feature(plugin)]
#![plugin(rocket_codegen)]
extern crate rocket;
extern crate juniper;
extern crate juniper_rocket;
use rocket::response::content;
use rocket::State;
use juniper::tests::model::Database;
use juniper::{EmptyMutation, RootNode};
type Schema = RootNode<'static, Database, EmptyMutation<Database>>;
#[get("/")]
fn graphiql() -> content::Html<String> {
juniper_rocket::graphiql_source("/graphql")
}
#[get("/graphql?<request>")]
fn get_graphql_handler(
context: State<Database>,
request: juniper_rocket::GraphQLRequest,
schema: State<Schema>,
) -> juniper_rocket::GraphQLResponse {
request.execute(&schema, &context)
}
#[post("/graphql", data = "<request>")]
fn post_graphql_handler(
context: State<Database>,
request: juniper_rocket::GraphQLRequest,
schema: State<Schema>,
) -> juniper_rocket::GraphQLResponse {
request.execute(&schema, &context)
}
fn main() {
rocket::ignite()
.manage(Database::new())
.manage(Schema::new(
Database::new(),
EmptyMutation::<Database>::new(),
))
.mount(
"/",
routes![graphiql, get_graphql_handler, post_graphql_handler],
)
.launch();
}

270
juniper_rocket/src/lib.rs Normal file
View file

@ -0,0 +1,270 @@
/*!
# juniper_rocket
This repository contains the [Rocket][Rocket] web server integration for
[Juniper][Juniper], a [GraphQL][GraphQL] implementation for Rust.
## Documentation
For documentation, including guides and examples, check out [Juniper][Juniper].
A basic usage example can also be found in the [Api documentation][documentation].
## Examples
Check [examples/rocket_server.rs][example] for example code of a working Rocket
server with GraphQL handlers.
## Links
* [Juniper][Juniper]
* [Api Reference][documentation]
* [Rocket][Rocket]
## License
This project is under the BSD-2 license.
Check the LICENSE file for details.
[Rocket]: https://rocket.rs
[Juniper]: https://github.com/graphql-rust/juniper
[GraphQL]: http://graphql.org
[documentation]: https://docs.rs/juniper_rocket
[example]: https://github.com/graphql-rust/juniper_rocket/blob/master/examples/rocket_server.rs
*/
#![feature(plugin)]
#![plugin(rocket_codegen)]
extern crate juniper;
extern crate serde_json;
extern crate rocket;
use std::io::{Cursor, Read};
use std::error::Error;
use rocket::Request;
use rocket::request::{FormItems, FromForm};
use rocket::data::{FromData, Outcome as FromDataOutcome};
use rocket::response::{content, Responder, Response};
use rocket::http::{ContentType, Status};
use rocket::Data;
use rocket::Outcome::{Failure, Forward, Success};
use juniper::InputValue;
use juniper::http;
use juniper::GraphQLType;
use juniper::RootNode;
/// Simple wrapper around an incoming GraphQL request
///
/// See the `http` module for more information. This type can be constructed
/// automatically from both GET and POST routes by implementing the `FromForm`
/// and `FromData` traits.
pub struct GraphQLRequest(http::GraphQLRequest);
/// Simple wrapper around the result of executing a GraphQL query
pub struct GraphQLResponse(Status, String);
/// Generate an HTML page containing GraphiQL
pub fn graphiql_source(graphql_endpoint_url: &str) -> content::Html<String> {
content::Html(juniper::graphiql::graphiql_source(graphql_endpoint_url))
}
impl GraphQLRequest {
/// Execute an incoming GraphQL query
pub fn execute<CtxT, QueryT, MutationT>(
&self,
root_node: &RootNode<QueryT, MutationT>,
context: &CtxT,
) -> GraphQLResponse
where
QueryT: GraphQLType<Context = CtxT>,
MutationT: GraphQLType<Context = CtxT>,
{
let response = self.0.execute(root_node, context);
let status = if response.is_ok() {
Status::Ok
} else {
Status::BadRequest
};
let json = serde_json::to_string_pretty(&response).unwrap();
GraphQLResponse(status, json)
}
}
impl<'f> FromForm<'f> for GraphQLRequest {
type Error = String;
fn from_form(form_items: &mut FormItems<'f>, strict: bool) -> Result<Self, String> {
let mut query = None;
let mut operation_name = None;
let mut variables = None;
for (key, value) in form_items {
match key.as_str() {
"query" => if query.is_some() {
return Err("Query parameter must not occur more than once".to_owned());
} else {
query = Some(value.as_str().to_string());
},
"operation_name" => if operation_name.is_some() {
return Err(
"Operation name parameter must not occur more than once".to_owned(),
);
} else {
operation_name = Some(value.as_str().to_string());
},
"variables" => if variables.is_some() {
return Err(
"Variables parameter must not occur more than once".to_owned(),
);
} else {
variables = Some(serde_json::from_str::<InputValue>(value.as_str())
.map_err(|err| err.description().to_owned())?);
},
_ => if strict {
return Err(format!("Prohibited extra field '{}'", key).to_owned());
},
}
}
if let Some(query) = query {
Ok(GraphQLRequest(
http::GraphQLRequest::new(query, operation_name, variables),
))
} else {
Err("Query parameter missing".to_owned())
}
}
}
impl FromData for GraphQLRequest {
type Error = String;
fn from_data(request: &Request, data: Data) -> FromDataOutcome<Self, Self::Error> {
if !request.content_type().map_or(false, |ct| ct.is_json()) {
return Forward(data);
}
let mut body = String::new();
if let Err(e) = data.open().read_to_string(&mut body) {
return Failure((Status::InternalServerError, format!("{:?}", e)));
}
match serde_json::from_str(&body) {
Ok(value) => Success(GraphQLRequest(value)),
Err(failure) => return Failure((Status::BadRequest, format!("{}", failure))),
}
}
}
impl<'r> Responder<'r> for GraphQLResponse {
fn respond_to(self, _: &Request) -> Result<Response<'r>, Status> {
let GraphQLResponse(status, body) = self;
Ok(
Response::build()
.header(ContentType::new("application", "json"))
.status(status)
.sized_body(Cursor::new(body))
.finalize(),
)
}
}
#[cfg(test)]
mod tests {
use rocket;
use rocket::Rocket;
use rocket::http::{ContentType, Method};
use rocket::State;
use juniper::RootNode;
use juniper::tests::model::Database;
use juniper::http::tests as http_tests;
use juniper::EmptyMutation;
type Schema = RootNode<'static, Database, EmptyMutation<Database>>;
#[get("/?<request>")]
fn get_graphql_handler(
context: State<Database>,
request: super::GraphQLRequest,
schema: State<Schema>,
) -> super::GraphQLResponse {
request.execute(&schema, &context)
}
#[post("/", data = "<request>")]
fn post_graphql_handler(
context: State<Database>,
request: super::GraphQLRequest,
schema: State<Schema>,
) -> super::GraphQLResponse {
request.execute(&schema, &context)
}
struct TestRocketIntegration {
rocket: Rocket,
}
/*
impl http_tests::HTTPIntegration for TestRocketIntegration
{
fn get(&self, url: &str) -> http_tests::TestResponse {
make_test_response(&self.rocket, MockRequest::new(
Method::Get,
url))
}
fn post(&self, url: &str, body: &str) -> http_tests::TestResponse {
make_test_response(
&self.rocket,
MockRequest::new(
Method::Post,
url,
).header(ContentType::JSON).body(body))
}
}
#[test]
fn test_rocket_integration() {
let integration = TestRocketIntegration {
rocket: make_rocket(),
};
http_tests::run_http_test_suite(&integration);
}
fn make_rocket() -> Rocket {
rocket::ignite()
.manage(Database::new())
.manage(Schema::new(Database::new(), EmptyMutation::<Database>::new()))
.mount("/", routes![post_graphql_handler, get_graphql_handler])
}
fn make_test_response<'r>(rocket: &'r Rocket, mut request: MockRequest<'r>) -> http_tests::TestResponse {
let mut response = request.dispatch_with(&rocket);
let status_code = response.status().code as i32;
let content_type = response.header_values("content-type").collect::<Vec<_>>().into_iter().next()
.expect("No content type header from handler").to_owned();
let body = response.body().expect("No body returned from GraphQL handler").into_string();
http_tests::TestResponse {
status_code: status_code,
body: body,
content_type: content_type,
}
}
*/
}

View file

@ -3,9 +3,8 @@ name = "juniper_tests"
version = "0.1.0"
[dependencies]
juniper = { path = "../juniper" }
juniper_codegen = { path = "../juniper_codegen" }
serde_json = { version = "^1.0.2" }
juniper = { version = "0.9.0", path = "../juniper" }
serde_json = { version = "1" }
[dev-dependencies]
fnv = "1.0.3"

View file

@ -9,7 +9,7 @@ use juniper::{self, FromInputValue, GraphQLType, InputValue, ToInputValue};
enum SomeEnum {
Regular,
#[graphql(name = "FULL", description = "field descr", deprecated = "depr")]
#[graphql(name = "full", description = "field descr", deprecated = "depr")]
Full,
}
@ -33,9 +33,9 @@ fn test_derived_enum() {
);
// Test FULL variant.
assert_eq!(SomeEnum::Full.to_input_value(), InputValue::String("FULL".into()));
assert_eq!(SomeEnum::Full.to_input_value(), InputValue::String("full".into()));
assert_eq!(
FromInputValue::from_input_value(&InputValue::String("FULL".into())),
FromInputValue::from_input_value(&InputValue::String("full".into())),
Some(SomeEnum::Full)
);
}

View file

@ -2,7 +2,7 @@
use fnv::FnvHashMap;
#[cfg(test)]
use juniper::{self, FromInputValue, GraphQLType, ToInputValue};
use juniper::{self, FromInputValue, GraphQLType, InputValue};
#[derive(GraphQLInputObject, Debug, PartialEq)]
#[graphql(name = "MyInput", description = "input descr")]
@ -10,6 +10,9 @@ struct Input {
regular_field: String,
#[graphql(name = "haha", default = "33", description = "haha descr")]
c: i32,
#[graphql(default)]
other: Option<bool>,
}
#[test]
@ -22,10 +25,31 @@ fn test_derived_input_object() {
assert_eq!(meta.name(), Some("MyInput"));
assert_eq!(meta.description(), Some(&"input descr".to_string()));
let obj = Input {
regular_field: "a".to_string(),
// Test default value injection.
let input_no_defaults: InputValue = ::serde_json::from_value(json!({
"regularField": "a",
})).unwrap();
let output_no_defaults: Input = FromInputValue::from_input_value(&input_no_defaults).unwrap();
assert_eq!(output_no_defaults, Input{
regular_field: "a".into(),
c: 33,
};
let restored: Input = FromInputValue::from_input_value(&obj.to_input_value()).unwrap();
assert_eq!(obj, restored);
other: None,
});
// Test with all values supplied.
let input: InputValue = ::serde_json::from_value(json!({
"regularField": "a",
"haha": 55,
"other": true,
})).unwrap();
let output: Input = FromInputValue::from_input_value(&input).unwrap();
assert_eq!(output, Input{
regular_field: "a".into(),
c: 55,
other: Some(true),
});
}

View file

@ -12,6 +12,11 @@ struct Obj {
c: i32,
}
#[derive(GraphQLObject, Debug, PartialEq)]
struct Nested {
obj: Obj,
}
struct Query;
graphql_object!(Query: () |&self| {
@ -21,6 +26,15 @@ graphql_object!(Query: () |&self| {
c: 22,
}
}
field nested() -> Nested {
Nested{
obj: Obj{
regular_field: false,
c: 333,
}
}
}
});
#[test]
@ -54,3 +68,30 @@ fn test_derived_object() {
].into_iter().collect()),
vec![])));
}
#[test]
fn test_derived_object_nested() {
let doc = r#"
{
nested {
obj {
regularField
renamedField
}
}
}"#;
let schema = RootNode::new(Query, EmptyMutation::<()>::new());
assert_eq!(
execute(doc, None, &schema, &Variables::new(), &()),
Ok((Value::object(vec![
("nested", Value::object(vec![
("obj", Value::object(vec![
("regularField", Value::boolean(false)),
("renamedField", Value::int(333)),
].into_iter().collect())
)].into_iter().collect())),
].into_iter().collect()),
vec![])));
}

View file

@ -1,7 +1,6 @@
#[macro_use]
extern crate juniper;
#[macro_use]
extern crate juniper_codegen;
extern crate serde_json;
#[cfg(test)]