Add ComplexObject macro. #355

This commit is contained in:
Sunli 2021-03-17 20:51:30 +08:00
parent 6099e14562
commit 15036be792
9 changed files with 614 additions and 4 deletions

View File

@ -158,6 +158,8 @@ pub struct SimpleObject {
#[darling(default)]
pub dummy: bool,
#[darling(default)]
pub complex: bool,
#[darling(default)]
pub name: Option<String>,
#[darling(default)]
pub rename_fields: Option<RenameRule>,
@ -584,3 +586,27 @@ pub struct NewType {
#[darling(default)]
pub internal: bool,
}
#[derive(FromMeta, Default)]
#[darling(default)]
pub struct ComplexObject {
pub internal: bool,
pub name: Option<String>,
pub rename_fields: Option<RenameRule>,
pub rename_args: Option<RenameRule>,
}
#[derive(FromMeta, Default)]
#[darling(default)]
pub struct ComplexObjectField {
pub skip: bool,
pub name: Option<String>,
pub deprecation: Deprecation,
pub cache_control: CacheControl,
pub external: bool,
pub provides: Option<String>,
pub requires: Option<String>,
pub guard: Option<Meta>,
pub visible: Option<Visible>,
pub complexity: Option<ComplexityType>,
}

View File

@ -0,0 +1,362 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::ext::IdentExt;
use syn::{Block, Error, FnArg, ImplItem, ItemImpl, Pat, ReturnType, Type, TypeReference};
use crate::args::{self, ComplexityType, RenameRuleExt, RenameTarget};
use crate::output_type::OutputType;
use crate::utils::{
gen_deprecation, generate_default, generate_guards, generate_validator, get_cfg_attrs,
get_crate_name, get_param_getter_ident, get_rustdoc, get_type_path_and_name,
parse_complexity_expr, parse_graphql_attrs, remove_graphql_attrs, visible_fn, GeneratorResult,
};
pub fn generate(
object_args: &args::ComplexObject,
item_impl: &mut ItemImpl,
) -> GeneratorResult<TokenStream> {
let crate_name = get_crate_name(object_args.internal);
let (self_ty, _) = get_type_path_and_name(item_impl.self_ty.as_ref())?;
let generics = &item_impl.generics;
let generics_params = &generics.params;
let where_clause = &item_impl.generics.where_clause;
let mut resolvers = Vec::new();
let mut schema_fields = Vec::new();
for item in &mut item_impl.items {
if let ImplItem::Method(method) = item {
let method_args: args::ObjectField =
parse_graphql_attrs(&method.attrs)?.unwrap_or_default();
if method_args.skip {
continue;
}
let field_name = method_args.name.clone().unwrap_or_else(|| {
object_args
.rename_fields
.rename(method.sig.ident.unraw().to_string(), RenameTarget::Field)
});
let field_desc = get_rustdoc(&method.attrs)?
.map(|s| quote! { ::std::option::Option::Some(#s) })
.unwrap_or_else(|| quote! {::std::option::Option::None});
let field_deprecation = gen_deprecation(&method_args.deprecation, &crate_name);
let external = method_args.external;
let requires = match &method_args.requires {
Some(requires) => quote! { ::std::option::Option::Some(#requires) },
None => quote! { ::std::option::Option::None },
};
let provides = match &method_args.provides {
Some(provides) => quote! { ::std::option::Option::Some(#provides) },
None => quote! { ::std::option::Option::None },
};
let ty = match &method.sig.output {
ReturnType::Type(_, ty) => OutputType::parse(ty)?,
ReturnType::Default => {
return Err(Error::new_spanned(
&method.sig.output,
"Resolver must have a return type",
)
.into())
}
};
let cache_control = {
let public = method_args.cache_control.is_public();
let max_age = method_args.cache_control.max_age;
quote! {
#crate_name::CacheControl {
public: #public,
max_age: #max_age,
}
}
};
let cfg_attrs = get_cfg_attrs(&method.attrs);
let mut create_ctx = true;
let mut args = Vec::new();
if method.sig.inputs.is_empty() {
return Err(Error::new_spanned(
&method.sig,
"The self receiver must be the first parameter.",
)
.into());
}
for (idx, arg) in method.sig.inputs.iter_mut().enumerate() {
if let FnArg::Receiver(receiver) = arg {
if idx != 0 {
return Err(Error::new_spanned(
receiver,
"The self receiver must be the first parameter.",
)
.into());
}
} else if let FnArg::Typed(pat) = arg {
if idx == 0 {
return Err(Error::new_spanned(
pat,
"The self receiver must be the first parameter.",
)
.into());
}
match (&*pat.pat, &*pat.ty) {
(Pat::Ident(arg_ident), Type::Path(arg_ty)) => {
args.push((
arg_ident.clone(),
arg_ty.clone(),
parse_graphql_attrs::<args::Argument>(&pat.attrs)?
.unwrap_or_default(),
));
remove_graphql_attrs(&mut pat.attrs);
}
(arg, Type::Reference(TypeReference { elem, .. })) => {
if let Type::Path(path) = elem.as_ref() {
if idx != 1 || path.path.segments.last().unwrap().ident != "Context"
{
return Err(Error::new_spanned(
arg,
"Only types that implement `InputType` can be used as input arguments.",
)
.into());
}
create_ctx = false;
}
}
_ => return Err(Error::new_spanned(arg, "Invalid argument type.").into()),
}
}
}
if create_ctx {
let arg = syn::parse2::<FnArg>(quote! { _: &#crate_name::Context<'_> }).unwrap();
method.sig.inputs.insert(1, arg);
}
let mut schema_args = Vec::new();
let mut use_params = Vec::new();
let mut get_params = Vec::new();
for (
ident,
ty,
args::Argument {
name,
desc,
default,
default_with,
validator,
visible,
..
},
) in &args
{
let name = name.clone().unwrap_or_else(|| {
object_args
.rename_args
.rename(ident.ident.unraw().to_string(), RenameTarget::Argument)
});
let desc = desc
.as_ref()
.map(|s| quote! {::std::option::Option::Some(#s)})
.unwrap_or_else(|| quote! {::std::option::Option::None});
let default = generate_default(&default, &default_with)?;
let schema_default = default
.as_ref()
.map(|value| {
quote! {
::std::option::Option::Some(::std::string::ToString::to_string(
&<#ty as #crate_name::InputType>::to_value(&#value)
))
}
})
.unwrap_or_else(|| quote! {::std::option::Option::None});
let validator = match &validator {
Some(meta) => {
let stream = generate_validator(&crate_name, meta)?;
quote!(::std::option::Option::Some(#stream))
}
None => quote!(::std::option::Option::None),
};
let visible = visible_fn(&visible);
schema_args.push(quote! {
args.insert(#name, #crate_name::registry::MetaInputValue {
name: #name,
description: #desc,
ty: <#ty as #crate_name::Type>::create_type_info(registry),
default_value: #schema_default,
validator: #validator,
visible: #visible,
});
});
let param_ident = &ident.ident;
use_params.push(quote! { #param_ident });
let default = match default {
Some(default) => {
quote! { ::std::option::Option::Some(|| -> #ty { #default }) }
}
None => quote! { ::std::option::Option::None },
};
// We're generating a new identifier,
// so remove the 'r#` prefix if present
let param_getter_name = get_param_getter_ident(&ident.ident.unraw().to_string());
get_params.push(quote! {
#[allow(non_snake_case)]
let #param_getter_name = || -> #crate_name::ServerResult<#ty> { ctx.param_value(#name, #default) };
#[allow(non_snake_case)]
let #ident: #ty = #param_getter_name()?;
});
}
let schema_ty = ty.value_type();
let visible = visible_fn(&method_args.visible);
let complexity = if let Some(complexity) = &method_args.complexity {
match complexity {
ComplexityType::Const(n) => {
quote! { ::std::option::Option::Some(#crate_name::registry::ComplexityType::Const(#n)) }
}
ComplexityType::Fn(s) => {
let (variables, expr) = parse_complexity_expr(s)?;
let mut parse_args = Vec::new();
for variable in variables {
if let Some((
ident,
ty,
args::Argument {
name,
default,
default_with,
..
},
)) = args
.iter()
.find(|(pat_ident, _, _)| pat_ident.ident == variable)
{
let default = match generate_default(&default, &default_with)? {
Some(default) => {
quote! { ::std::option::Option::Some(|| -> #ty { #default }) }
}
None => quote! { ::std::option::Option::None },
};
let name = name.clone().unwrap_or_else(|| {
object_args.rename_args.rename(
ident.ident.unraw().to_string(),
RenameTarget::Argument,
)
});
parse_args.push(quote! {
let #ident: #ty = __ctx.param_value(__variables_definition, __field, #name, #default)?;
});
}
}
quote! {
Some(#crate_name::registry::ComplexityType::Fn(|__ctx, __variables_definition, __field, child_complexity| {
#(#parse_args)*
Ok(#expr)
}))
}
}
}
} else {
quote! { ::std::option::Option::None }
};
schema_fields.push(quote! {
#(#cfg_attrs)*
fields.push((#field_name.to_string(), #crate_name::registry::MetaField {
name: ::std::borrow::ToOwned::to_owned(#field_name),
description: #field_desc,
args: {
let mut args = #crate_name::indexmap::IndexMap::new();
#(#schema_args)*
args
},
ty: <#schema_ty as #crate_name::Type>::create_type_info(registry),
deprecation: #field_deprecation,
cache_control: #cache_control,
external: #external,
provides: #provides,
requires: #requires,
visible: #visible,
compute_complexity: #complexity,
}));
});
let field_ident = &method.sig.ident;
if let OutputType::Value(inner_ty) = &ty {
let block = &method.block;
let new_block = quote!({
{
::std::result::Result::Ok(async move {
let value:#inner_ty = #block;
value
}.await)
}
});
method.block = syn::parse2::<Block>(new_block).expect("invalid block");
method.sig.output =
syn::parse2::<ReturnType>(quote! { -> #crate_name::Result<#inner_ty> })
.expect("invalid result type");
}
let resolve_obj = quote! {
{
let res = self.#field_ident(ctx, #(#use_params),*).await;
res.map_err(|err| err.into_server_error().at(ctx.item.pos))?
}
};
let guard = match &method_args.guard {
Some(meta_list) => generate_guards(&crate_name, meta_list)?,
None => None,
};
let guard = guard.map(|guard| {
quote! {
#guard.check(ctx).await
.map_err(|err| err.into_server_error().at(ctx.item.pos))?;
}
});
resolvers.push(quote! {
#(#cfg_attrs)*
if ctx.item.node.name.node == #field_name {
#(#get_params)*
#guard
let ctx_obj = ctx.with_selection_set(&ctx.item.node.selection_set);
let res = #resolve_obj;
return #crate_name::OutputType::resolve(&res, &ctx_obj, ctx.item).await.map(::std::option::Option::Some);
}
});
remove_graphql_attrs(&mut method.attrs);
}
}
let expanded = quote! {
#item_impl
#[allow(clippy::all, clippy::pedantic)]
#[#crate_name::async_trait::async_trait]
impl #generics #crate_name::ComplexObject for #self_ty#generics_params #where_clause {
fn fields(registry: &mut #crate_name::registry::Registry) -> ::std::vec::Vec<(::std::string::String, #crate_name::registry::MetaField)> {
let mut fields = ::std::vec::Vec::new();
#(#schema_fields)*
fields
}
async fn resolve_field(&self, ctx: &#crate_name::Context<'_>) -> #crate_name::ServerResult<::std::option::Option<#crate_name::Value>> {
#(#resolvers)*
::std::result::Result::Ok(::std::option::Option::None)
}
}
};
Ok(expanded.into())
}

View File

@ -4,6 +4,7 @@
extern crate proc_macro;
mod args;
mod complex_object;
mod description;
mod r#enum;
mod input_object;
@ -51,6 +52,21 @@ pub fn derive_simple_object(input: TokenStream) -> TokenStream {
}
}
#[proc_macro_attribute]
#[allow(non_snake_case)]
pub fn ComplexObject(args: TokenStream, input: TokenStream) -> TokenStream {
let object_args =
match args::ComplexObject::from_list(&parse_macro_input!(args as AttributeArgs)) {
Ok(object_args) => object_args,
Err(err) => return TokenStream::from(err.write_errors()),
};
let mut item_impl = parse_macro_input!(input as ItemImpl);
match complex_object::generate(&object_args, &mut item_impl) {
Ok(expanded) => expanded,
Err(err) => err.write_errors().into(),
}
}
#[proc_macro_derive(Enum, attributes(graphql))]
pub fn derive_enum(input: TokenStream) -> TokenStream {
let enum_args = match args::Enum::from_derive_input(&parse_macro_input!(input as DeriveInput)) {

View File

@ -151,6 +151,20 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult<TokenStream
let visible = visible_fn(&object_args.visible);
let mut concat_complex_fields = quote!();
let mut complex_resolver = quote!();
if object_args.complex {
concat_complex_fields = quote! {
fields.extend(<Self as #crate_name::ComplexObject>::fields(registry));
};
complex_resolver = quote! {
if let Some(value) = <Self as #crate_name::ComplexObject>::resolve_field(self, ctx).await? {
return Ok(Some(value));
}
};
}
let expanded = if object_args.concretes.is_empty() {
quote! {
#[allow(clippy::all, clippy::pedantic)]
@ -171,6 +185,7 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult<TokenStream
fields: {
let mut fields = #crate_name::indexmap::IndexMap::new();
#(#schema_fields)*
#concat_complex_fields
fields
},
cache_control: #cache_control,
@ -187,6 +202,7 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult<TokenStream
impl #impl_generics #crate_name::resolver_utils::ContainerType for #ident #ty_generics #where_clause {
async fn resolve_field(&self, ctx: &#crate_name::Context<'_>) -> #crate_name::ServerResult<::std::option::Option<#crate_name::Value>> {
#(#resolvers)*
#complex_resolver
::std::result::Result::Ok(::std::option::Option::None)
}
}

View File

@ -64,3 +64,27 @@ pub struct YetAnotherObject {
```
You can pass multiple generic types to `params()`, separated by a comma.
## Complex resolvers
Sometimes most of the fields of a GraphQL object simply return the value of the structure member, but a few
fields are calculated. Usually we use the `Object` macro to define such a GraphQL object.
But this can be done more beautifully with the `ComplexObject` macro. We can use the `SimpleObject` macro to define
some simple fields, and use the `ComplexObject` macro to define some other fields that need to be calculated.
```rust
#[derive(SimpleObject)]
#[graphql(complex)] // NOTE: If you want the `ComplexObject` macro to take effect, this `complex` attribute is required.
struct MyObj {
a: i32,
b: i32,
}
#[ComplexObject]
impl MyObj {
async fn c(&self) -> i32 {
self.a + self.b
}
}
```

View File

@ -63,3 +63,26 @@ pub struct YetAnotherObject {
```
你可以将多个通用类型传递给`params`,并用逗号分隔。
## 复杂字段
有时GraphQL对象的大多数字段仅返回结构成员的值但是少数字段需要计算。 通常我们使用`Object`宏来定义这样一个GraphQL对象。
用`ComplexObject`宏可以更漂亮的完成这件事,我们可以使用`SimpleObject`宏来定义
一些简单的字段,并使用`ComplexObject`宏来定义其他一些需要计算的字段。
```rust
#[derive(SimpleObject)]
#[graphql(complex)] // 注意: 如果你希望ComplexObject宏生效complex属性是必须的
struct MyObj {
a: i32,
b: i32,
}
#[ComplexObject]
impl MyObj {
async fn c(&self) -> i32 {
self.a + self.b
}
}
```

View File

@ -4,9 +4,9 @@ use std::sync::Arc;
use async_graphql_value::ConstValue;
use crate::parser::types::Field;
use crate::registry::Registry;
use crate::registry::{self, Registry};
use crate::{
registry, ContainerType, ContextSelectionSet, InputValueError, InputValueResult, Positioned,
ContainerType, Context, ContextSelectionSet, InputValueError, InputValueResult, Positioned,
Result, ServerResult, Value,
};
@ -186,7 +186,6 @@ impl<T: OutputType + ?Sized> OutputType for Arc<T> {
}
}
#[async_trait::async_trait]
impl<T: InputType> InputType for Arc<T> {
fn parse(value: Option<ConstValue>) -> InputValueResult<Self> {
T::parse(value)
@ -198,3 +197,11 @@ impl<T: InputType> InputType for Arc<T> {
T::to_value(&self)
}
}
#[doc(hidden)]
#[async_trait::async_trait]
pub trait ComplexObject {
fn fields(registry: &mut registry::Registry) -> Vec<(String, registry::MetaField)>;
async fn resolve_field(&self, ctx: &Context<'_>) -> ServerResult<Option<Value>>;
}

View File

@ -211,7 +211,8 @@ pub use async_graphql_value::{
SerializerError, Variables,
};
pub use base::{
Description, InputObjectType, InputType, InterfaceType, ObjectType, OutputType, Type, UnionType,
ComplexObject, Description, InputObjectType, InputType, InterfaceType, ObjectType, OutputType,
Type, UnionType,
};
pub use error::{
Error, ErrorExtensionValues, ErrorExtensions, InputValueError, InputValueResult,
@ -473,6 +474,82 @@ pub use async_graphql_derive::Object;
/// ```
pub use async_graphql_derive::SimpleObject;
/// Define a complex GraphQL object for SimpleObject's complex field resolver.
///
/// *[See also the Book](https://async-graphql.github.io/async-graphql/en/define_simple_object.html).*
///
/// Sometimes most of the fields of a GraphQL object simply return the value of the structure member, but a few
/// fields are calculated. Usually we use the `Object` macro to define such a GraphQL object.
///
/// But this can be done more beautifully with the `ComplexObject` macro. We can use the `SimpleObject` macro to define
/// some simple fields, and use the `ComplexObject` macro to define some other fields that need to be calculated.
///
/// # Macro parameters
///
/// | Attribute | description | Type | Optional |
/// |---------------|---------------------------|----------|----------|
/// | name | Object name | string | Y |
/// | rename_fields | Rename all the fields according to the given case convention. The possible values are "lowercase", "UPPERCASE", "PascalCase", "camelCase", "snake_case", "SCREAMING_SNAKE_CASE".| string | Y |
/// | rename_args | Rename all the arguments according to the given case convention. The possible values are "lowercase", "UPPERCASE", "PascalCase", "camelCase", "snake_case", "SCREAMING_SNAKE_CASE".| string | Y |
///
/// # Field parameters
///
/// | Attribute | description | Type | Optional |
/// |---------------|---------------------------|----------|----------|
/// | skip | Skip this field | bool | Y |
/// | name | Field name | string | Y |
/// | deprecation | Field deprecated | bool | Y |
/// | deprecation | Field deprecation reason | string | Y |
/// | cache_control | Field cache control | [`CacheControl`](struct.CacheControl.html) | Y |
/// | external | Mark a field as owned by another service. This allows service A to use fields from service B while also knowing at runtime the types of that field. | bool | Y |
/// | provides | Annotate the expected returned fieldset from a field on a base type that is guaranteed to be selectable by the gateway. | string | Y |
/// | requires | Annotate the required input fieldset from a base type for a resolver. It is used to develop a query plan where the required fields may not be needed by the client, but the service may need additional information from other services. | string | Y |
/// | guard | Field of guard | [`Guard`](guard/trait.Guard.html) | Y |
/// | visible | If `false`, it will not be displayed in introspection. *[See also the Book](https://async-graphql.github.io/async-graphql/en/visibility.html).* | bool | Y |
/// | visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y |
///
/// # Examples
///
/// ```rust
/// use async_graphql::*;
///
/// #[derive(SimpleObject)]
/// #[graphql(complex)] // NOTE: If you want the `ComplexObject` macro to take effect, this `complex` attribute is required.
/// struct MyObj {
/// a: i32,
/// b: i32,
/// }
///
/// #[ComplexObject]
/// impl MyObj {
/// async fn c(&self) -> i32 {
/// self.a + self.b
/// }
/// }
///
/// struct QueryRoot;
///
/// #[Object]
/// impl QueryRoot {
/// async fn obj(&self) -> MyObj {
/// MyObj { a: 10, b: 20 }
/// }
/// }
///
/// tokio::runtime::Runtime::new().unwrap().block_on(async move {
/// let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription);
/// let res = schema.execute("{ obj { a b c } }").await.into_result().unwrap().data;
/// assert_eq!(res, value!({
/// "obj": {
/// "a": 10,
/// "b": 20,
/// "c": 30,
/// },
/// }));
/// });
/// ```
pub use async_graphql_derive::ComplexObject;
/// Define a GraphQL enum
///
/// *[See also the Book](https://async-graphql.github.io/async-graphql/en/define_enum.html).*

59
tests/complex_object.rs Normal file
View File

@ -0,0 +1,59 @@
use async_graphql::*;
#[tokio::test]
pub async fn test_complex_object() {
#[derive(SimpleObject)]
#[graphql(complex)]
struct MyObj {
a: i32,
b: i32,
}
#[ComplexObject]
impl MyObj {
async fn c(&self) -> i32 {
self.a + self.b
}
}
#[derive(Interface)]
#[graphql(
field(name = "a", type = "&i32"),
field(name = "b", type = "&i32"),
field(name = "c", type = "i32")
)]
enum ObjInterface {
MyObj(MyObj),
}
struct Query;
#[Object]
impl Query {
async fn obj(&self) -> MyObj {
MyObj { a: 10, b: 20 }
}
async fn obj2(&self) -> ObjInterface {
MyObj { a: 10, b: 20 }.into()
}
}
let query = "{ obj { a b c } obj2 { a b c } }";
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
assert_eq!(
schema.execute(query).await.data,
value!({
"obj": {
"a": 10,
"b": 20,
"c": 30,
},
"obj2": {
"a": 10,
"b": 20,
"c": 30,
}
})
);
}