From 53cd75dd1565b77c222accb4cfc01efc6acfb601 Mon Sep 17 00:00:00 2001 From: Edward Rudd Date: Fri, 18 Feb 2022 14:16:41 -0500 Subject: [PATCH 01/24] do not request oldtime feature from chrono package - this removes the dependency on the old (unsupported) version of time --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 05c044f7..862ef858 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -53,7 +53,7 @@ fast_chemail = "0.9.6" # Feature optional dependencies bson = { version = "2.0.0", optional = true, features = ["chrono-0_4"] } -chrono = { version = "0.4.19", optional = true } +chrono = { version = "0.4.19", optional = true, default-features = false, features = ["clock", "std"] } chrono-tz = { version = "0.5.3", optional = true } hashbrown = { version = "0.12.0", optional = true } iso8601-duration = { version = "0.1.0", optional = true } From e3078a40bebf7b0b5aad958ceb7831a1a3786533 Mon Sep 17 00:00:00 2001 From: Edward Rudd Date: Fri, 18 Feb 2022 14:32:49 -0500 Subject: [PATCH 02/24] update sha2 dependency --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 05c044f7..bc1965b1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,7 +71,7 @@ time = { version = "0.3.5", optional = true, features = ["parsing", "formatting" # Non-feature optional dependencies blocking = { version = "1.0.2", optional = true } lru = { version = "0.7.1", optional = true } -sha2 = { version = "0.9.3", optional = true } +sha2 = { version = "0.10.2", optional = true } futures-timer = { version = "3.0.2", optional = true } futures-channel = { version = "0.3.13", optional = true } serde_cbor = { version = "0.11.1", optional = true } From 7d7f1f785026e2952fb6f511c77991fe205f3a11 Mon Sep 17 00:00:00 2001 From: Edward Rudd Date: Fri, 18 Feb 2022 14:38:30 -0500 Subject: [PATCH 03/24] update secrecy dependency --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 05c044f7..fa356329 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,7 +58,7 @@ chrono-tz = { version = "0.5.3", optional = true } hashbrown = { version = "0.12.0", optional = true } iso8601-duration = { version = "0.1.0", optional = true } log = { version = "0.4.14", optional = true } -secrecy = { version = "0.7.0", optional = true } +secrecy = { version = "0.8.0", optional = true } tracinglib = { version = "0.1.25", optional = true, package = "tracing" } tracing-futures = { version = "0.2.5", optional = true, features = ["std-future", "futures-03"] } opentelemetry = { version = "0.16.0", optional = true, default-features = false, features = ["trace"] } From f7ba8eeec2def7579ffdfe60889ab9dad2bd0cd0 Mon Sep 17 00:00:00 2001 From: Edward Rudd Date: Fri, 18 Feb 2022 14:35:07 -0500 Subject: [PATCH 04/24] update opentelemetry --- Cargo.toml | 2 +- src/extensions/opentelemetry.rs | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 05c044f7..4b9fb9b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,7 +61,7 @@ log = { version = "0.4.14", optional = true } secrecy = { version = "0.7.0", optional = true } tracinglib = { version = "0.1.25", optional = true, package = "tracing" } tracing-futures = { version = "0.2.5", optional = true, features = ["std-future", "futures-03"] } -opentelemetry = { version = "0.16.0", optional = true, default-features = false, features = ["trace"] } +opentelemetry = { version = "0.17.0", optional = true, default-features = false, features = ["trace"] } uuid = { version = "0.8.2", optional = true, features = ["v4", "serde"] } rust_decimal = { version = "1.14.3", optional = true } url = { version = "2.2.1", optional = true } diff --git a/src/extensions/opentelemetry.rs b/src/extensions/opentelemetry.rs index 54ad8056..3ae9e7a9 100644 --- a/src/extensions/opentelemetry.rs +++ b/src/extensions/opentelemetry.rs @@ -31,7 +31,8 @@ impl OpenTelemetry { /// Use `tracer` to create an OpenTelemetry extension. pub fn new(tracer: T) -> OpenTelemetry where - T: Tracer + Send + Sync, + T: Tracer + Send + Sync + 'static, + ::Span: Sync + Send, { Self { tracer: Arc::new(tracer), @@ -39,7 +40,11 @@ impl OpenTelemetry { } } -impl ExtensionFactory for OpenTelemetry { +impl ExtensionFactory for OpenTelemetry +where + T: Tracer + Send + Sync + 'static, + ::Span: Sync + Send, +{ fn create(&self) -> Arc { Arc::new(OpenTelemetryExtension { tracer: self.tracer.clone(), @@ -52,7 +57,11 @@ struct OpenTelemetryExtension { } #[async_trait::async_trait] -impl Extension for OpenTelemetryExtension { +impl Extension for OpenTelemetryExtension +where + T: Tracer + Send + Sync + 'static, + ::Span: Sync + Send, +{ async fn request(&self, ctx: &ExtensionContext<'_>, next: NextRequest<'_>) -> Response { next.run(ctx) .with_context(OpenTelemetryContext::current_with_span( From 40b525c9f81751e263d7e0a160d3dca7f718a6d4 Mon Sep 17 00:00:00 2001 From: Peter Kolloch Date: Wed, 23 Feb 2022 16:54:39 +0100 Subject: [PATCH 05/24] custom_scalars: i64 not encoded as string Remove the note that incorrectly claims that i64 are encoded as strings. Also see https://github.com/async-graphql/async-graphql/issues/196 --- docs/en/src/custom_scalars.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/src/custom_scalars.md b/docs/en/src/custom_scalars.md index 1a7fd6c1..dd16d438 100644 --- a/docs/en/src/custom_scalars.md +++ b/docs/en/src/custom_scalars.md @@ -4,7 +4,7 @@ In `Async-graphql` most common scalar types are built in, but you can also creat Using `async-graphql::Scalar`, you can add support for a scalar when you implement it. You only need to implement parsing and output functions. -The following example defines a 64-bit integer scalar where its input and output are strings. (Note: `Async-graphql` already supports 64-bit integers and uses strings as input and output.) +The following example defines a 64-bit integer scalar where its input and output are strings. ```rust use async_graphql::*; From a01e60e85c32ce2ab1309757f0b4d97a7b074d7a Mon Sep 17 00:00:00 2001 From: azzamsa <17734314+azzamsa@users.noreply.github.com> Date: Thu, 24 Feb 2022 06:09:32 +0700 Subject: [PATCH 06/24] docs: update method name --- docs/en/src/cursor_connections.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/en/src/cursor_connections.md b/docs/en/src/cursor_connections.md index a90f3ce2..2b2d2fec 100644 --- a/docs/en/src/cursor_connections.md +++ b/docs/en/src/cursor_connections.md @@ -34,7 +34,7 @@ impl Query { let mut connection = Connection::new(start > 0, end < 10000); connection.append( (start..end).into_iter().map(|n| - Ok(Edge::new_with_additional_fields(n, n as i32, EmptyFields)), + Ok(Edge::with_additional_fields(n, n as i32, EmptyFields)), ))?; Ok(connection) }) From 062e25ab6fedadd6211c19c46d37038163a50b2a Mon Sep 17 00:00:00 2001 From: Conrad Ludgate Date: Sat, 26 Feb 2022 07:13:07 +0000 Subject: [PATCH 07/24] bump actix-web --- integrations/actix-web/Cargo.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/integrations/actix-web/Cargo.toml b/integrations/actix-web/Cargo.toml index 0a005b97..aae14b85 100644 --- a/integrations/actix-web/Cargo.toml +++ b/integrations/actix-web/Cargo.toml @@ -15,9 +15,9 @@ categories = ["network-programming", "asynchronous"] async-graphql = { path = "../..", version = "3.0.31" } actix = "0.12.0" -actix-http = "3.0.0-rc.2" -actix-web = { version = "4.0.0-rc.3", default-features = false } -actix-web-actors = "4.0.0-beta.12" +actix-http = "3.0.0" +actix-web = { version = "4.0.1", default-features = false } +actix-web-actors = "4.0.0" async-channel = "1.6.1" futures-util = { version = "0.3.0", default-features = false } serde_json = "1.0.64" From 8871a53f82bc259897b7e9091c159e5ba111368a Mon Sep 17 00:00:00 2001 From: Sunli Date: Mon, 28 Feb 2022 15:34:43 +0800 Subject: [PATCH 08/24] Update examples --- examples | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples b/examples index 840fd0aa..61384c8f 160000 --- a/examples +++ b/examples @@ -1 +1 @@ -Subproject commit 840fd0aa36f6fa4633047ef3000bb51b35e26b7b +Subproject commit 61384c8fa50e1395e50015b2be77b4bc6f5de4d2 From 38725bad2ac9fca528b4da17b0f36a770ba5ba12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Zwoli=C5=84ski?= Date: Wed, 2 Mar 2022 11:08:14 +0100 Subject: [PATCH 09/24] Correct import path for cursor connection in docs --- docs/en/src/cursor_connections.md | 2 +- docs/zh-CN/src/cursor_connections.md | 4 ++-- src/types/connection/mod.rs | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/en/src/cursor_connections.md b/docs/en/src/cursor_connections.md index 2b2d2fec..618c1d23 100644 --- a/docs/en/src/cursor_connections.md +++ b/docs/en/src/cursor_connections.md @@ -6,7 +6,7 @@ Defining a cursor connection in `async-graphql` is very simple, you just call th ```rust use async_graphql::*; -use async_graphql::connection::*; +use async_graphql::types::connection::*; struct Query; diff --git a/docs/zh-CN/src/cursor_connections.md b/docs/zh-CN/src/cursor_connections.md index bcd5ff9e..ffe8b1ef 100644 --- a/docs/zh-CN/src/cursor_connections.md +++ b/docs/zh-CN/src/cursor_connections.md @@ -8,7 +8,7 @@ Relay定义了一套游标连接规范,以提供一致性的分页查询方式 ```rust use async_graphql::*; -use async_graphql::connection::*; +use async_graphql::types::connection::*; struct Query; @@ -42,4 +42,4 @@ impl Query { }) } } -``` \ No newline at end of file +``` diff --git a/src/types/connection/mod.rs b/src/types/connection/mod.rs index fc818552..c4976bb3 100644 --- a/src/types/connection/mod.rs +++ b/src/types/connection/mod.rs @@ -26,7 +26,7 @@ pub struct EmptyFields; /// /// ```rust /// use async_graphql::*; -/// use async_graphql::connection::*; +/// use async_graphql::types::connection::*; /// /// struct Query; /// @@ -116,7 +116,7 @@ where /// /// ```rust /// use async_graphql::*; -/// use async_graphql::connection::*; +/// use async_graphql::types::connection::*; /// /// #[derive(SimpleObject)] /// struct MyEdge { From cbba51b229933bd6e1823847d3b1cf378907ed8c Mon Sep 17 00:00:00 2001 From: Sunli Date: Fri, 4 Mar 2022 10:22:20 +0800 Subject: [PATCH 10/24] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f7c84356..2f939d77 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.32] 2022-3-4 + +- Bump `Actix-web` from `4.0.0-rc.3` to `4.0.1`. + # [3.0.31] 2022-02-17 - Add `OneOfObject` macro to support for oneof input object. From 9359b82da265956ef45fd1f7484c4663a8b26087 Mon Sep 17 00:00:00 2001 From: Sunli Date: Fri, 4 Mar 2022 10:22:58 +0800 Subject: [PATCH 11/24] Release 3.0.32 async-graphql@3.0.32 async-graphql-actix-web@3.0.32 async-graphql-axum@3.0.32 async-graphql-derive@3.0.32 async-graphql-parser@3.0.32 async-graphql-poem@3.0.32 async-graphql-rocket@3.0.32 async-graphql-tide@3.0.32 async-graphql-value@3.0.32 async-graphql-warp@3.0.32 Generated by cargo-workspaces --- Cargo.toml | 8 ++++---- derive/Cargo.toml | 4 ++-- integrations/actix-web/Cargo.toml | 4 ++-- integrations/axum/Cargo.toml | 4 ++-- integrations/poem/Cargo.toml | 4 ++-- integrations/rocket/Cargo.toml | 4 ++-- integrations/tide/Cargo.toml | 4 ++-- integrations/warp/Cargo.toml | 4 ++-- parser/Cargo.toml | 4 ++-- value/Cargo.toml | 2 +- 10 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a72ad5b1..01335a55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql" -version = "3.0.31" +version = "3.0.32" authors = ["sunli ", "Koxiaet"] edition = "2021" description = "A GraphQL server library implemented in Rust" @@ -27,9 +27,9 @@ password-strength-validator = ["zxcvbn"] unstable_oneof = ["async-graphql-derive/unstable_oneof"] [dependencies] -async-graphql-derive = { path = "derive", version = "3.0.31" } -async-graphql-value = { path = "value", version = "3.0.31" } -async-graphql-parser = { path = "parser", version = "3.0.31" } +async-graphql-derive = { path = "derive", version = "3.0.32" } +async-graphql-value = { path = "value", version = "3.0.32" } +async-graphql-parser = { path = "parser", version = "3.0.32" } async-stream = "0.3.0" async-trait = "0.1.48" diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 71a80bba..f5f3ada2 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-derive" -version = "3.0.31" +version = "3.0.32" authors = ["sunli ", "Koxiaet"] edition = "2021" description = "Macros for async-graphql" @@ -18,7 +18,7 @@ proc-macro = true unstable_oneof = [] [dependencies] -async-graphql-parser = { path = "../parser", version = "3.0.31" } +async-graphql-parser = { path = "../parser", version = "3.0.32" } proc-macro2 = "1.0.24" syn = { version = "1.0.64", features = ["full", "extra-traits", "visit-mut", "visit"] } quote = "1.0.9" diff --git a/integrations/actix-web/Cargo.toml b/integrations/actix-web/Cargo.toml index aae14b85..ac05948c 100644 --- a/integrations/actix-web/Cargo.toml +++ b/integrations/actix-web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-actix-web" -version = "3.0.31" +version = "3.0.32" authors = ["sunli ", "Koxiaet"] edition = "2021" description = "async-graphql for actix-web" @@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "3.0.31" } +async-graphql = { path = "../..", version = "3.0.32" } actix = "0.12.0" actix-http = "3.0.0" diff --git a/integrations/axum/Cargo.toml b/integrations/axum/Cargo.toml index 42779e7e..3ea21dd6 100644 --- a/integrations/axum/Cargo.toml +++ b/integrations/axum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-axum" -version = "3.0.31" +version = "3.0.32" authors = ["sunli "] edition = "2021" description = "async-graphql for axum" @@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql", "axum"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "3.0.31" } +async-graphql = { path = "../..", version = "3.0.32" } async-trait = "0.1.51" axum = { version = "0.4", features = ["ws", "headers"] } diff --git a/integrations/poem/Cargo.toml b/integrations/poem/Cargo.toml index afb106b7..173dd849 100644 --- a/integrations/poem/Cargo.toml +++ b/integrations/poem/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-poem" -version = "3.0.31" +version = "3.0.32" authors = ["sunli "] edition = "2021" description = "async-graphql for poem" @@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql", "poem"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "3.0.31" } +async-graphql = { path = "../..", version = "3.0.32" } poem = { version = "1.2.2", features = ["websocket"] } futures-util = { version = "0.3.0", default-features = false } diff --git a/integrations/rocket/Cargo.toml b/integrations/rocket/Cargo.toml index 29cb77ef..25a4c63a 100644 --- a/integrations/rocket/Cargo.toml +++ b/integrations/rocket/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-rocket" -version = "3.0.31" +version = "3.0.32" authors = ["Daniel Wiesenberg "] edition = "2021" description = "async-graphql for Rocket.rs" @@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql", "rocket"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "3.0.31" } +async-graphql = { path = "../..", version = "3.0.32" } rocket = { version = "0.5.0-rc.1", default-features = false } serde = "1.0.126" diff --git a/integrations/tide/Cargo.toml b/integrations/tide/Cargo.toml index 37d904b2..4fadf020 100644 --- a/integrations/tide/Cargo.toml +++ b/integrations/tide/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-tide" -version = "3.0.31" +version = "3.0.32" authors = ["vkill ", "sunli "] edition = "2021" description = "async-graphql for tide" @@ -16,7 +16,7 @@ default = ["websocket"] websocket = ["tide-websockets"] [dependencies] -async-graphql = { path = "../..", version = "3.0.31" } +async-graphql = { path = "../..", version = "3.0.32" } async-trait = "0.1.48" futures-util = "0.3.0" diff --git a/integrations/warp/Cargo.toml b/integrations/warp/Cargo.toml index 9383bbc6..c35ae9cc 100644 --- a/integrations/warp/Cargo.toml +++ b/integrations/warp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-warp" -version = "3.0.31" +version = "3.0.32" authors = ["sunli ", "Koxiaet"] edition = "2021" description = "async-graphql for warp" @@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "3.0.31" } +async-graphql = { path = "../..", version = "3.0.32" } warp = { version = "0.3.0", default-features = false, features = ["websocket"] } futures-util = { version = "0.3.0", default-features = false, features = ["sink"] } diff --git a/parser/Cargo.toml b/parser/Cargo.toml index 60689da1..c114393d 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-parser" -version = "3.0.31" +version = "3.0.32" authors = ["sunli ", "Koxiaet"] edition = "2021" description = "GraphQL query parser for async-graphql" @@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql-value = { path = "../value", version = "3.0.31" } +async-graphql-value = { path = "../value", version = "3.0.32" } pest = "2.1.3" pest_derive = "2.1.0" serde_json = "1.0.64" diff --git a/value/Cargo.toml b/value/Cargo.toml index 0fdebea7..85870879 100644 --- a/value/Cargo.toml +++ b/value/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-value" -version = "3.0.31" +version = "3.0.32" authors = ["sunli ", "Koxiaet"] edition = "2021" description = "GraphQL value for async-graphql" From 86c9b73bec53c450498893b36a3d641eba95d0f3 Mon Sep 17 00:00:00 2001 From: Jake Oliver Date: Fri, 4 Mar 2022 10:29:58 +0000 Subject: [PATCH 12/24] Update actix to 0.13 --- integrations/actix-web/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrations/actix-web/Cargo.toml b/integrations/actix-web/Cargo.toml index ac05948c..0d570a8f 100644 --- a/integrations/actix-web/Cargo.toml +++ b/integrations/actix-web/Cargo.toml @@ -14,7 +14,7 @@ categories = ["network-programming", "asynchronous"] [dependencies] async-graphql = { path = "../..", version = "3.0.32" } -actix = "0.12.0" +actix = "0.13.0" actix-http = "3.0.0" actix-web = { version = "4.0.1", default-features = false } actix-web-actors = "4.0.0" From a4b840a9842ab88d83ad0cbece7f4f2aa713da48 Mon Sep 17 00:00:00 2001 From: Sunli Date: Mon, 28 Feb 2022 15:26:33 +0800 Subject: [PATCH 13/24] Add support for oneof field on object. #766 --- CHANGELOG.md | 4 + Cargo.toml | 1 - README.md | 1 - derive/Cargo.toml | 3 - derive/src/args.rs | 9 +- derive/src/complex_object.rs | 195 +++++++------ derive/src/directive.rs | 2 +- derive/src/interface.rs | 125 ++++++--- derive/src/lib.rs | 3 - derive/src/object.rs | 199 +++++++++----- derive/src/oneof_object.rs | 8 +- derive/src/simple_object.rs | 1 + derive/src/subscription.rs | 260 ++++++++---------- derive/src/utils.rs | 15 +- src/base.rs | 3 + src/context.rs | 20 +- src/docs/complex_object.md | 1 + src/docs/interface.md | 1 + src/docs/object.md | 1 + src/docs/subscription.md | 1 + src/lib.rs | 7 +- src/model/field.rs | 4 + src/model/type.rs | 1 - src/registry/export_sdl.rs | 6 +- src/registry/mod.rs | 10 +- src/schema.rs | 4 +- src/types/connection/connection_type.rs | 2 + src/types/connection/edge.rs | 2 + src/types/query_root.rs | 4 +- src/validation/mod.rs | 1 + .../rules/arguments_of_correct_type.rs | 80 +++++- src/validation/rules/known_argument_names.rs | 4 +- src/validation/rules/mod.rs | 2 + .../oneof_fields_have_exactly_one_argument.rs | 66 +++++ src/validation/suggestion.rs | 10 +- src/validation/test_harness.rs | 20 ++ src/validation/utils.rs | 8 + tests/complex_object.rs | 84 ++++++ tests/interface.rs | 115 ++++++++ tests/object.rs | 77 ++++++ tests/subscription.rs | 43 +++ tests/validators.rs | 100 +++++++ 42 files changed, 1119 insertions(+), 384 deletions(-) create mode 100644 src/validation/rules/oneof_fields_have_exactly_one_argument.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f939d77..1757f3be 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.33] 2022-3-4 + +- Add support for oneof field on object. [#766](https://github.com/async-graphql/async-graphql/issues/766) + # [3.0.32] 2022-3-4 - Bump `Actix-web` from `4.0.0-rc.3` to `4.0.1`. diff --git a/Cargo.toml b/Cargo.toml index 01335a55..95ba08b8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,6 @@ decimal = ["rust_decimal"] cbor = ["serde_cbor"] chrono-duration = ["chrono", "iso8601-duration"] password-strength-validator = ["zxcvbn"] -unstable_oneof = ["async-graphql-derive/unstable_oneof"] [dependencies] async-graphql-derive = { path = "derive", version = "3.0.32" } diff --git a/README.md b/README.md index d615fe1b..acbc7df0 100644 --- a/README.md +++ b/README.md @@ -81,7 +81,6 @@ This crate offers the following features, all of which are not activated by defa - `smol_str`: Integrate with the [`smol_str` crate](https://crates.io/crates/smol_str). - `hashbrown`: Integrate with the [`hashbrown` crate](https://github.com/rust-lang/hashbrown). - `time`: Integrate with the [`time` crate](https://github.com/time-rs/time). -- `unstable_oneof`: Enable the `OneofObject` macro to define the oneof input object. ## Apollo Studio diff --git a/derive/Cargo.toml b/derive/Cargo.toml index f5f3ada2..c31188a9 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -14,9 +14,6 @@ categories = ["network-programming", "asynchronous"] [lib] proc-macro = true -[features] -unstable_oneof = [] - [dependencies] async-graphql-parser = { path = "../parser", version = "3.0.32" } proc-macro2 = "1.0.24" diff --git a/derive/src/args.rs b/derive/src/args.rs index e519e1f7..e817206e 100644 --- a/derive/src/args.rs +++ b/derive/src/args.rs @@ -271,6 +271,7 @@ pub struct ObjectField { #[darling(default, multiple)] pub derived: Vec, pub flatten: bool, + pub oneof: bool, } #[derive(FromMeta, Default, Clone)] @@ -401,7 +402,6 @@ pub struct InputObject { pub complex: bool, } -#[cfg(feature = "unstable_oneof")] #[derive(FromVariant)] #[darling(attributes(graphql), forward_attrs(doc))] pub struct OneofObjectField { @@ -419,7 +419,6 @@ pub struct OneofObjectField { pub secret: bool, } -#[cfg(feature = "unstable_oneof")] #[derive(FromDeriveInput)] #[darling(attributes(graphql), forward_attrs(doc))] pub struct OneofObject { @@ -459,7 +458,7 @@ pub struct InterfaceFieldArgument { #[derive(FromMeta)] pub struct InterfaceField { - pub name: String, + pub name: SpannedValue, #[darling(rename = "type")] pub ty: LitStr, #[darling(default)] @@ -478,6 +477,8 @@ pub struct InterfaceField { pub requires: Option, #[darling(default)] pub visible: Option, + #[darling(default)] + pub oneof: bool, } #[derive(FromVariant)] @@ -553,6 +554,7 @@ pub struct SubscriptionField { pub guard: Option>, pub visible: Option, pub complexity: Option, + pub oneof: bool, } #[derive(FromField)] @@ -754,6 +756,7 @@ pub struct ComplexObjectField { #[darling(multiple)] pub derived: Vec, pub flatten: bool, + pub oneof: bool, } #[derive(FromMeta, Default)] diff --git a/derive/src/complex_object.rs b/derive/src/complex_object.rs index 12d23e43..393cb1be 100644 --- a/derive/src/complex_object.rs +++ b/derive/src/complex_object.rs @@ -126,7 +126,7 @@ pub fn generate( if method_args.flatten { // Only used to inject the context placeholder if required. - extract_input_args(&crate_name, method)?; + extract_input_args::(&crate_name, method)?; let ty = match &method.sig.output { ReturnType::Type(_, ty) => OutputType::parse(ty)?, @@ -190,7 +190,120 @@ pub fn generate( } }; - let args = extract_input_args(&crate_name, method)?; + let mut args = extract_input_args::(&crate_name, method)?; + let mut schema_args = Vec::new(); + let mut use_params = Vec::new(); + let mut get_params = Vec::new(); + let mut is_oneof_field = false; + + if method_args.oneof { + is_oneof_field = true; + + if args.len() != 1 { + return Err(Error::new_spanned( + &method, + "The `oneof` field requires exactly one argument.", + ) + .into()); + } + let (ident, ty, argument) = args.pop().unwrap(); + schema_args.push(quote! { + #crate_name::static_assertions::assert_impl_one!(#ty: #crate_name::OneofObjectType); + if let #crate_name::registry::MetaType::InputObject { input_fields, .. } = registry.create_fake_input_type::<#ty>() { + args.extend(input_fields); + } + }); + use_params.push(quote! { #ident }); + + let validators = argument + .validator + .clone() + .unwrap_or_default() + .create_validators( + &crate_name, + quote!(&#ident), + quote!(#ty), + Some(quote!(.map_err(|err| err.into_server_error(__pos)))), + )?; + get_params.push(quote! { + #[allow(non_snake_case, unused_variables)] + let (__pos, #ident) = ctx.oneof_param_value::<#ty>()?; + #validators + }); + } else { + for ( + ident, + ty, + args::Argument { + name, + desc, + default, + default_with, + validator, + visible, + secret, + .. + }, + ) in &args + { + let name = name.clone().unwrap_or_else(|| { + object_args + .rename_args + .rename(ident.ident.unraw().to_string(), RenameTarget::Argument) + }); + let desc = desc + .as_ref() + .map(|s| quote! {::std::option::Option::Some(#s)}) + .unwrap_or_else(|| quote! {::std::option::Option::None}); + let default = generate_default(default, default_with)?; + let schema_default = default + .as_ref() + .map(|value| { + quote! { + ::std::option::Option::Some(::std::string::ToString::to_string( + &<#ty as #crate_name::InputType>::to_value(&#value) + )) + } + }) + .unwrap_or_else(|| quote! {::std::option::Option::None}); + + let visible = visible_fn(visible); + schema_args.push(quote! { + args.insert(::std::borrow::ToOwned::to_owned(#name), #crate_name::registry::MetaInputValue { + name: #name, + description: #desc, + ty: <#ty as #crate_name::InputType>::create_type_info(registry), + default_value: #schema_default, + visible: #visible, + is_secret: #secret, + }); + }); + + let param_ident = &ident.ident; + use_params.push(quote! { #param_ident }); + + let default = match default { + Some(default) => { + quote! { ::std::option::Option::Some(|| -> #ty { #default }) } + } + None => quote! { ::std::option::Option::None }, + }; + + let validators = validator.clone().unwrap_or_default().create_validators( + &crate_name, + quote!(&#ident), + quote!(ty), + Some(quote!(.map_err(|err| err.into_server_error(__pos)))), + )?; + + get_params.push(quote! { + #[allow(non_snake_case)] + let (__pos, #ident) = ctx.param_value::<#ty>(#name, #default)?; + #validators + }); + } + } + let ty = match &method.sig.output { ReturnType::Type(_, ty) => OutputType::parse(ty)?, ReturnType::Default => { @@ -201,83 +314,6 @@ pub fn generate( .into()) } }; - - let mut schema_args = Vec::new(); - let mut use_params = Vec::new(); - let mut get_params = Vec::new(); - - for ( - ident, - ty, - args::Argument { - name, - desc, - default, - default_with, - validator, - visible, - secret, - .. - }, - ) in &args - { - let name = name.clone().unwrap_or_else(|| { - object_args - .rename_args - .rename(ident.ident.unraw().to_string(), RenameTarget::Argument) - }); - let desc = desc - .as_ref() - .map(|s| quote! {::std::option::Option::Some(#s)}) - .unwrap_or_else(|| quote! {::std::option::Option::None}); - let default = generate_default(default, default_with)?; - let schema_default = default - .as_ref() - .map(|value| { - quote! { - ::std::option::Option::Some(::std::string::ToString::to_string( - &<#ty as #crate_name::InputType>::to_value(&#value) - )) - } - }) - .unwrap_or_else(|| quote! {::std::option::Option::None}); - - let visible = visible_fn(visible); - schema_args.push(quote! { - args.insert(#name, #crate_name::registry::MetaInputValue { - name: #name, - description: #desc, - ty: <#ty as #crate_name::InputType>::create_type_info(registry), - default_value: #schema_default, - visible: #visible, - is_secret: #secret, - }); - }); - - let param_ident = &ident.ident; - use_params.push(quote! { #param_ident }); - - let default = match default { - Some(default) => { - quote! { ::std::option::Option::Some(|| -> #ty { #default }) } - } - None => quote! { ::std::option::Option::None }, - }; - - let validators = validator.clone().unwrap_or_default().create_validators( - &crate_name, - quote!(&#ident), - quote!(ty), - Some(quote!(.map_err(|err| err.into_server_error(__pos)))), - )?; - - get_params.push(quote! { - #[allow(non_snake_case)] - let (__pos, #ident) = ctx.param_value::<#ty>(#name, #default)?; - #validators - }); - } - let schema_ty = ty.value_type(); let visible = visible_fn(&method_args.visible); @@ -350,6 +386,7 @@ pub fn generate( requires: #requires, visible: #visible, compute_complexity: #complexity, + oneof: #is_oneof_field, })); }); diff --git a/derive/src/directive.rs b/derive/src/directive.rs index e7c3e916..322b7037 100644 --- a/derive/src/directive.rs +++ b/derive/src/directive.rs @@ -82,7 +82,7 @@ pub fn generate( let visible = visible_fn(&visible); schema_args.push(quote! { - args.insert(#name, #crate_name::registry::MetaInputValue { + args.insert(::std::borrow::ToOwned::to_owned(#name), #crate_name::registry::MetaInputValue { name: #name, description: #desc, ty: <#arg_ty as #crate_name::InputType>::create_type_info(registry), diff --git a/derive/src/interface.rs b/derive/src/interface.rs index 5a224807..a8b5d0f4 100644 --- a/derive/src/interface.rs +++ b/derive/src/interface.rs @@ -136,8 +136,10 @@ pub fn generate(interface_args: &args::Interface) -> GeneratorResult GeneratorResult GeneratorResult }); use_params.push(quote! { ctx }); - for InterfaceFieldArgument { - name, - desc, - ty, - default, - default_with, - visible, - secret, - } in args - { + let mut is_oneof_field = false; + + if *oneof { + is_oneof_field = true; + + if args.len() != 1 { + return Err( + Error::new(name_span, "The `oneof` field requires one parameter.").into(), + ); + } + + let InterfaceFieldArgument { name, ty, .. } = &args[0]; let ident = Ident::new(name, Span::call_site()); - let name = interface_args - .rename_args - .rename(name, RenameTarget::Argument); let ty = match syn::parse_str::(&ty.value()) { Ok(ty) => ty, Err(_) => return Err(Error::new_spanned(&ty, "Expect type").into()), }; + decl_params.push(quote! { #ident: #ty }); use_params.push(quote! { #ident }); - - let default = generate_default(default, default_with)?; - let get_default = match &default { - Some(default) => quote! { ::std::option::Option::Some(|| -> #ty { #default }) }, - None => quote! { ::std::option::Option::None }, - }; get_params.push(quote! { - let (_, #ident) = ctx.param_value::<#ty>(#name, #get_default)?; + #[allow(non_snake_case, unused_variables)] + let (_, #ident) = ctx.oneof_param_value::<#ty>()?; }); - - let desc = desc - .as_ref() - .map(|s| quote! {::std::option::Option::Some(#s)}) - .unwrap_or_else(|| quote! {::std::option::Option::None}); - let schema_default = default - .as_ref() - .map(|value| { - quote! { - ::std::option::Option::Some(::std::string::ToString::to_string( - &<#ty as #crate_name::InputType>::to_value(&#value) - )) - } - }) - .unwrap_or_else(|| quote! {::std::option::Option::None}); - let visible = visible_fn(visible); schema_args.push(quote! { - args.insert(#name, #crate_name::registry::MetaInputValue { - name: #name, - description: #desc, - ty: <#ty as #crate_name::InputType>::create_type_info(registry), - default_value: #schema_default, - visible: #visible, - is_secret: #secret, - }); + #crate_name::static_assertions::assert_impl_one!(#ty: #crate_name::OneofObjectType); + if let #crate_name::registry::MetaType::InputObject { input_fields, .. } = registry.create_fake_input_type::<#ty>() { + args.extend(input_fields); + } }); + } else { + for InterfaceFieldArgument { + name, + desc, + ty, + default, + default_with, + visible, + secret, + } in args + { + let ident = Ident::new(name, Span::call_site()); + let name = interface_args + .rename_args + .rename(name, RenameTarget::Argument); + let ty = match syn::parse_str::(&ty.value()) { + Ok(ty) => ty, + Err(_) => return Err(Error::new_spanned(&ty, "Expect type").into()), + }; + decl_params.push(quote! { #ident: #ty }); + use_params.push(quote! { #ident }); + + let default = generate_default(default, default_with)?; + let get_default = match &default { + Some(default) => quote! { ::std::option::Option::Some(|| -> #ty { #default }) }, + None => quote! { ::std::option::Option::None }, + }; + get_params.push(quote! { + let (_, #ident) = ctx.param_value::<#ty>(#name, #get_default)?; + }); + + let desc = desc + .as_ref() + .map(|s| quote! {::std::option::Option::Some(#s)}) + .unwrap_or_else(|| quote! {::std::option::Option::None}); + let schema_default = default + .as_ref() + .map(|value| { + quote! { + ::std::option::Option::Some(::std::string::ToString::to_string( + &<#ty as #crate_name::InputType>::to_value(&#value) + )) + } + }) + .unwrap_or_else(|| quote! {::std::option::Option::None}); + let visible = visible_fn(visible); + schema_args.push(quote! { + args.insert(::std::borrow::ToOwned::to_owned(#name), #crate_name::registry::MetaInputValue { + name: #name, + description: #desc, + ty: <#ty as #crate_name::InputType>::create_type_info(registry), + default_value: #schema_default, + visible: #visible, + is_secret: #secret, + }); + }); + } } for enum_name in &enum_names { @@ -275,6 +309,7 @@ pub fn generate(interface_args: &args::Interface) -> GeneratorResult TokenStream { } } -#[cfg(feature = "unstable_oneof")] -#[cfg_attr(docsrs, doc(cfg(feature = "unstable_oneof")))] #[proc_macro_derive(OneofObject, attributes(graphql))] pub fn derive_oneof_object(input: TokenStream) -> TokenStream { let object_args = diff --git a/derive/src/object.rs b/derive/src/object.rs index ef24aecf..4bcfdc35 100644 --- a/derive/src/object.rs +++ b/derive/src/object.rs @@ -141,7 +141,15 @@ pub fn generate( return Err(Error::new_spanned(&method, "Must be asynchronous").into()); } - let args = extract_input_args(&crate_name, method)?; + if method_args.oneof { + return Err(Error::new_spanned( + &method, + "The `entity` and `oneof` attributes cannot be specified at the same time.", + ) + .into()); + } + + let args = extract_input_args::(&crate_name, method)?; let ty = match &method.sig.output { ReturnType::Type(_, ty) => OutputType::parse(ty)?, @@ -262,7 +270,7 @@ pub fn generate( if method_args.flatten { // Only used to inject the context placeholder if required. - extract_input_args(&crate_name, method)?; + extract_input_args::(&crate_name, method)?; let ty = match &method.sig.output { ReturnType::Type(_, ty) => OutputType::parse(ty)?, @@ -326,10 +334,121 @@ pub fn generate( } }; - let args = extract_input_args(&crate_name, method)?; + let mut args = extract_input_args::(&crate_name, method)?; let mut schema_args = Vec::new(); let mut use_params = Vec::new(); let mut get_params = Vec::new(); + let mut is_oneof_field = false; + + if method_args.oneof { + is_oneof_field = true; + + if args.len() != 1 { + return Err(Error::new_spanned( + &method, + "The `oneof` field requires exactly one argument.", + ) + .into()); + } + let (ident, ty, argument) = args.pop().unwrap(); + + schema_args.push(quote! { + #crate_name::static_assertions::assert_impl_one!(#ty: #crate_name::OneofObjectType); + if let #crate_name::registry::MetaType::InputObject { input_fields, .. } = registry.create_fake_input_type::<#ty>() { + args.extend(input_fields); + } + }); + use_params.push(quote! { #ident }); + + let validators = argument + .validator + .clone() + .unwrap_or_default() + .create_validators( + &crate_name, + quote!(&#ident), + quote!(#ty), + Some(quote!(.map_err(|err| err.into_server_error(__pos)))), + )?; + get_params.push(quote! { + #[allow(non_snake_case, unused_variables)] + let (__pos, #ident) = ctx.oneof_param_value::<#ty>()?; + #validators + }); + } else { + for ( + ident, + ty, + args::Argument { + name, + desc, + default, + default_with, + validator, + visible, + secret, + .. + }, + ) in &args + { + let name = name.clone().unwrap_or_else(|| { + object_args + .rename_args + .rename(ident.ident.unraw().to_string(), RenameTarget::Argument) + }); + let desc = desc + .as_ref() + .map(|s| quote! {::std::option::Option::Some(#s)}) + .unwrap_or_else(|| quote! {::std::option::Option::None}); + let default = generate_default(default, default_with)?; + let schema_default = default + .as_ref() + .map(|value| { + quote! { + ::std::option::Option::Some(::std::string::ToString::to_string( + &<#ty as #crate_name::InputType>::to_value(&#value) + )) + } + }) + .unwrap_or_else(|| quote! {::std::option::Option::None}); + + let visible = visible_fn(visible); + schema_args.push(quote! { + args.insert(::std::borrow::ToOwned::to_owned(#name), #crate_name::registry::MetaInputValue { + name: #name, + description: #desc, + ty: <#ty as #crate_name::InputType>::create_type_info(registry), + default_value: #schema_default, + visible: #visible, + is_secret: #secret, + }); + }); + + let param_ident = &ident.ident; + use_params.push(quote! { #param_ident }); + + let default = match default { + Some(default) => { + quote! { ::std::option::Option::Some(|| -> #ty { #default }) } + } + None => quote! { ::std::option::Option::None }, + }; + + let validators = validator.clone().unwrap_or_default().create_validators( + &crate_name, + quote!(&#ident), + quote!(#ty), + Some(quote!(.map_err(|err| err.into_server_error(__pos)))), + )?; + + get_params.push(quote! { + #[allow(non_snake_case, unused_variables)] + let (__pos, #ident) = ctx.param_value::<#ty>(#name, #default)?; + #validators + }); + } + } + let ty = match &method.sig.output { ReturnType::Type(_, ty) => OutputType::parse(ty)?, ReturnType::Default => { @@ -340,79 +459,6 @@ pub fn generate( .into()) } }; - - for ( - ident, - ty, - args::Argument { - name, - desc, - default, - default_with, - validator, - visible, - secret, - .. - }, - ) in &args - { - let name = name.clone().unwrap_or_else(|| { - object_args - .rename_args - .rename(ident.ident.unraw().to_string(), RenameTarget::Argument) - }); - let desc = desc - .as_ref() - .map(|s| quote! {::std::option::Option::Some(#s)}) - .unwrap_or_else(|| quote! {::std::option::Option::None}); - let default = generate_default(default, default_with)?; - let schema_default = default - .as_ref() - .map(|value| { - quote! { - ::std::option::Option::Some(::std::string::ToString::to_string( - &<#ty as #crate_name::InputType>::to_value(&#value) - )) - } - }) - .unwrap_or_else(|| quote! {::std::option::Option::None}); - - let visible = visible_fn(visible); - schema_args.push(quote! { - args.insert(#name, #crate_name::registry::MetaInputValue { - name: #name, - description: #desc, - ty: <#ty as #crate_name::InputType>::create_type_info(registry), - default_value: #schema_default, - visible: #visible, - is_secret: #secret, - }); - }); - - let param_ident = &ident.ident; - use_params.push(quote! { #param_ident }); - - let default = match default { - Some(default) => { - quote! { ::std::option::Option::Some(|| -> #ty { #default }) } - } - None => quote! { ::std::option::Option::None }, - }; - - let validators = validator.clone().unwrap_or_default().create_validators( - &crate_name, - quote!(&#ident), - quote!(#ty), - Some(quote!(.map_err(|err| err.into_server_error(__pos)))), - )?; - - get_params.push(quote! { - #[allow(non_snake_case, unused_variables)] - let (__pos, #ident) = ctx.param_value::<#ty>(#name, #default)?; - #validators - }); - } - let schema_ty = ty.value_type(); let visible = visible_fn(&method_args.visible); @@ -485,6 +531,7 @@ pub fn generate( requires: #requires, visible: #visible, compute_complexity: #complexity, + oneof: #is_oneof_field, }); }); diff --git a/derive/src/oneof_object.rs b/derive/src/oneof_object.rs index 8017fd48..588997f9 100644 --- a/derive/src/oneof_object.rs +++ b/derive/src/oneof_object.rs @@ -148,7 +148,7 @@ pub fn generate(object_args: &args::OneofObject) -> GeneratorResult fn parse(value: ::std::option::Option<#crate_name::Value>) -> #crate_name::InputValueResult { if let ::std::option::Option::Some(#crate_name::Value::Object(mut obj)) = value { #(#parse_item)* - ::std::result::Result::Err(#crate_name::InputValueError::expected_type(async_graphql::Value::Object(obj))) + ::std::result::Result::Err(#crate_name::InputValueError::expected_type(#crate_name::Value::Object(obj))) } else { ::std::result::Result::Err(#crate_name::InputValueError::expected_type(value.unwrap_or_default())) } @@ -170,6 +170,9 @@ pub fn generate(object_args: &args::OneofObject) -> GeneratorResult ::std::option::Option::Some(self) } } + + impl #crate_name::InputObjectType for #ident {} + impl #crate_name::OneofObjectType for #ident {} } } else { let mut code = Vec::new(); @@ -195,7 +198,7 @@ pub fn generate(object_args: &args::OneofObject) -> GeneratorResult fn __internal_parse(value: ::std::option::Option<#crate_name::Value>) -> #crate_name::InputValueResult where Self: #crate_name::InputType { if let ::std::option::Option::Some(#crate_name::Value::Object(mut obj)) = value { #(#parse_item)* - ::std::result::Result::Err(#crate_name::InputValueError::expected_type(async_graphql::Value::Object(obj))) + ::std::result::Result::Err(#crate_name::InputValueError::expected_type(#crate_name::Value::Object(obj))) } else { ::std::result::Result::Err(#crate_name::InputValueError::expected_type(value.unwrap_or_default())) } @@ -247,6 +250,7 @@ pub fn generate(object_args: &args::OneofObject) -> GeneratorResult } impl #crate_name::InputObjectType for #concrete_type {} + impl #crate_name::OneofObjectType for #concrete_type {} }; code.push(expanded); } diff --git a/derive/src/simple_object.rs b/derive/src/simple_object.rs index f98f9965..c4ae6f71 100644 --- a/derive/src/simple_object.rs +++ b/derive/src/simple_object.rs @@ -167,6 +167,7 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult(&crate_name, method)?; + + if field.oneof { + is_oneof_field = true; + + if args.len() != 1 { + return Err(Error::new_spanned( + &method, + "The `oneof` field requires exactly one argument.", + ) + .into()); + } + let (ident, ty, argument) = args.pop().unwrap(); + + schema_args.push(quote! { + #crate_name::static_assertions::assert_impl_one!(#ty: #crate_name::OneofObjectType); + if let #crate_name::registry::MetaType::InputObject { input_fields, .. } = registry.create_fake_input_type::<#ty>() { + args.extend(input_fields); + } + }); + use_params.push(quote! { #ident }); + + let validators = argument + .validator + .clone() + .unwrap_or_default() + .create_validators( + &crate_name, + quote!(&#ident), + quote!(#ty), + Some(quote!(.map_err(|err| err.into_server_error(__pos)))), + )?; + get_params.push(quote! { + #[allow(non_snake_case, unused_variables)] + let (__pos, #ident) = ctx.oneof_param_value::<#ty>()?; + #validators + }); + } else { + for ( + ident, + ty, + args::SubscriptionFieldArgument { + name, + desc, + default, + default_with, + validator, + visible: arg_visible, + secret, + }, + ) in &args + { + let name = name.clone().unwrap_or_else(|| { + subscription_args + .rename_args + .rename(ident.ident.unraw().to_string(), RenameTarget::Argument) + }); + let desc = desc + .as_ref() + .map(|s| quote! {::std::option::Option::Some(#s)}) + .unwrap_or_else(|| quote! {::std::option::Option::None}); + let default = generate_default(default, default_with)?; + + let schema_default = default + .as_ref() + .map(|value| { + quote! { + ::std::option::Option::Some(::std::string::ToString::to_string( + &<#ty as #crate_name::InputType>::to_value(&#value) + )) + } + }) + .unwrap_or_else(|| quote! {::std::option::Option::None}); + + let visible = visible_fn(arg_visible); + schema_args.push(quote! { + args.insert(::std::borrow::ToOwned::to_owned(#name), #crate_name::registry::MetaInputValue { + name: #name, + description: #desc, + ty: <#ty as #crate_name::InputType>::create_type_info(registry), + default_value: #schema_default, + visible: #visible, + is_secret: #secret, + }); + }); + + use_params.push(quote! { #ident }); + + let default = match default { + Some(default) => { + quote! { ::std::option::Option::Some(|| -> #ty { #default }) } + } + None => quote! { ::std::option::Option::None }, + }; + let validators = validator.clone().unwrap_or_default().create_validators( + &crate_name, + quote!(&#ident), + quote!(#ty), + Some(quote!(.map_err(|err| err.into_server_error(__pos)))), + )?; + + get_params.push(quote! { + #[allow(non_snake_case)] + let (__pos, #ident) = ctx.param_value::<#ty>(#name, #default)?; + #validators + }); + } + } + let ty = match &method.sig.output { ReturnType::Type(_, ty) => OutputType::parse(ty)?, ReturnType::Default => { @@ -78,138 +189,6 @@ pub fn generate( .into()) } }; - - let mut create_ctx = true; - let mut args = Vec::new(); - - for (idx, arg) in method.sig.inputs.iter_mut().enumerate() { - if let FnArg::Receiver(receiver) = arg { - if idx != 0 { - return Err(Error::new_spanned( - receiver, - "The self receiver must be the first parameter.", - ) - .into()); - } - } else if let FnArg::Typed(pat) = arg { - if idx == 0 { - return Err(Error::new_spanned( - pat, - "The self receiver must be the first parameter.", - ) - .into()); - } - - match (&*pat.pat, &*pat.ty) { - (Pat::Ident(arg_ident), Type::Path(arg_ty)) => { - args.push(( - arg_ident.clone(), - arg_ty.clone(), - parse_graphql_attrs::(&pat.attrs)? - .unwrap_or_default(), - )); - remove_graphql_attrs(&mut pat.attrs); - } - (arg, Type::Reference(TypeReference { elem, .. })) => { - if let Type::Path(path) = elem.as_ref() { - if idx != 1 || path.path.segments.last().unwrap().ident != "Context" - { - return Err(Error::new_spanned( - arg, - "Only types that implement `InputType` can be used as input arguments.", - ) - .into()); - } else { - create_ctx = false; - } - } - } - _ => { - return Err(Error::new_spanned(arg, "Incorrect argument type").into()); - } - } - } else { - return Err(Error::new_spanned(arg, "Incorrect argument type").into()); - } - } - - if create_ctx { - let arg = syn::parse2::(quote! { _: &#crate_name::Context<'_> }).unwrap(); - method.sig.inputs.insert(1, arg); - } - - let mut schema_args = Vec::new(); - let mut use_params = Vec::new(); - let mut get_params = Vec::new(); - - for ( - ident, - ty, - args::SubscriptionFieldArgument { - name, - desc, - default, - default_with, - validator, - visible: arg_visible, - secret, - }, - ) in &args - { - let name = name.clone().unwrap_or_else(|| { - subscription_args - .rename_args - .rename(ident.ident.unraw().to_string(), RenameTarget::Argument) - }); - let desc = desc - .as_ref() - .map(|s| quote! {::std::option::Option::Some(#s)}) - .unwrap_or_else(|| quote! {::std::option::Option::None}); - let default = generate_default(default, default_with)?; - - let schema_default = default - .as_ref() - .map(|value| { - quote! { - ::std::option::Option::Some(::std::string::ToString::to_string( - &<#ty as #crate_name::InputType>::to_value(&#value) - )) - } - }) - .unwrap_or_else(|| quote! {::std::option::Option::None}); - - let visible = visible_fn(arg_visible); - schema_args.push(quote! { - args.insert(#name, #crate_name::registry::MetaInputValue { - name: #name, - description: #desc, - ty: <#ty as #crate_name::InputType>::create_type_info(registry), - default_value: #schema_default, - visible: #visible, - is_secret: #secret, - }); - }); - - use_params.push(quote! { #ident }); - - let default = match default { - Some(default) => quote! { ::std::option::Option::Some(|| -> #ty { #default }) }, - None => quote! { ::std::option::Option::None }, - }; - let validators = validator.clone().unwrap_or_default().create_validators( - &crate_name, - quote!(&#ident), - quote!(#ty), - Some(quote!(.map_err(|err| err.into_server_error(__pos)))), - )?; - - get_params.push(quote! { - #[allow(non_snake_case)] - let (__pos, #ident) = ctx.param_value::<#ty>(#name, #default)?; - #validators - }); - } - let res_ty = ty.value_type(); let stream_ty = if let Type::ImplTrait(TypeImplTrait { bounds, .. }) = &res_ty { let mut r = None; @@ -307,6 +286,7 @@ pub fn generate( provides: ::std::option::Option::None, visible: #visible, compute_complexity: #complexity, + oneof: #is_oneof_field, }); }); diff --git a/derive/src/utils.rs b/derive/src/utils.rs index 1b578496..871483d8 100644 --- a/derive/src/utils.rs +++ b/derive/src/utils.rs @@ -13,7 +13,7 @@ use syn::{ }; use thiserror::Error; -use crate::args::{self, Argument, Deprecation, Visible}; +use crate::args::{self, Deprecation, Visible}; #[derive(Error, Debug)] pub enum GeneratorError { @@ -142,7 +142,9 @@ pub fn get_cfg_attrs(attrs: &[Attribute]) -> Vec { .collect() } -pub fn parse_graphql_attrs(attrs: &[Attribute]) -> GeneratorResult> { +pub fn parse_graphql_attrs( + attrs: &[Attribute], +) -> GeneratorResult> { for attr in attrs { if attr.path.is_ident("graphql") { let meta = attr.parse_meta()?; @@ -238,10 +240,10 @@ pub fn gen_deprecation(deprecation: &Deprecation, crate_name: &TokenStream) -> T } } -pub fn extract_input_args( +pub fn extract_input_args( crate_name: &proc_macro2::TokenStream, method: &mut ImplItemMethod, -) -> GeneratorResult> { +) -> GeneratorResult> { let mut args = Vec::new(); let mut create_ctx = true; @@ -278,8 +280,7 @@ pub fn extract_input_args( args.push(( arg_ident.clone(), pat.ty.as_ref().clone(), - parse_graphql_attrs::(&pat.attrs)? - .unwrap_or_default(), + parse_graphql_attrs::(&pat.attrs)?.unwrap_or_default(), )); } else { create_ctx = false; @@ -290,7 +291,7 @@ pub fn extract_input_args( args.push(( arg_ident.clone(), ty.clone(), - parse_graphql_attrs::(&pat.attrs)?.unwrap_or_default(), + parse_graphql_attrs::(&pat.attrs)?.unwrap_or_default(), )); remove_graphql_attrs(&mut pat.attrs); } diff --git a/src/base.rs b/src/base.rs index f9474e32..c0e7338e 100644 --- a/src/base.rs +++ b/src/base.rs @@ -148,6 +148,9 @@ pub trait UnionType: ContainerType {} /// A GraphQL input object. pub trait InputObjectType: InputType {} +/// A GraphQL oneof input object. +pub trait OneofObjectType: InputObjectType {} + #[async_trait::async_trait] impl OutputType for Box { fn type_name() -> Cow<'static, str> { diff --git a/src/context.rs b/src/context.rs index 41aa822b..39970b15 100644 --- a/src/context.rs +++ b/src/context.rs @@ -19,8 +19,8 @@ use crate::parser::types::{ }; use crate::schema::SchemaEnv; use crate::{ - Error, InputType, Lookahead, Name, PathSegment, Pos, Positioned, Result, ServerError, - ServerResult, UploadValue, Value, + Error, InputType, Lookahead, Name, OneofObjectType, PathSegment, Pos, Positioned, Result, + ServerError, ServerResult, UploadValue, Value, }; /// Data related functions of the context. @@ -604,6 +604,22 @@ impl<'a> ContextBase<'a, &'a Positioned> { self.get_param_value(&self.item.node.arguments, name, default) } + #[doc(hidden)] + pub fn oneof_param_value(&self) -> ServerResult<(Pos, T)> { + use indexmap::IndexMap; + + let mut map = IndexMap::new(); + + for (name, value) in &self.item.node.arguments { + let value = self.resolve_input_value(value.clone())?; + map.insert(name.node.clone(), value); + } + + InputType::parse(Some(Value::Object(map))) + .map(|value| (self.item.pos, value)) + .map_err(|e| e.into_server_error(self.item.pos)) + } + /// Creates a uniform interface to inspect the forthcoming selections. /// /// # Examples diff --git a/src/docs/complex_object.md b/src/docs/complex_object.md index b2bca4a1..06649fc1 100644 --- a/src/docs/complex_object.md +++ b/src/docs/complex_object.md @@ -36,6 +36,7 @@ some simple fields, and use the `ComplexObject` macro to define some other field | complexity | Custom field complexity. | string | Y | | derived | Generate derived fields *[See also the Book](https://async-graphql.github.io/async-graphql/en/derived_fields.html).* | object | Y | | flatten | Similar to serde (flatten) | boolean | Y | +| oneof | Oneof field | bool | Y | # Field argument attributes diff --git a/src/docs/interface.md b/src/docs/interface.md index d630af42..6a1953c8 100644 --- a/src/docs/interface.md +++ b/src/docs/interface.md @@ -30,6 +30,7 @@ Define a GraphQL interface | 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 | | 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 | +| oneof | Oneof field | bool | Y | # Field argument attributes diff --git a/src/docs/object.md b/src/docs/object.md index 9faeb8d3..c93953fa 100644 --- a/src/docs/object.md +++ b/src/docs/object.md @@ -39,6 +39,7 @@ All methods are converted to camelCase. | complexity | Custom field complexity. | string | Y | | derived | Generate derived fields *[See also the Book](https://async-graphql.github.io/async-graphql/en/derived_fields.html).* | object | Y | | flatten | Similar to serde (flatten) | boolean | Y | +| oneof | Oneof field | bool | Y | # Field argument attributes diff --git a/src/docs/subscription.md b/src/docs/subscription.md index b9a86fc2..9152d85f 100644 --- a/src/docs/subscription.md +++ b/src/docs/subscription.md @@ -32,6 +32,7 @@ The filter function should be synchronous. | complexity | Custom field complexity. *[See also the Book](https://async-graphql.github.io/async-graphql/en/depth_and_complexity.html).* | bool | Y | | complexity | Custom field complexity. | string | Y | | secret | Mark this field as a secret, it will not output the actual value in the log. | bool | Y | +| oneof | Oneof field | bool | Y | # Field argument attributes diff --git a/src/lib.rs b/src/lib.rs index 50f1e35d..df612cc0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -76,7 +76,6 @@ //! - `smol_str`: Integrate with the [`smol_str` crate](https://crates.io/crates/smol_str). //! - `hashbrown`: Integrate with the [`hashbrown` crate](https://github.com/rust-lang/hashbrown). //! - `time`: Integrate with the [`time` crate](https://github.com/time-rs/time). -//! - `unstable_oneof`: Enable the `OneofObject` macro to define the oneof input object. //! //! ## Integrations //! @@ -214,8 +213,8 @@ pub use async_graphql_value::{ SerializerError, Variables, }; pub use base::{ - ComplexObject, Description, InputObjectType, InputType, InterfaceType, ObjectType, OutputType, - UnionType, + ComplexObject, Description, InputObjectType, InputType, InterfaceType, ObjectType, + OneofObjectType, OutputType, UnionType, }; pub use custom_directive::{CustomDirective, CustomDirectiveFactory}; pub use error::{ @@ -267,8 +266,6 @@ pub use async_graphql_derive::MergedSubscription; pub use async_graphql_derive::NewType; #[doc = include_str!("docs/object.md")] pub use async_graphql_derive::Object; -#[cfg(feature = "unstable_oneof")] -#[cfg_attr(docsrs, doc(cfg(feature = "unstable_oneof")))] #[doc = include_str!("docs/oneof_object.md")] pub use async_graphql_derive::OneofObject; #[doc = include_str!("docs/scalar.md")] diff --git a/src/model/field.rs b/src/model/field.rs index 200ee3cd..e5b4ec17 100644 --- a/src/model/field.rs +++ b/src/model/field.rs @@ -50,4 +50,8 @@ impl<'a> __Field<'a> { async fn deprecation_reason(&self) -> Option<&str> { self.field.deprecation.reason() } + + async fn one_of(&self) -> bool { + self.field.oneof + } } diff --git a/src/model/type.rs b/src/model/type.rs index 991547da..160a3aa2 100644 --- a/src/model/type.rs +++ b/src/model/type.rs @@ -229,7 +229,6 @@ impl<'a> __Type<'a> { } } - #[cfg(feature = "unstable_oneof")] async fn one_of(&self) -> Option { if let TypeDetail::Named(registry::MetaType::InputObject { oneof, .. }) = &self.detail { Some(*oneof) diff --git a/src/registry/export_sdl.rs b/src/registry/export_sdl.rs index d70b6b8e..b19153bb 100644 --- a/src/registry/export_sdl.rs +++ b/src/registry/export_sdl.rs @@ -69,6 +69,10 @@ impl Registry { write!(sdl, "\t{}: {}", field.name, field.ty).ok(); } + if field.oneof { + write!(sdl, " @oneof").ok(); + } + if federation { if field.external { write!(sdl, " @external").ok(); @@ -202,7 +206,6 @@ impl Registry { name, input_fields, description, - #[cfg(feature = "unstable_oneof")] oneof, .. } => { @@ -210,7 +213,6 @@ impl Registry { writeln!(sdl, "\"\"\"\n{}\n\"\"\"", description.unwrap()).ok(); } write!(sdl, "input {} ", name).ok(); - #[cfg(feature = "unstable_oneof")] if *oneof { write!(sdl, "@oneof ").ok(); } diff --git a/src/registry/mod.rs b/src/registry/mod.rs index fc8dac36..e818e39e 100644 --- a/src/registry/mod.rs +++ b/src/registry/mod.rs @@ -157,7 +157,7 @@ impl Deprecation { pub struct MetaField { pub name: String, pub description: Option<&'static str>, - pub args: IndexMap<&'static str, MetaInputValue>, + pub args: IndexMap, pub ty: String, pub deprecation: Deprecation, pub cache_control: CacheControl, @@ -166,6 +166,7 @@ pub struct MetaField { pub provides: Option<&'static str>, pub visible: Option, pub compute_complexity: Option, + pub oneof: bool, } #[derive(Clone)] @@ -351,7 +352,7 @@ pub struct MetaDirective { pub name: &'static str, pub description: Option<&'static str>, pub locations: Vec, - pub args: IndexMap<&'static str, MetaInputValue>, + pub args: IndexMap, pub is_repeatable: bool, pub visible: Option, } @@ -566,6 +567,7 @@ impl Registry { provides: None, visible: None, compute_complexity: None, + oneof: false, }, ); @@ -577,7 +579,7 @@ impl Registry { args: { let mut args = IndexMap::new(); args.insert( - "representations", + "representations".to_string(), MetaInputValue { name: "representations", description: None, @@ -597,6 +599,7 @@ impl Registry { provides: None, visible: None, compute_complexity: None, + oneof: false, }, ); } @@ -627,6 +630,7 @@ impl Registry { provides: None, visible: None, compute_complexity: None, + oneof: false, }, ); fields diff --git a/src/schema.rs b/src/schema.rs index 65002fdb..bfdd0920 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -318,7 +318,7 @@ where ], args: { let mut args = IndexMap::new(); - args.insert("if", MetaInputValue { + args.insert("if".to_string(), MetaInputValue { name: "if", description: Some("Included when true."), ty: "Boolean!".to_string(), @@ -342,7 +342,7 @@ where ], args: { let mut args = IndexMap::new(); - args.insert("if", MetaInputValue { + args.insert("if".to_string(), MetaInputValue { name: "if", description: Some("Skipped when true."), ty: "Boolean!".to_string(), diff --git a/src/types/connection/connection_type.rs b/src/types/connection/connection_type.rs index 1d7a1499..e58dd7f0 100644 --- a/src/types/connection/connection_type.rs +++ b/src/types/connection/connection_type.rs @@ -193,6 +193,7 @@ where provides: None, visible: None, compute_complexity: None, + oneof: false, }, ); @@ -212,6 +213,7 @@ where provides: None, visible: None, compute_complexity: None, + oneof: false, }, ); diff --git a/src/types/connection/edge.rs b/src/types/connection/edge.rs index 1955c394..b4704f05 100644 --- a/src/types/connection/edge.rs +++ b/src/types/connection/edge.rs @@ -101,6 +101,7 @@ where provides: None, visible: None, compute_complexity: None, + oneof: false, }, ); @@ -118,6 +119,7 @@ where provides: None, visible: None, compute_complexity: None, + oneof: false, }, ); diff --git a/src/types/query_root.rs b/src/types/query_root.rs index f40dd5b4..d984b965 100644 --- a/src/types/query_root.rs +++ b/src/types/query_root.rs @@ -112,6 +112,7 @@ impl OutputType for QueryRoot { provides: None, visible: None, compute_complexity: None, + oneof: false, }, ); @@ -123,7 +124,7 @@ impl OutputType for QueryRoot { args: { let mut args = IndexMap::new(); args.insert( - "name", + "name".to_string(), registry::MetaInputValue { name: "name", description: None, @@ -143,6 +144,7 @@ impl OutputType for QueryRoot { provides: None, visible: None, compute_complexity: None, + oneof: false, }, ); } diff --git a/src/validation/mod.rs b/src/validation/mod.rs index fe8c030c..53b153e0 100644 --- a/src/validation/mod.rs +++ b/src/validation/mod.rs @@ -73,6 +73,7 @@ pub fn check_rules( .with(rules::KnownDirectives::default()) .with(rules::DirectivesUnique::default()) .with(rules::OverlappingFieldsCanBeMerged) + .with(rules::OneofFieldsHaveExactlyOneArgument) .with(rules::UploadFile) .with(visitors::CacheControlCalculate { cache_control: &mut cache_control, diff --git a/src/validation/rules/arguments_of_correct_type.rs b/src/validation/rules/arguments_of_correct_type.rs index 8e6d09ae..c4dc1382 100644 --- a/src/validation/rules/arguments_of_correct_type.rs +++ b/src/validation/rules/arguments_of_correct_type.rs @@ -11,7 +11,7 @@ use crate::{Name, Positioned, QueryPathSegment}; #[derive(Default)] pub struct ArgumentsOfCorrectType<'a> { - current_args: Option<&'a IndexMap<&'static str, MetaInputValue>>, + current_args: Option<&'a IndexMap>, } impl<'a> Visitor<'a> for ArgumentsOfCorrectType<'a> { @@ -983,4 +983,82 @@ mod tests { "#, ); } + + #[test] + fn oneof() { + expect_passes_rule!( + factory, + r#" + { + oneofArg(arg: {a: 10}) + } + "#, + ); + + expect_passes_rule!( + factory, + r#" + { + oneofArg(arg: {b: "abc"}) + } + "#, + ); + + expect_fails_rule!( + factory, + r#" + { + oneofArg(arg: {a: 10, b: "abc"}) + } + "#, + ); + } + + #[test] + fn oneof_opt() { + expect_passes_rule!( + factory, + r#" + { + oneofOpt(arg: {a: 10}) + } + "#, + ); + + expect_passes_rule!( + factory, + r#" + { + oneofOpt(arg: {b: "abc"}) + } + "#, + ); + + expect_passes_rule!( + factory, + r#" + { + oneofOpt + } + "#, + ); + + expect_passes_rule!( + factory, + r#" + { + oneofOpt(arg: null) + } + "#, + ); + + expect_fails_rule!( + factory, + r#" + { + oneofOpt(arg: {a: 10, b: "abc"}) + } + "#, + ); + } } diff --git a/src/validation/rules/known_argument_names.rs b/src/validation/rules/known_argument_names.rs index b80fe396..715193a1 100644 --- a/src/validation/rules/known_argument_names.rs +++ b/src/validation/rules/known_argument_names.rs @@ -17,7 +17,7 @@ enum ArgsType<'a> { #[derive(Default)] pub struct KnownArgumentNames<'a> { - current_args: Option<(&'a IndexMap<&'static str, MetaInputValue>, ArgsType<'a>)>, + current_args: Option<(&'a IndexMap, ArgsType<'a>)>, } impl<'a> KnownArgumentNames<'a> { @@ -26,7 +26,7 @@ impl<'a> KnownArgumentNames<'a> { " Did you mean", self.current_args .iter() - .map(|(args, _)| args.iter().map(|arg| *arg.0)) + .map(|(args, _)| args.iter().map(|arg| arg.0.as_str())) .flatten(), name, ) diff --git a/src/validation/rules/mod.rs b/src/validation/rules/mod.rs index e36f0391..b37d55f0 100644 --- a/src/validation/rules/mod.rs +++ b/src/validation/rules/mod.rs @@ -11,6 +11,7 @@ mod no_fragment_cycles; mod no_undefined_variables; mod no_unused_fragments; mod no_unused_variables; +mod oneof_fields_have_exactly_one_argument; mod overlapping_fields_can_be_merged; mod possible_fragment_spreads; mod provided_non_null_arguments; @@ -34,6 +35,7 @@ pub use no_fragment_cycles::NoFragmentCycles; pub use no_undefined_variables::NoUndefinedVariables; pub use no_unused_fragments::NoUnusedFragments; pub use no_unused_variables::NoUnusedVariables; +pub use oneof_fields_have_exactly_one_argument::OneofFieldsHaveExactlyOneArgument; pub use overlapping_fields_can_be_merged::OverlappingFieldsCanBeMerged; pub use possible_fragment_spreads::PossibleFragmentSpreads; pub use provided_non_null_arguments::ProvidedNonNullArguments; diff --git a/src/validation/rules/oneof_fields_have_exactly_one_argument.rs b/src/validation/rules/oneof_fields_have_exactly_one_argument.rs new file mode 100644 index 00000000..8a872cb7 --- /dev/null +++ b/src/validation/rules/oneof_fields_have_exactly_one_argument.rs @@ -0,0 +1,66 @@ +use crate::validation::visitor::{RuleError, Visitor}; +use crate::VisitorContext; +use async_graphql_parser::types::Field; +use async_graphql_parser::Positioned; + +pub struct OneofFieldsHaveExactlyOneArgument; + +impl<'a> Visitor<'a> for OneofFieldsHaveExactlyOneArgument { + fn enter_field(&mut self, ctx: &mut VisitorContext<'a>, field: &'a Positioned) { + if let Some(parent_type) = ctx.parent_type() { + if let Some(field_def) = parent_type + .fields() + .and_then(|fields| fields.get(field.node.name.node.as_str())) + { + if field_def.oneof && field.node.arguments.len() != 1 { + ctx.errors.push(RuleError::new( + vec![field.pos], + "Oneof fields requires have exactly one argument".to_string(), + )); + } + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn factory() -> OneofFieldsHaveExactlyOneArgument { + OneofFieldsHaveExactlyOneArgument + } + + #[test] + fn oneof_field() { + expect_passes_rule!( + factory, + r#" + query Foo { + oneofField(a: 10) + } + "#, + ); + } + + #[test] + fn oneof_not_exactly_one_argument() { + expect_fails_rule!( + factory, + r#" + query Foo { + oneofField(a: 10, b: "abc") + } + "#, + ); + + expect_fails_rule!( + factory, + r#" + query Foo { + oneofField + } + "#, + ); + } +} diff --git a/src/validation/suggestion.rs b/src/validation/suggestion.rs index a935d898..6e04566c 100644 --- a/src/validation/suggestion.rs +++ b/src/validation/suggestion.rs @@ -18,18 +18,20 @@ fn levenshtein_distance(s1: &str, s2: &str) -> usize { column[s1.len()] } -pub fn make_suggestion<'a, I>(prefix: &str, options: I, input: &str) -> Option +pub fn make_suggestion(prefix: &str, options: I, input: &str) -> Option where - I: Iterator, + I: IntoIterator, + A: AsRef, { let mut selected = Vec::new(); let mut distances = HashMap::new(); for opt in options { - let distance = levenshtein_distance(input, opt); + let opt = opt.as_ref().to_string(); + let distance = levenshtein_distance(input, &opt); let threshold = (input.len() / 2).max((opt.len() / 2).max(1)); if distance < threshold { - selected.push(opt); + selected.push(opt.clone()); distances.insert(opt, distance); } } diff --git a/src/validation/test_harness.rs b/src/validation/test_harness.rs index 5f97f505..fedb2d81 100644 --- a/src/validation/test_harness.rs +++ b/src/validation/test_harness.rs @@ -288,6 +288,13 @@ impl ComplicatedArgs { } } +#[derive(OneofObject)] +#[graphql(internal)] +enum OneofArg { + A(i32), + B(String), +} + pub struct Query; #[Object(internal)] @@ -335,6 +342,19 @@ impl Query { async fn complicated_args(&self) -> Option { unimplemented!() } + + async fn oneof_arg(&self, arg: OneofArg) -> String { + unimplemented!() + } + + async fn oneof_opt(&self, arg: Option) -> String { + unimplemented!() + } + + #[graphql(oneof)] + async fn oneof_field(&self, arg: OneofArg) -> String { + unimplemented!() + } } pub struct Mutation; diff --git a/src/validation/utils.rs b/src/validation/utils.rs index 5f1051c9..0c1214a4 100644 --- a/src/validation/utils.rs +++ b/src/validation/utils.rs @@ -120,9 +120,17 @@ pub fn is_valid_input_value( registry::MetaType::InputObject { input_fields, name: object_name, + oneof, .. } => match value { ConstValue::Object(values) => { + if *oneof && values.len() != 1 { + return Some(valid_error( + &path_node, + "Oneof input objects requires have exactly one field".to_string(), + )); + } + let mut input_names = values.keys().map(AsRef::as_ref).collect::>(); diff --git a/tests/complex_object.rs b/tests/complex_object.rs index 556599c2..f9e3d90f 100644 --- a/tests/complex_object.rs +++ b/tests/complex_object.rs @@ -492,3 +492,87 @@ async fn test_flatten_with_result() { }) ); } + +#[tokio::test] +async fn test_oneof_field() { + #[derive(OneofObject)] + enum TestArg { + A(i32), + B(String), + } + + #[derive(SimpleObject)] + #[graphql(complex)] + struct Query { + a: i32, + } + + #[ComplexObject] + impl Query { + #[graphql(oneof)] + async fn test(&self, arg: TestArg) -> String { + match arg { + TestArg::A(a) => format!("a:{}", a), + TestArg::B(b) => format!("b:{}", b), + } + } + } + + let schema = Schema::new(Query { a: 10 }, EmptyMutation, EmptySubscription); + let query = "{ test(a: 10) }"; + assert_eq!( + schema.execute(query).await.into_result().unwrap().data, + value!({ + "test": "a:10" + }) + ); + + let query = r#"{ test(b: "abc") }"#; + assert_eq!( + schema.execute(query).await.into_result().unwrap().data, + value!({ + "test": "b:abc" + }) + ); + + let query = r#"{ + __type(name: "Query") { + fields { + name + args { + name + type { + kind + name + } + } + } + } + }"#; + assert_eq!( + schema.execute(query).await.into_result().unwrap().data, + value!({ + "__type": { + "fields": [{ + "name": "a", + "args": [] + }, { + "name": "test", + "args": [{ + "name": "a", + "type": { + "kind": "SCALAR", + "name": "Int" + } + }, { + "name": "b", + "type": { + "kind": "SCALAR", + "name": "String" + } + }] + }] + } + }) + ); +} diff --git a/tests/interface.rs b/tests/interface.rs index 6d43a71b..b57b1118 100644 --- a/tests/interface.rs +++ b/tests/interface.rs @@ -473,3 +473,118 @@ pub async fn test_issue_330() { }) ); } + +#[tokio::test] +pub async fn test_oneof() { + #[derive(OneofObject)] + enum TestArg { + A(i32), + B(String), + } + + struct A; + + #[Object] + impl A { + async fn test(&self, arg: TestArg) -> String { + match arg { + TestArg::A(a) => format!("A:a:{}", a), + TestArg::B(b) => format!("A:b:{}", b), + } + } + } + + struct B; + + #[Object] + impl B { + async fn test(&self, arg: TestArg) -> String { + match arg { + TestArg::A(a) => format!("B:a:{}", a), + TestArg::B(b) => format!("B:b:{}", b), + } + } + } + + #[derive(Interface)] + #[graphql(field( + name = "test", + type = "String", + oneof, + arg(name = "arg", type = "TestArg") + ))] + enum Obj { + A(A), + B(B), + } + + struct Query; + + #[Object] + impl Query { + async fn a(&self) -> Obj { + A.into() + } + + async fn b(&self) -> Obj { + B.into() + } + } + + let schema = Schema::new(Query, EmptyMutation, EmptySubscription); + assert_eq!( + schema + .execute("{ a { test(a: 10) } }") + .await + .into_result() + .unwrap() + .data, + value!({ + "a": { + "test": "A:a:10" + } + }) + ); + + assert_eq!( + schema + .execute("{ a { test(b: \"abc\") } }") + .await + .into_result() + .unwrap() + .data, + value!({ + "a": { + "test": "A:b:abc" + } + }) + ); + + assert_eq!( + schema + .execute("{ b { test(a: 10) } }") + .await + .into_result() + .unwrap() + .data, + value!({ + "b": { + "test": "B:a:10" + } + }) + ); + + assert_eq!( + schema + .execute("{ b { test(b: \"def\") } }") + .await + .into_result() + .unwrap() + .data, + value!({ + "b": { + "test": "B:b:def" + } + }) + ); +} diff --git a/tests/object.rs b/tests/object.rs index e11d740e..71377b15 100644 --- a/tests/object.rs +++ b/tests/object.rs @@ -117,3 +117,80 @@ async fn test_flatten_with_context() { }) ); } + +#[tokio::test] +async fn test_oneof_field() { + #[derive(OneofObject)] + enum TestArg { + A(i32), + B(String), + } + + struct Query; + + #[Object] + impl Query { + #[graphql(oneof)] + async fn test(&self, arg: TestArg) -> String { + match arg { + TestArg::A(a) => format!("a:{}", a), + TestArg::B(b) => format!("b:{}", b), + } + } + } + + let schema = Schema::new(Query, EmptyMutation, EmptySubscription); + let query = "{ test(a: 10) }"; + assert_eq!( + schema.execute(query).await.into_result().unwrap().data, + value!({ + "test": "a:10" + }) + ); + + let query = r#"{ test(b: "abc") }"#; + assert_eq!( + schema.execute(query).await.into_result().unwrap().data, + value!({ + "test": "b:abc" + }) + ); + + let query = r#"{ + __type(name: "Query") { + fields { + name + args { + name + type { + kind + name + } + } + } + } + }"#; + assert_eq!( + schema.execute(query).await.into_result().unwrap().data, + value!({ + "__type": { + "fields": [{ + "name": "test", + "args": [{ + "name": "a", + "type": { + "kind": "SCALAR", + "name": "Int" + } + }, { + "name": "b", + "type": { + "kind": "SCALAR", + "name": "String" + } + }] + }] + } + }) + ); +} diff --git a/tests/subscription.rs b/tests/subscription.rs index 4fa494b0..9efd3956 100644 --- a/tests/subscription.rs +++ b/tests/subscription.rs @@ -425,3 +425,46 @@ pub async fn test_subscription_fieldresult() { assert!(stream.next().await.is_none()); } + +#[tokio::test] +pub async fn test_oneof_field() { + #[derive(OneofObject)] + enum TestArg { + A(i32), + B(String), + } + + struct Subscription; + + #[Subscription] + impl Subscription { + #[graphql(oneof)] + async fn test(&self, arg: TestArg) -> impl Stream { + let value = match arg { + TestArg::A(a) => format!("a:{}", a), + TestArg::B(b) => format!("b:{}", b), + }; + futures_util::stream::once(async move { value }) + } + } + + let schema = Schema::new(Query, EmptyMutation, Subscription); + + let mut stream = schema.execute_stream(r#"subscription { test(a: 10) }"#); + assert_eq!( + Response::new(value!({ + "test": "a:10" + })), + stream.next().await.unwrap() + ); + assert!(stream.next().await.is_none()); + + let mut stream = schema.execute_stream(r#"subscription { test(b: "abc") }"#); + assert_eq!( + Response::new(value!({ + "test": "b:abc" + })), + stream.next().await.unwrap() + ); + assert!(stream.next().await.is_none()); +} diff --git a/tests/validators.rs b/tests/validators.rs index 655e4aed..9bc2317a 100644 --- a/tests/validators.rs +++ b/tests/validators.rs @@ -789,3 +789,103 @@ pub async fn test_list_both_max_items_and_max_length() { }) ); } + +#[tokio::test] +pub async fn test_custom_validator_on_oneof_field() { + struct MyValidator; + + impl CustomValidator for MyValidator { + fn check(&self, value: &TestArg) -> Result<(), String> { + match value { + TestArg::A(a) => { + if *a < 100 { + Ok(()) + } else { + Err("invalid a".into()) + } + } + TestArg::B(b) => { + if b.len() < 5 { + Ok(()) + } else { + Err("invalid b".into()) + } + } + } + } + } + + #[derive(OneofObject)] + enum TestArg { + A(i32), + B(String), + } + + struct Query; + + #[Object] + impl Query { + #[graphql(oneof)] + async fn test(&self, #[graphql(validator(custom = "MyValidator"))] arg: TestArg) -> String { + match arg { + TestArg::A(a) => format!("a:{}", a), + TestArg::B(b) => format!("b:{}", b), + } + } + } + + let schema = Schema::new(Query, EmptyMutation, EmptySubscription); + assert_eq!( + schema + .execute("{ test(a: 10) }") + .await + .into_result() + .unwrap() + .data, + value!({ + "test": "a:10" + }) + ); + + assert_eq!( + schema + .execute("{ test(a: 200) }") + .await + .into_result() + .unwrap_err(), + vec![ServerError { + message: r#"Failed to parse "TestArg": invalid a"#.to_string(), + source: None, + locations: vec![Pos { column: 3, line: 1 }], + path: vec![PathSegment::Field("test".to_string())], + extensions: None + }] + ); + + assert_eq!( + schema + .execute(r#"{ test(b: "abcd") }"#) + .await + .into_result() + .unwrap() + .data, + value!({ + "test": "b:abcd" + }) + ); + + assert_eq!( + schema + .execute(r#"{ test(b: "abcde") }"#) + .await + .into_result() + .unwrap_err(), + vec![ServerError { + message: r#"Failed to parse "TestArg": invalid b"#.to_string(), + source: None, + locations: vec![Pos { column: 3, line: 1 }], + path: vec![PathSegment::Field("test".to_string())], + extensions: None + }] + ); +} From ef39dde9ceaa9c706e2cf7ee3a52e141328d3ea8 Mon Sep 17 00:00:00 2001 From: Sunli Date: Fri, 4 Mar 2022 21:37:15 +0800 Subject: [PATCH 14/24] Update feature-comparison.md --- feature-comparison.md | 54 ++++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/feature-comparison.md b/feature-comparison.md index 952d2961..14d2cb16 100644 --- a/feature-comparison.md +++ b/feature-comparison.md @@ -4,29 +4,31 @@ Comparing Features of Other Rust GraphQL Implementations **Please let me know if there is anything wrong.** -| | async-graphql | juniper(0.15.1) | -|----------------|---------------|-----------------| -| async/await | 👍 | 👍️ | -| Rustfmt friendly(No DSL) | 👍 | ⛔️ | -| Boilerplate | Less | Some | -| Type Safety | 👍 | 👍 | -| Query | 👍 | 👍 | -| Mutation | 👍 | 👍 | -| Interfaces | 👍 | 👍 | -| Union | 👍 | 👍 | -| Dataloading | 👍 | 👍 | -| Custom Scalar | 👍 | 👍 | -| Custom Error | 👍 | 👍 | -| Custom Directive | 👍 | ⛔ | -| Extensions | 👍 | ⛔️ | -| Cursor Connections | 👍 | ⛔️ | -| Query complexity/depth | 👍 | ⛔️ | -| Input validators | 👍 | ⛔️ | -| Field guard | 👍 | ⛔️ | -| Multipart request(upload file) | 👍 | ⛔️ | -| Subscription | 👍 | 👍️ | -| Opentracing | 👍 | ⛔️ | -| Apollo Federation | 👍 | ⛔️ | -| Apollo Tracing | 👍 | ⛔️ | -| Apollo Persisted Queries | 👍 | ⛔️ | -| Disabling introspection | 👍 | ⛔️ | +| | async-graphql | juniper(0.15.1) | +|------------------------------------------------------------------------|---------------|-----------------| +| async/await | 👍 | 👍️ | +| Rustfmt friendly(No DSL) | 👍 | ⛔️ | +| Boilerplate | Less | Some | +| Type Safety | 👍 | 👍 | +| Query | 👍 | 👍 | +| Mutation | 👍 | 👍 | +| Interfaces | 👍 | 👍 | +| Union | 👍 | 👍 | +| Dataloading | 👍 | 👍 | +| Custom Scalar | 👍 | 👍 | +| Custom Error | 👍 | 👍 | +| Custom Directive | 👍 | ⛔ | +| [Oneof Input Object](https://github.com/graphql/graphql-spec/pull/825) | 👍 | ⛔ | +| [Oneof Field](https://github.com/graphql/graphql-spec/pull/825) | 👍 | ⛔ | +| Extensions | 👍 | ⛔️ | +| Cursor Connections | 👍 | ⛔️ | +| Query complexity/depth | 👍 | ⛔️ | +| Input validators | 👍 | ⛔️ | +| Field guard | 👍 | ⛔️ | +| Multipart request(upload file) | 👍 | ⛔️ | +| Subscription | 👍 | 👍️ | +| Opentracing | 👍 | ⛔️ | +| Apollo Federation | 👍 | ⛔️ | +| Apollo Tracing | 👍 | ⛔️ | +| Apollo Persisted Queries | 👍 | ⛔️ | +| Disabling introspection | 👍 | ⛔️ | From 8bfad214034ab995bfa21aa8ea6850e3eb92adbb Mon Sep 17 00:00:00 2001 From: Sunli Date: Fri, 4 Mar 2022 21:38:13 +0800 Subject: [PATCH 15/24] Release 3.0.33 async-graphql@3.0.33 async-graphql-actix-web@3.0.33 async-graphql-axum@3.0.33 async-graphql-derive@3.0.33 async-graphql-parser@3.0.33 async-graphql-poem@3.0.33 async-graphql-rocket@3.0.33 async-graphql-tide@3.0.33 async-graphql-value@3.0.33 async-graphql-warp@3.0.33 Generated by cargo-workspaces --- Cargo.toml | 8 ++++---- derive/Cargo.toml | 4 ++-- integrations/actix-web/Cargo.toml | 4 ++-- integrations/axum/Cargo.toml | 4 ++-- integrations/poem/Cargo.toml | 4 ++-- integrations/rocket/Cargo.toml | 4 ++-- integrations/tide/Cargo.toml | 4 ++-- integrations/warp/Cargo.toml | 4 ++-- parser/Cargo.toml | 4 ++-- value/Cargo.toml | 2 +- 10 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 95ba08b8..c9093c16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql" -version = "3.0.32" +version = "3.0.33" authors = ["sunli ", "Koxiaet"] edition = "2021" description = "A GraphQL server library implemented in Rust" @@ -26,9 +26,9 @@ chrono-duration = ["chrono", "iso8601-duration"] password-strength-validator = ["zxcvbn"] [dependencies] -async-graphql-derive = { path = "derive", version = "3.0.32" } -async-graphql-value = { path = "value", version = "3.0.32" } -async-graphql-parser = { path = "parser", version = "3.0.32" } +async-graphql-derive = { path = "derive", version = "3.0.33" } +async-graphql-value = { path = "value", version = "3.0.33" } +async-graphql-parser = { path = "parser", version = "3.0.33" } async-stream = "0.3.0" async-trait = "0.1.48" diff --git a/derive/Cargo.toml b/derive/Cargo.toml index c31188a9..2d180806 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-derive" -version = "3.0.32" +version = "3.0.33" authors = ["sunli ", "Koxiaet"] edition = "2021" description = "Macros for async-graphql" @@ -15,7 +15,7 @@ categories = ["network-programming", "asynchronous"] proc-macro = true [dependencies] -async-graphql-parser = { path = "../parser", version = "3.0.32" } +async-graphql-parser = { path = "../parser", version = "3.0.33" } proc-macro2 = "1.0.24" syn = { version = "1.0.64", features = ["full", "extra-traits", "visit-mut", "visit"] } quote = "1.0.9" diff --git a/integrations/actix-web/Cargo.toml b/integrations/actix-web/Cargo.toml index 0d570a8f..3a452c3b 100644 --- a/integrations/actix-web/Cargo.toml +++ b/integrations/actix-web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-actix-web" -version = "3.0.32" +version = "3.0.33" authors = ["sunli ", "Koxiaet"] edition = "2021" description = "async-graphql for actix-web" @@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "3.0.32" } +async-graphql = { path = "../..", version = "3.0.33" } actix = "0.13.0" actix-http = "3.0.0" diff --git a/integrations/axum/Cargo.toml b/integrations/axum/Cargo.toml index 3ea21dd6..967d560a 100644 --- a/integrations/axum/Cargo.toml +++ b/integrations/axum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-axum" -version = "3.0.32" +version = "3.0.33" authors = ["sunli "] edition = "2021" description = "async-graphql for axum" @@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql", "axum"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "3.0.32" } +async-graphql = { path = "../..", version = "3.0.33" } async-trait = "0.1.51" axum = { version = "0.4", features = ["ws", "headers"] } diff --git a/integrations/poem/Cargo.toml b/integrations/poem/Cargo.toml index 173dd849..15a27920 100644 --- a/integrations/poem/Cargo.toml +++ b/integrations/poem/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-poem" -version = "3.0.32" +version = "3.0.33" authors = ["sunli "] edition = "2021" description = "async-graphql for poem" @@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql", "poem"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "3.0.32" } +async-graphql = { path = "../..", version = "3.0.33" } poem = { version = "1.2.2", features = ["websocket"] } futures-util = { version = "0.3.0", default-features = false } diff --git a/integrations/rocket/Cargo.toml b/integrations/rocket/Cargo.toml index 25a4c63a..0179110c 100644 --- a/integrations/rocket/Cargo.toml +++ b/integrations/rocket/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-rocket" -version = "3.0.32" +version = "3.0.33" authors = ["Daniel Wiesenberg "] edition = "2021" description = "async-graphql for Rocket.rs" @@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql", "rocket"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "3.0.32" } +async-graphql = { path = "../..", version = "3.0.33" } rocket = { version = "0.5.0-rc.1", default-features = false } serde = "1.0.126" diff --git a/integrations/tide/Cargo.toml b/integrations/tide/Cargo.toml index 4fadf020..e622d366 100644 --- a/integrations/tide/Cargo.toml +++ b/integrations/tide/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-tide" -version = "3.0.32" +version = "3.0.33" authors = ["vkill ", "sunli "] edition = "2021" description = "async-graphql for tide" @@ -16,7 +16,7 @@ default = ["websocket"] websocket = ["tide-websockets"] [dependencies] -async-graphql = { path = "../..", version = "3.0.32" } +async-graphql = { path = "../..", version = "3.0.33" } async-trait = "0.1.48" futures-util = "0.3.0" diff --git a/integrations/warp/Cargo.toml b/integrations/warp/Cargo.toml index c35ae9cc..be53ac66 100644 --- a/integrations/warp/Cargo.toml +++ b/integrations/warp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-warp" -version = "3.0.32" +version = "3.0.33" authors = ["sunli ", "Koxiaet"] edition = "2021" description = "async-graphql for warp" @@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "3.0.32" } +async-graphql = { path = "../..", version = "3.0.33" } warp = { version = "0.3.0", default-features = false, features = ["websocket"] } futures-util = { version = "0.3.0", default-features = false, features = ["sink"] } diff --git a/parser/Cargo.toml b/parser/Cargo.toml index c114393d..87bf9ded 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-parser" -version = "3.0.32" +version = "3.0.33" authors = ["sunli ", "Koxiaet"] edition = "2021" description = "GraphQL query parser for async-graphql" @@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql-value = { path = "../value", version = "3.0.32" } +async-graphql-value = { path = "../value", version = "3.0.33" } pest = "2.1.3" pest_derive = "2.1.0" serde_json = "1.0.64" diff --git a/value/Cargo.toml b/value/Cargo.toml index 85870879..9cfb1fee 100644 --- a/value/Cargo.toml +++ b/value/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-value" -version = "3.0.32" +version = "3.0.33" authors = ["sunli ", "Koxiaet"] edition = "2021" description = "GraphQL value for async-graphql" From abf4f4155cdd98c74fe36f8753ef4368f8a0746d Mon Sep 17 00:00:00 2001 From: gho1b Date: Sat, 5 Mar 2022 01:50:39 +0700 Subject: [PATCH 16/24] Update Integration Actix Web --- integrations/actix-web/Cargo.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/integrations/actix-web/Cargo.toml b/integrations/actix-web/Cargo.toml index 3c6160a1..a3450d9c 100644 --- a/integrations/actix-web/Cargo.toml +++ b/integrations/actix-web/Cargo.toml @@ -14,10 +14,10 @@ categories = ["network-programming", "asynchronous"] [dependencies] async-graphql = { path = "../..", version = "3.0.26" } -actix = "0.12.0" -actix-http = "3.0.0-beta.17" -actix-web = { version = "4.0.0-beta.19", default-features = false } -actix-web-actors = "4.0.0-beta.9" +actix = "0.13.0" +actix-http = "3.0.1" +actix-web = { version = "4.0.1", default-features = false } +actix-web-actors = "4.1.0" async-channel = "1.6.1" futures-util = { version = "0.3.0", default-features = false } serde_json = "1.0.64" From 5dad855f8ee734ade581c63ecee9c1b90da9b71d Mon Sep 17 00:00:00 2001 From: Sunli Date: Sat, 5 Mar 2022 09:06:59 +0800 Subject: [PATCH 17/24] Export `@oneOf` directive to SDL when Oneof type is defined. #766 --- CHANGELOG.md | 4 ++ src/docs/object.md | 37 +++++++++++++- src/docs/oneof_object.md | 1 - src/registry/export_sdl.rs | 11 +++++ .../oneof_fields_have_exactly_one_argument.rs | 48 ++++++++++++++++--- src/validation/utils.rs | 21 ++++++-- 6 files changed, 108 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1757f3be..ea87be5f 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.34] 2022-3-5 + +- Export `@oneOf` directive to SDL when Oneof type is defined. [#766](https://github.com/async-graphql/async-graphql/issues/766) + # [3.0.33] 2022-3-4 - Add support for oneof field on object. [#766](https://github.com/async-graphql/async-graphql/issues/766) diff --git a/src/docs/object.md b/src/docs/object.md index c93953fa..e7fd6d9f 100644 --- a/src/docs/object.md +++ b/src/docs/object.md @@ -189,4 +189,39 @@ assert_eq!(res, value!({ ] })); # }); -``` \ No newline at end of file +``` + +# Oneof field + +```rust +use async_graphql::*; + +#[derive(OneofObject)] +enum MyInputObject { + A(i32), + B(String), +} + +struct Query; + +#[Object] +impl Query { + #[graphql(oneof)] + async fn value(&self, input: MyInputObject) -> String { + match input { + MyInputObject::A(value) => format!("a:{}", value), + MyInputObject::B(value) => format!("b:{}", value), + } + } +} + +# tokio::runtime::Runtime::new().unwrap().block_on(async move { +let schema = Schema::new(Query, EmptyMutation, EmptySubscription); +let res = schema.execute(r#" +{ + value1: value(a:100) + value2: value(b:"abc") +}"#).await.into_result().unwrap().data; +assert_eq!(res, value!({ "value1": "a:100", "value2": "b:abc" })); +# }); +``` diff --git a/src/docs/oneof_object.md b/src/docs/oneof_object.md index 2624e7b0..4534ad36 100644 --- a/src/docs/oneof_object.md +++ b/src/docs/oneof_object.md @@ -35,7 +35,6 @@ struct Query; #[Object] impl Query { - /// value async fn value(&self, input: MyInputObject) -> String { match input { MyInputObject::A(value) => format!("a:{}", value), diff --git a/src/registry/export_sdl.rs b/src/registry/export_sdl.rs index b19153bb..4bbd909c 100644 --- a/src/registry/export_sdl.rs +++ b/src/registry/export_sdl.rs @@ -6,6 +6,17 @@ impl Registry { pub fn export_sdl(&self, federation: bool) -> String { let mut sdl = String::new(); + let has_oneof = self.types.values().any(|ty| match ty { + MetaType::InputObject { oneof: true, .. } => true, + MetaType::Object { fields, .. } => fields.values().any(|field| field.oneof), + _ => false, + }); + + if has_oneof { + sdl.write_str("directive @oneOf on INPUT_OBJECT | FIELD_DEFINITION\n\n") + .ok(); + } + for ty in self.types.values() { if ty.name().starts_with("__") { continue; diff --git a/src/validation/rules/oneof_fields_have_exactly_one_argument.rs b/src/validation/rules/oneof_fields_have_exactly_one_argument.rs index 8a872cb7..eaeb69d0 100644 --- a/src/validation/rules/oneof_fields_have_exactly_one_argument.rs +++ b/src/validation/rules/oneof_fields_have_exactly_one_argument.rs @@ -1,8 +1,9 @@ -use crate::validation::visitor::{RuleError, Visitor}; -use crate::VisitorContext; use async_graphql_parser::types::Field; use async_graphql_parser::Positioned; +use crate::validation::visitor::{RuleError, Visitor}; +use crate::{Value, VisitorContext}; + pub struct OneofFieldsHaveExactlyOneArgument; impl<'a> Visitor<'a> for OneofFieldsHaveExactlyOneArgument { @@ -12,11 +13,32 @@ impl<'a> Visitor<'a> for OneofFieldsHaveExactlyOneArgument { .fields() .and_then(|fields| fields.get(field.node.name.node.as_str())) { - if field_def.oneof && field.node.arguments.len() != 1 { - ctx.errors.push(RuleError::new( - vec![field.pos], - "Oneof fields requires have exactly one argument".to_string(), - )); + if field_def.oneof { + if field.node.arguments.len() != 1 { + ctx.errors.push(RuleError::new( + vec![field.pos], + "Oneof fields requires have exactly one argument".to_string(), + )); + return; + } + + let value = field.node.arguments[0] + .1 + .node + .clone() + .into_const_with(|var_name| { + ctx.variables + .and_then(|variables| variables.get(&var_name)) + .map(Clone::clone) + .ok_or(()) + }) + .ok(); + if let Some(Value::Null) = value { + ctx.errors.push(RuleError::new( + vec![field.pos], + "Oneof Fields require that exactly one argument must be supplied and that argument must not be null".to_string(), + )); + } } } } @@ -63,4 +85,16 @@ mod tests { "#, ); } + + #[test] + fn oneof_arguments_not_be_null() { + expect_fails_rule!( + factory, + r#" + query Foo { + oneofField(a: null) + } + "#, + ); + } } diff --git a/src/validation/utils.rs b/src/validation/utils.rs index 0c1214a4..59bef875 100644 --- a/src/validation/utils.rs +++ b/src/validation/utils.rs @@ -124,11 +124,22 @@ pub fn is_valid_input_value( .. } => match value { ConstValue::Object(values) => { - if *oneof && values.len() != 1 { - return Some(valid_error( - &path_node, - "Oneof input objects requires have exactly one field".to_string(), - )); + if *oneof { + if values.len() != 1 { + return Some(valid_error( + &path_node, + "Oneof input objects requires have exactly one field" + .to_string(), + )); + } + + if let ConstValue::Null = values[0] { + return Some(valid_error( + &path_node, + "Oneof Input Objects require that exactly one field must be supplied and that field must not be null" + .to_string(), + )); + } } let mut input_names = From 95abc178428652c5fd11b3d27366a288b3ec55af Mon Sep 17 00:00:00 2001 From: Sunli Date: Sat, 5 Mar 2022 09:07:41 +0800 Subject: [PATCH 18/24] Release 3.0.34 async-graphql@3.0.34 async-graphql-actix-web@3.0.34 async-graphql-axum@3.0.34 async-graphql-derive@3.0.34 async-graphql-parser@3.0.34 async-graphql-poem@3.0.34 async-graphql-rocket@3.0.34 async-graphql-tide@3.0.34 async-graphql-value@3.0.34 async-graphql-warp@3.0.34 Generated by cargo-workspaces --- Cargo.toml | 8 ++++---- derive/Cargo.toml | 4 ++-- integrations/actix-web/Cargo.toml | 4 ++-- integrations/axum/Cargo.toml | 4 ++-- integrations/poem/Cargo.toml | 4 ++-- integrations/rocket/Cargo.toml | 4 ++-- integrations/tide/Cargo.toml | 4 ++-- integrations/warp/Cargo.toml | 4 ++-- parser/Cargo.toml | 4 ++-- value/Cargo.toml | 2 +- 10 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c9093c16..8177c0ef 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql" -version = "3.0.33" +version = "3.0.34" authors = ["sunli ", "Koxiaet"] edition = "2021" description = "A GraphQL server library implemented in Rust" @@ -26,9 +26,9 @@ chrono-duration = ["chrono", "iso8601-duration"] password-strength-validator = ["zxcvbn"] [dependencies] -async-graphql-derive = { path = "derive", version = "3.0.33" } -async-graphql-value = { path = "value", version = "3.0.33" } -async-graphql-parser = { path = "parser", version = "3.0.33" } +async-graphql-derive = { path = "derive", version = "3.0.34" } +async-graphql-value = { path = "value", version = "3.0.34" } +async-graphql-parser = { path = "parser", version = "3.0.34" } async-stream = "0.3.0" async-trait = "0.1.48" diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 2d180806..5409e7ce 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-derive" -version = "3.0.33" +version = "3.0.34" authors = ["sunli ", "Koxiaet"] edition = "2021" description = "Macros for async-graphql" @@ -15,7 +15,7 @@ categories = ["network-programming", "asynchronous"] proc-macro = true [dependencies] -async-graphql-parser = { path = "../parser", version = "3.0.33" } +async-graphql-parser = { path = "../parser", version = "3.0.34" } proc-macro2 = "1.0.24" syn = { version = "1.0.64", features = ["full", "extra-traits", "visit-mut", "visit"] } quote = "1.0.9" diff --git a/integrations/actix-web/Cargo.toml b/integrations/actix-web/Cargo.toml index 3a452c3b..867bbb74 100644 --- a/integrations/actix-web/Cargo.toml +++ b/integrations/actix-web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-actix-web" -version = "3.0.33" +version = "3.0.34" authors = ["sunli ", "Koxiaet"] edition = "2021" description = "async-graphql for actix-web" @@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "3.0.33" } +async-graphql = { path = "../..", version = "3.0.34" } actix = "0.13.0" actix-http = "3.0.0" diff --git a/integrations/axum/Cargo.toml b/integrations/axum/Cargo.toml index 967d560a..0fe51129 100644 --- a/integrations/axum/Cargo.toml +++ b/integrations/axum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-axum" -version = "3.0.33" +version = "3.0.34" authors = ["sunli "] edition = "2021" description = "async-graphql for axum" @@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql", "axum"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "3.0.33" } +async-graphql = { path = "../..", version = "3.0.34" } async-trait = "0.1.51" axum = { version = "0.4", features = ["ws", "headers"] } diff --git a/integrations/poem/Cargo.toml b/integrations/poem/Cargo.toml index 15a27920..13e530ff 100644 --- a/integrations/poem/Cargo.toml +++ b/integrations/poem/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-poem" -version = "3.0.33" +version = "3.0.34" authors = ["sunli "] edition = "2021" description = "async-graphql for poem" @@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql", "poem"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "3.0.33" } +async-graphql = { path = "../..", version = "3.0.34" } poem = { version = "1.2.2", features = ["websocket"] } futures-util = { version = "0.3.0", default-features = false } diff --git a/integrations/rocket/Cargo.toml b/integrations/rocket/Cargo.toml index 0179110c..83528b11 100644 --- a/integrations/rocket/Cargo.toml +++ b/integrations/rocket/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-rocket" -version = "3.0.33" +version = "3.0.34" authors = ["Daniel Wiesenberg "] edition = "2021" description = "async-graphql for Rocket.rs" @@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql", "rocket"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "3.0.33" } +async-graphql = { path = "../..", version = "3.0.34" } rocket = { version = "0.5.0-rc.1", default-features = false } serde = "1.0.126" diff --git a/integrations/tide/Cargo.toml b/integrations/tide/Cargo.toml index e622d366..f66b686a 100644 --- a/integrations/tide/Cargo.toml +++ b/integrations/tide/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-tide" -version = "3.0.33" +version = "3.0.34" authors = ["vkill ", "sunli "] edition = "2021" description = "async-graphql for tide" @@ -16,7 +16,7 @@ default = ["websocket"] websocket = ["tide-websockets"] [dependencies] -async-graphql = { path = "../..", version = "3.0.33" } +async-graphql = { path = "../..", version = "3.0.34" } async-trait = "0.1.48" futures-util = "0.3.0" diff --git a/integrations/warp/Cargo.toml b/integrations/warp/Cargo.toml index be53ac66..3e4bfa31 100644 --- a/integrations/warp/Cargo.toml +++ b/integrations/warp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-warp" -version = "3.0.33" +version = "3.0.34" authors = ["sunli ", "Koxiaet"] edition = "2021" description = "async-graphql for warp" @@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "3.0.33" } +async-graphql = { path = "../..", version = "3.0.34" } warp = { version = "0.3.0", default-features = false, features = ["websocket"] } futures-util = { version = "0.3.0", default-features = false, features = ["sink"] } diff --git a/parser/Cargo.toml b/parser/Cargo.toml index 87bf9ded..2113d10f 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-parser" -version = "3.0.33" +version = "3.0.34" authors = ["sunli ", "Koxiaet"] edition = "2021" description = "GraphQL query parser for async-graphql" @@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql-value = { path = "../value", version = "3.0.33" } +async-graphql-value = { path = "../value", version = "3.0.34" } pest = "2.1.3" pest_derive = "2.1.0" serde_json = "1.0.64" diff --git a/value/Cargo.toml b/value/Cargo.toml index 9cfb1fee..102ef8a1 100644 --- a/value/Cargo.toml +++ b/value/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-value" -version = "3.0.33" +version = "3.0.34" authors = ["sunli ", "Koxiaet"] edition = "2021" description = "GraphQL value for async-graphql" From f0f18042148373864c4ab9d2781174637627575b Mon Sep 17 00:00:00 2001 From: BratSinot Date: Fri, 11 Mar 2022 10:47:32 +0200 Subject: [PATCH 19/24] Make HashMap more generic. --- CHANGELOG.md | 4 ++++ src/types/external/json_object/hashmap.rs | 8 +++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea87be5f..b20b3af4 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.35] 2022-3-11 + +- Make `HashMap` more generics for `InputOutput` and `OutputType`. + # [3.0.34] 2022-3-5 - Export `@oneOf` directive to SDL when Oneof type is defined. [#766](https://github.com/async-graphql/async-graphql/issues/766) diff --git a/src/types/external/json_object/hashmap.rs b/src/types/external/json_object/hashmap.rs index bbe1bf03..09f96450 100644 --- a/src/types/external/json_object/hashmap.rs +++ b/src/types/external/json_object/hashmap.rs @@ -1,7 +1,7 @@ use std::borrow::Cow; use std::collections::HashMap; use std::fmt::Display; -use std::hash::Hash; +use std::hash::{BuildHasher, Hash}; use std::str::FromStr; use async_graphql_parser::types::Field; @@ -17,11 +17,12 @@ use crate::{ ServerResult, Value, }; -impl InputType for HashMap +impl InputType for HashMap where K: ToString + FromStr + Eq + Hash + Send + Sync, K::Err: Display, V: Serialize + DeserializeOwned + Send + Sync, + S: Default + BuildHasher + Send + Sync, { type RawValueType = Self; @@ -75,10 +76,11 @@ where } #[async_trait::async_trait] -impl OutputType for HashMap +impl OutputType for HashMap where K: ToString + Eq + Hash + Send + Sync, V: Serialize + Send + Sync, + S: Send + Sync, { fn type_name() -> Cow<'static, str> { Cow::Borrowed("JSONObject") From 89097521072cca4142a20c9f9a4aa74358a01514 Mon Sep 17 00:00:00 2001 From: Sunli Date: Mon, 14 Mar 2022 09:19:27 +0800 Subject: [PATCH 20/24] 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)")] From 665f464448a6e6d028503978f0454ab5d59594f4 Mon Sep 17 00:00:00 2001 From: Sunli Date: Mon, 14 Mar 2022 09:46:06 +0800 Subject: [PATCH 21/24] Fixed recursive generic input objects failing to compile. #859 --- derive/src/input_object.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/derive/src/input_object.rs b/derive/src/input_object.rs index 70f5a934..6d545738 100644 --- a/derive/src/input_object.rs +++ b/derive/src/input_object.rs @@ -289,7 +289,7 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult #crate_name::Value::Object(map) } - fn __internal_federation_fields() -> ::std::option::Option<::std::string::String> { + fn __internal_federation_fields() -> ::std::option::Option<::std::string::String> where Self: #crate_name::InputType { #get_federation_fields } } @@ -336,5 +336,6 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult } quote!(#(#code)*) }; + Ok(expanded.into()) } From 2653dddc5bf134ee264f4286433e94f3f39a54f4 Mon Sep 17 00:00:00 2001 From: Sunli Date: Mon, 14 Mar 2022 09:48:27 +0800 Subject: [PATCH 22/24] Add `ErrorExtensionValues::get` method. #855 --- src/error.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/error.rs b/src/error.rs index 0edd55ec..bb154319 100644 --- a/src/error.rs +++ b/src/error.rs @@ -24,6 +24,11 @@ impl ErrorExtensionValues { pub fn unset(&mut self, name: impl AsRef) { self.0.remove(name.as_ref()); } + + /// Get an extension value. + pub fn get(&self, name: impl AsRef) -> Option<&Value> { + self.0.get(name.as_ref()) + } } /// An error in a GraphQL server. From 2bf314f61491fb4b42bb8eee5c06d5d7c88e05ee Mon Sep 17 00:00:00 2001 From: Sunli Date: Mon, 14 Mar 2022 09:54:27 +0800 Subject: [PATCH 23/24] Update CHANGELOG.md --- CHANGELOG.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4929c956..5bf7fad2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,13 +4,12 @@ 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 +# [3.0.35] 2022-3-14 - Make `HashMap` more generics for `InputOutput` and `OutputType`. +- Add support `group` attribute to Object/SimpleObject/ComplexObject/Subscription macros. [#838](https://github.com/async-graphql/async-graphql/issues/838) +- Fixed recursive generic input objects failing to compile. [#859](https://github.com/async-graphql/async-graphql/issues/859) +- Add `ErrorExtensionValues::get` method. [#855](https://github.com/async-graphql/async-graphql/issues/855) # [3.0.34] 2022-3-5 From 15f84c1d0033dd6148dab263d950e8fbd1873aa6 Mon Sep 17 00:00:00 2001 From: Sunli Date: Mon, 14 Mar 2022 11:28:31 +0800 Subject: [PATCH 24/24] Release 3.0.35 async-graphql@3.0.35 async-graphql-actix-web@3.0.35 async-graphql-axum@3.0.35 async-graphql-derive@3.0.35 async-graphql-parser@3.0.35 async-graphql-poem@3.0.35 async-graphql-rocket@3.0.35 async-graphql-tide@3.0.35 async-graphql-value@3.0.35 async-graphql-warp@3.0.35 Generated by cargo-workspaces --- Cargo.toml | 8 ++++---- derive/Cargo.toml | 4 ++-- integrations/actix-web/Cargo.toml | 4 ++-- integrations/axum/Cargo.toml | 4 ++-- integrations/poem/Cargo.toml | 4 ++-- integrations/rocket/Cargo.toml | 4 ++-- integrations/tide/Cargo.toml | 4 ++-- integrations/warp/Cargo.toml | 4 ++-- parser/Cargo.toml | 4 ++-- value/Cargo.toml | 2 +- 10 files changed, 21 insertions(+), 21 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8177c0ef..414407b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql" -version = "3.0.34" +version = "3.0.35" authors = ["sunli ", "Koxiaet"] edition = "2021" description = "A GraphQL server library implemented in Rust" @@ -26,9 +26,9 @@ chrono-duration = ["chrono", "iso8601-duration"] password-strength-validator = ["zxcvbn"] [dependencies] -async-graphql-derive = { path = "derive", version = "3.0.34" } -async-graphql-value = { path = "value", version = "3.0.34" } -async-graphql-parser = { path = "parser", version = "3.0.34" } +async-graphql-derive = { path = "derive", version = "3.0.35" } +async-graphql-value = { path = "value", version = "3.0.35" } +async-graphql-parser = { path = "parser", version = "3.0.35" } async-stream = "0.3.0" async-trait = "0.1.48" diff --git a/derive/Cargo.toml b/derive/Cargo.toml index 5409e7ce..bd65f76e 100644 --- a/derive/Cargo.toml +++ b/derive/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-derive" -version = "3.0.34" +version = "3.0.35" authors = ["sunli ", "Koxiaet"] edition = "2021" description = "Macros for async-graphql" @@ -15,7 +15,7 @@ categories = ["network-programming", "asynchronous"] proc-macro = true [dependencies] -async-graphql-parser = { path = "../parser", version = "3.0.34" } +async-graphql-parser = { path = "../parser", version = "3.0.35" } proc-macro2 = "1.0.24" syn = { version = "1.0.64", features = ["full", "extra-traits", "visit-mut", "visit"] } quote = "1.0.9" diff --git a/integrations/actix-web/Cargo.toml b/integrations/actix-web/Cargo.toml index 1a132b33..58050a06 100644 --- a/integrations/actix-web/Cargo.toml +++ b/integrations/actix-web/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-actix-web" -version = "3.0.34" +version = "3.0.35" authors = ["sunli ", "Koxiaet"] edition = "2021" description = "async-graphql for actix-web" @@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "3.0.34" } +async-graphql = { path = "../..", version = "3.0.35" } actix = "0.13.0" actix-http = "3.0.1" diff --git a/integrations/axum/Cargo.toml b/integrations/axum/Cargo.toml index 0fe51129..dc5474f1 100644 --- a/integrations/axum/Cargo.toml +++ b/integrations/axum/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-axum" -version = "3.0.34" +version = "3.0.35" authors = ["sunli "] edition = "2021" description = "async-graphql for axum" @@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql", "axum"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "3.0.34" } +async-graphql = { path = "../..", version = "3.0.35" } async-trait = "0.1.51" axum = { version = "0.4", features = ["ws", "headers"] } diff --git a/integrations/poem/Cargo.toml b/integrations/poem/Cargo.toml index 13e530ff..a1958695 100644 --- a/integrations/poem/Cargo.toml +++ b/integrations/poem/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-poem" -version = "3.0.34" +version = "3.0.35" authors = ["sunli "] edition = "2021" description = "async-graphql for poem" @@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql", "poem"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "3.0.34" } +async-graphql = { path = "../..", version = "3.0.35" } poem = { version = "1.2.2", features = ["websocket"] } futures-util = { version = "0.3.0", default-features = false } diff --git a/integrations/rocket/Cargo.toml b/integrations/rocket/Cargo.toml index 83528b11..ae77b142 100644 --- a/integrations/rocket/Cargo.toml +++ b/integrations/rocket/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-rocket" -version = "3.0.34" +version = "3.0.35" authors = ["Daniel Wiesenberg "] edition = "2021" description = "async-graphql for Rocket.rs" @@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql", "rocket"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "3.0.34" } +async-graphql = { path = "../..", version = "3.0.35" } rocket = { version = "0.5.0-rc.1", default-features = false } serde = "1.0.126" diff --git a/integrations/tide/Cargo.toml b/integrations/tide/Cargo.toml index f66b686a..b335fa66 100644 --- a/integrations/tide/Cargo.toml +++ b/integrations/tide/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-tide" -version = "3.0.34" +version = "3.0.35" authors = ["vkill ", "sunli "] edition = "2021" description = "async-graphql for tide" @@ -16,7 +16,7 @@ default = ["websocket"] websocket = ["tide-websockets"] [dependencies] -async-graphql = { path = "../..", version = "3.0.34" } +async-graphql = { path = "../..", version = "3.0.35" } async-trait = "0.1.48" futures-util = "0.3.0" diff --git a/integrations/warp/Cargo.toml b/integrations/warp/Cargo.toml index 3e4bfa31..4ea228ef 100644 --- a/integrations/warp/Cargo.toml +++ b/integrations/warp/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-warp" -version = "3.0.34" +version = "3.0.35" authors = ["sunli ", "Koxiaet"] edition = "2021" description = "async-graphql for warp" @@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql = { path = "../..", version = "3.0.34" } +async-graphql = { path = "../..", version = "3.0.35" } warp = { version = "0.3.0", default-features = false, features = ["websocket"] } futures-util = { version = "0.3.0", default-features = false, features = ["sink"] } diff --git a/parser/Cargo.toml b/parser/Cargo.toml index 2113d10f..b9e60990 100644 --- a/parser/Cargo.toml +++ b/parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-parser" -version = "3.0.34" +version = "3.0.35" authors = ["sunli ", "Koxiaet"] edition = "2021" description = "GraphQL query parser for async-graphql" @@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql"] categories = ["network-programming", "asynchronous"] [dependencies] -async-graphql-value = { path = "../value", version = "3.0.34" } +async-graphql-value = { path = "../value", version = "3.0.35" } pest = "2.1.3" pest_derive = "2.1.0" serde_json = "1.0.64" diff --git a/value/Cargo.toml b/value/Cargo.toml index 102ef8a1..9149358b 100644 --- a/value/Cargo.toml +++ b/value/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "async-graphql-value" -version = "3.0.34" +version = "3.0.35" authors = ["sunli ", "Koxiaet"] edition = "2021" description = "GraphQL value for async-graphql"