From e29b7a3627eb4598e8456dc40118e0cfb3aa9820 Mon Sep 17 00:00:00 2001 From: Sunli Date: Fri, 11 Dec 2020 16:03:28 +0800 Subject: [PATCH] Add `visible` attributes on types, fields, and parameters, allowing some content to be hidden based on conditions. --- derive/src/args.rs | 50 ++++- derive/src/enum.rs | 7 +- derive/src/input_object.rs | 8 +- derive/src/interface.rs | 10 +- derive/src/merged_object.rs | 4 +- derive/src/merged_subscription.rs | 4 +- derive/src/object.rs | 9 +- derive/src/scalar.rs | 6 +- derive/src/simple_object.rs | 8 +- derive/src/subscription.rs | 8 +- derive/src/union.rs | 6 +- derive/src/utils.rs | 12 ++ src/lib.rs | 32 +++ src/model/field.rs | 8 +- src/model/schema.rs | 12 +- src/model/type.rs | 20 +- src/registry/mod.rs | 35 +++- src/resolver_utils/scalar.rs | 1 + src/schema.rs | 2 + src/types/connection/connection_type.rs | 3 + src/types/connection/edge.rs | 3 + src/types/empty_mutation.rs | 1 + src/types/empty_subscription.rs | 1 + src/types/json.rs | 1 + src/types/merged_object.rs | 1 + src/types/query_root.rs | 4 + src/types/upload.rs | 1 + tests/introspection_visible.rs | 254 ++++++++++++++++++++++++ 28 files changed, 489 insertions(+), 22 deletions(-) create mode 100644 tests/introspection_visible.rs diff --git a/derive/src/args.rs b/derive/src/args.rs index 9e823d88..5ecca955 100644 --- a/derive/src/args.rs +++ b/derive/src/args.rs @@ -2,7 +2,7 @@ use darling::ast::{Data, Fields}; use darling::util::Ignored; use darling::{FromDeriveInput, FromField, FromMeta, FromVariant}; use inflector::Inflector; -use syn::{Attribute, Generics, Ident, Lit, LitStr, Meta, Type, Visibility}; +use syn::{Attribute, Generics, Ident, Lit, LitBool, LitStr, Meta, Type, Visibility}; #[derive(FromMeta)] #[darling(default)] @@ -44,6 +44,24 @@ impl FromMeta for DefaultValue { } } +#[derive(Debug)] +pub enum Visible { + None, + HiddenAlways, + FnName(String), +} + +impl FromMeta for Visible { + fn from_value(value: &Lit) -> darling::Result { + match value { + Lit::Bool(LitBool { value: true, .. }) => Ok(Visible::None), + Lit::Bool(LitBool { value: false, .. }) => Ok(Visible::HiddenAlways), + Lit::Str(str) => Ok(Visible::FnName(str.value())), + _ => Err(darling::Error::unexpected_lit_type(value)), + } + } +} + #[derive(FromField)] #[darling(attributes(graphql), forward_attrs(doc))] pub struct SimpleObjectField { @@ -70,6 +88,8 @@ pub struct SimpleObjectField { pub requires: Option, #[darling(default)] pub guard: Option, + #[darling(default)] + pub visible: Option, } #[derive(FromDeriveInput)] @@ -94,6 +114,8 @@ pub struct SimpleObject { pub cache_control: CacheControl, #[darling(default)] pub extends: bool, + #[darling(default)] + pub visible: Option, } #[derive(FromMeta, Default)] @@ -105,6 +127,7 @@ pub struct Argument { pub default_with: Option, pub validator: Option, pub key: bool, // for entity + pub visible: Option, } #[derive(FromMeta, Default)] @@ -117,6 +140,7 @@ pub struct Object { pub cache_control: CacheControl, pub extends: bool, pub use_type_description: bool, + pub visible: Option, } #[derive(FromMeta, Default)] @@ -131,6 +155,7 @@ pub struct ObjectField { pub provides: Option, pub requires: Option, pub guard: Option, + pub visible: Option, } #[derive(FromDeriveInput)] @@ -149,6 +174,8 @@ pub struct Enum { pub rename_items: Option, #[darling(default)] pub remote: Option, + #[darling(default)] + pub visible: Option, } #[derive(FromVariant)] @@ -162,6 +189,8 @@ pub struct EnumItem { pub name: Option, #[darling(default)] pub deprecation: Option, + #[darling(default)] + pub visible: Option, } #[derive(FromDeriveInput)] @@ -176,6 +205,8 @@ pub struct Union { pub internal: bool, #[darling(default)] pub name: Option, + #[darling(default)] + pub visible: Option, } #[derive(FromVariant)] @@ -206,6 +237,8 @@ pub struct InputObjectField { pub validator: Option, #[darling(default)] pub flatten: bool, + #[darling(default)] + pub visible: Option, } #[derive(FromDeriveInput)] @@ -222,6 +255,8 @@ pub struct InputObject { pub name: Option, #[darling(default)] pub rename_fields: Option, + #[darling(default)] + pub visible: Option, } #[derive(FromMeta)] @@ -235,6 +270,8 @@ pub struct InterfaceFieldArgument { pub default: Option, #[darling(default)] pub default_with: Option, + #[darling(default)] + pub visible: Option, } #[derive(FromMeta)] @@ -256,6 +293,8 @@ pub struct InterfaceField { pub provides: Option, #[darling(default)] pub requires: Option, + #[darling(default)] + pub visible: Option, } #[derive(FromVariant)] @@ -284,6 +323,8 @@ pub struct Interface { pub fields: Vec, #[darling(default)] pub extends: bool, + #[darling(default)] + pub visible: Option, } #[derive(FromMeta, Default)] @@ -292,6 +333,7 @@ pub struct Scalar { pub internal: bool, pub name: Option, pub use_type_description: bool, + pub visible: Option, } #[derive(FromMeta, Default)] @@ -312,6 +354,7 @@ pub struct SubscriptionFieldArgument { pub default: Option, pub default_with: Option, pub validator: Option, + pub visible: Option, } #[derive(FromMeta, Default)] @@ -321,6 +364,7 @@ pub struct SubscriptionField { pub name: Option, pub deprecation: Option, pub guard: Option, + pub visible: Option, } #[derive(FromField)] @@ -345,6 +389,8 @@ pub struct MergedObject { pub cache_control: CacheControl, #[darling(default)] pub extends: bool, + #[darling(default)] + pub visible: Option, } #[derive(FromField)] @@ -365,6 +411,8 @@ pub struct MergedSubscription { pub internal: bool, #[darling(default)] pub name: Option, + #[darling(default)] + pub visible: Option, } #[derive(Debug, Copy, Clone, FromMeta)] diff --git a/derive/src/enum.rs b/derive/src/enum.rs index 49626b56..dc38c89b 100644 --- a/derive/src/enum.rs +++ b/derive/src/enum.rs @@ -5,7 +5,7 @@ use syn::ext::IdentExt; use syn::Error; use crate::args::{self, RenameRuleExt, RenameTarget}; -use crate::utils::{get_crate_name, get_rustdoc, GeneratorResult}; +use crate::utils::{get_crate_name, get_rustdoc, visible_fn, GeneratorResult}; pub fn generate(enum_args: &args::Enum) -> GeneratorResult { let crate_name = get_crate_name(enum_args.internal); @@ -62,11 +62,14 @@ pub fn generate(enum_args: &args::Enum) -> GeneratorResult { value: #ident::#item_ident, } }); + + let visible = visible_fn(&variant.visible); schema_enum_items.push(quote! { enum_items.insert(#gql_item_name, #crate_name::registry::MetaEnumValue { name: #gql_item_name, description: #item_desc, deprecation: #item_deprecation, + visible: #visible, }); }); } @@ -119,6 +122,7 @@ pub fn generate(enum_args: &args::Enum) -> GeneratorResult { .into()); } + let visible = visible_fn(&enum_args.visible); let expanded = quote! { #[allow(clippy::all, clippy::pedantic)] impl #crate_name::resolver_utils::EnumType for #ident { @@ -143,6 +147,7 @@ pub fn generate(enum_args: &args::Enum) -> GeneratorResult { #(#schema_enum_items)* enum_items }, + visible: #visible, } }) } diff --git a/derive/src/input_object.rs b/derive/src/input_object.rs index 059de46b..9030b03a 100644 --- a/derive/src/input_object.rs +++ b/derive/src/input_object.rs @@ -6,7 +6,7 @@ use syn::Error; use crate::args::{self, RenameRuleExt, RenameTarget}; use crate::utils::{ - generate_default, generate_validator, get_crate_name, get_rustdoc, GeneratorResult, + generate_default, generate_validator, get_crate_name, get_rustdoc, visible_fn, GeneratorResult, }; pub fn generate(object_args: &args::InputObject) -> GeneratorResult { @@ -140,6 +140,7 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult }); fields.push(ident); + let visible = visible_fn(&field.visible); schema_fields.push(quote! { fields.insert(::std::borrow::ToOwned::to_owned(#name), #crate_name::registry::MetaInputValue { name: #name, @@ -147,6 +148,7 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult ty: <#ty as #crate_name::Type>::create_type_info(registry), default_value: #schema_default, validator: #validator, + visible: #visible, }); }) } @@ -159,6 +161,7 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult .into()); } + let visible = visible_fn(&object_args.visible); let expanded = quote! { #[allow(clippy::all, clippy::pedantic)] impl #crate_name::Type for #ident { @@ -174,7 +177,8 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult let mut fields = #crate_name::indexmap::IndexMap::new(); #(#schema_fields)* fields - } + }, + visible: #visible, }) } } diff --git a/derive/src/interface.rs b/derive/src/interface.rs index 82cead19..7b8cf8e3 100644 --- a/derive/src/interface.rs +++ b/derive/src/interface.rs @@ -8,7 +8,7 @@ use syn::{visit_mut, Error, Lifetime, Type}; use crate::args::{self, InterfaceField, InterfaceFieldArgument, RenameRuleExt, RenameTarget}; use crate::output_type::OutputType; -use crate::utils::{generate_default, get_crate_name, get_rustdoc, GeneratorResult}; +use crate::utils::{generate_default, get_crate_name, get_rustdoc, visible_fn, GeneratorResult}; pub fn generate(interface_args: &args::Interface) -> GeneratorResult { let crate_name = get_crate_name(interface_args.internal); @@ -139,6 +139,7 @@ pub fn generate(interface_args: &args::Interface) -> GeneratorResult GeneratorResult GeneratorResult GeneratorResult::create_type_info(registry), default_value: #schema_default, validator: ::std::option::Option::None, + visible: #visible, }); }); } @@ -257,6 +261,7 @@ pub fn generate(interface_args: &args::Interface) -> GeneratorResult GeneratorResult GeneratorResult GeneratorResult GeneratorResult { let crate_name = get_crate_name(object_args.internal); @@ -55,6 +55,7 @@ pub fn generate(object_args: &args::MergedObject) -> GeneratorResult GeneratorResult GeneratorResult { let crate_name = get_crate_name(object_args.internal); @@ -44,6 +44,7 @@ pub fn generate(object_args: &args::MergedSubscription) -> GeneratorResult), ); + let visible = visible_fn(&object_args.visible); let expanded = quote! { #[allow(clippy::all, clippy::pedantic)] impl #crate_name::Type for #ident { @@ -69,6 +70,7 @@ pub fn generate(object_args: &args::MergedSubscription) -> GeneratorResult quote!(::std::option::Option::None), }; + let visible = visible_fn(&visible); schema_args.push(quote! { args.insert(#name, #crate_name::registry::MetaInputValue { name: #name, @@ -359,6 +361,7 @@ pub fn generate( ty: <#ty as #crate_name::Type>::create_type_info(registry), default_value: #schema_default, validator: #validator, + visible: #visible, }); }); @@ -381,6 +384,7 @@ pub fn generate( } let schema_ty = ty.value_type(); + let visible = visible_fn(&method_args.visible); schema_fields.push(quote! { #(#cfg_attrs)* @@ -398,6 +402,7 @@ pub fn generate( external: #external, provides: #provides, requires: #requires, + visible: #visible, }); }); @@ -473,6 +478,7 @@ pub fn generate( .into()); } + let visible = visible_fn(&object_args.visible); let expanded = quote! { #item_impl @@ -494,6 +500,7 @@ pub fn generate( cache_control: #cache_control, extends: #extends, keys: ::std::option::Option::None, + visible: #visible, }); #(#create_entity_types)* #(#add_keys)* diff --git a/derive/src/scalar.rs b/derive/src/scalar.rs index 8d18af08..561ee384 100644 --- a/derive/src/scalar.rs +++ b/derive/src/scalar.rs @@ -3,7 +3,9 @@ use quote::quote; use syn::ItemImpl; use crate::args::{self, RenameTarget}; -use crate::utils::{get_crate_name, get_rustdoc, get_type_path_and_name, GeneratorResult}; +use crate::utils::{ + get_crate_name, get_rustdoc, get_type_path_and_name, visible_fn, GeneratorResult, +}; pub fn generate( scalar_args: &args::Scalar, @@ -27,6 +29,7 @@ pub fn generate( let self_ty = &item_impl.self_ty; let generic = &item_impl.generics; let where_clause = &item_impl.generics.where_clause; + let visible = visible_fn(&scalar_args.visible); let expanded = quote! { #item_impl @@ -41,6 +44,7 @@ pub fn generate( name: ::std::borrow::ToOwned::to_owned(#gql_typename), description: #desc, is_valid: |value| <#self_ty as #crate_name::ScalarType>::is_valid(value), + visible: #visible, }) } } diff --git a/derive/src/simple_object.rs b/derive/src/simple_object.rs index 55d79364..47f5447d 100644 --- a/derive/src/simple_object.rs +++ b/derive/src/simple_object.rs @@ -5,7 +5,7 @@ use syn::ext::IdentExt; use syn::Error; use crate::args::{self, RenameRuleExt, RenameTarget}; -use crate::utils::{generate_guards, get_crate_name, get_rustdoc, GeneratorResult}; +use crate::utils::{generate_guards, get_crate_name, get_rustdoc, visible_fn, GeneratorResult}; pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult { let crate_name = get_crate_name(object_args.internal); @@ -81,6 +81,8 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult GeneratorResult GeneratorResult GeneratorResult::create_type_info(registry), default_value: #schema_default, validator: #validator, + visible: #visible, }); }); @@ -235,6 +238,7 @@ pub fn generate( .expect("invalid result type"); } + let visible = visible_fn(&field.visible); schema_fields.push(quote! { #(#cfg_attrs)* fields.insert(::std::borrow::ToOwned::to_owned(#field_name), #crate_name::registry::MetaField { @@ -251,6 +255,7 @@ pub fn generate( external: false, requires: ::std::option::Option::None, provides: ::std::option::Option::None, + visible: #visible, }); }); @@ -388,6 +393,7 @@ pub fn generate( cache_control: ::std::default::Default::default(), extends: false, keys: ::std::option::Option::None, + visible: ::std::option::Option::None, }) } } diff --git a/derive/src/union.rs b/derive/src/union.rs index 40d94b67..d2afcb4a 100644 --- a/derive/src/union.rs +++ b/derive/src/union.rs @@ -7,7 +7,7 @@ use syn::visit_mut::VisitMut; use syn::{visit_mut, Error, Lifetime, Type}; use crate::args::{self, RenameTarget}; -use crate::utils::{get_crate_name, get_rustdoc, GeneratorResult}; +use crate::utils::{get_crate_name, get_rustdoc, visible_fn, GeneratorResult}; pub fn generate(union_args: &args::Union) -> GeneratorResult { let crate_name = get_crate_name(union_args.internal); @@ -148,6 +148,7 @@ pub fn generate(union_args: &args::Union) -> GeneratorResult { .into()); } + let visible = visible_fn(&union_args.visible); let expanded = quote! { #(#type_into_impls)* @@ -174,7 +175,8 @@ pub fn generate(union_args: &args::Union) -> GeneratorResult { let mut possible_types = #crate_name::indexmap::IndexSet::new(); #(#possible_types)* possible_types - } + }, + visible: #visible, } }) } diff --git a/derive/src/utils.rs b/derive/src/utils.rs index cf94f483..c1495423 100644 --- a/derive/src/utils.rs +++ b/derive/src/utils.rs @@ -8,6 +8,7 @@ use syn::{ use thiserror::Error; use crate::args; +use crate::args::Visible; #[derive(Error, Debug)] pub enum GeneratorError { @@ -389,3 +390,14 @@ pub fn get_type_path_and_name(ty: &Type) -> GeneratorResult<(&TypePath, String)> _ => Err(Error::new_spanned(ty, "Invalid type").into()), } } + +pub fn visible_fn(visible: &Option) -> TokenStream { + match visible { + None | Some(Visible::None) => quote! { ::std::option::Option::None }, + Some(Visible::HiddenAlways) => quote! { ::std::option::Option::Some(|_| false) }, + Some(Visible::FnName(name)) => { + let ident = Ident::new(name, Span::call_site()); + quote! { ::std::option::Option::Some(#ident) } + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 97096a23..035742ce 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -244,6 +244,8 @@ pub type FieldResult = Result; /// | cache_control | Object cache control | [`CacheControl`](struct.CacheControl.html) | Y | /// | extends | Add fields to an entity that's defined in another service | bool | Y | /// | 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. | bool | Y | +/// | visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y | /// /// # Field parameters /// @@ -258,6 +260,8 @@ pub type FieldResult = Result; /// | provides | Annotate the expected returned fieldset from a field on a base type that is guaranteed to be selectable by the gateway. | string | Y | /// | requires | Annotate the required input fieldset from a base type for a resolver. It is used to develop a query plan where the required fields may not be needed by the client, but the service may need additional information from other services. | string | Y | /// | guard | Field of guard | [`Guard`](guard/trait.Guard.html) | Y | +/// | visible | If `false`, it will not be displayed in introspection. | 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 /// @@ -270,6 +274,8 @@ pub type FieldResult = Result; /// | default_with | Expression to generate default value | code string | Y | /// | validator | Input value validator | [`InputValueValidator`](validators/trait.InputValueValidator.html) | Y | /// | key | Is entity key | bool | Y | +/// | visible | If `false`, it will not be displayed in introspection. | bool | Y | +/// | visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y | /// /// # Valid field return types /// @@ -360,6 +366,8 @@ pub use async_graphql_derive::Object; /// | 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 | /// | cache_control | Object cache control | [`CacheControl`](struct.CacheControl.html) | Y | /// | extends | Add fields to an entity that's defined in another service | bool | Y | +/// | visible | If `false`, it will not be displayed in introspection. | bool | Y | +/// | visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y | /// /// # Field parameters /// @@ -374,6 +382,8 @@ pub use async_graphql_derive::Object; /// | provides | Annotate the expected returned fieldset from a field on a base type that is guaranteed to be selectable by the gateway. | string | Y | /// | requires | Annotate the required input fieldset from a base type for a resolver. It is used to develop a query plan where the required fields may not be needed by the client, but the service may need additional information from other services. | string | Y | /// | guard | Field of guard | [`Guard`](guard/trait.Guard.html) | Y | +/// | visible | If `false`, it will not be displayed in introspection. | bool | Y | +/// | visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y | /// /// # Examples /// @@ -406,6 +416,8 @@ pub use async_graphql_derive::SimpleObject; /// | name | Enum name | string | Y | /// | rename_items | 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 | /// | remote | Derive a remote enum | string | Y | +/// | visible | If `false`, it will not be displayed in introspection. | bool | Y | +/// | visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y | /// /// # Item parameters /// @@ -413,6 +425,8 @@ pub use async_graphql_derive::SimpleObject; /// |-------------|---------------------------|----------|----------| /// | name | Item name | string | Y | /// | deprecation | Item deprecation reason | string | Y | +/// | visible | If `false`, it will not be displayed in introspection. | bool | Y | +/// | visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y | /// /// # Examples /// @@ -461,6 +475,8 @@ pub use async_graphql_derive::Enum; /// |---------------|---------------------------|----------|----------| /// | name | Object name | string | Y | /// | rename_fields | Rename all the fields according to the given case convention. The possible values are "lowercase", "UPPERCASE", "PascalCase", "camelCase", "snake_case", "SCREAMING_SNAKE_CASE".| string | Y | +/// | visible | If `false`, it will not be displayed in introspection. | bool | Y | +/// | visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y | /// /// # Field parameters /// @@ -472,6 +488,8 @@ pub use async_graphql_derive::Enum; /// | default_with | Expression to generate default value | code string | Y | /// | validator | Input value validator | [`InputValueValidator`](validators/trait.InputValueValidator.html) | Y | /// | flatten | Similar to serde (flatten) | boolean | Y | +/// | visible | If `false`, it will not be displayed in introspection. | bool | Y | +/// | visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y | /// /// # Examples /// @@ -520,6 +538,8 @@ pub use async_graphql_derive::InputObject; /// | 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 | Fields of this Interface | [InterfaceField] | N | /// | extends | Add fields to an entity that's defined in another service | bool | Y | +/// | visible | If `false`, it will not be displayed in introspection. | bool | Y | +/// | visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y | /// /// # Field parameters /// @@ -534,6 +554,8 @@ pub use async_graphql_derive::InputObject; /// | external | Mark a field as owned by another service. This allows service A to use fields from service B while also knowing at runtime the types of that field. | bool | Y | /// | provides | Annotate the expected returned fieldset from a field on a base type that is guaranteed to be selectable by the gateway. | string | Y | /// | requires | Annotate the required input fieldset from a base type for a resolver. It is used to develop a query plan where the required fields may not be needed by the client, but the service may need additional information from other services. | string | Y | +/// | visible | If `false`, it will not be displayed in introspection. | 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 /// @@ -545,6 +567,8 @@ pub use async_graphql_derive::InputObject; /// | 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 | +/// | visible | If `false`, it will not be displayed in introspection. | bool | Y | +/// | visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y | /// /// # Define an interface /// @@ -650,6 +674,8 @@ pub use async_graphql_derive::Interface; /// | Attribute | description | Type | Optional | /// |-------------|---------------------------|----------|----------| /// | name | Object name | string | Y | +/// | visible | If `false`, it will not be displayed in introspection. | bool | Y | +/// | visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y | /// /// # Item parameters /// @@ -737,6 +763,8 @@ pub use async_graphql_derive::Union; /// | name | Field name | string | Y | /// | deprecation | Field deprecation reason | string | Y | /// | guard | Field of guard | [`Guard`](guard/trait.Guard.html) | Y | +/// | visible | If `false`, it will not be displayed in introspection. | 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 /// @@ -748,6 +776,8 @@ pub use async_graphql_derive::Union; /// | default | Argument default value | literal | Y | /// | default_with | Expression to generate default value | code string | Y | /// | validator | Input value validator | [`InputValueValidator`](validators/trait.InputValueValidator.html) | Y | +/// | visible | If `false`, it will not be displayed in introspection. | bool | Y | +/// | visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y | /// /// # Examples /// @@ -789,6 +819,8 @@ pub use async_graphql_derive::Scalar; /// | cache_control | Object cache control | [`CacheControl`](struct.CacheControl.html) | Y | /// | extends | Add fields to an entity that's defined in another service | bool | Y | /// | 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. | bool | Y | +/// | visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y | /// /// # Examples /// diff --git a/src/model/field.rs b/src/model/field.rs index 632a3a49..eaf6e3a8 100644 --- a/src/model/field.rs +++ b/src/model/field.rs @@ -1,5 +1,5 @@ use crate::model::{__InputValue, __Type}; -use crate::{registry, Object}; +use crate::{registry, Context, Object}; pub struct __Field<'a> { pub registry: &'a registry::Registry, @@ -17,10 +17,14 @@ impl<'a> __Field<'a> { self.field.description.map(ToString::to_string) } - async fn args(&self) -> Vec<__InputValue<'a>> { + async fn args(&self, ctx: &Context<'_>) -> Vec<__InputValue<'a>> { self.field .args .values() + .filter(|input_value| match &input_value.visible { + Some(f) => f(ctx), + None => true, + }) .map(|input_value| __InputValue { registry: self.registry, input_value, diff --git a/src/model/schema.rs b/src/model/schema.rs index a11b8975..67afe712 100644 --- a/src/model/schema.rs +++ b/src/model/schema.rs @@ -1,5 +1,5 @@ use crate::model::{__Directive, __Type}; -use crate::{registry, Object}; +use crate::{registry, Context, Object}; pub struct __Schema<'a> { pub registry: &'a registry::Registry, @@ -9,12 +9,18 @@ pub struct __Schema<'a> { #[Object(internal, name = "__Schema")] impl<'a> __Schema<'a> { /// A list of all types supported by this server. - async fn types(&self) -> Vec<__Type<'a>> { + async fn types(&self, ctx: &Context<'_>) -> Vec<__Type<'a>> { let mut types: Vec<_> = self .registry .types .values() - .map(|ty| (ty.name(), __Type::new_simple(self.registry, ty))) + .filter_map(|ty| { + if ty.is_visible(ctx) { + Some((ty.name(), __Type::new_simple(self.registry, ty))) + } else { + None + } + }) .collect(); types.sort_by(|a, b| a.0.cmp(b.0)); types.into_iter().map(|(_, ty)| ty).collect() diff --git a/src/model/type.rs b/src/model/type.rs index 94aad5ff..98aef615 100644 --- a/src/model/type.rs +++ b/src/model/type.rs @@ -1,5 +1,5 @@ use crate::model::{__EnumValue, __Field, __InputValue, __TypeKind}; -use crate::{registry, Object}; +use crate::{registry, Context, Object}; enum TypeDetail<'a> { Named(&'a registry::MetaType), @@ -95,12 +95,17 @@ impl<'a> __Type<'a> { async fn fields( &self, + ctx: &Context<'_>, #[graphql(default = false)] include_deprecated: bool, ) -> Option>> { if let TypeDetail::Named(ty) = &self.detail { ty.fields().map(|fields| { fields .values() + .filter(|field| match &field.visible { + Some(f) => f(ctx), + None => true, + }) .filter(|field| { (include_deprecated || field.deprecation.is_none()) && !field.name.starts_with("__") @@ -158,13 +163,18 @@ impl<'a> __Type<'a> { async fn enum_values( &self, + ctx: &Context<'_>, #[graphql(default = false)] include_deprecated: bool, ) -> Option>> { if let TypeDetail::Named(registry::MetaType::Enum { enum_values, .. }) = &self.detail { Some( enum_values .values() - .filter(|field| include_deprecated || field.deprecation.is_none()) + .filter(|value| match &value.visible { + Some(f) => f(ctx), + None => true, + }) + .filter(|value| include_deprecated || value.deprecation.is_none()) .map(|value| __EnumValue { registry: self.registry, value, @@ -176,13 +186,17 @@ impl<'a> __Type<'a> { } } - async fn input_fields(&self) -> Option>> { + async fn input_fields(&self, ctx: &Context<'_>) -> Option>> { if let TypeDetail::Named(registry::MetaType::InputObject { input_fields, .. }) = &self.detail { Some( input_fields .values() + .filter(|input_value| match &input_value.visible { + Some(f) => f(ctx), + None => true, + }) .map(|input_value| __InputValue { registry: self.registry, input_value, diff --git a/src/registry/mod.rs b/src/registry/mod.rs index 0b833a77..dfe6010f 100644 --- a/src/registry/mod.rs +++ b/src/registry/mod.rs @@ -9,7 +9,7 @@ use indexmap::set::IndexSet; use crate::parser::types::{BaseType as ParsedBaseType, Type as ParsedType}; use crate::validators::InputValueValidator; -use crate::{model, Any, Type, Value}; +use crate::{model, Any, Context, Type, Value}; pub use cache_control::CacheControl; @@ -92,6 +92,7 @@ pub struct MetaInputValue { pub ty: String, pub default_value: Option, pub validator: Option>, + pub visible: Option, } #[derive(Clone)] @@ -105,6 +106,7 @@ pub struct MetaField { pub external: bool, pub requires: Option<&'static str>, pub provides: Option<&'static str>, + pub visible: Option, } #[derive(Clone)] @@ -112,13 +114,17 @@ pub struct MetaEnumValue { pub name: &'static str, pub description: Option<&'static str>, pub deprecation: Option<&'static str>, + pub visible: Option, } +type MetaVisibleFn = fn(&Context<'_>) -> bool; + pub enum MetaType { Scalar { name: String, description: Option<&'static str>, is_valid: fn(value: &Value) -> bool, + visible: Option, }, Object { name: String, @@ -127,6 +133,7 @@ pub enum MetaType { cache_control: CacheControl, extends: bool, keys: Option>, + visible: Option, }, Interface { name: String, @@ -135,21 +142,25 @@ pub enum MetaType { possible_types: IndexSet, extends: bool, keys: Option>, + visible: Option, }, Union { name: String, description: Option<&'static str>, possible_types: IndexSet, + visible: Option, }, Enum { name: String, description: Option<&'static str>, enum_values: IndexMap<&'static str, MetaEnumValue>, + visible: Option, }, InputObject { name: String, description: Option<&'static str>, input_fields: IndexMap, + visible: Option, }, } @@ -166,6 +177,21 @@ impl MetaType { } } + pub fn is_visible(&self, ctx: &Context<'_>) -> bool { + let visible = match self { + MetaType::Scalar { visible, .. } => visible, + MetaType::Object { visible, .. } => visible, + MetaType::Interface { visible, .. } => visible, + MetaType::Union { visible, .. } => visible, + MetaType::Enum { visible, .. } => visible, + MetaType::InputObject { visible, .. } => visible, + }; + match visible { + Some(f) => f(ctx), + None => true, + } + } + pub fn name(&self) -> &str { match self { MetaType::Scalar { name, .. } => &name, @@ -281,6 +307,7 @@ impl Registry { cache_control: Default::default(), extends: false, keys: None, + visible: None, }, ); let ty = f(self); @@ -394,6 +421,7 @@ impl Registry { name: "_Entity".to_string(), description: None, possible_types, + visible: None, }, ); } @@ -420,6 +448,7 @@ impl Registry { external: false, requires: None, provides: None, + visible: None, }, ); fields @@ -427,6 +456,7 @@ impl Registry { cache_control: Default::default(), extends: false, keys: None, + visible: None, }, ); @@ -446,6 +476,7 @@ impl Registry { external: false, requires: None, provides: None, + visible: None, }, ); @@ -464,6 +495,7 @@ impl Registry { ty: "[_Any!]!".to_string(), default_value: None, validator: None, + visible: None, }, ); args @@ -474,6 +506,7 @@ impl Registry { external: false, requires: None, provides: None, + visible: None, }, ); } diff --git a/src/resolver_utils/scalar.rs b/src/resolver_utils/scalar.rs index 6899fcf4..14c67ce5 100644 --- a/src/resolver_utils/scalar.rs +++ b/src/resolver_utils/scalar.rs @@ -121,6 +121,7 @@ macro_rules! scalar_internal { name: ::std::borrow::ToOwned::to_owned($name), description: $desc, is_valid: |value| <$ty as $crate::ScalarType>::is_valid(value), + visible: ::std::option::Option::None, }) } } diff --git a/src/schema.rs b/src/schema.rs index 6adaceaa..43882932 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -266,6 +266,7 @@ where ty: "Boolean!".to_string(), default_value: None, validator: None, + visible: None, }); args } @@ -287,6 +288,7 @@ where ty: "Boolean!".to_string(), default_value: None, validator: None, + visible: None, }); args } diff --git a/src/types/connection/connection_type.rs b/src/types/connection/connection_type.rs index 7ce2d3e5..6ac20cd6 100644 --- a/src/types/connection/connection_type.rs +++ b/src/types/connection/connection_type.rs @@ -159,6 +159,7 @@ where external: false, requires: None, provides: None, + visible: None, }, ); @@ -176,6 +177,7 @@ where external: false, requires: None, provides: None, + visible: None, }, ); @@ -185,6 +187,7 @@ where cache_control: Default::default(), extends: false, keys: None, + visible: None, } }) } diff --git a/src/types/connection/edge.rs b/src/types/connection/edge.rs index bfc2eedc..263b756a 100644 --- a/src/types/connection/edge.rs +++ b/src/types/connection/edge.rs @@ -78,6 +78,7 @@ where external: false, requires: None, provides: None, + visible: None, }, ); @@ -93,6 +94,7 @@ where external: false, requires: None, provides: None, + visible: None, }, ); @@ -102,6 +104,7 @@ where cache_control: Default::default(), extends: false, keys: None, + visible: None, } }) } diff --git a/src/types/empty_mutation.rs b/src/types/empty_mutation.rs index 016f0cf9..1bf66bf1 100644 --- a/src/types/empty_mutation.rs +++ b/src/types/empty_mutation.rs @@ -44,6 +44,7 @@ impl Type for EmptyMutation { cache_control: Default::default(), extends: false, keys: None, + visible: None, }) } } diff --git a/src/types/empty_subscription.rs b/src/types/empty_subscription.rs index f0082f46..368669dd 100644 --- a/src/types/empty_subscription.rs +++ b/src/types/empty_subscription.rs @@ -24,6 +24,7 @@ impl Type for EmptySubscription { cache_control: Default::default(), extends: false, keys: None, + visible: None, }) } } diff --git a/src/types/json.rs b/src/types/json.rs index fb116e7b..3bbdf045 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -84,6 +84,7 @@ impl Type for OutputJson { name: Self::type_name().to_string(), description: None, is_valid: |_| true, + visible: None, }) } } diff --git a/src/types/merged_object.rs b/src/types/merged_object.rs index 394b4e87..a0914d96 100644 --- a/src/types/merged_object.rs +++ b/src/types/merged_object.rs @@ -51,6 +51,7 @@ impl Type for MergedObject { cache_control: cc, extends: false, keys: None, + visible: None, } }) } diff --git a/src/types/query_root.rs b/src/types/query_root.rs index 4096ab8f..975f7dcd 100644 --- a/src/types/query_root.rs +++ b/src/types/query_root.rs @@ -45,6 +45,7 @@ impl Type for QueryRoot { external: false, requires: None, provides: None, + visible: None, }, ); @@ -63,6 +64,7 @@ impl Type for QueryRoot { ty: "String!".to_string(), default_value: None, validator: None, + visible: None, }, ); args @@ -73,6 +75,7 @@ impl Type for QueryRoot { external: false, requires: None, provides: None, + visible: None, }, ); } @@ -106,6 +109,7 @@ impl ContainerType for QueryRoot { .registry .types .get(&type_name) + .filter(|ty| ty.is_visible(ctx)) .map(|ty| __Type::new_simple(&ctx.schema_env.registry, ty)), &ctx_obj, ctx.item, diff --git a/src/types/upload.rs b/src/types/upload.rs index 25fccaaf..277954d1 100644 --- a/src/types/upload.rs +++ b/src/types/upload.rs @@ -110,6 +110,7 @@ impl Type for Upload { name: Self::type_name().to_string(), description: None, is_valid: |value| matches!(value, Value::String(_)), + visible: None, }) } } diff --git a/tests/introspection_visible.rs b/tests/introspection_visible.rs new file mode 100644 index 00000000..8c1828ec --- /dev/null +++ b/tests/introspection_visible.rs @@ -0,0 +1,254 @@ +use async_graphql::*; +use serde::Deserialize; + +#[async_std::test] +pub async fn test_type_visible() { + #[derive(SimpleObject)] + #[graphql(visible = false)] + struct MyObj { + a: i32, + } + + struct Query; + + #[Object] + impl Query { + async fn obj(&self) -> MyObj { + todo!() + } + } + + let schema = Schema::new(Query, EmptyMutation, EmptySubscription); + + assert_eq!( + schema + .execute(r#"{ __type(name: "MyObj") { name } }"#) + .await + .into_result() + .unwrap() + .data, + value!({ + "__type": null, + }) + ); + + #[derive(Deserialize)] + struct QueryResponse { + #[serde(rename = "__schema")] + schema: SchemaResponse, + } + + #[derive(Deserialize)] + struct SchemaResponse { + types: Vec, + } + + #[derive(Deserialize)] + struct TypeResponse { + name: String, + } + + let resp: QueryResponse = from_value( + schema + .execute(r#"{ __schema { types { name } } }"#) + .await + .into_result() + .unwrap() + .data, + ) + .unwrap(); + + assert!(resp + .schema + .types + .into_iter() + .find(|ty| ty.name == "MyObj") + .is_none()); +} + +#[async_std::test] +pub async fn test_field_visible() { + #[derive(SimpleObject)] + struct MyObj { + a: i32, + #[graphql(visible = false)] + b: i32, + } + + struct Query; + + #[Object] + impl Query { + async fn obj(&self) -> MyObj { + todo!() + } + + #[graphql(visible = false)] + async fn c(&self) -> i32 { + todo!() + } + } + + let schema = Schema::new(Query, EmptyMutation, EmptySubscription); + + #[derive(Debug, Deserialize)] + struct QueryResponse { + #[serde(rename = "__type")] + ty: TypeResponse, + } + + #[derive(Debug, Deserialize)] + struct TypeResponse { + fields: Vec, + } + + #[derive(Debug, Deserialize)] + struct FieldResposne { + name: String, + } + + let resp: QueryResponse = from_value( + schema + .execute(r#"{ __type(name: "MyObj") { fields { name } } }"#) + .await + .into_result() + .unwrap() + .data, + ) + .unwrap(); + assert_eq!( + resp.ty + .fields + .iter() + .map(|field| field.name.as_str()) + .collect::>(), + vec!["a"] + ); + + let resp: QueryResponse = from_value( + schema + .execute(r#"{ __type(name: "Query") { fields { name } } }"#) + .await + .into_result() + .unwrap() + .data, + ) + .unwrap(); + assert_eq!( + resp.ty + .fields + .iter() + .map(|field| field.name.as_str()) + .collect::>(), + vec!["obj"] + ); +} + +#[async_std::test] +pub async fn test_enum_value_visible() { + #[derive(Enum, Eq, PartialEq, Copy, Clone)] + enum MyEnum { + A, + B, + #[graphql(visible = false)] + C, + } + + struct Query; + + #[Object] + impl Query { + async fn e(&self) -> MyEnum { + todo!() + } + } + + let schema = Schema::new(Query, EmptyMutation, EmptySubscription); + + #[derive(Debug, Deserialize)] + struct QueryResponse { + #[serde(rename = "__type")] + ty: TypeResponse, + } + + #[derive(Debug, Deserialize)] + #[serde(rename_all = "camelCase")] + struct TypeResponse { + enum_values: Vec, + } + + #[derive(Debug, Deserialize)] + struct EnumValueResponse { + name: String, + } + + let resp: QueryResponse = from_value( + schema + .execute(r#"{ __type(name: "MyEnum") { enumValues { name } } }"#) + .await + .into_result() + .unwrap() + .data, + ) + .unwrap(); + assert_eq!( + resp.ty + .enum_values + .iter() + .map(|value| value.name.as_str()) + .collect::>(), + vec!["A", "B"] + ); +} + +#[async_std::test] +pub async fn test_visible_fn() { + struct IsAdmin(bool); + + #[derive(SimpleObject)] + #[graphql(visible = "is_admin")] + struct MyObj { + a: i32, + } + + fn is_admin(ctx: &Context<'_>) -> bool { + ctx.data_unchecked::().0 + } + + struct Query; + + #[Object] + impl Query { + async fn obj(&self) -> MyObj { + todo!() + } + } + + let schema = Schema::new(Query, EmptyMutation, EmptySubscription); + + assert_eq!( + schema + .execute(Request::new(r#"{ __type(name: "MyObj") { name } }"#).data(IsAdmin(false))) + .await + .into_result() + .unwrap() + .data, + value!({ + "__type": null, + }) + ); + + assert_eq!( + schema + .execute(Request::new(r#"{ __type(name: "MyObj") { name } }"#).data(IsAdmin(true))) + .await + .into_result() + .unwrap() + .data, + value!({ + "__type": { + "name": "MyObj", + }, + }) + ); +}