Rework #[derive(GraphQLInputObject)] macro implementation (#1052)

Co-authored-by: Kai Ren <tyranron@gmail.com>
This commit is contained in:
ilslv 2022-06-28 14:27:28 +03:00 committed by GitHub
parent 9ca2364bfe
commit 927e42201a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 1928 additions and 1230 deletions

View file

@ -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,
}

View file

@ -49,7 +49,7 @@ struct ExampleInputObject {
#[derive(GraphQLInputObject, Debug)]
struct InputWithDefaults {
#[graphql(default = "123")]
#[graphql(default = 123)]
a: i32,
}

View file

@ -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),
}
}
}

View file

@ -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

View file

@ -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;

View file

@ -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())
}

View file

@ -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::{

View file

@ -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,

View file

@ -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
}

View file

@ -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| &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
}
}

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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]

View file

@ -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,

View file

@ -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
}
}
}

View file

@ -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()

View file

@ -0,0 +1,13 @@
use juniper::{GraphQLInputObject, GraphQLObject};
#[derive(GraphQLObject)]
struct ObjectA {
test: String,
}
#[derive(GraphQLInputObject)]
struct Object {
field: ObjectA,
}
fn main() {}

View file

@ -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)

View file

@ -1,11 +0,0 @@
#[derive(juniper::GraphQLObject)]
struct ObjectA {
test: String,
}
#[derive(juniper::GraphQLInputObject)]
struct Object {
field: ObjectA,
}
fn main() {}

View file

@ -1,4 +1,6 @@
#[derive(juniper::GraphQLInputObject)]
use juniper::GraphQLInputObject;
#[derive(GraphQLInputObject)]
struct Object {}
fn main() {}

View file

@ -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 {}
| ^^

View file

@ -1,4 +1,6 @@
#[derive(juniper::GraphQLInputObject)]
use juniper::GraphQLInputObject;
#[derive(GraphQLInputObject)]
struct Object {
#[graphql(name = "__test")]
test: String,

View file

@ -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 GraphQLs 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

View file

@ -1,4 +1,6 @@
#[derive(juniper::GraphQLInputObject)]
use juniper::GraphQLInputObject;
#[derive(GraphQLInputObject)]
struct Object {
test: String,
#[graphql(name = "test")]

View file

@ -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 | | }
| |_^

View file

@ -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"));
}

View file

@ -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![],
)),
);
}
}

View file

@ -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;