From 0a6a9439928a82fe46472c487b27857693b53ff7 Mon Sep 17 00:00:00 2001 From: tyranron Date: Fri, 15 May 2020 18:31:01 +0300 Subject: [PATCH 01/29] Refactor ground for union macros reimplementation --- juniper_codegen/Cargo.toml | 16 ++--- juniper_codegen/src/derive_union.rs | 45 ++++++++------ juniper_codegen/src/impl_union.rs | 94 ++++++++++++++--------------- juniper_codegen/src/lib.rs | 49 +++++++-------- juniper_codegen/src/result.rs | 17 +++--- juniper_codegen/src/util/mod.rs | 3 + juniper_codegen/src/util/mode.rs | 20 ++++++ 7 files changed, 137 insertions(+), 107 deletions(-) create mode 100644 juniper_codegen/src/util/mode.rs diff --git a/juniper_codegen/Cargo.toml b/juniper_codegen/Cargo.toml index be8639cd1..603f77a74 100644 --- a/juniper_codegen/Cargo.toml +++ b/juniper_codegen/Cargo.toml @@ -1,6 +1,7 @@ [package] name = "juniper_codegen" version = "0.14.2" +edition = "2018" authors = [ "Magnus Hallin ", "Christoph Herzog ", @@ -9,20 +10,19 @@ description = "Internal custom derive trait for Juniper GraphQL" license = "BSD-2-Clause" documentation = "https://docs.rs/juniper" repository = "https://github.com/graphql-rust/juniper" -edition = "2018" + +[badges] +travis-ci = { repository = "graphql-rust/juniper" } [lib] proc-macro = true [dependencies] +proc-macro-error = "1.0.2" proc-macro2 = "1.0.1" -syn = { version = "1.0.3", features = ["full", "extra-traits", "parsing"] } quote = "1.0.3" -futures = "0.3.1" -proc-macro-error = "1.0.2" +syn = { version = "1.0.3", features = ["full", "extra-traits", "parsing"] } [dev-dependencies] -juniper = { version = "0.14.2", path = "../juniper"} - -[badges] -travis-ci = { repository = "graphql-rust/juniper" } +futures = "0.3.1" +juniper = { version = "0.14.2", path = "../juniper" } diff --git a/juniper_codegen/src/derive_union.rs b/juniper_codegen/src/derive_union.rs index 0b94304e2..8854c3bb3 100644 --- a/juniper_codegen/src/derive_union.rs +++ b/juniper_codegen/src/derive_union.rs @@ -1,20 +1,25 @@ -use crate::{ - result::{GraphQLScope, UnsupportedAttribute}, - util::{self, span_container::SpanContainer}, -}; use proc_macro2::TokenStream; use quote::quote; use syn::{self, ext::IdentExt, spanned::Spanned, Data, Fields}; -pub fn build_derive_union( - ast: syn::DeriveInput, - is_internal: bool, - error: GraphQLScope, -) -> syn::Result { +use crate::{ + result::{GraphQLScope, UnsupportedAttribute}, + util::{self, span_container::SpanContainer, Mode}, +}; + +const SCOPE: GraphQLScope = GraphQLScope::DeriveUnion; + +pub fn expand(input: TokenStream, mode: Mode) -> syn::Result { + let is_internal = matches!(mode, Mode::Internal); + + let ast = + syn::parse2::(input).unwrap_or_else(|e| proc_macro_error::abort!(e)); + let ast_span = ast.span(); let enum_fields = match ast.data { Data::Enum(data) => data.variants, - _ => return Err(error.custom_error(ast_span, "can only be applied to enums")), + Data::Struct(_) => unimplemented!(), + _ => return Err(SCOPE.custom_error(ast_span, "can only be applied to enums and structs")), }; // Parse attributes. @@ -43,7 +48,7 @@ pub fn build_derive_union( }; if let Some(ident) = field_attrs.skip { - error.unsupported_attribute_within(ident.span(), UnsupportedAttribute::Skip); + SCOPE.unsupported_attribute_within(ident.span(), UnsupportedAttribute::Skip); return None; } @@ -67,7 +72,7 @@ pub fn build_derive_union( }; if iter.next().is_some() { - error.custom( + SCOPE.custom( inner.span(), "all members must be unnamed with a single element e.g. Some(T)", ); @@ -76,7 +81,7 @@ pub fn build_derive_union( first.ty.clone() } _ => { - error.custom( + SCOPE.custom( variant_name.span(), "only unnamed fields with a single element are allowed, e.g., Some(T)", ); @@ -86,21 +91,21 @@ pub fn build_derive_union( }; if let Some(description) = field_attrs.description { - error.unsupported_attribute_within( + SCOPE.unsupported_attribute_within( description.span_ident(), UnsupportedAttribute::Description, ); } if let Some(default) = field_attrs.default { - error.unsupported_attribute_within( + SCOPE.unsupported_attribute_within( default.span_ident(), UnsupportedAttribute::Default, ); } if name.starts_with("__") { - error.no_double_underscore(if let Some(name) = field_attrs.name { + SCOPE.no_double_underscore(if let Some(name) = field_attrs.name { name.span_ident() } else { variant_name.span() @@ -127,16 +132,16 @@ pub fn build_derive_union( if !attrs.interfaces.is_empty() { attrs.interfaces.iter().for_each(|elm| { - error.unsupported_attribute(elm.span(), UnsupportedAttribute::Interface) + SCOPE.unsupported_attribute(elm.span(), UnsupportedAttribute::Interface) }); } if fields.is_empty() { - error.not_empty(ast_span); + SCOPE.not_empty(ast_span); } if name.starts_with("__") && !is_internal { - error.no_double_underscore(if let Some(name) = attrs.name { + SCOPE.no_double_underscore(if let Some(name) = attrs.name { name.span_ident() } else { ident.span() @@ -157,7 +162,7 @@ pub fn build_derive_union( }; if !all_variants_different { - error.custom(ident.span(), "each variant must have a different type"); + SCOPE.custom(ident.span(), "each variant must have a different type"); } // Early abort after GraphQL properties diff --git a/juniper_codegen/src/impl_union.rs b/juniper_codegen/src/impl_union.rs index fa9aea4b0..539c58701 100644 --- a/juniper_codegen/src/impl_union.rs +++ b/juniper_codegen/src/impl_union.rs @@ -1,54 +1,17 @@ -use crate::{ - result::GraphQLScope, - util::{self, span_container::SpanContainer}, -}; use proc_macro2::TokenStream; use quote::quote; use syn::{ext::IdentExt, spanned::Spanned}; -struct ResolverVariant { - pub ty: syn::Type, - pub resolver: syn::Expr, -} - -struct ResolveBody { - pub variants: Vec, -} - -impl syn::parse::Parse for ResolveBody { - fn parse(input: syn::parse::ParseStream) -> Result { - input.parse::()?; - input.parse::()?; - - let match_body; - syn::braced!( match_body in input ); - - let mut variants = Vec::new(); - while !match_body.is_empty() { - let ty = match_body.parse::()?; - match_body.parse::()?; - let resolver = match_body.parse::()?; +use crate::{ + result::GraphQLScope, + util::{self, span_container::SpanContainer, Mode}, +}; - variants.push(ResolverVariant { ty, resolver }); +const SCOPE: GraphQLScope = GraphQLScope::ImplUnion; - // Optinal trailing comma. - match_body.parse::().ok(); - } +pub fn expand(attrs: TokenStream, body: TokenStream, mode: Mode) -> syn::Result { + let is_internal = matches!(mode, Mode::Internal); - if !input.is_empty() { - return Err(input.error("unexpected input")); - } - - Ok(Self { variants }) - } -} - -pub fn impl_union( - is_internal: bool, - attrs: TokenStream, - body: TokenStream, - error: GraphQLScope, -) -> syn::Result { let body_span = body.span(); let _impl = util::parse_impl::ImplBlock::parse(attrs, body)?; @@ -56,7 +19,7 @@ pub fn impl_union( // Validate trait target name, if present. if let Some((name, path)) = &_impl.target_trait { if !(name == "GraphQLUnion" || name == "juniper.GraphQLUnion") { - return Err(error.custom_error( + return Err(SCOPE.custom_error( path.span(), "Invalid impl target trait: expected 'GraphQLUnion'", )); @@ -89,7 +52,7 @@ pub fn impl_union( let method = match method { Some(method) => method, None => { - return Err(error.custom_error( + return Err(SCOPE.custom_error( body_span, "expected exactly one method with signature: fn resolve(&self) { ... }", )) @@ -103,7 +66,7 @@ pub fn impl_union( let body = syn::parse::(body_raw.into())?; if body.variants.is_empty() { - error.not_empty(method.span()) + SCOPE.not_empty(method.span()) } proc_macro_error::abort_if_dirty(); @@ -221,3 +184,40 @@ pub fn impl_union( Ok(output.into()) } + +struct ResolverVariant { + pub ty: syn::Type, + pub resolver: syn::Expr, +} + +struct ResolveBody { + pub variants: Vec, +} + +impl syn::parse::Parse for ResolveBody { + fn parse(input: syn::parse::ParseStream) -> Result { + input.parse::()?; + input.parse::()?; + + let match_body; + syn::braced!( match_body in input ); + + let mut variants = Vec::new(); + while !match_body.is_empty() { + let ty = match_body.parse::()?; + match_body.parse::()?; + let resolver = match_body.parse::()?; + + variants.push(ResolverVariant { ty, resolver }); + + // Optinal trailing comma. + match_body.parse::().ok(); + } + + if !input.is_empty() { + return Err(input.error("unexpected input")); + } + + Ok(Self { variants }) + } +} diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs index 55ffc1080..0baf8b744 100644 --- a/juniper_codegen/src/lib.rs +++ b/juniper_codegen/src/lib.rs @@ -25,6 +25,8 @@ use proc_macro::TokenStream; use proc_macro_error::proc_macro_error; use result::GraphQLScope; +use self::util::Mode; + #[proc_macro_error] #[proc_macro_derive(GraphQLEnum, attributes(graphql))] pub fn derive_enum(input: TokenStream) -> TokenStream { @@ -93,16 +95,6 @@ pub fn derive_object_internal(input: TokenStream) -> TokenStream { } } -#[proc_macro_error] -#[proc_macro_derive(GraphQLUnion, attributes(graphql))] -pub fn derive_union(input: TokenStream) -> TokenStream { - let ast = syn::parse::(input).unwrap(); - let gen = derive_union::build_derive_union(ast, false, GraphQLScope::DeriveUnion); - match gen { - Ok(gen) => gen.into(), - Err(err) => proc_macro_error::abort!(err), - } -} /// This custom derive macro implements the #[derive(GraphQLScalarValue)] /// derive. /// @@ -554,27 +546,36 @@ pub fn graphql_subscription_internal(args: TokenStream, input: TokenStream) -> T )) } +#[proc_macro_error] +#[proc_macro_derive(GraphQLUnion, attributes(graphql))] +pub fn derive_union(input: TokenStream) -> TokenStream { + derive_union::expand(input.into(), Mode::Public) + .unwrap_or_else(|e| proc_macro_error::abort!(e)) + .into() +} + +#[proc_macro_error] +#[proc_macro_derive(GraphQLUnionInternal, attributes(graphql))] +#[doc(hidden)] +pub fn derive_union_internal(input: TokenStream) -> TokenStream { + derive_union::expand(input.into(), Mode::Internal) + .unwrap_or_else(|e| proc_macro_error::abort!(e)) + .into() +} + #[proc_macro_error] #[proc_macro_attribute] pub fn graphql_union(attrs: TokenStream, body: TokenStream) -> TokenStream { - let attrs = proc_macro2::TokenStream::from(attrs); - let body = proc_macro2::TokenStream::from(body); - let gen = impl_union::impl_union(false, attrs, body, GraphQLScope::ImplUnion); - match gen { - Ok(gen) => gen.into(), - Err(err) => proc_macro_error::abort!(err), - } + impl_union::expand(attrs.into(), body.into(), Mode::Public) + .unwrap_or_else(|e| proc_macro_error::abort!(e)) + .into() } #[proc_macro_error] #[proc_macro_attribute] #[doc(hidden)] pub fn graphql_union_internal(attrs: TokenStream, body: TokenStream) -> TokenStream { - let attrs = proc_macro2::TokenStream::from(attrs); - let body = proc_macro2::TokenStream::from(body); - let gen = impl_union::impl_union(true, attrs, body, GraphQLScope::ImplUnion); - match gen { - Ok(gen) => gen.into(), - Err(err) => proc_macro_error::abort!(err), - } + impl_union::expand(attrs.into(), body.into(), Mode::Internal) + .unwrap_or_else(|e| proc_macro_error::abort!(e)) + .into() } diff --git a/juniper_codegen/src/result.rs b/juniper_codegen/src/result.rs index dea2a5532..537d81b1c 100644 --- a/juniper_codegen/src/result.rs +++ b/juniper_codegen/src/result.rs @@ -5,7 +5,8 @@ use proc_macro2::Span; use proc_macro_error::{Diagnostic, Level}; use std::fmt; -pub const GRAPHQL_SPECIFICATION: &'static str = "https://spec.graphql.org/June2018/"; +/// URL of the GraphQL specification (June 2018 Edition). +pub const SPEC_URL: &'static str = "https://spec.graphql.org/June2018/"; #[allow(unused_variables)] pub enum GraphQLScope { @@ -20,7 +21,7 @@ pub enum GraphQLScope { } impl GraphQLScope { - pub fn specification_section(&self) -> &str { + pub fn spec_section(&self) -> &str { match self { GraphQLScope::DeriveObject | GraphQLScope::ImplObject => "#sec-Objects", GraphQLScope::DeriveInputObject => "#sec-Input-Objects", @@ -57,13 +58,13 @@ pub enum UnsupportedAttribute { } impl GraphQLScope { - fn specification_link(&self) -> String { - format!("{}{}", GRAPHQL_SPECIFICATION, self.specification_section()) + fn spec_link(&self) -> String { + format!("{}{}", SPEC_URL, self.spec_section()) } pub fn custom>(&self, span: Span, msg: S) { Diagnostic::spanned(span, Level::Error, format!("{} {}", self, msg.as_ref())) - .note(self.specification_link()) + .note(self.spec_link()) .emit(); } @@ -97,7 +98,7 @@ impl GraphQLScope { Level::Error, format!("{} expects at least one field", self), ) - .note(self.specification_link()) + .note(self.spec_link()) .emit(); } @@ -120,7 +121,7 @@ impl GraphQLScope { ), ) .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()) + .note(self.spec_link()) .emit(); }); }) @@ -132,7 +133,7 @@ impl GraphQLScope { 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 GraphQL’s introspection system.".to_string(), ) - .note(format!("{}#sec-Schema", GRAPHQL_SPECIFICATION)) + .note(format!("{}#sec-Schema", SPEC_URL)) .emit(); } } diff --git a/juniper_codegen/src/util/mod.rs b/juniper_codegen/src/util/mod.rs index 60ca23dee..9e8375c81 100644 --- a/juniper_codegen/src/util/mod.rs +++ b/juniper_codegen/src/util/mod.rs @@ -1,6 +1,7 @@ #![allow(clippy::single_match)] pub mod duplicate; +pub mod mode; pub mod parse_impl; pub mod span_container; @@ -14,6 +15,8 @@ use syn::{ MetaNameValue, NestedMeta, Token, }; +pub use mode::Mode; + pub fn juniper_path(is_internal: bool) -> syn::Path { let name = if is_internal { "crate" } else { "juniper" }; syn::parse_str::(name).unwrap() diff --git a/juniper_codegen/src/util/mode.rs b/juniper_codegen/src/util/mode.rs new file mode 100644 index 000000000..c5796f84c --- /dev/null +++ b/juniper_codegen/src/util/mode.rs @@ -0,0 +1,20 @@ +//! Code generation mode. + +/// Code generation mode for macros. +pub enum Mode { + /// Generated code is intended to be used by library users. + Public, + + /// Generated code is use only inside the library itself. + Internal, +} + +impl Mode { + pub fn crate_path(&self) -> syn::Path { + syn::parse_str::(match self { + Self::Public => "::juniper", + Self::Internal => "crate", + }) + .unwrap_or_else(|e| proc_macro_error::abort!(e)) + } +} From d7ea1c9d375ee88d375d5d1f6ab146070ff1a3af Mon Sep 17 00:00:00 2001 From: tyranron Date: Mon, 18 May 2020 17:58:46 +0300 Subject: [PATCH 02/29] Implement #[derive(GraphQLUnion)] for enums - add marker::GraphQLUnion trait in 'juniper' Additionally: - re-export 'futures' crate in 'juniper' for convenient reuse in generated code without requiring library user to provide 'futures' crate by himself --- juniper/src/lib.rs | 6 +- juniper/src/types/marker.rs | 24 ++ juniper_codegen/src/derive_enum.rs | 1 + juniper_codegen/src/derive_input_object.rs | 1 + juniper_codegen/src/derive_object.rs | 1 + juniper_codegen/src/derive_scalar_value.rs | 2 +- juniper_codegen/src/derive_union.rs | 19 +- .../src/graphql_union/attribute.rs | 1 + juniper_codegen/src/graphql_union/derive.rs | 382 ++++++++++++++++++ juniper_codegen/src/graphql_union/mod.rs | 225 +++++++++++ juniper_codegen/src/impl_object.rs | 1 + juniper_codegen/src/impl_scalar.rs | 2 +- juniper_codegen/src/lib.rs | 18 +- juniper_codegen/src/result.rs | 13 +- juniper_codegen/src/util/mod.rs | 75 ++-- juniper_codegen/src/util/mode.rs | 12 + juniper_codegen/src/util/option_ext.rs | 24 ++ juniper_codegen/src/util/span_container.rs | 7 +- 18 files changed, 751 insertions(+), 63 deletions(-) create mode 100644 juniper_codegen/src/graphql_union/attribute.rs create mode 100644 juniper_codegen/src/graphql_union/derive.rs create mode 100644 juniper_codegen/src/graphql_union/mod.rs create mode 100644 juniper_codegen/src/util/option_ext.rs diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs index 32e26f3bc..010978da9 100644 --- a/juniper/src/lib.rs +++ b/juniper/src/lib.rs @@ -111,6 +111,10 @@ extern crate uuid; #[cfg(any(test, feature = "bson"))] extern crate bson; +// This one is required for use by code generated with`juniper_codegen` macros. +#[doc(hidden)] +pub use futures; + // Depend on juniper_codegen and re-export everything in it. // This allows users to just depend on juniper and get the derive // functionality automatically. @@ -179,7 +183,7 @@ pub use crate::{ types::{ async_await::GraphQLTypeAsync, base::{Arguments, GraphQLType, TypeKind}, - marker, + marker::{self, GraphQLUnion}, scalars::{EmptyMutation, EmptySubscription, ID}, subscriptions::{GraphQLSubscriptionType, SubscriptionConnection, SubscriptionCoordinator}, }, diff --git a/juniper/src/types/marker.rs b/juniper/src/types/marker.rs index e3d42432c..5e2fc909e 100644 --- a/juniper/src/types/marker.rs +++ b/juniper/src/types/marker.rs @@ -23,6 +23,30 @@ pub trait GraphQLObjectType: GraphQLType { fn mark() {} } +/// Maker trait for [GraphQL unions][1]. +/// +/// This trait extends the [`GraphQLType`] and is only used to mark [union][1]. 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 [GraphQL unions][1]. +/// Other types ([scalars][2], [enums][3], [objects][4], [input objects][5] and [interfaces][6]) are +/// not allowed. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Unions +/// [2]: https://spec.graphql.org/June2018/#sec-Scalars +/// [3]: https://spec.graphql.org/June2018/#sec-Enums +/// [4]: https://spec.graphql.org/June2018/#sec-Objects +/// [5]: https://spec.graphql.org/June2018/#sec-Input-Objects +/// [6]: https://spec.graphql.org/June2018/#sec-Interfaces +pub trait GraphQLUnion: GraphQLType { + /// An arbitrary function without meaning. + /// + /// May contain compile timed check logic which ensures that types are used correctly according + /// to the [GraphQL specification][1]. + /// + /// [1]: https://spec.graphql.org/June2018/ + fn mark() {} +} + /// Marker trait for types which can be used as output types. /// /// The GraphQL specification differentiates between input and output diff --git a/juniper_codegen/src/derive_enum.rs b/juniper_codegen/src/derive_enum.rs index 9dc9b7dce..3e3d0e5ed 100644 --- a/juniper_codegen/src/derive_enum.rs +++ b/juniper_codegen/src/derive_enum.rs @@ -145,6 +145,7 @@ pub fn impl_enum( include_type_generics: true, generic_scalar: true, no_async: attrs.no_async.is_some(), + mode: is_internal.into(), }; let juniper_crate_name = if is_internal { "crate" } else { "juniper" }; diff --git a/juniper_codegen/src/derive_input_object.rs b/juniper_codegen/src/derive_input_object.rs index 843bfb2da..a85cd4d85 100644 --- a/juniper_codegen/src/derive_input_object.rs +++ b/juniper_codegen/src/derive_input_object.rs @@ -145,6 +145,7 @@ pub fn impl_input_object( include_type_generics: true, generic_scalar: true, no_async: attrs.no_async.is_some(), + mode: is_internal.into(), }; let juniper_crate_name = if is_internal { "crate" } else { "juniper" }; diff --git a/juniper_codegen/src/derive_object.rs b/juniper_codegen/src/derive_object.rs index 04dc4556c..ad5b7cc71 100644 --- a/juniper_codegen/src/derive_object.rs +++ b/juniper_codegen/src/derive_object.rs @@ -132,6 +132,7 @@ pub fn build_derive_object( include_type_generics: true, generic_scalar: true, no_async: attrs.no_async.is_some(), + mode: is_internal.into(), }; let juniper_crate_name = if is_internal { "crate" } else { "juniper" }; diff --git a/juniper_codegen/src/derive_scalar_value.rs b/juniper_codegen/src/derive_scalar_value.rs index 6474324b6..c951b1e5c 100644 --- a/juniper_codegen/src/derive_scalar_value.rs +++ b/juniper_codegen/src/derive_scalar_value.rs @@ -127,7 +127,7 @@ fn impl_scalar_struct( executor: &'a #crate_name::Executor, ) -> #crate_name::BoxFuture<'a, #crate_name::ExecutionResult<__S>> { use #crate_name::GraphQLType; - use futures::future; + use #crate_name::futures::future; let v = self.resolve(info, selection_set, executor); Box::pin(future::ready(v)) } diff --git a/juniper_codegen/src/derive_union.rs b/juniper_codegen/src/derive_union.rs index 8854c3bb3..8417ef783 100644 --- a/juniper_codegen/src/derive_union.rs +++ b/juniper_codegen/src/derive_union.rs @@ -1,4 +1,5 @@ use proc_macro2::TokenStream; +use proc_macro_error::ResultExt as _; use quote::quote; use syn::{self, ext::IdentExt, spanned::Spanned, Data, Fields}; @@ -10,12 +11,9 @@ use crate::{ const SCOPE: GraphQLScope = GraphQLScope::DeriveUnion; pub fn expand(input: TokenStream, mode: Mode) -> syn::Result { - let is_internal = matches!(mode, Mode::Internal); - - let ast = - syn::parse2::(input).unwrap_or_else(|e| proc_macro_error::abort!(e)); - + let ast = syn::parse2::(input).unwrap_or_abort(); let ast_span = ast.span(); + let enum_fields = match ast.data { Data::Enum(data) => data.variants, Data::Struct(_) => unimplemented!(), @@ -66,10 +64,7 @@ pub fn expand(input: TokenStream, mode: Mode) -> syn::Result { let _type = match field.fields { Fields::Unnamed(inner) => { let mut iter = inner.unnamed.iter(); - let first = match iter.next() { - Some(val) => val, - None => unreachable!(), - }; + let first = iter.next().unwrap(); if iter.next().is_some() { SCOPE.custom( @@ -140,7 +135,7 @@ pub fn expand(input: TokenStream, mode: Mode) -> syn::Result { SCOPE.not_empty(ast_span); } - if name.starts_with("__") && !is_internal { + if name.starts_with("__") && matches!(mode, Mode::Public) { SCOPE.no_double_underscore(if let Some(name) = attrs.name { name.span_ident() } else { @@ -180,8 +175,8 @@ pub fn expand(input: TokenStream, mode: Mode) -> syn::Result { include_type_generics: true, generic_scalar: true, no_async: attrs.no_async.is_some(), + mode, }; - let juniper_crate_name = if is_internal { "crate" } else { "juniper" }; - Ok(definition.into_union_tokens(juniper_crate_name)) + Ok(definition.into_union_tokens()) } diff --git a/juniper_codegen/src/graphql_union/attribute.rs b/juniper_codegen/src/graphql_union/attribute.rs new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/juniper_codegen/src/graphql_union/attribute.rs @@ -0,0 +1 @@ + diff --git a/juniper_codegen/src/graphql_union/derive.rs b/juniper_codegen/src/graphql_union/derive.rs new file mode 100644 index 000000000..42c07ae9b --- /dev/null +++ b/juniper_codegen/src/graphql_union/derive.rs @@ -0,0 +1,382 @@ +use proc_macro2::{Span, TokenStream}; +use proc_macro_error::ResultExt as _; +use quote::quote; +use syn::{self, ext::IdentExt, parse_quote, spanned::Spanned as _, Data, Fields}; + +use crate::{ + result::GraphQLScope, + util::{span_container::SpanContainer, Mode}, +}; + +use super::{UnionMeta, UnionVariantMeta}; + +const SCOPE: GraphQLScope = GraphQLScope::DeriveUnion; + +/// Expands `#[derive(GraphQLUnion)]` macro into generated code. +pub fn expand(input: TokenStream, mode: Mode) -> syn::Result { + let ast = syn::parse2::(input).unwrap_or_abort(); + + match &ast.data { + Data::Enum(_) => expand_enum(ast, mode), + Data::Struct(_) => unimplemented!(), // TODO + _ => Err(SCOPE.custom_error(ast.span(), "can only be applied to enums and structs")), + } + .map(UnionDefinition::into_tokens) +} + +fn expand_enum(ast: syn::DeriveInput, mode: Mode) -> syn::Result { + let meta = UnionMeta::from_attrs(&ast.attrs)?; + + let enum_span = ast.span(); + let enum_ident = ast.ident; + + // TODO: validate type has no generics + + let name = meta + .name + .clone() + .map(SpanContainer::into_inner) + .unwrap_or_else(|| enum_ident.unraw().to_string()); // TODO: PascalCase + if matches!(mode, Mode::Public) && name.starts_with("__") { + SCOPE.no_double_underscore( + meta.name + .map(|n| n.span_ident()) + .unwrap_or_else(|| enum_ident.span()), + ); + } + + let variants: Vec<_> = match ast.data { + Data::Enum(data) => data.variants, + _ => unreachable!(), + } + .into_iter() + .filter_map(|var| graphql_union_variant_from_enum_variant(var, &enum_ident)) + .collect(); + if variants.is_empty() { + SCOPE.not_empty(enum_span); + } + + // NOTICE: This is not an optimal implementation, as it's possible to bypass this check by using + // a full qualified path instead (`crate::Test` vs `Test`). Since this requirement is mandatory, + // the `std::convert::Into` implementation is used to enforce this requirement. However, due + // to the bad error message this implementation should stay and provide guidance. + let all_variants_different = { + let mut types: Vec<_> = variants.iter().map(|var| &var.ty).collect(); + types.dedup(); + types.len() == variants.len() + }; + if !all_variants_different { + SCOPE.custom(enum_ident.span(), "each variant must have a different type"); + } + + proc_macro_error::abort_if_dirty(); + + Ok(UnionDefinition { + name, + ty: syn::parse_str(&enum_ident.to_string()).unwrap_or_abort(), + description: meta.description.map(SpanContainer::into_inner), + context: meta.context.map(SpanContainer::into_inner), + scalar: meta.scalar.map(SpanContainer::into_inner), + generics: ast.generics, + variants, + span: enum_span, + mode, + }) +} + +fn graphql_union_variant_from_enum_variant( + var: syn::Variant, + enum_ident: &syn::Ident, +) -> Option { + let meta = UnionVariantMeta::from_attrs(&var.attrs) + .map_err(|e| proc_macro_error::emit_error!(e)) + .ok()?; + if meta.ignore.is_some() { + return None; + } + + let var_span = var.span(); + let var_ident = var.ident; + let path = quote! { #enum_ident::#var_ident }; + + let ty = match var.fields { + Fields::Unnamed(fields) => { + let mut iter = fields.unnamed.iter(); + let first = iter.next().unwrap(); + if iter.next().is_none() { + Ok(first.ty.clone()) + } else { + Err(fields.span()) + } + } + _ => Err(var_ident.span()), + } + .map_err(|span| { + SCOPE.custom( + span, + "only unnamed variants with a single field are allowed, e.g. Some(T)", + ) + }) + .ok()?; + + Some(UnionVariantDefinition { + ty, + path, + span: var_span, + }) +} + +struct UnionVariantDefinition { + pub ty: syn::Type, + pub path: TokenStream, + pub span: Span, +} + +struct UnionDefinition { + pub name: String, + pub ty: syn::Type, + pub description: Option, + pub context: Option, + pub scalar: Option, + pub generics: syn::Generics, + pub variants: Vec, + pub span: Span, + pub mode: Mode, +} + +impl UnionDefinition { + pub fn into_tokens(self) -> TokenStream { + let crate_path = self.mode.crate_path(); + + let name = &self.name; + let ty = &self.ty; + + let context = self + .context + .as_ref() + .map(|ctx| quote! { #ctx }) + .unwrap_or_else(|| quote! { () }); + + let scalar = self + .scalar + .as_ref() + .map(|scl| quote! { #scl }) + .unwrap_or_else(|| quote! { __S }); + let default_scalar = self + .scalar + .as_ref() + .map(|scl| quote! { #scl }) + .unwrap_or_else(|| quote! { #crate_path::DefaultScalarValue }); + + let description = self + .description + .as_ref() + .map(|desc| quote! { .description(#desc) }); + + let var_types: Vec<_> = self.variants.iter().map(|var| &var.ty).collect(); + + let match_names = self.variants.iter().map(|var| { + let var_ty = &var.ty; + let var_path = &var.path; + quote! { + #var_path(_) => <#var_ty as #crate_path::GraphQLType<#scalar>>::name(&()) + .unwrap().to_string(), + } + }); + + let match_resolves: Vec<_> = self + .variants + .iter() + .map(|var| { + let var_path = &var.path; + quote! { + match self { #var_path(ref val) => Some(val), _ => None, } + } + }) + .collect(); + let resolve_into_type = self.variants.iter().zip(match_resolves.iter()).map(|(var, expr)| { + let var_ty = &var.ty; + + let get_name = quote! { (<#var_ty as #crate_path::GraphQLType<#scalar>>::name(&())) }; + quote! { + if type_name == #get_name.unwrap() { + return #crate_path::IntoResolvable::into( + { #expr }, + executor.context() + ) + .and_then(|res| match res { + Some((ctx, r)) => executor.replaced_context(ctx).resolve_with_ctx(&(), &r), + None => Ok(#crate_path::Value::null()), + }); + } + } + }); + let resolve_into_type_async = + self.variants + .iter() + .zip(match_resolves.iter()) + .map(|(var, expr)| { + let var_ty = &var.ty; + + let get_name = + quote! { (<#var_ty as #crate_path::GraphQLType<#scalar>>::name(&())) }; + quote! { + if type_name == #get_name.unwrap() { + let res = #crate_path::IntoResolvable::into( + { #expr }, + executor.context() + ); + return #crate_path::futures::future::FutureExt::boxed(async move { + match res? { + Some((ctx, r)) => { + let subexec = executor.replaced_context(ctx); + subexec.resolve_with_ctx_async(&(), &r).await + }, + None => Ok(#crate_path::Value::null()), + } + }); + } + } + }); + + let (impl_generics, ty_generics, _) = self.generics.split_for_impl(); + let mut ext_generics = self.generics.clone(); + if self.scalar.is_none() { + ext_generics.params.push(parse_quote! { #scalar }); + ext_generics + .where_clause + .get_or_insert_with(|| parse_quote! { where }) + .predicates + .push(parse_quote! { #scalar: #crate_path::ScalarValue }); + } + let (ext_impl_generics, _, where_clause) = ext_generics.split_for_impl(); + + let mut where_async = where_clause + .cloned() + .unwrap_or_else(|| parse_quote! { where }); + where_async + .predicates + .push(parse_quote! { Self: Send + Sync }); + if self.scalar.is_none() { + where_async + .predicates + .push(parse_quote! { #scalar: Send + Sync }); + } + + let type_impl = quote! { + #[automatically_derived] + impl#ext_impl_generics #crate_path::GraphQLType<#scalar> for #ty#ty_generics + #where_clause + { + type Context = #context; + type TypeInfo = (); + + fn name(_ : &Self::TypeInfo) -> Option<&str> { + Some(#name) + } + + fn meta<'r>( + info: &Self::TypeInfo, + registry: &mut #crate_path::Registry<'r, #scalar> + ) -> #crate_path::meta::MetaType<'r, #scalar> + where #scalar: 'r, + { + let types = &[ + #( registry.get_type::<&#var_types>(&(())), )* + ]; + registry.build_union_type::<#ty#ty_generics>(info, types) + #description + .into_meta() + } + + fn concrete_type_name( + &self, + _: &Self::Context, + _: &Self::TypeInfo, + ) -> String { + match self { + #( #match_names )* + } + } + + fn resolve_into_type( + &self, + _: &Self::TypeInfo, + type_name: &str, + _: Option<&[#crate_path::Selection<#scalar>]>, + executor: &#crate_path::Executor, + ) -> #crate_path::ExecutionResult<#scalar> { + #( #resolve_into_type )* + panic!( + "Concrete type {} is not handled by instance resolvers on GraphQL Union {}", + type_name, #name, + ); + } + } + }; + + let async_type_impl = quote! { + #[automatically_derived] + impl#ext_impl_generics #crate_path::GraphQLTypeAsync<#scalar> for #ty#ty_generics + #where_async + { + fn resolve_into_type_async<'b>( + &'b self, + _: &'b Self::TypeInfo, + type_name: &str, + _: Option<&'b [#crate_path::Selection<'b, #scalar>]>, + executor: &'b #crate_path::Executor<'b, 'b, Self::Context, #scalar> + ) -> #crate_path::BoxFuture<'b, #crate_path::ExecutionResult<#scalar>> { + #( #resolve_into_type_async )* + panic!( + "Concrete type {} is not handled by instance resolvers on GraphQL Union {}", + type_name, #name, + ); + } + } + }; + + let conversion_impls = self.variants.iter().map(|var| { + let var_ty = &var.ty; + let var_path = &var.path; + quote! { + #[automatically_derived] + impl#impl_generics ::std::convert::From<#var_ty> for #ty#ty_generics { + fn from(v: #var_ty) -> Self { + #var_path(v) + } + } + } + }); + + let output_type_impl = quote! { + #[automatically_derived] + impl#ext_impl_generics #crate_path::marker::IsOutputType<#scalar> for #ty#ty_generics + #where_clause + { + fn mark() { + #( <#var_types as #crate_path::marker::GraphQLObjectType<#scalar>>::mark(); )* + } + } + }; + + let union_impl = quote! { + #[automatically_derived] + impl#impl_generics #crate_path::marker::GraphQLUnion for #ty#ty_generics { + fn mark() { + #( <#var_types as #crate_path::marker::GraphQLObjectType< + #default_scalar, + >>::mark(); )* + } + } + }; + + quote! { + #( #conversion_impls )* + #union_impl + #output_type_impl + #type_impl + #async_type_impl + } + } +} diff --git a/juniper_codegen/src/graphql_union/mod.rs b/juniper_codegen/src/graphql_union/mod.rs new file mode 100644 index 000000000..78f2a64d2 --- /dev/null +++ b/juniper_codegen/src/graphql_union/mod.rs @@ -0,0 +1,225 @@ +pub mod attribute; +pub mod derive; + +use syn::{ + parse::{Parse, ParseStream}, + spanned::Spanned as _, +}; + +use crate::util::{ + filter_graphql_attrs, get_doc_comment, span_container::SpanContainer, OptionExt as _, +}; + +/// Available metadata behind `#[graphql]` (or `#[graphql_union]`) attribute when generating code +/// for [GraphQL union][1] type. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Unions +#[derive(Debug, Default)] +struct UnionMeta { + /// Explicitly specified name of [GraphQL union][1] type. + /// + /// If absent, then `PascalCase`d Rust type name is used by default. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + pub name: Option>, + + /// Explicitly specified [description][2] of [GraphQL union][1] type. + /// + /// If absent, then Rust doc comment is used as [description][2], if any. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + /// [2]: https://spec.graphql.org/June2018/#sec-Descriptions + pub description: Option>, + + /// Explicitly specified type of `juniper::Context` to use for resolving this [GraphQL union][1] + /// type with. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + pub context: Option>, + + /// Explicitly specified type of `juniper::ScalarValue` to use for resolving this + /// [GraphQL union][1] type with. + /// + /// If absent, then generated code will be generic over any `juniper::ScalarValue` type, which, + /// in turn, requires all [union][1] variants to be generic over any `juniper::ScalarValue` type + /// too. That's why this type should be specified only if one of the variants implements + /// `juniper::GraphQLType` in non-generic over `juniper::ScalarValue` type way. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + pub scalar: Option>, +} + +impl Parse for UnionMeta { + fn parse(input: ParseStream) -> syn::Result { + let mut output = Self::default(); + + // TODO: check for duplicates? + while !input.is_empty() { + let ident: syn::Ident = input.parse()?; + match ident.to_string().as_str() { + "name" => { + input.parse::()?; + let name = input.parse::()?; + output + .name + .replace(SpanContainer::new( + ident.span(), + Some(name.span()), + name.value(), + )) + .none_or_else(|_| syn::Error::new(ident.span(), "duplicated attribute"))? + } + "desc" | "description" => { + input.parse::()?; + let desc = input.parse::()?; + output + .description + .replace(SpanContainer::new( + ident.span(), + Some(desc.span()), + desc.value(), + )) + .none_or_else(|_| syn::Error::new(ident.span(), "duplicated attribute"))? + } + "ctx" | "context" | "Context" => { + input.parse::()?; + let ctx = input.parse::()?; + output + .context + .replace(SpanContainer::new(ident.span(), Some(ctx.span()), ctx)) + .none_or_else(|_| syn::Error::new(ident.span(), "duplicated attribute"))? + } + "scalar" | "Scalar" | "ScalarValue" => { + input.parse::()?; + let scl = input.parse::()?; + output + .scalar + .replace(SpanContainer::new(ident.span(), Some(scl.span()), scl)) + .none_or_else(|_| syn::Error::new(ident.span(), "duplicated attribute"))? + } + _ => { + return Err(syn::Error::new(ident.span(), "unknown attribute")); + } + } + if input.lookahead1().peek(syn::Token![,]) { + input.parse::()?; + } + } + + Ok(output) + } +} + +impl UnionMeta { + /// Tries to merge two [`UnionMeta`]s into single one, reporting about duplicates, if any. + fn try_merge(self, mut other: Self) -> syn::Result { + Ok(Self { + name: { + if let Some(v) = self.name { + other.name.replace(v).none_or_else(|dup| { + syn::Error::new(dup.span_ident(), "duplicated attribute") + })?; + } + other.name + }, + description: { + if let Some(v) = self.description { + other.description.replace(v).none_or_else(|dup| { + syn::Error::new(dup.span_ident(), "duplicated attribute") + })?; + } + other.description + }, + context: { + if let Some(v) = self.context { + other.context.replace(v).none_or_else(|dup| { + syn::Error::new(dup.span_ident(), "duplicated attribute") + })?; + } + other.context + }, + scalar: { + if let Some(v) = self.scalar { + other.scalar.replace(v).none_or_else(|dup| { + syn::Error::new(dup.span_ident(), "duplicated attribute") + })?; + } + other.scalar + }, + }) + } + + /// Parses [`UnionMeta`] from the given attributes placed on type definition. + pub fn from_attrs(attrs: &[syn::Attribute]) -> syn::Result { + let mut meta = filter_graphql_attrs(attrs) + .map(|attr| attr.parse_args()) + .try_fold(Self::default(), |prev, curr| prev.try_merge(curr?))?; + + if meta.description.is_none() { + meta.description = get_doc_comment(attrs); + } + + Ok(meta) + } +} + +/// Available metadata behind `#[graphql]` attribute when generating code for [GraphQL union][1]'s +/// variant. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Unions +#[derive(Debug, Default)] +struct UnionVariantMeta { + /// Explicitly specified marker for the variant/field being ignored and not included into + /// [GraphQL union][1]. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + pub ignore: Option>, +} + +impl Parse for UnionVariantMeta { + fn parse(input: ParseStream) -> syn::Result { + let mut output = Self::default(); + + while !input.is_empty() { + let ident: syn::Ident = input.parse()?; + match ident.to_string().as_str() { + "ignore" | "skip" => output + .ignore + .replace(SpanContainer::new(ident.span(), None, ident.clone())) + .none_or_else(|_| syn::Error::new(ident.span(), "duplicated attribute"))?, + _ => { + return Err(syn::Error::new(ident.span(), "unknown attribute")); + } + } + if input.lookahead1().peek(syn::Token![,]) { + input.parse::()?; + } + } + + Ok(output) + } +} + +impl UnionVariantMeta { + /// Tries to merge two [`UnionVariantMeta`]s into single one, reporting about duplicates, if + /// any. + fn try_merge(self, mut other: Self) -> syn::Result { + Ok(Self { + ignore: { + if let Some(v) = self.ignore { + other.ignore.replace(v).none_or_else(|dup| { + syn::Error::new(dup.span_ident(), "duplicated attribute") + })?; + } + other.ignore + }, + }) + } + + /// Parses [`UnionVariantMeta`] from the given attributes placed on variant/field definition. + pub fn from_attrs(attrs: &[syn::Attribute]) -> syn::Result { + filter_graphql_attrs(attrs) + .map(|attr| attr.parse_args()) + .try_fold(Self::default(), |prev, curr| prev.try_merge(curr?)) + } +} diff --git a/juniper_codegen/src/impl_object.rs b/juniper_codegen/src/impl_object.rs index 4cee482f0..6624e9a73 100644 --- a/juniper_codegen/src/impl_object.rs +++ b/juniper_codegen/src/impl_object.rs @@ -228,6 +228,7 @@ fn create( include_type_generics: false, generic_scalar: false, no_async: _impl.attrs.no_async.is_some(), + mode: is_internal.into(), }; Ok(definition) diff --git a/juniper_codegen/src/impl_scalar.rs b/juniper_codegen/src/impl_scalar.rs index 37a976583..fc449aa8a 100644 --- a/juniper_codegen/src/impl_scalar.rs +++ b/juniper_codegen/src/impl_scalar.rs @@ -264,7 +264,7 @@ pub fn build_scalar( executor: &'a #crate_name::Executor, ) -> #crate_name::BoxFuture<'a, #crate_name::ExecutionResult<#async_generic_type>> { use #crate_name::GraphQLType; - use futures::future; + use #crate_name::futures::future; let v = self.resolve(info, selection_set, executor); Box::pin(future::ready(v)) } diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs index 0baf8b744..2f07db473 100644 --- a/juniper_codegen/src/lib.rs +++ b/juniper_codegen/src/lib.rs @@ -21,8 +21,10 @@ mod impl_object; mod impl_scalar; mod impl_union; +mod graphql_union; + use proc_macro::TokenStream; -use proc_macro_error::proc_macro_error; +use proc_macro_error::{proc_macro_error, ResultExt as _}; use result::GraphQLScope; use self::util::Mode; @@ -549,8 +551,9 @@ pub fn graphql_subscription_internal(args: TokenStream, input: TokenStream) -> T #[proc_macro_error] #[proc_macro_derive(GraphQLUnion, attributes(graphql))] pub fn derive_union(input: TokenStream) -> TokenStream { - derive_union::expand(input.into(), Mode::Public) - .unwrap_or_else(|e| proc_macro_error::abort!(e)) + //derive_union::expand(input.into(), Mode::Public) + graphql_union::derive::expand(input.into(), Mode::Public) + .unwrap_or_abort() .into() } @@ -558,8 +561,9 @@ pub fn derive_union(input: TokenStream) -> TokenStream { #[proc_macro_derive(GraphQLUnionInternal, attributes(graphql))] #[doc(hidden)] pub fn derive_union_internal(input: TokenStream) -> TokenStream { - derive_union::expand(input.into(), Mode::Internal) - .unwrap_or_else(|e| proc_macro_error::abort!(e)) + //derive_union::expand(input.into(), Mode::Internal) + graphql_union::derive::expand(input.into(), Mode::Internal) + .unwrap_or_abort() .into() } @@ -567,7 +571,7 @@ pub fn derive_union_internal(input: TokenStream) -> TokenStream { #[proc_macro_attribute] pub fn graphql_union(attrs: TokenStream, body: TokenStream) -> TokenStream { impl_union::expand(attrs.into(), body.into(), Mode::Public) - .unwrap_or_else(|e| proc_macro_error::abort!(e)) + .unwrap_or_abort() .into() } @@ -576,6 +580,6 @@ pub fn graphql_union(attrs: TokenStream, body: TokenStream) -> TokenStream { #[doc(hidden)] pub fn graphql_union_internal(attrs: TokenStream, body: TokenStream) -> TokenStream { impl_union::expand(attrs.into(), body.into(), Mode::Internal) - .unwrap_or_else(|e| proc_macro_error::abort!(e)) + .unwrap_or_abort() .into() } diff --git a/juniper_codegen/src/result.rs b/juniper_codegen/src/result.rs index 537d81b1c..988fa3081 100644 --- a/juniper_codegen/src/result.rs +++ b/juniper_codegen/src/result.rs @@ -37,7 +37,7 @@ impl fmt::Display for GraphQLScope { let name = match self { GraphQLScope::DeriveObject | GraphQLScope::ImplObject => "object", GraphQLScope::DeriveInputObject => "input object", - GraphQLScope::DeriveUnion | GraphQLScope::ImplUnion => "union", + GraphQLScope::DeriveUnion | GraphQLScope::ImplUnion => "Union", GraphQLScope::DeriveEnum => "enum", GraphQLScope::DeriveScalar | GraphQLScope::ImplScalar => "scalar", }; @@ -63,7 +63,7 @@ impl GraphQLScope { } pub fn custom>(&self, span: Span, msg: S) { - Diagnostic::spanned(span, Level::Error, format!("{} {}", self, msg.as_ref())) + Diagnostic::spanned(span, Level::Error, format!("{}: {}", self, msg.as_ref())) .note(self.spec_link()) .emit(); } @@ -131,9 +131,12 @@ impl GraphQLScope { 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 GraphQL’s introspection system.".to_string(), + "All types and directives defined within a schema must not have a name which begins \ + with `__` (two underscores), as this is used exclusively by GraphQL’s introspection \ + system." + .into(), ) - .note(format!("{}#sec-Schema", SPEC_URL)) - .emit(); + .note(format!("{}#sec-Schema", SPEC_URL)) + .emit(); } } diff --git a/juniper_codegen/src/util/mod.rs b/juniper_codegen/src/util/mod.rs index 9e8375c81..2b2da23dc 100644 --- a/juniper_codegen/src/util/mod.rs +++ b/juniper_codegen/src/util/mod.rs @@ -2,6 +2,7 @@ pub mod duplicate; pub mod mode; +pub mod option_ext; pub mod parse_impl; pub mod span_container; @@ -15,7 +16,7 @@ use syn::{ MetaNameValue, NestedMeta, Token, }; -pub use mode::Mode; +pub use self::{mode::Mode, option_ext::OptionExt}; pub fn juniper_path(is_internal: bool) -> syn::Path { let name = if is_internal { "crate" } else { "juniper" }; @@ -88,6 +89,13 @@ pub fn find_graphql_attr(attrs: &[Attribute]) -> Option<&Attribute> { .find(|attr| path_eq_single(&attr.path, "graphql")) } +/// Filters given `attrs` to contain `#[graphql]` attributes only. +pub fn filter_graphql_attrs(attrs: &[Attribute]) -> impl Iterator { + attrs + .iter() + .filter(|attr| path_eq_single(&attr.path, "graphql")) +} + pub fn get_deprecated(attrs: &[Attribute]) -> Option> { attrs .iter() @@ -672,6 +680,7 @@ pub struct GraphQLTypeDefiniton { pub generic_scalar: bool, // FIXME: make this redundant. pub no_async: bool, + pub mode: Mode, } impl GraphQLTypeDefiniton { @@ -870,7 +879,7 @@ impl GraphQLTypeDefiniton { Err(e) => Err(e), } }; - use futures::future; + use #juniper_crate_name::futures::future; future::FutureExt::boxed(f) }, ) @@ -887,7 +896,7 @@ impl GraphQLTypeDefiniton { Err(e) => Err(e), } }; - use futures::future; + use #juniper_crate_name::futures::future; future::FutureExt::boxed(f) ) } else { @@ -897,7 +906,7 @@ impl GraphQLTypeDefiniton { Ok(None) => Ok(#juniper_crate_name::Value::null()), Err(e) => Err(e), }; - use futures::future; + use #juniper_crate_name::futures::future; future::FutureExt::boxed(future::ready(v)) ) }; @@ -937,7 +946,7 @@ impl GraphQLTypeDefiniton { ) -> #juniper_crate_name::BoxFuture<'b, #juniper_crate_name::ExecutionResult<#scalar>> where #scalar: Send + Sync, { - use futures::future; + use #juniper_crate_name::futures::future; use #juniper_crate_name::GraphQLType; match field { #( #resolve_matches_async )* @@ -1180,7 +1189,7 @@ impl GraphQLTypeDefiniton { }; quote!( #name => { - futures::FutureExt::boxed(async move { + #juniper_crate_name::futures::FutureExt::boxed(async move { let res #_type = { #code }; let res = #juniper_crate_name::IntoFieldResult::<_, #scalar>::into_result(res)?; let executor= executor.as_owned_executor(); @@ -1270,7 +1279,7 @@ impl GraphQLTypeDefiniton { args: #juniper_crate_name::Arguments<'args, #scalar>, executor: &'ref_e #juniper_crate_name::Executor<'ref_e, 'e, Self::Context, #scalar>, ) -> std::pin::Pin>, #juniper_crate_name::FieldError<#scalar> @@ -1287,7 +1296,7 @@ impl GraphQLTypeDefiniton { 'res: 'f, { use #juniper_crate_name::Value; - use futures::stream::StreamExt as _; + use #juniper_crate_name::futures::stream::StreamExt as _; match field_name { #( #resolve_matches_async )* @@ -1305,8 +1314,8 @@ impl GraphQLTypeDefiniton { ) } - pub fn into_union_tokens(self, juniper_crate_name: &str) -> TokenStream { - let juniper_crate_name = syn::parse_str::(juniper_crate_name).unwrap(); + pub fn into_union_tokens(self) -> TokenStream { + let crate_path = self.mode.crate_path(); let name = &self.name; let ty = &self._type; @@ -1326,7 +1335,7 @@ impl GraphQLTypeDefiniton { // See more comments below. quote!(__S) } else { - quote!(#juniper_crate_name::DefaultScalarValue) + quote!(#crate_path::DefaultScalarValue) } }); @@ -1351,7 +1360,7 @@ impl GraphQLTypeDefiniton { let resolver_code = &field.resolver_code; quote!( - #resolver_code(ref x) => <#var_ty as #juniper_crate_name::GraphQLType<#scalar>>::name(&()).unwrap().to_string(), + #resolver_code(ref x) => <#var_ty as #crate_path::GraphQLType<#scalar>>::name(&()).unwrap().to_string(), ) }); @@ -1377,15 +1386,15 @@ impl GraphQLTypeDefiniton { let var_ty = &field._type; quote! { - if type_name == (<#var_ty as #juniper_crate_name::GraphQLType<#scalar>>::name(&())).unwrap() { - return #juniper_crate_name::IntoResolvable::into( + if type_name == (<#var_ty as #crate_path::GraphQLType<#scalar>>::name(&())).unwrap() { + return #crate_path::IntoResolvable::into( { #expr }, 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()), + None => Ok(#crate_path::Value::null()), } }); } @@ -1396,8 +1405,8 @@ impl GraphQLTypeDefiniton { let var_ty = &field._type; quote! { - if type_name == (<#var_ty as #juniper_crate_name::GraphQLType<#scalar>>::name(&())).unwrap() { - let inner_res = #juniper_crate_name::IntoResolvable::into( + if type_name == (<#var_ty as #crate_path::GraphQLType<#scalar>>::name(&())).unwrap() { + let inner_res = #crate_path::IntoResolvable::into( { #expr }, executor.context() ); @@ -1408,11 +1417,11 @@ impl GraphQLTypeDefiniton { let subexec = executor.replaced_context(ctx); subexec.resolve_with_ctx_async(&(), &r).await }, - Ok(None) => Ok(#juniper_crate_name::Value::null()), + Ok(None) => Ok(#crate_path::Value::null()), Err(e) => Err(e), } }; - use futures::future; + use #crate_path::futures::future; return future::FutureExt::boxed(f); } } @@ -1430,7 +1439,7 @@ impl GraphQLTypeDefiniton { // Insert ScalarValue constraint. where_clause .predicates - .push(parse_quote!(__S: #juniper_crate_name::ScalarValue)); + .push(parse_quote!(__S: #crate_path::ScalarValue)); } let (impl_generics, _, where_clause) = generics.split_for_impl(); @@ -1442,16 +1451,16 @@ impl GraphQLTypeDefiniton { where_async.predicates.push(parse_quote!(Self: Send + Sync)); let async_type_impl = quote!( - impl#impl_generics #juniper_crate_name::GraphQLTypeAsync<#scalar> for #ty + impl#impl_generics #crate_path::GraphQLTypeAsync<#scalar> for #ty #where_async { fn resolve_into_type_async<'b>( &'b self, _info: &'b Self::TypeInfo, type_name: &str, - _: Option<&'b [#juniper_crate_name::Selection<'b, #scalar>]>, - executor: &'b #juniper_crate_name::Executor<'b, 'b, Self::Context, #scalar> - ) -> #juniper_crate_name::BoxFuture<'b, #juniper_crate_name::ExecutionResult<#scalar>> { + _: Option<&'b [#crate_path::Selection<'b, #scalar>]>, + executor: &'b #crate_path::Executor<'b, 'b, Self::Context, #scalar> + ) -> #crate_path::BoxFuture<'b, #crate_path::ExecutionResult<#scalar>> { let context = &executor.context(); #( #resolve_into_type_async )* @@ -1477,20 +1486,20 @@ impl GraphQLTypeDefiniton { let object_marks = self.fields.iter().map(|field| { let _ty = &field._type; quote!( - <#_ty as #juniper_crate_name::marker::GraphQLObjectType<#scalar>>::mark(); + <#_ty as #crate_path::marker::GraphQLObjectType<#scalar>>::mark(); ) }); let mut type_impl = quote! { #( #convesion_impls )* - impl #impl_generics #juniper_crate_name::marker::IsOutputType<#scalar> for #ty #where_clause { + impl #impl_generics #crate_path::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 #crate_path::GraphQLType<#scalar> for #ty #where_clause { type Context = #context; type TypeInfo = (); @@ -1501,8 +1510,8 @@ impl GraphQLTypeDefiniton { fn meta<'r>( info: &Self::TypeInfo, - registry: &mut #juniper_crate_name::Registry<'r, #scalar> - ) -> #juniper_crate_name::meta::MetaType<'r, #scalar> + registry: &mut #crate_path::Registry<'r, #scalar> + ) -> #crate_path::meta::MetaType<'r, #scalar> where #scalar: 'r, { @@ -1525,9 +1534,9 @@ impl GraphQLTypeDefiniton { &self, _info: &Self::TypeInfo, type_name: &str, - _: Option<&[#juniper_crate_name::Selection<#scalar>]>, - executor: &#juniper_crate_name::Executor, - ) -> #juniper_crate_name::ExecutionResult<#scalar> { + _: Option<&[#crate_path::Selection<#scalar>]>, + executor: &#crate_path::Executor, + ) -> #crate_path::ExecutionResult<#scalar> { let context = &executor.context(); #( #resolve_into_type )* @@ -1663,7 +1672,7 @@ impl GraphQLTypeDefiniton { executor: &'a #juniper_crate_name::Executor, ) -> #juniper_crate_name::BoxFuture<'a, #juniper_crate_name::ExecutionResult<#scalar>> { use #juniper_crate_name::GraphQLType; - use futures::future; + use #juniper_crate_name::futures::future; let v = self.resolve(info, selection_set, executor); future::FutureExt::boxed(future::ready(v)) } diff --git a/juniper_codegen/src/util/mode.rs b/juniper_codegen/src/util/mode.rs index c5796f84c..601f7a9cd 100644 --- a/juniper_codegen/src/util/mode.rs +++ b/juniper_codegen/src/util/mode.rs @@ -1,6 +1,7 @@ //! Code generation mode. /// Code generation mode for macros. +#[derive(Debug)] pub enum Mode { /// Generated code is intended to be used by library users. Public, @@ -18,3 +19,14 @@ impl Mode { .unwrap_or_else(|e| proc_macro_error::abort!(e)) } } + +// TODO: Remove once all macros are refactored with `Mode`. +impl From for Mode { + fn from(is_internal: bool) -> Self { + if is_internal { + Mode::Internal + } else { + Mode::Public + } + } +} diff --git a/juniper_codegen/src/util/option_ext.rs b/juniper_codegen/src/util/option_ext.rs new file mode 100644 index 000000000..3007abd65 --- /dev/null +++ b/juniper_codegen/src/util/option_ext.rs @@ -0,0 +1,24 @@ +/// Handy extension of [`Option`] methods used in this crate. +pub trait OptionExt { + type Inner; + + /// Transforms the `Option` into a `Result<(), E>`, mapping `None` to `Ok(())` and `Some(v)` + /// to `Err(err(v))`. + fn none_or_else(self, err: F) -> Result<(), E> + where + F: FnOnce(Self::Inner) -> E; +} + +impl OptionExt for Option { + type Inner = T; + + fn none_or_else(self, err: F) -> Result<(), E> + where + F: FnOnce(T) -> E, + { + match self { + Some(v) => Err(err(v)), + None => Ok(()), + } + } +} diff --git a/juniper_codegen/src/util/span_container.rs b/juniper_codegen/src/util/span_container.rs index a808bbeba..d449fbb71 100644 --- a/juniper_codegen/src/util/span_container.rs +++ b/juniper_codegen/src/util/span_container.rs @@ -1,8 +1,9 @@ +use std::ops; + use proc_macro2::{Span, TokenStream}; use quote::ToTokens; -use std::cmp::{Eq, PartialEq}; -#[derive(Debug, Clone)] +#[derive(Clone, Debug)] pub struct SpanContainer { expr: Option, ident: Span, @@ -47,7 +48,7 @@ impl AsRef for SpanContainer { } } -impl std::ops::Deref for SpanContainer { +impl ops::Deref for SpanContainer { type Target = T; fn deref(&self) -> &Self::Target { From 8f59c4d68a2bfa8df48c79b22a9a1ca386b8b5ed Mon Sep 17 00:00:00 2001 From: tyranron Date: Thu, 21 May 2020 17:29:06 +0300 Subject: [PATCH 03/29] Polish #[derive(GraphQLUnion)] for enums --- .../juniper_tests/src/codegen/derive_union.rs | 29 +++++ .../juniper_tests/src/codegen/impl_union.rs | 60 ++++++++++ juniper_codegen/src/graphql_union/derive.rs | 103 +++++++++++++----- juniper_codegen/src/graphql_union/mod.rs | 61 ++++++++++- juniper_codegen/src/util/span_container.rs | 27 ++++- 5 files changed, 249 insertions(+), 31 deletions(-) diff --git a/integration_tests/juniper_tests/src/codegen/derive_union.rs b/integration_tests/juniper_tests/src/codegen/derive_union.rs index fcec2e74c..39a586ea2 100644 --- a/integration_tests/juniper_tests/src/codegen/derive_union.rs +++ b/integration_tests/juniper_tests/src/codegen/derive_union.rs @@ -28,6 +28,35 @@ pub enum Character { Two(Droid), } +#[derive(juniper::GraphQLUnion)] +#[graphql(Scalar = juniper::DefaultScalarValue)] +pub enum CharacterGeneric { + One(Human), + Two(Droid), + #[allow(dead_code)] + #[graphql(ignore)] + Hidden(T), +} + +#[derive(juniper::GraphQLUnion)] +#[graphql(on Droid = CharacterDyn::as_droid)] +pub enum CharacterDyn { + One(Human), + //#[graphql(ignore)] + #[graphql(with = CharacterDyn::as_droid)] + Two(Droid), +} + +impl CharacterDyn { + fn as_droid(&self, _: &()) -> Option<&Droid> { + match self { + Self::Two(droid) => Some(droid), + _ => None, + } + } +} + + // Context Test pub struct CustomContext { is_left: bool, diff --git a/integration_tests/juniper_tests/src/codegen/impl_union.rs b/integration_tests/juniper_tests/src/codegen/impl_union.rs index 5ed28a3f4..527230a3e 100644 --- a/integration_tests/juniper_tests/src/codegen/impl_union.rs +++ b/integration_tests/juniper_tests/src/codegen/impl_union.rs @@ -42,3 +42,63 @@ impl<'a> GraphQLUnion for &'a dyn Character { } } } + +/* +#[juniper::graphql_union] +impl GraphQLUnion for dyn Character { + fn resolve_human(&self) -> Option<&Human> { + self.as_human() + } + + fn resolve_droid(&self) -> Option<&Droid> { + self.as_droid() + } +} +*/ + +/* +#[derive(GraphQLUnion)] +#[graphql( + Human = Char::resolve_human, + Droid = Char::resolve_droid, +)] +#[graphql(with(Char::resolve_human) => Human)] +#[graphql(object = Droid, with = Char::resolve_droid)] +struct Char { + id: String, +} + +impl Char { + fn resolve_human(&self, _: &Context) -> Option<&Human> { + unimplemented!() + } + fn resolve_droid(&self, _: &Context) -> Option<&Droid> { + unimplemented!() + } +} + +#[graphq_union] +trait Charctr { + fn as_human(&self) -> Option<&Human> { None } + fn as_droid(&self, _: &Context) -> Option<&Droid> { None } +} + +#[graphq_union( + Human = Char::resolve_human, + Droid = Char::resolve_droid, +)] +#[graphql(object = Human, with = Charctr2::resolve_human)] +#[graphql(object = Droid, with = Charctr2::resolve_droid)] +trait Charctr2 { + fn id(&self) -> &str; +} + +impl dyn Charctr2 { + fn resolve_human(&self, _: &Context) -> Option<&Human> { + unimplemented!() + } + fn resolve_droid(&self, _: &Context) -> Option<&Droid> { + unimplemented!() + } +} +*/ \ No newline at end of file diff --git a/juniper_codegen/src/graphql_union/derive.rs b/juniper_codegen/src/graphql_union/derive.rs index 42c07ae9b..847220405 100644 --- a/juniper_codegen/src/graphql_union/derive.rs +++ b/juniper_codegen/src/graphql_union/derive.rs @@ -30,8 +30,6 @@ fn expand_enum(ast: syn::DeriveInput, mode: Mode) -> syn::Result syn::Result = match ast.data { + let mut variants: Vec<_> = match ast.data { Data::Enum(data) => data.variants, _ => unreachable!(), } .into_iter() .filter_map(|var| graphql_union_variant_from_enum_variant(var, &enum_ident)) .collect(); + if !meta.custom_resolvers.is_empty() { + let crate_path = mode.crate_path(); + // TODO: refactor into separate function + for (ty, rslvr) in meta.custom_resolvers { + let span = rslvr.span_joined(); + + let resolver_fn = rslvr.into_inner(); + let resolver_code = parse_quote! { + #resolver_fn(self, #crate_path::FromContext::from(context)) + }; + // Doing this may be quite an expensive, because resolving may contain some heavy + // computation, so we're preforming it twice. Unfortunately, we have no other options + // here, until the `juniper::GraphQLType` itself will allow to do it in some cleverer + // way. + let resolver_check = parse_quote! { + ({ #resolver_code } as ::std::option::Option<&#ty>).is_some() + }; + + if let Some(var) = variants.iter_mut().find(|v| v.ty == ty) { + var.resolver_code = resolver_code; + var.resolver_check = resolver_check; + var.span = span; + } else { + variants.push(UnionVariantDefinition { + ty, + resolver_code, + resolver_check, + enum_path: None, + span, + }) + } + } + } if variants.is_empty() { SCOPE.not_empty(enum_span); } @@ -97,7 +128,19 @@ fn graphql_union_variant_from_enum_variant( let var_span = var.span(); let var_ident = var.ident; - let path = quote! { #enum_ident::#var_ident }; + let enum_path = quote! { #enum_ident::#var_ident }; + + // TODO + if meta.custom_resolver.is_some() { + unimplemented!() + } + + let resolver_code = parse_quote! { + match self { #enum_ident::#var_ident(ref v) => Some(v), _ => None, } + }; + let resolver_check = parse_quote! { + matches!(self, #enum_path(_)) + }; let ty = match var.fields { Fields::Unnamed(fields) => { @@ -121,14 +164,18 @@ fn graphql_union_variant_from_enum_variant( Some(UnionVariantDefinition { ty, - path, + resolver_code, + resolver_check, + enum_path: Some(enum_path), span: var_span, }) } struct UnionVariantDefinition { pub ty: syn::Type, - pub path: TokenStream, + pub resolver_code: syn::Expr, + pub resolver_check: syn::Expr, + pub enum_path: Option, pub span: Span, } @@ -177,23 +224,16 @@ impl UnionDefinition { let match_names = self.variants.iter().map(|var| { let var_ty = &var.ty; - let var_path = &var.path; + let var_check = &var.resolver_check; quote! { - #var_path(_) => <#var_ty as #crate_path::GraphQLType<#scalar>>::name(&()) - .unwrap().to_string(), + if #var_check { + return <#var_ty as #crate_path::GraphQLType<#scalar>>::name(&()) + .unwrap().to_string(); + } } }); - let match_resolves: Vec<_> = self - .variants - .iter() - .map(|var| { - let var_path = &var.path; - quote! { - match self { #var_path(ref val) => Some(val), _ => None, } - } - }) - .collect(); + let match_resolves: Vec<_> = self.variants.iter().map(|var| &var.resolver_code).collect(); let resolve_into_type = self.variants.iter().zip(match_resolves.iter()).map(|(var, expr)| { let var_ty = &var.ty; @@ -291,12 +331,15 @@ impl UnionDefinition { fn concrete_type_name( &self, - _: &Self::Context, + context: &Self::Context, _: &Self::TypeInfo, ) -> String { - match self { - #( #match_names )* - } + #( #match_names )* + panic!( + "GraphQL union {} cannot be resolved into any of its variants in its \ + current state", + #name, + ); } fn resolve_into_type( @@ -306,9 +349,10 @@ impl UnionDefinition { _: Option<&[#crate_path::Selection<#scalar>]>, executor: &#crate_path::Executor, ) -> #crate_path::ExecutionResult<#scalar> { + let context = executor.context(); #( #resolve_into_type )* panic!( - "Concrete type {} is not handled by instance resolvers on GraphQL Union {}", + "Concrete type {} is not handled by instance resolvers on GraphQL union {}", type_name, #name, ); } @@ -327,26 +371,27 @@ impl UnionDefinition { _: Option<&'b [#crate_path::Selection<'b, #scalar>]>, executor: &'b #crate_path::Executor<'b, 'b, Self::Context, #scalar> ) -> #crate_path::BoxFuture<'b, #crate_path::ExecutionResult<#scalar>> { + let context = executor.context(); #( #resolve_into_type_async )* panic!( - "Concrete type {} is not handled by instance resolvers on GraphQL Union {}", + "Concrete type {} is not handled by instance resolvers on GraphQL union {}", type_name, #name, ); } } }; - let conversion_impls = self.variants.iter().map(|var| { + let conversion_impls = self.variants.iter().filter_map(|var| { let var_ty = &var.ty; - let var_path = &var.path; - quote! { + let var_path = var.enum_path.as_ref()?; + Some(quote! { #[automatically_derived] impl#impl_generics ::std::convert::From<#var_ty> for #ty#ty_generics { fn from(v: #var_ty) -> Self { #var_path(v) } } - } + }) }); let output_type_impl = quote! { diff --git a/juniper_codegen/src/graphql_union/mod.rs b/juniper_codegen/src/graphql_union/mod.rs index 78f2a64d2..c1b76fde0 100644 --- a/juniper_codegen/src/graphql_union/mod.rs +++ b/juniper_codegen/src/graphql_union/mod.rs @@ -1,6 +1,8 @@ pub mod attribute; pub mod derive; +use std::collections::HashMap; + use syn::{ parse::{Parse, ParseStream}, spanned::Spanned as _, @@ -47,13 +49,21 @@ struct UnionMeta { /// /// [1]: https://spec.graphql.org/June2018/#sec-Unions pub scalar: Option>, + + /// Explicitly specified custom resolver functions for [GraphQL union][1] variants. + /// + /// If absent, then macro will try to auto-infer all the possible variants from the type + /// declaration, if possible. That's why specifying a custom resolver function has sense, when + /// some custom [union][1] variant resolving logic is involved, or variants cannot be inferred. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + pub custom_resolvers: HashMap>, } impl Parse for UnionMeta { fn parse(input: ParseStream) -> syn::Result { let mut output = Self::default(); - // TODO: check for duplicates? while !input.is_empty() { let ident: syn::Ident = input.parse()?; match ident.to_string().as_str() { @@ -97,6 +107,17 @@ impl Parse for UnionMeta { .replace(SpanContainer::new(ident.span(), Some(scl.span()), scl)) .none_or_else(|_| syn::Error::new(ident.span(), "duplicated attribute"))? } + "on" => { + let ty = input.parse::()?; + input.parse::()?; + let rslvr = input.parse::()?; + let rslvr_spanned = SpanContainer::new(ident.span(), Some(ty.span()), rslvr); + let rslvr_span = rslvr_spanned.span_joined(); + output + .custom_resolvers + .insert(ty, rslvr_spanned) + .none_or_else(|_| syn::Error::new(rslvr_span, "duplicated attribute"))? + } _ => { return Err(syn::Error::new(ident.span(), "unknown attribute")); } @@ -146,6 +167,19 @@ impl UnionMeta { } other.scalar }, + custom_resolvers: { + if !self.custom_resolvers.is_empty() { + for (ty, rslvr) in self.custom_resolvers { + other + .custom_resolvers + .insert(ty, rslvr) + .none_or_else(|dup| { + syn::Error::new(dup.span_joined(), "duplicated attribute") + })?; + } + } + other.custom_resolvers + }, }) } @@ -174,6 +208,15 @@ struct UnionVariantMeta { /// /// [1]: https://spec.graphql.org/June2018/#sec-Unions pub ignore: Option>, + + /// Explicitly specified custom resolver function for this [GraphQL union][1] variant. + /// + /// If absent, then macro will generate the code which just returns the variant inner value. + /// Usually, specifying a custom resolver function has sense, when some custom resolving logic + /// is involved. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + pub custom_resolver: Option>, } impl Parse for UnionVariantMeta { @@ -187,6 +230,14 @@ impl Parse for UnionVariantMeta { .ignore .replace(SpanContainer::new(ident.span(), None, ident.clone())) .none_or_else(|_| syn::Error::new(ident.span(), "duplicated attribute"))?, + "with" => { + input.parse::()?; + let rslvr = input.parse::()?; + output + .custom_resolver + .replace(SpanContainer::new(ident.span(), Some(rslvr.span()), rslvr)) + .none_or_else(|_| syn::Error::new(ident.span(), "duplicated attribute"))? + } _ => { return Err(syn::Error::new(ident.span(), "unknown attribute")); } @@ -213,6 +264,14 @@ impl UnionVariantMeta { } other.ignore }, + custom_resolver: { + if let Some(v) = self.custom_resolver { + other.custom_resolver.replace(v).none_or_else(|dup| { + syn::Error::new(dup.span_ident(), "duplicated attribute") + })?; + } + other.custom_resolver + }, }) } diff --git a/juniper_codegen/src/util/span_container.rs b/juniper_codegen/src/util/span_container.rs index d449fbb71..f335da9fe 100644 --- a/juniper_codegen/src/util/span_container.rs +++ b/juniper_codegen/src/util/span_container.rs @@ -1,4 +1,7 @@ -use std::ops; +use std::{ + hash::{Hash, Hasher}, + ops, +}; use proc_macro2::{Span, TokenStream}; use quote::ToTokens; @@ -25,6 +28,19 @@ impl SpanContainer { self.ident } + pub fn span_joined(&self) -> Span { + if let Some(s) = self.expr { + // TODO: Use `Span::join` once stabilized and available on stable: + // https://github.com/rust-lang/rust/issues/54725 + // self.ident.join(s).unwrap() + + // At the moment, just return the second, more meaningful part. + s + } else { + self.ident + } + } + pub fn into_inner(self) -> T { self.val } @@ -69,3 +85,12 @@ impl PartialEq for SpanContainer { &self.val == other } } + +impl Hash for SpanContainer { + fn hash(&self, state: &mut H) + where + H: Hasher, + { + self.val.hash(state) + } +} From fe24eeebba30b06e1f097626b7dfc371274b2536 Mon Sep 17 00:00:00 2001 From: tyranron Date: Mon, 25 May 2020 13:54:39 +0300 Subject: [PATCH 04/29] Check duplicate custom resolvers between enum variants and whole enum type --- .../juniper_tests/src/codegen/derive_union.rs | 27 +++++++--- juniper_codegen/src/graphql_union/derive.rs | 52 +++++++++++++------ juniper_codegen/src/util/mode.rs | 2 +- 3 files changed, 58 insertions(+), 23 deletions(-) diff --git a/integration_tests/juniper_tests/src/codegen/derive_union.rs b/integration_tests/juniper_tests/src/codegen/derive_union.rs index 39a586ea2..dfaf51843 100644 --- a/integration_tests/juniper_tests/src/codegen/derive_union.rs +++ b/integration_tests/juniper_tests/src/codegen/derive_union.rs @@ -30,7 +30,7 @@ pub enum Character { #[derive(juniper::GraphQLUnion)] #[graphql(Scalar = juniper::DefaultScalarValue)] -pub enum CharacterGeneric { +pub enum CharacterWithGeneric { One(Human), Two(Droid), #[allow(dead_code)] @@ -39,15 +39,30 @@ pub enum CharacterGeneric { } #[derive(juniper::GraphQLUnion)] -#[graphql(on Droid = CharacterDyn::as_droid)] -pub enum CharacterDyn { +#[graphql(on Droid = CharacterCustomFn::as_droid)] +pub enum CharacterCustomFn { One(Human), - //#[graphql(ignore)] - #[graphql(with = CharacterDyn::as_droid)] + #[graphql(ignore)] + Two(Droid, usize, u8), +} + +impl CharacterCustomFn { + fn as_droid(&self, _: &()) -> Option<&Droid> { + match self { + Self::Two(droid, _, _) => Some(droid), + _ => None, + } + } +} + +#[derive(juniper::GraphQLUnion)] +pub enum CharacterCustomVariantFn { + One(Human), + #[graphql(with = CharacterCustomVariantFn::as_droid)] Two(Droid), } -impl CharacterDyn { +impl CharacterCustomVariantFn { fn as_droid(&self, _: &()) -> Option<&Droid> { match self { Self::Two(droid) => Some(droid), diff --git a/juniper_codegen/src/graphql_union/derive.rs b/juniper_codegen/src/graphql_union/derive.rs index 847220405..f6db1af39 100644 --- a/juniper_codegen/src/graphql_union/derive.rs +++ b/juniper_codegen/src/graphql_union/derive.rs @@ -1,6 +1,6 @@ use proc_macro2::{Span, TokenStream}; use proc_macro_error::ResultExt as _; -use quote::quote; +use quote::{quote, ToTokens as _}; use syn::{self, ext::IdentExt, parse_quote, spanned::Spanned as _, Data, Fields}; use crate::{ @@ -38,7 +38,8 @@ fn expand_enum(ast: syn::DeriveInput, mode: Mode) -> syn::Result syn::Result unreachable!(), } .into_iter() - .filter_map(|var| graphql_union_variant_from_enum_variant(var, &enum_ident)) + .filter_map(|var| graphql_union_variant_from_enum_variant(var, &enum_ident, &meta, mode)) .collect(); if !meta.custom_resolvers.is_empty() { let crate_path = mode.crate_path(); @@ -118,6 +119,8 @@ fn expand_enum(ast: syn::DeriveInput, mode: Mode) -> syn::Result Option { let meta = UnionVariantMeta::from_attrs(&var.attrs) .map_err(|e| proc_macro_error::emit_error!(e)) @@ -128,19 +131,6 @@ fn graphql_union_variant_from_enum_variant( let var_span = var.span(); let var_ident = var.ident; - let enum_path = quote! { #enum_ident::#var_ident }; - - // TODO - if meta.custom_resolver.is_some() { - unimplemented!() - } - - let resolver_code = parse_quote! { - match self { #enum_ident::#var_ident(ref v) => Some(v), _ => None, } - }; - let resolver_check = parse_quote! { - matches!(self, #enum_path(_)) - }; let ty = match var.fields { Fields::Unnamed(fields) => { @@ -162,6 +152,36 @@ fn graphql_union_variant_from_enum_variant( }) .ok()?; + let enum_path = quote! { #enum_ident::#var_ident }; + + let resolver_code = if let Some(rslvr) = meta.custom_resolver { + if let Some(other) = enum_meta.custom_resolvers.get(&ty) { + SCOPE.custom( + rslvr.span_ident(), + format!( + "variant `{}` already has custom resolver `{}` declared on the enum", + ty.to_token_stream(), + other.to_token_stream(), + ), + ); + } + + let crate_path = mode.crate_path(); + let resolver_fn = rslvr.into_inner(); + + parse_quote! { + #resolver_fn(self, #crate_path::FromContext::from(context)) + } + } else { + parse_quote! { + match self { #enum_ident::#var_ident(ref v) => Some(v), _ => None, } + } + }; + + let resolver_check = parse_quote! { + matches!(self, #enum_path(_)) + }; + Some(UnionVariantDefinition { ty, resolver_code, diff --git a/juniper_codegen/src/util/mode.rs b/juniper_codegen/src/util/mode.rs index 601f7a9cd..c236309bf 100644 --- a/juniper_codegen/src/util/mode.rs +++ b/juniper_codegen/src/util/mode.rs @@ -1,7 +1,7 @@ //! Code generation mode. /// Code generation mode for macros. -#[derive(Debug)] +#[derive(Clone, Copy, Debug)] pub enum Mode { /// Generated code is intended to be used by library users. Public, From a59964ebc7da61069040cccae3acff972e645e0c Mon Sep 17 00:00:00 2001 From: tyranron Date: Mon, 25 May 2020 14:20:05 +0300 Subject: [PATCH 05/29] Remove From impls generation in favor of derive_more usage --- integration_tests/juniper_tests/Cargo.toml | 5 +++-- .../juniper_tests/src/codegen/derive_union.rs | 20 ++++++++++--------- juniper_codegen/src/graphql_union/derive.rs | 14 ------------- 3 files changed, 14 insertions(+), 25 deletions(-) diff --git a/integration_tests/juniper_tests/Cargo.toml b/integration_tests/juniper_tests/Cargo.toml index 719018127..893571478 100644 --- a/integration_tests/juniper_tests/Cargo.toml +++ b/integration_tests/juniper_tests/Cargo.toml @@ -1,12 +1,13 @@ [package] name = "juniper_tests" version = "0.1.0" -publish = false edition = "2018" +publish = false [dependencies] -juniper = { path = "../../juniper" } +derive_more = "0.99.7" futures = "0.3.1" +juniper = { path = "../../juniper" } [dev-dependencies] serde_json = { version = "1" } diff --git a/integration_tests/juniper_tests/src/codegen/derive_union.rs b/integration_tests/juniper_tests/src/codegen/derive_union.rs index dfaf51843..a0d0ea750 100644 --- a/integration_tests/juniper_tests/src/codegen/derive_union.rs +++ b/integration_tests/juniper_tests/src/codegen/derive_union.rs @@ -1,7 +1,9 @@ // Test for union's derive macro +use derive_more::From; #[cfg(test)] use fnv::FnvHashMap; +use juniper::GraphQLUnion; #[cfg(test)] use juniper::{ @@ -21,14 +23,14 @@ pub struct Droid { primary_function: String, } -#[derive(juniper::GraphQLUnion)] +#[derive(GraphQLUnion)] #[graphql(description = "A Collection of things")] pub enum Character { One(Human), Two(Droid), } -#[derive(juniper::GraphQLUnion)] +#[derive(GraphQLUnion)] #[graphql(Scalar = juniper::DefaultScalarValue)] pub enum CharacterWithGeneric { One(Human), @@ -38,7 +40,7 @@ pub enum CharacterWithGeneric { Hidden(T), } -#[derive(juniper::GraphQLUnion)] +#[derive(GraphQLUnion)] #[graphql(on Droid = CharacterCustomFn::as_droid)] pub enum CharacterCustomFn { One(Human), @@ -55,7 +57,7 @@ impl CharacterCustomFn { } } -#[derive(juniper::GraphQLUnion)] +#[derive(GraphQLUnion)] pub enum CharacterCustomVariantFn { One(Human), #[graphql(with = CharacterCustomVariantFn::as_droid)] @@ -94,7 +96,7 @@ pub struct DroidContext { } /// A Collection of things -#[derive(juniper::GraphQLUnion)] +#[derive(From, GraphQLUnion)] #[graphql(Context = CustomContext)] pub enum CharacterContext { One(HumanContext), @@ -135,7 +137,7 @@ impl DroidCompat { } } -#[derive(juniper::GraphQLUnion)] +#[derive(GraphQLUnion)] #[graphql(Context = CustomContext)] pub enum DifferentContext { A(DroidContext), @@ -143,15 +145,15 @@ pub enum DifferentContext { } // NOTICE: this can not compile due to generic implementation of GraphQLType<__S> -// #[derive(juniper::GraphQLUnion)] +// #[derive(GraphQLUnion)] // pub enum CharacterCompatFail { // One(HumanCompat), // Two(DroidCompat), // } /// A Collection of things -#[derive(juniper::GraphQLUnion)] -#[graphql(Scalar = juniper::DefaultScalarValue)] +#[derive(GraphQLUnion)] +#[graphql(scalar = juniper::DefaultScalarValue)] pub enum CharacterCompat { One(HumanCompat), Two(DroidCompat), diff --git a/juniper_codegen/src/graphql_union/derive.rs b/juniper_codegen/src/graphql_union/derive.rs index f6db1af39..c18dfddbf 100644 --- a/juniper_codegen/src/graphql_union/derive.rs +++ b/juniper_codegen/src/graphql_union/derive.rs @@ -401,19 +401,6 @@ impl UnionDefinition { } }; - let conversion_impls = self.variants.iter().filter_map(|var| { - let var_ty = &var.ty; - let var_path = var.enum_path.as_ref()?; - Some(quote! { - #[automatically_derived] - impl#impl_generics ::std::convert::From<#var_ty> for #ty#ty_generics { - fn from(v: #var_ty) -> Self { - #var_path(v) - } - } - }) - }); - let output_type_impl = quote! { #[automatically_derived] impl#ext_impl_generics #crate_path::marker::IsOutputType<#scalar> for #ty#ty_generics @@ -437,7 +424,6 @@ impl UnionDefinition { }; quote! { - #( #conversion_impls )* #union_impl #output_type_impl #type_impl From 8e88d2cbcb81adfb70f03435e0e2cde0f49950df Mon Sep 17 00:00:00 2001 From: tyranron Date: Mon, 25 May 2020 15:43:39 +0300 Subject: [PATCH 06/29] Support structs --- .../juniper_tests/src/codegen/derive_union.rs | 28 ++ juniper_codegen/src/graphql_union/derive.rs | 321 +++++------------- juniper_codegen/src/graphql_union/mod.rs | 246 +++++++++++++- 3 files changed, 356 insertions(+), 239 deletions(-) diff --git a/integration_tests/juniper_tests/src/codegen/derive_union.rs b/integration_tests/juniper_tests/src/codegen/derive_union.rs index a0d0ea750..0fc682f9b 100644 --- a/integration_tests/juniper_tests/src/codegen/derive_union.rs +++ b/integration_tests/juniper_tests/src/codegen/derive_union.rs @@ -73,6 +73,34 @@ impl CharacterCustomVariantFn { } } +#[derive(GraphQLUnion)] +#[graphql(on Human = CharacterGenericStruct::as_human)] +#[graphql(on Droid = CharacterGenericStruct::as_droid)] +pub struct CharacterGenericStruct { + human: Human, + droid: Droid, + is_droid: bool, + _gen: T, +} + +impl CharacterGenericStruct { + fn as_human(&self, _: &()) -> Option<&Human> { + if self.is_droid { + None + } else { + Some(&self.human) + } + } + + fn as_droid(&self, _: &()) -> Option<&Droid> { + if self.is_droid { + Some(&self.droid) + } else { + None + } + } +} + // Context Test pub struct CustomContext { diff --git a/juniper_codegen/src/graphql_union/derive.rs b/juniper_codegen/src/graphql_union/derive.rs index c18dfddbf..26495b41a 100644 --- a/juniper_codegen/src/graphql_union/derive.rs +++ b/juniper_codegen/src/graphql_union/derive.rs @@ -1,4 +1,4 @@ -use proc_macro2::{Span, TokenStream}; +use proc_macro2::TokenStream; use proc_macro_error::ResultExt as _; use quote::{quote, ToTokens as _}; use syn::{self, ext::IdentExt, parse_quote, spanned::Spanned as _, Data, Fields}; @@ -8,7 +8,7 @@ use crate::{ util::{span_container::SpanContainer, Mode}, }; -use super::{UnionMeta, UnionVariantMeta}; +use super::{UnionDefinition, UnionMeta, UnionVariantDefinition, UnionVariantMeta}; const SCOPE: GraphQLScope = GraphQLScope::DeriveUnion; @@ -18,7 +18,7 @@ pub fn expand(input: TokenStream, mode: Mode) -> syn::Result { match &ast.data { Data::Enum(_) => expand_enum(ast, mode), - Data::Struct(_) => unimplemented!(), // TODO + Data::Struct(_) => expand_struct(ast, mode), _ => Err(SCOPE.custom_error(ast.span(), "can only be applied to enums and structs")), } .map(UnionDefinition::into_tokens) @@ -85,7 +85,7 @@ fn expand_enum(ast: syn::DeriveInput, mode: Mode) -> syn::Result syn::Result, - pub span: Span, -} +fn expand_struct(ast: syn::DeriveInput, mode: Mode) -> syn::Result { + let meta = UnionMeta::from_attrs(&ast.attrs)?; -struct UnionDefinition { - pub name: String, - pub ty: syn::Type, - pub description: Option, - pub context: Option, - pub scalar: Option, - pub generics: syn::Generics, - pub variants: Vec, - pub span: Span, - pub mode: Mode, -} + let struct_span = ast.span(); + let struct_ident = ast.ident; -impl UnionDefinition { - pub fn into_tokens(self) -> TokenStream { - let crate_path = self.mode.crate_path(); - - let name = &self.name; - let ty = &self.ty; - - let context = self - .context - .as_ref() - .map(|ctx| quote! { #ctx }) - .unwrap_or_else(|| quote! { () }); - - let scalar = self - .scalar - .as_ref() - .map(|scl| quote! { #scl }) - .unwrap_or_else(|| quote! { __S }); - let default_scalar = self - .scalar - .as_ref() - .map(|scl| quote! { #scl }) - .unwrap_or_else(|| quote! { #crate_path::DefaultScalarValue }); - - let description = self - .description - .as_ref() - .map(|desc| quote! { .description(#desc) }); - - let var_types: Vec<_> = self.variants.iter().map(|var| &var.ty).collect(); - - let match_names = self.variants.iter().map(|var| { - let var_ty = &var.ty; - let var_check = &var.resolver_check; - quote! { - if #var_check { - return <#var_ty as #crate_path::GraphQLType<#scalar>>::name(&()) - .unwrap().to_string(); - } - } - }); - - let match_resolves: Vec<_> = self.variants.iter().map(|var| &var.resolver_code).collect(); - let resolve_into_type = self.variants.iter().zip(match_resolves.iter()).map(|(var, expr)| { - let var_ty = &var.ty; - - let get_name = quote! { (<#var_ty as #crate_path::GraphQLType<#scalar>>::name(&())) }; - quote! { - if type_name == #get_name.unwrap() { - return #crate_path::IntoResolvable::into( - { #expr }, - executor.context() - ) - .and_then(|res| match res { - Some((ctx, r)) => executor.replaced_context(ctx).resolve_with_ctx(&(), &r), - None => Ok(#crate_path::Value::null()), - }); - } - } - }); - let resolve_into_type_async = - self.variants - .iter() - .zip(match_resolves.iter()) - .map(|(var, expr)| { - let var_ty = &var.ty; - - let get_name = - quote! { (<#var_ty as #crate_path::GraphQLType<#scalar>>::name(&())) }; - quote! { - if type_name == #get_name.unwrap() { - let res = #crate_path::IntoResolvable::into( - { #expr }, - executor.context() - ); - return #crate_path::futures::future::FutureExt::boxed(async move { - match res? { - Some((ctx, r)) => { - let subexec = executor.replaced_context(ctx); - subexec.resolve_with_ctx_async(&(), &r).await - }, - None => Ok(#crate_path::Value::null()), - } - }); - } - } - }); - - let (impl_generics, ty_generics, _) = self.generics.split_for_impl(); - let mut ext_generics = self.generics.clone(); - if self.scalar.is_none() { - ext_generics.params.push(parse_quote! { #scalar }); - ext_generics - .where_clause - .get_or_insert_with(|| parse_quote! { where }) - .predicates - .push(parse_quote! { #scalar: #crate_path::ScalarValue }); - } - let (ext_impl_generics, _, where_clause) = ext_generics.split_for_impl(); - - let mut where_async = where_clause - .cloned() - .unwrap_or_else(|| parse_quote! { where }); - where_async - .predicates - .push(parse_quote! { Self: Send + Sync }); - if self.scalar.is_none() { - where_async - .predicates - .push(parse_quote! { #scalar: Send + Sync }); - } + let name = meta + .name + .clone() + .map(SpanContainer::into_inner) + .unwrap_or_else(|| struct_ident.unraw().to_string()); // TODO: PascalCase + if matches!(mode, Mode::Public) && name.starts_with("__") { + SCOPE.no_double_underscore( + meta.name + .as_ref() + .map(SpanContainer::span_ident) + .unwrap_or_else(|| struct_ident.span()), + ); + } - let type_impl = quote! { - #[automatically_derived] - impl#ext_impl_generics #crate_path::GraphQLType<#scalar> for #ty#ty_generics - #where_clause - { - type Context = #context; - type TypeInfo = (); - - fn name(_ : &Self::TypeInfo) -> Option<&str> { - Some(#name) - } - - fn meta<'r>( - info: &Self::TypeInfo, - registry: &mut #crate_path::Registry<'r, #scalar> - ) -> #crate_path::meta::MetaType<'r, #scalar> - where #scalar: 'r, - { - let types = &[ - #( registry.get_type::<&#var_types>(&(())), )* - ]; - registry.build_union_type::<#ty#ty_generics>(info, types) - #description - .into_meta() - } - - fn concrete_type_name( - &self, - context: &Self::Context, - _: &Self::TypeInfo, - ) -> String { - #( #match_names )* - panic!( - "GraphQL union {} cannot be resolved into any of its variants in its \ - current state", - #name, - ); - } - - fn resolve_into_type( - &self, - _: &Self::TypeInfo, - type_name: &str, - _: Option<&[#crate_path::Selection<#scalar>]>, - executor: &#crate_path::Executor, - ) -> #crate_path::ExecutionResult<#scalar> { - let context = executor.context(); - #( #resolve_into_type )* - panic!( - "Concrete type {} is not handled by instance resolvers on GraphQL union {}", - type_name, #name, - ); - } - } - }; - - let async_type_impl = quote! { - #[automatically_derived] - impl#ext_impl_generics #crate_path::GraphQLTypeAsync<#scalar> for #ty#ty_generics - #where_async - { - fn resolve_into_type_async<'b>( - &'b self, - _: &'b Self::TypeInfo, - type_name: &str, - _: Option<&'b [#crate_path::Selection<'b, #scalar>]>, - executor: &'b #crate_path::Executor<'b, 'b, Self::Context, #scalar> - ) -> #crate_path::BoxFuture<'b, #crate_path::ExecutionResult<#scalar>> { - let context = executor.context(); - #( #resolve_into_type_async )* - panic!( - "Concrete type {} is not handled by instance resolvers on GraphQL union {}", - type_name, #name, - ); - } - } - }; - - let output_type_impl = quote! { - #[automatically_derived] - impl#ext_impl_generics #crate_path::marker::IsOutputType<#scalar> for #ty#ty_generics - #where_clause - { - fn mark() { - #( <#var_types as #crate_path::marker::GraphQLObjectType<#scalar>>::mark(); )* - } - } - }; - - let union_impl = quote! { - #[automatically_derived] - impl#impl_generics #crate_path::marker::GraphQLUnion for #ty#ty_generics { - fn mark() { - #( <#var_types as #crate_path::marker::GraphQLObjectType< - #default_scalar, - >>::mark(); )* - } + let crate_path = mode.crate_path(); + let variants: Vec<_> = meta + .custom_resolvers + .into_iter() + .map(|(ty, rslvr)| { + let span = rslvr.span_joined(); + + let resolver_fn = rslvr.into_inner(); + let resolver_code = parse_quote! { + #resolver_fn(self, #crate_path::FromContext::from(context)) + }; + // Doing this may be quite an expensive, because resolving may contain some heavy + // computation, so we're preforming it twice. Unfortunately, we have no other options + // here, until the `juniper::GraphQLType` itself will allow to do it in some cleverer + // way. + let resolver_check = parse_quote! { + ({ #resolver_code } as ::std::option::Option<&#ty>).is_some() + }; + + UnionVariantDefinition { + ty, + resolver_code, + resolver_check, + enum_path: None, + span, } - }; + }) + .collect(); + if variants.is_empty() { + SCOPE.custom(struct_span, "expects at least one union variant"); + } - quote! { - #union_impl - #output_type_impl - #type_impl - #async_type_impl - } + // NOTICE: This is not an optimal implementation, as it's possible to bypass this check by using + // a full qualified path instead (`crate::Test` vs `Test`). Since this requirement is mandatory, + // the `std::convert::Into` implementation is used to enforce this requirement. However, due + // to the bad error message this implementation should stay and provide guidance. + let all_variants_different = { + let mut types: Vec<_> = variants.iter().map(|var| &var.ty).collect(); + types.dedup(); + types.len() == variants.len() + }; + if !all_variants_different { + SCOPE.custom( + struct_ident.span(), + "each union variant must have a different type", + ); } + + proc_macro_error::abort_if_dirty(); + + Ok(UnionDefinition { + name, + ty: syn::parse_str(&struct_ident.to_string()).unwrap_or_abort(), + description: meta.description.map(SpanContainer::into_inner), + context: meta.context.map(SpanContainer::into_inner), + scalar: meta.scalar.map(SpanContainer::into_inner), + generics: ast.generics, + variants, + span: struct_span, + mode, + }) } diff --git a/juniper_codegen/src/graphql_union/mod.rs b/juniper_codegen/src/graphql_union/mod.rs index c1b76fde0..3594a0b37 100644 --- a/juniper_codegen/src/graphql_union/mod.rs +++ b/juniper_codegen/src/graphql_union/mod.rs @@ -3,13 +3,16 @@ pub mod derive; use std::collections::HashMap; +use proc_macro2::{Span, TokenStream}; +use quote::quote; use syn::{ parse::{Parse, ParseStream}, + parse_quote, spanned::Spanned as _, }; use crate::util::{ - filter_graphql_attrs, get_doc_comment, span_container::SpanContainer, OptionExt as _, + filter_graphql_attrs, get_doc_comment, span_container::SpanContainer, Mode, OptionExt as _, }; /// Available metadata behind `#[graphql]` (or `#[graphql_union]`) attribute when generating code @@ -282,3 +285,244 @@ impl UnionVariantMeta { .try_fold(Self::default(), |prev, curr| prev.try_merge(curr?)) } } + +struct UnionVariantDefinition { + pub ty: syn::Type, + pub resolver_code: syn::Expr, + pub resolver_check: syn::Expr, + pub enum_path: Option, + pub span: Span, +} + +struct UnionDefinition { + pub name: String, + pub ty: syn::Type, + pub description: Option, + pub context: Option, + pub scalar: Option, + pub generics: syn::Generics, + pub variants: Vec, + pub span: Span, + pub mode: Mode, +} + +impl UnionDefinition { + pub fn into_tokens(self) -> TokenStream { + let crate_path = self.mode.crate_path(); + + let name = &self.name; + let ty = &self.ty; + + let context = self + .context + .as_ref() + .map(|ctx| quote! { #ctx }) + .unwrap_or_else(|| quote! { () }); + + let scalar = self + .scalar + .as_ref() + .map(|scl| quote! { #scl }) + .unwrap_or_else(|| quote! { __S }); + let default_scalar = self + .scalar + .as_ref() + .map(|scl| quote! { #scl }) + .unwrap_or_else(|| quote! { #crate_path::DefaultScalarValue }); + + let description = self + .description + .as_ref() + .map(|desc| quote! { .description(#desc) }); + + let var_types: Vec<_> = self.variants.iter().map(|var| &var.ty).collect(); + + let match_names = self.variants.iter().map(|var| { + let var_ty = &var.ty; + let var_check = &var.resolver_check; + quote! { + if #var_check { + return <#var_ty as #crate_path::GraphQLType<#scalar>>::name(&()) + .unwrap().to_string(); + } + } + }); + + let match_resolves: Vec<_> = self.variants.iter().map(|var| &var.resolver_code).collect(); + let resolve_into_type = self.variants.iter().zip(match_resolves.iter()).map(|(var, expr)| { + let var_ty = &var.ty; + + let get_name = quote! { (<#var_ty as #crate_path::GraphQLType<#scalar>>::name(&())) }; + quote! { + if type_name == #get_name.unwrap() { + return #crate_path::IntoResolvable::into( + { #expr }, + executor.context() + ) + .and_then(|res| match res { + Some((ctx, r)) => executor.replaced_context(ctx).resolve_with_ctx(&(), &r), + None => Ok(#crate_path::Value::null()), + }); + } + } + }); + let resolve_into_type_async = + self.variants + .iter() + .zip(match_resolves.iter()) + .map(|(var, expr)| { + let var_ty = &var.ty; + + let get_name = + quote! { (<#var_ty as #crate_path::GraphQLType<#scalar>>::name(&())) }; + quote! { + if type_name == #get_name.unwrap() { + let res = #crate_path::IntoResolvable::into( + { #expr }, + executor.context() + ); + return #crate_path::futures::future::FutureExt::boxed(async move { + match res? { + Some((ctx, r)) => { + let subexec = executor.replaced_context(ctx); + subexec.resolve_with_ctx_async(&(), &r).await + }, + None => Ok(#crate_path::Value::null()), + } + }); + } + } + }); + + let (impl_generics, ty_generics, _) = self.generics.split_for_impl(); + let mut ext_generics = self.generics.clone(); + if self.scalar.is_none() { + ext_generics.params.push(parse_quote! { #scalar }); + ext_generics + .where_clause + .get_or_insert_with(|| parse_quote! { where }) + .predicates + .push(parse_quote! { #scalar: #crate_path::ScalarValue }); + } + let (ext_impl_generics, _, where_clause) = ext_generics.split_for_impl(); + + let mut where_async = where_clause + .cloned() + .unwrap_or_else(|| parse_quote! { where }); + where_async + .predicates + .push(parse_quote! { Self: Send + Sync }); + if self.scalar.is_none() { + where_async + .predicates + .push(parse_quote! { #scalar: Send + Sync }); + } + + let type_impl = quote! { + #[automatically_derived] + impl#ext_impl_generics #crate_path::GraphQLType<#scalar> for #ty#ty_generics + #where_clause + { + type Context = #context; + type TypeInfo = (); + + fn name(_ : &Self::TypeInfo) -> Option<&str> { + Some(#name) + } + + fn meta<'r>( + info: &Self::TypeInfo, + registry: &mut #crate_path::Registry<'r, #scalar> + ) -> #crate_path::meta::MetaType<'r, #scalar> + where #scalar: 'r, + { + let types = &[ + #( registry.get_type::<&#var_types>(&(())), )* + ]; + registry.build_union_type::<#ty#ty_generics>(info, types) + #description + .into_meta() + } + + fn concrete_type_name( + &self, + context: &Self::Context, + _: &Self::TypeInfo, + ) -> String { + #( #match_names )* + panic!( + "GraphQL union {} cannot be resolved into any of its variants in its \ + current state", + #name, + ); + } + + fn resolve_into_type( + &self, + _: &Self::TypeInfo, + type_name: &str, + _: Option<&[#crate_path::Selection<#scalar>]>, + executor: &#crate_path::Executor, + ) -> #crate_path::ExecutionResult<#scalar> { + let context = executor.context(); + #( #resolve_into_type )* + panic!( + "Concrete type {} is not handled by instance resolvers on GraphQL union {}", + type_name, #name, + ); + } + } + }; + + let async_type_impl = quote! { + #[automatically_derived] + impl#ext_impl_generics #crate_path::GraphQLTypeAsync<#scalar> for #ty#ty_generics + #where_async + { + fn resolve_into_type_async<'b>( + &'b self, + _: &'b Self::TypeInfo, + type_name: &str, + _: Option<&'b [#crate_path::Selection<'b, #scalar>]>, + executor: &'b #crate_path::Executor<'b, 'b, Self::Context, #scalar> + ) -> #crate_path::BoxFuture<'b, #crate_path::ExecutionResult<#scalar>> { + let context = executor.context(); + #( #resolve_into_type_async )* + panic!( + "Concrete type {} is not handled by instance resolvers on GraphQL union {}", + type_name, #name, + ); + } + } + }; + + let output_type_impl = quote! { + #[automatically_derived] + impl#ext_impl_generics #crate_path::marker::IsOutputType<#scalar> for #ty#ty_generics + #where_clause + { + fn mark() { + #( <#var_types as #crate_path::marker::GraphQLObjectType<#scalar>>::mark(); )* + } + } + }; + + let union_impl = quote! { + #[automatically_derived] + impl#impl_generics #crate_path::marker::GraphQLUnion for #ty#ty_generics { + fn mark() { + #( <#var_types as #crate_path::marker::GraphQLObjectType< + #default_scalar, + >>::mark(); )* + } + } + }; + + quote! { + #union_impl + #output_type_impl + #type_impl + #async_type_impl + } + } +} From 92c4e0705cb5ad676a0c75d80e4564017e4255e3 Mon Sep 17 00:00:00 2001 From: tyranron Date: Tue, 26 May 2020 18:46:46 +0300 Subject: [PATCH 07/29] Bootstrap #[graphql_union] for traits --- .../juniper_tests/src/codegen/impl_union.rs | 104 ------- .../juniper_tests/src/codegen/mod.rs | 4 +- .../juniper_tests/src/codegen/union_attr.rs | 49 +++ .../{derive_union.rs => union_derive.rs} | 0 juniper_codegen/src/graphql_union/attr.rs | 284 ++++++++++++++++++ .../src/graphql_union/attribute.rs | 1 - juniper_codegen/src/graphql_union/derive.rs | 22 +- juniper_codegen/src/graphql_union/mod.rs | 2 +- juniper_codegen/src/lib.rs | 10 +- juniper_codegen/src/result.rs | 21 +- 10 files changed, 364 insertions(+), 133 deletions(-) delete mode 100644 integration_tests/juniper_tests/src/codegen/impl_union.rs create mode 100644 integration_tests/juniper_tests/src/codegen/union_attr.rs rename integration_tests/juniper_tests/src/codegen/{derive_union.rs => union_derive.rs} (100%) create mode 100644 juniper_codegen/src/graphql_union/attr.rs delete mode 100644 juniper_codegen/src/graphql_union/attribute.rs diff --git a/integration_tests/juniper_tests/src/codegen/impl_union.rs b/integration_tests/juniper_tests/src/codegen/impl_union.rs deleted file mode 100644 index 527230a3e..000000000 --- a/integration_tests/juniper_tests/src/codegen/impl_union.rs +++ /dev/null @@ -1,104 +0,0 @@ -// Trait. - -#[derive(juniper::GraphQLObject)] -struct Human { - id: String, - home_planet: String, -} - -#[derive(juniper::GraphQLObject)] -struct Droid { - id: String, - primary_function: String, -} - -trait Character { - fn as_human(&self) -> Option<&Human> { - None - } - fn as_droid(&self) -> Option<&Droid> { - None - } -} - -impl Character for Human { - fn as_human(&self) -> Option<&Human> { - Some(&self) - } -} - -impl Character for Droid { - fn as_droid(&self) -> Option<&Droid> { - Some(&self) - } -} - -#[juniper::graphql_union] -impl<'a> GraphQLUnion for &'a dyn Character { - fn resolve(&self) { - match self { - Human => self.as_human(), - Droid => self.as_droid(), - } - } -} - -/* -#[juniper::graphql_union] -impl GraphQLUnion for dyn Character { - fn resolve_human(&self) -> Option<&Human> { - self.as_human() - } - - fn resolve_droid(&self) -> Option<&Droid> { - self.as_droid() - } -} -*/ - -/* -#[derive(GraphQLUnion)] -#[graphql( - Human = Char::resolve_human, - Droid = Char::resolve_droid, -)] -#[graphql(with(Char::resolve_human) => Human)] -#[graphql(object = Droid, with = Char::resolve_droid)] -struct Char { - id: String, -} - -impl Char { - fn resolve_human(&self, _: &Context) -> Option<&Human> { - unimplemented!() - } - fn resolve_droid(&self, _: &Context) -> Option<&Droid> { - unimplemented!() - } -} - -#[graphq_union] -trait Charctr { - fn as_human(&self) -> Option<&Human> { None } - fn as_droid(&self, _: &Context) -> Option<&Droid> { None } -} - -#[graphq_union( - Human = Char::resolve_human, - Droid = Char::resolve_droid, -)] -#[graphql(object = Human, with = Charctr2::resolve_human)] -#[graphql(object = Droid, with = Charctr2::resolve_droid)] -trait Charctr2 { - fn id(&self) -> &str; -} - -impl dyn Charctr2 { - fn resolve_human(&self, _: &Context) -> Option<&Human> { - unimplemented!() - } - fn resolve_droid(&self, _: &Context) -> Option<&Droid> { - unimplemented!() - } -} -*/ \ No newline at end of file diff --git a/integration_tests/juniper_tests/src/codegen/mod.rs b/integration_tests/juniper_tests/src/codegen/mod.rs index f2bd68dc0..c3d846c45 100644 --- a/integration_tests/juniper_tests/src/codegen/mod.rs +++ b/integration_tests/juniper_tests/src/codegen/mod.rs @@ -2,8 +2,8 @@ mod derive_enum; mod derive_input_object; mod derive_object; mod derive_object_with_raw_idents; -mod derive_union; +mod union_derive; mod impl_object; mod impl_scalar; -mod impl_union; +mod union_attr; mod scalar_value_transparent; diff --git a/integration_tests/juniper_tests/src/codegen/union_attr.rs b/integration_tests/juniper_tests/src/codegen/union_attr.rs new file mode 100644 index 000000000..45cd9827d --- /dev/null +++ b/integration_tests/juniper_tests/src/codegen/union_attr.rs @@ -0,0 +1,49 @@ +use juniper::{GraphQLObject, graphql_union}; + +#[derive(GraphQLObject)] +struct Human { + id: String, + home_planet: String, +} + +#[derive(GraphQLObject)] +struct Droid { + id: String, + primary_function: String, +} + +#[graphql_union] +#[graphql(description = "A Collection of things")] +trait Character { + fn as_human(&self, _: &()) -> Option<&Human> { + None + } + fn as_droid(&self) -> Option<&Droid> { + None + } +} + + +/* +impl Character for Human { + fn as_human(&self) -> Option<&Human> { + Some(&self) + } +} + +impl Character for Droid { + fn as_droid(&self) -> Option<&Droid> { + Some(&self) + } +} + +#[juniper::graphql_union] +impl<'a> GraphQLUnion for &'a dyn Character { + fn resolve(&self) { + match self { + Human => self.as_human(), + Droid => self.as_droid(), + } + } +} +*/ \ No newline at end of file diff --git a/integration_tests/juniper_tests/src/codegen/derive_union.rs b/integration_tests/juniper_tests/src/codegen/union_derive.rs similarity index 100% rename from integration_tests/juniper_tests/src/codegen/derive_union.rs rename to integration_tests/juniper_tests/src/codegen/union_derive.rs diff --git a/juniper_codegen/src/graphql_union/attr.rs b/juniper_codegen/src/graphql_union/attr.rs new file mode 100644 index 000000000..7378b039e --- /dev/null +++ b/juniper_codegen/src/graphql_union/attr.rs @@ -0,0 +1,284 @@ +use std::ops::Deref as _; + +use proc_macro2::{Span, TokenStream}; +use proc_macro_error::ResultExt as _; +use quote::{quote, ToTokens as _}; +use syn::{self, ext::IdentExt as _, parse_quote, spanned::Spanned as _}; + +use crate::{ + result::GraphQLScope, + util::{span_container::SpanContainer, Mode}, +}; + +use super::{UnionDefinition, UnionMeta, UnionVariantDefinition, UnionVariantMeta}; + +const SCOPE: GraphQLScope = GraphQLScope::AttrUnion; + +/// Expands `#[graphql_union]` macro into generated code. +pub fn expand(attr: TokenStream, body: TokenStream, mode: Mode) -> syn::Result { + if !attr.is_empty() { + return Err(syn::Error::new( + Span::call_site(), + "#[graphql_union] attribute itself does not support any parameters, \ + use helper #[graphql] attributes instead to specify any parameters", + )); + } + + let ast = syn::parse2::(body.clone()).map_err(|_| { + syn::Error::new( + Span::call_site(), + "#[graphql_union] attribute is applicable to trait definitions only", + ) + })?; + + let meta = UnionMeta::from_attrs(&ast.attrs)?; + + let trait_span = ast.span(); + let trait_ident = ast.ident; + + let name = meta + .name + .clone() + .map(SpanContainer::into_inner) + .unwrap_or_else(|| trait_ident.unraw().to_string()); // TODO: PascalCase + if matches!(mode, Mode::Public) && name.starts_with("__") { + SCOPE.no_double_underscore( + meta.name + .as_ref() + .map(SpanContainer::span_ident) + .unwrap_or_else(|| trait_ident.span()), + ); + } + + let mut variants: Vec<_> = ast + .items + .into_iter() + .filter_map(|i| match i { + syn::TraitItem::Method(m) => { + parse_variant_from_trait_method(m, &trait_ident, &meta, mode) + } + _ => None, + }) + .collect(); + + proc_macro_error::abort_if_dirty(); + + if !meta.custom_resolvers.is_empty() { + let crate_path = mode.crate_path(); + // TODO: modify variants + } + if variants.is_empty() { + SCOPE.custom(trait_span, "expects at least one union variant"); + } + + // NOTICE: This is not an optimal implementation, as it's possible to bypass this check by using + // a full qualified path instead (`crate::Test` vs `Test`). Since this requirement is mandatory, + // the `std::convert::Into` implementation is used to enforce this requirement. However, due + // to the bad error message this implementation should stay and provide guidance. + let all_variants_different = { + let mut types: Vec<_> = variants.iter().map(|var| &var.ty).collect(); + types.dedup(); + types.len() == variants.len() + }; + if !all_variants_different { + SCOPE.custom(trait_span, "each union variant must have a different type"); + } + + proc_macro_error::abort_if_dirty(); + + let generated_code = UnionDefinition { + name, + ty: syn::parse_str(&trait_ident.to_string()).unwrap_or_abort(), // TODO: trait object + description: meta.description.map(SpanContainer::into_inner), + context: meta.context.map(SpanContainer::into_inner), + scalar: meta.scalar.map(SpanContainer::into_inner), + generics: ast.generics, + variants, + span: trait_span, + mode, + } + .into_tokens(); + + Ok(quote! { + #body + + #generated_code + }) +} + +fn parse_variant_from_trait_method( + method: syn::TraitItemMethod, + trait_ident: &syn::Ident, + trait_meta: &UnionMeta, + mode: Mode, +) -> Option { + let meta = UnionVariantMeta::from_attrs(&method.attrs) + .map_err(|e| proc_macro_error::emit_error!(e)) + .ok()?; + if let Some(rslvr) = meta.custom_resolver { + SCOPE.custom( + rslvr.span_ident(), + "cannot use #[graphql(with = ...)] attribute on a trait method, instead use \ + #[graphql(ignore)] on the method with #[graphql(on ... = ...)] on the trait itself", + ) + } + if meta.ignore.is_some() { + return None; + } + + let method_span = method.sig.span(); + let method_ident = &method.sig.ident; + + let ty = parse_trait_method_output_type(&method.sig) + .map_err(|span| { + SCOPE.custom( + span, + "trait method return type can be `Option<&VariantType>` only", + ) + }) + .ok()?; + let accepts_context = parse_trait_method_input_args(&method.sig) + .map_err(|span| { + SCOPE.custom( + span, + "trait method can accept `&self` and optionally `&Context` only", + ) + }) + .ok()?; + // TODO: validate signature to not be async + + let resolver_code = { + if let Some(other) = trait_meta.custom_resolvers.get(&ty) { + SCOPE.custom( + method_span, + format!( + "trait method `{}` conflicts with the custom resolver `{}` declared on the \ + trait to resolve the variant type `{}`, use `#[graphql(ignore)]` attribute to \ + ignore this trait method for union variants resolution", + method_ident, + other.to_token_stream(), + ty.to_token_stream(), + ), + ); + } + + if accepts_context { + let crate_path = mode.crate_path(); + + parse_quote! { + #trait_ident::#method_ident(self, #crate_path::FromContext::from(context)) + } + } else { + parse_quote! { + #trait_ident::#method_ident(self) + } + } + }; + + // Doing this may be quite an expensive, because resolving may contain some heavy + // computation, so we're preforming it twice. Unfortunately, we have no other options + // here, until the `juniper::GraphQLType` itself will allow to do it in some cleverer + // way. + let resolver_check = parse_quote! { + ({ #resolver_code } as ::std::option::Option<&#ty>).is_some() + }; + + Some(UnionVariantDefinition { + ty, + resolver_code, + resolver_check, + enum_path: None, + span: method_span, + }) +} + +/// Parses type of [GraphQL union][1] variant from the return type of trait method. +/// +/// If return type is invalid, then returns the [`Span`] to display the corresponding error at. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Unions +fn parse_trait_method_output_type(sig: &syn::Signature) -> Result { + let ret_ty = match &sig.output { + syn::ReturnType::Type(_, ty) => ty.deref(), + _ => return Err(sig.span()), + }; + + let path = match unparenthesize(ret_ty) { + syn::Type::Path(syn::TypePath { qself: None, path }) => path, + _ => return Err(ret_ty.span()), + }; + + let (ident, args) = match path.segments.last() { + Some(syn::PathSegment { + ident, + arguments: syn::PathArguments::AngleBracketed(generic), + }) => (ident, &generic.args), + _ => return Err(ret_ty.span()), + }; + + if ident.unraw() != "Option" { + return Err(ret_ty.span()); + } + + if args.len() != 1 { + return Err(ret_ty.span()); + } + let var_ty = match args.first() { + Some(syn::GenericArgument::Type(inner_ty)) => match unparenthesize(inner_ty) { + syn::Type::Reference(inner_ty) => { + if inner_ty.mutability.is_some() { + return Err(inner_ty.span()); + } + unparenthesize(inner_ty.elem.deref()).clone() + } + _ => return Err(ret_ty.span()), + }, + _ => return Err(ret_ty.span()), + }; + Ok(var_ty) +} + +/// Parses trait method input arguments and validates them to be acceptable for resolving into +/// [GraphQL union][1] variant type. Indicates whether method accepts context or not. +/// +/// If input arguments are invalid, then returns the [`Span`] to display the corresponding error at. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Unions +fn parse_trait_method_input_args(sig: &syn::Signature) -> Result { + match sig.receiver() { + Some(syn::FnArg::Receiver(rcv)) => { + if rcv.reference.is_none() || rcv.mutability.is_some() { + return Err(rcv.span()); + } + } + _ => return Err(sig.span()), + } + + if sig.inputs.len() > 2 { + return Err(sig.inputs.span()); + } + + let second_arg_ty = match sig.inputs.iter().skip(1).next() { + Some(syn::FnArg::Typed(arg)) => arg.ty.deref(), + None => return Ok(false), + _ => return Err(sig.inputs.span()), + }; + match unparenthesize(second_arg_ty) { + syn::Type::Reference(ref_ty) => { + if ref_ty.mutability.is_some() { + return Err(ref_ty.span()); + } + } + ty => return Err(ty.span()), + } + + Ok(true) +} + +/// Retrieves the innermost non-parenthesized [`syn::Type`] from the given one. +fn unparenthesize(ty: &syn::Type) -> &syn::Type { + match ty { + syn::Type::Paren(ty) => unparenthesize(ty.elem.deref()), + _ => ty, + } +} diff --git a/juniper_codegen/src/graphql_union/attribute.rs b/juniper_codegen/src/graphql_union/attribute.rs deleted file mode 100644 index 8b1378917..000000000 --- a/juniper_codegen/src/graphql_union/attribute.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/juniper_codegen/src/graphql_union/derive.rs b/juniper_codegen/src/graphql_union/derive.rs index 26495b41a..8b4574713 100644 --- a/juniper_codegen/src/graphql_union/derive.rs +++ b/juniper_codegen/src/graphql_union/derive.rs @@ -1,7 +1,7 @@ use proc_macro2::TokenStream; use proc_macro_error::ResultExt as _; use quote::{quote, ToTokens as _}; -use syn::{self, ext::IdentExt, parse_quote, spanned::Spanned as _, Data, Fields}; +use syn::{self, ext::IdentExt as _, parse_quote, spanned::Spanned as _, Data, Fields}; use crate::{ result::GraphQLScope, @@ -49,8 +49,11 @@ fn expand_enum(ast: syn::DeriveInput, mode: Mode) -> syn::Result unreachable!(), } .into_iter() - .filter_map(|var| graphql_union_variant_from_enum_variant(var, &enum_ident, &meta, mode)) + .filter_map(|var| parse_variant_from_enum_variant(var, &enum_ident, &meta, mode)) .collect(); + + proc_macro_error::abort_if_dirty(); + if !meta.custom_resolvers.is_empty() { let crate_path = mode.crate_path(); // TODO: refactor into separate function @@ -98,10 +101,7 @@ fn expand_enum(ast: syn::DeriveInput, mode: Mode) -> syn::Result syn::Result syn::Result syn::Result TokenStream { #[proc_macro_error] #[proc_macro_attribute] -pub fn graphql_union(attrs: TokenStream, body: TokenStream) -> TokenStream { - impl_union::expand(attrs.into(), body.into(), Mode::Public) +pub fn graphql_union(attr: TokenStream, body: TokenStream) -> TokenStream { + //impl_union::expand(attr.into(), body.into(), Mode::Public) + graphql_union::attr::expand(attr.into(), body.into(), Mode::Public) .unwrap_or_abort() .into() } @@ -578,8 +579,9 @@ pub fn graphql_union(attrs: TokenStream, body: TokenStream) -> TokenStream { #[proc_macro_error] #[proc_macro_attribute] #[doc(hidden)] -pub fn graphql_union_internal(attrs: TokenStream, body: TokenStream) -> TokenStream { - impl_union::expand(attrs.into(), body.into(), Mode::Internal) +pub fn graphql_union_internal(attr: TokenStream, body: TokenStream) -> TokenStream { + //impl_union::expand(attr.into(), body.into(), Mode::Internal) + graphql_union::attr::expand(attr.into(), body.into(), Mode::Internal) .unwrap_or_abort() .into() } diff --git a/juniper_codegen/src/result.rs b/juniper_codegen/src/result.rs index 988fa3081..754e2c998 100644 --- a/juniper_codegen/src/result.rs +++ b/juniper_codegen/src/result.rs @@ -10,6 +10,7 @@ pub const SPEC_URL: &'static str = "https://spec.graphql.org/June2018/"; #[allow(unused_variables)] pub enum GraphQLScope { + AttrUnion, DeriveObject, DeriveInputObject, DeriveUnion, @@ -23,11 +24,11 @@ pub enum GraphQLScope { impl GraphQLScope { pub fn spec_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", + Self::DeriveObject | Self::ImplObject => "#sec-Objects", + Self::DeriveInputObject => "#sec-Input-Objects", + Self::AttrUnion | Self::DeriveUnion | Self::ImplUnion => "#sec-Unions", + Self::DeriveEnum => "#sec-Enums", + Self::DeriveScalar | Self::ImplScalar => "#sec-Scalars", } } } @@ -35,11 +36,11 @@ 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::DeriveInputObject => "input object", - GraphQLScope::DeriveUnion | GraphQLScope::ImplUnion => "Union", - GraphQLScope::DeriveEnum => "enum", - GraphQLScope::DeriveScalar | GraphQLScope::ImplScalar => "scalar", + Self::DeriveObject | Self::ImplObject => "object", + Self::DeriveInputObject => "input object", + Self::AttrUnion | Self::DeriveUnion | Self::ImplUnion => "union", + Self::DeriveEnum => "enum", + Self::DeriveScalar | Self::ImplScalar => "scalar", }; write!(f, "GraphQL {}", name) From 4a235383724946c9287e40551632c2ef4769c898 Mon Sep 17 00:00:00 2001 From: tyranron Date: Wed, 27 May 2020 18:28:41 +0300 Subject: [PATCH 08/29] Implement #[graphql_union] for traits - support multiple #[graphql_union] attributes in non-ambiguous way - relax Sized requirement on GraphQLType --- .../juniper_tests/src/codegen/mod.rs | 4 +- .../juniper_tests/src/codegen/union_attr.rs | 11 +- .../juniper_tests/src/codegen/union_derive.rs | 11 +- juniper/src/executor/mod.rs | 2 +- juniper/src/lib.rs | 8 +- juniper/src/types/async_await.rs | 4 +- juniper/src/types/base.rs | 4 +- juniper/src/types/subscriptions.rs | 4 +- juniper_codegen/src/graphql_union/attr.rs | 108 ++++++++++++------ juniper_codegen/src/graphql_union/derive.rs | 18 +-- juniper_codegen/src/graphql_union/mod.rs | 54 +++++---- juniper_codegen/src/lib.rs | 8 +- juniper_codegen/src/util/mod.rs | 19 ++- 13 files changed, 159 insertions(+), 96 deletions(-) diff --git a/integration_tests/juniper_tests/src/codegen/mod.rs b/integration_tests/juniper_tests/src/codegen/mod.rs index c3d846c45..99d2b3651 100644 --- a/integration_tests/juniper_tests/src/codegen/mod.rs +++ b/integration_tests/juniper_tests/src/codegen/mod.rs @@ -2,8 +2,8 @@ mod derive_enum; mod derive_input_object; mod derive_object; mod derive_object_with_raw_idents; -mod union_derive; mod impl_object; mod impl_scalar; -mod union_attr; mod scalar_value_transparent; +mod union_attr; +mod union_derive; diff --git a/integration_tests/juniper_tests/src/codegen/union_attr.rs b/integration_tests/juniper_tests/src/codegen/union_attr.rs index 45cd9827d..498b911d3 100644 --- a/integration_tests/juniper_tests/src/codegen/union_attr.rs +++ b/integration_tests/juniper_tests/src/codegen/union_attr.rs @@ -1,4 +1,4 @@ -use juniper::{GraphQLObject, graphql_union}; +use juniper::{graphql_union, GraphQLObject}; #[derive(GraphQLObject)] struct Human { @@ -12,8 +12,8 @@ struct Droid { primary_function: String, } -#[graphql_union] -#[graphql(description = "A Collection of things")] +#[graphql_union(name = "Character")] +#[graphql_union(description = "A Collection of things")] trait Character { fn as_human(&self, _: &()) -> Option<&Human> { None @@ -21,9 +21,10 @@ trait Character { fn as_droid(&self) -> Option<&Droid> { None } + #[graphql_union(ignore)] + fn some(&self); } - /* impl Character for Human { fn as_human(&self) -> Option<&Human> { @@ -46,4 +47,4 @@ impl<'a> GraphQLUnion for &'a dyn Character { } } } -*/ \ No newline at end of file +*/ diff --git a/integration_tests/juniper_tests/src/codegen/union_derive.rs b/integration_tests/juniper_tests/src/codegen/union_derive.rs index 0fc682f9b..5dabea7a8 100644 --- a/integration_tests/juniper_tests/src/codegen/union_derive.rs +++ b/integration_tests/juniper_tests/src/codegen/union_derive.rs @@ -3,7 +3,7 @@ use derive_more::From; #[cfg(test)] use fnv::FnvHashMap; -use juniper::GraphQLUnion; +use juniper::{GraphQLObject, GraphQLUnion}; #[cfg(test)] use juniper::{ @@ -11,13 +11,13 @@ use juniper::{ Value, Variables, }; -#[derive(juniper::GraphQLObject)] +#[derive(GraphQLObject)] pub struct Human { id: String, home_planet: String, } -#[derive(juniper::GraphQLObject)] +#[derive(GraphQLObject)] pub struct Droid { id: String, primary_function: String, @@ -101,7 +101,6 @@ impl CharacterGenericStruct { } } - // Context Test pub struct CustomContext { is_left: bool, @@ -109,14 +108,14 @@ pub struct CustomContext { impl juniper::Context for CustomContext {} -#[derive(juniper::GraphQLObject)] +#[derive(GraphQLObject)] #[graphql(Context = CustomContext)] pub struct HumanContext { id: String, home_planet: String, } -#[derive(juniper::GraphQLObject)] +#[derive(GraphQLObject)] #[graphql(Context = CustomContext)] pub struct DroidContext { id: String, diff --git a/juniper/src/executor/mod.rs b/juniper/src/executor/mod.rs index d1378c17c..275faae45 100644 --- a/juniper/src/executor/mod.rs +++ b/juniper/src/executor/mod.rs @@ -1262,7 +1262,7 @@ where /// Create a union meta type pub fn build_union_type(&mut self, info: &T::TypeInfo, types: &[Type<'r>]) -> UnionMeta<'r> where - T: GraphQLType, + T: GraphQLType + ?Sized, { let name = T::name(info).expect("Union types must be named. Implement name()"); diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs index 010978da9..824bc3091 100644 --- a/juniper/src/lib.rs +++ b/juniper/src/lib.rs @@ -93,6 +93,8 @@ Juniper has not reached 1.0 yet, thus some API instability should be expected. #![doc(html_root_url = "https://docs.rs/juniper/0.14.2")] #![warn(missing_docs)] +use std::fmt; + #[doc(hidden)] pub extern crate serde; @@ -114,6 +116,8 @@ extern crate bson; // This one is required for use by code generated with`juniper_codegen` macros. #[doc(hidden)] pub use futures; +#[doc(inline)] +pub use futures::future::BoxFuture; // Depend on juniper_codegen and re-export everything in it. // This allows users to just depend on juniper and get the derive @@ -165,7 +169,6 @@ use crate::{ parser::{parse_document_source, ParseError, Spanning}, validation::{validate_input_values, visit_all_rules, ValidatorContext}, }; -use std::fmt; pub use crate::{ ast::{FromInputValue, InputValue, Selection, ToInputValue, Type}, @@ -191,9 +194,6 @@ pub use crate::{ value::{DefaultScalarValue, Object, ParseScalarResult, ParseScalarValue, ScalarValue, Value}, }; -/// A pinned, boxed future that can be polled. -pub type BoxFuture<'a, T> = std::pin::Pin + 'a + Send>>; - /// An error that prevented query execution #[derive(Debug, PartialEq)] #[allow(missing_docs)] diff --git a/juniper/src/types/async_await.rs b/juniper/src/types/async_await.rs index ef5f9ec47..156c2ab52 100644 --- a/juniper/src/types/async_await.rs +++ b/juniper/src/types/async_await.rs @@ -98,7 +98,7 @@ fn resolve_selection_set_into_async<'a, 'e, T, CtxT, S>( executor: &'e Executor<'e, 'e, CtxT, S>, ) -> BoxFuture<'a, Value> where - T: GraphQLTypeAsync, + T: GraphQLTypeAsync + ?Sized, T::TypeInfo: Send + Sync, S: ScalarValue + Send + Sync, CtxT: Send + Sync, @@ -129,7 +129,7 @@ pub(crate) async fn resolve_selection_set_into_async_recursive<'a, T, CtxT, S>( executor: &'a Executor<'a, 'a, CtxT, S>, ) -> Value where - T: GraphQLTypeAsync + Send + Sync, + T: GraphQLTypeAsync + Send + Sync + ?Sized, T::TypeInfo: Send + Sync, S: ScalarValue + Send + Sync, CtxT: Send + Sync, diff --git a/juniper/src/types/base.rs b/juniper/src/types/base.rs index 1ce3754f4..c80188ed0 100644 --- a/juniper/src/types/base.rs +++ b/juniper/src/types/base.rs @@ -230,7 +230,7 @@ impl GraphQLType for User ``` */ -pub trait GraphQLType: Sized +pub trait GraphQLType where S: ScalarValue, { @@ -355,7 +355,7 @@ pub(crate) fn resolve_selection_set_into( result: &mut Object, ) -> bool where - T: GraphQLType, + T: GraphQLType + ?Sized, S: ScalarValue, { let meta_type = executor diff --git a/juniper/src/types/subscriptions.rs b/juniper/src/types/subscriptions.rs index 5b6ccc56c..050769cd9 100644 --- a/juniper/src/types/subscriptions.rs +++ b/juniper/src/types/subscriptions.rs @@ -184,7 +184,7 @@ where 'e: 'fut, 'ref_e: 'fut, 'res: 'fut, - T: GraphQLSubscriptionType, + T: GraphQLSubscriptionType + ?Sized, T::TypeInfo: Send + Sync, S: ScalarValue + Send + Sync, CtxT: Send + Sync, @@ -203,7 +203,7 @@ async fn resolve_selection_set_into_stream_recursive<'i, 'inf, 'ref_e, 'e, 'res, executor: &'ref_e Executor<'ref_e, 'e, CtxT, S>, ) -> Value> where - T: GraphQLSubscriptionType + Send + Sync, + T: GraphQLSubscriptionType + Send + Sync + ?Sized, T::TypeInfo: Send + Sync, S: ScalarValue + Send + Sync, CtxT: Send + Sync, diff --git a/juniper_codegen/src/graphql_union/attr.rs b/juniper_codegen/src/graphql_union/attr.rs index 7378b039e..29beeb8e4 100644 --- a/juniper_codegen/src/graphql_union/attr.rs +++ b/juniper_codegen/src/graphql_union/attr.rs @@ -1,4 +1,4 @@ -use std::ops::Deref as _; +use std::{mem, ops::Deref as _}; use proc_macro2::{Span, TokenStream}; use proc_macro_error::ResultExt as _; @@ -7,34 +7,62 @@ use syn::{self, ext::IdentExt as _, parse_quote, spanned::Spanned as _}; use crate::{ result::GraphQLScope, - util::{span_container::SpanContainer, Mode}, + util::{path_eq_single, span_container::SpanContainer, unparenthesize, Mode}, }; use super::{UnionDefinition, UnionMeta, UnionVariantDefinition, UnionVariantMeta}; const SCOPE: GraphQLScope = GraphQLScope::AttrUnion; -/// Expands `#[graphql_union]` macro into generated code. -pub fn expand(attr: TokenStream, body: TokenStream, mode: Mode) -> syn::Result { - if !attr.is_empty() { - return Err(syn::Error::new( - Span::call_site(), - "#[graphql_union] attribute itself does not support any parameters, \ - use helper #[graphql] attributes instead to specify any parameters", - )); +/// Returns name of the `proc_macro_attribute` for deriving `GraphQLUnion` implementation depending +/// on the provided `mode`. +fn attr_path(mode: Mode) -> &'static str { + match mode { + Mode::Public => "graphql_union", + Mode::Internal => "graphql_union_internal", } +} + +/// Expands `#[graphql_union]`/`#[graphql_union_internal]` macros into generated code. +pub fn expand(attr_args: TokenStream, body: TokenStream, mode: Mode) -> syn::Result { + let attr_path = attr_path(mode); - let ast = syn::parse2::(body.clone()).map_err(|_| { + let mut ast = syn::parse2::(body).map_err(|_| { syn::Error::new( Span::call_site(), - "#[graphql_union] attribute is applicable to trait definitions only", + format!( + "#[{}] attribute is applicable to trait definitions only", + attr_path, + ), ) })?; - let meta = UnionMeta::from_attrs(&ast.attrs)?; + let mut trait_attrs = Vec::with_capacity(ast.attrs.len() + 1); + trait_attrs.push({ + let attr_path = syn::Ident::new(attr_path, Span::call_site()); + parse_quote! { #[#attr_path(#attr_args)] } + }); + trait_attrs.extend_from_slice(&ast.attrs); + + // Remove repeated attributes from the definition, to omit duplicate expansion. + ast.attrs = ast + .attrs + .into_iter() + .filter_map(|attr| { + if path_eq_single(&attr.path, attr_path) { + None + } else { + Some(attr) + } + }) + .collect(); + + let meta = UnionMeta::from_attrs(attr_path, &trait_attrs)?; + + //panic!("{:?}", meta); let trait_span = ast.span(); - let trait_ident = ast.ident; + let trait_ident = &ast.ident; let name = meta .name @@ -52,10 +80,10 @@ pub fn expand(attr: TokenStream, body: TokenStream, mode: Mode) -> syn::Result = ast .items - .into_iter() + .iter_mut() .filter_map(|i| match i { syn::TraitItem::Method(m) => { - parse_variant_from_trait_method(m, &trait_ident, &meta, mode) + parse_variant_from_trait_method(m, trait_ident, &meta, mode) } _ => None, }) @@ -88,38 +116,57 @@ pub fn expand(attr: TokenStream, body: TokenStream, mode: Mode) -> syn::Result Option { - let meta = UnionVariantMeta::from_attrs(&method.attrs) + let attr_path = attr_path(mode); + let method_attrs = method.attrs.clone(); + + // Remove repeated attributes from the method, to omit incorrect expansion. + method.attrs = mem::take(&mut method.attrs) + .into_iter() + .filter_map(|attr| { + if path_eq_single(&attr.path, attr_path) { + None + } else { + Some(attr) + } + }) + .collect(); + + let meta = UnionVariantMeta::from_attrs(attr_path, &method_attrs) .map_err(|e| proc_macro_error::emit_error!(e)) .ok()?; + if let Some(rslvr) = meta.custom_resolver { SCOPE.custom( rslvr.span_ident(), - "cannot use #[graphql(with = ...)] attribute on a trait method, instead use \ - #[graphql(ignore)] on the method with #[graphql(on ... = ...)] on the trait itself", + format!( + "cannot use #[{0}(with = ...)] attribute on a trait method, instead use \ + #[{0}(ignore)] on the method with #[{0}(on ... = ...)] on the trait itself", + attr_path, + ), ) } if meta.ignore.is_some() { @@ -153,11 +200,12 @@ fn parse_variant_from_trait_method( method_span, format!( "trait method `{}` conflicts with the custom resolver `{}` declared on the \ - trait to resolve the variant type `{}`, use `#[graphql(ignore)]` attribute to \ + trait to resolve the variant type `{}`, use `#[{}(ignore)]` attribute to \ ignore this trait method for union variants resolution", method_ident, other.to_token_stream(), ty.to_token_stream(), + attr_path, ), ); } @@ -274,11 +322,3 @@ fn parse_trait_method_input_args(sig: &syn::Signature) -> Result { Ok(true) } - -/// Retrieves the innermost non-parenthesized [`syn::Type`] from the given one. -fn unparenthesize(ty: &syn::Type) -> &syn::Type { - match ty { - syn::Type::Paren(ty) => unparenthesize(ty.elem.deref()), - _ => ty, - } -} diff --git a/juniper_codegen/src/graphql_union/derive.rs b/juniper_codegen/src/graphql_union/derive.rs index 8b4574713..87a15d1ad 100644 --- a/juniper_codegen/src/graphql_union/derive.rs +++ b/juniper_codegen/src/graphql_union/derive.rs @@ -1,6 +1,6 @@ use proc_macro2::TokenStream; use proc_macro_error::ResultExt as _; -use quote::{quote, ToTokens as _}; +use quote::{quote, ToTokens}; use syn::{self, ext::IdentExt as _, parse_quote, spanned::Spanned as _, Data, Fields}; use crate::{ @@ -12,7 +12,7 @@ use super::{UnionDefinition, UnionMeta, UnionVariantDefinition, UnionVariantMeta const SCOPE: GraphQLScope = GraphQLScope::DeriveUnion; -/// Expands `#[derive(GraphQLUnion)]` macro into generated code. +/// Expands `#[derive(GraphQLUnion)]`/`#[derive(GraphQLUnionInternal)]` macros into generated code. pub fn expand(input: TokenStream, mode: Mode) -> syn::Result { let ast = syn::parse2::(input).unwrap_or_abort(); @@ -21,11 +21,11 @@ pub fn expand(input: TokenStream, mode: Mode) -> syn::Result { Data::Struct(_) => expand_struct(ast, mode), _ => Err(SCOPE.custom_error(ast.span(), "can only be applied to enums and structs")), } - .map(UnionDefinition::into_tokens) + .map(ToTokens::into_token_stream) } fn expand_enum(ast: syn::DeriveInput, mode: Mode) -> syn::Result { - let meta = UnionMeta::from_attrs(&ast.attrs)?; + let meta = UnionMeta::from_attrs("graphql", &ast.attrs)?; let enum_span = ast.span(); let enum_ident = ast.ident; @@ -108,7 +108,8 @@ fn expand_enum(ast: syn::DeriveInput, mode: Mode) -> syn::Result Option { - let meta = UnionVariantMeta::from_attrs(&var.attrs) + let meta = UnionVariantMeta::from_attrs("graphql", &var.attrs) .map_err(|e| proc_macro_error::emit_error!(e)) .ok()?; if meta.ignore.is_some() { @@ -195,7 +196,7 @@ fn parse_variant_from_enum_variant( } fn expand_struct(ast: syn::DeriveInput, mode: Mode) -> syn::Result { - let meta = UnionMeta::from_attrs(&ast.attrs)?; + let meta = UnionMeta::from_attrs("graphql", &ast.attrs)?; let struct_span = ast.span(); let struct_ident = ast.ident; @@ -266,7 +267,8 @@ fn expand_struct(ast: syn::DeriveInput, mode: Mode) -> syn::Result syn::Result { - let mut meta = filter_graphql_attrs(attrs) + pub fn from_attrs(name: &str, attrs: &[syn::Attribute]) -> syn::Result { + let mut meta = filter_attrs(name, attrs) .map(|attr| attr.parse_args()) .try_fold(Self::default(), |prev, curr| prev.try_merge(curr?))?; @@ -200,8 +200,8 @@ impl UnionMeta { } } -/// Available metadata behind `#[graphql]` attribute when generating code for [GraphQL union][1]'s -/// variant. +/// Available metadata behind `#[graphql]` (or `#[graphql_union]`) attribute when generating code +/// for [GraphQL union][1]'s variant. /// /// [1]: https://spec.graphql.org/June2018/#sec-Unions #[derive(Debug, Default)] @@ -279,8 +279,8 @@ impl UnionVariantMeta { } /// Parses [`UnionVariantMeta`] from the given attributes placed on variant/field definition. - pub fn from_attrs(attrs: &[syn::Attribute]) -> syn::Result { - filter_graphql_attrs(attrs) + pub fn from_attrs(name: &str, attrs: &[syn::Attribute]) -> syn::Result { + filter_attrs(name, attrs) .map(|attr| attr.parse_args()) .try_fold(Self::default(), |prev, curr| prev.try_merge(curr?)) } @@ -297,6 +297,7 @@ struct UnionVariantDefinition { struct UnionDefinition { pub name: String, pub ty: syn::Type, + pub is_trait_object: bool, pub description: Option, pub context: Option, pub scalar: Option, @@ -306,8 +307,8 @@ struct UnionDefinition { pub mode: Mode, } -impl UnionDefinition { - pub fn into_tokens(self) -> TokenStream { +impl ToTokens for UnionDefinition { + fn to_tokens(&self, into: &mut TokenStream) { let crate_path = self.mode.crate_path(); let name = &self.name; @@ -394,8 +395,15 @@ impl UnionDefinition { } }); - let (impl_generics, ty_generics, _) = self.generics.split_for_impl(); - let mut ext_generics = self.generics.clone(); + let (_, ty_generics, _) = self.generics.split_for_impl(); + + let mut base_generics = self.generics.clone(); + if self.is_trait_object { + base_generics.params.push(parse_quote! { '__obj }); + } + let (impl_generics, _, _) = base_generics.split_for_impl(); + + let mut ext_generics = base_generics.clone(); if self.scalar.is_none() { ext_generics.params.push(parse_quote! { #scalar }); ext_generics @@ -418,9 +426,14 @@ impl UnionDefinition { .push(parse_quote! { #scalar: Send + Sync }); } + let mut ty_full = quote! { #ty#ty_generics }; + if self.is_trait_object { + ty_full = quote! { dyn #ty_full + '__obj }; + } + let type_impl = quote! { #[automatically_derived] - impl#ext_impl_generics #crate_path::GraphQLType<#scalar> for #ty#ty_generics + impl#ext_impl_generics #crate_path::GraphQLType<#scalar> for #ty_full #where_clause { type Context = #context; @@ -439,7 +452,7 @@ impl UnionDefinition { let types = &[ #( registry.get_type::<&#var_types>(&(())), )* ]; - registry.build_union_type::<#ty#ty_generics>(info, types) + registry.build_union_type::<#ty_full>(info, types) #description .into_meta() } @@ -476,7 +489,7 @@ impl UnionDefinition { let async_type_impl = quote! { #[automatically_derived] - impl#ext_impl_generics #crate_path::GraphQLTypeAsync<#scalar> for #ty#ty_generics + impl#ext_impl_generics #crate_path::GraphQLTypeAsync<#scalar> for #ty_full #where_async { fn resolve_into_type_async<'b>( @@ -498,7 +511,7 @@ impl UnionDefinition { let output_type_impl = quote! { #[automatically_derived] - impl#ext_impl_generics #crate_path::marker::IsOutputType<#scalar> for #ty#ty_generics + impl#ext_impl_generics #crate_path::marker::IsOutputType<#scalar> for #ty_full #where_clause { fn mark() { @@ -509,7 +522,7 @@ impl UnionDefinition { let union_impl = quote! { #[automatically_derived] - impl#impl_generics #crate_path::marker::GraphQLUnion for #ty#ty_generics { + impl#impl_generics #crate_path::marker::GraphQLUnion for #ty_full { fn mark() { #( <#var_types as #crate_path::marker::GraphQLObjectType< #default_scalar, @@ -518,11 +531,6 @@ impl UnionDefinition { } }; - quote! { - #union_impl - #output_type_impl - #type_impl - #async_type_impl - } + into.append_all(&[union_impl, output_type_impl, type_impl, async_type_impl]); } } diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs index 70acb09dd..0a10d4d6a 100644 --- a/juniper_codegen/src/lib.rs +++ b/juniper_codegen/src/lib.rs @@ -552,7 +552,7 @@ pub fn graphql_subscription_internal(args: TokenStream, input: TokenStream) -> T #[proc_macro_derive(GraphQLUnion, attributes(graphql))] pub fn derive_union(input: TokenStream) -> TokenStream { //derive_union::expand(input.into(), Mode::Public) - graphql_union::derive::expand(input.into(), Mode::Public) + self::graphql_union::derive::expand(input.into(), Mode::Public) .unwrap_or_abort() .into() } @@ -562,7 +562,7 @@ pub fn derive_union(input: TokenStream) -> TokenStream { #[doc(hidden)] pub fn derive_union_internal(input: TokenStream) -> TokenStream { //derive_union::expand(input.into(), Mode::Internal) - graphql_union::derive::expand(input.into(), Mode::Internal) + self::graphql_union::derive::expand(input.into(), Mode::Internal) .unwrap_or_abort() .into() } @@ -571,7 +571,7 @@ pub fn derive_union_internal(input: TokenStream) -> TokenStream { #[proc_macro_attribute] pub fn graphql_union(attr: TokenStream, body: TokenStream) -> TokenStream { //impl_union::expand(attr.into(), body.into(), Mode::Public) - graphql_union::attr::expand(attr.into(), body.into(), Mode::Public) + self::graphql_union::attr::expand(attr.into(), body.into(), Mode::Public) .unwrap_or_abort() .into() } @@ -581,7 +581,7 @@ pub fn graphql_union(attr: TokenStream, body: TokenStream) -> TokenStream { #[doc(hidden)] pub fn graphql_union_internal(attr: TokenStream, body: TokenStream) -> TokenStream { //impl_union::expand(attr.into(), body.into(), Mode::Internal) - graphql_union::attr::expand(attr.into(), body.into(), Mode::Internal) + self::graphql_union::attr::expand(attr.into(), body.into(), Mode::Internal) .unwrap_or_abort() .into() } diff --git a/juniper_codegen/src/util/mod.rs b/juniper_codegen/src/util/mod.rs index 2b2da23dc..f5f838687 100644 --- a/juniper_codegen/src/util/mod.rs +++ b/juniper_codegen/src/util/mod.rs @@ -6,6 +6,8 @@ pub mod option_ext; pub mod parse_impl; pub mod span_container; +use std::ops::Deref as _; + use proc_macro2::{Span, TokenStream}; use proc_macro_error::abort; use quote::quote; @@ -78,6 +80,14 @@ pub fn type_is_identifier_ref(ty: &syn::Type, name: &str) -> bool { } } +/// Retrieves the innermost non-parenthesized [`syn::Type`] from the given one. +pub fn unparenthesize(ty: &syn::Type) -> &syn::Type { + match ty { + syn::Type::Paren(ty) => unparenthesize(ty.elem.deref()), + _ => ty, + } +} + #[derive(Debug)] pub struct DeprecationAttr { pub reason: Option, @@ -89,11 +99,14 @@ pub fn find_graphql_attr(attrs: &[Attribute]) -> Option<&Attribute> { .find(|attr| path_eq_single(&attr.path, "graphql")) } -/// Filters given `attrs` to contain `#[graphql]` attributes only. -pub fn filter_graphql_attrs(attrs: &[Attribute]) -> impl Iterator { +/// Filters given `attrs` to contain attributes only with the given `name`. +pub fn filter_attrs<'a>( + name: &'a str, + attrs: &'a [Attribute], +) -> impl Iterator + 'a { attrs .iter() - .filter(|attr| path_eq_single(&attr.path, "graphql")) + .filter(move |attr| path_eq_single(&attr.path, name)) } pub fn get_deprecated(attrs: &[Attribute]) -> Option> { From 9d7e99f28a291ee0b52f509ed79cc7c55344ed1f Mon Sep 17 00:00:00 2001 From: tyranron Date: Fri, 29 May 2020 18:17:21 +0300 Subject: [PATCH 09/29] Relax more usages of :Sized trait bound --- juniper/src/executor/mod.rs | 47 ++++----- juniper/src/schema/meta.rs | 6 +- juniper/src/types/containers.rs | 176 ++++++++++++++------------------ juniper/src/types/pointers.rs | 70 +++++++------ juniper/src/types/scalars.rs | 18 ++-- 5 files changed, 153 insertions(+), 164 deletions(-) diff --git a/juniper/src/executor/mod.rs b/juniper/src/executor/mod.rs index 275faae45..998c7d50f 100644 --- a/juniper/src/executor/mod.rs +++ b/juniper/src/executor/mod.rs @@ -242,8 +242,9 @@ impl IntoFieldError for FieldError { } #[doc(hidden)] -pub trait IntoResolvable<'a, S, T: GraphQLType, C>: Sized +pub trait IntoResolvable<'a, S, T, C> where + T: GraphQLType, S: ScalarValue, { #[doc(hidden)] @@ -404,7 +405,7 @@ where pub fn resolve_with_ctx(&self, info: &T::TypeInfo, value: &T) -> ExecutionResult where NewCtxT: FromContext, - T: GraphQLType, + T: GraphQLType + ?Sized, { self.replaced_context(>::from(self.context)) .resolve(info, value) @@ -413,7 +414,7 @@ where /// Resolve a single arbitrary value into an `ExecutionResult` pub fn resolve(&self, info: &T::TypeInfo, value: &T) -> ExecutionResult where - T: GraphQLType, + T: GraphQLType + ?Sized, { value.resolve(info, self.current_selection_set, self) } @@ -421,7 +422,7 @@ where /// Resolve a single arbitrary value into an `ExecutionResult` pub async fn resolve_async(&self, info: &T::TypeInfo, value: &T) -> ExecutionResult where - T: crate::GraphQLTypeAsync + Send + Sync, + T: crate::GraphQLTypeAsync + Send + Sync + ?Sized, T::TypeInfo: Send + Sync, CtxT: Send + Sync, S: Send + Sync, @@ -468,18 +469,15 @@ where /// If the field fails to resolve, `null` will be returned. pub async fn resolve_into_value_async(&self, info: &T::TypeInfo, value: &T) -> Value where - T: crate::GraphQLTypeAsync + Send + Sync, + T: crate::GraphQLTypeAsync + Send + Sync + ?Sized, T::TypeInfo: Send + Sync, CtxT: Send + Sync, S: Send + Sync, { - match self.resolve_async(info, value).await { - Ok(v) => v, - Err(e) => { - self.push_error(e); - Value::null() - } - } + self.resolve_async(info, value).await.unwrap_or_else(|e| { + self.push_error(e); + Value::null() + }) } /// Derive a new executor by replacing the context @@ -1103,7 +1101,7 @@ where /// construct its metadata and store it. pub fn get_type(&mut self, info: &T::TypeInfo) -> Type<'r> where - T: GraphQLType, + T: GraphQLType + ?Sized, { if let Some(name) = T::name(info) { let validated_name = name.parse::().unwrap(); @@ -1124,7 +1122,7 @@ where /// Create a field with the provided name pub fn field(&mut self, name: &str, info: &T::TypeInfo) -> Field<'r, S> where - T: GraphQLType, + T: GraphQLType + ?Sized, { Field { name: name.to_owned(), @@ -1156,7 +1154,7 @@ where /// Create an argument with the provided name pub fn arg(&mut self, name: &str, info: &T::TypeInfo) -> Argument<'r, S> where - T: GraphQLType + FromInputValue, + T: GraphQLType + FromInputValue + ?Sized, { Argument::new(name, self.get_type::(info)) } @@ -1172,7 +1170,7 @@ where info: &T::TypeInfo, ) -> Argument<'r, S> where - T: GraphQLType + ToInputValue + FromInputValue, + T: GraphQLType + ToInputValue + FromInputValue + ?Sized, { Argument::new(name, self.get_type::>(info)).default_value(value.to_input_value()) } @@ -1188,20 +1186,23 @@ where /// This expects the type to implement `FromInputValue`. pub fn build_scalar_type(&mut self, info: &T::TypeInfo) -> ScalarMeta<'r, S> where - T: FromInputValue + GraphQLType + ParseScalarValue + 'r, + T: FromInputValue + GraphQLType + ParseScalarValue + ?Sized + 'r, { let name = T::name(info).expect("Scalar types must be named. Implement name()"); ScalarMeta::new::(Cow::Owned(name.to_string())) } /// Create a list meta type - pub fn build_list_type>(&mut self, info: &T::TypeInfo) -> ListMeta<'r> { + pub fn build_list_type + ?Sized>( + &mut self, + info: &T::TypeInfo, + ) -> ListMeta<'r> { let of_type = self.get_type::(info); ListMeta::new(of_type) } /// Create a nullable meta type - pub fn build_nullable_type>( + pub fn build_nullable_type + ?Sized>( &mut self, info: &T::TypeInfo, ) -> NullableMeta<'r> { @@ -1219,7 +1220,7 @@ where fields: &[Field<'r, S>], ) -> ObjectMeta<'r, S> where - T: GraphQLType, + T: GraphQLType + ?Sized, { let name = T::name(info).expect("Object types must be named. Implement name()"); @@ -1235,7 +1236,7 @@ where values: &[EnumValue], ) -> EnumMeta<'r, S> where - T: FromInputValue + GraphQLType, + T: FromInputValue + GraphQLType + ?Sized, { let name = T::name(info).expect("Enum types must be named. Implement name()"); @@ -1250,7 +1251,7 @@ where fields: &[Field<'r, S>], ) -> InterfaceMeta<'r, S> where - T: GraphQLType, + T: GraphQLType + ?Sized, { let name = T::name(info).expect("Interface types must be named. Implement name()"); @@ -1276,7 +1277,7 @@ where args: &[Argument<'r, S>], ) -> InputObjectMeta<'r, S> where - T: FromInputValue + GraphQLType, + T: FromInputValue + GraphQLType + ?Sized, { let name = T::name(info).expect("Input object types must be named. Implement name()"); diff --git a/juniper/src/schema/meta.rs b/juniper/src/schema/meta.rs index d86210356..b024ed05e 100644 --- a/juniper/src/schema/meta.rs +++ b/juniper/src/schema/meta.rs @@ -572,8 +572,10 @@ where S: ScalarValue, { /// Build a new input type with the specified name and input fields - pub fn new>(name: Cow<'a, str>, input_fields: &[Argument<'a, S>]) -> Self -where { + pub fn new(name: Cow<'a, str>, input_fields: &[Argument<'a, S>]) -> Self + where + T: FromInputValue + ?Sized, + { InputObjectMeta { name, description: None, diff --git a/juniper/src/types/containers.rs b/juniper/src/types/containers.rs index e587dee51..589624028 100644 --- a/juniper/src/types/containers.rs +++ b/juniper/src/types/containers.rs @@ -1,15 +1,11 @@ use crate::{ ast::{FromInputValue, InputValue, Selection, ToInputValue}, - executor::ExecutionResult, + executor::{ExecutionResult, Executor, Registry}, schema::meta::MetaType, + types::{async_await::GraphQLTypeAsync, base::GraphQLType}, value::{ScalarValue, Value}, }; -use crate::{ - executor::{Executor, Registry}, - types::base::GraphQLType, -}; - impl GraphQLType for Option where S: ScalarValue, @@ -42,6 +38,30 @@ where } } +impl GraphQLTypeAsync for Option +where + T: GraphQLTypeAsync, + T::TypeInfo: Send + Sync, + S: ScalarValue + Send + Sync, + CtxT: Send + Sync, +{ + fn resolve_async<'a>( + &'a self, + info: &'a Self::TypeInfo, + _selection_set: Option<&'a [Selection]>, + executor: &'a Executor, + ) -> crate::BoxFuture<'a, ExecutionResult> { + let f = async move { + let value = match *self { + Some(ref obj) => executor.resolve_into_value_async(info, obj).await, + None => Value::null(), + }; + Ok(value) + }; + Box::pin(f) + } +} + impl FromInputValue for Option where T: FromInputValue, @@ -50,10 +70,7 @@ where fn from_input_value<'a>(v: &'a InputValue) -> Option> { match v { &InputValue::Null => Some(None), - v => match v.convert() { - Some(x) => Some(Some(x)), - None => None, - }, + v => v.convert().map(Some), } } } @@ -100,6 +117,24 @@ where } } +impl GraphQLTypeAsync for Vec +where + T: GraphQLTypeAsync, + T::TypeInfo: Send + Sync, + S: ScalarValue + Send + Sync, + CtxT: Send + Sync, +{ + fn resolve_async<'a>( + &'a self, + info: &'a Self::TypeInfo, + _selection_set: Option<&'a [Selection]>, + executor: &'a Executor, + ) -> crate::BoxFuture<'a, ExecutionResult> { + let f = resolve_into_list_async(executor, info, self.iter()); + Box::pin(f) + } +} + impl FromInputValue for Vec where T: FromInputValue, @@ -117,13 +152,7 @@ where { None } } - ref other => { - if let Some(e) = other.convert() { - Some(vec![e]) - } else { - None - } - } + ref other => other.convert().map(|e| vec![e]), } } } @@ -134,11 +163,11 @@ where S: ScalarValue, { fn to_input_value(&self) -> InputValue { - InputValue::list(self.iter().map(|v| v.to_input_value()).collect()) + InputValue::list(self.iter().map(T::to_input_value).collect()) } } -impl<'a, S, T, CtxT> GraphQLType for &'a [T] +impl GraphQLType for [T] where S: ScalarValue, T: GraphQLType, @@ -167,25 +196,43 @@ where } } +impl GraphQLTypeAsync for [T] +where + T: GraphQLTypeAsync, + T::TypeInfo: Send + Sync, + S: ScalarValue + Send + Sync, + CtxT: Send + Sync, +{ + fn resolve_async<'a>( + &'a self, + info: &'a Self::TypeInfo, + _selection_set: Option<&'a [Selection]>, + executor: &'a Executor, + ) -> crate::BoxFuture<'a, ExecutionResult> { + let f = resolve_into_list_async(executor, info, self.iter()); + Box::pin(f) + } +} + impl<'a, T, S> ToInputValue for &'a [T] where T: ToInputValue, S: ScalarValue, { fn to_input_value(&self) -> InputValue { - InputValue::list(self.iter().map(|v| v.to_input_value()).collect()) + InputValue::list(self.iter().map(T::to_input_value).collect()) } } -fn resolve_into_list( +fn resolve_into_list<'t, S, T, I>( executor: &Executor, info: &T::TypeInfo, iter: I, ) -> ExecutionResult where S: ScalarValue, - I: Iterator + ExactSizeIterator, - T: GraphQLType, + I: Iterator + ExactSizeIterator, + T: GraphQLType + ?Sized + 't, { let stop_on_null = executor .current_type() @@ -195,30 +242,26 @@ where let mut result = Vec::with_capacity(iter.len()); for o in iter { - match executor.resolve(info, &o) { - Ok(value) => { - if stop_on_null && value.is_null() { - return Ok(value); - } else { - result.push(value) - } - } - Err(e) => return Err(e), + let val = executor.resolve(info, o)?; + if stop_on_null && val.is_null() { + return Ok(val); + } else { + result.push(val) } } Ok(Value::list(result)) } -async fn resolve_into_list_async<'a, S, T, I>( +async fn resolve_into_list_async<'a, 't, S, T, I>( executor: &'a Executor<'a, 'a, T::Context, S>, info: &'a T::TypeInfo, items: I, ) -> ExecutionResult where S: ScalarValue + Send + Sync, - I: Iterator + ExactSizeIterator, - T: crate::GraphQLTypeAsync, + I: Iterator + ExactSizeIterator, + T: GraphQLTypeAsync + ?Sized + 't, T::TypeInfo: Send + Sync, T::Context: Send + Sync, { @@ -231,8 +274,7 @@ where .expect("Current type is not a list type") .is_non_null(); - let iter = - items.map(|item| async move { executor.resolve_into_value_async(info, &item).await }); + let iter = items.map(|item| async move { executor.resolve_into_value_async(info, item).await }); let mut futures = FuturesOrdered::from_iter(iter); let mut values = Vec::with_capacity(futures.len()); @@ -245,63 +287,3 @@ where Ok(Value::list(values)) } - -impl crate::GraphQLTypeAsync for Vec -where - T: crate::GraphQLTypeAsync, - T::TypeInfo: Send + Sync, - S: ScalarValue + Send + Sync, - CtxT: Send + Sync, -{ - fn resolve_async<'a>( - &'a self, - info: &'a Self::TypeInfo, - _selection_set: Option<&'a [Selection]>, - executor: &'a Executor, - ) -> crate::BoxFuture<'a, ExecutionResult> { - let f = resolve_into_list_async(executor, info, self.iter()); - Box::pin(f) - } -} - -impl crate::GraphQLTypeAsync for &[T] -where - T: crate::GraphQLTypeAsync, - T::TypeInfo: Send + Sync, - S: ScalarValue + Send + Sync, - CtxT: Send + Sync, -{ - fn resolve_async<'a>( - &'a self, - info: &'a Self::TypeInfo, - _selection_set: Option<&'a [Selection]>, - executor: &'a Executor, - ) -> crate::BoxFuture<'a, ExecutionResult> { - let f = resolve_into_list_async(executor, info, self.iter()); - Box::pin(f) - } -} - -impl crate::GraphQLTypeAsync for Option -where - T: crate::GraphQLTypeAsync, - T::TypeInfo: Send + Sync, - S: ScalarValue + Send + Sync, - CtxT: Send + Sync, -{ - fn resolve_async<'a>( - &'a self, - info: &'a Self::TypeInfo, - _selection_set: Option<&'a [Selection]>, - executor: &'a Executor, - ) -> crate::BoxFuture<'a, ExecutionResult> { - let f = async move { - let value = match *self { - Some(ref obj) => executor.resolve_into_value_async(info, obj).await, - None => Value::null(), - }; - Ok(value) - }; - Box::pin(f) - } -} diff --git a/juniper/src/types/pointers.rs b/juniper/src/types/pointers.rs index b599b2cae..68274fad3 100644 --- a/juniper/src/types/pointers.rs +++ b/juniper/src/types/pointers.rs @@ -1,17 +1,21 @@ -use crate::ast::{FromInputValue, InputValue, Selection, ToInputValue}; use std::{fmt::Debug, sync::Arc}; use crate::{ + ast::{FromInputValue, InputValue, Selection, ToInputValue}, executor::{ExecutionResult, Executor, Registry}, schema::meta::MetaType, - types::base::{Arguments, GraphQLType}, + types::{ + async_await::GraphQLTypeAsync, + base::{Arguments, GraphQLType}, + }, value::ScalarValue, + BoxFuture, }; impl GraphQLType for Box where S: ScalarValue, - T: GraphQLType, + T: GraphQLType + ?Sized, { type Context = CtxT; type TypeInfo = T::TypeInfo; @@ -57,6 +61,23 @@ where } } +impl crate::GraphQLTypeAsync for Box +where + T: GraphQLTypeAsync + ?Sized, + T::TypeInfo: Send + Sync, + S: ScalarValue + Send + Sync, + CtxT: Send + Sync, +{ + fn resolve_async<'a>( + &'a self, + info: &'a Self::TypeInfo, + selection_set: Option<&'a [Selection]>, + executor: &'a Executor, + ) -> BoxFuture<'a, ExecutionResult> { + (**self).resolve_async(info, selection_set, executor) + } +} + impl FromInputValue for Box where S: ScalarValue, @@ -83,7 +104,7 @@ where impl<'e, S, T, CtxT> GraphQLType for &'e T where S: ScalarValue, - T: GraphQLType, + T: GraphQLType + ?Sized, { type Context = CtxT; type TypeInfo = T::TypeInfo; @@ -129,10 +150,10 @@ where } } -impl<'e, S, T> crate::GraphQLTypeAsync for &'e T +impl<'e, S, T> GraphQLTypeAsync for &'e T where S: ScalarValue + Send + Sync, - T: crate::GraphQLTypeAsync, + T: GraphQLTypeAsync + ?Sized, T::TypeInfo: Send + Sync, T::Context: Send + Sync, { @@ -142,8 +163,8 @@ where field_name: &'b str, arguments: &'b Arguments, executor: &'b Executor, - ) -> crate::BoxFuture<'b, ExecutionResult> { - crate::GraphQLTypeAsync::resolve_field_async(&**self, info, field_name, arguments, executor) + ) -> BoxFuture<'b, ExecutionResult> { + GraphQLTypeAsync::resolve_field_async(&**self, info, field_name, arguments, executor) } fn resolve_async<'a>( @@ -151,8 +172,8 @@ where info: &'a Self::TypeInfo, selection_set: Option<&'a [Selection]>, executor: &'a Executor, - ) -> crate::BoxFuture<'a, ExecutionResult> { - crate::GraphQLTypeAsync::resolve_async(&**self, info, selection_set, executor) + ) -> BoxFuture<'a, ExecutionResult> { + GraphQLTypeAsync::resolve_async(&**self, info, selection_set, executor) } } @@ -169,7 +190,7 @@ where impl GraphQLType for Arc where S: ScalarValue, - T: GraphQLType, + T: GraphQLType + ?Sized, { type Context = T::Context; type TypeInfo = T::TypeInfo; @@ -215,36 +236,19 @@ where } } -impl crate::GraphQLTypeAsync for Box -where - T: crate::GraphQLTypeAsync, - T::TypeInfo: Send + Sync, - S: ScalarValue + Send + Sync, - CtxT: Send + Sync, -{ - fn resolve_async<'a>( - &'a self, - info: &'a Self::TypeInfo, - selection_set: Option<&'a [Selection]>, - executor: &'a Executor, - ) -> crate::BoxFuture<'a, crate::ExecutionResult> { - (**self).resolve_async(info, selection_set, executor) - } -} - -impl<'e, S, T> crate::GraphQLTypeAsync for std::sync::Arc +impl<'e, S, T> GraphQLTypeAsync for Arc where S: ScalarValue + Send + Sync, - T: crate::GraphQLTypeAsync, - >::TypeInfo: Send + Sync, - >::Context: Send + Sync, + T: GraphQLTypeAsync + ?Sized, + >::TypeInfo: Send + Sync, + >::Context: Send + Sync, { fn resolve_async<'a>( &'a self, info: &'a Self::TypeInfo, selection_set: Option<&'a [Selection]>, executor: &'a Executor, - ) -> crate::BoxFuture<'a, crate::ExecutionResult> { + ) -> BoxFuture<'a, ExecutionResult> { (**self).resolve_async(info, selection_set, executor) } } diff --git a/juniper/src/types/scalars.rs b/juniper/src/types/scalars.rs index 1e9c29b50..f1724d97d 100644 --- a/juniper/src/types/scalars.rs +++ b/juniper/src/types/scalars.rs @@ -192,7 +192,7 @@ where }) } -impl<'a, S> GraphQLType for &'a str +impl GraphQLType for str where S: ScalarValue, { @@ -216,11 +216,11 @@ where _: Option<&[Selection]>, _: &Executor, ) -> ExecutionResult { - Ok(Value::scalar(String::from(*self))) + Ok(Value::scalar(String::from(self))) } } -impl<'e, S> crate::GraphQLTypeAsync for &'e str +impl crate::GraphQLTypeAsync for str where S: ScalarValue + Send + Sync, { @@ -325,11 +325,11 @@ where /// If you instantiate `RootNode` with this as the mutation, no mutation will be /// generated for the schema. #[derive(Debug, Default)] -pub struct EmptyMutation { +pub struct EmptyMutation { phantom: PhantomData, } -impl EmptyMutation { +impl EmptyMutation { /// Construct a new empty mutation pub fn new() -> EmptyMutation { EmptyMutation { @@ -339,7 +339,7 @@ impl EmptyMutation { } // This is safe due to never using `T`. -unsafe impl Send for EmptyMutation {} +unsafe impl Send for EmptyMutation {} impl GraphQLType for EmptyMutation where @@ -375,14 +375,14 @@ where /// If you instantiate `RootNode` with this as the subscription, /// no subscriptions will be generated for the schema. #[derive(Default)] -pub struct EmptySubscription { +pub struct EmptySubscription { phantom: PhantomData, } // This is safe due to never using `T`. -unsafe impl Send for EmptySubscription {} +unsafe impl Send for EmptySubscription {} -impl EmptySubscription { +impl EmptySubscription { /// Construct a new empty subscription pub fn new() -> Self { EmptySubscription { From df39c15be04910ee47494ccb14ea4474191c18a2 Mon Sep 17 00:00:00 2001 From: tyranron Date: Fri, 29 May 2020 18:38:37 +0300 Subject: [PATCH 10/29] Test trait implementation and pack Send + Sync into trait object --- .../fail/union/derive_enum_field.stderr | 9 ++ .../juniper_tests/src/codegen/union_attr.rs | 122 ++++++++++++++++-- .../juniper_tests/src/codegen/union_derive.rs | 4 +- juniper_codegen/src/graphql_union/mod.rs | 2 +- 4 files changed, 120 insertions(+), 17 deletions(-) diff --git a/integration_tests/codegen_fail/fail/union/derive_enum_field.stderr b/integration_tests/codegen_fail/fail/union/derive_enum_field.stderr index 58d13adf2..27ffee901 100644 --- a/integration_tests/codegen_fail/fail/union/derive_enum_field.stderr +++ b/integration_tests/codegen_fail/fail/union/derive_enum_field.stderr @@ -1,3 +1,12 @@ +error[E0277]: the trait bound `Test: juniper::types::marker::GraphQLObjectType` is not satisfied + --> $DIR/derive_enum_field.rs:7:10 + | +7 | #[derive(juniper::GraphQLUnion)] + | ^^^^^^^^^^^^^^^^^^^^^ the trait `juniper::types::marker::GraphQLObjectType` 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) + error[E0277]: the trait bound `Test: juniper::types::marker::GraphQLObjectType<__S>` is not satisfied --> $DIR/derive_enum_field.rs:7:10 | diff --git a/integration_tests/juniper_tests/src/codegen/union_attr.rs b/integration_tests/juniper_tests/src/codegen/union_attr.rs index 498b911d3..27beddce9 100644 --- a/integration_tests/juniper_tests/src/codegen/union_attr.rs +++ b/integration_tests/juniper_tests/src/codegen/union_attr.rs @@ -1,4 +1,10 @@ -use juniper::{graphql_union, GraphQLObject}; +use juniper::{graphql_object, graphql_union, GraphQLObject}; + +#[cfg(test)] +use juniper::{ + self, execute, DefaultScalarValue, EmptyMutation, EmptySubscription, GraphQLType, RootNode, + Value, Variables, +}; #[derive(GraphQLObject)] struct Human { @@ -14,7 +20,7 @@ struct Droid { #[graphql_union(name = "Character")] #[graphql_union(description = "A Collection of things")] -trait Character { +trait Character { fn as_human(&self, _: &()) -> Option<&Human> { None } @@ -22,29 +28,117 @@ trait Character { None } #[graphql_union(ignore)] - fn some(&self); + fn some(&self) {} } -/* -impl Character for Human { - fn as_human(&self) -> Option<&Human> { +impl Character for Human { + fn as_human(&self, _: &()) -> Option<&Human> { Some(&self) } } -impl Character for Droid { +impl Character for Droid { fn as_droid(&self) -> Option<&Droid> { Some(&self) } } -#[juniper::graphql_union] -impl<'a> GraphQLUnion for &'a dyn Character { - fn resolve(&self) { - match self { - Human => self.as_human(), - Droid => self.as_droid(), +pub struct Query { + is_human: bool, +} + +#[graphql_object] +impl Query { + fn context(&self) -> Box + Send + Sync> { + let ch: Box + Send + Sync> = if self.is_human { + Box::new(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }) + } else { + Box::new(Droid { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }) + }; + ch + } +} + +const DOC: &str = r#" +{ + context { + ... on Human { + humanId: id + homePlanet + } + ... on Droid { + droidId: id + primaryFunction } } +}"#; + +#[tokio::test] +async fn resolves_human() { + let schema = RootNode::new( + Query { is_human: true }, + EmptyMutation::<()>::new(), + EmptySubscription::<()>::new(), + ); + + let actual = execute(DOC, None, &schema, &Variables::new(), &()).await; + + let expected = Ok(( + Value::object( + vec![( + "context", + Value::object( + vec![ + ("humanId", Value::scalar("human-32".to_string())), + ("homePlanet", Value::scalar("earth".to_string())), + ] + .into_iter() + .collect(), + ), + )] + .into_iter() + .collect(), + ), + vec![], + )); + + assert_eq!(actual, expected); +} + +#[tokio::test] +async fn resolves_droid() { + let schema = RootNode::new( + Query { is_human: false }, + EmptyMutation::<()>::new(), + EmptySubscription::<()>::new(), + ); + + let actual = execute(DOC, None, &schema, &Variables::new(), &()).await; + + let expected = Ok(( + Value::object( + vec![( + "context", + Value::object( + vec![ + ("droidId", Value::scalar("droid-99".to_string())), + ("primaryFunction", Value::scalar("run".to_string())), + ] + .into_iter() + .collect(), + ), + )] + .into_iter() + .collect(), + ), + vec![], + )); + + assert_eq!(actual, expected); } -*/ diff --git a/integration_tests/juniper_tests/src/codegen/union_derive.rs b/integration_tests/juniper_tests/src/codegen/union_derive.rs index 5dabea7a8..017c3cb59 100644 --- a/integration_tests/juniper_tests/src/codegen/union_derive.rs +++ b/integration_tests/juniper_tests/src/codegen/union_derive.rs @@ -3,7 +3,7 @@ use derive_more::From; #[cfg(test)] use fnv::FnvHashMap; -use juniper::{GraphQLObject, GraphQLUnion}; +use juniper::{graphql_object, GraphQLObject, GraphQLUnion}; #[cfg(test)] use juniper::{ @@ -188,7 +188,7 @@ pub enum CharacterCompat { pub struct Query; -#[juniper::graphql_object( +#[graphql_object( Context = CustomContext, )] impl Query { diff --git a/juniper_codegen/src/graphql_union/mod.rs b/juniper_codegen/src/graphql_union/mod.rs index f7d05aa13..e2b5052aa 100644 --- a/juniper_codegen/src/graphql_union/mod.rs +++ b/juniper_codegen/src/graphql_union/mod.rs @@ -428,7 +428,7 @@ impl ToTokens for UnionDefinition { let mut ty_full = quote! { #ty#ty_generics }; if self.is_trait_object { - ty_full = quote! { dyn #ty_full + '__obj }; + ty_full = quote! { dyn #ty_full + '__obj + Send + Sync }; } let type_impl = quote! { From 4feb1415bf2f94b784ae78a06dfdb6755f147aed Mon Sep 17 00:00:00 2001 From: tyranron Date: Fri, 29 May 2020 19:23:53 +0300 Subject: [PATCH 11/29] Support custom resolver for traits --- .../juniper_tests/src/codegen/union_attr.rs | 87 ++++++++++++++++--- juniper_codegen/src/graphql_union/attr.rs | 35 +++++++- 2 files changed, 109 insertions(+), 13 deletions(-) diff --git a/integration_tests/juniper_tests/src/codegen/union_attr.rs b/integration_tests/juniper_tests/src/codegen/union_attr.rs index 27beddce9..310901e01 100644 --- a/integration_tests/juniper_tests/src/codegen/union_attr.rs +++ b/integration_tests/juniper_tests/src/codegen/union_attr.rs @@ -1,3 +1,5 @@ +use std::any::Any; + use juniper::{graphql_object, graphql_union, GraphQLObject}; #[cfg(test)] @@ -18,8 +20,15 @@ struct Droid { primary_function: String, } +#[derive(GraphQLObject)] +struct Jedi { + id: String, + rank: String, +} + #[graphql_union(name = "Character")] #[graphql_union(description = "A Collection of things")] +#[graphql_union(on Jedi = resolve_character_jedi)] trait Character { fn as_human(&self, _: &()) -> Option<&Human> { None @@ -28,6 +37,10 @@ trait Character { None } #[graphql_union(ignore)] + fn as_jedi(&self) -> Option<&Jedi> { + None + } + #[graphql_union(ignore)] fn some(&self) {} } @@ -43,23 +56,41 @@ impl Character for Droid { } } -pub struct Query { - is_human: bool, +impl Character for Jedi { + fn as_jedi(&self) -> Option<&Jedi> { + Some(&self) + } +} + +fn resolve_character_jedi<'a, T>( + jedi: &'a (dyn Character + Send + Sync), + _: &(), +) -> Option<&'a Jedi> { + jedi.as_jedi() +} + +enum Query { + Human, + Droid, + Jedi, } #[graphql_object] impl Query { fn context(&self) -> Box + Send + Sync> { - let ch: Box + Send + Sync> = if self.is_human { - Box::new(Human { + let ch: Box + Send + Sync> = match self { + Self::Human => Box::new(Human { id: "human-32".to_string(), home_planet: "earth".to_string(), - }) - } else { - Box::new(Droid { + }), + Self::Droid => Box::new(Droid { id: "droid-99".to_string(), primary_function: "run".to_string(), - }) + }), + Self::Jedi => Box::new(Jedi { + id: "Obi Wan Kenobi".to_string(), + rank: "Master".to_string(), + }), }; ch } @@ -76,13 +107,17 @@ const DOC: &str = r#" droidId: id primaryFunction } + ... on Jedi { + jediId: id + rank + } } }"#; #[tokio::test] async fn resolves_human() { let schema = RootNode::new( - Query { is_human: true }, + Query::Human, EmptyMutation::<()>::new(), EmptySubscription::<()>::new(), ); @@ -114,7 +149,7 @@ async fn resolves_human() { #[tokio::test] async fn resolves_droid() { let schema = RootNode::new( - Query { is_human: false }, + Query::Droid, EmptyMutation::<()>::new(), EmptySubscription::<()>::new(), ); @@ -142,3 +177,35 @@ async fn resolves_droid() { assert_eq!(actual, expected); } + +#[tokio::test] +async fn resolves_jedi() { + let schema = RootNode::new( + Query::Jedi, + EmptyMutation::<()>::new(), + EmptySubscription::<()>::new(), + ); + + let actual = execute(DOC, None, &schema, &Variables::new(), &()).await; + + let expected = Ok(( + Value::object( + vec![( + "context", + Value::object( + vec![ + ("jediId", Value::scalar("Obi Wan Kenobi".to_string())), + ("rank", Value::scalar("Master".to_string())), + ] + .into_iter() + .collect(), + ), + )] + .into_iter() + .collect(), + ), + vec![], + )); + + assert_eq!(actual, expected); +} diff --git a/juniper_codegen/src/graphql_union/attr.rs b/juniper_codegen/src/graphql_union/attr.rs index 29beeb8e4..579904aaf 100644 --- a/juniper_codegen/src/graphql_union/attr.rs +++ b/juniper_codegen/src/graphql_union/attr.rs @@ -59,8 +59,6 @@ pub fn expand(attr_args: TokenStream, body: TokenStream, mode: Mode) -> syn::Res let meta = UnionMeta::from_attrs(attr_path, &trait_attrs)?; - //panic!("{:?}", meta); - let trait_span = ast.span(); let trait_ident = &ast.ident; @@ -93,7 +91,38 @@ pub fn expand(attr_args: TokenStream, body: TokenStream, mode: Mode) -> syn::Res if !meta.custom_resolvers.is_empty() { let crate_path = mode.crate_path(); - // TODO: modify variants + // TODO: refactor into separate function + for (ty, rslvr) in meta.custom_resolvers { + let span = rslvr.span_joined(); + + let resolver_fn = rslvr.into_inner(); + let resolver_code = parse_quote! { + #resolver_fn(self, #crate_path::FromContext::from(context)) + }; + // Doing this may be quite an expensive, because resolving may contain some heavy + // computation, so we're preforming it twice. Unfortunately, we have no other options + // here, until the `juniper::GraphQLType` itself will allow to do it in some cleverer + // way. + let resolver_check = parse_quote! { + ({ #resolver_code } as ::std::option::Option<&#ty>).is_some() + }; + + // TODO: We may not check here for existence, as we do the duplication check when + // parsing methods. + if let Some(var) = variants.iter_mut().find(|v| v.ty == ty) { + var.resolver_code = resolver_code; + var.resolver_check = resolver_check; + var.span = span; + } else { + variants.push(UnionVariantDefinition { + ty, + resolver_code, + resolver_check, + enum_path: None, + span, + }) + } + } } if variants.is_empty() { SCOPE.custom(trait_span, "expects at least one union variant"); From 38464a44ceff753715c14e3839d50bacd7af1324 Mon Sep 17 00:00:00 2001 From: tyranron Date: Mon, 1 Jun 2020 11:50:10 +0300 Subject: [PATCH 12/29] Strip out old code --- juniper_codegen/src/derive_union.rs | 182 ---------------- juniper_codegen/src/graphql_union/attr.rs | 1 - juniper_codegen/src/impl_union.rs | 223 ------------------- juniper_codegen/src/lib.rs | 6 - juniper_codegen/src/result.rs | 1 - juniper_codegen/src/util/mod.rs | 247 +--------------------- juniper_codegen/src/util/parse_impl.rs | 30 --- 7 files changed, 2 insertions(+), 688 deletions(-) delete mode 100644 juniper_codegen/src/derive_union.rs delete mode 100644 juniper_codegen/src/impl_union.rs diff --git a/juniper_codegen/src/derive_union.rs b/juniper_codegen/src/derive_union.rs deleted file mode 100644 index 8417ef783..000000000 --- a/juniper_codegen/src/derive_union.rs +++ /dev/null @@ -1,182 +0,0 @@ -use proc_macro2::TokenStream; -use proc_macro_error::ResultExt as _; -use quote::quote; -use syn::{self, ext::IdentExt, spanned::Spanned, Data, Fields}; - -use crate::{ - result::{GraphQLScope, UnsupportedAttribute}, - util::{self, span_container::SpanContainer, Mode}, -}; - -const SCOPE: GraphQLScope = GraphQLScope::DeriveUnion; - -pub fn expand(input: TokenStream, mode: Mode) -> syn::Result { - let ast = syn::parse2::(input).unwrap_or_abort(); - let ast_span = ast.span(); - - let enum_fields = match ast.data { - Data::Enum(data) => data.variants, - Data::Struct(_) => unimplemented!(), - _ => return Err(SCOPE.custom_error(ast_span, "can only be applied to enums and structs")), - }; - - // Parse attributes. - let attrs = util::ObjectAttributes::from_attrs(&ast.attrs)?; - - let ident = &ast.ident; - 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 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 let Some(ident) = field_attrs.skip { - SCOPE.unsupported_attribute_within(ident.span(), UnsupportedAttribute::Skip); - return None; - } - - let variant_name = field.ident; - let name = field_attrs - .name - .clone() - .map(SpanContainer::into_inner) - .unwrap_or_else(|| util::to_camel_case(&variant_name.unraw().to_string())); - - let resolver_code = quote!( - #ident :: #variant_name - ); - - let _type = match field.fields { - Fields::Unnamed(inner) => { - let mut iter = inner.unnamed.iter(); - let first = iter.next().unwrap(); - - if iter.next().is_some() { - SCOPE.custom( - inner.span(), - "all members must be unnamed with a single element e.g. Some(T)", - ); - } - - first.ty.clone() - } - _ => { - SCOPE.custom( - variant_name.span(), - "only unnamed fields with a single element are allowed, e.g., Some(T)", - ); - - return None; - } - }; - - if let Some(description) = field_attrs.description { - SCOPE.unsupported_attribute_within( - description.span_ident(), - UnsupportedAttribute::Description, - ); - } - - if let Some(default) = field_attrs.default { - SCOPE.unsupported_attribute_within( - default.span_ident(), - UnsupportedAttribute::Default, - ); - } - - if name.starts_with("__") { - SCOPE.no_double_underscore(if let Some(name) = field_attrs.name { - name.span_ident() - } else { - variant_name.span() - }); - } - - Some(util::GraphQLTypeDefinitionField { - name, - _type, - args: Vec::new(), - description: None, - deprecation: field_attrs.deprecation.map(SpanContainer::into_inner), - resolver_code, - is_type_inferred: true, - is_async: false, - default: None, - span, - }) - }) - .collect::>(); - - // Early abort after checking all fields - proc_macro_error::abort_if_dirty(); - - if !attrs.interfaces.is_empty() { - attrs.interfaces.iter().for_each(|elm| { - SCOPE.unsupported_attribute(elm.span(), UnsupportedAttribute::Interface) - }); - } - - if fields.is_empty() { - SCOPE.not_empty(ast_span); - } - - if name.starts_with("__") && matches!(mode, Mode::Public) { - SCOPE.no_double_underscore(if let Some(name) = attrs.name { - name.span_ident() - } else { - ident.span() - }); - } - - // NOTICE: This is not an optimal implementation. It is possible - // to bypass this check by using a full qualified path instead - // (crate::Test vs Test). Since this requirement is mandatory, the - // `std::convert::Into` implementation is used to enforce this - // requirement. However, due to the bad error message this - // implementation should stay and provide guidance. - let all_variants_different = { - let mut all_types: Vec<_> = fields.iter().map(|field| &field._type).collect(); - let before = all_types.len(); - all_types.dedup(); - before == all_types.len() - }; - - if !all_variants_different { - SCOPE.custom(ident.span(), "each variant must have a different type"); - } - - // Early abort after GraphQL properties - proc_macro_error::abort_if_dirty(); - - let definition = util::GraphQLTypeDefiniton { - name, - _type: syn::parse_str(&ast.ident.to_string()).unwrap(), - context: attrs.context.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(), - mode, - }; - - Ok(definition.into_union_tokens()) -} diff --git a/juniper_codegen/src/graphql_union/attr.rs b/juniper_codegen/src/graphql_union/attr.rs index 579904aaf..edfb57a58 100644 --- a/juniper_codegen/src/graphql_union/attr.rs +++ b/juniper_codegen/src/graphql_union/attr.rs @@ -1,7 +1,6 @@ use std::{mem, ops::Deref as _}; use proc_macro2::{Span, TokenStream}; -use proc_macro_error::ResultExt as _; use quote::{quote, ToTokens as _}; use syn::{self, ext::IdentExt as _, parse_quote, spanned::Spanned as _}; diff --git a/juniper_codegen/src/impl_union.rs b/juniper_codegen/src/impl_union.rs deleted file mode 100644 index 539c58701..000000000 --- a/juniper_codegen/src/impl_union.rs +++ /dev/null @@ -1,223 +0,0 @@ -use proc_macro2::TokenStream; -use quote::quote; -use syn::{ext::IdentExt, spanned::Spanned}; - -use crate::{ - result::GraphQLScope, - util::{self, span_container::SpanContainer, Mode}, -}; - -const SCOPE: GraphQLScope = GraphQLScope::ImplUnion; - -pub fn expand(attrs: TokenStream, body: TokenStream, mode: Mode) -> syn::Result { - let is_internal = matches!(mode, Mode::Internal); - - let body_span = body.span(); - let _impl = util::parse_impl::ImplBlock::parse(attrs, body)?; - - // FIXME: what is the purpose of this construct? - // Validate trait target name, if present. - if let Some((name, path)) = &_impl.target_trait { - if !(name == "GraphQLUnion" || name == "juniper.GraphQLUnion") { - return Err(SCOPE.custom_error( - path.span(), - "Invalid impl target trait: expected 'GraphQLUnion'", - )); - } - } - - let type_ident = &_impl.type_ident; - let name = _impl - .attrs - .name - .clone() - .map(SpanContainer::into_inner) - .unwrap_or_else(|| type_ident.unraw().to_string()); - let crate_name = util::juniper_path(is_internal); - - let scalar = _impl - .attrs - .scalar - .as_ref() - .map(|s| quote!( #s )) - .unwrap_or_else(|| { - quote! { #crate_name::DefaultScalarValue } - }); - - let method = _impl - .methods - .iter() - .find(|&m| _impl.parse_resolve_method(&m).is_ok()); - - let method = match method { - Some(method) => method, - None => { - return Err(SCOPE.custom_error( - body_span, - "expected exactly one method with signature: fn resolve(&self) { ... }", - )) - } - }; - - let resolve_args = _impl.parse_resolve_method(method)?; - - let stmts = &method.block.stmts; - let body_raw = quote!( #( #stmts )* ); - let body = syn::parse::(body_raw.into())?; - - if body.variants.is_empty() { - SCOPE.not_empty(method.span()) - } - - proc_macro_error::abort_if_dirty(); - - let meta_types = body.variants.iter().map(|var| { - let var_ty = &var.ty; - - quote! { - registry.get_type::<&#var_ty>(&(())), - } - }); - - let concrete_type_resolver = body.variants.iter().map(|var| { - let var_ty = &var.ty; - let resolve = &var.resolver; - - quote! { - if ({#resolve} as std::option::Option<&#var_ty>).is_some() { - return <#var_ty as #crate_name::GraphQLType<#scalar>>::name(&()).unwrap().to_string(); - } - } - }); - - let resolve_into_type = body.variants.iter().map(|var| { - let var_ty = &var.ty; - let resolve = &var.resolver; - - quote! { - if type_name == (<#var_ty as #crate_name::GraphQLType<#scalar>>::name(&())).unwrap() { - return executor.resolve(&(), &{ #resolve }); - } - } - }); - - let generics = _impl.generics; - let (impl_generics, _, where_clause) = generics.split_for_impl(); - - let description = match _impl.description.as_ref() { - Some(value) => quote!( .description( #value ) ), - None => quote!(), - }; - let context = _impl - .attrs - .context - .map(|c| quote! { #c }) - .unwrap_or_else(|| quote! { () }); - - let ty = _impl.target_type; - - let object_marks = body.variants.iter().map(|field| { - let _ty = &field.ty; - quote!( - <#_ty as #crate_name::marker::GraphQLObjectType<#scalar>>::mark(); - ) - }); - - let output = quote! { - impl #impl_generics #crate_name::marker::IsOutputType<#scalar> for #ty #where_clause { - fn mark() { - #( #object_marks )* - } - } - - impl #impl_generics #crate_name::GraphQLType<#scalar> for #ty #where_clause - { - type Context = #context; - type TypeInfo = (); - - fn name(_ : &Self::TypeInfo) -> Option<&str> { - Some(#name) - } - - fn meta<'r>( - info: &Self::TypeInfo, - registry: &mut #crate_name::Registry<'r, #scalar> - ) -> #crate_name::meta::MetaType<'r, #scalar> - where - #scalar: 'r, - { - let types = &[ - #( #meta_types )* - ]; - registry.build_union_type::<#ty>( - info, types - ) - #description - .into_meta() - } - - #[allow(unused_variables)] - fn concrete_type_name(&self, context: &Self::Context, _info: &Self::TypeInfo) -> String { - #( #concrete_type_resolver )* - - panic!("Concrete type not handled by instance resolvers on {}", #name); - } - - fn resolve_into_type( - &self, - _info: &Self::TypeInfo, - type_name: &str, - _: Option<&[#crate_name::Selection<#scalar>]>, - executor: &#crate_name::Executor, - ) -> #crate_name::ExecutionResult<#scalar> { - let context = &executor.context(); - #( #resolve_args )* - - #( #resolve_into_type )* - - panic!("Concrete type not handled by instance resolvers on {}", #name); - } - } - - - }; - - Ok(output.into()) -} - -struct ResolverVariant { - pub ty: syn::Type, - pub resolver: syn::Expr, -} - -struct ResolveBody { - pub variants: Vec, -} - -impl syn::parse::Parse for ResolveBody { - fn parse(input: syn::parse::ParseStream) -> Result { - input.parse::()?; - input.parse::()?; - - let match_body; - syn::braced!( match_body in input ); - - let mut variants = Vec::new(); - while !match_body.is_empty() { - let ty = match_body.parse::()?; - match_body.parse::()?; - let resolver = match_body.parse::()?; - - variants.push(ResolverVariant { ty, resolver }); - - // Optinal trailing comma. - match_body.parse::().ok(); - } - - if !input.is_empty() { - return Err(input.error("unexpected input")); - } - - Ok(Self { variants }) - } -} diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs index 0a10d4d6a..dc39d3faa 100644 --- a/juniper_codegen/src/lib.rs +++ b/juniper_codegen/src/lib.rs @@ -16,10 +16,8 @@ mod derive_enum; mod derive_input_object; mod derive_object; mod derive_scalar_value; -mod derive_union; mod impl_object; mod impl_scalar; -mod impl_union; mod graphql_union; @@ -551,7 +549,6 @@ pub fn graphql_subscription_internal(args: TokenStream, input: TokenStream) -> T #[proc_macro_error] #[proc_macro_derive(GraphQLUnion, attributes(graphql))] pub fn derive_union(input: TokenStream) -> TokenStream { - //derive_union::expand(input.into(), Mode::Public) self::graphql_union::derive::expand(input.into(), Mode::Public) .unwrap_or_abort() .into() @@ -561,7 +558,6 @@ pub fn derive_union(input: TokenStream) -> TokenStream { #[proc_macro_derive(GraphQLUnionInternal, attributes(graphql))] #[doc(hidden)] pub fn derive_union_internal(input: TokenStream) -> TokenStream { - //derive_union::expand(input.into(), Mode::Internal) self::graphql_union::derive::expand(input.into(), Mode::Internal) .unwrap_or_abort() .into() @@ -570,7 +566,6 @@ pub fn derive_union_internal(input: TokenStream) -> TokenStream { #[proc_macro_error] #[proc_macro_attribute] pub fn graphql_union(attr: TokenStream, body: TokenStream) -> TokenStream { - //impl_union::expand(attr.into(), body.into(), Mode::Public) self::graphql_union::attr::expand(attr.into(), body.into(), Mode::Public) .unwrap_or_abort() .into() @@ -580,7 +575,6 @@ pub fn graphql_union(attr: TokenStream, body: TokenStream) -> TokenStream { #[proc_macro_attribute] #[doc(hidden)] pub fn graphql_union_internal(attr: TokenStream, body: TokenStream) -> TokenStream { - //impl_union::expand(attr.into(), body.into(), Mode::Internal) self::graphql_union::attr::expand(attr.into(), body.into(), Mode::Internal) .unwrap_or_abort() .into() diff --git a/juniper_codegen/src/result.rs b/juniper_codegen/src/result.rs index 754e2c998..1c28fef02 100644 --- a/juniper_codegen/src/result.rs +++ b/juniper_codegen/src/result.rs @@ -53,7 +53,6 @@ pub enum UnsupportedAttribute { Skip, Interface, Scalar, - Description, Deprecation, Default, } diff --git a/juniper_codegen/src/util/mod.rs b/juniper_codegen/src/util/mod.rs index f5f838687..82351e616 100644 --- a/juniper_codegen/src/util/mod.rs +++ b/juniper_codegen/src/util/mod.rs @@ -20,11 +20,6 @@ use syn::{ pub use self::{mode::Mode, option_ext::OptionExt}; -pub fn juniper_path(is_internal: bool) -> syn::Path { - let name = if is_internal { "crate" } else { "juniper" }; - syn::parse_str::(name).unwrap() -} - /// Returns the name of a type. /// If the type does not end in a simple ident, `None` is returned. pub fn name_of_type(ty: &syn::Type) -> Option { @@ -80,7 +75,8 @@ pub fn type_is_identifier_ref(ty: &syn::Type, name: &str) -> bool { } } -/// Retrieves the innermost non-parenthesized [`syn::Type`] from the given one. +/// Retrieves the innermost non-parenthesized [`syn::Type`] from the given one (unwraps nested +/// [`syn::TypeParen`]s asap). pub fn unparenthesize(ty: &syn::Type) -> &syn::Type { match ty { syn::Type::Paren(ty) => unparenthesize(ty.elem.deref()), @@ -1327,245 +1323,6 @@ impl GraphQLTypeDefiniton { ) } - pub fn into_union_tokens(self) -> TokenStream { - let crate_path = self.mode.crate_path(); - - 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!(#crate_path::DefaultScalarValue) - } - }); - - let description = self - .description - .as_ref() - .map(|description| quote!( .description(#description) )); - - let meta_types = self.fields.iter().map(|field| { - let var_ty = &field._type; - - quote! { - registry.get_type::<&#var_ty>(&(())), - } - }); - - let matcher_variants = self - .fields - .iter() - .map(|field| { - let var_ty = &field._type; - let resolver_code = &field.resolver_code; - - quote!( - #resolver_code(ref x) => <#var_ty as #crate_path::GraphQLType<#scalar>>::name(&()).unwrap().to_string(), - ) - }); - - let concrete_type_resolver = quote!( - match self { - #( #matcher_variants )* - } - ); - - let matcher_expr: Vec<_> = self - .fields - .iter() - .map(|field| { - let resolver_code = &field.resolver_code; - - quote!( - match self { #resolver_code(ref val) => Some(val), _ => None, } - ) - }) - .collect(); - - let resolve_into_type = self.fields.iter().zip(matcher_expr.iter()).map(|(field, expr)| { - let var_ty = &field._type; - - quote! { - if type_name == (<#var_ty as #crate_path::GraphQLType<#scalar>>::name(&())).unwrap() { - return #crate_path::IntoResolvable::into( - { #expr }, - executor.context() - ) - .and_then(|res| { - match res { - Some((ctx, r)) => executor.replaced_context(ctx).resolve_with_ctx(&(), &r), - None => Ok(#crate_path::Value::null()), - } - }); - } - } - }); - - let resolve_into_type_async = self.fields.iter().zip(matcher_expr.iter()).map(|(field, expr)| { - let var_ty = &field._type; - - quote! { - if type_name == (<#var_ty as #crate_path::GraphQLType<#scalar>>::name(&())).unwrap() { - let inner_res = #crate_path::IntoResolvable::into( - { #expr }, - executor.context() - ); - - let f = async move { - match inner_res { - Ok(Some((ctx, r))) => { - let subexec = executor.replaced_context(ctx); - subexec.resolve_with_ctx_async(&(), &r).await - }, - Ok(None) => Ok(#crate_path::Value::null()), - Err(e) => Err(e), - } - }; - use #crate_path::futures::future; - return future::FutureExt::boxed(f); - } - } - }); - - 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: #crate_path::ScalarValue)); - } - - 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_impl = quote!( - impl#impl_generics #crate_path::GraphQLTypeAsync<#scalar> for #ty - #where_async - { - fn resolve_into_type_async<'b>( - &'b self, - _info: &'b Self::TypeInfo, - type_name: &str, - _: Option<&'b [#crate_path::Selection<'b, #scalar>]>, - executor: &'b #crate_path::Executor<'b, 'b, Self::Context, #scalar> - ) -> #crate_path::BoxFuture<'b, #crate_path::ExecutionResult<#scalar>> { - let context = &executor.context(); - - #( #resolve_into_type_async )* - - panic!("Concrete type not handled by instance resolvers on {}", #name); - } - } - ); - - let convesion_impls = self.fields.iter().map(|field| { - let variant_ty = &field._type; - let resolver_code = &field.resolver_code; - - quote!( - impl std::convert::From<#variant_ty> for #ty { - fn from(val: #variant_ty) -> Self { - #resolver_code(val) - } - } - ) - }); - - let object_marks = self.fields.iter().map(|field| { - let _ty = &field._type; - quote!( - <#_ty as #crate_path::marker::GraphQLObjectType<#scalar>>::mark(); - ) - }); - - let mut type_impl = quote! { - #( #convesion_impls )* - - impl #impl_generics #crate_path::marker::IsOutputType<#scalar> for #ty #where_clause { - fn mark() { - #( #object_marks )* - } - } - - impl #impl_generics #crate_path::GraphQLType<#scalar> for #ty #where_clause - { - type Context = #context; - type TypeInfo = (); - - fn name(_ : &Self::TypeInfo) -> Option<&str> { - Some(#name) - } - - fn meta<'r>( - info: &Self::TypeInfo, - registry: &mut #crate_path::Registry<'r, #scalar> - ) -> #crate_path::meta::MetaType<'r, #scalar> - where - #scalar: 'r, - { - let types = &[ - #( #meta_types )* - ]; - registry.build_union_type::<#ty>( - info, types - ) - #description - .into_meta() - } - - #[allow(unused_variables)] - fn concrete_type_name(&self, context: &Self::Context, _info: &Self::TypeInfo) -> String { - #concrete_type_resolver - } - - fn resolve_into_type( - &self, - _info: &Self::TypeInfo, - type_name: &str, - _: Option<&[#crate_path::Selection<#scalar>]>, - executor: &#crate_path::Executor, - ) -> #crate_path::ExecutionResult<#scalar> { - let context = &executor.context(); - - #( #resolve_into_type )* - - panic!("Concrete type not handled by instance resolvers on {}", #name); - } - } - }; - - if !self.no_async { - type_impl.extend(async_type_impl) - } - - type_impl - } - pub fn into_enum_tokens(self, juniper_crate_name: &str) -> TokenStream { let juniper_crate_name = syn::parse_str::(juniper_crate_name).unwrap(); diff --git a/juniper_codegen/src/util/parse_impl.rs b/juniper_codegen/src/util/parse_impl.rs index 93bc26bd6..0aa8392dd 100644 --- a/juniper_codegen/src/util/parse_impl.rs +++ b/juniper_codegen/src/util/parse_impl.rs @@ -19,36 +19,6 @@ pub struct ImplBlock { } impl ImplBlock { - /// Parse a `fn resolve()` method declaration found in most - /// generators which rely on `impl` blocks. - pub fn parse_resolve_method( - &self, - method: &syn::ImplItemMethod, - ) -> syn::Result> { - if method.sig.ident != "resolve" { - return Err(syn::Error::new( - method.sig.ident.span(), - "expect the method named `resolve`", - )); - } - - if let syn::ReturnType::Type(_, _) = &method.sig.output { - return Err(syn::Error::new( - method.sig.output.span(), - "method must not have a declared return type", - )); - } - - //NOTICE: `fn resolve()` is a subset of `fn () -> ` - self.parse_method(method, false, |captured, _, _| { - Err(syn::Error::new( - captured.span(), - "only executor or context types are allowed", - )) - }) - .map(|(tokens, _empty)| tokens) - } - /// Parse a `fn () -> ` method declaration found in /// objects. pub fn parse_method< From cc07fe246a73903749961acb4e6effee90d0c060 Mon Sep 17 00:00:00 2001 From: tyranron Date: Mon, 1 Jun 2020 13:33:24 +0300 Subject: [PATCH 13/29] Polish common code --- juniper_codegen/src/graphql_union/mod.rs | 237 +++++++++++++++-------- 1 file changed, 157 insertions(+), 80 deletions(-) diff --git a/juniper_codegen/src/graphql_union/mod.rs b/juniper_codegen/src/graphql_union/mod.rs index e2b5052aa..d30c07955 100644 --- a/juniper_codegen/src/graphql_union/mod.rs +++ b/juniper_codegen/src/graphql_union/mod.rs @@ -1,3 +1,7 @@ +//! Code generation for [GraphQL union][1] type. +//! +//! [1]: https://spec.graphql.org/June2018/#sec-Unions + pub mod attr; pub mod derive; @@ -15,8 +19,59 @@ use crate::util::{ filter_attrs, get_doc_comment, span_container::SpanContainer, Mode, OptionExt as _, }; -/// Available metadata behind `#[graphql]` (or `#[graphql_union]`) attribute when generating code -/// for [GraphQL union][1] type. +/// Attempts to merge an [`Option`]ed `$field` of a `$self` struct with the same `$field` of +/// `$another` struct. If both are [`Some`], then throws a duplication error with a [`Span`] related +/// to the `$another` struct (a later one). +/// +/// The type of [`Span`] may be explicitly specified as one of the [`SpanContainer`] methods. +/// By default, [`SpanContainer::span_ident`] is used. +macro_rules! try_merge_opt { + ($field:ident: $self:ident, $another:ident => $span:ident) => {{ + if let Some(v) = $self.$field { + $another + .$field + .replace(v) + .none_or_else(|dup| dup_attr_err(dup.$span()))?; + } + $another.$field + }}; + + ($field:ident: $self:ident, $another:ident) => { + try_merge_opt!($field: $self, $another => span_ident) + }; +} + +/// Attempts to merge a [`HashMap`]ed `$field` of a `$self` struct with the same `$field` of +/// `$another` struct. If some [`HashMap`] entries are duplicated, then throws a duplication error +/// with a [`Span`] related to the `$another` struct (a later one). +/// +/// The type of [`Span`] may be explicitly specified as one of the [`SpanContainer`] methods. +/// By default, [`SpanContainer::span_ident`] is used. +macro_rules! try_merge_hashmap { + ($field:ident: $self:ident, $another:ident => $span:ident) => {{ + if !$self.$field.is_empty() { + for (ty, rslvr) in $self.$field { + $another + .$field + .insert(ty, rslvr) + .none_or_else(|dup| dup_attr_err(dup.$span()))?; + } + } + $another.$field + }}; + + ($field:ident: $self:ident, $another:ident) => { + try_merge_hashmap!($field: $self, $another => span_ident) + }; +} + +/// Creates and returns duplication error pointing to the given `span`. +fn dup_attr_err(span: Span) -> syn::Error { + syn::Error::new(span, "duplicated attribute") +} + +/// Available metadata (arguments) behind `#[graphql]` (or `#[graphql_union]`) attribute when +/// generating code for [GraphQL union][1] type. /// /// [1]: https://spec.graphql.org/June2018/#sec-Unions #[derive(Debug, Default)] @@ -39,6 +94,8 @@ struct UnionMeta { /// Explicitly specified type of `juniper::Context` to use for resolving this [GraphQL union][1] /// type with. /// + /// If absent, then unit type `()` is assumed as type of `juniper::Context`. + /// /// [1]: https://spec.graphql.org/June2018/#sec-Unions pub context: Option>, @@ -48,7 +105,7 @@ struct UnionMeta { /// If absent, then generated code will be generic over any `juniper::ScalarValue` type, which, /// in turn, requires all [union][1] variants to be generic over any `juniper::ScalarValue` type /// too. That's why this type should be specified only if one of the variants implements - /// `juniper::GraphQLType` in non-generic over `juniper::ScalarValue` type way. + /// `juniper::GraphQLType` in a non-generic way over `juniper::ScalarValue` type. /// /// [1]: https://spec.graphql.org/June2018/#sec-Unions pub scalar: Option>, @@ -80,7 +137,7 @@ impl Parse for UnionMeta { Some(name.span()), name.value(), )) - .none_or_else(|_| syn::Error::new(ident.span(), "duplicated attribute"))? + .none_or_else(|_| dup_attr_err(ident.span()))? } "desc" | "description" => { input.parse::()?; @@ -92,7 +149,7 @@ impl Parse for UnionMeta { Some(desc.span()), desc.value(), )) - .none_or_else(|_| syn::Error::new(ident.span(), "duplicated attribute"))? + .none_or_else(|_| dup_attr_err(ident.span()))? } "ctx" | "context" | "Context" => { input.parse::()?; @@ -100,7 +157,7 @@ impl Parse for UnionMeta { output .context .replace(SpanContainer::new(ident.span(), Some(ctx.span()), ctx)) - .none_or_else(|_| syn::Error::new(ident.span(), "duplicated attribute"))? + .none_or_else(|_| dup_attr_err(ident.span()))? } "scalar" | "Scalar" | "ScalarValue" => { input.parse::()?; @@ -108,7 +165,7 @@ impl Parse for UnionMeta { output .scalar .replace(SpanContainer::new(ident.span(), Some(scl.span()), scl)) - .none_or_else(|_| syn::Error::new(ident.span(), "duplicated attribute"))? + .none_or_else(|_| dup_attr_err(ident.span()))? } "on" => { let ty = input.parse::()?; @@ -119,7 +176,7 @@ impl Parse for UnionMeta { output .custom_resolvers .insert(ty, rslvr_spanned) - .none_or_else(|_| syn::Error::new(rslvr_span, "duplicated attribute"))? + .none_or_else(|_| dup_attr_err(rslvr_span))? } _ => { return Err(syn::Error::new(ident.span(), "unknown attribute")); @@ -136,57 +193,17 @@ impl Parse for UnionMeta { impl UnionMeta { /// Tries to merge two [`UnionMeta`]s into single one, reporting about duplicates, if any. - fn try_merge(self, mut other: Self) -> syn::Result { + fn try_merge(self, mut another: Self) -> syn::Result { Ok(Self { - name: { - if let Some(v) = self.name { - other.name.replace(v).none_or_else(|dup| { - syn::Error::new(dup.span_ident(), "duplicated attribute") - })?; - } - other.name - }, - description: { - if let Some(v) = self.description { - other.description.replace(v).none_or_else(|dup| { - syn::Error::new(dup.span_ident(), "duplicated attribute") - })?; - } - other.description - }, - context: { - if let Some(v) = self.context { - other.context.replace(v).none_or_else(|dup| { - syn::Error::new(dup.span_ident(), "duplicated attribute") - })?; - } - other.context - }, - scalar: { - if let Some(v) = self.scalar { - other.scalar.replace(v).none_or_else(|dup| { - syn::Error::new(dup.span_ident(), "duplicated attribute") - })?; - } - other.scalar - }, - custom_resolvers: { - if !self.custom_resolvers.is_empty() { - for (ty, rslvr) in self.custom_resolvers { - other - .custom_resolvers - .insert(ty, rslvr) - .none_or_else(|dup| { - syn::Error::new(dup.span_joined(), "duplicated attribute") - })?; - } - } - other.custom_resolvers - }, + name: try_merge_opt!(name: self, another), + description: try_merge_opt!(description: self, another), + context: try_merge_opt!(context: self, another), + scalar: try_merge_opt!(scalar: self, another), + custom_resolvers: try_merge_hashmap!(custom_resolvers: self, another => span_joined), }) } - /// Parses [`UnionMeta`] from the given attributes placed on type definition. + /// Parses [`UnionMeta`] from the given multiple `name`d attributes placed on type definition. pub fn from_attrs(name: &str, attrs: &[syn::Attribute]) -> syn::Result { let mut meta = filter_attrs(name, attrs) .map(|attr| attr.parse_args()) @@ -200,8 +217,8 @@ impl UnionMeta { } } -/// Available metadata behind `#[graphql]` (or `#[graphql_union]`) attribute when generating code -/// for [GraphQL union][1]'s variant. +/// Available metadata (arguments) behind `#[graphql]` (or `#[graphql_union]`) attribute when +/// generating code for [GraphQL union][1]'s variant. /// /// [1]: https://spec.graphql.org/June2018/#sec-Unions #[derive(Debug, Default)] @@ -232,14 +249,14 @@ impl Parse for UnionVariantMeta { "ignore" | "skip" => output .ignore .replace(SpanContainer::new(ident.span(), None, ident.clone())) - .none_or_else(|_| syn::Error::new(ident.span(), "duplicated attribute"))?, + .none_or_else(|_| dup_attr_err(ident.span()))?, "with" => { input.parse::()?; let rslvr = input.parse::()?; output .custom_resolver .replace(SpanContainer::new(ident.span(), Some(rslvr.span()), rslvr)) - .none_or_else(|_| syn::Error::new(ident.span(), "duplicated attribute"))? + .none_or_else(|_| dup_attr_err(ident.span()))? } _ => { return Err(syn::Error::new(ident.span(), "unknown attribute")); @@ -257,28 +274,15 @@ impl Parse for UnionVariantMeta { impl UnionVariantMeta { /// Tries to merge two [`UnionVariantMeta`]s into single one, reporting about duplicates, if /// any. - fn try_merge(self, mut other: Self) -> syn::Result { + fn try_merge(self, mut another: Self) -> syn::Result { Ok(Self { - ignore: { - if let Some(v) = self.ignore { - other.ignore.replace(v).none_or_else(|dup| { - syn::Error::new(dup.span_ident(), "duplicated attribute") - })?; - } - other.ignore - }, - custom_resolver: { - if let Some(v) = self.custom_resolver { - other.custom_resolver.replace(v).none_or_else(|dup| { - syn::Error::new(dup.span_ident(), "duplicated attribute") - })?; - } - other.custom_resolver - }, + ignore: try_merge_opt!(ignore: self, another), + custom_resolver: try_merge_opt!(custom_resolver: self, another), }) } - /// Parses [`UnionVariantMeta`] from the given attributes placed on variant/field definition. + /// Parses [`UnionVariantMeta`] from the given multiple `name`d attributes placed on + /// variant/field/method definition. pub fn from_attrs(name: &str, attrs: &[syn::Attribute]) -> syn::Result { filter_attrs(name, attrs) .map(|attr| attr.parse_args()) @@ -286,24 +290,96 @@ impl UnionVariantMeta { } } +/// Definition of [GraphQL union][1] variant for code generation. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Unions struct UnionVariantDefinition { + /// Rust type that this [GraphQL union][1] variant resolves into. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions pub ty: syn::Type, + + /// Rust code for value resolution of this [GraphQL union][1] variant. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions pub resolver_code: syn::Expr, + + /// Rust code for checking whether [GraphQL union][1] should be resolved into this variant. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions pub resolver_check: syn::Expr, + + /// Rust enum variant path that this [GraphQL union][1] variant is associated with. + /// + /// It's available only when code generation happens for Rust enums. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions pub enum_path: Option, + + /// [`Span`] that points to the Rust source code which defines this [GraphQL union][1] variant. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions pub span: Span, } +/// Definition of [GraphQL union][1] for code generation. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Unions struct UnionDefinition { + /// Name of this [GraphQL union][1] in GraphQL schema. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions pub name: String, + + /// Rust type that this [GraphQL union][1] is represented with. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions pub ty: syn::Type, + + /// Generics of the Rust type that this [GraphQL union][1] is implemented for. + pub generics: syn::Generics, + + /// Indicator whether code should be generated for a trait object, rather than for a regular + /// Rust type. pub is_trait_object: bool, + + /// Description of this [GraphQL union][1] to put into GraphQL schema. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions pub description: Option, + + /// Rust type of `juniper::Context` to generate `juniper::GraphQLType` implementation with + /// for this [GraphQL union][1]. + /// + /// If [`None`] then generated code will use unit type `()` as `juniper::Context`. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions pub context: Option, + + /// Rust type of `juniper::ScalarValue` to generate `juniper::GraphQLType` implementation with + /// for this [GraphQL union][1]. + /// + /// If [`None`] then generated code will be generic over any `juniper::ScalarValue` type, which, + /// in turn, requires all [union][1] variants to be generic over any `juniper::ScalarValue` type + /// too. That's why this type should be specified only if one of the variants implements + /// `juniper::GraphQLType` in a non-generic way over `juniper::ScalarValue` type. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions pub scalar: Option, - pub generics: syn::Generics, + + /// Variants definitions of this [GraphQL union][1]. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions pub variants: Vec, + + /// [`Span`] that points to the Rust source code which defines this [GraphQL union][1]. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions pub span: Span, + + /// [`Mode`] to generate code in for this [GraphQL union][1]. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions pub mode: Mode, } @@ -374,8 +450,9 @@ impl ToTokens for UnionDefinition { .map(|(var, expr)| { let var_ty = &var.ty; - let get_name = - quote! { (<#var_ty as #crate_path::GraphQLType<#scalar>>::name(&())) }; + let get_name = quote! { + (<#var_ty as #crate_path::GraphQLType<#scalar>>::name(&())) + }; quote! { if type_name == #get_name.unwrap() { let res = #crate_path::IntoResolvable::into( From 810cc8709c00e9712db31cca1bcaafbeab119185 Mon Sep 17 00:00:00 2001 From: tyranron Date: Mon, 1 Jun 2020 13:48:00 +0300 Subject: [PATCH 14/29] Impl PascalCasing for default union names --- juniper_codegen/src/graphql_union/attr.rs | 4 +- juniper_codegen/src/graphql_union/derive.rs | 6 +-- juniper_codegen/src/util/mod.rs | 41 +++++++++++++++++++++ 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/juniper_codegen/src/graphql_union/attr.rs b/juniper_codegen/src/graphql_union/attr.rs index edfb57a58..862c86516 100644 --- a/juniper_codegen/src/graphql_union/attr.rs +++ b/juniper_codegen/src/graphql_union/attr.rs @@ -6,7 +6,7 @@ use syn::{self, ext::IdentExt as _, parse_quote, spanned::Spanned as _}; use crate::{ result::GraphQLScope, - util::{path_eq_single, span_container::SpanContainer, unparenthesize, Mode}, + util::{path_eq_single, span_container::SpanContainer, to_pascal_case, unparenthesize, Mode}, }; use super::{UnionDefinition, UnionMeta, UnionVariantDefinition, UnionVariantMeta}; @@ -65,7 +65,7 @@ pub fn expand(attr_args: TokenStream, body: TokenStream, mode: Mode) -> syn::Res .name .clone() .map(SpanContainer::into_inner) - .unwrap_or_else(|| trait_ident.unraw().to_string()); // TODO: PascalCase + .unwrap_or_else(|| to_pascal_case(&trait_ident.unraw().to_string())); if matches!(mode, Mode::Public) && name.starts_with("__") { SCOPE.no_double_underscore( meta.name diff --git a/juniper_codegen/src/graphql_union/derive.rs b/juniper_codegen/src/graphql_union/derive.rs index 87a15d1ad..09ec5e541 100644 --- a/juniper_codegen/src/graphql_union/derive.rs +++ b/juniper_codegen/src/graphql_union/derive.rs @@ -5,7 +5,7 @@ use syn::{self, ext::IdentExt as _, parse_quote, spanned::Spanned as _, Data, Fi use crate::{ result::GraphQLScope, - util::{span_container::SpanContainer, Mode}, + util::{span_container::SpanContainer, to_pascal_case, Mode}, }; use super::{UnionDefinition, UnionMeta, UnionVariantDefinition, UnionVariantMeta}; @@ -34,7 +34,7 @@ fn expand_enum(ast: syn::DeriveInput, mode: Mode) -> syn::Result syn::Result String { dest } +/// Returns a copy of the given string, transformed into `PascalCase`. +pub fn to_pascal_case(s: &str) -> String { + let mut dest = String::new(); + + for part in s.split('_') { + if part.len() == 1 { + dest.push_str(&part.to_uppercase()); + } else if part.len() > 1 { + let first = part + .chars() + .next() + .unwrap() + .to_uppercase() + .collect::(); + let second = &part[1..]; + + dest.push_str(&first); + dest.push_str(second); + } + } + + dest +} + pub(crate) fn to_upper_snake_case(s: &str) -> String { let mut last_lower = false; let mut upper = String::new(); @@ -1884,6 +1908,23 @@ mod test { assert_eq!(&to_camel_case("")[..], ""); } + #[test] + fn test_to_pascal_case() { + for (input, expected) in &[ + ("test", "Test"), + ("_test", "Test"), + ("first_second", "FirstSecond"), + ("first_", "First"), + ("a_b_c", "ABC"), + ("a_bc", "ABc"), + ("a_b", "AB"), + ("a", "A"), + ("", ""), + ] { + assert_eq!(&to_pascal_case(input), expected); + } + } + #[test] fn test_to_upper_snake_case() { assert_eq!(to_upper_snake_case("abc"), "ABC"); From 81af517fb086e2a4c2b8a3248597419ec10c8d0c Mon Sep 17 00:00:00 2001 From: tyranron Date: Mon, 1 Jun 2020 13:55:03 +0300 Subject: [PATCH 15/29] Verify trait method signature to not be async --- juniper_codegen/src/graphql_union/attr.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/juniper_codegen/src/graphql_union/attr.rs b/juniper_codegen/src/graphql_union/attr.rs index 862c86516..78beda0a4 100644 --- a/juniper_codegen/src/graphql_union/attr.rs +++ b/juniper_codegen/src/graphql_union/attr.rs @@ -220,7 +220,13 @@ fn parse_variant_from_trait_method( ) }) .ok()?; - // TODO: validate signature to not be async + if let Some(is_async) = &method.sig.asyncness { + SCOPE.custom( + is_async.span(), + "async union variants resolvers are not supported yet", + ); + return None; + } let resolver_code = { if let Some(other) = trait_meta.custom_resolvers.get(&ty) { From f8d3e3398151534862a6357e843d3a9cce15861d Mon Sep 17 00:00:00 2001 From: tyranron Date: Mon, 1 Jun 2020 15:38:55 +0300 Subject: [PATCH 16/29] Fix type-level union variants duplication check with static assertion and finish codegen code polishing --- juniper/Cargo.toml | 1 + juniper/src/lib.rs | 5 +- juniper_codegen/src/graphql_union/attr.rs | 79 +++++--------- juniper_codegen/src/graphql_union/derive.rs | 114 +++++--------------- juniper_codegen/src/graphql_union/mod.rs | 73 ++++++++++++- juniper_codegen/src/result.rs | 9 +- 6 files changed, 129 insertions(+), 152 deletions(-) diff --git a/juniper/Cargo.toml b/juniper/Cargo.toml index 202b03c0a..a543eed74 100644 --- a/juniper/Cargo.toml +++ b/juniper/Cargo.toml @@ -42,6 +42,7 @@ futures = "0.3.1" indexmap = { version = "1.0.0", features = ["serde-1"] } serde = { version = "1.0.8", features = ["derive"] } serde_json = { version="1.0.2", optional = true } +static_assertions = "1.1" url = { version = "2", optional = true } uuid = { version = "0.8", optional = true } diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs index 824bc3091..2aef16cf7 100644 --- a/juniper/src/lib.rs +++ b/juniper/src/lib.rs @@ -113,9 +113,10 @@ extern crate uuid; #[cfg(any(test, feature = "bson"))] extern crate bson; -// This one is required for use by code generated with`juniper_codegen` macros. +// These ones are required for use by the code generated with `juniper_codegen` macros. #[doc(hidden)] -pub use futures; +pub use {futures, static_assertions as sa}; + #[doc(inline)] pub use futures::future::BoxFuture; diff --git a/juniper_codegen/src/graphql_union/attr.rs b/juniper_codegen/src/graphql_union/attr.rs index 78beda0a4..ffb11d10d 100644 --- a/juniper_codegen/src/graphql_union/attr.rs +++ b/juniper_codegen/src/graphql_union/attr.rs @@ -1,20 +1,26 @@ +//! Code generation for `#[graphql_union]`/`#[graphql_union_internal]` macros. + use std::{mem, ops::Deref as _}; use proc_macro2::{Span, TokenStream}; use quote::{quote, ToTokens as _}; -use syn::{self, ext::IdentExt as _, parse_quote, spanned::Spanned as _}; +use syn::{ext::IdentExt as _, parse_quote, spanned::Spanned as _}; use crate::{ result::GraphQLScope, util::{path_eq_single, span_container::SpanContainer, to_pascal_case, unparenthesize, Mode}, }; -use super::{UnionDefinition, UnionMeta, UnionVariantDefinition, UnionVariantMeta}; +use super::{ + all_variants_different, emerge_union_variants_from_meta, UnionDefinition, UnionMeta, + UnionVariantDefinition, UnionVariantMeta, +}; -const SCOPE: GraphQLScope = GraphQLScope::AttrUnion; +/// [`GraphQLScope`] of `#[graphql_union]`/`#[graphql_union_internal]` macros. +const SCOPE: GraphQLScope = GraphQLScope::UnionAttr; -/// Returns name of the `proc_macro_attribute` for deriving `GraphQLUnion` implementation depending -/// on the provided `mode`. +/// Returns the concrete name of the `proc_macro_attribute` for deriving `GraphQLUnion` +/// implementation depending on the provided `mode`. fn attr_path(mode: Mode) -> &'static str { match mode { Mode::Public => "graphql_union", @@ -22,7 +28,7 @@ fn attr_path(mode: Mode) -> &'static str { } } -/// Expands `#[graphql_union]`/`#[graphql_union_internal]` macros into generated code. +/// Expands `#[graphql_union]`/`#[graphql_union_internal]` macro into generated code. pub fn expand(attr_args: TokenStream, body: TokenStream, mode: Mode) -> syn::Result { let attr_path = attr_path(mode); @@ -88,55 +94,13 @@ pub fn expand(attr_args: TokenStream, body: TokenStream, mode: Mode) -> syn::Res proc_macro_error::abort_if_dirty(); - if !meta.custom_resolvers.is_empty() { - let crate_path = mode.crate_path(); - // TODO: refactor into separate function - for (ty, rslvr) in meta.custom_resolvers { - let span = rslvr.span_joined(); - - let resolver_fn = rslvr.into_inner(); - let resolver_code = parse_quote! { - #resolver_fn(self, #crate_path::FromContext::from(context)) - }; - // Doing this may be quite an expensive, because resolving may contain some heavy - // computation, so we're preforming it twice. Unfortunately, we have no other options - // here, until the `juniper::GraphQLType` itself will allow to do it in some cleverer - // way. - let resolver_check = parse_quote! { - ({ #resolver_code } as ::std::option::Option<&#ty>).is_some() - }; - - // TODO: We may not check here for existence, as we do the duplication check when - // parsing methods. - if let Some(var) = variants.iter_mut().find(|v| v.ty == ty) { - var.resolver_code = resolver_code; - var.resolver_check = resolver_check; - var.span = span; - } else { - variants.push(UnionVariantDefinition { - ty, - resolver_code, - resolver_check, - enum_path: None, - span, - }) - } - } - } + emerge_union_variants_from_meta(&mut variants, meta.custom_resolvers, mode); + if variants.is_empty() { SCOPE.custom(trait_span, "expects at least one union variant"); } - // NOTICE: This is not an optimal implementation, as it's possible to bypass this check by using - // a full qualified path instead (`crate::Test` vs `Test`). Since this requirement is mandatory, - // the `std::convert::Into` implementation is used to enforce this requirement. However, due - // to the bad error message this implementation should stay and provide guidance. - let all_variants_different = { - let mut types: Vec<_> = variants.iter().map(|var| &var.ty).collect(); - types.dedup(); - types.len() == variants.len() - }; - if !all_variants_different { + if !all_variants_different(&variants) { SCOPE.custom(trait_span, "each union variant must have a different type"); } @@ -162,6 +126,12 @@ pub fn expand(attr_args: TokenStream, body: TokenStream, mode: Mode) -> syn::Res }) } +/// Parses given Rust trait `method` as [GraphQL union][1] variant. +/// +/// On failure returns [`None`] and internally fills up [`proc_macro_error`] with the corresponding +/// errors. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Unions fn parse_variant_from_trait_method( method: &mut syn::TraitItemMethod, trait_ident: &syn::Ident, @@ -257,10 +227,9 @@ fn parse_variant_from_trait_method( } }; - // Doing this may be quite an expensive, because resolving may contain some heavy - // computation, so we're preforming it twice. Unfortunately, we have no other options - // here, until the `juniper::GraphQLType` itself will allow to do it in some cleverer - // way. + // Doing this may be quite an expensive, because resolving may contain some heavy computation, + // so we're preforming it twice. Unfortunately, we have no other options here, until the + // `juniper::GraphQLType` itself will allow to do it in some cleverer way. let resolver_check = parse_quote! { ({ #resolver_code } as ::std::option::Option<&#ty>).is_some() }; diff --git a/juniper_codegen/src/graphql_union/derive.rs b/juniper_codegen/src/graphql_union/derive.rs index 09ec5e541..f30991e08 100644 --- a/juniper_codegen/src/graphql_union/derive.rs +++ b/juniper_codegen/src/graphql_union/derive.rs @@ -1,18 +1,24 @@ +//! Code generation for `#[derive(GraphQLUnion)]`/`#[derive(GraphQLUnionInternal)]` macros. + use proc_macro2::TokenStream; use proc_macro_error::ResultExt as _; use quote::{quote, ToTokens}; -use syn::{self, ext::IdentExt as _, parse_quote, spanned::Spanned as _, Data, Fields}; +use syn::{ext::IdentExt as _, parse_quote, spanned::Spanned as _, Data, Fields}; use crate::{ result::GraphQLScope, util::{span_container::SpanContainer, to_pascal_case, Mode}, }; -use super::{UnionDefinition, UnionMeta, UnionVariantDefinition, UnionVariantMeta}; +use super::{ + all_variants_different, emerge_union_variants_from_meta, UnionDefinition, UnionMeta, + UnionVariantDefinition, UnionVariantMeta, +}; -const SCOPE: GraphQLScope = GraphQLScope::DeriveUnion; +/// [`GraphQLScope`] of `#[derive(GraphQLUnion)]`/`#[derive(GraphQLUnionInternal)]` macros. +const SCOPE: GraphQLScope = GraphQLScope::UnionDerive; -/// Expands `#[derive(GraphQLUnion)]`/`#[derive(GraphQLUnionInternal)]` macros into generated code. +/// Expands `#[derive(GraphQLUnion)]`/`#[derive(GraphQLUnionInternal)]` macro into generated code. pub fn expand(input: TokenStream, mode: Mode) -> syn::Result { let ast = syn::parse2::(input).unwrap_or_abort(); @@ -24,6 +30,8 @@ pub fn expand(input: TokenStream, mode: Mode) -> syn::Result { .map(ToTokens::into_token_stream) } +/// Expands into generated code a `#[derive(GraphQLUnion)]`/`#[derive(GraphQLUnionInternal)]` macro +/// placed on a Rust enum. fn expand_enum(ast: syn::DeriveInput, mode: Mode) -> syn::Result { let meta = UnionMeta::from_attrs("graphql", &ast.attrs)?; @@ -54,53 +62,13 @@ fn expand_enum(ast: syn::DeriveInput, mode: Mode) -> syn::Result).is_some() - }; - - if let Some(var) = variants.iter_mut().find(|v| v.ty == ty) { - var.resolver_code = resolver_code; - var.resolver_check = resolver_check; - var.span = span; - } else { - variants.push(UnionVariantDefinition { - ty, - resolver_code, - resolver_check, - enum_path: None, - span, - }) - } - } - } + emerge_union_variants_from_meta(&mut variants, meta.custom_resolvers, mode); + if variants.is_empty() { SCOPE.custom(enum_span, "expects at least one union variant"); } - // NOTICE: This is not an optimal implementation, as it's possible to bypass this check by using - // a full qualified path instead (`crate::Test` vs `Test`). Since this requirement is mandatory, - // the `std::convert::Into` implementation is used to enforce this requirement. However, due - // to the bad error message this implementation should stay and provide guidance. - let all_variants_different = { - let mut types: Vec<_> = variants.iter().map(|var| &var.ty).collect(); - types.dedup(); - types.len() == variants.len() - }; - if !all_variants_different { + if !all_variants_different(&variants) { SCOPE.custom(enum_span, "each union variant must have a different type"); } @@ -120,6 +88,12 @@ fn expand_enum(ast: syn::DeriveInput, mode: Mode) -> syn::Result syn::Result { let meta = UnionMeta::from_attrs("graphql", &ast.attrs)?; @@ -215,51 +191,13 @@ fn expand_struct(ast: syn::DeriveInput, mode: Mode) -> syn::Result = meta - .custom_resolvers - .into_iter() - .map(|(ty, rslvr)| { - let span = rslvr.span_joined(); - - let resolver_fn = rslvr.into_inner(); - let resolver_code = parse_quote! { - #resolver_fn(self, #crate_path::FromContext::from(context)) - }; - // Doing this may be quite an expensive, because resolving may contain some heavy - // computation, so we're preforming it twice. Unfortunately, we have no other options - // here, until the `juniper::GraphQLType` itself will allow to do it in some cleverer - // way. - let resolver_check = parse_quote! { - ({ #resolver_code } as ::std::option::Option<&#ty>).is_some() - }; - - UnionVariantDefinition { - ty, - resolver_code, - resolver_check, - enum_path: None, - span, - } - }) - .collect(); - - proc_macro_error::abort_if_dirty(); - + let mut variants = vec![]; + emerge_union_variants_from_meta(&mut variants, meta.custom_resolvers, mode); if variants.is_empty() { SCOPE.custom(struct_span, "expects at least one union variant"); } - // NOTICE: This is not an optimal implementation, as it's possible to bypass this check by using - // a full qualified path instead (`crate::Test` vs `Test`). Since this requirement is mandatory, - // the `std::convert::Into` implementation is used to enforce this requirement. However, due - // to the bad error message this implementation should stay and provide guidance. - let all_variants_different = { - let mut types: Vec<_> = variants.iter().map(|var| &var.ty).collect(); - types.dedup(); - types.len() == variants.len() - }; - if !all_variants_different { + if !all_variants_different(&variants) { SCOPE.custom(struct_span, "each union variant must have a different type"); } diff --git a/juniper_codegen/src/graphql_union/mod.rs b/juniper_codegen/src/graphql_union/mod.rs index d30c07955..8c4c4a260 100644 --- a/juniper_codegen/src/graphql_union/mod.rs +++ b/juniper_codegen/src/graphql_union/mod.rs @@ -1,4 +1,4 @@ -//! Code generation for [GraphQL union][1] type. +//! Code generation for [GraphQL union][1]. //! //! [1]: https://spec.graphql.org/June2018/#sec-Unions @@ -70,6 +70,9 @@ fn dup_attr_err(span: Span) -> syn::Error { syn::Error::new(span, "duplicated attribute") } +/// Helper alias for the type of [`UnionMeta::custom_resolvers`] field. +type UnionMetaResolvers = HashMap>; + /// Available metadata (arguments) behind `#[graphql]` (or `#[graphql_union]`) attribute when /// generating code for [GraphQL union][1] type. /// @@ -117,7 +120,7 @@ struct UnionMeta { /// some custom [union][1] variant resolving logic is involved, or variants cannot be inferred. /// /// [1]: https://spec.graphql.org/June2018/#sec-Unions - pub custom_resolvers: HashMap>, + pub custom_resolvers: UnionMetaResolvers, } impl Parse for UnionMeta { @@ -601,6 +604,8 @@ impl ToTokens for UnionDefinition { #[automatically_derived] impl#impl_generics #crate_path::marker::GraphQLUnion for #ty_full { fn mark() { + #crate_path::sa::assert_type_ne_all!(#(#var_types),*); + #( <#var_types as #crate_path::marker::GraphQLObjectType< #default_scalar, >>::mark(); )* @@ -611,3 +616,67 @@ impl ToTokens for UnionDefinition { into.append_all(&[union_impl, output_type_impl, type_impl, async_type_impl]); } } + +/// Emerges [`UnionMeta::custom_resolvers`] into the given [GraphQL union][1] `variants`. +/// +/// If duplication happens, then resolving code is overwritten with the one from `custom_resolvers`. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Unions +fn emerge_union_variants_from_meta( + variants: &mut Vec, + custom_resolvers: UnionMetaResolvers, + mode: Mode, +) { + if custom_resolvers.is_empty() { + return; + } + + let crate_path = mode.crate_path(); + + for (ty, rslvr) in custom_resolvers { + let span = rslvr.span_joined(); + + let resolver_fn = rslvr.into_inner(); + let resolver_code = parse_quote! { + #resolver_fn(self, #crate_path::FromContext::from(context)) + }; + // Doing this may be quite an expensive, because resolving may contain some heavy + // computation, so we're preforming it twice. Unfortunately, we have no other options here, + // until the `juniper::GraphQLType` itself will allow to do it in some cleverer way. + let resolver_check = parse_quote! { + ({ #resolver_code } as ::std::option::Option<&#ty>).is_some() + }; + + if let Some(var) = variants.iter_mut().find(|v| v.ty == ty) { + var.resolver_code = resolver_code; + var.resolver_check = resolver_check; + var.span = span; + } else { + variants.push(UnionVariantDefinition { + ty, + resolver_code, + resolver_check, + enum_path: None, + span, + }) + } + } +} + +/// Checks whether all [GraphQL union][1] `variants` represent a different Rust type. +/// +/// # Notice +/// +/// This is not an optimal implementation, as it's possible to bypass this check by using a full +/// qualified path instead (`crate::Test` vs `Test`). Since this requirement is mandatory, the +/// static assertion [`assert_type_ne_all!`][2] is used to enforce this requirement in the generated +/// code. However, due to the bad error message this implementation should stay and provide +/// guidance. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Unions +/// [2]: https://docs.rs/static_assertions/latest/static_assertions/macro.assert_type_ne_all.html +fn all_variants_different(variants: &Vec) -> bool { + let mut types: Vec<_> = variants.iter().map(|var| &var.ty).collect(); + types.dedup(); + types.len() == variants.len() +} diff --git a/juniper_codegen/src/result.rs b/juniper_codegen/src/result.rs index 1c28fef02..5c68544bf 100644 --- a/juniper_codegen/src/result.rs +++ b/juniper_codegen/src/result.rs @@ -10,13 +10,12 @@ pub const SPEC_URL: &'static str = "https://spec.graphql.org/June2018/"; #[allow(unused_variables)] pub enum GraphQLScope { - AttrUnion, + UnionAttr, DeriveObject, DeriveInputObject, - DeriveUnion, + UnionDerive, DeriveEnum, DeriveScalar, - ImplUnion, ImplScalar, ImplObject, } @@ -26,7 +25,7 @@ impl GraphQLScope { match self { Self::DeriveObject | Self::ImplObject => "#sec-Objects", Self::DeriveInputObject => "#sec-Input-Objects", - Self::AttrUnion | Self::DeriveUnion | Self::ImplUnion => "#sec-Unions", + Self::UnionAttr | Self::UnionDerive => "#sec-Unions", Self::DeriveEnum => "#sec-Enums", Self::DeriveScalar | Self::ImplScalar => "#sec-Scalars", } @@ -38,7 +37,7 @@ impl fmt::Display for GraphQLScope { let name = match self { Self::DeriveObject | Self::ImplObject => "object", Self::DeriveInputObject => "input object", - Self::AttrUnion | Self::DeriveUnion | Self::ImplUnion => "union", + Self::UnionAttr | Self::UnionDerive => "union", Self::DeriveEnum => "enum", Self::DeriveScalar | Self::ImplScalar => "scalar", }; From a088cdc6bb85166fc112542470610f6d8fca476f Mon Sep 17 00:00:00 2001 From: tyranron Date: Mon, 1 Jun 2020 16:25:29 +0300 Subject: [PATCH 17/29] Update CHANGELOG --- juniper/CHANGELOG.md | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index 35e6dabfb..d50577ead 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -2,7 +2,7 @@ ## Features -- Support raw identifiers in field and argument names. (#[object] macro) +- Support raw identifiers in field and argument names. (`#[object]` macro) - Most error types now implement `std::error::Error`: - `GraphQLError` @@ -29,10 +29,21 @@ 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)) +- Reworked `#[derive(GraphQLUnion)]` macro ([#666]): + - applicable to enums and structs; + - supports custom resolvers; + - supports generics; + - supports multiple `#[graphql]` attributes. +- New `#[graphql_union]` macro ([#666]): + - applicable to traits; + - supports custom resolvers; + - supports generics; + - supports multiple `#[graphql_union]` attributes. + - Better error messages for all proc macros (see [#631](https://github.com/graphql-rust/juniper/pull/631) -- Improved lookahead visibility for aliased fields (see [#662](https://github.com/graphql-rust/juniper/pull/631)) +- Improved lookahead visibility for aliased fields (see [#662](https://github.com/graphql-rust/juniper/pull/631)) ## Breaking Changes @@ -45,10 +56,10 @@ See [#618](https://github.com/graphql-rust/juniper/pull/618). - Remove deprecated `ScalarValue` custom derive (renamed to GraphQLScalarValue) -- `graphql_union!` macro removed, replaced by `#[graphql_union]` proc macro +- `graphql_union!` macro removed, replaced by `#[graphql_union]` proc macro and custom resolvers of `#[derive(GraphQLUnion)]` macro. +- `#[derive(GraphQLUnion)]` macro doesn't generate `From` impls for enum variants anymore, consider [`derive_more`](https//docs.rs/derive_more)) crate to do that ([#666]). -- ScalarRefValue trait removed - Trait was not required. +- `ScalarRefValue` trait removed. Trait was not required. - Changed return type of GraphQLType::resolve to `ExecutionResult` This was done to unify the return type of all resolver methods @@ -59,7 +70,7 @@ See [#618](https://github.com/graphql-rust/juniper/pull/618). add subscription type to `RootNode`, add subscription endpoint to `playground_source()` -- Putting a scalar type into a string is not allowed anymore, e..g, +- 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 @@ -75,6 +86,8 @@ See [#618](https://github.com/graphql-rust/juniper/pull/618). - When using LookAheadMethods to access child selections, children are always found using their alias if it exists rather than their name (see [#662](https://github.com/graphql-rust/juniper/pull/631)). These methods are also deprecated in favour of the new `children` method. +[#666]: https://github.com/graphql-rust/juniper/pull/666 + # [[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) From 23015f61b06a3b1ab2303f000a2b15934ce57535 Mon Sep 17 00:00:00 2001 From: tyranron Date: Mon, 1 Jun 2020 19:32:14 +0300 Subject: [PATCH 18/29] Bootstrap new book docs for unions --- docs/book/content/types/unions.md | 397 +++++++++++++++++++++++------- docs/book/tests/Cargo.toml | 2 + 2 files changed, 308 insertions(+), 91 deletions(-) diff --git a/docs/book/content/types/unions.md b/docs/book/content/types/unions.md index a2793a626..56cacbea7 100644 --- a/docs/book/content/types/unions.md +++ b/docs/book/content/types/unions.md @@ -1,75 +1,197 @@ -# Unions +Unions +====== -From a server's point of view, GraphQL unions are similar to interfaces: the -only exception is that they don't contain fields on their own. +From a server's point of view, [GraphQL unions][1] are similar to interfaces: the only exception is that they don't contain fields on their own. + +For implementing [GraphQL union][1] Juniper provides: +- `#[derive(GraphQLUnion)]` macro for enums and structs; +- `#[graphql_union]` for traits. -In Juniper, the `graphql_union!` has identical syntax to the -[interface macro](interfaces.md), but does not support defining -fields. Therefore, the same considerations about using traits, -placeholder types, or enums still apply to unions. For simple -situations, Juniper provides `#[derive(GraphQLUnion)]` for enums. -If we look at the same examples as in the interfaces chapter, we see the -similarities and the tradeoffs: -## Traits -### Downcasting via accessor methods +## Enums + +Most of the time, we need just a trivial and straightforward Rust enum to represent a [GraphQL union][1]. ```rust -#[derive(juniper::GraphQLObject)] +use derive_more::From; +use juniper::{GraphQLObject, GraphQLUnion}; + +#[derive(GraphQLObject)] struct Human { id: String, home_planet: String, } -#[derive(juniper::GraphQLObject)] +#[derive(GraphQLObject)] struct Droid { id: String, primary_function: String, } -trait Character { - // Downcast methods, each concrete class will need to implement one of these - fn as_human(&self) -> Option<&Human> { None } - fn as_droid(&self) -> Option<&Droid> { None } +#[derive(From, GraphQLUnion)] +enum Character { + Human(Human), + Droid(Droid), } +# +# fn main() {} +``` -impl Character for Human { - fn as_human(&self) -> Option<&Human> { Some(&self) } + +### Ignoring enum variants + +In some rare situations we may want to omit exposing enum variant in GraphQL schema. + +As an example, let's consider the situation when we need to bind some type parameter for doing interesting type-level stuff in our resolvers. To achieve that, we need to carry the one with `PhantomData`, but we don't want the latest being exposed in GraphQL schema. + +> __WARNING__: +> It's _library user responsibility_ to ensure that ignored enum variant is _never_ returned from resolvers, otherwise resolving GraphQL query will __panic in runtime__. + +```rust +# use std::marker::PhantomData; +use derive_more::From; +use juniper::{GraphQLObject, GraphQLUnion}; + +#[derive(GraphQLObject)] +struct Human { + id: String, + home_planet: String, } -impl Character for Droid { - fn as_droid(&self) -> Option<&Droid> { Some(&self) } +#[derive(GraphQLObject)] +struct Droid { + id: String, + primary_function: String, +} + +#[derive(From, GraphQLUnion)] +enum Character { + Human(Human), + Droid(Droid), + #[from(ignore)] + #[graphql(ignore)] // or `#[graphql(skip)]`, on your choice + _State(PhatomData), +} +# +# fn main() {} +``` + + +### Custom resolvers + +If some custom logic should be involved to resolve a [GraphQL union][1] variant, we may specify the function responsible for that. + +```rust +# #![allow(dead_code)] +use juniper::{GraphQLObject, GraphQLUnion}; + +#[derive(GraphQLObject)] +#[graphql(Context = CustomContext)] +struct Human { + id: String, + home_planet: String, +} + +#[derive(GraphQLObject)] +#[graphql(Context = CustomContext)] +struct Droid { + id: String, + primary_function: String, } -#[juniper::graphql_union] -impl<'a> GraphQLUnion for &'a dyn Character { - fn resolve(&self) { - match self { - Human => self.as_human(), - Droid => self.as_droid(), - } +pub struct CustomContext { + droid: Droid, +} +impl juniper::Context for CustomContext {} + +#[derive(GraphQLUnion)] +#[graphql(Context = CustomContext)] +enum Character { + Human(Human), + #[graphql(with = Character::droid_from_context)] + Droid(Droid), +} + +impl Character { + // NOTICE: The function signature is mandatory to accept `&self`, `&Context` + // and return `Option<&VariantType>`. + fn droid_from_context<'c>(&self, ctx: &'c CustomContext) -> Option<&'c Droid> { + Some(&ctx.droid) } } +# +# fn main() {} +``` + +With a custom resolver we can even declare a new [GraphQL union][1] variant, which Rust type is absent in the initial enum definition (the attribute syntax `#[graphql(on VariantType = resolver_fn)]` follows the [GraphQL syntax for dispatching union variant](https://spec.graphql.org/June2018/#example-f8163)). + +```rust +use juniper::{GraphQLObject, GraphQLUnion}; + +#[derive(GraphQLObject)] +#[graphql(Context = CustomContext)] +struct Human { + id: String, + home_planet: String, +} + +#[derive(GraphQLObject)] +#[graphql(Context = CustomContext)] +struct Droid { + id: String, + primary_function: String, +} + +#[derive(GraphQLObject)] +#[graphql(Context = CustomContext)] +struct Ewok { + id: String, + is_funny: bool, +} + +pub struct CustomContext { + ewok: Ewok, +} +impl juniper::Context for CustomContext {} +#[derive(GraphQLUnion)] +#[graphql(Context = CustomContext)] +#[graphql(on Ewok = Character::ewok_from_context)] +enum Character { + Human(Human), + Droid(Droid), +} + +impl Character { + fn ewok_from_context<'c>(&self, ctx: &'c CustomContext) -> Option<&'c Ewok> { + Some(&ctx.ewok) + } +} +# # fn main() {} ``` -### Using an extra database lookup -FIXME: This example does not compile at the moment + + +## Structs + +Using Rust structs as [GraphQL unions][1] is very similar to using enums, with the nuance that specifying custom resolver is the only way to declare a [GraphQL union][1] variant. ```rust # use std::collections::HashMap; -#[derive(juniper::GraphQLObject)] +use juniper::{GraphQLObject, GraphQLUnion}; + +#[derive(GraphQLObject)] #[graphql(Context = Database)] struct Human { id: String, home_planet: String, } -#[derive(juniper::GraphQLObject)] +#[derive(GraphQLObject)] #[graphql(Context = Database)] struct Droid { id: String, @@ -80,49 +202,91 @@ struct Database { humans: HashMap, droids: HashMap, } - impl juniper::Context for Database {} -trait Character { - fn id(&self) -> &str; +#[derive(GraphQLUnion)] +#[graphql( + on Human = Character::get_human, + on Droid = Character::get_droid, +)] +struct Character { + id: String, } -impl Character for Human { - fn id(&self) -> &str { self.id.as_str() } +impl Character { + fn get_human<'db>(&self, ctx: &'db Database) -> Option<'db Human>{ + ctx.humans.get(&self.id) + } + + fn get_droid<'db>(&self, ctx: &'db Database) -> Option<'db Human>{ + ctx.humans.get(&self.id) + } } +# +# fn main() {} +``` -impl Character for Droid { - fn id(&self) -> &str { self.id.as_str() } + + + +## Traits + +Sometimes it may seem very reasonable to use Rust trait for representing a [GraphQL union][1]. However, to do that, we should introduce a separate `#[graphql_union]` macro, because [Rust doesn't allow to use derive macros on traits](https://doc.rust-lang.org/stable/reference/procedural-macros.html#derive-macros) at the moment. + +> __NOTICE__: +> A __trait has to be [object safe](https://doc.rust-lang.org/stable/reference/items/traits.html#object-safety)__, because schema resolvers will need to return a [trait object](https://doc.rust-lang.org/stable/reference/types/trait-object.html) to specify a [GraphQL union][1] behind it. + +```rust +use juniper::{graphql_union, GraphQLObject}; + +#[derive(GraphQLObject)] +struct Human { + id: String, + home_planet: String, } +#[derive(GraphQLObject)] +struct Droid { + id: String, + primary_function: String, +} -#[juniper::graphql_union( - Context = Database -)] -impl<'a> GraphQLUnion for &'a dyn Character { - fn resolve(&self, context: &Database) { - match self { - Human => context.humans.get(self.id()), - Droid => context.droids.get(self.id()), - } - } +#[graphql_union] +trait Character { + // NOTICE: The function signature is mandatory to accept `&self` + // and return `Option<&VariantType>`. + fn as_human(&self) -> Option<&Human> { None } + fn as_droid(&self) -> Option<&Droid> { None } } +impl Character for Human { + fn as_human(&self) -> Option<&Human> { Some(&self) } +} + +impl Character for Droid { + fn as_droid(&self) -> Option<&Droid> { Some(&self) } +} +# # fn main() {} ``` -## Placeholder objects + +### Custom context + +If a context is required in trait method to resolve a [GraphQL union][1] variant, we may just specify it in arguments. ```rust # use std::collections::HashMap; -#[derive(juniper::GraphQLObject)] +use juniper::{graphql_union, GraphQLObject}; + +#[derive(GraphQLObject)] #[graphql(Context = Database)] struct Human { id: String, home_planet: String, } -#[derive(juniper::GraphQLObject)] +#[derive(GraphQLObject)] #[graphql(Context = Database)] struct Droid { id: String, @@ -133,87 +297,138 @@ struct Database { humans: HashMap, droids: HashMap, } - impl juniper::Context for Database {} -struct Character { - id: String, +#[graphql_union(Context = Database)] +trait Character { + // NOTICE: The function signature, however, may optionally accept `&Context`. + fn as_human<'db>(&self, ctx: &'db Database) -> Option<&'db Human> { None } + fn as_droid<'db>(&self, ctx: &'db Database) -> Option<&'db Droid> { None } } -#[juniper::graphql_union( - Context = Database, -)] -impl GraphQLUnion for Character { - fn resolve(&self, context: &Database) { - match self { - Human => { context.humans.get(&self.id) }, - Droid => { context.droids.get(&self.id) }, - } +impl Character for Human { + fn as_human<'db>(&self, ctx: &'db Database) -> Option<&'db Human> { + ctx.humans.get(&self.id) } } +impl Character for Droid { + fn as_droid<'db>(&self, ctx: &'db Database) -> Option<&'db Droid> { + ctx.droids.get(&self.id) + } +} +# # fn main() {} ``` -## Enums (Impl) + +### Ignoring trait methods + +As with enums, we may want to omit some trait methods to be assumed as [GraphQL union][1] variants and ignore them. ```rust -#[derive(juniper::GraphQLObject)] +use juniper::{graphql_union, GraphQLObject}; + +#[derive(GraphQLObject)] struct Human { id: String, home_planet: String, } -#[derive(juniper::GraphQLObject)] +#[derive(GraphQLObject)] struct Droid { id: String, primary_function: String, } -# #[allow(dead_code)] -enum Character { - Human(Human), - Droid(Droid), +#[graphql_union] +trait Character { + fn as_human(&self) -> Option<&Human> { None } + fn as_droid(&self) -> Option<&Droid> { None } + #[graphql_union(ignore)] // or `#[graphql_union(skip)]`, on your choice + fn id(&self) -> &str; } -#[juniper::graphql_union] -impl Character { - fn resolve(&self) { - match self { - Human => { match *self { Character::Human(ref h) => Some(h), _ => None } }, - Droid => { match *self { Character::Droid(ref d) => Some(d), _ => None } }, - } - } +impl Character for Human { + fn as_human(&self) -> Option<&Human> { Some(&self) } + fn id(&self) -> &str { self.id.as_str() } } +impl Character for Droid { + fn as_droid(&self) -> Option<&Droid> { Some(&self) } + fn id(&self) -> &str { self.id.as_str() } +} +# # fn main() {} ``` -## Enums (Derive) -This example is similar to `Enums (Impl)`. To successfully use the -derive macro, ensure that each variant of the enum has a different -type. Since each variant is different, the device macro provides -`std::convert::Into` converter for each variant. +### Custom resolvers + +And, of course, similarly to enums and structs, it's not mandatory to use trait methods as [GraphQL union][1] variants resolvers, the custom functions may be specified as well. ```rust -#[derive(juniper::GraphQLObject)] +# use std::collections::HashMap; +use juniper::{graphql_union, GraphQLObject}; + +#[derive(GraphQLObject)] +#[graphql(Context = Database)] struct Human { id: String, home_planet: String, } -#[derive(juniper::GraphQLObject)] +#[derive(GraphQLObject)] +#[graphql(Context = Database)] struct Droid { id: String, primary_function: String, } -#[derive(juniper::GraphQLUnion)] -enum Character { - Human(Human), - Droid(Droid), +struct Database { + humans: HashMap, + droids: HashMap, +} +impl juniper::Context for Database {} + +#[graphql_union(Context = Database)] +#[graphql_union( + on Human = DynCharacter::get_human, + on Droid = get_droid, +)] +trait Character { + #[graphql_union(ignore)] // or `#[graphql_union(skip)]`, on your choice + fn id(&self) -> &str; } +impl Character for Human { + fn id(&self) -> &str { self.id.as_str() } +} + +impl Character for Droid { + fn id(&self) -> &str { self.id.as_str() } +} + +// Used trait object is always `Send` and `Sync`. +type DynCharacter = dyn Character + Send + Sync; + +impl DynCharacter { + fn get_human<'db>(&self, ctx: &'db Database) -> Option<&'db Human> { + ctx.humans.get(self.id()) + } +} + +// Custom resolver function doesn't have to be a method. +// It's only a matter of the function signature to match the requirements. +fn get_droid<'db>(ch: &DynCharacter, ctx: &'db Database) -> Option<&'db Human> { + ctx.humans.get(ch.id()) +} +# # fn main() {} ``` + + + + + +[1]: https://spec.graphql.org/June2018/#sec-Unions diff --git a/docs/book/tests/Cargo.toml b/docs/book/tests/Cargo.toml index d7c4d0bc9..120f1a894 100644 --- a/docs/book/tests/Cargo.toml +++ b/docs/book/tests/Cargo.toml @@ -9,6 +9,8 @@ build = "build.rs" juniper = { path = "../../../juniper" } juniper_iron = { path = "../../../juniper_iron" } juniper_subscriptions = { path = "../../../juniper_subscriptions" } + +derive_more = "0.99.7" futures = "0.3" tokio = { version = "0.2", features = ["rt-core", "blocking", "stream", "rt-util"] } iron = "0.5.0" From 25ec5d8f9e0236a97603078ea7539e870c03633b Mon Sep 17 00:00:00 2001 From: tyranron Date: Tue, 2 Jun 2020 12:10:35 +0300 Subject: [PATCH 19/29] Apply typos corrections provided by @LegNeato --- docs/book/content/types/unions.md | 30 +++++++++---------- .../juniper_tests/src/codegen/union_derive.rs | 2 +- juniper/CHANGELOG.md | 20 ++++++------- juniper/src/lib.rs | 2 +- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/docs/book/content/types/unions.md b/docs/book/content/types/unions.md index 56cacbea7..3a0dbf2a5 100644 --- a/docs/book/content/types/unions.md +++ b/docs/book/content/types/unions.md @@ -42,12 +42,12 @@ enum Character { ### Ignoring enum variants -In some rare situations we may want to omit exposing enum variant in GraphQL schema. +In some rare situations we may want to omit exposing an enum variant in the GraphQL schema. -As an example, let's consider the situation when we need to bind some type parameter for doing interesting type-level stuff in our resolvers. To achieve that, we need to carry the one with `PhantomData`, but we don't want the latest being exposed in GraphQL schema. +As an example, let's consider the situation where we need to bind some type parameter `T` for doing interesting type-level stuff in our resolvers. To achieve this we need to have `PhantomData`, but we don't want it exposed in the GraphQL schema. > __WARNING__: -> It's _library user responsibility_ to ensure that ignored enum variant is _never_ returned from resolvers, otherwise resolving GraphQL query will __panic in runtime__. +> It's the _library user's responsibility_ to ensure that ignored enum variant is _never_ returned from resolvers, otherwise resolving the GraphQL query will __panic at runtime__. ```rust # use std::marker::PhantomData; @@ -71,7 +71,7 @@ enum Character { Human(Human), Droid(Droid), #[from(ignore)] - #[graphql(ignore)] // or `#[graphql(skip)]`, on your choice + #[graphql(ignore)] // or `#[graphql(skip)]`, your choice _State(PhatomData), } # @@ -81,7 +81,7 @@ enum Character { ### Custom resolvers -If some custom logic should be involved to resolve a [GraphQL union][1] variant, we may specify the function responsible for that. +If some custom logic is needed to resolve a [GraphQL union][1] variant, you may specify a function to do so: ```rust # #![allow(dead_code)] @@ -115,7 +115,7 @@ enum Character { } impl Character { - // NOTICE: The function signature is mandatory to accept `&self`, `&Context` + // NOTICE: The function signature must contain `&self` and `&Context`, // and return `Option<&VariantType>`. fn droid_from_context<'c>(&self, ctx: &'c CustomContext) -> Option<&'c Droid> { Some(&ctx.droid) @@ -125,7 +125,7 @@ impl Character { # fn main() {} ``` -With a custom resolver we can even declare a new [GraphQL union][1] variant, which Rust type is absent in the initial enum definition (the attribute syntax `#[graphql(on VariantType = resolver_fn)]` follows the [GraphQL syntax for dispatching union variant](https://spec.graphql.org/June2018/#example-f8163)). +With a custom resolver we can even declare a new [GraphQL union][1] variant where the Rust type is absent in the initial enum definition. The attribute syntax `#[graphql(on VariantType = resolver_fn)]` follows the [GraphQL syntax for dispatching union variants](https://spec.graphql.org/June2018/#example-f8163). ```rust use juniper::{GraphQLObject, GraphQLUnion}; @@ -178,7 +178,7 @@ impl Character { ## Structs -Using Rust structs as [GraphQL unions][1] is very similar to using enums, with the nuance that specifying custom resolver is the only way to declare a [GraphQL union][1] variant. +Using Rust structs as [GraphQL unions][1] is very similar to using enums, with the nuance that specifying a custom resolver is the only way to declare a [GraphQL union][1] variant. ```rust # use std::collections::HashMap; @@ -231,7 +231,7 @@ impl Character { ## Traits -Sometimes it may seem very reasonable to use Rust trait for representing a [GraphQL union][1]. However, to do that, we should introduce a separate `#[graphql_union]` macro, because [Rust doesn't allow to use derive macros on traits](https://doc.rust-lang.org/stable/reference/procedural-macros.html#derive-macros) at the moment. +To use a Rust trait definition as a [GraphQL union][1] you need to use the `#[graphql_union]` macro. [Rust doesn't allow derive macros on traits](https://doc.rust-lang.org/stable/reference/procedural-macros.html#derive-macros), so using `#[derive(GraphQLUnion)]` on traits doesn't work. > __NOTICE__: > A __trait has to be [object safe](https://doc.rust-lang.org/stable/reference/items/traits.html#object-safety)__, because schema resolvers will need to return a [trait object](https://doc.rust-lang.org/stable/reference/types/trait-object.html) to specify a [GraphQL union][1] behind it. @@ -253,7 +253,7 @@ struct Droid { #[graphql_union] trait Character { - // NOTICE: The function signature is mandatory to accept `&self` + // NOTICE: The method signature must contain `&self` // and return `Option<&VariantType>`. fn as_human(&self) -> Option<&Human> { None } fn as_droid(&self) -> Option<&Droid> { None } @@ -273,7 +273,7 @@ impl Character for Droid { ### Custom context -If a context is required in trait method to resolve a [GraphQL union][1] variant, we may just specify it in arguments. +If a context is required in a trait method to resolve a [GraphQL union][1] variant, specify it as an argument. ```rust # use std::collections::HashMap; @@ -301,7 +301,7 @@ impl juniper::Context for Database {} #[graphql_union(Context = Database)] trait Character { - // NOTICE: The function signature, however, may optionally accept `&Context`. + // NOTICE: The method signature may optionally contain `&Context`. fn as_human<'db>(&self, ctx: &'db Database) -> Option<&'db Human> { None } fn as_droid<'db>(&self, ctx: &'db Database) -> Option<&'db Droid> { None } } @@ -345,7 +345,7 @@ struct Droid { trait Character { fn as_human(&self) -> Option<&Human> { None } fn as_droid(&self) -> Option<&Droid> { None } - #[graphql_union(ignore)] // or `#[graphql_union(skip)]`, on your choice + #[graphql_union(ignore)] // or `#[graphql_union(skip)]`, your choice fn id(&self) -> &str; } @@ -365,7 +365,7 @@ impl Character for Droid { ### Custom resolvers -And, of course, similarly to enums and structs, it's not mandatory to use trait methods as [GraphQL union][1] variants resolvers, the custom functions may be specified as well. +Similarly to enums and structs, it's not mandatory to use trait methods as [GraphQL union][1] variant resolvers. and instead custom functions may be specified: ```rust # use std::collections::HashMap; @@ -397,7 +397,7 @@ impl juniper::Context for Database {} on Droid = get_droid, )] trait Character { - #[graphql_union(ignore)] // or `#[graphql_union(skip)]`, on your choice + #[graphql_union(ignore)] // or `#[graphql_union(skip)]`, your choice fn id(&self) -> &str; } diff --git a/integration_tests/juniper_tests/src/codegen/union_derive.rs b/integration_tests/juniper_tests/src/codegen/union_derive.rs index 017c3cb59..7838140e0 100644 --- a/integration_tests/juniper_tests/src/codegen/union_derive.rs +++ b/integration_tests/juniper_tests/src/codegen/union_derive.rs @@ -171,7 +171,7 @@ pub enum DifferentContext { B(Droid), } -// NOTICE: this can not compile due to generic implementation of GraphQLType<__S> +// NOTICE: This doesn't compile due to generic implementation of `GraphQLType<__S>`. // #[derive(GraphQLUnion)] // pub enum CharacterCompatFail { // One(HumanCompat), diff --git a/juniper/CHANGELOG.md b/juniper/CHANGELOG.md index d50577ead..154c06d01 100644 --- a/juniper/CHANGELOG.md +++ b/juniper/CHANGELOG.md @@ -30,15 +30,15 @@ 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)) - Reworked `#[derive(GraphQLUnion)]` macro ([#666]): - - applicable to enums and structs; - - supports custom resolvers; - - supports generics; - - supports multiple `#[graphql]` attributes. + - Applicable to enums and structs. + - Supports custom resolvers. + - Supports generics. + - Supports multiple `#[graphql]` attributes. - New `#[graphql_union]` macro ([#666]): - - applicable to traits; - - supports custom resolvers; - - supports generics; - - supports multiple `#[graphql_union]` attributes. + - Applicable to traits. + - Supports custom resolvers. + - Supports generics. + - Supports multiple `#[graphql_union]` attributes. - Better error messages for all proc macros (see [#631](https://github.com/graphql-rust/juniper/pull/631) @@ -56,8 +56,8 @@ See [#618](https://github.com/graphql-rust/juniper/pull/618). - Remove deprecated `ScalarValue` custom derive (renamed to GraphQLScalarValue) -- `graphql_union!` macro removed, replaced by `#[graphql_union]` proc macro and custom resolvers of `#[derive(GraphQLUnion)]` macro. -- `#[derive(GraphQLUnion)]` macro doesn't generate `From` impls for enum variants anymore, consider [`derive_more`](https//docs.rs/derive_more)) crate to do that ([#666]). +- `graphql_union!` macro removed, replaced by `#[graphql_union]` proc macro and custom resolvers for the `#[derive(GraphQLUnion)]` macro. +- The `#[derive(GraphQLUnion)]` macro doesn't generate `From` impls for enum variants anymore. Consider using the [`derive_more`](https//docs.rs/derive_more) crate directly ([#666]). - `ScalarRefValue` trait removed. Trait was not required. diff --git a/juniper/src/lib.rs b/juniper/src/lib.rs index 5b2adad48..14bf1fa52 100644 --- a/juniper/src/lib.rs +++ b/juniper/src/lib.rs @@ -113,7 +113,7 @@ extern crate uuid; #[cfg(any(test, feature = "bson"))] extern crate bson; -// These ones are required for use by the code generated with `juniper_codegen` macros. +// These are required by the code generated via the `juniper_codegen` macros. #[doc(hidden)] pub use {futures, static_assertions as sa}; From 2287679f6b254d22e0d355d8c7920555ab11f3d3 Mon Sep 17 00:00:00 2001 From: tyranron Date: Tue, 2 Jun 2020 12:42:22 +0300 Subject: [PATCH 20/29] Fix book tests --- docs/book/content/types/unions.md | 30 ++++++++++++++++++++---------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/docs/book/content/types/unions.md b/docs/book/content/types/unions.md index 3a0dbf2a5..92c592925 100644 --- a/docs/book/content/types/unions.md +++ b/docs/book/content/types/unions.md @@ -15,6 +15,7 @@ For implementing [GraphQL union][1] Juniper provides: Most of the time, we need just a trivial and straightforward Rust enum to represent a [GraphQL union][1]. ```rust +# #![allow(dead_code)] use derive_more::From; use juniper::{GraphQLObject, GraphQLUnion}; @@ -72,7 +73,7 @@ enum Character { Droid(Droid), #[from(ignore)] #[graphql(ignore)] // or `#[graphql(skip)]`, your choice - _State(PhatomData), + _State(PhantomData), } # # fn main() {} @@ -128,6 +129,7 @@ impl Character { With a custom resolver we can even declare a new [GraphQL union][1] variant where the Rust type is absent in the initial enum definition. The attribute syntax `#[graphql(on VariantType = resolver_fn)]` follows the [GraphQL syntax for dispatching union variants](https://spec.graphql.org/June2018/#example-f8163). ```rust +# #![allow(dead_code)] use juniper::{GraphQLObject, GraphQLUnion}; #[derive(GraphQLObject)] @@ -162,11 +164,17 @@ impl juniper::Context for CustomContext {} enum Character { Human(Human), Droid(Droid), + #[graphql(ignore)] // or `#[graphql(skip)]`, your choice + Ewok, } impl Character { fn ewok_from_context<'c>(&self, ctx: &'c CustomContext) -> Option<&'c Ewok> { - Some(&ctx.ewok) + if matches!(self, Self::Ewok) { + Some(&ctx.ewok) + } else { + None + } } } # @@ -206,6 +214,7 @@ impl juniper::Context for Database {} #[derive(GraphQLUnion)] #[graphql( + Context = Database, on Human = Character::get_human, on Droid = Character::get_droid, )] @@ -214,12 +223,12 @@ struct Character { } impl Character { - fn get_human<'db>(&self, ctx: &'db Database) -> Option<'db Human>{ + fn get_human<'db>(&self, ctx: &'db Database) -> Option<&'db Human>{ ctx.humans.get(&self.id) } - fn get_droid<'db>(&self, ctx: &'db Database) -> Option<'db Human>{ - ctx.humans.get(&self.id) + fn get_droid<'db>(&self, ctx: &'db Database) -> Option<&'db Droid>{ + ctx.droids.get(&self.id) } } # @@ -276,6 +285,7 @@ impl Character for Droid { If a context is required in a trait method to resolve a [GraphQL union][1] variant, specify it as an argument. ```rust +# #![allow(unused_variables)] # use std::collections::HashMap; use juniper::{graphql_union, GraphQLObject}; @@ -365,7 +375,7 @@ impl Character for Droid { ### Custom resolvers -Similarly to enums and structs, it's not mandatory to use trait methods as [GraphQL union][1] variant resolvers. and instead custom functions may be specified: +Similarly to enums and structs, it's not mandatory to use trait methods as [GraphQL union][1] variant resolvers, and instead custom functions may be specified: ```rust # use std::collections::HashMap; @@ -410,9 +420,9 @@ impl Character for Droid { } // Used trait object is always `Send` and `Sync`. -type DynCharacter = dyn Character + Send + Sync; +type DynCharacter<'a> = dyn Character + Send + Sync + 'a; -impl DynCharacter { +impl<'a> DynCharacter<'a> { fn get_human<'db>(&self, ctx: &'db Database) -> Option<&'db Human> { ctx.humans.get(self.id()) } @@ -420,8 +430,8 @@ impl DynCharacter { // Custom resolver function doesn't have to be a method. // It's only a matter of the function signature to match the requirements. -fn get_droid<'db>(ch: &DynCharacter, ctx: &'db Database) -> Option<&'db Human> { - ctx.humans.get(ch.id()) +fn get_droid<'db>(ch: &DynCharacter<'_>, ctx: &'db Database) -> Option<&'db Droid> { + ctx.droids.get(ch.id()) } # # fn main() {} From 838e82622e6e963f896c154f877c2246fb6d087d Mon Sep 17 00:00:00 2001 From: tyranron Date: Tue, 2 Jun 2020 13:09:41 +0300 Subject: [PATCH 21/29] Add ScalarValue considerations to book --- docs/book/content/types/unions.md | 37 ++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/docs/book/content/types/unions.md b/docs/book/content/types/unions.md index 92c592925..f568d0f64 100644 --- a/docs/book/content/types/unions.md +++ b/docs/book/content/types/unions.md @@ -170,7 +170,7 @@ enum Character { impl Character { fn ewok_from_context<'c>(&self, ctx: &'c CustomContext) -> Option<&'c Ewok> { - if matches!(self, Self::Ewok) { + if let Self::Ewok = self { Some(&ctx.ewok) } else { None @@ -440,5 +440,40 @@ fn get_droid<'db>(ch: &DynCharacter<'_>, ctx: &'db Database) -> Option<&'db Droi +## `ScalarValue` considerations + +By default, `#[derive(GraphQLUnion)]` and `#[graphql_union]` macros generate code, which is generic over a [`ScalarValue`][2] type. This may introduce a problem when at least one of [GraphQL union][1] variants is restricted to a concrete [`ScalarValue`][2] type in its implementation. To resolve such problem, a concrete [`ScalarValue`][2] type should be specified: + +```rust +# #![allow(dead_code)] +use juniper::{DefaultScalarValue, GraphQLObject, GraphQLUnion}; + +#[derive(GraphQLObject)] +#[graphql(Scalar = DefaultScalarValue)] +struct Human { + id: String, + home_planet: String, +} + +#[derive(GraphQLObject)] +struct Droid { + id: String, + primary_function: String, +} + +#[derive(GraphQLUnion)] +#[graphql(Scalar = DefaultScalarValue)] // removing this line will fail compilation +enum Character { + Human(Human), + Droid(Droid), +} +# +# fn main() {} +``` + + + + [1]: https://spec.graphql.org/June2018/#sec-Unions +[2]: https://docs.rs/juniper/latest/juniper/trait.ScalarValue.html From ab74bd85f8d3e4f61c05d92101ea007dba89789a Mon Sep 17 00:00:00 2001 From: tyranron Date: Tue, 2 Jun 2020 14:59:57 +0300 Subject: [PATCH 22/29] Document macro definitions --- docs/book/content/types/unions.md | 15 +- juniper_codegen/Cargo.toml | 1 + juniper_codegen/src/lib.rs | 578 ++++++++++++++++++++++++++++++ 3 files changed, 586 insertions(+), 8 deletions(-) diff --git a/docs/book/content/types/unions.md b/docs/book/content/types/unions.md index f568d0f64..458902d10 100644 --- a/docs/book/content/types/unions.md +++ b/docs/book/content/types/unions.md @@ -80,9 +80,9 @@ enum Character { ``` -### Custom resolvers +### External resolver functions -If some custom logic is needed to resolve a [GraphQL union][1] variant, you may specify a function to do so: +If some custom logic is needed to resolve a [GraphQL union][1] variant, you may specify an external function to do so: ```rust # #![allow(dead_code)] @@ -126,7 +126,7 @@ impl Character { # fn main() {} ``` -With a custom resolver we can even declare a new [GraphQL union][1] variant where the Rust type is absent in the initial enum definition. The attribute syntax `#[graphql(on VariantType = resolver_fn)]` follows the [GraphQL syntax for dispatching union variants](https://spec.graphql.org/June2018/#example-f8163). +With an external resolver function we can even declare a new [GraphQL union][1] variant where the Rust type is absent in the initial enum definition. The attribute syntax `#[graphql(on VariantType = resolver_fn)]` follows the [GraphQL syntax for dispatching union variants](https://spec.graphql.org/June2018/#example-f8163). ```rust # #![allow(dead_code)] @@ -186,7 +186,7 @@ impl Character { ## Structs -Using Rust structs as [GraphQL unions][1] is very similar to using enums, with the nuance that specifying a custom resolver is the only way to declare a [GraphQL union][1] variant. +Using Rust structs as [GraphQL unions][1] is very similar to using enums, with the nuance that specifying an external resolver function is the only way to declare a [GraphQL union][1] variant. ```rust # use std::collections::HashMap; @@ -262,8 +262,7 @@ struct Droid { #[graphql_union] trait Character { - // NOTICE: The method signature must contain `&self` - // and return `Option<&VariantType>`. + // NOTICE: The method signature must contain `&self` and return `Option<&VariantType>`. fn as_human(&self) -> Option<&Human> { None } fn as_droid(&self) -> Option<&Droid> { None } } @@ -373,7 +372,7 @@ impl Character for Droid { ``` -### Custom resolvers +### External resolver functions Similarly to enums and structs, it's not mandatory to use trait methods as [GraphQL union][1] variant resolvers, and instead custom functions may be specified: @@ -428,7 +427,7 @@ impl<'a> DynCharacter<'a> { } } -// Custom resolver function doesn't have to be a method. +// External resolver function doesn't have to be a method of a type. // It's only a matter of the function signature to match the requirements. fn get_droid<'db>(ch: &DynCharacter<'_>, ctx: &'db Database) -> Option<&'db Droid> { ctx.droids.get(ch.id()) diff --git a/juniper_codegen/Cargo.toml b/juniper_codegen/Cargo.toml index 603f77a74..21faf7cca 100644 --- a/juniper_codegen/Cargo.toml +++ b/juniper_codegen/Cargo.toml @@ -24,5 +24,6 @@ quote = "1.0.3" syn = { version = "1.0.3", features = ["full", "extra-traits", "parsing"] } [dev-dependencies] +derive_more = "0.99.7" futures = "0.3.1" juniper = { version = "0.14.2", path = "../juniper" } diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs index dc39d3faa..2a2aac7c0 100644 --- a/juniper_codegen/src/lib.rs +++ b/juniper_codegen/src/lib.rs @@ -546,6 +546,304 @@ pub fn graphql_subscription_internal(args: TokenStream, input: TokenStream) -> T )) } +/// `#[derive(GraphQLUnion)]` macro for deriving a [GraphQL union][1] implementation for enums and +/// structs. +/// +/// The `#[graphql]` helper attribute is used for configuring the derived implementation. Specifying +/// multiple `#[graphql]` attributes on the same definition is totally okay. They all will be +/// treated as a single attribute. +/// +/// ``` +/// use derive_more::From; +/// use juniper::{GraphQLObject, GraphQLUnion}; +/// +/// #[derive(GraphQLObject)] +/// struct Human { +/// id: String, +/// home_planet: String, +/// } +/// +/// #[derive(GraphQLObject)] +/// struct Droid { +/// id: String, +/// primary_function: String, +/// } +/// +/// #[derive(From, GraphQLUnion)] +/// enum CharacterEnum { +/// Human(Human), +/// Droid(Droid), +/// } +/// ``` +/// +/// # Custom name and description +/// +/// The name of [GraphQL union][1] may be overriden with a `name` attribute's argument. By default, +/// a type name in `PascalCase` is used. +/// +/// The description of [GraphQL union][1] may be specified either with a `description`/`desc` +/// attribute's argument, or with a regular Rust doc comment. +/// +/// ``` +/// # use juniper::{GraphQLObject, GraphQLUnion}; +/// # +/// # #[derive(GraphQLObject)] +/// # struct Human { +/// # id: String, +/// # home_planet: String, +/// # } +/// # +/// # #[derive(GraphQLObject)] +/// # struct Droid { +/// # id: String, +/// # primary_function: String, +/// # } +/// # +/// #[derive(GraphQLUnion)] +/// #[graphql(name = "Character", desc = "Possible episode characters.")] +/// enum Chrctr { +/// Human(Human), +/// Droid(Droid), +/// } +/// +/// // NOTICE: Rust docs are used as GraphQL description. +/// /// Possible episode characters. +/// #[derive(GraphQLUnion)] +/// enum CharacterWithDocs { +/// Human(Human), +/// Droid(Droid), +/// } +/// +/// // NOTICE: `description` argument takes precedence over Rust docs. +/// /// Not a GraphQL description anymore. +/// #[derive(GraphQLUnion)] +/// #[graphql(description = "Possible episode characters.")] +/// enum CharacterWithDescription { +/// Human(Human), +/// Droid(Droid), +/// } +/// ``` +/// +/// # Custom context +/// +/// By default, the generated implementation uses [unit type `()`][4] as context. To use a custom +/// context type for [GraphQL union][1] variants types or external resolver functions, specify it +/// with `context`/`Context` attribute's argument. +/// +/// ``` +/// # use juniper::{GraphQLObject, GraphQLUnion}; +/// # +/// #[derive(GraphQLObject)] +/// #[graphql(Context = CustomContext)] +/// struct Human { +/// id: String, +/// home_planet: String, +/// } +/// +/// #[derive(GraphQLObject)] +/// #[graphql(Context = CustomContext)] +/// struct Droid { +/// id: String, +/// primary_function: String, +/// } +/// +/// pub struct CustomContext; +/// impl juniper::Context for CustomContext {} +/// +/// #[derive(GraphQLUnion)] +/// #[graphql(Context = CustomContext)] +/// enum Character { +/// Human(Human), +/// Droid(Droid), +/// } +/// ``` +/// +/// # Custom `ScalarValue` +/// +/// By default, this macro generates code, which is generic over a `ScalarValue` type. +/// This may introduce a problem when at least one of [GraphQL union][1] variants is restricted to a +/// concrete `ScalarValue` type in its implementation. To resolve such problem, a concrete +/// `ScalarValue` type should be specified with a `scalar`/`Scalar`/`ScalarValue` attribute's +/// argument. +/// +/// ``` +/// # use juniper::{DefaultScalarValue, GraphQLObject, GraphQLUnion}; +/// # +/// #[derive(GraphQLObject)] +/// #[graphql(Scalar = DefaultScalarValue)] +/// struct Human { +/// id: String, +/// home_planet: String, +/// } +/// +/// #[derive(GraphQLObject)] +/// struct Droid { +/// id: String, +/// primary_function: String, +/// } +/// +/// // NOTICE: Removing `Scalar` argument will fail compilation. +/// #[derive(GraphQLUnion)] +/// #[graphql(Scalar = DefaultScalarValue)] +/// enum Character { +/// Human(Human), +/// Droid(Droid), +/// } +/// ``` +/// +/// # Ignoring enum variants +/// +/// To omit exposing an enum variant in the GraphQL schema, use an `ignore`/`skip` attribute's +/// argument directly on that variant. +/// +/// > __WARNING__: +/// > It's the _library user's responsibility_ to ensure that ignored enum variant is _never_ +/// > returned from resolvers, otherwise resolving the GraphQL query will __panic at runtime__. +/// +/// ``` +/// # use std::marker::PhantomData; +/// use derive_more::From; +/// use juniper::{GraphQLObject, GraphQLUnion}; +/// +/// #[derive(GraphQLObject)] +/// struct Human { +/// id: String, +/// home_planet: String, +/// } +/// +/// #[derive(GraphQLObject)] +/// struct Droid { +/// id: String, +/// primary_function: String, +/// } +/// +/// #[derive(From, GraphQLUnion)] +/// enum Character { +/// Human(Human), +/// Droid(Droid), +/// #[from(ignore)] +/// #[graphql(ignore)] // or `#[graphql(skip)]`, your choice +/// _State(PhantomData), +/// } +/// ``` +/// +/// # External resolver functions +/// +/// To use a custom logic for resolving a [GraphQL union][1] variant, an external resolver function +/// may be specified with: +/// - either a `with` attribute's argument on an enum variant; +/// - or an `on` attribute's argument on an enum/struct itself. +/// +/// ``` +/// # use juniper::{GraphQLObject, GraphQLUnion}; +/// # +/// #[derive(GraphQLObject)] +/// #[graphql(Context = CustomContext)] +/// struct Human { +/// id: String, +/// home_planet: String, +/// } +/// +/// #[derive(GraphQLObject)] +/// #[graphql(Context = CustomContext)] +/// struct Droid { +/// id: String, +/// primary_function: String, +/// } +/// +/// pub struct CustomContext { +/// droid: Droid, +/// } +/// impl juniper::Context for CustomContext {} +/// +/// #[derive(GraphQLUnion)] +/// #[graphql(Context = CustomContext)] +/// enum Character { +/// Human(Human), +/// #[graphql(with = Character::droid_from_context)] +/// Droid(Droid), +/// } +/// +/// impl Character { +/// // NOTICE: The function signature must contain `&self` and `&Context`, +/// // and return `Option<&VariantType>`. +/// fn droid_from_context<'c>(&self, ctx: &'c CustomContext) -> Option<&'c Droid> { +/// Some(&ctx.droid) +/// } +/// } +/// +/// #[derive(GraphQLUnion)] +/// #[graphql(Context = CustomContext)] +/// #[graphql(on Droid = CharacterWithoutDroid::droid_from_context)] +/// enum CharacterWithoutDroid { +/// Human(Human), +/// #[graphql(ignore)] +/// Droid, +/// } +/// +/// impl CharacterWithoutDroid { +/// fn droid_from_context<'c>(&self, ctx: &'c CustomContext) -> Option<&'c Droid> { +/// if let Self::Droid = self { +/// Some(&ctx.droid) +/// } else { +/// None +/// } +/// } +/// } +/// ``` +/// +/// # Deriving structs +/// +/// Specifying external resolver functions is mandatory for using a struct as a [GraphQL union][1], +/// because this is the only way to declare [GraphQL union][1] variants in this case. +/// +/// ``` +/// # use std::collections::HashMap; +/// # use juniper::{GraphQLObject, GraphQLUnion}; +/// # +/// #[derive(GraphQLObject)] +/// #[graphql(Context = Database)] +/// struct Human { +/// id: String, +/// home_planet: String, +/// } +/// +/// #[derive(GraphQLObject)] +/// #[graphql(Context = Database)] +/// struct Droid { +/// id: String, +/// primary_function: String, +/// } +/// +/// struct Database { +/// humans: HashMap, +/// droids: HashMap, +/// } +/// impl juniper::Context for Database {} +/// +/// #[derive(GraphQLUnion)] +/// #[graphql( +/// Context = Database, +/// on Human = Character::get_human, +/// on Droid = Character::get_droid, +/// )] +/// struct Character { +/// id: String, +/// } +/// +/// impl Character { +/// fn get_human<'db>(&self, ctx: &'db Database) -> Option<&'db Human>{ +/// ctx.humans.get(&self.id) +/// } +/// +/// fn get_droid<'db>(&self, ctx: &'db Database) -> Option<&'db Droid>{ +/// ctx.droids.get(&self.id) +/// } +/// } +/// ``` +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Unions +/// [4]: https://doc.rust-lang.org/stable/std/primitive.unit.html #[proc_macro_error] #[proc_macro_derive(GraphQLUnion, attributes(graphql))] pub fn derive_union(input: TokenStream) -> TokenStream { @@ -563,6 +861,286 @@ pub fn derive_union_internal(input: TokenStream) -> TokenStream { .into() } +/// `#[graphql_union]` macro for deriving a [GraphQL union][1] implementation for traits. +/// +/// Specifying multiple `#[graphql_union]` attributes on the same definition is totally okay. They +/// all will be treated as a single attribute. +/// +/// A __trait has to be [object safe][2]__, because schema resolvers will need to return a +/// [trait object][3] to specify a [GraphQL union][1] behind it. The [trait object][3] has to be +/// [`Send`] and [`Sync`]. +/// +/// ``` +/// use juniper::{graphql_union, GraphQLObject}; +/// +/// #[derive(GraphQLObject)] +/// struct Human { +/// id: String, +/// home_planet: String, +/// } +/// +/// #[derive(GraphQLObject)] +/// struct Droid { +/// id: String, +/// primary_function: String, +/// } +/// +/// #[graphql_union] +/// trait Character { +/// // NOTICE: The method signature must contain `&self` and return `Option<&VariantType>`. +/// fn as_human(&self) -> Option<&Human> { None } +/// fn as_droid(&self) -> Option<&Droid> { None } +/// } +/// +/// impl Character for Human { +/// fn as_human(&self) -> Option<&Human> { Some(&self) } +/// } +/// +/// impl Character for Droid { +/// fn as_droid(&self) -> Option<&Droid> { Some(&self) } +/// } +/// ``` +/// +/// # Custom name and description +/// +/// The name of [GraphQL union][1] may be overriden with a `name` attribute's argument. By default, +/// a type name in `PascalCase` is used. +/// +/// The description of [GraphQL union][1] may be specified either with a `description`/`desc` +/// attribute's argument, or with a regular Rust doc comment. +/// +/// ``` +/// # use juniper::{graphql_union, GraphQLObject}; +/// # +/// # #[derive(GraphQLObject)] +/// # struct Human { +/// # id: String, +/// # home_planet: String, +/// # } +/// # +/// # #[derive(GraphQLObject)] +/// # struct Droid { +/// # id: String, +/// # primary_function: String, +/// # } +/// # +/// #[graphql_union(name = "Character", desc = "Possible episode characters.")] +/// trait Chrctr { +/// fn as_human(&self) -> Option<&Human> { None } +/// fn as_droid(&self) -> Option<&Droid> { None } +/// } +/// +/// // NOTICE: Rust docs are used as GraphQL description. +/// /// Possible episode characters. +/// trait CharacterWithDocs { +/// fn as_human(&self) -> Option<&Human> { None } +/// fn as_droid(&self) -> Option<&Droid> { None } +/// } +/// +/// // NOTICE: `description` argument takes precedence over Rust docs. +/// /// Not a GraphQL description anymore. +/// #[graphql_union(description = "Possible episode characters.")] +/// trait CharacterWithDescription { +/// fn as_human(&self) -> Option<&Human> { None } +/// fn as_droid(&self) -> Option<&Droid> { None } +/// } +/// # +/// # impl Chrctr for Human {} +/// # impl Chrctr for Droid {} +/// # impl CharacterWithDocs for Human {} +/// # impl CharacterWithDocs for Droid {} +/// # impl CharacterWithDescription for Human {} +/// # impl CharacterWithDescription for Droid {} +/// ``` +/// +/// # Custom context +/// +/// By default, the generated implementation uses [unit type `()`][4] as context. To use a custom +/// context type in trait methods or external resolver functions, specify it with +/// `context`/`Context` attribute's argument. +/// +/// ``` +/// # use std::collections::HashMap; +/// # use juniper::{graphql_union, GraphQLObject}; +/// # +/// #[derive(GraphQLObject)] +/// #[graphql(Context = Database)] +/// struct Human { +/// id: String, +/// home_planet: String, +/// } +/// +/// #[derive(GraphQLObject)] +/// #[graphql(Context = Database)] +/// struct Droid { +/// id: String, +/// primary_function: String, +/// } +/// +/// struct Database { +/// humans: HashMap, +/// droids: HashMap, +/// } +/// impl juniper::Context for Database {} +/// +/// #[graphql_union(Context = Database)] +/// trait Character { +/// fn as_human<'db>(&self, ctx: &'db Database) -> Option<&'db Human> { None } +/// fn as_droid<'db>(&self, ctx: &'db Database) -> Option<&'db Droid> { None } +/// } +/// +/// impl Character for Human { +/// fn as_human<'db>(&self, ctx: &'db Database) -> Option<&'db Human> { +/// ctx.humans.get(&self.id) +/// } +/// } +/// +/// impl Character for Droid { +/// fn as_droid<'db>(&self, ctx: &'db Database) -> Option<&'db Droid> { +/// ctx.droids.get(&self.id) +/// } +/// } +/// ``` +/// +/// # Custom `ScalarValue` +/// +/// By default, `#[graphql_union]` macro generates code, which is generic over a `ScalarValue` type. +/// This may introduce a problem when at least one of [GraphQL union][1] variants is restricted to a +/// concrete `ScalarValue` type in its implementation. To resolve such problem, a concrete +/// `ScalarValue` type should be specified with a `scalar`/`Scalar`/`ScalarValue` attribute's +/// argument. +/// +/// ``` +/// # use juniper::{graphql_union, DefaultScalarValue, GraphQLObject}; +/// # +/// #[derive(GraphQLObject)] +/// #[graphql(Scalar = DefaultScalarValue)] +/// struct Human { +/// id: String, +/// home_planet: String, +/// } +/// +/// #[derive(GraphQLObject)] +/// struct Droid { +/// id: String, +/// primary_function: String, +/// } +/// +/// // NOTICE: Removing `Scalar` argument will fail compilation. +/// #[graphql_union(Scalar = DefaultScalarValue)] +/// trait Character { +/// fn as_human(&self) -> Option<&Human> { None } +/// fn as_droid(&self) -> Option<&Droid> { None } +/// } +/// # +/// # impl Character for Human {} +/// # impl Character for Droid {} +/// ``` +/// +/// # Ignoring trait methods +/// +/// To omit some trait method to be assumed as a [GraphQL union][1] variant and ignore it, use an +/// `ignore`/`skip` attribute's argument directly on that method. +/// +/// ``` +/// # use juniper::{graphql_union, GraphQLObject}; +/// # +/// # #[derive(GraphQLObject)] +/// # struct Human { +/// # id: String, +/// # home_planet: String, +/// # } +/// # +/// # #[derive(GraphQLObject)] +/// # struct Droid { +/// # id: String, +/// # primary_function: String, +/// # } +/// # +/// #[graphql_union] +/// trait Character { +/// fn as_human(&self) -> Option<&Human> { None } +/// fn as_droid(&self) -> Option<&Droid> { None } +/// #[graphql_union(ignore)] // or `#[graphql_union(skip)]`, your choice +/// fn id(&self) -> &str; +/// } +/// # +/// # impl Character for Human { +/// # fn id(&self) -> &str { self.id.as_str() } +/// # } +/// # +/// # impl Character for Droid { +/// # fn id(&self) -> &str { self.id.as_str() } +/// # } +/// ``` +/// +/// # External resolver functions +/// +/// It's not mandatory to use trait methods as [GraphQL union][1] variant resolvers, and instead +/// custom functions may be specified with an `on` attribute's argument. +/// +/// ``` +/// # use std::collections::HashMap; +/// # use juniper::{graphql_union, GraphQLObject}; +/// # +/// #[derive(GraphQLObject)] +/// #[graphql(Context = Database)] +/// struct Human { +/// id: String, +/// home_planet: String, +/// } +/// +/// #[derive(GraphQLObject)] +/// #[graphql(Context = Database)] +/// struct Droid { +/// id: String, +/// primary_function: String, +/// } +/// +/// struct Database { +/// humans: HashMap, +/// droids: HashMap, +/// } +/// impl juniper::Context for Database {} +/// +/// #[graphql_union(Context = Database)] +/// #[graphql_union( +/// on Human = DynCharacter::get_human, +/// on Droid = get_droid, +/// )] +/// trait Character { +/// #[graphql_union(ignore)] +/// fn id(&self) -> &str; +/// } +/// +/// impl Character for Human { +/// fn id(&self) -> &str { self.id.as_str() } +/// } +/// +/// impl Character for Droid { +/// fn id(&self) -> &str { self.id.as_str() } +/// } +/// +/// // NOTICE: Used trait object is always `Send` and `Sync`. +/// type DynCharacter<'a> = dyn Character + Send + Sync + 'a; +/// +/// impl<'a> DynCharacter<'a> { +/// fn get_human<'db>(&self, ctx: &'db Database) -> Option<&'db Human> { +/// ctx.humans.get(self.id()) +/// } +/// } +/// +/// // NOTICE: Custom resolver function doesn't have to be a method of a type. +/// // It's only a matter of the function signature to match the requirements. +/// fn get_droid<'db>(ch: &DynCharacter<'_>, ctx: &'db Database) -> Option<&'db Droid> { +/// ctx.droids.get(ch.id()) +/// } +/// ``` +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Unions +/// [2]: https://doc.rust-lang.org/stable/reference/items/traits.html#object-safety +/// [3]: https://doc.rust-lang.org/stable/reference/types/trait-object.html +/// [4]: https://doc.rust-lang.org/stable/std/primitive.unit.html #[proc_macro_error] #[proc_macro_attribute] pub fn graphql_union(attr: TokenStream, body: TokenStream) -> TokenStream { From 8d517632eef6fc766875109afcc8e90851724124 Mon Sep 17 00:00:00 2001 From: tyranron Date: Tue, 2 Jun 2020 16:33:41 +0300 Subject: [PATCH 23/29] Try infer context type from trait method signature --- juniper_codegen/src/graphql_union/attr.rs | 49 +++++++++++++++++---- juniper_codegen/src/graphql_union/derive.rs | 1 + juniper_codegen/src/graphql_union/mod.rs | 10 +++++ juniper_codegen/src/lib.rs | 8 ++-- 4 files changed, 56 insertions(+), 12 deletions(-) diff --git a/juniper_codegen/src/graphql_union/attr.rs b/juniper_codegen/src/graphql_union/attr.rs index ffb11d10d..6935590b2 100644 --- a/juniper_codegen/src/graphql_union/attr.rs +++ b/juniper_codegen/src/graphql_union/attr.rs @@ -1,6 +1,6 @@ //! Code generation for `#[graphql_union]`/`#[graphql_union_internal]` macros. -use std::{mem, ops::Deref as _}; +use std::{collections::HashSet, mem, ops::Deref as _}; use proc_macro2::{Span, TokenStream}; use quote::{quote, ToTokens as _}; @@ -92,6 +92,16 @@ pub fn expand(attr_args: TokenStream, body: TokenStream, mode: Mode) -> syn::Res }) .collect(); + if meta.context.is_none() && !all_contexts_different(&variants) { + SCOPE.custom( + trait_span, + "cannot infer the appropriate context type, either specify the #[{}(context = MyType)] \ + attribute, so the trait methods can use any type which impls \ + `juniper::FromContext`, or use the same type for all context arguments in \ + trait methods signatures", + ); + } + proc_macro_error::abort_if_dirty(); emerge_union_variants_from_meta(&mut variants, meta.custom_resolvers, mode); @@ -106,12 +116,17 @@ pub fn expand(attr_args: TokenStream, body: TokenStream, mode: Mode) -> syn::Res proc_macro_error::abort_if_dirty(); + let context = meta + .context + .map(SpanContainer::into_inner) + .or_else(|| variants.iter().find_map(|v| v.context_ty.as_ref()).cloned()); + let generated_code = UnionDefinition { name, ty: parse_quote! { #trait_ident }, is_trait_object: true, description: meta.description.map(SpanContainer::into_inner), - context: meta.context.map(SpanContainer::into_inner), + context, scalar: meta.scalar.map(SpanContainer::into_inner), generics: ast.generics.clone(), variants, @@ -182,7 +197,7 @@ fn parse_variant_from_trait_method( ) }) .ok()?; - let accepts_context = parse_trait_method_input_args(&method.sig) + let method_context_ty = parse_trait_method_input_args(&method.sig) .map_err(|span| { SCOPE.custom( span, @@ -214,7 +229,7 @@ fn parse_variant_from_trait_method( ); } - if accepts_context { + if method_context_ty.is_some() { let crate_path = mode.crate_path(); parse_quote! { @@ -239,6 +254,7 @@ fn parse_variant_from_trait_method( resolver_code, resolver_check, enum_path: None, + context_ty: method_context_ty, span: method_span, }) } @@ -290,12 +306,12 @@ fn parse_trait_method_output_type(sig: &syn::Signature) -> Result Result { +fn parse_trait_method_input_args(sig: &syn::Signature) -> Result, Span> { match sig.receiver() { Some(syn::FnArg::Receiver(rcv)) => { if rcv.reference.is_none() || rcv.mutability.is_some() { @@ -311,7 +327,7 @@ fn parse_trait_method_input_args(sig: &syn::Signature) -> Result { let second_arg_ty = match sig.inputs.iter().skip(1).next() { Some(syn::FnArg::Typed(arg)) => arg.ty.deref(), - None => return Ok(false), + None => return Ok(None), _ => return Err(sig.inputs.span()), }; match unparenthesize(second_arg_ty) { @@ -319,9 +335,24 @@ fn parse_trait_method_input_args(sig: &syn::Signature) -> Result { if ref_ty.mutability.is_some() { return Err(ref_ty.span()); } + Ok(Some(ref_ty.elem.deref().clone())) } - ty => return Err(ty.span()), + ty => Err(ty.span()), } +} - Ok(true) +/// Checks whether all [GraphQL union][1] `variants` contains a different type of +/// `juniper::Context`. +/// +/// [1]: https://spec.graphql.org/June2018/#sec-Unions +fn all_contexts_different(variants: &Vec) -> bool { + let all: Vec<_> = variants + .iter() + .filter_map(|v| v.context_ty.as_ref()) + .collect(); + let deduped: HashSet<_> = variants + .iter() + .filter_map(|v| v.context_ty.as_ref()) + .collect(); + deduped.len() == all.len() } diff --git a/juniper_codegen/src/graphql_union/derive.rs b/juniper_codegen/src/graphql_union/derive.rs index f30991e08..a1542fb41 100644 --- a/juniper_codegen/src/graphql_union/derive.rs +++ b/juniper_codegen/src/graphql_union/derive.rs @@ -165,6 +165,7 @@ fn parse_variant_from_enum_variant( resolver_code, resolver_check, enum_path: Some(enum_path), + context_ty: None, span: var_span, }) } diff --git a/juniper_codegen/src/graphql_union/mod.rs b/juniper_codegen/src/graphql_union/mod.rs index 8c4c4a260..37101671d 100644 --- a/juniper_codegen/src/graphql_union/mod.rs +++ b/juniper_codegen/src/graphql_union/mod.rs @@ -319,6 +319,15 @@ struct UnionVariantDefinition { /// [1]: https://spec.graphql.org/June2018/#sec-Unions pub enum_path: Option, + /// Rust type of `juniper::Context` that this [GraphQL union][1] variant requires for + /// resolution. + /// + /// It's available only when code generation happens for Rust traits and a trait method contains + /// context argument. + /// + /// [1]: https://spec.graphql.org/June2018/#sec-Unions + pub context_ty: Option, + /// [`Span`] that points to the Rust source code which defines this [GraphQL union][1] variant. /// /// [1]: https://spec.graphql.org/June2018/#sec-Unions @@ -657,6 +666,7 @@ fn emerge_union_variants_from_meta( resolver_code, resolver_check, enum_path: None, + context_ty: None, span, }) } diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs index 2a2aac7c0..b3e8feb66 100644 --- a/juniper_codegen/src/lib.rs +++ b/juniper_codegen/src/lib.rs @@ -955,9 +955,11 @@ pub fn derive_union_internal(input: TokenStream) -> TokenStream { /// /// # Custom context /// -/// By default, the generated implementation uses [unit type `()`][4] as context. To use a custom -/// context type in trait methods or external resolver functions, specify it with -/// `context`/`Context` attribute's argument. +/// By default, the generated implementation tries to infer `juniper::Context` type from signatures +/// of trait methods, and uses [unit type `()`][4] if signatures contains no context arguments. +/// +/// If `juniper::Context` type cannot be inferred or is inferred incorrectly, then specify it +/// explicitly with `context`/`Context` attribute's argument. /// /// ``` /// # use std::collections::HashMap; From ced1fe7a1d951910902f64c8ba21db48cb2e8170 Mon Sep 17 00:00:00 2001 From: tyranron Date: Tue, 2 Jun 2020 17:18:24 +0300 Subject: [PATCH 24/29] Fix juniper crate tests --- .../src/executor_tests/interfaces_unions.rs | 11 +-- juniper/src/macros/tests/union.rs | 83 ++++++++----------- juniper_codegen/src/graphql_union/mod.rs | 8 +- 3 files changed, 44 insertions(+), 58 deletions(-) diff --git a/juniper/src/executor_tests/interfaces_unions.rs b/juniper/src/executor_tests/interfaces_unions.rs index 51a17ed9f..54fcad306 100644 --- a/juniper/src/executor_tests/interfaces_unions.rs +++ b/juniper/src/executor_tests/interfaces_unions.rs @@ -167,6 +167,7 @@ mod union { value::Value, }; + #[crate::graphql_union_internal] trait Pet { fn as_dog(&self) -> Option<&Dog> { None @@ -176,16 +177,6 @@ mod union { } } - #[crate::graphql_union_internal] - impl<'a> GraphQLUnion for &'a dyn Pet { - fn resolve(&self) { - match self { - Dog => self.as_dog(), - Cat => self.as_cat(), - } - } - } - struct Dog { name: String, woofs: bool, diff --git a/juniper/src/macros/tests/union.rs b/juniper/src/macros/tests/union.rs index 6dfbfe754..9db321eef 100644 --- a/juniper/src/macros/tests/union.rs +++ b/juniper/src/macros/tests/union.rs @@ -14,81 +14,70 @@ use std::marker::PhantomData; use crate::{ ast::InputValue, + graphql_object_internal, schema::model::RootNode, types::scalars::{EmptyMutation, EmptySubscription}, value::{DefaultScalarValue, Object, Value}, + GraphQLUnionInternal, }; struct Concrete; +#[graphql_object_internal] +impl Concrete { + fn simple() -> i32 { + 123 + } +} + +#[derive(GraphQLUnionInternal)] +#[graphql(name = "ACustomNamedUnion", scalar = DefaultScalarValue)] enum CustomName { Concrete(Concrete), } +#[derive(GraphQLUnionInternal)] +#[graphql(on Concrete = WithLifetime::resolve, scalar = DefaultScalarValue)] enum WithLifetime<'a> { + #[graphql(ignore)] Int(PhantomData<&'a i32>), } -enum WithGenerics { - Generic(T), -} -enum DescriptionFirst { - Concrete(Concrete), -} - -struct Root; - -#[crate::graphql_object_internal] -impl Concrete { - fn simple() -> i32 { - 123 - } -} - -#[crate::graphql_union_internal(name = "ACustomNamedUnion")] -impl CustomName { - fn resolve(&self) { - match self { - Concrete => match *self { - CustomName::Concrete(ref c) => Some(c), - }, +impl<'a> WithLifetime<'a> { + fn resolve(&self, _: &()) -> Option<&Concrete> { + if matches!(self, Self::Int(_)) { + Some(&Concrete) + } else { + None } } } -#[crate::graphql_union_internal] -impl<'a> WithLifetime<'a> { - fn resolve(&self) { - match self { - Concrete => match *self { - WithLifetime::Int(_) => Some(&Concrete), - }, - } - } +#[derive(GraphQLUnionInternal)] +#[graphql(on Concrete = WithGenerics::resolve, scalar = DefaultScalarValue)] +enum WithGenerics { + #[graphql(ignore)] + Generic(T), } -#[crate::graphql_union_internal] impl WithGenerics { - fn resolve(&self) { - match self { - Concrete => match *self { - WithGenerics::Generic(_) => Some(&Concrete), - }, + fn resolve(&self, _: &()) -> Option<&Concrete> { + if matches!(self, Self::Generic(_)) { + Some(&Concrete) + } else { + None } } } -#[crate::graphql_union_internal(description = "A description")] -impl DescriptionFirst { - fn resolve(&self) { - match self { - Concrete => match *self { - DescriptionFirst::Concrete(ref c) => Some(c), - }, - } - } +#[derive(GraphQLUnionInternal)] +#[graphql(description = "A description", scalar = DefaultScalarValue)] +enum DescriptionFirst { + Concrete(Concrete), } +struct Root; + // FIXME: make async work #[crate::graphql_object_internal(noasync)] impl<'a> Root { diff --git a/juniper_codegen/src/graphql_union/mod.rs b/juniper_codegen/src/graphql_union/mod.rs index 37101671d..208a8e198 100644 --- a/juniper_codegen/src/graphql_union/mod.rs +++ b/juniper_codegen/src/graphql_union/mod.rs @@ -426,6 +426,12 @@ impl ToTokens for UnionDefinition { let var_types: Vec<_> = self.variants.iter().map(|var| &var.ty).collect(); + let all_variants_unique = if var_types.len() > 1 { + Some(quote! { #crate_path::sa::assert_type_ne_all!(#(#var_types),*); }) + } else { + None + }; + let match_names = self.variants.iter().map(|var| { let var_ty = &var.ty; let var_check = &var.resolver_check; @@ -613,7 +619,7 @@ impl ToTokens for UnionDefinition { #[automatically_derived] impl#impl_generics #crate_path::marker::GraphQLUnion for #ty_full { fn mark() { - #crate_path::sa::assert_type_ne_all!(#(#var_types),*); + #all_variants_unique #( <#var_types as #crate_path::marker::GraphQLObjectType< #default_scalar, From 6815418e67c2622b4c4f59ef5a0a37e05c9eaebf Mon Sep 17 00:00:00 2001 From: tyranron Date: Tue, 2 Jun 2020 18:25:15 +0300 Subject: [PATCH 25/29] Rework existing codegen failure tests --- .../fail/union/derive_enum_field.rs | 12 ------- .../fail/union/derive_no_fields.rs | 4 --- .../fail/union/derive_no_fields.stderr | 7 ---- .../fail/union/derive_same_type.stderr | 10 ------ .../codegen_fail/fail/union/enum_no_fields.rs | 6 ++++ .../fail/union/enum_no_fields.stderr | 7 ++++ .../fail/union/enum_non_object_variant.rs | 14 ++++++++ ....stderr => enum_non_object_variant.stderr} | 12 +++---- .../fail/union/enum_same_type_pretty.rs | 9 ++++++ .../fail/union/enum_same_type_pretty.stderr | 10 ++++++ ...ve_same_type.rs => enum_same_type_ugly.rs} | 4 ++- .../fail/union/enum_same_type_ugly.stderr | 10 ++++++ .../fail/union/impl_enum_field.rs | 23 ------------- .../fail/union/impl_enum_field.stderr | 8 ----- .../codegen_fail/fail/union/impl_no_fields.rs | 10 ------ .../fail/union/impl_no_fields.stderr | 9 ------ .../fail/union/impl_same_type.rs.disabled | 32 ------------------- .../fail/union/struct_no_fields.rs | 6 ++++ .../fail/union/struct_no_fields.stderr | 7 ++++ .../fail/union/struct_non_object_variant.rs | 19 +++++++++++ .../union/struct_non_object_variant.stderr | 17 ++++++++++ .../fail/union/struct_same_type_pretty.rs | 18 +++++++++++ .../fail/union/struct_same_type_pretty.stderr | 5 +++ .../fail/union/struct_same_type_ugly.rs | 18 +++++++++++ .../fail/union/struct_same_type_ugly.stderr | 10 ++++++ .../fail/union/trait_no_fields.rs | 6 ++++ .../fail/union/trait_no_fields.stderr | 7 ++++ .../fail/union/trait_non_object_variant.rs | 14 ++++++++ .../union/trait_non_object_variant.stderr | 17 ++++++++++ .../fail/union/trait_same_type_pretty.rs | 9 ++++++ .../fail/union/trait_same_type_pretty.stderr | 10 ++++++ .../fail/union/trait_same_type_ugly.rs | 9 ++++++ .../fail/union/trait_same_type_ugly.stderr | 10 ++++++ juniper_codegen/src/graphql_union/attr.rs | 11 ++++--- juniper_codegen/src/graphql_union/derive.rs | 12 +++++-- juniper_codegen/src/result.rs | 2 +- 36 files changed, 264 insertions(+), 130 deletions(-) delete mode 100644 integration_tests/codegen_fail/fail/union/derive_enum_field.rs delete mode 100644 integration_tests/codegen_fail/fail/union/derive_no_fields.rs delete mode 100644 integration_tests/codegen_fail/fail/union/derive_no_fields.stderr delete mode 100644 integration_tests/codegen_fail/fail/union/derive_same_type.stderr create mode 100644 integration_tests/codegen_fail/fail/union/enum_no_fields.rs create mode 100644 integration_tests/codegen_fail/fail/union/enum_no_fields.stderr create mode 100644 integration_tests/codegen_fail/fail/union/enum_non_object_variant.rs rename integration_tests/codegen_fail/fail/union/{derive_enum_field.stderr => enum_non_object_variant.stderr} (59%) create mode 100644 integration_tests/codegen_fail/fail/union/enum_same_type_pretty.rs create mode 100644 integration_tests/codegen_fail/fail/union/enum_same_type_pretty.stderr rename integration_tests/codegen_fail/fail/union/{derive_same_type.rs => enum_same_type_ugly.rs} (59%) create mode 100644 integration_tests/codegen_fail/fail/union/enum_same_type_ugly.stderr delete mode 100644 integration_tests/codegen_fail/fail/union/impl_enum_field.rs delete mode 100644 integration_tests/codegen_fail/fail/union/impl_enum_field.stderr delete mode 100644 integration_tests/codegen_fail/fail/union/impl_no_fields.rs delete mode 100644 integration_tests/codegen_fail/fail/union/impl_no_fields.stderr delete mode 100644 integration_tests/codegen_fail/fail/union/impl_same_type.rs.disabled create mode 100644 integration_tests/codegen_fail/fail/union/struct_no_fields.rs create mode 100644 integration_tests/codegen_fail/fail/union/struct_no_fields.stderr create mode 100644 integration_tests/codegen_fail/fail/union/struct_non_object_variant.rs create mode 100644 integration_tests/codegen_fail/fail/union/struct_non_object_variant.stderr create mode 100644 integration_tests/codegen_fail/fail/union/struct_same_type_pretty.rs create mode 100644 integration_tests/codegen_fail/fail/union/struct_same_type_pretty.stderr create mode 100644 integration_tests/codegen_fail/fail/union/struct_same_type_ugly.rs create mode 100644 integration_tests/codegen_fail/fail/union/struct_same_type_ugly.stderr create mode 100644 integration_tests/codegen_fail/fail/union/trait_no_fields.rs create mode 100644 integration_tests/codegen_fail/fail/union/trait_no_fields.stderr create mode 100644 integration_tests/codegen_fail/fail/union/trait_non_object_variant.rs create mode 100644 integration_tests/codegen_fail/fail/union/trait_non_object_variant.stderr create mode 100644 integration_tests/codegen_fail/fail/union/trait_same_type_pretty.rs create mode 100644 integration_tests/codegen_fail/fail/union/trait_same_type_pretty.stderr create mode 100644 integration_tests/codegen_fail/fail/union/trait_same_type_ugly.rs create mode 100644 integration_tests/codegen_fail/fail/union/trait_same_type_ugly.stderr diff --git a/integration_tests/codegen_fail/fail/union/derive_enum_field.rs b/integration_tests/codegen_fail/fail/union/derive_enum_field.rs deleted file mode 100644 index 11b945065..000000000 --- a/integration_tests/codegen_fail/fail/union/derive_enum_field.rs +++ /dev/null @@ -1,12 +0,0 @@ -#[derive(juniper::GraphQLEnum)] -pub enum Test { - A, - B, -} - -#[derive(juniper::GraphQLUnion)] -enum Character { - Test(Test), -} - -fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/derive_no_fields.rs b/integration_tests/codegen_fail/fail/union/derive_no_fields.rs deleted file mode 100644 index 4e4cb17db..000000000 --- a/integration_tests/codegen_fail/fail/union/derive_no_fields.rs +++ /dev/null @@ -1,4 +0,0 @@ -#[derive(juniper::GraphQLUnion)] -enum Character {} - -fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/derive_no_fields.stderr b/integration_tests/codegen_fail/fail/union/derive_no_fields.stderr deleted file mode 100644 index 5c069f6fa..000000000 --- a/integration_tests/codegen_fail/fail/union/derive_no_fields.stderr +++ /dev/null @@ -1,7 +0,0 @@ -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 diff --git a/integration_tests/codegen_fail/fail/union/derive_same_type.stderr b/integration_tests/codegen_fail/fail/union/derive_same_type.stderr deleted file mode 100644 index 6e62556e5..000000000 --- a/integration_tests/codegen_fail/fail/union/derive_same_type.stderr +++ /dev/null @@ -1,10 +0,0 @@ -error[E0119]: conflicting implementations of trait `std::convert::From` 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) diff --git a/integration_tests/codegen_fail/fail/union/enum_no_fields.rs b/integration_tests/codegen_fail/fail/union/enum_no_fields.rs new file mode 100644 index 000000000..a2b14cd2a --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/enum_no_fields.rs @@ -0,0 +1,6 @@ +use juniper::graphql_union; + +#[graphql_union] +trait Character {} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/enum_no_fields.stderr b/integration_tests/codegen_fail/fail/union/enum_no_fields.stderr new file mode 100644 index 000000000..09115b2b1 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/enum_no_fields.stderr @@ -0,0 +1,7 @@ +error: GraphQL union expects at least one union variant + --> $DIR/enum_no_fields.rs:4:1 + | +4 | trait Character {} + | ^^^^^^^^^^^^^^^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Unions diff --git a/integration_tests/codegen_fail/fail/union/enum_non_object_variant.rs b/integration_tests/codegen_fail/fail/union/enum_non_object_variant.rs new file mode 100644 index 000000000..1019baf16 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/enum_non_object_variant.rs @@ -0,0 +1,14 @@ +use juniper::{GraphQLEnum, GraphQLUnion}; + +#[derive(GraphQLEnum)] +pub enum Test { + A, + B, +} + +#[derive(GraphQLUnion)] +enum Character { + Test(Test), +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/derive_enum_field.stderr b/integration_tests/codegen_fail/fail/union/enum_non_object_variant.stderr similarity index 59% rename from integration_tests/codegen_fail/fail/union/derive_enum_field.stderr rename to integration_tests/codegen_fail/fail/union/enum_non_object_variant.stderr index 27ffee901..1da87bdd8 100644 --- a/integration_tests/codegen_fail/fail/union/derive_enum_field.stderr +++ b/integration_tests/codegen_fail/fail/union/enum_non_object_variant.stderr @@ -1,17 +1,17 @@ error[E0277]: the trait bound `Test: juniper::types::marker::GraphQLObjectType` is not satisfied - --> $DIR/derive_enum_field.rs:7:10 + --> $DIR/enum_non_object_variant.rs:9:10 | -7 | #[derive(juniper::GraphQLUnion)] - | ^^^^^^^^^^^^^^^^^^^^^ the trait `juniper::types::marker::GraphQLObjectType` is not implemented for `Test` +9 | #[derive(GraphQLUnion)] + | ^^^^^^^^^^^^ the trait `juniper::types::marker::GraphQLObjectType` 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) error[E0277]: the trait bound `Test: juniper::types::marker::GraphQLObjectType<__S>` is not satisfied - --> $DIR/derive_enum_field.rs:7:10 + --> $DIR/enum_non_object_variant.rs:9:10 | -7 | #[derive(juniper::GraphQLUnion)] - | ^^^^^^^^^^^^^^^^^^^^^ the trait `juniper::types::marker::GraphQLObjectType<__S>` is not implemented for `Test` +9 | #[derive(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) diff --git a/integration_tests/codegen_fail/fail/union/enum_same_type_pretty.rs b/integration_tests/codegen_fail/fail/union/enum_same_type_pretty.rs new file mode 100644 index 000000000..cbc1a12e0 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/enum_same_type_pretty.rs @@ -0,0 +1,9 @@ +use juniper::GraphQLUnion; + +#[derive(GraphQLUnion)] +enum Character { + A(u8), + B(u8), +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/enum_same_type_pretty.stderr b/integration_tests/codegen_fail/fail/union/enum_same_type_pretty.stderr new file mode 100644 index 000000000..65cac9c36 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/enum_same_type_pretty.stderr @@ -0,0 +1,10 @@ +error: GraphQL union must have a different type for each union variant + --> $DIR/enum_same_type_pretty.rs:4:1 + | +4 | / enum Character { +5 | | A(u8), +6 | | B(u8), +7 | | } + | |_^ + | + = note: https://spec.graphql.org/June2018/#sec-Unions diff --git a/integration_tests/codegen_fail/fail/union/derive_same_type.rs b/integration_tests/codegen_fail/fail/union/enum_same_type_ugly.rs similarity index 59% rename from integration_tests/codegen_fail/fail/union/derive_same_type.rs rename to integration_tests/codegen_fail/fail/union/enum_same_type_ugly.rs index e267a601b..28cf2af84 100644 --- a/integration_tests/codegen_fail/fail/union/derive_same_type.rs +++ b/integration_tests/codegen_fail/fail/union/enum_same_type_ugly.rs @@ -1,4 +1,6 @@ -#[derive(juniper::GraphQLUnion)] +use juniper::GraphQLUnion; + +#[derive(GraphQLUnion)] enum Character { A(std::string::String), B(String), diff --git a/integration_tests/codegen_fail/fail/union/enum_same_type_ugly.stderr b/integration_tests/codegen_fail/fail/union/enum_same_type_ugly.stderr new file mode 100644 index 000000000..236daf7f2 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/enum_same_type_ugly.stderr @@ -0,0 +1,10 @@ +error[E0119]: conflicting implementations of trait `::mark::_::{{closure}}#0::MutuallyExclusive` for type `std::string::String`: + --> $DIR/enum_same_type_ugly.rs:3:10 + | +3 | #[derive(GraphQLUnion)] + | ^^^^^^^^^^^^ + | | + | first implementation here + | conflicting implementation for `std::string::String` + | + = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/integration_tests/codegen_fail/fail/union/impl_enum_field.rs b/integration_tests/codegen_fail/fail/union/impl_enum_field.rs deleted file mode 100644 index f3b67dd2b..000000000 --- a/integration_tests/codegen_fail/fail/union/impl_enum_field.rs +++ /dev/null @@ -1,23 +0,0 @@ -#[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() {} diff --git a/integration_tests/codegen_fail/fail/union/impl_enum_field.stderr b/integration_tests/codegen_fail/fail/union/impl_enum_field.stderr deleted file mode 100644 index 0337b9985..000000000 --- a/integration_tests/codegen_fail/fail/union/impl_enum_field.stderr +++ /dev/null @@ -1,8 +0,0 @@ -error[E0277]: the trait bound `Test: juniper::types::marker::GraphQLObjectType` is not satisfied - --> $DIR/impl_enum_field.rs:12:1 - | -12 | #[juniper::graphql_union] - | ^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `juniper::types::marker::GraphQLObjectType` 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) diff --git a/integration_tests/codegen_fail/fail/union/impl_no_fields.rs b/integration_tests/codegen_fail/fail/union/impl_no_fields.rs deleted file mode 100644 index bafbe4778..000000000 --- a/integration_tests/codegen_fail/fail/union/impl_no_fields.rs +++ /dev/null @@ -1,10 +0,0 @@ -enum Character {} - -#[juniper::graphql_union] -impl Character { - fn resolve(&self) { - match self {} - } -} - -fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/impl_no_fields.stderr b/integration_tests/codegen_fail/fail/union/impl_no_fields.stderr deleted file mode 100644 index 7eb1e9c75..000000000 --- a/integration_tests/codegen_fail/fail/union/impl_no_fields.stderr +++ /dev/null @@ -1,9 +0,0 @@ -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 diff --git a/integration_tests/codegen_fail/fail/union/impl_same_type.rs.disabled b/integration_tests/codegen_fail/fail/union/impl_same_type.rs.disabled deleted file mode 100644 index 4f070957f..000000000 --- a/integration_tests/codegen_fail/fail/union/impl_same_type.rs.disabled +++ /dev/null @@ -1,32 +0,0 @@ -// NOTICE: This can not be tested. Implementing Into for each -// variant is not possible since we did not created the -// enum. Therefore, it is possible that the enum already has existing -// Into 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() {} diff --git a/integration_tests/codegen_fail/fail/union/struct_no_fields.rs b/integration_tests/codegen_fail/fail/union/struct_no_fields.rs new file mode 100644 index 000000000..51fb88995 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/struct_no_fields.rs @@ -0,0 +1,6 @@ +use juniper::GraphQLUnion; + +#[derive(GraphQLUnion)] +struct Character; + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/struct_no_fields.stderr b/integration_tests/codegen_fail/fail/union/struct_no_fields.stderr new file mode 100644 index 000000000..3d6aaeb2b --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/struct_no_fields.stderr @@ -0,0 +1,7 @@ +error: GraphQL union expects at least one union variant + --> $DIR/struct_no_fields.rs:4:1 + | +4 | struct Character; + | ^^^^^^^^^^^^^^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Unions diff --git a/integration_tests/codegen_fail/fail/union/struct_non_object_variant.rs b/integration_tests/codegen_fail/fail/union/struct_non_object_variant.rs new file mode 100644 index 000000000..553cde13d --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/struct_non_object_variant.rs @@ -0,0 +1,19 @@ +use juniper::{GraphQLEnum, GraphQLUnion}; + +#[derive(GraphQLEnum)] +pub enum Test { + A, + B, +} + +#[derive(GraphQLUnion)] +#[graphql(on Test = Character::a)] +struct Character; + +impl Character { + fn a(&self, _: &()) -> Option<&Test> { + None + } +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/struct_non_object_variant.stderr b/integration_tests/codegen_fail/fail/union/struct_non_object_variant.stderr new file mode 100644 index 000000000..53d7ad7e5 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/struct_non_object_variant.stderr @@ -0,0 +1,17 @@ +error[E0277]: the trait bound `Test: juniper::types::marker::GraphQLObjectType` is not satisfied + --> $DIR/struct_non_object_variant.rs:9:10 + | +9 | #[derive(GraphQLUnion)] + | ^^^^^^^^^^^^ the trait `juniper::types::marker::GraphQLObjectType` 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) + +error[E0277]: the trait bound `Test: juniper::types::marker::GraphQLObjectType<__S>` is not satisfied + --> $DIR/struct_non_object_variant.rs:9:10 + | +9 | #[derive(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) diff --git a/integration_tests/codegen_fail/fail/union/struct_same_type_pretty.rs b/integration_tests/codegen_fail/fail/union/struct_same_type_pretty.rs new file mode 100644 index 000000000..9df112110 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/struct_same_type_pretty.rs @@ -0,0 +1,18 @@ +use juniper::GraphQLUnion; + +#[derive(GraphQLUnion)] +#[graphql(on i32 = Character::a)] +#[graphql(on i32 = Character::b)] +struct Character; + +impl Character { + fn a(&self, _: &()) -> Option<&i32> { + None + } + + fn b(&self, _: &()) -> Option<&i32> { + None + } +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/struct_same_type_pretty.stderr b/integration_tests/codegen_fail/fail/union/struct_same_type_pretty.stderr new file mode 100644 index 000000000..fca94bfb8 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/struct_same_type_pretty.stderr @@ -0,0 +1,5 @@ +error: duplicated attribute + --> $DIR/struct_same_type_pretty.rs:5:14 + | +5 | #[graphql(on i32 = Character::b)] + | ^^^ diff --git a/integration_tests/codegen_fail/fail/union/struct_same_type_ugly.rs b/integration_tests/codegen_fail/fail/union/struct_same_type_ugly.rs new file mode 100644 index 000000000..b1467262c --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/struct_same_type_ugly.rs @@ -0,0 +1,18 @@ +use juniper::GraphQLUnion; + +#[derive(GraphQLUnion)] +#[graphql(on String = Character::a)] +#[graphql(on std::string::String = Character::b)] +struct Character; + +impl Character { + fn a(&self, _: &()) -> Option<&String> { + None + } + + fn b(&self, _: &()) -> Option<&String> { + None + } +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/struct_same_type_ugly.stderr b/integration_tests/codegen_fail/fail/union/struct_same_type_ugly.stderr new file mode 100644 index 000000000..e901067c5 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/struct_same_type_ugly.stderr @@ -0,0 +1,10 @@ +error[E0119]: conflicting implementations of trait `::mark::_::{{closure}}#0::MutuallyExclusive` for type `std::string::String`: + --> $DIR/struct_same_type_ugly.rs:3:10 + | +3 | #[derive(GraphQLUnion)] + | ^^^^^^^^^^^^ + | | + | first implementation here + | conflicting implementation for `std::string::String` + | + = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/integration_tests/codegen_fail/fail/union/trait_no_fields.rs b/integration_tests/codegen_fail/fail/union/trait_no_fields.rs new file mode 100644 index 000000000..66d35f020 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_no_fields.rs @@ -0,0 +1,6 @@ +use juniper::GraphQLUnion; + +#[derive(GraphQLUnion)] +enum Character {} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/trait_no_fields.stderr b/integration_tests/codegen_fail/fail/union/trait_no_fields.stderr new file mode 100644 index 000000000..890cc0c67 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_no_fields.stderr @@ -0,0 +1,7 @@ +error: GraphQL union expects at least one union variant + --> $DIR/trait_no_fields.rs:4:1 + | +4 | enum Character {} + | ^^^^^^^^^^^^^^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Unions diff --git a/integration_tests/codegen_fail/fail/union/trait_non_object_variant.rs b/integration_tests/codegen_fail/fail/union/trait_non_object_variant.rs new file mode 100644 index 000000000..4a1626b24 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_non_object_variant.rs @@ -0,0 +1,14 @@ +use juniper::{graphql_union, GraphQLEnum}; + +#[derive(GraphQLEnum)] +pub enum Test { + A, + B, +} + +#[graphql_union] +trait Character { + fn a(&self) -> Option<&Test>; +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/trait_non_object_variant.stderr b/integration_tests/codegen_fail/fail/union/trait_non_object_variant.stderr new file mode 100644 index 000000000..98e0193b3 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_non_object_variant.stderr @@ -0,0 +1,17 @@ +error[E0277]: the trait bound `Test: juniper::types::marker::GraphQLObjectType` is not satisfied + --> $DIR/trait_non_object_variant.rs:9:1 + | +9 | #[graphql_union] + | ^^^^^^^^^^^^^^^^ the trait `juniper::types::marker::GraphQLObjectType` 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) + +error[E0277]: the trait bound `Test: juniper::types::marker::GraphQLObjectType<__S>` is not satisfied + --> $DIR/trait_non_object_variant.rs:9:1 + | +9 | #[graphql_union] + | ^^^^^^^^^^^^^^^^ 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 an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/integration_tests/codegen_fail/fail/union/trait_same_type_pretty.rs b/integration_tests/codegen_fail/fail/union/trait_same_type_pretty.rs new file mode 100644 index 000000000..e84f5eb99 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_same_type_pretty.rs @@ -0,0 +1,9 @@ +use juniper::graphql_union; + +#[graphql_union] +trait Character { + fn a(&self) -> Option<&u8>; + fn b(&self) -> Option<&u8>; +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/trait_same_type_pretty.stderr b/integration_tests/codegen_fail/fail/union/trait_same_type_pretty.stderr new file mode 100644 index 000000000..f899e3075 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_same_type_pretty.stderr @@ -0,0 +1,10 @@ +error: GraphQL union must have a different type for each union variant + --> $DIR/trait_same_type_pretty.rs:4:1 + | +4 | / trait Character { +5 | | fn a(&self) -> Option<&u8>; +6 | | fn b(&self) -> Option<&u8>; +7 | | } + | |_^ + | + = note: https://spec.graphql.org/June2018/#sec-Unions diff --git a/integration_tests/codegen_fail/fail/union/trait_same_type_ugly.rs b/integration_tests/codegen_fail/fail/union/trait_same_type_ugly.rs new file mode 100644 index 000000000..b6274ad23 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_same_type_ugly.rs @@ -0,0 +1,9 @@ +use juniper::graphql_union; + +#[graphql_union] +trait Character { + fn a(&self) -> Option<&String>; + fn b(&self) -> Option<&std::string::String>; +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/trait_same_type_ugly.stderr b/integration_tests/codegen_fail/fail/union/trait_same_type_ugly.stderr new file mode 100644 index 000000000..39684e76d --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_same_type_ugly.stderr @@ -0,0 +1,10 @@ +error[E0119]: conflicting implementations of trait `<(dyn Character + std::marker::Send + std::marker::Sync + '__obj) as juniper::types::marker::GraphQLUnion>::mark::_::{{closure}}#0::MutuallyExclusive` for type `std::string::String`: + --> $DIR/trait_same_type_ugly.rs:3:1 + | +3 | #[graphql_union] + | ^^^^^^^^^^^^^^^^ + | | + | first implementation here + | conflicting implementation for `std::string::String` + | + = note: this error originates in a macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/juniper_codegen/src/graphql_union/attr.rs b/juniper_codegen/src/graphql_union/attr.rs index 6935590b2..51e2598e2 100644 --- a/juniper_codegen/src/graphql_union/attr.rs +++ b/juniper_codegen/src/graphql_union/attr.rs @@ -111,7 +111,10 @@ pub fn expand(attr_args: TokenStream, body: TokenStream, mode: Mode) -> syn::Res } if !all_variants_different(&variants) { - SCOPE.custom(trait_span, "each union variant must have a different type"); + SCOPE.custom( + trait_span, + "must have a different type for each union variant", + ); } proc_macro_error::abort_if_dirty(); @@ -193,7 +196,7 @@ fn parse_variant_from_trait_method( .map_err(|span| { SCOPE.custom( span, - "trait method return type can be `Option<&VariantType>` only", + "expects trait method return type to be `Option<&VariantType>` only", ) }) .ok()?; @@ -201,14 +204,14 @@ fn parse_variant_from_trait_method( .map_err(|span| { SCOPE.custom( span, - "trait method can accept `&self` and optionally `&Context` only", + "expects trait method to accept `&self` only and, optionally, `&Context`", ) }) .ok()?; if let Some(is_async) = &method.sig.asyncness { SCOPE.custom( is_async.span(), - "async union variants resolvers are not supported yet", + "doesn't support async union variants resolvers yet", ); return None; } diff --git a/juniper_codegen/src/graphql_union/derive.rs b/juniper_codegen/src/graphql_union/derive.rs index a1542fb41..fd92d2613 100644 --- a/juniper_codegen/src/graphql_union/derive.rs +++ b/juniper_codegen/src/graphql_union/derive.rs @@ -69,7 +69,10 @@ fn expand_enum(ast: syn::DeriveInput, mode: Mode) -> syn::Result syn::Result>(&self, span: Span, msg: S) { - Diagnostic::spanned(span, Level::Error, format!("{}: {}", self, msg.as_ref())) + Diagnostic::spanned(span, Level::Error, format!("{} {}", self, msg.as_ref())) .note(self.spec_link()) .emit(); } From 8f40bde1b857318a0328513f1c4c0dfae7d16292 Mon Sep 17 00:00:00 2001 From: tyranron Date: Wed, 3 Jun 2020 14:57:37 +0300 Subject: [PATCH 26/29] Cover up codegen fail cases Additionally: - remove redundant PascalCasing on union name - refactor GraphQLScope::custom() usage - provide better error messages when codegen fails - refactor terminology 'custom resolver' -> 'external resolver function' --- .../fail/union/attr_wrong_item.rs | 6 ++ .../fail/union/attr_wrong_item.stderr | 7 ++ .../fail/union/derive_wrong_item.rs | 6 ++ .../fail/union/derive_wrong_item.stderr | 5 ++ ...licts_with_variant_external_resolver_fn.rs | 15 ++++ ...s_with_variant_external_resolver_fn.stderr | 7 ++ .../union/enum_name_double_underscored.rs | 13 +++ .../union/enum_name_double_underscored.stderr | 7 ++ .../codegen_fail/fail/union/enum_no_fields.rs | 6 +- .../fail/union/enum_no_fields.stderr | 4 +- .../fail/union/enum_wrong_variant_field.rs | 18 ++++ .../union/enum_wrong_variant_field.stderr | 15 ++++ .../union/struct_name_double_underscored.rs | 18 ++++ .../struct_name_double_underscored.stderr | 7 ++ .../fail/union/trait_fail_infer_context.rs | 35 ++++++++ .../union/trait_fail_infer_context.stderr | 18 ++++ ...hod_conflicts_with_external_resolver_fn.rs | 13 +++ ...conflicts_with_external_resolver_fn.stderr | 8 ++ .../union/trait_name_double_underscored.rs | 13 +++ .../trait_name_double_underscored.stderr | 7 ++ .../fail/union/trait_no_fields.rs | 6 +- .../fail/union/trait_no_fields.stderr | 4 +- .../fail/union/trait_with_attr_on_method.rs | 14 ++++ .../union/trait_with_attr_on_method.stderr | 8 ++ .../union/trait_wrong_method_input_args.rs | 13 +++ .../trait_wrong_method_input_args.stderr | 7 ++ .../union/trait_wrong_method_return_type.rs | 13 +++ .../trait_wrong_method_return_type.stderr | 7 ++ juniper_codegen/src/derive_enum.rs | 2 +- juniper_codegen/src/graphql_union/attr.rs | 82 ++++++++----------- juniper_codegen/src/graphql_union/derive.rs | 41 +++++----- juniper_codegen/src/graphql_union/mod.rs | 40 ++++----- juniper_codegen/src/impl_object.rs | 2 +- juniper_codegen/src/lib.rs | 4 +- juniper_codegen/src/result.rs | 7 +- juniper_codegen/src/util/mod.rs | 41 ---------- 36 files changed, 374 insertions(+), 145 deletions(-) create mode 100644 integration_tests/codegen_fail/fail/union/attr_wrong_item.rs create mode 100644 integration_tests/codegen_fail/fail/union/attr_wrong_item.stderr create mode 100644 integration_tests/codegen_fail/fail/union/derive_wrong_item.rs create mode 100644 integration_tests/codegen_fail/fail/union/derive_wrong_item.stderr create mode 100644 integration_tests/codegen_fail/fail/union/enum_external_resolver_fn_conflicts_with_variant_external_resolver_fn.rs create mode 100644 integration_tests/codegen_fail/fail/union/enum_external_resolver_fn_conflicts_with_variant_external_resolver_fn.stderr create mode 100644 integration_tests/codegen_fail/fail/union/enum_name_double_underscored.rs create mode 100644 integration_tests/codegen_fail/fail/union/enum_name_double_underscored.stderr create mode 100644 integration_tests/codegen_fail/fail/union/enum_wrong_variant_field.rs create mode 100644 integration_tests/codegen_fail/fail/union/enum_wrong_variant_field.stderr create mode 100644 integration_tests/codegen_fail/fail/union/struct_name_double_underscored.rs create mode 100644 integration_tests/codegen_fail/fail/union/struct_name_double_underscored.stderr create mode 100644 integration_tests/codegen_fail/fail/union/trait_fail_infer_context.rs create mode 100644 integration_tests/codegen_fail/fail/union/trait_fail_infer_context.stderr create mode 100644 integration_tests/codegen_fail/fail/union/trait_method_conflicts_with_external_resolver_fn.rs create mode 100644 integration_tests/codegen_fail/fail/union/trait_method_conflicts_with_external_resolver_fn.stderr create mode 100644 integration_tests/codegen_fail/fail/union/trait_name_double_underscored.rs create mode 100644 integration_tests/codegen_fail/fail/union/trait_name_double_underscored.stderr create mode 100644 integration_tests/codegen_fail/fail/union/trait_with_attr_on_method.rs create mode 100644 integration_tests/codegen_fail/fail/union/trait_with_attr_on_method.stderr create mode 100644 integration_tests/codegen_fail/fail/union/trait_wrong_method_input_args.rs create mode 100644 integration_tests/codegen_fail/fail/union/trait_wrong_method_input_args.stderr create mode 100644 integration_tests/codegen_fail/fail/union/trait_wrong_method_return_type.rs create mode 100644 integration_tests/codegen_fail/fail/union/trait_wrong_method_return_type.stderr diff --git a/integration_tests/codegen_fail/fail/union/attr_wrong_item.rs b/integration_tests/codegen_fail/fail/union/attr_wrong_item.rs new file mode 100644 index 000000000..76d7b27ea --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/attr_wrong_item.rs @@ -0,0 +1,6 @@ +use juniper::graphql_union; + +#[graphql_union] +enum Character {} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/attr_wrong_item.stderr b/integration_tests/codegen_fail/fail/union/attr_wrong_item.stderr new file mode 100644 index 000000000..419835d51 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/attr_wrong_item.stderr @@ -0,0 +1,7 @@ +error: #[graphql_union] attribute is applicable to trait definitions only + --> $DIR/attr_wrong_item.rs:3:1 + | +3 | #[graphql_union] + | ^^^^^^^^^^^^^^^^ + | + = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/integration_tests/codegen_fail/fail/union/derive_wrong_item.rs b/integration_tests/codegen_fail/fail/union/derive_wrong_item.rs new file mode 100644 index 000000000..174cd2ad1 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/derive_wrong_item.rs @@ -0,0 +1,6 @@ +use juniper::GraphQLUnion; + +#[derive(GraphQLUnion)] +union Character { id: i32 } + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/derive_wrong_item.stderr b/integration_tests/codegen_fail/fail/union/derive_wrong_item.stderr new file mode 100644 index 000000000..fdeb15b40 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/derive_wrong_item.stderr @@ -0,0 +1,5 @@ +error: GraphQL union can only be derived for enums and structs + --> $DIR/derive_wrong_item.rs:4:1 + | +4 | union Character { id: i32 } + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/integration_tests/codegen_fail/fail/union/enum_external_resolver_fn_conflicts_with_variant_external_resolver_fn.rs b/integration_tests/codegen_fail/fail/union/enum_external_resolver_fn_conflicts_with_variant_external_resolver_fn.rs new file mode 100644 index 000000000..2da4466f6 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/enum_external_resolver_fn_conflicts_with_variant_external_resolver_fn.rs @@ -0,0 +1,15 @@ +use juniper::{GraphQLObject, GraphQLUnion}; + +#[derive(GraphQLUnion)] +#[graphql(on Human = resolve_fn1)] +enum Character { + #[graphql(with = resolve_fn2)] + A(Human), +} + +#[derive(GraphQLObject)] +pub struct Human { + id: String, +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/enum_external_resolver_fn_conflicts_with_variant_external_resolver_fn.stderr b/integration_tests/codegen_fail/fail/union/enum_external_resolver_fn_conflicts_with_variant_external_resolver_fn.stderr new file mode 100644 index 000000000..12957f131 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/enum_external_resolver_fn_conflicts_with_variant_external_resolver_fn.stderr @@ -0,0 +1,7 @@ +error: GraphQL union variant `Human` already has external resolver function `resolve_fn1` declared on the enum + --> $DIR/enum_external_resolver_fn_conflicts_with_variant_external_resolver_fn.rs:6:15 + | +6 | #[graphql(with = resolve_fn2)] + | ^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Unions diff --git a/integration_tests/codegen_fail/fail/union/enum_name_double_underscored.rs b/integration_tests/codegen_fail/fail/union/enum_name_double_underscored.rs new file mode 100644 index 000000000..2d0fa6902 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/enum_name_double_underscored.rs @@ -0,0 +1,13 @@ +use juniper::{GraphQLObject, GraphQLUnion}; + +#[derive(GraphQLUnion)] +enum __Character { + A(Human), +} + +#[derive(GraphQLObject)] +pub struct Human { + id: String, +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/enum_name_double_underscored.stderr b/integration_tests/codegen_fail/fail/union/enum_name_double_underscored.stderr new file mode 100644 index 000000000..df5db23fd --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/enum_name_double_underscored.stderr @@ -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 GraphQL’s introspection system. + --> $DIR/enum_name_double_underscored.rs:4:6 + | +4 | enum __Character { + | ^^^^^^^^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Schema diff --git a/integration_tests/codegen_fail/fail/union/enum_no_fields.rs b/integration_tests/codegen_fail/fail/union/enum_no_fields.rs index a2b14cd2a..66d35f020 100644 --- a/integration_tests/codegen_fail/fail/union/enum_no_fields.rs +++ b/integration_tests/codegen_fail/fail/union/enum_no_fields.rs @@ -1,6 +1,6 @@ -use juniper::graphql_union; +use juniper::GraphQLUnion; -#[graphql_union] -trait Character {} +#[derive(GraphQLUnion)] +enum Character {} fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/enum_no_fields.stderr b/integration_tests/codegen_fail/fail/union/enum_no_fields.stderr index 09115b2b1..85ea25850 100644 --- a/integration_tests/codegen_fail/fail/union/enum_no_fields.stderr +++ b/integration_tests/codegen_fail/fail/union/enum_no_fields.stderr @@ -1,7 +1,7 @@ error: GraphQL union expects at least one union variant --> $DIR/enum_no_fields.rs:4:1 | -4 | trait Character {} - | ^^^^^^^^^^^^^^^^^^ +4 | enum Character {} + | ^^^^^^^^^^^^^^^^^ | = note: https://spec.graphql.org/June2018/#sec-Unions diff --git a/integration_tests/codegen_fail/fail/union/enum_wrong_variant_field.rs b/integration_tests/codegen_fail/fail/union/enum_wrong_variant_field.rs new file mode 100644 index 000000000..84492eb88 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/enum_wrong_variant_field.rs @@ -0,0 +1,18 @@ +use juniper::{GraphQLObject, GraphQLUnion}; + +#[derive(GraphQLUnion)] +enum Character1 { + A { human: Human }, +} + +#[derive(GraphQLUnion)] +enum Character2 { + A(Human, u8), +} + +#[derive(GraphQLObject)] +pub struct Human { + id: String, +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/enum_wrong_variant_field.stderr b/integration_tests/codegen_fail/fail/union/enum_wrong_variant_field.stderr new file mode 100644 index 000000000..4f5def6a9 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/enum_wrong_variant_field.stderr @@ -0,0 +1,15 @@ +error: GraphQL union enum allows only unnamed variants with a single field, e.g. `Some(T)` + --> $DIR/enum_wrong_variant_field.rs:5:5 + | +5 | A { human: Human }, + | ^ + | + = note: https://spec.graphql.org/June2018/#sec-Unions + +error: GraphQL union enum allows only unnamed variants with a single field, e.g. `Some(T)` + --> $DIR/enum_wrong_variant_field.rs:10:6 + | +10 | A(Human, u8), + | ^^^^^^^^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Unions diff --git a/integration_tests/codegen_fail/fail/union/struct_name_double_underscored.rs b/integration_tests/codegen_fail/fail/union/struct_name_double_underscored.rs new file mode 100644 index 000000000..220bce55f --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/struct_name_double_underscored.rs @@ -0,0 +1,18 @@ +use juniper::{GraphQLObject, GraphQLUnion}; + +#[derive(GraphQLUnion)] +#[graphql(on Human = __Character::a)] +struct __Character; + +impl __Character { + fn a(&self, _: &()) -> Option<&Human> { + None + } +} + +#[derive(GraphQLObject)] +pub struct Human { + id: String, +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/struct_name_double_underscored.stderr b/integration_tests/codegen_fail/fail/union/struct_name_double_underscored.stderr new file mode 100644 index 000000000..48054c637 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/struct_name_double_underscored.stderr @@ -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 GraphQL’s introspection system. + --> $DIR/struct_name_double_underscored.rs:5:8 + | +5 | struct __Character; + | ^^^^^^^^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Schema diff --git a/integration_tests/codegen_fail/fail/union/trait_fail_infer_context.rs b/integration_tests/codegen_fail/fail/union/trait_fail_infer_context.rs new file mode 100644 index 000000000..7077fe8c0 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_fail_infer_context.rs @@ -0,0 +1,35 @@ +use juniper::{graphql_union, FromContext, GraphQLObject}; + +#[graphql_union] +trait Character { + fn a(&self, ctx: &SubContext) -> Option<&Human>; + fn b(&self, ctx: &CustomContext) -> Option<&Droid>; +} + +#[derive(GraphQLObject)] +#[graphql(context = CustomContext)] +pub struct Human { + id: String, + home_planet: String, +} + +#[derive(GraphQLObject)] +#[graphql(context = CustomContext)] +pub struct Droid { + id: String, + primary_function: String, +} + +pub struct CustomContext; +impl juniper::Context for CustomContext {} + +pub struct SubContext; +impl juniper::Context for SubContext {} + +impl FromContext for SubContext { + fn from(_: &CustomContext) -> &Self { + &Self + } +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/trait_fail_infer_context.stderr b/integration_tests/codegen_fail/fail/union/trait_fail_infer_context.stderr new file mode 100644 index 000000000..6ffa46c57 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_fail_infer_context.stderr @@ -0,0 +1,18 @@ +error[E0277]: the trait bound `CustomContext: juniper::executor::FromContext` is not satisfied + --> $DIR/trait_fail_infer_context.rs:3:1 + | +3 | #[graphql_union] + | ^^^^^^^^^^^^^^^^ the trait `juniper::executor::FromContext` is not implemented for `CustomContext` + | + = note: required by `juniper::executor::FromContext::from` + = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0308]: mismatched types + --> $DIR/trait_fail_infer_context.rs:3:1 + | +3 | #[graphql_union] + | ^^^^^^^^^^^^^^^^ expected struct `CustomContext`, found struct `SubContext` + | + = note: expected reference `&CustomContext` + found reference `&SubContext` + = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/integration_tests/codegen_fail/fail/union/trait_method_conflicts_with_external_resolver_fn.rs b/integration_tests/codegen_fail/fail/union/trait_method_conflicts_with_external_resolver_fn.rs new file mode 100644 index 000000000..aff4b1987 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_method_conflicts_with_external_resolver_fn.rs @@ -0,0 +1,13 @@ +use juniper::{graphql_union, GraphQLObject}; + +#[graphql_union(on Human = some_fn)] +trait Character { + fn a(&self) -> Option<&Human>; +} + +#[derive(GraphQLObject)] +pub struct Human { + id: String, +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/trait_method_conflicts_with_external_resolver_fn.stderr b/integration_tests/codegen_fail/fail/union/trait_method_conflicts_with_external_resolver_fn.stderr new file mode 100644 index 000000000..0034185a0 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_method_conflicts_with_external_resolver_fn.stderr @@ -0,0 +1,8 @@ +error: GraphQL union trait method `a` conflicts with the external resolver function `some_fn` declared on the trait to resolve the variant type `Human` + --> $DIR/trait_method_conflicts_with_external_resolver_fn.rs:5:5 + | +5 | fn a(&self) -> Option<&Human>; + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Unions + = note: use `#[graphql_union(ignore)]` attribute to ignore this trait method for union variants resolution diff --git a/integration_tests/codegen_fail/fail/union/trait_name_double_underscored.rs b/integration_tests/codegen_fail/fail/union/trait_name_double_underscored.rs new file mode 100644 index 000000000..86cb52258 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_name_double_underscored.rs @@ -0,0 +1,13 @@ +use juniper::{graphql_union, GraphQLObject}; + +#[graphql_union] +trait __Character { + fn a(&self) -> Option<&Human>; +} + +#[derive(GraphQLObject)] +pub struct Human { + id: String, +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/trait_name_double_underscored.stderr b/integration_tests/codegen_fail/fail/union/trait_name_double_underscored.stderr new file mode 100644 index 000000000..d010fee27 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_name_double_underscored.stderr @@ -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 GraphQL’s introspection system. + --> $DIR/trait_name_double_underscored.rs:4:7 + | +4 | trait __Character { + | ^^^^^^^^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Schema diff --git a/integration_tests/codegen_fail/fail/union/trait_no_fields.rs b/integration_tests/codegen_fail/fail/union/trait_no_fields.rs index 66d35f020..a2b14cd2a 100644 --- a/integration_tests/codegen_fail/fail/union/trait_no_fields.rs +++ b/integration_tests/codegen_fail/fail/union/trait_no_fields.rs @@ -1,6 +1,6 @@ -use juniper::GraphQLUnion; +use juniper::graphql_union; -#[derive(GraphQLUnion)] -enum Character {} +#[graphql_union] +trait Character {} fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/trait_no_fields.stderr b/integration_tests/codegen_fail/fail/union/trait_no_fields.stderr index 890cc0c67..8261623dc 100644 --- a/integration_tests/codegen_fail/fail/union/trait_no_fields.stderr +++ b/integration_tests/codegen_fail/fail/union/trait_no_fields.stderr @@ -1,7 +1,7 @@ error: GraphQL union expects at least one union variant --> $DIR/trait_no_fields.rs:4:1 | -4 | enum Character {} - | ^^^^^^^^^^^^^^^^^ +4 | trait Character {} + | ^^^^^^^^^^^^^^^^^^ | = note: https://spec.graphql.org/June2018/#sec-Unions diff --git a/integration_tests/codegen_fail/fail/union/trait_with_attr_on_method.rs b/integration_tests/codegen_fail/fail/union/trait_with_attr_on_method.rs new file mode 100644 index 000000000..730c6a6da --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_with_attr_on_method.rs @@ -0,0 +1,14 @@ +use juniper::{graphql_union, GraphQLObject}; + +#[graphql_union] +trait Character { + #[graphql_union(with = something)] + fn a(&self) -> Option<&Human>; +} + +#[derive(GraphQLObject)] +pub struct Human { + id: String, +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/trait_with_attr_on_method.stderr b/integration_tests/codegen_fail/fail/union/trait_with_attr_on_method.stderr new file mode 100644 index 000000000..aba0ab2ef --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_with_attr_on_method.stderr @@ -0,0 +1,8 @@ +error: GraphQL union cannot use #[graphql_union(with = ...)] attribute on a trait method + --> $DIR/trait_with_attr_on_method.rs:5:21 + | +5 | #[graphql_union(with = something)] + | ^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Unions + = note: instead use #[graphql_union(ignore)] on the method with #[graphql_union(on ... = ...)] on the trait itself diff --git a/integration_tests/codegen_fail/fail/union/trait_wrong_method_input_args.rs b/integration_tests/codegen_fail/fail/union/trait_wrong_method_input_args.rs new file mode 100644 index 000000000..da9ef3b65 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_wrong_method_input_args.rs @@ -0,0 +1,13 @@ +use juniper::{graphql_union, GraphQLObject}; + +#[graphql_union] +trait Character { + fn a(&self, ctx: &(), rand: u8) -> Option<&Human>; +} + +#[derive(GraphQLObject)] +pub struct Human { + id: String, +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/trait_wrong_method_input_args.stderr b/integration_tests/codegen_fail/fail/union/trait_wrong_method_input_args.stderr new file mode 100644 index 000000000..9b2fa2d85 --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_wrong_method_input_args.stderr @@ -0,0 +1,7 @@ +error: GraphQL union expects trait method to accept `&self` only and, optionally, `&Context` + --> $DIR/trait_wrong_method_input_args.rs:5:10 + | +5 | fn a(&self, ctx: &(), rand: u8) -> Option<&Human>; + | ^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Unions diff --git a/integration_tests/codegen_fail/fail/union/trait_wrong_method_return_type.rs b/integration_tests/codegen_fail/fail/union/trait_wrong_method_return_type.rs new file mode 100644 index 000000000..b63f4353c --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_wrong_method_return_type.rs @@ -0,0 +1,13 @@ +use juniper::{graphql_union, GraphQLObject}; + +#[graphql_union] +trait Character { + fn a(&self) -> &Human; +} + +#[derive(GraphQLObject)] +pub struct Human { + id: String, +} + +fn main() {} diff --git a/integration_tests/codegen_fail/fail/union/trait_wrong_method_return_type.stderr b/integration_tests/codegen_fail/fail/union/trait_wrong_method_return_type.stderr new file mode 100644 index 000000000..bbc40568a --- /dev/null +++ b/integration_tests/codegen_fail/fail/union/trait_wrong_method_return_type.stderr @@ -0,0 +1,7 @@ +error: GraphQL union expects trait method return type to be `Option<&VariantType>` only + --> $DIR/trait_wrong_method_return_type.rs:5:20 + | +5 | fn a(&self) -> &Human; + | ^^^^^^ + | + = note: https://spec.graphql.org/June2018/#sec-Unions diff --git a/juniper_codegen/src/derive_enum.rs b/juniper_codegen/src/derive_enum.rs index 3e3d0e5ed..b1a7e4cd1 100644 --- a/juniper_codegen/src/derive_enum.rs +++ b/juniper_codegen/src/derive_enum.rs @@ -58,7 +58,7 @@ pub fn impl_enum( let _type = match field.fields { Fields::Unit => syn::parse_str(&field_name.to_string()).unwrap(), _ => { - error.custom( + error.emit_custom( field.fields.span(), "all fields of the enum must be unnamed, e.g., None", ); diff --git a/juniper_codegen/src/graphql_union/attr.rs b/juniper_codegen/src/graphql_union/attr.rs index 51e2598e2..d8561f699 100644 --- a/juniper_codegen/src/graphql_union/attr.rs +++ b/juniper_codegen/src/graphql_union/attr.rs @@ -1,6 +1,6 @@ //! Code generation for `#[graphql_union]`/`#[graphql_union_internal]` macros. -use std::{collections::HashSet, mem, ops::Deref as _}; +use std::{mem, ops::Deref as _}; use proc_macro2::{Span, TokenStream}; use quote::{quote, ToTokens as _}; @@ -8,7 +8,7 @@ use syn::{ext::IdentExt as _, parse_quote, spanned::Spanned as _}; use crate::{ result::GraphQLScope, - util::{path_eq_single, span_container::SpanContainer, to_pascal_case, unparenthesize, Mode}, + util::{path_eq_single, span_container::SpanContainer, unparenthesize, Mode}, }; use super::{ @@ -16,8 +16,8 @@ use super::{ UnionVariantDefinition, UnionVariantMeta, }; -/// [`GraphQLScope`] of `#[graphql_union]`/`#[graphql_union_internal]` macros. -const SCOPE: GraphQLScope = GraphQLScope::UnionAttr; +/// [`GraphQLScope`] of errors for `#[graphql_union]`/`#[graphql_union_internal]` macros. +const ERR: GraphQLScope = GraphQLScope::UnionAttr; /// Returns the concrete name of the `proc_macro_attribute` for deriving `GraphQLUnion` /// implementation depending on the provided `mode`. @@ -71,9 +71,9 @@ pub fn expand(attr_args: TokenStream, body: TokenStream, mode: Mode) -> syn::Res .name .clone() .map(SpanContainer::into_inner) - .unwrap_or_else(|| to_pascal_case(&trait_ident.unraw().to_string())); + .unwrap_or_else(|| trait_ident.unraw().to_string()); if matches!(mode, Mode::Public) && name.starts_with("__") { - SCOPE.no_double_underscore( + ERR.no_double_underscore( meta.name .as_ref() .map(SpanContainer::span_ident) @@ -92,26 +92,16 @@ pub fn expand(attr_args: TokenStream, body: TokenStream, mode: Mode) -> syn::Res }) .collect(); - if meta.context.is_none() && !all_contexts_different(&variants) { - SCOPE.custom( - trait_span, - "cannot infer the appropriate context type, either specify the #[{}(context = MyType)] \ - attribute, so the trait methods can use any type which impls \ - `juniper::FromContext`, or use the same type for all context arguments in \ - trait methods signatures", - ); - } - proc_macro_error::abort_if_dirty(); - emerge_union_variants_from_meta(&mut variants, meta.custom_resolvers, mode); + emerge_union_variants_from_meta(&mut variants, meta.external_resolvers, mode); if variants.is_empty() { - SCOPE.custom(trait_span, "expects at least one union variant"); + ERR.emit_custom(trait_span, "expects at least one union variant"); } if !all_variants_different(&variants) { - SCOPE.custom( + ERR.emit_custom( trait_span, "must have a different type for each union variant", ); @@ -175,15 +165,20 @@ fn parse_variant_from_trait_method( .map_err(|e| proc_macro_error::emit_error!(e)) .ok()?; - if let Some(rslvr) = meta.custom_resolver { - SCOPE.custom( + if let Some(rslvr) = meta.external_resolver { + ERR.custom( rslvr.span_ident(), format!( - "cannot use #[{0}(with = ...)] attribute on a trait method, instead use \ - #[{0}(ignore)] on the method with #[{0}(on ... = ...)] on the trait itself", + "cannot use #[{}(with = ...)] attribute on a trait method", attr_path, ), ) + .note(format!( + "instead use #[{0}(ignore)] on the method with #[{0}(on ... = ...)] on the trait \ + itself", + attr_path, + )) + .emit() } if meta.ignore.is_some() { return None; @@ -194,7 +189,7 @@ fn parse_variant_from_trait_method( let ty = parse_trait_method_output_type(&method.sig) .map_err(|span| { - SCOPE.custom( + ERR.emit_custom( span, "expects trait method return type to be `Option<&VariantType>` only", ) @@ -202,14 +197,14 @@ fn parse_variant_from_trait_method( .ok()?; let method_context_ty = parse_trait_method_input_args(&method.sig) .map_err(|span| { - SCOPE.custom( + ERR.emit_custom( span, "expects trait method to accept `&self` only and, optionally, `&Context`", ) }) .ok()?; if let Some(is_async) = &method.sig.asyncness { - SCOPE.custom( + ERR.emit_custom( is_async.span(), "doesn't support async union variants resolvers yet", ); @@ -217,19 +212,24 @@ fn parse_variant_from_trait_method( } let resolver_code = { - if let Some(other) = trait_meta.custom_resolvers.get(&ty) { - SCOPE.custom( + if let Some(other) = trait_meta.external_resolvers.get(&ty) { + ERR.custom( method_span, format!( - "trait method `{}` conflicts with the custom resolver `{}` declared on the \ - trait to resolve the variant type `{}`, use `#[{}(ignore)]` attribute to \ - ignore this trait method for union variants resolution", + "trait method `{}` conflicts with the external resolver function `{}` declared \ + on the trait to resolve the variant type `{}`", method_ident, other.to_token_stream(), ty.to_token_stream(), - attr_path, + ), - ); + ) + .note(format!( + "use `#[{}(ignore)]` attribute to ignore this trait method for union variants \ + resolution", + attr_path, + )) + .emit(); } if method_context_ty.is_some() { @@ -343,19 +343,3 @@ fn parse_trait_method_input_args(sig: &syn::Signature) -> Result Err(ty.span()), } } - -/// Checks whether all [GraphQL union][1] `variants` contains a different type of -/// `juniper::Context`. -/// -/// [1]: https://spec.graphql.org/June2018/#sec-Unions -fn all_contexts_different(variants: &Vec) -> bool { - let all: Vec<_> = variants - .iter() - .filter_map(|v| v.context_ty.as_ref()) - .collect(); - let deduped: HashSet<_> = variants - .iter() - .filter_map(|v| v.context_ty.as_ref()) - .collect(); - deduped.len() == all.len() -} diff --git a/juniper_codegen/src/graphql_union/derive.rs b/juniper_codegen/src/graphql_union/derive.rs index fd92d2613..8f0f7ed56 100644 --- a/juniper_codegen/src/graphql_union/derive.rs +++ b/juniper_codegen/src/graphql_union/derive.rs @@ -7,7 +7,7 @@ use syn::{ext::IdentExt as _, parse_quote, spanned::Spanned as _, Data, Fields}; use crate::{ result::GraphQLScope, - util::{span_container::SpanContainer, to_pascal_case, Mode}, + util::{span_container::SpanContainer, unparenthesize, Mode}, }; use super::{ @@ -15,8 +15,9 @@ use super::{ UnionVariantDefinition, UnionVariantMeta, }; -/// [`GraphQLScope`] of `#[derive(GraphQLUnion)]`/`#[derive(GraphQLUnionInternal)]` macros. -const SCOPE: GraphQLScope = GraphQLScope::UnionDerive; +/// [`GraphQLScope`] of errors for `#[derive(GraphQLUnion)]`/`#[derive(GraphQLUnionInternal)]` +/// macros. +const ERR: GraphQLScope = GraphQLScope::UnionDerive; /// Expands `#[derive(GraphQLUnion)]`/`#[derive(GraphQLUnionInternal)]` macro into generated code. pub fn expand(input: TokenStream, mode: Mode) -> syn::Result { @@ -25,7 +26,7 @@ pub fn expand(input: TokenStream, mode: Mode) -> syn::Result { match &ast.data { Data::Enum(_) => expand_enum(ast, mode), Data::Struct(_) => expand_struct(ast, mode), - _ => Err(SCOPE.custom_error(ast.span(), "can only be applied to enums and structs")), + _ => Err(ERR.custom_error(ast.span(), "can only be derived for enums and structs")), } .map(ToTokens::into_token_stream) } @@ -42,9 +43,9 @@ fn expand_enum(ast: syn::DeriveInput, mode: Mode) -> syn::Result syn::Result Err(var_ident.span()), } .map_err(|span| { - SCOPE.custom( + ERR.emit_custom( span, "enum allows only unnamed variants with a single field, e.g. `Some(T)`", ) @@ -135,12 +136,12 @@ fn parse_variant_from_enum_variant( let enum_path = quote! { #enum_ident::#var_ident }; - let resolver_code = if let Some(rslvr) = meta.custom_resolver { - if let Some(other) = enum_meta.custom_resolvers.get(&ty) { - SCOPE.custom( + let resolver_code = if let Some(rslvr) = meta.external_resolver { + if let Some(other) = enum_meta.external_resolvers.get(&ty) { + ERR.emit_custom( rslvr.span_ident(), format!( - "variant `{}` already has custom resolver `{}` declared on the enum", + "variant `{}` already has external resolver function `{}` declared on the enum", ty.to_token_stream(), other.to_token_stream(), ), @@ -185,9 +186,9 @@ fn expand_struct(ast: syn::DeriveInput, mode: Mode) -> syn::Result syn::Result syn::Error { syn::Error::new(span, "duplicated attribute") } -/// Helper alias for the type of [`UnionMeta::custom_resolvers`] field. +/// Helper alias for the type of [`UnionMeta::external_resolvers`] field. type UnionMetaResolvers = HashMap>; /// Available metadata (arguments) behind `#[graphql]` (or `#[graphql_union]`) attribute when @@ -81,7 +81,7 @@ type UnionMetaResolvers = HashMap>; struct UnionMeta { /// Explicitly specified name of [GraphQL union][1] type. /// - /// If absent, then `PascalCase`d Rust type name is used by default. + /// If absent, then Rust type name is used by default. /// /// [1]: https://spec.graphql.org/June2018/#sec-Unions pub name: Option>, @@ -113,14 +113,15 @@ struct UnionMeta { /// [1]: https://spec.graphql.org/June2018/#sec-Unions pub scalar: Option>, - /// Explicitly specified custom resolver functions for [GraphQL union][1] variants. + /// Explicitly specified external resolver functions for [GraphQL union][1] variants. /// /// If absent, then macro will try to auto-infer all the possible variants from the type - /// declaration, if possible. That's why specifying a custom resolver function has sense, when - /// some custom [union][1] variant resolving logic is involved, or variants cannot be inferred. + /// declaration, if possible. That's why specifying an external resolver function has sense, + /// when some custom [union][1] variant resolving logic is involved, or variants cannot be + /// inferred. /// /// [1]: https://spec.graphql.org/June2018/#sec-Unions - pub custom_resolvers: UnionMetaResolvers, + pub external_resolvers: UnionMetaResolvers, } impl Parse for UnionMeta { @@ -177,7 +178,7 @@ impl Parse for UnionMeta { let rslvr_spanned = SpanContainer::new(ident.span(), Some(ty.span()), rslvr); let rslvr_span = rslvr_spanned.span_joined(); output - .custom_resolvers + .external_resolvers .insert(ty, rslvr_spanned) .none_or_else(|_| dup_attr_err(rslvr_span))? } @@ -202,7 +203,7 @@ impl UnionMeta { description: try_merge_opt!(description: self, another), context: try_merge_opt!(context: self, another), scalar: try_merge_opt!(scalar: self, another), - custom_resolvers: try_merge_hashmap!(custom_resolvers: self, another => span_joined), + external_resolvers: try_merge_hashmap!(external_resolvers: self, another => span_joined), }) } @@ -232,14 +233,14 @@ struct UnionVariantMeta { /// [1]: https://spec.graphql.org/June2018/#sec-Unions pub ignore: Option>, - /// Explicitly specified custom resolver function for this [GraphQL union][1] variant. + /// Explicitly specified external resolver function for this [GraphQL union][1] variant. /// /// If absent, then macro will generate the code which just returns the variant inner value. - /// Usually, specifying a custom resolver function has sense, when some custom resolving logic - /// is involved. + /// Usually, specifying an external resolver function has sense, when some custom resolving + /// logic is involved. /// /// [1]: https://spec.graphql.org/June2018/#sec-Unions - pub custom_resolver: Option>, + pub external_resolver: Option>, } impl Parse for UnionVariantMeta { @@ -257,7 +258,7 @@ impl Parse for UnionVariantMeta { input.parse::()?; let rslvr = input.parse::()?; output - .custom_resolver + .external_resolver .replace(SpanContainer::new(ident.span(), Some(rslvr.span()), rslvr)) .none_or_else(|_| dup_attr_err(ident.span()))? } @@ -280,7 +281,7 @@ impl UnionVariantMeta { fn try_merge(self, mut another: Self) -> syn::Result { Ok(Self { ignore: try_merge_opt!(ignore: self, another), - custom_resolver: try_merge_opt!(custom_resolver: self, another), + external_resolver: try_merge_opt!(external_resolver: self, another), }) } @@ -632,23 +633,24 @@ impl ToTokens for UnionDefinition { } } -/// Emerges [`UnionMeta::custom_resolvers`] into the given [GraphQL union][1] `variants`. +/// Emerges [`UnionMeta::external_resolvers`] into the given [GraphQL union][1] `variants`. /// -/// If duplication happens, then resolving code is overwritten with the one from `custom_resolvers`. +/// If duplication happens, then resolving code is overwritten with the one from +/// `external_resolvers`. /// /// [1]: https://spec.graphql.org/June2018/#sec-Unions fn emerge_union_variants_from_meta( variants: &mut Vec, - custom_resolvers: UnionMetaResolvers, + external_resolvers: UnionMetaResolvers, mode: Mode, ) { - if custom_resolvers.is_empty() { + if external_resolvers.is_empty() { return; } let crate_path = mode.crate_path(); - for (ty, rslvr) in custom_resolvers { + for (ty, rslvr) in external_resolvers { let span = rslvr.span_joined(); let resolver_fn = rslvr.into_inner(); diff --git a/juniper_codegen/src/impl_object.rs b/juniper_codegen/src/impl_object.rs index 6624e9a73..367792671 100644 --- a/juniper_codegen/src/impl_object.rs +++ b/juniper_codegen/src/impl_object.rs @@ -65,7 +65,7 @@ fn create( let _type = match method.sig.output { syn::ReturnType::Type(_, ref t) => *t.clone(), syn::ReturnType::Default => { - error.custom(method.sig.span(), "return value required"); + error.emit_custom(method.sig.span(), "return value required"); return None; } }; diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs index b3e8feb66..dfae2c8b5 100644 --- a/juniper_codegen/src/lib.rs +++ b/juniper_codegen/src/lib.rs @@ -579,7 +579,7 @@ pub fn graphql_subscription_internal(args: TokenStream, input: TokenStream) -> T /// # Custom name and description /// /// The name of [GraphQL union][1] may be overriden with a `name` attribute's argument. By default, -/// a type name in `PascalCase` is used. +/// a type name is used. /// /// The description of [GraphQL union][1] may be specified either with a `description`/`desc` /// attribute's argument, or with a regular Rust doc comment. @@ -904,7 +904,7 @@ pub fn derive_union_internal(input: TokenStream) -> TokenStream { /// # Custom name and description /// /// The name of [GraphQL union][1] may be overriden with a `name` attribute's argument. By default, -/// a type name in `PascalCase` is used. +/// a type name is used. /// /// The description of [GraphQL union][1] may be specified either with a `description`/`desc` /// attribute's argument, or with a regular Rust doc comment. diff --git a/juniper_codegen/src/result.rs b/juniper_codegen/src/result.rs index a21bc801e..73a495961 100644 --- a/juniper_codegen/src/result.rs +++ b/juniper_codegen/src/result.rs @@ -61,10 +61,13 @@ impl GraphQLScope { format!("{}{}", SPEC_URL, self.spec_section()) } - pub fn custom>(&self, span: Span, msg: S) { + pub fn custom>(&self, span: Span, msg: S) -> Diagnostic { Diagnostic::spanned(span, Level::Error, format!("{} {}", self, msg.as_ref())) .note(self.spec_link()) - .emit(); + } + + pub fn emit_custom>(&self, span: Span, msg: S) { + self.custom(span, msg).emit() } pub fn custom_error>(&self, span: Span, msg: S) -> syn::Error { diff --git a/juniper_codegen/src/util/mod.rs b/juniper_codegen/src/util/mod.rs index 0e543ce6a..82351e616 100644 --- a/juniper_codegen/src/util/mod.rs +++ b/juniper_codegen/src/util/mod.rs @@ -255,30 +255,6 @@ pub fn to_camel_case(s: &str) -> String { dest } -/// Returns a copy of the given string, transformed into `PascalCase`. -pub fn to_pascal_case(s: &str) -> String { - let mut dest = String::new(); - - for part in s.split('_') { - if part.len() == 1 { - dest.push_str(&part.to_uppercase()); - } else if part.len() > 1 { - let first = part - .chars() - .next() - .unwrap() - .to_uppercase() - .collect::(); - let second = &part[1..]; - - dest.push_str(&first); - dest.push_str(second); - } - } - - dest -} - pub(crate) fn to_upper_snake_case(s: &str) -> String { let mut last_lower = false; let mut upper = String::new(); @@ -1908,23 +1884,6 @@ mod test { assert_eq!(&to_camel_case("")[..], ""); } - #[test] - fn test_to_pascal_case() { - for (input, expected) in &[ - ("test", "Test"), - ("_test", "Test"), - ("first_second", "FirstSecond"), - ("first_", "First"), - ("a_b_c", "ABC"), - ("a_bc", "ABc"), - ("a_b", "AB"), - ("a", "A"), - ("", ""), - ] { - assert_eq!(&to_pascal_case(input), expected); - } - } - #[test] fn test_to_upper_snake_case() { assert_eq!(to_upper_snake_case("abc"), "ABC"); From ecfe7e18e7e1b9ca3aa9c978f2fbe2299fe91416 Mon Sep 17 00:00:00 2001 From: tyranron Date: Wed, 3 Jun 2020 17:30:40 +0300 Subject: [PATCH 27/29] Cover up positive cases of #[graphql_union] macro Additionally: - use unit type () as default for EmptyMutation and EmptySubscriptions --- .../juniper_tests/src/codegen/union_attr.rs | 1099 ++++++++++++++--- juniper/src/types/scalars.rs | 4 +- 2 files changed, 933 insertions(+), 170 deletions(-) diff --git a/integration_tests/juniper_tests/src/codegen/union_attr.rs b/integration_tests/juniper_tests/src/codegen/union_attr.rs index 310901e01..940aa2e14 100644 --- a/integration_tests/juniper_tests/src/codegen/union_attr.rs +++ b/integration_tests/juniper_tests/src/codegen/union_attr.rs @@ -1,11 +1,6 @@ -use std::any::Any; - -use juniper::{graphql_object, graphql_union, GraphQLObject}; - -#[cfg(test)] use juniper::{ - self, execute, DefaultScalarValue, EmptyMutation, EmptySubscription, GraphQLType, RootNode, - Value, Variables, + execute, graphql_object, graphql_union, graphql_value, DefaultScalarValue, EmptyMutation, + EmptySubscription, GraphQLObject, GraphQLType, RootNode, ScalarValue, Variables, }; #[derive(GraphQLObject)] @@ -21,191 +16,959 @@ struct Droid { } #[derive(GraphQLObject)] -struct Jedi { +struct Ewok { id: String, - rank: String, + funny: bool, } -#[graphql_union(name = "Character")] -#[graphql_union(description = "A Collection of things")] -#[graphql_union(on Jedi = resolve_character_jedi)] -trait Character { - fn as_human(&self, _: &()) -> Option<&Human> { - None +pub enum CustomContext { + Human, + Droid, + Ewok, +} +impl juniper::Context for CustomContext {} + +#[derive(GraphQLObject)] +#[graphql(context = CustomContext)] +pub struct HumanCustomContext { + id: String, + home_planet: String, +} + +#[derive(GraphQLObject)] +#[graphql(context = CustomContext)] +pub struct DroidCustomContext { + id: String, + primary_function: String, +} + +#[derive(GraphQLObject)] +#[graphql(context = CustomContext)] +struct EwokCustomContext { + id: String, + funny: bool, +} + +fn schema<'q, C, S, Q>(query_root: Q) -> RootNode<'q, Q, EmptyMutation, EmptySubscription, S> +where + Q: GraphQLType + 'q, + S: ScalarValue + 'q, +{ + RootNode::new( + query_root, + EmptyMutation::::new(), + EmptySubscription::::new(), + ) +} + +mod trivial { + use super::*; + + #[graphql_union] + trait Character { + fn as_human(&self) -> Option<&Human> { + None + } + fn as_droid(&self) -> Option<&Droid> { + None + } + } + + impl Character for Human { + fn as_human(&self) -> Option<&Human> { + Some(&self) + } + } + + impl Character for Droid { + fn as_droid(&self) -> Option<&Droid> { + Some(&self) + } + } + + type DynCharacter<'a> = dyn Character + Send + Sync + 'a; + + enum QueryRoot { + Human, + Droid, } - fn as_droid(&self) -> Option<&Droid> { - None + + #[graphql_object] + impl QueryRoot { + fn character(&self) -> Box> { + let ch: Box> = match self { + Self::Human => Box::new(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + Self::Droid => Box::new(Droid { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + }; + ch + } } - #[graphql_union(ignore)] - fn as_jedi(&self) -> Option<&Jedi> { - None + + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + ... on Droid { + droidId: id + primaryFunction + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema(QueryRoot::Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_droid() { + let schema = schema(QueryRoot::Droid); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn is_graphql_union() { + const DOC: &str = r#"{ + __type(name: "Character") { + kind + } + }"#; + + let schema = schema(QueryRoot::Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"kind": "UNION"}}), vec![])), + ); + } + + #[tokio::test] + async fn uses_type_name() { + const DOC: &str = r#"{ + __type(name: "Character") { + name + } + }"#; + + let schema = schema(QueryRoot::Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"name": "Character"}}), vec![])), + ); + } + + #[tokio::test] + async fn has_no_description() { + const DOC: &str = r#"{ + __type(name: "Character") { + description + } + }"#; + + let schema = schema(QueryRoot::Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"description": None}}), vec![])), + ); } - #[graphql_union(ignore)] - fn some(&self) {} } -impl Character for Human { - fn as_human(&self, _: &()) -> Option<&Human> { - Some(&self) +mod generic { + use super::*; + + #[graphql_union] + trait Character { + fn as_human(&self) -> Option<&Human> { + None + } + fn as_droid(&self) -> Option<&Droid> { + None + } + } + + impl Character for Human { + fn as_human(&self) -> Option<&Human> { + Some(&self) + } + } + + impl Character for Droid { + fn as_droid(&self) -> Option<&Droid> { + Some(&self) + } + } + + type DynCharacter<'a, A, B> = dyn Character + Send + Sync + 'a; + + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object] + impl QueryRoot { + fn character(&self) -> Box> { + let ch: Box> = match self { + Self::Human => Box::new(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + Self::Droid => Box::new(Droid { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + }; + ch + } + } + + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + ... on Droid { + droidId: id + primaryFunction + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema(QueryRoot::Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_droid() { + let schema = schema(QueryRoot::Droid); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_type_name_without_type_params() { + const DOC: &str = r#"{ + __type(name: "Character") { + name + } + }"#; + + let schema = schema(QueryRoot::Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"name": "Character"}}), vec![])), + ); } } -impl Character for Droid { - fn as_droid(&self) -> Option<&Droid> { - Some(&self) +mod description_from_doc_comments { + use super::*; + + /// Rust docs. + #[graphql_union] + trait Character { + fn as_human(&self) -> Option<&Human> { + None + } + } + + impl Character for Human { + fn as_human(&self) -> Option<&Human> { + Some(&self) + } + } + + type DynCharacter<'a> = dyn Character + Send + Sync + 'a; + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn character(&self) -> Box> { + Box::new(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }) + } + } + + #[tokio::test] + async fn resolves_human() { + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_doc_comment_as_description() { + const DOC: &str = r#"{ + __type(name: "Character") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"description": "Rust docs."}}), + vec![], + )), + ); } } -impl Character for Jedi { - fn as_jedi(&self) -> Option<&Jedi> { - Some(&self) +mod explicit_name_and_description { + use super::*; + + /// Rust docs. + #[graphql_union(name = "MyChar", desc = "My character.")] + trait Character { + fn as_human(&self) -> Option<&Human> { + None + } + } + + impl Character for Human { + fn as_human(&self) -> Option<&Human> { + Some(&self) + } + } + + type DynCharacter<'a> = dyn Character + Send + Sync + 'a; + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn character(&self) -> Box> { + Box::new(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }) + } + } + + #[tokio::test] + async fn resolves_human() { + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_custom_name() { + const DOC: &str = r#"{ + __type(name: "MyChar") { + name + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"name": "MyChar"}}), vec![])), + ); + } + + #[tokio::test] + async fn uses_custom_description() { + const DOC: &str = r#"{ + __type(name: "MyChar") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"description": "My character."}}), + vec![], + )), + ); } } -fn resolve_character_jedi<'a, T>( - jedi: &'a (dyn Character + Send + Sync), - _: &(), -) -> Option<&'a Jedi> { - jedi.as_jedi() +mod explicit_scalar { + use super::*; + + #[graphql_union(scalar = DefaultScalarValue)] + trait Character { + fn as_human(&self) -> Option<&Human> { + None + } + fn as_droid(&self) -> Option<&Droid> { + None + } + } + + impl Character for Human { + fn as_human(&self) -> Option<&Human> { + Some(&self) + } + } + + impl Character for Droid { + fn as_droid(&self) -> Option<&Droid> { + Some(&self) + } + } + + type DynCharacter<'a> = dyn Character + Send + Sync + 'a; + + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object(scalar = DefaultScalarValue)] + impl QueryRoot { + fn character(&self) -> Box> { + let ch: Box> = match self { + Self::Human => Box::new(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + Self::Droid => Box::new(Droid { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + }; + ch + } + } + + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + ... on Droid { + droidId: id + primaryFunction + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema::<_, DefaultScalarValue, _>(QueryRoot::Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_droid() { + let schema = schema::<_, DefaultScalarValue, _>(QueryRoot::Droid); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } } -enum Query { - Human, - Droid, - Jedi, +mod inferred_custom_context { + use super::*; + + #[graphql_union] + trait Character { + fn as_human(&self, _: &CustomContext) -> Option<&HumanCustomContext> { + None + } + fn as_droid(&self, _: &()) -> Option<&DroidCustomContext> { + None + } + } + + impl Character for HumanCustomContext { + fn as_human(&self, _: &CustomContext) -> Option<&HumanCustomContext> { + Some(&self) + } + } + + impl Character for DroidCustomContext { + fn as_droid(&self, _: &()) -> Option<&DroidCustomContext> { + Some(&self) + } + } + + type DynCharacter<'a> = dyn Character + Send + Sync + 'a; + + struct QueryRoot; + + #[graphql_object(context = CustomContext)] + impl QueryRoot { + fn character(&self, ctx: &CustomContext) -> Box> { + let ch: Box> = match ctx { + CustomContext::Human => Box::new(HumanCustomContext { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + CustomContext::Droid => Box::new(DroidCustomContext { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + _ => unimplemented!(), + }; + ch + } + } + + const DOC: &str = r#"{ + character { + ... on HumanCustomContext { + humanId: id + homePlanet + } + ... on DroidCustomContext { + droidId: id + primaryFunction + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &CustomContext::Human).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_droid() { + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &CustomContext::Droid).await, + Ok(( + graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } +} + +mod explicit_custom_context { + use super::*; + + #[graphql_union(context = CustomContext)] + trait Character { + fn as_human(&self) -> Option<&HumanCustomContext> { + None + } + fn as_droid(&self) -> Option<&DroidCustomContext> { + None + } + } + + impl Character for HumanCustomContext { + fn as_human(&self) -> Option<&HumanCustomContext> { + Some(&self) + } + } + + impl Character for DroidCustomContext { + fn as_droid(&self) -> Option<&DroidCustomContext> { + Some(&self) + } + } + + type DynCharacter<'a> = dyn Character + Send + Sync + 'a; + + struct QueryRoot; + + #[graphql_object(context = CustomContext)] + impl QueryRoot { + fn character(&self, ctx: &CustomContext) -> Box> { + let ch: Box> = match ctx { + CustomContext::Human => Box::new(HumanCustomContext { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + CustomContext::Droid => Box::new(DroidCustomContext { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + _ => unimplemented!(), + }; + ch + } + } + + const DOC: &str = r#"{ + character { + ... on HumanCustomContext { + humanId: id + homePlanet + } + ... on DroidCustomContext { + droidId: id + primaryFunction + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &CustomContext::Human).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_droid() { + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &CustomContext::Droid).await, + Ok(( + graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } } -#[graphql_object] -impl Query { - fn context(&self) -> Box + Send + Sync> { - let ch: Box + Send + Sync> = match self { - Self::Human => Box::new(Human { +mod ignored_methods { + use super::*; + + #[graphql_union] + trait Character { + fn as_human(&self) -> Option<&Human> { + None + } + #[graphql_union(ignore)] + fn ignored(&self) -> Option<&Ewok> { + None + } + #[graphql_union(skip)] + fn skipped(&self) {} + } + + impl Character for Human { + fn as_human(&self) -> Option<&Human> { + Some(&self) + } + } + + type DynCharacter<'a> = dyn Character + Send + Sync + 'a; + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn character(&self) -> Box> { + Box::new(Human { id: "human-32".to_string(), home_planet: "earth".to_string(), - }), - Self::Droid => Box::new(Droid { - id: "droid-99".to_string(), - primary_function: "run".to_string(), - }), - Self::Jedi => Box::new(Jedi { - id: "Obi Wan Kenobi".to_string(), - rank: "Master".to_string(), - }), - }; - ch + }) + } + } + + #[tokio::test] + async fn resolves_human() { + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn ignores_ewok() { + const DOC: &str = r#"{ + __type(name: "Character") { + possibleTypes { + name + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"possibleTypes": [{"name": "Human"}]}}), + vec![], + )), + ); } } -const DOC: &str = r#" -{ - context { - ... on Human { - humanId: id - homePlanet - } - ... on Droid { - droidId: id - primaryFunction - } - ... on Jedi { - jediId: id - rank - } - } -}"#; - -#[tokio::test] -async fn resolves_human() { - let schema = RootNode::new( - Query::Human, - EmptyMutation::<()>::new(), - EmptySubscription::<()>::new(), - ); - - let actual = execute(DOC, None, &schema, &Variables::new(), &()).await; - - let expected = Ok(( - Value::object( - vec![( - "context", - Value::object( - vec![ - ("humanId", Value::scalar("human-32".to_string())), - ("homePlanet", Value::scalar("earth".to_string())), - ] - .into_iter() - .collect(), - ), - )] - .into_iter() - .collect(), - ), - vec![], - )); - - assert_eq!(actual, expected); -} - -#[tokio::test] -async fn resolves_droid() { - let schema = RootNode::new( - Query::Droid, - EmptyMutation::<()>::new(), - EmptySubscription::<()>::new(), - ); - - let actual = execute(DOC, None, &schema, &Variables::new(), &()).await; - - let expected = Ok(( - Value::object( - vec![( - "context", - Value::object( - vec![ - ("droidId", Value::scalar("droid-99".to_string())), - ("primaryFunction", Value::scalar("run".to_string())), - ] - .into_iter() - .collect(), - ), - )] - .into_iter() - .collect(), - ), - vec![], - )); - - assert_eq!(actual, expected); -} - -#[tokio::test] -async fn resolves_jedi() { - let schema = RootNode::new( - Query::Jedi, - EmptyMutation::<()>::new(), - EmptySubscription::<()>::new(), - ); - - let actual = execute(DOC, None, &schema, &Variables::new(), &()).await; - - let expected = Ok(( - Value::object( - vec![( - "context", - Value::object( - vec![ - ("jediId", Value::scalar("Obi Wan Kenobi".to_string())), - ("rank", Value::scalar("Master".to_string())), - ] - .into_iter() - .collect(), - ), - )] - .into_iter() - .collect(), - ), - vec![], - )); - - assert_eq!(actual, expected); +mod full_featured { + use super::*; + + /// Rust doc. + #[graphql_union(name = "MyChar")] + #[graphql_union(description = "My character.")] + #[graphql_union(context = CustomContext, scalar = DefaultScalarValue)] + #[graphql_union(on EwokCustomContext = resolve_ewok)] + trait Character { + fn as_human(&self, _: &()) -> Option<&HumanCustomContext> { + None + } + fn as_droid(&self) -> Option<&DroidCustomContext> { + None + } + #[graphql_union(ignore)] + fn as_ewok(&self) -> Option<&EwokCustomContext> { + None + } + #[graphql_union(ignore)] + fn ignored(&self) {} + } + + impl Character for HumanCustomContext { + fn as_human(&self, _: &()) -> Option<&HumanCustomContext> { + Some(&self) + } + } + + impl Character for DroidCustomContext { + fn as_droid(&self) -> Option<&DroidCustomContext> { + Some(&self) + } + } + + impl Character for EwokCustomContext { + fn as_ewok(&self) -> Option<&EwokCustomContext> { + Some(&self) + } + } + + type DynCharacter<'a, T> = dyn Character + Send + Sync + 'a; + + fn resolve_ewok<'a, T>( + ewok: &'a DynCharacter<'a, T>, + _: &CustomContext, + ) -> Option<&'a EwokCustomContext> { + ewok.as_ewok() + } + + struct QueryRoot; + + #[graphql_object(context = CustomContext)] + impl QueryRoot { + fn character(&self, ctx: &CustomContext) -> Box> { + let ch: Box> = match ctx { + CustomContext::Human => Box::new(HumanCustomContext { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + CustomContext::Droid => Box::new(DroidCustomContext { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + CustomContext::Ewok => Box::new(EwokCustomContext { + id: "ewok-1".to_string(), + funny: true, + }), + }; + ch + } + } + + const DOC: &str = r#"{ + character { + ... on HumanCustomContext { + humanId: id + homePlanet + } + ... on DroidCustomContext { + droidId: id + primaryFunction + } + ... on EwokCustomContext { + ewokId: id + funny + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &CustomContext::Human).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_droid() { + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &CustomContext::Droid).await, + Ok(( + graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_ewok() { + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &CustomContext::Ewok).await, + Ok(( + graphql_value!({"character": {"ewokId": "ewok-1", "funny": true}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_custom_name() { + const DOC: &str = r#"{ + __type(name: "MyChar") { + name + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &CustomContext::Ewok).await, + Ok((graphql_value!({"__type": {"name": "MyChar"}}), vec![])), + ); + } + + #[tokio::test] + async fn uses_custom_description() { + const DOC: &str = r#"{ + __type(name: "MyChar") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &CustomContext::Ewok).await, + Ok(( + graphql_value!({"__type": {"description": "My character."}}), + vec![], + )), + ); + } } diff --git a/juniper/src/types/scalars.rs b/juniper/src/types/scalars.rs index 78471d266..a2963c935 100644 --- a/juniper/src/types/scalars.rs +++ b/juniper/src/types/scalars.rs @@ -325,7 +325,7 @@ where /// If you instantiate `RootNode` with this as the mutation, no mutation will be /// generated for the schema. #[derive(Debug)] -pub struct EmptyMutation { +pub struct EmptyMutation { phantom: PhantomData, } @@ -382,7 +382,7 @@ impl Default for EmptyMutation { /// /// If you instantiate `RootNode` with this as the subscription, /// no subscriptions will be generated for the schema. -pub struct EmptySubscription { +pub struct EmptySubscription { phantom: PhantomData, } From 13d96aebc166eda76955a32d90c5c8acd1e04e36 Mon Sep 17 00:00:00 2001 From: tyranron Date: Wed, 3 Jun 2020 19:17:01 +0300 Subject: [PATCH 28/29] Cover up positive cases of #[derive(GraphQLUnion)] macro --- .../juniper_tests/src/codegen/union_attr.rs | 103 ++ .../juniper_tests/src/codegen/union_derive.rs | 1558 ++++++++++++++--- 2 files changed, 1402 insertions(+), 259 deletions(-) diff --git a/integration_tests/juniper_tests/src/codegen/union_attr.rs b/integration_tests/juniper_tests/src/codegen/union_attr.rs index 940aa2e14..7377dbea3 100644 --- a/integration_tests/juniper_tests/src/codegen/union_attr.rs +++ b/integration_tests/juniper_tests/src/codegen/union_attr.rs @@ -1,3 +1,5 @@ +//! Tests for `#[graphql_union]` macro. + use juniper::{ execute, graphql_object, graphql_union, graphql_value, DefaultScalarValue, EmptyMutation, EmptySubscription, GraphQLObject, GraphQLType, RootNode, ScalarValue, Variables, @@ -808,6 +810,107 @@ mod ignored_methods { } } +mod external_resolver { + use super::*; + + #[graphql_union(context = Database)] + #[graphql_union(on Droid = DynCharacter::as_droid)] + trait Character { + fn as_human(&self) -> Option<&Human> { + None + } + } + + impl Character for Human { + fn as_human(&self) -> Option<&Human> { + Some(&self) + } + } + + impl Character for Droid {} + + type DynCharacter<'a> = dyn Character + Send + Sync + 'a; + + impl<'a> DynCharacter<'a> { + fn as_droid<'db>(&self, db: &'db Database) -> Option<&'db Droid> { + db.droid.as_ref() + } + } + + struct Database { + droid: Option, + } + impl juniper::Context for Database {} + + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object(context = Database)] + impl QueryRoot { + fn character(&self) -> Box> { + let ch: Box> = match self { + Self::Human => Box::new(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + Self::Droid => Box::new(Droid { + id: "?????".to_string(), + primary_function: "???".to_string(), + }), + }; + ch + } + } + + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + ... on Droid { + droidId: id + primaryFunction + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema(QueryRoot::Human); + let db = Database { droid: None }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_droid() { + let schema = schema(QueryRoot::Droid); + let db = Database { + droid: Some(Droid { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).await, + Ok(( + graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } +} + mod full_featured { use super::*; diff --git a/integration_tests/juniper_tests/src/codegen/union_derive.rs b/integration_tests/juniper_tests/src/codegen/union_derive.rs index 7838140e0..55d4c047a 100644 --- a/integration_tests/juniper_tests/src/codegen/union_derive.rs +++ b/integration_tests/juniper_tests/src/codegen/union_derive.rs @@ -1,348 +1,1388 @@ -// Test for union's derive macro +//! Tests for `#[derive(GraphQLUnion)]` macro. -use derive_more::From; -#[cfg(test)] -use fnv::FnvHashMap; -use juniper::{graphql_object, GraphQLObject, GraphQLUnion}; +use std::marker::PhantomData; -#[cfg(test)] use juniper::{ - self, execute, DefaultScalarValue, EmptyMutation, EmptySubscription, GraphQLType, RootNode, - Value, Variables, + execute, graphql_object, graphql_value, DefaultScalarValue, EmptyMutation, EmptySubscription, + GraphQLObject, GraphQLType, GraphQLUnion, RootNode, ScalarValue, Variables, }; #[derive(GraphQLObject)] -pub struct Human { +struct Human { id: String, home_planet: String, } #[derive(GraphQLObject)] -pub struct Droid { +struct Droid { id: String, primary_function: String, } -#[derive(GraphQLUnion)] -#[graphql(description = "A Collection of things")] -pub enum Character { - One(Human), - Two(Droid), +#[derive(GraphQLObject)] +struct Ewok { + id: String, + funny: bool, } -#[derive(GraphQLUnion)] -#[graphql(Scalar = juniper::DefaultScalarValue)] -pub enum CharacterWithGeneric { - One(Human), - Two(Droid), - #[allow(dead_code)] - #[graphql(ignore)] - Hidden(T), +pub enum CustomContext { + Human, + Droid, + Ewok, } +impl juniper::Context for CustomContext {} -#[derive(GraphQLUnion)] -#[graphql(on Droid = CharacterCustomFn::as_droid)] -pub enum CharacterCustomFn { - One(Human), - #[graphql(ignore)] - Two(Droid, usize, u8), +#[derive(GraphQLObject)] +#[graphql(context = CustomContext)] +pub struct HumanCustomContext { + id: String, + home_planet: String, } -impl CharacterCustomFn { - fn as_droid(&self, _: &()) -> Option<&Droid> { - match self { - Self::Two(droid, _, _) => Some(droid), - _ => None, - } - } +#[derive(GraphQLObject)] +#[graphql(context = CustomContext)] +pub struct DroidCustomContext { + id: String, + primary_function: String, +} + +#[derive(GraphQLObject)] +#[graphql(context = CustomContext)] +struct EwokCustomContext { + id: String, + funny: bool, } -#[derive(GraphQLUnion)] -pub enum CharacterCustomVariantFn { - One(Human), - #[graphql(with = CharacterCustomVariantFn::as_droid)] - Two(Droid), +fn schema<'q, C, S, Q>(query_root: Q) -> RootNode<'q, Q, EmptyMutation, EmptySubscription, S> +where + Q: GraphQLType + 'q, + S: ScalarValue + 'q, +{ + RootNode::new( + query_root, + EmptyMutation::::new(), + EmptySubscription::::new(), + ) } -impl CharacterCustomVariantFn { - fn as_droid(&self, _: &()) -> Option<&Droid> { - match self { - Self::Two(droid) => Some(droid), - _ => None, +mod trivial_enum { + use super::*; + + #[derive(GraphQLUnion)] + enum Character { + A(Human), + B(Droid), + } + + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object] + impl QueryRoot { + fn character(&self) -> Character { + match self { + Self::Human => Character::A(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + Self::Droid => Character::B(Droid { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + } } } -} -#[derive(GraphQLUnion)] -#[graphql(on Human = CharacterGenericStruct::as_human)] -#[graphql(on Droid = CharacterGenericStruct::as_droid)] -pub struct CharacterGenericStruct { - human: Human, - droid: Droid, - is_droid: bool, - _gen: T, + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + ... on Droid { + droidId: id + primaryFunction + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema(QueryRoot::Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_droid() { + let schema = schema(QueryRoot::Droid); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn is_graphql_union() { + const DOC: &str = r#"{ + __type(name: "Character") { + kind + } + }"#; + + let schema = schema(QueryRoot::Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"kind": "UNION"}}), vec![])), + ); + } + + #[tokio::test] + async fn uses_type_name() { + const DOC: &str = r#"{ + __type(name: "Character") { + name + } + }"#; + + let schema = schema(QueryRoot::Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"name": "Character"}}), vec![])), + ); + } + + #[tokio::test] + async fn has_no_description() { + const DOC: &str = r#"{ + __type(name: "Character") { + description + } + }"#; + + let schema = schema(QueryRoot::Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"description": None}}), vec![])), + ); + } } -impl CharacterGenericStruct { - fn as_human(&self, _: &()) -> Option<&Human> { - if self.is_droid { - None - } else { - Some(&self.human) +mod generic_enum { + use super::*; + + #[derive(GraphQLUnion)] + enum Character { + A(Human), + B(Droid), + #[graphql(ignore)] + _State(A, B), + } + + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object] + impl QueryRoot { + fn character(&self) -> Character { + match self { + Self::Human => Character::A(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + Self::Droid => Character::B(Droid { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + } } } - fn as_droid(&self, _: &()) -> Option<&Droid> { - if self.is_droid { - Some(&self.droid) - } else { - None + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + ... on Droid { + droidId: id + primaryFunction + } } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema(QueryRoot::Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); } -} -// Context Test -pub struct CustomContext { - is_left: bool, -} + #[tokio::test] + async fn resolves_droid() { + let schema = schema(QueryRoot::Droid); -impl juniper::Context for CustomContext {} + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } -#[derive(GraphQLObject)] -#[graphql(Context = CustomContext)] -pub struct HumanContext { - id: String, - home_planet: String, -} + #[tokio::test] + async fn uses_type_name_without_type_params() { + const DOC: &str = r#"{ + __type(name: "Character") { + name + } + }"#; -#[derive(GraphQLObject)] -#[graphql(Context = CustomContext)] -pub struct DroidContext { - id: String, - primary_function: String, -} + let schema = schema(QueryRoot::Human); -/// A Collection of things -#[derive(From, GraphQLUnion)] -#[graphql(Context = CustomContext)] -pub enum CharacterContext { - One(HumanContext), - Two(DroidContext), + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"name": "Character"}}), vec![])), + ); + } } -// #[juniper::object] compatibility +mod description_from_doc_comments { + use super::*; -pub struct HumanCompat { - id: String, - home_planet: String, -} + /// Rust docs. + #[derive(GraphQLUnion)] + enum Character { + A(Human), + } + + struct QueryRoot; -#[juniper::graphql_object] -impl HumanCompat { - fn id(&self) -> &String { - &self.id + #[graphql_object] + impl QueryRoot { + fn character(&self) -> Character { + Character::A(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }) + } } - fn home_planet(&self) -> &String { - &self.home_planet + #[tokio::test] + async fn resolves_human() { + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_doc_comment_as_description() { + const DOC: &str = r#"{ + __type(name: "Character") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"description": "Rust docs."}}), + vec![], + )), + ); } } -pub struct DroidCompat { - id: String, - primary_function: String, +mod explicit_name_and_description { + use super::*; + + /// Rust docs. + #[derive(GraphQLUnion)] + #[graphql(name = "MyChar", desc = "My character.")] + enum Character { + A(Human), + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn character(&self) -> Character { + Character::A(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }) + } + } + + #[tokio::test] + async fn resolves_human() { + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_custom_name() { + const DOC: &str = r#"{ + __type(name: "MyChar") { + name + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok((graphql_value!({"__type": {"name": "MyChar"}}), vec![])), + ); + } + + #[tokio::test] + async fn uses_custom_description() { + const DOC: &str = r#"{ + __type(name: "MyChar") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"description": "My character."}}), + vec![], + )), + ); + } } -#[juniper::graphql_object] -impl DroidCompat { - fn id(&self) -> &String { - &self.id +mod explicit_scalar { + use super::*; + + #[derive(GraphQLUnion)] + #[graphql(scalar = DefaultScalarValue)] + enum Character { + A(Human), + B(Droid), } - fn primary_function(&self) -> &String { - &self.primary_function + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object] + impl QueryRoot { + fn character(&self) -> Character { + match self { + Self::Human => Character::A(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + Self::Droid => Character::B(Droid { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + } + } + } + + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + ... on Droid { + droidId: id + primaryFunction + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema::<_, DefaultScalarValue, _>(QueryRoot::Human); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_droid() { + let schema = schema::<_, DefaultScalarValue, _>(QueryRoot::Droid); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); } } -#[derive(GraphQLUnion)] -#[graphql(Context = CustomContext)] -pub enum DifferentContext { - A(DroidContext), - B(Droid), +mod custom_context { + use super::*; + + #[derive(GraphQLUnion)] + #[graphql(context = CustomContext)] + enum Character { + A(HumanCustomContext), + B(DroidCustomContext), + } + + struct QueryRoot; + + #[graphql_object(context = CustomContext)] + impl QueryRoot { + fn character(&self, ctx: &CustomContext) -> Character { + match ctx { + CustomContext::Human => Character::A(HumanCustomContext { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + CustomContext::Droid => Character::B(DroidCustomContext { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + _ => unimplemented!(), + } + } + } + + const DOC: &str = r#"{ + character { + ... on HumanCustomContext { + humanId: id + homePlanet + } + ... on DroidCustomContext { + droidId: id + primaryFunction + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &CustomContext::Human).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_droid() { + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &CustomContext::Droid).await, + Ok(( + graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } } -// NOTICE: This doesn't compile due to generic implementation of `GraphQLType<__S>`. -// #[derive(GraphQLUnion)] -// pub enum CharacterCompatFail { -// One(HumanCompat), -// Two(DroidCompat), -// } - -/// A Collection of things -#[derive(GraphQLUnion)] -#[graphql(scalar = juniper::DefaultScalarValue)] -pub enum CharacterCompat { - One(HumanCompat), - Two(DroidCompat), +mod different_context { + use super::*; + + #[derive(GraphQLUnion)] + #[graphql(context = CustomContext)] + enum Character { + A(HumanCustomContext), + B(Droid), + } + + struct QueryRoot; + + #[graphql_object(context = CustomContext)] + impl QueryRoot { + fn character(&self, ctx: &CustomContext) -> Character { + match ctx { + CustomContext::Human => Character::A(HumanCustomContext { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + CustomContext::Droid => Character::B(Droid { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + _ => unimplemented!(), + } + } + } + + const DOC: &str = r#"{ + character { + ... on HumanCustomContext { + humanId: id + homePlanet + } + ... on Droid { + droidId: id + primaryFunction + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &CustomContext::Human).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_droid() { + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &CustomContext::Droid).await, + Ok(( + graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } } -pub struct Query; +mod ignored_enum_variants { + use super::*; + + #[derive(GraphQLUnion)] + enum Character { + A(Human), + #[graphql(ignore)] + _C(Ewok), + #[graphql(skip)] + _D, + } + + struct QueryRoot; -#[graphql_object( - Context = CustomContext, -)] -impl Query { - fn context(&self, ctx: &CustomContext) -> CharacterContext { - if ctx.is_left { - HumanContext { + #[graphql_object] + impl QueryRoot { + fn character(&self) -> Character { + Character::A(Human { id: "human-32".to_string(), home_planet: "earth".to_string(), + }) + } + } + + #[tokio::test] + async fn resolves_human() { + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } } - .into() - } else { - DroidContext { + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn ignores_ewok() { + const DOC: &str = r#"{ + __type(name: "Character") { + possibleTypes { + name + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &()).await, + Ok(( + graphql_value!({"__type": {"possibleTypes": [{"name": "Human"}]}}), + vec![], + )), + ); + } +} + +mod external_resolver_enum { + use super::*; + + #[derive(GraphQLUnion)] + #[graphql(context = Database)] + #[graphql(on Droid = Character::as_droid)] + enum Character { + A(Human), + #[graphql(ignore)] + B, + } + + impl Character { + fn as_droid<'db>(&self, db: &'db Database) -> Option<&'db Droid> { + if let Self::B = self { + db.droid.as_ref() + } else { + None + } + } + } + + struct Database { + droid: Option, + } + impl juniper::Context for Database {} + + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object(context = Database)] + impl QueryRoot { + fn character(&self) -> Character { + match self { + Self::Human => Character::A(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + Self::Droid => Character::B, + } + } + } + + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + ... on Droid { + droidId: id + primaryFunction + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema(QueryRoot::Human); + let db = Database { droid: None }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_droid() { + let schema = schema(QueryRoot::Droid); + let db = Database { + droid: Some(Droid { id: "droid-99".to_string(), primary_function: "run".to_string(), + }), + }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).await, + Ok(( + graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } +} + +mod external_resolver_enum_variant { + use super::*; + + #[derive(GraphQLUnion)] + #[graphql(context = Database)] + enum Character { + A(Human), + #[graphql(with = Character::as_droid)] + B(Droid), + } + + impl Character { + fn as_droid<'db>(&self, db: &'db Database) -> Option<&'db Droid> { + if let Self::B(_) = self { + db.droid.as_ref() + } else { + None } - .into() } } + + struct Database { + droid: Option, + } + impl juniper::Context for Database {} + + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object(context = Database)] + impl QueryRoot { + fn character(&self) -> Character { + match self { + Self::Human => Character::A(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + Self::Droid => Character::B(Droid { + id: "?????".to_string(), + primary_function: "???".to_string(), + }), + } + } + } + + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + ... on Droid { + droidId: id + primaryFunction + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema(QueryRoot::Human); + let db = Database { droid: None }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_droid() { + let schema = schema(QueryRoot::Droid); + let db = Database { + droid: Some(Droid { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).await, + Ok(( + graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } } -#[tokio::test] -async fn test_derived_union_doc_macro() { - assert_eq!( - >::name(&()), - Some("Character") - ); - - let mut registry: juniper::Registry = juniper::Registry::new(FnvHashMap::default()); - let meta = Character::meta(&(), &mut registry); - - assert_eq!(meta.name(), Some("Character")); - assert_eq!( - meta.description(), - Some(&"A Collection of things".to_string()) - ); +mod full_featured_enum { + use super::*; + + /// Rust doc. + #[derive(GraphQLUnion)] + #[graphql(name = "MyChar")] + #[graphql(description = "My character.")] + #[graphql(context = CustomContext, scalar = DefaultScalarValue)] + #[graphql(on EwokCustomContext = resolve_ewok)] + enum Character { + A(HumanCustomContext), + #[graphql(with = Character::as_droid)] + B(DroidCustomContext), + #[graphql(ignore)] + C(EwokCustomContext), + #[graphql(ignore)] + _State(T), + } + + impl Character { + fn as_droid(&self, ctx: &CustomContext) -> Option<&DroidCustomContext> { + if let CustomContext::Droid = ctx { + if let Self::B(droid) = self { + return Some(droid); + } + } + None + } + } + + fn resolve_ewok<'a, T>( + ewok: &'a Character, + _: &CustomContext, + ) -> Option<&'a EwokCustomContext> { + if let Character::C(ewok) = ewok { + Some(ewok) + } else { + None + } + } + + struct QueryRoot; + + #[graphql_object(context = CustomContext)] + impl QueryRoot { + fn character(&self, ctx: &CustomContext) -> Character<()> { + match ctx { + CustomContext::Human => Character::A(HumanCustomContext { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + CustomContext::Droid => Character::B(DroidCustomContext { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + CustomContext::Ewok => Character::C(EwokCustomContext { + id: "ewok-1".to_string(), + funny: true, + }), + } + } + } + + const DOC: &str = r#"{ + character { + ... on HumanCustomContext { + humanId: id + homePlanet + } + ... on DroidCustomContext { + droidId: id + primaryFunction + } + ... on EwokCustomContext { + ewokId: id + funny + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &CustomContext::Human).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_droid() { + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &CustomContext::Droid).await, + Ok(( + graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_ewok() { + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &CustomContext::Ewok).await, + Ok(( + graphql_value!({"character": {"ewokId": "ewok-1", "funny": true}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_custom_name() { + const DOC: &str = r#"{ + __type(name: "MyChar") { + name + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &CustomContext::Ewok).await, + Ok((graphql_value!({"__type": {"name": "MyChar"}}), vec![])), + ); + } + + #[tokio::test] + async fn uses_custom_description() { + const DOC: &str = r#"{ + __type(name: "MyChar") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &CustomContext::Ewok).await, + Ok(( + graphql_value!({"__type": {"description": "My character."}}), + vec![], + )), + ); + } } -#[tokio::test] -async fn test_derived_union_doc_string() { - assert_eq!( - >::name(&()), - Some("CharacterContext") - ); - - let mut registry: juniper::Registry = juniper::Registry::new(FnvHashMap::default()); - let meta = CharacterContext::meta(&(), &mut registry); - - assert_eq!(meta.name(), Some("CharacterContext")); - assert_eq!( - meta.description(), - Some(&"A Collection of things".to_string()) - ); +mod trivial_struct { + use super::*; + + #[derive(GraphQLUnion)] + #[graphql(context = Database)] + #[graphql( + on Human = Character::as_human, + on Droid = Character::as_droid, + )] + struct Character { + id: String, + } + + impl Character { + fn as_human<'db>(&self, db: &'db Database) -> Option<&'db Human> { + if let Some(human) = &db.human { + if human.id == self.id { + return Some(human); + } + } + None + } + + fn as_droid<'db>(&self, db: &'db Database) -> Option<&'db Droid> { + if let Some(droid) = &db.droid { + if droid.id == self.id { + return Some(droid); + } + } + None + } + } + + struct Database { + human: Option, + droid: Option, + } + impl juniper::Context for Database {} + + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object(context = Database)] + impl QueryRoot { + fn character(&self) -> Character { + Character { + id: match self { + Self::Human => "human-32", + Self::Droid => "droid-99", + } + .to_string(), + } + } + } + + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + ... on Droid { + droidId: id + primaryFunction + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema(QueryRoot::Human); + let db = Database { + human: Some(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + droid: None, + }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_droid() { + let schema = schema(QueryRoot::Droid); + let db = Database { + human: None, + droid: Some(Droid { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).await, + Ok(( + graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } } -#[tokio::test] -async fn test_derived_union_left() { - let doc = r#" - { - context { - ... on HumanContext { +mod generic_struct { + use super::*; + + #[derive(GraphQLUnion)] + #[graphql(context = Database)] + #[graphql(on Human = Character::as_human)] + struct Character { + id: String, + _s: PhantomData<(A, B)>, + } + + impl Character { + fn as_human<'db>(&self, db: &'db Database) -> Option<&'db Human> { + if let Some(human) = &db.human { + if human.id == self.id { + return Some(human); + } + } + None + } + } + + struct Database { + human: Option, + } + impl juniper::Context for Database {} + + struct QueryRoot; + + #[graphql_object(context = Database)] + impl QueryRoot { + fn character(&self) -> Character { + Character { + id: "human-32".to_string(), + _s: PhantomData, + } + } + } + + #[tokio::test] + async fn resolves_human() { + const DOC: &str = r#"{ + character { + ... on Human { humanId: id homePlanet } - ... on DroidContext { - droidId: id - primaryFunction - } } }"#; - let schema = RootNode::new( - Query, - EmptyMutation::::new(), - EmptySubscription::::new(), - ); - - assert_eq!( - execute( - doc, - None, - &schema, - &Variables::new(), - &CustomContext { is_left: true } - ) - .await, - Ok(( - Value::object( - vec![( - "context", - Value::object( - vec![ - ("humanId", Value::scalar("human-32".to_string())), - ("homePlanet", Value::scalar("earth".to_string())), - ] - .into_iter() - .collect(), - ), - )] - .into_iter() - .collect() - ), - vec![] - )) - ); + let schema = schema(QueryRoot); + let db = Database { + human: Some(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_type_name_without_type_params() { + const DOC: &str = r#"{ + __type(name: "Character") { + name + } + }"#; + + let schema = schema(QueryRoot); + let db = Database { human: None }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).await, + Ok((graphql_value!({"__type": {"name": "Character"}}), vec![])), + ); + } } -#[tokio::test] -async fn test_derived_union_right() { - let doc = r#" - { - context { - ... on HumanContext { - humanId: id - homePlanet +mod full_featured_struct { + use super::*; + + /// Rust doc. + #[derive(GraphQLUnion)] + #[graphql(name = "MyChar")] + #[graphql(description = "My character.")] + #[graphql(context = Database, scalar = DefaultScalarValue)] + #[graphql(on Human = Character::as_human)] + #[graphql(on Droid = Character::as_droid)] + struct Character { + id: String, + _s: PhantomData, + } + + impl Character { + fn as_human<'db>(&self, db: &'db Database) -> Option<&'db Human> { + if let Some(human) = &db.human { + if human.id == self.id { + return Some(human); } - ... on DroidContext { - droidId: id - primaryFunction + } + None + } + } + + impl Character { + fn as_droid<'db>(&self, db: &'db Database) -> Option<&'db Droid> { + if let Some(droid) = &db.droid { + if droid.id == self.id { + return Some(droid); + } + } + None + } + } + + struct Database { + human: Option, + droid: Option, + } + impl juniper::Context for Database {} + + enum QueryRoot { + Human, + Droid, + } + + #[graphql_object(context = Database)] + impl QueryRoot { + fn character(&self) -> Character<()> { + Character { + id: match self { + Self::Human => "human-32", + Self::Droid => "droid-99", } + .to_string(), + _s: PhantomData, + } + } + } + + const DOC: &str = r#"{ + character { + ... on Human { + humanId: id + homePlanet + } + ... on Droid { + droidId: id + primaryFunction + } + } + }"#; + + #[tokio::test] + async fn resolves_human() { + let schema = schema(QueryRoot::Human); + let db = Database { + human: Some(Human { + id: "human-32".to_string(), + home_planet: "earth".to_string(), + }), + droid: None, + }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).await, + Ok(( + graphql_value!({"character": {"humanId": "human-32", "homePlanet": "earth"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn resolves_droid() { + let schema = schema(QueryRoot::Droid); + let db = Database { + human: None, + droid: Some(Droid { + id: "droid-99".to_string(), + primary_function: "run".to_string(), + }), + }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).await, + Ok(( + graphql_value!({"character": {"droidId": "droid-99", "primaryFunction": "run"}}), + vec![], + )), + ); + } + + #[tokio::test] + async fn uses_custom_name() { + const DOC: &str = r#"{ + __type(name: "MyChar") { + name + } + }"#; + + let schema = schema(QueryRoot::Human); + let db = Database { + human: None, + droid: None, + }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).await, + Ok((graphql_value!({"__type": {"name": "MyChar"}}), vec![])), + ); + } + + #[tokio::test] + async fn uses_custom_description() { + const DOC: &str = r#"{ + __type(name: "MyChar") { + description } }"#; - let schema = RootNode::new( - Query, - EmptyMutation::::new(), - EmptySubscription::::new(), - ); - - assert_eq!( - execute( - doc, - None, - &schema, - &Variables::new(), - &CustomContext { is_left: false } - ) - .await, - Ok(( - Value::object( - vec![( - "context", - Value::object( - vec![ - ("droidId", Value::scalar("droid-99".to_string())), - ("primaryFunction", Value::scalar("run".to_string())), - ] - .into_iter() - .collect(), - ), - )] - .into_iter() - .collect() - ), - vec![] - )) - ); + let schema = schema(QueryRoot::Human); + let db = Database { + human: None, + droid: None, + }; + + assert_eq!( + execute(DOC, None, &schema, &Variables::new(), &db).await, + Ok(( + graphql_value!({"__type": {"description": "My character."}}), + vec![], + )), + ); + } } From d9cb3f1dc91ff84075b07e274219b9ac9f7bb714 Mon Sep 17 00:00:00 2001 From: tyranron Date: Thu, 4 Jun 2020 10:55:22 +0300 Subject: [PATCH 29/29] Apply corrections provided by @LegNeato --- docs/book/content/types/unions.md | 12 ++++++------ juniper_codegen/src/lib.rs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/book/content/types/unions.md b/docs/book/content/types/unions.md index 458902d10..16775eb56 100644 --- a/docs/book/content/types/unions.md +++ b/docs/book/content/types/unions.md @@ -1,10 +1,10 @@ Unions ====== -From a server's point of view, [GraphQL unions][1] are similar to interfaces: the only exception is that they don't contain fields on their own. +From the server's point of view, [GraphQL unions][1] are similar to interfaces - the only exception is that they don't contain fields on their own. -For implementing [GraphQL union][1] Juniper provides: -- `#[derive(GraphQLUnion)]` macro for enums and structs; +For implementing [GraphQL unions][1] Juniper provides: +- `#[derive(GraphQLUnion)]` macro for enums and structs. - `#[graphql_union]` for traits. @@ -12,7 +12,7 @@ For implementing [GraphQL union][1] Juniper provides: ## Enums -Most of the time, we need just a trivial and straightforward Rust enum to represent a [GraphQL union][1]. +Most of the time, we just need a trivial and straightforward Rust enum to represent a [GraphQL union][1]. ```rust # #![allow(dead_code)] @@ -374,7 +374,7 @@ impl Character for Droid { ### External resolver functions -Similarly to enums and structs, it's not mandatory to use trait methods as [GraphQL union][1] variant resolvers, and instead custom functions may be specified: +Similarly to enums and structs, it's not mandatory to use trait methods as [GraphQL union][1] variant resolvers. Instead, custom functions may be specified: ```rust # use std::collections::HashMap; @@ -418,7 +418,7 @@ impl Character for Droid { fn id(&self) -> &str { self.id.as_str() } } -// Used trait object is always `Send` and `Sync`. +// The trait object is always `Send` and `Sync`. type DynCharacter<'a> = dyn Character + Send + Sync + 'a; impl<'a> DynCharacter<'a> { diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs index dfae2c8b5..9e6578910 100644 --- a/juniper_codegen/src/lib.rs +++ b/juniper_codegen/src/lib.rs @@ -1123,7 +1123,7 @@ pub fn derive_union_internal(input: TokenStream) -> TokenStream { /// fn id(&self) -> &str { self.id.as_str() } /// } /// -/// // NOTICE: Used trait object is always `Send` and `Sync`. +/// // NOTICE: The trait object is always `Send` and `Sync`. /// type DynCharacter<'a> = dyn Character + Send + Sync + 'a; /// /// impl<'a> DynCharacter<'a> {