Rework #[derive(GraphQLInputObject)]
macro implementation (#1052)
Co-authored-by: Kai Ren <tyranron@gmail.com>
This commit is contained in:
parent
9ca2364bfe
commit
927e42201a
29 changed files with 1928 additions and 1230 deletions
|
@ -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,
|
||||
}
|
||||
|
||||
|
|
|
@ -49,7 +49,7 @@ struct ExampleInputObject {
|
|||
|
||||
#[derive(GraphQLInputObject, Debug)]
|
||||
struct InputWithDefaults {
|
||||
#[graphql(default = "123")]
|
||||
#[graphql(default = 123)]
|
||||
a: i32,
|
||||
}
|
||||
|
||||
|
|
65
juniper_codegen/src/common/default.rs
Normal file
65
juniper_codegen/src/common/default.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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())
|
||||
}
|
|
@ -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::{
|
||||
|
|
|
@ -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,
|
||||
|
||||
|
|
136
juniper_codegen/src/graphql_input_object/derive.rs
Normal file
136
juniper_codegen/src/graphql_input_object/derive.rs
Normal 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
|
||||
}
|
797
juniper_codegen/src/graphql_input_object/mod.rs
Normal file
797
juniper_codegen/src/graphql_input_object/mod.rs
Normal 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| <.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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
use juniper::{GraphQLInputObject, GraphQLObject};
|
||||
|
||||
#[derive(GraphQLObject)]
|
||||
struct ObjectA {
|
||||
test: String,
|
||||
}
|
||||
|
||||
#[derive(GraphQLInputObject)]
|
||||
struct Object {
|
||||
field: ObjectA,
|
||||
}
|
||||
|
||||
fn main() {}
|
|
@ -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)
|
|
@ -1,11 +0,0 @@
|
|||
#[derive(juniper::GraphQLObject)]
|
||||
struct ObjectA {
|
||||
test: String,
|
||||
}
|
||||
|
||||
#[derive(juniper::GraphQLInputObject)]
|
||||
struct Object {
|
||||
field: ObjectA,
|
||||
}
|
||||
|
||||
fn main() {}
|
|
@ -1,4 +1,6 @@
|
|||
#[derive(juniper::GraphQLInputObject)]
|
||||
use juniper::GraphQLInputObject;
|
||||
|
||||
#[derive(GraphQLInputObject)]
|
||||
struct Object {}
|
||||
|
||||
fn main() {}
|
||||
|
|
|
@ -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 {}
|
||||
| ^^
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
#[derive(juniper::GraphQLInputObject)]
|
||||
use juniper::GraphQLInputObject;
|
||||
|
||||
#[derive(GraphQLInputObject)]
|
||||
struct Object {
|
||||
#[graphql(name = "__test")]
|
||||
test: String,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
#[derive(juniper::GraphQLInputObject)]
|
||||
use juniper::GraphQLInputObject;
|
||||
|
||||
#[derive(GraphQLInputObject)]
|
||||
struct Object {
|
||||
test: String,
|
||||
#[graphql(name = "test")]
|
||||
|
|
|
@ -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 | | }
|
||||
| |_^
|
||||
|
|
|
@ -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"));
|
||||
}
|
708
tests/integration/src/codegen/input_object_derive.rs
Normal file
708
tests/integration/src/codegen/input_object_derive.rs
Normal 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![],
|
||||
)),
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue