Rework guards

This commit is contained in:
Sunli 2021-11-16 10:22:39 +08:00
parent 28f027a8bc
commit 60245769f5
9 changed files with 76 additions and 427 deletions

View File

@ -1,5 +1,5 @@
use darling::ast::{Data, Fields};
use darling::util::Ignored;
use darling::util::{Ignored, SpannedValue};
use darling::{FromDeriveInput, FromField, FromMeta, FromVariant};
use inflector::Inflector;
use syn::{
@ -142,7 +142,7 @@ pub struct SimpleObjectField {
#[darling(default)]
pub requires: Option<String>,
#[darling(default)]
pub guard: Option<Meta>,
pub guard: Option<SpannedValue<String>>,
#[darling(default)]
pub visible: Option<Visible>,
#[darling(default, multiple)]
@ -258,7 +258,7 @@ pub struct ObjectField {
pub external: bool,
pub provides: Option<String>,
pub requires: Option<String>,
pub guard: Option<Meta>,
pub guard: Option<SpannedValue<String>>,
pub visible: Option<Visible>,
pub complexity: Option<ComplexityType>,
#[darling(default, multiple)]
@ -495,7 +495,7 @@ pub struct SubscriptionField {
pub skip: bool,
pub name: Option<String>,
pub deprecation: Deprecation,
pub guard: Option<Meta>,
pub guard: Option<SpannedValue<String>>,
pub visible: Option<Visible>,
pub complexity: Option<ComplexityType>,
}
@ -693,7 +693,7 @@ pub struct ComplexObjectField {
pub external: bool,
pub provides: Option<String>,
pub requires: Option<String>,
pub guard: Option<Meta>,
pub guard: Option<SpannedValue<String>>,
pub visible: Option<Visible>,
pub complexity: Option<ComplexityType>,
}

View File

@ -13,8 +13,8 @@ use crate::args::{self, ComplexityType, RenameRuleExt, RenameTarget};
use crate::output_type::OutputType;
use crate::utils::{
extract_input_args, gen_deprecation, generate_default, generate_guards, get_cfg_attrs,
get_crate_name, get_param_getter_ident, get_rustdoc, get_type_path_and_name,
parse_complexity_expr, parse_graphql_attrs, remove_graphql_attrs, visible_fn, GeneratorResult,
get_crate_name, get_rustdoc, get_type_path_and_name, parse_complexity_expr,
parse_graphql_attrs, remove_graphql_attrs, visible_fn, GeneratorResult,
};
pub fn generate(
@ -234,12 +234,9 @@ pub fn generate(
Some(quote!(.map_err(|err| err.into_server_error(__pos)))),
)?;
let param_getter_name = get_param_getter_ident(&ident.ident.unraw().to_string());
get_params.push(quote! {
#[allow(non_snake_case)]
let #param_getter_name = || { ctx.param_value::<#ty>(#name, #default) };
#[allow(non_snake_case)]
let (__pos, #ident) = #param_getter_name()?;
let (__pos, #ident) = ctx.param_value::<#ty>(#name, #default)?;
#validators
});
}
@ -343,8 +340,11 @@ pub fn generate(
}
};
let guard_map_err = quote! {
.map_err(|err| err.into_server_error(ctx.item.pos))
};
let guard = match &method_args.guard {
Some(meta_list) => generate_guards(&crate_name, meta_list)?,
Some(code) => Some(generate_guards(&crate_name, code, guard_map_err)?),
None => None,
};

View File

@ -13,8 +13,8 @@ use crate::args::{self, ComplexityType, RenameRuleExt, RenameTarget};
use crate::output_type::OutputType;
use crate::utils::{
extract_input_args, gen_deprecation, generate_default, generate_guards, get_cfg_attrs,
get_crate_name, get_param_getter_ident, get_rustdoc, get_type_path_and_name,
parse_complexity_expr, parse_graphql_attrs, remove_graphql_attrs, visible_fn, GeneratorResult,
get_crate_name, get_rustdoc, get_type_path_and_name, parse_complexity_expr,
parse_graphql_attrs, remove_graphql_attrs, visible_fn, GeneratorResult,
};
pub fn generate(
@ -369,13 +369,9 @@ pub fn generate(
Some(quote!(.map_err(|err| err.into_server_error(__pos)))),
)?;
let param_getter_name =
get_param_getter_ident(&ident.ident.unraw().to_string());
get_params.push(quote! {
#[allow(non_snake_case)]
let #param_getter_name = || ctx.param_value::<#ty>(#name, #default);
#[allow(non_snake_case, unused_variables)]
let (__pos, #ident) = #param_getter_name()?;
let (__pos, #ident) = ctx.param_value::<#ty>(#name, #default)?;
#validators
});
}
@ -479,17 +475,14 @@ pub fn generate(
}
};
let guard_map_err = quote! {
.map_err(|err| err.into_server_error(ctx.item.pos))
};
let guard = match &method_args.guard {
Some(meta_list) => generate_guards(&crate_name, meta_list)?,
Some(code) => Some(generate_guards(&crate_name, code, guard_map_err)?),
None => None,
};
let guard = guard.map(|guard| {
quote! {
#guard.check(ctx).await.map_err(|err| err.into_server_error(ctx.item.pos))?;
}
});
resolvers.push(quote! {
#(#cfg_attrs)*
if ctx.item.node.name.node == #field_name {

View File

@ -168,13 +168,13 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult<TokenStream
});
});
let guard_map_err = quote! {
.map_err(|err| err.into_server_error(ctx.item.pos))
};
let guard = match &field.guard {
Some(meta) => generate_guards(&crate_name, meta)?,
Some(code) => Some(generate_guards(&crate_name, code, guard_map_err)?),
None => None,
};
let guard = guard.map(
|guard| quote! { #guard.check(ctx).await.map_err(|err| err.into_server_error(ctx.item.pos))?; },
);
let with_function = derived.as_ref().and_then(|x| x.with.as_ref());

View File

@ -9,9 +9,9 @@ use syn::{
use crate::args::{self, ComplexityType, RenameRuleExt, RenameTarget, SubscriptionField};
use crate::output_type::OutputType;
use crate::utils::{
gen_deprecation, generate_default, generate_guards, get_cfg_attrs, get_crate_name,
get_param_getter_ident, get_rustdoc, get_type_path_and_name, parse_complexity_expr,
parse_graphql_attrs, remove_graphql_attrs, visible_fn, GeneratorResult,
gen_deprecation, generate_default, generate_guards, get_cfg_attrs, get_crate_name, get_rustdoc,
get_type_path_and_name, parse_complexity_expr, parse_graphql_attrs, remove_graphql_attrs,
visible_fn, GeneratorResult,
};
pub fn generate(
@ -203,12 +203,9 @@ pub fn generate(
Some(quote!(.map_err(|err| err.into_server_error(__pos)))),
)?;
let param_getter_name = get_param_getter_ident(&ident.ident.unraw().to_string());
get_params.push(quote! {
#[allow(non_snake_case)]
let #param_getter_name = || { ctx.param_value::<#ty>(#name, #default) };
#[allow(non_snake_case)]
let (__pos, #ident) = #param_getter_name()?;
let (__pos, #ident) = ctx.param_value::<#ty>(#name, #default)?;
#validators
});
}
@ -322,16 +319,16 @@ pub fn generate(
})
};
let guard = match &field.guard {
Some(meta_list) => generate_guards(&crate_name, meta_list)?,
None => None,
};
let guard = guard.map(|guard| quote! {
#guard.check(ctx).await.map_err(|err| {
let guard_map_err = quote! {
.map_err(|err| {
err.into_server_error(ctx.item.pos)
.with_path(::std::vec![#crate_name::PathSegment::Field(::std::borrow::ToOwned::to_owned(&*field_name))])
})?;
});
})
};
let guard = match &field.guard {
Some(code) => Some(generate_guards(&crate_name, code, guard_map_err)?),
None => None,
};
let stream_fn = quote! {
let field_name = ::std::clone::Clone::clone(&ctx.item.node.response_key().node);
let field = ::std::sync::Arc::new(::std::clone::Clone::clone(&ctx.item));

View File

@ -1,18 +1,18 @@
use std::collections::HashSet;
use darling::util::SpannedValue;
use darling::FromMeta;
use proc_macro2::{Span, TokenStream, TokenTree};
use proc_macro_crate::{crate_name, FoundCrate};
use quote::quote;
use syn::visit::Visit;
use syn::{
Attribute, Error, Expr, ExprPath, FnArg, Ident, ImplItemMethod, Lit, LitStr, Meta, NestedMeta,
Pat, PatIdent, Type, TypeGroup, TypeParamBound, TypeReference,
Attribute, Error, Expr, ExprPath, FnArg, Ident, ImplItemMethod, Lit, LitStr, Meta, Pat,
PatIdent, Type, TypeGroup, TypeParamBound, TypeReference,
};
use thiserror::Error;
use crate::args;
use crate::args::{Argument, Deprecation, Visible};
use crate::args::{self, Argument, Deprecation, Visible};
#[derive(Error, Debug)]
pub enum GeneratorError {
@ -48,143 +48,18 @@ pub fn get_crate_name(internal: bool) -> TokenStream {
pub fn generate_guards(
crate_name: &TokenStream,
args: &Meta,
) -> GeneratorResult<Option<TokenStream>> {
match args {
Meta::List(args) => match args.path.get_ident().map(ToString::to_string) {
Some(ident) if ident == "guard" => {
if args.nested.len() != 1 {
return Err(Error::new_spanned(
args,
"Chained rules isn't possible anymore, please use operators.",
)
.into());
}
if let NestedMeta::Meta(rule) = &args.nested[0] {
generate_guards(crate_name, rule)
} else {
Err(Error::new_spanned(&args.nested[0], "Invalid rule.").into())
}
}
Some(ident) if ident == "and" => {
if args.nested.len() != 2 {
return Err(
Error::new_spanned(args, "and operator support only 2 operands.").into(),
);
}
let first_rule: Option<TokenStream>;
let second_rule: Option<TokenStream>;
if let NestedMeta::Meta(rule) = &args.nested[0] {
first_rule = generate_guards(crate_name, rule)?;
} else {
return Err(Error::new_spanned(&args.nested[0], "Invalid rule.").into());
}
if let NestedMeta::Meta(rule) = &args.nested[1] {
second_rule = generate_guards(crate_name, rule)?;
} else {
return Err(Error::new_spanned(&args.nested[1], "Invalid rule.").into());
}
Ok(Some(
quote! { #crate_name::guard::GuardExt::and(#first_rule, #second_rule) },
))
}
Some(ident) if ident == "or" => {
if args.nested.len() != 2 {
return Err(
Error::new_spanned(args, "or operator support only 2 operands.").into(),
);
}
let first_rule: Option<TokenStream>;
let second_rule: Option<TokenStream>;
if let NestedMeta::Meta(rule) = &args.nested[0] {
first_rule = generate_guards(crate_name, rule)?;
} else {
return Err(Error::new_spanned(&args.nested[0], "Invalid rule.").into());
}
if let NestedMeta::Meta(rule) = &args.nested[1] {
second_rule = generate_guards(crate_name, rule)?;
} else {
return Err(Error::new_spanned(&args.nested[1], "Invalid rule.").into());
}
Ok(Some(
quote! { #crate_name::guard::GuardExt::or(#first_rule, #second_rule) },
))
}
Some(ident) if ident == "chain" => {
if args.nested.len() < 2 {
return Err(Error::new_spanned(
args,
"chain operator need at least 1 operand.",
)
.into());
}
let mut guards: Option<TokenStream> = None;
for arg in &args.nested {
if let NestedMeta::Meta(rule) = &arg {
let guard = generate_guards(crate_name, rule)?;
if guards.is_none() {
guards = guard;
} else {
guards =
Some(quote! { #crate_name::guard::GuardExt::and(#guards, #guard) });
}
}
}
Ok(guards)
}
Some(ident) if ident == "race" => {
if args.nested.len() < 2 {
return Err(
Error::new_spanned(args, "race operator need at least 1 operand.").into(),
);
}
let mut guards: Option<TokenStream> = None;
for arg in &args.nested {
if let NestedMeta::Meta(rule) = &arg {
let guard = generate_guards(crate_name, rule)?;
if guards.is_none() {
guards = guard;
} else {
guards =
Some(quote! { #crate_name::guard::GuardExt::or(#guards, #guard) });
}
}
}
Ok(guards)
}
_ => {
let ty = &args.path;
let mut params = Vec::new();
for attr in &args.nested {
if let NestedMeta::Meta(Meta::NameValue(nv)) = attr {
let name = &nv.path;
if let Lit::Str(value) = &nv.lit {
let value_str = value.value();
if let Some(value_str) = value_str.strip_prefix('@') {
let getter_name = get_param_getter_ident(value_str);
params.push(
quote! { #name: #getter_name().map(|(_, value)| value)? },
);
} else {
let expr = syn::parse_str::<Expr>(&value_str)?;
params.push(quote! { #name: (#expr).into() });
}
} else {
return Err(Error::new_spanned(
&nv.lit,
"Value must be string literal",
)
.into());
}
} else {
return Err(Error::new_spanned(attr, "Invalid property for guard").into());
}
}
Ok(Some(quote! { #ty { #(#params),* } }))
}
},
_ => Err(Error::new_spanned(args, "Invalid guards").into()),
}
code: &SpannedValue<String>,
map_err: TokenStream,
) -> GeneratorResult<TokenStream> {
let expr: Expr =
syn::parse_str(code).map_err(|err| Error::new(code.span(), err.to_string()))?;
let code = quote! {{
use #crate_name::guard::GuardExt;
#expr
}};
Ok(quote! {
#crate_name::guard::Guard::check(&#code, &ctx).await #map_err ?;
})
}
pub fn get_rustdoc(attrs: &[Attribute]) -> GeneratorResult<Option<String>> {
@ -258,10 +133,6 @@ pub fn generate_default(
}
}
pub fn get_param_getter_ident(name: &str) -> Ident {
Ident::new(&format!("__{}_getter", name), Span::call_site())
}
pub fn get_cfg_attrs(attrs: &[Attribute]) -> Vec<Attribute> {
attrs
.iter()

View File

@ -2,7 +2,7 @@ use darling::util::SpannedValue;
use darling::FromMeta;
use proc_macro2::TokenStream;
use quote::{quote, ToTokens};
use syn::{Expr, Lit, Result};
use syn::{Error, Expr, Lit, Result};
#[derive(Clone)]
pub enum Number {
@ -111,7 +111,8 @@ impl Validators {
}
for s in &self.custom {
let expr: Expr = syn::parse_str(s)?;
let expr: Expr =
syn::parse_str(s).map_err(|err| Error::new(s.span(), err.to_string()))?;
codes.push(quote! {
#crate_name::CustomValidator::check(&(#expr), &ctx, #value).await
.map_err(|err_msg| #crate_name::InputValueError::<#ty>::custom(err_msg))

View File

@ -5,8 +5,6 @@ use crate::{Context, Result};
/// Field guard
///
/// Guard is a pre-condition for a field that is resolved if `Ok(())` is returned, otherwise an error is returned.
///
/// This trait is defined through the [`async-trait`](https://crates.io/crates/async-trait) macro.
#[async_trait::async_trait]
pub trait Guard {
/// Check whether the guard will allow access to the field.

View File

@ -12,8 +12,10 @@ pub struct RoleGuard {
role: Role,
}
mod guards {
pub use super::RoleGuard;
impl RoleGuard {
fn new(role: Role) -> Self {
Self { role }
}
}
#[async_trait::async_trait]
@ -29,31 +31,20 @@ impl Guard for RoleGuard {
struct Username(String);
struct UserGuard {
username: String,
struct UserGuard<'a> {
username: &'a str,
}
#[async_trait::async_trait]
impl Guard for UserGuard {
async fn check(&self, ctx: &Context<'_>) -> Result<()> {
if ctx.data_opt::<Username>().map(|name| &name.0).as_deref() == Some(&self.username) {
Ok(())
} else {
Err("Forbidden".into())
}
impl<'a> UserGuard<'a> {
fn new(username: &'a str) -> Self {
Self { username }
}
}
struct Age(i32);
struct AgeGuard {
age: i32,
}
#[async_trait::async_trait]
impl Guard for AgeGuard {
impl<'a> Guard for UserGuard<'a> {
async fn check(&self, ctx: &Context<'_>) -> Result<()> {
if ctx.data_opt::<Age>().map(|name| &name.0) == Some(&self.age) {
if ctx.data_opt::<Username>().map(|name| name.0.as_str()) == Some(self.username) {
Ok(())
} else {
Err("Forbidden".into())
@ -65,7 +56,7 @@ impl Guard for AgeGuard {
pub async fn test_guard_simple_rule() {
#[derive(SimpleObject)]
struct Query {
#[graphql(guard(guards::RoleGuard(role = "Role::Admin")))]
#[graphql(guard = "RoleGuard::new(Role::Admin)")]
value: i32,
}
@ -73,7 +64,7 @@ pub async fn test_guard_simple_rule() {
#[Subscription]
impl Subscription {
#[graphql(guard(RoleGuard(role = "Role::Admin")))]
#[graphql(guard = "RoleGuard::new(Role::Admin)")]
async fn values(&self) -> impl Stream<Item = i32> {
futures_util::stream::iter(vec![1, 2, 3])
}
@ -143,10 +134,7 @@ pub async fn test_guard_simple_rule() {
pub async fn test_guard_and_operator() {
#[derive(SimpleObject)]
struct Query {
#[graphql(guard(and(
RoleGuard(role = "Role::Admin"),
UserGuard(username = r#""test""#)
)))]
#[graphql(guard = r#"RoleGuard::new(Role::Admin).and(UserGuard::new("test"))"#)]
value: i32,
}
@ -230,7 +218,7 @@ pub async fn test_guard_and_operator() {
pub async fn test_guard_or_operator() {
#[derive(SimpleObject)]
struct Query {
#[graphql(guard(or(RoleGuard(role = "Role::Admin"), UserGuard(username = r#""test""#))))]
#[graphql(guard = r#"RoleGuard::new(Role::Admin).or(UserGuard::new("test"))"#)]
value: i32,
}
@ -296,211 +284,6 @@ pub async fn test_guard_or_operator() {
);
}
#[tokio::test]
pub async fn test_guard_chain_operator() {
#[derive(SimpleObject)]
struct Query {
#[graphql(guard(chain(
RoleGuard(role = "Role::Admin"),
UserGuard(username = r#""test""#),
AgeGuard(age = r#"21"#)
)))]
value: i32,
}
let schema = Schema::new(Query { value: 10 }, EmptyMutation, EmptySubscription);
let query = "{ value }";
assert_eq!(
schema
.execute(
Request::new(query)
.data(Role::Admin)
.data(Username("test".to_string()))
.data(Age(21))
)
.await
.data,
value!({"value": 10})
);
let query = "{ value }";
assert_eq!(
schema
.execute(
Request::new(query)
.data(Role::Guest)
.data(Username("test".to_string()))
.data(Age(21))
)
.await
.into_result()
.unwrap_err(),
vec![ServerError {
message: "Forbidden".to_string(),
source: None,
locations: vec![Pos { line: 1, column: 3 }],
path: vec![PathSegment::Field("value".to_owned())],
extensions: None,
}]
);
let query = "{ value }";
assert_eq!(
schema
.execute(
Request::new(query)
.data(Role::Admin)
.data(Username("test1".to_string()))
.data(Age(21))
)
.await
.into_result()
.unwrap_err(),
vec![ServerError {
message: "Forbidden".to_string(),
source: None,
locations: vec![Pos { line: 1, column: 3 }],
path: vec![PathSegment::Field("value".to_owned())],
extensions: None,
}]
);
let query = "{ value }";
assert_eq!(
schema
.execute(
Request::new(query)
.data(Role::Admin)
.data(Username("test".to_string()))
.data(Age(22))
)
.await
.into_result()
.unwrap_err(),
vec![ServerError {
message: "Forbidden".to_string(),
source: None,
locations: vec![Pos { line: 1, column: 3 }],
path: vec![PathSegment::Field("value".to_owned())],
extensions: None,
}]
);
let query = "{ value }";
assert_eq!(
schema
.execute(
Request::new(query)
.data(Role::Guest)
.data(Username("test1".to_string()))
.data(Age(22))
)
.await
.into_result()
.unwrap_err(),
vec![ServerError {
message: "Forbidden".to_string(),
source: None,
locations: vec![Pos { line: 1, column: 3 }],
path: vec![PathSegment::Field("value".to_owned())],
extensions: None,
}]
);
}
#[tokio::test]
pub async fn test_guard_race_operator() {
#[derive(SimpleObject)]
struct Query {
#[graphql(guard(race(
RoleGuard(role = "Role::Admin"),
UserGuard(username = r#""test""#),
AgeGuard(age = r#"21"#)
)))]
value: i32,
}
let schema = Schema::new(Query { value: 10 }, EmptyMutation, EmptySubscription);
let query = "{ value }";
assert_eq!(
schema
.execute(
Request::new(query)
.data(Role::Admin)
.data(Username("test".to_string()))
.data(Age(21))
)
.await
.data,
value!({"value": 10})
);
let query = "{ value }";
assert_eq!(
schema
.execute(
Request::new(query)
.data(Role::Guest)
.data(Username("test".to_string()))
.data(Age(22))
)
.await
.data,
value!({"value": 10})
);
let query = "{ value }";
assert_eq!(
schema
.execute(
Request::new(query)
.data(Role::Admin)
.data(Username("test1".to_string()))
.data(Age(22))
)
.await
.data,
value!({"value": 10})
);
let query = "{ value }";
assert_eq!(
schema
.execute(
Request::new(query)
.data(Role::Guest)
.data(Username("test1".to_string()))
.data(Age(21))
)
.await
.data,
value!({"value": 10})
);
let query = "{ value }";
assert_eq!(
schema
.execute(
Request::new(query)
.data(Role::Guest)
.data(Username("test1".to_string()))
.data(Age(22))
)
.await
.into_result()
.unwrap_err(),
vec![ServerError {
message: "Forbidden".to_string(),
source: None,
locations: vec![Pos { line: 1, column: 3 }],
path: vec![PathSegment::Field("value".to_owned())],
extensions: None,
}]
);
}
#[tokio::test]
pub async fn test_guard_use_params() {
struct EqGuard {
@ -508,6 +291,12 @@ pub async fn test_guard_use_params() {
actual: i32,
}
impl EqGuard {
fn new(expect: i32, actual: i32) -> Self {
Self { expect, actual }
}
}
#[async_trait::async_trait]
impl Guard for EqGuard {
async fn check(&self, _ctx: &Context<'_>) -> Result<()> {
@ -523,7 +312,7 @@ pub async fn test_guard_use_params() {
#[Object]
impl Query {
#[graphql(guard(EqGuard(expect = "100", actual = "@value")))]
#[graphql(guard = "EqGuard::new(100, value)")]
async fn get(&self, value: i32) -> i32 {
value
}