Merge pull request #333 from theduke/graphql-object-proc-macro

Graphql object proc macro
This commit is contained in:
theduke 2019-05-14 08:46:56 +02:00 committed by GitHub
commit 61f288b54c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
60 changed files with 2788 additions and 1023 deletions

View file

@ -5,9 +5,9 @@ rust:
- beta
- nightly
# TODO: re-enable once new versions are released.
# Prevent accidentally breaking older Rust versions
- 1.32.0
- 1.31.0
# - 1.33.0
matrix:
include:

View file

@ -10,10 +10,11 @@ jobs:
rustup_toolchain: beta
nightly:
rustup_toolchain: nightly
minimum_supported_version_plus_one:
rustup_toolchain: 1.32.0
minimum_supported_version:
rustup_toolchain: 1.31.0
# TODO: re-enable once new versions are released.
# minimum_supported_version_plus_one:
# rustup_toolchain: 1.32.0
#minimum_supported_version:
# rustup_toolchain: 1.33.0
steps:
- ${{ if ne(parameters.name, 'Windows') }}:
# Linux and macOS.

View file

@ -30,9 +30,6 @@ result can then be converted to JSON for use with tools and libraries such as
[graphql-client](https://github.com/graphql-rust/graphql-client):
```rust
# // Only needed due to 2018 edition because the macro is not accessible.
# extern crate juniper;
# extern crate serde_json;
use juniper::{EmptyMutation, FieldResult, IntrospectionFormat};
// Define our schema.
@ -47,11 +44,14 @@ impl juniper::Context for Context {}
struct Query;
juniper::graphql_object!(Query: Context |&self| {
field example(&executor, id: String) -> FieldResult<Example> {
#[juniper::object(
Context = Context,
)]
impl Query {
fn example(id: String) -> FieldResult<Example> {
unimplemented!()
}
});
}
type Schema = juniper::RootNode<'static, Query, EmptyMutation<Context>>;

View file

@ -23,21 +23,22 @@ enum SignUpResult {
Error(Vec<ValidationError>),
}
juniper::graphql_object!(SignUpResult: () |&self| {
field user() -> Option<&User> {
#[juniper::object]
impl SignUpResult {
fn user(&self) -> Option<&User> {
match *self {
SignUpResult::Ok(ref user) => Some(user),
SignUpResult::Error(_) => None,
}
}
field error() -> Option<&Vec<ValidationError>> {
fn error(&self) -> Option<&Vec<ValidationError>> {
match *self {
SignUpResult::Ok(_) => None,
SignUpResult::Error(ref errors) => Some(errors)
}
}
});
}
# fn main() {}
```

View file

@ -25,25 +25,31 @@ struct ValidationError {
# #[allow(dead_code)]
struct MutationResult<T>(Result<T, Vec<ValidationError>>);
juniper::graphql_object!(MutationResult<User>: () as "UserResult" |&self| {
field user() -> Option<&User> {
#[juniper::object(
name = "UserResult",
)]
impl MutationResult<User> {
fn user(&self) -> Option<&User> {
self.0.as_ref().ok()
}
field error() -> Option<&Vec<ValidationError>> {
fn error(&self) -> Option<&Vec<ValidationError>> {
self.0.as_ref().err()
}
});
}
juniper::graphql_object!(MutationResult<ForumPost>: () as "ForumPostResult" |&self| {
field forum_post() -> Option<&ForumPost> {
#[juniper::object(
name = "ForumPostResult",
)]
impl MutationResult<ForumPost> {
fn forum_post(&self) -> Option<&ForumPost> {
self.0.as_ref().ok()
}
field error() -> Option<&Vec<ValidationError>> {
fn error(&self) -> Option<&Vec<ValidationError>> {
self.0.as_ref().err()
}
});
}
# fn main() {}
```

View file

@ -20,7 +20,7 @@ naturally map to GraphQL features, such as `Option<T>`, `Vec<T>`, `Box<T>`,
For more advanced mappings, Juniper provides multiple macros to map your Rust
types to a GraphQL schema. The most important one is the
[graphql_object!][jp_obj_macro] macro that is used for declaring an object with
[object][jp_object] procedural macro that is used for declaring an object with
resolvers, which you will use for the `Query` and `Mutation` roots.
```rust
@ -60,7 +60,7 @@ struct NewHuman {
}
// Now, we create our root Query and Mutation types with resolvers by using the
// graphql_object! macro.
// object macro.
// Objects can have contexts that allow accessing shared state like a database
// pool.
@ -74,17 +74,23 @@ impl juniper::Context for Context {}
struct Query;
juniper::graphql_object!(Query: Context |&self| {
#[juniper::object(
// Here we specify the context type for the object.
// We need to do this in every type that
// needs access to the context.
Context = Context,
)]
impl Query {
field apiVersion() -> &str {
fn apiVersion() -> &str {
"1.0"
}
// Arguments to resolvers can either be simple types or input objects.
// The executor is a special (optional) argument that allows accessing the context.
field human(&executor, id: String) -> FieldResult<Human> {
// Get the context from the executor.
let context = executor.context();
// To gain access to the context, we specify a argument
// that is a reference to the Context type.
// Juniper automatically injects the correct context here.
fn human(context: &Context, id: String) -> FieldResult<Human> {
// Get a db connection.
let connection = context.pool.get_connection()?;
// Execute a db query.
@ -93,18 +99,23 @@ juniper::graphql_object!(Query: Context |&self| {
// Return the result.
Ok(human)
}
});
}
// Now, we do the same for our Mutation type.
struct Mutation;
juniper::graphql_object!(Mutation: Context |&self| {
#[juniper::object(
Context = Context,
)]
impl Mutation {
field createHuman(&executor, new_human: NewHuman) -> FieldResult<Human> {
fn createHuman(context: &Context, new_human: NewHuman) -> FieldResult<Human> {
let db = executor.context().pool.get_connection()?;
let human: Human = db.insert_human(&new_human)?;
Ok(human)
}
});
}
// A root schema consists of a query and a mutation.
// Request queries can be executed against a RootNode.
@ -130,6 +141,7 @@ You can invoke `juniper::execute` directly to run a GraphQL query:
# #[macro_use] extern crate juniper;
use juniper::{FieldResult, Variables, EmptyMutation};
#[derive(juniper::GraphQLEnum, Clone, Copy)]
enum Episode {
NewHope,
@ -137,18 +149,23 @@ enum Episode {
Jedi,
}
struct Query;
juniper::graphql_object!(Query: Ctx |&self| {
field favoriteEpisode(&executor) -> FieldResult<Episode> {
// Use the special &executor argument to fetch our fav episode.
Ok(executor.context().0)
}
});
// Arbitrary context data.
struct Ctx(Episode);
impl juniper::Context for Ctx {}
struct Query;
#[juniper::object(
Context = Ctx,
)]
impl Query {
fn favoriteEpisode(context: &Ctx) -> FieldResult<Episode> {
Ok(context.0)
}
}
// A root schema consists of a query and a mutation.
// Request queries can be executed against a RootNode.
type Schema = juniper::RootNode<'static, Query, EmptyMutation<Ctx>>;
@ -181,4 +198,4 @@ fn main() {
[rocket]: servers/rocket.md
[iron]: servers/iron.md
[tutorial]: ./tutorial.html
[jp_obj_macro]: https://docs.rs/juniper/latest/juniper/macro.graphql_object.html
[jp_obj_macro]: https://docs.rs/juniper/latest/juniper/macro.object.html

View file

@ -20,19 +20,20 @@ object somewhere but never references it, it will not be exposed in a schema.
## The query root
The query root is just a GraphQL object. You define it like any other GraphQL
object in Juniper, most commonly using the `graphql_object!` macro:
object in Juniper, most commonly using the `object` proc macro:
```rust
# use juniper::FieldResult;
# #[derive(juniper::GraphQLObject)] struct User { name: String }
struct Root;
juniper::graphql_object!(Root: () |&self| {
field userWithUsername(username: String) -> FieldResult<Option<User>> {
#[juniper::object]
impl Root {
fn userWithUsername(username: String) -> FieldResult<Option<User>> {
// Look up user in database...
# unimplemented!()
}
});
}
# fn main() { }
```
@ -47,12 +48,13 @@ usually performs some mutating side-effect, such as updating a database.
# #[derive(juniper::GraphQLObject)] struct User { name: String }
struct Mutations;
juniper::graphql_object!(Mutations: () |&self| {
field signUpUser(name: String, email: String) -> FieldResult<User> {
#[juniper::object]
impl Mutations {
fn signUpUser(name: String, email: String) -> FieldResult<User> {
// Validate inputs and save user in database...
# unimplemented!()
}
});
}
# fn main() { }
```

View file

@ -47,11 +47,12 @@ fn context_factory(_: &mut Request) -> IronResult<()> {
struct Root;
juniper::graphql_object!(Root: () |&self| {
field foo() -> String {
#[juniper::object]
impl Root {
fn foo() -> String {
"Bar".to_owned()
}
});
}
# #[allow(unreachable_code, unused_variables)]
fn main() {
@ -98,13 +99,14 @@ fn context_factory(req: &mut Request) -> IronResult<Context> {
struct Root;
juniper::graphql_object!(Root: Context |&self| {
field my_addr(&executor) -> String {
let context = executor.context();
#[juniper::object(
Context = Context,
)]
impl Root {
field my_addr(context: &Context) -> String {
format!("Hello, you're coming from {}", context.remote_addr)
}
});
}
# fn main() {
# let _graphql_endpoint = juniper_iron::GraphQLHandler::new(
@ -115,10 +117,6 @@ juniper::graphql_object!(Root: Context |&self| {
# }
```
## Accessing global data
FIXME: Show how the `persistent` crate works with contexts using e.g. `r2d2`.
[iron]: http://ironframework.io
[graphiql]: https://github.com/graphql/graphiql
[mount]: https://github.com/iron/mount

View file

@ -14,12 +14,14 @@ struct Coordinate {
struct Root;
# #[derive(juniper::GraphQLObject)] struct User { name: String }
juniper::graphql_object!(Root: () |&self| {
field users_at_location(coordinate: Coordinate, radius: f64) -> Vec<User> {
#[juniper::object]
impl Root {
fn users_at_location(coordinate: Coordinate, radius: f64) -> Vec<User> {
// Send coordinate to database
// ...
# unimplemented!()
}
});
}
# fn main() {}
```
@ -43,12 +45,14 @@ struct WorldCoordinate {
struct Root;
# #[derive(juniper::GraphQLObject)] struct User { name: String }
juniper::graphql_object!(Root: () |&self| {
field users_at_location(coordinate: WorldCoordinate, radius: f64) -> Vec<User> {
#[juniper::object]
impl Root {
fn users_at_location(coordinate: WorldCoordinate, radius: f64) -> Vec<User> {
// Send coordinate to database
// ...
# unimplemented!()
}
});
}
# fn main() {}
```

View file

@ -49,7 +49,7 @@ impl Character for Droid {
fn as_droid(&self) -> Option<&Droid> { Some(&self) }
}
juniper::graphql_interface!(<'a> &'a Character: () as "Character" where Scalar = <S>|&self| {
juniper::graphql_interface!(<'a> &'a Character: () as "Character" where Scalar = <S> |&self| {
field id() -> &str { self.id() }
instance_resolvers: |_| {
@ -79,14 +79,14 @@ we'll use two hashmaps, but this could be two tables and some SQL calls instead:
```rust
# use std::collections::HashMap;
#[derive(juniper::GraphQLObject)]
#[graphql(Context = "Database")]
#[graphql(Context = Database)]
struct Human {
id: String,
home_planet: String,
}
#[derive(juniper::GraphQLObject)]
#[graphql(Context = "Database")]
#[graphql(Context = Database)]
struct Droid {
id: String,
primary_function: String,

View file

@ -2,8 +2,8 @@
If you've got a struct that can't be mapped directly to GraphQL, that contains
computed fields or circular structures, you have to use a more powerful tool:
the `graphql_object!` macro. This macro lets you define GraphQL objects similar
to how you define methods in a Rust `impl` block for a type. Continuing with the
the `object` procedural macro. This macro lets you define GraphQL object
fields in a Rust `impl` block for a type. Continuing with the
example from the last chapter, this is how you would define `Person` using the
macro:
@ -14,15 +14,16 @@ struct Person {
age: i32,
}
juniper::graphql_object!(Person: () |&self| {
field name() -> &str {
#[juniper::object]
impl Person {
fn name(&self) -> &str {
self.name.as_str()
}
field age() -> i32 {
fn age(&self) -> i32 {
self.age
}
});
}
# fn main() { }
```
@ -42,12 +43,13 @@ struct House {
inhabitants: Vec<Person>,
}
juniper::graphql_object!(House: () |&self| {
#[juniper::object]
impl House {
// Creates the field inhabitantWithName(name), returning a nullable person
field inhabitant_with_name(name: String) -> Option<&Person> {
fn inhabitant_with_name(&self, name: String) -> Option<&Person> {
self.inhabitants.iter().find(|p| p.name == name)
}
});
}
# fn main() {}
```
@ -68,15 +70,19 @@ struct Person {
website_url: String,
}
juniper::graphql_object!(Person: () as "PersonObject" |&self| {
field name() -> &str {
#[juniper::object(
// With this attribtue you can change the public GraphQL name of the type.
name = "PersonObject",
)]
impl Person {
fn name(&self) -> &str {
self.name.as_str()
}
field websiteURL() -> &str {
fn websiteURL(&self) -> &str {
self.website_url.as_str()
}
});
}
# fn main() { }
```
@ -90,4 +96,4 @@ GraphQL fields expose more features than Rust's standard method syntax gives us:
* Per-argument descriptions
These, and more features, are described more thorougly in [the reference
documentation](https://docs.rs/juniper/0.8.1/juniper/macro.graphql_object.html).
documentation](https://docs.rs/juniper/latest/juniper/macro.object.html).

View file

@ -154,7 +154,7 @@ attribute:
struct Person {
name: String,
age: i32,
#[graphql(deprecation="Please use the name field instead")]
#[graphql(deprecated = "Please use the name field instead")]
first_name: String,
}

View file

@ -25,14 +25,16 @@ struct Example {
filename: PathBuf,
}
juniper::graphql_object!(Example: () |&self| {
field contents() -> FieldResult<String> {
#[juniper::object]
impl Example {
fn contents() -> FieldResult<String> {
let mut file = File::open(&self.filename)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
field foo() -> FieldResult<Option<String>> {
fn foo() -> FieldResult<Option<String>> {
// Some invalid bytes.
let invalid = vec![128, 223];
@ -41,7 +43,7 @@ juniper::graphql_object!(Example: () |&self| {
Err(e) => Err(e)?,
}
}
});
}
# fn main() {}
```
@ -141,14 +143,15 @@ struct Example {
whatever: Option<bool>,
}
juniper::graphql_object!(Example: () |&self| {
field whatever() -> Result<bool, CustomError> {
#[juniper::object]
impl Example {
fn whatever() -> Result<bool, CustomError> {
if let Some(value) = self.whatever {
return Ok(value);
}
Err(CustomError::WhateverNotSet)
}
});
}
# fn main() {}
```

View file

@ -31,42 +31,57 @@ We would like a `friends` field on `User` that returns a list of `User` objects.
In order to write such a field though, the database must be queried.
To solve this, we mark the `Database` as a valid context type and assign it to
the user object. Then, we use the special `&executor` argument to access the
current context object:
the user object.
To gain access to the context, we need to specify an argument with the same
type as the specified `Context` for the type:
```rust
# use std::collections::HashMap;
extern crate juniper;
// This struct represents our context.
struct Database {
users: HashMap<i32, User>,
}
// Mark the Database as a valid context type for Juniper
impl juniper::Context for Database {}
struct User {
id: i32,
name: String,
friend_ids: Vec<i32>,
}
// 1. Mark the Database as a valid context type for Juniper
impl juniper::Context for Database {}
// 2. Assign Database as the context type for User
juniper::graphql_object!(User: Database |&self| {
// 3. Use the special executor argument
field friends(&executor) -> Vec<&User> {
// 4. Use the executor to access the context object
let database = executor.context();
// Assign Database as the context type for User
#[juniper::object(
Context = Database,
)]
impl User {
// 3. Inject the context by specifying an argument
// with the context type.
// Note:
// - the type must be a reference
// - the name of the argument SHOULD be context
fn friends(&self, context: &Database) -> Vec<&User> {
// 5. Use the database to lookup users
self.friend_ids.iter()
.map(|id| database.users.get(id).expect("Could not find user with ID"))
.map(|id| context.users.get(id).expect("Could not find user with ID"))
.collect()
}
field name() -> &str { self.name.as_str() }
field id() -> i32 { self.id }
});
fn name(&self) -> &str {
self.name.as_str()
}
fn id(&self) -> i32 {
self.id
}
}
# fn main() { }
```

View file

@ -42,7 +42,7 @@ impl Character for Droid {
fn as_droid(&self) -> Option<&Droid> { Some(&self) }
}
juniper::graphql_union!(<'a> &'a Character: () as "Character" where Scalar = <S> |&self| {
juniper::graphql_union!(<'a> &'a Character: () as "Character" where Scalar = <S> |&self| {
instance_resolvers: |_| {
// The left hand side indicates the concrete type T, the right hand
// side should be an expression returning Option<T>
@ -61,14 +61,14 @@ FIXME: This example does not compile at the moment
```rust
# use std::collections::HashMap;
#[derive(juniper::GraphQLObject)]
#[graphql(Context = "Database")]
#[graphql(Context = Database)]
struct Human {
id: String,
home_planet: String,
}
#[derive(juniper::GraphQLObject)]
#[graphql(Context = "Database")]
#[graphql(Context = Database)]
struct Droid {
id: String,
primary_function: String,
@ -108,14 +108,14 @@ juniper::graphql_union!(<'a> &'a Character: Database as "Character" where Scalar
```rust
# use std::collections::HashMap;
#[derive(juniper::GraphQLObject)]
#[graphql(Context = "Database")]
#[graphql(Context = Database)]
struct Human {
id: String,
home_planet: String,
}
#[derive(juniper::GraphQLObject)]
#[graphql(Context = "Database")]
#[graphql(Context = Database)]
struct Droid {
id: String,
primary_function: String,

View file

@ -6,13 +6,14 @@ edition = "2018"
build = "build.rs"
[dependencies]
juniper = { version = "0.11", path = "../../../juniper" }
juniper_iron = { version = "0.3", path = "../../../juniper_iron" }
juniper = { path = "../../../juniper" }
juniper_iron = { path = "../../../juniper_iron" }
iron = "^0.5.0"
mount = "^0.3.0"
skeptic = "0.13"
serde_json = "1.0.39"
[build-dependencies]
skeptic = "0.13"

View file

@ -4,15 +4,7 @@ version = "0.1.0"
publish = false
edition = "2018"
[dependencies]
juniper = { version = "0.11.0", path = "../../juniper" }
serde_json = { version = "1" }
[dev-dependencies]
juniper = { path = "../../juniper" }
serde_json = { version = "1" }
fnv = "1.0.3"
indexmap = "1.0"
[[test]]
name = "integration_tests"
path = "src/lib.rs"
harness = true

View file

@ -19,7 +19,7 @@ enum DocEnum {
Foo,
}
/// Doc 1.
/// Doc 1.\
/// Doc 2.
///
/// Doc 4.
@ -85,7 +85,7 @@ fn test_multi_doc_comment() {
let meta = MultiDocEnum::meta(&(), &mut registry);
assert_eq!(
meta.description(),
Some(&"Doc 1. Doc 2.\nDoc 4.".to_string())
Some(&"Doc 1. Doc 2.\n\nDoc 4.".to_string())
);
}

View file

@ -27,7 +27,7 @@ struct DocComment {
regular_field: bool,
}
/// Doc 1.
/// Doc 1.\
/// Doc 2.
///
/// Doc 4.
@ -151,7 +151,7 @@ fn test_multi_doc_comment() {
let meta = MultiDocComment::meta(&(), &mut registry);
assert_eq!(
meta.description(),
Some(&"Doc 1. Doc 2.\nDoc 4.".to_string())
Some(&"Doc 1. Doc 2.\n\nDoc 4.".to_string())
);
}

View file

@ -12,20 +12,20 @@ use juniper::{self, execute, EmptyMutation, GraphQLType, RootNode, Value, Variab
#[graphql(
name = "MyObj",
description = "obj descr",
scalar = "DefaultScalarValue"
scalar = DefaultScalarValue
)]
struct Obj {
regular_field: bool,
#[graphql(
name = "renamedField",
description = "descr",
deprecation = "field descr"
deprecated = "field deprecation"
)]
c: i32,
}
#[derive(GraphQLObject, Debug, PartialEq)]
#[graphql(scalar = "DefaultScalarValue")]
#[graphql(scalar = DefaultScalarValue)]
struct Nested {
obj: Obj,
}
@ -39,7 +39,7 @@ struct DocComment {
regular_field: bool,
}
/// Doc 1.
/// Doc 1.\
/// Doc 2.
///
/// Doc 4.
@ -75,56 +75,57 @@ struct Context;
impl juniper::Context for Context {}
#[derive(GraphQLObject, Debug)]
#[graphql(Context = "Context")]
#[graphql(Context = Context)]
struct WithCustomContext {
a: bool,
}
juniper::graphql_object!(Query: () |&self| {
field obj() -> Obj {
Obj{
regular_field: true,
c: 22,
}
}
field nested() -> Nested {
Nested{
obj: Obj{
regular_field: false,
c: 333,
}
#[juniper::object]
impl Query {
fn obj() -> Obj {
Obj {
regular_field: true,
c: 22,
}
}
field doc() -> DocComment {
DocComment{
regular_field: true,
}
fn nested() -> Nested {
Nested {
obj: Obj {
regular_field: false,
c: 333,
},
}
}
field multi_doc() -> MultiDocComment {
MultiDocComment{
regular_field: true,
}
fn doc() -> DocComment {
DocComment {
regular_field: true,
}
}
field override_doc() -> OverrideDocComment {
OverrideDocComment{
regular_field: true,
}
fn multi_doc() -> MultiDocComment {
MultiDocComment {
regular_field: true,
}
}
field skipped_field_obj() -> SkippedFieldObj {
SkippedFieldObj{
fn override_doc() -> OverrideDocComment {
OverrideDocComment {
regular_field: true,
}
}
fn skipped_field_obj() -> SkippedFieldObj {
SkippedFieldObj {
regular_field: false,
skipped: 42,
}
}
});
}
#[test]
fn test_doc_comment() {
fn test_doc_comment_simple() {
let mut registry: juniper::Registry = juniper::Registry::new(FnvHashMap::default());
let meta = DocComment::meta(&(), &mut registry);
assert_eq!(meta.description(), Some(&"Object comment.".to_string()));
@ -143,14 +144,14 @@ fn test_multi_doc_comment() {
let meta = MultiDocComment::meta(&(), &mut registry);
assert_eq!(
meta.description(),
Some(&"Doc 1. Doc 2.\nDoc 4.".to_string())
Some(&"Doc 1. Doc 2.\n\nDoc 4.".to_string())
);
check_descriptions(
"MultiDocComment",
&Value::scalar("Doc 1. Doc 2.\nDoc 4."),
&Value::scalar("Doc 1. Doc 2.\n\nDoc 4."),
"regularField",
&Value::scalar("Field 1. Field 2."),
&Value::scalar("Field 1.\nField 2."),
);
}

View file

@ -1,3 +1,5 @@
mod util;
mod derive_enum;
mod derive_input_object;
mod derive_object;

View file

@ -0,0 +1,54 @@
use juniper::{DefaultScalarValue, GraphQLType, RootNode, Value, Variables};
use std::default::Default;
pub fn run_query<Query, Mutation, Context>(query: &str) -> Value
where
Query: GraphQLType<DefaultScalarValue, TypeInfo = (), Context = Context> + Default,
Mutation: GraphQLType<DefaultScalarValue, TypeInfo = (), Context = Context> + Default,
Context: Default,
{
let schema = RootNode::new(Query::default(), Mutation::default());
let (result, errs) =
juniper::execute(query, None, &schema, &Variables::new(), &Context::default())
.expect("Execution failed");
assert_eq!(errs, []);
result
}
pub fn run_info_query<Query, Mutation, Context>(type_name: &str) -> Value
where
Query: GraphQLType<DefaultScalarValue, TypeInfo = (), Context = Context> + Default,
Mutation: GraphQLType<DefaultScalarValue, TypeInfo = (), Context = Context> + Default,
Context: Default,
{
let query = format!(
r#"
{{
__type(name: "{}") {{
name,
description,
fields {{
name
description
args {{
name
description
type {{
name
}}
}}
}}
}}
}}
"#,
type_name
);
let result = run_query::<Query, Mutation, Context>(&query);
result
.as_object_value()
.expect("Result is not an object")
.get_field_value("__type")
.expect("__type field missing")
.clone()
}

View file

@ -151,15 +151,18 @@ juniper::graphql_scalar!(i64 as "Long" where Scalar = MyScalarValue {
struct TestType;
juniper::graphql_object!(TestType: () where Scalar = MyScalarValue |&self| {
field long_field() -> i64 {
#[juniper::object(
Scalar = MyScalarValue
)]
impl TestType {
fn long_field() -> i64 {
(::std::i32::MAX as i64) + 1
}
field long_with_arg(long_arg: i64) -> i64 {
fn long_with_arg(long_arg: i64) -> i64 {
long_arg
}
});
}
#[cfg(test)]
fn run_variable_query<F>(query: &str, vars: Variables<MyScalarValue>, f: F)

View file

@ -1,11 +1,4 @@
extern crate juniper;
#[cfg(test)]
extern crate serde_json;
#[cfg(test)]
extern crate fnv;
#[cfg(test)]
extern crate indexmap;
mod codegen;
#[cfg(test)]
mod custom_scalar;

View file

@ -1,10 +1,27 @@
# master
- Refactored all crates to the 2018 edition. [#345](https://github.com/graphql-rust/juniper/pull/345)
- The minimum required Rust version is now `1.31.0`.
### object macro
The `graphql_object!` macro is deprecated and will be removed in the future.
It is replaced by the new [object](https://docs.rs/juniper/latest/juniper/macro.object.html) procedural macro.
[#333](https://github.com/graphql-rust/juniper/pull/333)
### 2018 Edition
All crates were refactored to the Rust 2018 edition.
This should not have any impact on your code, since juniper already was 2018 compatible.
[#345](https://github.com/graphql-rust/juniper/pull/345)
### Other changes
- The minimum required Rust version is now `1.34.0`.
- The `ScalarValue` custom derive has been renamed to `GraphQLScalarValue`.
- Added built-in support for the canonical schema introspection query via
`juniper::introspect()`. [#307](https://github.com/graphql-rust/juniper/issues/307)
`juniper::introspect()`.
[#307](https://github.com/graphql-rust/juniper/issues/307)
- Fix introspection query validity
The DirectiveLocation::InlineFragment had an invalid literal value,
which broke third party tools like apollo cli.

View file

@ -23,7 +23,6 @@ harness = false
path = "benches/bench.rs"
[features]
nightly = []
expose-test-schema = []
default = [
"chrono",

View file

@ -6,6 +6,8 @@ upload-doc = false
pre-release-replacements = [
# Juniper's changelog
{file="CHANGELOG.md", search="# master", replace="# master\n\n- No changes yet\n\n# [[{{version}}] {{date}}](https://github.com/graphql-rust/juniper/releases/tag/{{crate_name}}-{{version}})"},
# codegen
{file="../juniper_codegen/Cargo.toml", search="juniper = \\{ version = \"[^\"]+\"", replace="juniper = { version = \"{{version}}\""},
# Tests.
{file="../integration_tests/juniper_tests/Cargo.toml", search="juniper = \\{ version = \"[^\"]+\"", replace="juniper = { version = \"{{version}}\""},
# Hyper

View file

@ -5,15 +5,16 @@ use crate::value::{DefaultScalarValue, Object, Value};
struct TestType;
graphql_object!(TestType: () |&self| {
field a() -> &str {
#[crate::object_internal]
impl TestType {
fn a() -> &str {
"a"
}
field b() -> &str {
fn b() -> &str {
"b"
}
});
}
fn run_variable_query<F>(query: &str, vars: Variables<DefaultScalarValue>, f: F)
where

View file

@ -17,15 +17,16 @@ enum Color {
}
struct TestType;
graphql_object!(TestType: () |&self| {
field to_string(color: Color) -> String {
#[crate::object_internal]
impl TestType {
fn to_string(color: Color) -> String {
format!("Color::{:?}", color)
}
field a_color() -> Color {
fn a_color() -> Color {
Color::Red
}
});
}
fn run_variable_query<F>(query: &str, vars: Variables<DefaultScalarValue>, f: F)
where

View file

@ -7,34 +7,57 @@ mod field_execution {
struct DataType;
struct DeepDataType;
graphql_object!(DataType: () |&self| {
field a() -> &str { "Apple" }
field b() -> &str { "Banana" }
field c() -> &str { "Cookie" }
field d() -> &str { "Donut" }
field e() -> &str { "Egg" }
field f() -> &str { "Fish" }
#[crate::object_internal]
impl DataType {
fn a() -> &str {
"Apple"
}
fn b() -> &str {
"Banana"
}
fn c() -> &str {
"Cookie"
}
fn d() -> &str {
"Donut"
}
fn e() -> &str {
"Egg"
}
fn f() -> &str {
"Fish"
}
field pic(size: Option<i32>) -> String {
fn pic(size: Option<i32>) -> String {
format!("Pic of size: {}", size.unwrap_or(50))
}
field deep() -> DeepDataType {
fn deep() -> DeepDataType {
DeepDataType
}
});
}
graphql_object!(DeepDataType: () |&self| {
field a() -> &str { "Already Been Done" }
field b() -> &str { "Boring" }
field c() -> Vec<Option<&str>> { vec![Some("Contrived"), None, Some("Confusing")] }
#[crate::object_internal]
impl DeepDataType {
fn a() -> &str {
"Already Been Done"
}
fn b() -> &str {
"Boring"
}
fn c() -> Vec<Option<&str>> {
vec![Some("Contrived"), None, Some("Confusing")]
}
field deeper() -> Vec<Option<DataType>> { vec![Some(DataType), None, Some(DataType) ] }
});
fn deeper() -> Vec<Option<DataType>> {
vec![Some(DataType), None, Some(DataType)]
}
}
#[test]
fn test() {
let schema = RootNode::new(DataType, EmptyMutation::<()>::new());
let schema =
RootNode::<_, _, crate::DefaultScalarValue>::new(DataType, EmptyMutation::<()>::new());
let doc = r"
query Example($size: Int) {
a,
@ -139,12 +162,21 @@ mod merge_parallel_fragments {
struct Type;
graphql_object!(Type: () |&self| {
field a() -> &str { "Apple" }
field b() -> &str { "Banana" }
field c() -> &str { "Cherry" }
field deep() -> Type { Type }
});
#[crate::object_internal]
impl Type {
fn a() -> &str {
"Apple"
}
fn b() -> &str {
"Banana"
}
fn c() -> &str {
"Cherry"
}
fn deep() -> Type {
Type
}
}
#[test]
fn test() {
@ -214,21 +246,43 @@ mod merge_parallel_inline_fragments {
struct Type;
struct Other;
graphql_object!(Type: () |&self| {
field a() -> &str { "Apple" }
field b() -> &str { "Banana" }
field c() -> &str { "Cherry" }
field deep() -> Type { Type }
field other() -> Vec<Other> { vec![Other, Other] }
});
#[crate::object_internal]
impl Type {
fn a() -> &str {
"Apple"
}
fn b() -> &str {
"Banana"
}
fn c() -> &str {
"Cherry"
}
fn deep() -> Type {
Type
}
fn other() -> Vec<Other> {
vec![Other, Other]
}
}
graphql_object!(Other: () |&self| {
field a() -> &str { "Apple" }
field b() -> &str { "Banana" }
field c() -> &str { "Cherry" }
field deep() -> Type { Type }
field other() -> Vec<Other> { vec![Other, Other] }
});
#[crate::object_internal]
impl Other {
fn a() -> &str {
"Apple"
}
fn b() -> &str {
"Banana"
}
fn c() -> &str {
"Cherry"
}
fn deep() -> Type {
Type
}
fn other() -> Vec<Other> {
vec![Other, Other]
}
}
#[test]
fn test() {
@ -342,9 +396,14 @@ mod threads_context_correctly {
impl Context for TestContext {}
graphql_object!(Schema: TestContext |&self| {
field a(&executor) -> String { executor.context().value.clone() }
});
#[crate::object_internal(
Context = TestContext,
)]
impl Schema {
fn a(context: &TestContext) -> String {
context.value.clone()
}
}
#[test]
fn test() {
@ -403,36 +462,42 @@ mod dynamic_context_switching {
struct ItemRef;
graphql_object!(Schema: OuterContext |&self| {
field item_opt(&executor, key: i32) -> Option<(&InnerContext, ItemRef)> {
#[crate::object_internal(Context = OuterContext)]
impl Schema {
fn item_opt(context: &OuterContext, key: i32) -> Option<(&InnerContext, ItemRef)> {
executor.context().items.get(&key).map(|c| (c, ItemRef))
}
field item_res(&executor, key: i32) -> FieldResult<(&InnerContext, ItemRef)> {
let res = executor.context().items.get(&key)
fn item_res(context: &OuterContext, key: i32) -> FieldResult<(&InnerContext, ItemRef)> {
let res = context
.items
.get(&key)
.ok_or(format!("Could not find key {}", key))
.map(|c| (c, ItemRef))?;
Ok(res)
}
field item_res_opt(&executor, key: i32) -> FieldResult<Option<(&InnerContext, ItemRef)>> {
fn item_res_opt(
context: &OuterContext,
key: i32,
) -> FieldResult<Option<(&InnerContext, ItemRef)>> {
if key > 100 {
Err(format!("Key too large: {}", key))?;
}
Ok(executor.context().items.get(&key)
.map(|c| (c, ItemRef)))
Ok(context.items.get(&key).map(|c| (c, ItemRef)))
}
field item_always(&executor, key: i32) -> (&InnerContext, ItemRef) {
executor.context().items.get(&key)
.map(|c| (c, ItemRef))
.unwrap()
fn item_always(context: &OuterContext, key: i32) -> (&InnerContext, ItemRef) {
context.items.get(&key).map(|c| (c, ItemRef)).unwrap()
}
});
}
graphql_object!(ItemRef: InnerContext |&self| {
field value(&executor) -> String { executor.context().value.clone() }
});
#[crate::object_internal(Context = InnerContext)]
impl ItemRef {
fn value(context: &InnerContext) -> String {
context.value.clone()
}
}
#[test]
fn test_opt() {
@ -736,19 +801,37 @@ mod propagates_errors_to_nullable_fields {
}
}
graphql_object!(Schema: () |&self| {
field inner() -> Inner { Inner }
field inners() -> Vec<Inner> { (0..5).map(|_| Inner).collect() }
field nullable_inners() -> Vec<Option<Inner>> { (0..5).map(|_| Some(Inner)).collect() }
});
#[crate::object_internal]
impl Schema {
fn inner() -> Inner {
Inner
}
fn inners() -> Vec<Inner> {
(0..5).map(|_| Inner).collect()
}
fn nullable_inners() -> Vec<Option<Inner>> {
(0..5).map(|_| Some(Inner)).collect()
}
}
graphql_object!(Inner: () |&self| {
field nullable_field() -> Option<Inner> { Some(Inner) }
field non_nullable_field() -> Inner { Inner }
field nullable_error_field() -> FieldResult<Option<&str>> { Err("Error for nullableErrorField")? }
field non_nullable_error_field() -> FieldResult<&str> { Err("Error for nonNullableErrorField")? }
field custom_error_field() -> Result<&str, CustomError> { Err(CustomError::NotFound) }
});
#[crate::object_internal]
impl Inner {
fn nullable_field() -> Option<Inner> {
Some(Inner)
}
fn non_nullable_field() -> Inner {
Inner
}
fn nullable_error_field() -> FieldResult<Option<&str>> {
Err("Error for nullableErrorField")?
}
fn non_nullable_error_field() -> FieldResult<&str> {
Err("Error for nonNullableErrorField")?
}
fn custom_error_field() -> Result<&str, CustomError> {
Err(CustomError::NotFound)
}
}
#[test]
fn nullable_first_level() {
@ -985,13 +1068,17 @@ mod named_operations {
struct Schema;
graphql_object!(Schema: () |&self| {
field a() -> &str { "b" }
});
#[crate::object_internal]
impl Schema {
fn a() -> &str {
"b"
}
}
#[test]
fn uses_inline_operation_if_no_name_provided() {
let schema = RootNode::new(Schema, EmptyMutation::<()>::new());
let schema =
RootNode::<_, _, crate::DefaultScalarValue>::new(Schema, EmptyMutation::<()>::new());
let doc = r"{ a }";
let vars = vec![].into_iter().collect();

View file

@ -37,12 +37,17 @@ mod interface {
}
}
graphql_object!(Dog: () |&self| {
field name() -> &str { &self.name }
field woofs() -> bool { self.woofs }
interfaces: [&Pet]
});
#[crate::object_internal(
interfaces = [&Pet]
)]
impl Dog {
fn name(&self) -> &str {
&self.name
}
fn woofs(&self) -> bool {
self.woofs
}
}
struct Cat {
name: String,
@ -58,22 +63,28 @@ mod interface {
}
}
graphql_object!(Cat: () |&self| {
field name() -> &str { &self.name }
field meows() -> bool { self.meows }
interfaces: [&Pet]
});
#[crate::object_internal(
interfaces = [&Pet]
)]
impl Cat {
fn name(&self) -> &str {
&self.name
}
fn meows(&self) -> bool {
self.meows
}
}
struct Schema {
pets: Vec<Box<Pet>>,
}
graphql_object!(Schema: () |&self| {
field pets() -> Vec<&Pet> {
#[crate::object_internal]
impl Schema {
fn pets(&self) -> Vec<&Pet> {
self.pets.iter().map(|p| p.as_ref()).collect()
}
});
}
#[test]
fn test() {
@ -177,10 +188,15 @@ mod union {
}
}
graphql_object!(Dog: () |&self| {
field name() -> &str { &self.name }
field woofs() -> bool { self.woofs }
});
#[crate::object_internal]
impl Dog {
fn name(&self) -> &str {
&self.name
}
fn woofs(&self) -> bool {
self.woofs
}
}
struct Cat {
name: String,
@ -193,20 +209,26 @@ mod union {
}
}
graphql_object!(Cat: () |&self| {
field name() -> &str { &self.name }
field meows() -> bool { self.meows }
});
#[crate::object_internal]
impl Cat {
fn name(&self) -> &str {
&self.name
}
fn meows(&self) -> bool {
self.meows
}
}
struct Schema {
pets: Vec<Box<Pet>>,
}
graphql_object!(Schema: () |&self| {
field pets() -> Vec<&Pet> {
#[crate::object_internal]
impl Schema {
fn pets(&self) -> Vec<&Pet> {
self.pets.iter().map(|p| p.as_ref()).collect()
}
});
}
#[test]
fn test() {

View file

@ -64,14 +64,27 @@ enum EnumDeprecation {
struct Root;
graphql_object!(Root: () |&self| {
field default_name() -> DefaultName { DefaultName::Foo }
field named() -> Named { Named::Foo }
field no_trailing_comma() -> NoTrailingComma { NoTrailingComma::Foo }
field enum_description() -> EnumDescription { EnumDescription::Foo }
field enum_value_description() -> EnumValueDescription { EnumValueDescription::Foo }
field enum_deprecation() -> EnumDeprecation { EnumDeprecation::Foo }
});
#[crate::object_internal]
impl Root {
fn default_name() -> DefaultName {
DefaultName::Foo
}
fn named() -> Named {
Named::Foo
}
fn no_trailing_comma() -> NoTrailingComma {
NoTrailingComma::Foo
}
fn enum_description() -> EnumDescription {
EnumDescription::Foo
}
fn enum_value_description() -> EnumValueDescription {
EnumValueDescription::Foo
}
fn enum_deprecation() -> EnumDeprecation {
EnumDeprecation::Foo
}
}
fn run_type_info_query<F>(doc: &str, f: F)
where

View file

@ -79,8 +79,9 @@ struct FieldWithDefaults {
field_two: i32,
}
graphql_object!(Root: () |&self| {
field test_field(
#[crate::object_internal]
impl Root {
fn test_field(
a1: DefaultName,
a2: NoTrailingComma,
a3: Derive,
@ -95,7 +96,7 @@ graphql_object!(Root: () |&self| {
) -> i32 {
0
}
});
}
fn run_type_info_query<F>(doc: &str, f: F)
where

View file

@ -51,22 +51,26 @@ graphql_interface!(Interface: () as "SampleInterface" |&self| {
}
});
graphql_object!(Root: () |&self| {
description: "The root query object in the schema"
interfaces: [Interface]
field sample_enum() -> Sample {
/// The root query object in the schema
#[crate::object_internal(
interfaces = [&Interface]
Scalar = crate::DefaultScalarValue,
)]
impl Root {
fn sample_enum() -> Sample {
Sample::One
}
field sample_scalar(
first: i32 as "The first number",
second = 123: i32 as "The second number"
) -> Scalar as "A sample scalar field on the object" {
#[graphql(arguments(
first(description = "The first number",),
second(description = "The second number", default = 123,),
))]
/// A sample scalar field on the object
fn sample_scalar(first: i32, second: i32) -> Scalar {
Scalar(first + second)
}
});
}
#[test]
fn test_execution() {

View file

@ -62,59 +62,67 @@ struct InputWithDefaults {
a: i32,
}
graphql_object!(TestType: () |&self| {
field field_with_object_input(input: Option<TestInputObject>) -> String {
#[crate::object_internal]
impl TestType {
fn field_with_object_input(input: Option<TestInputObject>) -> String {
format!("{:?}", input)
}
field field_with_nullable_string_input(input: Option<String>) -> String {
fn field_with_nullable_string_input(input: Option<String>) -> String {
format!("{:?}", input)
}
field field_with_non_nullable_string_input(input: String) -> String {
fn field_with_non_nullable_string_input(input: String) -> String {
format!("{:?}", input)
}
field field_with_default_argument_value(input = ("Hello World".to_owned()): String) -> String {
#[graphql(
arguments(
input(
default = "Hello World".to_string(),
)
)
)]
fn field_with_default_argument_value(input: String) -> String {
format!("{:?}", input)
}
field field_with_nested_object_input(input: Option<TestNestedInputObject>) -> String {
fn field_with_nested_object_input(input: Option<TestNestedInputObject>) -> String {
format!("{:?}", input)
}
field list(input: Option<Vec<Option<String>>>) -> String {
fn list(input: Option<Vec<Option<String>>>) -> String {
format!("{:?}", input)
}
field nn_list(input: Vec<Option<String>>) -> String {
fn nn_list(input: Vec<Option<String>>) -> String {
format!("{:?}", input)
}
field list_nn(input: Option<Vec<String>>) -> String {
fn list_nn(input: Option<Vec<String>>) -> String {
format!("{:?}", input)
}
field nn_list_nn(input: Vec<String>) -> String {
fn nn_list_nn(input: Vec<String>) -> String {
format!("{:?}", input)
}
field example_input(arg: ExampleInputObject) -> String {
fn example_input(arg: ExampleInputObject) -> String {
format!("a: {:?}, b: {:?}", arg.a, arg.b)
}
field input_with_defaults(arg: InputWithDefaults) -> String {
fn input_with_defaults(arg: InputWithDefaults) -> String {
format!("a: {:?}", arg.a)
}
field integer_input(value: i32) -> String {
fn integer_input(value: i32) -> String {
format!("value: {}", value)
}
field float_input(value: f64) -> String {
fn float_input(value: f64) -> String {
format!("value: {}", value)
}
});
}
fn run_variable_query<F>(query: &str, vars: Variables<DefaultScalarValue>, f: F)
where

View file

@ -209,20 +209,22 @@ mod integration_test {
#[test]
fn test_serialization() {
struct Root;
graphql_object!(Root: () |&self| {
field exampleNaiveDate() -> NaiveDate {
#[crate::object_internal]
impl Root {
fn exampleNaiveDate() -> NaiveDate {
NaiveDate::from_ymd(2015, 3, 14)
}
field exampleNaiveDateTime() -> NaiveDateTime {
fn exampleNaiveDateTime() -> NaiveDateTime {
NaiveDate::from_ymd(2016, 7, 8).and_hms(9, 10, 11)
}
field exampleDateTimeFixedOffset() -> DateTime<FixedOffset> {
DateTime::parse_from_rfc3339("1996-12-19T16:39:57-08:00").unwrap()
fn exampleDateTimeFixedOffset() -> DateTime<FixedOffset> {
DateTime::parse_from_rfc3339("1996-12-19T16:39:57-08:00").unwrap()
}
field exampleDateTimeUtc() -> DateTime<Utc> {
Utc.timestamp(61, 0)
fn exampleDateTimeUtc() -> DateTime<Utc> {
Utc.timestamp(61, 0)
}
});
}
let doc = r#"
{

View file

@ -108,8 +108,15 @@ extern crate uuid;
// Depend on juniper_codegen and re-export everything in it.
// This allows users to just depend on juniper and get the derive
// functionality automatically.
#[doc(hidden)]
pub use juniper_codegen::*;
pub use juniper_codegen::{
object, GraphQLEnum, GraphQLInputObject, GraphQLObject, GraphQLScalarValue, ScalarValue,
};
// Internal macros are not exported,
// but declared at the root to make them easier to use.
#[allow(unused_imports)]
use juniper_codegen::{
object_internal, GraphQLEnumInternal, GraphQLInputObjectInternal, GraphQLScalarValueInternal,
};
#[macro_use]
mod value;

View file

@ -61,13 +61,18 @@ impl Character for Droid {
fn id(&self) -> &str { &self.id }
}
juniper::graphql_object!(Human: Database as "Human" |&self| {
field id() -> &str { &self.id }
});
#[juniper::object(Context = Database)]
impl Human {
fn id(&self) -> &str { &self.id }
}
juniper::graphql_object!(Droid: Database as "Droid" |&self| {
field id() -> &str { &self.id }
});
#[juniper::object(
name = "Droid",
Context = Database,
)]
impl Droid {
fn id(&self) -> &str { &self.id }
}
// You can introduce lifetimes or generic parameters by < > before the name.
juniper::graphql_interface!(<'a> &'a Character: Database as "Character" |&self| {

View file

@ -1,6 +1,12 @@
/**
## DEPRECATION WARNING
The `graphql_object!` macro is deprecated and will be removed soon.
Use the new[object](https://docs.rs/juniper/latest/juniper/macro.object.html) macro instead.
Expose GraphQL objects
This is a short-hand macro that implements the `GraphQLType` trait for a given
type. By using this macro instead of implementing it manually, you gain type
safety and reduce repetitive declarations.
@ -308,7 +314,6 @@ arg_name: ArgType
```
[1]: struct.Executor.html
*/
#[macro_export]
macro_rules! graphql_object {

View file

@ -26,63 +26,111 @@ struct Point {
x: i32,
}
graphql_object!(Root: () |&self| {
field simple() -> i32 { 0 }
field exec_arg(&executor) -> i32 { 0 }
field exec_arg_and_more(&executor, arg: i32) -> i32 { 0 }
#[crate::object_internal]
impl Root {
fn simple() -> i32 {
0
}
fn exec_arg(executor: &Executor) -> i32 {
0
}
fn exec_arg_and_more(executor: &Executor, arg: i32) -> i32 {
0
}
field single_arg(arg: i32) -> i32 { 0 }
field multi_args(
arg1: i32,
arg2: i32
) -> i32 { 0 }
field multi_args_trailing_comma(
arg1: i32,
arg2: i32,
) -> i32 { 0 }
fn single_arg(arg: i32) -> i32 {
0
}
field single_arg_descr(arg: i32 as "The arg") -> i32 { 0 }
field multi_args_descr(
arg1: i32 as "The first arg",
arg2: i32 as "The second arg"
) -> i32 { 0 }
field multi_args_descr_trailing_comma(
arg1: i32 as "The first arg",
arg2: i32 as "The second arg",
) -> i32 { 0 }
fn multi_args(arg1: i32, arg2: i32) -> i32 {
0
}
field attr_arg_descr(#[doc = "The arg"] arg: i32) -> i32 { 0 }
field attr_arg_descr_collapse(
#[doc = "The arg"]
#[doc = "and more details"]
arg: i32,
) -> i32 { 0 }
fn multi_args_trailing_comma(arg1: i32, arg2: i32) -> i32 {
0
}
field arg_with_default(arg = 123: i32) -> i32 { 0 }
field multi_args_with_default(
arg1 = 123: i32,
arg2 = 456: i32
) -> i32 { 0 }
field multi_args_with_default_trailing_comma(
arg1 = 123: i32,
arg2 = 456: i32,
) -> i32 { 0 }
#[graphql(arguments(arg(description = "The arg")))]
fn single_arg_descr(arg: i32) -> i32 {
0
}
field arg_with_default_descr(arg = 123: i32 as "The arg") -> i32 { 0 }
field multi_args_with_default_descr(
arg1 = 123: i32 as "The first arg",
arg2 = 456: i32 as "The second arg"
) -> i32 { 0 }
field multi_args_with_default_trailing_comma_descr(
arg1 = 123: i32 as "The first arg",
arg2 = 456: i32 as "The second arg",
) -> i32 { 0 }
#[graphql(arguments(
arg1(description = "The first arg",),
arg2(description = "The second arg")
))]
fn multi_args_descr(arg1: i32, arg2: i32) -> i32 {
0
}
field args_with_complex_default(
arg1 = ("test".to_owned()): String as "A string default argument",
arg2 = (Point { x: 1 }): Point as "An input object default argument",
) -> i32 { 0 }
});
#[graphql(arguments(
arg1(description = "The first arg",),
arg2(description = "The second arg")
))]
fn multi_args_descr_trailing_comma(arg1: i32, arg2: i32) -> i32 {
0
}
// TODO: enable once [RFC 2565](https://github.com/rust-lang/rust/issues/60406) is implemented
// fn attr_arg_descr(#[doc = "The arg"] arg: i32) -> i32 { 0 }
// fn attr_arg_descr_collapse(
// #[doc = "The arg"]
// #[doc = "and more details"]
// arg: i32,
// ) -> i32 { 0 }
#[graphql(arguments(arg(default = 123,),))]
fn arg_with_default(arg: i32) -> i32 {
0
}
#[graphql(arguments(arg1(default = 123,), arg2(default = 456,)))]
fn multi_args_with_default(arg1: i32, arg2: i32) -> i32 {
0
}
#[graphql(arguments(arg1(default = 123,), arg2(default = 456,),))]
fn multi_args_with_default_trailing_comma(arg1: i32, arg2: i32) -> i32 {
0
}
#[graphql(arguments(arg(default = 123, description = "The arg")))]
fn arg_with_default_descr(arg: i32) -> i32 {
0
}
#[graphql(arguments(
arg1(default = 123, description = "The first arg"),
arg2(default = 456, description = "The second arg")
))]
fn multi_args_with_default_descr(arg1: i32, arg2: i32) -> i32 {
0
}
#[graphql(arguments(
arg1(default = 123, description = "The first arg",),
arg2(default = 456, description = "The second arg",)
))]
fn multi_args_with_default_trailing_comma_descr(arg1: i32, arg2: i32) -> i32 {
0
}
#[graphql(
arguments(
arg1(
default = "test".to_string(),
description = "A string default argument",
),
arg2(
default = Point{ x: 1 },
description = "An input object default argument",
)
),
)]
fn args_with_complex_default(arg1: String, arg2: Point) -> i32 {
0
}
}
fn run_args_info_query<F>(field_name: &str, f: F)
where
@ -509,71 +557,73 @@ fn introspect_field_multi_args_descr_trailing_comma() {
});
}
#[test]
fn introspect_field_attr_arg_descr() {
run_args_info_query("attrArgDescr", |args| {
assert_eq!(args.len(), 1);
// TODO: enable once [RFC 2565](https://github.com/rust-lang/rust/issues/60406) is implemented
// #[test]
// fn introspect_field_attr_arg_descr() {
// run_args_info_query("attrArgDescr", |args| {
// assert_eq!(args.len(), 1);
assert!(args.contains(&Value::object(
vec![
("name", Value::scalar("arg")),
("description", Value::scalar("The arg")),
("defaultValue", Value::null()),
(
"type",
Value::object(
vec![
("name", Value::null()),
(
"ofType",
Value::object(
vec![("name", Value::scalar("Int"))].into_iter().collect(),
),
),
]
.into_iter()
.collect(),
),
),
]
.into_iter()
.collect(),
)));
});
}
// assert!(args.contains(&Value::object(
// vec![
// ("name", Value::scalar("arg")),
// ("description", Value::scalar("The arg")),
// ("defaultValue", Value::null()),
// (
// "type",
// Value::object(
// vec![
// ("name", Value::null()),
// (
// "ofType",
// Value::object(
// vec![("name", Value::scalar("Int"))].into_iter().collect(),
// ),
// ),
// ]
// .into_iter()
// .collect(),
// ),
// ),
// ]
// .into_iter()
// .collect(),
// )));
// });
// }
#[test]
fn introspect_field_attr_arg_descr_collapse() {
run_args_info_query("attrArgDescrCollapse", |args| {
assert_eq!(args.len(), 1);
// TODO: enable once [RFC 2565](https://github.com/rust-lang/rust/issues/60406) is implemented
// #[test]
// fn introspect_field_attr_arg_descr_collapse() {
// run_args_info_query("attrArgDescrCollapse", |args| {
// assert_eq!(args.len(), 1);
assert!(args.contains(&Value::object(
vec![
("name", Value::scalar("arg")),
("description", Value::scalar("The arg\nand more details")),
("defaultValue", Value::null()),
(
"type",
Value::object(
vec![
("name", Value::null()),
(
"ofType",
Value::object(
vec![("name", Value::scalar("Int"))].into_iter().collect(),
),
),
]
.into_iter()
.collect(),
),
),
]
.into_iter()
.collect(),
)));
});
}
// assert!(args.contains(&Value::object(
// vec![
// ("name", Value::scalar("arg")),
// ("description", Value::scalar("The arg\nand more details")),
// ("defaultValue", Value::null()),
// (
// "type",
// Value::object(
// vec![
// ("name", Value::null()),
// (
// "ofType",
// Value::object(
// vec![("name", Value::scalar("Int"))].into_iter().collect(),
// ),
// ),
// ]
// .into_iter()
// .collect(),
// ),
// ),
// ]
// .into_iter()
// .collect(),
// )));
// });
// }
#[test]
fn introspect_field_arg_with_default() {

View file

@ -20,49 +20,87 @@ Syntax to validate:
*/
graphql_object!(Root: () |&self| {
field simple() -> i32 { 0 }
field description() -> i32 as "Field description" { 0 }
field deprecated "Deprecation reason"
deprecated() -> i32 { 0 }
field deprecated "Deprecation reason"
deprecated_descr() -> i32 as "Field description" { 0 }
#[crate::object_internal(
interfaces = [&Interface],
)]
impl Root {
fn simple() -> i32 {
0
}
/// Field description
field attr_description() -> i32 { 0 }
fn description() -> i32 {
0
}
#[deprecated]
fn deprecated_outer() -> bool {
true
}
#[deprecated(note = "Deprecation Reason")]
fn deprecated_outer_with_reason() -> bool {
true
}
#[graphql(deprecated = "Deprecation reason")]
fn deprecated() -> i32 {
0
}
#[graphql(deprecated = "Deprecation reason", description = "Field description")]
fn deprecated_descr() -> i32 {
0
}
/// Field description
fn attr_description() -> i32 {
0
}
/// Field description
/// with `collapse_docs` behavior
field attr_description_collapse() -> i32 { 0 }
fn attr_description_collapse() -> i32 {
0
}
/// Get the i32 representation of 0.
///
/// - This comment is longer.
/// - These two lines are rendered as bullets by GraphiQL.
/// - subsection
field attr_description_long() -> i32 { 0 }
fn attr_description_long() -> i32 {
0
}
#[deprecated]
field attr_deprecated() -> i32 { 0 }
#[graphql(deprecated)]
fn attr_deprecated() -> i32 {
0
}
#[deprecated(note = "Deprecation reason")]
field attr_deprecated_reason() -> i32 { 0 }
#[graphql(deprecated = "Deprecation reason")]
fn attr_deprecated_reason() -> i32 {
0
}
/// Field description
#[deprecated(note = "Deprecation reason")]
field attr_deprecated_descr() -> i32 { 0 }
#[graphql(deprecated = "Deprecation reason")]
fn attr_deprecated_descr() -> i32 {
0
}
field with_field_result() -> FieldResult<i32> { Ok(0) }
fn with_field_result() -> FieldResult<i32> {
Ok(0)
}
field with_return() -> i32 { return 0; }
fn with_return() -> i32 {
return 0;
}
field with_return_field_result() -> FieldResult<i32> { return Ok(0); }
interfaces: [Interface]
});
fn with_return_field_result() -> FieldResult<i32> {
return Ok(0);
}
}
graphql_interface!(Interface: () |&self| {
field simple() -> i32 { 0 }
@ -247,6 +285,44 @@ fn introspect_interface_field_description() {
});
}
#[test]
fn introspect_object_field_deprecated_outer() {
run_field_info_query("Root", "deprecatedOuter", |field| {
assert_eq!(
field.get_field_value("name"),
Some(&Value::scalar("deprecatedOuter"))
);
assert_eq!(field.get_field_value("description"), Some(&Value::null()));
assert_eq!(
field.get_field_value("isDeprecated"),
Some(&Value::scalar(true))
);
assert_eq!(
field.get_field_value("deprecationReason"),
Some(&Value::null()),
);
});
}
#[test]
fn introspect_object_field_deprecated_outer_with_reason() {
run_field_info_query("Root", "deprecatedOuterWithReason", |field| {
assert_eq!(
field.get_field_value("name"),
Some(&Value::scalar("deprecatedOuterWithReason"))
);
assert_eq!(field.get_field_value("description"), Some(&Value::null()));
assert_eq!(
field.get_field_value("isDeprecated"),
Some(&Value::scalar(true))
);
assert_eq!(
field.get_field_value("deprecationReason"),
Some(&Value::scalar("Deprecation Reason")),
);
});
}
#[test]
fn introspect_object_field_deprecated() {
run_field_info_query("Root", "deprecated", |field| {

View file

@ -0,0 +1,269 @@
use super::util;
use crate::{graphql_value, EmptyMutation, RootNode};
#[derive(Default)]
struct Context {
flag1: bool,
}
impl crate::Context for Context {}
struct WithLifetime<'a> {
value: &'a str,
}
#[crate::object_internal(Context=Context)]
impl<'a> WithLifetime<'a> {
fn value(&'a self) -> &'a str {
self.value
}
}
struct WithContext;
#[crate::object_internal(Context=Context)]
impl WithContext {
fn ctx(ctx: &Context) -> bool {
ctx.flag1
}
}
#[derive(Default)]
struct Query {
b: bool,
}
#[crate::object_internal(
scalar = crate::DefaultScalarValue,
name = "Query",
context = Context,
)]
/// Query Description.
impl<'a> Query {
#[graphql(description = "With Self Description")]
fn with_self(&self) -> bool {
self.b
}
fn independent() -> i32 {
100
}
fn with_executor(_exec: &Executor<Context>) -> bool {
true
}
fn with_executor_and_self(&self, _exec: &Executor<Context>) -> bool {
true
}
fn with_context(_context: &Context) -> bool {
true
}
fn with_context_and_self(&self, _context: &Context) -> bool {
true
}
#[graphql(name = "renamed")]
fn has_custom_name() -> bool {
true
}
#[graphql(description = "attr")]
fn has_description_attr() -> bool {
true
}
/// Doc description
fn has_description_doc_comment() -> bool {
true
}
fn has_argument(arg1: bool) -> bool {
arg1
}
#[graphql(arguments(default_arg(default = true)))]
fn default_argument(default_arg: bool) -> bool {
default_arg
}
#[graphql(arguments(arg(description = "my argument description")))]
fn arg_with_description(arg: bool) -> bool {
arg
}
fn with_context_child(&self) -> WithContext {
WithContext
}
fn with_lifetime_child(&self) -> WithLifetime<'a> {
WithLifetime { value: "blub" }
}
}
#[derive(Default)]
struct Mutation;
#[crate::object_internal(context = Context)]
impl Mutation {
fn empty() -> bool {
true
}
}
#[test]
fn object_introspect() {
let res = util::run_info_query::<Query, Mutation, Context>("Query");
assert_eq!(
res,
crate::graphql_value!({
"name": "Query",
"description": "Query Description.",
"fields": [
{
"name": "withSelf",
"description": "With Self Description",
"args": [],
},
{
"name": "independent",
"description": None,
"args": [],
},
{
"name": "withExecutor",
"description": None,
"args": [],
},
{
"name": "withExecutorAndSelf",
"description": None,
"args": [],
},
{
"name": "withContext",
"description": None,
"args": [],
},
{
"name": "withContextAndSelf",
"description": None,
"args": [],
},
{
"name": "renamed",
"description": None,
"args": [],
},
{
"name": "hasDescriptionAttr",
"description": "attr",
"args": [],
},
{
"name": "hasDescriptionDocComment",
"description": "Doc description",
"args": [],
},
{
"name": "hasArgument",
"description": None,
"args": [
{
"name": "arg1",
"description": None,
"type": {
"name": None,
},
}
],
},
{
"name": "defaultArgument",
"description": None,
"args": [
{
"name": "defaultArg",
"description": None,
"type": {
"name": "Boolean",
},
}
],
},
{
"name": "argWithDescription",
"description": None,
"args": [
{
"name": "arg",
"description": "my argument description",
"type": {
"name": None
},
}
],
},
{
"name": "withContextChild",
"description": None,
"args": [],
},
{
"name": "withLifetimeChild",
"description": None,
"args": [],
},
]
})
);
}
#[test]
fn object_query() {
let doc = r#"
query {
withSelf
independent
withExecutor
withExecutorAndSelf
withContext
withContextAndSelf
renamed
hasArgument(arg1: true)
defaultArgument
argWithDescription(arg: true)
withContextChild {
ctx
}
withLifetimeChild {
value
}
}
"#;
let schema = RootNode::new(Query { b: true }, EmptyMutation::<Context>::new());
let vars = std::collections::HashMap::new();
let (result, errs) = crate::execute(doc, None, &schema, &vars, &Context { flag1: true })
.expect("Execution failed");
assert_eq!(errs, []);
assert_eq!(
result,
graphql_value!({
"withSelf": true,
"independent": 100,
"withExecutor": true,
"withExecutorAndSelf": true,
"withContext": true,
"withContextAndSelf": true,
"renamed": true,
"hasArgument": true,
"defaultArgument": true,
"argWithDescription": true,
"withContextChild": { "ctx": true },
"withLifetimeChild": { "value": "blub" },
})
);
}

View file

@ -42,9 +42,12 @@ struct ResolversWithTrailingComma;
struct Root;
graphql_object!(Concrete: () |&self| {
field simple() -> i32 { 0 }
});
#[crate::object_internal]
impl Concrete {
fn simple() -> i32 {
0
}
}
graphql_interface!(CustomName: () as "ACustomNamedInterface" |&self| {
field simple() -> i32 { 0 }
@ -108,24 +111,40 @@ graphql_interface!(ResolversWithTrailingComma: () |&self| {
field simple() -> i32 { 0 }
});
graphql_object!(<'a> Root: () as "Root" |&self| {
field custom_name() -> CustomName { CustomName {} }
field with_lifetime() -> WithLifetime<'a> { WithLifetime { data: PhantomData } }
field with_generics() -> WithGenerics<i32> { WithGenerics { data: 123 } }
field description_first() -> DescriptionFirst { DescriptionFirst {} }
field fields_first() -> FieldsFirst { FieldsFirst {} }
field interfaces_first() -> InterfacesFirst { InterfacesFirst {} }
field commas_with_trailing() -> CommasWithTrailing { CommasWithTrailing {} }
field commas_on_meta() -> CommasOnMeta { CommasOnMeta {} }
field resolvers_with_trailing_comma() -> ResolversWithTrailingComma {
ResolversWithTrailingComma {}
#[crate::object_internal]
impl<'a> Root {
fn custom_name() -> CustomName {
CustomName {}
}
});
fn with_lifetime() -> WithLifetime<'a> {
WithLifetime { data: PhantomData }
}
fn with_generics() -> WithGenerics<i32> {
WithGenerics { data: 123 }
}
fn description_first() -> DescriptionFirst {
DescriptionFirst {}
}
fn fields_first() -> FieldsFirst {
FieldsFirst {}
}
fn interfaces_first() -> InterfacesFirst {
InterfacesFirst {}
}
fn commas_with_trailing() -> CommasWithTrailing {
CommasWithTrailing {}
}
fn commas_on_meta() -> CommasOnMeta {
CommasOnMeta {}
}
fn resolvers_with_trailing_comma() -> ResolversWithTrailingComma {
ResolversWithTrailingComma {}
}
}
fn run_type_info_query<F>(type_name: &str, f: F)
where

View file

@ -1,6 +1,8 @@
mod args;
mod field;
mod impl_object;
mod interface;
mod object;
mod scalar;
mod union;
mod util;

View file

@ -18,41 +18,29 @@ Syntax to validate:
*/
struct Interface;
struct CustomName;
graphql_object!(CustomName: () as "ACustomNamedType" |&self| {
field simple() -> i32 { 0 }
});
#[allow(dead_code)]
struct WithLifetime<'a> {
data: PhantomData<&'a i32>,
}
graphql_object!(<'a> WithLifetime<'a>: () as "WithLifetime" |&self| {
field simple() -> i32 { 0 }
});
#[allow(dead_code)]
struct WithGenerics<T> {
data: T,
}
struct DescriptionFirst;
struct FieldsFirst;
struct InterfacesFirst;
struct CommasWithTrailing;
struct CommasOnMeta;
struct Root;
graphql_object!(CustomName: () as "ACustomNamedType" |&self| {
field simple() -> i32 { 0 }
});
graphql_object!(<'a> WithLifetime<'a>: () as "WithLifetime" |&self| {
field simple() -> i32 { 0 }
});
graphql_object!(<T> WithGenerics<T>: () as "WithGenerics" |&self| {
field simple() -> i32 { 0 }
});
struct Interface;
struct DescriptionFirst;
graphql_interface!(Interface: () |&self| {
field simple() -> i32 { 0 }
@ -60,7 +48,6 @@ graphql_interface!(Interface: () |&self| {
DescriptionFirst => Some(DescriptionFirst {}),
}
});
graphql_object!(DescriptionFirst: () |&self| {
description: "A description"
@ -69,6 +56,7 @@ graphql_object!(DescriptionFirst: () |&self| {
interfaces: [Interface]
});
struct FieldsFirst;
graphql_object!(FieldsFirst: () |&self| {
field simple() -> i32 { 0 }
@ -77,6 +65,7 @@ graphql_object!(FieldsFirst: () |&self| {
interfaces: [Interface]
});
struct InterfacesFirst;
graphql_object!(InterfacesFirst: ()|&self| {
interfaces: [Interface]
@ -85,6 +74,7 @@ graphql_object!(InterfacesFirst: ()|&self| {
description: "A description"
});
struct CommasWithTrailing;
graphql_object!(CommasWithTrailing: () |&self| {
interfaces: [Interface],
@ -93,6 +83,7 @@ graphql_object!(CommasWithTrailing: () |&self| {
description: "A description",
});
struct CommasOnMeta;
graphql_object!(CommasOnMeta: () |&self| {
interfaces: [Interface],
description: "A description",
@ -100,6 +91,8 @@ graphql_object!(CommasOnMeta: () |&self| {
field simple() -> i32 { 0 }
});
struct Root;
struct InnerContext;
impl Context for InnerContext {}

View file

@ -78,12 +78,21 @@ graphql_scalar!(ScalarDescription {
}
});
graphql_object!(Root: () |&self| {
field default_name() -> DefaultName { DefaultName(0) }
field other_order() -> OtherOrder { OtherOrder(0) }
field named() -> Named { Named(0) }
field scalar_description() -> ScalarDescription { ScalarDescription(0) }
});
#[crate::object_internal]
impl Root {
fn default_name() -> DefaultName {
DefaultName(0)
}
fn other_order() -> OtherOrder {
OtherOrder(0)
}
fn named() -> Named {
Named(0)
}
fn scalar_description() -> ScalarDescription {
ScalarDescription(0)
}
}
fn run_type_info_query<F>(doc: &str, f: F)
where

View file

@ -46,9 +46,12 @@ enum ResolversWithTrailingComma {
struct Root;
graphql_object!(Concrete: () |&self| {
field simple() -> i32 { 123 }
});
#[crate::object_internal]
impl Concrete {
fn simple() -> i32 {
123
}
}
graphql_union!(CustomName: () as "ACustomNamedUnion" |&self| {
instance_resolvers: |&_| {
@ -56,13 +59,13 @@ graphql_union!(CustomName: () as "ACustomNamedUnion" |&self| {
}
});
graphql_union!(<'a> WithLifetime<'a>: () as "WithLifetime" |&self| {
graphql_union!(<'a> WithLifetime<'a>: () as "WithLifetime" |&self| {
instance_resolvers: |&_| {
Concrete => match *self { WithLifetime::Int(_) => Some(Concrete) }
}
});
graphql_union!(<T> WithGenerics<T>: () as "WithGenerics" |&self| {
graphql_union!(<T> WithGenerics<T>: () as "WithGenerics" |&self| {
instance_resolvers: |&_| {
Concrete => match *self { WithGenerics::Generic(_) => Some(Concrete) }
}
@ -96,17 +99,30 @@ graphql_union!(ResolversWithTrailingComma: () |&self| {
description: "A description"
});
graphql_object!(<'a> Root: () as "Root" |&self| {
field custom_name() -> CustomName { CustomName::Concrete(Concrete) }
field with_lifetime() -> WithLifetime<'a> { WithLifetime::Int(PhantomData) }
field with_generics() -> WithGenerics<i32> { WithGenerics::Generic(123) }
field description_first() -> DescriptionFirst { DescriptionFirst::Concrete(Concrete) }
field resolvers_first() -> ResolversFirst { ResolversFirst::Concrete(Concrete) }
field commas_with_trailing() -> CommasWithTrailing { CommasWithTrailing::Concrete(Concrete) }
field resolvers_with_trailing_comma() -> ResolversWithTrailingComma {
#[crate::object_internal]
impl<'a> Root {
fn custom_name() -> CustomName {
CustomName::Concrete(Concrete)
}
fn with_lifetime() -> WithLifetime<'a> {
WithLifetime::Int(PhantomData)
}
fn with_generics() -> WithGenerics<i32> {
WithGenerics::Generic(123)
}
fn description_first() -> DescriptionFirst {
DescriptionFirst::Concrete(Concrete)
}
fn resolvers_first() -> ResolversFirst {
ResolversFirst::Concrete(Concrete)
}
fn commas_with_trailing() -> CommasWithTrailing {
CommasWithTrailing::Concrete(Concrete)
}
fn resolvers_with_trailing_comma() -> ResolversWithTrailingComma {
ResolversWithTrailingComma::Concrete(Concrete)
}
});
}
fn run_type_info_query<F>(type_name: &str, f: F)
where

View file

@ -0,0 +1,54 @@
use crate::{DefaultScalarValue, GraphQLType, RootNode, Value, Variables};
use std::default::Default;
pub fn run_query<Query, Mutation, Context>(query: &str) -> Value
where
Query: GraphQLType<DefaultScalarValue, TypeInfo = (), Context = Context> + Default,
Mutation: GraphQLType<DefaultScalarValue, TypeInfo = (), Context = Context> + Default,
Context: Default,
{
let schema = RootNode::new(Query::default(), Mutation::default());
let (result, errs) =
crate::execute(query, None, &schema, &Variables::new(), &Context::default())
.expect("Execution failed");
assert_eq!(errs, []);
result
}
pub fn run_info_query<Query, Mutation, Context>(type_name: &str) -> Value
where
Query: GraphQLType<DefaultScalarValue, TypeInfo = (), Context = Context> + Default,
Mutation: GraphQLType<DefaultScalarValue, TypeInfo = (), Context = Context> + Default,
Context: Default,
{
let query = format!(
r#"
{{
__type(name: "{}") {{
name,
description,
fields {{
name
description
args {{
name
description
type {{
name
}}
}}
}}
}}
}}
"#,
type_name
);
let result = run_query::<Query, Mutation, Context>(&query);
result
.as_object_value()
.expect("Result is not an object")
.get_field_value("__type")
.expect("__type field missing")
.clone()
}

View file

@ -31,27 +31,31 @@ struct Foo {
struct Query;
graphql_object!(Query: () where Scalar = <S> |&self| {
field int_field() -> i32 {
#[crate::object_internal(Scalar = S)]
impl<'a, S> Query
where
S: crate::ScalarValue + 'a,
{
fn int_field() -> i32 {
42
}
field float_field() -> f64 {
fn float_field() -> f64 {
3.14
}
field string_field() -> String {
fn string_field() -> String {
"".into()
}
field bool_field() -> bool {
fn bool_field() -> bool {
true
}
field enum_field(_foo: Foo) -> Enum {
fn enum_field(_foo: Foo) -> Enum {
Enum::EnumValue
}
});
}
fn scalar_meta<T>(name: &'static str) -> MetaType<DefaultScalarValue>
where

View file

@ -73,52 +73,68 @@ where
}
}
graphql_object!(<'a> SchemaType<'a, S>: SchemaType<'a, S> as "__Schema"
where Scalar = <S: 'a> |&self|
#[crate::object_internal(
name = "__Schema"
Context = SchemaType<'a, S>,
Scalar = S,
)]
impl<'a, S> SchemaType<'a, S>
where
S: crate::ScalarValue + 'a,
{
field types() -> Vec<TypeType<S>> {
fn types(&self) -> Vec<TypeType<S>> {
self.type_list()
.into_iter()
.filter(|t| t.to_concrete().map(|t| t.name() != Some("_EmptyMutation")).unwrap_or(false))
.collect()
.filter(|t| {
t.to_concrete()
.map(|t| t.name() != Some("_EmptyMutation"))
.unwrap_or(false)
})
.collect::<Vec<_>>()
}
field query_type() -> TypeType<S> {
fn query_type(&self) -> TypeType<S> {
self.query_type()
}
field mutation_type() -> Option<TypeType<S>> {
fn mutation_type(&self) -> Option<TypeType<S>> {
self.mutation_type()
}
// Included for compatibility with the introspection query in GraphQL.js
field subscription_type() -> Option<TypeType<S>> {
fn subscription_type(&self) -> Option<TypeType<S>> {
None
}
field directives() -> Vec<&DirectiveType<S>> {
fn directives(&self) -> Vec<&DirectiveType<S>> {
self.directive_list()
}
});
}
graphql_object!(<'a> TypeType<'a, S>: SchemaType<'a, S> as "__Type"
where Scalar = <S: 'a> |&self|
#[crate::object_internal(
name = "__Type"
Context = SchemaType<'a, S>,
Scalar = S,
)]
impl<'a, S> TypeType<'a, S>
where
S: crate::ScalarValue + 'a,
{
field name() -> Option<&str> {
fn name(&self) -> Option<&str> {
match *self {
TypeType::Concrete(t) => t.name(),
_ => None,
}
}
field description() -> Option<&String> {
fn description(&self) -> Option<&String> {
match *self {
TypeType::Concrete(t) => t.description(),
_ => None,
}
}
field kind() -> TypeKind {
fn kind(&self) -> TypeKind {
match *self {
TypeType::Concrete(t) => t.type_kind(),
TypeType::List(_) => TypeKind::List,
@ -126,190 +142,238 @@ graphql_object!(<'a> TypeType<'a, S>: SchemaType<'a, S> as "__Type"
}
}
field fields(include_deprecated = false: bool) -> Option<Vec<&Field<S>>> {
#[graphql(arguments(include_deprecated(default = false)))]
fn fields(&self, include_deprecated: bool) -> Option<Vec<&Field<S>>> {
match *self {
TypeType::Concrete(&MetaType::Interface(InterfaceMeta { ref fields, .. })) |
TypeType::Concrete(&MetaType::Object(ObjectMeta { ref fields, .. })) =>
Some(fields
TypeType::Concrete(&MetaType::Interface(InterfaceMeta { ref fields, .. }))
| TypeType::Concrete(&MetaType::Object(ObjectMeta { ref fields, .. })) => Some(
fields
.iter()
.filter(|f| include_deprecated || !f.deprecation_status.is_deprecated())
.filter(|f| !f.name.starts_with("__"))
.collect()),
.collect(),
),
_ => None,
}
}
field of_type() -> Option<&Box<TypeType<S>>> {
fn of_type(&self) -> Option<&Box<TypeType<S>>> {
match *self {
TypeType::Concrete(_) => None,
TypeType::List(ref l) | TypeType::NonNull(ref l) => Some(l),
}
}
field input_fields() -> Option<&Vec<Argument<S>>> {
fn input_fields(&self) -> Option<&Vec<Argument<S>>> {
match *self {
TypeType::Concrete(&MetaType::InputObject(InputObjectMeta { ref input_fields, .. })) =>
Some(input_fields),
TypeType::Concrete(&MetaType::InputObject(InputObjectMeta {
ref input_fields,
..
})) => Some(input_fields),
_ => None,
}
}
field interfaces(&executor) -> Option<Vec<TypeType<S>>> {
fn interfaces(&self, schema: &SchemaType<'a, S>) -> Option<Vec<TypeType<S>>> {
match *self {
TypeType::Concrete(&MetaType::Object(ObjectMeta { ref interface_names, .. })) => {
let schema = executor.context();
Some(interface_names
TypeType::Concrete(&MetaType::Object(ObjectMeta {
ref interface_names,
..
})) => Some(
interface_names
.iter()
.filter_map(|n| schema.type_by_name(n))
.collect())
}
.collect(),
),
_ => None,
}
}
field possible_types(&executor) -> Option<Vec<TypeType<S>>> {
let schema = executor.context();
fn possible_types(&self, schema: &SchemaType<'a, S>) -> Option<Vec<TypeType<S>>> {
match *self {
TypeType::Concrete(&MetaType::Union(UnionMeta { ref of_type_names, .. })) => {
Some(of_type_names
TypeType::Concrete(&MetaType::Union(UnionMeta {
ref of_type_names, ..
})) => Some(
of_type_names
.iter()
.filter_map(|tn| schema.type_by_name(tn))
.collect())
}
TypeType::Concrete(&MetaType::Interface(InterfaceMeta{name: ref iface_name, .. })) => {
Some(schema.concrete_type_list()
.collect(),
),
TypeType::Concrete(&MetaType::Interface(InterfaceMeta {
name: ref iface_name,
..
})) => Some(
schema
.concrete_type_list()
.iter()
.filter_map(|&ct|
if let MetaType::Object(ObjectMeta{
.filter_map(|&ct| {
if let MetaType::Object(ObjectMeta {
ref name,
ref interface_names,
..
}) = *ct {
}) = *ct
{
if interface_names.contains(&iface_name.to_string()) {
schema.type_by_name(name)
} else { None }
} else { None }
)
.collect())
}
} else {
None
}
} else {
None
}
})
.collect(),
),
_ => None,
}
}
field enum_values(include_deprecated = false: bool) -> Option<Vec<&EnumValue>> {
#[graphql(arguments(include_deprecated(default = false)))]
fn enum_values(&self, include_deprecated: bool) -> Option<Vec<&EnumValue>> {
match *self {
TypeType::Concrete(&MetaType::Enum(EnumMeta { ref values, .. })) =>
Some(values
TypeType::Concrete(&MetaType::Enum(EnumMeta { ref values, .. })) => Some(
values
.iter()
.filter(|f| include_deprecated || !f.deprecation_status.is_deprecated())
.collect()),
.collect(),
),
_ => None,
}
}
});
}
graphql_object!(<'a> Field<'a, S>: SchemaType<'a, S> as "__Field"
where Scalar = <S: 'a> |&self|
#[crate::object_internal(
name = "__Field",
Context = SchemaType<'a, S>,
Scalar = S,
)]
impl<'a, S> Field<'a, S>
where
S: crate::ScalarValue + 'a,
{
field name() -> &String {
fn name(&self) -> &String {
&self.name
}
field description() -> &Option<String> {
fn description(&self) -> &Option<String> {
&self.description
}
field args() -> Vec<&Argument<S>> {
self.arguments.as_ref().map_or_else(Vec::new, |v| v.iter().collect())
fn args(&self) -> Vec<&Argument<S>> {
self.arguments
.as_ref()
.map_or_else(Vec::new, |v| v.iter().collect())
}
field type(&executor) -> TypeType<S> {
executor.context().make_type(&self.field_type)
#[graphql(name = "type")]
fn _type(&self, context: &SchemaType<'a, S>) -> TypeType<S> {
context.make_type(&self.field_type)
}
field is_deprecated() -> bool {
fn is_deprecated(&self) -> bool {
self.deprecation_status.is_deprecated()
}
field deprecation_reason() -> Option<&String> {
fn deprecation_reason(&self) -> Option<&String> {
self.deprecation_status.reason()
}
});
}
graphql_object!(<'a> Argument<'a, S>: SchemaType<'a, S> as "__InputValue"
where Scalar = <S: 'a> |&self|
#[crate::object_internal(
name = "__InputValue",
Context = SchemaType<'a, S>,
Scalar = S,
)]
impl<'a, S> Argument<'a, S>
where
S: crate::ScalarValue + 'a,
{
field name() -> &String {
fn name(&self) -> &String {
&self.name
}
field description() -> &Option<String> {
fn description(&self) -> &Option<String> {
&self.description
}
field type(&executor) -> TypeType<S> {
executor.context().make_type(&self.arg_type)
#[graphql(name = "type")]
fn _type(&self, context: &SchemaType<'a, S>) -> TypeType<S> {
context.make_type(&self.arg_type)
}
field default_value() -> Option<String> {
fn default_value(&self) -> Option<String> {
self.default_value.as_ref().map(|v| format!("{}", v))
}
});
}
graphql_object!(EnumValue: () as "__EnumValue" where Scalar = <S> |&self| {
field name() -> &String {
#[crate::object_internal(
name = "__EnumValue",
Scalar = S,
)]
impl<'a, S> EnumValue
where
S: crate::ScalarValue + 'a,
{
fn name(&self) -> &String {
&self.name
}
field description() -> &Option<String> {
fn description(&self) -> &Option<String> {
&self.description
}
field is_deprecated() -> bool {
fn is_deprecated(&self) -> bool {
self.deprecation_status.is_deprecated()
}
field deprecation_reason() -> Option<&String> {
fn deprecation_reason(&self) -> Option<&String> {
self.deprecation_status.reason()
}
});
}
graphql_object!(<'a> DirectiveType<'a, S>: SchemaType<'a, S> as "__Directive"
where Scalar = <S: 'a> |&self|
#[crate::object_internal(
name = "__Directive",
Context = SchemaType<'a, S>,
Scalar = S,
)]
impl<'a, S> DirectiveType<'a, S>
where
S: crate::ScalarValue + 'a,
{
field name() -> &String {
fn name(&self) -> &String {
&self.name
}
field description() -> &Option<String> {
fn description(&self) -> &Option<String> {
&self.description
}
field locations() -> &Vec<DirectiveLocation> {
fn locations(&self) -> &Vec<DirectiveLocation> {
&self.locations
}
field args() -> &Vec<Argument<S>> {
fn args(&self) -> &Vec<Argument<S>> {
&self.arguments
}
// Included for compatibility with the introspection query in GraphQL.js
field deprecated "Use the locations array instead"
on_operation() -> bool {
#[graphql(deprecated = "Use the locations array instead")]
fn on_operation(&self) -> bool {
self.locations.contains(&DirectiveLocation::Query)
}
// Included for compatibility with the introspection query in GraphQL.js
field deprecated "Use the locations array instead"
on_fragment() -> bool {
self.locations.contains(&DirectiveLocation::FragmentDefinition) ||
self.locations.contains(&DirectiveLocation::InlineFragment) ||
self.locations.contains(&DirectiveLocation::FragmentSpread)
#[graphql(deprecated = "Use the locations array instead")]
fn on_fragment(&self) -> bool {
self.locations
.contains(&DirectiveLocation::FragmentDefinition)
|| self.locations.contains(&DirectiveLocation::InlineFragment)
|| self.locations.contains(&DirectiveLocation::FragmentSpread)
}
// Included for compatibility with the introspection query in GraphQL.js
field deprecated "Use the locations array instead"
on_field() -> bool {
#[graphql(deprecated = "Use the locations array instead")]
fn on_field(&self) -> bool {
self.locations.contains(&DirectiveLocation::Field)
}
});
}

View file

@ -29,80 +29,93 @@ graphql_interface!(<'a> &'a Character: Database as "Character" |&self| {
}
});
graphql_object!(<'a> &'a Human: Database as "Human" |&self| {
description: "A humanoid creature in the Star Wars universe."
interfaces: [&Character]
field id() -> &str as "The id of the human"{
#[crate::object_internal(
Context = Database,
Scalar = crate::DefaultScalarValue,
interfaces = [&dyn Character],
)]
/// A humanoid creature in the Star Wars universe.
impl<'a> &'a Human {
/// The id of the human
fn id(&self) -> &str {
self.id()
}
field name() -> Option<&str> as "The name of the human" {
/// The name of the human
fn name(&self) -> Option<&str> {
Some(self.name())
}
field friends(&executor) -> Vec<&Character>
as "The friends of the human" {
executor.context().get_friends(self.as_character())
/// The friends of the human
fn friends(&self, ctx: &Database) -> Vec<&Character> {
ctx.get_friends(self.as_character())
}
field appears_in() -> &[Episode] as "Which movies they appear in" {
/// Which movies they appear in
fn appears_in(&self) -> &[Episode] {
self.appears_in()
}
field home_planet() -> &Option<String> as "The home planet of the human" {
/// The home planet of the human
fn home_planet(&self) -> &Option<String> {
self.home_planet()
}
});
}
graphql_object!(<'a> &'a Droid: Database as "Droid" |&self| {
description: "A mechanical creature in the Star Wars universe."
interfaces: [&Character]
field id() -> &str as "The id of the droid" {
#[crate::object_internal(
Context = Database,
Scalar = crate::DefaultScalarValue,
interfaces = [&dyn Character],
)]
/// A mechanical creature in the Star Wars universe.
impl<'a> &'a Droid {
/// The id of the droid
fn id(&self) -> &str {
self.id()
}
field name() -> Option<&str> as "The name of the droid" {
/// The name of the droid
fn name(&self) -> Option<&str> {
Some(self.name())
}
field friends(&executor) -> Vec<&Character>
as "The friends of the droid" {
executor.context().get_friends(self.as_character())
/// The friends of the droid
fn friends(&self, ctx: &Database) -> Vec<&Character> {
ctx.get_friends(self.as_character())
}
field appears_in() -> &[Episode] as "Which movies they appear in" {
/// Which movies they appear in
fn appears_in(&self) -> &[Episode] {
self.appears_in()
}
field primary_function() -> &Option<String> as "The primary function of the droid" {
/// The primary function of the droid
fn primary_function(&self) -> &Option<String> {
self.primary_function()
}
});
}
graphql_object!(Database: Database as "Query" |&self| {
description: "The root query object of the schema"
field human(
id: String as "id of the human"
) -> Option<&Human> {
#[crate::object_internal(
name = "Query",
Context = Database,
Scalar = crate::DefaultScalarValue,
)]
/// The root query object of the schema
impl Database {
#[graphql(arguments(id(description = "id of the human")))]
fn human(&self, id: String) -> Option<&Human> {
self.get_human(&id)
}
field droid(
id: String as "id of the droid"
) -> Option<&Droid> {
#[graphql(arguments(id(description = "id of the droid")))]
fn droid(&self, id: String) -> Option<&Droid> {
self.get_droid(&id)
}
field hero(
episode: Option<Episode> as
"If omitted, returns the hero of the whole saga. If provided, returns \
the hero of that particular episode"
) -> Option<&Character> {
#[graphql(arguments(episode(
description = "If omitted, returns the hero of the whole saga. If provided, returns the hero of that particular episode"
)))]
fn hero(&self, episode: Option<Episode>) -> Option<&Character> {
Some(self.get_hero(episode).as_character())
}
});
}

View file

@ -344,56 +344,56 @@ pub(crate) fn schema_introspection_result() -> value::Value {
{
"kind": "ENUM",
"name": "__TypeKind",
"description": "GraphQL type kind\nThe GraphQL specification defines a number of type kinds - the meta type of a type.",
"description": "GraphQL type kind\n\nThe GraphQL specification defines a number of type kinds - the meta type of a type.",
"fields": Null,
"inputFields": Null,
"interfaces": Null,
"enumValues": [
{
"name": "SCALAR",
"description": "## Scalar types\nScalar types appear as the leaf nodes of GraphQL queries. Strings, numbers, and booleans are the built in types, and while it's possible to define your own, it's relatively uncommon.",
"description": "## Scalar types\n\nScalar types appear as the leaf nodes of GraphQL queries. Strings, numbers, and booleans are the built in types, and while it's possible to define your own, it's relatively uncommon.",
"isDeprecated": false,
"deprecationReason": Null
},
{
"name": "OBJECT",
"description": "## Object types\nThe most common type to be implemented by users. Objects have fields and can implement interfaces.",
"description": "## Object types\n\nThe most common type to be implemented by users. Objects have fields and can implement interfaces.",
"isDeprecated": false,
"deprecationReason": Null
},
{
"name": "INTERFACE",
"description": "## Interface types\nInterface types are used to represent overlapping fields between multiple types, and can be queried for their concrete type.",
"description": "## Interface types\n\nInterface types are used to represent overlapping fields between multiple types, and can be queried for their concrete type.",
"isDeprecated": false,
"deprecationReason": Null
},
{
"name": "UNION",
"description": "## Union types\nUnions are similar to interfaces but can not contain any fields on their own.",
"description": "## Union types\n\nUnions are similar to interfaces but can not contain any fields on their own.",
"isDeprecated": false,
"deprecationReason": Null
},
{
"name": "ENUM",
"description": "## Enum types\nLike scalars, enum types appear as the leaf nodes of GraphQL queries.",
"description": "## Enum types\n\nLike scalars, enum types appear as the leaf nodes of GraphQL queries.",
"isDeprecated": false,
"deprecationReason": Null
},
{
"name": "INPUT_OBJECT",
"description": "## Input objects\nRepresents complex values provided in queries _into_ the system.",
"description": "## Input objects\n\nRepresents complex values provided in queries _into_ the system.",
"isDeprecated": false,
"deprecationReason": Null
},
{
"name": "LIST",
"description": "## List types\nRepresent lists of other types. This library provides implementations for vectors and slices, but other Rust types can be extended to serve as GraphQL lists.",
"description": "## List types\n\nRepresent lists of other types. This library provides implementations for vectors and slices, but other Rust types can be extended to serve as GraphQL lists.",
"isDeprecated": false,
"deprecationReason": Null
},
{
"name": "NON_NULL",
"description": "## Non-null types\nIn GraphQL, nullable types are the default. By putting a `!` after a type, it becomes non-nullable.",
"description": "## Non-null types\n\nIn GraphQL, nullable types are the default. By putting a `!` after a type, it becomes non-nullable.",
"isDeprecated": false,
"deprecationReason": Null
}
@ -827,6 +827,7 @@ pub(crate) fn schema_introspection_result() -> value::Value {
"args": [
{
"name": "id",
"description": Null,
"description": "id of the droid",
"type": {
"kind": "NON_NULL",

View file

@ -12,33 +12,33 @@ use crate::schema::meta::{Argument, MetaType};
/// GraphQL type kind
///
/// 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.
#[derive(Clone, Eq, PartialEq, Debug, GraphQLEnum)]
#[graphql(name = "__TypeKind")]
pub enum TypeKind {
/// ## Scalar types
///
/// Scalar types appear as the leaf nodes of GraphQL queries. Strings,
/// numbers, and booleans are the built in types, and while it's possible
/// Scalar types appear as the leaf nodes of GraphQL queries. Strings,\
/// numbers, and booleans are the built in types, and while it's possible\
/// to define your own, it's relatively uncommon.
Scalar,
/// ## Object types
///
/// The most common type to be implemented by users. Objects have fields
/// The most common type to be implemented by users. Objects have fields\
/// and can implement interfaces.
Object,
/// ## Interface types
///
/// Interface types are used to represent overlapping fields between
/// Interface types are used to represent overlapping fields between\
/// multiple types, and can be queried for their concrete type.
Interface,
/// ## Union types
///
/// Unions are similar to interfaces but can not contain any fields on
/// Unions are similar to interfaces but can not contain any fields on\
/// their own.
Union,
@ -55,14 +55,14 @@ pub enum TypeKind {
/// ## List types
///
/// Represent lists of other types. This library provides implementations
/// for vectors and slices, but other Rust types can be extended to serve
/// Represent lists of other types. This library provides implementations\
/// for vectors and slices, but other Rust types can be extended to serve\
/// as GraphQL lists.
List,
/// ## Non-null types
///
/// In GraphQL, nullable types are the default. By putting a `!` after a
/// In GraphQL, nullable types are the default. By putting a `!` after a\
/// type, it becomes non-nullable.
#[graphql(name = "NON_NULL")]
NonNull,

View file

@ -16,10 +16,13 @@ proc-macro = true
[dependencies]
proc-macro2 = "0.4"
syn = { version = "0.14", features = ["full", "extra-traits"] }
syn = { version = "0.15.28", features = ["full", "extra-traits", "parsing"] }
quote = "0.6"
regex = "1"
lazy_static = "1.0.0"
[dev-dependencies]
juniper = { version = "0.11.1", path = "../juniper" }
[badges]
travis-ci = { repository = "graphql-rust/juniper" }

View file

@ -1,143 +1,13 @@
use proc_macro2::{Span, TokenStream};
use proc_macro2::TokenStream;
use quote::quote;
use syn::{self, parse_quote, Data, DeriveInput, Field, Fields, Ident};
use syn::{self, Data, Fields};
use crate::util::*;
use crate::util;
#[derive(Default, Debug)]
struct ObjAttrs {
name: Option<String>,
description: Option<String>,
context: Option<Ident>,
scalar: Option<Ident>,
}
impl ObjAttrs {
fn from_input(input: &DeriveInput) -> ObjAttrs {
let mut res = ObjAttrs::default();
// Check doc comments for description.
res.description = get_doc_comment(&input.attrs);
// Check attributes for name and description.
if let Some(items) = get_graphql_attr(&input.attrs) {
for item in items {
if let Some(AttributeValue::String(val)) =
keyed_item_value(&item, "name", AttributeValidation::String)
{
if is_valid_name(&*val) {
res.name = Some(val);
continue;
} else {
panic!(
"Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but \"{}\" does not",
&*val
);
}
}
if let Some(AttributeValue::String(val)) =
keyed_item_value(&item, "description", AttributeValidation::String)
{
res.description = Some(val);
continue;
}
if let Some(AttributeValue::String(scalar)) =
keyed_item_value(&item, "scalar", AttributeValidation::String)
{
res.scalar = Some(Ident::new(&scalar as &str, Span::call_site()));
continue;
}
if let Some(AttributeValue::String(ctx)) =
keyed_item_value(&item, "Context", AttributeValidation::String)
{
res.context = Some(Ident::new(&ctx as &str, Span::call_site()));
continue;
}
panic!(format!(
"Unknown struct attribute for #[derive(GraphQLObject)]: {:?}",
item
));
}
}
res
}
}
#[derive(Default)]
struct ObjFieldAttrs {
name: Option<String>,
description: Option<String>,
deprecation: Option<DeprecationAttr>,
skip: bool,
}
impl ObjFieldAttrs {
fn from_input(variant: &Field) -> ObjFieldAttrs {
let mut res = ObjFieldAttrs::default();
// Check doc comments for description.
res.description = get_doc_comment(&variant.attrs);
// Check builtin deprecated attribute for deprecation.
res.deprecation = get_deprecated(&variant.attrs);
// Check attributes.
if let Some(items) = get_graphql_attr(&variant.attrs) {
for item in items {
if let Some(AttributeValue::String(val)) =
keyed_item_value(&item, "name", AttributeValidation::String)
{
if is_valid_name(&*val) {
res.name = Some(val);
continue;
} else {
panic!(
"Names must match /^[_a-zA-Z][_a-zA-Z0-9]*$/ but \"{}\" does not",
&*val
);
}
}
if let Some(AttributeValue::String(val)) =
keyed_item_value(&item, "description", AttributeValidation::String)
{
res.description = Some(val);
continue;
}
if let Some(AttributeValue::String(val)) =
keyed_item_value(&item, "deprecation", AttributeValidation::String)
{
res.deprecation = Some(DeprecationAttr { reason: Some(val) });
continue;
}
match keyed_item_value(&item, "deprecated", AttributeValidation::String) {
Some(AttributeValue::String(val)) => {
res.deprecation = Some(DeprecationAttr { reason: Some(val) });
continue;
}
Some(AttributeValue::Bare) => {
res.deprecation = Some(DeprecationAttr { reason: None });
continue;
}
None => {}
}
if let Some(_) = keyed_item_value(&item, "skip", AttributeValidation::Bare) {
res.skip = true;
continue;
}
panic!(format!(
"Unknown field attribute for #[derive(GraphQLObject)]: {:?}",
item
));
}
}
res
}
}
pub fn impl_object(ast: &syn::DeriveInput) -> TokenStream {
let fields = match ast.data {
Data::Struct(ref data) => match data.fields {
Fields::Named(ref fields) => fields.named.iter().collect::<Vec<_>>(),
pub fn build_derive_object(ast: syn::DeriveInput, is_internal: bool) -> TokenStream {
let struct_fields = match ast.data {
Data::Struct(data) => match data.fields {
Fields::Named(fields) => fields.named,
_ => {
panic!("#[derive(GraphQLObject)] may only be used on regular structs with fields");
}
@ -148,140 +18,63 @@ pub fn impl_object(ast: &syn::DeriveInput) -> TokenStream {
};
// Parse attributes.
let ident = &ast.ident;
let generics = &ast.generics;
let ident_name = ident.to_string();
let attrs = ObjAttrs::from_input(ast);
let attrs = match util::ObjectAttributes::from_attrs(&ast.attrs) {
Ok(a) => a,
Err(e) => {
panic!("Invalid #[graphql(...)] attribute: {}", e);
}
};
if attrs.interfaces.len() > 0 {
panic!("Invalid #[graphql(...)] attribute 'interfaces': #[derive(GraphQLObject) does not support 'interfaces'");
}
let name = attrs.name.unwrap_or(ast.ident.to_string());
let build_description = match attrs.description {
Some(s) => quote! { builder.description(#s) },
None => quote! { builder },
};
let mut meta_fields = TokenStream::new();
let mut resolvers = TokenStream::new();
let fields = struct_fields.into_iter().filter_map(|field| {
let field_attrs = match util::FieldAttributes::from_attrs(
field.attrs,
util::FieldAttributeParseMode::Object,
) {
Ok(attrs) => attrs,
Err(e) => panic!("Invalid #[graphql] attribute: \n{}", e),
};
for field in fields {
let field_ty = &field.ty;
let field_attrs = ObjFieldAttrs::from_input(field);
let field_ident = field.ident.as_ref().unwrap();
// Check if we should skip this field.
if field_attrs.skip {
continue;
None
} else {
let field_name = field.ident.unwrap();
let name = field_attrs
.name
.clone()
.unwrap_or_else(|| util::to_camel_case(&field_name.to_string()));
let resolver_code = quote!(
&self . #field_name
);
Some(util::GraphQLTypeDefinitionField {
name,
_type: field.ty,
args: Vec::new(),
description: field_attrs.description,
deprecation: field_attrs.deprecation,
resolver_code,
})
}
});
// Build value.
let name = match field_attrs.name {
Some(ref name) => {
// Custom name specified.
name.to_string()
}
None => {
// Note: auto camel casing when no custom name specified.
crate::util::to_camel_case(&field_ident.to_string())
}
};
let build_description = match field_attrs.description {
Some(s) => quote! { field.description(#s) },
None => quote! { field },
};
let build_deprecation = match field_attrs.deprecation {
Some(DeprecationAttr { reason: Some(s) }) => quote! { field.deprecated(Some(#s)) },
Some(DeprecationAttr { reason: None }) => quote! { field.deprecated(None) },
None => quote! { field },
};
meta_fields.extend(quote! {
{
let field = registry.field::<#field_ty>(#name, &());
let field = #build_description;
let field = #build_deprecation;
field
},
});
// Build from_input clause.
resolvers.extend(quote! {
#name => executor.resolve_with_ctx(&(), &self.#field_ident),
});
}
let (_, ty_generics, _) = generics.split_for_impl();
let mut generics = generics.clone();
if attrs.scalar.is_none() {
generics.params.push(parse_quote!(__S));
{
let where_clause = generics.where_clause.get_or_insert(parse_quote!(where));
where_clause
.predicates
.push(parse_quote!(__S: juniper::ScalarValue));
where_clause
.predicates
.push(parse_quote!(for<'__b> &'__b __S: juniper::ScalarRefValue<'__b>));
}
}
let scalar = attrs
.scalar
.unwrap_or_else(|| Ident::new("__S", Span::call_site()));
let ctx = attrs
.context
.map(|ident| quote!( #ident ))
.unwrap_or(quote!(()));
let (impl_generics, _, where_clause) = generics.split_for_impl();
let body = quote! {
impl#impl_generics juniper::GraphQLType<#scalar> for #ident #ty_generics
#where_clause
{
type Context = #ctx;
type TypeInfo = ();
fn name(_: &()) -> Option<&str> {
Some(#name)
}
fn concrete_type_name(&self, _: &Self::Context, _: &()) -> String {
#name.to_string()
}
fn meta<'r>(
_: &(),
registry: &mut juniper::Registry<'r, #scalar>
) -> juniper::meta::MetaType<'r, #scalar>
where #scalar: 'r
{
let fields = &[
#(#meta_fields)*
];
let builder = registry.build_object_type::<#ident>(&(), fields);
let builder = #build_description;
builder.into_meta()
}
fn resolve_field(
&self,
_: &(),
field_name: &str,
_: &juniper::Arguments<#scalar>,
executor: &juniper::Executor<Self::Context, #scalar>
) -> juniper::ExecutionResult<#scalar>
{
match field_name {
#(#resolvers)*
_ => panic!("Field {} not found on type {}", field_name, #ident_name),
}
}
}
let definition = util::GraphQLTypeDefiniton {
name,
_type: syn::parse_str(&ast.ident.to_string()).unwrap(),
context: attrs.context,
scalar: attrs.scalar,
description: attrs.description,
fields: fields.collect(),
generics: ast.generics.clone(),
interfaces: None,
include_type_generics: true,
generic_scalar: true,
};
body
let juniper_crate_name = if is_internal { "crate" } else { "juniper" };
definition.into_tokens(juniper_crate_name)
}

View file

@ -0,0 +1,229 @@
use crate::util;
use proc_macro::TokenStream;
use quote::quote;
/// Generate code for the juniper::object macro.
pub fn build_object(args: TokenStream, body: TokenStream, is_internal: bool) -> TokenStream {
let impl_attrs = match syn::parse::<util::ObjectAttributes>(args) {
Ok(attrs) => attrs,
Err(e) => {
panic!("Invalid attributes:\n{}", e);
}
};
let item = match syn::parse::<syn::Item>(body) {
Ok(item) => item,
Err(err) => {
panic!("Parsing error:\n{}", err);
}
};
let mut _impl = match item {
syn::Item::Impl(_impl) => _impl,
_ => {
panic!("#[juniper::object] can only be applied to impl blocks");
}
};
match _impl.trait_ {
Some((_, ref path, _)) => {
let name = path
.segments
.iter()
.map(|segment| segment.ident.to_string())
.collect::<Vec<_>>()
.join(".");
if !(name == "GraphQLObject" || name == "juniper.GraphQLObject") {
panic!("The impl block must implement the 'GraphQLObject' trait");
}
}
None => {
// panic!("The impl block must implement the 'GraphQLObject' trait");
}
}
let name = match impl_attrs.name.as_ref() {
Some(type_name) => type_name.clone(),
None => match &*_impl.self_ty {
syn::Type::Path(ref type_path) => type_path
.path
.segments
.iter()
.last()
.unwrap()
.ident
.to_string(),
syn::Type::Reference(ref reference) => match &*reference.elem {
syn::Type::Path(ref type_path) => type_path
.path
.segments
.iter()
.last()
.unwrap()
.ident
.to_string(),
_ => {
panic!("Could not determine a name for the object type: specify one with #[juniper::object(name = \"SomeName\")");
}
},
_ => {
panic!("Could not determine a name for the object type: specify one with #[juniper::object(name = \"SomeName\")");
}
},
};
let target_type = *_impl.self_ty.clone();
let description = impl_attrs
.description
.or(util::get_doc_comment(&_impl.attrs));
let mut definition = util::GraphQLTypeDefiniton {
name,
_type: target_type.clone(),
context: impl_attrs.context,
scalar: impl_attrs.scalar,
description,
fields: Vec::new(),
generics: _impl.generics.clone(),
interfaces: if impl_attrs.interfaces.len() > 0 {
Some(impl_attrs.interfaces)
} else {
None
},
include_type_generics: false,
generic_scalar: false,
};
for item in _impl.items {
match item {
syn::ImplItem::Method(method) => {
let _type = match &method.sig.decl.output {
syn::ReturnType::Type(_, ref t) => (**t).clone(),
syn::ReturnType::Default => {
panic!(
"Invalid field method {}: must return a value",
method.sig.ident
);
}
};
let attrs = match util::FieldAttributes::from_attrs(
method.attrs,
util::FieldAttributeParseMode::Impl,
) {
Ok(attrs) => attrs,
Err(err) => panic!(
"Invalid #[graphql(...)] attribute on field {}:\n{}",
method.sig.ident, err
),
};
let mut args = Vec::new();
let mut resolve_parts = Vec::new();
for arg in method.sig.decl.inputs {
match arg {
_self @ syn::FnArg::SelfRef(_) => {
// Can be ignored.
// "self" will already be in scope.
// resolve_args.push(quote!(self));
}
syn::FnArg::SelfValue(_) => {
panic!(
"Invalid method receiver {}(self, ...): did you mean '&self'?",
method.sig.ident
);
}
syn::FnArg::Captured(ref captured) => {
let arg_ident = match &captured.pat {
syn::Pat::Ident(ref pat_ident) => &pat_ident.ident,
_ => {
panic!("Invalid token for function argument");
}
};
let arg_name = arg_ident.to_string();
let context_type = definition.context.as_ref();
// Check for executor arguments.
if util::type_is_identifier_ref(&captured.ty, "Executor") {
resolve_parts.push(quote!(let #arg_ident = executor;));
}
// Make sure executor is specified as a reference.
else if util::type_is_identifier(&captured.ty, "Executor") {
panic!("Invalid executor argument: to access the Executor, you need to specify the type as a reference.\nDid you mean &Executor?");
}
// Check for executor arg.
else if context_type
.clone()
.map(|ctx| util::type_is_ref_of(&captured.ty, ctx))
.unwrap_or(false)
{
resolve_parts.push(quote!( let #arg_ident = executor.context(); ));
}
// Make sure the user does not specify the Context
// without a reference. (&Context)
else if context_type
.clone()
.map(|ctx| ctx == &captured.ty)
.unwrap_or(false)
{
panic!(
"Invalid context argument: to access the context, you need to specify the type as a reference.\nDid you mean &{}?",
quote!(captured.ty),
);
} else {
let ty = &captured.ty;
// TODO: respect graphql attribute overwrite.
let final_name = util::to_camel_case(&arg_name);
resolve_parts.push(quote!(
let #arg_ident = args
.get::<#ty>(#final_name)
.expect(&format!("Internal error: missing argument {} - validation must have failed", #final_name));
));
args.push(util::GraphQLTypeDefinitionFieldArg {
description: attrs.argument(&arg_name).and_then(|arg| {
arg.description.as_ref().map(|d| d.value())
}),
default: attrs
.argument(&arg_name)
.and_then(|arg| arg.default.clone()),
_type: ty.clone(),
name: final_name,
})
}
}
_ => panic!("Invalid argument type in method {}", method.sig.ident),
}
}
let body = &method.block;
let return_ty = &method.sig.decl.output;
let resolver_code = quote!(
(|| #return_ty {
#( #resolve_parts )*
#body
})()
);
let name = attrs
.name
.unwrap_or(util::to_camel_case(&method.sig.ident.to_string()));
definition.fields.push(util::GraphQLTypeDefinitionField {
name,
_type,
args,
description: attrs.description,
deprecation: attrs.deprecation,
resolver_code,
});
}
_ => {
panic!("Invalid item for GraphQL Object: only type declarations and methods are allowed");
}
}
}
let juniper_crate_name = if is_internal { "crate" } else { "juniper" };
definition.into_tokens(juniper_crate_name).into()
}

View file

@ -12,6 +12,7 @@ mod derive_enum;
mod derive_input_object;
mod derive_object;
mod derive_scalar_value;
mod impl_object;
mod util;
use proc_macro::TokenStream;
@ -49,7 +50,7 @@ pub fn derive_input_object_internal(input: TokenStream) -> TokenStream {
#[proc_macro_derive(GraphQLObject, attributes(graphql))]
pub fn derive_object(input: TokenStream) -> TokenStream {
let ast = syn::parse::<syn::DeriveInput>(input).unwrap();
let gen = derive_object::impl_object(&ast);
let gen = derive_object::build_derive_object(ast, false);
gen.into()
}
@ -73,3 +74,252 @@ pub fn derive_scalar_value_internal(input: TokenStream) -> TokenStream {
let gen = derive_scalar_value::impl_scalar_value(&ast, true);
gen.into()
}
/**
The `object` proc macro is the primary way of defining GraphQL resolvers
that can not be implemented with the GraphQLObject derive.
It enables you to write GraphQL field resolvers for a type by declaring a
regular Rust `impl` block. Under the hood, the procedural macro implements
the GraphQLType trait.
`object` comes with many features that allow customization of
your fields, all of which are detailed below.
### Getting Started
This simple example will show you the most basic use of `object`.
More advanced use cases are introduced step by step.
```
// So we can declare it as a plain struct without any members.
struct Query;
// We prefix the impl Block with the procedural macro.
#[juniper::object]
impl Query {
// A **warning**: only GraphQL fields can be specified in this impl block.
// If you want to define normal methods on the struct,
// you have to do so in a separate, normal `impl` block.
// This defines a simple, static field which does not require any context.
// You can return any value that implements the `GraphQLType` trait.
// This trait is implemented for:
// - basic scalar types like bool, &str, String, i32, f64
// - GraphQL compatible wrappers like Option<_>, Vec<_>.
// - types which use the `#derive[juniper::GraphQLObject]`
// - `object` structs.
//
// An important note regarding naming:
// By default, field names will be converted to camel case.
// For your GraphQL queries, the field will be available as `apiVersion`.
//
// You can also manually customize the field name if required. (See below)
fn api_version() -> &'static str {
"0.1"
}
// This field takes two arguments.
// GraphQL arguments are just regular function parameters.
// **Note**: in Juniper, arguments are non-nullable by default.
// for optional arguments, you have to specify them with Option<T>.
fn add(a: f64, b: f64, c: Option<f64>) -> f64 {
a + b + c.unwrap_or(0.0)
}
}
```
## Accessing self
```
struct Person {
first_name: String,
last_name: String,
}
impl Person {
// The full name method is useful outside of GraphQL,
// so we define it as a normal method.
fn build_full_name(&self) -> String {
format!("{} {}", self.first_name, self.last_name)
}
}
#[juniper::object]
impl Person {
fn first_name(&self) -> &str {
&self.first_name
}
fn last_name(&self) -> &str {
&self.last_name
}
fn full_name(&self) -> String {
self.build_full_name()
}
}
```
## Context (+ Executor)
You can specify a context that will be available across
all your resolvers during query execution.
The Context can be injected into your resolvers by just
specifying an argument with the same type as the context
(but as a reference).
```
# #[derive(juniper::GraphQLObject)] struct User { id: i32 }
# struct DbPool;
# impl DbPool { fn user(&self, id: i32) -> Option<User> { unimplemented!() } }
struct Context {
db: DbPool,
}
// Mark our struct for juniper.
impl juniper::Context for Context {}
struct Query;
#[juniper::object(
// Here we specify the context type for this object.
Context = Context,
)]
impl Query {
// Context is injected by specifying a argument
// as a reference to the Context.
fn user(context: &Context, id: i32) -> Option<User> {
context.db.user(id)
}
// You can also gain access to the executor, which
// allows you to do look aheads.
fn with_executor(executor: &Executor) -> bool {
let info = executor.look_ahead();
// ...
true
}
}
```
## Customization (Documentation, Renaming, ...)
```
struct InternalQuery;
// Doc comments can be used to specify graphql documentation.
/// GRAPHQL DOCUMENTATION.
/// More info for GraphQL users....
#[juniper::object(
// You can rename the type for GraphQL by specifying the name here.
name = "Query",
// You can also specify a description here.
// If present, doc comments will be ignored.
description = "...",
)]
impl InternalQuery {
// Documentation doc comments also work on fields.
/// GraphQL description...
fn field_with_description() -> bool { true }
// Fields can also be customized with the #[graphql] attribute.
#[graphql(
// overwrite the public name
name = "actualFieldName",
// Can be used instead of doc comments.
description = "field description",
)]
fn internal_name() -> bool { true }
// Fields can be deprecated too.
#[graphql(
deprecated = "deprecatin info...",
// Note: just "deprecated," without a description works too.
)]
fn deprecated_field_simple() -> bool { true }
// Customizing field arguments is a little awkward right now.
// This will improve once [RFC 2564](https://github.com/rust-lang/rust/issues/60406)
// is implemented, which will allow attributes on function parameters.
#[graphql(
arguments(
arg1(
// You can specify default values.
// A default can be any valid expression that yields the right type.
default = true,
description = "Argument description....",
),
arg2(
default = false,
description = "arg2 description...",
),
),
)]
fn args(arg1: bool, arg2: bool) -> bool {
arg1 && arg2
}
}
```
## Lifetimes, Generics and custom Scalars
Lifetimes work just like you'd expect.
```
struct WithLifetime<'a> {
value: &'a str,
}
#[juniper::object]
impl<'a> WithLifetime<'a> {
fn value(&self) -> &str {
self.value
}
}
```
Juniper has support for custom scalars.
Mostly you will only need the default scalar type juniper::DefaultScalarValue.
You can easily specify a custom scalar though.
```
# type MyCustomScalar = juniper::DefaultScalarValue;
struct Query;
#[juniper::object(
Scalar = MyCustomScalar,
)]
impl Query {
// ...
}
```
*/
#[proc_macro_attribute]
pub fn object(args: TokenStream, input: TokenStream) -> TokenStream {
let gen = impl_object::build_object(args, input, false);
gen.into()
}
/// A proc macro for defining a GraphQL object.
#[doc(hidden)]
#[proc_macro_attribute]
pub fn object_internal(args: TokenStream, input: TokenStream) -> TokenStream {
let gen = impl_object::build_object(args, input, true);
gen.into()
}

View file

@ -1,9 +1,44 @@
use quote::quote;
use regex::Regex;
use syn::{Attribute, Lit, Meta, MetaList, MetaNameValue, NestedMeta};
use std::collections::HashMap;
use syn::{
parse, parse_quote, punctuated::Punctuated, Attribute, Lit, Meta, MetaList, MetaNameValue,
NestedMeta, Token,
};
/// Compares a path to a one-segment string value,
/// return true if equal.
pub fn path_eq_single(path: &syn::Path, value: &str) -> bool {
path.segments.len() == 1 && path.segments[0].ident == value
}
/// Check if a type is a reference to another type.
pub fn type_is_ref_of(ty: &syn::Type, target: &syn::Type) -> bool {
match ty {
syn::Type::Reference(_ref) => &*_ref.elem == target,
_ => false,
}
}
/// Check if a Type is a simple identifier.
pub fn type_is_identifier(ty: &syn::Type, name: &str) -> bool {
match ty {
syn::Type::Path(ref type_path) => path_eq_single(&type_path.path, name),
_ => false,
}
}
/// Check if a Type is a reference to a given identifier.
pub fn type_is_identifier_ref(ty: &syn::Type, name: &str) -> bool {
match ty {
syn::Type::Reference(_ref) => type_is_identifier(&*_ref.elem, name),
_ => false,
}
}
pub enum AttributeValidation {
Any,
Bare,
// Bare,
String,
}
@ -12,10 +47,17 @@ pub enum AttributeValue {
String(String),
}
#[derive(Debug)]
pub struct DeprecationAttr {
pub reason: Option<String>,
}
pub fn find_graphql_attr(attrs: &Vec<Attribute>) -> Option<&Attribute> {
attrs
.iter()
.find(|attr| path_eq_single(&attr.path, "graphql"))
}
pub fn get_deprecated(attrs: &Vec<Attribute>) -> Option<DeprecationAttr> {
for attr in attrs {
match attr.interpret_meta() {
@ -34,14 +76,23 @@ pub fn get_deprecated(attrs: &Vec<Attribute>) -> Option<DeprecationAttr> {
fn get_deprecated_meta_list(list: &MetaList) -> DeprecationAttr {
for meta in &list.nested {
match meta {
&NestedMeta::Meta(Meta::NameValue(ref nv)) if nv.ident == "note" => match &nv.lit {
&Lit::Str(ref strlit) => {
return DeprecationAttr {
reason: Some(strlit.value().to_string()),
};
&NestedMeta::Meta(Meta::NameValue(ref nv)) => {
if nv.ident == "note" {
match &nv.lit {
&Lit::Str(ref strlit) => {
return DeprecationAttr {
reason: Some(strlit.value()),
};
}
_ => panic!("deprecated attribute note value only has string literal"),
}
} else {
panic!(
"Unrecognized setting on #[deprecated(..)] attribute: {}",
nv.ident
);
}
_ => panic!("deprecated attribute note value only has string literal"),
},
}
_ => {}
}
}
@ -60,35 +111,57 @@ pub fn get_doc_comment(attrs: &Vec<Attribute>) -> Option<String> {
// Concatenates doc strings into one string.
fn join_doc_strings(docs: &Vec<String>) -> String {
let s: String = docs
.iter()
// Trim any extra spaces.
.map(|x| x.trim().to_string())
// Convert empty comments to newlines.
.map(|x| if x == "" { "\n".to_string() } else { x.clone() })
.collect::<Vec<String>>()
.join(" ");
// Clean up spacing on empty lines.
s.replace(" \n ", "\n")
// Note: this is guaranteed since this function is only called
// from get_doc_strings().
debug_assert!(docs.len() > 0);
let last_index = docs.len() - 1;
docs.iter()
.map(|s| s.as_str().trim_end())
// Trim leading space.
.map(|s| {
if s.chars().next() == Some(' ') {
&s[1..]
} else {
s
}
})
// Add newline, exept when string ends in a continuation backslash or is the last line.
.enumerate()
.fold(String::new(), |mut buffer, (index, s)| {
if index == last_index {
buffer.push_str(s);
} else if s.ends_with('\\') {
buffer.push_str(s.trim_end_matches('\\'));
buffer.push(' ');
} else {
buffer.push_str(s);
buffer.push('\n');
}
buffer
})
}
// Gets doc strings from doc comment attributes.
fn get_doc_strings(items: &Vec<MetaNameValue>) -> Option<Vec<String>> {
let mut docs = Vec::new();
for item in items {
if item.ident == "doc" {
match item.lit {
Lit::Str(ref strlit) => {
docs.push(strlit.value().to_string());
let comments = items
.iter()
.filter_map(|item| {
if item.ident == "doc" {
match item.lit {
Lit::Str(ref strlit) => Some(strlit.value().to_string()),
_ => panic!("doc attributes only have string literal"),
}
_ => panic!("doc attributes only have string literal"),
} else {
None
}
}
})
.collect::<Vec<_>>();
if comments.len() > 0 {
Some(comments)
} else {
None
}
if !docs.is_empty() {
return Some(docs);
}
None
}
// Gets doc comment attributes.
@ -130,12 +203,12 @@ pub fn keyed_item_value(
match &nameval.lit {
// We have a string attribute value.
&Lit::Str(ref strlit) => match validation {
AttributeValidation::Bare => {
panic!(format!(
"Invalid format for attribute \"{:?}\": expected a bare attribute without a value",
item
));
}
// AttributeValidation::Bare => {
// panic!(format!(
// "Invalid format for attribute \"{:?}\": expected a bare attribute without a value",
// item
// ));
// }
_ => Some(AttributeValue::String(strlit.value())),
},
_ => None,
@ -212,6 +285,567 @@ pub fn is_valid_name(field_name: &str) -> bool {
GRAPHQL_NAME_SPEC.is_match(field_name)
}
#[derive(Default, Debug)]
pub struct ObjectAttributes {
pub name: Option<String>,
pub description: Option<String>,
pub context: Option<syn::Type>,
pub scalar: Option<syn::Type>,
pub interfaces: Vec<syn::Type>,
}
impl syn::parse::Parse for ObjectAttributes {
fn parse(input: syn::parse::ParseStream) -> syn::parse::Result<Self> {
let mut output = Self {
name: None,
description: None,
context: None,
scalar: None,
interfaces: Vec::new(),
};
// Skip potential parantheses which are present for regular attributes but not for proc macro
// attributes.
let inner = (|| {
let mut content;
syn::parenthesized!(content in input);
Ok(content)
})();
let input = match inner.as_ref() {
Ok(content) => content,
Err(_) => input,
};
while !input.is_empty() {
let ident: syn::Ident = input.parse()?;
match ident.to_string().as_str() {
"name" => {
input.parse::<syn::Token![=]>()?;
let val = input.parse::<syn::LitStr>()?;
output.name = Some(val.value());
}
"description" => {
input.parse::<syn::Token![=]>()?;
let val = input.parse::<syn::LitStr>()?;
output.description = Some(val.value());
}
"context" | "Context" => {
input.parse::<syn::Token![=]>()?;
// TODO: remove legacy support for string based Context.
let ctx = if let Ok(val) = input.parse::<syn::LitStr>() {
eprintln!("DEPRECATION WARNING: using a string literal for the Context is deprecated");
eprintln!("Use a normal type instead - example: 'Context = MyContextType'");
syn::parse_str::<syn::Type>(&val.value())?
} else {
input.parse::<syn::Type>()?
};
output.context = Some(ctx);
}
"scalar" | "Scalar" => {
input.parse::<syn::Token![=]>()?;
let val = input.parse::<syn::Type>()?;
output.scalar = Some(val);
}
"interfaces" => {
input.parse::<syn::Token![=]>()?;
let mut content;
syn::bracketed!(content in input);
output.interfaces =
syn::punctuated::Punctuated::<syn::Type, syn::Token![,]>::parse_terminated(
&content,
)?
.into_iter()
.collect();
}
other => {
return Err(input.error(format!("Unknown attribute: {}", other)));
}
}
if input.lookahead1().peek(syn::Token![,]) {
input.parse::<syn::Token![,]>()?;
}
}
Ok(output)
}
}
impl ObjectAttributes {
pub fn from_attrs(attrs: &Vec<syn::Attribute>) -> syn::parse::Result<Self> {
let attr_opt = find_graphql_attr(attrs);
if let Some(attr) = attr_opt {
// Need to unwrap outer (), which are not present for proc macro attributes,
// but are present for regular ones.
let mut a = syn::parse::<Self>(attr.tts.clone().into())?;
if a.description.is_none() {
a.description = get_doc_comment(attrs);
}
Ok(a)
} else {
let mut a = Self::default();
a.description = get_doc_comment(attrs);
Ok(a)
}
}
}
#[derive(Debug)]
pub struct FieldAttributeArgument {
pub name: syn::Ident,
pub default: Option<syn::Expr>,
pub description: Option<syn::LitStr>,
}
impl parse::Parse for FieldAttributeArgument {
fn parse(input: parse::ParseStream) -> parse::Result<Self> {
let name = input.parse()?;
let mut arg = Self {
name,
default: None,
description: None,
};
let mut content;
syn::parenthesized!(content in input);
while !content.is_empty() {
let name = content.parse::<syn::Ident>()?;
content.parse::<Token![=]>()?;
match name.to_string().as_str() {
"description" => {
arg.description = Some(content.parse()?);
}
"default" => {
arg.default = Some(content.parse()?);
}
other => {
return Err(content.error(format!("Invalid attribute argument key {}", other)));
}
}
// Discard trailing comma.
content.parse::<Token![,]>().ok();
}
Ok(arg)
}
}
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub enum FieldAttributeParseMode {
Object,
Impl,
}
enum FieldAttribute {
Name(syn::LitStr),
Description(syn::LitStr),
Deprecation(DeprecationAttr),
Skip(syn::Ident),
Arguments(HashMap<String, FieldAttributeArgument>),
}
impl parse::Parse for FieldAttribute {
fn parse(input: parse::ParseStream) -> parse::Result<Self> {
let ident = input.parse::<syn::Ident>()?;
match ident.to_string().as_str() {
"name" => {
input.parse::<Token![=]>()?;
let lit = input.parse::<syn::LitStr>()?;
let raw = lit.value();
if !is_valid_name(&raw) {
Err(input.error(format!(
"Invalid #[graphql(name = ...)] attribute: \n\
'{}' is not a valid field name\nNames must \
match /^[_a-zA-Z][_a-zA-Z0-9]*$/",
raw,
)))
} else {
Ok(FieldAttribute::Name(lit))
}
}
"description" => {
input.parse::<Token![=]>()?;
Ok(FieldAttribute::Description(input.parse()?))
}
"deprecated" | "deprecation" => {
let reason = if input.peek(Token![=]) {
input.parse::<Token![=]>()?;
Some(input.parse::<syn::LitStr>()?.value())
} else {
None
};
Ok(FieldAttribute::Deprecation(DeprecationAttr {
reason: reason,
}))
}
"skip" => Ok(FieldAttribute::Skip(ident)),
"arguments" => {
let mut arg_content;
syn::parenthesized!(arg_content in input);
let args = Punctuated::<FieldAttributeArgument, Token![,]>::parse_terminated(
&arg_content,
)?;
let map = args
.into_iter()
.map(|arg| (arg.name.to_string(), arg))
.collect();
Ok(FieldAttribute::Arguments(map))
}
other => Err(input.error(format!("Unknown attribute: {}", other))),
}
}
}
#[derive(Default)]
pub struct FieldAttributes {
pub name: Option<String>,
pub description: Option<String>,
pub deprecation: Option<DeprecationAttr>,
// Only relevant for GraphQLObject derive.
pub skip: bool,
/// Only relevant for object macro.
pub arguments: HashMap<String, FieldAttributeArgument>,
}
impl parse::Parse for FieldAttributes {
fn parse(input: syn::parse::ParseStream) -> syn::parse::Result<Self> {
// Remove wrapping parantheses.
let mut content;
syn::parenthesized!(content in input);
let items = Punctuated::<FieldAttribute, Token![,]>::parse_terminated(&content)?;
let mut output = Self {
name: None,
description: None,
deprecation: None,
skip: false,
arguments: Default::default(),
};
for item in items {
match item {
FieldAttribute::Name(name) => {
output.name = Some(name.value());
}
FieldAttribute::Description(name) => {
output.description = Some(name.value());
}
FieldAttribute::Deprecation(attr) => {
output.deprecation = Some(attr);
}
FieldAttribute::Skip(_) => {
output.skip = true;
}
FieldAttribute::Arguments(args) => {
output.arguments = args;
}
}
}
if !content.is_empty() {
Err(content.error("Unexpected input"))
} else {
Ok(output)
}
}
}
impl FieldAttributes {
pub fn from_attrs(
attrs: Vec<syn::Attribute>,
_mode: FieldAttributeParseMode,
) -> syn::parse::Result<Self> {
let doc_comment = get_doc_comment(&attrs);
let deprecation = get_deprecated(&attrs);
let attr_opt = attrs
.into_iter()
.find(|attr| path_eq_single(&attr.path, "graphql"));
let mut output = match attr_opt {
Some(attr) => syn::parse(attr.tts.into())?,
None => Self::default(),
};
// Check for regular doc comment.
if output.description.is_none() {
output.description = doc_comment;
}
if output.deprecation.is_none() {
output.deprecation = deprecation;
}
Ok(output)
}
pub fn argument(&self, name: &str) -> Option<&FieldAttributeArgument> {
self.arguments.get(name)
}
}
#[derive(Debug)]
pub struct GraphQLTypeDefinitionFieldArg {
pub name: String,
pub description: Option<String>,
pub default: Option<syn::Expr>,
pub _type: syn::Type,
}
#[derive(Debug)]
pub struct GraphQLTypeDefinitionField {
pub name: String,
pub _type: syn::Type,
pub description: Option<String>,
pub deprecation: Option<DeprecationAttr>,
pub args: Vec<GraphQLTypeDefinitionFieldArg>,
pub resolver_code: proc_macro2::TokenStream,
}
/// Definition of a graphql type based on information extracted
/// by various macros.
/// The definition can be rendered to Rust code.
#[derive(Debug)]
pub struct GraphQLTypeDefiniton {
pub name: String,
pub _type: syn::Type,
pub context: Option<syn::Type>,
pub scalar: Option<syn::Type>,
pub description: Option<String>,
pub fields: Vec<GraphQLTypeDefinitionField>,
pub generics: syn::Generics,
pub interfaces: Option<Vec<syn::Type>>,
// Due to syn parsing differences,
// when parsing an impl the type generics are included in the type
// directly, but in syn::DeriveInput, the type generics are
// in the generics field.
// This flag signifies if the type generics need to be
// included manually.
pub include_type_generics: bool,
// This flag indicates if the generated code should always be
// generic over the ScalarValue.
// If false, the scalar is only generic if a generic parameter
// is specified manually.
pub generic_scalar: bool,
}
impl GraphQLTypeDefiniton {
pub fn into_tokens(self, juniper_crate_name: &str) -> proc_macro2::TokenStream {
let juniper_crate_name = syn::parse_str::<syn::Path>(juniper_crate_name).unwrap();
let name = &self.name;
let ty = &self._type;
let context = self
.context
.as_ref()
.map(|ctx| quote!( #ctx ))
.unwrap_or(quote!(()));
let field_definitions = self.fields.iter().map(|field| {
let args = field.args.iter().map(|arg| {
let arg_type = &arg._type;
let arg_name = &arg.name;
let description = match arg.description.as_ref() {
Some(value) => quote!( .description( #value ) ),
None => quote!(),
};
let code = match arg.default.as_ref() {
Some(value) => quote!(
.argument(
registry.arg_with_default::<#arg_type>(#arg_name, &#value, info)
#description
)
),
None => quote!(
.argument(
registry.arg::<#arg_type>(#arg_name, info)
#description
)
),
};
code
});
let description = match field.description.as_ref() {
Some(description) => quote!( .description(#description) ),
None => quote!(),
};
let deprecation = match field.deprecation.as_ref() {
Some(deprecation) => {
if let Some(reason) = deprecation.reason.as_ref() {
quote!( .deprecated(Some(#reason)) )
} else {
quote!( .deprecated(None) )
}
}
None => quote!(),
};
let field_name = &field.name;
let _type = &field._type;
quote! {
registry
.field_convert::<#_type, _, Self::Context>(#field_name, info)
#(#args)*
#description
#deprecation
}
});
let resolve_matches = self.fields.iter().map(|field| {
let name = &field.name;
let code = &field.resolver_code;
quote!(
#name => {
let res = { #code };
#juniper_crate_name::IntoResolvable::into(
res,
executor.context()
)
.and_then(|res| {
match res {
Some((ctx, r)) => executor.replaced_context(ctx).resolve_with_ctx(&(), &r),
None => Ok(#juniper_crate_name::Value::null()),
}
})
},
)
});
let description = self
.description
.as_ref()
.map(|description| quote!( .description(#description) ));
let interfaces = self.interfaces.as_ref().map(|items| {
quote!(
.interfaces(&[
#( registry.get_type::< #items >(&()) ,)*
])
)
});
let scalar = self
.scalar
.as_ref()
.map(|s| quote!( #s ))
.unwrap_or_else(|| {
if self.generic_scalar {
// If generic_scalar is true, we always insert a generic scalar.
// See more comments below.
quote!(__S)
} else {
quote!(#juniper_crate_name::DefaultScalarValue)
}
});
// Preserve the original type_generics before modification,
// since alteration makes them invalid if self.generic_scalar
// is specified.
let (_, type_generics, _) = self.generics.split_for_impl();
let mut generics = self.generics.clone();
if self.scalar.is_some() {
// A custom scalar type was specified.
// Therefore, we always insert a where clause that marks the scalar as
// compatible with ScalarValueRef.
// This is done to prevent the user from having to specify this
// manually.
let where_clause = generics.where_clause.get_or_insert(parse_quote!(where));
where_clause.predicates.push(
parse_quote!(for<'__b> &'__b #scalar: #juniper_crate_name::ScalarRefValue<'__b>),
);
} else if self.generic_scalar {
// No custom scalar specified, but always generic specified.
// Therefore we inject the generic scalar.
generics.params.push(parse_quote!(__S));
let where_clause = generics.where_clause.get_or_insert(parse_quote!(where));
// Insert ScalarValue constraint.
where_clause
.predicates
.push(parse_quote!(__S: #juniper_crate_name::ScalarValue));
// Insert a where clause that marks the scalar as
// compatible with ScalarValueRef.
// Same as in branch above.
where_clause
.predicates
.push(parse_quote!(for<'__b> &'__b __S: #juniper_crate_name::ScalarRefValue<'__b>));
}
let type_generics_tokens = if self.include_type_generics {
Some(type_generics)
} else {
None
};
let (impl_generics, _, where_clause) = generics.split_for_impl();
let output = quote!(
impl#impl_generics #juniper_crate_name::GraphQLType<#scalar> for #ty #type_generics_tokens
#where_clause
{
type Context = #context;
type TypeInfo = ();
fn name(_: &Self::TypeInfo) -> Option<&str> {
Some(#name)
}
fn meta<'r>(
info: &Self::TypeInfo,
registry: &mut #juniper_crate_name::Registry<'r, #scalar>
) -> #juniper_crate_name::meta::MetaType<'r, #scalar>
where #scalar : 'r,
for<'z> &'z #scalar: #juniper_crate_name::ScalarRefValue<'z>,
{
let fields = vec![
#( #field_definitions ),*
];
let meta = registry.build_object_type::<#ty>( info, &fields )
#description
#interfaces;
meta.into_meta()
}
#[allow(unused_variables)]
#[allow(unused_mut)]
fn resolve_field(
&self,
_info: &(),
field: &str,
args: &#juniper_crate_name::Arguments<#scalar>,
executor: &#juniper_crate_name::Executor<Self::Context, #scalar>,
) -> #juniper_crate_name::ExecutionResult<#scalar> {
match field {
#( #resolve_matches )*
_ => {
panic!("Field {} not found on type {}", field, "Mutation");
}
}
}
fn concrete_type_name(&self, _: &Self::Context, _: &Self::TypeInfo) -> String {
#name.to_string()
}
}
);
output
}
}
#[cfg(test)]
mod test {
use super::*;
@ -296,25 +930,31 @@ mod test {
#[test]
fn test_multiple() {
let result = join_doc_strings(&strs_to_strings(vec!["foo", "bar"]));
assert_eq!(&result, "foo bar");
assert_eq!(&result, "foo\nbar");
}
#[test]
fn test_trims_spaces() {
let result = join_doc_strings(&strs_to_strings(vec![" foo ", "bar ", " baz"]));
assert_eq!(&result, "foo bar baz");
assert_eq!(&result, "foo\nbar\nbaz");
}
#[test]
fn test_empty() {
let result = join_doc_strings(&strs_to_strings(vec!["foo", "", "bar"]));
assert_eq!(&result, "foo\nbar");
assert_eq!(&result, "foo\n\nbar");
}
#[test]
fn test_newline_spaces() {
let result = join_doc_strings(&strs_to_strings(vec!["foo ", "", " bar"]));
assert_eq!(&result, "foo\nbar");
assert_eq!(&result, "foo\n\nbar");
}
#[test]
fn test_continuation_backslash() {
let result = join_doc_strings(&strs_to_strings(vec!["foo\\", "x\\", "y", "bar"]));
assert_eq!(&result, "foo x y\nbar");
}
}

View file

@ -37,27 +37,29 @@ use juniper::{Context, EmptyMutation};
# struct QueryRoot;
# struct Database { users: HashMap<String, User> }
#
# juniper::graphql_object!(User: Database |&self| {
# field id() -> FieldResult<&String> {
# #[juniper::object( Context = Database )]
# impl User {
# fn id(&self) -> FieldResult<&String> {
# Ok(&self.id)
# }
#
# field name() -> FieldResult<&String> {
# fn name(&self) -> FieldResult<&String> {
# Ok(&self.name)
# }
#
# field friends(&executor) -> FieldResult<Vec<&User>> {
# fn friends(context: &Database) -> FieldResult<Vec<&User>> {
# Ok(self.friend_ids.iter()
# .filter_map(|id| executor.context().users.get(id))
# .collect())
# }
# });
# }
#
# juniper::graphql_object!(QueryRoot: Database |&self| {
# field user(&executor, id: String) -> FieldResult<Option<&User>> {
# #[juniper::object( Context = Database )]
# impl QueryRoot {
# fn user(context: &Database, 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

View file

@ -134,13 +134,18 @@ where
///
/// struct QueryRoot;
///
/// juniper::graphql_object! (QueryRoot: ExampleContext |&self| {
/// field say_hello(&executor) -> String {
/// let context = executor.context();
///
/// format!("good morning {}, the app state is {:?}", context.1, context.0)
/// #[juniper::object(
/// Context = ExampleContext
/// )]
/// impl QueryRoot {
/// fn say_hello(context: &ExampleContext) -> String {
/// format!(
/// "good morning {}, the app state is {:?}",
/// context.1,
/// context.0
/// )
/// }
/// });
/// }
///
/// let schema = RootNode::new(QueryRoot, EmptyMutation::new());
///