Skip to content

Make graphql_object macro supports deriving object field resolvers #652

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions integration_tests/juniper_tests/src/codegen/mod.rs
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ mod derive_object;
mod derive_object_with_raw_idents;
mod derive_union;
mod impl_object;
mod impl_object_with_derive_fields;
mod impl_scalar;
mod impl_union;
mod scalar_value_transparent;
4 changes: 4 additions & 0 deletions juniper/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -32,6 +32,10 @@ See [#618](https://github.com/graphql-rust/juniper/pull/618).
- Better error messages for all proc macros (see
[#631](https://github.com/graphql-rust/juniper/pull/631)

- Procedural macro `graphql_object` supports deriving resolvers for fields in
struct (see [#553](https://github.com/graphql-rust/juniper/issues/553))
- requires derive macro `GraphQLObjectInfo`.

## Breaking Changes

- `juniper::graphiql` has moved to `juniper::http::graphiql`
4 changes: 2 additions & 2 deletions juniper/src/lib.rs
Original file line number Diff line number Diff line change
@@ -116,7 +116,7 @@ extern crate bson;
// functionality automatically.
pub use juniper_codegen::{
graphql_object, graphql_scalar, graphql_subscription, graphql_union, GraphQLEnum,
GraphQLInputObject, GraphQLObject, GraphQLScalarValue, GraphQLUnion,
GraphQLInputObject, GraphQLObject, GraphQLObjectInfo, GraphQLScalarValue, GraphQLUnion,
};
// Internal macros are not exported,
// but declared at the root to make them easier to use.
@@ -178,7 +178,7 @@ pub use crate::{
},
types::{
async_await::GraphQLTypeAsync,
base::{Arguments, GraphQLType, TypeKind},
base::{Arguments, GraphQLType, GraphQLTypeInfo, TypeKind},
marker,
scalars::{EmptyMutation, EmptySubscription, ID},
subscriptions::{GraphQLSubscriptionType, SubscriptionConnection, SubscriptionCoordinator},
50 changes: 49 additions & 1 deletion juniper/src/types/base.rs
Original file line number Diff line number Diff line change
@@ -6,7 +6,7 @@ use crate::{
ast::{Directive, FromInputValue, InputValue, Selection},
executor::{ExecutionResult, Executor, Registry, Variables},
parser::Spanning,
schema::meta::{Argument, MetaType},
schema::meta::{Argument, Field, MetaType},
value::{DefaultScalarValue, Object, ScalarValue, Value},
};

@@ -341,6 +341,54 @@ where
}
}

/// `GraphQLTypeInfo` holds the meta information for the given type.
///
/// The macros remove duplicated definitions of fields and arguments, and add
/// type checks on all resolve functions automatically.
pub trait GraphQLTypeInfo<S = DefaultScalarValue>: Sized
where
S: ScalarValue,
{
/// The expected context type for this GraphQL type
///
/// The context is threaded through query execution to all affected nodes,
/// and can be used to hold common data, e.g. database connections or
/// request session information.
type Context;

/// Type that may carry additional schema information
///
/// This can be used to implement a schema that is partly dynamic,
/// meaning that it can use information that is not known at compile time,
/// for instance by reading it from a configuration file at start-up.
type TypeInfo;

/// The field definitions of fields for fields derived from the struct of
/// this GraphQL type.
fn fields<'r>(info: &Self::TypeInfo, registry: &mut Registry<'r, S>) -> Vec<Field<'r, S>>
where
S: 'r;

/// Resolve the value of a single field on this type.
///
/// The arguments object contain all specified arguments, with default
/// values substituted for the ones not provided by the query.
///
/// The executor can be used to drive selections into sub-objects.
///
/// The default implementation panics.
#[allow(unused_variables)]
fn resolve_field(
&self,
info: &Self::TypeInfo,
field_name: &str,
arguments: &Arguments<S>,
executor: &Executor<Self::Context, S>,
) -> ExecutionResult<S> {
panic!("resolve_field must be implemented by object types");
}
}

/// Resolver logic for queries'/mutations' selection set.
/// Calls appropriate resolver method for each field or fragment found
/// and then merges returned values into `result` or pushes errors to
1 change: 1 addition & 0 deletions juniper_codegen/src/derive_enum.rs
Original file line number Diff line number Diff line change
@@ -143,6 +143,7 @@ pub fn impl_enum(
generics: syn::Generics::default(),
interfaces: None,
include_type_generics: true,
include_struct_fields: false,
generic_scalar: true,
no_async: attrs.no_async.is_some(),
};
1 change: 1 addition & 0 deletions juniper_codegen/src/derive_input_object.rs
Original file line number Diff line number Diff line change
@@ -143,6 +143,7 @@ pub fn impl_input_object(
generics: ast.generics,
interfaces: None,
include_type_generics: true,
include_struct_fields: false,
generic_scalar: true,
no_async: attrs.no_async.is_some(),
};
188 changes: 121 additions & 67 deletions juniper_codegen/src/derive_object.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,68 @@
use crate::{
result::{GraphQLScope, UnsupportedAttribute},
util::{self, span_container::SpanContainer},
util::{self, duplicate::Duplicate, span_container::SpanContainer},
};
use proc_macro2::TokenStream;
use quote::quote;
use syn::{self, ext::IdentExt, spanned::Spanned, Data, Fields};

pub fn create_field_definition(
field: syn::Field,
error: &GraphQLScope,
) -> Option<util::GraphQLTypeDefinitionField> {
let span = field.span();
let field_attrs = match util::FieldAttributes::from_attrs(
&field.attrs,
util::FieldAttributeParseMode::Object,
) {
Ok(attrs) => attrs,
Err(e) => {
proc_macro_error::emit_error!(e);
return None;
}
};

if field_attrs.skip.is_some() {
return None;
}

let field_name = &field.ident.unwrap();
let name = field_attrs
.name
.clone()
.map(SpanContainer::into_inner)
.unwrap_or_else(|| util::to_camel_case(&field_name.unraw().to_string()));

if name.starts_with("__") {
error.no_double_underscore(if let Some(name) = field_attrs.name {
name.span_ident()
} else {
field_name.span()
});
}

if let Some(default) = field_attrs.default {
error.unsupported_attribute_within(default.span_ident(), UnsupportedAttribute::Default);
}

let resolver_code = quote!(
&self . #field_name
);

Some(util::GraphQLTypeDefinitionField {
name,
_type: field.ty,
args: Vec::new(),
description: field_attrs.description.map(SpanContainer::into_inner),
deprecation: field_attrs.deprecation.map(SpanContainer::into_inner),
resolver_code,
default: None,
is_type_inferred: true,
is_async: false,
span,
})
}

pub fn build_derive_object(
ast: syn::DeriveInput,
is_internal: bool,
@@ -32,64 +89,17 @@ pub fn build_derive_object(

let fields = struct_fields
.into_iter()
.filter_map(|field| {
let span = field.span();
let field_attrs = match util::FieldAttributes::from_attrs(
&field.attrs,
util::FieldAttributeParseMode::Object,
) {
Ok(attrs) => attrs,
Err(e) => {
proc_macro_error::emit_error!(e);
return None;
}
};

if field_attrs.skip.is_some() {
return None;
}

let field_name = &field.ident.unwrap();
let name = field_attrs
.name
.clone()
.map(SpanContainer::into_inner)
.unwrap_or_else(|| util::to_camel_case(&field_name.unraw().to_string()));

if name.starts_with("__") {
error.no_double_underscore(if let Some(name) = field_attrs.name {
name.span_ident()
} else {
field_name.span()
});
}

if let Some(default) = field_attrs.default {
error.unsupported_attribute_within(
default.span_ident(),
UnsupportedAttribute::Default,
);
}

let resolver_code = quote!(
&self . #field_name
);

Some(util::GraphQLTypeDefinitionField {
name,
_type: field.ty,
args: Vec::new(),
description: field_attrs.description.map(SpanContainer::into_inner),
deprecation: field_attrs.deprecation.map(SpanContainer::into_inner),
resolver_code,
default: None,
is_type_inferred: true,
is_async: false,
span,
})
})
.filter_map(|field| create_field_definition(field, &error))
.collect::<Vec<_>>();

if let Some(duplicates) = Duplicate::find_by_key(&fields, |field| field.name.as_str()) {
error.duplicate(duplicates.iter());
}

if fields.is_empty() {
error.not_empty(ast_span);
}

// Early abort after checking all fields
proc_macro_error::abort_if_dirty();

@@ -99,12 +109,6 @@ pub fn build_derive_object(
});
}

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()
@@ -113,10 +117,6 @@ pub fn build_derive_object(
});
}

if fields.is_empty() {
error.not_empty(ast_span);
}

// Early abort after GraphQL properties
proc_macro_error::abort_if_dirty();

@@ -130,10 +130,64 @@ pub fn build_derive_object(
generics: ast.generics,
interfaces: None,
include_type_generics: true,
include_struct_fields: false,
generic_scalar: true,
no_async: attrs.no_async.is_some(),
};

let juniper_crate_name = if is_internal { "crate" } else { "juniper" };
Ok(definition.into_tokens(juniper_crate_name))
}

pub fn build_derive_object_info(
ast: syn::DeriveInput,
is_internal: bool,
error: GraphQLScope,
) -> syn::Result<TokenStream> {
let ast_span = ast.span();
let struct_fields = match ast.data {
Data::Struct(data) => match data.fields {
Fields::Named(fields) => fields.named,
_ => return Err(error.custom_error(ast_span, "only named fields are allowed")),
},
_ => return Err(error.custom_error(ast_span, "can only be applied to structs")),
};

// Parse attributes.
let attrs = util::ObjectInfoAttributes::from_attrs(&ast.attrs)?;

let ident = &ast.ident;
let fields = struct_fields
.into_iter()
.filter_map(|field| create_field_definition(field, &error))
.collect::<Vec<_>>();

if let Some(duplicates) = Duplicate::find_by_key(&fields, |field| field.name.as_str()) {
error.duplicate(duplicates.iter());
}

if fields.is_empty() {
error.not_empty(ast_span);
}

// Early abort after checking all fields
proc_macro_error::abort_if_dirty();

let definition = util::GraphQLTypeDefiniton {
name: ident.unraw().to_string(),
_type: syn::parse_str(&ast.ident.to_string()).unwrap(),
context: attrs.context.map(SpanContainer::into_inner),
scalar: attrs.scalar.map(SpanContainer::into_inner),
description: None,
fields,
generics: ast.generics,
interfaces: None,
include_type_generics: true,
include_struct_fields: false,
generic_scalar: true,
no_async: false,
};

let juniper_crate_name = if is_internal { "crate" } else { "juniper" };
Ok(definition.into_info_tokens(juniper_crate_name))
}
1 change: 1 addition & 0 deletions juniper_codegen/src/derive_union.rs
Original file line number Diff line number Diff line change
@@ -173,6 +173,7 @@ pub fn build_derive_union(
generics: ast.generics,
interfaces: None,
include_type_generics: true,
include_struct_fields: false,
generic_scalar: true,
no_async: attrs.no_async.is_some(),
};
1 change: 1 addition & 0 deletions juniper_codegen/src/impl_object.rs
Original file line number Diff line number Diff line change
@@ -226,6 +226,7 @@ fn create(
None
},
include_type_generics: false,
include_struct_fields: _impl.attrs.derive_fields.is_some(),
generic_scalar: false,
no_async: _impl.attrs.no_async.is_some(),
};
24 changes: 23 additions & 1 deletion juniper_codegen/src/lib.rs
Original file line number Diff line number Diff line change
@@ -93,6 +93,28 @@ pub fn derive_object_internal(input: TokenStream) -> TokenStream {
}
}

#[proc_macro_error]
#[proc_macro_derive(GraphQLObjectInfo, attributes(graphql))]
pub fn derive_object_info(input: TokenStream) -> TokenStream {
let ast = syn::parse::<syn::DeriveInput>(input).unwrap();
let gen = derive_object::build_derive_object_info(ast, false, GraphQLScope::DeriveObjectInfo);
match gen {
Ok(gen) => gen.into(),
Err(err) => proc_macro_error::abort!(err),
}
}

#[proc_macro_error]
#[proc_macro_derive(GraphQLObjectInfoInternal, attributes(graphql))]
pub fn derive_object_info_internal(input: TokenStream) -> TokenStream {
let ast = syn::parse::<syn::DeriveInput>(input).unwrap();
let gen = derive_object::build_derive_object_info(ast, true, GraphQLScope::DeriveObjectInfo);
match gen {
Ok(gen) => gen.into(),
Err(err) => proc_macro_error::abort!(err),
}
}

#[proc_macro_error]
#[proc_macro_derive(GraphQLUnion, attributes(graphql))]
pub fn derive_union(input: TokenStream) -> TokenStream {
@@ -472,7 +494,7 @@ pub fn graphql_object_internal(args: TokenStream, input: TokenStream) -> TokenSt
/// struct UserID(String);
///
/// #[juniper::graphql_scalar(
/// // You can rename the type for GraphQL by specifying the name here.
/// // You can rename the type for GraphQL by specifying the name here.
/// name = "MyName",
/// // You can also specify a description here.
/// // If present, doc comments will be ignored.
9 changes: 7 additions & 2 deletions juniper_codegen/src/result.rs
Original file line number Diff line number Diff line change
@@ -10,6 +10,7 @@ pub const GRAPHQL_SPECIFICATION: &'static str = "https://spec.graphql.org/June20
#[allow(unused_variables)]
pub enum GraphQLScope {
DeriveObject,
DeriveObjectInfo,
DeriveInputObject,
DeriveUnion,
DeriveEnum,
@@ -22,7 +23,9 @@ pub enum GraphQLScope {
impl GraphQLScope {
pub fn specification_section(&self) -> &str {
match self {
GraphQLScope::DeriveObject | GraphQLScope::ImplObject => "#sec-Objects",
GraphQLScope::DeriveObject
| GraphQLScope::DeriveObjectInfo
| GraphQLScope::ImplObject => "#sec-Objects",
GraphQLScope::DeriveInputObject => "#sec-Input-Objects",
GraphQLScope::DeriveUnion | GraphQLScope::ImplUnion => "#sec-Unions",
GraphQLScope::DeriveEnum => "#sec-Enums",
@@ -34,7 +37,9 @@ impl GraphQLScope {
impl fmt::Display for GraphQLScope {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let name = match self {
GraphQLScope::DeriveObject | GraphQLScope::ImplObject => "object",
GraphQLScope::DeriveObject
| GraphQLScope::DeriveObjectInfo
| GraphQLScope::ImplObject => "object",
GraphQLScope::DeriveInputObject => "input object",
GraphQLScope::DeriveUnion | GraphQLScope::ImplUnion => "union",
GraphQLScope::DeriveEnum => "enum",
432 changes: 340 additions & 92 deletions juniper_codegen/src/util/mod.rs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions rustfmt.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
edition = "2018"
merge_imports = true
use_field_init_shorthand = true