From 927e42201aa5ec14a2a454915563c73c78df2704 Mon Sep 17 00:00:00 2001 From: ilslv <47687266+ilslv@users.noreply.github.com> Date: Tue, 28 Jun 2022 14:27:28 +0300 Subject: [PATCH] Rework `#[derive(GraphQLInputObject)]` macro implementation (#1052) Co-authored-by: Kai Ren <tyranron@gmail.com> --- .../introspection/input_object.rs | 4 +- juniper/src/executor_tests/variables.rs | 2 +- juniper_codegen/src/common/default.rs | 65 ++ juniper_codegen/src/common/field/mod.rs | 8 +- juniper_codegen/src/common/mod.rs | 1 + juniper_codegen/src/derive_input_object.rs | 151 ---- juniper_codegen/src/graphql_enum/derive.rs | 3 +- juniper_codegen/src/graphql_enum/mod.rs | 16 +- .../src/graphql_input_object/derive.rs | 136 +++ .../src/graphql_input_object/mod.rs | 797 ++++++++++++++++++ juniper_codegen/src/graphql_interface/mod.rs | 3 +- juniper_codegen/src/graphql_object/mod.rs | 3 +- juniper_codegen/src/graphql_union/mod.rs | 3 +- juniper_codegen/src/lib.rs | 115 ++- juniper_codegen/src/result.rs | 73 +- juniper_codegen/src/util/duplicate.rs | 46 - juniper_codegen/src/util/mod.rs | 712 +--------------- .../derive_incompatible_field_type.rs | 13 + ... => derive_incompatible_field_type.stderr} | 49 +- .../derive_incompatible_object.rs | 11 - .../fail/input-object/derive_no_fields.rs | 4 +- .../fail/input-object/derive_no_fields.stderr | 10 +- .../fail/input-object/derive_no_underscore.rs | 4 +- .../input-object/derive_no_underscore.stderr | 7 +- .../fail/input-object/derive_unique_name.rs | 4 +- .../input-object/derive_unique_name.stderr | 17 +- .../src/codegen/derive_input_object.rs | 191 ----- .../src/codegen/input_object_derive.rs | 708 ++++++++++++++++ tests/integration/src/codegen/mod.rs | 2 +- 29 files changed, 1928 insertions(+), 1230 deletions(-) create mode 100644 juniper_codegen/src/common/default.rs delete mode 100644 juniper_codegen/src/derive_input_object.rs create mode 100644 juniper_codegen/src/graphql_input_object/derive.rs create mode 100644 juniper_codegen/src/graphql_input_object/mod.rs delete mode 100644 juniper_codegen/src/util/duplicate.rs create mode 100644 tests/codegen/fail/input-object/derive_incompatible_field_type.rs rename tests/codegen/fail/input-object/{derive_incompatible_object.stderr => derive_incompatible_field_type.stderr} (50%) delete mode 100644 tests/codegen/fail/input-object/derive_incompatible_object.rs delete mode 100644 tests/integration/src/codegen/derive_input_object.rs create mode 100644 tests/integration/src/codegen/input_object_derive.rs diff --git a/juniper/src/executor_tests/introspection/input_object.rs b/juniper/src/executor_tests/introspection/input_object.rs index f68b9796..450f774d 100644 --- a/juniper/src/executor_tests/introspection/input_object.rs +++ b/juniper/src/executor_tests/introspection/input_object.rs @@ -76,9 +76,9 @@ struct FieldDescription { #[derive(GraphQLInputObject, Debug)] struct FieldWithDefaults { - #[graphql(default = "123")] + #[graphql(default = 123)] field_one: i32, - #[graphql(default = "456", description = "The second field")] + #[graphql(default = 456, description = "The second field")] field_two: i32, } diff --git a/juniper/src/executor_tests/variables.rs b/juniper/src/executor_tests/variables.rs index 257f26c4..023bd61e 100644 --- a/juniper/src/executor_tests/variables.rs +++ b/juniper/src/executor_tests/variables.rs @@ -49,7 +49,7 @@ struct ExampleInputObject { #[derive(GraphQLInputObject, Debug)] struct InputWithDefaults { - #[graphql(default = "123")] + #[graphql(default = 123)] a: i32, } diff --git a/juniper_codegen/src/common/default.rs b/juniper_codegen/src/common/default.rs new file mode 100644 index 00000000..03fb68a0 --- /dev/null +++ b/juniper_codegen/src/common/default.rs @@ -0,0 +1,65 @@ +//! Common functions, definitions and extensions for parsing and code generation +//! of [GraphQL default values][0] +//! +//! [0]: https://spec.graphql.org/October2021#DefaultValue + +use proc_macro2::TokenStream; +use quote::{quote, ToTokens}; +use syn::{ + parse::{Parse, ParseStream}, + token, +}; + +use crate::common::parse::ParseBufferExt as _; + +/// Representation of a [GraphQL default value][0] for code generation. +/// +/// [0]: https://spec.graphql.org/October2021#DefaultValue +#[derive(Clone, Debug)] +pub(crate) enum Value { + /// [`Default`] implementation should be used. + Default, + + /// Explicit [`Expr`]ession to be used as the [default value][0]. + /// + /// [`Expr`]: syn::Expr + /// [0]: https://spec.graphql.org/October2021#DefaultValue + Expr(Box<syn::Expr>), +} + +impl Default for Value { + fn default() -> Self { + Self::Default + } +} + +impl From<Option<syn::Expr>> for Value { + fn from(opt: Option<syn::Expr>) -> Self { + match opt { + Some(expr) => Self::Expr(Box::new(expr)), + None => Self::Default, + } + } +} + +impl Parse for Value { + fn parse(input: ParseStream<'_>) -> syn::Result<Self> { + Ok(input + .try_parse::<token::Eq>()? + .map(|_| input.parse::<syn::Expr>()) + .transpose()? + .into()) + } +} + +impl ToTokens for Value { + fn to_tokens(&self, into: &mut TokenStream) { + match self { + Self::Default => quote! { + ::std::default::Default::default() + } + .to_tokens(into), + Self::Expr(expr) => expr.to_tokens(into), + } + } +} diff --git a/juniper_codegen/src/common/field/mod.rs b/juniper_codegen/src/common/field/mod.rs index 2efe969f..d9fc7526 100644 --- a/juniper_codegen/src/common/field/mod.rs +++ b/juniper_codegen/src/common/field/mod.rs @@ -1,7 +1,7 @@ //! Common functions, definitions and extensions for parsing and code generation //! of [GraphQL fields][1] //! -//! [1]: https://spec.graphql.org/June2018/#sec-Language.Fields. +//! [1]: https://spec.graphql.org/June2018/#sec-Language.Fields pub(crate) mod arg; @@ -42,8 +42,8 @@ pub(crate) struct Attr { /// Explicitly specified [description][2] of this [GraphQL field][1]. /// - /// If [`None`], then Rust doc comment is used as the [description][2], if - /// any. + /// If [`None`], then Rust doc comment will be used as the [description][2], + /// if any. /// /// [1]: https://spec.graphql.org/June2018/#sec-Language.Fields /// [2]: https://spec.graphql.org/June2018/#sec-Descriptions @@ -51,7 +51,7 @@ pub(crate) struct Attr { /// Explicitly specified [deprecation][2] of this [GraphQL field][1]. /// - /// If [`None`], then Rust `#[deprecated]` attribute is used as the + /// If [`None`], then Rust `#[deprecated]` attribute will be used as the /// [deprecation][2], if any. /// /// [1]: https://spec.graphql.org/June2018/#sec-Language.Fields diff --git a/juniper_codegen/src/common/mod.rs b/juniper_codegen/src/common/mod.rs index fd16d954..e3f72d63 100644 --- a/juniper_codegen/src/common/mod.rs +++ b/juniper_codegen/src/common/mod.rs @@ -1,5 +1,6 @@ //! Common functions, definitions and extensions for code generation, used by this crate. +pub(crate) mod default; pub(crate) mod field; pub(crate) mod gen; pub(crate) mod parse; diff --git a/juniper_codegen/src/derive_input_object.rs b/juniper_codegen/src/derive_input_object.rs deleted file mode 100644 index 48d63dbc..00000000 --- a/juniper_codegen/src/derive_input_object.rs +++ /dev/null @@ -1,151 +0,0 @@ -#![allow(clippy::match_wild_err_arm)] -use crate::{ - result::{GraphQLScope, UnsupportedAttribute}, - util::{self, span_container::SpanContainer, RenameRule}, -}; -use proc_macro2::TokenStream; -use quote::{quote, ToTokens}; -use syn::{self, ext::IdentExt, spanned::Spanned, Data, Fields}; - -pub fn impl_input_object(ast: syn::DeriveInput, error: GraphQLScope) -> syn::Result<TokenStream> { - let ast_span = ast.span(); - let fields = match ast.data { - Data::Struct(data) => match data.fields { - Fields::Named(named) => named.named, - _ => { - return Err( - error.custom_error(ast_span, "all fields must be named, e.g., `test: String`") - ) - } - }, - _ => return Err(error.custom_error(ast_span, "can only be used on structs with fields")), - }; - - // Parse attributes. - let attrs = util::ObjectAttributes::from_attrs(&ast.attrs)?; - - // Parse attributes. - let ident = &ast.ident; - let name = attrs - .name - .clone() - .map(SpanContainer::into_inner) - .unwrap_or_else(|| ident.to_string()); - - let fields = 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; - } - }; - - let field_ident = field.ident.as_ref().unwrap(); - let name = match field_attrs.name { - Some(ref name) => name.to_string(), - None => attrs - .rename - .unwrap_or(RenameRule::CamelCase) - .apply(&field_ident.unraw().to_string()), - }; - - if let Some(span) = field_attrs.skip { - error.unsupported_attribute_within(span.span(), UnsupportedAttribute::Skip) - } - - if let Some(span) = field_attrs.deprecation { - error.unsupported_attribute_within( - span.span_ident(), - UnsupportedAttribute::Deprecation, - ) - } - - if name.starts_with("__") { - error.no_double_underscore(if let Some(name) = field_attrs.name { - name.span_ident() - } else { - name.span() - }); - } - - let resolver_code = quote!(#field_ident); - - let default = field_attrs - .default - .map(|default| match default.into_inner() { - Some(expr) => expr.into_token_stream(), - None => quote! { Default::default() }, - }); - - Some(util::GraphQLTypeDefinitionField { - name, - _type: field.ty, - args: Vec::new(), - description: field_attrs.description.map(SpanContainer::into_inner), - deprecation: None, - resolver_code, - is_type_inferred: true, - is_async: false, - default, - span, - }) - }) - .collect::<Vec<_>>(); - - proc_macro_error::abort_if_dirty(); - - if fields.is_empty() { - error.not_empty(ast_span); - } - - if let Some(duplicates) = - crate::util::duplicate::Duplicate::find_by_key(&fields, |field| &field.name) - { - error.duplicate(duplicates.iter()) - } - - if !attrs.interfaces.is_empty() { - attrs.interfaces.iter().for_each(|elm| { - error.unsupported_attribute(elm.span(), UnsupportedAttribute::Interface) - }); - } - - if let Some(duplicates) = - crate::util::duplicate::Duplicate::find_by_key(&fields, |field| field.name.as_str()) - { - error.duplicate(duplicates.iter()); - } - - if !attrs.is_internal && name.starts_with("__") { - error.no_double_underscore(if let Some(name) = attrs.name { - name.span_ident() - } else { - ident.span() - }); - } - - proc_macro_error::abort_if_dirty(); - - let definition = util::GraphQLTypeDefiniton { - name, - _type: syn::parse_str(&ast.ident.to_string()).unwrap(), - context: attrs.context.map(SpanContainer::into_inner), - scalar: attrs.scalar.map(SpanContainer::into_inner), - description: attrs.description.map(SpanContainer::into_inner), - fields, - generics: ast.generics, - interfaces: vec![], - include_type_generics: true, - generic_scalar: true, - no_async: attrs.no_async.is_some(), - }; - - Ok(definition.into_input_object_tokens()) -} diff --git a/juniper_codegen/src/graphql_enum/derive.rs b/juniper_codegen/src/graphql_enum/derive.rs index 7600226e..03ae30c2 100644 --- a/juniper_codegen/src/graphql_enum/derive.rs +++ b/juniper_codegen/src/graphql_enum/derive.rs @@ -1,8 +1,9 @@ //! Code generation for `#[derive(GraphQLEnum)]` macro. +use std::collections::HashSet; + use proc_macro2::TokenStream; use quote::ToTokens as _; -use std::collections::HashSet; use syn::{ext::IdentExt as _, parse_quote, spanned::Spanned}; use crate::{ diff --git a/juniper_codegen/src/graphql_enum/mod.rs b/juniper_codegen/src/graphql_enum/mod.rs index 8556d8b6..87763e64 100644 --- a/juniper_codegen/src/graphql_enum/mod.rs +++ b/juniper_codegen/src/graphql_enum/mod.rs @@ -30,7 +30,7 @@ use crate::{ }; /// Available arguments behind `#[graphql]` attribute placed on a Rust enum -/// definition, when generating code for a [GraphQL enum][0] type. +/// definition, when generating code for a [GraphQL enum][0]. /// /// [0]: https://spec.graphql.org/October2021#sec-Enums #[derive(Debug, Default)] @@ -44,8 +44,8 @@ struct ContainerAttr { /// Explicitly specified [description][2] of this [GraphQL enum][0]. /// - /// If [`None`], then Rust doc comment will be used as [description][2], if - /// any. + /// If [`None`], then Rust doc comment will be used as the [description][2], + /// if any. /// /// [0]: https://spec.graphql.org/October2021#sec-Enums /// [2]: https://spec.graphql.org/October2021#sec-Descriptions @@ -190,14 +190,15 @@ impl ContainerAttr { struct VariantAttr { /// Explicitly specified name of this [GraphQL enum value][1]. /// - /// If [`None`], then Rust enum variant's name is used by default. + /// If [`None`], then Rust enum variant's name will be used by default. /// /// [1]: https://spec.graphql.org/October2021#sec-Enum-Value name: Option<SpanContainer<String>>, /// Explicitly specified [description][2] of this [GraphQL enum value][1]. /// - /// If [`None`], then Rust doc comment is used as [description][2], if any. + /// If [`None`], then Rust doc comment will be used as the [description][2], + /// if any. /// /// [1]: https://spec.graphql.org/October2021#sec-Enum-Value /// [2]: https://spec.graphql.org/October2021#sec-Descriptions @@ -205,7 +206,7 @@ struct VariantAttr { /// Explicitly specified [deprecation][2] of this [GraphQL enum value][1]. /// - /// If [`None`], then Rust `#[deprecated]` attribute is used as the + /// If [`None`], then Rust `#[deprecated]` attribute will be used as the /// [deprecation][2], if any. /// /// If the inner [`Option`] is [`None`], then no [reason][3] was provided. @@ -357,8 +358,9 @@ struct Definition { /// [0]: https://spec.graphql.org/October2021#sec-Enums ident: syn::Ident, - /// [`syn::Generics`] of the Rust enum behind this [GraphQL enum][0]. + /// [`Generics`] of the Rust enum behind this [GraphQL enum][0]. /// + /// [`Generics`]: syn::Generics /// [0]: https://spec.graphql.org/October2021#sec-Enums generics: syn::Generics, diff --git a/juniper_codegen/src/graphql_input_object/derive.rs b/juniper_codegen/src/graphql_input_object/derive.rs new file mode 100644 index 00000000..c1c333b1 --- /dev/null +++ b/juniper_codegen/src/graphql_input_object/derive.rs @@ -0,0 +1,136 @@ +//! Code generation for `#[derive(GraphQLInputObject)]` macro. + +use std::collections::HashSet; + +use proc_macro2::TokenStream; +use quote::ToTokens as _; +use syn::{ext::IdentExt as _, parse_quote, spanned::Spanned}; + +use crate::{ + common::scalar, + result::GraphQLScope, + util::{span_container::SpanContainer, RenameRule}, +}; + +use super::{ContainerAttr, Definition, FieldAttr, FieldDefinition}; + +/// [`GraphQLScope`] of errors for `#[derive(GraphQLInputObject)]` macro. +const ERR: GraphQLScope = GraphQLScope::InputObjectDerive; + +/// Expands `#[derive(GraphQLInputObject)]` macro into generated code. +pub fn expand(input: TokenStream) -> syn::Result<TokenStream> { + let ast = syn::parse2::<syn::DeriveInput>(input)?; + let attr = ContainerAttr::from_attrs("graphql", &ast.attrs)?; + + let data = if let syn::Data::Struct(data) = &ast.data { + data + } else { + return Err(ERR.custom_error(ast.span(), "can only be derived on structs")); + }; + + let renaming = attr + .rename_fields + .map(SpanContainer::into_inner) + .unwrap_or(RenameRule::CamelCase); + + let is_internal = attr.is_internal; + let fields = data + .fields + .iter() + .filter_map(|f| parse_field(f, renaming, is_internal)) + .collect::<Vec<_>>(); + + proc_macro_error::abort_if_dirty(); + + if !fields.iter().any(|f| !f.ignored) { + return Err(ERR.custom_error(data.fields.span(), "expected at least 1 non-ignored field")); + } + + let unique_fields = fields.iter().map(|v| &v.name).collect::<HashSet<_>>(); + if unique_fields.len() != fields.len() { + return Err(ERR.custom_error( + data.fields.span(), + "expected all fields to have unique names", + )); + } + + let name = attr + .name + .clone() + .map(SpanContainer::into_inner) + .unwrap_or_else(|| ast.ident.unraw().to_string()) + .into_boxed_str(); + if !attr.is_internal && name.starts_with("__") { + ERR.no_double_underscore( + attr.name + .as_ref() + .map(SpanContainer::span_ident) + .unwrap_or_else(|| ast.ident.span()), + ); + } + + let context = attr + .context + .map_or_else(|| parse_quote! { () }, SpanContainer::into_inner); + + let description = attr.description.map(|d| d.into_inner().into_boxed_str()); + + let scalar = scalar::Type::parse(attr.scalar.as_deref(), &ast.generics); + + proc_macro_error::abort_if_dirty(); + + let definition = Definition { + ident: ast.ident, + generics: ast.generics, + name, + description, + context, + scalar, + fields, + }; + + Ok(definition.into_token_stream()) +} + +/// Parses a [`FieldDefinition`] from the given struct field definition. +/// +/// Returns [`None`] if the parsing fails. +fn parse_field(f: &syn::Field, renaming: RenameRule, is_internal: bool) -> Option<FieldDefinition> { + let field_attr = FieldAttr::from_attrs("graphql", &f.attrs) + .map_err(|e| proc_macro_error::emit_error!(e)) + .ok()?; + + let ident = f.ident.as_ref().or_else(|| err_unnamed_field(f))?; + + let name = field_attr + .name + .map_or_else( + || renaming.apply(&ident.unraw().to_string()), + SpanContainer::into_inner, + ) + .into_boxed_str(); + if !is_internal && name.starts_with("__") { + ERR.no_double_underscore(f.span()); + } + + let default = field_attr.default.map(SpanContainer::into_inner); + let description = field_attr + .description + .map(|d| d.into_inner().into_boxed_str()); + + Some(FieldDefinition { + ident: ident.clone(), + ty: f.ty.clone(), + default, + name, + description, + ignored: field_attr.ignore.is_some(), + }) +} + +/// Emits "expected named struct field" [`syn::Error`] pointing to the given +/// `span`. +pub(crate) fn err_unnamed_field<T, S: Spanned>(span: &S) -> Option<T> { + ERR.emit_custom(span.span(), "expected named struct field"); + None +} diff --git a/juniper_codegen/src/graphql_input_object/mod.rs b/juniper_codegen/src/graphql_input_object/mod.rs new file mode 100644 index 00000000..ecb0aaa8 --- /dev/null +++ b/juniper_codegen/src/graphql_input_object/mod.rs @@ -0,0 +1,797 @@ +//! Code generation for [GraphQL input objects][0]. +//! +//! [0]: https://spec.graphql.org/October2021#sec-Input-Objects + +pub(crate) mod derive; + +use std::convert::TryInto as _; + +use proc_macro2::TokenStream; +use quote::{format_ident, quote, ToTokens}; +use syn::{ + ext::IdentExt as _, + parse::{Parse, ParseStream}, + parse_quote, + spanned::Spanned, + token, +}; + +use crate::{ + common::{ + default, + parse::{ + attr::{err, OptionExt as _}, + ParseBufferExt as _, + }, + scalar, + }, + util::{filter_attrs, get_doc_comment, span_container::SpanContainer, RenameRule}, +}; + +/// Available arguments behind `#[graphql]` attribute placed on a Rust struct +/// definition, when generating code for a [GraphQL input object][0]. +/// +/// [0]: https://spec.graphql.org/October2021#sec-Input-Objects +#[derive(Debug, Default)] +struct ContainerAttr { + /// Explicitly specified name of this [GraphQL input object][0]. + /// + /// If [`None`], then Rust struct name will be used by default. + /// + /// [0]: https://spec.graphql.org/October2021#sec-Input-Objects + name: Option<SpanContainer<String>>, + + /// Explicitly specified [description][2] of this [GraphQL input object][0]. + /// + /// If [`None`], then Rust doc comment will be used as the [description][2], + /// if any. + /// + /// [0]: https://spec.graphql.org/October2021#sec-Input-Objects + /// [2]: https://spec.graphql.org/October2021#sec-Descriptions + description: Option<SpanContainer<String>>, + + /// Explicitly specified type of [`Context`] to use for resolving this + /// [GraphQL input object][0] type with. + /// + /// If [`None`], then unit type `()` is assumed as a type of [`Context`]. + /// + /// [`Context`]: juniper::Context + /// [0]: https://spec.graphql.org/October2021#sec-Input-Objects + context: Option<SpanContainer<syn::Type>>, + + /// Explicitly specified type (or type parameter with its bounds) of + /// [`ScalarValue`] to use for resolving this [GraphQL input object][0] type + /// with. + /// + /// If [`None`], then generated code will be generic over any + /// [`ScalarValue`] type. + /// + /// [`GraphQLType`]: juniper::GraphQLType + /// [`ScalarValue`]: juniper::ScalarValue + /// [0]: https://spec.graphql.org/October2021#sec-Input-Objects + scalar: Option<SpanContainer<scalar::AttrValue>>, + + /// Explicitly specified [`RenameRule`] for all fields of this + /// [GraphQL input object][0]. + /// + /// If [`None`], then the [`RenameRule::CamelCase`] rule will be + /// applied by default. + /// + /// [0]: https://spec.graphql.org/October2021#sec-Input-Objects + rename_fields: Option<SpanContainer<RenameRule>>, + + /// Indicator whether the generated code is intended to be used only inside + /// the [`juniper`] library. + is_internal: bool, +} + +impl Parse for ContainerAttr { + fn parse(input: ParseStream<'_>) -> syn::Result<Self> { + let mut out = Self::default(); + while !input.is_empty() { + let ident = input.parse_any_ident()?; + match ident.to_string().as_str() { + "name" => { + input.parse::<token::Eq>()?; + let name = input.parse::<syn::LitStr>()?; + out.name + .replace(SpanContainer::new( + ident.span(), + Some(name.span()), + name.value(), + )) + .none_or_else(|_| err::dup_arg(&ident))? + } + "desc" | "description" => { + input.parse::<token::Eq>()?; + let desc = input.parse::<syn::LitStr>()?; + out.description + .replace(SpanContainer::new( + ident.span(), + Some(desc.span()), + desc.value(), + )) + .none_or_else(|_| err::dup_arg(&ident))? + } + "ctx" | "context" | "Context" => { + input.parse::<token::Eq>()?; + let ctx = input.parse::<syn::Type>()?; + out.context + .replace(SpanContainer::new(ident.span(), Some(ctx.span()), ctx)) + .none_or_else(|_| err::dup_arg(&ident))? + } + "scalar" | "Scalar" | "ScalarValue" => { + input.parse::<token::Eq>()?; + let scl = input.parse::<scalar::AttrValue>()?; + out.scalar + .replace(SpanContainer::new(ident.span(), Some(scl.span()), scl)) + .none_or_else(|_| err::dup_arg(&ident))? + } + "rename_all" => { + input.parse::<token::Eq>()?; + let val = input.parse::<syn::LitStr>()?; + out.rename_fields + .replace(SpanContainer::new( + ident.span(), + Some(val.span()), + val.try_into()?, + )) + .none_or_else(|_| err::dup_arg(&ident))?; + } + "internal" => { + out.is_internal = true; + } + name => { + return Err(err::unknown_arg(&ident, name)); + } + } + input.try_parse::<token::Comma>()?; + } + Ok(out) + } +} + +impl ContainerAttr { + /// Tries to merge two [`ContainerAttr`]s into a single one, reporting about + /// duplicates, if any. + fn try_merge(self, mut another: Self) -> syn::Result<Self> { + Ok(Self { + 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), + rename_fields: try_merge_opt!(rename_fields: self, another), + is_internal: self.is_internal || another.is_internal, + }) + } + + /// Parses [`ContainerAttr`] from the given multiple `name`d + /// [`syn::Attribute`]s placed on a struct or impl block definition. + pub(crate) fn from_attrs(name: &str, attrs: &[syn::Attribute]) -> syn::Result<Self> { + let mut attr = filter_attrs(name, attrs) + .map(|attr| attr.parse_args()) + .try_fold(Self::default(), |prev, curr| prev.try_merge(curr?))?; + + if attr.description.is_none() { + attr.description = get_doc_comment(attrs); + } + + Ok(attr) + } +} + +/// Available arguments behind `#[graphql]` attribute when generating code for +/// [GraphQL input object][0]'s [field][1]. +/// +/// [0]: https://spec.graphql.org/October2021#sec-Input-Objects +/// [1]: https://spec.graphql.org/October2021#InputFieldsDefinition +#[derive(Debug, Default)] +struct FieldAttr { + /// Explicitly specified name of this [GraphQL input object field][1]. + /// + /// If [`None`], then Rust struct field name will be used by default. + /// + /// [1]: https://spec.graphql.org/October2021#InputValueDefinition + name: Option<SpanContainer<String>>, + + /// Explicitly specified [default value][2] of this + /// [GraphQL input object field][1] to be used used in case a field value is + /// not provided. + /// + /// If [`None`], the this [field][1] will have no [default value][2]. + /// + /// [1]: https://spec.graphql.org/October2021#InputValueDefinition + /// [2]: https://spec.graphql.org/October2021#DefaultValue + default: Option<SpanContainer<default::Value>>, + + /// Explicitly specified [description][2] of this + /// [GraphQL input object field][1]. + /// + /// If [`None`], then Rust doc comment will be used as the [description][2], + /// if any. + /// + /// [1]: https://spec.graphql.org/October2021#InputValueDefinition + /// [2]: https://spec.graphql.org/October2021#sec-Descriptions + description: Option<SpanContainer<String>>, + + /// Explicitly specified marker for the Rust struct field to be ignored and + /// not included into the code generated for a [GraphQL input object][0] + /// implementation. + /// + /// Ignored Rust struct fields still consider the [`default`] attribute's + /// argument. + /// + /// [`default`]: Self::default + /// [0]: https://spec.graphql.org/October2021#sec-Input-Objects + ignore: Option<SpanContainer<syn::Ident>>, +} + +impl Parse for FieldAttr { + fn parse(input: ParseStream<'_>) -> syn::Result<Self> { + let mut out = Self::default(); + while !input.is_empty() { + let ident = input.parse_any_ident()?; + match ident.to_string().as_str() { + "name" => { + input.parse::<token::Eq>()?; + let name = input.parse::<syn::LitStr>()?; + out.name + .replace(SpanContainer::new( + ident.span(), + Some(name.span()), + name.value(), + )) + .none_or_else(|_| err::dup_arg(&ident))? + } + "default" => { + let val = input.parse::<default::Value>()?; + out.default + .replace(SpanContainer::new(ident.span(), Some(val.span()), val)) + .none_or_else(|_| err::dup_arg(&ident))? + } + "desc" | "description" => { + input.parse::<token::Eq>()?; + let desc = input.parse::<syn::LitStr>()?; + out.description + .replace(SpanContainer::new( + ident.span(), + Some(desc.span()), + desc.value(), + )) + .none_or_else(|_| err::dup_arg(&ident))? + } + "ignore" | "skip" => out + .ignore + .replace(SpanContainer::new(ident.span(), None, ident.clone())) + .none_or_else(|_| err::dup_arg(&ident))?, + name => { + return Err(err::unknown_arg(&ident, name)); + } + } + input.try_parse::<token::Comma>()?; + } + Ok(out) + } +} + +impl FieldAttr { + /// Tries to merge two [`FieldAttr`]s into a single one, reporting about + /// duplicates, if any. + fn try_merge(self, mut another: Self) -> syn::Result<Self> { + Ok(Self { + name: try_merge_opt!(name: self, another), + default: try_merge_opt!(default: self, another), + description: try_merge_opt!(description: self, another), + ignore: try_merge_opt!(ignore: self, another), + }) + } + + /// Parses [`FieldAttr`] from the given multiple `name`d [`syn::Attribute`]s + /// placed on a trait definition. + fn from_attrs(name: &str, attrs: &[syn::Attribute]) -> syn::Result<Self> { + let mut attr = filter_attrs(name, attrs) + .map(|attr| attr.parse_args()) + .try_fold(Self::default(), |prev, curr| prev.try_merge(curr?))?; + + if attr.description.is_none() { + attr.description = get_doc_comment(attrs); + } + + Ok(attr) + } +} + +/// Representation of a [GraphQL input object field][1] for code generation. +/// +/// [1]: https://spec.graphql.org/October2021#InputFieldsDefinition +#[derive(Debug)] +struct FieldDefinition { + /// [`Ident`] of the Rust struct field behind this + /// [GraphQL input object field][1]. + /// + /// [`Ident`]: syn::Ident + /// [1]: https://spec.graphql.org/October2021#InputValueDefinition + ident: syn::Ident, + + /// Rust type that this [GraphQL input object field][1] is represented with. + /// + /// It should contain all its generics, if any. + /// + /// [1]: https://spec.graphql.org/October2021#InputValueDefinition + ty: syn::Type, + + /// [Default value][2] of this [GraphQL input object field][1] to be used in + /// case a [field][1] value is not provided. + /// + /// [1]: https://spec.graphql.org/October2021#InputValueDefinition + /// [2]: https://spec.graphql.org/October2021#DefaultValue + default: Option<default::Value>, + + /// Name of this [GraphQL input object field][1] in GraphQL schema. + /// + /// [1]: https://spec.graphql.org/October2021#InputValueDefinition + name: Box<str>, + + /// [Description][2] of this [GraphQL input object field][1] to put into + /// GraphQL schema. + /// + /// [1]: https://spec.graphql.org/October2021#InputValueDefinition + /// [2]: https://spec.graphql.org/October2021#sec-Descriptions + description: Option<Box<str>>, + + /// Indicator whether the Rust struct field behinds this + /// [GraphQL input object field][1] is being ignored and should not be + /// included into the generated code. + /// + /// Ignored Rust struct fields still consider the [`default`] attribute's + /// argument. + /// + /// [`default`]: Self::default + /// [0]: https://spec.graphql.org/October2021#sec-Input-Objects + ignored: bool, +} + +/// Representation of [GraphQL input object][0] for code generation. +/// +/// [0]: https://spec.graphql.org/October2021#sec-Input-Objects +#[derive(Debug)] +struct Definition { + /// [`Ident`] of the Rust struct behind this [GraphQL input object][0]. + /// + /// [`Ident`]: syn::Ident + /// [0]: https://spec.graphql.org/October2021#sec-Input-Objects + ident: syn::Ident, + + /// [`Generics`] of the Rust enum behind this [GraphQL input object][0]. + /// + /// [`Generics`]: syn::Generics + /// [0]: https://spec.graphql.org/October2021#sec-Input-Objects + generics: syn::Generics, + + /// Name of this [GraphQL input object][0] in GraphQL schema. + /// + /// [0]: https://spec.graphql.org/October2021#sec-Input-Objects + name: Box<str>, + + /// [Description][2] of this [GraphQL input object][0] to put into GraphQL + /// schema. + /// + /// [0]: https://spec.graphql.org/October2021#sec-Input-Objects + /// [2]: https://spec.graphql.org/October2021#sec-Descriptions + description: Option<Box<str>>, + + /// Rust type of [`Context`] to generate [`GraphQLType`] implementation with + /// for this [GraphQL input object][0]. + /// + /// [`GraphQLType`]: juniper::GraphQLType + /// [`Context`]: juniper::Context + /// [0]: https://spec.graphql.org/October2021#sec-Input-Objects + context: syn::Type, + + /// [`ScalarValue`] parametrization to generate [`GraphQLType`] + /// implementation with for this [GraphQL input object][0]. + /// + /// [`GraphQLType`]: juniper::GraphQLType + /// [`ScalarValue`]: juniper::ScalarValue + /// [0]: https://spec.graphql.org/October2021#sec-Input-Objects + scalar: scalar::Type, + + /// [Fields][1] of this [GraphQL input object][0]. + /// + /// [0]: https://spec.graphql.org/October2021#sec-Input-Objects + /// [1]: https://spec.graphql.org/October2021#InputFieldsDefinition + fields: Vec<FieldDefinition>, +} + +impl ToTokens for Definition { + fn to_tokens(&self, into: &mut TokenStream) { + self.impl_input_type_tokens().to_tokens(into); + self.impl_graphql_type_tokens().to_tokens(into); + self.impl_graphql_value_tokens().to_tokens(into); + self.impl_graphql_value_async_tokens().to_tokens(into); + self.impl_from_input_value_tokens().to_tokens(into); + self.impl_to_input_value_tokens().to_tokens(into); + self.impl_reflection_traits_tokens().to_tokens(into); + } +} + +impl Definition { + /// Returns generated code implementing [`marker::IsInputType`] trait for + /// this [GraphQL input object][0]. + /// + /// [`marker::IsInputType`]: juniper::marker::IsInputType + /// [0]: https://spec.graphql.org/October2021#sec-Input-Objects + #[must_use] + fn impl_input_type_tokens(&self) -> TokenStream { + let ident = &self.ident; + let scalar = &self.scalar; + + let generics = self.impl_generics(false); + let (impl_generics, _, where_clause) = generics.split_for_impl(); + let (_, ty_generics, _) = self.generics.split_for_impl(); + + let assert_fields_input_values = self.fields.iter().filter_map(|f| { + let ty = &f.ty; + + (!f.ignored).then(|| { + quote! { + <#ty as ::juniper::marker::IsInputType<#scalar>>::mark(); + } + }) + }); + + quote! { + #[automatically_derived] + impl#impl_generics ::juniper::marker::IsInputType<#scalar> + for #ident#ty_generics + #where_clause + { + fn mark() { + #( #assert_fields_input_values )* + } + } + } + } + + /// Returns generated code implementing [`GraphQLType`] trait for this + /// [GraphQL input object][0]. + /// + /// [`GraphQLType`]: juniper::GraphQLType + /// [0]: https://spec.graphql.org/October2021#sec-Input-Objects + #[must_use] + fn impl_graphql_type_tokens(&self) -> TokenStream { + let ident = &self.ident; + let scalar = &self.scalar; + let name = &self.name; + + let generics = self.impl_generics(false); + let (impl_generics, _, where_clause) = generics.split_for_impl(); + let (_, ty_generics, _) = self.generics.split_for_impl(); + + let description = self + .description + .as_ref() + .map(|desc| quote! { .description(#desc) }); + + let fields = self.fields.iter().filter_map(|f| { + let ty = &f.ty; + let name = &f.name; + + (!f.ignored).then(|| { + let arg = if let Some(default) = &f.default { + quote! { .arg_with_default::<#ty>(#name, &#default, info) } + } else { + quote! { .arg::<#ty>(#name, info) } + }; + let description = f + .description + .as_ref() + .map(|desc| quote! { .description(#desc) }); + + quote! { registry#arg#description } + }) + }); + + quote! { + #[automatically_derived] + impl#impl_generics ::juniper::GraphQLType<#scalar> + for #ident#ty_generics + #where_clause + { + fn name(_: &Self::TypeInfo) -> Option<&'static str> { + Some(#name) + } + + fn meta<'r>( + info: &Self::TypeInfo, + registry: &mut ::juniper::Registry<'r, #scalar>, + ) -> ::juniper::meta::MetaType<'r, #scalar> + where + #scalar: 'r, + { + let fields = [#( #fields ),*]; + registry + .build_input_object_type::<#ident#ty_generics>(info, &fields) + #description + .into_meta() + } + } + } + } + + /// Returns generated code implementing [`GraphQLValue`] trait for this + /// [GraphQL input object][0]. + /// + /// [`GraphQLValue`]: juniper::GraphQLValue + /// [0]: https://spec.graphql.org/October2021#sec-Input-Objects + #[must_use] + fn impl_graphql_value_tokens(&self) -> TokenStream { + let ident = &self.ident; + let scalar = &self.scalar; + let context = &self.context; + + let generics = self.impl_generics(false); + let (impl_generics, _, where_clause) = generics.split_for_impl(); + let (_, ty_generics, _) = self.generics.split_for_impl(); + + quote! { + #[automatically_derived] + impl#impl_generics ::juniper::GraphQLValue<#scalar> + for #ident#ty_generics + #where_clause + { + type Context = #context; + type TypeInfo = (); + + fn type_name<'__i>(&self, info: &'__i Self::TypeInfo) -> Option<&'__i str> { + <Self as ::juniper::GraphQLType<#scalar>>::name(info) + } + } + } + } + + /// Returns generated code implementing [`GraphQLValueAsync`] trait for this + /// [GraphQL input object][0]. + /// + /// [`GraphQLValueAsync`]: juniper::GraphQLValueAsync + /// [0]: https://spec.graphql.org/October2021#sec-Input-Objects + #[must_use] + fn impl_graphql_value_async_tokens(&self) -> TokenStream { + let ident = &self.ident; + let scalar = &self.scalar; + + let generics = self.impl_generics(true); + let (impl_generics, _, where_clause) = generics.split_for_impl(); + let (_, ty_generics, _) = self.generics.split_for_impl(); + + quote! { + #[allow(non_snake_case)] + #[automatically_derived] + impl#impl_generics ::juniper::GraphQLValueAsync<#scalar> + for #ident#ty_generics + #where_clause {} + } + } + + /// Returns generated code implementing [`FromInputValue`] trait for this + /// [GraphQL input object][0]. + /// + /// [`FromInputValue`]: juniper::FromInputValue + /// [0]: https://spec.graphql.org/October2021#sec-Input-Objects + #[must_use] + fn impl_from_input_value_tokens(&self) -> TokenStream { + let ident = &self.ident; + let scalar = &self.scalar; + + let generics = self.impl_generics(false); + let (impl_generics, _, where_clause) = generics.split_for_impl(); + let (_, ty_generics, _) = self.generics.split_for_impl(); + + let fields = self.fields.iter().map(|f| { + let ident = &f.ident; + + let construct = if f.ignored { + f.default.as_ref().map_or_else( + || { + let expr = default::Value::default(); + quote! { #expr } + }, + |expr| quote! { #expr }, + ) + } else { + let name = &f.name; + + let fallback = f.default.as_ref().map_or_else( + || { + quote! { + ::juniper::FromInputValue::<#scalar>::from_implicit_null() + .map_err(::juniper::IntoFieldError::into_field_error)? + } + }, + |expr| quote! { #expr }, + ); + + quote! { + match obj.get(#name) { + Some(v) => { + ::juniper::FromInputValue::<#scalar>::from_input_value(v) + .map_err(::juniper::IntoFieldError::into_field_error)? + } + None => { #fallback } + } + } + }; + + quote! { #ident: { #construct }, } + }); + + quote! { + #[automatically_derived] + impl#impl_generics ::juniper::FromInputValue<#scalar> + for #ident#ty_generics + #where_clause + { + type Error = ::juniper::FieldError<#scalar>; + + fn from_input_value( + value: &::juniper::InputValue<#scalar>, + ) -> Result<Self, Self::Error> { + let obj = value + .to_object_value() + .ok_or_else(|| ::juniper::FieldError::<#scalar>::from( + ::std::format!("Expected input object, found: {}", value)) + )?; + + Ok(#ident { + #( #fields )* + }) + } + } + } + } + + /// Returns generated code implementing [`ToInputValue`] trait for this + /// [GraphQL input object][0]. + /// + /// [`ToInputValue`]: juniper::ToInputValue + /// [0]: https://spec.graphql.org/October2021#sec-Input-Objects + #[must_use] + fn impl_to_input_value_tokens(&self) -> TokenStream { + let ident = &self.ident; + let scalar = &self.scalar; + + let generics = self.impl_generics(false); + let (impl_generics, _, where_clause) = generics.split_for_impl(); + let (_, ty_generics, _) = self.generics.split_for_impl(); + + let fields = self.fields.iter().filter_map(|f| { + let ident = &f.ident; + let name = &f.name; + + (!f.ignored).then(|| { + quote! { + (#name, ::juniper::ToInputValue::to_input_value(&self.#ident)) + } + }) + }); + + quote! { + #[automatically_derived] + impl#impl_generics ::juniper::ToInputValue<#scalar> + for #ident#ty_generics + #where_clause + { + fn to_input_value(&self) -> ::juniper::InputValue<#scalar> { + ::juniper::InputValue::object( + #[allow(deprecated)] + ::std::array::IntoIter::new([#( #fields ),*]) + .collect() + ) + } + } + } + } + + /// Returns generated code implementing [`BaseType`], [`BaseSubTypes`] and + /// [`WrappedType`] traits for this [GraphQL input object][0]. + /// + /// [`BaseSubTypes`]: juniper::macros::reflect::BaseSubTypes + /// [`BaseType`]: juniper::macros::reflect::BaseType + /// [`WrappedType`]: juniper::macros::reflect::WrappedType + /// [0]: https://spec.graphql.org/October2021#sec-Input-Objects + #[must_use] + fn impl_reflection_traits_tokens(&self) -> TokenStream { + let ident = &self.ident; + let name = &self.name; + let scalar = &self.scalar; + + let generics = self.impl_generics(false); + let (impl_generics, _, where_clause) = generics.split_for_impl(); + let (_, ty_generics, _) = self.generics.split_for_impl(); + + quote! { + #[automatically_derived] + impl#impl_generics ::juniper::macros::reflect::BaseType<#scalar> + for #ident#ty_generics + #where_clause + { + const NAME: ::juniper::macros::reflect::Type = #name; + } + + impl#impl_generics ::juniper::macros::reflect::BaseSubTypes<#scalar> + for #ident#ty_generics + #where_clause + { + const NAMES: ::juniper::macros::reflect::Types = + &[<Self as ::juniper::macros::reflect::BaseType<#scalar>>::NAME]; + } + + impl#impl_generics ::juniper::macros::reflect::WrappedType<#scalar> + for #ident#ty_generics + #where_clause + { + const VALUE: ::juniper::macros::reflect::WrappedValue = 1; + } + } + } + + /// Returns prepared [`syn::Generics`] for [`GraphQLType`] trait (and + /// similar) implementation of this struct. + /// + /// If `for_async` is `true`, then additional predicates are added to suit + /// the [`GraphQLAsyncValue`] trait (and similar) requirements. + /// + /// [`GraphQLAsyncValue`]: juniper::GraphQLAsyncValue + /// [`GraphQLType`]: juniper::GraphQLType + #[must_use] + fn impl_generics(&self, for_async: bool) -> syn::Generics { + let mut generics = self.generics.clone(); + + let scalar = &self.scalar; + if scalar.is_implicit_generic() { + generics.params.push(parse_quote! { #scalar }); + } + if scalar.is_generic() { + generics + .make_where_clause() + .predicates + .push(parse_quote! { #scalar: ::juniper::ScalarValue }); + } + if let Some(bound) = scalar.bounds() { + generics.make_where_clause().predicates.push(bound); + } + + if for_async { + let self_ty = if self.generics.lifetimes().next().is_some() { + // Modify lifetime names to omit "lifetime name `'a` shadows a + // lifetime name that is already in scope" error. + let mut generics = self.generics.clone(); + for lt in generics.lifetimes_mut() { + let ident = lt.lifetime.ident.unraw(); + lt.lifetime.ident = format_ident!("__fa__{}", ident); + } + + let lifetimes = generics.lifetimes().map(|lt| <.lifetime); + let ident = &self.ident; + let (_, ty_generics, _) = generics.split_for_impl(); + + quote! { for<#( #lifetimes ),*> #ident#ty_generics } + } else { + quote! { Self } + }; + generics + .make_where_clause() + .predicates + .push(parse_quote! { #self_ty: Sync }); + + if scalar.is_generic() { + generics + .make_where_clause() + .predicates + .push(parse_quote! { #scalar: Send + Sync }); + } + } + + generics + } +} diff --git a/juniper_codegen/src/graphql_interface/mod.rs b/juniper_codegen/src/graphql_interface/mod.rs index 700fa3dc..18c3749d 100644 --- a/juniper_codegen/src/graphql_interface/mod.rs +++ b/juniper_codegen/src/graphql_interface/mod.rs @@ -66,7 +66,8 @@ struct Attr { /// Explicitly specified [description][2] of [GraphQL interface][1] type. /// - /// If [`None`], then Rust doc comment is used as [description][2], if any. + /// If [`None`], then Rust doc comment will be used as the [description][2], + /// if any. /// /// [1]: https://spec.graphql.org/June2018/#sec-Interfaces /// [2]: https://spec.graphql.org/June2018/#sec-Descriptions diff --git a/juniper_codegen/src/graphql_object/mod.rs b/juniper_codegen/src/graphql_object/mod.rs index 0f3b05ac..88afd996 100644 --- a/juniper_codegen/src/graphql_object/mod.rs +++ b/juniper_codegen/src/graphql_object/mod.rs @@ -44,7 +44,8 @@ pub(crate) struct Attr { /// Explicitly specified [description][2] of this [GraphQL object][1] type. /// - /// If [`None`], then Rust doc comment is used as [description][2], if any. + /// If [`None`], then Rust doc comment will be used as the [description][2], + /// if any. /// /// [1]: https://spec.graphql.org/June2018/#sec-Objects /// [2]: https://spec.graphql.org/June2018/#sec-Descriptions diff --git a/juniper_codegen/src/graphql_union/mod.rs b/juniper_codegen/src/graphql_union/mod.rs index 21fb9cb4..c59b6732 100644 --- a/juniper_codegen/src/graphql_union/mod.rs +++ b/juniper_codegen/src/graphql_union/mod.rs @@ -47,7 +47,8 @@ struct Attr { /// Explicitly specified [description][2] of [GraphQL union][1] type. /// - /// If [`None`], then Rust doc comment is used as [description][2], if any. + /// If [`None`], then Rust doc comment will be used as the [description][2], + /// if any. /// /// [1]: https://spec.graphql.org/June2018/#sec-Unions /// [2]: https://spec.graphql.org/June2018/#sec-Descriptions diff --git a/juniper_codegen/src/lib.rs b/juniper_codegen/src/lib.rs index 70c346ee..b0eeb053 100644 --- a/juniper_codegen/src/lib.rs +++ b/juniper_codegen/src/lib.rs @@ -100,10 +100,9 @@ macro_rules! try_merge_hashset { }; } -mod derive_input_object; - mod common; mod graphql_enum; +mod graphql_input_object; mod graphql_interface; mod graphql_object; mod graphql_scalar; @@ -115,15 +114,115 @@ use proc_macro::TokenStream; use proc_macro_error::{proc_macro_error, ResultExt as _}; use result::GraphQLScope; +/// `#[derive(GraphQLInputObject)]` macro for deriving a +/// [GraphQL input object][0] implementation for a Rust struct. Each +/// non-ignored field type must itself be [GraphQL input object][0] or a +/// [GraphQL scalar][2]. +/// +/// 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. +/// +/// ```rust +/// use juniper::GraphQLInputObject; +/// +/// #[derive(GraphQLInputObject)] +/// struct Point2D { +/// x: f64, +/// y: f64, +/// } +/// ``` +/// +/// # Custom name and description +/// +/// The name of a [GraphQL input object][0] or its [fields][1] may be overridden +/// with the `name` attribute's argument. By default, a type name or a struct +/// field name is used in a `camelCase`. +/// +/// The description of a [GraphQL input object][0] or its [fields][1] may be +/// specified either with the `description`/`desc` attribute's argument, or with +/// a regular Rust doc comment. +/// +/// ```rust +/// # use juniper::GraphQLInputObject; +/// # +/// #[derive(GraphQLInputObject)] +/// #[graphql( +/// // Rename the type for GraphQL by specifying the name here. +/// name = "Point", +/// // You may also specify a description here. +/// // If present, doc comments will be ignored. +/// desc = "A point is the simplest two-dimensional primitive.", +/// )] +/// struct Point2D { +/// /// Abscissa value. +/// x: f64, +/// +/// #[graphql(name = "y", desc = "Ordinate value")] +/// y_coord: f64, +/// } +/// ``` +/// +/// # Renaming policy +/// +/// By default, all [GraphQL input object fields][1] are renamed in a +/// `camelCase` manner (so a `y_coord` Rust struct field becomes a +/// `yCoord` [value][1] in GraphQL schema, and so on). This complies with +/// default GraphQL naming conventions as [demonstrated in spec][0]. +/// +/// However, if you need for some reason another naming convention, it's +/// possible to do so by using the `rename_all` attribute's argument. At the +/// moment, it supports the following policies only: `SCREAMING_SNAKE_CASE`, +/// `camelCase`, `none` (disables any renaming). +/// +/// ```rust +/// # use juniper::GraphQLInputObject; +/// # +/// #[derive(GraphQLInputObject)] +/// #[graphql(rename_all = "none")] // disables renaming +/// struct Point2D { +/// x: f64, +/// y_coord: f64, // will be `y_coord` instead of `yCoord` in GraphQL schema +/// } +/// ``` +/// +/// # Ignoring fields +/// +/// To omit exposing a Rust field in a GraphQL schema, use the `ignore` +/// attribute's argument directly on that field. Ignored fields must implement +/// [`Default`] or have the `default = <expression>` attribute's argument. +/// +/// ```rust +/// # use juniper::GraphQLInputObject; +/// # +/// enum System { +/// Cartesian, +/// } +/// +/// #[derive(GraphQLInputObject)] +/// struct Point2D { +/// x: f64, +/// y: f64, +/// #[graphql(ignore)] +/// shift: f64, // `Default::default()` impl is used. +/// #[graphql(skip, default = System::Cartesian)] +/// // ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +/// // This attribute is required, as we need to be to construct `Point2D` +/// // from `{ x: 0.0, y: 0.0 }` GraphQL input. +/// system: System, +/// } +/// ``` +/// +/// [`ScalarValue`]: juniper::ScalarValue +/// [0]: https://spec.graphql.org/October2021#sec-Input-Objects +/// [1]: https://spec.graphql.org/October2021#InputFieldsDefinition +/// [2]: https://spec.graphql.org/October2021#sec-Scalars #[proc_macro_error] #[proc_macro_derive(GraphQLInputObject, attributes(graphql))] pub fn derive_input_object(input: TokenStream) -> TokenStream { - let ast = syn::parse::<syn::DeriveInput>(input).unwrap(); - let gen = derive_input_object::impl_input_object(ast, GraphQLScope::DeriveInputObject); - match gen { - Ok(gen) => gen.into(), - Err(err) => proc_macro_error::abort!(err), - } + graphql_input_object::derive::expand(input.into()) + .unwrap_or_abort() + .into() } /// `#[derive(GraphQLEnum)]` macro for deriving a [GraphQL enum][0] diff --git a/juniper_codegen/src/result.rs b/juniper_codegen/src/result.rs index 5c979d69..9ec3793b 100644 --- a/juniper_codegen/src/result.rs +++ b/juniper_codegen/src/result.rs @@ -1,15 +1,16 @@ //! -use crate::util::duplicate::Duplicate; +use std::fmt; + use proc_macro2::Span; use proc_macro_error::{Diagnostic, Level}; -use std::fmt; /// URL of the GraphQL specification (June 2018 Edition). pub const SPEC_URL: &str = "https://spec.graphql.org/June2018/"; pub enum GraphQLScope { EnumDerive, + InputObjectDerive, InterfaceAttr, InterfaceDerive, ObjectAttr, @@ -19,19 +20,18 @@ pub enum GraphQLScope { ScalarValueDerive, UnionAttr, UnionDerive, - DeriveInputObject, } impl GraphQLScope { pub fn spec_section(&self) -> &str { match self { Self::EnumDerive => "#sec-Enums", + Self::InputObjectDerive => "#sec-Input-Objects", Self::InterfaceAttr | Self::InterfaceDerive => "#sec-Interfaces", Self::ObjectAttr | Self::ObjectDerive => "#sec-Objects", Self::ScalarAttr | Self::ScalarDerive => "#sec-Scalars", Self::ScalarValueDerive => "#sec-Scalars.Built-in-Scalars", Self::UnionAttr | Self::UnionDerive => "#sec-Unions", - Self::DeriveInputObject => "#sec-Input-Objects", } } } @@ -40,25 +40,17 @@ impl fmt::Display for GraphQLScope { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let name = match self { Self::EnumDerive => "enum", + Self::InputObjectDerive => "input object", Self::InterfaceAttr | Self::InterfaceDerive => "interface", Self::ObjectAttr | Self::ObjectDerive => "object", Self::ScalarAttr | Self::ScalarDerive => "scalar", Self::ScalarValueDerive => "built-in scalars", Self::UnionAttr | Self::UnionDerive => "union", - Self::DeriveInputObject => "input object", }; write!(f, "GraphQL {}", name) } } -#[allow(unused_variables)] -#[derive(Debug)] -pub enum UnsupportedAttribute { - Skip, - Interface, - Deprecation, -} - impl GraphQLScope { fn spec_link(&self) -> String { format!("{}{}", SPEC_URL, self.spec_section()) @@ -82,61 +74,6 @@ impl GraphQLScope { syn::Error::new(span, format!("{} {}", self, msg.as_ref())) } - pub fn unsupported_attribute(&self, attribute: Span, kind: UnsupportedAttribute) { - Diagnostic::spanned( - attribute, - Level::Error, - format!("attribute `{:?}` can not be used at the top level of {}", kind, self), - ) - .note("The macro is known to Juniper. However, not all valid #[graphql] attributes are available for each macro".to_string()) - .emit(); - } - - pub fn unsupported_attribute_within(&self, attribute: Span, kind: UnsupportedAttribute) { - Diagnostic::spanned( - attribute, - Level::Error, - format!("attribute `{:?}` can not be used inside of {}", kind, self), - ) - .note("The macro is known to Juniper. However, not all valid #[graphql] attributes are available for each macro".to_string()) - .emit(); - } - - pub fn not_empty(&self, container: Span) { - Diagnostic::spanned( - container, - Level::Error, - format!("{} expects at least one field", self), - ) - .note(self.spec_link()) - .emit(); - } - - pub fn duplicate<'a, T: syn::spanned::Spanned + 'a>( - &self, - duplicates: impl IntoIterator<Item = &'a Duplicate<T>>, - ) { - duplicates - .into_iter() - .for_each(|dup| { - dup.spanned[1..] - .iter() - .for_each(|spanned| { - Diagnostic::spanned( - spanned.span(), - Level::Error, - format!( - "{} does not allow fields with the same name", - self - ), - ) - .help(format!("There is at least one other field with the same name `{}`, possibly renamed via the #[graphql] attribute", dup.name)) - .note(self.spec_link()) - .emit(); - }); - }) - } - pub fn no_double_underscore(&self, field: Span) { Diagnostic::spanned( field, diff --git a/juniper_codegen/src/util/duplicate.rs b/juniper_codegen/src/util/duplicate.rs deleted file mode 100644 index b056eb71..00000000 --- a/juniper_codegen/src/util/duplicate.rs +++ /dev/null @@ -1,46 +0,0 @@ -//! - -use std::collections::HashMap; - -pub struct Duplicate<T> { - pub name: String, - pub spanned: Vec<T>, -} - -impl<T> Duplicate<T> { - pub fn find_by_key<'a, F>(items: &'a [T], name: F) -> Option<Vec<Duplicate<&'a T>>> - where - T: 'a, - F: Fn(&'a T) -> &'a str, - { - let mut mapping: HashMap<&str, Vec<&T>> = HashMap::with_capacity(items.len()); - - for item in items { - if let Some(vals) = mapping.get_mut(name(item)) { - vals.push(item); - } else { - mapping.insert(name(item), vec![item]); - } - } - - let duplicates = mapping - .into_iter() - .filter_map(|(k, v)| { - if v.len() != 1 { - Some(Duplicate { - name: k.to_string(), - spanned: v, - }) - } else { - None - } - }) - .collect::<Vec<_>>(); - - if !duplicates.is_empty() { - Some(duplicates) - } else { - None - } - } -} diff --git a/juniper_codegen/src/util/mod.rs b/juniper_codegen/src/util/mod.rs index bda4bc6b..7edc1f44 100644 --- a/juniper_codegen/src/util/mod.rs +++ b/juniper_codegen/src/util/mod.rs @@ -1,25 +1,17 @@ #![allow(clippy::single_match)] -pub mod duplicate; pub mod span_container; -use std::{collections::HashMap, convert::TryFrom, str::FromStr}; +use std::{convert::TryFrom, str::FromStr}; -use proc_macro2::{Span, TokenStream}; use proc_macro_error::abort; -use quote::{quote, quote_spanned}; use span_container::SpanContainer; use syn::{ - ext::IdentExt as _, parse::{Parse, ParseStream}, - parse_quote, - punctuated::Punctuated, spanned::Spanned, - token, Attribute, Ident, Lit, Meta, MetaList, MetaNameValue, NestedMeta, + Attribute, Lit, Meta, MetaList, MetaNameValue, NestedMeta, }; -use crate::common::parse::ParseBufferExt as _; - /// Compares a path to a one-segment string value, /// return true if equal. pub fn path_eq_single(path: &syn::Path, value: &str) -> bool { @@ -31,12 +23,6 @@ pub struct DeprecationAttr { pub reason: Option<String>, } -pub fn find_graphql_attr(attrs: &[Attribute]) -> Option<&Attribute> { - attrs - .iter() - .find(|attr| path_eq_single(&attr.path, "graphql")) -} - /// Filters given `attrs` to contain attributes only with the given `name`. pub fn filter_attrs<'a>( name: &'a str, @@ -230,20 +216,6 @@ pub(crate) fn to_upper_snake_case(s: &str) -> String { upper } -#[doc(hidden)] -pub fn is_valid_name(field_name: &str) -> bool { - let mut chars = field_name.chars(); - - match chars.next() { - // first char can't be a digit - Some(c) if c.is_ascii_alphabetic() || c == '_' => (), - // can't be an empty string or any other character - _ => return false, - }; - - chars.all(|c| c.is_ascii_alphanumeric() || c == '_') -} - /// The different possible ways to change case of fields in a struct, or variants in an enum. #[derive(Clone, Copy, Debug, PartialEq)] pub enum RenameRule { @@ -292,674 +264,26 @@ impl Parse for RenameRule { } } -#[derive(Default, Debug)] -pub struct ObjectAttributes { - pub name: Option<SpanContainer<String>>, - pub description: Option<SpanContainer<String>>, - pub context: Option<SpanContainer<syn::Type>>, - pub scalar: Option<SpanContainer<syn::Type>>, - pub interfaces: Vec<SpanContainer<syn::Type>>, - pub no_async: Option<SpanContainer<()>>, - pub is_internal: bool, - pub rename: Option<RenameRule>, -} - -impl Parse for ObjectAttributes { - fn parse(input: ParseStream<'_>) -> syn::Result<Self> { - let mut output = Self::default(); - - while !input.is_empty() { - let ident = input.parse_any_ident()?; - match ident.to_string().as_str() { - "name" => { - input.parse::<token::Eq>()?; - let val = input.parse::<syn::LitStr>()?; - output.name = Some(SpanContainer::new( - ident.span(), - Some(val.span()), - val.value(), - )); - } - "description" => { - input.parse::<token::Eq>()?; - let val = input.parse::<syn::LitStr>()?; - output.description = Some(SpanContainer::new( - ident.span(), - Some(val.span()), - val.value(), - )); - } - "context" | "Context" => { - input.parse::<token::Eq>()?; - // TODO: remove legacy support for string based Context. - let ctx = if let Ok(val) = input.parse::<syn::LitStr>() { - eprintln!("DEPRECATION WARNING: using a string literal for the Context is deprecated"); - eprintln!("Use a normal type instead - example: 'Context = MyContextType'"); - syn::parse_str::<syn::Type>(&val.value())? - } else { - input.parse::<syn::Type>()? - }; - output.context = Some(SpanContainer::new(ident.span(), Some(ctx.span()), ctx)); - } - "scalar" | "Scalar" => { - input.parse::<token::Eq>()?; - let val = input.parse::<syn::Type>()?; - output.scalar = Some(SpanContainer::new(ident.span(), Some(val.span()), val)); - } - "impl" | "implements" | "interfaces" => { - input.parse::<token::Eq>()?; - output.interfaces = input.parse_maybe_wrapped_and_punctuated::< - syn::Type, token::Bracket, token::Comma, - >()?.into_iter() - .map(|interface| { - SpanContainer::new(ident.span(), Some(interface.span()), interface) - }) - .collect(); - } - // FIXME: make this unneccessary. - "noasync" => { - output.no_async = Some(SpanContainer::new(ident.span(), None, ())); - } - "internal" => { - output.is_internal = true; - } - "rename" => { - input.parse::<token::Eq>()?; - output.rename = Some(input.parse::<RenameRule>()?); - } - _ => { - return Err(syn::Error::new(ident.span(), "unknown attribute")); - } - } - input.try_parse::<token::Comma>()?; - } - - Ok(output) - } -} - -impl ObjectAttributes { - pub fn from_attrs(attrs: &[syn::Attribute]) -> syn::Result<Self> { - let attr_opt = find_graphql_attr(attrs); - if let Some(attr) = attr_opt { - // Need to unwrap outer (), which are not present for proc macro attributes, - // but are present for regular ones. - - let mut a: Self = attr.parse_args()?; - if a.description.is_none() { - a.description = get_doc_comment(attrs); - } - Ok(a) - } else { - Ok(Self { - description: get_doc_comment(attrs), - ..Self::default() - }) - } - } -} - -#[derive(Debug)] -pub struct FieldAttributeArgument { - pub name: syn::Ident, - pub rename: Option<SpanContainer<syn::LitStr>>, - pub default: Option<syn::Expr>, - pub description: Option<syn::LitStr>, -} - -impl Parse for FieldAttributeArgument { - fn parse(input: ParseStream<'_>) -> syn::Result<Self> { - let name = input.parse::<Ident>()?.unraw(); - - let mut arg = Self { - name, - rename: None, - default: None, - description: None, - }; - - let content; - syn::parenthesized!(content in input); - while !content.is_empty() { - let name = content.parse::<syn::Ident>()?; - content.parse::<token::Eq>()?; - - match name.to_string().as_str() { - "name" => { - let val: syn::LitStr = content.parse()?; - arg.rename = Some(SpanContainer::new(name.span(), Some(val.span()), val)); - } - "description" => { - arg.description = Some(content.parse()?); - } - "default" => { - arg.default = Some(content.parse()?); - } - _ => return Err(syn::Error::new(name.span(), "unknown attribute")), - } - - // Discard trailing comma. - content.parse::<token::Comma>().ok(); - } - - Ok(arg) - } -} - -#[derive(PartialEq, Eq, Clone, Copy, Debug)] -pub enum FieldAttributeParseMode { - Object, -} - -enum FieldAttribute { - Name(SpanContainer<syn::LitStr>), - Description(SpanContainer<syn::LitStr>), - Deprecation(SpanContainer<DeprecationAttr>), - Skip(SpanContainer<syn::Ident>), - Arguments(HashMap<String, FieldAttributeArgument>), - Default(Box<SpanContainer<Option<syn::Expr>>>), -} - -impl Parse for FieldAttribute { - fn parse(input: ParseStream<'_>) -> syn::Result<Self> { - let ident = input.parse::<syn::Ident>()?; - - match ident.to_string().as_str() { - "name" => { - input.parse::<token::Eq>()?; - let lit = input.parse::<syn::LitStr>()?; - let raw = lit.value(); - if !is_valid_name(&raw) { - Err(syn::Error::new(lit.span(), "name consists of not allowed characters. (must match /^[_a-zA-Z][_a-zA-Z0-9]*$/)")) - } else { - Ok(FieldAttribute::Name(SpanContainer::new( - ident.span(), - Some(lit.span()), - lit, - ))) - } - } - "description" => { - input.parse::<token::Eq>()?; - let lit = input.parse::<syn::LitStr>()?; - Ok(FieldAttribute::Description(SpanContainer::new( - ident.span(), - Some(lit.span()), - lit, - ))) - } - "deprecated" | "deprecation" => { - let reason = if input.peek(token::Eq) { - input.parse::<token::Eq>()?; - Some(input.parse::<syn::LitStr>()?) - } else { - None - }; - Ok(FieldAttribute::Deprecation(SpanContainer::new( - ident.span(), - reason.as_ref().map(|val| val.span()), - DeprecationAttr { - reason: reason.map(|val| val.value()), - }, - ))) - } - "skip" => Ok(FieldAttribute::Skip(SpanContainer::new( - ident.span(), - None, - ident, - ))), - "arguments" => { - let arg_content; - syn::parenthesized!(arg_content in input); - let args = Punctuated::<FieldAttributeArgument, token::Comma>::parse_terminated( - &arg_content, - )?; - let map = args - .into_iter() - .map(|arg| (arg.name.to_string(), arg)) - .collect(); - Ok(FieldAttribute::Arguments(map)) - } - "default" => { - let default_expr = if input.peek(token::Eq) { - input.parse::<token::Eq>()?; - let lit = input.parse::<syn::LitStr>()?; - let default_expr = lit.parse::<syn::Expr>()?; - SpanContainer::new(ident.span(), Some(lit.span()), Some(default_expr)) - } else { - SpanContainer::new(ident.span(), None, None) - }; - - Ok(FieldAttribute::Default(Box::new(default_expr))) - } - _ => Err(syn::Error::new(ident.span(), "unknown attribute")), - } - } -} - -#[derive(Default)] -pub struct FieldAttributes { - pub name: Option<SpanContainer<String>>, - pub description: Option<SpanContainer<String>>, - pub deprecation: Option<SpanContainer<DeprecationAttr>>, - /// Only relevant for GraphQLObject derive. - pub skip: Option<SpanContainer<syn::Ident>>, - /// Only relevant for object macro. - pub arguments: HashMap<String, FieldAttributeArgument>, - /// Only relevant for object input objects. - pub default: Option<SpanContainer<Option<syn::Expr>>>, -} - -impl Parse for FieldAttributes { - fn parse(input: ParseStream<'_>) -> syn::Result<Self> { - let items = Punctuated::<FieldAttribute, token::Comma>::parse_terminated(input)?; - - let mut output = Self::default(); - - for item in items { - match item { - FieldAttribute::Name(name) => { - output.name = Some(name.map(|val| val.value())); - } - FieldAttribute::Description(name) => { - output.description = Some(name.map(|val| val.value())); - } - FieldAttribute::Deprecation(attr) => { - output.deprecation = Some(attr); - } - FieldAttribute::Skip(ident) => { - output.skip = Some(ident); - } - FieldAttribute::Arguments(args) => { - output.arguments = args; - } - FieldAttribute::Default(expr) => { - output.default = Some(*expr); - } - } - } - - if !input.is_empty() { - Err(input.error("Unexpected input")) - } else { - Ok(output) - } - } -} - -impl FieldAttributes { - pub fn from_attrs( - attrs: &[syn::Attribute], - _mode: FieldAttributeParseMode, - ) -> syn::Result<Self> { - let doc_comment = get_doc_comment(attrs); - let deprecation = get_deprecated(attrs); - - let attr_opt = attrs.iter().find(|attr| attr.path.is_ident("graphql")); - - let mut output = match attr_opt { - Some(attr) => attr.parse_args()?, - None => Self::default(), - }; - - // Check for regular doc comment. - if output.description.is_none() { - output.description = doc_comment; - } - if output.deprecation.is_none() { - output.deprecation = deprecation; - } - - Ok(output) - } -} - -#[derive(Debug)] -pub struct GraphQLTypeDefinitionFieldArg { - pub name: String, - pub description: Option<String>, - pub default: Option<syn::Expr>, - pub _type: Box<syn::Type>, -} - -#[derive(Debug)] -pub struct GraphQLTypeDefinitionField { - pub name: String, - pub _type: syn::Type, - pub description: Option<String>, - pub deprecation: Option<DeprecationAttr>, - pub args: Vec<GraphQLTypeDefinitionFieldArg>, - pub resolver_code: TokenStream, - pub is_type_inferred: bool, - pub is_async: bool, - pub default: Option<TokenStream>, - pub span: Span, -} - -impl syn::spanned::Spanned for GraphQLTypeDefinitionField { - fn span(&self) -> Span { - self.span - } -} - -impl<'a> syn::spanned::Spanned for &'a GraphQLTypeDefinitionField { - fn span(&self) -> Span { - self.span - } -} - -/// Definition of a graphql type based on information extracted -/// by various macros. -/// The definition can be rendered to Rust code. -#[derive(Debug)] -pub struct GraphQLTypeDefiniton { - pub name: String, - pub _type: syn::Type, - pub context: Option<syn::Type>, - pub scalar: Option<syn::Type>, - pub description: Option<String>, - pub fields: Vec<GraphQLTypeDefinitionField>, - pub generics: syn::Generics, - pub interfaces: Vec<syn::Type>, - // Due to syn parsing differences, - // when parsing an impl the type generics are included in the type - // directly, but in syn::DeriveInput, the type generics are - // in the generics field. - // This flag signifies if the type generics need to be - // included manually. - pub include_type_generics: bool, - // This flag indicates if the generated code should always be - // generic over the ScalarValue. - // If false, the scalar is only generic if a generic parameter - // is specified manually. - pub generic_scalar: bool, - // FIXME: make this redundant. - pub no_async: bool, -} - -impl GraphQLTypeDefiniton { - #[allow(unused)] - fn has_async_field(&self) -> bool { - self.fields.iter().any(|field| field.is_async) - } - - pub fn into_input_object_tokens(self) -> TokenStream { - let name = &self.name; - let ty = &self._type; - let context = self - .context - .as_ref() - .map(|ctx| quote!( #ctx )) - .unwrap_or_else(|| quote!(())); - - let scalar = self - .scalar - .as_ref() - .map(|s| quote!( #s )) - .unwrap_or_else(|| { - if self.generic_scalar { - // If generic_scalar is true, we always insert a generic scalar. - // See more comments below. - quote!(__S) - } else { - quote!(::juniper::DefaultScalarValue) - } - }); - - let meta_fields = self - .fields - .iter() - .map(|field| { - // HACK: use a different interface for the GraphQLField? - let field_ty = &field._type; - let field_name = &field.name; - - let description = match field.description.as_ref() { - Some(description) => quote!( .description(#description) ), - None => quote!(), - }; - - let deprecation = match field.deprecation.as_ref() { - Some(deprecation) => { - if let Some(reason) = deprecation.reason.as_ref() { - quote!( .deprecated(Some(#reason)) ) - } else { - quote!( .deprecated(None) ) - } - } - None => quote!(), - }; - - let create_meta_field = match field.default { - Some(ref def) => { - quote! { - registry.arg_with_default::<#field_ty>( #field_name, &#def, &()) - } - } - None => { - quote! { - registry.arg::<#field_ty>(#field_name, &()) - } - } - }; - - quote!( - { - #create_meta_field - #description - #deprecation - }, - ) - }) - .collect::<Vec<_>>(); - - let from_inputs = self - .fields - .iter() - .map(|field| { - let field_ident = &field.resolver_code; - let field_name = &field.name; - - // Build from_input clause. - let from_input_default = match field.default { - Some(ref def) => { - quote! { - Some(&&::juniper::InputValue::Null) | None if true => #def, - } - } - None => quote! {}, - }; - - quote!( - #field_ident: { - match obj.get(#field_name) { - #from_input_default - Some(ref v) => { - ::juniper::FromInputValue::<#scalar>::from_input_value(v) - .map_err(::juniper::IntoFieldError::into_field_error)? - }, - None => { - ::juniper::FromInputValue::<#scalar>::from_implicit_null() - .map_err(::juniper::IntoFieldError::into_field_error)? - }, - } - }, - ) - }) - .collect::<Vec<_>>(); - - let to_inputs = self - .fields - .iter() - .map(|field| { - let field_name = &field.name; - let field_ident = &field.resolver_code; - // Build to_input clause. - quote!( - (#field_name, self.#field_ident.to_input_value()), - ) - }) - .collect::<Vec<_>>(); - - let description = self - .description - .as_ref() - .map(|description| quote!( .description(#description) )); - - // Preserve the original type_generics before modification, - // since alteration makes them invalid if self.generic_scalar - // is specified. - let (_, type_generics, _) = self.generics.split_for_impl(); - - let mut generics = self.generics.clone(); - - if self.scalar.is_none() && self.generic_scalar { - // No custom scalar specified, but always generic specified. - // Therefore we inject the generic scalar. - - generics.params.push(parse_quote!(__S)); - - let where_clause = generics.where_clause.get_or_insert(parse_quote!(where)); - // Insert ScalarValue constraint. - where_clause - .predicates - .push(parse_quote!(__S: ::juniper::ScalarValue)); - } - - let type_generics_tokens = if self.include_type_generics { - Some(type_generics) - } else { - None - }; - - let (impl_generics, _, where_clause) = generics.split_for_impl(); - - let mut where_async = where_clause.cloned().unwrap_or_else(|| parse_quote!(where)); - - where_async - .predicates - .push(parse_quote!( #scalar: Send + Sync )); - where_async.predicates.push(parse_quote!(Self: Sync)); - - let async_type = quote!( - impl#impl_generics ::juniper::GraphQLValueAsync<#scalar> for #ty #type_generics_tokens - #where_async - {} - ); - - let marks = self.fields.iter().map(|field| { - let field_ty = &field._type; - quote_spanned! { field_ty.span() => - <#field_ty as ::juniper::marker::IsInputType<#scalar>>::mark(); - } - }); - - let mut body = quote!( - impl#impl_generics ::juniper::marker::IsInputType<#scalar> for #ty #type_generics_tokens - #where_clause { - fn mark() { - #( #marks )* - } - } - - impl#impl_generics ::juniper::GraphQLType<#scalar> for #ty #type_generics_tokens - #where_clause - { - fn name(_: &()) -> Option<&'static str> { - Some(#name) - } - - fn meta<'r>( - _: &(), - registry: &mut ::juniper::Registry<'r, #scalar> - ) -> ::juniper::meta::MetaType<'r, #scalar> - where #scalar: 'r - { - let fields = &[ - #( #meta_fields )* - ]; - registry.build_input_object_type::<#ty>(&(), fields) - #description - .into_meta() - } - } - - impl#impl_generics ::juniper::GraphQLValue<#scalar> for #ty #type_generics_tokens - #where_clause - { - type Context = #context; - type TypeInfo = (); - - fn type_name<'__i>(&self, info: &'__i Self::TypeInfo) -> Option<&'__i str> { - <Self as ::juniper::GraphQLType<#scalar>>::name(info) - } - } - - impl#impl_generics ::juniper::FromInputValue<#scalar> for #ty #type_generics_tokens - #where_clause - { - type Error = ::juniper::FieldError<#scalar>; - - fn from_input_value( - value: &::juniper::InputValue<#scalar> - ) -> Result<Self, Self::Error> { - let obj = value - .to_object_value() - .ok_or_else(|| ::juniper::FieldError::<#scalar>::from( - format!("Expected input object, found: {}", value)) - )?; - Ok(#ty { - #( #from_inputs )* - }) - } - } - - impl#impl_generics ::juniper::ToInputValue<#scalar> for #ty #type_generics_tokens - #where_clause - { - fn to_input_value(&self) -> ::juniper::InputValue<#scalar> { - ::juniper::InputValue::object(vec![ - #( #to_inputs )* - ].into_iter().collect()) - } - } - - impl#impl_generics ::juniper::macros::reflect::BaseType<#scalar> - for #ty #type_generics_tokens - #where_clause - { - const NAME: ::juniper::macros::reflect::Type = #name; - } - - impl#impl_generics ::juniper::macros::reflect::BaseSubTypes<#scalar> - for #ty #type_generics_tokens - #where_clause - { - const NAMES: ::juniper::macros::reflect::Types = - &[<Self as ::juniper::macros::reflect::BaseType<#scalar>>::NAME]; - } - - impl#impl_generics ::juniper::macros::reflect::WrappedType<#scalar> - for #ty #type_generics_tokens - #where_clause - { - const VALUE: ::juniper::macros::reflect::WrappedValue = 1; - } - ); - - if !self.no_async { - body.extend(async_type); - } - - body - } -} - #[cfg(test)] mod test { - use super::*; + use proc_macro2::Span; use syn::{Ident, LitStr}; + use super::*; + + fn is_valid_name(field_name: &str) -> bool { + let mut chars = field_name.chars(); + + match chars.next() { + // first char can't be a digit + Some(c) if c.is_ascii_alphabetic() || c == '_' => (), + // can't be an empty string or any other character + _ => return false, + }; + + chars.all(|c| c.is_ascii_alphanumeric() || c == '_') + } + fn strs_to_strings(source: Vec<&str>) -> Vec<String> { source .iter() diff --git a/tests/codegen/fail/input-object/derive_incompatible_field_type.rs b/tests/codegen/fail/input-object/derive_incompatible_field_type.rs new file mode 100644 index 00000000..b3e26444 --- /dev/null +++ b/tests/codegen/fail/input-object/derive_incompatible_field_type.rs @@ -0,0 +1,13 @@ +use juniper::{GraphQLInputObject, GraphQLObject}; + +#[derive(GraphQLObject)] +struct ObjectA { + test: String, +} + +#[derive(GraphQLInputObject)] +struct Object { + field: ObjectA, +} + +fn main() {} diff --git a/tests/codegen/fail/input-object/derive_incompatible_object.stderr b/tests/codegen/fail/input-object/derive_incompatible_field_type.stderr similarity index 50% rename from tests/codegen/fail/input-object/derive_incompatible_object.stderr rename to tests/codegen/fail/input-object/derive_incompatible_field_type.stderr index 6767eead..5ddabb55 100644 --- a/tests/codegen/fail/input-object/derive_incompatible_object.stderr +++ b/tests/codegen/fail/input-object/derive_incompatible_field_type.stderr @@ -1,8 +1,8 @@ error[E0277]: the trait bound `ObjectA: IsInputType<__S>` is not satisfied - --> fail/input-object/derive_incompatible_object.rs:8:12 + --> fail/input-object/derive_incompatible_field_type.rs:8:10 | -8 | field: ObjectA, - | ^^^^^^^ the trait `IsInputType<__S>` is not implemented for `ObjectA` +8 | #[derive(GraphQLInputObject)] + | ^^^^^^^^^^^^^^^^^^ the trait `IsInputType<__S>` is not implemented for `ObjectA` | = help: the following other types implement trait `IsInputType<S>`: <&T as IsInputType<S>> @@ -14,12 +14,13 @@ error[E0277]: the trait bound `ObjectA: IsInputType<__S>` is not satisfied <Vec<T> as IsInputType<S>> <[T; N] as IsInputType<S>> and 13 others + = note: this error originates in the derive macro `GraphQLInputObject` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `ObjectA: FromInputValue<__S>` is not satisfied - --> fail/input-object/derive_incompatible_object.rs:6:10 + --> fail/input-object/derive_incompatible_field_type.rs:8:10 | -6 | #[derive(juniper::GraphQLInputObject)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `FromInputValue<__S>` is not implemented for `ObjectA` +8 | #[derive(GraphQLInputObject)] + | ^^^^^^^^^^^^^^^^^^ the trait `FromInputValue<__S>` is not implemented for `ObjectA` | = help: the following other types implement trait `FromInputValue<S>`: <Arc<T> as FromInputValue<S>> @@ -36,13 +37,13 @@ note: required by a bound in `Registry::<'r, S>::arg` | | T: GraphQLType<S> + FromInputValue<S>, | ^^^^^^^^^^^^^^^^^ required by this bound in `Registry::<'r, S>::arg` - = note: this error originates in the derive macro `juniper::GraphQLInputObject` (in Nightly builds, run with -Z macro-backtrace for more info) + = note: this error originates in the derive macro `GraphQLInputObject` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `ObjectA: FromInputValue<__S>` is not satisfied - --> fail/input-object/derive_incompatible_object.rs:6:10 + --> fail/input-object/derive_incompatible_field_type.rs:8:10 | -6 | #[derive(juniper::GraphQLInputObject)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `FromInputValue<__S>` is not implemented for `ObjectA` +8 | #[derive(GraphQLInputObject)] + | ^^^^^^^^^^^^^^^^^^ the trait `FromInputValue<__S>` is not implemented for `ObjectA` | = help: the following other types implement trait `FromInputValue<S>`: <Arc<T> as FromInputValue<S>> @@ -54,18 +55,22 @@ error[E0277]: the trait bound `ObjectA: FromInputValue<__S>` is not satisfied <[T; N] as FromInputValue<S>> <bool as FromInputValue<__S>> and 10 others - = note: this error originates in the derive macro `juniper::GraphQLInputObject` (in Nightly builds, run with -Z macro-backtrace for more info) + = note: this error originates in the derive macro `GraphQLInputObject` (in Nightly builds, run with -Z macro-backtrace for more info) -error[E0599]: no method named `to_input_value` found for struct `ObjectA` in the current scope - --> fail/input-object/derive_incompatible_object.rs:6:10 +error[E0277]: the trait bound `ObjectA: ToInputValue<_>` is not satisfied + --> fail/input-object/derive_incompatible_field_type.rs:8:10 | -2 | struct ObjectA { - | ------- method `to_input_value` not found for this struct -... -6 | #[derive(juniper::GraphQLInputObject)] - | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ method not found in `ObjectA` +8 | #[derive(GraphQLInputObject)] + | ^^^^^^^^^^^^^^^^^^ the trait `ToInputValue<_>` is not implemented for `ObjectA` | - = help: items from traits can only be used if the trait is implemented and in scope - = note: the following trait defines an item `to_input_value`, perhaps you need to implement it: - candidate #1: `ToInputValue` - = note: this error originates in the derive macro `juniper::GraphQLInputObject` (in Nightly builds, run with -Z macro-backtrace for more info) + = help: the following other types implement trait `ToInputValue<S>`: + <&'a T as ToInputValue<S>> + <&'a [T] as ToInputValue<S>> + <&'a str as ToInputValue<S>> + <Arc<T> as ToInputValue<S>> + <Box<T> as ToInputValue<S>> + <ID as ToInputValue<__S>> + <Object as ToInputValue<__S>> + <TypeKind as ToInputValue<__S>> + and 14 others + = note: this error originates in the derive macro `GraphQLInputObject` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/codegen/fail/input-object/derive_incompatible_object.rs b/tests/codegen/fail/input-object/derive_incompatible_object.rs deleted file mode 100644 index 98cdb65b..00000000 --- a/tests/codegen/fail/input-object/derive_incompatible_object.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[derive(juniper::GraphQLObject)] -struct ObjectA { - test: String, -} - -#[derive(juniper::GraphQLInputObject)] -struct Object { - field: ObjectA, -} - -fn main() {} diff --git a/tests/codegen/fail/input-object/derive_no_fields.rs b/tests/codegen/fail/input-object/derive_no_fields.rs index eedbe26e..2861298f 100644 --- a/tests/codegen/fail/input-object/derive_no_fields.rs +++ b/tests/codegen/fail/input-object/derive_no_fields.rs @@ -1,4 +1,6 @@ -#[derive(juniper::GraphQLInputObject)] +use juniper::GraphQLInputObject; + +#[derive(GraphQLInputObject)] struct Object {} fn main() {} diff --git a/tests/codegen/fail/input-object/derive_no_fields.stderr b/tests/codegen/fail/input-object/derive_no_fields.stderr index 9362b428..4ab046c5 100644 --- a/tests/codegen/fail/input-object/derive_no_fields.stderr +++ b/tests/codegen/fail/input-object/derive_no_fields.stderr @@ -1,7 +1,5 @@ -error: GraphQL input object expects at least one field - --> fail/input-object/derive_no_fields.rs:2:1 +error: GraphQL input object expected at least 1 non-ignored field + --> fail/input-object/derive_no_fields.rs:4:15 | -2 | struct Object {} - | ^^^^^^^^^^^^^^^^ - | - = note: https://spec.graphql.org/June2018/#sec-Input-Objects +4 | struct Object {} + | ^^ diff --git a/tests/codegen/fail/input-object/derive_no_underscore.rs b/tests/codegen/fail/input-object/derive_no_underscore.rs index 71ab5b9d..46b86a3c 100644 --- a/tests/codegen/fail/input-object/derive_no_underscore.rs +++ b/tests/codegen/fail/input-object/derive_no_underscore.rs @@ -1,4 +1,6 @@ -#[derive(juniper::GraphQLInputObject)] +use juniper::GraphQLInputObject; + +#[derive(GraphQLInputObject)] struct Object { #[graphql(name = "__test")] test: String, diff --git a/tests/codegen/fail/input-object/derive_no_underscore.stderr b/tests/codegen/fail/input-object/derive_no_underscore.stderr index 86a6a9b2..b30b8aa9 100644 --- a/tests/codegen/fail/input-object/derive_no_underscore.stderr +++ b/tests/codegen/fail/input-object/derive_no_underscore.stderr @@ -1,7 +1,8 @@ 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. - --> fail/input-object/derive_no_underscore.rs:3:15 + --> fail/input-object/derive_no_underscore.rs:5:5 | -3 | #[graphql(name = "__test")] - | ^^^^ +5 | / #[graphql(name = "__test")] +6 | | test: String, + | |________________^ | = note: https://spec.graphql.org/June2018/#sec-Schema diff --git a/tests/codegen/fail/input-object/derive_unique_name.rs b/tests/codegen/fail/input-object/derive_unique_name.rs index ecaa8631..a0d9a5b2 100644 --- a/tests/codegen/fail/input-object/derive_unique_name.rs +++ b/tests/codegen/fail/input-object/derive_unique_name.rs @@ -1,4 +1,6 @@ -#[derive(juniper::GraphQLInputObject)] +use juniper::GraphQLInputObject; + +#[derive(GraphQLInputObject)] struct Object { test: String, #[graphql(name = "test")] diff --git a/tests/codegen/fail/input-object/derive_unique_name.stderr b/tests/codegen/fail/input-object/derive_unique_name.stderr index 066fdbe6..20a72cfa 100644 --- a/tests/codegen/fail/input-object/derive_unique_name.stderr +++ b/tests/codegen/fail/input-object/derive_unique_name.stderr @@ -1,9 +1,10 @@ -error: GraphQL input object does not allow fields with the same name - --> fail/input-object/derive_unique_name.rs:4:5 +error: GraphQL input object expected all fields to have unique names + --> fail/input-object/derive_unique_name.rs:4:15 | -4 | / #[graphql(name = "test")] -5 | | test2: String, - | |_________________^ - | - = help: There is at least one other field with the same name `test`, possibly renamed via the #[graphql] attribute - = note: https://spec.graphql.org/June2018/#sec-Input-Objects +4 | struct Object { + | _______________^ +5 | | test: String, +6 | | #[graphql(name = "test")] +7 | | test2: String, +8 | | } + | |_^ diff --git a/tests/integration/src/codegen/derive_input_object.rs b/tests/integration/src/codegen/derive_input_object.rs deleted file mode 100644 index f85b042e..00000000 --- a/tests/integration/src/codegen/derive_input_object.rs +++ /dev/null @@ -1,191 +0,0 @@ -use fnv::FnvHashMap; -use juniper::{ - graphql_input_value, marker, DefaultScalarValue, FieldError, FromInputValue, - GraphQLInputObject, GraphQLType, GraphQLValue, InputValue, Registry, ToInputValue, -}; - -#[derive(GraphQLInputObject, Debug, PartialEq)] -#[graphql( - name = "MyInput", - description = "input descr", - scalar = DefaultScalarValue -)] -struct Input { - regular_field: String, - #[graphql(name = "haha", default = "33", description = "haha descr")] - c: i32, - - #[graphql(default)] - other: Option<bool>, -} - -#[derive(GraphQLInputObject, Debug, PartialEq)] -#[graphql(rename = "none")] -struct NoRenameInput { - regular_field: String, -} - -/// Object comment. -#[derive(GraphQLInputObject, Debug, PartialEq)] -struct DocComment { - /// Field comment. - regular_field: bool, -} - -/// Doc 1.\ -/// Doc 2. -/// -/// Doc 4. -#[derive(GraphQLInputObject, Debug, PartialEq)] -struct MultiDocComment { - /// Field 1. - /// Field 2. - regular_field: bool, -} - -/// This is not used as the description. -#[derive(GraphQLInputObject, Debug, PartialEq)] -#[graphql(description = "obj override")] -struct OverrideDocComment { - /// This is not used as the description. - #[graphql(description = "field override")] - regular_field: bool, -} - -#[derive(Debug, PartialEq)] -struct Fake; - -impl<'a> marker::IsInputType<DefaultScalarValue> for &'a Fake {} - -impl<'a> FromInputValue for &'a Fake { - type Error = FieldError; - - fn from_input_value(_v: &InputValue) -> Result<&'a Fake, Self::Error> { - Err("This is fake".into()) - } -} - -impl<'a> ToInputValue for &'a Fake { - fn to_input_value(&self) -> InputValue { - graphql_input_value!("this is fake") - } -} - -impl<'a> GraphQLType<DefaultScalarValue> for &'a Fake { - fn name(_: &()) -> Option<&'static str> { - None - } - fn meta<'r>(_: &(), registry: &mut Registry<'r>) -> juniper::meta::MetaType<'r> - where - DefaultScalarValue: 'r, - { - let meta = registry.build_enum_type::<&'a Fake>( - &(), - &[juniper::meta::EnumValue { - name: "fake".to_string(), - description: None, - deprecation_status: juniper::meta::DeprecationStatus::Current, - }], - ); - meta.into_meta() - } -} - -impl<'a> GraphQLValue<DefaultScalarValue> for &'a Fake { - type Context = (); - type TypeInfo = (); - - fn type_name<'i>(&self, info: &'i Self::TypeInfo) -> Option<&'i str> { - <Self as GraphQLType>::name(info) - } -} - -#[derive(GraphQLInputObject, Debug, PartialEq)] -#[graphql(scalar = DefaultScalarValue)] -struct WithLifetime<'a> { - regular_field: &'a Fake, -} - -#[test] -fn test_derived_input_object() { - assert_eq!( - <Input as GraphQLType<DefaultScalarValue>>::name(&()), - Some("MyInput") - ); - - // Validate meta info. - let mut registry: Registry<'_> = Registry::new(FnvHashMap::default()); - let meta = Input::meta(&(), &mut registry); - assert_eq!(meta.name(), Some("MyInput")); - assert_eq!(meta.description(), Some("input descr")); - - // Test default value injection. - - let input_no_defaults = graphql_input_value!({ - "regularField": "a", - }); - let output_no_defaults = Input::from_input_value(&input_no_defaults).unwrap(); - assert_eq!( - output_no_defaults, - Input { - regular_field: "a".into(), - c: 33, - other: None, - }, - ); - - // Test with all values supplied. - - let input: InputValue = ::serde_json::from_value(serde_json::json!({ - "regularField": "a", - "haha": 55, - "other": true, - })) - .unwrap(); - - let output: Input = FromInputValue::from_input_value(&input).unwrap(); - assert_eq!( - output, - Input { - regular_field: "a".into(), - c: 55, - other: Some(true), - }, - ); - - // Test disable renaming - - let input: InputValue = ::serde_json::from_value(serde_json::json!({ - "regular_field": "hello", - })) - .unwrap(); - - let output: NoRenameInput = FromInputValue::from_input_value(&input).unwrap(); - assert_eq!( - output, - NoRenameInput { - regular_field: "hello".into(), - }, - ); -} - -#[test] -fn test_doc_comment() { - let mut registry: Registry<'_> = Registry::new(FnvHashMap::default()); - let meta = DocComment::meta(&(), &mut registry); - assert_eq!(meta.description(), Some("Object comment.")); -} - -#[test] -fn test_multi_doc_comment() { - let mut registry: Registry<'_> = Registry::new(FnvHashMap::default()); - let meta = MultiDocComment::meta(&(), &mut registry); - assert_eq!(meta.description(), Some("Doc 1. Doc 2.\n\nDoc 4.")); -} - -#[test] -fn test_doc_comment_override() { - let mut registry: Registry<'_> = Registry::new(FnvHashMap::default()); - let meta = OverrideDocComment::meta(&(), &mut registry); - assert_eq!(meta.description(), Some("obj override")); -} diff --git a/tests/integration/src/codegen/input_object_derive.rs b/tests/integration/src/codegen/input_object_derive.rs new file mode 100644 index 00000000..085815b7 --- /dev/null +++ b/tests/integration/src/codegen/input_object_derive.rs @@ -0,0 +1,708 @@ +//! Tests for `#[derive(GraphQLInputObject)]` macro. + +use juniper::{execute, graphql_object, graphql_value, graphql_vars, GraphQLInputObject}; + +use crate::util::schema; + +mod trivial { + use super::*; + + #[derive(GraphQLInputObject)] + struct Point2D { + x: f64, + y: f64, + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn x(point: Point2D) -> f64 { + point.x + } + } + + #[tokio::test] + async fn resolves() { + const DOC: &str = r#"{ + x(point: { x: 10, y: 20 }) + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"x": 10.0}), vec![])), + ); + } + + #[tokio::test] + async fn is_graphql_input_object() { + const DOC: &str = r#"{ + __type(name: "Point2D") { + kind + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT"}}), vec![])), + ); + } + + #[tokio::test] + async fn uses_type_name() { + const DOC: &str = r#"{ + __type(name: "Point2D") { + name + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"name": "Point2D"}}), vec![])), + ); + } + + #[tokio::test] + async fn has_no_description() { + const DOC: &str = r#"{ + __type(name: "Point2D") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"description": null}}), vec![])), + ); + } + + #[tokio::test] + async fn has_input_fields() { + const DOC: &str = r#"{ + __type(name: "Point2D") { + inputFields { + name + description + type { + ofType { + name + } + } + defaultValue + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"inputFields": [ + { + "name": "x", + "description": null, + "type": {"ofType": {"name": "Float"}}, + "defaultValue": null, + }, + { + "name": "y", + "description": null, + "type": {"ofType": {"name": "Float"}}, + "defaultValue": null, + }, + ]}}), + vec![], + )), + ); + } +} + +mod default_value { + use super::*; + + #[derive(GraphQLInputObject)] + struct Point2D { + #[graphql(default = 10.0)] + x: f64, + #[graphql(default = 10.0)] + y: f64, + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn x(point: Point2D) -> f64 { + point.x + } + } + + #[tokio::test] + async fn resolves() { + const DOC: &str = r#"{ + x(point: { y: 20 }) + x2: x(point: { x: 20 }) + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"x": 10.0, "x2": 20.0}), vec![])), + ); + } + + #[tokio::test] + async fn is_graphql_input_object() { + const DOC: &str = r#"{ + __type(name: "Point2D") { + kind + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT"}}), vec![])), + ); + } + + #[tokio::test] + async fn has_input_fields() { + const DOC: &str = r#"{ + __type(name: "Point2D") { + inputFields { + name + description + type { + ofType { + name + } + } + defaultValue + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"inputFields": [ + { + "name": "x", + "description": null, + "type": {"ofType": null}, + "defaultValue": "10", + }, + { + "name": "y", + "description": null, + "type": {"ofType": null}, + "defaultValue": "10", + }, + ]}}), + vec![], + )), + ); + } +} + +mod ignored_field { + use super::*; + + #[derive(Clone, Copy, Debug, Eq, PartialEq)] + enum System { + Cartesian, + } + + #[derive(GraphQLInputObject)] + struct Point2D { + x: f64, + y: f64, + #[graphql(ignore)] + shift: f64, + #[graphql(skip, default = System::Cartesian)] + system: System, + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn x(point: Point2D) -> f64 { + assert_eq!(point.shift, f64::default()); + assert_eq!(point.system, System::Cartesian); + point.x + } + } + + #[tokio::test] + async fn resolves() { + const DOC: &str = r#"{ + x(point: { x: 10, y: 20 }) + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"x": 10.0}), vec![])), + ); + } + + #[tokio::test] + async fn is_graphql_input_object() { + const DOC: &str = r#"{ + __type(name: "Point2D") { + kind + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT"}}), vec![])), + ); + } + + #[tokio::test] + async fn uses_type_name() { + const DOC: &str = r#"{ + __type(name: "Point2D") { + name + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"name": "Point2D"}}), vec![])), + ); + } + + #[tokio::test] + async fn has_no_description() { + const DOC: &str = r#"{ + __type(name: "Point2D") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"description": null}}), vec![])), + ); + } + + #[tokio::test] + async fn has_input_fields() { + const DOC: &str = r#"{ + __type(name: "Point2D") { + inputFields { + name + description + type { + ofType { + name + } + } + defaultValue + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"inputFields": [ + { + "name": "x", + "description": null, + "type": {"ofType": {"name": "Float"}}, + "defaultValue": null, + }, + { + "name": "y", + "description": null, + "type": {"ofType": {"name": "Float"}}, + "defaultValue": null, + }, + ]}}), + vec![], + )), + ); + } +} + +mod description_from_doc_comment { + use super::*; + + /// Point in a Cartesian system. + #[derive(GraphQLInputObject)] + struct Point2D { + /// Abscissa value. + x: f64, + + /// Ordinate value. + y_coord: f64, + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn x(point: Point2D) -> f64 { + point.x + } + } + + #[tokio::test] + async fn resolves() { + const DOC: &str = r#"{ + x(point: { x: 10, yCoord: 20 }) + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"x": 10.0}), vec![])), + ); + } + + #[tokio::test] + async fn is_graphql_input_object() { + const DOC: &str = r#"{ + __type(name: "Point2D") { + kind + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT"}}), vec![])), + ); + } + + #[tokio::test] + async fn uses_type_name() { + const DOC: &str = r#"{ + __type(name: "Point2D") { + name + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"name": "Point2D"}}), vec![])), + ); + } + + #[tokio::test] + async fn has_description() { + const DOC: &str = r#"{ + __type(name: "Point2D") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": { + "description": "Point in a Cartesian system.", + }}), + vec![] + )), + ); + } + + #[tokio::test] + async fn has_input_fields() { + const DOC: &str = r#"{ + __type(name: "Point2D") { + inputFields { + name + description + type { + ofType { + name + } + } + defaultValue + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"inputFields": [ + { + "name": "x", + "description": "Abscissa value.", + "type": {"ofType": {"name": "Float"}}, + "defaultValue": null, + }, + { + "name": "yCoord", + "description": "Ordinate value.", + "type": {"ofType": {"name": "Float"}}, + "defaultValue": null, + }, + ]}}), + vec![], + )), + ); + } +} + +mod description_from_graphql_attr { + use super::*; + + /// Ignored doc. + #[derive(GraphQLInputObject)] + #[graphql(name = "Point", desc = "Point in a Cartesian system.")] + struct Point2D { + /// Ignored doc. + #[graphql(name = "x", description = "Abscissa value.")] + x_coord: f64, + + /// Ordinate value. + y: f64, + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn x(point: Point2D) -> f64 { + point.x_coord + } + } + + #[tokio::test] + async fn resolves() { + const DOC: &str = r#"{ + x(point: { x: 10, y: 20 }) + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"x": 10.0}), vec![])), + ); + } + + #[tokio::test] + async fn is_graphql_input_object() { + const DOC: &str = r#"{ + __type(name: "Point") { + kind + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT"}}), vec![])), + ); + } + + #[tokio::test] + async fn uses_type_name() { + const DOC: &str = r#"{ + __type(name: "Point") { + name + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"name": "Point"}}), vec![])), + ); + } + + #[tokio::test] + async fn has_description() { + const DOC: &str = r#"{ + __type(name: "Point") { + description + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": { + "description": "Point in a Cartesian system.", + }}), + vec![] + )), + ); + } + + #[tokio::test] + async fn has_input_fields() { + const DOC: &str = r#"{ + __type(name: "Point") { + inputFields { + name + description + type { + ofType { + name + } + } + defaultValue + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"inputFields": [ + { + "name": "x", + "description": "Abscissa value.", + "type": {"ofType": {"name": "Float"}}, + "defaultValue": null, + }, + { + "name": "y", + "description": "Ordinate value.", + "type": {"ofType": {"name": "Float"}}, + "defaultValue": null, + }, + ]}}), + vec![], + )), + ); + } +} + +mod renamed_all_fields { + use super::*; + + #[derive(GraphQLInputObject)] + #[graphql(rename_all = "none")] + struct Point2D { + x_coord: f64, + y: f64, + } + + struct QueryRoot; + + #[graphql_object] + impl QueryRoot { + fn x(point: Point2D) -> f64 { + point.x_coord + } + } + + #[tokio::test] + async fn resolves() { + const DOC: &str = r#"{ + x(point: { x_coord: 10, y: 20 }) + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"x": 10.0}), vec![])), + ); + } + + #[tokio::test] + async fn is_graphql_input_object() { + const DOC: &str = r#"{ + __type(name: "Point2D") { + kind + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok((graphql_value!({"__type": {"kind": "INPUT_OBJECT"}}), vec![])), + ); + } + + #[tokio::test] + async fn has_input_fields() { + const DOC: &str = r#"{ + __type(name: "Point2D") { + inputFields { + name + description + type { + ofType { + name + } + } + defaultValue + } + } + }"#; + + let schema = schema(QueryRoot); + + assert_eq!( + execute(DOC, None, &schema, &graphql_vars! {}, &()).await, + Ok(( + graphql_value!({"__type": {"inputFields": [ + { + "name": "x_coord", + "description": null, + "type": {"ofType": {"name": "Float"}}, + "defaultValue": null, + }, + { + "name": "y", + "description": null, + "type": {"ofType": {"name": "Float"}}, + "defaultValue": null, + }, + ]}}), + vec![], + )), + ); + } +} diff --git a/tests/integration/src/codegen/mod.rs b/tests/integration/src/codegen/mod.rs index 830f6e1a..fa65f453 100644 --- a/tests/integration/src/codegen/mod.rs +++ b/tests/integration/src/codegen/mod.rs @@ -1,6 +1,6 @@ -mod derive_input_object; mod derive_object_with_raw_idents; mod enum_derive; +mod input_object_derive; mod interface_attr_struct; mod interface_attr_trait; mod interface_derive;