diff --git a/integration_tests/juniper_tests/src/codegen/impl_object_with_derive_fields.rs b/integration_tests/juniper_tests/src/codegen/impl_object_with_derive_fields.rs new file mode 100644 index 000000000..ee1971a30 --- /dev/null +++ b/integration_tests/juniper_tests/src/codegen/impl_object_with_derive_fields.rs @@ -0,0 +1,418 @@ +#[cfg(test)] +use fnv::FnvHashMap; + +use juniper::{DefaultScalarValue, GraphQLObjectInfo}; + +#[cfg(test)] +use juniper::{ + self, execute, EmptyMutation, EmptySubscription, GraphQLType, Registry, RootNode, Value, + Variables, +}; + +#[derive(Default)] +struct Context { + value: String, +} + +impl juniper::Context for Context {} + +#[derive(GraphQLObjectInfo)] +#[graphql(scalar = DefaultScalarValue)] +struct Obj { + regular_field: bool, + + #[graphql(name = "renamedField")] + renamed_field_orig: i32, + + #[graphql(skip)] + skipped_field: i32, +} + +#[juniper::graphql_object(name = "MyObj", description = "obj descr", derive_fields)] +impl Obj { + fn resolve_field() -> &str { + "obj::resolve_field" + } +} + +#[derive(GraphQLObjectInfo)] +#[graphql(scalar = DefaultScalarValue)] +struct Nested { + obj: Obj, + nested_field: bool, +} + +#[juniper::graphql_object(derive_fields)] +impl Nested { + fn nested_resolve_field() -> &str { + "nested::resolve_field" + } +} + +#[derive(GraphQLObjectInfo)] +#[graphql(Context = Context, scalar = DefaultScalarValue)] +struct WithContext { + value: bool, +} + +#[juniper::graphql_object(Context = Context, derive_fields)] +impl WithContext { + fn resolve_field(ctx: &Context) -> &str { + ctx.value.as_str() + } +} + +// FIXME: Field with lifetime doesn't even work for derive GraphQLObject +// due to 'cannot infer an appropriate lifetime'. + +// #[derive(GraphQLObjectInfo)] +// #[graphql(Context = Context, scalar = DefaultScalarValue)] +// struct WithLifetime<'a> { +// value: &'a str, +// } + +// #[juniper::graphql_object(Context = Context)] +// impl<'a> WithLifetime<'a> { +// fn custom_field() -> bool { +// true +// } +// } + +struct Query; + +#[juniper::graphql_object(Context = Context, scalar = DefaultScalarValue)] +impl Query { + fn obj() -> Obj { + Obj { + regular_field: true, + renamed_field_orig: 22, + skipped_field: 33, + } + } + + fn nested(&self) -> Nested { + Nested { + obj: Obj { + regular_field: false, + renamed_field_orig: 222, + skipped_field: 333, + }, + nested_field: true, + } + } + + fn with_context(&self) -> WithContext { + WithContext { value: true } + } + + // fn with_lifetime(&self) -> WithLifetime<'a> { + // WithLifetime { value: "blub" } + // } +} + +#[tokio::test] +async fn test_derived_object_fields() { + assert_eq!( + >::name(&()), + Some("MyObj") + ); + + // Verify meta info. + let mut registry = Registry::new(FnvHashMap::default()); + let meta = Obj::meta(&(), &mut registry); + + assert_eq!(meta.name(), Some("MyObj")); + assert_eq!(meta.description(), Some(&"obj descr".to_string())); + + let doc = r#" + { + obj { + regularField + renamedField + resolveField + } + }"#; + + let schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::new(), + ); + + let context = Context { + value: String::from("context value"), + }; + + assert_eq!( + execute(doc, None, &schema, &Variables::new(), &context).await, + Ok(( + Value::object( + vec![( + "obj", + Value::object( + vec![ + ("regularField", Value::scalar(true)), + ("renamedField", Value::scalar(22)), + ("resolveField", Value::scalar("obj::resolve_field")), + ] + .into_iter() + .collect(), + ), + )] + .into_iter() + .collect() + ), + vec![] + )) + ); +} + +#[tokio::test] +#[should_panic] +async fn test_cannot_query_skipped_field() { + let doc = r#" + { + obj { + skippedField + } + }"#; + let schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::new(), + ); + let context = Context { + value: String::from("context value"), + }; + execute(doc, None, &schema, &Variables::new(), &context) + .await + .unwrap(); +} + +#[tokio::test] +async fn test_derived_object_fields_nested() { + let doc = r#" + { + nested { + obj { + regularField + renamedField + resolveField + } + nestedField + nestedResolveField + } + }"#; + + let schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::new(), + ); + + let context = Context { + value: String::from("context value"), + }; + + assert_eq!( + execute(doc, None, &schema, &Variables::new(), &context).await, + Ok(( + Value::object( + vec![( + "nested", + Value::object( + vec![ + ( + "obj", + Value::object( + vec![ + ("regularField", Value::scalar(false)), + ("renamedField", Value::scalar(222)), + ("resolveField", Value::scalar("obj::resolve_field")), + ] + .into_iter() + .collect() + ), + ), + ("nestedField", Value::scalar(true)), + ("nestedResolveField", Value::scalar("nested::resolve_field")) + ] + .into_iter() + .collect() + ), + )] + .into_iter() + .collect() + ), + vec![] + )) + ); +} + +#[tokio::test] +async fn test_field_resolver_with_context() { + let doc = r#" + { + withContext { + value + resolveField + } + }"#; + + let schema = RootNode::new( + Query, + EmptyMutation::::new(), + EmptySubscription::::new(), + ); + + let context = Context { + value: String::from("context value"), + }; + + assert_eq!( + execute(doc, None, &schema, &Variables::new(), &context).await, + Ok(( + Value::object( + vec![( + "withContext", + Value::object( + vec![ + ("value", Value::scalar(true)), + ("resolveField", Value::scalar("context value")), + ] + .into_iter() + .collect(), + ), + )] + .into_iter() + .collect() + ), + vec![] + )) + ); +} + +#[tokio::test] +#[should_panic] +async fn test_duplicate_object_field() { + #[derive(GraphQLObjectInfo)] + #[graphql(scalar = DefaultScalarValue)] + struct TestObject { + value: bool, + } + + #[juniper::graphql_object(derive_fields)] + impl TestObject { + fn value() -> bool { + true + } + } + + struct TestQuery; + + #[juniper::graphql_object(scalar = DefaultScalarValue)] + impl TestQuery { + fn test(&self) -> TestObject { + TestObject { value: false } + } + } + + let doc = r#" + { + test { + value + } + }"#; + let schema = RootNode::new( + TestQuery, + EmptyMutation::<()>::new(), + EmptySubscription::<()>::new(), + ); + execute(doc, None, &schema, &Variables::new(), &()) + .await + .unwrap(); +} + +#[tokio::test] +#[should_panic] +async fn test_duplicate_object_field_with_custom_name() { + #[derive(GraphQLObjectInfo)] + #[graphql(scalar = DefaultScalarValue)] + struct TestObject { + #[graphql(name = "renamed")] + value: bool, + } + + #[juniper::graphql_object(derive_fields)] + impl TestObject { + fn renamed() -> bool { + true + } + } + + struct TestQuery; + + #[juniper::graphql_object(scalar = DefaultScalarValue)] + impl TestQuery { + fn test(&self) -> TestObject { + TestObject { value: false } + } + } + + let doc = r#" + { + test { + renamed + } + }"#; + let schema = RootNode::new( + TestQuery, + EmptyMutation::<()>::new(), + EmptySubscription::<()>::new(), + ); + execute(doc, None, &schema, &Variables::new(), &()) + .await + .unwrap(); +} + +#[tokio::test] +#[should_panic] +async fn test_duplicate_field_resolver_with_custom_name() { + #[derive(GraphQLObjectInfo)] + #[graphql(scalar = DefaultScalarValue)] + struct TestObject { + value: bool, + } + + #[juniper::graphql_object(derive_fields)] + impl TestObject { + #[graphql(name = "value")] + fn renamed() -> bool { + true + } + } + + struct TestQuery; + + #[juniper::graphql_object(scalar = DefaultScalarValue)] + impl TestQuery { + fn test(&self) -> TestObject { + TestObject { value: false } + } + } + + let doc = r#" + { + test { + value + } + }"#; + let schema = RootNode::new( + TestQuery, + EmptyMutation::<()>::new(), + EmptySubscription::<()>::new(), + ); + execute(doc, None, &schema, &Variables::new(), &()) + .await + .unwrap(); +} diff --git a/integration_tests/juniper_tests/src/codegen/mod.rs b/integration_tests/juniper_tests/src/codegen/mod.rs index f2bd68dc0..843510ba9 100644 --- a/integration_tests/juniper_tests/src/codegen/mod.rs +++ b/integration_tests/juniper_tests/src/codegen/mod.rs @@ -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; diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index 794042581..6597460b2 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -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` diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs index 32e26f3bc..472c1fd84 100644 --- a/juniper/src/lib.rs +++ b/juniper/src/lib.rs @@ -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}, diff --git a/juniper/src/types/base.rs b/juniper/src/types/base.rs index 1ce3754f4..cd3f39f9c 100644 --- a/juniper/src/types/base.rs +++ b/juniper/src/types/base.rs @@ -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: 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> + 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, + executor: &Executor, + ) -> ExecutionResult { + 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 diff --git a/juniper_codegen/src/derive_enum.rs b/juniper_codegen/src/derive_enum.rs index 9dc9b7dce..7198836e2 100644 --- a/juniper_codegen/src/derive_enum.rs +++ b/juniper_codegen/src/derive_enum.rs @@ -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(), }; diff --git a/juniper_codegen/src/derive_input_object.rs b/juniper_codegen/src/derive_input_object.rs index 843bfb2da..c12296b31 100644 --- a/juniper_codegen/src/derive_input_object.rs +++ b/juniper_codegen/src/derive_input_object.rs @@ -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(), }; diff --git a/juniper_codegen/src/derive_object.rs b/juniper_codegen/src/derive_object.rs index 04dc4556c..8ffa99e0e 100644 --- a/juniper_codegen/src/derive_object.rs +++ b/juniper_codegen/src/derive_object.rs @@ -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 { + 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::>(); + 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,6 +130,7 @@ 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(), }; @@ -137,3 +138,56 @@ pub fn build_derive_object( 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 { + 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::>(); + + 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)) +} diff --git a/juniper_codegen/src/derive_union.rs b/juniper_codegen/src/derive_union.rs index 0b94304e2..f75a5c856 100644 --- a/juniper_codegen/src/derive_union.rs +++ b/juniper_codegen/src/derive_union.rs @@ -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(), }; diff --git a/juniper_codegen/src/impl_object.rs b/juniper_codegen/src/impl_object.rs index 4cee482f0..2dfa9928e 100644 --- a/juniper_codegen/src/impl_object.rs +++ b/juniper_codegen/src/impl_object.rs @@ -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(), }; diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs index 55ffc1080..8864b0800 100644 --- a/juniper_codegen/src/lib.rs +++ b/juniper_codegen/src/lib.rs @@ -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::(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::(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. diff --git a/juniper_codegen/src/result.rs b/juniper_codegen/src/result.rs index dea2a5532..232d2e917 100644 --- a/juniper_codegen/src/result.rs +++ b/juniper_codegen/src/result.rs @@ -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", diff --git a/juniper_codegen/src/util/mod.rs b/juniper_codegen/src/util/mod.rs index 60ca23dee..bce047a64 100644 --- a/juniper_codegen/src/util/mod.rs +++ b/juniper_codegen/src/util/mod.rs @@ -279,6 +279,7 @@ pub struct ObjectAttributes { pub scalar: Option>, pub interfaces: Vec>, pub no_async: Option>, + pub derive_fields: Option>, } impl syn::parse::Parse for ObjectAttributes { @@ -290,6 +291,7 @@ impl syn::parse::Parse for ObjectAttributes { scalar: None, interfaces: Vec::new(), no_async: None, + derive_fields: None, }; while !input.is_empty() { @@ -348,6 +350,9 @@ impl syn::parse::Parse for ObjectAttributes { "noasync" => { output.no_async = Some(SpanContainer::new(ident.span(), None, ())); } + "derive_fields" => { + output.derive_fields = Some(SpanContainer::new(ident.span(), None, ())); + } _ => { return Err(syn::Error::new(ident.span(), "unknown attribute")); } @@ -381,6 +386,68 @@ impl ObjectAttributes { } } +#[derive(Default, Debug)] +pub struct ObjectInfoAttributes { + pub context: Option>, + pub scalar: Option>, +} + +impl syn::parse::Parse for ObjectInfoAttributes { + fn parse(input: syn::parse::ParseStream) -> syn::parse::Result { + let mut output = Self { + context: None, + scalar: None, + }; + + while !input.is_empty() { + let ident: syn::Ident = input.parse()?; + match ident.to_string().as_str() { + "context" | "Context" => { + input.parse::()?; + // TODO: remove legacy support for string based Context. + let ctx = if let Ok(val) = input.parse::() { + eprintln!("DEPRECATION WARNING: using a string literal for the Context is deprecated"); + eprintln!("Use a normal type instead - example: 'Context = MyContextType'"); + syn::parse_str::(&val.value())? + } else { + input.parse::()? + }; + output.context = Some(SpanContainer::new(ident.span(), Some(ctx.span()), ctx)); + } + "scalar" | "Scalar" => { + input.parse::()?; + let val = input.parse::()?; + output.scalar = Some(SpanContainer::new(ident.span(), Some(val.span()), val)); + } + _ => { + return Err(syn::Error::new(ident.span(), "unknown attribute")); + } + } + if input.lookahead1().peek(syn::Token![,]) { + input.parse::()?; + } + } + + Ok(output) + } +} + +impl ObjectInfoAttributes { + pub fn from_attrs(attrs: &[syn::Attribute]) -> syn::parse::Result { + 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 a: Self = attr.parse_args()?; + Ok(a) + } else { + let a = Self::default(); + Ok(a) + } + } +} + #[derive(Debug)] pub struct FieldAttributeArgument { pub name: syn::Ident, @@ -662,6 +729,9 @@ pub struct GraphQLTypeDefiniton { // This flag signifies if the type generics need to be // included manually. pub include_type_generics: bool, + // This flag indicates if the field resolvers derived from the type + // struct should be included. + pub include_struct_fields: 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 @@ -677,87 +747,72 @@ impl GraphQLTypeDefiniton { self.fields.iter().any(|field| field.is_async) } - pub fn into_tokens(self, juniper_crate_name: &str) -> TokenStream { - let juniper_crate_name = syn::parse_str::(juniper_crate_name).unwrap(); + fn field_definition_tokens(&self) -> Vec { + self.fields + .iter() + .map(|field| { + let args = field.args.iter().map(|arg| { + let arg_type = &arg._type; + let arg_name = &arg.name; - let name = &self.name; - let ty = &self._type; - let context = self - .context - .as_ref() - .map(|ctx| quote!( #ctx )) - .unwrap_or_else(|| quote!(())); + let description = match arg.description.as_ref() { + Some(value) => quote!( .description( #value ) ), + None => 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; + // 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 + ) + ), + } + }); - let description = match arg.description.as_ref() { - Some(value) => quote!( .description( #value ) ), + let description = match field.description.as_ref() { + Some(description) => quote!( .description(#description) ), None => quote!(), }; - // 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 - ) - ), - } - }); - - 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) ) + 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; + None => quote!(), + }; - let _type = &field._type; - quote! { - registry - .field_convert::<#_type, _, Self::Context>(#field_name, info) - #(#args)* - #description - #deprecation - } - }); + let field_name = &field.name; - 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 _type = &field._type; + quote! { + registry + .field_convert::<#_type, _, Self::Context>(#field_name, info) + #(#args)* + #description + #deprecation } - }); + }) + .collect() + } - let resolve_matches = self.fields.iter().map(|field| { + fn resolve_matches_tokens( + &self, + scalar: &TokenStream, + juniper_crate_name: &syn::Path, + ) -> Vec { + self.fields.iter().map(|field| { let name = &field.name; let code = &field.resolver_code; @@ -793,7 +848,100 @@ impl GraphQLTypeDefiniton { }, ) } - }); + }).collect() + } + + pub fn into_tokens(self, juniper_crate_name: &str) -> TokenStream { + let juniper_crate_name = syn::parse_str::(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 field_definitions = if self.include_struct_fields { + let impl_fields = self.field_definition_tokens(); + let impl_fields = quote!(vec![ #( #impl_fields ),* ]); + let struct_fields = quote!( + >::fields(info, registry) + ); + quote!({ + let fields = vec![ #impl_fields, #struct_fields ].concat(); + + let mut mapping: std::collections::HashMap<&str, Vec<&_>> = + std::collections::HashMap::with_capacity(fields.len()); + + for item in &fields { + let name = item.name.as_str(); + if let Some(vals) = mapping.get_mut(name) { + vals.push(item); + } else { + mapping.insert(name, vec![item]); + } + } + + let duplicates = mapping + .into_iter() + .filter_map(|(name, spanned)| { + if spanned.len() > 1 { + Some(name.to_string()) + } else { + None + } + }) + .collect::>(); + + if !duplicates.is_empty() { + duplicates.iter().for_each(|field| { + panic!("Field with the same name `{}` defined in both struct and impl on type {:?}", + field, + >::name(info) + ); + }) + } + fields + }) + } else { + let impl_fields = self.field_definition_tokens(); + quote!(vec![ #( #impl_fields ),* ]) + }; + + let resolve_matches = if self.include_struct_fields { + let impl_matches = self.resolve_matches_tokens(&scalar, &juniper_crate_name); + quote!(match field { + #( #impl_matches )* + _ => > + ::resolve_field(self, _info, field, args, executor) + }) + } else { + let impl_matches = self.resolve_matches_tokens(&scalar, &juniper_crate_name); + quote!(match field { + #( #impl_matches )* + _ => { + panic!("Field {} not found on type {:?}", + field, + >::name(_info) + ); + } + }) + }; let description = self .description @@ -912,6 +1060,27 @@ impl GraphQLTypeDefiniton { } }); + let resolve_matches_async = if self.include_struct_fields { + quote!(match field { + #( #resolve_matches_async )* + _ => { + let v = > + ::resolve_field(self, info, field, args, executor); + future::FutureExt::boxed(future::ready(v)) + } + }) + } else { + quote!(match field { + #( #resolve_matches_async )* + _ => { + panic!("Field {} not found on type {:?}", + field, + >::name(info) + ); + } + }) + }; + let mut where_async = where_clause.cloned().unwrap_or_else(|| parse_quote!(where)); where_async @@ -936,15 +1105,7 @@ impl GraphQLTypeDefiniton { { use futures::future; use #juniper_crate_name::GraphQLType; - match field { - #( #resolve_matches_async )* - _ => { - panic!("Field {} not found on type {:?}", - field, - >::name(info) - ); - } - } + #resolve_matches_async } } ) @@ -992,9 +1153,7 @@ impl GraphQLTypeDefiniton { ) -> #juniper_crate_name::meta::MetaType<'r, #scalar> where #scalar : 'r, { - let fields = vec![ - #( #field_definitions ),* - ]; + let fields = #field_definitions; let meta = registry.build_object_type::<#ty>( info, &fields ) #description #interfaces; @@ -1010,15 +1169,7 @@ impl GraphQLTypeDefiniton { args: &#juniper_crate_name::Arguments<#scalar>, executor: &#juniper_crate_name::Executor, ) -> #juniper_crate_name::ExecutionResult<#scalar> { - match field { - #( #resolve_matches )* - _ => { - panic!("Field {} not found on type {:?}", - field, - >::name(_info) - ); - } - } + #resolve_matches } @@ -1033,6 +1184,103 @@ impl GraphQLTypeDefiniton { output } + pub fn into_info_tokens(self, juniper_crate_name: &str) -> TokenStream { + let juniper_crate_name = syn::parse_str::(juniper_crate_name).unwrap(); + + 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 field_definitions = self.field_definition_tokens(); + let resolve_matches = self.resolve_matches_tokens(&scalar, &juniper_crate_name); + + // 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 output = quote!( + impl#impl_generics #juniper_crate_name::GraphQLTypeInfo<#scalar> for #ty #type_generics_tokens + #where_clause + { + type Context = #context; + type TypeInfo = (); + + fn fields<'r>( + info: &Self::TypeInfo, + registry: &mut #juniper_crate_name::Registry<'r, #scalar> + ) -> Vec<#juniper_crate_name::meta::Field<'r, #scalar>> + where #scalar : 'r, + { + vec![ + #( #field_definitions ),* + ] + } + + #[allow(unused_variables)] + #[allow(unused_mut)] + fn resolve_field( + self: &Self, + _info: &(), + field: &str, + args: &#juniper_crate_name::Arguments<#scalar>, + executor: &#juniper_crate_name::Executor, + ) -> #juniper_crate_name::ExecutionResult<#scalar> { + match field { + #( #resolve_matches )* + _ => { + panic!("Field {} not found on type {:?}", + field, + >::name(_info) + ); + } + } + } + } + ); + + output + } + pub fn into_subscription_tokens(self, juniper_crate_name: &str) -> TokenStream { let juniper_crate_name = syn::parse_str::(juniper_crate_name).unwrap(); diff --git a/rustfmt.toml b/rustfmt.toml index 8586c40cb..df8e2451c 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,2 +1,3 @@ +edition = "2018" merge_imports = true use_field_init_shorthand = true