Custom directive

This commit is contained in:
Sunli 2021-11-19 18:49:37 +08:00
parent 3f458dcb31
commit 1b50738af7
20 changed files with 738 additions and 105 deletions

View File

@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [3.0.6] 2021-11-19
- Custom directives. [Book](https://async-graphql.github.io/async-graphql/en/custom_directive.html)
## [3.0.5] 2021-11-19
- Remove skipped fields from the document before executing the query.

View File

@ -1,3 +1,5 @@
use std::fmt::{self, Display, Formatter};
use darling::ast::{Data, Fields};
use darling::util::{Ignored, SpannedValue};
use darling::{FromDeriveInput, FromField, FromMeta, FromVariant};
@ -697,3 +699,29 @@ pub struct ComplexObjectField {
pub visible: Option<Visible>,
pub complexity: Option<ComplexityType>,
}
#[derive(FromMeta, Default)]
#[darling(default)]
pub struct Directive {
pub internal: bool,
pub name: Option<String>,
pub visible: Option<Visible>,
pub repeatable: bool,
pub rename_args: Option<RenameRule>,
#[darling(multiple, rename = "location")]
pub locations: Vec<DirectiveLocation>,
}
#[derive(Debug, Copy, Clone, FromMeta)]
#[darling(rename_all = "lowercase")]
pub enum DirectiveLocation {
Field,
}
impl Display for DirectiveLocation {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
DirectiveLocation::Field => write!(f, "FIELD"),
}
}
}

174
derive/src/directive.rs Normal file
View File

@ -0,0 +1,174 @@
use proc_macro::TokenStream;
use quote::quote;
use syn::ext::IdentExt;
use syn::{Error, FnArg, ItemFn, Pat};
use crate::args;
use crate::args::{Argument, RenameRuleExt, RenameTarget};
use crate::utils::{
generate_default, get_crate_name, get_rustdoc, parse_graphql_attrs, remove_graphql_attrs,
visible_fn, GeneratorResult,
};
pub fn generate(
directive_args: &args::Directive,
item_fn: &mut ItemFn,
) -> GeneratorResult<TokenStream> {
let crate_name = get_crate_name(directive_args.internal);
let ident = &item_fn.sig.ident;
let vis = &item_fn.vis;
let directive_name = directive_args
.name
.clone()
.unwrap_or_else(|| item_fn.sig.ident.to_string());
let desc = get_rustdoc(&item_fn.attrs)?
.map(|s| quote!(::std::option::Option::Some(#s)))
.unwrap_or_else(|| quote!(::std::option::Option::None));
let visible = visible_fn(&directive_args.visible);
let repeatable = directive_args.repeatable;
let mut get_params = Vec::new();
let mut use_params = Vec::new();
let mut schema_args = Vec::new();
for arg in item_fn.sig.inputs.iter_mut() {
let mut arg_info = None;
if let FnArg::Typed(pat) = arg {
if let Pat::Ident(ident) = &*pat.pat {
arg_info = Some((ident.clone(), pat.ty.clone(), pat.attrs.clone()));
remove_graphql_attrs(&mut pat.attrs);
}
}
let (arg_ident, arg_ty, arg_attrs) = match arg_info {
Some(info) => info,
None => {
return Err(Error::new_spanned(arg, "Invalid argument type.").into());
}
};
let Argument {
name,
desc,
default,
default_with,
validator,
visible,
secret,
..
} = parse_graphql_attrs::<args::Argument>(&arg_attrs)?.unwrap_or_default();
let name = name.clone().unwrap_or_else(|| {
directive_args
.rename_args
.rename(arg_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(
&<#arg_ty as #crate_name::InputType>::to_value(&#value)
))
}
})
.unwrap_or_else(|| 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: <#arg_ty as #crate_name::InputType>::create_type_info(registry),
default_value: #schema_default,
visible: #visible,
is_secret: #secret,
});
});
let validators = validator.clone().unwrap_or_default().create_validators(
&crate_name,
quote!(&#arg_ident),
quote!(ty),
Some(quote!(.map_err(|err| err.into_server_error(__pos)))),
)?;
let default = match default {
Some(default) => {
quote! { ::std::option::Option::Some(|| -> #arg_ty { #default }) }
}
None => quote! { ::std::option::Option::None },
};
get_params.push(quote! {
let (__pos, #arg_ident) = ctx.param_value::<#arg_ty>(#name, #default)?;
#validators
});
use_params.push(quote! { #arg_ident });
}
let locations = directive_args
.locations
.iter()
.map(|loc| {
let loc = quote::format_ident!("{}", loc.to_string());
quote!(#crate_name::registry::__DirectiveLocation::#loc)
})
.collect::<Vec<_>>();
if locations.is_empty() {
return Err(Error::new(
ident.span(),
"At least one location is required for the directive.",
)
.into());
}
let expanded = quote! {
#[allow(non_camel_case_types)]
#vis struct #ident;
#[#crate_name::async_trait::async_trait]
impl #crate_name::CustomDirectiveFactory for #ident {
fn name(&self) -> &'static str {
#directive_name
}
fn register(&self, registry: &mut #crate_name::registry::Registry) {
let meta = #crate_name::registry::MetaDirective {
name: #directive_name,
description: #desc,
locations: vec![#(#locations),*],
args: {
#[allow(unused_mut)]
let mut args = #crate_name::indexmap::IndexMap::new();
#(#schema_args)*
args
},
is_repeatable: #repeatable,
visible: #visible,
};
registry.add_directive(meta);
}
fn create(
&self,
ctx: &#crate_name::ContextDirective<'_>,
directive: &#crate_name::parser::types::Directive,
) -> #crate_name::ServerResult<::std::boxed::Box<dyn #crate_name::CustomDirective>> {
#item_fn
#(#get_params)*
Ok(::std::boxed::Box::new(#ident(#(#use_params),*)))
}
}
};
Ok(expanded.into())
}

View File

@ -7,6 +7,7 @@ extern crate proc_macro;
mod args;
mod complex_object;
mod description;
mod directive;
mod r#enum;
mod input_object;
mod interface;
@ -25,7 +26,7 @@ mod validators;
use darling::{FromDeriveInput, FromMeta};
use proc_macro::TokenStream;
use syn::parse_macro_input;
use syn::{AttributeArgs, DeriveInput, ItemImpl};
use syn::{AttributeArgs, DeriveInput, ItemFn, ItemImpl};
#[proc_macro_attribute]
#[allow(non_snake_case)]
@ -201,3 +202,18 @@ pub fn derive_newtype(input: TokenStream) -> TokenStream {
Err(err) => err.write_errors().into(),
}
}
#[proc_macro_attribute]
#[allow(non_snake_case)]
pub fn Directive(args: TokenStream, input: TokenStream) -> TokenStream {
let directive_args =
match args::Directive::from_list(&parse_macro_input!(args as AttributeArgs)) {
Ok(directive_args) => directive_args,
Err(err) => return TokenStream::from(err.write_errors()),
};
let mut item_fn = parse_macro_input!(input as ItemFn);
match directive::generate(&directive_args, &mut item_fn) {
Ok(expanded) => expanded,
Err(err) => err.write_errors().into(),
}
}

View File

@ -160,6 +160,10 @@ impl Validators {
});
}
if codes.is_empty() {
return Ok(quote!());
}
let codes = codes.into_iter().map(|s| quote!(#s #map_err ?));
if self.list {

View File

@ -37,4 +37,5 @@
- [Advanced topics](advanced_topics.md)
- [Custom scalars](custom_scalars.md)
- [Optimizing N+1 queries](dataloader.md)
- [Custom directive](custom_directive.md)
- [Apollo Federation](apollo_federation.md)

View File

@ -0,0 +1,39 @@
# Custom directive
`Async-graphql` can easily customize directives, which can extend the behavior of GraphQL.
To create a custom directive, you need to implement the `CustomDirective` trait, and then use the `Directive` macro to
generate a factory function that receives the parameters of the directive and returns an instance of the directive.
Currently `Async-graphql` only supports directive located at `FIELD`.
```rust
struct ConcatDirective {
value: String,
}
#[async_trait::async_trait]
impl CustomDirective for ConcatDirective {
async fn resolve_field(&self, _ctx: &Context<'_>, resolve: ResolveFut<'_>) -> ServerResult<Option<Value>> {
resolve.await.map(|value| {
value.map(|value| match value {
Value::String(str) => Value::String(str + &self.value),
_ => value,
})
})
}
}
#[Directive(location = "field")]
fn concat(value: String) -> impl CustomDirective {
ConcatDirective { value }
}
```
Register the directive when building the schema:
```rust
let schema = Schema::build(Query, EmptyMutation, EmptySubscription)
.directive(concat)
.finish();
```

View File

@ -36,5 +36,5 @@
- [高级主题](advanced_topics.md)
- [自定义标量](custom_scalars.md)
- [优化查询解决N+1问题](dataloader.md)
- [自定义扩展](custom_extensions.md)
- [自定义指令](custom_directive.md)
- [Apollo Federation集成](apollo_federation.md)

View File

@ -0,0 +1,38 @@
# 自定义指令
`Async-graphql`可以很方便的自定义指令这可以扩展GraphQL的行为。
创建一个自定义指令,需要实现 `CustomDirective` trait然后用`Directive`宏生成一个工厂函数,该函数接收指令的参数并返回指令的实例。
目前`Async-graphql`仅支持添加`FIELD`位置的指令。
```rust
struct ConcatDirective {
value: String,
}
#[async_trait::async_trait]
impl CustomDirective for ConcatDirective {
async fn resolve_field(&self, _ctx: &Context<'_>, resolve: ResolveFut<'_>) -> ServerResult<Option<Value>> {
resolve.await.map(|value| {
value.map(|value| match value {
Value::String(str) => Value::String(str + &self.value),
_ => value,
})
})
}
}
#[Directive(location = "field")]
fn concat(value: String) -> impl CustomDirective {
ConcatDirective { value }
}
```
创建模式时注册指令:
```rust
let schema = Schema::build(Query, EmptyMutation, EmptySubscription)
.directive(concat)
.finish();
```

View File

@ -1,7 +0,0 @@
# 自定义扩展
一个GraphQL扩展对象能够接收一个查询执行各个阶段的事件你可以收集想要的数据这些数据能够在查询结果中返回。
只需要实现`async_graphql::Extension`就能够定义一个扩展对象,然后在创建`Schema`的时候调用`Schema::extension`应用扩展。
你可以参考[Apollo tracing](https://github.com/async-graphql/async-graphql/blob/master/src/extensions/tracing.rs)来实现自己的扩展类型。

View File

@ -17,6 +17,7 @@ Comparing Features of Other Rust GraphQL Implementations
| Dataloading | 👍 | 👍 |
| Custom Scalar | 👍 | 👍 |
| Custom Error | 👍 | 👍 |
| Custom Directive | 👍 | ⛔ |
| Extensions | 👍 | ⛔️ |
| Cursor Connections | 👍 | ⛔️ |
| Query complexity/depth | 👍 | ⛔️ |

View File

@ -14,7 +14,7 @@ use serde::Serialize;
use crate::extensions::Extensions;
use crate::parser::types::{
Field, FragmentDefinition, OperationDefinition, Selection, SelectionSet,
Directive, Field, FragmentDefinition, OperationDefinition, Selection, SelectionSet,
};
use crate::schema::SchemaEnv;
use crate::{
@ -59,6 +59,9 @@ pub type ContextSelectionSet<'a> = ContextBase<'a, &'a Positioned<SelectionSet>>
/// Context object for resolve field
pub type Context<'a> = ContextBase<'a, &'a Positioned<Field>>;
/// Context object for execute directive.
pub type ContextDirective<'a> = ContextBase<'a, &'a Positioned<Directive>>;
/// A segment in the path to the current query.
///
/// This is a borrowed form of [`PathSegment`](enum.PathSegment.html) used during execution instead
@ -497,6 +500,32 @@ impl<'a, T> ContextBase<'a, T> {
.node
.into_const_with(|name| self.var_value(&name, pos))
}
#[doc(hidden)]
fn get_param_value<Q: InputType>(
&self,
arguments: &[(Positioned<Name>, Positioned<InputValue>)],
name: &str,
default: Option<fn() -> Q>,
) -> ServerResult<(Pos, Q)> {
let value = arguments
.iter()
.find(|(n, _)| n.node.as_str() == name)
.map(|(_, value)| value)
.cloned();
if value.is_none() {
if let Some(default) = default {
return Ok((Pos::default(), default()));
}
}
let (pos, value) = match value {
Some(value) => (value.pos, Some(self.resolve_input_value(value)?)),
None => (Pos::default(), None),
};
InputType::parse(value)
.map(|value| (pos, value))
.map_err(|e| e.into_server_error(pos))
}
}
impl<'a> ContextBase<'a, &'a Positioned<SelectionSet>> {
@ -521,19 +550,7 @@ impl<'a> ContextBase<'a, &'a Positioned<Field>> {
name: &str,
default: Option<fn() -> T>,
) -> ServerResult<(Pos, T)> {
let value = self.item.node.get_argument(name).cloned();
if value.is_none() {
if let Some(default) = default {
return Ok((Pos::default(), default()));
}
}
let (pos, value) = match value {
Some(value) => (value.pos, Some(self.resolve_input_value(value)?)),
None => (Pos::default(), None),
};
InputType::parse(value)
.map(|value| (pos, value))
.map_err(|e| e.into_server_error(pos))
self.get_param_value(&self.item.node.arguments, name, default)
}
/// Creates a uniform interface to inspect the forthcoming selections.
@ -618,6 +635,17 @@ impl<'a> ContextBase<'a, &'a Positioned<Field>> {
}
}
impl<'a> ContextBase<'a, &'a Positioned<Directive>> {
#[doc(hidden)]
pub fn param_value<T: InputType>(
&self,
name: &str,
default: Option<fn() -> T>,
) -> ServerResult<(Pos, T)> {
self.get_param_value(&self.item.node.arguments, name, default)
}
}
/// Selection field.
#[derive(Clone, Copy)]
pub struct SelectionField<'a> {

31
src/custom_directive.rs Normal file
View File

@ -0,0 +1,31 @@
use crate::extensions::ResolveFut;
use crate::parser::types::Directive;
use crate::registry::Registry;
use crate::{Context, ContextDirective, ServerResult, Value};
#[doc(hidden)]
pub trait CustomDirectiveFactory: Send + Sync + 'static {
fn name(&self) -> &'static str;
fn register(&self, registry: &mut Registry);
fn create(
&self,
ctx: &ContextDirective<'_>,
directive: &Directive,
) -> ServerResult<Box<dyn CustomDirective>>;
}
/// Represents a custom directive.
#[async_trait::async_trait]
#[allow(unused_variables)]
pub trait CustomDirective: Sync + Send + 'static {
/// Called at resolve field.
async fn resolve_field(
&self,
ctx: &Context<'_>,
resolve: ResolveFut<'_>,
) -> ServerResult<Option<Value>> {
resolve.await
}
}

View File

@ -120,7 +120,8 @@ type ValidationFut<'a> =
type ExecuteFut<'a> = &'a mut (dyn Future<Output = Response> + Send + Unpin);
type ResolveFut<'a> = &'a mut (dyn Future<Output = ServerResult<Option<Value>>> + Send + Unpin);
/// A future type used to resolve the field
pub type ResolveFut<'a> = &'a mut (dyn Future<Output = ServerResult<Option<Value>>> + Send + Unpin);
/// The remainder of a extension chain for request.
pub struct NextRequest<'a> {

View File

@ -164,6 +164,7 @@
#![cfg_attr(docsrs, feature(doc_cfg))]
mod base;
mod custom_directive;
mod error;
mod guard;
mod look_ahead;
@ -212,10 +213,12 @@ pub use base::{
ComplexObject, Description, InputObjectType, InputType, InterfaceType, ObjectType, OutputType,
UnionType,
};
pub use custom_directive::{CustomDirective, CustomDirectiveFactory};
pub use error::{
Error, ErrorExtensionValues, ErrorExtensions, InputValueError, InputValueResult,
ParseRequestError, PathSegment, Result, ResultExt, ServerError, ServerResult,
};
pub use extensions::ResolveFut;
pub use guard::{Guard, GuardExt};
pub use look_ahead::Lookahead;
pub use registry::CacheControl;
@ -246,7 +249,7 @@ pub type FieldResult<T> = Result<T>;
///
/// All methods are converted to camelCase.
///
/// # Macro parameters
/// # Macro attributes
///
/// | Attribute | description | Type | Optional |
/// |---------------|---------------------------|----------|----------|
@ -258,8 +261,9 @@ pub type FieldResult<T> = Result<T>;
/// | use_type_description | Specifies that the description of the type is on the type declaration. [`Description`]()(derive.Description.html) | bool | 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 |
/// | serial | Resolve each field sequentially. | bool | Y |
///
/// # Field parameters
/// # Field attributes
///
/// | Attribute | description | Type | Optional |
/// |---------------|---------------------------|----------|----------|
@ -275,8 +279,11 @@ pub type FieldResult<T> = Result<T>;
/// | guard | Field of guard *[See also the Book](https://async-graphql.github.io/async-graphql/en/field_guard.html)* | string | 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 |
/// | complexity | Custom field complexity. *[See also the Book](https://async-graphql.github.io/async-graphql/en/depth_and_complexity.html).* | bool | Y |
/// | complexity | Custom field complexity. | string | Y |
/// | derived | Generate derived fields *[See also the Book](https://async-graphql.github.io/async-graphql/en/derived_fields.html).* | object | Y |
///
/// # Field argument parameters
/// # Field argument attributes
///
/// | Attribute | description | Type | Optional |
/// |--------------|------------------------------------------|------------ |----------|
@ -285,17 +292,13 @@ pub type FieldResult<T> = Result<T>;
/// | default | Use `Default::default` for default value | none | Y |
/// | default | Argument default value | literal | Y |
/// | default_with | Expression to generate default value | code string | Y |
/// | derived | Generate derived fields *[See also the Book](https://async-graphql.github.io/async-graphql/en/derived_fields.html).* | object | Y |
/// | validator | Input value validator *[See also the Book](https://async-graphql.github.io/async-graphql/en/input_value_validators.html)* | object | Y |
/// | complexity | Custom field complexity. *[See also the Book](https://async-graphql.github.io/async-graphql/en/depth_and_complexity.html).* | bool | Y |
/// | complexity | Custom field complexity. | string | 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 |
/// | secret | Mark this field as a secret, it will not output the actual value in the log. | bool | Y |
/// | serial | Resolve each field sequentially. | bool | Y |
/// | key | Is entity key(for Federation) | bool | Y |
///
/// # Derived argument parameters
/// # Derived argument attributes
///
/// | Attribute | description | Type | Optional |
/// |--------------|------------------------------------------|------------ |----------|
@ -322,7 +325,7 @@ pub type FieldResult<T> = Result<T>;
///
/// ```ignore
/// #[Object]
/// impl QueryRoot {
/// impl Query {
/// async fn value(&self, ctx: &Context<'_>) -> { ... }
/// }
/// ```
@ -334,12 +337,12 @@ pub type FieldResult<T> = Result<T>;
/// ```rust
/// use async_graphql::*;
///
/// struct QueryRoot {
/// struct Query {
/// value: i32,
/// }
///
/// #[Object]
/// impl QueryRoot {
/// impl Query {
/// /// value
/// async fn value(&self) -> i32 {
/// self.value
@ -361,7 +364,7 @@ pub type FieldResult<T> = Result<T>;
/// }
///
/// tokio::runtime::Runtime::new().unwrap().block_on(async move {
/// let schema = Schema::new(QueryRoot { value: 10 }, EmptyMutation, EmptySubscription);
/// let schema = Schema::new(Query { value: 10 }, EmptyMutation, EmptySubscription);
/// let res = schema.execute(r#"{
/// value
/// valueRef
@ -406,10 +409,10 @@ pub type FieldResult<T> = Result<T>;
/// }
/// }
///
/// struct QueryRoot;
/// struct Query;
///
/// #[Object]
/// impl QueryRoot {
/// impl Query {
/// async fn objs(&self) -> Vec<Box<dyn MyTrait>> {
/// vec![
/// Box::new(MyObj("a".to_string())),
@ -419,7 +422,7 @@ pub type FieldResult<T> = Result<T>;
/// }
///
/// tokio::runtime::Runtime::new().unwrap().block_on(async move {
/// let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription);
/// let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
/// let res = schema.execute("{ objs { name } }").await.into_result().unwrap().data;
/// assert_eq!(res, value!({
/// "objs": [
@ -437,7 +440,7 @@ pub use async_graphql_derive::Object;
///
/// Similar to `Object`, but defined on a structure that automatically generates getters for all fields. For a list of valid field types, see [`Object`](attr.Object.html). All fields are converted to camelCase.
///
/// # Macro parameters
/// # Macro attributes
///
/// | Attribute | description | Type | Optional |
/// |---------------|---------------------------|----------|----------|
@ -450,7 +453,7 @@ pub use async_graphql_derive::Object;
/// | concretes | Specify how the concrete type of the generic SimpleObject should be implemented. *[See also the Book](https://async-graphql.github.io/async-graphql/en/define_simple_object.html#generic-simpleobjects) | ConcreteType | Y |
/// | serial | Resolve each field sequentially. | bool | Y |
///
/// # Field parameters
/// # Field attributes
///
/// | Attribute | description | Type | Optional |
/// |---------------|---------------------------|----------|----------|
@ -458,7 +461,7 @@ pub use async_graphql_derive::Object;
/// | name | Field name | string | Y |
/// | deprecation | Field deprecated | bool | Y |
/// | deprecation | Field deprecation reason | string | Y |
/// | derived | Generate derived fields *[See also the Book](https://async-graphql.github.io/async-graphql/en/derived_fields.html).* | object | Y |
/// | derived | Generate derived fields *[See also the Book](https://async-graphql.github.io/async-graphql/en/derived_fields.html).* | object | Y |
/// | owned | Field resolver return a ownedship value | bool | 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 |
@ -468,7 +471,7 @@ pub use async_graphql_derive::Object;
/// | 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 |
///
/// # Derived argument parameters
/// # Derived attributes
///
/// | Attribute | description | Type | Optional |
/// |--------------|------------------------------------------|------------ |----------|
@ -484,12 +487,12 @@ pub use async_graphql_derive::Object;
/// use async_graphql::*;
///
/// #[derive(SimpleObject)]
/// struct QueryRoot {
/// struct Query {
/// value: i32,
/// }
///
/// tokio::runtime::Runtime::new().unwrap().block_on(async move {
/// let schema = Schema::new(QueryRoot{ value: 10 }, EmptyMutation, EmptySubscription);
/// let schema = Schema::new(Query{ value: 10 }, EmptyMutation, EmptySubscription);
/// let res = schema.execute("{ value }").await.into_result().unwrap().data;
/// assert_eq!(res, value!({
/// "value": 10,
@ -508,7 +511,7 @@ pub use async_graphql_derive::SimpleObject;
/// 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
/// # Macro attributes
///
/// | Attribute | description | Type | Optional |
/// |---------------|---------------------------|----------|----------|
@ -516,15 +519,15 @@ pub use async_graphql_derive::SimpleObject;
/// | 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
/// # Field attributes
///
/// | Attribute | description | Type | Optional |
/// |---------------|---------------------------|----------|----------|
/// | skip | Skip this field | bool | Y |
/// | name | Field name | string | Y |
/// | desc | Field description | string | Y |
/// | deprecation | Field deprecated | bool | Y |
/// | deprecation | Field deprecation reason | string | Y |
/// | derived | Generate derived fields *[See also the Book](https://async-graphql.github.io/async-graphql/en/derived_fields.html).* | object | 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 |
@ -532,15 +535,23 @@ pub use async_graphql_derive::SimpleObject;
/// | guard | Field of guard *[See also the Book](https://async-graphql.github.io/async-graphql/en/field_guard.html)* | string | 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 |
/// | secret | Mark this field as a secret, it will not output the actual value in the log. | bool | Y |
/// | complexity | Custom field complexity. *[See also the Book](https://async-graphql.github.io/async-graphql/en/depth_and_complexity.html).* | bool | Y |
/// | complexity | Custom field complexity. | string | Y |
/// | derived | Generate derived fields *[See also the Book](https://async-graphql.github.io/async-graphql/en/derived_fields.html).* | object | Y |
///
/// # Derived argument parameters
/// # Field argument attributes
///
/// | Attribute | description | Type | Optional |
/// |--------------|------------------------------------------|------------ |----------|
/// | name | Generated derived field name | string | N |
/// | into | Type to derived an into | string | Y |
/// | with | Function to apply to manage advanced use cases | string| Y |
/// | name | Argument name | string | Y |
/// | desc | Argument description | string | Y |
/// | default | Use `Default::default` for default value | none | Y |
/// | default | Argument default value | literal | Y |
/// | default_with | Expression to generate default value | code string | Y |
/// | validator | Input value validator *[See also the Book](https://async-graphql.github.io/async-graphql/en/input_value_validators.html)* | object | 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 |
/// | secret | Mark this field as a secret, it will not output the actual value in the log. | bool | Y |
///
/// # Examples
///
@ -561,17 +572,17 @@ pub use async_graphql_derive::SimpleObject;
/// }
/// }
///
/// struct QueryRoot;
/// struct Query;
///
/// #[Object]
/// impl QueryRoot {
/// impl Query {
/// 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 schema = Schema::new(Query, EmptyMutation, EmptySubscription);
/// let res = schema.execute("{ obj { a b c } }").await.into_result().unwrap().data;
/// assert_eq!(res, value!({
/// "obj": {
@ -588,7 +599,7 @@ pub use async_graphql_derive::ComplexObject;
///
/// *[See also the Book](https://async-graphql.github.io/async-graphql/en/define_enum.html).*
///
/// # Macro parameters
/// # Macro attributes
///
/// | Attribute | description | Type | Optional |
/// |--------------|---------------------------|----------|----------|
@ -598,7 +609,7 @@ pub use async_graphql_derive::ComplexObject;
/// | 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 |
///
/// # Item parameters
/// # Item attributes
///
/// | Attribute | description | Type | Optional |
/// |-------------|---------------------------|----------|----------|
@ -619,13 +630,13 @@ pub use async_graphql_derive::ComplexObject;
/// #[graphql(name = "b")] B,
/// }
///
/// struct QueryRoot {
/// struct Query {
/// value1: MyEnum,
/// value2: MyEnum,
/// }
///
/// #[Object]
/// impl QueryRoot {
/// impl Query {
/// /// value1
/// async fn value1(&self) -> MyEnum {
/// self.value1
@ -638,7 +649,7 @@ pub use async_graphql_derive::ComplexObject;
/// }
///
/// tokio::runtime::Runtime::new().unwrap().block_on(async move {
/// let schema = Schema::new(QueryRoot{ value1: MyEnum::A, value2: MyEnum::B }, EmptyMutation, EmptySubscription);
/// let schema = Schema::new(Query{ value1: MyEnum::A, value2: MyEnum::B }, EmptyMutation, EmptySubscription);
/// let res = schema.execute("{ value1 value2 }").await.into_result().unwrap().data;
/// assert_eq!(res, value!({ "value1": "A", "value2": "b" }));
/// });
@ -649,7 +660,7 @@ pub use async_graphql_derive::Enum;
///
/// *[See also the Book](https://async-graphql.github.io/async-graphql/en/define_input_object.html).*
///
/// # Macro parameters
/// # Macro attributes
///
/// | Attribute | description | Type | Optional |
/// |---------------|---------------------------|----------|----------|
@ -658,7 +669,7 @@ pub use async_graphql_derive::Enum;
/// | 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 |
///
/// # Field parameters
/// # Field attributes
///
/// | Attribute | description | Type | Optional |
/// |--------------|------------------------------------------|-------------|----------|
@ -685,10 +696,10 @@ pub use async_graphql_derive::Enum;
/// b: i32,
/// }
///
/// struct QueryRoot;
/// struct Query;
///
/// #[Object]
/// impl QueryRoot {
/// impl Query {
/// /// value
/// async fn value(&self, input: MyInputObject) -> i32 {
/// input.a * input.b
@ -696,7 +707,7 @@ pub use async_graphql_derive::Enum;
/// }
///
/// tokio::runtime::Runtime::new().unwrap().block_on(async move {
/// let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription);
/// let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
/// let res = schema.execute(r#"
/// {
/// value1: value(input:{a:9, b:3})
@ -711,7 +722,7 @@ pub use async_graphql_derive::InputObject;
///
/// *[See also the Book](https://async-graphql.github.io/async-graphql/en/define_interface.html).*
///
/// # Macro parameters
/// # Macro attributes
///
/// | Attribute | description | Type | Optional |
/// |---------------|---------------------------|----------|----------|
@ -723,7 +734,7 @@ pub use async_graphql_derive::InputObject;
/// | 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 |
///
/// # Field parameters
/// # Field attributes
///
/// | Attribute | description | Type | Optional |
/// |-------------|---------------------------|----------|----------|
@ -740,7 +751,7 @@ pub use async_graphql_derive::InputObject;
/// | 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 |
///
/// # Field argument parameters
/// # Field argument attributes
///
/// | Attribute | description | Type | Optional |
/// |--------------|------------------------------------------|-------------|----------|
@ -817,17 +828,17 @@ pub use async_graphql_derive::InputObject;
/// TypeA(TypeA)
/// }
///
/// struct QueryRoot;
/// struct Query;
///
/// #[Object]
/// impl QueryRoot {
/// impl Query {
/// async fn type_a(&self) -> MyInterface {
/// TypeA { value: 10 }.into()
/// }
/// }
///
/// tokio::runtime::Runtime::new().unwrap().block_on(async move {
/// let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription).data("hello".to_string()).finish();
/// let schema = Schema::build(Query, EmptyMutation, EmptySubscription).data("hello".to_string()).finish();
/// let res = schema.execute(r#"
/// {
/// typeA {
@ -853,7 +864,7 @@ pub use async_graphql_derive::Interface;
///
/// *[See also the Book](https://async-graphql.github.io/async-graphql/en/define_union.html).*
///
/// # Macro parameters
/// # Macro attributes
///
/// | Attribute | description | Type | Optional |
/// |-------------|---------------------------|----------|----------|
@ -861,7 +872,7 @@ pub use async_graphql_derive::Interface;
/// | 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 |
///
/// # Item parameters
/// # Item attributes
///
/// | Attribute | description | Type | Optional |
/// |--------------|------------------------------------------|----------|----------|
@ -890,17 +901,17 @@ pub use async_graphql_derive::Interface;
/// TypeB(TypeB),
/// }
///
/// struct QueryRoot;
/// struct Query;
///
/// #[Object]
/// impl QueryRoot {
/// impl Query {
/// async fn all_data(&self) -> Vec<MyUnion> {
/// vec![TypeA { value_a: 10 }.into(), TypeB { value_b: 20 }.into()]
/// }
/// }
///
/// tokio::runtime::Runtime::new().unwrap().block_on(async move {
/// let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription).data("hello".to_string()).finish();
/// let schema = Schema::build(Query, EmptyMutation, EmptySubscription).data("hello".to_string()).finish();
/// let res = schema.execute(r#"
/// {
/// allData {
@ -931,7 +942,7 @@ pub use async_graphql_derive::Union;
/// Starting with the third parameter is one or more filtering conditions, The filter condition is the parameter of the field.
/// The filter function should be synchronous.
///
/// # Macro parameters
/// # Macro attributes
///
/// | Attribute | description | Type | Optional |
/// |---------------|---------------------------|----------|----------|
@ -943,7 +954,7 @@ pub use async_graphql_derive::Union;
/// | visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y |
/// | use_type_description | Specifies that the description of the type is on the type declaration. [`Description`]()(derive.Description.html) | bool | Y |
///
/// # Field parameters
/// # Field attributes
///
/// | Attribute | description | Type | Optional |
/// |-------------|---------------------------|----------|----------|
@ -953,9 +964,11 @@ pub use async_graphql_derive::Union;
/// | guard | Field of guard *[See also the Book](https://async-graphql.github.io/async-graphql/en/field_guard.html)* | string | 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 |
/// | complexity | Custom field complexity. *[See also the Book](https://async-graphql.github.io/async-graphql/en/depth_and_complexity.html).* | bool | Y |
/// | complexity | Custom field complexity. | string | Y |
/// | secret | Mark this field as a secret, it will not output the actual value in the log. | bool | Y |
///
/// # Field argument parameters
/// # Field argument attributes
///
/// | Attribute | description | Type | Optional |
/// |--------------|------------------------------------------|------------ |----------|
@ -988,7 +1001,7 @@ pub use async_graphql_derive::Subscription;
/// Define a Scalar
///
/// # Macro parameters
/// # Macro attributes
///
/// | Attribute | description | Type | Optional |
/// |-------------|---------------------------|----------|----------|
@ -1001,7 +1014,7 @@ pub use async_graphql_derive::Scalar;
///
/// It also implements `From<InnerType>` and `Into<InnerType>`.
///
/// # Macro parameters
/// # Macro attributes
///
/// | Attribute | description | Type | Optional |
/// |-------------|---------------------------|----------|----------|
@ -1021,10 +1034,10 @@ pub use async_graphql_derive::Scalar;
/// #[derive(NewType)]
/// struct Weight(f64);
///
/// struct QueryRoot;
/// struct Query;
///
/// #[Object]
/// impl QueryRoot {
/// impl Query {
/// async fn value(&self) -> Weight {
/// Weight(1.234)
/// }
@ -1035,7 +1048,7 @@ pub use async_graphql_derive::Scalar;
/// let weight_f64: f64 = weight.into();
///
/// tokio::runtime::Runtime::new().unwrap().block_on(async move {
/// let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription).data("hello".to_string()).finish();
/// let schema = Schema::build(Query, EmptyMutation, EmptySubscription).data("hello".to_string()).finish();
///
/// let res = schema.execute("{ value }").await.into_result().unwrap().data;
/// assert_eq!(res, value!({
@ -1044,7 +1057,7 @@ pub use async_graphql_derive::Scalar;
///
/// let res = schema.execute(r#"
/// {
/// __type(name: "QueryRoot") {
/// __type(name: "Query") {
/// fields {
/// name type {
/// kind
@ -1079,17 +1092,17 @@ pub use async_graphql_derive::Scalar;
/// #[graphql(name)] // or: #[graphql(name = true)], #[graphql(name = "Weight")]
/// struct Weight(f64);
///
/// struct QueryRoot;
/// struct Query;
///
/// #[Object]
/// impl QueryRoot {
/// impl Query {
/// async fn value(&self) -> Weight {
/// Weight(1.234)
/// }
/// }
///
/// tokio::runtime::Runtime::new().unwrap().block_on(async move {
/// let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription).data("hello".to_string()).finish();
/// let schema = Schema::build(Query, EmptyMutation, EmptySubscription).data("hello".to_string()).finish();
///
/// let res = schema.execute("{ value }").await.into_result().unwrap().data;
/// assert_eq!(res, value!({
@ -1098,7 +1111,7 @@ pub use async_graphql_derive::Scalar;
///
/// let res = schema.execute(r#"
/// {
/// __type(name: "QueryRoot") {
/// __type(name: "Query") {
/// fields {
/// name type {
/// kind
@ -1135,7 +1148,7 @@ pub use async_graphql_derive::NewType;
///
/// *[See also the Book](https://async-graphql.github.io/async-graphql/en/merging_objects.html).*
///
/// # Macro parameters
/// # Macro attributes
///
/// | Attribute | description | Type | Optional |
/// |---------------|---------------------------|----------|----------|
@ -1177,7 +1190,7 @@ pub use async_graphql_derive::MergedObject;
///
/// *[See also the Book](https://async-graphql.github.io/async-graphql/en/merging_objects.html).*
///
/// # Macro parameters
/// # Macro attributes
///
/// | Attribute | description | Type | Optional |
/// |---------------|---------------------------|----------|----------|
@ -1258,3 +1271,79 @@ pub use async_graphql_derive::MergedSubscription;
/// });
/// ```
pub use async_graphql_derive::Description;
/// Define a directive for query.
///
/// *[See also the Book](https://async-graphql.github.io/async-graphql/en/custom_directive.html).*
///
/// # Macro attributes
///
/// | Attribute | description | Type | Optional |
/// |---------------|---------------------------|----------|----------|
/// | name | Object name | string | 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 |
/// | repeatable | It means that the directive can be used multiple times in the same location. | bool | 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 |
/// | locations | Specify the location where the directive is available, multiples are allowed. The possible values is "field", ... | string | N |
///
/// # Directive attributes
///
/// | Attribute | description | Type | Optional |
/// |--------------|------------------------------------------|------------ |----------|
/// | name | Argument name | string | Y |
/// | desc | Argument description | string | Y |
/// | default | Use `Default::default` for default value | none | Y |
/// | default | Argument default value | literal | Y |
/// | default_with | Expression to generate default value | code string | Y |
/// | validator | Input value validator *[See also the Book](https://async-graphql.github.io/async-graphql/en/input_value_validators.html)* | object | 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 |
/// | secret | Mark this field as a secret, it will not output the actual value in the log. | bool | Y |
///
/// # Examples
///
/// ```rust
/// use async_graphql::*;
///
/// struct ConcatDirective {
/// value: String,
/// }
///
/// #[async_trait::async_trait]
/// impl CustomDirective for ConcatDirective {
/// async fn resolve_field(&self, _ctx: &Context<'_>, resolve: ResolveFut<'_>) -> ServerResult<Option<Value>> {
/// resolve.await.map(|value| {
/// value.map(|value| match value {
/// Value::String(str) => Value::String(str + &self.value),
/// _ => value,
/// })
/// })
/// }
/// }
///
/// #[Directive(location = "field")]
/// fn concat(value: String) -> impl CustomDirective {
/// ConcatDirective { value }
/// }
///
/// struct Query;
///
/// #[Object]
/// impl Query {
/// async fn value(&self) -> &'static str {
/// "abc"
/// }
/// }
///
/// tokio::runtime::Runtime::new().unwrap().block_on(async move {
/// let schema = Schema::build(Query, EmptyMutation, EmptySubscription)
/// .directive(concat)
/// .finish();
/// let res = schema.execute(r#"{ value @concat(value: "def") }"#).await.into_result().unwrap().data;
/// assert_eq!(res, value!({
/// "value": "abcdef",
/// }));
/// });
/// ```
pub use async_graphql_derive::Directive;

View File

@ -7,6 +7,7 @@ use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use indexmap::map::IndexMap;
use indexmap::set::IndexSet;
pub use crate::model::__DirectiveLocation;
use crate::parser::types::{
BaseType as ParsedBaseType, Field, Type as ParsedType, VariableDefinition,
};
@ -350,6 +351,7 @@ pub struct MetaDirective {
pub locations: Vec<model::__DirectiveLocation>,
pub args: IndexMap<&'static str, MetaInputValue>,
pub is_repeatable: bool,
pub visible: Option<MetaVisibleFn>,
}
#[derive(Default)]
@ -763,6 +765,12 @@ impl Registry {
}
}
for directive in self.directives.values() {
for arg in directive.args.values() {
traverse_input_value(&self.types, &mut used_types, arg);
}
}
for type_name in Some(&self.query_type)
.into_iter()
.chain(self.mutation_type.iter())
@ -886,6 +894,14 @@ impl Registry {
}
}
for directive in self.directives.values() {
if is_visible(ctx, &directive.visible) {
for arg in directive.args.values() {
traverse_input_value(ctx, &self.types, &mut visible_types, arg);
}
}
}
for type_name in Some(&self.query_type)
.into_iter()
.chain(self.mutation_type.iter())

View File

@ -1,3 +1,4 @@
use futures_util::FutureExt;
use std::future::Future;
use std::pin::Pin;
@ -5,7 +6,9 @@ use indexmap::IndexMap;
use crate::extensions::ResolveInfo;
use crate::parser::types::Selection;
use crate::{Context, ContextSelectionSet, Name, OutputType, ServerError, ServerResult, Value};
use crate::{
Context, ContextBase, ContextSelectionSet, Name, OutputType, ServerError, ServerResult, Value,
};
/// Represents a GraphQL container object.
///
@ -152,14 +155,14 @@ impl<'a> Fields<'a> {
continue;
}
self.0.push(Box::pin({
let resolve_fut = Box::pin({
let ctx = ctx.clone();
async move {
let ctx_field = ctx.with_field(field);
let field_name = ctx_field.item.node.response_key().node.clone();
let extensions = &ctx.query_env.extensions;
if extensions.is_empty() {
if extensions.is_empty() && field.node.directives.is_empty() {
Ok((
field_name,
root.resolve_field(&ctx_field).await?.unwrap_or_default(),
@ -199,17 +202,57 @@ impl<'a> Fields<'a> {
};
let resolve_fut = root.resolve_field(&ctx_field);
futures_util::pin_mut!(resolve_fut);
Ok((
field_name,
extensions
.resolve(resolve_info, &mut resolve_fut)
.await?
.unwrap_or_default(),
))
if field.node.directives.is_empty() {
futures_util::pin_mut!(resolve_fut);
Ok((
field_name,
extensions
.resolve(resolve_info, &mut resolve_fut)
.await?
.unwrap_or_default(),
))
} else {
let mut resolve_fut = resolve_fut.boxed();
for directive in &field.node.directives {
if let Some(directive_factory) = ctx
.schema_env
.custom_directives
.get(directive.node.name.node.as_str())
{
let ctx_directive = ContextBase {
path_node: ctx_field.path_node,
item: directive,
schema_env: ctx_field.schema_env,
query_env: ctx_field.query_env,
};
let directive_instance = directive_factory
.create(&ctx_directive, &directive.node)?;
resolve_fut = Box::pin({
let ctx_field = ctx_field.clone();
async move {
directive_instance
.resolve_field(&ctx_field, &mut resolve_fut)
.await
}
});
}
}
Ok((
field_name,
extensions
.resolve(resolve_info, &mut resolve_fut)
.await?
.unwrap_or_default(),
))
}
}
}
}));
});
self.0.push(resolve_fut);
}
selection => {
let (type_condition, selection_set) = match selection {

View File

@ -1,4 +1,5 @@
use std::any::Any;
use std::collections::HashMap;
use std::ops::Deref;
use std::sync::Arc;
@ -6,6 +7,7 @@ use futures_util::stream::{self, Stream, StreamExt};
use indexmap::map::IndexMap;
use crate::context::{Data, QueryEnvInner};
use crate::custom_directive::CustomDirectiveFactory;
use crate::extensions::{ExtensionFactory, Extensions};
use crate::model::__DirectiveLocation;
use crate::parser::types::{Directive, DocumentOperations, OperationType, Selection, SelectionSet};
@ -31,6 +33,7 @@ pub struct SchemaBuilder<Query, Mutation, Subscription> {
complexity: Option<usize>,
depth: Option<usize>,
extensions: Vec<Box<dyn ExtensionFactory>>,
custom_directives: HashMap<&'static str, Box<dyn CustomDirectiveFactory>>,
}
impl<Query, Mutation, Subscription> SchemaBuilder<Query, Mutation, Subscription> {
@ -131,6 +134,27 @@ impl<Query, Mutation, Subscription> SchemaBuilder<Query, Mutation, Subscription>
self
}
/// Register a custom directive.
///
/// # Panics
///
/// Panics if the directive with the same name is already registered.
pub fn directive<T: CustomDirectiveFactory>(mut self, directive: T) -> Self {
let name = directive.name();
let instance = Box::new(directive);
instance.register(&mut self.registry);
if name == "skip"
|| name == "include"
|| self.custom_directives.insert(name, instance).is_some()
{
panic!("Directive `{}` already exists", name);
}
self
}
/// Build schema.
pub fn finish(mut self) -> Schema<Query, Mutation, Subscription> {
// federation
@ -149,6 +173,7 @@ impl<Query, Mutation, Subscription> SchemaBuilder<Query, Mutation, Subscription>
env: SchemaEnv(Arc::new(SchemaEnvInner {
registry: self.registry,
data: self.data,
custom_directives: self.custom_directives,
})),
}))
}
@ -158,6 +183,7 @@ impl<Query, Mutation, Subscription> SchemaBuilder<Query, Mutation, Subscription>
pub struct SchemaEnvInner {
pub registry: Registry,
pub data: Data,
pub custom_directives: HashMap<&'static str, Box<dyn CustomDirectiveFactory>>,
}
#[doc(hidden)]
@ -244,6 +270,7 @@ where
complexity: None,
depth: None,
extensions: Default::default(),
custom_directives: Default::default(),
}
}
@ -289,6 +316,7 @@ where
args
},
is_repeatable: false,
visible: None,
});
registry.add_directive(MetaDirective {
@ -312,6 +340,7 @@ where
args
},
is_repeatable: false,
visible: None,
});
// register scalars

View File

@ -76,3 +76,54 @@ pub async fn test_directive_include() {
})
);
}
#[tokio::test]
pub async fn test_custom_directive() {
struct Concat {
prefix: String,
suffix: String,
}
#[async_trait::async_trait]
impl CustomDirective for Concat {
async fn resolve_field(
&self,
_ctx: &Context<'_>,
resolve: ResolveFut<'_>,
) -> ServerResult<Option<Value>> {
resolve.await.map(|value| {
value.map(|value| match value {
Value::String(str) => Value::String(self.prefix.clone() + &str + &self.suffix),
_ => value,
})
})
}
}
#[Directive(location = "field")]
fn concat(prefix: String, suffix: String) -> impl CustomDirective {
Concat { prefix, suffix }
}
struct QueryRoot;
#[Object]
impl QueryRoot {
pub async fn value(&self) -> &'static str {
"abc"
}
}
let schema = Schema::build(QueryRoot, EmptyMutation, EmptySubscription)
.directive(concat)
.finish();
assert_eq!(
schema
.execute(r#"{ value @concat(prefix: "&", suffix: "*") }"#)
.await
.into_result()
.unwrap()
.data,
value!({ "value": "&abc*" })
);
}

View File

@ -261,6 +261,30 @@ pub async fn test_visible_fn() {
#[tokio::test]
pub async fn test_indirect_hiding_type() {
#[derive(Enum, Eq, PartialEq, Copy, Clone)]
enum MyEnum1 {
A,
}
#[derive(Enum, Eq, PartialEq, Copy, Clone)]
enum MyEnum2 {
A,
}
struct MyDirective;
impl CustomDirective for MyDirective {}
#[Directive(location = "field")]
fn my_directive1(_a: MyEnum1) -> impl CustomDirective {
MyDirective
}
#[Directive(location = "field", visible = false)]
fn my_directive2(_a: MyEnum2) -> impl CustomDirective {
MyDirective
}
#[derive(SimpleObject)]
struct MyObj1 {
a: i32,
@ -380,7 +404,10 @@ pub async fn test_indirect_hiding_type() {
}
}
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
let schema = Schema::build(Query, EmptyMutation, EmptySubscription)
.directive(my_directive1)
.directive(my_directive2)
.finish();
assert_eq!(
schema
.execute(r#"{ __type(name: "MyObj1") { name } }"#)
@ -530,6 +557,26 @@ pub async fn test_indirect_hiding_type() {
.data,
value!({ "__type": null })
);
assert_eq!(
schema
.execute(r#"{ __type(name: "MyEnum1") { name } }"#)
.await
.into_result()
.unwrap()
.data,
value!({ "__type": { "name": "MyEnum1" } })
);
assert_eq!(
schema
.execute(r#"{ __type(name: "MyEnum2") { name } }"#)
.await
.into_result()
.unwrap()
.data,
value!({ "__type": null })
);
}
#[tokio::test]