From 1b50738af7cec77db271a09142aa45eef588e020 Mon Sep 17 00:00:00 2001 From: Sunli Date: Fri, 19 Nov 2021 18:49:37 +0800 Subject: [PATCH] Custom directive --- CHANGELOG.md | 4 + derive/src/args.rs | 28 ++++ derive/src/directive.rs | 174 +++++++++++++++++++++ derive/src/lib.rs | 18 ++- derive/src/validators.rs | 4 + docs/en/src/SUMMARY.md | 1 + docs/en/src/custom_directive.md | 39 +++++ docs/zh-CN/src/SUMMARY.md | 2 +- docs/zh-CN/src/custom_directive.md | 38 +++++ docs/zh-CN/src/custom_extensions.md | 7 - feature-comparison.md | 1 + src/context.rs | 56 +++++-- src/custom_directive.rs | 31 ++++ src/extensions/mod.rs | 3 +- src/lib.rs | 225 +++++++++++++++++++--------- src/registry/mod.rs | 16 ++ src/resolver_utils/container.rs | 67 +++++++-- src/schema.rs | 29 ++++ tests/directive.rs | 51 +++++++ tests/introspection_visible.rs | 49 +++++- 20 files changed, 738 insertions(+), 105 deletions(-) create mode 100644 derive/src/directive.rs create mode 100644 docs/en/src/custom_directive.md create mode 100644 docs/zh-CN/src/custom_directive.md delete mode 100644 docs/zh-CN/src/custom_extensions.md create mode 100644 src/custom_directive.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index dff26336..7971c975 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/derive/src/args.rs b/derive/src/args.rs index 9c48c35e..88e69952 100644 --- a/derive/src/args.rs +++ b/derive/src/args.rs @@ -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, pub complexity: Option, } + +#[derive(FromMeta, Default)] +#[darling(default)] +pub struct Directive { + pub internal: bool, + pub name: Option, + pub visible: Option, + pub repeatable: bool, + pub rename_args: Option, + #[darling(multiple, rename = "location")] + pub locations: Vec, +} + +#[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"), + } + } +} diff --git a/derive/src/directive.rs b/derive/src/directive.rs new file mode 100644 index 00000000..e7c3e916 --- /dev/null +++ b/derive/src/directive.rs @@ -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 { + 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::(&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::>(); + + 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> { + #item_fn + + #(#get_params)* + Ok(::std::boxed::Box::new(#ident(#(#use_params),*))) + } + } + }; + + Ok(expanded.into()) +} diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 5d1d10e9..ad28e8cf 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -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(), + } +} diff --git a/derive/src/validators.rs b/derive/src/validators.rs index bc84a646..8da2ceb9 100644 --- a/derive/src/validators.rs +++ b/derive/src/validators.rs @@ -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 { diff --git a/docs/en/src/SUMMARY.md b/docs/en/src/SUMMARY.md index 479dfe0f..9e4fdd3d 100644 --- a/docs/en/src/SUMMARY.md +++ b/docs/en/src/SUMMARY.md @@ -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) diff --git a/docs/en/src/custom_directive.md b/docs/en/src/custom_directive.md new file mode 100644 index 00000000..787bc7d1 --- /dev/null +++ b/docs/en/src/custom_directive.md @@ -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> { + 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(); +``` diff --git a/docs/zh-CN/src/SUMMARY.md b/docs/zh-CN/src/SUMMARY.md index 8b093b55..3d2e86e0 100644 --- a/docs/zh-CN/src/SUMMARY.md +++ b/docs/zh-CN/src/SUMMARY.md @@ -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) diff --git a/docs/zh-CN/src/custom_directive.md b/docs/zh-CN/src/custom_directive.md new file mode 100644 index 00000000..c4a43b38 --- /dev/null +++ b/docs/zh-CN/src/custom_directive.md @@ -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> { + 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(); +``` diff --git a/docs/zh-CN/src/custom_extensions.md b/docs/zh-CN/src/custom_extensions.md deleted file mode 100644 index f867afa3..00000000 --- a/docs/zh-CN/src/custom_extensions.md +++ /dev/null @@ -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)来实现自己的扩展类型。 \ No newline at end of file diff --git a/feature-comparison.md b/feature-comparison.md index 62a6c736..952d2961 100644 --- a/feature-comparison.md +++ b/feature-comparison.md @@ -17,6 +17,7 @@ Comparing Features of Other Rust GraphQL Implementations | Dataloading | 👍 | 👍 | | Custom Scalar | 👍 | 👍 | | Custom Error | 👍 | 👍 | +| Custom Directive | 👍 | ⛔ | | Extensions | 👍 | ⛔️ | | Cursor Connections | 👍 | ⛔️ | | Query complexity/depth | 👍 | ⛔️ | diff --git a/src/context.rs b/src/context.rs index a236b9f8..e546a8d9 100644 --- a/src/context.rs +++ b/src/context.rs @@ -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> /// Context object for resolve field pub type Context<'a> = ContextBase<'a, &'a Positioned>; +/// Context object for execute directive. +pub type ContextDirective<'a> = ContextBase<'a, &'a Positioned>; + /// 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( + &self, + arguments: &[(Positioned, Positioned)], + name: &str, + default: Option 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> { @@ -521,19 +550,7 @@ impl<'a> ContextBase<'a, &'a Positioned> { name: &str, default: Option 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> { } } +impl<'a> ContextBase<'a, &'a Positioned> { + #[doc(hidden)] + pub fn param_value( + &self, + name: &str, + default: Option T>, + ) -> ServerResult<(Pos, T)> { + self.get_param_value(&self.item.node.arguments, name, default) + } +} + /// Selection field. #[derive(Clone, Copy)] pub struct SelectionField<'a> { diff --git a/src/custom_directive.rs b/src/custom_directive.rs new file mode 100644 index 00000000..da7d9e98 --- /dev/null +++ b/src/custom_directive.rs @@ -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>; +} + +/// 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> { + resolve.await + } +} diff --git a/src/extensions/mod.rs b/src/extensions/mod.rs index 40aec1e9..0b12691d 100644 --- a/src/extensions/mod.rs +++ b/src/extensions/mod.rs @@ -120,7 +120,8 @@ type ValidationFut<'a> = type ExecuteFut<'a> = &'a mut (dyn Future + Send + Unpin); -type ResolveFut<'a> = &'a mut (dyn Future>> + Send + Unpin); +/// A future type used to resolve the field +pub type ResolveFut<'a> = &'a mut (dyn Future>> + Send + Unpin); /// The remainder of a extension chain for request. pub struct NextRequest<'a> { diff --git a/src/lib.rs b/src/lib.rs index a41c3eee..4b131709 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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 = Result; /// /// All methods are converted to camelCase. /// -/// # Macro parameters +/// # Macro attributes /// /// | Attribute | description | Type | Optional | /// |---------------|---------------------------|----------|----------| @@ -258,8 +261,9 @@ pub type FieldResult = Result; /// | 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 = Result; /// | 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 = Result; /// | 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 = Result; /// /// ```ignore /// #[Object] -/// impl QueryRoot { +/// impl Query { /// async fn value(&self, ctx: &Context<'_>) -> { ... } /// } /// ``` @@ -334,12 +337,12 @@ pub type FieldResult = Result; /// ```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 = Result; /// } /// /// 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 = Result; /// } /// } /// -/// struct QueryRoot; +/// struct Query; /// /// #[Object] -/// impl QueryRoot { +/// impl Query { /// async fn objs(&self) -> Vec> { /// vec![ /// Box::new(MyObj("a".to_string())), @@ -419,7 +422,7 @@ pub type FieldResult = Result; /// } /// /// 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 { /// 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` and `Into`. /// -/// # 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> { +/// 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; diff --git a/src/registry/mod.rs b/src/registry/mod.rs index c5f06397..c01d02e2 100644 --- a/src/registry/mod.rs +++ b/src/registry/mod.rs @@ -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, pub args: IndexMap<&'static str, MetaInputValue>, pub is_repeatable: bool, + pub visible: Option, } #[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()) diff --git a/src/resolver_utils/container.rs b/src/resolver_utils/container.rs index 2364e25b..5e962bbc 100644 --- a/src/resolver_utils/container.rs +++ b/src/resolver_utils/container.rs @@ -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 { diff --git a/src/schema.rs b/src/schema.rs index 4c63a592..362b1046 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -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 { complexity: Option, depth: Option, extensions: Vec>, + custom_directives: HashMap<&'static str, Box>, } impl SchemaBuilder { @@ -131,6 +134,27 @@ impl SchemaBuilder self } + /// Register a custom directive. + /// + /// # Panics + /// + /// Panics if the directive with the same name is already registered. + pub fn directive(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 { // federation @@ -149,6 +173,7 @@ impl SchemaBuilder env: SchemaEnv(Arc::new(SchemaEnvInner { registry: self.registry, data: self.data, + custom_directives: self.custom_directives, })), })) } @@ -158,6 +183,7 @@ impl SchemaBuilder pub struct SchemaEnvInner { pub registry: Registry, pub data: Data, + pub custom_directives: HashMap<&'static str, Box>, } #[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 diff --git a/tests/directive.rs b/tests/directive.rs index 8ed815b7..d10fd924 100644 --- a/tests/directive.rs +++ b/tests/directive.rs @@ -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> { + 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*" }) + ); +} diff --git a/tests/introspection_visible.rs b/tests/introspection_visible.rs index 14a8578f..4817aba4 100644 --- a/tests/introspection_visible.rs +++ b/tests/introspection_visible.rs @@ -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]