Make Object and Subscription macros support #cfg(...) attribute. #281

This commit is contained in:
Sunli 2020-09-27 10:20:20 +08:00
parent b47d08c5b5
commit 7d3eb9b62c
7 changed files with 56 additions and 115 deletions

View File

@ -205,7 +205,6 @@ pub struct Field {
pub owned: bool, pub owned: bool,
pub guard: Option<TokenStream>, pub guard: Option<TokenStream>,
pub post_guard: Option<TokenStream>, pub post_guard: Option<TokenStream>,
pub features: Vec<String>,
} }
impl Field { impl Field {
@ -217,7 +216,6 @@ impl Field {
let mut external = false; let mut external = false;
let mut provides = None; let mut provides = None;
let mut requires = None; let mut requires = None;
let mut features = Vec::new();
let mut owned = false; let mut owned = false;
let mut guard = None; let mut guard = None;
let mut post_guard = None; let mut post_guard = None;
@ -290,20 +288,6 @@ impl Field {
"Attribute 'requires' should be a string.", "Attribute 'requires' should be a string.",
)); ));
} }
} else if nv.path.is_ident("feature") {
if let syn::Lit::Str(lit) = &nv.lit {
features = lit
.value()
.to_string()
.split(',')
.map(|s| s.trim().to_string())
.collect();
} else {
return Err(Error::new_spanned(
&nv.lit,
"Attribute 'feature' should be a string.",
));
}
} }
} }
NestedMeta::Meta(Meta::List(ls)) => { NestedMeta::Meta(Meta::List(ls)) => {
@ -334,7 +318,6 @@ impl Field {
owned, owned,
guard, guard,
post_guard, post_guard,
features,
})) }))
} }
} }

View File

@ -1,6 +1,6 @@
use crate::args; use crate::args;
use crate::output_type::OutputType; use crate::output_type::OutputType;
use crate::utils::{feature_block, get_crate_name, get_param_getter_ident, get_rustdoc}; use crate::utils::{get_cfg_attrs, get_crate_name, get_param_getter_ident, get_rustdoc};
use inflector::Inflector; use inflector::Inflector;
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::quote; use quote::quote;
@ -45,6 +45,8 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result<
for item in &mut item_impl.items { for item in &mut item_impl.items {
if let ImplItem::Method(method) = item { if let ImplItem::Method(method) = item {
if args::Entity::parse(&crate_name, &method.attrs)?.is_some() { if args::Entity::parse(&crate_name, &method.attrs)?.is_some() {
let cfg_attrs = get_cfg_attrs(&method.attrs);
if method.sig.asyncness.is_none() { if method.sig.asyncness.is_none() {
return Err(Error::new_spanned(&method, "Must be asynchronous")); return Err(Error::new_spanned(&method, "Must be asynchronous"));
} }
@ -182,6 +184,7 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result<
find_entities.push(( find_entities.push((
args.len(), args.len(),
quote! { quote! {
#(#cfg_attrs)*
if typename == &<#entity_type as #crate_name::Type>::type_name() { if typename == &<#entity_type as #crate_name::Type>::type_name() {
if let (#(#key_pat),*) = (#(#key_getter),*) { if let (#(#key_pat),*) = (#(#key_getter),*) {
#(#requires_getter)* #(#requires_getter)*
@ -221,7 +224,6 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result<
.map(|s| quote! {Some(#s)}) .map(|s| quote! {Some(#s)})
.unwrap_or_else(|| quote! {None}); .unwrap_or_else(|| quote! {None});
let external = field.external; let external = field.external;
let features = field.features;
let requires = match &field.requires { let requires = match &field.requires {
Some(requires) => quote! { Some(#requires) }, Some(requires) => quote! { Some(#requires) },
None => quote! { None }, None => quote! { None },
@ -246,6 +248,7 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result<
} }
} }
}; };
let cfg_attrs = get_cfg_attrs(&method.attrs);
let mut create_ctx = true; let mut create_ctx = true;
let mut args = Vec::new(); let mut args = Vec::new();
@ -357,6 +360,7 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result<
let schema_ty = ty.value_type(); let schema_ty = ty.value_type();
schema_fields.push(quote! { schema_fields.push(quote! {
#(#cfg_attrs)*
fields.insert(#field_name.to_string(), #crate_name::registry::MetaField { fields.insert(#field_name.to_string(), #crate_name::registry::MetaField {
name: #field_name.to_string(), name: #field_name.to_string(),
description: #field_desc, description: #field_desc,
@ -390,13 +394,6 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result<
.expect("invalid result type"); .expect("invalid result type");
} }
method.block =
syn::parse2::<Block>(feature_block(&crate_name, &features, &field_name, {
let block = &method.block;
quote! { #block }
}))
.expect("invalid block");
let resolve_obj = quote! { let resolve_obj = quote! {
{ {
let res = self.#field_ident(ctx, #(#use_params),*).await; let res = self.#field_ident(ctx, #(#use_params),*).await;
@ -418,6 +415,7 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result<
}); });
resolvers.push(quote! { resolvers.push(quote! {
#(#cfg_attrs)*
if ctx.item.node.name.node == #field_name { if ctx.item.node.name.node == #field_name {
#(#get_params)* #(#get_params)*
#guard #guard

View File

@ -1,5 +1,5 @@
use crate::args; use crate::args;
use crate::utils::{feature_block, get_crate_name, get_rustdoc}; use crate::utils::{get_crate_name, get_rustdoc};
use inflector::Inflector; use inflector::Inflector;
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::quote; use quote::quote;
@ -103,33 +103,20 @@ pub fn generate(object_args: &args::Object, input: &DeriveInput) -> Result<Token
.post_guard .post_guard
.map(|guard| quote! { #guard.check(ctx, &res).await.map_err(|err| err.into_error_with_path(ctx.item.pos, ctx.path_node.as_ref()))?; }); .map(|guard| quote! { #guard.check(ctx, &res).await.map_err(|err| err.into_error_with_path(ctx.item.pos, ctx.path_node.as_ref()))?; });
let features = &field.features;
getters.push(if !field.owned { getters.push(if !field.owned {
let block = feature_block(
&crate_name,
&features,
&field_name,
quote! { Ok(&self.#ident) },
);
quote! { quote! {
#[inline] #[inline]
#[allow(missing_docs)] #[allow(missing_docs)]
#vis async fn #ident(&self, ctx: &#crate_name::Context<'_>) -> #crate_name::FieldResult<&#ty> { #vis async fn #ident(&self, ctx: &#crate_name::Context<'_>) -> #crate_name::FieldResult<&#ty> {
#block Ok(&self.#ident)
} }
} }
} else { } else {
let block = feature_block(
&crate_name,
&features,
&field_name,
quote! { Ok(self.#ident.clone()) },
);
quote! { quote! {
#[inline] #[inline]
#[allow(missing_docs)] #[allow(missing_docs)]
#vis async fn #ident(&self, ctx: &#crate_name::Context<'_>) -> #crate_name::FieldResult<#ty> { #vis async fn #ident(&self, ctx: &#crate_name::Context<'_>) -> #crate_name::FieldResult<#ty> {
#block Ok(self.#ident.clone())
} }
} }
}); });

View File

@ -1,6 +1,6 @@
use crate::args; use crate::args;
use crate::output_type::OutputType; use crate::output_type::OutputType;
use crate::utils::{feature_block, get_crate_name, get_param_getter_ident, get_rustdoc}; use crate::utils::{get_cfg_attrs, get_crate_name, get_param_getter_ident, get_rustdoc};
use inflector::Inflector; use inflector::Inflector;
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::quote; use quote::quote;
@ -59,7 +59,7 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result<
.as_ref() .as_ref()
.map(|s| quote! {Some(#s)}) .map(|s| quote! {Some(#s)})
.unwrap_or_else(|| quote! {None}); .unwrap_or_else(|| quote! {None});
let features = field.features; let cfg_attrs = get_cfg_attrs(&method.attrs);
if method.sig.asyncness.is_none() { if method.sig.asyncness.is_none() {
return Err(Error::new_spanned( return Err(Error::new_spanned(
@ -207,14 +207,8 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result<
.expect("invalid result type"); .expect("invalid result type");
} }
method.block =
syn::parse2::<Block>(feature_block(&crate_name, &features, &field_name, {
let block = &method.block;
quote! { #block }
}))
.expect("invalid block");
schema_fields.push(quote! { schema_fields.push(quote! {
#(#cfg_attrs)*
fields.insert(#field_name.to_string(), #crate_name::registry::MetaField { fields.insert(#field_name.to_string(), #crate_name::registry::MetaField {
name: #field_name.to_string(), name: #field_name.to_string(),
description: #field_desc, description: #field_desc,
@ -327,6 +321,7 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result<
}; };
create_stream.push(quote! { create_stream.push(quote! {
#(#cfg_attrs)*
if ctx.item.node.name.node == #field_name { if ctx.item.node.name.node == #field_name {
return ::std::boxed::Box::pin( return ::std::boxed::Box::pin(
#crate_name::futures::TryStreamExt::try_flatten( #crate_name::futures::TryStreamExt::try_flatten(

View File

@ -278,29 +278,10 @@ pub fn get_param_getter_ident(name: &str) -> Ident {
Ident::new(&format!("__{}_getter", name), Span::call_site()) Ident::new(&format!("__{}_getter", name), Span::call_site())
} }
pub fn feature_block( pub fn get_cfg_attrs(attrs: &[Attribute]) -> Vec<Attribute> {
crate_name: &TokenStream, attrs
features: &[String], .iter()
field_name: &str, .filter(|attr| !attr.path.segments.is_empty() && attr.path.segments[0].ident == "cfg")
block: TokenStream, .cloned()
) -> TokenStream { .collect()
if !features.is_empty() {
let error_message = format!(
"`{}` is only available if the features `{}` are enabled",
field_name,
features.join(",")
);
quote!({
#[cfg(not(all(#(feature = #features),*)))]
{
return Err(#crate_name::FieldError::from(#error_message)).map_err(::std::convert::Into::into);
}
#[cfg(all(#(feature = #features),*))]
{
#block
}
})
} else {
block
}
} }

View File

@ -205,7 +205,6 @@ pub type Result<T> = std::result::Result<T, Error>;
/// | provides | Annotate the expected returned fieldset from a field on a base type that is guaranteed to be selectable by the gateway. | string | 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 | /// | 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 | /// | guard | Field of guard | [`Guard`](guard/trait.Guard.html) | Y |
/// | feature | It's like a `#[cfg(feature = "foo")]` attribute but instead of not compiling this field it will just return a proper `FieldError` to tell you this feature is not enabled | string ("feature1,feature2") | Y |
/// ///
/// # Field argument parameters /// # Field argument parameters
/// ///
@ -319,7 +318,6 @@ 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 | /// | 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 | /// | 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 | /// | guard | Field of guard | [`Guard`](guard/trait.Guard.html) | Y |
/// | feature | It's like a `#[cfg(feature = "foo")]` attribute but instead of not compiling this field it will just return a proper `FieldError` to tell you this feature is not enabled | string ("feature1,feature2") | Y |
/// ///
/// # Examples /// # Examples
/// ///
@ -672,7 +670,6 @@ pub use async_graphql_derive::Union;
/// | desc | Field description | string | Y | /// | desc | Field description | string | Y |
/// | deprecation | Field deprecation reason | string | Y | /// | deprecation | Field deprecation reason | string | Y |
/// | guard | Field of guard | [`Guard`](guard/trait.Guard.html) | Y | /// | guard | Field of guard | [`Guard`](guard/trait.Guard.html) | Y |
/// | feature | It's like a `#[cfg(feature = "foo")]` attribute but instead of not compiling this field it will just return a proper `FieldError` to tell you this feature is not enabled | string ("feature1,feature2") | Y |
/// ///
/// # Field argument parameters /// # Field argument parameters
/// ///

View File

@ -2,35 +2,32 @@
use async_graphql::*; use async_graphql::*;
use futures::{Stream, StreamExt}; use futures::{Stream, StreamExt};
use std::pin::Pin;
#[async_std::test] #[async_std::test]
pub async fn test_field_features() { pub async fn test_field_features() {
#[derive(SimpleObject)] #[derive(SimpleObject)]
struct MyObj { struct MyObj {
value: i32, value: i32,
#[cfg(feature = "bson")]
#[field(feature = "bson")]
value_bson: i32, value_bson: i32,
#[cfg(feature = "abc")]
#[field(feature = "abc")]
value_abc: i32, value_abc: i32,
} }
struct Subscription; struct SubscriptionRoot;
#[Subscription] #[Subscription]
impl Subscription { impl SubscriptionRoot {
async fn values(&self) -> impl Stream<Item = i32> { async fn values(&self) -> impl Stream<Item = i32> {
futures::stream::once(async move { 10 }) futures::stream::once(async move { 10 })
} }
#[field(feature = "bson")] #[cfg(feature = "bson")]
async fn values_bson(&self) -> impl Stream<Item = i32> { async fn values_bson(&self) -> impl Stream<Item = i32> {
futures::stream::once(async move { 10 }) futures::stream::once(async move { 10 })
} }
#[field(feature = "abc")] #[cfg(feature = "abc")]
async fn values_abc( async fn values_abc(
&self, &self,
) -> Pin<Box<dyn async_graphql::futures::Stream<Item = i32> + Send + 'static>> { ) -> Pin<Box<dyn async_graphql::futures::Stream<Item = i32> + Send + 'static>> {
@ -46,12 +43,12 @@ pub async fn test_field_features() {
10 10
} }
#[field(feature = "bson")] #[cfg(feature = "bson")]
async fn value_bson(&self) -> i32 { async fn value_bson(&self) -> i32 {
10 10
} }
#[field(feature = "abc")] #[cfg(feature = "abc")]
async fn value_abc(&self) -> i32 { async fn value_abc(&self) -> i32 {
10 10
} }
@ -59,13 +56,15 @@ pub async fn test_field_features() {
async fn obj(&self) -> MyObj { async fn obj(&self) -> MyObj {
MyObj { MyObj {
value: 10, value: 10,
#[cfg(feature = "bson")]
value_bson: 10, value_bson: 10,
#[cfg(feature = "abc")]
value_abc: 10, value_abc: 10,
} }
} }
} }
let schema = Schema::new(QueryRoot, EmptyMutation, Subscription); let schema = Schema::new(QueryRoot, EmptyMutation, SubscriptionRoot);
let query = "{ value }"; let query = "{ value }";
assert_eq!( assert_eq!(
schema.execute(query).await.data, schema.execute(query).await.data,
@ -85,13 +84,13 @@ pub async fn test_field_features() {
let query = "{ valueAbc }"; let query = "{ valueAbc }";
assert_eq!( assert_eq!(
schema.execute(query).await.into_result().unwrap_err(), schema.execute(query).await.into_result().unwrap_err(),
Error::Query { Error::Rule {
pos: Pos { column: 3, line: 1 }, errors: vec![RuleError {
path: Some(serde_json::json!(["valueAbc"])), locations: vec![Pos { line: 1, column: 3 }],
err: QueryError::FieldError { message: r#"Unknown field "valueAbc" on type "QueryRoot". Did you mean "value"?"#
err: "`valueAbc` is only available if the features `abc` are enabled".to_string(), .to_string(),
extended_error: None }]
} .into(),
} }
); );
@ -114,13 +113,13 @@ pub async fn test_field_features() {
let query = "{ obj { valueAbc } }"; let query = "{ obj { valueAbc } }";
assert_eq!( assert_eq!(
schema.execute(query).await.into_result().unwrap_err(), schema.execute(query).await.into_result().unwrap_err(),
Error::Query { Error::Rule {
pos: Pos { column: 9, line: 1 }, errors: vec![RuleError {
path: Some(serde_json::json!(["obj", "valueAbc"])), locations: vec![Pos { line: 1, column: 9 }],
err: QueryError::FieldError { message: r#"Unknown field "valueAbc" on type "MyObj". Did you mean "value"?"#
err: "`valueAbc` is only available if the features `abc` are enabled".to_string(), .to_string(),
extended_error: None }]
} .into(),
} }
); );
@ -152,16 +151,17 @@ pub async fn test_field_features() {
.unwrap() .unwrap()
.error .error
.unwrap(), .unwrap(),
Error::Query { Error::Rule {
pos: Pos { errors: vec![RuleError {
column: 16, locations: vec![Pos {
line: 1 line: 1,
}, column: 16
path: Some(serde_json::json!(["valuesAbc"])), }],
err: QueryError::FieldError { message:
err: "`valuesAbc` is only available if the features `abc` are enabled".to_string(), r#"Unknown field "valuesAbc" on type "SubscriptionRoot". Did you mean "values", "valuesBson"?"#
extended_error: None .to_string(),
} }]
.into(),
} }
); );
} }