From 22f7e0953759002231e5ef5e694f168bbb37c3a9 Mon Sep 17 00:00:00 2001 From: Sunli Date: Sun, 4 Oct 2020 07:49:56 +0800 Subject: [PATCH 1/5] Fix typo. #297 --- derive/src/input_object.rs | 6 +++--- src/error.rs | 4 ++-- src/types/external/bson.rs | 2 +- src/types/external/json_object/btreemap.rs | 2 +- src/types/external/json_object/hashmap.rs | 2 +- src/types/external/list/btree_set.rs | 4 ++-- src/types/external/list/hash_set.rs | 4 ++-- src/types/external/list/linked_list.rs | 4 ++-- src/types/external/list/vec.rs | 4 ++-- src/types/external/list/vec_deque.rs | 4 ++-- src/types/external/optional.rs | 2 +- src/types/maybe_undefined.rs | 2 +- 12 files changed, 20 insertions(+), 20 deletions(-) diff --git a/derive/src/input_object.rs b/derive/src/input_object.rs index 3bf6a615..e6458201 100644 --- a/derive/src/input_object.rs +++ b/derive/src/input_object.rs @@ -75,7 +75,7 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult get_fields.push(quote! { let #ident: #ty = #crate_name::InputValueType::parse( Some(#crate_name::Value::Object(obj.clone())) - ).map_err(#crate_name::InputValueError::propogate)?; + ).map_err(#crate_name::InputValueError::propagate)?; }); fields.push(ident); @@ -111,7 +111,7 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult let #ident: #ty = { match obj.get(#name) { Some(value) => #crate_name::InputValueType::parse(Some(value.clone())) - .map_err(#crate_name::InputValueError::propogate)?, + .map_err(#crate_name::InputValueError::propagate)?, None => #default, } }; @@ -119,7 +119,7 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult } else { get_fields.push(quote! { let #ident: #ty = #crate_name::InputValueType::parse(obj.get(#name).cloned()) - .map_err(#crate_name::InputValueError::propogate)?; + .map_err(#crate_name::InputValueError::propagate)?; }); } diff --git a/src/error.rs b/src/error.rs index 3ac86ce3..e6eef7ea 100644 --- a/src/error.rs +++ b/src/error.rs @@ -122,8 +122,8 @@ impl InputValueError { Self::new(format!(r#"Failed to parse "{}": {}"#, T::type_name(), msg)) } - /// Propogate the error message to a different type. - pub fn propogate(self) -> InputValueError { + /// Propagate the error message to a different type. + pub fn propagate(self) -> InputValueError { InputValueError::new(format!( r#"{} (occurred while parsing "{}")"#, self.message, diff --git a/src/types/external/bson.rs b/src/types/external/bson.rs index cf96f2af..94eace3f 100644 --- a/src/types/external/bson.rs +++ b/src/types/external/bson.rs @@ -24,7 +24,7 @@ impl ScalarType for ObjectId { impl ScalarType for UtcDateTime { fn parse(value: Value) -> InputValueResult { >::parse(value) - .map_err(InputValueError::propogate) + .map_err(InputValueError::propagate) .map(UtcDateTime::from) } diff --git a/src/types/external/json_object/btreemap.rs b/src/types/external/json_object/btreemap.rs index 663563ec..2720c96c 100644 --- a/src/types/external/json_object/btreemap.rs +++ b/src/types/external/json_object/btreemap.rs @@ -16,7 +16,7 @@ where .into_iter() .map(|(name, value)| Ok((name.into_string(), T::parse(Some(value))?))) .collect::>() - .map_err(InputValueError::propogate), + .map_err(InputValueError::propagate), _ => Err(InputValueError::expected_type(value)), } } diff --git a/src/types/external/json_object/hashmap.rs b/src/types/external/json_object/hashmap.rs index e992f9c0..e6463dfd 100644 --- a/src/types/external/json_object/hashmap.rs +++ b/src/types/external/json_object/hashmap.rs @@ -16,7 +16,7 @@ where .into_iter() .map(|(name, value)| Ok((name.into_string(), T::parse(Some(value))?))) .collect::>() - .map_err(InputValueError::propogate), + .map_err(InputValueError::propagate), _ => Err(InputValueError::expected_type(value)), } } diff --git a/src/types/external/list/btree_set.rs b/src/types/external/list/btree_set.rs index f0b76d68..bb9b69d4 100644 --- a/src/types/external/list/btree_set.rs +++ b/src/types/external/list/btree_set.rs @@ -29,11 +29,11 @@ impl InputValueType for BTreeSet { .into_iter() .map(|value| InputValueType::parse(Some(value))) .collect::>() - .map_err(InputValueError::propogate), + .map_err(InputValueError::propagate), value => Ok({ let mut result = Self::default(); result.insert( - InputValueType::parse(Some(value)).map_err(InputValueError::propogate)?, + InputValueType::parse(Some(value)).map_err(InputValueError::propagate)?, ); result }), diff --git a/src/types/external/list/hash_set.rs b/src/types/external/list/hash_set.rs index 88d547f1..c12d2ea0 100644 --- a/src/types/external/list/hash_set.rs +++ b/src/types/external/list/hash_set.rs @@ -31,11 +31,11 @@ impl InputValueType for HashSet { .into_iter() .map(|value| InputValueType::parse(Some(value))) .collect::>() - .map_err(InputValueError::propogate), + .map_err(InputValueError::propagate), value => Ok({ let mut result = Self::default(); result.insert( - InputValueType::parse(Some(value)).map_err(InputValueError::propogate)?, + InputValueType::parse(Some(value)).map_err(InputValueError::propagate)?, ); result }), diff --git a/src/types/external/list/linked_list.rs b/src/types/external/list/linked_list.rs index 28ecad4f..8b2362d8 100644 --- a/src/types/external/list/linked_list.rs +++ b/src/types/external/list/linked_list.rs @@ -29,11 +29,11 @@ impl InputValueType for LinkedList { .into_iter() .map(|value| InputValueType::parse(Some(value))) .collect::>() - .map_err(InputValueError::propogate), + .map_err(InputValueError::propagate), value => Ok({ let mut result = Self::default(); result.push_front( - InputValueType::parse(Some(value)).map_err(InputValueError::propogate)?, + InputValueType::parse(Some(value)).map_err(InputValueError::propagate)?, ); result }), diff --git a/src/types/external/list/vec.rs b/src/types/external/list/vec.rs index 98ff2807..710cd889 100644 --- a/src/types/external/list/vec.rs +++ b/src/types/external/list/vec.rs @@ -28,9 +28,9 @@ impl InputValueType for Vec { .into_iter() .map(|value| InputValueType::parse(Some(value))) .collect::>() - .map_err(InputValueError::propogate), + .map_err(InputValueError::propagate), value => Ok(vec![ - InputValueType::parse(Some(value)).map_err(InputValueError::propogate)? + InputValueType::parse(Some(value)).map_err(InputValueError::propagate)? ]), } } diff --git a/src/types/external/list/vec_deque.rs b/src/types/external/list/vec_deque.rs index 4eb9e796..913bcadb 100644 --- a/src/types/external/list/vec_deque.rs +++ b/src/types/external/list/vec_deque.rs @@ -29,11 +29,11 @@ impl InputValueType for VecDeque { .into_iter() .map(|value| InputValueType::parse(Some(value))) .collect::>() - .map_err(InputValueError::propogate), + .map_err(InputValueError::propagate), value => Ok({ let mut result = Self::default(); result.push_back( - InputValueType::parse(Some(value)).map_err(InputValueError::propogate)?, + InputValueType::parse(Some(value)).map_err(InputValueError::propagate)?, ); result }), diff --git a/src/types/external/optional.rs b/src/types/external/optional.rs index 83cac8a9..ddac1602 100644 --- a/src/types/external/optional.rs +++ b/src/types/external/optional.rs @@ -25,7 +25,7 @@ impl InputValueType for Option { match value.unwrap_or_default() { Value::Null => Ok(None), value => Ok(Some( - T::parse(Some(value)).map_err(InputValueError::propogate)?, + T::parse(Some(value)).map_err(InputValueError::propagate)?, )), } } diff --git a/src/types/maybe_undefined.rs b/src/types/maybe_undefined.rs index 3c6bebb1..ae3b6f3c 100644 --- a/src/types/maybe_undefined.rs +++ b/src/types/maybe_undefined.rs @@ -130,7 +130,7 @@ impl InputValueType for MaybeUndefined { None => Ok(MaybeUndefined::Undefined), Some(Value::Null) => Ok(MaybeUndefined::Null), Some(value) => Ok(MaybeUndefined::Value( - T::parse(Some(value)).map_err(InputValueError::propogate)?, + T::parse(Some(value)).map_err(InputValueError::propagate)?, )), } } From 71fa94392aa888e2a7d6ae42aed1507a2e962fc1 Mon Sep 17 00:00:00 2001 From: Roman Kudryashov Date: Sun, 4 Oct 2020 12:21:53 +0300 Subject: [PATCH 2/5] Fix custom scalars doc --- docs/en/src/custom_scalars.md | 2 +- docs/zh-CN/src/custom_scalars.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/src/custom_scalars.md b/docs/en/src/custom_scalars.md index 7565269f..38ce23e8 100644 --- a/docs/en/src/custom_scalars.md +++ b/docs/en/src/custom_scalars.md @@ -19,7 +19,7 @@ impl ScalarType for StringNumber { Ok(value.parse().map(StringNumber)?) } else { // If the type does not match - Err(InputValueError::ExpectedType(value)) + Err(InputValueError::expected_type(value)) } } diff --git a/docs/zh-CN/src/custom_scalars.md b/docs/zh-CN/src/custom_scalars.md index 2c7e4ca4..71e15df5 100644 --- a/docs/zh-CN/src/custom_scalars.md +++ b/docs/zh-CN/src/custom_scalars.md @@ -20,7 +20,7 @@ impl ScalarType for StringNumber { Ok(value.parse().map(StringNumber)?) } else { // 类型不匹配 - Err(InputValueError::ExpectedType(value)) + Err(InputValueError::expected_type(value)) } } From 5f2b7580f45b19721413a984e8fb8604bbaa6b72 Mon Sep 17 00:00:00 2001 From: Sunli Date: Mon, 5 Oct 2020 07:05:32 +0800 Subject: [PATCH 3/5] Update CI --- .github/workflows/ci.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8a1831ff..8ca1305a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,3 +28,14 @@ jobs: run: cargo build --all --verbose - name: Run tests run: cargo test --all --verbose + + # build on nightly + - uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + override: true + components: clippy, rustfmt + - name: Build on nightly without features + run: cargo build --no-default-features + - name: Build on nightly + run: cargo build --all --verbose From 53eab09f196e90f2550f47ac5f60acbe39bee870 Mon Sep 17 00:00:00 2001 From: AurelienFT <32803821+AurelienFT@users.noreply.github.com> Date: Tue, 6 Oct 2020 11:16:51 +0200 Subject: [PATCH 4/5] Rework guard (#296) Rework guard #293 --- derive/src/args.rs | 4 - derive/src/object.rs | 17 +- derive/src/simple_object.rs | 11 +- derive/src/subscription.rs | 7 - derive/src/utils.rs | 188 +++++++++++-------- docs/en/src/SUMMARY.md | 1 + docs/en/src/field_guard.md | 69 +++++++ src/guard.rs | 18 +- src/lib.rs | 3 - tests/guard.rs | 354 +++++++++++++++++++++++++++--------- tests/post_guard.rs | 327 --------------------------------- 11 files changed, 479 insertions(+), 520 deletions(-) create mode 100644 docs/en/src/field_guard.md delete mode 100644 tests/post_guard.rs diff --git a/derive/src/args.rs b/derive/src/args.rs index c0bcf094..3c6b88b5 100644 --- a/derive/src/args.rs +++ b/derive/src/args.rs @@ -69,8 +69,6 @@ pub struct SimpleObjectField { pub requires: Option, #[darling(default)] pub guard: Option, - #[darling(default)] - pub post_guard: Option, } #[derive(FromDeriveInput)] @@ -123,7 +121,6 @@ pub struct ObjectField { pub provides: Option, pub requires: Option, pub guard: Option, - pub post_guard: Option, } #[derive(FromDeriveInput)] @@ -302,7 +299,6 @@ pub struct SubscriptionField { pub name: Option, pub deprecation: Option, pub guard: Option, - pub post_guard: Option, } #[derive(FromMeta, Default)] diff --git a/derive/src/object.rs b/derive/src/object.rs index df950b9a..d884c5dd 100644 --- a/derive/src/object.rs +++ b/derive/src/object.rs @@ -1,8 +1,8 @@ use crate::args; use crate::output_type::OutputType; use crate::utils::{ - generate_default, generate_guards, generate_post_guards, generate_validator, get_cfg_attrs, - get_crate_name, get_param_getter_ident, get_rustdoc, parse_graphql_attrs, remove_graphql_attrs, + generate_default, generate_guards, generate_validator, get_cfg_attrs, get_crate_name, + get_param_getter_ident, get_rustdoc, parse_graphql_attrs, remove_graphql_attrs, GeneratorResult, }; use inflector::Inflector; @@ -428,18 +428,6 @@ pub fn generate( } }); - let post_guard = match &method_args.post_guard { - Some(meta_list) => generate_post_guards(&crate_name, meta_list)?, - None => None, - }; - - let post_guard = post_guard.map(|guard| { - quote! { - #guard.check(ctx, &res).await - .map_err(|err| err.into_server_error().at(ctx.item.pos))?; - } - }); - resolvers.push(quote! { #(#cfg_attrs)* if ctx.item.node.name.node == #field_name { @@ -447,7 +435,6 @@ pub fn generate( #guard let ctx_obj = ctx.with_selection_set(&ctx.item.node.selection_set); let res = #resolve_obj; - #post_guard return #crate_name::OutputValueType::resolve(&res, &ctx_obj, ctx.item).await.map(::std::option::Option::Some); } }); diff --git a/derive/src/simple_object.rs b/derive/src/simple_object.rs index ccae27d6..af1cb54c 100644 --- a/derive/src/simple_object.rs +++ b/derive/src/simple_object.rs @@ -1,7 +1,5 @@ use crate::args; -use crate::utils::{ - generate_guards, generate_post_guards, get_crate_name, get_rustdoc, GeneratorResult, -}; +use crate::utils::{generate_guards, get_crate_name, get_rustdoc, GeneratorResult}; use darling::ast::Data; use inflector::Inflector; use proc_macro::TokenStream; @@ -102,12 +100,6 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult generate_post_guards(&crate_name, &meta)?, - None => None, - }; - let post_guard = post_guard.map(|guard| quote! { #guard.check(ctx, &res).await.map_err(|err| err.into_server_error().at(ctx.item.pos))?; }); - getters.push(if !field.owned { quote! { #[inline] @@ -131,7 +123,6 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult GeneratorResult> { match args { - Meta::List(args) => { - let mut guards = None; - for item in &args.nested { - if let NestedMeta::Meta(Meta::List(ls)) = item { - let ty = &ls.path; + Meta::List(args) => match args.path.get_ident() { + Some(ident) => match ident.to_string().as_str() { + "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()) + } + } + "and" => { + if args.nested.len() != 2 { + return Err(Error::new_spanned( + args, + "and operator support only 2 operands.", + ) + .into()); + } + let first_rule: Option; + let second_rule: Option; + 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) }, + )) + } + "or" => { + if args.nested.len() != 2 { + return Err(Error::new_spanned( + args, + "or operator support only 2 operands.", + ) + .into()); + } + let first_rule: Option; + let second_rule: Option; + 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) }, + )) + } + "chain" => { + if args.nested.len() < 2 { + return Err(Error::new_spanned( + args, + "chain operator need at least 1 operand.", + ) + .into()); + } + let mut guards: Option = 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(#guard, #guards) }, + ); + } + } + } + Ok(guards) + } + "race" => { + if args.nested.len() < 2 { + return Err(Error::new_spanned( + args, + "race operator need at least 1 operand.", + ) + .into()); + } + let mut guards: Option = 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(#guard, #guards) }, + ); + } + } + } + Ok(guards) + } + _ => { + let ty = &args.path; let mut params = Vec::new(); - for attr in &ls.nested { + for attr in &args.nested { if let NestedMeta::Meta(Meta::NameValue(nv)) = attr { let name = &nv.path; if let Lit::Str(value) = &nv.lit { @@ -153,72 +260,11 @@ pub fn generate_guards( ); } } - let guard = quote! { #ty { #(#params),* } }; - if guards.is_none() { - guards = Some(guard); - } else { - guards = - Some(quote! { #crate_name::guard::GuardExt::and(#guard, #guards) }); - } - } else { - return Err(Error::new_spanned(item, "Invalid guard").into()); + Ok(Some(quote! { #ty { #(#params),* } })) } - } - Ok(guards) - } - _ => Err(Error::new_spanned(args, "Invalid guards").into()), - } -} - -pub fn generate_post_guards( - crate_name: &TokenStream, - args: &Meta, -) -> GeneratorResult> { - match args { - Meta::List(args) => { - let mut guards = None; - for item in &args.nested { - if let NestedMeta::Meta(Meta::List(ls)) = item { - let ty = &ls.path; - let mut params = Vec::new(); - for attr in &ls.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 value_str.starts_with('@') { - let getter_name = get_param_getter_ident(&value_str[1..]); - params.push(quote! { #name: #getter_name()? }); - } else { - let expr = syn::parse_str::(&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() - ); - } - } - let guard = quote! { #ty { #(#params),* } }; - if guards.is_none() { - guards = Some(guard); - } else { - guards = - Some(quote! { #crate_name::guard::PostGuardExt::and(#guard, #guards) }); - } - } else { - return Err(Error::new_spanned(item, "Invalid guard").into()); - } - } - Ok(guards) - } + }, + None => Err(Error::new_spanned(args, "Invalid guards").into()), + }, _ => Err(Error::new_spanned(args, "Invalid guards").into()), } } diff --git a/docs/en/src/SUMMARY.md b/docs/en/src/SUMMARY.md index 1dad68f6..71734d00 100644 --- a/docs/en/src/SUMMARY.md +++ b/docs/en/src/SUMMARY.md @@ -17,6 +17,7 @@ - [Query and Mutation](query_and_mutation.md) - [Subscription](subscription.md) - [Utilities](utilities.md) + - [Field guard](field_guard.md) - [Input value validators](input_value_validators.md) - [Cache control](cache_control.md) - [Cursor connections](cursor_connections.md) diff --git a/docs/en/src/field_guard.md b/docs/en/src/field_guard.md new file mode 100644 index 00000000..1e5bd35f --- /dev/null +++ b/docs/en/src/field_guard.md @@ -0,0 +1,69 @@ +# Field Guard + +You can define `guard` to field of an `Object`. This permit to add checks before run the code logic of the field. +`Guard` are made of rules that you need to define before. A rule is a structure which implement the trait `Guard`. + +```rust +#[derive(Eq, PartialEq, Copy, Clone)] +enum Role { + Admin, + Guest, +} + +struct RoleGuard { + role: Role, +} + +#[async_trait::async_trait] +impl Guard for RoleGuard { + async fn check(&self, ctx: &Context<'_>) -> Result<()> { + if ctx.data_opt::() == Some(&self.role) { + Ok(()) + } else { + Err("Forbidden".into()) + } + } +} +``` + +Once you have defined your rule you can use it in the `guard` field attribute. +This attribute support 4 operators to create complex rules : + +- `and` : perform a `and` operation between two rules. (If one rule return an error the `and` operator will return the error. If both rules return a error it's the first one that will be returned). + +- `or` : perform a `and` operation between two rules. (If both rules return an error the error returned is the first one) + +- `chain` : take a set of rules and run them until one return an error or return `Ok()` if all rules pass. + +- `race` : take a set of rules and run them until one return `Ok()` if they all fail it return the last error. + +```rust +#[derive(SimpleObject)] +struct Query { + #[graphql(guard(RoleGuard(role = "Role::Admin")))] + value: i32, + #[graphql(guard(and( + RoleGuard(role = "Role::Admin"), + UserGuard(username = r#""test""#) + )))] + value2: i32, + #[graphql(guard(or( + RoleGuard(role = "Role::Admin"), + UserGuard(username = r#""test""#) + )))] + value3: i32, + #[graphql(guard(chain( + RoleGuard(role = "Role::Admin"), + UserGuard(username = r#""test""#), + AgeGuard(age = r#"21"#) + )))] + value4: i32, + #[graphql(guard(race( + RoleGuard(role = "Role::Admin"), + UserGuard(username = r#""test""#), + AgeGuard(age = r#"21"#) + )))] + value5: i32, +} +``` + diff --git a/src/guard.rs b/src/guard.rs index 8821e645..33f423a6 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -16,10 +16,15 @@ pub trait Guard { /// An extension trait for `Guard`. pub trait GuardExt: Guard + Sized { - /// Merge the two guards. + /// Perform `and` operator on two rules fn and(self, other: R) -> And { And(self, other) } + + /// Perform `or` operator on two rules + fn or(self, other: R) -> Or { + Or(self, other) + } } impl GuardExt for T {} @@ -35,6 +40,17 @@ impl Guard for And { } } +/// Guard for [`GuardExt::or`](trait.GuardExt.html#method.or). +pub struct Or(A, B); + +#[async_trait::async_trait] +impl Guard for Or { + async fn check(&self, ctx: &Context<'_>) -> Result<()> { + let second_result = self.1.check(ctx).await; + self.0.check(ctx).await.or(second_result) + } +} + /// Field post guard /// /// This is a post-condition for a field that is resolved if `Ok(()` is returned, otherwise an error is returned. diff --git a/src/lib.rs b/src/lib.rs index 55dfdddc..20a71f7c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -205,7 +205,6 @@ pub use types::*; /// | 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 | -/// | post_guard | Field of post guard | [`PostGuard`](guard/trait.PostGuard.html) | Y | /// /// # Field argument parameters /// @@ -321,7 +320,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 | /// | 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 | -/// | post_guard | Field of post guard | [`PostGuard`](guard/trait.PostGuard.html) | Y | /// /// # Examples /// @@ -678,7 +676,6 @@ 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 | -/// | post_guard | Field of post guard | [`PostGuard`](guard/trait.PostGuard.html) | Y | /// /// # Field argument parameters /// diff --git a/tests/guard.rs b/tests/guard.rs index 5a9e2afa..377cc0ca 100644 --- a/tests/guard.rs +++ b/tests/guard.rs @@ -40,28 +40,31 @@ impl Guard for UserGuard { } } +struct Age(i32); + +struct AgeGuard { + age: i32, +} + +#[async_trait::async_trait] +impl Guard for AgeGuard { + async fn check(&self, ctx: &Context<'_>) -> Result<()> { + if ctx.data_opt::().map(|name| &name.0) == Some(&self.age) { + Ok(()) + } else { + Err("Forbidden".into()) + } + } +} + #[async_std::test] -pub async fn test_guard() { +pub async fn test_guard_simple_rule() { #[derive(SimpleObject)] - struct MyObj { + struct Query { #[graphql(guard(RoleGuard(role = "Role::Admin")))] value: i32, } - struct Query; - - #[Object] - impl Query { - #[graphql(guard(RoleGuard(role = "Role::Admin")))] - async fn value(&self) -> i32 { - 1 - } - - async fn obj(&self) -> MyObj { - MyObj { value: 99 } - } - } - struct Subscription; #[Subscription] @@ -72,36 +75,7 @@ pub async fn test_guard() { } } - let schema = Schema::new(Query, EmptyMutation, Subscription); - - let query = "{ obj { value } }"; - assert_eq!( - schema - .execute(Request::new(query).data(Role::Admin)) - .await - .data, - serde_json::json!({ - "obj": {"value": 99} - }) - ); - - let query = "{ obj { value } }"; - assert_eq!( - schema - .execute(Request::new(query).data(Role::Guest)) - .await - .into_result() - .unwrap_err(), - vec![ServerError { - message: "Forbidden".to_owned(), - locations: vec![Pos { line: 1, column: 9 }], - path: vec![ - PathSegment::Field("obj".to_owned()), - PathSegment::Field("value".to_owned()) - ], - extensions: None, - }] - ); + let schema = Schema::new(Query { value: 10 }, EmptyMutation, Subscription); let query = "{ value }"; assert_eq!( @@ -109,9 +83,7 @@ pub async fn test_guard() { .execute(Request::new(query).data(Role::Admin)) .await .data, - serde_json::json!({ - "value": 1, - }) + serde_json::json!({"value": 10}) ); let query = "{ value }"; @@ -163,10 +135,13 @@ pub async fn test_guard() { } #[async_std::test] -pub async fn test_multiple_guards() { +pub async fn test_guard_and_operator() { #[derive(SimpleObject)] struct Query { - #[graphql(guard(RoleGuard(role = "Role::Admin"), UserGuard(username = r#""test""#)))] + #[graphql(guard(and( + RoleGuard(role = "Role::Admin"), + UserGuard(username = r#""test""#) + )))] value: i32, } @@ -244,54 +219,269 @@ pub async fn test_multiple_guards() { } #[async_std::test] -pub async fn test_guard_forward_arguments() { - struct UserGuard { - id: ID, +pub async fn test_guard_or_operator() { + #[derive(SimpleObject)] + struct Query { + #[graphql(guard(or(RoleGuard(role = "Role::Admin"), UserGuard(username = r#""test""#))))] + value: i32, } - #[async_trait::async_trait] - impl Guard for UserGuard { - async fn check(&self, ctx: &Context<'_>) -> Result<()> { - if ctx.data_opt::() != Some(&self.id) { - Err("Forbidden".into()) - } else { - Ok(()) - } - } - } + let schema = Schema::new(Query { value: 10 }, EmptyMutation, EmptySubscription); - struct QueryRoot; - - #[Object] - impl QueryRoot { - #[graphql(guard(UserGuard(id = "@id")))] - async fn user(&self, id: ID) -> ID { - id - } - } - - let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription); - - let query = r#"{ user(id: "abc") }"#; + let query = "{ value }"; assert_eq!( schema - .execute(Request::new(query).data(ID::from("abc"))) + .execute( + Request::new(query) + .data(Role::Admin) + .data(Username("test".to_string())) + ) .await .data, - serde_json::json!({"user": "abc"}) + serde_json::json!({"value": 10}) ); - let query = r#"{ user(id: "abc") }"#; + let query = "{ value }"; assert_eq!( schema - .execute(Request::new(query).data(ID::from("aaa"))) + .execute( + Request::new(query) + .data(Role::Guest) + .data(Username("test".to_string())) + ) + .await + .data, + serde_json::json!({"value": 10}) + ); + + let query = "{ value }"; + assert_eq!( + schema + .execute( + Request::new(query) + .data(Role::Admin) + .data(Username("test1".to_string())) + ) + .await + .data, + serde_json::json!({"value": 10}) + ); + + let query = "{ value }"; + assert_eq!( + schema + .execute( + Request::new(query) + .data(Role::Guest) + .data(Username("test1".to_string())) + ) .await .into_result() .unwrap_err(), vec![ServerError { message: "Forbidden".to_string(), locations: vec![Pos { line: 1, column: 3 }], - path: vec![PathSegment::Field("user".to_owned())], + path: vec![PathSegment::Field("value".to_owned())], + extensions: None, + }] + ); +} + +#[async_std::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, + serde_json::json!({"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(), + 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(), + 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(), + 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(), + locations: vec![Pos { line: 1, column: 3 }], + path: vec![PathSegment::Field("value".to_owned())], + extensions: None, + }] + ); +} + +#[async_std::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, + serde_json::json!({"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, + serde_json::json!({"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, + serde_json::json!({"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, + serde_json::json!({"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(), + locations: vec![Pos { line: 1, column: 3 }], + path: vec![PathSegment::Field("value".to_owned())], extensions: None, }] ); diff --git a/tests/post_guard.rs b/tests/post_guard.rs deleted file mode 100644 index 55913b7c..00000000 --- a/tests/post_guard.rs +++ /dev/null @@ -1,327 +0,0 @@ -use async_graphql::guard::PostGuard; -use async_graphql::*; - -#[derive(Eq, PartialEq, Copy, Clone)] -enum Role { - Admin, - Guest, -} - -struct RoleGuard { - role: Role, -} - -#[async_trait::async_trait] -impl PostGuard for RoleGuard { - async fn check(&self, ctx: &Context<'_>, _result: &i32) -> Result<()> { - if ctx.data_opt::() == Some(&self.role) { - Ok(()) - } else { - Err("Forbidden".into()) - } - } -} - -#[derive(SimpleObject)] -struct MyObj { - #[graphql(owned, post_guard(UserGuard(username = r#""test""#, value = "88")))] - value: i32, -} - -struct Username(String); - -struct UserGuard { - value: i32, - username: String, -} - -#[async_trait::async_trait] -impl PostGuard for UserGuard { - async fn check(&self, ctx: &Context<'_>, result: &i32) -> Result<()> { - assert_eq!(*result, self.value); - if ctx.data_opt::().as_ref().map(|s| s.0.as_str()) == Some(&self.username) { - Ok(()) - } else { - Err("Forbidden".into()) - } - } -} - -#[async_trait::async_trait] -impl PostGuard for UserGuard { - async fn check(&self, ctx: &Context<'_>, result: &MyObj) -> Result<()> { - assert_eq!(result.value, self.value); - if ctx.data_opt::().as_ref().map(|s| s.0.as_str()) == Some(&self.username) { - Ok(()) - } else { - Err("Forbidden".into()) - } - } -} - -#[async_std::test] -pub async fn test_post_guard() { - struct Query; - - #[Object] - impl Query { - #[graphql(post_guard(UserGuard(username = r#""test""#, value = "99")))] - async fn value(&self) -> i32 { - 99 - } - - async fn obj(&self) -> MyObj { - MyObj { value: 88 } - } - } - - let schema = Schema::new(Query, EmptyMutation, EmptySubscription); - - let query = "{ value }"; - assert_eq!( - schema - .execute(Request::new(query).data(Username("test".to_string()))) - .await - .data, - serde_json::json!({ - "value": 99 - }) - ); - - let query = "{ value }"; - assert_eq!( - schema - .execute(Request::new(query).data(Username("test1".to_string()))) - .await - .into_result() - .unwrap_err(), - vec![ServerError { - message: "Forbidden".to_string(), - locations: vec![Pos { line: 1, column: 3 }], - path: vec![PathSegment::Field("value".to_owned())], - extensions: None, - }] - ); - - let query = "{ obj { value } }"; - assert_eq!( - schema - .execute(Request::new(query).data(Username("test".to_string()))) - .await - .data, - serde_json::json!({ - "obj": { "value": 88 } - }) - ); - - let query = "{ obj { value } }"; - assert_eq!( - schema - .execute(Request::new(query).data(Username("test1".to_string()))) - .await - .into_result() - .unwrap_err(), - vec![ServerError { - message: "Forbidden".to_string(), - locations: vec![Pos { line: 1, column: 9 }], - path: vec![ - PathSegment::Field("obj".to_owned()), - PathSegment::Field("value".to_owned()) - ], - extensions: None, - }] - ); -} - -#[async_std::test] -pub async fn test_multiple_post_guards() { - #[derive(SimpleObject)] - struct Query { - #[graphql(post_guard( - RoleGuard(role = "Role::Admin"), - UserGuard(username = r#""test""#, value = "10") - ))] - 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())) - ) - .await - .data, - serde_json::json!({"value": 10}) - ); - - let query = "{ value }"; - assert_eq!( - schema - .execute( - Request::new(query) - .data(Role::Guest) - .data(Username("test".to_string())) - ) - .await - .into_result() - .unwrap_err(), - vec![ServerError { - message: "Forbidden".to_string(), - 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())) - ) - .await - .into_result() - .unwrap_err(), - vec![ServerError { - message: "Forbidden".to_string(), - 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())) - ) - .await - .into_result() - .unwrap_err(), - vec![ServerError { - message: "Forbidden".to_string(), - locations: vec![Pos { line: 1, column: 3 }], - path: vec![PathSegment::Field("value".to_owned())], - extensions: None, - }] - ); -} - -#[async_std::test] -pub async fn test_post_guard_forward_arguments() { - struct UserGuard { - id: ID, - } - - #[async_trait::async_trait] - impl PostGuard for UserGuard { - async fn check(&self, ctx: &Context<'_>, result: &ID) -> Result<()> { - assert_eq!(result.as_str(), "haha"); - if ctx.data_opt::() != Some(&self.id) { - Err("Forbidden".into()) - } else { - Ok(()) - } - } - } - - struct QueryRoot; - - #[Object] - impl QueryRoot { - #[graphql(post_guard(UserGuard(id = "@_id")))] - async fn user(&self, _id: ID) -> ID { - "haha".into() - } - } - - let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription); - - let query = r#"{ user(id: "abc") }"#; - assert_eq!( - schema - .execute(Request::new(query).data(ID::from("abc"))) - .await - .data, - serde_json::json!({"user": "haha"}) - ); - - let query = r#"{ user(id: "abc") }"#; - assert_eq!( - schema - .execute(Request::new(query).data(ID::from("aaa"))) - .await - .into_result() - .unwrap_err(), - vec![ServerError { - message: "Forbidden".to_string(), - locations: vec![Pos { line: 1, column: 3 }], - path: vec![PathSegment::Field("user".to_owned())], - extensions: None, - }] - ); -} - -#[async_std::test] -pub async fn test_post_guard_generic() { - struct UserGuard { - id: ID, - } - - #[async_trait::async_trait] - impl PostGuard for UserGuard { - async fn check(&self, ctx: &Context<'_>, _result: &T) -> Result<()> { - if ctx.data_opt::() != Some(&self.id) { - Err("Forbidden".into()) - } else { - Ok(()) - } - } - } - - struct QueryRoot; - - #[Object] - impl QueryRoot { - #[graphql(post_guard(UserGuard(id = r#""abc""#)))] - async fn user(&self) -> ID { - "haha".into() - } - } - - let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription); - - let query = r#"{ user }"#; - assert_eq!( - schema - .execute(Request::new(query).data(ID::from("abc"))) - .await - .data, - serde_json::json!({"user": "haha"}) - ); - - let query = r#"{ user }"#; - assert_eq!( - schema - .execute(Request::new(query).data(ID::from("aaa"))) - .await - .into_result() - .unwrap_err(), - vec![ServerError { - message: "Forbidden".to_string(), - locations: vec![Pos { line: 1, column: 3 }], - path: vec![PathSegment::Field("user".to_owned())], - extensions: None, - }] - ); -} From e170d3b73507389ec1d261ed407dafccdb77f0a8 Mon Sep 17 00:00:00 2001 From: Sunli Date: Wed, 7 Oct 2020 13:40:03 +0800 Subject: [PATCH 5/5] Update docs. --- docs/en/src/field_guard.md | 6 ++-- docs/zh-CN/src/field_guard.md | 68 +++++++++++++++++++++++++++++++++++ src/guard.rs | 35 ------------------ 3 files changed, 71 insertions(+), 38 deletions(-) create mode 100644 docs/zh-CN/src/field_guard.md diff --git a/docs/en/src/field_guard.md b/docs/en/src/field_guard.md index 1e5bd35f..0bdb8fa6 100644 --- a/docs/en/src/field_guard.md +++ b/docs/en/src/field_guard.md @@ -31,11 +31,11 @@ This attribute support 4 operators to create complex rules : - `and` : perform a `and` operation between two rules. (If one rule return an error the `and` operator will return the error. If both rules return a error it's the first one that will be returned). -- `or` : perform a `and` operation between two rules. (If both rules return an error the error returned is the first one) +- `or` : perform a `or` operation between two rules. (If both rules return an error the error returned is the first one) -- `chain` : take a set of rules and run them until one return an error or return `Ok()` if all rules pass. +- `chain` : take a set of rules and run them until one return an error or return `Ok` if all rules pass. -- `race` : take a set of rules and run them until one return `Ok()` if they all fail it return the last error. +- `race` : take a set of rules and run them until one return `Ok` if they all fail it return the last error. ```rust #[derive(SimpleObject)] diff --git a/docs/zh-CN/src/field_guard.md b/docs/zh-CN/src/field_guard.md new file mode 100644 index 00000000..76a554d7 --- /dev/null +++ b/docs/zh-CN/src/field_guard.md @@ -0,0 +1,68 @@ +# 字段守卫(Field Guard) + +您可以在给`Object`的字段定义`守卫`。 这允许在运行字段的代码逻辑之前添加检查。 +义`守卫`由你预先定义的规则组成。 规则是一种实现`Guard`特质的结构。 +```rust +#[derive(Eq, PartialEq, Copy, Clone)] +enum Role { + Admin, + Guest, +} + +struct RoleGuard { + role: Role, +} + +#[async_trait::async_trait] +impl Guard for RoleGuard { + async fn check(&self, ctx: &Context<'_>) -> Result<()> { + if ctx.data_opt::() == Some(&self.role) { + Ok(()) + } else { + Err("Forbidden".into()) + } + } +} +``` + +一旦定义了规则,就可以在`guard`字段属性中使用它。 +此属性支持4个运算符创建复杂的规则: + +-`and`:在两个规则之间执行`与`运算。 (如果一个规则返回错误,则`and`运算符将返回错误。如果两个规则均返回错误,则将是第一个返回的错误)。 + +-`or`:在两个规则之间执行`或`运算。 (如果两个规则都返回错误,则返回的错误是第一个) + +-`chain`:采用一组规则并运行它们,直到返回错误或如果所有规则都通过则返回`Ok`。 + +-`race`:采用一组规则并运行它们,直到其中一个返回`Ok`。 + +```rust +#[derive(SimpleObject)] +struct Query { + #[graphql(guard(RoleGuard(role = "Role::Admin")))] + value: i32, + #[graphql(guard(and( + RoleGuard(role = "Role::Admin"), + UserGuard(username = r#""test""#) + )))] + value2: i32, + #[graphql(guard(or( + RoleGuard(role = "Role::Admin"), + UserGuard(username = r#""test""#) + )))] + value3: i32, + #[graphql(guard(chain( + RoleGuard(role = "Role::Admin"), + UserGuard(username = r#""test""#), + AgeGuard(age = r#"21"#) + )))] + value4: i32, + #[graphql(guard(race( + RoleGuard(role = "Role::Admin"), + UserGuard(username = r#""test""#), + AgeGuard(age = r#"21"#) + )))] + value5: i32, +} +``` + diff --git a/src/guard.rs b/src/guard.rs index 33f423a6..1759afea 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -1,7 +1,6 @@ //! Field guards use crate::{Context, Result}; -use serde::export::PhantomData; /// Field guard /// @@ -50,37 +49,3 @@ impl Guard for Or { self.0.check(ctx).await.or(second_result) } } - -/// Field post guard -/// -/// This is a post-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 PostGuard { - /// Check whether to allow the result of the field through. - async fn check(&self, ctx: &Context<'_>, result: &T) -> Result<()>; -} - -/// An extension trait for `PostGuard` -pub trait PostGuardExt: PostGuard + Sized { - /// Merge the two guards. - fn and>(self, other: R) -> PostAnd { - PostAnd(self, other, PhantomData) - } -} - -impl, R: Send + Sync> PostGuardExt for T {} - -/// PostGuard for [`PostGuardExt::and`](trait.PostGuardExt.html#method.and). -pub struct PostAnd, B: PostGuard>(A, B, PhantomData); - -#[async_trait::async_trait] -impl + Send + Sync, B: PostGuard + Send + Sync> PostGuard - for PostAnd -{ - async fn check(&self, ctx: &Context<'_>, result: &T) -> Result<()> { - self.0.check(ctx, result).await?; - self.1.check(ctx, result).await - } -}