From 216b363ce7630c1dc294d7521e8e1a34ecdfdef2 Mon Sep 17 00:00:00 2001 From: Aurelien Foucault Date: Sat, 3 Oct 2020 15:16:18 +0200 Subject: [PATCH 1/9] Support guard simple rule with the rework (recursivity) --- derive/src/utils.rs | 111 ++++++++++++++-------- tests/guard.rs | 223 ++------------------------------------------ 2 files changed, 81 insertions(+), 253 deletions(-) diff --git a/derive/src/utils.rs b/derive/src/utils.rs index 276f75f8..f51cb16b 100644 --- a/derive/src/utils.rs +++ b/derive/src/utils.rs @@ -121,52 +121,87 @@ pub fn generate_guards( crate_name: &TokenStream, args: &Meta, ) -> GeneratorResult> { + println!("{:#?}\n", args); 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()); + println!("args = {:#?}\n", args.path); + match args.path.get_ident() { + Some(ident) => { + match ident.to_string().as_str() { + "guard" => { + println!("ident guard found {:#?}\n", ident); + if args.nested.len() > 1 { + return Err(Error::new_spanned(args, "Chained rules isn't possible anymore, please use operators.").into()); } - } else { - return Err( - Error::new_spanned(attr, "Invalid property for guard").into() - ); + // why moved ? Want to use it in error + match &args.nested[0] { + NestedMeta::Meta(rule) => { + println! ("rule sended = {:#?}\n", rule); + return generate_guards(crate_name, rule); + } + _ => { + return Err(Error::new_spanned(args, "Invalid guard (to be improve)").into()); + } + } + } + "and" => { + println!("ident and found {:#?}\n", ident); + return Err(Error::new_spanned(args, "WIP").into()); + } + _ => { + let mut guards = None; + //args == "guard" + println!("items = {:#?}\n", &args.nested); + let ty = &args.path; + //ty = the rule + println!("ty = {:#?}\n", ty); + let mut params = Vec::new(); + //rules params + 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 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::GuardExt::and(#guard, #guards) }); + } + println!("end of function"); + Ok(guards) + //return Err(Error::new_spanned(ident, "Invalid guard (to be improve 2)").into()); } } - 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()); + }, + None => { + println!("failed for the moment"); + return Err(Error::new_spanned(args, "WIP").into()); } } - Ok(guards) } - _ => Err(Error::new_spanned(args, "Invalid guards").into()), + _ => Err(Error::new_spanned(args, "Invalid guards (old)").into()), } } diff --git a/tests/guard.rs b/tests/guard.rs index 5a9e2afa..48a264da 100644 --- a/tests/guard.rs +++ b/tests/guard.rs @@ -40,137 +40,21 @@ impl Guard for UserGuard { } } -#[async_std::test] -pub async fn test_guard() { - #[derive(SimpleObject)] - struct MyObj { - #[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] - impl Subscription { - #[graphql(guard(RoleGuard(role = "Role::Admin")))] - async fn values(&self) -> impl Stream { - futures::stream::iter(vec![1, 2, 3]) - } - } - - 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 query = "{ value }"; - assert_eq!( - schema - .execute(Request::new(query).data(Role::Admin)) - .await - .data, - serde_json::json!({ - "value": 1, - }) - ); - - let query = "{ value }"; - assert_eq!( - schema - .execute(Request::new(query).data(Role::Guest)) - .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, - }] - ); - - assert_eq!( - schema - .execute_stream(Request::new("subscription { values }").data(Role::Admin)) - .map(|item| item.data) - .collect::>() - .await, - vec![ - serde_json::json! ({"values": 1}), - serde_json::json! ({"values": 2}), - serde_json::json! ({"values": 3}) - ] - ); - - assert_eq!( - schema - .execute_stream(Request::new("subscription { values }").data(Role::Guest)) - .boxed() - .next() - .await - .unwrap() - .errors, - vec![ServerError { - message: "Forbidden".to_string(), - locations: vec![Pos { - line: 1, - column: 16 - }], - path: vec![PathSegment::Field("values".to_owned())], - extensions: None, - }] - ); -} - #[async_std::test] pub async fn test_multiple_guards() { #[derive(SimpleObject)] struct Query { - #[graphql(guard(RoleGuard(role = "Role::Admin"), UserGuard(username = r#""test""#)))] + #[graphql(guard(RoleGuard(role = "Role::Admin")))] value: i32, } - let schema = Schema::new(Query { value: 10 }, EmptyMutation, EmptySubscription); + #[derive(SimpleObject)] + struct Mutation { + value: i32, + } + + + let schema = Schema::new(Query { value: 10 }, Mutation {value: 11}, EmptySubscription); let query = "{ value }"; assert_eq!( @@ -204,95 +88,4 @@ pub async fn test_multiple_guards() { }] ); - 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_guard_forward_arguments() { - struct UserGuard { - id: ID, - } - - #[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(()) - } - } - } - - 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") }"#; - assert_eq!( - schema - .execute(Request::new(query).data(ID::from("abc"))) - .await - .data, - serde_json::json!({"user": "abc"}) - ); - - 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, - }] - ); } From beb4b2238a94d64a4b411263e5545f9a9aaf6332 Mon Sep 17 00:00:00 2001 From: Aurelien Foucault Date: Sat, 3 Oct 2020 21:34:29 +0200 Subject: [PATCH 2/9] Implement the new and operator --- derive/src/utils.rs | 56 +++++++++++++++++++-------------------------- tests/guard.rs | 26 ++++++++++++++++++--- 2 files changed, 47 insertions(+), 35 deletions(-) diff --git a/derive/src/utils.rs b/derive/src/utils.rs index f51cb16b..f3585f19 100644 --- a/derive/src/utils.rs +++ b/derive/src/utils.rs @@ -129,34 +129,36 @@ pub fn generate_guards( Some(ident) => { match ident.to_string().as_str() { "guard" => { - println!("ident guard found {:#?}\n", ident); - if args.nested.len() > 1 { + if args.nested.len() != 1 { return Err(Error::new_spanned(args, "Chained rules isn't possible anymore, please use operators.").into()); } - // why moved ? Want to use it in error - match &args.nested[0] { - NestedMeta::Meta(rule) => { - println! ("rule sended = {:#?}\n", rule); - return generate_guards(crate_name, rule); - } - _ => { - return Err(Error::new_spanned(args, "Invalid guard (to be improve)").into()); - } + if let NestedMeta::Meta(rule) = &args.nested[0] { + return generate_guards(crate_name, rule); + } else { + return Err(Error::new_spanned(&args.nested[0], "Invalid rule.").into()); } } "and" => { - println!("ident and found {:#?}\n", ident); - return Err(Error::new_spanned(args, "WIP").into()); + 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) })) } _ => { - let mut guards = None; - //args == "guard" - println!("items = {:#?}\n", &args.nested); let ty = &args.path; - //ty = the rule - println!("ty = {:#?}\n", ty); let mut params = Vec::new(); - //rules params for attr in &args.nested { if let NestedMeta::Meta(Meta::NameValue(nv)) = attr { let name = &nv.path; @@ -182,26 +184,16 @@ 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) }); - } - println!("end of function"); - Ok(guards) - //return Err(Error::new_spanned(ident, "Invalid guard (to be improve 2)").into()); + Ok(Some(quote! { #ty { #(#params),* } })) } } }, None => { - println!("failed for the moment"); - return Err(Error::new_spanned(args, "WIP").into()); + Err(Error::new_spanned(args, "Invalid guards").into()) } } } - _ => Err(Error::new_spanned(args, "Invalid guards (old)").into()), + _ => Err(Error::new_spanned(args, "Invalid guards").into()), } } diff --git a/tests/guard.rs b/tests/guard.rs index 48a264da..91819757 100644 --- a/tests/guard.rs +++ b/tests/guard.rs @@ -42,19 +42,20 @@ impl Guard for UserGuard { #[async_std::test] pub async fn test_multiple_guards() { + #[derive(SimpleObject)] struct Query { - #[graphql(guard(RoleGuard(role = "Role::Admin")))] + #[graphql(guard(and(RoleGuard(role = "Role::Admin"), UserGuard(username = r#""test""#))))] value: i32, } #[derive(SimpleObject)] struct Mutation { - value: i32, + value_m: i32, } - let schema = Schema::new(Query { value: 10 }, Mutation {value: 11}, EmptySubscription); + let schema = Schema::new(Query { value: 10 }, Mutation {valueM: 11}, EmptySubscription); let query = "{ value }"; assert_eq!( @@ -88,4 +89,23 @@ pub async fn test_multiple_guards() { }] ); + 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, + }] + ); + } From 5b03bce2e86d7ef0dac407b89aaef0091f17c7b4 Mon Sep 17 00:00:00 2001 From: Aurelien Foucault Date: Sat, 3 Oct 2020 23:00:34 +0200 Subject: [PATCH 3/9] Add or operator --- derive/src/utils.rs | 18 ++++++ src/guard.rs | 20 ++++++- tests/guard.rs | 140 +++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 167 insertions(+), 11 deletions(-) diff --git a/derive/src/utils.rs b/derive/src/utils.rs index f3585f19..6e1b9df3 100644 --- a/derive/src/utils.rs +++ b/derive/src/utils.rs @@ -156,6 +156,24 @@ pub fn generate_guards( } 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) })) + } _ => { let ty = &args.path; let mut params = Vec::new(); diff --git a/src/guard.rs b/src/guard.rs index 8821e645..16a5130b 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 {} @@ -30,8 +35,17 @@ pub struct And(A, B); #[async_trait::async_trait] impl Guard for And { async fn check(&self, ctx: &Context<'_>) -> Result<()> { - self.0.check(ctx).await?; - self.1.check(ctx).await + self.0.check(ctx).await.and(self.1.check(ctx).await) + } +} + +/// 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<()> { + self.0.check(ctx).await.or(self.1.check(ctx).await) } } diff --git a/tests/guard.rs b/tests/guard.rs index 91819757..9bdc7a65 100644 --- a/tests/guard.rs +++ b/tests/guard.rs @@ -41,7 +41,49 @@ impl Guard for UserGuard { } #[async_std::test] -pub async fn test_multiple_guards() { +pub async fn test_guard_simple_rule() { + + #[derive(SimpleObject)] + struct Query { + #[graphql(guard(RoleGuard(role = "Role::Admin")))] + value: i32, + } + + let schema = Schema::new(Query { value: 10 }, EmptyMutation, EmptySubscription); + + let query = "{ value }"; + assert_eq!( + schema + .execute( + Request::new(query) + .data(Role::Admin) + ) + .await + .data, + serde_json::json!({"value": 10}) + ); + + let query = "{ value }"; + assert_eq!( + schema + .execute( + Request::new(query) + .data(Role::Guest) + ) + .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_and_operator() { #[derive(SimpleObject)] struct Query { @@ -49,13 +91,7 @@ pub async fn test_multiple_guards() { value: i32, } - #[derive(SimpleObject)] - struct Mutation { - value_m: i32, - } - - - let schema = Schema::new(Query { value: 10 }, Mutation {valueM: 11}, EmptySubscription); + let schema = Schema::new(Query { value: 10 }, EmptyMutation, EmptySubscription); let query = "{ value }"; assert_eq!( @@ -108,4 +144,92 @@ pub async fn test_multiple_guards() { }] ); + 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_guard_or_operator() { + + #[derive(SimpleObject)] + struct Query { + #[graphql(guard(or(RoleGuard(role = "Role::Admin"), UserGuard(username = r#""test""#))))] + 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 + .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("value".to_owned())], + extensions: None, + }] + ); } From 549aa7cc72b3cfd5315f65f8d5813d4526ffc275 Mon Sep 17 00:00:00 2001 From: Aurelien Foucault Date: Sat, 3 Oct 2020 23:03:40 +0200 Subject: [PATCH 4/9] Fmt --- derive/src/utils.rs | 170 ++++++++++++++++++++++++-------------------- tests/guard.rs | 18 ++--- 2 files changed, 97 insertions(+), 91 deletions(-) diff --git a/derive/src/utils.rs b/derive/src/utils.rs index 6e1b9df3..0fdbeb77 100644 --- a/derive/src/utils.rs +++ b/derive/src/utils.rs @@ -126,89 +126,101 @@ pub fn generate_guards( Meta::List(args) => { println!("args = {:#?}\n", args.path); 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] { - return generate_guards(crate_name, rule); - } else { - return Err(Error::new_spanned(&args.nested[0], "Invalid rule.").into()); - } + 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()); } - "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) })) - } - _ => { - 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 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() - ); - } - } - Ok(Some(quote! { #ty { #(#params),* } })) + if let NestedMeta::Meta(rule) = &args.nested[0] { + return generate_guards(crate_name, rule); + } else { + return 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) }, + )) + } + _ => { + 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 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() + ); + } + } + Ok(Some(quote! { #ty { #(#params),* } })) + } }, - None => { - Err(Error::new_spanned(args, "Invalid guards").into()) - } + None => Err(Error::new_spanned(args, "Invalid guards").into()), } } _ => Err(Error::new_spanned(args, "Invalid guards").into()), diff --git a/tests/guard.rs b/tests/guard.rs index 9bdc7a65..0a33e5b7 100644 --- a/tests/guard.rs +++ b/tests/guard.rs @@ -42,7 +42,6 @@ impl Guard for UserGuard { #[async_std::test] pub async fn test_guard_simple_rule() { - #[derive(SimpleObject)] struct Query { #[graphql(guard(RoleGuard(role = "Role::Admin")))] @@ -54,10 +53,7 @@ pub async fn test_guard_simple_rule() { let query = "{ value }"; assert_eq!( schema - .execute( - Request::new(query) - .data(Role::Admin) - ) + .execute(Request::new(query).data(Role::Admin)) .await .data, serde_json::json!({"value": 10}) @@ -66,10 +62,7 @@ pub async fn test_guard_simple_rule() { let query = "{ value }"; assert_eq!( schema - .execute( - Request::new(query) - .data(Role::Guest) - ) + .execute(Request::new(query).data(Role::Guest)) .await .into_result() .unwrap_err(), @@ -84,10 +77,12 @@ pub async fn test_guard_simple_rule() { #[async_std::test] pub async fn test_guard_and_operator() { - #[derive(SimpleObject)] struct Query { - #[graphql(guard(and(RoleGuard(role = "Role::Admin"), UserGuard(username = r#""test""#))))] + #[graphql(guard(and( + RoleGuard(role = "Role::Admin"), + UserGuard(username = r#""test""#) + )))] value: i32, } @@ -166,7 +161,6 @@ pub async fn test_guard_and_operator() { #[async_std::test] pub async fn test_guard_or_operator() { - #[derive(SimpleObject)] struct Query { #[graphql(guard(or(RoleGuard(role = "Role::Admin"), UserGuard(username = r#""test""#))))] From 92716a21864bf14ee60a543bd82004c17b8418d9 Mon Sep 17 00:00:00 2001 From: Aurelien Foucault Date: Sat, 3 Oct 2020 23:52:23 +0200 Subject: [PATCH 5/9] Clippy --- derive/src/utils.rs | 4 ++-- src/guard.rs | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/derive/src/utils.rs b/derive/src/utils.rs index 0fdbeb77..796a9307 100644 --- a/derive/src/utils.rs +++ b/derive/src/utils.rs @@ -136,9 +136,9 @@ pub fn generate_guards( .into()); } if let NestedMeta::Meta(rule) = &args.nested[0] { - return generate_guards(crate_name, rule); + generate_guards(crate_name, rule) } else { - return Err(Error::new_spanned(&args.nested[0], "Invalid rule.").into()); + Err(Error::new_spanned(&args.nested[0], "Invalid rule.").into()) } } "and" => { diff --git a/src/guard.rs b/src/guard.rs index 16a5130b..bcc25e1e 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -35,7 +35,8 @@ pub struct And(A, B); #[async_trait::async_trait] impl Guard for And { async fn check(&self, ctx: &Context<'_>) -> Result<()> { - self.0.check(ctx).await.and(self.1.check(ctx).await) + let second_result = self.1.check(ctx).await; + self.0.check(ctx).await.and(second_result) } } @@ -45,7 +46,8 @@ pub struct Or(A, B); #[async_trait::async_trait] impl Guard for Or { async fn check(&self, ctx: &Context<'_>) -> Result<()> { - self.0.check(ctx).await.or(self.1.check(ctx).await) + let second_result = self.1.check(ctx).await; + self.0.check(ctx).await.or(second_result) } } From ec5a466bcb27a917629cc2f4d22c58a3004db010 Mon Sep 17 00:00:00 2001 From: Aurelien Foucault Date: Tue, 6 Oct 2020 00:10:15 +0200 Subject: [PATCH 6/9] Add chain and race operator --- derive/src/utils.rs | 46 +++++++- src/guard.rs | 4 +- tests/guard.rs | 253 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 298 insertions(+), 5 deletions(-) diff --git a/derive/src/utils.rs b/derive/src/utils.rs index 796a9307..2a6a1035 100644 --- a/derive/src/utils.rs +++ b/derive/src/utils.rs @@ -121,10 +121,8 @@ pub fn generate_guards( crate_name: &TokenStream, args: &Meta, ) -> GeneratorResult> { - println!("{:#?}\n", args); match args { Meta::List(args) => { - println!("args = {:#?}\n", args.path); match args.path.get_ident() { Some(ident) => match ident.to_string().as_str() { "guard" => { @@ -189,6 +187,50 @@ pub fn generate_guards( 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(); diff --git a/src/guard.rs b/src/guard.rs index bcc25e1e..33f423a6 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -35,8 +35,8 @@ pub struct And(A, B); #[async_trait::async_trait] impl Guard for And { async fn check(&self, ctx: &Context<'_>) -> Result<()> { - let second_result = self.1.check(ctx).await; - self.0.check(ctx).await.and(second_result) + self.0.check(ctx).await?; + self.1.check(ctx).await } } diff --git a/tests/guard.rs b/tests/guard.rs index 0a33e5b7..3b958498 100644 --- a/tests/guard.rs +++ b/tests/guard.rs @@ -40,6 +40,23 @@ 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_simple_rule() { #[derive(SimpleObject)] @@ -48,7 +65,17 @@ pub async fn test_guard_simple_rule() { value: i32, } - let schema = Schema::new(Query { value: 10 }, EmptyMutation, EmptySubscription); + struct Subscription; + + #[Subscription] + impl Subscription { + #[graphql(guard(RoleGuard(role = "Role::Admin")))] + async fn values(&self) -> impl Stream { + futures::stream::iter(vec![1, 2, 3]) + } + } + + let schema = Schema::new(Query { value: 10 }, EmptyMutation, Subscription); let query = "{ value }"; assert_eq!( @@ -73,6 +100,38 @@ pub async fn test_guard_simple_rule() { extensions: None, }] ); + + assert_eq!( + schema + .execute_stream(Request::new("subscription { values }").data(Role::Admin)) + .map(|item| item.data) + .collect::>() + .await, + vec![ + serde_json::json! ({"values": 1}), + serde_json::json! ({"values": 2}), + serde_json::json! ({"values": 3}) + ] + ); + + assert_eq!( + schema + .execute_stream(Request::new("subscription { values }").data(Role::Guest)) + .boxed() + .next() + .await + .unwrap() + .errors, + vec![ServerError { + message: "Forbidden".to_string(), + locations: vec![Pos { + line: 1, + column: 16 + }], + path: vec![PathSegment::Field("values".to_owned())], + extensions: None, + }] + ); } #[async_std::test] @@ -227,3 +286,195 @@ pub async fn test_guard_or_operator() { }] ); } + +#[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, + }] + ); +} \ No newline at end of file From 42dc36f9f43a4c04b7c49e441eaeaccb7c6c6c2d Mon Sep 17 00:00:00 2001 From: Aurelien Foucault Date: Tue, 6 Oct 2020 00:17:29 +0200 Subject: [PATCH 7/9] Fmt, clippy and remove post guard --- 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 | 267 ++++++++++++----------------- src/lib.rs | 3 - tests/guard.rs | 14 +- tests/post_guard.rs | 327 ------------------------------------ 8 files changed, 121 insertions(+), 529 deletions(-) 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) => { - 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()) - } + 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()); } - "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) }, - )) + if let NestedMeta::Meta(rule) = &args.nested[0] { + generate_guards(crate_name, rule) + } else { + Err(Error::new_spanned(&args.nested[0], "Invalid rule.").into()) } - "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) }, - )) + } + "and" => { + if args.nested.len() != 2 { + return Err(Error::new_spanned( + args, + "and operator support only 2 operands.", + ) + .into()); } - "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) + 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()); } - "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) + 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()); } - _ => { - 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 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()); - } + 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 { - return Err( - Error::new_spanned(attr, "Invalid property for guard").into() + guards = Some( + quote! { #crate_name::guard::GuardExt::and(#guard, #guards) }, ); } } - Ok(Some(quote! { #ty { #(#params),* } })) } - }, - None => Err(Error::new_spanned(args, "Invalid guards").into()), - } - } - _ => 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; + 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 { @@ -305,19 +260,11 @@ pub fn generate_post_guards( ); } } - 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(Some(quote! { #ty { #(#params),* } })) } - } - Ok(guards) - } + }, + None => Err(Error::new_spanned(args, "Invalid guards").into()), + }, _ => Err(Error::new_spanned(args, "Invalid guards").into()), } } 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 3b958498..377cc0ca 100644 --- a/tests/guard.rs +++ b/tests/guard.rs @@ -291,7 +291,11 @@ pub async fn test_guard_or_operator() { 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"#))))] + #[graphql(guard(chain( + RoleGuard(role = "Role::Admin"), + UserGuard(username = r#""test""#), + AgeGuard(age = r#"21"#) + )))] value: i32, } @@ -396,7 +400,11 @@ pub async fn test_guard_chain_operator() { 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"#))))] + #[graphql(guard(race( + RoleGuard(role = "Role::Admin"), + UserGuard(username = r#""test""#), + AgeGuard(age = r#"21"#) + )))] value: i32, } @@ -477,4 +485,4 @@ pub async fn test_guard_race_operator() { extensions: None, }] ); -} \ No newline at end of file +} 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 b00f70a09e4ce4a56554a558d5ac9c0b4d2b91aa Mon Sep 17 00:00:00 2001 From: Aurelien Foucault Date: Tue, 6 Oct 2020 10:49:36 +0200 Subject: [PATCH 8/9] Add docs --- docs/en/src/SUMMARY.md | 1 + docs/en/src/field_guard.md | 69 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 docs/en/src/field_guard.md 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, +} +``` + From 17a85c451aedd52ba5456ef4a5e849d7de7785dc Mon Sep 17 00:00:00 2001 From: Aurelien Foucault Date: Thu, 8 Oct 2020 15:32:37 +0200 Subject: [PATCH 9/9] Add eq for errors --- src/error.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/error.rs b/src/error.rs index e6eef7ea..d3173dc2 100644 --- a/src/error.rs +++ b/src/error.rs @@ -147,7 +147,7 @@ impl From for InputValueError { pub type InputValueResult = Result>; /// An error with a message and optional extensions. -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] pub struct Error { /// The error message. pub message: String,