(codegen) Implement impl_object macro + unify code with object derive

This commit implements a new proc macro `impl_object` that replaces
the old graphql_object! macro.

The code shares a lot of similarities with the GraphQLObject
custom derive, so the code was unified to handle both
more generically.

Also, doc comment processing was standardized and improved.
This commit is contained in:
Christoph Herzog 2019-05-07 10:52:10 +02:00
parent 794568e463
commit 758f3f7d40
No known key found for this signature in database
GPG key ID: DAFF71D48B493238
5 changed files with 1200 additions and 302 deletions

View file

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

View file

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

View file

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

View file

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

View file

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