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", "juniper",
"integration_tests/juniper_tests", "integration_tests/juniper_tests",
"integration_tests/async_await", "integration_tests/async_await",
"integration_tests/codegen_fail",
"juniper_hyper", "juniper_hyper",
"juniper_iron", "juniper_iron",
"juniper_rocket", "juniper_rocket",

View file

@ -1,5 +1,5 @@
#[cfg(test)] #[cfg(test)]
use juniper::{graphql_value, GraphQLError, RootNode, Value}; use juniper::{graphql_value, EmptyMutation, EmptySubscription, GraphQLError, RootNode, Value};
#[derive(juniper::GraphQLEnum)] #[derive(juniper::GraphQLEnum)]
enum UserKind { enum UserKind {
@ -71,19 +71,9 @@ impl Query {
} }
} }
struct Mutation;
#[juniper::graphql_object]
impl Mutation {}
struct Subscription;
#[juniper::graphql_subscription]
impl Subscription {}
#[tokio::test] #[tokio::test]
async fn async_simple() { async fn async_simple() {
let schema = RootNode::new(Query, Mutation, Subscription); let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new());
let doc = r#" let doc = r#"
query { query {
fieldSync fieldSync
@ -125,7 +115,7 @@ async fn async_simple() {
#[tokio::test] #[tokio::test]
async fn async_field_validation_error() { 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#" let doc = r#"
query { query {
nonExistentField nonExistentField
@ -152,24 +142,25 @@ async fn async_field_validation_error() {
assert!(is_validation_error); assert!(is_validation_error);
} }
#[tokio::test] // FIXME: test seems broken by design, re-enable later
async fn resolve_into_stream_validation_error() { // #[tokio::test]
let schema = RootNode::new(Query, Mutation, Subscription); // async fn resolve_into_stream_validation_error() {
let doc = r#" // let schema = RootNode::new(Query, EmptyMutation::new(), EmptySubscription::new());
subscription { // let doc = r#"
nonExistent // subscription {
} // nonExistent
"#; // }
let vars = Default::default(); // "#;
let result = juniper::resolve_into_stream(doc, None, &schema, &vars, &()).await; // let vars = Default::default();
assert!(result.is_err()); // let result = juniper::resolve_into_stream(doc, None, &schema, &vars, &()).await;
// assert!(result.is_err());
let error = result.err().unwrap(); // let error = result.err().unwrap();
let is_validation_error = match error { // let is_validation_error = match error {
GraphQLError::ValidationError(_) => true, // GraphQLError::ValidationError(_) => true,
_ => false, // _ => false,
}; // };
assert!(is_validation_error); // assert!(is_validation_error);
} // }
fn main() {} 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( #[graphql(
name = "MyInput", name = "MyInput",
description = "input descr", description = "input descr",
scalar = "DefaultScalarValue" scalar = DefaultScalarValue
)] )]
struct Input { struct Input {
regular_field: String, regular_field: String,
@ -86,7 +86,7 @@ impl<'a> GraphQLType<DefaultScalarValue> for &'a Fake {
} }
#[derive(GraphQLInputObject, Debug, PartialEq)] #[derive(GraphQLInputObject, Debug, PartialEq)]
#[graphql(scalar = "DefaultScalarValue")] #[graphql(scalar = DefaultScalarValue)]
struct WithLifetime<'a> { struct WithLifetime<'a> {
regular_field: &'a Fake, 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;
mod derive_object_with_raw_idents; mod derive_object_with_raw_idents;
mod derive_union; mod derive_union;
mod impl_object;
mod impl_scalar; mod impl_scalar;
mod impl_union; mod impl_union;
mod scalar_value_transparent; 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)) - 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 ## Breaking Changes
- `juniper::graphiql` has moved to `juniper::http::graphiql` - `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 type to `RootNode`,
add subscription endpoint to `playground_source()` 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) # [[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) - 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)] #[derive(crate::GraphQLEnumInternal)]
enum UserKind { enum UserKind {
@ -70,14 +70,9 @@ impl Query {
} }
} }
struct Mutation;
#[crate::graphql_object_internal]
impl Mutation {}
#[tokio::test] #[tokio::test]
async fn async_simple() { 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#" let doc = r#"
query { query {
fieldSync fieldSync

View file

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

View file

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

View file

@ -24,7 +24,11 @@ struct Human {
struct MyQuery; struct MyQuery;
#[crate::graphql_object_internal(context = MyContext)] #[crate::graphql_object_internal(context = MyContext)]
impl MyQuery {} impl MyQuery {
fn test(&self) -> i32 {
0 // NOTICE: does not serve a purpose
}
}
type Schema = type Schema =
RootNode<'static, MyQuery, EmptyMutation<MyContext>, MySubscription, DefaultScalarValue>; 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 async_await;
pub mod base; pub mod base;
pub mod containers; pub mod containers;
pub mod marker;
pub mod name; pub mod name;
pub mod pointers; pub mod pointers;
pub mod scalars; pub mod scalars;

View file

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

View file

@ -18,8 +18,8 @@ proc-macro = true
proc-macro2 = "1.0.1" proc-macro2 = "1.0.1"
syn = { version = "1.0.3", features = ["full", "extra-traits", "parsing"] } syn = { version = "1.0.3", features = ["full", "extra-traits", "parsing"] }
quote = "1.0.3" quote = "1.0.3"
proc-macro-error = "0.3.4"
futures = "0.3.1" futures = "0.3.1"
proc-macro-error = "1.0.2"
[dev-dependencies] [dev-dependencies]
juniper = { version = "0.14.2", path = "../juniper"} 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 proc_macro2::TokenStream;
use crate::util;
use quote::quote; 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() { 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 { let variants = match ast.data {
Data::Enum(enum_data) => enum_data.variants, Data::Enum(enum_data) => enum_data.variants,
_ => { _ => return Err(error.custom_error(ast_span, "can only be applied to enums")),
panic!("#[derive(GraphlQLEnum)] may only be applied to enums, not to structs");
}
}; };
// Parse attributes. // Parse attributes.
let attrs = match util::ObjectAttributes::from_attrs(&ast.attrs) { let attrs = 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 ident = &ast.ident; let ident = &ast.ident;
let name = attrs.name.unwrap_or_else(|| ident.to_string()); let name = attrs
.name
let mut mapping = std::collections::HashMap::new(); .clone()
.map(SpanContainer::into_inner)
.unwrap_or_else(|| ident.unraw().to_string());
let fields = variants let fields = variants
.into_iter() .into_iter()
.filter_map(|field| { .filter_map(|field| {
let span = field.span();
let field_attrs = match util::FieldAttributes::from_attrs( let field_attrs = match util::FieldAttributes::from_attrs(
field.attrs, &field.attrs,
util::FieldAttributeParseMode::Object, util::FieldAttributeParseMode::Object,
) { ) {
Ok(attrs) => attrs, 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 field_name = field.ident;
let name = field_attrs let name = field_attrs
.name .name
.clone() .clone()
.unwrap_or_else(|| util::to_upper_snake_case(&field_name.to_string())); .map(SpanContainer::into_inner)
.unwrap_or_else(|| util::to_upper_snake_case(&field_name.unraw().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());
}
}
let resolver_code = quote!( #ident::#field_name ); let resolver_code = quote!( #ident::#field_name );
let _type = match field.fields { let _type = match field.fields {
Fields::Unit => syn::parse_str(&field_name.to_string()).unwrap(), 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 { Some(util::GraphQLTypeDefinitionField {
name, name,
_type, _type,
args: Vec::new(), args: Vec::new(),
description: field_attrs.description, description: field_attrs.description.map(SpanContainer::into_inner),
deprecation: field_attrs.deprecation, deprecation: field_attrs.deprecation.map(SpanContainer::into_inner),
resolver_code, resolver_code,
is_type_inferred: true, is_type_inferred: true,
is_async: false, is_async: false,
default: None,
span,
}) })
}
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if fields.len() == 0 { proc_macro_error::abort_if_dirty();
panic!("#[derive(GraphQLEnum)] requires at least one variants");
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 { let definition = util::GraphQLTypeDefiniton {
name, name,
_type: syn::parse_str(&ast.ident.to_string()).unwrap(), _type: syn::parse_str(&ast.ident.to_string()).unwrap(),
context: attrs.context, context: attrs.context.map(SpanContainer::into_inner),
scalar: None, scalar: None,
description: attrs.description, description: attrs.description.map(SpanContainer::into_inner),
fields, fields,
// NOTICE: only unit variants allow -> no generics possible // NOTICE: only unit variants allow -> no generics possible
generics: syn::Generics::default(), generics: syn::Generics::default(),
interfaces: None, interfaces: None,
include_type_generics: true, include_type_generics: true,
generic_scalar: 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" }; 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)] #![allow(clippy::match_wild_err_arm)]
use std::str::FromStr; use crate::{
result::{GraphQLScope, UnsupportedAttribute},
use proc_macro2::{Span, TokenStream}; util::{self, span_container::SpanContainer},
};
use proc_macro2::TokenStream;
use quote::{quote, ToTokens}; use quote::{quote, ToTokens};
use syn::{self, parse_quote, Data, DeriveInput, Field, Fields, Ident, Meta, NestedMeta}; use syn::{self, ext::IdentExt, spanned::Spanned, Data, Fields};
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)
};
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 { let fields = match ast.data {
Data::Struct(ref data) => match data.fields { Data::Struct(data) => match data.fields {
Fields::Named(ref named) => named.named.iter().collect::<Vec<_>>(), Fields::Named(named) => named.named,
_ => { _ => {
panic!( return Err(
"#[derive(GraphQLInputObject)] may only be used on regular structs with fields" error.custom_error(ast_span, "all fields must be named, e.g., `test: String`")
); )
} }
}, },
_ => { _ => return Err(error.custom_error(ast_span, "can only be used on structs with fields")),
panic!("#[derive(GraphlQLInputObject)] may only be applied to structs, not to enums");
}
}; };
// Parse attributes.
let attrs = util::ObjectAttributes::from_attrs(&ast.attrs)?;
// Parse attributes. // Parse attributes.
let ident = &ast.ident; let ident = &ast.ident;
let attrs = ObjAttrs::from_input(ast); let name = attrs
let name = attrs.name.unwrap_or_else(|| ast.ident.to_string()); .name
let generics = &ast.generics; .clone()
.map(SpanContainer::into_inner)
.unwrap_or_else(|| ident.to_string());
let meta_description = match attrs.description { let fields = fields
Some(descr) => quote! { let meta = meta.description(#descr); }, .into_iter()
None => quote! { let meta = meta; }, .filter_map(|field| {
}; let span = field.span();
let field_attrs = match util::FieldAttributes::from_attrs(
let mut meta_fields = TokenStream::new(); &field.attrs,
let mut from_inputs = TokenStream::new(); util::FieldAttributeParseMode::Object,
let mut to_inputs = TokenStream::new(); ) {
Ok(attrs) => attrs,
let (_, ty_generics, _) = generics.split_for_impl(); Err(e) => {
proc_macro_error::emit_error!(e);
let mut generics = generics.clone(); return None;
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));
} }
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(); let field_ident = field.ident.as_ref().unwrap();
// Build value.
let name = match field_attrs.name { let name = match field_attrs.name {
Some(ref name) => { Some(ref name) => name.to_string(),
// Custom name specified. None => crate::util::to_camel_case(&field_ident.unraw().to_string()),
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! {},
}; };
let default = { if let Some(span) = field_attrs.skip {
if field_attrs.default { error.unsupported_attribute_within(span.span(), UnsupportedAttribute::Skip)
Some(quote! { Default::default() }) }
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 { } else {
match field_attrs.default_expr { name.span()
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()),
}); });
} }
let body = quote! { let resolver_code = quote!(#field_ident);
impl#impl_generics #juniper_path::GraphQLType<#scalar> for #ident #ty_generics
#where_clause
{
type Context = ();
type TypeInfo = ();
fn name(_: &()) -> Option<&'static str> { let default = field_attrs
Some(#name) .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>( match crate::util::duplicate::Duplicate::find_by_key(&fields, |field| &field.name) {
_: &(), Some(duplicates) => error.duplicate(duplicates.iter()),
registry: &mut #juniper_path::Registry<'r, #scalar> None => {}
) -> #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()
}
} }
impl#impl_generics #juniper_path::FromInputValue<#scalar> for #ident #ty_generics if !attrs.interfaces.is_empty() {
#where_clause attrs.interfaces.iter().for_each(|elm| {
{ error.unsupported_attribute(elm.span(), UnsupportedAttribute::Interface)
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
}
}
} }
impl#impl_generics #juniper_path::ToInputValue<#scalar> for #ident #ty_generics if let Some(duplicates) =
#where_clause crate::util::duplicate::Duplicate::find_by_key(&fields, |field| field.name.as_str())
{ {
fn to_input_value(&self) -> #juniper_path::InputValue<#scalar> { error.duplicate(duplicates.iter());
#juniper_path::InputValue::object(vec![
#to_inputs
].into_iter().collect())
} }
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 proc_macro2::TokenStream;
use quote::quote; 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,
pub fn build_derive_object(ast: syn::DeriveInput, is_internal: bool) -> TokenStream { is_internal: bool,
error: GraphQLScope,
) -> syn::Result<TokenStream> {
let ast_span = ast.span();
let struct_fields = match ast.data { let struct_fields = match ast.data {
Data::Struct(data) => match data.fields { Data::Struct(data) => match data.fields {
Fields::Named(fields) => fields.named, Fields::Named(fields) => fields.named,
_ => { _ => return Err(error.custom_error(ast_span, "only named fields are allowed")),
panic!("#[derive(GraphQLObject)] may only be used on regular structs with fields");
}
}, },
_ => { _ => return Err(error.custom_error(ast_span, "can only be applied to structs")),
panic!("#[derive(GraphlQLObject)] may only be applied to structs, not to enums");
}
}; };
// Parse attributes. // Parse attributes.
let attrs = match util::ObjectAttributes::from_attrs(&ast.attrs) { let attrs = 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 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( let field_attrs = match util::FieldAttributes::from_attrs(
field.attrs, &field.attrs,
util::FieldAttributeParseMode::Object, util::FieldAttributeParseMode::Object,
) { ) {
Ok(attrs) => attrs, 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 { if field_attrs.skip.is_some() {
None return None;
} else { }
let field_name = field.ident.unwrap();
let field_name = &field.ident.unwrap();
let name = field_attrs let name = field_attrs
.name .name
.clone() .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!( let resolver_code = quote!(
&self . #field_name &self . #field_name
@ -56,29 +79,61 @@ pub fn build_derive_object(ast: syn::DeriveInput, is_internal: bool) -> TokenStr
name, name,
_type: field.ty, _type: field.ty,
args: Vec::new(), args: Vec::new(),
description: field_attrs.description, description: field_attrs.description.map(SpanContainer::into_inner),
deprecation: field_attrs.deprecation, deprecation: field_attrs.deprecation.map(SpanContainer::into_inner),
resolver_code, resolver_code,
default: None,
is_type_inferred: true, is_type_inferred: true,
is_async: false, 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 { let definition = util::GraphQLTypeDefiniton {
name, name,
_type: syn::parse_str(&ast.ident.to_string()).unwrap(), _type: syn::parse_str(&ast.ident.to_string()).unwrap(),
context: attrs.context, context: attrs.context.map(SpanContainer::into_inner),
scalar: attrs.scalar, scalar: attrs.scalar.map(SpanContainer::into_inner),
description: attrs.description, description: attrs.description.map(SpanContainer::into_inner),
fields: fields.collect(), fields,
generics: ast.generics, generics: ast.generics,
interfaces: None, interfaces: None,
include_type_generics: true, include_type_generics: true,
generic_scalar: 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" }; 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 proc_macro2::TokenStream;
use quote::quote; use quote::quote;
use syn::{self, Data, Fields, Ident, Variant}; use syn::{self, spanned::Spanned, Data, Fields, Ident, Variant};
use crate::util;
#[derive(Debug, Default)] #[derive(Debug, Default)]
struct TransparentAttributes { struct TransparentAttributes {
@ -36,9 +37,7 @@ impl syn::parse::Parse for TransparentAttributes {
"transparent" => { "transparent" => {
output.transparent = Some(true); output.transparent = Some(true);
} }
other => { _ => return Err(syn::Error::new(ident.span(), "unknown attribute")),
return Err(input.error(format!("Unknown attribute: {}", other)));
}
} }
if input.lookahead1().peek(syn::Token![,]) { if input.lookahead1().peek(syn::Token![,]) {
input.parse::<syn::Token![,]>()?; input.parse::<syn::Token![,]>()?;
@ -55,7 +54,8 @@ impl TransparentAttributes {
Some(attr) => { Some(attr) => {
let mut parsed: TransparentAttributes = attr.parse_args()?; let mut parsed: TransparentAttributes = attr.parse_args()?;
if parsed.description.is_none() { 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) 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; let ident = &ast.ident;
match ast.data { match ast.data {
Data::Enum(ref enum_data) => impl_scalar_enum(ident, enum_data, is_internal), 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), Data::Struct(ref struct_data) => impl_scalar_struct(ast, struct_data, is_internal, error),
Data::Union(_) => { Data::Union(_) => Err(error.custom_error(ast.span(), "may not be applied to unions")),
panic!("#[derive(GraphQLScalarValue)] may not be applied to unions");
}
} }
} }
@ -80,22 +82,21 @@ fn impl_scalar_struct(
ast: &syn::DeriveInput, ast: &syn::DeriveInput,
data: &syn::DataStruct, data: &syn::DataStruct,
is_internal: bool, is_internal: bool,
) -> TokenStream { error: GraphQLScope,
) -> syn::Result<TokenStream> {
let field = match data.fields { let field = match data.fields {
syn::Fields::Unnamed(ref fields) if fields.unnamed.len() == 1 => { syn::Fields::Unnamed(ref fields) if fields.unnamed.len() == 1 => {
fields.unnamed.first().unwrap() 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 ident = &ast.ident;
let attrs = match TransparentAttributes::from_attrs(&ast.attrs) { let attrs = TransparentAttributes::from_attrs(&ast.attrs)?;
Ok(attrs) => attrs,
Err(e) => {
panic!("Invalid #[graphql] attribute: {}", e);
}
};
let inner_ty = &field.ty; let inner_ty = &field.ty;
let name = attrs.name.unwrap_or_else(|| ident.to_string()); let name = attrs.name.unwrap_or_else(|| ident.to_string());
@ -133,7 +134,7 @@ fn impl_scalar_struct(
} }
); );
quote!( let content = quote!(
#_async #_async
impl<S> #crate_name::GraphQLType<S> for #ident 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) <#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 let froms = data
.variants .variants
.iter() .iter()
.map(|v| derive_from_variant(v, ident)) .map(|v| derive_from_variant(v, ident, &error))
.collect::<Result<Vec<_>, String>>() .collect::<Result<Vec<_>, _>>()?;
.unwrap_or_else(|s| panic!("{}", s));
let serialize = derive_serialize(data.variants.iter(), ident, is_internal); let serialize = derive_serialize(data.variants.iter(), ident, is_internal);
let display = derive_display(data.variants.iter(), ident); let display = derive_display(data.variants.iter(), ident);
quote! { Ok(quote! {
#(#froms)* #(#froms)*
#serialize #serialize
#display #display
} })
} }
fn derive_display<'a, I>(variants: I, ident: &Ident) -> TokenStream 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 { let ty = match variant.fields {
Fields::Unnamed(ref u) if u.unnamed.len() == 1 => &u.unnamed.first().unwrap().ty, Fields::Unnamed(ref u) if u.unnamed.len() == 1 => &u.unnamed.first().unwrap().ty,
_ => { _ => {
return Err(String::from( return Err(error.custom_error(
"Only enums with exactly one unnamed field per variant are supported", 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 proc_macro2::TokenStream;
use quote::quote; 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,
pub fn build_derive_union(ast: syn::DeriveInput, is_internal: bool) -> TokenStream { is_internal: bool,
error: GraphQLScope,
) -> syn::Result<TokenStream> {
let ast_span = ast.span();
let enum_fields = match ast.data { let enum_fields = match ast.data {
Data::Enum(data) => data.variants, Data::Enum(data) => data.variants,
_ => { _ => return Err(error.custom_error(ast_span, "can only be applied to enums")),
panic!("#[derive(GraphQLUnion)] can only be applied to enums");
}
}; };
// Parse attributes. // Parse attributes.
let attrs = match util::ObjectAttributes::from_attrs(&ast.attrs) { let attrs = 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 ident = &ast.ident; 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( let field_attrs = match util::FieldAttributes::from_attrs(
field.attrs, &field.attrs,
util::FieldAttributeParseMode::Object, util::FieldAttributeParseMode::Object,
) { ) {
Ok(attrs) => attrs, 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 variant_name = field.ident;
let name = field_attrs let name = field_attrs
.name .name
.clone() .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!( let resolver_code = quote!(
#ident :: #variant_name #ident :: #variant_name
@ -59,16 +67,44 @@ pub fn build_derive_union(ast: syn::DeriveInput, is_internal: bool) -> TokenStre
}; };
if iter.next().is_some() { 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() 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() { if let Some(description) = field_attrs.description {
panic!("#[derive(GraphQLUnion)] does not allow documentation of fields"); 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 { Some(util::GraphQLTypeDefinitionField {
@ -76,15 +112,36 @@ pub fn build_derive_union(ast: syn::DeriveInput, is_internal: bool) -> TokenStre
_type, _type,
args: Vec::new(), args: Vec::new(),
description: None, description: None,
deprecation: field_attrs.deprecation, deprecation: field_attrs.deprecation.map(SpanContainer::into_inner),
resolver_code, resolver_code,
is_type_inferred: true, is_type_inferred: true,
is_async: false, 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 // NOTICE: This is not an optimal implementation. It is possible
// to bypass this check by using a full qualified path instead // 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 { 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 { let definition = util::GraphQLTypeDefiniton {
name, name,
_type: syn::parse_str(&ast.ident.to_string()).unwrap(), _type: syn::parse_str(&ast.ident.to_string()).unwrap(),
context: attrs.context, context: attrs.context.map(SpanContainer::into_inner),
scalar: attrs.scalar, scalar: attrs.scalar.map(SpanContainer::into_inner),
description: attrs.description, description: attrs.description.map(SpanContainer::into_inner),
fields, fields,
generics: ast.generics, generics: ast.generics,
interfaces: None, interfaces: None,
include_type_generics: true, include_type_generics: true,
generic_scalar: 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" }; 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)] #![allow(clippy::collapsible_if)]
use crate::util; use crate::{
use proc_macro::TokenStream; result::{GraphQLScope, UnsupportedAttribute},
util::{self, span_container::SpanContainer},
};
use proc_macro2::TokenStream;
use quote::quote; use quote::quote;
use syn::{ext::IdentExt, spanned::Spanned};
/// Generate code for the juniper::graphql_object macro. /// Generate code for the juniper::graphql_object macro.
pub fn build_object(args: TokenStream, body: TokenStream, is_internal: bool) -> TokenStream { pub fn build_object(
let definition = create(args, body); 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" }; let juniper_crate_name = if is_internal { "crate" } else { "juniper" };
definition.into_tokens(juniper_crate_name).into() definition.into_tokens(juniper_crate_name).into()
} }
/// Generate code for the juniper::graphql_subscription macro. /// Generate code for the juniper::graphql_subscription macro.
pub fn build_subscription(args: TokenStream, body: TokenStream, is_internal: bool) -> TokenStream { pub fn build_subscription(
let definition = create(args, body); 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" }; let juniper_crate_name = if is_internal { "crate" } else { "juniper" };
definition definition
.into_subscription_tokens(juniper_crate_name) .into_subscription_tokens(juniper_crate_name)
.into() .into()
} }
fn create(args: TokenStream, body: TokenStream) -> util::GraphQLTypeDefiniton { fn create(
let _impl = util::parse_impl::ImplBlock::parse(args, body); 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 let name = _impl
.attrs .attrs
.name .name
.clone() .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 { let fields = _impl
name, .methods
_type: *_impl.target_type.clone(), .iter()
context: _impl.attrs.context, .filter_map(|method| {
scalar: _impl.attrs.scalar, let span = method.span();
description: _impl.description, let _type = match method.sig.output {
fields: Vec::new(), syn::ReturnType::Type(_, ref t) => *t.clone(),
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(),
syn::ReturnType::Default => { syn::ReturnType::Default => {
panic!( error.custom(method.sig.span(), "return value required");
"Invalid field method {}: must return a value", return None;
method.sig.ident
);
} }
}; };
let is_async = method.sig.asyncness.is_some(); let is_async = method.sig.asyncness.is_some();
let attrs = match util::FieldAttributes::from_attrs( let attrs = match util::FieldAttributes::from_attrs(
method.attrs, &method.attrs,
util::FieldAttributeParseMode::Impl, util::FieldAttributeParseMode::Impl,
) { ) {
Ok(attrs) => attrs, Ok(attrs) => attrs,
Err(err) => panic!( Err(err) => {
"Invalid #[graphql(...)] attribute on field {}:\n{}", proc_macro_error::emit_error!(err);
method.sig.ident, err return None;
),
};
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");
} }
}; };
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; 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!( let expect_text = format!(
"Internal error: missing argument {} - validation must have failed", "Internal error: missing argument {} - validation must have failed",
&final_name &final_name
); );
let mut_modifier = if is_mut { quote!(mut) } else { quote!() }; 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 let #mut_modifier #arg_ident = args
.get::<#ty>(#final_name) .get::<#ty>(#final_name)
.expect(#expect_text); .expect(#expect_text);
)); );
args.push(util::GraphQLTypeDefinitionFieldArg {
let field_type = util::GraphQLTypeDefinitionFieldArg {
description: attrs description: attrs
.argument(&arg_name) .argument(&arg_name)
.and_then(|arg| arg.description.as_ref().map(|d| d.value())), .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()), .and_then(|arg| arg.default.clone()),
_type: ty.clone(), _type: ty.clone(),
name: final_name, 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 body = &method.block;
let resolver_code = quote!( let resolver_code = quote!(
@ -164,18 +148,87 @@ fn create(args: TokenStream, body: TokenStream) -> util::GraphQLTypeDefiniton {
let ident = &method.sig.ident; let ident = &method.sig.ident;
let name = attrs let name = attrs
.name .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, name,
_type, _type,
args, args,
description: attrs.description, description: attrs.description.map(SpanContainer::into_inner),
deprecation: attrs.deprecation, deprecation: attrs.deprecation.map(SpanContainer::into_inner),
resolver_code, resolver_code,
is_type_inferred: false, is_type_inferred: false,
is_async, 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)] #![allow(clippy::collapsible_if)]
use crate::util; use crate::{
use proc_macro::TokenStream; result::GraphQLScope,
util::{self, span_container::SpanContainer},
};
use proc_macro2::TokenStream;
use quote::quote; use quote::quote;
use syn::spanned::Spanned;
#[derive(Debug)] #[derive(Debug)]
struct ScalarCodegenInput { struct ScalarCodegenInput {
@ -169,46 +173,52 @@ impl syn::parse::Parse for ScalarCodegenInput {
} }
/// Generate code for the juniper::graphql_scalar proc macro. /// Generate code for the juniper::graphql_scalar proc macro.
pub fn build_scalar(attributes: TokenStream, body: TokenStream, is_internal: bool) -> TokenStream { pub fn build_scalar(
let attrs = match syn::parse::<util::FieldAttributes>(attributes) { attributes: TokenStream,
Ok(attrs) => attrs, body: TokenStream,
Err(e) => { is_internal: bool,
panic!("Invalid attributes:\n{}", e); 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 let impl_for_type = input.impl_for_type.ok_or(error.custom_error(
.impl_for_type body_span,
.expect("Unable to find target for implementation target for `GraphQLScalar`"); "unable to find target for implementation target for `GraphQLScalar`",
))?;
let custom_data_type = input let custom_data_type = input
.custom_data_type .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 let resolve_body = input
.resolve_body .resolve_body
.expect("Unable to find body of `resolve` method"); .ok_or(error.custom_error(body_span, "unable to find body of `resolve` method"))?;
let from_input_value_arg = input let from_input_value_arg = input.from_input_value_arg.ok_or(error.custom_error(
.from_input_value_arg body_span,
.expect("Unable to find argument for `from_input_value` method"); "unable to find argument for `from_input_value` method",
let from_input_value_body = input ))?;
.from_input_value_body let from_input_value_body = input.from_input_value_body.ok_or(error.custom_error(
.expect("Unable to find body of `from_input_value` method"); body_span,
let from_input_value_result = input "unable to find body of `from_input_value` method",
.from_input_value_result ))?;
.expect("Unable to find return type 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 let from_str_arg = input
.from_str_arg .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 let from_str_body = input
.from_str_body .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 let from_str_result = input
.from_str_result .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 let name = attrs
.name .name
.map(SpanContainer::into_inner)
.unwrap_or_else(|| impl_for_type.ident.to_string()); .unwrap_or_else(|| impl_for_type.ident.to_string());
let crate_name = match is_internal { let crate_name = match is_internal {
true => quote!(crate), true => quote!(crate),
@ -261,9 +271,15 @@ pub fn build_scalar(attributes: TokenStream, body: TokenStream, is_internal: boo
} }
); );
quote!( let content = quote!(
#_async #_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 impl#generic_type_decl #crate_name::GraphQLType<#generic_type> for #impl_for_type
#generic_type_bound #generic_type_bound
{ {
@ -322,5 +338,7 @@ pub fn build_scalar(attributes: TokenStream, body: TokenStream, is_internal: boo
#from_str_body #from_str_body
} }
} }
).into() );
Ok(content)
} }

View file

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

View file

@ -9,6 +9,7 @@
extern crate proc_macro; extern crate proc_macro;
mod result;
mod util; mod util;
mod derive_enum; mod derive_enum;
@ -21,56 +22,86 @@ mod impl_scalar;
mod impl_union; mod impl_union;
use proc_macro::TokenStream; use proc_macro::TokenStream;
use proc_macro_error::proc_macro_error;
use result::GraphQLScope;
#[proc_macro_error]
#[proc_macro_derive(GraphQLEnum, attributes(graphql))] #[proc_macro_derive(GraphQLEnum, attributes(graphql))]
pub fn derive_enum(input: TokenStream) -> TokenStream { pub fn derive_enum(input: TokenStream) -> TokenStream {
let ast = syn::parse::<syn::DeriveInput>(input).unwrap(); let ast = syn::parse::<syn::DeriveInput>(input).unwrap();
let gen = derive_enum::impl_enum(ast, false); let gen = derive_enum::impl_enum(ast, false, GraphQLScope::DeriveEnum);
gen.into() match gen {
Ok(gen) => gen.into(),
Err(err) => proc_macro_error::abort!(err),
}
} }
#[proc_macro_error]
#[proc_macro_derive(GraphQLEnumInternal, attributes(graphql))] #[proc_macro_derive(GraphQLEnumInternal, attributes(graphql))]
#[doc(hidden)] #[doc(hidden)]
pub fn derive_enum_internal(input: TokenStream) -> TokenStream { pub fn derive_enum_internal(input: TokenStream) -> TokenStream {
let ast = syn::parse::<syn::DeriveInput>(input).unwrap(); let ast = syn::parse::<syn::DeriveInput>(input).unwrap();
let gen = derive_enum::impl_enum(ast, true); let gen = derive_enum::impl_enum(ast, true, GraphQLScope::DeriveEnum);
gen.into() match gen {
Ok(gen) => gen.into(),
Err(err) => proc_macro_error::abort!(err),
}
} }
#[proc_macro_error]
#[proc_macro_derive(GraphQLInputObject, attributes(graphql))] #[proc_macro_derive(GraphQLInputObject, attributes(graphql))]
pub fn derive_input_object(input: TokenStream) -> TokenStream { pub fn derive_input_object(input: TokenStream) -> TokenStream {
let ast = syn::parse::<syn::DeriveInput>(input).unwrap(); let ast = syn::parse::<syn::DeriveInput>(input).unwrap();
let gen = derive_input_object::impl_input_object(&ast, false); let gen = derive_input_object::impl_input_object(ast, false, GraphQLScope::DeriveInputObject);
gen.into() match gen {
Ok(gen) => gen.into(),
Err(err) => proc_macro_error::abort!(err),
}
} }
#[proc_macro_error]
#[proc_macro_derive(GraphQLInputObjectInternal, attributes(graphql))] #[proc_macro_derive(GraphQLInputObjectInternal, attributes(graphql))]
#[doc(hidden)] #[doc(hidden)]
pub fn derive_input_object_internal(input: TokenStream) -> TokenStream { pub fn derive_input_object_internal(input: TokenStream) -> TokenStream {
let ast = syn::parse::<syn::DeriveInput>(input).unwrap(); let ast = syn::parse::<syn::DeriveInput>(input).unwrap();
let gen = derive_input_object::impl_input_object(&ast, true); let gen = derive_input_object::impl_input_object(ast, true, GraphQLScope::DeriveInputObject);
gen.into() match gen {
Ok(gen) => gen.into(),
Err(err) => proc_macro_error::abort!(err),
}
} }
#[proc_macro_error]
#[proc_macro_derive(GraphQLObject, attributes(graphql))] #[proc_macro_derive(GraphQLObject, attributes(graphql))]
pub fn derive_object(input: TokenStream) -> TokenStream { pub fn derive_object(input: TokenStream) -> TokenStream {
let ast = syn::parse::<syn::DeriveInput>(input).unwrap(); let ast = syn::parse::<syn::DeriveInput>(input).unwrap();
let gen = derive_object::build_derive_object(ast, false); let gen = derive_object::build_derive_object(ast, false, GraphQLScope::DeriveObject);
gen.into() match gen {
Ok(gen) => gen.into(),
Err(err) => proc_macro_error::abort!(err),
}
} }
#[proc_macro_error]
#[proc_macro_derive(GraphQLObjectInternal, attributes(graphql))] #[proc_macro_derive(GraphQLObjectInternal, attributes(graphql))]
pub fn derive_object_internal(input: TokenStream) -> TokenStream { pub fn derive_object_internal(input: TokenStream) -> TokenStream {
let ast = syn::parse::<syn::DeriveInput>(input).unwrap(); let ast = syn::parse::<syn::DeriveInput>(input).unwrap();
let gen = derive_object::build_derive_object(ast, true); let gen = derive_object::build_derive_object(ast, true, GraphQLScope::DeriveObject);
gen.into() match gen {
Ok(gen) => gen.into(),
Err(err) => proc_macro_error::abort!(err),
}
} }
#[proc_macro_error]
#[proc_macro_derive(GraphQLUnion, attributes(graphql))] #[proc_macro_derive(GraphQLUnion, attributes(graphql))]
pub fn derive_union(input: TokenStream) -> TokenStream { pub fn derive_union(input: TokenStream) -> TokenStream {
let ast = syn::parse::<syn::DeriveInput>(input).unwrap(); let ast = syn::parse::<syn::DeriveInput>(input).unwrap();
let gen = derive_union::build_derive_union(ast, false); let gen = derive_union::build_derive_union(ast, false, GraphQLScope::DeriveUnion);
gen.into() match gen {
Ok(gen) => gen.into(),
Err(err) => proc_macro_error::abort!(err),
}
} }
/// This custom derive macro implements the #[derive(GraphQLScalarValue)] /// This custom derive macro implements the #[derive(GraphQLScalarValue)]
/// derive. /// derive.
@ -114,19 +145,27 @@ pub fn derive_union(input: TokenStream) -> TokenStream {
/// ///
/// TODO: write documentation. /// TODO: write documentation.
/// ///
#[proc_macro_error]
#[proc_macro_derive(GraphQLScalarValue, attributes(graphql))] #[proc_macro_derive(GraphQLScalarValue, attributes(graphql))]
pub fn derive_scalar_value(input: TokenStream) -> TokenStream { pub fn derive_scalar_value(input: TokenStream) -> TokenStream {
let ast = syn::parse::<syn::DeriveInput>(input).unwrap(); let ast = syn::parse::<syn::DeriveInput>(input).unwrap();
let gen = derive_scalar_value::impl_scalar_value(&ast, false); let gen = derive_scalar_value::impl_scalar_value(&ast, false, GraphQLScope::DeriveScalar);
gen.into() match gen {
Ok(gen) => gen.into(),
Err(err) => proc_macro_error::abort!(err),
}
} }
#[proc_macro_error]
#[proc_macro_derive(GraphQLScalarValueInternal)] #[proc_macro_derive(GraphQLScalarValueInternal)]
#[doc(hidden)] #[doc(hidden)]
pub fn derive_scalar_value_internal(input: TokenStream) -> TokenStream { pub fn derive_scalar_value_internal(input: TokenStream) -> TokenStream {
let ast = syn::parse::<syn::DeriveInput>(input).unwrap(); let ast = syn::parse::<syn::DeriveInput>(input).unwrap();
let gen = derive_scalar_value::impl_scalar_value(&ast, true); let gen = derive_scalar_value::impl_scalar_value(&ast, true, GraphQLScope::DeriveScalar);
gen.into() match gen {
Ok(gen) => gen.into(),
Err(err) => proc_macro_error::abort!(err),
}
} }
/** /**
@ -359,7 +398,9 @@ struct Query;
Scalar = MyCustomScalar, Scalar = MyCustomScalar,
)] )]
impl Query { impl Query {
// ... fn test(&self) -> i32 {
0
}
} }
``` ```
@ -382,16 +423,32 @@ impl User {
``` ```
*/ */
#[proc_macro_error]
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn graphql_object(args: TokenStream, input: TokenStream) -> TokenStream { 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. /// A proc macro for defining a GraphQL object.
#[doc(hidden)] #[proc_macro_error]
#[proc_macro_attribute] #[proc_macro_attribute]
#[doc(hidden)]
pub fn graphql_object_internal(args: TokenStream, input: TokenStream) -> TokenStream { 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 /// 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, /// In addition to implementing `GraphQLType` for the type in question,
/// `FromInputValue` and `ToInputValue` is also implemented. This makes the type /// `FromInputValue` and `ToInputValue` is also implemented. This makes the type
/// usable as arguments and default values. /// usable as arguments and default values.
#[proc_macro_error]
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn graphql_scalar(args: TokenStream, input: TokenStream) -> TokenStream { 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. /// A proc macro for defining a GraphQL scalar.
#[doc(hidden)] #[proc_macro_error]
#[proc_macro_attribute] #[proc_macro_attribute]
#[doc(hidden)]
pub fn graphql_scalar_internal(args: TokenStream, input: TokenStream) -> TokenStream { 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. /// A proc macro for defining a GraphQL subscription.
#[proc_macro_error]
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn graphql_subscription(args: TokenStream, input: TokenStream) -> TokenStream { 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] #[proc_macro_attribute]
#[doc(hidden)]
pub fn graphql_subscription_internal(args: TokenStream, input: TokenStream) -> TokenStream { 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_attribute]
#[proc_macro_error::proc_macro_error]
pub fn graphql_union(attrs: TokenStream, body: TokenStream) -> TokenStream { pub fn graphql_union(attrs: TokenStream, body: TokenStream) -> TokenStream {
match impl_union::impl_union(false, attrs, body) { let attrs = proc_macro2::TokenStream::from(attrs);
Ok(toks) => toks, 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), Err(err) => proc_macro_error::abort!(err),
} }
} }
#[doc(hidden)] #[proc_macro_error]
#[proc_macro_attribute] #[proc_macro_attribute]
#[proc_macro_error::proc_macro_error] #[doc(hidden)]
pub fn graphql_union_internal(attrs: TokenStream, body: TokenStream) -> TokenStream { pub fn graphql_union_internal(attrs: TokenStream, body: TokenStream) -> TokenStream {
match impl_union::impl_union(true, attrs, body) { let attrs = proc_macro2::TokenStream::from(attrs);
Ok(toks) => toks, 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), 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)] #![allow(clippy::single_match)]
pub mod duplicate;
pub mod parse_impl; pub mod parse_impl;
pub mod span_container;
use proc_macro2::{Span, TokenStream};
use proc_macro_error::abort;
use quote::quote; use quote::quote;
use span_container::SpanContainer;
use std::collections::HashMap; use std::collections::HashMap;
use syn::{ use syn::{
parse, parse_quote, punctuated::Punctuated, Attribute, Lit, Meta, MetaList, MetaNameValue, parse, parse_quote, punctuated::Punctuated, spanned::Spanned, Attribute, Lit, Meta, MetaList,
NestedMeta, Token, MetaNameValue, NestedMeta, Token,
}; };
pub fn juniper_path(is_internal: bool) -> syn::Path { 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)] #[derive(Debug)]
pub struct DeprecationAttr { pub struct DeprecationAttr {
pub reason: Option<String>, 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")) .find(|attr| path_eq_single(&attr.path, "graphql"))
} }
pub fn get_deprecated(attrs: &[Attribute]) -> Option<DeprecationAttr> { pub fn get_deprecated(attrs: &[Attribute]) -> Option<SpanContainer<DeprecationAttr>> {
for attr in attrs { attrs
match attr.parse_meta() { .iter()
.filter_map(|attr| match attr.parse_meta() {
Ok(Meta::List(ref list)) if list.path.is_ident("deprecated") => { 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") => { Ok(Meta::Path(ref path)) if path.is_ident("deprecated") => Some(SpanContainer::new(
return Some(DeprecationAttr { reason: None }); path.span(),
} None,
_ => {} DeprecationAttr { reason: None },
} )),
} _ => None,
None })
.next()
} }
fn get_deprecated_meta_list(list: &MetaList) -> DeprecationAttr { fn get_deprecated_meta_list(list: &MetaList) -> DeprecationAttr {
@ -116,13 +113,16 @@ fn get_deprecated_meta_list(list: &MetaList) -> DeprecationAttr {
reason: Some(strlit.value()), 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 { } else {
panic!( abort!(syn::Error::new(
"Unrecognized setting on #[deprecated(..)] attribute: {:?}", nv.path.span(),
nv.path, "unrecognized setting on #[deprecated(..)] attribute",
); ));
} }
} }
} }
@ -130,10 +130,10 @@ fn get_deprecated_meta_list(list: &MetaList) -> DeprecationAttr {
} }
// Gets doc comment. // 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(items) = get_doc_attr(attrs) {
if let Some(doc_strings) = get_doc_strings(&items) { 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 None
@ -167,25 +167,30 @@ fn join_doc_strings(docs: &[String]) -> String {
} }
// Gets doc strings from doc comment attributes. // 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 let comments = items
.iter() .iter()
.filter_map(|item| { .filter_map(|item| {
if item.path.is_ident("doc") { if item.path.is_ident("doc") {
match item.lit { match item.lit {
Lit::Str(ref strlit) => Some(strlit.value()), Lit::Str(ref strlit) => {
_ => panic!("doc attributes only have string literal"), 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 { } else {
None None
} }
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if comments.is_empty() { span.map(|span| SpanContainer::new(span, None, comments))
None
} else {
Some(comments)
}
} }
// Gets doc comment attributes. // Gets doc comment attributes.
@ -203,47 +208,6 @@ fn get_doc_attr(attrs: &[Attribute]) -> Option<Vec<MetaNameValue>> {
None 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! // Note: duplicated from juniper crate!
#[doc(hidden)] #[doc(hidden)]
pub fn to_camel_case(s: &str) -> String { pub fn to_camel_case(s: &str) -> String {
@ -309,12 +273,12 @@ pub fn is_valid_name(field_name: &str) -> bool {
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct ObjectAttributes { pub struct ObjectAttributes {
pub name: Option<String>, pub name: Option<SpanContainer<String>>,
pub description: Option<String>, pub description: Option<SpanContainer<String>>,
pub context: Option<syn::Type>, pub context: Option<SpanContainer<syn::Type>>,
pub scalar: Option<syn::Type>, pub scalar: Option<SpanContainer<syn::Type>>,
pub interfaces: Vec<syn::Type>, pub interfaces: Vec<SpanContainer<syn::Type>>,
pub no_async: bool, pub no_async: Option<SpanContainer<()>>,
} }
impl syn::parse::Parse for ObjectAttributes { impl syn::parse::Parse for ObjectAttributes {
@ -325,7 +289,7 @@ impl syn::parse::Parse for ObjectAttributes {
context: None, context: None,
scalar: None, scalar: None,
interfaces: Vec::new(), interfaces: Vec::new(),
no_async: false, no_async: None,
}; };
while !input.is_empty() { while !input.is_empty() {
@ -334,12 +298,20 @@ impl syn::parse::Parse for ObjectAttributes {
"name" => { "name" => {
input.parse::<syn::Token![=]>()?; input.parse::<syn::Token![=]>()?;
let val = input.parse::<syn::LitStr>()?; let val = input.parse::<syn::LitStr>()?;
output.name = Some(val.value()); output.name = Some(SpanContainer::new(
ident.span(),
Some(val.span()),
val.value(),
));
} }
"description" => { "description" => {
input.parse::<syn::Token![=]>()?; input.parse::<syn::Token![=]>()?;
let val = input.parse::<syn::LitStr>()?; 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" => { "context" | "Context" => {
input.parse::<syn::Token![=]>()?; input.parse::<syn::Token![=]>()?;
@ -351,12 +323,12 @@ impl syn::parse::Parse for ObjectAttributes {
} else { } else {
input.parse::<syn::Type>()? input.parse::<syn::Type>()?
}; };
output.context = Some(ctx); output.context = Some(SpanContainer::new(ident.span(), Some(ctx.span()), ctx));
} }
"scalar" | "Scalar" => { "scalar" | "Scalar" => {
input.parse::<syn::Token![=]>()?; input.parse::<syn::Token![=]>()?;
let val = input.parse::<syn::Type>()?; let val = input.parse::<syn::Type>()?;
output.scalar = Some(val); output.scalar = Some(SpanContainer::new(ident.span(), Some(val.span()), val));
} }
"interfaces" => { "interfaces" => {
input.parse::<syn::Token![=]>()?; input.parse::<syn::Token![=]>()?;
@ -367,14 +339,17 @@ impl syn::parse::Parse for ObjectAttributes {
&content, &content,
)? )?
.into_iter() .into_iter()
.map(|interface| {
SpanContainer::new(ident.span(), Some(interface.span()), interface)
})
.collect(); .collect();
} }
// FIXME: make this unneccessary. // FIXME: make this unneccessary.
"noasync" => { "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![,]) { if input.lookahead1().peek(syn::Token![,]) {
@ -409,6 +384,7 @@ impl ObjectAttributes {
#[derive(Debug)] #[derive(Debug)]
pub struct FieldAttributeArgument { pub struct FieldAttributeArgument {
pub name: syn::Ident, pub name: syn::Ident,
pub rename: Option<SpanContainer<syn::LitStr>>,
pub default: Option<syn::Expr>, pub default: Option<syn::Expr>,
pub description: Option<syn::LitStr>, pub description: Option<syn::LitStr>,
} }
@ -419,6 +395,7 @@ impl parse::Parse for FieldAttributeArgument {
let mut arg = Self { let mut arg = Self {
name, name,
rename: None,
default: None, default: None,
description: None, description: None,
}; };
@ -430,15 +407,17 @@ impl parse::Parse for FieldAttributeArgument {
content.parse::<Token![=]>()?; content.parse::<Token![=]>()?;
match name.to_string().as_str() { 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" => { "description" => {
arg.description = Some(content.parse()?); arg.description = Some(content.parse()?);
} }
"default" => { "default" => {
arg.default = Some(content.parse()?); arg.default = Some(content.parse()?);
} }
other => { _ => return Err(syn::Error::new(name.span(), "unknown attribute")),
return Err(content.error(format!("Invalid attribute argument key {}", other)));
}
} }
// Discard trailing comma. // Discard trailing comma.
@ -456,11 +435,12 @@ pub enum FieldAttributeParseMode {
} }
enum FieldAttribute { enum FieldAttribute {
Name(syn::LitStr), Name(SpanContainer<syn::LitStr>),
Description(syn::LitStr), Description(SpanContainer<syn::LitStr>),
Deprecation(DeprecationAttr), Deprecation(SpanContainer<DeprecationAttr>),
Skip(syn::Ident), Skip(SpanContainer<syn::Ident>),
Arguments(HashMap<String, FieldAttributeArgument>), Arguments(HashMap<String, FieldAttributeArgument>),
Default(SpanContainer<Option<syn::Expr>>),
} }
impl parse::Parse for FieldAttribute { impl parse::Parse for FieldAttribute {
@ -473,30 +453,44 @@ impl parse::Parse for FieldAttribute {
let lit = input.parse::<syn::LitStr>()?; let lit = input.parse::<syn::LitStr>()?;
let raw = lit.value(); let raw = lit.value();
if !is_valid_name(&raw) { if !is_valid_name(&raw) {
Err(input.error(format!( Err(syn::Error::new(lit.span(), "name consists of not allowed characters. (must match /^[_a-zA-Z][_a-zA-Z0-9]*$/)"))
"Invalid #[graphql(name = ...)] attribute: \n\
'{}' is not a valid field name\nNames must \
match /^[_a-zA-Z][_a-zA-Z0-9]*$/",
raw,
)))
} else { } else {
Ok(FieldAttribute::Name(lit)) Ok(FieldAttribute::Name(SpanContainer::new(
ident.span(),
Some(lit.span()),
lit,
)))
} }
} }
"description" => { "description" => {
input.parse::<Token![=]>()?; 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" => { "deprecated" | "deprecation" => {
let reason = if input.peek(Token![=]) { let reason = if input.peek(Token![=]) {
input.parse::<Token![=]>()?; input.parse::<Token![=]>()?;
Some(input.parse::<syn::LitStr>()?.value()) Some(input.parse::<syn::LitStr>()?)
} else { } else {
None 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" => { "arguments" => {
let arg_content; let arg_content;
syn::parenthesized!(arg_content in input); syn::parenthesized!(arg_content in input);
@ -509,20 +503,34 @@ impl parse::Parse for FieldAttribute {
.collect(); .collect();
Ok(FieldAttribute::Arguments(map)) 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)] #[derive(Default)]
pub struct FieldAttributes { pub struct FieldAttributes {
pub name: Option<String>, pub name: Option<SpanContainer<String>>,
pub description: Option<String>, pub description: Option<SpanContainer<String>>,
pub deprecation: Option<DeprecationAttr>, pub deprecation: Option<SpanContainer<DeprecationAttr>>,
// Only relevant for GraphQLObject derive. // Only relevant for GraphQLObject derive.
pub skip: bool, pub skip: Option<SpanContainer<syn::Ident>>,
/// Only relevant for object macro. /// Only relevant for object macro.
pub arguments: HashMap<String, FieldAttributeArgument>, pub arguments: HashMap<String, FieldAttributeArgument>,
/// Only relevant for object input objects.
pub default: Option<SpanContainer<Option<syn::Expr>>>,
} }
impl parse::Parse for FieldAttributes { impl parse::Parse for FieldAttributes {
@ -533,27 +541,31 @@ impl parse::Parse for FieldAttributes {
name: None, name: None,
description: None, description: None,
deprecation: None, deprecation: None,
skip: false, skip: None,
arguments: Default::default(), arguments: Default::default(),
default: None,
}; };
for item in items { for item in items {
match item { match item {
FieldAttribute::Name(name) => { FieldAttribute::Name(name) => {
output.name = Some(name.value()); output.name = Some(name.map(|val| val.value()));
} }
FieldAttribute::Description(name) => { FieldAttribute::Description(name) => {
output.description = Some(name.value()); output.description = Some(name.map(|val| val.value()));
} }
FieldAttribute::Deprecation(attr) => { FieldAttribute::Deprecation(attr) => {
output.deprecation = Some(attr); output.deprecation = Some(attr);
} }
FieldAttribute::Skip(_) => { FieldAttribute::Skip(ident) => {
output.skip = true; output.skip = Some(ident);
} }
FieldAttribute::Arguments(args) => { FieldAttribute::Arguments(args) => {
output.arguments = args; output.arguments = args;
} }
FieldAttribute::Default(expr) => {
output.default = Some(expr);
}
} }
} }
@ -567,7 +579,7 @@ impl parse::Parse for FieldAttributes {
impl FieldAttributes { impl FieldAttributes {
pub fn from_attrs( pub fn from_attrs(
attrs: Vec<syn::Attribute>, attrs: &[syn::Attribute],
_mode: FieldAttributeParseMode, _mode: FieldAttributeParseMode,
) -> syn::parse::Result<Self> { ) -> syn::parse::Result<Self> {
let doc_comment = get_doc_comment(&attrs); let doc_comment = get_doc_comment(&attrs);
@ -611,14 +623,23 @@ pub struct GraphQLTypeDefinitionField {
pub description: Option<String>, pub description: Option<String>,
pub deprecation: Option<DeprecationAttr>, pub deprecation: Option<DeprecationAttr>,
pub args: Vec<GraphQLTypeDefinitionFieldArg>, pub args: Vec<GraphQLTypeDefinitionFieldArg>,
pub resolver_code: proc_macro2::TokenStream, pub resolver_code: TokenStream,
pub is_type_inferred: bool, pub is_type_inferred: bool,
pub is_async: bool, pub is_async: bool,
pub default: Option<TokenStream>,
pub span: Span,
} }
pub fn unraw(s: &str) -> String { impl syn::spanned::Spanned for GraphQLTypeDefinitionField {
use syn::ext::IdentExt; fn span(&self) -> Span {
quote::format_ident!("{}", s).unraw().to_string() 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 /// Definition of a graphql type based on information extracted
@ -656,7 +677,7 @@ impl GraphQLTypeDefiniton {
self.fields.iter().any(|field| field.is_async) 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 juniper_crate_name = syn::parse_str::<syn::Path>(juniper_crate_name).unwrap();
let name = &self.name; let name = &self.name;
@ -670,7 +691,7 @@ impl GraphQLTypeDefiniton {
let field_definitions = self.fields.iter().map(|field| { let field_definitions = self.fields.iter().map(|field| {
let args = field.args.iter().map(|arg| { let args = field.args.iter().map(|arg| {
let arg_type = &arg._type; let arg_type = &arg._type;
let arg_name = unraw(&arg.name); let arg_name = &arg.name;
let description = match arg.description.as_ref() { let description = match arg.description.as_ref() {
Some(value) => quote!( .description( #value ) ), Some(value) => quote!( .description( #value ) ),
@ -710,7 +731,7 @@ impl GraphQLTypeDefiniton {
None => quote!(), None => quote!(),
}; };
let field_name = unraw(&field.name); let field_name = &field.name;
let _type = &field._type; let _type = &field._type;
quote! { 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!( 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 impl#impl_generics #juniper_crate_name::GraphQLType<#scalar> for #ty #type_generics_tokens
#where_clause #where_clause
{ {
@ -987,7 +1033,7 @@ impl GraphQLTypeDefiniton {
output 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 juniper_crate_name = syn::parse_str::<syn::Path>(juniper_crate_name).unwrap();
let name = &self.name; 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 juniper_crate_name = syn::parse_str::<syn::Path>(juniper_crate_name).unwrap();
let name = &self.name; 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! { let mut type_impl = quote! {
#( #convesion_impls )* #( #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 impl #impl_generics #juniper_crate_name::GraphQLType<#scalar> for #ty #where_clause
{ {
type Context = #context; type Context = #context;
@ -1482,7 +1541,7 @@ impl GraphQLTypeDefiniton {
type_impl 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 juniper_crate_name = syn::parse_str::<syn::Path>(juniper_crate_name).unwrap();
let name = &self.name; let name = &self.name;
@ -1609,6 +1668,12 @@ impl GraphQLTypeDefiniton {
); );
let mut body = quote!( 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 impl#impl_generics #juniper_crate_name::GraphQLType<#scalar> for #ty
#where_clause #where_clause
{ {
@ -1676,12 +1741,246 @@ impl GraphQLTypeDefiniton {
body 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)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use quote::__private::*;
use syn::{Ident, LitStr}; use syn::{Ident, LitStr};
fn strs_to_strings(source: Vec<&str>) -> Vec<String> { fn strs_to_strings(source: Vec<&str>) -> Vec<String> {

View file

@ -1,38 +1,11 @@
//! Parse impl blocks. //! Parse impl blocks.
#![allow(clippy::or_fun_call)] #![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 quote::quote;
use std::{convert::From, fmt}; use std::convert::From;
use syn::{spanned::Spanned, PatType};
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()
}
}
pub struct ImplBlock { pub struct ImplBlock {
pub attrs: util::ObjectAttributes, pub attrs: util::ObjectAttributes,
@ -46,64 +19,96 @@ pub struct ImplBlock {
} }
impl ImplBlock { impl ImplBlock {
/// Check if the block has the special `resolve()` method. /// Parse a `fn resolve()` method declaration found in most
pub fn has_resolve_method(&self) -> bool { /// generators which rely on `impl` blocks.
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.
pub fn parse_resolve_method( pub fn parse_resolve_method(
&self, &self,
method: &syn::ImplItemMethod, method: &syn::ImplItemMethod,
) -> Result<Vec<proc_macro2::TokenStream>, ResolveFnError> { ) -> syn::Result<Vec<TokenStream>> {
if method.sig.ident != "resolve" { 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 { if let syn::ReturnType::Type(_, _) = &method.sig.output {
syn::ReturnType::Type(_, _) => { return Err(syn::Error::new(
return Err("resolve() method must not have a declared return type".into()); 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. /// Parse a `fn <NAME>() -> <TYPE>` method declaration found in
match arguments.next() { /// 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)) => { Some(syn::FnArg::Receiver(rec)) => {
let _consume = arguments.next();
if rec.reference.is_none() || rec.mutability.is_some() { if rec.reference.is_none() || rec.mutability.is_some() {
panic!( return Err(syn::Error::new(
"Invalid method receiver {}(self, ...): did you mean '&self'?", rec.span(),
method.sig.ident "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 resolve_parts = Vec::new();
let mut additional_arguments = Vec::new();
for arg in arguments { for arg in arguments {
match arg { match arg {
syn::FnArg::Receiver(_) => { syn::FnArg::Receiver(_) => {
return Err(format!( if !is_self_optional {
"Malformed method signature {}: self receiver must be the first argument", return Err(syn::Error::new(
method.sig.ident method.sig.ident.span(),
) "self receiver must be the first argument",
.into()); ));
}
} }
syn::FnArg::Typed(captured) => { 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) => { syn::Pat::Ident(ref pat_ident) => {
(&pat_ident.ident, pat_ident.mutability.is_some()) (&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(); let context_type = self.attrs.context.as_ref();
@ -114,7 +119,10 @@ impl ImplBlock {
} }
// Make sure executor is specified as a reference. // Make sure executor is specified as a reference.
else if util::type_is_identifier(&captured.ty, "Executor") { 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. // Check for context arg.
else if context_type else if context_type
@ -128,37 +136,28 @@ impl ImplBlock {
// without a reference. (&Context) // without a reference. (&Context)
else if context_type else if context_type
.clone() .clone()
.map(|ctx| ctx == &*captured.ty) .map(|ctx| ctx.inner() == &*captured.ty)
.unwrap_or(false) .unwrap_or(false)
{ {
return Err(format!( return Err(syn::Error::new(
"Invalid context argument: to access the context, you need to specify the type as a reference.\nDid you mean &{}?", captured.ty.span(),
quote!(captured.ty), format!("to access the context, you need to specify the type as a reference.\nDid you mean &{}?", quote!(captured.ty)),
).into()); ));
} else { } 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 { pub fn parse(attr_tokens: TokenStream, body: TokenStream) -> syn::Result<ImplBlock> {
let attrs = match syn::parse::<util::ObjectAttributes>(attr_tokens) { let attrs = syn::parse2::<util::ObjectAttributes>(attr_tokens)?;
Ok(attrs) => attrs, let mut _impl = syn::parse2::<syn::ItemImpl>(body)?;
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);
}
};
let target_trait = match _impl.clone().trait_ { let target_trait = match _impl.clone().trait_ {
Some((_, path, _)) => { Some((_, path, _)) => {
@ -176,7 +175,10 @@ impl ImplBlock {
let type_ident = if let Some(ident) = util::name_of_type(&*_impl.self_ty) { let type_ident = if let Some(ident) = util::name_of_type(&*_impl.self_ty) {
ident ident
} else { } 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(); let target_type = _impl.self_ty.clone();
@ -194,19 +196,22 @@ impl ImplBlock {
methods.push(method); 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, attrs,
type_ident, type_ident,
target_trait, target_trait,
target_type, target_type,
generics: _impl.generics, generics: _impl.generics,
description, description: description.map(SpanContainer::into_inner),
methods, 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
}
}