From e80e4c9af9b6176977416a164820010658423694 Mon Sep 17 00:00:00 2001 From: Sunli Date: Mon, 14 Mar 2022 09:19:27 +0800 Subject: [PATCH] Add support `group` attribute to Object/SimpleObject/ComplexObject/Subscription macros. #838 --- CHANGELOG.md | 4 + derive/src/args.rs | 9 +- derive/src/complex_object.rs | 2 +- derive/src/object.rs | 2 +- derive/src/simple_object.rs | 2 +- derive/src/subscription.rs | 2 +- src/docs/complex_object.md | 1 + src/docs/object.md | 1 + src/docs/simple_object.md | 1 + src/docs/subscription.md | 1 + tests/guard.rs | 195 +++++++++++++++++++++++++++++++++++ 11 files changed, 214 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b20b3af4..4929c956 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# [3.0.36] 2022-3-11 + +- Add support `group` attribute to Object/SimpleObject/ComplexObject/Subscription macros. [#838](https://github.com/async-graphql/async-graphql/issues/838) + # [3.0.35] 2022-3-11 - Make `HashMap` more generics for `InputOutput` and `OutputType`. diff --git a/derive/src/args.rs b/derive/src/args.rs index e817206e..eb0c7d85 100644 --- a/derive/src/args.rs +++ b/derive/src/args.rs @@ -200,6 +200,8 @@ pub struct SimpleObject { // for InputObject #[darling(default)] pub input_name: Option, + #[darling(default)] + pub guard: Option>, } #[derive(FromMeta, Default)] @@ -229,6 +231,8 @@ pub struct Object { pub serial: bool, #[darling(multiple, rename = "concrete")] pub concretes: Vec, + #[darling(default)] + pub guard: Option>, } pub enum ComplexityType { @@ -531,6 +535,8 @@ pub struct Subscription { pub use_type_description: bool, pub extends: bool, pub visible: Option, + #[darling(default)] + pub guard: Option>, } #[derive(FromMeta, Default)] @@ -559,7 +565,6 @@ pub struct SubscriptionField { #[derive(FromField)] pub struct MergedObjectField { - pub ident: Option, pub ty: Type, } @@ -587,7 +592,6 @@ pub struct MergedObject { #[derive(FromField)] pub struct MergedSubscriptionField { - pub ident: Option, pub ty: Type, } @@ -738,6 +742,7 @@ pub struct ComplexObject { pub name: Option, pub rename_fields: Option, pub rename_args: Option, + pub guard: Option>, } #[derive(FromMeta, Default)] diff --git a/derive/src/complex_object.rs b/derive/src/complex_object.rs index 393cb1be..33322525 100644 --- a/derive/src/complex_object.rs +++ b/derive/src/complex_object.rs @@ -417,7 +417,7 @@ pub fn generate( let guard_map_err = quote! { .map_err(|err| err.into_server_error(ctx.item.pos)) }; - let guard = match &method_args.guard { + let guard = match method_args.guard.as_ref().or(object_args.guard.as_ref()) { Some(code) => Some(generate_guards(&crate_name, code, guard_map_err)?), None => None, }; diff --git a/derive/src/object.rs b/derive/src/object.rs index 4bcfdc35..3716bc8a 100644 --- a/derive/src/object.rs +++ b/derive/src/object.rs @@ -562,7 +562,7 @@ pub fn generate( let guard_map_err = quote! { .map_err(|err| err.into_server_error(ctx.item.pos)) }; - let guard = match &method_args.guard { + let guard = match method_args.guard.as_ref().or(object_args.guard.as_ref()) { Some(code) => Some(generate_guards(&crate_name, code, guard_map_err)?), None => None, }; diff --git a/derive/src/simple_object.rs b/derive/src/simple_object.rs index c4ae6f71..a84500f8 100644 --- a/derive/src/simple_object.rs +++ b/derive/src/simple_object.rs @@ -184,7 +184,7 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult Some(generate_guards(&crate_name, code, guard_map_err)?), None => None, }; diff --git a/derive/src/subscription.rs b/derive/src/subscription.rs index 8f839297..ee5b7efe 100644 --- a/derive/src/subscription.rs +++ b/derive/src/subscription.rs @@ -305,7 +305,7 @@ pub fn generate( .with_path(::std::vec![#crate_name::PathSegment::Field(::std::borrow::ToOwned::to_owned(&*field_name))]) }) }; - let guard = match &field.guard { + let guard = match field.guard.as_ref().or(subscription_args.guard.as_ref()) { Some(code) => Some(generate_guards(&crate_name, code, guard_map_err)?), None => None, }; diff --git a/src/docs/complex_object.md b/src/docs/complex_object.md index 06649fc1..8cd71b32 100644 --- a/src/docs/complex_object.md +++ b/src/docs/complex_object.md @@ -15,6 +15,7 @@ some simple fields, and use the `ComplexObject` macro to define some other field | name | Object name | string | Y | | rename_fields | Rename all the fields according to the given case convention. The possible values are "lowercase", "UPPERCASE", "PascalCase", "camelCase", "snake_case", "SCREAMING_SNAKE_CASE". | string | Y | | rename_args | Rename all the arguments according to the given case convention. The possible values are "lowercase", "UPPERCASE", "PascalCase", "camelCase", "snake_case", "SCREAMING_SNAKE_CASE". | string | Y | +| guard | Field of guard *[See also the Book](https://async-graphql.github.io/async-graphql/en/field_guard.html)* | string | Y | # Field attributes diff --git a/src/docs/object.md b/src/docs/object.md index e7fd6d9f..fcea0da9 100644 --- a/src/docs/object.md +++ b/src/docs/object.md @@ -18,6 +18,7 @@ All methods are converted to camelCase. | visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y | | serial | Resolve each field sequentially. | bool | Y | | concretes | Specify how the concrete type of the generic SimpleObject should be implemented. | ConcreteType | Y | +| guard | Field of guard *[See also the Book](https://async-graphql.github.io/async-graphql/en/field_guard.html)* | string | Y | # Field attributes diff --git a/src/docs/simple_object.md b/src/docs/simple_object.md index 43b85716..fdf24bc2 100644 --- a/src/docs/simple_object.md +++ b/src/docs/simple_object.md @@ -16,6 +16,7 @@ Similar to `Object`, but defined on a structure that automatically generates get | visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y | | concretes | Specify how the concrete type of the generic SimpleObject should be implemented. *[See also the Book](https://async-graphql.github.io/async-graphql/en/define_simple_object.html#generic-simpleobjects) | ConcreteType | Y | | serial | Resolve each field sequentially. | bool | Y | +| guard | Field of guard *[See also the Book](https://async-graphql.github.io/async-graphql/en/field_guard.html)* | string | Y | # Field attributes diff --git a/src/docs/subscription.md b/src/docs/subscription.md index 9152d85f..a030846e 100644 --- a/src/docs/subscription.md +++ b/src/docs/subscription.md @@ -18,6 +18,7 @@ The filter function should be synchronous. | visible | If `false`, it will not be displayed in introspection. *[See also the Book](https://async-graphql.github.io/async-graphql/en/visibility.html).* | bool | Y | | visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y | | use_type_description | Specifies that the description of the type is on the type declaration. [`Description`]()(derive.Description.html) | bool | Y | +| guard | Field of guard *[See also the Book](https://async-graphql.github.io/async-graphql/en/field_guard.html)* | string | Y | # Field attributes diff --git a/tests/guard.rs b/tests/guard.rs index a9ef4e31..a7963f26 100644 --- a/tests/guard.rs +++ b/tests/guard.rs @@ -346,6 +346,158 @@ pub async fn test_guard_use_params() { ); } +#[tokio::test] +pub async fn test_guard_on_simple_object() { + #[derive(SimpleObject)] + #[graphql(guard = "RoleGuard::new(Role::Admin)")] + struct Query { + value: i32, + } + + let schema = Schema::new(Query { value: 100 }, EmptyMutation, EmptySubscription); + + let query = "{ value }"; + assert_eq!( + schema + .execute(Request::new(query).data(Role::Admin)) + .await + .data, + value!({"value": 100}) + ); + + let query = "{ value }"; + assert_eq!( + schema + .execute(Request::new(query).data(Role::Guest)) + .await + .into_result() + .unwrap_err(), + vec![ServerError { + message: "Forbidden".to_string(), + source: None, + locations: vec![Pos { line: 1, column: 3 }], + path: vec![PathSegment::Field("value".to_owned())], + extensions: None, + }] + ); +} + +#[tokio::test] +pub async fn test_guard_on_simple_object_field() { + #[derive(SimpleObject)] + #[graphql] + struct Query { + #[graphql(guard = "RoleGuard::new(Role::Admin)")] + value: i32, + } + + let schema = Schema::new(Query { value: 100 }, EmptyMutation, EmptySubscription); + + let query = "{ value }"; + assert_eq!( + schema + .execute(Request::new(query).data(Role::Admin)) + .await + .data, + value!({"value": 100}) + ); + + let query = "{ value }"; + assert_eq!( + schema + .execute(Request::new(query).data(Role::Guest)) + .await + .into_result() + .unwrap_err(), + vec![ServerError { + message: "Forbidden".to_string(), + source: None, + locations: vec![Pos { line: 1, column: 3 }], + path: vec![PathSegment::Field("value".to_owned())], + extensions: None, + }] + ); +} + +#[tokio::test] +pub async fn test_guard_on_object() { + struct Query; + + #[Object(guard = "RoleGuard::new(Role::Admin)")] + impl Query { + async fn value(&self) -> i32 { + 100 + } + } + + let schema = Schema::new(Query, EmptyMutation, EmptySubscription); + + let query = "{ value }"; + assert_eq!( + schema + .execute(Request::new(query).data(Role::Admin)) + .await + .data, + value!({"value": 100}) + ); + + let query = "{ value }"; + assert_eq!( + schema + .execute(Request::new(query).data(Role::Guest)) + .await + .into_result() + .unwrap_err(), + vec![ServerError { + message: "Forbidden".to_string(), + source: None, + locations: vec![Pos { line: 1, column: 3 }], + path: vec![PathSegment::Field("value".to_owned())], + extensions: None, + }] + ); +} + +#[tokio::test] +pub async fn test_guard_on_object_field() { + struct Query; + + #[Object] + impl Query { + #[graphql(guard = "RoleGuard::new(Role::Admin)")] + async fn value(&self) -> i32 { + 100 + } + } + + let schema = Schema::new(Query, EmptyMutation, EmptySubscription); + + let query = "{ value }"; + assert_eq!( + schema + .execute(Request::new(query).data(Role::Admin)) + .await + .data, + value!({"value": 100}) + ); + + let query = "{ value }"; + assert_eq!( + schema + .execute(Request::new(query).data(Role::Guest)) + .await + .into_result() + .unwrap_err(), + vec![ServerError { + message: "Forbidden".to_string(), + source: None, + locations: vec![Pos { line: 1, column: 3 }], + path: vec![PathSegment::Field("value".to_owned())], + extensions: None, + }] + ); +} + #[tokio::test] pub async fn test_guard_on_complex_object() { #[derive(SimpleObject)] @@ -354,6 +506,49 @@ pub async fn test_guard_on_complex_object() { value1: i32, } + #[ComplexObject(guard = "RoleGuard::new(Role::Admin)")] + impl Query { + async fn value2(&self) -> i32 { + 100 + } + } + + let schema = Schema::new(Query { value1: 10 }, EmptyMutation, EmptySubscription); + + let query = "{ value2 }"; + assert_eq!( + schema + .execute(Request::new(query).data(Role::Admin)) + .await + .data, + value!({"value2": 100}) + ); + + let query = "{ value2 }"; + assert_eq!( + schema + .execute(Request::new(query).data(Role::Guest)) + .await + .into_result() + .unwrap_err(), + vec![ServerError { + message: "Forbidden".to_string(), + source: None, + locations: vec![Pos { line: 1, column: 3 }], + path: vec![PathSegment::Field("value2".to_owned())], + extensions: None, + }] + ); +} + +#[tokio::test] +pub async fn test_guard_on_complex_object_field() { + #[derive(SimpleObject)] + #[graphql(complex)] + struct Query { + value1: i32, + } + #[ComplexObject] impl Query { #[graphql(guard = "RoleGuard::new(Role::Admin)")]