commit
b46951717d
50 changed files with 1668 additions and 1027 deletions
|
@ -8,10 +8,8 @@ rust:
|
||||||
- nightly
|
- nightly
|
||||||
|
|
||||||
# Prevent accidentally breaking older Rust versions
|
# Prevent accidentally breaking older Rust versions
|
||||||
- 1.17.0
|
|
||||||
- 1.18.0
|
|
||||||
- 1.19.0
|
|
||||||
- 1.20.0
|
- 1.20.0
|
||||||
|
- 1.21.0
|
||||||
|
|
||||||
env:
|
env:
|
||||||
global:
|
global:
|
||||||
|
|
|
@ -3,4 +3,6 @@ members = [
|
||||||
"juniper",
|
"juniper",
|
||||||
"juniper_codegen",
|
"juniper_codegen",
|
||||||
"juniper_tests",
|
"juniper_tests",
|
||||||
|
"juniper_iron",
|
||||||
|
"juniper_rocket",
|
||||||
]
|
]
|
||||||
|
|
4
Makefile.toml
Normal file
4
Makefile.toml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
|
||||||
|
[tasks.init]
|
||||||
|
condition = { channels = ["beta", "stable"] }
|
||||||
|
env = { "CARGO_MAKE_WORKSPACE_SKIP_MEMBERS" = "juniper_rocket;juniper_tests" }
|
180
README.md
180
README.md
|
@ -4,7 +4,7 @@
|
||||||
> GraphQL server library for Rust
|
> 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://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)
|
[![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)
|
[![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)
|
[![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
|
[GraphQL][graphql] is a data query language developed by Facebook intended to
|
||||||
serve mobile and web application frontends. Juniper makes it possible to write
|
serve mobile and web application frontends.
|
||||||
GraphQL servers in Rust that are type-safe and blazingly fast.
|
|
||||||
|
*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
|
Juniper does not include a web server - instead it provides building blocks to
|
||||||
make integration with existing servers straightforward. It optionally provides a
|
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)
|
* [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
|
The best place to get started is the [Juniper Book][book], which contains
|
||||||
[dependencies]
|
guides with plenty of examples, covering all features of Juniper. (very much WIP)
|
||||||
juniper = { git = "https://github.com/graphql-rust/juniper" }
|
|
||||||
```
|
|
||||||
|
|
||||||
If you want Iron integration, you need to depend on the `juniper_iron` crate.
|
To get started quickly and get a feel for Juniper, check out the
|
||||||
feature flag:
|
[Quickstart][book_quickstart] section.
|
||||||
|
|
||||||
```toml
|
For specific information about macros, types and the Juniper api, the
|
||||||
[dependencies]
|
[API Reference][docsrs] is the best place to look.
|
||||||
juniper = { git = "https://github.com/graphql-rust/juniper" }
|
|
||||||
juniper_iron = { git = "https://github.com/graphql-rust/juniper_iron" }
|
|
||||||
|
|
||||||
```
|
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
|
## Features
|
||||||
|
|
||||||
Juniper supports the full GraphQL query language according to the
|
Juniper supports the full GraphQL query language according to the
|
||||||
[specification][graphql_spec], including the introspective schema and all
|
[specification][graphql_spec], including interfaces, unions, schema
|
||||||
validations. It does not, however, support the schema language.
|
introspection, and validations.
|
||||||
|
It does not, however, support the schema language.
|
||||||
|
|
||||||
As an exception to other GraphQL libraries for other languages, Juniper builds
|
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
|
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
|
`[Episode!]!`. The corresponding Rust type for e.g. `[Episode]` would be
|
||||||
`Option<Vec<Option<Episode>>>`.
|
`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
|
## API Stability
|
||||||
|
|
||||||
Juniper has not reached 1.0 yet, thus some API instability should be expected.
|
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
|
[graphql]: http://graphql.org
|
||||||
|
[graphiql]: https://github.com/graphql/graphiql
|
||||||
[iron]: http://ironframework.io
|
[iron]: http://ironframework.io
|
||||||
[swschema]: http://graphql.org/docs/typesystem/
|
|
||||||
[graphql_spec]: http://facebook.github.io/graphql
|
[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
|
[tokio]: https://github.com/tokio-rs/tokio
|
||||||
[rocket_examples]: https://github.com/graphql-rust/juniper_rocket/tree/master/examples
|
[rocket_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_rocket/examples
|
||||||
[iron_examples]: https://github.com/graphql-rust/juniper_iron/tree/master/examples
|
[iron_examples]: https://github.com/graphql-rust/juniper/tree/master/juniper_iron/examples
|
||||||
[Rocket]: https://rocket.rs
|
[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
|
||||||
|
|
||||||
|
|
19
appveyor.yml
19
appveyor.yml
|
@ -5,28 +5,16 @@
|
||||||
environment:
|
environment:
|
||||||
matrix:
|
matrix:
|
||||||
# Stable channel
|
# Stable channel
|
||||||
- TARGET: i686-pc-windows-gnu
|
|
||||||
CHANNEL: stable
|
|
||||||
- TARGET: i686-pc-windows-msvc
|
|
||||||
CHANNEL: stable
|
|
||||||
- TARGET: x86_64-pc-windows-gnu
|
- TARGET: x86_64-pc-windows-gnu
|
||||||
CHANNEL: stable
|
CHANNEL: stable
|
||||||
- TARGET: x86_64-pc-windows-msvc
|
- TARGET: x86_64-pc-windows-msvc
|
||||||
CHANNEL: stable
|
CHANNEL: stable
|
||||||
# Beta channel
|
# Beta channel
|
||||||
- TARGET: i686-pc-windows-gnu
|
|
||||||
CHANNEL: beta
|
|
||||||
- TARGET: i686-pc-windows-msvc
|
|
||||||
CHANNEL: beta
|
|
||||||
- TARGET: x86_64-pc-windows-gnu
|
- TARGET: x86_64-pc-windows-gnu
|
||||||
CHANNEL: beta
|
CHANNEL: beta
|
||||||
- TARGET: x86_64-pc-windows-msvc
|
- TARGET: x86_64-pc-windows-msvc
|
||||||
CHANNEL: beta
|
CHANNEL: beta
|
||||||
# Nightly channel
|
# Nightly channel
|
||||||
- TARGET: i686-pc-windows-gnu
|
|
||||||
CHANNEL: nightly
|
|
||||||
- TARGET: i686-pc-windows-msvc
|
|
||||||
CHANNEL: nightly
|
|
||||||
- TARGET: x86_64-pc-windows-gnu
|
- TARGET: x86_64-pc-windows-gnu
|
||||||
CHANNEL: nightly
|
CHANNEL: nightly
|
||||||
- TARGET: x86_64-pc-windows-msvc
|
- TARGET: x86_64-pc-windows-msvc
|
||||||
|
@ -49,4 +37,9 @@ install:
|
||||||
build: false
|
build: false
|
||||||
|
|
||||||
test_script:
|
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
89
changelog/0.9.0.md
Normal 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>
|
|
@ -1,22 +1,6 @@
|
||||||
Change log
|
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
|
## [0.8.1] – 2017-06-15
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "juniper"
|
name = "juniper"
|
||||||
version = "0.8.1"
|
version = "0.9.0"
|
||||||
authors = ["Magnus Hallin <mhallin@fastmail.com>"]
|
authors = ["Magnus Hallin <mhallin@fastmail.com>"]
|
||||||
description = "GraphQL server library"
|
description = "GraphQL server library"
|
||||||
license = "BSD-2-Clause"
|
license = "BSD-2-Clause"
|
||||||
|
@ -22,18 +22,25 @@ path = "benches/bench.rs"
|
||||||
[features]
|
[features]
|
||||||
nightly = []
|
nightly = []
|
||||||
expose-test-schema = []
|
expose-test-schema = []
|
||||||
default = ["chrono", "url", "uuid"]
|
default = [
|
||||||
|
"chrono",
|
||||||
|
"url",
|
||||||
|
"uuid"
|
||||||
|
]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
juniper_codegen = { version = "0.9.0", path = "../juniper_codegen" }
|
||||||
|
|
||||||
fnv = "1.0.3"
|
fnv = "1.0.3"
|
||||||
chrono = { version = "^0.4.0", optional = true }
|
ordermap = { version = "0.2.11", features = ["serde-1"] }
|
||||||
ordermap = { version = "^0.2.11", features = ["serde-1"] }
|
serde = { version = "1.0.8" }
|
||||||
serde = { version = "^1.0.8" }
|
serde_derive = {version="1.0.2" }
|
||||||
serde_derive = {version="^1.0.8" }
|
|
||||||
serde_json = { version="^1.0.2", optional = true }
|
chrono = { version = "0.4.0", optional = true }
|
||||||
url = { version = "^1.5.1", optional = true }
|
serde_json = { version="1.0.2", optional = true }
|
||||||
|
url = { version = "1.5.1", optional = true }
|
||||||
uuid = { version = "0.5.1", optional = true }
|
uuid = { version = "0.5.1", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
bencher = "^0.1.2"
|
bencher = "0.1.2"
|
||||||
serde_json = { version = "^1.0.2" }
|
serde_json = { version = "1.0.2" }
|
||||||
|
|
|
@ -150,9 +150,9 @@ pub type Document<'a> = Vec<Definition<'a>>;
|
||||||
/// Parse an unstructured input value into a Rust data type.
|
/// Parse an unstructured input value into a Rust data type.
|
||||||
///
|
///
|
||||||
/// The conversion _can_ fail, and must in that case return None. Implemented
|
/// The conversion _can_ fail, and must in that case return None. Implemented
|
||||||
/// automatically by the convenience macros `graphql_enum!` and
|
/// automatically by the convenience macro `graphql_scalar!` or by deriving GraphQLEnum.
|
||||||
/// `graphql_scalar!`. Must be implemented manually when manually exposing new
|
///
|
||||||
/// enums or scalars.
|
/// Must be implemented manually when manually exposing new enums or scalars.
|
||||||
pub trait FromInputValue: Sized {
|
pub trait FromInputValue: Sized {
|
||||||
/// Performs the conversion.
|
/// Performs the conversion.
|
||||||
fn from_input_value(v: &InputValue) -> Option<Self>;
|
fn from_input_value(v: &InputValue) -> Option<Self>;
|
||||||
|
|
|
@ -9,7 +9,8 @@ use validation::RuleError;
|
||||||
use parser::SourcePosition;
|
use parser::SourcePosition;
|
||||||
use types::scalars::EmptyMutation;
|
use types::scalars::EmptyMutation;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(GraphQLEnum, Debug)]
|
||||||
|
#[graphql(_internal)]
|
||||||
enum Color {
|
enum Color {
|
||||||
Red,
|
Red,
|
||||||
Green,
|
Green,
|
||||||
|
@ -17,12 +18,6 @@ enum Color {
|
||||||
}
|
}
|
||||||
struct TestType;
|
struct TestType;
|
||||||
|
|
||||||
graphql_enum!(Color {
|
|
||||||
Color::Red => "RED",
|
|
||||||
Color::Green => "GREEN",
|
|
||||||
Color::Blue => "BLUE",
|
|
||||||
});
|
|
||||||
|
|
||||||
graphql_object!(TestType: () |&self| {
|
graphql_object!(TestType: () |&self| {
|
||||||
field to_string(color: Color) -> String {
|
field to_string(color: Color) -> String {
|
||||||
format!("Color::{:?}", color)
|
format!("Color::{:?}", color)
|
||||||
|
|
|
@ -5,34 +5,6 @@ use value::Value;
|
||||||
use schema::model::RootNode;
|
use schema::model::RootNode;
|
||||||
use types::scalars::EmptyMutation;
|
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:
|
Syntax to validate:
|
||||||
|
@ -45,37 +17,53 @@ Syntax to validate:
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
graphql_enum!(DefaultName {
|
#[derive(GraphQLEnum)]
|
||||||
DefaultName::Foo => "FOO",
|
#[graphql(_internal)]
|
||||||
DefaultName::Bar => "BAR",
|
enum DefaultName {
|
||||||
});
|
Foo,
|
||||||
|
Bar,
|
||||||
|
}
|
||||||
|
|
||||||
graphql_enum!(Named as "ANamedEnum" {
|
#[derive(GraphQLEnum)]
|
||||||
Named::Foo => "FOO",
|
#[graphql(name = "ANamedEnum", _internal)]
|
||||||
Named::Bar => "BAR",
|
enum Named {
|
||||||
});
|
Foo,
|
||||||
|
Bar,
|
||||||
|
}
|
||||||
|
|
||||||
graphql_enum!(NoTrailingComma {
|
#[derive(GraphQLEnum)]
|
||||||
NoTrailingComma::Foo => "FOO",
|
#[graphql(_internal)]
|
||||||
NoTrailingComma::Bar => "BAR"
|
enum NoTrailingComma {
|
||||||
});
|
Foo,
|
||||||
|
Bar,
|
||||||
|
}
|
||||||
|
|
||||||
graphql_enum!(EnumDescription {
|
#[derive(GraphQLEnum)]
|
||||||
description: "A description of the enum itself"
|
#[graphql(description = "A description of the enum itself", _internal)]
|
||||||
|
enum EnumDescription {
|
||||||
|
Foo,
|
||||||
|
Bar,
|
||||||
|
}
|
||||||
|
|
||||||
EnumDescription::Foo => "FOO",
|
#[derive(GraphQLEnum)]
|
||||||
EnumDescription::Bar => "BAR",
|
#[graphql(_internal)]
|
||||||
});
|
enum EnumValueDescription {
|
||||||
|
#[graphql(description = "The FOO value")]
|
||||||
|
Foo,
|
||||||
|
#[graphql(description = "The BAR value")]
|
||||||
|
Bar,
|
||||||
|
}
|
||||||
|
|
||||||
graphql_enum!(EnumValueDescription {
|
#[derive(GraphQLEnum)]
|
||||||
EnumValueDescription::Foo => "FOO" as "The FOO value",
|
#[graphql(_internal)]
|
||||||
EnumValueDescription::Bar => "BAR" as "The BAR value",
|
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 {
|
struct Root;
|
||||||
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",
|
|
||||||
});
|
|
||||||
|
|
||||||
graphql_object!(Root: () |&self| {
|
graphql_object!(Root: () |&self| {
|
||||||
field default_name() -> DefaultName { DefaultName::Foo }
|
field default_name() -> DefaultName { DefaultName::Foo }
|
|
@ -8,82 +8,81 @@ use types::scalars::EmptyMutation;
|
||||||
|
|
||||||
struct Root;
|
struct Root;
|
||||||
|
|
||||||
graphql_input_object!(
|
#[derive(GraphQLInputObject)]
|
||||||
struct DefaultName {
|
#[graphql(_internal)]
|
||||||
field_one: String,
|
struct DefaultName {
|
||||||
field_two: String,
|
field_one: String,
|
||||||
}
|
field_two: String,
|
||||||
);
|
}
|
||||||
|
|
||||||
graphql_input_object!(
|
#[derive(GraphQLInputObject)]
|
||||||
struct NoTrailingComma {
|
#[graphql(_internal)]
|
||||||
field_one: String,
|
struct NoTrailingComma {
|
||||||
field_two: String
|
field_one: String,
|
||||||
}
|
field_two: String
|
||||||
);
|
}
|
||||||
|
|
||||||
graphql_input_object!(
|
#[derive(GraphQLInputObject, Debug)]
|
||||||
#[derive(Debug)]
|
#[graphql(_internal)]
|
||||||
struct Derive {
|
struct Derive {
|
||||||
field_one: String,
|
field_one: String,
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
graphql_input_object!(
|
#[derive(GraphQLInputObject, Debug)]
|
||||||
struct Named as "ANamedInputObject" {
|
#[graphql(name = "ANamedInputObject", _internal)]
|
||||||
field_one: String,
|
struct Named {
|
||||||
}
|
field_one: String,
|
||||||
);
|
}
|
||||||
|
|
||||||
graphql_input_object!(
|
#[derive(GraphQLInputObject, Debug)]
|
||||||
description: "Description for the input object"
|
#[graphql(description = "Description for the input object", _internal)]
|
||||||
|
struct Description {
|
||||||
|
field_one: String,
|
||||||
|
}
|
||||||
|
|
||||||
struct Description {
|
#[derive(GraphQLInputObject, Debug)]
|
||||||
field_one: String,
|
#[graphql(_internal)]
|
||||||
}
|
pub struct Public {
|
||||||
);
|
field_one: String,
|
||||||
|
}
|
||||||
|
|
||||||
graphql_input_object!(
|
#[derive(GraphQLInputObject, Debug)]
|
||||||
pub struct Public {
|
#[graphql(description = "Description for the input object", _internal)]
|
||||||
field_one: String,
|
pub struct PublicWithDescription {
|
||||||
}
|
field_one: String,
|
||||||
);
|
}
|
||||||
|
|
||||||
graphql_input_object!(
|
#[derive(GraphQLInputObject, Debug)]
|
||||||
description: "Description for the input object"
|
#[graphql(name = "APublicNamedInputObjectWithDescription",
|
||||||
|
description = "Description for the input object",
|
||||||
|
_internal)]
|
||||||
|
pub struct NamedPublicWithDescription {
|
||||||
|
field_one: String,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct PublicWithDescription {
|
#[derive(GraphQLInputObject, Debug)]
|
||||||
field_one: String,
|
#[graphql(name = "APublicNamedInputObject", _internal)]
|
||||||
}
|
pub struct NamedPublic {
|
||||||
);
|
field_one: String,
|
||||||
|
}
|
||||||
|
|
||||||
graphql_input_object!(
|
#[derive(GraphQLInputObject, Debug)]
|
||||||
description: "Description for the input object"
|
#[graphql(_internal)]
|
||||||
|
struct FieldDescription {
|
||||||
|
#[graphql(description = "The first field")]
|
||||||
|
field_one: String,
|
||||||
|
#[graphql(description = "The second field")]
|
||||||
|
field_two: String,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct NamedPublicWithDescription as "APublicNamedInputObjectWithDescription" {
|
#[derive(GraphQLInputObject, Debug)]
|
||||||
field_one: String,
|
#[graphql(_internal)]
|
||||||
}
|
struct FieldWithDefaults {
|
||||||
);
|
#[graphql(default = "123")]
|
||||||
|
field_one: i32,
|
||||||
graphql_input_object!(
|
#[graphql(default = "456", description = "The second field")]
|
||||||
pub struct NamedPublic as "APublicNamedInputObject" {
|
field_two: i32,
|
||||||
field_one: String,
|
}
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
graphql_input_object!(
|
|
||||||
struct FieldDescription {
|
|
||||||
field_one: String as "The first field",
|
|
||||||
field_two: String as "The second field",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
graphql_input_object!(
|
|
||||||
struct FieldWithDefaults {
|
|
||||||
field_one = 123: i32,
|
|
||||||
field_two = 456: i32 as "The second field",
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
graphql_object!(Root: () |&self| {
|
graphql_object!(Root: () |&self| {
|
||||||
field test_field(
|
field test_field(
|
|
@ -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 executor::Variables;
|
||||||
use value::Value;
|
use value::Value;
|
||||||
use schema::model::RootNode;
|
use schema::model::RootNode;
|
||||||
use types::scalars::EmptyMutation;
|
use types::scalars::EmptyMutation;
|
||||||
|
|
||||||
|
#[derive(GraphQLEnum)]
|
||||||
|
#[graphql(name = "SampleEnum", _internal)]
|
||||||
enum Sample {
|
enum Sample {
|
||||||
One,
|
One,
|
||||||
Two,
|
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| {
|
graphql_interface!(Interface: () as "SampleInterface" |&self| {
|
||||||
description: "A sample interface"
|
description: "A sample interface"
|
||||||
|
|
|
@ -30,39 +30,35 @@ graphql_scalar!(TestComplexScalar {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
#[derive(GraphQLInputObject, Debug)]
|
||||||
|
#[graphql(_internal)]
|
||||||
|
struct TestInputObject {
|
||||||
|
a: Option<String>,
|
||||||
|
b: Option<Vec<Option<String>>>,
|
||||||
|
c: String,
|
||||||
|
d: Option<TestComplexScalar>,
|
||||||
|
}
|
||||||
|
|
||||||
graphql_input_object!(
|
#[derive(GraphQLInputObject, Debug)]
|
||||||
#[derive(Debug)]
|
#[graphql(_internal)]
|
||||||
struct TestInputObject {
|
struct TestNestedInputObject {
|
||||||
a: Option<String>,
|
na: TestInputObject,
|
||||||
b: Option<Vec<Option<String>>>,
|
nb: String,
|
||||||
c: String,
|
}
|
||||||
d: Option<TestComplexScalar>,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
graphql_input_object!(
|
#[derive(GraphQLInputObject, Debug)]
|
||||||
#[derive(Debug)]
|
#[graphql(_internal)]
|
||||||
struct TestNestedInputObject {
|
struct ExampleInputObject {
|
||||||
na: TestInputObject,
|
a: Option<String>,
|
||||||
nb: String,
|
b: i32,
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
graphql_input_object!(
|
#[derive(GraphQLInputObject, Debug)]
|
||||||
#[derive(Debug)]
|
#[graphql(_internal)]
|
||||||
struct ExampleInputObject {
|
struct InputWithDefaults {
|
||||||
a: Option<String>,
|
#[graphql(default = "123")]
|
||||||
b: i32,
|
a: i32,
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
graphql_input_object!(
|
|
||||||
#[derive(Debug)]
|
|
||||||
struct InputWithDefaults {
|
|
||||||
a = 123: i32,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
graphql_object!(TestType: () |&self| {
|
graphql_object!(TestType: () |&self| {
|
||||||
field field_with_object_input(input: Option<TestInputObject>) -> String {
|
field field_with_object_input(input: Option<TestInputObject>) -> String {
|
||||||
|
|
|
@ -2,117 +2,90 @@
|
||||||
|
|
||||||
# GraphQL
|
# GraphQL
|
||||||
|
|
||||||
[GraphQL][1] is a data query language developed by Facebook intended to serve
|
[GraphQL][graphql] is a data query language developed by Facebook intended to
|
||||||
mobile and web application frontends. A server provides a schema, containing
|
serve mobile and web application frontends.
|
||||||
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
|
*Juniper* makes it possible to write GraphQL servers in Rust that are
|
||||||
schema, as well as an optional integration into the [Iron framework][Iron] and
|
type-safe and blazingly fast. We also try to make declaring and resolving
|
||||||
[Rocket]. It tries to keep the number of dynamic operations to a minimum, and
|
GraphQL schemas as convenient as possible as Rust will allow.
|
||||||
give you as the schema developer the control of the query execution path.
|
|
||||||
|
|
||||||
Juniper only depends on `serde` and `serde_derive` by default, making it
|
Juniper does not include a web server - instead it provides building blocks to
|
||||||
lightweight and easy to drop into any project.
|
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
|
## Getting Started
|
||||||
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
|
The best place to get started is the [Juniper Book][book], which contains
|
||||||
#[macro_use] extern crate juniper;
|
guides with plenty of examples, covering all features of Juniper.
|
||||||
# use std::collections::HashMap;
|
|
||||||
use juniper::{Context, FieldResult};
|
|
||||||
|
|
||||||
struct User { id: String, name: String, friend_ids: Vec<String> }
|
To get started quickly and get a feel for Juniper, check out the
|
||||||
struct QueryRoot;
|
[Quickstart][book_quickstart] section.
|
||||||
struct Database { users: HashMap<String, User> }
|
|
||||||
|
|
||||||
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
|
You can also check out [src/tests/schema.rs][test_schema_rs] to see a complex
|
||||||
// object to provide e.g. database access to the field accessors. This object
|
schema including polymorphism with traits and interfaces.
|
||||||
// must implement the `Context` trait. If you don't need a context, use the
|
For an example of web framework integration,
|
||||||
// empty tuple `()` to indicate this.
|
see the [rocket][rocket_examples] and [iron][iron_examples] examples folders.
|
||||||
//
|
|
||||||
// In this example, we use the Database struct as our context.
|
|
||||||
graphql_object!(User: Database |&self| {
|
|
||||||
|
|
||||||
// Expose a simple field as a GraphQL string.
|
|
||||||
field id() -> &String {
|
|
||||||
&self.id
|
|
||||||
}
|
|
||||||
|
|
||||||
field name() -> &String {
|
## Features
|
||||||
&self.name
|
|
||||||
}
|
|
||||||
|
|
||||||
// FieldResult<T> is an alias for Result<T, FieldError>, which can be
|
Juniper supports the full GraphQL query language according to the
|
||||||
// converted to from anything that implements std::fmt::Display - simply
|
[specification][graphql_spec], including interfaces, unions, schema
|
||||||
// return an error with a string using the ? operator from this method and
|
introspection, and validations.
|
||||||
// it will be correctly inserted into the execution response.
|
It does not, however, support the schema language.
|
||||||
field secret() -> FieldResult<&String> {
|
|
||||||
Err("Can't touch this".to_owned())?
|
|
||||||
}
|
|
||||||
|
|
||||||
// Field accessors can optionally take an "executor" as their first
|
As an exception to other GraphQL libraries for other languages, Juniper builds
|
||||||
// argument. This object can help guide query execution and provides
|
non-null types by default. A field of type `Vec<Episode>` will be converted into
|
||||||
// access to the context instance.
|
`[Episode!]!`. The corresponding Rust type for e.g. `[Episode]` would be
|
||||||
//
|
`Option<Vec<Option<Episode>>>`.
|
||||||
// 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()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// The context object is passed down to all referenced types - all your exposed
|
## Integrations
|
||||||
// types need to have the same context type.
|
|
||||||
graphql_object!(QueryRoot: Database |&self| {
|
|
||||||
|
|
||||||
// Arguments work just like they do on functions.
|
### Data types
|
||||||
field user(&executor, id: String) -> Option<&User> {
|
|
||||||
executor.context().users.get(&id)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
# 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
|
* [uuid][uuid]
|
||||||
this macro. For more in-depth information on how to expose fields and types, see
|
* [url][url]
|
||||||
the [`graphql_object!`][3] macro.
|
* [chrono][chrono]
|
||||||
|
|
||||||
### Built-in object type integrations
|
### Web Frameworks
|
||||||
|
|
||||||
Juniper has [built-in integrations][object_integrations] for converting existing object types to
|
* [rocket][rocket]
|
||||||
GraphQL objects for popular crates.
|
* [iron][iron]
|
||||||
|
|
||||||
## Integrating with web servers
|
|
||||||
|
|
||||||
The most obvious usecase is to expose the GraphQL schema over an HTTP endpoint.
|
## API Stability
|
||||||
To support this, Juniper offers additional crates that integrate with popular web frameworks.
|
|
||||||
|
|
||||||
* [juniper_iron][juniper_iron]: Handlers for [Iron][Iron]
|
Juniper has not reached 1.0 yet, thus some API instability should be expected.
|
||||||
* [juniper_rocket][juniper_rocket]: Handlers for [Rocket][Rocket]
|
|
||||||
|
|
||||||
[1]: http://graphql.org
|
[graphql]: http://graphql.org
|
||||||
[3]: macro.graphql_object!.html
|
[graphiql]: https://github.com/graphql/graphiql
|
||||||
[Iron]: http://ironframework.io
|
[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
|
[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)]
|
#![warn(missing_docs)]
|
||||||
|
@ -136,7 +109,13 @@ extern crate url;
|
||||||
#[cfg(any(test, feature = "uuid"))]
|
#[cfg(any(test, feature = "uuid"))]
|
||||||
extern crate 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]
|
#[macro_use]
|
||||||
mod value;
|
mod value;
|
||||||
|
@ -147,6 +126,7 @@ pub mod parser;
|
||||||
mod types;
|
mod types;
|
||||||
mod schema;
|
mod schema;
|
||||||
mod validation;
|
mod validation;
|
||||||
|
mod util;
|
||||||
mod executor;
|
mod executor;
|
||||||
// This needs to be public until docs have support for private modules:
|
// This needs to be public until docs have support for private modules:
|
||||||
// https://github.com/rust-lang/cargo/issues/1520
|
// https://github.com/rust-lang/cargo/issues/1520
|
||||||
|
@ -164,6 +144,9 @@ pub mod tests;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod executor_tests;
|
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 parser::{parse_document_source, ParseError, Spanning};
|
||||||
use validation::{validate_input_values, visit_all_rules, ValidatorContext};
|
use validation::{validate_input_values, visit_all_rules, ValidatorContext};
|
||||||
use executor::execute_validated_query;
|
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("")[..], "");
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)* );
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -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)*
|
|
||||||
);
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -1,6 +1,4 @@
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod enums;
|
|
||||||
#[macro_use]
|
|
||||||
mod object;
|
mod object;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod interface;
|
mod interface;
|
||||||
|
@ -11,8 +9,6 @@ mod args;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod field;
|
mod field;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
mod input_object;
|
|
||||||
#[macro_use]
|
|
||||||
mod union;
|
mod union;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
|
@ -19,12 +19,11 @@ Syntax to validate:
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
graphql_input_object!(
|
#[derive(GraphQLInputObject)]
|
||||||
#[derive(Debug)]
|
#[graphql(_internal)]
|
||||||
struct Point {
|
struct Point {
|
||||||
x: i32,
|
x: i32,
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
|
||||||
graphql_object!(Root: () |&self| {
|
graphql_object!(Root: () |&self| {
|
||||||
field simple() -> i32 { 0 }
|
field simple() -> i32 { 0 }
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
mod enums;
|
|
||||||
mod scalar;
|
mod scalar;
|
||||||
#[allow(dead_code)]
|
|
||||||
mod input_object;
|
|
||||||
mod args;
|
mod args;
|
||||||
mod field;
|
mod field;
|
||||||
mod object;
|
mod object;
|
||||||
|
@ -9,6 +6,4 @@ mod interface;
|
||||||
mod union;
|
mod union;
|
||||||
|
|
||||||
|
|
||||||
// This asserts that the input objects defined public actually became public
|
|
||||||
#[allow(unused_imports)]
|
|
||||||
use self::input_object::{NamedPublic, NamedPublicWithDescription};
|
|
||||||
|
|
|
@ -49,13 +49,17 @@ pub struct DirectiveType<'a> {
|
||||||
pub arguments: Vec<Argument<'a>>,
|
pub arguments: Vec<Argument<'a>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
#[derive(GraphQLEnum, Clone, PartialEq, Eq, Debug)]
|
||||||
|
#[graphql(name = "__DirectiveLocation", _internal)]
|
||||||
pub enum DirectiveLocation {
|
pub enum DirectiveLocation {
|
||||||
Query,
|
Query,
|
||||||
Mutation,
|
Mutation,
|
||||||
Field,
|
Field,
|
||||||
|
#[graphql(name = "FRAGMENT_DEFINITION")]
|
||||||
FragmentDefinition,
|
FragmentDefinition,
|
||||||
|
#[graphql(name = "FRAGMENT_SPREAD")]
|
||||||
FragmentSpread,
|
FragmentSpread,
|
||||||
|
#[graphql(name = "INLINE_SPREAD")]
|
||||||
InlineFragment,
|
InlineFragment,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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| {
|
graphql_object!(<'a> DirectiveType<'a>: SchemaType<'a> as "__Directive" |&self| {
|
||||||
field name() -> &String {
|
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",
|
|
||||||
});
|
|
||||||
|
|
|
@ -2,8 +2,10 @@
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
|
#[derive(GraphQLEnum, Copy, Clone, Eq, PartialEq, Debug)]
|
||||||
|
#[graphql(_internal)]
|
||||||
pub enum Episode {
|
pub enum Episode {
|
||||||
|
#[graphql(name = "NEW_HOPE")]
|
||||||
NewHope,
|
NewHope,
|
||||||
Empire,
|
Empire,
|
||||||
Jedi,
|
Jedi,
|
||||||
|
|
|
@ -3,12 +3,6 @@ use executor::Context;
|
||||||
|
|
||||||
impl Context for Database {}
|
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| {
|
graphql_interface!(<'a> &'a Character: Database as "Character" |&self| {
|
||||||
description: "A character in the Star Wars Trilogy"
|
description: "A character in the Star Wars Trilogy"
|
||||||
|
|
||||||
|
|
|
@ -13,7 +13,9 @@ use parser::Spanning;
|
||||||
///
|
///
|
||||||
/// The GraphQL specification defines a number of type kinds - the meta type
|
/// The GraphQL specification defines a number of type kinds - the meta type
|
||||||
/// of a 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 {
|
pub enum TypeKind {
|
||||||
/// ## Scalar types
|
/// ## Scalar types
|
||||||
///
|
///
|
||||||
|
@ -48,6 +50,7 @@ pub enum TypeKind {
|
||||||
/// ## Input objects
|
/// ## Input objects
|
||||||
///
|
///
|
||||||
/// Represents complex values provided in queries _into_ the system.
|
/// Represents complex values provided in queries _into_ the system.
|
||||||
|
#[graphql(name = "INPUT_OBJECT")]
|
||||||
InputObject,
|
InputObject,
|
||||||
|
|
||||||
/// ## List types
|
/// ## List types
|
||||||
|
@ -61,6 +64,7 @@ pub enum TypeKind {
|
||||||
///
|
///
|
||||||
/// In GraphQL, nullable types are the default. By putting a `!` after a
|
/// In GraphQL, nullable types are the default. By putting a `!` after a
|
||||||
/// type, it becomes non-nullable.
|
/// type, it becomes non-nullable.
|
||||||
|
#[graphql(name = "NON_NULL")]
|
||||||
NonNull,
|
NonNull,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
42
juniper/src/util.rs
Normal file
42
juniper/src/util.rs
Normal 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("")[..], "");
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "juniper_codegen"
|
name = "juniper_codegen"
|
||||||
version = "0.8.1"
|
version = "0.9.0"
|
||||||
authors = ["Magnus Hallin <mhallin@fastmail.com>"]
|
authors = ["Magnus Hallin <mhallin@fastmail.com>"]
|
||||||
description = "Internal custom derive trait for Juniper GraphQL"
|
description = "Internal custom derive trait for Juniper GraphQL"
|
||||||
license = "BSD-2-Clause"
|
license = "BSD-2-Clause"
|
||||||
|
@ -11,8 +11,8 @@ repository = "https://github.com/graphql-rust/juniper"
|
||||||
proc-macro = true
|
proc-macro = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
syn = "0.11.11"
|
syn = "0.11"
|
||||||
quote = "0.3.15"
|
quote = "0.3"
|
||||||
|
|
||||||
[badges]
|
[badges]
|
||||||
travis-ci = { repository = "graphql-rust/juniper" }
|
travis-ci = { repository = "graphql-rust/juniper" }
|
||||||
|
|
|
@ -9,11 +9,17 @@ use util::*;
|
||||||
struct EnumAttrs {
|
struct EnumAttrs {
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
|
internal: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl EnumAttrs {
|
impl EnumAttrs {
|
||||||
fn from_input(input: &DeriveInput) -> 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.
|
// Check attributes for name and description.
|
||||||
if let Some(items) = get_graphl_attr(&input.attrs) {
|
if let Some(items) = get_graphl_attr(&input.attrs) {
|
||||||
|
@ -26,6 +32,15 @@ impl EnumAttrs {
|
||||||
res.description = Some(val);
|
res.description = Some(val);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
match item {
|
||||||
|
&NestedMetaItem::MetaItem(MetaItem::Word(ref ident)) => {
|
||||||
|
if ident == "_internal" {
|
||||||
|
res.internal = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
panic!(format!(
|
panic!(format!(
|
||||||
"Unknown attribute for #[derive(GraphQLEnum)]: {:?}",
|
"Unknown attribute for #[derive(GraphQLEnum)]: {:?}",
|
||||||
item
|
item
|
||||||
|
@ -109,7 +124,7 @@ pub fn impl_enum(ast: &syn::DeriveInput) -> Tokens {
|
||||||
// Build value.
|
// Build value.
|
||||||
let name = var_attrs
|
let name = var_attrs
|
||||||
.name
|
.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 {
|
let descr = match var_attrs.description {
|
||||||
Some(s) => quote!{ Some(#s.to_string()) },
|
Some(s) => quote!{ Some(#s.to_string()) },
|
||||||
None => quote!{ None },
|
None => quote!{ None },
|
||||||
|
@ -119,7 +134,7 @@ pub fn impl_enum(ast: &syn::DeriveInput) -> Tokens {
|
||||||
None => quote!{ None },
|
None => quote!{ None },
|
||||||
};
|
};
|
||||||
let value = quote!{
|
let value = quote!{
|
||||||
::juniper::meta::EnumValue{
|
_juniper::meta::EnumValue{
|
||||||
name: #name.to_string(),
|
name: #name.to_string(),
|
||||||
description: #descr,
|
description: #descr,
|
||||||
deprecation_reason: #depr,
|
deprecation_reason: #depr,
|
||||||
|
@ -129,7 +144,7 @@ pub fn impl_enum(ast: &syn::DeriveInput) -> Tokens {
|
||||||
|
|
||||||
// Build resolve match clause.
|
// Build resolve match clause.
|
||||||
let resolve = quote!{
|
let resolve = quote!{
|
||||||
&#ident::#var_ident => ::juniper::Value::String(#name.to_string()),
|
&#ident::#var_ident => _juniper::Value::String(#name.to_string()),
|
||||||
};
|
};
|
||||||
resolves.push(resolve);
|
resolves.push(resolve);
|
||||||
|
|
||||||
|
@ -142,13 +157,13 @@ pub fn impl_enum(ast: &syn::DeriveInput) -> Tokens {
|
||||||
// Buil to_input clause.
|
// Buil to_input clause.
|
||||||
let to_input = quote!{
|
let to_input = quote!{
|
||||||
&#ident::#var_ident =>
|
&#ident::#var_ident =>
|
||||||
::juniper::InputValue::string(#name.to_string()),
|
_juniper::InputValue::string(#name.to_string()),
|
||||||
};
|
};
|
||||||
to_inputs.push(to_input);
|
to_inputs.push(to_input);
|
||||||
}
|
}
|
||||||
|
|
||||||
quote! {
|
let body = quote! {
|
||||||
impl ::juniper::GraphQLType for #ident {
|
impl _juniper::GraphQLType for #ident {
|
||||||
type Context = ();
|
type Context = ();
|
||||||
type TypeInfo = ();
|
type TypeInfo = ();
|
||||||
|
|
||||||
|
@ -156,7 +171,7 @@ pub fn impl_enum(ast: &syn::DeriveInput) -> Tokens {
|
||||||
Some(#name)
|
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>(&(), &[
|
let meta = registry.build_enum_type::<#ident>(&(), &[
|
||||||
#(#values)*
|
#(#values)*
|
||||||
]);
|
]);
|
||||||
|
@ -164,15 +179,15 @@ pub fn impl_enum(ast: &syn::DeriveInput) -> Tokens {
|
||||||
meta.into_meta()
|
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 {
|
match self {
|
||||||
#(#resolves)*
|
#(#resolves)*
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ::juniper::FromInputValue for #ident {
|
impl _juniper::FromInputValue for #ident {
|
||||||
fn from_input_value(v: &::juniper::InputValue) -> Option<#ident> {
|
fn from_input_value(v: &_juniper::InputValue) -> Option<#ident> {
|
||||||
match v.as_enum_value().or_else(|| v.as_string_value()) {
|
match v.as_enum_value().or_else(|| v.as_string_value()) {
|
||||||
#(#from_inputs)*
|
#(#from_inputs)*
|
||||||
_ => None,
|
_ => None,
|
||||||
|
@ -180,12 +195,49 @@ pub fn impl_enum(ast: &syn::DeriveInput) -> Tokens {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ::juniper::ToInputValue for #ident {
|
impl _juniper::ToInputValue for #ident {
|
||||||
fn to_input_value(&self) -> ::juniper::InputValue {
|
fn to_input_value(&self) -> _juniper::InputValue {
|
||||||
match self {
|
match self {
|
||||||
#(#to_inputs)*
|
#(#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
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ use util::*;
|
||||||
struct ObjAttrs {
|
struct ObjAttrs {
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
|
internal: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ObjAttrs {
|
impl ObjAttrs {
|
||||||
|
@ -26,6 +27,15 @@ impl ObjAttrs {
|
||||||
res.description = Some(val);
|
res.description = Some(val);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
match item {
|
||||||
|
&NestedMetaItem::MetaItem(MetaItem::Word(ref ident)) => {
|
||||||
|
if ident == "_internal" {
|
||||||
|
res.internal = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
panic!(format!(
|
panic!(format!(
|
||||||
"Unknown attribute for #[derive(GraphQLInputObject)]: {:?}",
|
"Unknown attribute for #[derive(GraphQLInputObject)]: {:?}",
|
||||||
item
|
item
|
||||||
|
@ -40,7 +50,8 @@ impl ObjAttrs {
|
||||||
struct ObjFieldAttrs {
|
struct ObjFieldAttrs {
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
default: Option<String>,
|
default: bool,
|
||||||
|
default_expr: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ObjFieldAttrs {
|
impl ObjFieldAttrs {
|
||||||
|
@ -59,9 +70,18 @@ impl ObjFieldAttrs {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(val) = keyed_item_value(item, "default", true) {
|
if let Some(val) = keyed_item_value(item, "default", true) {
|
||||||
res.default = Some(val);
|
res.default_expr = Some(val);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
match item {
|
||||||
|
&NestedMetaItem::MetaItem(MetaItem::Word(ref ident)) => {
|
||||||
|
if ident == "default" {
|
||||||
|
res.default = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {},
|
||||||
|
}
|
||||||
panic!(format!(
|
panic!(format!(
|
||||||
"Unknown attribute for #[derive(GraphQLInputObject)]: {:?}",
|
"Unknown attribute for #[derive(GraphQLInputObject)]: {:?}",
|
||||||
item
|
item
|
||||||
|
@ -122,14 +142,20 @@ pub fn impl_input_object(ast: &syn::DeriveInput) -> Tokens {
|
||||||
None => quote!{ let field = field; },
|
None => quote!{ let field = field; },
|
||||||
};
|
};
|
||||||
|
|
||||||
let default = match field_attrs.default {
|
let default = {
|
||||||
Some(ref def) => match syn::parse_token_trees(def) {
|
if field_attrs.default {
|
||||||
Ok(t) => Some(quote!{ #(#t)* }),
|
Some(quote! { Default::default() } )
|
||||||
Err(_) => {
|
} else {
|
||||||
panic!("#graphql(default = ?) must be a valid Rust expression inside a string");
|
match field_attrs.default_expr {
|
||||||
|
Some(ref def) => match syn::parse_token_trees(def) {
|
||||||
|
Ok(t) => Some(quote! { #(#t)* }),
|
||||||
|
Err(_) => {
|
||||||
|
panic!("#graphql(default = ?) must be a valid Rust expression inside a string");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
None => None,
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
None => None,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let create_meta_field = match default {
|
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 {
|
let from_input_default = match default {
|
||||||
Some(ref def) => {
|
Some(ref def) => {
|
||||||
quote!{
|
quote!{
|
||||||
Some(&&::juniper::InputValue::Null) | None if true => #def,
|
Some(&&_juniper::InputValue::Null) | None if true => #def,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => quote!{},
|
None => quote!{},
|
||||||
|
@ -169,8 +195,11 @@ pub fn impl_input_object(ast: &syn::DeriveInput) -> Tokens {
|
||||||
// TODO: investigate the unwraps here, they seem dangerous!
|
// TODO: investigate the unwraps here, they seem dangerous!
|
||||||
match obj.get(#name) {
|
match obj.get(#name) {
|
||||||
#from_input_default
|
#from_input_default
|
||||||
Some(v) => ::juniper::FromInputValue::from_input_value(v).unwrap(),
|
Some(v) => _juniper::FromInputValue::from_input_value(v).unwrap(),
|
||||||
_ => ::juniper::FromInputValue::from_input_value(&::juniper::InputValue::null()).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);
|
to_inputs.push(to_input);
|
||||||
}
|
}
|
||||||
|
|
||||||
quote! {
|
let body = quote! {
|
||||||
impl ::juniper::GraphQLType for #ident {
|
impl _juniper::GraphQLType for #ident {
|
||||||
type Context = ();
|
type Context = ();
|
||||||
type TypeInfo = ();
|
type TypeInfo = ();
|
||||||
|
|
||||||
|
@ -192,7 +221,7 @@ pub fn impl_input_object(ast: &syn::DeriveInput) -> Tokens {
|
||||||
Some(#name)
|
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 = &[
|
let fields = &[
|
||||||
#(#meta_fields)*
|
#(#meta_fields)*
|
||||||
];
|
];
|
||||||
|
@ -202,8 +231,8 @@ pub fn impl_input_object(ast: &syn::DeriveInput) -> Tokens {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ::juniper::FromInputValue for #ident {
|
impl _juniper::FromInputValue for #ident {
|
||||||
fn from_input_value(value: &::juniper::InputValue) -> Option<#ident> {
|
fn from_input_value(value: &_juniper::InputValue) -> Option<#ident> {
|
||||||
if let Some(obj) = value.to_object_value() {
|
if let Some(obj) = value.to_object_value() {
|
||||||
let item = #ident {
|
let item = #ident {
|
||||||
#(#from_inputs)*
|
#(#from_inputs)*
|
||||||
|
@ -216,12 +245,46 @@ pub fn impl_input_object(ast: &syn::DeriveInput) -> Tokens {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ::juniper::ToInputValue for #ident {
|
impl _juniper::ToInputValue for #ident {
|
||||||
fn to_input_value(&self) -> ::juniper::InputValue {
|
fn to_input_value(&self) -> _juniper::InputValue {
|
||||||
::juniper::InputValue::object(vec![
|
_juniper::InputValue::object(vec![
|
||||||
#(#to_inputs)*
|
#(#to_inputs)*
|
||||||
].into_iter().collect())
|
].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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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"]
|
#![recursion_limit = "1024"]
|
||||||
|
|
||||||
extern crate proc_macro;
|
extern crate proc_macro;
|
||||||
|
|
|
@ -80,3 +80,38 @@ fn test_to_camel_case() {
|
||||||
assert_eq!(&to_camel_case("a")[..], "a");
|
assert_eq!(&to_camel_case("a")[..], "a");
|
||||||
assert_eq!(&to_camel_case("")[..], "");
|
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
2
juniper_iron/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
target
|
||||||
|
Cargo.lock
|
0
juniper_iron/CHANGELOG.md
Normal file
0
juniper_iron/CHANGELOG.md
Normal file
31
juniper_iron/Cargo.toml
Normal file
31
juniper_iron/Cargo.toml
Normal 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
25
juniper_iron/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.
|
31
juniper_iron/README.md
Normal file
31
juniper_iron/README.md
Normal 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
|
||||||
|
|
43
juniper_iron/examples/iron_server.rs
Normal file
43
juniper_iron/examples/iron_server.rs
Normal 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
405
juniper_iron/src/lib.rs
Normal 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
2
juniper_rocket/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
target
|
||||||
|
Cargo.lock
|
27
juniper_rocket/Cargo.toml
Normal file
27
juniper_rocket/Cargo.toml
Normal 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
25
juniper_rocket/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.
|
35
juniper_rocket/README.md
Normal file
35
juniper_rocket/README.md
Normal 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
|
||||||
|
|
||||||
|
|
51
juniper_rocket/examples/rocket_server.rs
Normal file
51
juniper_rocket/examples/rocket_server.rs
Normal 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
270
juniper_rocket/src/lib.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*/
|
||||||
|
}
|
|
@ -3,9 +3,8 @@ name = "juniper_tests"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
juniper = { path = "../juniper" }
|
juniper = { version = "0.9.0", path = "../juniper" }
|
||||||
juniper_codegen = { path = "../juniper_codegen" }
|
serde_json = { version = "1" }
|
||||||
serde_json = { version = "^1.0.2" }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
fnv = "1.0.3"
|
fnv = "1.0.3"
|
||||||
|
|
|
@ -9,7 +9,7 @@ use juniper::{self, FromInputValue, GraphQLType, InputValue, ToInputValue};
|
||||||
enum SomeEnum {
|
enum SomeEnum {
|
||||||
Regular,
|
Regular,
|
||||||
|
|
||||||
#[graphql(name = "FULL", description = "field descr", deprecated = "depr")]
|
#[graphql(name = "full", description = "field descr", deprecated = "depr")]
|
||||||
Full,
|
Full,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,9 +33,9 @@ fn test_derived_enum() {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Test FULL variant.
|
// 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!(
|
assert_eq!(
|
||||||
FromInputValue::from_input_value(&InputValue::String("FULL".into())),
|
FromInputValue::from_input_value(&InputValue::String("full".into())),
|
||||||
Some(SomeEnum::Full)
|
Some(SomeEnum::Full)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
use fnv::FnvHashMap;
|
use fnv::FnvHashMap;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use juniper::{self, FromInputValue, GraphQLType, ToInputValue};
|
use juniper::{self, FromInputValue, GraphQLType, InputValue};
|
||||||
|
|
||||||
#[derive(GraphQLInputObject, Debug, PartialEq)]
|
#[derive(GraphQLInputObject, Debug, PartialEq)]
|
||||||
#[graphql(name = "MyInput", description = "input descr")]
|
#[graphql(name = "MyInput", description = "input descr")]
|
||||||
|
@ -10,6 +10,9 @@ struct Input {
|
||||||
regular_field: String,
|
regular_field: String,
|
||||||
#[graphql(name = "haha", default = "33", description = "haha descr")]
|
#[graphql(name = "haha", default = "33", description = "haha descr")]
|
||||||
c: i32,
|
c: i32,
|
||||||
|
|
||||||
|
#[graphql(default)]
|
||||||
|
other: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -22,10 +25,31 @@ fn test_derived_input_object() {
|
||||||
assert_eq!(meta.name(), Some("MyInput"));
|
assert_eq!(meta.name(), Some("MyInput"));
|
||||||
assert_eq!(meta.description(), Some(&"input descr".to_string()));
|
assert_eq!(meta.description(), Some(&"input descr".to_string()));
|
||||||
|
|
||||||
let obj = Input {
|
// Test default value injection.
|
||||||
regular_field: "a".to_string(),
|
|
||||||
|
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,
|
c: 33,
|
||||||
};
|
other: None,
|
||||||
let restored: Input = FromInputValue::from_input_value(&obj.to_input_value()).unwrap();
|
});
|
||||||
assert_eq!(obj, restored);
|
|
||||||
|
// 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),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,6 +12,11 @@ struct Obj {
|
||||||
c: i32,
|
c: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(GraphQLObject, Debug, PartialEq)]
|
||||||
|
struct Nested {
|
||||||
|
obj: Obj,
|
||||||
|
}
|
||||||
|
|
||||||
struct Query;
|
struct Query;
|
||||||
|
|
||||||
graphql_object!(Query: () |&self| {
|
graphql_object!(Query: () |&self| {
|
||||||
|
@ -21,6 +26,15 @@ graphql_object!(Query: () |&self| {
|
||||||
c: 22,
|
c: 22,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
field nested() -> Nested {
|
||||||
|
Nested{
|
||||||
|
obj: Obj{
|
||||||
|
regular_field: false,
|
||||||
|
c: 333,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -54,3 +68,30 @@ fn test_derived_object() {
|
||||||
].into_iter().collect()),
|
].into_iter().collect()),
|
||||||
vec![])));
|
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![])));
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate juniper;
|
extern crate juniper;
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate juniper_codegen;
|
|
||||||
extern crate serde_json;
|
extern crate serde_json;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
Loading…
Reference in a new issue