Ensure Specification(June 2018) Compliance (#631)

* Implemented most test cases from the specification

* Unified error handling for all generators

- Removed proc-macro-error -> not required -> use syn::Error
- Everything below lib.rs uses proc_macro2::TokenStream
  instead of proc_macro::TokenStream
- Replaced error handling in attribute parsers

* WIP better error messages for *all* macros

* Refactored GraphQLInputObject and minor tweaks

- removed support for Scalar within a string ("DefaultScalarValue")
- removed unraw function and replaced it with the built-in one
- added error messages and return types for all functions within utils
- added more constraints to fulfill the GraphQL spec

* Fixed test-cases which are not compliant with the specification

* Removed unused function

* Added constrains, updated error messages, added marker

* Added argument rename within impl_graphql and fixed `__` tests

* Formatted and cleanup

* Added GraphQLTypeAsync for input object

* Moved codegen tests to separate module

Nightly and stable produce different outputs, thus only test nightly.

* Added IsInputType/IsOutputType traits for type checking

Co-authored-by: Christian Legnitto <LegNeato@users.noreply.github.com>
This commit is contained in:
Jonas Meurer 2020-05-02 04:24:01 +02:00 committed by GitHub
parent 358ca27d28
commit 558eae91df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
77 changed files with 2581 additions and 1015 deletions

View file

@ -6,6 +6,7 @@ members = [
"juniper",
"integration_tests/juniper_tests",
"integration_tests/async_await",
"integration_tests/codegen_fail",
"juniper_hyper",
"juniper_iron",
"juniper_rocket",

View file

@ -1,5 +1,5 @@
#[cfg(test)]
use juniper::{graphql_value, GraphQLError, RootNode, Value};
use juniper::{graphql_value, EmptyMutation, EmptySubscription, GraphQLError, RootNode, Value};
#[derive(juniper::GraphQLEnum)]
enum UserKind {
@ -71,19 +71,9 @@ impl Query {
}
}
struct Mutation;
#[juniper::graphql_object]
impl Mutation {}
struct Subscription;
#[juniper::graphql_subscription]
impl Subscription {}
#[tokio::test]
async fn async_simple() {
let schema = RootNode::new(Query, Mutation, Subscription);
let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new());
let doc = r#"
query {
fieldSync
@ -125,7 +115,7 @@ async fn async_simple() {
#[tokio::test]
async fn async_field_validation_error() {
let schema = RootNode::new(Query, Mutation, Subscription);
let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new());
let doc = r#"
query {
nonExistentField
@ -152,24 +142,25 @@ async fn async_field_validation_error() {
assert!(is_validation_error);
}
#[tokio::test]
async fn resolve_into_stream_validation_error() {
let schema = RootNode::new(Query, Mutation, Subscription);
let doc = r#"
subscription {
nonExistent
}
"#;
let vars = Default::default();
let result = juniper::resolve_into_stream(doc, None, &schema, &vars, &()).await;
assert!(result.is_err());
// FIXME: test seems broken by design, re-enable later
// #[tokio::test]
// async fn resolve_into_stream_validation_error() {
// let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new());
// let doc = r#"
// subscription {
// nonExistent
// }
// "#;
// let vars = Default::default();
// let result = juniper::resolve_into_stream(doc, None, &schema, &vars, &()).await;
// assert!(result.is_err());
let error = result.err().unwrap();
let is_validation_error = match error {
GraphQLError::ValidationError(_) => true,
_ => false,
};
assert!(is_validation_error);
}
// let error = result.err().unwrap();
// let is_validation_error = match error {
// GraphQLError::ValidationError(_) => true,
// _ => false,
// };
// assert!(is_validation_error);
// }
fn main() {}

View file

@ -0,0 +1,14 @@
[package]
name = "juniper_codegen_tests"
version = "0.1.0"
publish = false
edition = "2018"
[dependencies]
juniper = { path = "../../juniper" }
futures = "0.3.1"
[dev-dependencies]
serde_json = { version = "1" }
tokio = { version = "0.2", features = ["rt-core", "time", "macros"] }
trybuild = "1.0.25"

View file

@ -0,0 +1,29 @@
[tasks.release]
disabled = true
[tasks.release-some]
disabled = true
[tasks.release-local-test]
disabled = true
[tasks.release-some-local-test]
disabled = true
[tasks.release-dry-run]
disabled = true
[tasks.release-some-dry-run]
disabled = true
[tasks.test]
condition = { channels = ["nightly"] }
[tasks.test-custom]
condition = { channels = ["nightly"] }
[tasks.test-flow]
condition = { channels = ["nightly"] }
[tasks.test-multi-flow-phase]
condition = { channels = ["nightly"] }
[tasks.test-thread-safe]
condition = { channels = ["nightly"] }
[tasks.test-verbose]
condition = { channels = ["nightly"] }
[tasks.test-with-args]
condition = { channels = ["nightly"] }
[tasks.ci-coverage-flow]
condition = { channels = ["nightly"] }

View file

@ -0,0 +1,4 @@
#[derive(juniper::GraphQLEnum)]
pub enum Test {}
fn main() { }

View file

@ -0,0 +1,7 @@
error: GraphQL enum expects at least one field
--> $DIR/derive_no_fields.rs:2:1
|
2 | pub enum Test {}
| ^^^^^^^^^^^^^^^^
|
= note: https://spec.graphql.org/June2018/#sec-Enums

View file

@ -0,0 +1,11 @@
#[derive(juniper::GraphQLObject)]
struct ObjectA {
test: String,
}
#[derive(juniper::GraphQLInputObject)]
struct Object {
field: ObjectA,
}
fn main() {}

View file

@ -0,0 +1,30 @@
error[E0277]: the trait bound `ObjectA: juniper::ast::FromInputValue<__S>` is not satisfied
--> $DIR/derive_incompatible_object.rs:6:10
|
6 | #[derive(juniper::GraphQLInputObject)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `juniper::ast::FromInputValue<__S>` is not implemented for `ObjectA`
|
= note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound `ObjectA: juniper::ast::FromInputValue<__S>` is not satisfied
--> $DIR/derive_incompatible_object.rs:6:10
|
6 | #[derive(juniper::GraphQLInputObject)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `juniper::ast::FromInputValue<__S>` is not implemented for `ObjectA`
|
= note: required by `juniper::ast::FromInputValue::from_input_value`
= note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0599]: no method named `to_input_value` found for struct `ObjectA` in the current scope
--> $DIR/derive_incompatible_object.rs:6:10
|
2 | struct ObjectA {
| -------------- method `to_input_value` not found for this
...
6 | #[derive(juniper::GraphQLInputObject)]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ method not found in `ObjectA`
|
= help: items from traits can only be used if the trait is implemented and in scope
= note: the following trait defines an item `to_input_value`, perhaps you need to implement it:
candidate #1: `juniper::ast::ToInputValue`
= note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)

View file

@ -0,0 +1,4 @@
#[derive(juniper::GraphQLInputObject)]
struct Object {}
fn main() {}

View file

@ -0,0 +1,7 @@
error: GraphQL input object expects at least one field
--> $DIR/derive_no_fields.rs:2:1
|
2 | struct Object {}
| ^^^^^^^^^^^^^^^^
|
= note: https://spec.graphql.org/June2018/#sec-Input-Objects

View file

@ -0,0 +1,7 @@
#[derive(juniper::GraphQLInputObject)]
struct Object {
#[graphql(name = "__test")]
test: String,
}
fn main() {}

View file

@ -0,0 +1,7 @@
error: All types and directives defined within a schema must not have a name which begins with `__` (two underscores), as this is used exclusively by GraphQLs introspection system.
--> $DIR/derive_no_underscore.rs:3:15
|
3 | #[graphql(name = "__test")]
| ^^^^
|
= note: https://spec.graphql.org/June2018/#sec-Schema

View file

@ -0,0 +1,8 @@
#[derive(juniper::GraphQLInputObject)]
struct Object {
test: String,
#[graphql(name = "test")]
test2: String,
}
fn main() {}

View file

@ -0,0 +1,9 @@
error: GraphQL input object does not allow fields with the same name
--> $DIR/derive_unique_name.rs:4:5
|
4 | / #[graphql(name = "test")]
5 | | test2: String,
| |_________________^
|
= help: There is at least one other field with the same name `test`, possibly renamed via the #[graphql] attribute
= note: https://spec.graphql.org/June2018/#sec-Input-Objects

View file

@ -0,0 +1,23 @@
#[derive(juniper::GraphQLObject)]
#[graphql(scalar = juniper::DefaultScalarValue)]
pub struct ObjA {
test: String,
}
enum Character {
A(ObjA),
}
juniper::graphql_interface!(Character: () where Scalar = juniper::DefaultScalarValue |&self| {
field id(__test: ObjA) -> &str {
match *self {
Character::A(_) => "funA",
}
}
instance_resolvers: |_| {
&ObjA => match *self { Character::A(ref h) => Some(h) },
}
});
fn main() {}

View file

@ -0,0 +1,27 @@
error[E0277]: the trait bound `ObjA: juniper::ast::FromInputValue` is not satisfied
--> $DIR/impl_argument_no_object.rs:11:1
|
11 | / juniper::graphql_interface!(Character: () where Scalar = juniper::DefaultScalarValue |&self| {
12 | | field id(__test: ObjA) -> &str {
13 | | match *self {
14 | | Character::A(_) => "funA",
... |
20 | | }
21 | | });
| |___^ the trait `juniper::ast::FromInputValue` is not implemented for `ObjA`
|
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)
error[E0277]: the trait bound `ObjA: juniper::ast::FromInputValue` is not satisfied
--> $DIR/impl_argument_no_object.rs:11:1
|
11 | / juniper::graphql_interface!(Character: () where Scalar = juniper::DefaultScalarValue |&self| {
12 | | field id(__test: ObjA) -> &str {
13 | | match *self {
14 | | Character::A(_) => "funA",
... |
20 | | }
21 | | });
| |___^ the trait `juniper::ast::FromInputValue` is not implemented for `ObjA`
|
= note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info)

View file

@ -0,0 +1,23 @@
#[derive(juniper::GraphQLObject)]
#[graphql(scalar = juniper::DefaultScalarValue)]
pub struct ObjA {
test: String,
}
enum Character {
A(ObjA),
}
juniper::graphql_interface!(Character: () where Scalar = juniper::DefaultScalarValue |&self| {
field id(__test: String) -> &str {
match *self {
Character::A(_) => "funA",
}
}
instance_resolvers: |_| {
&ObjA => match *self { Character::A(ref h) => Some(h) },
}
});
fn main() {}

View file

@ -0,0 +1,12 @@
enum Character {}
juniper::graphql_interface!(Character: () where Scalar = <S> |&self| {
field id() -> &str {
match *self {
}
}
instance_resolvers: |_| {}
});
fn main() {}

View file

@ -0,0 +1,23 @@
#[derive(juniper::GraphQLInputObject)]
#[graphql(scalar = juniper::DefaultScalarValue)]
pub struct ObjA {
test: String,
}
enum Character {
A(ObjA),
}
juniper::graphql_interface!(Character: () where Scalar = juniper::DefaultScalarValue |&self| {
field id() -> &str {
match *self {
Character::A(_) => "funA",
}
}
instance_resolvers: |_| {
&ObjA => match *self { Character::A(ref h) => Some(h) },
}
});
fn main() {}

View file

@ -0,0 +1,23 @@
#[derive(juniper::GraphQLObject)]
#[graphql(scalar = juniper::DefaultScalarValue)]
pub struct ObjA {
test: String,
}
enum Character {
A(ObjA),
}
juniper::graphql_interface!(Character: () where Scalar = juniper::DefaultScalarValue |&self| {
field __id() -> &str {
match *self {
Character::A(_) => "funA",
}
}
instance_resolvers: |_| {
&ObjA => match *self { Character::A(ref h) => Some(h) },
}
});
fn main() {}

View file

@ -0,0 +1,29 @@
#[derive(juniper::GraphQLObject)]
#[graphql(scalar = juniper::DefaultScalarValue)]
pub struct ObjA {
test: String,
}
enum Character {
A(ObjA),
}
juniper::graphql_interface!(Character: () where Scalar = juniper::DefaultScalarValue |&self| {
field id() -> &str {
match *self {
Character::A(_) => "funA",
}
}
field id() -> &str {
match *self {
Character::A(_) => "funA",
}
}
instance_resolvers: |_| {
&ObjA => match *self { Character::A(ref h) => Some(h) },
}
});
fn main() {}

View file

@ -0,0 +1,8 @@
#[derive(juniper::GraphQLObject)]
struct Object {
test: String,
#[graphql(name = "test")]
test2: String,
}
fn main() {}

View file

@ -0,0 +1,9 @@
error: GraphQL object does not allow fields with the same name
--> $DIR/derive_fields_unique.rs:4:5
|
4 | / #[graphql(name = "test")]
5 | | test2: String,
| |_________________^
|
= help: There is at least one other field with the same name `test`, possibly renamed via the #[graphql] attribute
= note: https://spec.graphql.org/June2018/#sec-Objects

View file

@ -0,0 +1,12 @@
// FIXME: enable this if interfaces are supported
#[derive(juniper::GraphQLInputObject)]
struct ObjectA {
test: String,
}
#[derive(juniper::GraphQLObject)]
struct Object {
field: ObjectA,
}
fn main() {}

View file

@ -0,0 +1,4 @@
#[derive(juniper::GraphQLObject)]
struct Object {}
fn main() {}

View file

@ -0,0 +1,7 @@
error: GraphQL object expects at least one field
--> $DIR/derive_no_fields.rs:2:1
|
2 | struct Object {}
| ^^^^^^^^^^^^^^^^
|
= note: https://spec.graphql.org/June2018/#sec-Objects

View file

@ -0,0 +1,7 @@
#[derive(juniper::GraphQLObject)]
struct Object {
#[graphql(name = "__test")]
test: String,
}
fn main() {}

View file

@ -0,0 +1,7 @@
error: All types and directives defined within a schema must not have a name which begins with `__` (two underscores), as this is used exclusively by GraphQLs introspection system.
--> $DIR/derive_no_underscore.rs:3:15
|
3 | #[graphql(name = "__test")]
| ^^^^
|
= note: https://spec.graphql.org/June2018/#sec-Schema

View file

@ -0,0 +1,15 @@
#[derive(juniper::GraphQLObject)]
struct Obj {
field: String,
}
struct Object {}
#[juniper::graphql_object]
impl Object {
fn test(&self, test: Obj) -> String {
String::new()
}
}
fn main() {}

View file

@ -0,0 +1,7 @@
error[E0277]: the trait bound `Obj: juniper::ast::FromInputValue` is not satisfied
--> $DIR/impl_argument_no_object.rs:8:1
|
8 | #[juniper::graphql_object]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `juniper::ast::FromInputValue` is not implemented for `Obj`
|
= note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)

View file

@ -0,0 +1,14 @@
struct Object {}
#[juniper::graphql_object]
impl Object {
fn test(&self) -> String {
String::new()
}
fn test(&self) -> String {
String::new()
}
}
fn main() {}

View file

@ -0,0 +1,10 @@
error: GraphQL object does not allow fields with the same name
--> $DIR/impl_fields_unique.rs:9:5
|
9 | / fn test(&self) -> String {
10 | | String::new()
11 | | }
| |_____^
|
= help: There is at least one other field with the same name `test`, possibly renamed via the #[graphql] attribute
= note: https://spec.graphql.org/June2018/#sec-Objects

View file

@ -0,0 +1,19 @@
// FIXME: enable this if interfaces are supported
#[derive(juniper::GraphQLInputObject)]
#[graphql(scalar = juniper::DefaultScalarValue)]
struct Obj {
field: String,
}
struct Object {}
#[juniper::graphql_object]
impl Object {
fn test(&self) -> Obj {
Obj {
field: String::new(),
}
}
}
fn main() {}

View file

@ -0,0 +1,11 @@
struct Object {}
#[juniper::graphql_object]
impl Object {
#[graphql(arguments(arg(name = "__arg")))]
fn test(&self, arg: String) -> String {
arg
}
}
fn main() {}

View file

@ -0,0 +1,7 @@
error: All types and directives defined within a schema must not have a name which begins with `__` (two underscores), as this is used exclusively by GraphQLs introspection system.
--> $DIR/impl_no_argument_underscore.rs:5:29
|
5 | #[graphql(arguments(arg(name = "__arg")))]
| ^^^^
|
= note: https://spec.graphql.org/June2018/#sec-Schema

View file

@ -0,0 +1,6 @@
struct Object {}
#[juniper::graphql_object]
impl Object {}
fn main() {}

View file

@ -0,0 +1,7 @@
error: GraphQL object expects at least one field
--> $DIR/impl_no_fields.rs:4:1
|
4 | impl Object {}
| ^^^^^^^^^^^^^^
|
= note: https://spec.graphql.org/June2018/#sec-Objects

View file

@ -0,0 +1,11 @@
struct Object {}
#[juniper::graphql_object]
impl Object {
#[graphql(name = "__test")]
fn test(&self) -> String {
String::new()
}
}
fn main() {}

View file

@ -0,0 +1,7 @@
error: All types and directives defined within a schema must not have a name which begins with `__` (two underscores), as this is used exclusively by GraphQLs introspection system.
--> $DIR/impl_no_underscore.rs:5:15
|
5 | #[graphql(name = "__test")]
| ^^^^
|
= note: https://spec.graphql.org/June2018/#sec-Schema

View file

@ -0,0 +1,12 @@
#[derive(juniper::GraphQLEnum)]
pub enum Test {
A,
B,
}
#[derive(juniper::GraphQLUnion)]
enum Character {
Test(Test),
}
fn main() {}

View file

@ -0,0 +1,8 @@
error[E0277]: the trait bound `Test: juniper::types::marker::GraphQLObjectType<__S>` is not satisfied
--> $DIR/derive_enum_field.rs:7:10
|
7 | #[derive(juniper::GraphQLUnion)]
| ^^^^^^^^^^^^^^^^^^^^^ the trait `juniper::types::marker::GraphQLObjectType<__S>` is not implemented for `Test`
|
= note: required by `juniper::types::marker::GraphQLObjectType::mark`
= note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)

View file

@ -0,0 +1,4 @@
#[derive(juniper::GraphQLUnion)]
enum Character {}
fn main() {}

View file

@ -0,0 +1,7 @@
error: GraphQL union expects at least one field
--> $DIR/derive_no_fields.rs:2:1
|
2 | enum Character {}
| ^^^^^^^^^^^^^^^^^
|
= note: https://spec.graphql.org/June2018/#sec-Unions

View file

@ -0,0 +1,7 @@
#[derive(juniper::GraphQLUnion)]
enum Character {
A(std::string::String),
B(String),
}
fn main() {}

View file

@ -0,0 +1,10 @@
error[E0119]: conflicting implementations of trait `std::convert::From<std::string::String>` for type `Character`:
--> $DIR/derive_same_type.rs:1:10
|
1 | #[derive(juniper::GraphQLUnion)]
| ^^^^^^^^^^^^^^^^^^^^^
| |
| first implementation here
| conflicting implementation for `Character`
|
= note: this error originates in a derive macro (in Nightly builds, run with -Z macro-backtrace for more info)

View file

@ -0,0 +1,23 @@
#[derive(juniper::GraphQLEnum)]
#[graphql(context = ())]
pub enum Test {
A,
B,
}
enum Character {
Test(Test),
}
#[juniper::graphql_union]
impl Character {
fn resolve(&self) {
match self {
Test => match *self {
Character::Test(ref h) => Some(h),
},
}
}
}
fn main() {}

View file

@ -0,0 +1,8 @@
error[E0277]: the trait bound `Test: juniper::types::marker::GraphQLObjectType<juniper::value::scalar::DefaultScalarValue>` is not satisfied
--> $DIR/impl_enum_field.rs:12:1
|
12 | #[juniper::graphql_union]
| ^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `juniper::types::marker::GraphQLObjectType<juniper::value::scalar::DefaultScalarValue>` is not implemented for `Test`
|
= note: required by `juniper::types::marker::GraphQLObjectType::mark`
= note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)

View file

@ -0,0 +1,10 @@
enum Character {}
#[juniper::graphql_union]
impl Character {
fn resolve(&self) {
match self {}
}
}
fn main() {}

View file

@ -0,0 +1,9 @@
error: GraphQL union expects at least one field
--> $DIR/impl_no_fields.rs:5:5
|
5 | / fn resolve(&self) {
6 | | match self {}
7 | | }
| |_____^
|
= note: https://spec.graphql.org/June2018/#sec-Unions

View file

@ -0,0 +1,32 @@
// NOTICE: This can not be tested. Implementing Into<T> for each
// variant is not possible since we did not created the
// enum. Therefore, it is possible that the enum already has existing
// Into<T> implementations.
#[derive(juniper::GraphQLObject)]
pub struct Test {
test: String,
}
enum Character {
A(Test),
B(Test),
}
#[juniper::graphql_union]
impl Character {
fn resolve(&self) {
match self {
Test => match *self {
Character::A(ref h) => Some(h),
_ => None,
},
Test => match *self {
Character::B(ref h) => Some(h),
_ => None,
},
}
}
}
fn main() {}

View file

@ -0,0 +1,38 @@
// TODO: [Object] Type Validation: §4 (interfaces) for objects
// TODO: [Non-Null] §1 A NonNull type must not wrap another NonNull type.
#[cfg(test)]
use std::{
fs::{read_dir, DirEntry},
io,
path::{Path, PathBuf},
};
#[cfg(test)]
fn visit_dirs(dir: &Path, cb: &dyn Fn(&DirEntry)) -> io::Result<()> {
if dir.is_dir() {
for entry in read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
visit_dirs(&path, cb)?;
} else {
cb(&entry);
}
}
}
Ok(())
}
#[test]
fn test_failing_compiliation() {
let t = trybuild::TestCases::new();
let dir = PathBuf::from("fail");
visit_dirs(dir.as_path(), &|entry: &DirEntry| {
if let Some(Some("rs")) = entry.path().extension().map(|os| os.to_str()) {
t.compile_fail(entry.path());
}
})
.unwrap();
}

View file

@ -9,7 +9,7 @@ use juniper::{
#[graphql(
name = "MyInput",
description = "input descr",
scalar = "DefaultScalarValue"
scalar = DefaultScalarValue
)]
struct Input {
regular_field: String,
@ -86,7 +86,7 @@ impl<'a> GraphQLType<DefaultScalarValue> for &'a Fake {
}
#[derive(GraphQLInputObject, Debug, PartialEq)]
#[graphql(scalar = "DefaultScalarValue")]
#[graphql(scalar = DefaultScalarValue)]
struct WithLifetime<'a> {
regular_field: &'a Fake,
}

View file

@ -0,0 +1,86 @@
use juniper::DefaultScalarValue;
#[cfg(test)]
use juniper::Object;
#[cfg(test)]
use juniper::{self, execute, EmptyMutation, EmptySubscription, RootNode, Value, Variables};
pub struct MyObject;
#[juniper::graphql_object]
impl MyObject {
#[graphql(arguments(arg(name = "test")))]
fn test(&self, arg: String) -> String {
arg
}
}
#[tokio::test]
async fn check_argument_rename() {
let doc = format!(
r#"
{{
__type(name: "{}") {{
name,
description,
fields {{
name
description
}}
}}
}}
"#,
"MyObject"
);
run_type_info_query(&doc, |(_, values)| {
assert_eq!(
*values,
vec![Value::object(
vec![
("name", Value::scalar("test")),
("description", Value::null()),
]
.into_iter()
.collect(),
)]
);
})
.await;
}
#[cfg(test)]
async fn run_type_info_query<F>(doc: &str, f: F)
where
F: Fn((&Object<DefaultScalarValue>, &Vec<Value>)) -> (),
{
let schema = RootNode::new(
MyObject,
EmptyMutation::<()>::new(),
EmptySubscription::<()>::new(),
);
let (result, errs) = execute(doc, None, &schema, &Variables::new(), &())
.await
.expect("Execution failed");
assert_eq!(errs, []);
println!("Result: {:#?}", result);
let type_info = result
.as_object_value()
.expect("Result is not an object")
.get_field_value("__type")
.expect("__type field missing")
.as_object_value()
.expect("__type field not an object value");
let fields = type_info
.get_field_value("fields")
.expect("fields field missing")
.as_list_value()
.expect("fields not a list");
f((type_info, fields));
}

View file

@ -3,6 +3,7 @@ mod derive_input_object;
mod derive_object;
mod derive_object_with_raw_idents;
mod derive_union;
mod impl_object;
mod impl_scalar;
mod impl_union;
mod scalar_value_transparent;

View file

@ -29,6 +29,9 @@ See [#618](https://github.com/graphql-rust/juniper/pull/618).
- Derive macro `GraphQLEnum` supports custom context (see [#621](https://github.com/graphql-rust/juniper/pull/621))
- Better error messages for all proc macros (see
[#631](https://github.com/graphql-rust/juniper/pull/631)
## Breaking Changes
- `juniper::graphiql` has moved to `juniper::http::graphiql`
@ -54,6 +57,16 @@ See [#618](https://github.com/graphql-rust/juniper/pull/618).
add subscription type to `RootNode`,
add subscription endpoint to `playground_source()`
- Putting a scalar type into a string is not allowed anymore, e..g,
`#[graphql(scalar = "DefaultScalarValue")]`. Only
`#[derive(GraphQLInputObject)]` supported this syntax. The
refactoring of GraphQLInputObject allowed to drop the support
(see [#631](https://github.com/graphql-rust/juniper/pull/631)).
- Support for renaming arguments within an GraphQL object
`#[graphql(arguments(argA(name = "test")))]`
(see [#631](https://github.com/graphql-rust/juniper/pull/631))
# [[0.14.2] 2019-12-16](https://github.com/graphql-rust/juniper/releases/tag/juniper-0.14.2)
- Fix incorrect validation with non-executed operations [#455](https://github.com/graphql-rust/juniper/issues/455)

View file

@ -1,4 +1,4 @@
use crate::{RootNode, Value};
use crate::{EmptyMutation, RootNode, Value};
#[derive(crate::GraphQLEnumInternal)]
enum UserKind {
@ -70,14 +70,9 @@ impl Query {
}
}
struct Mutation;
#[crate::graphql_object_internal]
impl Mutation {}
#[tokio::test]
async fn async_simple() {
let schema = RootNode::new(Query, Mutation, crate::EmptySubscription::new());
let schema = RootNode::new(Query, EmptyMutation::new(), crate::EmptySubscription::new());
let doc = r#"
query {
fieldSync

View file

@ -38,7 +38,7 @@ impl GraphQLScalar for TestComplexScalar {
}
#[derive(GraphQLInputObject, Debug)]
#[graphql(scalar = "DefaultScalarValue")]
#[graphql(scalar = DefaultScalarValue)]
struct TestInputObject {
a: Option<String>,
b: Option<Vec<Option<String>>>,
@ -47,7 +47,7 @@ struct TestInputObject {
}
#[derive(GraphQLInputObject, Debug)]
#[graphql(scalar = "DefaultScalarValue")]
#[graphql(scalar = DefaultScalarValue)]
struct TestNestedInputObject {
na: TestInputObject,
nb: String,

View file

@ -179,6 +179,7 @@ pub use crate::{
types::{
async_await::GraphQLTypeAsync,
base::{Arguments, GraphQLType, TypeKind},
marker,
scalars::{EmptyMutation, EmptySubscription, ID},
subscriptions::{GraphQLSubscriptionType, SubscriptionConnection, SubscriptionCoordinator},
},

View file

@ -24,7 +24,11 @@ struct Human {
struct MyQuery;
#[crate::graphql_object_internal(context = MyContext)]
impl MyQuery {}
impl MyQuery {
fn test(&self) -> i32 {
0 // NOTICE: does not serve a purpose
}
}
type Schema =
RootNode<'static, MyQuery, EmptyMutation<MyContext>, MySubscription, DefaultScalarValue>;

123
juniper/src/types/marker.rs Normal file
View file

@ -0,0 +1,123 @@
//! Marker traits for GraphQL types.
//!
//! This module provide specialized types for GraphQL. To ensure that
//! only specification compliant construct compile, these marker
//! traits are used. Encountering an error where one of these traits
//! is involved implies that the construct is not valid in GraphQL.
use crate::{GraphQLType, ScalarValue};
/// Maker object for GraphQL objects.
///
/// This trait extends the GraphQLType and is only used to mark
/// object. During compile this addition information is required to
/// prevent unwanted structure compiling. If an object requires this
/// trait instead of the GraphQLType, then it explicitly requires an
/// GraphQL objects. Other types (scalars, enums, and input objects)
/// are not allowed.
pub trait GraphQLObjectType<S: ScalarValue>: GraphQLType<S> {
/// An arbitrary function without meaning.
///
/// May contain compile timed check logic which ensures that types
/// are used correctly according to the GraphQL specification.
fn mark() {}
}
/// Marker trait for types which can be used as output types.
///
/// The GraphQL specification differentiates between input and output
/// types. Each type which can be used as an output type should
/// implement this trait. The specification defines enum, scalar,
/// object, union, and interface as output types.
pub trait IsOutputType<S: ScalarValue>: GraphQLType<S> {
/// An arbitrary function without meaning.
///
/// May contain compile timed check logic which ensures that types
/// are used correctly according to the GraphQL specification.
fn mark() {}
}
/// Marker trait for types which can be used as input types.
///
/// The GraphQL specification differentiates between input and output
/// types. Each type which can be used as an input type should
/// implement this trait. The specification defines enum, scalar, and
/// input object input types.
pub trait IsInputType<S: ScalarValue>: GraphQLType<S> {
/// An arbitrary function without meaning.
///
/// May contain compile timed check logic which ensures that types
/// are used correctly according to the GraphQL specification.
fn mark() {}
}
impl<S, T> IsInputType<S> for Option<T>
where
T: IsInputType<S>,
S: ScalarValue,
{
}
impl<S, T> IsOutputType<S> for Option<T>
where
T: IsOutputType<S>,
S: ScalarValue,
{
}
impl<S, T> IsOutputType<S> for Vec<T>
where
T: IsOutputType<S>,
S: ScalarValue,
{
}
impl<'a, S, T> IsOutputType<S> for &'a [T]
where
T: IsOutputType<S>,
S: ScalarValue,
{
}
impl<S, T> IsInputType<S> for Vec<T>
where
T: IsInputType<S>,
S: ScalarValue,
{
}
impl<'a, S, T> IsInputType<S> for &'a [T]
where
T: IsInputType<S>,
S: ScalarValue,
{
}
impl<'a, S, T> IsInputType<S> for &T
where
T: IsInputType<S>,
S: ScalarValue,
{
}
impl<'a, S, T> IsOutputType<S> for &T
where
T: IsOutputType<S>,
S: ScalarValue,
{
}
impl<S, T> IsInputType<S> for Box<T>
where
T: IsInputType<S>,
S: ScalarValue,
{
}
impl<S, T> IsOutputType<S> for Box<T>
where
T: IsOutputType<S>,
S: ScalarValue,
{
}
impl<'a, S> IsInputType<S> for &str where S: ScalarValue {}
impl<'a, S> IsOutputType<S> for &str where S: ScalarValue {}

View file

@ -1,6 +1,7 @@
pub mod async_await;
pub mod base;
pub mod containers;
pub mod marker;
pub mod name;
pub mod pointers;
pub mod scalars;

View file

@ -1,6 +1,6 @@
use juniper::{
graphql_object, graphql_subscription, DefaultScalarValue, ExecutionError, FieldError,
GraphQLEnum, Value, Variables,
graphql_object, DefaultScalarValue, EmptyMutation, EmptySubscription, ExecutionError,
FieldError, GraphQLEnum, Value, Variables,
};
pub type QueryResult = Result<
@ -37,6 +37,7 @@ pub enum UserKind {
Guest,
}
#[derive(juniper::GraphQLObject)]
pub struct User {
pub id: i32,
pub kind: UserKind,
@ -57,9 +58,6 @@ impl User {
}
}
#[graphql_object(Context = Context)]
impl User {}
pub struct Query;
#[graphql_object(Context = Context)]
@ -91,18 +89,9 @@ impl Query {
}
}
pub struct Mutation;
#[graphql_object(Context = Context)]
impl Mutation {}
pub struct Subscription;
#[graphql_subscription(Context = Context)]
impl Subscription {}
pub fn new_schema() -> juniper::RootNode<'static, Query, Mutation, Subscription> {
juniper::RootNode::new(Query, Mutation, Subscription)
pub fn new_schema(
) -> juniper::RootNode<'static, Query, EmptyMutation<Context>, EmptySubscription<Context>> {
juniper::RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new())
}
pub fn execute_sync(query: &str, vars: Variables) -> QueryResult {

View file

@ -18,8 +18,8 @@ proc-macro = true
proc-macro2 = "1.0.1"
syn = { version = "1.0.3", features = ["full", "extra-traits", "parsing"] }
quote = "1.0.3"
proc-macro-error = "0.3.4"
futures = "0.3.1"
proc-macro-error = "1.0.2"
[dev-dependencies]
juniper = { version = "0.14.2", path = "../juniper"}

View file

@ -1,109 +1,152 @@
use crate::{
result::{GraphQLScope, UnsupportedAttribute},
util::{self, span_container::SpanContainer},
};
use proc_macro2::TokenStream;
use crate::util;
use quote::quote;
use syn::{self, Data, Fields};
use syn::{self, ext::IdentExt, spanned::Spanned, Data, Fields};
pub fn impl_enum(
ast: syn::DeriveInput,
is_internal: bool,
error: GraphQLScope,
) -> syn::Result<TokenStream> {
let ast_span = ast.span();
pub fn impl_enum(ast: syn::DeriveInput, is_internal: bool) -> TokenStream {
if !ast.generics.params.is_empty() {
panic!("#[derive(GraphQLEnum) does not support generics or lifetimes");
return Err(error.custom_error(ast_span, "does not support generics or lifetimes"));
}
let variants = match ast.data {
Data::Enum(enum_data) => enum_data.variants,
_ => {
panic!("#[derive(GraphlQLEnum)] may only be applied to enums, not to structs");
}
_ => return Err(error.custom_error(ast_span, "can only be applied to enums")),
};
// Parse attributes.
let attrs = match util::ObjectAttributes::from_attrs(&ast.attrs) {
Ok(a) => a,
Err(e) => {
panic!("Invalid #[graphql(...)] attribute: {}", e);
}
};
if !attrs.interfaces.is_empty() {
panic!("Invalid #[graphql(...)] attribute 'interfaces': #[derive(GraphQLEnum) does not support 'interfaces'");
}
if attrs.scalar.is_some() {
panic!("Invalid #[graphql(...)] attribute 'scalar': #[derive(GraphQLEnum) does not support explicit scalars");
}
// Parse attributes.
let attrs = util::ObjectAttributes::from_attrs(&ast.attrs)?;
let ident = &ast.ident;
let name = attrs.name.unwrap_or_else(|| ident.to_string());
let mut mapping = std::collections::HashMap::new();
let name = attrs
.name
.clone()
.map(SpanContainer::into_inner)
.unwrap_or_else(|| ident.unraw().to_string());
let fields = variants
.into_iter()
.filter_map(|field| {
let span = field.span();
let field_attrs = match util::FieldAttributes::from_attrs(
field.attrs,
&field.attrs,
util::FieldAttributeParseMode::Object,
) {
Ok(attrs) => attrs,
Err(e) => panic!("Invalid #[graphql] attribute for field: \n{}", e),
Err(err) => {
proc_macro_error::emit_error!(err);
return None;
}
};
if field_attrs.skip {
panic!("#[derive(GraphQLEnum)] does not support #[graphql(skip)] on fields");
} else {
let field_name = field.ident;
let name = field_attrs
.name
.clone()
.unwrap_or_else(|| util::to_upper_snake_case(&field_name.to_string()));
match mapping.get(&name) {
Some(other_field_name) =>
panic!(format!("#[derive(GraphQLEnum)] all variants needs to be unique. Another field name `{}` has the same identifier `{}`, thus `{}` can not be named `{}`. One of the fields is manually renamed!", other_field_name, name, field_name, name)),
None => {
mapping.insert(name.clone(), field_name.clone());
}
}
.map(SpanContainer::into_inner)
.unwrap_or_else(|| util::to_upper_snake_case(&field_name.unraw().to_string()));
let resolver_code = quote!( #ident::#field_name );
let _type = match field.fields {
Fields::Unit => syn::parse_str(&field_name.to_string()).unwrap(),
_ => panic!("#[derive(GraphQLEnum)] all fields of the enum must be unnamed"),
_ => {
error.custom(
field.fields.span(),
"all fields of the enum must be unnamed, e.g., None",
);
return None;
}
};
if let Some(skip) = field_attrs.skip {
error.unsupported_attribute(skip.span(), UnsupportedAttribute::Skip);
return None;
}
if name.starts_with("__") {
error.no_double_underscore(if let Some(name) = field_attrs.name {
name.span_ident()
} else {
field_name.span()
});
}
if let Some(default) = field_attrs.default {
error.unsupported_attribute_within(
default.span_ident(),
UnsupportedAttribute::Default,
);
}
Some(util::GraphQLTypeDefinitionField {
name,
_type,
args: Vec::new(),
description: field_attrs.description,
deprecation: field_attrs.deprecation,
description: field_attrs.description.map(SpanContainer::into_inner),
deprecation: field_attrs.deprecation.map(SpanContainer::into_inner),
resolver_code,
is_type_inferred: true,
is_async: false,
default: None,
span,
})
}
})
.collect::<Vec<_>>();
if fields.len() == 0 {
panic!("#[derive(GraphQLEnum)] requires at least one variants");
proc_macro_error::abort_if_dirty();
if fields.is_empty() {
error.not_empty(ast_span);
}
match crate::util::duplicate::Duplicate::find_by_key(&fields, |field| &field.name) {
Some(duplicates) => error.duplicate(duplicates.iter()),
None => {}
}
if !attrs.interfaces.is_empty() {
attrs.interfaces.iter().for_each(|elm| {
error.unsupported_attribute(elm.span(), UnsupportedAttribute::Interface)
});
}
if let Some(scalar) = attrs.scalar {
error.unsupported_attribute(scalar.span_ident(), UnsupportedAttribute::Scalar);
}
if name.starts_with("__") && !is_internal {
error.no_double_underscore(if let Some(name) = attrs.name {
name.span_ident()
} else {
ident.span()
});
}
proc_macro_error::abort_if_dirty();
let definition = util::GraphQLTypeDefiniton {
name,
_type: syn::parse_str(&ast.ident.to_string()).unwrap(),
context: attrs.context,
context: attrs.context.map(SpanContainer::into_inner),
scalar: None,
description: attrs.description,
description: attrs.description.map(SpanContainer::into_inner),
fields,
// NOTICE: only unit variants allow -> no generics possible
generics: syn::Generics::default(),
interfaces: None,
include_type_generics: true,
generic_scalar: true,
no_async: attrs.no_async,
no_async: attrs.no_async.is_some(),
};
let juniper_crate_name = if is_internal { "crate" } else { "juniper" };
definition.into_enum_tokens(juniper_crate_name)
Ok(definition.into_enum_tokens(juniper_crate_name))
}

View file

@ -1,331 +1,152 @@
#![allow(clippy::match_wild_err_arm)]
use std::str::FromStr;
use proc_macro2::{Span, TokenStream};
use crate::{
result::{GraphQLScope, UnsupportedAttribute},
util::{self, span_container::SpanContainer},
};
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::{self, parse_quote, Data, DeriveInput, Field, Fields, Ident, Meta, NestedMeta};
use crate::util::*;
#[derive(Default, Debug)]
struct ObjAttrs {
name: Option<String>,
description: Option<String>,
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;
}
panic!(format!(
"Unknown attribute for #[derive(GraphQLInputObject)]: {:?}",
item
));
}
}
res
}
}
#[derive(Default)]
struct ObjFieldAttrs {
name: Option<String>,
description: Option<String>,
default: bool,
default_expr: Option<String>,
}
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 attributes for name and description.
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, "default", AttributeValidation::Any)
{
res.default_expr = Some(val);
continue;
}
if let NestedMeta::Meta(Meta::Path(ref path)) = item {
if path.is_ident("default") {
res.default = true;
continue;
}
}
panic!(format!(
"Unknown attribute for #[derive(GraphQLInputObject)]: {:?}",
item
));
}
}
res
}
}
pub fn impl_input_object(ast: &syn::DeriveInput, is_internal: bool) -> TokenStream {
let juniper_path = if is_internal {
quote!(crate)
} else {
quote!(juniper)
};
use syn::{self, ext::IdentExt, spanned::Spanned, Data, Fields};
pub fn impl_input_object(
ast: syn::DeriveInput,
is_internal: bool,
error: GraphQLScope,
) -> syn::Result<TokenStream> {
let ast_span = ast.span();
let fields = match ast.data {
Data::Struct(ref data) => match data.fields {
Fields::Named(ref named) => named.named.iter().collect::<Vec<_>>(),
Data::Struct(data) => match data.fields {
Fields::Named(named) => named.named,
_ => {
panic!(
"#[derive(GraphQLInputObject)] may only be used on regular structs with fields"
);
return Err(
error.custom_error(ast_span, "all fields must be named, e.g., `test: String`")
)
}
},
_ => {
panic!("#[derive(GraphlQLInputObject)] may only be applied to structs, not to enums");
}
_ => return Err(error.custom_error(ast_span, "can only be used on structs with fields")),
};
// Parse attributes.
let attrs = util::ObjectAttributes::from_attrs(&ast.attrs)?;
// Parse attributes.
let ident = &ast.ident;
let attrs = ObjAttrs::from_input(ast);
let name = attrs.name.unwrap_or_else(|| ast.ident.to_string());
let generics = &ast.generics;
let name = attrs
.name
.clone()
.map(SpanContainer::into_inner)
.unwrap_or_else(|| ident.to_string());
let meta_description = match attrs.description {
Some(descr) => quote! { let meta = meta.description(#descr); },
None => quote! { let meta = meta; },
};
let mut meta_fields = TokenStream::new();
let mut from_inputs = TokenStream::new();
let mut to_inputs = TokenStream::new();
let (_, ty_generics, _) = generics.split_for_impl();
let mut generics = generics.clone();
let scalar = if let Some(scalar) = attrs.scalar {
scalar
} else {
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_path::ScalarValue));
let fields = fields
.into_iter()
.filter_map(|field| {
let span = field.span();
let field_attrs = match util::FieldAttributes::from_attrs(
&field.attrs,
util::FieldAttributeParseMode::Object,
) {
Ok(attrs) => attrs,
Err(e) => {
proc_macro_error::emit_error!(e);
return None;
}
Ident::new("__S", Span::call_site())
};
let (impl_generics, _, where_clause) = generics.split_for_impl();
for field in fields {
let field_ty = &field.ty;
let field_attrs = ObjFieldAttrs::from_input(field);
let field_ident = field.ident.as_ref().unwrap();
// 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(&unraw(&field_ident.to_string()))
}
};
let field_description = match field_attrs.description {
Some(s) => quote! { let field = field.description(#s); },
None => quote! {},
Some(ref name) => name.to_string(),
None => crate::util::to_camel_case(&field_ident.unraw().to_string()),
};
let default = {
if field_attrs.default {
Some(quote! { Default::default() })
if let Some(span) = field_attrs.skip {
error.unsupported_attribute_within(span.span(), UnsupportedAttribute::Skip)
}
if let Some(span) = field_attrs.deprecation {
error.unsupported_attribute_within(
span.span_ident(),
UnsupportedAttribute::Deprecation,
)
}
if name.starts_with("__") {
error.no_double_underscore(if let Some(name) = field_attrs.name {
name.span_ident()
} else {
match field_attrs.default_expr {
Some(ref def) => match proc_macro::TokenStream::from_str(def) {
Ok(t) => match syn::parse::<syn::Expr>(t) {
Ok(e) => {
let mut tokens = TokenStream::new();
e.to_tokens(&mut tokens);
Some(tokens)
}
Err(e) => {
let _ = e;
panic!("#graphql(default = ?) must be a valid Rust expression inside a string");
}
},
Err(e) => {
let _ = e;
panic!("#graphql(default = ?) must be a valid Rust expression inside a string");
}
},
None => None,
}
}
};
let create_meta_field = match default {
Some(ref def) => {
quote! {
let field = registry.arg_with_default::<#field_ty>( #name, &#def, &());
}
}
None => {
quote! {
let field = registry.arg::<#field_ty>(#name, &());
}
}
};
meta_fields.extend(quote! {
{
#create_meta_field
#field_description
field
},
});
// Build from_input clause.
let from_input_default = match default {
Some(ref def) => {
quote! {
Some(&&#juniper_path::InputValue::Null) | None if true => #def,
}
}
None => quote! {},
};
from_inputs.extend(quote!{
#field_ident: {
// TODO: investigate the unwraps here, they seem dangerous!
match obj.get(#name) {
#from_input_default
Some(ref v) => #juniper_path::FromInputValue::from_input_value(v).unwrap(),
None => {
#juniper_path::FromInputValue::from_input_value(&#juniper_path::InputValue::<#scalar>::null())
.unwrap()
},
}
},
});
// Build to_input clause.
to_inputs.extend(quote! {
(#name, self.#field_ident.to_input_value()),
name.span()
});
}
let body = quote! {
impl#impl_generics #juniper_path::GraphQLType<#scalar> for #ident #ty_generics
#where_clause
{
type Context = ();
type TypeInfo = ();
let resolver_code = quote!(#field_ident);
fn name(_: &()) -> Option<&'static str> {
Some(#name)
let default = field_attrs
.default
.map(|default| match default.into_inner() {
Some(expr) => expr.into_token_stream(),
None => quote! { Default::default() },
});
Some(util::GraphQLTypeDefinitionField {
name,
_type: field.ty,
args: Vec::new(),
description: field_attrs.description.map(SpanContainer::into_inner),
deprecation: None,
resolver_code,
is_type_inferred: true,
is_async: false,
default,
span,
})
})
.collect::<Vec<_>>();
proc_macro_error::abort_if_dirty();
if fields.is_empty() {
error.not_empty(ast_span);
}
fn meta<'r>(
_: &(),
registry: &mut #juniper_path::Registry<'r, #scalar>
) -> #juniper_path::meta::MetaType<'r, #scalar>
where #scalar: 'r
{
let fields = &[
#meta_fields
];
let meta = registry.build_input_object_type::<#ident>(&(), fields);
#meta_description
meta.into_meta()
}
match crate::util::duplicate::Duplicate::find_by_key(&fields, |field| &field.name) {
Some(duplicates) => error.duplicate(duplicates.iter()),
None => {}
}
impl#impl_generics #juniper_path::FromInputValue<#scalar> for #ident #ty_generics
#where_clause
{
fn from_input_value(value: &#juniper_path::InputValue<#scalar>) -> Option<Self>
{
if let Some(obj) = value.to_object_value() {
let item = #ident {
#from_inputs
};
Some(item)
}
else {
None
}
}
if !attrs.interfaces.is_empty() {
attrs.interfaces.iter().for_each(|elm| {
error.unsupported_attribute(elm.span(), UnsupportedAttribute::Interface)
});
}
impl#impl_generics #juniper_path::ToInputValue<#scalar> for #ident #ty_generics
#where_clause
if let Some(duplicates) =
crate::util::duplicate::Duplicate::find_by_key(&fields, |field| field.name.as_str())
{
fn to_input_value(&self) -> #juniper_path::InputValue<#scalar> {
#juniper_path::InputValue::object(vec![
#to_inputs
].into_iter().collect())
error.duplicate(duplicates.iter());
}
if name.starts_with("__") && !is_internal {
error.no_double_underscore(if let Some(name) = attrs.name {
name.span_ident()
} else {
ident.span()
});
}
proc_macro_error::abort_if_dirty();
let definition = util::GraphQLTypeDefiniton {
name,
_type: syn::parse_str(&ast.ident.to_string()).unwrap(),
context: attrs.context.map(SpanContainer::into_inner),
scalar: attrs.scalar.map(SpanContainer::into_inner),
description: attrs.description.map(SpanContainer::into_inner),
fields,
generics: ast.generics,
interfaces: None,
include_type_generics: true,
generic_scalar: true,
no_async: attrs.no_async.is_some(),
};
body
let juniper_crate_name = if is_internal { "crate" } else { "juniper" };
Ok(definition.into_input_object_tokens(juniper_crate_name))
}

View file

@ -1,52 +1,75 @@
use crate::{
result::{GraphQLScope, UnsupportedAttribute},
util::{self, span_container::SpanContainer},
};
use proc_macro2::TokenStream;
use quote::quote;
use syn::{self, Data, Fields};
use syn::{self, ext::IdentExt, spanned::Spanned, Data, Fields};
use crate::util;
pub fn build_derive_object(ast: syn::DeriveInput, is_internal: bool) -> TokenStream {
pub fn build_derive_object(
ast: syn::DeriveInput,
is_internal: bool,
error: GraphQLScope,
) -> syn::Result<TokenStream> {
let ast_span = ast.span();
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");
}
_ => return Err(error.custom_error(ast_span, "only named fields are allowed")),
},
_ => {
panic!("#[derive(GraphlQLObject)] may only be applied to structs, not to enums");
}
_ => return Err(error.custom_error(ast_span, "can only be applied to structs")),
};
// Parse attributes.
let attrs = match util::ObjectAttributes::from_attrs(&ast.attrs) {
Ok(a) => a,
Err(e) => {
panic!("Invalid #[graphql(...)] attribute: {}", e);
}
};
if !attrs.interfaces.is_empty() {
panic!("Invalid #[graphql(...)] attribute 'interfaces': #[derive(GraphQLObject) does not support 'interfaces'");
}
let ident = &ast.ident;
let name = attrs.name.unwrap_or_else(|| ident.to_string());
let attrs = util::ObjectAttributes::from_attrs(&ast.attrs)?;
let fields = struct_fields.into_iter().filter_map(|field| {
let ident = &ast.ident;
let name = attrs
.name
.clone()
.map(SpanContainer::into_inner)
.unwrap_or_else(|| ident.unraw().to_string());
let fields = struct_fields
.into_iter()
.filter_map(|field| {
let span = field.span();
let field_attrs = match util::FieldAttributes::from_attrs(
field.attrs,
&field.attrs,
util::FieldAttributeParseMode::Object,
) {
Ok(attrs) => attrs,
Err(e) => panic!("Invalid #[graphql] attribute: \n{}", e),
Err(e) => {
proc_macro_error::emit_error!(e);
return None;
}
};
if field_attrs.skip {
None
} else {
let field_name = field.ident.unwrap();
if field_attrs.skip.is_some() {
return None;
}
let field_name = &field.ident.unwrap();
let name = field_attrs
.name
.clone()
.unwrap_or_else(|| util::to_camel_case(&field_name.to_string()));
.map(SpanContainer::into_inner)
.unwrap_or_else(|| util::to_camel_case(&field_name.unraw().to_string()));
if name.starts_with("__") {
error.no_double_underscore(if let Some(name) = field_attrs.name {
name.span_ident()
} else {
field_name.span()
});
}
if let Some(default) = field_attrs.default {
error.unsupported_attribute_within(
default.span_ident(),
UnsupportedAttribute::Default,
);
}
let resolver_code = quote!(
&self . #field_name
@ -56,29 +79,61 @@ pub fn build_derive_object(ast: syn::DeriveInput, is_internal: bool) -> TokenStr
name,
_type: field.ty,
args: Vec::new(),
description: field_attrs.description,
deprecation: field_attrs.deprecation,
description: field_attrs.description.map(SpanContainer::into_inner),
deprecation: field_attrs.deprecation.map(SpanContainer::into_inner),
resolver_code,
default: None,
is_type_inferred: true,
is_async: false,
span,
})
}
})
.collect::<Vec<_>>();
// Early abort after checking all fields
proc_macro_error::abort_if_dirty();
if !attrs.interfaces.is_empty() {
attrs.interfaces.iter().for_each(|elm| {
error.unsupported_attribute(elm.span(), UnsupportedAttribute::Interface)
});
}
if let Some(duplicates) =
crate::util::duplicate::Duplicate::find_by_key(&fields, |field| field.name.as_str())
{
error.duplicate(duplicates.iter());
}
if name.starts_with("__") && !is_internal {
error.no_double_underscore(if let Some(name) = attrs.name {
name.span_ident()
} else {
ident.span()
});
}
if fields.is_empty() {
error.not_empty(ast_span);
}
// Early abort after GraphQL properties
proc_macro_error::abort_if_dirty();
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(),
context: attrs.context.map(SpanContainer::into_inner),
scalar: attrs.scalar.map(SpanContainer::into_inner),
description: attrs.description.map(SpanContainer::into_inner),
fields,
generics: ast.generics,
interfaces: None,
include_type_generics: true,
generic_scalar: true,
no_async: attrs.no_async,
no_async: attrs.no_async.is_some(),
};
let juniper_crate_name = if is_internal { "crate" } else { "juniper" };
definition.into_tokens(juniper_crate_name)
Ok(definition.into_tokens(juniper_crate_name))
}

View file

@ -1,9 +1,10 @@
use crate::{
result::GraphQLScope,
util::{self, span_container::SpanContainer},
};
use proc_macro2::TokenStream;
use quote::quote;
use syn::{self, Data, Fields, Ident, Variant};
use crate::util;
use syn::{self, spanned::Spanned, Data, Fields, Ident, Variant};
#[derive(Debug, Default)]
struct TransparentAttributes {
@ -36,9 +37,7 @@ impl syn::parse::Parse for TransparentAttributes {
"transparent" => {
output.transparent = Some(true);
}
other => {
return Err(input.error(format!("Unknown attribute: {}", other)));
}
_ => return Err(syn::Error::new(ident.span(), "unknown attribute")),
}
if input.lookahead1().peek(syn::Token![,]) {
input.parse::<syn::Token![,]>()?;
@ -55,7 +54,8 @@ impl TransparentAttributes {
Some(attr) => {
let mut parsed: TransparentAttributes = attr.parse_args()?;
if parsed.description.is_none() {
parsed.description = util::get_doc_comment(attrs);
parsed.description =
util::get_doc_comment(attrs).map(SpanContainer::into_inner);
}
Ok(parsed)
}
@ -64,15 +64,17 @@ impl TransparentAttributes {
}
}
pub fn impl_scalar_value(ast: &syn::DeriveInput, is_internal: bool) -> TokenStream {
pub fn impl_scalar_value(
ast: &syn::DeriveInput,
is_internal: bool,
error: GraphQLScope,
) -> syn::Result<TokenStream> {
let ident = &ast.ident;
match ast.data {
Data::Enum(ref enum_data) => impl_scalar_enum(ident, enum_data, is_internal),
Data::Struct(ref struct_data) => impl_scalar_struct(ast, struct_data, is_internal),
Data::Union(_) => {
panic!("#[derive(GraphQLScalarValue)] may not be applied to unions");
}
Data::Enum(ref enum_data) => impl_scalar_enum(ident, enum_data, is_internal, error),
Data::Struct(ref struct_data) => impl_scalar_struct(ast, struct_data, is_internal, error),
Data::Union(_) => Err(error.custom_error(ast.span(), "may not be applied to unions")),
}
}
@ -80,22 +82,21 @@ fn impl_scalar_struct(
ast: &syn::DeriveInput,
data: &syn::DataStruct,
is_internal: bool,
) -> TokenStream {
error: GraphQLScope,
) -> syn::Result<TokenStream> {
let field = match data.fields {
syn::Fields::Unnamed(ref fields) if fields.unnamed.len() == 1 => {
fields.unnamed.first().unwrap()
}
_ => {
panic!("#[derive(GraphQLScalarValue)] may only be applied to enums or tuple structs with a single field");
return Err(error.custom_error(
data.fields.span(),
"requires exact one field, e.g., Test(i32)",
))
}
};
let ident = &ast.ident;
let attrs = match TransparentAttributes::from_attrs(&ast.attrs) {
Ok(attrs) => attrs,
Err(e) => {
panic!("Invalid #[graphql] attribute: {}", e);
}
};
let attrs = TransparentAttributes::from_attrs(&ast.attrs)?;
let inner_ty = &field.ty;
let name = attrs.name.unwrap_or_else(|| ident.to_string());
@ -133,7 +134,7 @@ fn impl_scalar_struct(
}
);
quote!(
let content = quote!(
#_async
impl<S> #crate_name::GraphQLType<S> for #ident
@ -198,27 +199,33 @@ fn impl_scalar_struct(
<#inner_ty as #crate_name::ParseScalarValue<S>>::from_str(value)
}
}
)
);
Ok(content)
}
fn impl_scalar_enum(ident: &syn::Ident, data: &syn::DataEnum, is_internal: bool) -> TokenStream {
fn impl_scalar_enum(
ident: &syn::Ident,
data: &syn::DataEnum,
is_internal: bool,
error: GraphQLScope,
) -> syn::Result<TokenStream> {
let froms = data
.variants
.iter()
.map(|v| derive_from_variant(v, ident))
.collect::<Result<Vec<_>, String>>()
.unwrap_or_else(|s| panic!("{}", s));
.map(|v| derive_from_variant(v, ident, &error))
.collect::<Result<Vec<_>, _>>()?;
let serialize = derive_serialize(data.variants.iter(), ident, is_internal);
let display = derive_display(data.variants.iter(), ident);
quote! {
Ok(quote! {
#(#froms)*
#serialize
#display
}
})
}
fn derive_display<'a, I>(variants: I, ident: &Ident) -> TokenStream
@ -269,14 +276,19 @@ where
}
}
fn derive_from_variant(variant: &Variant, ident: &Ident) -> Result<TokenStream, String> {
fn derive_from_variant(
variant: &Variant,
ident: &Ident,
error: &GraphQLScope,
) -> syn::Result<TokenStream> {
let ty = match variant.fields {
Fields::Unnamed(ref u) if u.unnamed.len() == 1 => &u.unnamed.first().unwrap().ty,
_ => {
return Err(String::from(
"Only enums with exactly one unnamed field per variant are supported",
));
return Err(error.custom_error(
variant.fields.span(),
"requires exact one field, e.g., Test(i32)",
))
}
};

View file

@ -1,50 +1,58 @@
use crate::{
result::{GraphQLScope, UnsupportedAttribute},
util::{self, span_container::SpanContainer},
};
use proc_macro2::TokenStream;
use quote::quote;
use syn::{self, Data, Fields};
use syn::{self, ext::IdentExt, spanned::Spanned, Data, Fields};
use crate::util;
pub fn build_derive_union(ast: syn::DeriveInput, is_internal: bool) -> TokenStream {
pub fn build_derive_union(
ast: syn::DeriveInput,
is_internal: bool,
error: GraphQLScope,
) -> syn::Result<TokenStream> {
let ast_span = ast.span();
let enum_fields = match ast.data {
Data::Enum(data) => data.variants,
_ => {
panic!("#[derive(GraphQLUnion)] can only be applied to enums");
}
_ => return Err(error.custom_error(ast_span, "can only be applied to enums")),
};
// Parse attributes.
let attrs = match util::ObjectAttributes::from_attrs(&ast.attrs) {
Ok(a) => a,
Err(e) => {
panic!("Invalid #[graphql(...)] attribute for enum: {}", e);
}
};
if !attrs.interfaces.is_empty() {
panic!("#[derive(GraphQLUnion)] does not support interfaces");
}
let attrs = util::ObjectAttributes::from_attrs(&ast.attrs)?;
let ident = &ast.ident;
let name = attrs.name.unwrap_or_else(|| ident.to_string());
let name = attrs
.name
.clone()
.map(SpanContainer::into_inner)
.unwrap_or_else(|| ident.unraw().to_string());
let fields = enum_fields.into_iter().filter_map(|field| {
let fields = enum_fields
.into_iter()
.filter_map(|field| {
let span = field.span();
let field_attrs = match util::FieldAttributes::from_attrs(
field.attrs,
&field.attrs,
util::FieldAttributeParseMode::Object,
) {
Ok(attrs) => attrs,
Err(e) => panic!("Invalid #[graphql] attribute for field: \n{}", e),
Err(e) => {
proc_macro_error::emit_error!(e);
return None;
}
};
if let Some(ident) = field_attrs.skip {
error.unsupported_attribute_within(ident.span(), UnsupportedAttribute::Skip);
return None;
}
if field_attrs.skip {
panic!("#[derive(GraphQLUnion)] does not support #[graphql(skip)] on fields");
} else {
let variant_name = field.ident;
let name = field_attrs
.name
.clone()
.unwrap_or_else(|| util::to_camel_case(&variant_name.to_string()));
.map(SpanContainer::into_inner)
.unwrap_or_else(|| util::to_camel_case(&variant_name.unraw().to_string()));
let resolver_code = quote!(
#ident :: #variant_name
@ -59,16 +67,44 @@ pub fn build_derive_union(ast: syn::DeriveInput, is_internal: bool) -> TokenStre
};
if iter.next().is_some() {
panic!("#[derive(GraphQLUnion)] all members must be unnamed with a single element e.g. Some(T)");
error.custom(
inner.span(),
"all members must be unnamed with a single element e.g. Some(T)",
);
}
first.ty.clone()
}
_ => panic!("#[derive(GraphQLUnion)] all fields of the enum must be unnamed"),
_ => {
error.custom(
variant_name.span(),
"only unnamed fields with a single element are allowed, e.g., Some(T)",
);
return None;
}
};
if field_attrs.description.is_some() {
panic!("#[derive(GraphQLUnion)] does not allow documentation of fields");
if let Some(description) = field_attrs.description {
error.unsupported_attribute_within(
description.span_ident(),
UnsupportedAttribute::Description,
);
}
if let Some(default) = field_attrs.default {
error.unsupported_attribute_within(
default.span_ident(),
UnsupportedAttribute::Default,
);
}
if name.starts_with("__") {
error.no_double_underscore(if let Some(name) = field_attrs.name {
name.span_ident()
} else {
variant_name.span()
});
}
Some(util::GraphQLTypeDefinitionField {
@ -76,15 +112,36 @@ pub fn build_derive_union(ast: syn::DeriveInput, is_internal: bool) -> TokenStre
_type,
args: Vec::new(),
description: None,
deprecation: field_attrs.deprecation,
deprecation: field_attrs.deprecation.map(SpanContainer::into_inner),
resolver_code,
is_type_inferred: true,
is_async: false,
default: None,
span,
})
}
});
})
.collect::<Vec<_>>();
let fields = fields.collect::<Vec<_>>();
// Early abort after checking all fields
proc_macro_error::abort_if_dirty();
if !attrs.interfaces.is_empty() {
attrs.interfaces.iter().for_each(|elm| {
error.unsupported_attribute(elm.span(), UnsupportedAttribute::Interface)
});
}
if fields.is_empty() {
error.not_empty(ast_span);
}
if name.starts_with("__") && !is_internal {
error.no_double_underscore(if let Some(name) = attrs.name {
name.span_ident()
} else {
ident.span()
});
}
// NOTICE: This is not an optimal implementation. It is possible
// to bypass this check by using a full qualified path instead
@ -100,23 +157,26 @@ pub fn build_derive_union(ast: syn::DeriveInput, is_internal: bool) -> TokenStre
};
if !all_variants_different {
panic!("#[derive(GraphQLUnion)] each variant must have a different type");
error.custom(ident.span(), "each variant must have a different type");
}
// Early abort after GraphQL properties
proc_macro_error::abort_if_dirty();
let definition = util::GraphQLTypeDefiniton {
name,
_type: syn::parse_str(&ast.ident.to_string()).unwrap(),
context: attrs.context,
scalar: attrs.scalar,
description: attrs.description,
context: attrs.context.map(SpanContainer::into_inner),
scalar: attrs.scalar.map(SpanContainer::into_inner),
description: attrs.description.map(SpanContainer::into_inner),
fields,
generics: ast.generics,
interfaces: None,
include_type_generics: true,
generic_scalar: true,
no_async: attrs.no_async,
no_async: attrs.no_async.is_some(),
};
let juniper_crate_name = if is_internal { "crate" } else { "juniper" };
definition.into_union_tokens(juniper_crate_name)
Ok(definition.into_union_tokens(juniper_crate_name))
}

View file

@ -1,146 +1,124 @@
#![allow(clippy::collapsible_if)]
use crate::util;
use proc_macro::TokenStream;
use crate::{
result::{GraphQLScope, UnsupportedAttribute},
util::{self, span_container::SpanContainer},
};
use proc_macro2::TokenStream;
use quote::quote;
use syn::{ext::IdentExt, spanned::Spanned};
/// Generate code for the juniper::graphql_object macro.
pub fn build_object(args: TokenStream, body: TokenStream, is_internal: bool) -> TokenStream {
let definition = create(args, body);
pub fn build_object(
args: TokenStream,
body: TokenStream,
is_internal: bool,
error: GraphQLScope,
) -> TokenStream {
let definition = match create(args, body, is_internal, error) {
Ok(definition) => definition,
Err(err) => return err.to_compile_error(),
};
let juniper_crate_name = if is_internal { "crate" } else { "juniper" };
definition.into_tokens(juniper_crate_name).into()
}
/// Generate code for the juniper::graphql_subscription macro.
pub fn build_subscription(args: TokenStream, body: TokenStream, is_internal: bool) -> TokenStream {
let definition = create(args, body);
pub fn build_subscription(
args: TokenStream,
body: TokenStream,
is_internal: bool,
error: GraphQLScope,
) -> TokenStream {
let definition = match create(args, body, is_internal, error) {
Ok(definition) => definition,
Err(err) => return err.to_compile_error(),
};
let juniper_crate_name = if is_internal { "crate" } else { "juniper" };
definition
.into_subscription_tokens(juniper_crate_name)
.into()
}
fn create(args: TokenStream, body: TokenStream) -> util::GraphQLTypeDefiniton {
let _impl = util::parse_impl::ImplBlock::parse(args, body);
fn create(
args: TokenStream,
body: TokenStream,
is_internal: bool,
error: GraphQLScope,
) -> syn::Result<util::GraphQLTypeDefiniton> {
let body_span = body.span();
let _impl = util::parse_impl::ImplBlock::parse(args, body)?;
let name = _impl
.attrs
.name
.clone()
.unwrap_or_else(|| _impl.type_ident.to_string());
.map(SpanContainer::into_inner)
.unwrap_or_else(|| _impl.type_ident.unraw().to_string());
let mut definition = util::GraphQLTypeDefiniton {
name,
_type: *_impl.target_type.clone(),
context: _impl.attrs.context,
scalar: _impl.attrs.scalar,
description: _impl.description,
fields: Vec::new(),
generics: _impl.generics.clone(),
interfaces: if !_impl.attrs.interfaces.is_empty() {
Some(_impl.attrs.interfaces)
} else {
None
},
include_type_generics: false,
generic_scalar: false,
no_async: _impl.attrs.no_async,
};
for method in _impl.methods {
let _type = match &method.sig.output {
syn::ReturnType::Type(_, ref t) => (**t).clone(),
let fields = _impl
.methods
.iter()
.filter_map(|method| {
let span = method.span();
let _type = match method.sig.output {
syn::ReturnType::Type(_, ref t) => *t.clone(),
syn::ReturnType::Default => {
panic!(
"Invalid field method {}: must return a value",
method.sig.ident
);
error.custom(method.sig.span(), "return value required");
return None;
}
};
let is_async = method.sig.asyncness.is_some();
let attrs = match util::FieldAttributes::from_attrs(
method.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.inputs {
match arg {
syn::FnArg::Receiver(rec) => {
if rec.reference.is_none() || rec.mutability.is_some() {
panic!(
"Invalid method receiver {}(self, ...): did you mean '&self'?",
method.sig.ident
);
}
}
syn::FnArg::Typed(ref captured) => {
let (arg_ident, is_mut) = match &*captured.pat {
syn::Pat::Ident(ref pat_ident) => {
(&pat_ident.ident, pat_ident.mutability.is_some())
}
_ => {
panic!("Invalid token for function argument");
Err(err) => {
proc_macro_error::emit_error!(err);
return None;
}
};
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 context 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 {
// Regular argument.
let parse_method =
_impl.parse_method(&method, true, |captured, arg_ident, is_mut: bool| {
let arg_name = arg_ident.unraw().to_string();
let ty = &captured.ty;
// TODO: respect graphql attribute overwrite.
let final_name = util::to_camel_case(&arg_name);
let final_name = attrs
.argument(&arg_name)
.and_then(|attrs| attrs.rename.clone().map(|ident| ident.value()))
.unwrap_or_else(|| util::to_camel_case(&arg_name));
let expect_text = format!(
"Internal error: missing argument {} - validation must have failed",
&final_name
);
let mut_modifier = if is_mut { quote!(mut) } else { quote!() };
resolve_parts.push(quote!(
if final_name.starts_with("__") {
error.no_double_underscore(
if let Some(name) = attrs
.argument(&arg_name)
.and_then(|attrs| attrs.rename.as_ref())
{
name.span_ident()
} else {
arg_ident.span()
},
);
}
let resolver = quote!(
let #mut_modifier #arg_ident = args
.get::<#ty>(#final_name)
.expect(#expect_text);
));
args.push(util::GraphQLTypeDefinitionFieldArg {
);
let field_type = util::GraphQLTypeDefinitionFieldArg {
description: attrs
.argument(&arg_name)
.and_then(|arg| arg.description.as_ref().map(|d| d.value())),
@ -149,11 +127,17 @@ fn create(args: TokenStream, body: TokenStream) -> util::GraphQLTypeDefiniton {
.and_then(|arg| arg.default.clone()),
_type: ty.clone(),
name: final_name,
})
}
}
}
};
Ok((resolver, field_type))
});
let (resolve_parts, args) = match parse_method {
Ok((resolve_parts, args)) => (resolve_parts, args),
Err(err) => {
proc_macro_error::emit_error!(err);
return None;
}
};
let body = &method.block;
let resolver_code = quote!(
@ -164,18 +148,87 @@ fn create(args: TokenStream, body: TokenStream) -> util::GraphQLTypeDefiniton {
let ident = &method.sig.ident;
let name = attrs
.name
.unwrap_or_else(|| util::to_camel_case(&ident.to_string()));
.clone()
.map(SpanContainer::into_inner)
.unwrap_or_else(|| util::to_camel_case(&ident.unraw().to_string()));
definition.fields.push(util::GraphQLTypeDefinitionField {
if name.starts_with("__") {
error.no_double_underscore(if let Some(name) = attrs.name {
name.span_ident()
} else {
ident.span()
});
}
if let Some(default) = attrs.default {
error.unsupported_attribute_within(
default.span_ident(),
UnsupportedAttribute::Default,
);
}
Some(util::GraphQLTypeDefinitionField {
name,
_type,
args,
description: attrs.description,
deprecation: attrs.deprecation,
description: attrs.description.map(SpanContainer::into_inner),
deprecation: attrs.deprecation.map(SpanContainer::into_inner),
resolver_code,
is_type_inferred: false,
is_async,
default: None,
span,
})
})
.collect::<Vec<_>>();
// Early abort after checking all fields
proc_macro_error::abort_if_dirty();
match crate::util::duplicate::Duplicate::find_by_key(&fields, |field| &field.name) {
Some(duplicates) => error.duplicate(duplicates.iter()),
None => {}
}
if name.starts_with("__") && !is_internal {
error.no_double_underscore(if let Some(name) = _impl.attrs.name {
name.span_ident()
} else {
_impl.type_ident.span()
});
}
definition
if fields.is_empty() {
error.not_empty(body_span);
}
// Early abort after GraphQL properties
proc_macro_error::abort_if_dirty();
let definition = util::GraphQLTypeDefiniton {
name,
_type: *_impl.target_type.clone(),
scalar: _impl.attrs.scalar.map(SpanContainer::into_inner),
context: _impl.attrs.context.map(SpanContainer::into_inner),
description: _impl.description,
fields,
generics: _impl.generics.clone(),
interfaces: if !_impl.attrs.interfaces.is_empty() {
Some(
_impl
.attrs
.interfaces
.into_iter()
.map(SpanContainer::into_inner)
.collect(),
)
} else {
None
},
include_type_generics: false,
generic_scalar: false,
no_async: _impl.attrs.no_async.is_some(),
};
Ok(definition)
}

View file

@ -1,8 +1,12 @@
#![allow(clippy::collapsible_if)]
use crate::util;
use proc_macro::TokenStream;
use crate::{
result::GraphQLScope,
util::{self, span_container::SpanContainer},
};
use proc_macro2::TokenStream;
use quote::quote;
use syn::spanned::Spanned;
#[derive(Debug)]
struct ScalarCodegenInput {
@ -169,46 +173,52 @@ impl syn::parse::Parse for ScalarCodegenInput {
}
/// Generate code for the juniper::graphql_scalar proc macro.
pub fn build_scalar(attributes: TokenStream, body: TokenStream, is_internal: bool) -> TokenStream {
let attrs = match syn::parse::<util::FieldAttributes>(attributes) {
Ok(attrs) => attrs,
Err(e) => {
panic!("Invalid attributes:\n{}", e);
}
};
pub fn build_scalar(
attributes: TokenStream,
body: TokenStream,
is_internal: bool,
error: GraphQLScope,
) -> syn::Result<TokenStream> {
let body_span = body.span();
let input = syn::parse_macro_input!(body as ScalarCodegenInput);
let attrs = syn::parse2::<util::FieldAttributes>(attributes)?;
let input = syn::parse2::<ScalarCodegenInput>(body)?;
let impl_for_type = input
.impl_for_type
.expect("Unable to find target for implementation target for `GraphQLScalar`");
let impl_for_type = input.impl_for_type.ok_or(error.custom_error(
body_span,
"unable to find target for implementation target for `GraphQLScalar`",
))?;
let custom_data_type = input
.custom_data_type
.expect("Unable to find custom scalar data type");
.ok_or(error.custom_error(body_span, "unable to find custom scalar data type"))?;
let resolve_body = input
.resolve_body
.expect("Unable to find body of `resolve` method");
let from_input_value_arg = input
.from_input_value_arg
.expect("Unable to find argument for `from_input_value` method");
let from_input_value_body = input
.from_input_value_body
.expect("Unable to find body of `from_input_value` method");
let from_input_value_result = input
.from_input_value_result
.expect("Unable to find return type of `from_input_value` method");
.ok_or(error.custom_error(body_span, "unable to find body of `resolve` method"))?;
let from_input_value_arg = input.from_input_value_arg.ok_or(error.custom_error(
body_span,
"unable to find argument for `from_input_value` method",
))?;
let from_input_value_body = input.from_input_value_body.ok_or(error.custom_error(
body_span,
"unable to find body of `from_input_value` method",
))?;
let from_input_value_result = input.from_input_value_result.ok_or(error.custom_error(
body_span,
"unable to find return type of `from_input_value` method",
))?;
let from_str_arg = input
.from_str_arg
.expect("Unable to find argument for `from_str` method");
.ok_or(error.custom_error(body_span, "unable to find argument for `from_str` method"))?;
let from_str_body = input
.from_str_body
.expect("Unable to find body of `from_str` method");
.ok_or(error.custom_error(body_span, "unable to find body of `from_str` method"))?;
let from_str_result = input
.from_str_result
.expect("Unable to find return type of `from_str` method");
.ok_or(error.custom_error(body_span, "unable to find return type of `from_str` method"))?;
let name = attrs
.name
.map(SpanContainer::into_inner)
.unwrap_or_else(|| impl_for_type.ident.to_string());
let crate_name = match is_internal {
true => quote!(crate),
@ -261,9 +271,15 @@ pub fn build_scalar(attributes: TokenStream, body: TokenStream, is_internal: boo
}
);
quote!(
let content = quote!(
#_async
impl#generic_type_decl #crate_name::marker::IsInputType<#generic_type> for #impl_for_type
#generic_type_bound { }
impl#generic_type_decl #crate_name::marker::IsOutputType<#generic_type> for #impl_for_type
#generic_type_bound { }
impl#generic_type_decl #crate_name::GraphQLType<#generic_type> for #impl_for_type
#generic_type_bound
{
@ -322,5 +338,7 @@ pub fn build_scalar(attributes: TokenStream, body: TokenStream, is_internal: boo
#from_str_body
}
}
).into()
);
Ok(content)
}

View file

@ -1,10 +1,10 @@
use proc_macro::TokenStream;
use proc_macro_error::MacroError;
use crate::{
result::GraphQLScope,
util::{self, span_container::SpanContainer},
};
use proc_macro2::TokenStream;
use quote::quote;
use syn::spanned::Spanned;
use crate::util;
use syn::{ext::IdentExt, spanned::Spanned};
struct ResolverVariant {
pub ty: syn::Type,
@ -36,7 +36,7 @@ impl syn::parse::Parse for ResolveBody {
}
if !input.is_empty() {
return Err(input.error("Unexpected input"));
return Err(input.error("unexpected input"));
}
Ok(Self { variants })
@ -47,15 +47,18 @@ pub fn impl_union(
is_internal: bool,
attrs: TokenStream,
body: TokenStream,
) -> Result<TokenStream, MacroError> {
let _impl = util::parse_impl::ImplBlock::parse(attrs, body);
error: GraphQLScope,
) -> syn::Result<TokenStream> {
let body_span = body.span();
let _impl = util::parse_impl::ImplBlock::parse(attrs, body)?;
// FIXME: what is the purpose of this construct?
// Validate trait target name, if present.
if let Some((name, path)) = &_impl.target_trait {
if !(name == "GraphQLUnion" || name == "juniper.GraphQLUnion") {
return Err(MacroError::new(
return Err(error.custom_error(
path.span(),
"Invalid impl target trait: expected 'GraphQLUnion'".to_string(),
"Invalid impl target trait: expected 'GraphQLUnion'",
));
}
}
@ -65,7 +68,8 @@ pub fn impl_union(
.attrs
.name
.clone()
.unwrap_or_else(|| type_ident.to_string());
.map(SpanContainer::into_inner)
.unwrap_or_else(|| type_ident.unraw().to_string());
let crate_name = util::juniper_path(is_internal);
let scalar = _impl
@ -77,36 +81,33 @@ pub fn impl_union(
quote! { #crate_name::DefaultScalarValue }
});
if !_impl.has_resolve_method() {
return Err(MacroError::new(
_impl.target_type.span(),
"Invalid impl body: expected one method with signature: fn resolve(&self) { ... }"
.to_string(),
));
}
let method = _impl
.methods
.iter()
.find(|&m| _impl.parse_resolve_method(&m).is_ok());
if _impl.methods.is_empty() || method.is_none() {
return Err(MacroError::new(
_impl.target_type.span(),
"Invalid impl body: expected one method with signature: fn resolve(&self) { ... }"
.to_string(),
));
let method = match method {
Some(method) => method,
None => {
return Err(error.custom_error(
body_span,
"expected exactly one method with signature: fn resolve(&self) { ... }",
))
}
};
let method = method.expect("checked above");
let resolve_args = _impl
.parse_resolve_method(method)
.expect("Invalid impl body: expected one method with signature: fn resolve(&self) { ... }");
let resolve_args = _impl.parse_resolve_method(method)?;
let stmts = &method.block.stmts;
let body_raw = quote!( #( #stmts )* );
let body = syn::parse::<ResolveBody>(body_raw.into())?;
if body.variants.is_empty() {
error.not_empty(method.span())
}
proc_macro_error::abort_if_dirty();
let meta_types = body.variants.iter().map(|var| {
let var_ty = &var.ty;
@ -152,7 +153,20 @@ pub fn impl_union(
let ty = _impl.target_type;
let object_marks = body.variants.iter().map(|field| {
let _ty = &field.ty;
quote!(
<#_ty as #crate_name::marker::GraphQLObjectType<#scalar>>::mark();
)
});
let output = quote! {
impl #impl_generics #crate_name::marker::IsOutputType<#scalar> for #ty #where_clause {
fn mark() {
#( #object_marks )*
}
}
impl #impl_generics #crate_name::GraphQLType<#scalar> for #ty #where_clause
{
type Context = #context;
@ -204,5 +218,6 @@ pub fn impl_union(
};
Ok(output.into())
}

View file

@ -9,6 +9,7 @@
extern crate proc_macro;
mod result;
mod util;
mod derive_enum;
@ -21,56 +22,86 @@ mod impl_scalar;
mod impl_union;
use proc_macro::TokenStream;
use proc_macro_error::proc_macro_error;
use result::GraphQLScope;
#[proc_macro_error]
#[proc_macro_derive(GraphQLEnum, attributes(graphql))]
pub fn derive_enum(input: TokenStream) -> TokenStream {
let ast = syn::parse::<syn::DeriveInput>(input).unwrap();
let gen = derive_enum::impl_enum(ast, false);
gen.into()
let gen = derive_enum::impl_enum(ast, false, GraphQLScope::DeriveEnum);
match gen {
Ok(gen) => gen.into(),
Err(err) => proc_macro_error::abort!(err),
}
}
#[proc_macro_error]
#[proc_macro_derive(GraphQLEnumInternal, attributes(graphql))]
#[doc(hidden)]
pub fn derive_enum_internal(input: TokenStream) -> TokenStream {
let ast = syn::parse::<syn::DeriveInput>(input).unwrap();
let gen = derive_enum::impl_enum(ast, true);
gen.into()
let gen = derive_enum::impl_enum(ast, true, GraphQLScope::DeriveEnum);
match gen {
Ok(gen) => gen.into(),
Err(err) => proc_macro_error::abort!(err),
}
}
#[proc_macro_error]
#[proc_macro_derive(GraphQLInputObject, attributes(graphql))]
pub fn derive_input_object(input: TokenStream) -> TokenStream {
let ast = syn::parse::<syn::DeriveInput>(input).unwrap();
let gen = derive_input_object::impl_input_object(&ast, false);
gen.into()
let gen = derive_input_object::impl_input_object(ast, false, GraphQLScope::DeriveInputObject);
match gen {
Ok(gen) => gen.into(),
Err(err) => proc_macro_error::abort!(err),
}
}
#[proc_macro_error]
#[proc_macro_derive(GraphQLInputObjectInternal, attributes(graphql))]
#[doc(hidden)]
pub fn derive_input_object_internal(input: TokenStream) -> TokenStream {
let ast = syn::parse::<syn::DeriveInput>(input).unwrap();
let gen = derive_input_object::impl_input_object(&ast, true);
gen.into()
let gen = derive_input_object::impl_input_object(ast, true, GraphQLScope::DeriveInputObject);
match gen {
Ok(gen) => gen.into(),
Err(err) => proc_macro_error::abort!(err),
}
}
#[proc_macro_error]
#[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::build_derive_object(ast, false);
gen.into()
let gen = derive_object::build_derive_object(ast, false, GraphQLScope::DeriveObject);
match gen {
Ok(gen) => gen.into(),
Err(err) => proc_macro_error::abort!(err),
}
}
#[proc_macro_error]
#[proc_macro_derive(GraphQLObjectInternal, attributes(graphql))]
pub fn derive_object_internal(input: TokenStream) -> TokenStream {
let ast = syn::parse::<syn::DeriveInput>(input).unwrap();
let gen = derive_object::build_derive_object(ast, true);
gen.into()
let gen = derive_object::build_derive_object(ast, true, GraphQLScope::DeriveObject);
match gen {
Ok(gen) => gen.into(),
Err(err) => proc_macro_error::abort!(err),
}
}
#[proc_macro_error]
#[proc_macro_derive(GraphQLUnion, attributes(graphql))]
pub fn derive_union(input: TokenStream) -> TokenStream {
let ast = syn::parse::<syn::DeriveInput>(input).unwrap();
let gen = derive_union::build_derive_union(ast, false);
gen.into()
let gen = derive_union::build_derive_union(ast, false, GraphQLScope::DeriveUnion);
match gen {
Ok(gen) => gen.into(),
Err(err) => proc_macro_error::abort!(err),
}
}
/// This custom derive macro implements the #[derive(GraphQLScalarValue)]
/// derive.
@ -114,19 +145,27 @@ pub fn derive_union(input: TokenStream) -> TokenStream {
///
/// TODO: write documentation.
///
#[proc_macro_error]
#[proc_macro_derive(GraphQLScalarValue, attributes(graphql))]
pub fn derive_scalar_value(input: TokenStream) -> TokenStream {
let ast = syn::parse::<syn::DeriveInput>(input).unwrap();
let gen = derive_scalar_value::impl_scalar_value(&ast, false);
gen.into()
let gen = derive_scalar_value::impl_scalar_value(&ast, false, GraphQLScope::DeriveScalar);
match gen {
Ok(gen) => gen.into(),
Err(err) => proc_macro_error::abort!(err),
}
}
#[proc_macro_error]
#[proc_macro_derive(GraphQLScalarValueInternal)]
#[doc(hidden)]
pub fn derive_scalar_value_internal(input: TokenStream) -> TokenStream {
let ast = syn::parse::<syn::DeriveInput>(input).unwrap();
let gen = derive_scalar_value::impl_scalar_value(&ast, true);
gen.into()
let gen = derive_scalar_value::impl_scalar_value(&ast, true, GraphQLScope::DeriveScalar);
match gen {
Ok(gen) => gen.into(),
Err(err) => proc_macro_error::abort!(err),
}
}
/**
@ -359,7 +398,9 @@ struct Query;
Scalar = MyCustomScalar,
)]
impl Query {
// ...
fn test(&self) -> i32 {
0
}
}
```
@ -382,16 +423,32 @@ impl User {
```
*/
#[proc_macro_error]
#[proc_macro_attribute]
pub fn graphql_object(args: TokenStream, input: TokenStream) -> TokenStream {
impl_object::build_object(args, input, false)
let args = proc_macro2::TokenStream::from(args);
let input = proc_macro2::TokenStream::from(input);
TokenStream::from(impl_object::build_object(
args,
input,
false,
GraphQLScope::ImplObject,
))
}
/// A proc macro for defining a GraphQL object.
#[doc(hidden)]
#[proc_macro_error]
#[proc_macro_attribute]
#[doc(hidden)]
pub fn graphql_object_internal(args: TokenStream, input: TokenStream) -> TokenStream {
impl_object::build_object(args, input, true)
let args = proc_macro2::TokenStream::from(args);
let input = proc_macro2::TokenStream::from(input);
TokenStream::from(impl_object::build_object(
args,
input,
true,
GraphQLScope::ImplObject,
))
}
/// Expose GraphQL scalars
@ -443,45 +500,81 @@ pub fn graphql_object_internal(args: TokenStream, input: TokenStream) -> TokenSt
/// In addition to implementing `GraphQLType` for the type in question,
/// `FromInputValue` and `ToInputValue` is also implemented. This makes the type
/// usable as arguments and default values.
#[proc_macro_error]
#[proc_macro_attribute]
pub fn graphql_scalar(args: TokenStream, input: TokenStream) -> TokenStream {
impl_scalar::build_scalar(args, input, false)
let args = proc_macro2::TokenStream::from(args);
let input = proc_macro2::TokenStream::from(input);
let gen = impl_scalar::build_scalar(args, input, false, GraphQLScope::ImplScalar);
match gen {
Ok(gen) => gen.into(),
Err(err) => proc_macro_error::abort!(err),
}
}
/// A proc macro for defining a GraphQL scalar.
#[doc(hidden)]
#[proc_macro_error]
#[proc_macro_attribute]
#[doc(hidden)]
pub fn graphql_scalar_internal(args: TokenStream, input: TokenStream) -> TokenStream {
impl_scalar::build_scalar(args, input, true)
let args = proc_macro2::TokenStream::from(args);
let input = proc_macro2::TokenStream::from(input);
let gen = impl_scalar::build_scalar(args, input, true, GraphQLScope::ImplScalar);
match gen {
Ok(gen) => gen.into(),
Err(err) => proc_macro_error::abort!(err),
}
}
/// A proc macro for defining a GraphQL subscription.
#[proc_macro_error]
#[proc_macro_attribute]
pub fn graphql_subscription(args: TokenStream, input: TokenStream) -> TokenStream {
impl_object::build_subscription(args, input, false)
let args = proc_macro2::TokenStream::from(args);
let input = proc_macro2::TokenStream::from(input);
TokenStream::from(impl_object::build_subscription(
args,
input,
false,
GraphQLScope::ImplObject,
))
}
#[doc(hidden)]
#[proc_macro_error]
#[proc_macro_attribute]
#[doc(hidden)]
pub fn graphql_subscription_internal(args: TokenStream, input: TokenStream) -> TokenStream {
impl_object::build_subscription(args, input, true)
let args = proc_macro2::TokenStream::from(args);
let input = proc_macro2::TokenStream::from(input);
TokenStream::from(impl_object::build_subscription(
args,
input,
true,
GraphQLScope::ImplObject,
))
}
#[proc_macro_error]
#[proc_macro_attribute]
#[proc_macro_error::proc_macro_error]
pub fn graphql_union(attrs: TokenStream, body: TokenStream) -> TokenStream {
match impl_union::impl_union(false, attrs, body) {
Ok(toks) => toks,
let attrs = proc_macro2::TokenStream::from(attrs);
let body = proc_macro2::TokenStream::from(body);
let gen = impl_union::impl_union(false, attrs, body, GraphQLScope::ImplUnion);
match gen {
Ok(gen) => gen.into(),
Err(err) => proc_macro_error::abort!(err),
}
}
#[doc(hidden)]
#[proc_macro_error]
#[proc_macro_attribute]
#[proc_macro_error::proc_macro_error]
#[doc(hidden)]
pub fn graphql_union_internal(attrs: TokenStream, body: TokenStream) -> TokenStream {
match impl_union::impl_union(true, attrs, body) {
Ok(toks) => toks,
let attrs = proc_macro2::TokenStream::from(attrs);
let body = proc_macro2::TokenStream::from(body);
let gen = impl_union::impl_union(true, attrs, body, GraphQLScope::ImplUnion);
match gen {
Ok(gen) => gen.into(),
Err(err) => proc_macro_error::abort!(err),
}
}

View file

@ -0,0 +1,138 @@
//!
use crate::util::duplicate::Duplicate;
use proc_macro2::Span;
use proc_macro_error::{Diagnostic, Level};
use std::fmt;
pub const GRAPHQL_SPECIFICATION: &'static str = "https://spec.graphql.org/June2018/";
#[allow(unused_variables)]
pub enum GraphQLScope {
DeriveObject,
DeriveInputObject,
DeriveUnion,
DeriveEnum,
DeriveScalar,
ImplUnion,
ImplScalar,
ImplObject,
}
impl GraphQLScope {
pub fn specification_section(&self) -> &str {
match self {
GraphQLScope::DeriveObject | GraphQLScope::ImplObject => "#sec-Objects",
GraphQLScope::DeriveInputObject => "#sec-Input-Objects",
GraphQLScope::DeriveUnion | GraphQLScope::ImplUnion => "#sec-Unions",
GraphQLScope::DeriveEnum => "#sec-Enums",
GraphQLScope::DeriveScalar | GraphQLScope::ImplScalar => "#sec-Scalars",
}
}
}
impl fmt::Display for GraphQLScope {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let name = match self {
GraphQLScope::DeriveObject | GraphQLScope::ImplObject => "object",
GraphQLScope::DeriveInputObject => "input object",
GraphQLScope::DeriveUnion | GraphQLScope::ImplUnion => "union",
GraphQLScope::DeriveEnum => "enum",
GraphQLScope::DeriveScalar | GraphQLScope::ImplScalar => "scalar",
};
write!(f, "GraphQL {}", name)
}
}
#[allow(unused_variables)]
#[derive(Debug)]
pub enum UnsupportedAttribute {
Skip,
Interface,
Scalar,
Description,
Deprecation,
Default,
}
impl GraphQLScope {
fn specification_link(&self) -> String {
format!("{}{}", GRAPHQL_SPECIFICATION, self.specification_section())
}
pub fn custom<S: AsRef<str>>(&self, span: Span, msg: S) {
Diagnostic::spanned(span, Level::Error, format!("{} {}", self, msg.as_ref()))
.note(self.specification_link())
.emit();
}
pub fn custom_error<S: AsRef<str>>(&self, span: Span, msg: S) -> syn::Error {
syn::Error::new(span, format!("{} {}", self, msg.as_ref()))
}
pub fn unsupported_attribute(&self, attribute: Span, kind: UnsupportedAttribute) {
Diagnostic::spanned(
attribute,
Level::Error,
format!("attribute `{:?}` can not be used at the top level of {}", kind, self),
)
.note("The macro is known to Juniper. However, not all valid #[graphql] attributes are available for each macro".to_string())
.emit();
}
pub fn unsupported_attribute_within(&self, attribute: Span, kind: UnsupportedAttribute) {
Diagnostic::spanned(
attribute,
Level::Error,
format!("attribute `{:?}` can not be used inside of {}", kind, self),
)
.note("The macro is known to Juniper. However, not all valid #[graphql] attributes are available for each macro".to_string())
.emit();
}
pub fn not_empty(&self, container: Span) {
Diagnostic::spanned(
container,
Level::Error,
format!("{} expects at least one field", self),
)
.note(self.specification_link())
.emit();
}
pub fn duplicate<'a, T: syn::spanned::Spanned + 'a>(
&self,
duplicates: impl IntoIterator<Item = &'a Duplicate<T>>,
) {
duplicates
.into_iter()
.for_each(|dup| {
(&dup.spanned[1..])
.iter()
.for_each(|spanned| {
Diagnostic::spanned(
spanned.span(),
Level::Error,
format!(
"{} does not allow fields with the same name",
self
),
)
.help(format!("There is at least one other field with the same name `{}`, possibly renamed via the #[graphql] attribute", dup.name))
.note(self.specification_link())
.emit();
});
})
}
pub fn no_double_underscore(&self, field: Span) {
Diagnostic::spanned(
field,
Level::Error,
"All types and directives defined within a schema must not have a name which begins with `__` (two underscores), as this is used exclusively by GraphQLs introspection system.".to_string(),
)
.note(format!("{}#sec-Schema", GRAPHQL_SPECIFICATION))
.emit();
}
}

View file

@ -0,0 +1,46 @@
//!
use std::collections::HashMap;
pub struct Duplicate<T> {
pub name: String,
pub spanned: Vec<T>,
}
impl<T> Duplicate<T> {
pub fn find_by_key<'a, F>(items: &'a [T], name: F) -> Option<Vec<Duplicate<&'a T>>>
where
T: 'a,
F: Fn(&'a T) -> &'a str,
{
let mut mapping: HashMap<&str, Vec<&T>> = HashMap::with_capacity(items.len());
for item in items {
if let Some(vals) = mapping.get_mut(name(item)) {
vals.push(item);
} else {
mapping.insert(name(item), vec![item]);
}
}
let duplicates = mapping
.into_iter()
.filter_map(|(k, v)| {
if v.len() != 1 {
Some(Duplicate {
name: k.to_string(),
spanned: v,
})
} else {
None
}
})
.collect::<Vec<_>>();
if !duplicates.is_empty() {
Some(duplicates)
} else {
None
}
}
}

View file

@ -1,12 +1,17 @@
#![allow(clippy::single_match)]
pub mod duplicate;
pub mod parse_impl;
pub mod span_container;
use proc_macro2::{Span, TokenStream};
use proc_macro_error::abort;
use quote::quote;
use span_container::SpanContainer;
use std::collections::HashMap;
use syn::{
parse, parse_quote, punctuated::Punctuated, Attribute, Lit, Meta, MetaList, MetaNameValue,
NestedMeta, Token,
parse, parse_quote, punctuated::Punctuated, spanned::Spanned, Attribute, Lit, Meta, MetaList,
MetaNameValue, NestedMeta, Token,
};
pub fn juniper_path(is_internal: bool) -> syn::Path {
@ -69,17 +74,6 @@ pub fn type_is_identifier_ref(ty: &syn::Type, name: &str) -> bool {
}
}
pub enum AttributeValidation {
Any,
// Bare,
String,
}
pub enum AttributeValue {
Bare,
String(String),
}
#[derive(Debug)]
pub struct DeprecationAttr {
pub reason: Option<String>,
@ -91,19 +85,22 @@ pub fn find_graphql_attr(attrs: &[Attribute]) -> Option<&Attribute> {
.find(|attr| path_eq_single(&attr.path, "graphql"))
}
pub fn get_deprecated(attrs: &[Attribute]) -> Option<DeprecationAttr> {
for attr in attrs {
match attr.parse_meta() {
pub fn get_deprecated(attrs: &[Attribute]) -> Option<SpanContainer<DeprecationAttr>> {
attrs
.iter()
.filter_map(|attr| match attr.parse_meta() {
Ok(Meta::List(ref list)) if list.path.is_ident("deprecated") => {
return Some(get_deprecated_meta_list(list));
let val = get_deprecated_meta_list(list);
Some(SpanContainer::new(list.path.span(), None, val))
}
Ok(Meta::Path(ref path)) if path.is_ident("deprecated") => {
return Some(DeprecationAttr { reason: None });
}
_ => {}
}
}
None
Ok(Meta::Path(ref path)) if path.is_ident("deprecated") => Some(SpanContainer::new(
path.span(),
None,
DeprecationAttr { reason: None },
)),
_ => None,
})
.next()
}
fn get_deprecated_meta_list(list: &MetaList) -> DeprecationAttr {
@ -116,13 +113,16 @@ fn get_deprecated_meta_list(list: &MetaList) -> DeprecationAttr {
reason: Some(strlit.value()),
};
}
_ => panic!("deprecated attribute note value only has string literal"),
_ => abort!(syn::Error::new(
nv.lit.span(),
"only strings are allowed for deprecation",
)),
}
} else {
panic!(
"Unrecognized setting on #[deprecated(..)] attribute: {:?}",
nv.path,
);
abort!(syn::Error::new(
nv.path.span(),
"unrecognized setting on #[deprecated(..)] attribute",
));
}
}
}
@ -130,10 +130,10 @@ fn get_deprecated_meta_list(list: &MetaList) -> DeprecationAttr {
}
// Gets doc comment.
pub fn get_doc_comment(attrs: &[Attribute]) -> Option<String> {
pub fn get_doc_comment(attrs: &[Attribute]) -> Option<SpanContainer<String>> {
if let Some(items) = get_doc_attr(attrs) {
if let Some(doc_strings) = get_doc_strings(&items) {
return Some(join_doc_strings(&doc_strings));
return Some(doc_strings.map(|strings| join_doc_strings(&strings)));
}
}
None
@ -167,25 +167,30 @@ fn join_doc_strings(docs: &[String]) -> String {
}
// Gets doc strings from doc comment attributes.
fn get_doc_strings(items: &[MetaNameValue]) -> Option<Vec<String>> {
fn get_doc_strings(items: &[MetaNameValue]) -> Option<SpanContainer<Vec<String>>> {
let mut span = None;
let comments = items
.iter()
.filter_map(|item| {
if item.path.is_ident("doc") {
match item.lit {
Lit::Str(ref strlit) => Some(strlit.value()),
_ => panic!("doc attributes only have string literal"),
Lit::Str(ref strlit) => {
if span.is_none() {
span = Some(strlit.span());
}
Some(strlit.value())
}
_ => abort!(syn::Error::new(
item.lit.span(),
"doc attributes only have string literal"
)),
}
} else {
None
}
})
.collect::<Vec<_>>();
if comments.is_empty() {
None
} else {
Some(comments)
}
span.map(|span| SpanContainer::new(span, None, comments))
}
// Gets doc comment attributes.
@ -203,47 +208,6 @@ fn get_doc_attr(attrs: &[Attribute]) -> Option<Vec<MetaNameValue>> {
None
}
// Get the nested items of a a #[graphql(...)] attribute.
pub fn get_graphql_attr(attrs: &[Attribute]) -> Option<Vec<NestedMeta>> {
for attr in attrs {
match attr.parse_meta() {
Ok(Meta::List(ref list)) if list.path.is_ident("graphql") => {
return Some(list.nested.iter().cloned().collect());
}
_ => {}
}
}
None
}
pub fn keyed_item_value(
item: &NestedMeta,
name: &str,
validation: AttributeValidation,
) -> Option<AttributeValue> {
match *item {
// Attributes in the form of `#[graphql(name = "value")]`.
NestedMeta::Meta(Meta::NameValue(ref nameval)) if nameval.path.is_ident(name) => {
match nameval.lit {
// We have a string attribute value.
Lit::Str(ref strlit) => Some(AttributeValue::String(strlit.value())),
_ => None,
}
}
// Attributes in the form of `#[graphql(name)]`.
NestedMeta::Meta(Meta::Path(ref path)) if path.is_ident(name) => match validation {
AttributeValidation::String => {
panic!(format!(
"Invalid format for attribute \"{:?}\": expected a string value",
item
));
}
_ => Some(AttributeValue::Bare),
},
_ => None,
}
}
// Note: duplicated from juniper crate!
#[doc(hidden)]
pub fn to_camel_case(s: &str) -> String {
@ -309,12 +273,12 @@ pub fn is_valid_name(field_name: &str) -> bool {
#[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>,
pub no_async: bool,
pub name: Option<SpanContainer<String>>,
pub description: Option<SpanContainer<String>>,
pub context: Option<SpanContainer<syn::Type>>,
pub scalar: Option<SpanContainer<syn::Type>>,
pub interfaces: Vec<SpanContainer<syn::Type>>,
pub no_async: Option<SpanContainer<()>>,
}
impl syn::parse::Parse for ObjectAttributes {
@ -325,7 +289,7 @@ impl syn::parse::Parse for ObjectAttributes {
context: None,
scalar: None,
interfaces: Vec::new(),
no_async: false,
no_async: None,
};
while !input.is_empty() {
@ -334,12 +298,20 @@ impl syn::parse::Parse for ObjectAttributes {
"name" => {
input.parse::<syn::Token![=]>()?;
let val = input.parse::<syn::LitStr>()?;
output.name = Some(val.value());
output.name = Some(SpanContainer::new(
ident.span(),
Some(val.span()),
val.value(),
));
}
"description" => {
input.parse::<syn::Token![=]>()?;
let val = input.parse::<syn::LitStr>()?;
output.description = Some(val.value());
output.description = Some(SpanContainer::new(
ident.span(),
Some(val.span()),
val.value(),
));
}
"context" | "Context" => {
input.parse::<syn::Token![=]>()?;
@ -351,12 +323,12 @@ impl syn::parse::Parse for ObjectAttributes {
} else {
input.parse::<syn::Type>()?
};
output.context = Some(ctx);
output.context = Some(SpanContainer::new(ident.span(), Some(ctx.span()), ctx));
}
"scalar" | "Scalar" => {
input.parse::<syn::Token![=]>()?;
let val = input.parse::<syn::Type>()?;
output.scalar = Some(val);
output.scalar = Some(SpanContainer::new(ident.span(), Some(val.span()), val));
}
"interfaces" => {
input.parse::<syn::Token![=]>()?;
@ -367,14 +339,17 @@ impl syn::parse::Parse for ObjectAttributes {
&content,
)?
.into_iter()
.map(|interface| {
SpanContainer::new(ident.span(), Some(interface.span()), interface)
})
.collect();
}
// FIXME: make this unneccessary.
"noasync" => {
output.no_async = true;
output.no_async = Some(SpanContainer::new(ident.span(), None, ()));
}
other => {
return Err(input.error(format!("Unknown attribute: {}", other)));
_ => {
return Err(syn::Error::new(ident.span(), "unknown attribute"));
}
}
if input.lookahead1().peek(syn::Token![,]) {
@ -409,6 +384,7 @@ impl ObjectAttributes {
#[derive(Debug)]
pub struct FieldAttributeArgument {
pub name: syn::Ident,
pub rename: Option<SpanContainer<syn::LitStr>>,
pub default: Option<syn::Expr>,
pub description: Option<syn::LitStr>,
}
@ -419,6 +395,7 @@ impl parse::Parse for FieldAttributeArgument {
let mut arg = Self {
name,
rename: None,
default: None,
description: None,
};
@ -430,15 +407,17 @@ impl parse::Parse for FieldAttributeArgument {
content.parse::<Token![=]>()?;
match name.to_string().as_str() {
"name" => {
let val: syn::LitStr = content.parse()?;
arg.rename = Some(SpanContainer::new(name.span(), Some(val.span()), val));
}
"description" => {
arg.description = Some(content.parse()?);
}
"default" => {
arg.default = Some(content.parse()?);
}
other => {
return Err(content.error(format!("Invalid attribute argument key {}", other)));
}
_ => return Err(syn::Error::new(name.span(), "unknown attribute")),
}
// Discard trailing comma.
@ -456,11 +435,12 @@ pub enum FieldAttributeParseMode {
}
enum FieldAttribute {
Name(syn::LitStr),
Description(syn::LitStr),
Deprecation(DeprecationAttr),
Skip(syn::Ident),
Name(SpanContainer<syn::LitStr>),
Description(SpanContainer<syn::LitStr>),
Deprecation(SpanContainer<DeprecationAttr>),
Skip(SpanContainer<syn::Ident>),
Arguments(HashMap<String, FieldAttributeArgument>),
Default(SpanContainer<Option<syn::Expr>>),
}
impl parse::Parse for FieldAttribute {
@ -473,30 +453,44 @@ impl parse::Parse for FieldAttribute {
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,
)))
Err(syn::Error::new(lit.span(), "name consists of not allowed characters. (must match /^[_a-zA-Z][_a-zA-Z0-9]*$/)"))
} else {
Ok(FieldAttribute::Name(lit))
Ok(FieldAttribute::Name(SpanContainer::new(
ident.span(),
Some(lit.span()),
lit,
)))
}
}
"description" => {
input.parse::<Token![=]>()?;
Ok(FieldAttribute::Description(input.parse()?))
let lit = input.parse::<syn::LitStr>()?;
Ok(FieldAttribute::Description(SpanContainer::new(
ident.span(),
Some(lit.span()),
lit,
)))
}
"deprecated" | "deprecation" => {
let reason = if input.peek(Token![=]) {
input.parse::<Token![=]>()?;
Some(input.parse::<syn::LitStr>()?.value())
Some(input.parse::<syn::LitStr>()?)
} else {
None
};
Ok(FieldAttribute::Deprecation(DeprecationAttr { reason }))
Ok(FieldAttribute::Deprecation(SpanContainer::new(
ident.span(),
reason.as_ref().map(|val| val.span()),
DeprecationAttr {
reason: reason.map(|val| val.value()),
},
)))
}
"skip" => Ok(FieldAttribute::Skip(ident)),
"skip" => Ok(FieldAttribute::Skip(SpanContainer::new(
ident.span(),
None,
ident,
))),
"arguments" => {
let arg_content;
syn::parenthesized!(arg_content in input);
@ -509,20 +503,34 @@ impl parse::Parse for FieldAttribute {
.collect();
Ok(FieldAttribute::Arguments(map))
}
other => Err(input.error(format!("Unknown attribute: {}", other))),
"default" => {
let default_expr = if input.peek(Token![=]) {
input.parse::<Token![=]>()?;
let lit = input.parse::<syn::LitStr>()?;
let default_expr = lit.parse::<syn::Expr>()?;
SpanContainer::new(ident.span(), Some(lit.span()), Some(default_expr))
} else {
SpanContainer::new(ident.span(), None, None)
};
Ok(FieldAttribute::Default(default_expr))
}
_ => Err(syn::Error::new(ident.span(), "unknown attribute")),
}
}
}
#[derive(Default)]
pub struct FieldAttributes {
pub name: Option<String>,
pub description: Option<String>,
pub deprecation: Option<DeprecationAttr>,
pub name: Option<SpanContainer<String>>,
pub description: Option<SpanContainer<String>>,
pub deprecation: Option<SpanContainer<DeprecationAttr>>,
// Only relevant for GraphQLObject derive.
pub skip: bool,
pub skip: Option<SpanContainer<syn::Ident>>,
/// Only relevant for object macro.
pub arguments: HashMap<String, FieldAttributeArgument>,
/// Only relevant for object input objects.
pub default: Option<SpanContainer<Option<syn::Expr>>>,
}
impl parse::Parse for FieldAttributes {
@ -533,27 +541,31 @@ impl parse::Parse for FieldAttributes {
name: None,
description: None,
deprecation: None,
skip: false,
skip: None,
arguments: Default::default(),
default: None,
};
for item in items {
match item {
FieldAttribute::Name(name) => {
output.name = Some(name.value());
output.name = Some(name.map(|val| val.value()));
}
FieldAttribute::Description(name) => {
output.description = Some(name.value());
output.description = Some(name.map(|val| val.value()));
}
FieldAttribute::Deprecation(attr) => {
output.deprecation = Some(attr);
}
FieldAttribute::Skip(_) => {
output.skip = true;
FieldAttribute::Skip(ident) => {
output.skip = Some(ident);
}
FieldAttribute::Arguments(args) => {
output.arguments = args;
}
FieldAttribute::Default(expr) => {
output.default = Some(expr);
}
}
}
@ -567,7 +579,7 @@ impl parse::Parse for FieldAttributes {
impl FieldAttributes {
pub fn from_attrs(
attrs: Vec<syn::Attribute>,
attrs: &[syn::Attribute],
_mode: FieldAttributeParseMode,
) -> syn::parse::Result<Self> {
let doc_comment = get_doc_comment(&attrs);
@ -611,14 +623,23 @@ pub struct GraphQLTypeDefinitionField {
pub description: Option<String>,
pub deprecation: Option<DeprecationAttr>,
pub args: Vec<GraphQLTypeDefinitionFieldArg>,
pub resolver_code: proc_macro2::TokenStream,
pub resolver_code: TokenStream,
pub is_type_inferred: bool,
pub is_async: bool,
pub default: Option<TokenStream>,
pub span: Span,
}
pub fn unraw(s: &str) -> String {
use syn::ext::IdentExt;
quote::format_ident!("{}", s).unraw().to_string()
impl syn::spanned::Spanned for GraphQLTypeDefinitionField {
fn span(&self) -> Span {
self.span
}
}
impl<'a> syn::spanned::Spanned for &'a GraphQLTypeDefinitionField {
fn span(&self) -> Span {
self.span
}
}
/// Definition of a graphql type based on information extracted
@ -656,7 +677,7 @@ impl GraphQLTypeDefiniton {
self.fields.iter().any(|field| field.is_async)
}
pub fn into_tokens(self, juniper_crate_name: &str) -> proc_macro2::TokenStream {
pub fn into_tokens(self, juniper_crate_name: &str) -> TokenStream {
let juniper_crate_name = syn::parse_str::<syn::Path>(juniper_crate_name).unwrap();
let name = &self.name;
@ -670,7 +691,7 @@ impl GraphQLTypeDefiniton {
let field_definitions = self.fields.iter().map(|field| {
let args = field.args.iter().map(|arg| {
let arg_type = &arg._type;
let arg_name = unraw(&arg.name);
let arg_name = &arg.name;
let description = match arg.description.as_ref() {
Some(value) => quote!( .description( #value ) ),
@ -710,7 +731,7 @@ impl GraphQLTypeDefiniton {
None => quote!(),
};
let field_name = unraw(&field.name);
let field_name = &field.name;
let _type = &field._type;
quote! {
@ -929,7 +950,32 @@ impl GraphQLTypeDefiniton {
)
};
// FIXME: enable this if interfaces are supported
// let marks = self.fields.iter().map(|field| {
// let field_ty = &field._type;
// let field_marks = field.args.iter().map(|arg| {
// let arg_ty = &arg._type;
// quote!(<#arg_ty as #juniper_crate_name::marker::IsInputType<#scalar>>::mark();)
// });
// quote!(
// #( #field_marks)*
// <#field_ty as #juniper_crate_name::marker::IsOutputType<#scalar>>::mark();
// )
// });
let output = quote!(
impl#impl_generics #juniper_crate_name::marker::IsOutputType<#scalar> for #ty #type_generics_tokens #where_clause {
fn mark() {
// FIXME: enable this if interfaces are supported
// #( #marks )*
}
}
impl#impl_generics #juniper_crate_name::marker::GraphQLObjectType<#scalar> for #ty #type_generics_tokens #where_clause
{ }
impl#impl_generics #juniper_crate_name::GraphQLType<#scalar> for #ty #type_generics_tokens
#where_clause
{
@ -987,7 +1033,7 @@ impl GraphQLTypeDefiniton {
output
}
pub fn into_subscription_tokens(self, juniper_crate_name: &str) -> proc_macro2::TokenStream {
pub fn into_subscription_tokens(self, juniper_crate_name: &str) -> TokenStream {
let juniper_crate_name = syn::parse_str::<syn::Path>(juniper_crate_name).unwrap();
let name = &self.name;
@ -1256,7 +1302,7 @@ impl GraphQLTypeDefiniton {
)
}
pub fn into_union_tokens(self, juniper_crate_name: &str) -> proc_macro2::TokenStream {
pub fn into_union_tokens(self, juniper_crate_name: &str) -> TokenStream {
let juniper_crate_name = syn::parse_str::<syn::Path>(juniper_crate_name).unwrap();
let name = &self.name;
@ -1425,9 +1471,22 @@ impl GraphQLTypeDefiniton {
)
});
let object_marks = self.fields.iter().map(|field| {
let _ty = &field._type;
quote!(
<#_ty as #juniper_crate_name::marker::GraphQLObjectType<#scalar>>::mark();
)
});
let mut type_impl = quote! {
#( #convesion_impls )*
impl #impl_generics #juniper_crate_name::marker::IsOutputType<#scalar> for #ty #where_clause {
fn mark() {
#( #object_marks )*
}
}
impl #impl_generics #juniper_crate_name::GraphQLType<#scalar> for #ty #where_clause
{
type Context = #context;
@ -1482,7 +1541,7 @@ impl GraphQLTypeDefiniton {
type_impl
}
pub fn into_enum_tokens(self, juniper_crate_name: &str) -> proc_macro2::TokenStream {
pub fn into_enum_tokens(self, juniper_crate_name: &str) -> TokenStream {
let juniper_crate_name = syn::parse_str::<syn::Path>(juniper_crate_name).unwrap();
let name = &self.name;
@ -1609,6 +1668,12 @@ impl GraphQLTypeDefiniton {
);
let mut body = quote!(
impl#impl_generics #juniper_crate_name::marker::IsInputType<#scalar> for #ty
#where_clause { }
impl#impl_generics #juniper_crate_name::marker::IsOutputType<#scalar> for #ty
#where_clause { }
impl#impl_generics #juniper_crate_name::GraphQLType<#scalar> for #ty
#where_clause
{
@ -1676,12 +1741,246 @@ impl GraphQLTypeDefiniton {
body
}
pub fn into_input_object_tokens(self, juniper_crate_name: &str) -> 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_else(|| quote!(()));
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)
}
});
let meta_fields = self
.fields
.iter()
.map(|field| {
// HACK: use a different interface for the GraphQLField?
let field_ty = &field._type;
let field_name = &field.name;
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 create_meta_field = match field.default {
Some(ref def) => {
quote! {
registry.arg_with_default::<#field_ty>( #field_name, &#def, &())
}
}
None => {
quote! {
registry.arg::<#field_ty>(#field_name, &())
}
}
};
quote!(
{
#create_meta_field
#description
#deprecation
},
)
})
.collect::<Vec<_>>();
let from_inputs = self.fields.iter().map(|field| {
let field_ident = &field.resolver_code;
let field_name = &field.name;
// Build from_input clause.
let from_input_default = match field.default {
Some(ref def) => {
quote! {
Some(&&#juniper_crate_name::InputValue::Null) | None if true => #def,
}
}
None => quote! {},
};
quote!(
#field_ident: {
// TODO: investigate the unwraps here, they seem dangerous!
match obj.get(#field_name) {
#from_input_default
Some(ref v) => #juniper_crate_name::FromInputValue::from_input_value(v).unwrap(),
None => {
#juniper_crate_name::FromInputValue::from_input_value(&#juniper_crate_name::InputValue::<#scalar>::null())
.unwrap()
},
}
},
)
}).collect::<Vec<_>>();
let to_inputs = self
.fields
.iter()
.map(|field| {
let field_name = &field.name;
let field_ident = &field.resolver_code;
// Build to_input clause.
quote!(
(#field_name, self.#field_ident.to_input_value()),
)
})
.collect::<Vec<_>>();
let description = self
.description
.as_ref()
.map(|description| quote!( .description(#description) ));
// 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_none() && 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));
}
let type_generics_tokens = if self.include_type_generics {
Some(type_generics)
} else {
None
};
let (impl_generics, _, where_clause) = generics.split_for_impl();
let mut where_async = where_clause.cloned().unwrap_or_else(|| parse_quote!(where));
where_async
.predicates
.push(parse_quote!( #scalar: Send + Sync ));
where_async.predicates.push(parse_quote!(Self: Send + Sync));
let async_type = quote!(
impl#impl_generics #juniper_crate_name::GraphQLTypeAsync<#scalar> for #ty #type_generics_tokens
#where_async
{}
);
// FIXME: enable this if interfaces are supported
// let marks = self.fields.iter().map(|field| {
// let _ty = &field._type;
// quote!(<#_ty as #juniper_crate_name::marker::IsInputType<#scalar>>::mark();)
// });
let mut body = quote!(
impl#impl_generics #juniper_crate_name::marker::IsInputType<#scalar> for #ty #type_generics_tokens
#where_clause {
fn mark() {
// FIXME: enable this if interfaces are supported
// #( #marks )*
}
}
impl#impl_generics #juniper_crate_name::GraphQLType<#scalar> for #ty #type_generics_tokens
#where_clause
{
type Context = #context;
type TypeInfo = ();
fn name(_: &()) -> Option<&'static str> {
Some(#name)
}
fn meta<'r>(
_: &(),
registry: &mut #juniper_crate_name::Registry<'r, #scalar>
) -> #juniper_crate_name::meta::MetaType<'r, #scalar>
where #scalar: 'r
{
let fields = &[
#( #meta_fields )*
];
registry.build_input_object_type::<#ty>(&(), fields)
#description
.into_meta()
}
}
impl#impl_generics #juniper_crate_name::FromInputValue<#scalar> for #ty #type_generics_tokens
#where_clause
{
fn from_input_value(value: &#juniper_crate_name::InputValue<#scalar>) -> Option<Self>
{
if let Some(obj) = value.to_object_value() {
let item = #ty {
#( #from_inputs )*
};
Some(item)
}
else {
None
}
}
}
impl#impl_generics #juniper_crate_name::ToInputValue<#scalar> for #ty #type_generics_tokens
#where_clause
{
fn to_input_value(&self) -> #juniper_crate_name::InputValue<#scalar> {
#juniper_crate_name::InputValue::object(vec![
#( #to_inputs )*
].into_iter().collect())
}
}
);
if !self.no_async {
body.extend(async_type);
}
body
}
}
#[cfg(test)]
mod test {
use super::*;
use quote::__private::*;
use syn::{Ident, LitStr};
fn strs_to_strings(source: Vec<&str>) -> Vec<String> {

View file

@ -1,38 +1,11 @@
//! Parse impl blocks.
#![allow(clippy::or_fun_call)]
use proc_macro::TokenStream;
use crate::util::{self, span_container::SpanContainer};
use proc_macro2::{Ident, TokenStream};
use quote::quote;
use std::{convert::From, fmt};
use crate::util;
#[derive(Debug)]
pub struct ResolveFnError(String);
impl From<&str> for ResolveFnError {
fn from(item: &str) -> Self {
ResolveFnError(item.to_string())
}
}
impl From<String> for ResolveFnError {
fn from(item: String) -> Self {
ResolveFnError(item)
}
}
impl fmt::Display for ResolveFnError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0.as_str())
}
}
impl std::error::Error for ResolveFnError {
fn description(&self) -> &str {
self.0.as_str()
}
}
use std::convert::From;
use syn::{spanned::Spanned, PatType};
pub struct ImplBlock {
pub attrs: util::ObjectAttributes,
@ -46,64 +19,96 @@ pub struct ImplBlock {
}
impl ImplBlock {
/// Check if the block has the special `resolve()` method.
pub fn has_resolve_method(&self) -> bool {
self.methods.iter().any(|m| m.sig.ident == "resolve")
}
/// Parse a 'fn resolve()' method declaration found in union or interface
/// `impl` blocks.
/// Returns the variable definitions needed for the resolve body.
/// Parse a `fn resolve()` method declaration found in most
/// generators which rely on `impl` blocks.
pub fn parse_resolve_method(
&self,
method: &syn::ImplItemMethod,
) -> Result<Vec<proc_macro2::TokenStream>, ResolveFnError> {
) -> syn::Result<Vec<TokenStream>> {
if method.sig.ident != "resolve" {
return Err("Expect a method named 'fn resolve(...)".into());
return Err(syn::Error::new(
method.sig.ident.span(),
"expect the method named `resolve`",
));
}
match &method.sig.output {
syn::ReturnType::Type(_, _) => {
return Err("resolve() method must not have a declared return type".into());
if let syn::ReturnType::Type(_, _) = &method.sig.output {
return Err(syn::Error::new(
method.sig.output.span(),
"method must not have a declared return type",
));
}
syn::ReturnType::Default => {}
};
let mut arguments = method.sig.inputs.iter();
//NOTICE: `fn resolve()` is a subset of `fn <NAME>() -> <TYPE>`
self.parse_method(method, false, |captured, _, _| {
Err(syn::Error::new(
captured.span(),
"only executor or context types are allowed",
))
})
.map(|(tokens, _empty)| tokens)
}
// Verify '&self' argument.
match arguments.next() {
/// Parse a `fn <NAME>() -> <TYPE>` method declaration found in
/// objects.
pub fn parse_method<
F: Fn(
&PatType,
&Ident,
bool,
) -> syn::Result<(TokenStream, util::GraphQLTypeDefinitionFieldArg)>,
>(
&self,
method: &syn::ImplItemMethod,
is_self_optional: bool,
f: F,
) -> syn::Result<(Vec<TokenStream>, Vec<util::GraphQLTypeDefinitionFieldArg>)> {
let mut arguments = method.sig.inputs.iter().peekable();
// Verify `&self` argument.
match arguments.peek() {
Some(syn::FnArg::Receiver(rec)) => {
let _consume = arguments.next();
if rec.reference.is_none() || rec.mutability.is_some() {
panic!(
"Invalid method receiver {}(self, ...): did you mean '&self'?",
method.sig.ident
);
return Err(syn::Error::new(
rec.span(),
"invalid argument: did you mean `&self`?",
));
}
}
_ => {
return Err("Expected a '&self' argument".into());
if !is_self_optional {
return Err(syn::Error::new(
method.sig.span(),
"expected a `&self` argument",
));
}
}
}
let mut resolve_parts = Vec::new();
let mut additional_arguments = Vec::new();
for arg in arguments {
match arg {
syn::FnArg::Receiver(_) => {
return Err(format!(
"Malformed method signature {}: self receiver must be the first argument",
method.sig.ident
)
.into());
if !is_self_optional {
return Err(syn::Error::new(
method.sig.ident.span(),
"self receiver must be the first argument",
));
}
}
syn::FnArg::Typed(captured) => {
let (arg_ident, _is_mut) = match &*captured.pat {
let (arg_ident, is_mut) = match &*captured.pat {
syn::Pat::Ident(ref pat_ident) => {
(&pat_ident.ident, pat_ident.mutability.is_some())
}
_ => {
panic!("Invalid token for function argument");
return Err(syn::Error::new(
captured.pat.span(),
"expected identifier for function argument",
));
}
};
let context_type = self.attrs.context.as_ref();
@ -114,7 +119,10 @@ impl ImplBlock {
}
// 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?");
return Err(syn::Error::new(
captured.ty.span(),
"to access the Executor, you need to specify the type as a reference.\nDid you mean &Executor?"
));
}
// Check for context arg.
else if context_type
@ -128,37 +136,28 @@ impl ImplBlock {
// without a reference. (&Context)
else if context_type
.clone()
.map(|ctx| ctx == &*captured.ty)
.map(|ctx| ctx.inner() == &*captured.ty)
.unwrap_or(false)
{
return Err(format!(
"Invalid context argument: to access the context, you need to specify the type as a reference.\nDid you mean &{}?",
quote!(captured.ty),
).into());
return Err(syn::Error::new(
captured.ty.span(),
format!("to access the context, you need to specify the type as a reference.\nDid you mean &{}?", quote!(captured.ty)),
));
} else {
return Err("Invalid argument for 'resolve' method: only executor or context are allowed".into());
let (tokens, ty) = f(captured, arg_ident, is_mut)?;
resolve_parts.push(tokens);
additional_arguments.push(ty);
}
}
}
}
Ok(resolve_parts)
Ok((resolve_parts, additional_arguments))
}
pub fn parse(attr_tokens: TokenStream, body: TokenStream) -> ImplBlock {
let attrs = match syn::parse::<util::ObjectAttributes>(attr_tokens) {
Ok(attrs) => attrs,
Err(e) => {
panic!("Invalid attributes:\n{}", e);
}
};
let mut _impl = match syn::parse::<syn::ItemImpl>(body) {
Ok(item) => item,
Err(err) => {
panic!("Parsing error:\n{}", err);
}
};
pub fn parse(attr_tokens: TokenStream, body: TokenStream) -> syn::Result<ImplBlock> {
let attrs = syn::parse2::<util::ObjectAttributes>(attr_tokens)?;
let mut _impl = syn::parse2::<syn::ItemImpl>(body)?;
let target_trait = match _impl.clone().trait_ {
Some((_, path, _)) => {
@ -176,7 +175,10 @@ impl ImplBlock {
let type_ident = if let Some(ident) = util::name_of_type(&*_impl.self_ty) {
ident
} else {
panic!("Could not determine a name for the impl type");
return Err(syn::Error::new(
_impl.self_ty.span(),
"could not determine a name for the impl type",
));
};
let target_type = _impl.self_ty.clone();
@ -194,19 +196,22 @@ impl ImplBlock {
methods.push(method);
}
_ => {
panic!("Invalid item: only type declarations and methods are allowed");
return Err(syn::Error::new(
item.span(),
"only type declarations and methods are allowed",
));
}
}
}
Self {
Ok(Self {
attrs,
type_ident,
target_trait,
target_type,
generics: _impl.generics,
description,
description: description.map(SpanContainer::into_inner),
methods,
}
})
}
}

View file

@ -0,0 +1,70 @@
use proc_macro2::{Span, TokenStream};
use quote::ToTokens;
use std::cmp::{Eq, PartialEq};
#[derive(Debug, Clone)]
pub struct SpanContainer<T> {
expr: Option<Span>,
ident: Span,
val: T,
}
impl<T: ToTokens> ToTokens for SpanContainer<T> {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.val.to_tokens(tokens)
}
}
impl<T> SpanContainer<T> {
pub fn new(ident: Span, expr: Option<Span>, val: T) -> Self {
Self { ident, expr, val }
}
pub fn span_ident(&self) -> Span {
self.ident
}
pub fn into_inner(self) -> T {
self.val
}
pub fn inner(&self) -> &T {
&self.val
}
pub fn map<U, F: Fn(T) -> U>(self, f: F) -> SpanContainer<U> {
SpanContainer {
expr: self.expr,
ident: self.ident,
val: f(self.val),
}
}
}
impl<T> AsRef<T> for SpanContainer<T> {
fn as_ref(&self) -> &T {
&self.val
}
}
impl<T> std::ops::Deref for SpanContainer<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.val
}
}
impl<T: PartialEq> PartialEq for SpanContainer<T> {
fn eq(&self, other: &Self) -> bool {
self.val == other.val
}
}
impl<T: Eq> Eq for SpanContainer<T> {}
impl<T: PartialEq> PartialEq<T> for SpanContainer<T> {
fn eq(&self, other: &T) -> bool {
&self.val == other
}
}