From ac044d6d303db45c406280e32d5a6ab5b8b98f6a Mon Sep 17 00:00:00 2001 From: sunli Date: Thu, 19 Mar 2020 17:20:12 +0800 Subject: [PATCH] add GraphQL Cursor Connections --- Cargo.toml | 3 + README.md | 8 +- .../examples/upload-file.rs | 4 +- async-graphql-actix-web/src/lib.rs | 14 +- async-graphql-actix-web/src/session.rs | 38 ++-- async-graphql-derive/src/enum.rs | 49 +---- async-graphql-derive/src/input_object.rs | 22 +- async-graphql-derive/src/interface.rs | 36 +-- async-graphql-derive/src/object.rs | 27 ++- async-graphql-derive/src/subscription.rs | 20 +- async-graphql-derive/src/union.rs | 23 +- async-graphql-derive/src/utils.rs | 23 +- examples/actix-web.rs | 6 +- examples/starwars/mod.rs | 10 +- examples/starwars/model.rs | 46 +++- examples/tide.rs | 6 +- src/base.rs | 116 ++++------ src/context.rs | 12 +- src/error.rs | 3 + src/http/mod.rs | 14 +- src/lib.rs | 27 ++- src/model/field.rs | 7 +- src/model/type.rs | 49 +++-- src/query.rs | 16 +- src/registry.rs | 34 +-- src/resolver.rs | 23 +- src/scalars/bool.rs | 4 +- src/scalars/datetime.rs | 4 +- src/scalars/floats.rs | 4 +- src/scalars/id.rs | 4 +- src/scalars/integers.rs | 4 +- src/scalars/mod.rs | 34 +-- src/scalars/string.rs | 10 +- src/scalars/uuid.rs | 4 +- src/schema.rs | 7 +- src/subscription.rs | 14 +- src/types/connection/connection.rs | 189 ++++++++++++++++ src/types/connection/edge.rs | 113 ++++++++++ src/types/connection/mod.rs | 205 ++++++++++++++++++ src/types/connection/page_info.rs | 31 +++ src/types/connection/slice.rs | 46 ++++ src/types/empty_mutation.rs | 25 ++- src/types/empty_subscription.rs | 26 ++- src/types/enum.rs | 8 +- src/types/list.rs | 41 +--- src/types/mod.rs | 8 +- src/types/optional.rs | 39 +--- src/types/query_root.rs | 32 ++- src/types/upload.rs | 6 +- src/validation/context.rs | 18 +- .../rules/fields_on_correct_type.rs | 4 +- src/validation/utils.rs | 19 +- tests/enum.rs | 6 +- tests/input_object.rs | 2 +- tests/list.rs | 4 +- tests/optional.rs | 4 +- tests/scalars.rs | 2 +- 57 files changed, 1102 insertions(+), 451 deletions(-) create mode 100644 src/types/connection/connection.rs create mode 100644 src/types/connection/edge.rs create mode 100644 src/types/connection/mod.rs create mode 100644 src/types/connection/page_info.rs create mode 100644 src/types/connection/slice.rs diff --git a/Cargo.toml b/Cargo.toml index 3af71593..f935474d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,9 @@ serde_derive = "1.0.104" serde_json = "1.0.48" fnv = "1.0.6" bytes = "0.5.4" +Inflector = "0.11.4" +base64 = "0.12.0" +byteorder = "1.3.4" chrono = { version = "0.4.10", optional = true } uuid = { version = "0.8.1", optional = true } diff --git a/README.md b/README.md index 3aeab98d..a5953f64 100644 --- a/README.md +++ b/README.md @@ -47,8 +47,7 @@ Open `http://localhost:8000` in browser * Minimal overhead * Easy integration (hyper, actix_web, tide ...) * Upload files (Multipart request) -* Subscription (WebSocket transport) - +* Subscription (WebSocket transport ## Integrations * Actix-web [async-graphql-actix-web](https://crates.io/crates/async-graphql-actix-web) @@ -68,7 +67,6 @@ Open `http://localhost:8000` in browser - [X] List - [X] Non-Null - [X] Object - - [X] Lifetime cycle - [X] Enum - [X] InputObject - [X] Field default value @@ -99,6 +97,7 @@ Open `http://localhost:8000` in browser - [X] Schema - [X] Multipart Request (https://github.com/jaydenseric/graphql-multipart-request-spec) - [X] Actix-web + - [X] Cursor Connections - [X] Subscription - [X] Filter - [X] WebSocket transport @@ -143,3 +142,6 @@ Licensed under either of ## References * [GraphQL](https://graphql.org) +* [GraphQL Multipart Request](https://github.com/jaydenseric/graphql-multipart-request-spec) +* [GraphQL Cursor Connections Specification](https://facebook.github.io/relay/graphql/connections.htm) +* [GraphQL over WebSocket Protocol](https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md) diff --git a/async-graphql-actix-web/examples/upload-file.rs b/async-graphql-actix-web/examples/upload-file.rs index e283a0ab..3b743606 100644 --- a/async-graphql-actix-web/examples/upload-file.rs +++ b/async-graphql-actix-web/examples/upload-file.rs @@ -1,5 +1,5 @@ use actix_web::{web, App, HttpServer}; -use async_graphql::{GQLEmptySubscription, Schema, Upload}; +use async_graphql::{EmptySubscription, Schema, Upload}; struct QueryRoot; @@ -36,7 +36,7 @@ impl MutationRoot { #[actix_rt::main] async fn main() -> std::io::Result<()> { HttpServer::new(move || { - let schema = Schema::new(QueryRoot, MutationRoot, GQLEmptySubscription); + let schema = Schema::new(QueryRoot, MutationRoot, EmptySubscription); let handler = async_graphql_actix_web::HandlerBuilder::new(schema) .enable_subscription() .build(); diff --git a/async-graphql-actix-web/src/lib.rs b/async-graphql-actix-web/src/lib.rs index e59aa471..65e404a0 100644 --- a/async-graphql-actix-web/src/lib.rs +++ b/async-graphql-actix-web/src/lib.rs @@ -13,7 +13,7 @@ use actix_web::web::{BytesMut, Payload}; use actix_web::{web, FromRequest, HttpRequest, HttpResponse, Responder}; use actix_web_actors::ws; use async_graphql::http::{GQLRequest, GQLResponse}; -use async_graphql::{GQLObject, GQLSubscription, Schema}; +use async_graphql::{ObjectType, Schema, SubscriptionType}; use bytes::Bytes; use futures::StreamExt; use mime::Mime; @@ -35,9 +35,9 @@ pub struct HandlerBuilder { impl HandlerBuilder where - Query: GQLObject + Send + Sync + 'static, - Mutation: GQLObject + Send + Sync + 'static, - Subscription: GQLSubscription + Send + Sync + 'static, + Query: ObjectType + Send + Sync + 'static, + Mutation: ObjectType + Send + Sync + 'static, + Subscription: SubscriptionType + Send + Sync + 'static, { /// Create an HTTP handler builder pub fn new(schema: Schema) -> Self { @@ -152,9 +152,9 @@ async fn handle_request( mut payload: Payload, ) -> actix_web::Result where - Query: GQLObject + Send + Sync, - Mutation: GQLObject + Send + Sync, - Subscription: GQLSubscription + Send + Sync, + Query: ObjectType + Send + Sync, + Mutation: ObjectType + Send + Sync, + Subscription: SubscriptionType + Send + Sync, { if let Ok(ct) = get_content_type(req.headers()) { if ct.essence_str() == mime::MULTIPART_FORM_DATA { diff --git a/async-graphql-actix-web/src/session.rs b/async-graphql-actix-web/src/session.rs index a4480b43..6e54dbf2 100644 --- a/async-graphql-actix-web/src/session.rs +++ b/async-graphql-actix-web/src/session.rs @@ -5,10 +5,10 @@ use actix::{ }; use actix_web_actors::ws::{Message, ProtocolError, WebsocketContext}; use async_graphql::http::{GQLError, GQLRequest, GQLResponse}; -use async_graphql::{GQLObject, GQLSubscription, Schema, Subscribe, Variables}; +use async_graphql::{ObjectType, Schema, Subscribe, SubscriptionType, Variables}; use std::collections::HashMap; use std::sync::Arc; -use std::time::Instant; +use std::time::{Duration, Instant}; #[derive(Serialize, Deserialize)] struct OperationMessage { @@ -27,9 +27,9 @@ pub struct WsSession { impl WsSession where - Query: GQLObject + Send + Sync + 'static, - Mutation: GQLObject + Send + Sync + 'static, - Subscription: GQLSubscription + Send + Sync + 'static, + Query: ObjectType + Send + Sync + 'static, + Mutation: ObjectType + Send + Sync + 'static, + Subscription: SubscriptionType + Send + Sync + 'static, { pub fn new(schema: Arc>) -> Self { Self { @@ -39,17 +39,27 @@ where subscribes: Default::default(), } } + + fn hb(&self, ctx: &mut WebsocketContext) { + ctx.run_interval(Duration::new(1, 0), |act, ctx| { + if Instant::now().duration_since(act.hb) > Duration::new(10, 0) { + ctx.stop(); + } + }); + } } impl Actor for WsSession where - Query: GQLObject + Sync + Send + 'static, - Mutation: GQLObject + Sync + Send + 'static, - Subscription: GQLSubscription + Send + Sync + 'static, + Query: ObjectType + Sync + Send + 'static, + Mutation: ObjectType + Sync + Send + 'static, + Subscription: SubscriptionType + Send + Sync + 'static, { type Context = WebsocketContext; fn started(&mut self, ctx: &mut Self::Context) { + self.hb(ctx); + new_client(ctx.address().recipient()) .into_actor(self) .then(|client_id, actor, _| { @@ -68,9 +78,9 @@ where impl StreamHandler> for WsSession where - Query: GQLObject + Sync + Send + 'static, - Mutation: GQLObject + Sync + Send + 'static, - Subscription: GQLSubscription + Send + Sync + 'static, + Query: ObjectType + Sync + Send + 'static, + Mutation: ObjectType + Sync + Send + 'static, + Subscription: SubscriptionType + Send + Sync + 'static, { fn handle(&mut self, msg: Result, ctx: &mut Self::Context) { let msg = match msg { @@ -164,9 +174,9 @@ where impl Handler for WsSession where - Query: GQLObject + Send + Sync + 'static, - Mutation: GQLObject + Send + Sync + 'static, - Subscription: GQLSubscription + Send + Sync + 'static, + Query: ObjectType + Send + Sync + 'static, + Mutation: ObjectType + Send + Sync + 'static, + Subscription: SubscriptionType + Send + Sync + 'static, { type Result = ResponseActFuture>; diff --git a/async-graphql-derive/src/enum.rs b/async-graphql-derive/src/enum.rs index f30c6b5c..8beb3be6 100644 --- a/async-graphql-derive/src/enum.rs +++ b/async-graphql-derive/src/enum.rs @@ -1,5 +1,5 @@ use crate::args; -use crate::utils::get_crate_name; +use crate::utils::{check_reserved_name, get_crate_name}; use inflector::Inflector; use proc_macro::TokenStream; use quote::quote; @@ -16,6 +16,8 @@ pub fn generate(enum_args: &args::Enum, input: &DeriveInput) -> Result Result Result &'static [#crate_name::GQLEnumItem<#ident>] { + impl #crate_name::EnumType for #ident { + fn items() -> &'static [#crate_name::EnumItem<#ident>] { &[#(#items),*] } } - impl #crate_name::GQLType for #ident { + impl #crate_name::Type for #ident { fn type_name() -> std::borrow::Cow<'static, str> { std::borrow::Cow::Borrowed(#gql_typename) } @@ -90,7 +92,7 @@ pub fn generate(enum_args: &args::Enum, input: &DeriveInput) -> Result String { registry.create_type::(|registry| { #crate_name::registry::Type::Enum { - name: #gql_typename, + name: #gql_typename.to_string(), description: #desc, enum_values: { let mut enum_items = std::collections::HashMap::new(); @@ -102,43 +104,16 @@ pub fn generate(enum_args: &args::Enum, input: &DeriveInput) -> Result Option { - #crate_name::GQLEnum::parse_enum(value) + #crate_name::EnumType::parse_enum(value) } } #[#crate_name::async_trait::async_trait] - impl #crate_name::GQLOutputValue for #ident { + impl #crate_name::OutputValueType for #ident { async fn resolve(value: &Self, _: &#crate_name::ContextSelectionSet<'_>) -> #crate_name::Result { - #crate_name::GQLEnum::resolve_enum(value) - } - } - - impl #crate_name::GQLType for &#ident { - fn type_name() -> std::borrow::Cow<'static, str> { - std::borrow::Cow::Borrowed(#gql_typename) - } - - fn create_type_info(registry: &mut #crate_name::registry::Registry) -> String { - registry.create_type::(|registry| { - #crate_name::registry::Type::Enum { - name: #gql_typename, - description: #desc, - enum_values: { - let mut enum_items = std::collections::HashMap::new(); - #(#schema_enum_items)* - enum_items - }, - } - }) - } - } - - #[#crate_name::async_trait::async_trait] - impl #crate_name::GQLOutputValue for &#ident { - async fn resolve(value: &Self, _: &#crate_name::ContextSelectionSet<'_>) -> #crate_name::Result { - #crate_name::GQLEnum::resolve_enum(*value) + #crate_name::EnumType::resolve_enum(value) } } }; diff --git a/async-graphql-derive/src/input_object.rs b/async-graphql-derive/src/input_object.rs index 8b3581e6..d49c71f0 100644 --- a/async-graphql-derive/src/input_object.rs +++ b/async-graphql-derive/src/input_object.rs @@ -1,5 +1,5 @@ use crate::args; -use crate::utils::{build_value_repr, get_crate_name}; +use crate::utils::{build_value_repr, check_reserved_name, get_crate_name}; use inflector::Inflector; use proc_macro::TokenStream; use quote::quote; @@ -35,6 +35,8 @@ pub fn generate(object_args: &args::InputObject, input: &DeriveInput) -> Result< .name .clone() .unwrap_or_else(|| ident.to_string()); + check_reserved_name(&gql_typename, object_args.internal)?; + let desc = object_args .desc .as_ref() @@ -71,17 +73,17 @@ pub fn generate(object_args: &args::InputObject, input: &DeriveInput) -> Result< get_fields.push(quote! { let #ident:#ty = { match obj.get(#name) { - Some(value) => #crate_name::GQLInputValue::parse(value)?, + Some(value) => #crate_name::InputValueType::parse(value)?, None => { let default = #default_repr; - #crate_name::GQLInputValue::parse(&default)? + #crate_name::InputValueType::parse(&default)? } } }; }); } else { get_fields.push(quote! { - let #ident:#ty = #crate_name::GQLInputValue::parse(obj.get(#name).unwrap_or(&#crate_name::Value::Null))?; + let #ident:#ty = #crate_name::InputValueType::parse(obj.get(#name).unwrap_or(&#crate_name::Value::Null))?; }); } @@ -90,7 +92,7 @@ pub fn generate(object_args: &args::InputObject, input: &DeriveInput) -> Result< #crate_name::registry::InputValue { name: #name, description: #desc, - ty: <#ty as #crate_name::GQLType>::create_type_info(registry), + ty: <#ty as #crate_name::Type>::create_type_info(registry), default_value: #default, } }) @@ -99,23 +101,23 @@ pub fn generate(object_args: &args::InputObject, input: &DeriveInput) -> Result< let expanded = quote! { #new_struct - impl #crate_name::GQLType for #ident { + impl #crate_name::Type for #ident { fn type_name() -> std::borrow::Cow<'static, str> { std::borrow::Cow::Borrowed(#gql_typename) } fn create_type_info(registry: &mut #crate_name::registry::Registry) -> String { registry.create_type::(|registry| #crate_name::registry::Type::InputObject { - name: #gql_typename, + name: #gql_typename.to_string(), description: #desc, input_fields: vec![#(#schema_fields),*] }) } } - impl #crate_name::GQLInputValue for #ident { + impl #crate_name::InputValueType for #ident { fn parse(value: &#crate_name::Value) -> Option { - use #crate_name::GQLType; + use #crate_name::Type; if let #crate_name::Value::Object(obj) = value { #(#get_fields)* @@ -126,7 +128,7 @@ pub fn generate(object_args: &args::InputObject, input: &DeriveInput) -> Result< } } - impl #crate_name::GQLInputObject for #ident {} + impl #crate_name::InputObjectType for #ident {} }; Ok(expanded.into()) } diff --git a/async-graphql-derive/src/interface.rs b/async-graphql-derive/src/interface.rs index 9f24ffe2..09d8c56d 100644 --- a/async-graphql-derive/src/interface.rs +++ b/async-graphql-derive/src/interface.rs @@ -1,7 +1,7 @@ use crate::args; use crate::args::{InterfaceField, InterfaceFieldArgument}; use crate::output_type::OutputType; -use crate::utils::{build_value_repr, get_crate_name}; +use crate::utils::{build_value_repr, check_reserved_name, get_crate_name}; use inflector::Inflector; use proc_macro::TokenStream; use proc_macro2::{Ident, Span}; @@ -29,6 +29,7 @@ pub fn generate(interface_args: &args::Interface, input: &DeriveInput) -> Result .name .clone() .unwrap_or_else(|| ident.to_string()); + check_reserved_name(&gql_typename, interface_args.internal)?; let desc = interface_args .desc .as_ref() @@ -51,16 +52,16 @@ pub fn generate(interface_args: &args::Interface, input: &DeriveInput) -> Result } }); registry_types.push(quote! { - <#p as #crate_name::GQLType>::create_type_info(registry); - registry.add_implements(&<#p as #crate_name::GQLType>::type_name(), #gql_typename); + <#p as #crate_name::Type>::create_type_info(registry); + registry.add_implements(&<#p as #crate_name::Type>::type_name(), #gql_typename); }); possible_types.push(quote! { - possible_types.insert(<#p as #crate_name::GQLType>::type_name().to_string()); + possible_types.insert(<#p as #crate_name::Type>::type_name().to_string()); }); inline_fragment_resolvers.push(quote! { - if name == <#p as #crate_name::GQLType>::type_name() { + if name == <#p as #crate_name::Type>::type_name() { if let #ident::#enum_name(obj) = self { - #crate_name::do_resolve(ctx, obj, result).await?; + #crate_name::do_resolve_values(ctx, obj, result).await?; } return Ok(()); } @@ -134,7 +135,7 @@ pub fn generate(interface_args: &args::Interface, input: &DeriveInput) -> Result args.insert(#name, #crate_name::registry::InputValue { name: #name, description: #desc, - ty: <#ty as #crate_name::GQLType>::create_type_info(registry), + ty: <#ty as #crate_name::Type>::create_type_info(registry), default_value: #schema_default, }); }); @@ -173,15 +174,15 @@ pub fn generate(interface_args: &args::Interface, input: &DeriveInput) -> Result let schema_ty = ty.value_type(); schema_fields.push(quote! { - fields.insert(#name, #crate_name::registry::Field { - name: #name, + fields.insert(#name.to_string(), #crate_name::registry::Field { + name: #name.to_string(), description: #desc, args: { let mut args = std::collections::HashMap::new(); #(#schema_args)* args }, - ty: <#schema_ty as #crate_name::GQLType>::create_type_info(registry), + ty: <#schema_ty as #crate_name::Type>::create_type_info(registry), deprecation: #deprecation, }); }); @@ -202,7 +203,7 @@ pub fn generate(interface_args: &args::Interface, input: &DeriveInput) -> Result if field.name.as_str() == #name { #(#get_params)* let ctx_obj = ctx.with_item(&field.selection_set); - return #crate_name::GQLOutputValue::resolve(&#resolve_obj, &ctx_obj).await. + return #crate_name::OutputValueType::resolve(&#resolve_obj, &ctx_obj).await. map_err(|err| err.with_position(field.position).into()); } }); @@ -218,7 +219,7 @@ pub fn generate(interface_args: &args::Interface, input: &DeriveInput) -> Result #(#methods)* } - impl #generics #crate_name::GQLType for #ident #generics { + impl #generics #crate_name::Type for #ident #generics { fn type_name() -> std::borrow::Cow<'static, str> { std::borrow::Cow::Borrowed(#gql_typename) } @@ -228,7 +229,7 @@ pub fn generate(interface_args: &args::Interface, input: &DeriveInput) -> Result #(#registry_types)* #crate_name::registry::Type::Interface { - name: #gql_typename, + name: #gql_typename.to_string(), description: #desc, fields: { let mut fields = std::collections::HashMap::new(); @@ -246,7 +247,7 @@ pub fn generate(interface_args: &args::Interface, input: &DeriveInput) -> Result } #[#crate_name::async_trait::async_trait] - impl #generics #crate_name::GQLObject for #ident #generics { + impl #generics #crate_name::ObjectType for #ident #generics { async fn resolve_field(&self, ctx: &#crate_name::Context<'_>, field: &#crate_name::graphql_parser::query::Field) -> #crate_name::Result<#crate_name::serde_json::Value> { use #crate_name::ErrorWithPosition; @@ -267,6 +268,13 @@ pub fn generate(interface_args: &args::Interface, input: &DeriveInput) -> Result }); } } + + #[#crate_name::async_trait::async_trait] + impl #generics #crate_name::OutputValueType for #ident #generics { + async fn resolve(value: &Self, ctx: &#crate_name::ContextSelectionSet<'_>) -> #crate_name::Result<#crate_name::serde_json::Value> { + #crate_name::do_resolve(ctx, value).await + } + } }; Ok(expanded.into()) } diff --git a/async-graphql-derive/src/object.rs b/async-graphql-derive/src/object.rs index 9368cbd7..9535b680 100644 --- a/async-graphql-derive/src/object.rs +++ b/async-graphql-derive/src/object.rs @@ -1,6 +1,6 @@ use crate::args; use crate::output_type::OutputType; -use crate::utils::{build_value_repr, get_crate_name}; +use crate::utils::{build_value_repr, check_reserved_name, get_crate_name}; use inflector::Inflector; use proc_macro::TokenStream; use quote::quote; @@ -25,6 +25,8 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< .name .clone() .unwrap_or_else(|| self_name.clone()); + check_reserved_name(&gql_typename, object_args.internal)?; + let desc = object_args .desc .as_ref() @@ -134,7 +136,7 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< args.insert(#name, #crate_name::registry::InputValue { name: #name, description: #desc, - ty: <#ty as #crate_name::GQLType>::create_type_info(registry), + ty: <#ty as #crate_name::Type>::create_type_info(registry), default_value: #schema_default, }); }); @@ -156,15 +158,15 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< let schema_ty = ty.value_type(); schema_fields.push(quote! { - fields.insert(#field_name, #crate_name::registry::Field { - name: #field_name, + fields.insert(#field_name.to_string(), #crate_name::registry::Field { + name: #field_name.to_string(), description: #field_desc, args: { let mut args = std::collections::HashMap::new(); #(#schema_args)* args }, - ty: <#schema_ty as #crate_name::GQLType>::create_type_info(registry), + ty: <#schema_ty as #crate_name::Type>::create_type_info(registry), deprecation: #field_deprecation, }); }); @@ -191,7 +193,7 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< if field.name.as_str() == #field_name { #(#get_params)* let ctx_obj = ctx.with_item(&field.selection_set); - return #crate_name::GQLOutputValue::resolve(&#resolve_obj, &ctx_obj).await. + return #crate_name::OutputValueType::resolve(&#resolve_obj, &ctx_obj).await. map_err(|err| err.with_position(field.position).into()); } }); @@ -204,14 +206,14 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< let expanded = quote! { #item_impl - impl #generics #crate_name::GQLType for #self_ty { + impl #generics #crate_name::Type for #self_ty { fn type_name() -> std::borrow::Cow<'static, str> { std::borrow::Cow::Borrowed(#gql_typename) } fn create_type_info(registry: &mut #crate_name::registry::Registry) -> String { registry.create_type::(|registry| #crate_name::registry::Type::Object { - name: #gql_typename, + name: #gql_typename.to_string(), description: #desc, fields: { let mut fields = std::collections::HashMap::new(); @@ -223,7 +225,7 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< } #[#crate_name::async_trait::async_trait] - impl#generics #crate_name::GQLObject for #self_ty { + impl#generics #crate_name::ObjectType for #self_ty { async fn resolve_field(&self, ctx: &#crate_name::Context<'_>, field: &#crate_name::graphql_parser::query::Field) -> #crate_name::Result<#crate_name::serde_json::Value> { use #crate_name::ErrorWithPosition; @@ -243,6 +245,13 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< }); } } + + #[#crate_name::async_trait::async_trait] + impl #generics #crate_name::OutputValueType for #self_ty { + async fn resolve(value: &Self, ctx: &#crate_name::ContextSelectionSet<'_>) -> #crate_name::Result<#crate_name::serde_json::Value> { + #crate_name::do_resolve(ctx, value).await + } + } }; Ok(expanded.into()) } diff --git a/async-graphql-derive/src/subscription.rs b/async-graphql-derive/src/subscription.rs index 544b43b9..861770e3 100644 --- a/async-graphql-derive/src/subscription.rs +++ b/async-graphql-derive/src/subscription.rs @@ -1,5 +1,5 @@ use crate::args; -use crate::utils::{build_value_repr, get_crate_name}; +use crate::utils::{build_value_repr, check_reserved_name, get_crate_name}; use inflector::Inflector; use proc_macro::TokenStream; use quote::quote; @@ -24,6 +24,8 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< .name .clone() .unwrap_or_else(|| self_name.clone()); + check_reserved_name(&gql_typename, object_args.internal)?; + let desc = object_args .desc .as_ref() @@ -157,7 +159,7 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< args.insert(#name, #crate_name::registry::InputValue { name: #name, description: #desc, - ty: <#ty as #crate_name::GQLType>::create_type_info(registry), + ty: <#ty as #crate_name::Type>::create_type_info(registry), default_value: #schema_default, }); }); @@ -178,15 +180,15 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< } schema_fields.push(quote! { - fields.insert(#field_name, #crate_name::registry::Field { - name: #field_name, + fields.insert(#field_name.to_string(), #crate_name::registry::Field { + name: #field_name.to_string(), description: #field_desc, args: { let mut args = std::collections::HashMap::new(); #(#schema_args)* args }, - ty: <#ty as #crate_name::GQLType>::create_type_info(registry), + ty: <#ty as #crate_name::Type>::create_type_info(registry), deprecation: #field_deprecation, }); }); @@ -204,7 +206,7 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< if self.#ident(msg, #(#use_params)*) { let ctx_selection_set = ctx_field.with_item(&field.selection_set); let value = - #crate_name::GQLOutputValue::resolve(msg, &ctx_selection_set).await?; + #crate_name::OutputValueType::resolve(msg, &ctx_selection_set).await?; return Ok(Some(value)); } } @@ -218,14 +220,14 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< let expanded = quote! { #item_impl - impl #generics #crate_name::GQLType for #self_ty { + impl #generics #crate_name::Type for #self_ty { fn type_name() -> std::borrow::Cow<'static, str> { std::borrow::Cow::Borrowed(#gql_typename) } fn create_type_info(registry: &mut #crate_name::registry::Registry) -> String { registry.create_type::(|registry| #crate_name::registry::Type::Object { - name: #gql_typename, + name: #gql_typename.to_string(), description: #desc, fields: { let mut fields = std::collections::HashMap::new(); @@ -237,7 +239,7 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result< } #[#crate_name::async_trait::async_trait] - impl #crate_name::GQLSubscription for SubscriptionRoot { + impl #crate_name::SubscriptionType for SubscriptionRoot { fn create_type(field: &#crate_name::graphql_parser::query::Field, types: &mut std::collections::HashMap) -> #crate_name::Result<()> { use #crate_name::ErrorWithPosition; #(#create_types)* diff --git a/async-graphql-derive/src/union.rs b/async-graphql-derive/src/union.rs index 724725e3..8f05fb9a 100644 --- a/async-graphql-derive/src/union.rs +++ b/async-graphql-derive/src/union.rs @@ -1,5 +1,5 @@ use crate::args; -use crate::utils::get_crate_name; +use crate::utils::{check_reserved_name, get_crate_name}; use proc_macro::TokenStream; use quote::quote; use syn::{Data, DeriveInput, Error, Fields, Result, Type}; @@ -25,6 +25,8 @@ pub fn generate(interface_args: &args::Interface, input: &DeriveInput) -> Result .name .clone() .unwrap_or_else(|| ident.to_string()); + check_reserved_name(&gql_typename, interface_args.internal)?; + let desc = interface_args .desc .as_ref() @@ -47,15 +49,15 @@ pub fn generate(interface_args: &args::Interface, input: &DeriveInput) -> Result } }); registry_types.push(quote! { - <#p as #crate_name::GQLType>::create_type_info(registry); + <#p as #crate_name::Type>::create_type_info(registry); }); possible_types.push(quote! { - possible_types.insert(<#p as #crate_name::GQLType>::type_name().to_string()); + possible_types.insert(<#p as #crate_name::Type>::type_name().to_string()); }); inline_fragment_resolvers.push(quote! { - if name == <#p as #crate_name::GQLType>::type_name() { + if name == <#p as #crate_name::Type>::type_name() { if let #ident::#enum_name(obj) = self { - #crate_name::do_resolve(ctx, obj, result).await?; + #crate_name::do_resolve_values(ctx, obj, result).await?; } return Ok(()); } @@ -71,7 +73,7 @@ pub fn generate(interface_args: &args::Interface, input: &DeriveInput) -> Result #(#type_into_impls)* - impl #generics #crate_name::GQLType for #ident #generics { + impl #generics #crate_name::Type for #ident #generics { fn type_name() -> std::borrow::Cow<'static, str> { std::borrow::Cow::Borrowed(#gql_typename) } @@ -94,7 +96,7 @@ pub fn generate(interface_args: &args::Interface, input: &DeriveInput) -> Result } #[#crate_name::async_trait::async_trait] - impl #generics #crate_name::GQLObject for #ident #generics { + impl #generics #crate_name::ObjectType for #ident #generics { async fn resolve_field(&self, ctx: &#crate_name::Context<'_>, field: &#crate_name::graphql_parser::query::Field) -> #crate_name::Result<#crate_name::serde_json::Value> { use #crate_name::ErrorWithPosition; anyhow::bail!(#crate_name::QueryError::FieldNotFound { @@ -112,6 +114,13 @@ pub fn generate(interface_args: &args::Interface, input: &DeriveInput) -> Result }); } } + + #[#crate_name::async_trait::async_trait] + impl #generics #crate_name::OutputValueType for #ident #generics { + async fn resolve(value: &Self, ctx: &#crate_name::ContextSelectionSet<'_>) -> #crate_name::Result<#crate_name::serde_json::Value> { + #crate_name::do_resolve(ctx, value).await + } + } }; Ok(expanded.into()) } diff --git a/async-graphql-derive/src/utils.rs b/async-graphql-derive/src/utils.rs index d3c4c5cb..873ff6c2 100644 --- a/async-graphql-derive/src/utils.rs +++ b/async-graphql-derive/src/utils.rs @@ -2,7 +2,7 @@ use graphql_parser::parse_query; use graphql_parser::query::{Definition, OperationDefinition, ParseError, Query, Value}; use proc_macro2::{Span, TokenStream}; use quote::quote; -use syn::Ident; +use syn::{Error, Ident, Result}; pub fn get_crate_name(internal: bool) -> TokenStream { match internal { @@ -14,7 +14,7 @@ pub fn get_crate_name(internal: bool) -> TokenStream { } } -pub fn parse_value(s: &str) -> Result { +pub fn parse_value(s: &str) -> std::result::Result { let mut doc = parse_query(&format!("query ($a:Int!={}) {{ dummy }}", s))?; let definition = doc.definitions.remove(0); if let Definition::Operation(OperationDefinition::Query(Query { @@ -78,3 +78,22 @@ pub fn build_value_repr(crate_name: &TokenStream, value: &Value) -> TokenStream } } } + +pub fn check_reserved_name(name: &str, internal: bool) -> Result<()> { + if internal { + return Ok(()); + } + if name.ends_with("Connection") { + Err(Error::new( + Span::call_site(), + "The name ending with 'Connection' is reserved", + )) + } else if name == "PageInfo" { + Err(Error::new( + Span::call_site(), + "The name 'PageInfo' is reserved", + )) + } else { + Ok(()) + } +} diff --git a/examples/actix-web.rs b/examples/actix-web.rs index 16c685ab..72e7a370 100644 --- a/examples/actix-web.rs +++ b/examples/actix-web.rs @@ -2,9 +2,9 @@ mod starwars; use actix_web::{guard, web, App, HttpResponse, HttpServer}; use async_graphql::http::{graphiql_source, playground_source, GQLRequest, GQLResponse}; -use async_graphql::{GQLEmptyMutation, GQLEmptySubscription, Schema}; +use async_graphql::{EmptyMutation, EmptySubscription, Schema}; -type StarWarsSchema = Schema; +type StarWarsSchema = Schema; async fn index(s: web::Data, req: web::Json) -> web::Json { web::Json(req.into_inner().execute(&s).await) @@ -27,7 +27,7 @@ async fn main() -> std::io::Result<()> { HttpServer::new(move || { App::new() .data( - Schema::new(starwars::QueryRoot, GQLEmptyMutation, GQLEmptySubscription) + Schema::new(starwars::QueryRoot, EmptyMutation, EmptySubscription) .data(starwars::StarWars::new()), ) .service(web::resource("/").guard(guard::Post()).to(index)) diff --git a/examples/starwars/mod.rs b/examples/starwars/mod.rs index 4adc7c3b..674d3498 100644 --- a/examples/starwars/mod.rs +++ b/examples/starwars/mod.rs @@ -1,7 +1,7 @@ mod model; use model::Episode; -pub use model::*; +pub use model::QueryRoot; use slab::Slab; use std::collections::HashMap; @@ -124,4 +124,12 @@ impl StarWars { pub fn droid(&self, id: &str) -> Option { self.droid_data.get(id).cloned() } + + pub fn humans(&self) -> Vec { + self.human_data.values().cloned().collect() + } + + pub fn droids(&self) -> Vec { + self.droid_data.values().cloned().collect() + } } diff --git a/examples/starwars/model.rs b/examples/starwars/model.rs index 345cce6b..053cc7db 100644 --- a/examples/starwars/model.rs +++ b/examples/starwars/model.rs @@ -1,5 +1,5 @@ use super::StarWars; -use async_graphql::Context; +use async_graphql::{Connection, Context, DataSource, EmptyEdgeFields, Result}; #[async_graphql::Enum(desc = "One of the films in the Star Wars Trilogy")] pub enum Episode { @@ -110,6 +110,28 @@ impl QueryRoot { ctx.data::().human(&id).map(|id| Human(id)) } + #[field] + async fn humans( + &self, + ctx: &Context<'_>, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result> { + let humans = ctx + .data::() + .humans() + .iter() + .map(|id| *id) + .collect::>(); + humans + .as_slice() + .query(ctx, after, before, first, last) + .await + .map(|connection| connection.map(|id| Human(*id))) + } + #[field] async fn droid( &self, @@ -118,6 +140,28 @@ impl QueryRoot { ) -> Option { ctx.data::().droid(&id).map(|id| Droid(id)) } + + #[field] + async fn droids( + &self, + ctx: &Context<'_>, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result> { + let droids = ctx + .data::() + .droids() + .iter() + .map(|id| *id) + .collect::>(); + droids + .as_slice() + .query(ctx, after, before, first, last) + .await + .map(|connection| connection.map(|id| Human(*id))) + } } #[async_graphql::Interface( diff --git a/examples/tide.rs b/examples/tide.rs index dc032fba..23e6995c 100644 --- a/examples/tide.rs +++ b/examples/tide.rs @@ -1,11 +1,11 @@ mod starwars; use async_graphql::http::{graphiql_source, playground_source, GQLRequest}; -use async_graphql::{GQLEmptyMutation, GQLEmptySubscription, Schema}; +use async_graphql::{EmptyMutation, EmptySubscription, Schema}; use mime; use tide::{self, Request, Response}; -type StarWarsSchema = Schema; +type StarWarsSchema = Schema; async fn index(mut request: Request) -> Response { let gql_request: GQLRequest = request.body_json().await.unwrap(); @@ -28,7 +28,7 @@ async fn gql_graphiql(_request: Request) -> Response { #[async_std::main] async fn main() -> std::io::Result<()> { let mut app = tide::with_state( - Schema::new(starwars::QueryRoot, GQLEmptyMutation, GQLEmptySubscription) + Schema::new(starwars::QueryRoot, EmptyMutation, EmptySubscription) .data(starwars::StarWars::new()), ); app.at("/").post(index); diff --git a/src/base.rs b/src/base.rs index 006b16b1..05d437ae 100644 --- a/src/base.rs +++ b/src/base.rs @@ -1,9 +1,10 @@ +use crate::registry::Registry; use crate::{registry, Context, ContextSelectionSet, Result}; use graphql_parser::query::{Field, Value}; use std::borrow::Cow; /// Represents a GraphQL type -pub trait GQLType { +pub trait Type { /// Type the name. fn type_name() -> Cow<'static, str>; @@ -17,23 +18,23 @@ pub trait GQLType { } /// Represents a GraphQL input value -pub trait GQLInputValue: GQLType + Sized { +pub trait InputValueType: Type + Sized { fn parse(value: &Value) -> Option; } /// Represents a GraphQL output value #[async_trait::async_trait] -pub trait GQLOutputValue: GQLType { +pub trait OutputValueType: Type { async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result; } /// Represents a GraphQL object #[async_trait::async_trait] -pub trait GQLObject: GQLOutputValue { - /// This function returns true of type `GQLEmptyMutation` only +pub trait ObjectType: OutputValueType { + /// This function returns true of type `EmptyMutation` only #[doc(hidden)] fn is_empty() -> bool { - return false; + false } /// Resolves a field value and outputs it as a json value `serde_json::Value`. @@ -49,7 +50,7 @@ pub trait GQLObject: GQLOutputValue { } /// Represents a GraphQL input object -pub trait GQLInputObject: GQLInputValue {} +pub trait InputObjectType: InputValueType {} /// Represents a GraphQL scalar /// @@ -62,7 +63,7 @@ pub trait GQLInputObject: GQLInputValue {} /// /// struct MyInt(i32); /// -/// impl GQLScalar for MyInt { +/// impl Scalar for MyInt { /// fn type_name() -> &'static str { /// "MyInt" /// } @@ -82,7 +83,7 @@ pub trait GQLInputObject: GQLInputValue {} /// /// impl_scalar!(MyInt); // // Don't forget this one /// ``` -pub trait GQLScalar: Sized + Send { +pub trait Scalar: Sized + Send { /// The type name of a scalar. fn type_name() -> &'static str; @@ -109,52 +110,28 @@ pub trait GQLScalar: Sized + Send { #[doc(hidden)] macro_rules! impl_scalar_internal { ($ty:ty) => { - impl crate::GQLType for $ty { + impl crate::Type for $ty { fn type_name() -> std::borrow::Cow<'static, str> { - std::borrow::Cow::Borrowed(<$ty as crate::GQLScalar>::type_name()) + std::borrow::Cow::Borrowed(<$ty as crate::Scalar>::type_name()) } fn create_type_info(registry: &mut crate::registry::Registry) -> String { registry.create_type::<$ty, _>(|_| crate::registry::Type::Scalar { - name: <$ty as crate::GQLScalar>::type_name().to_string(), + name: <$ty as crate::Scalar>::type_name().to_string(), description: <$ty>::description(), - is_valid: |value| <$ty as crate::GQLScalar>::is_valid(value), + is_valid: |value| <$ty as crate::Scalar>::is_valid(value), }) } } - impl crate::GQLType for &$ty { - fn type_name() -> std::borrow::Cow<'static, str> { - std::borrow::Cow::Borrowed(<$ty as crate::GQLScalar>::type_name()) - } - - fn create_type_info(registry: &mut crate::registry::Registry) -> String { - registry.create_type::<$ty, _>(|_| crate::registry::Type::Scalar { - name: <$ty as crate::GQLScalar>::type_name().to_string(), - description: <$ty>::description(), - is_valid: |value| <$ty as crate::GQLScalar>::is_valid(value), - }) - } - } - - impl crate::GQLInputValue for $ty { + impl crate::InputValueType for $ty { fn parse(value: &crate::Value) -> Option { - <$ty as crate::GQLScalar>::parse(value) + <$ty as crate::Scalar>::parse(value) } } #[async_trait::async_trait] - impl crate::GQLOutputValue for $ty { - async fn resolve( - value: &Self, - _: &crate::ContextSelectionSet<'_>, - ) -> crate::Result { - value.to_json() - } - } - - #[async_trait::async_trait] - impl crate::GQLOutputValue for &$ty { + impl crate::OutputValueType for $ty { async fn resolve( value: &Self, _: &crate::ContextSelectionSet<'_>, @@ -168,52 +145,28 @@ macro_rules! impl_scalar_internal { #[macro_export] macro_rules! impl_scalar { ($ty:ty) => { - impl async_graphql::GQLType for $ty { + impl async_graphql::Type for $ty { fn type_name() -> std::borrow::Cow<'static, str> { - std::borrow::Cow::Borrowed(<$ty as async_graphql::GQLScalar>::type_name()) + std::borrow::Cow::Borrowed(<$ty as async_graphql::Scalar>::type_name()) } fn create_type_info(registry: &mut async_graphql::registry::Registry) -> String { registry.create_type::<$ty, _>(|_| async_graphql::registry::Type::Scalar { - name: <$ty as async_graphql::GQLScalar>::type_name().to_string(), + name: <$ty as async_graphql::Scalar>::type_name().to_string(), description: <$ty>::description(), - is_valid: |value| <$ty as async_graphql::GQLScalar>::is_valid(value), + is_valid: |value| <$ty as async_graphql::Scalar>::is_valid(value), }) } } - impl async_graphql::GQLType for &$ty { - fn type_name() -> std::borrow::Cow<'static, str> { - std::borrow::Cow::Borrowed(<$ty as async_graphql::GQLScalar>::type_name()) - } - - fn create_type_info(registry: &mut async_graphql::registry::Registry) -> String { - registry.create_type::<$ty, _>(|_| async_graphql::registry::Type::Scalar { - name: <$ty as async_graphql::GQLScalar>::type_name().to_string(), - description: <$ty>::description(), - is_valid: |value| <$ty as async_graphql::GQLScalar>::is_valid(value), - }) - } - } - - impl async_graphql::GQLInputValue for $ty { + impl async_graphql::InputValueType for $ty { fn parse(value: &async_graphql::Value) -> Option { - <$ty as async_graphql::GQLScalar>::parse(value) + <$ty as async_graphql::Scalar>::parse(value) } } #[async_graphql::async_trait::async_trait] - impl async_graphql::GQLOutputValue for $ty { - async fn resolve( - value: &Self, - _: &async_graphql::ContextSelectionSet<'_>, - ) -> async_graphql::Result { - value.to_json() - } - } - - #[async_graphql::async_trait::async_trait] - impl async_graphql::GQLOutputValue for &$ty { + impl async_graphql::OutputValueType for $ty { async fn resolve( value: &Self, _: &async_graphql::ContextSelectionSet<'_>, @@ -224,12 +177,19 @@ macro_rules! impl_scalar { }; } -/// Represents a GraphQL output value -#[async_trait::async_trait] -impl GQLOutputValue for T { - async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result { - let mut result = serde_json::Map::::new(); - crate::resolver::do_resolve(ctx, value, &mut result).await?; - Ok(result.into()) +impl Type for &T { + fn type_name() -> Cow<'static, str> { + T::type_name() + } + + fn create_type_info(registry: &mut Registry) -> String { + T::create_type_info(registry) + } +} + +#[async_trait::async_trait] +impl OutputValueType for &T { + async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result { + T::resolve(*value, ctx).await } } diff --git a/src/context.rs b/src/context.rs index 8343735c..d7d0207a 100644 --- a/src/context.rs +++ b/src/context.rs @@ -1,5 +1,5 @@ use crate::registry::Registry; -use crate::{ErrorWithPosition, GQLInputValue, GQLType, QueryError, Result}; +use crate::{ErrorWithPosition, InputValueType, QueryError, Result, Type}; use bytes::Bytes; use fnv::FnvHasher; use graphql_parser::query::{ @@ -235,7 +235,7 @@ impl<'a, T> ContextBase<'a, T> { .map(|(_, value)| value) { let value = self.resolve_input_value(value.clone())?; - let res: bool = GQLInputValue::parse(&value).ok_or_else(|| { + let res: bool = InputValueType::parse(&value).ok_or_else(|| { QueryError::ExpectedType { expect: bool::qualified_type_name(), actual: value, @@ -262,7 +262,7 @@ impl<'a, T> ContextBase<'a, T> { .map(|(_, value)| value) { let value = self.resolve_input_value(value.clone())?; - let res: bool = GQLInputValue::parse(&value).ok_or_else(|| { + let res: bool = InputValueType::parse(&value).ok_or_else(|| { QueryError::ExpectedType { expect: bool::qualified_type_name(), actual: value, @@ -296,7 +296,7 @@ impl<'a, T> ContextBase<'a, T> { impl<'a> ContextBase<'a, &'a Field> { #[doc(hidden)] - pub fn param_value Value>( + pub fn param_value Value>( &self, name: &str, default: F, @@ -310,7 +310,7 @@ impl<'a> ContextBase<'a, &'a Field> { { Some(value) => { let value = self.resolve_input_value(value)?; - let res = GQLInputValue::parse(&value).ok_or_else(|| { + let res = InputValueType::parse(&value).ok_or_else(|| { QueryError::ExpectedType { expect: T::qualified_type_name(), actual: value, @@ -321,7 +321,7 @@ impl<'a> ContextBase<'a, &'a Field> { } None => { let value = default(); - let res = GQLInputValue::parse(&value).ok_or_else(|| { + let res = InputValueType::parse(&value).ok_or_else(|| { QueryError::ExpectedType { expect: T::qualified_type_name(), actual: value.clone(), diff --git a/src/error.rs b/src/error.rs index f894371d..a45b360b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -71,6 +71,9 @@ pub enum QueryError { #[error("Unrecognized inline fragment \"{name}\" on type \"{object}\"")] UnrecognizedInlineFragment { object: String, name: String }, + + #[error("Argument \"{field_name}\" must be a non-negative integer")] + ArgumentMustBeNonNegative { field_name: String }, } pub trait ErrorWithPosition { diff --git a/src/http/mod.rs b/src/http/mod.rs index a6753802..d99e63fc 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -6,7 +6,7 @@ pub use playground_source::playground_source; use crate::error::{RuleError, RuleErrors}; use crate::query::PreparedQuery; -use crate::{GQLObject, GQLSubscription, PositionError, Result, Schema, Variables}; +use crate::{ObjectType, PositionError, Result, Schema, SubscriptionType, Variables}; use graphql_parser::Pos; use serde::ser::{SerializeMap, SerializeSeq}; use serde::{Serialize, Serializer}; @@ -26,9 +26,9 @@ impl GQLRequest { schema: &Schema, ) -> GQLResponse where - Query: GQLObject + Send + Sync, - Mutation: GQLObject + Send + Sync, - Subscription: GQLSubscription + Send + Sync, + Query: ObjectType + Send + Sync, + Mutation: ObjectType + Send + Sync, + Subscription: SubscriptionType + Send + Sync, { match self.prepare(schema) { Ok(query) => GQLResponse(query.execute().await), @@ -41,9 +41,9 @@ impl GQLRequest { schema: &'a Schema, ) -> Result> where - Query: GQLObject + Send + Sync, - Mutation: GQLObject + Send + Sync, - Subscription: GQLSubscription + Send + Sync, + Query: ObjectType + Send + Sync, + Mutation: ObjectType + Send + Sync, + Subscription: SubscriptionType + Send + Sync, { let vars = match self.variables.take() { Some(value) => match Variables::parse_from_json(value) { diff --git a/src/lib.rs b/src/lib.rs index 403b2840..1154ad82 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -82,7 +82,7 @@ pub use serde_json; pub mod http; -pub use base::GQLScalar; +pub use base::Scalar; pub use context::{Context, Variables}; pub use error::{ErrorWithPosition, PositionError, QueryError, QueryParseError}; pub use graphql_parser::query::Value; @@ -90,7 +90,10 @@ pub use query::{PreparedQuery, QueryBuilder}; pub use scalars::ID; pub use schema::Schema; pub use subscription::SubscribeBuilder; -pub use types::{GQLEmptyMutation, GQLEmptySubscription, Upload}; +pub use types::{ + Connection, DataSource, EmptyEdgeFields, EmptyMutation, EmptySubscription, QueryOperation, + Upload, +}; pub type Result = anyhow::Result; pub type Error = anyhow::Error; @@ -101,15 +104,15 @@ pub use context::ContextSelectionSet; #[doc(hidden)] pub mod registry; #[doc(hidden)] -pub use base::{GQLInputObject, GQLInputValue, GQLObject, GQLOutputValue, GQLType}; +pub use base::{InputObjectType, InputValueType, ObjectType, OutputValueType, Type}; #[doc(hidden)] pub use context::ContextBase; #[doc(hidden)] -pub use resolver::do_resolve; +pub use resolver::{do_resolve, do_resolve_values}; #[doc(hidden)] -pub use subscription::{GQLSubscription, Subscribe}; +pub use subscription::{Subscribe, SubscriptionType}; #[doc(hidden)] -pub use types::{GQLEnum, GQLEnumItem}; +pub use types::{EnumItem, EnumType}; /// Define a GraphQL object /// @@ -143,8 +146,8 @@ pub use types::{GQLEnum, GQLEnumItem}; /// - Vec, such as `Vec` /// - Slice, such as `&[i32]` /// - Option, such as `Option` -/// - GQLObject and `&GQLObject` -/// - GQLEnum +/// - Object and &Object +/// - Enum /// - Result, such as `Result` /// /// # Context @@ -192,7 +195,7 @@ pub use types::{GQLEnum, GQLEnumItem}; /// /// #[async_std::main] /// async fn main() { -/// let schema = Schema::new(MyObject{ value: 10 }, GQLEmptyMutation, GQLEmptySubscription); +/// let schema = Schema::new(MyObject{ value: 10 }, EmptyMutation, EmptySubscription); /// let res = schema.query(r#"{ /// value /// valueRef @@ -259,7 +262,7 @@ pub use async_graphql_derive::Object; /// /// #[async_std::main] /// async fn main() { -/// let schema = Schema::new(MyObject{ value1: MyEnum::A, value2: MyEnum::B }, GQLEmptyMutation, GQLEmptySubscription); +/// let schema = Schema::new(MyObject{ value1: MyEnum::A, value2: MyEnum::B }, EmptyMutation, EmptySubscription); /// let res = schema.query("{ value1 value2 }").execute().await.unwrap(); /// assert_eq!(res, serde_json::json!({ "value1": "A", "value2": "b" })); /// } @@ -307,7 +310,7 @@ pub use async_graphql_derive::Enum; /// /// #[async_std::main] /// async fn main() { -/// let schema = Schema::new(MyObject, GQLEmptyMutation, GQLEmptySubscription); +/// let schema = Schema::new(MyObject, EmptyMutation, EmptySubscription); /// let res = schema.query(r#" /// { /// value1: value(input:{a:9, b:3}) @@ -412,7 +415,7 @@ pub use async_graphql_derive::InputObject; /// /// #[async_std::main] /// async fn main() { -/// let schema = Schema::new(QueryRoot, GQLEmptyMutation, GQLEmptySubscription).data("hello".to_string()); +/// let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription).data("hello".to_string()); /// let res = schema.query(r#" /// { /// typeA { diff --git a/src/model/field.rs b/src/model/field.rs index aca0cbc8..0e30c7d1 100644 --- a/src/model/field.rs +++ b/src/model/field.rs @@ -24,14 +24,17 @@ impl<'a> __Field<'a> { #[field] async fn args(&self) -> Vec<__InputValue<'a>> { - self.field + let mut args = self + .field .args .values() .map(|input_value| __InputValue { registry: self.registry, input_value, }) - .collect() + .collect::>(); + args.sort_by(|a, b| a.input_value.name.cmp(b.input_value.name)); + args } #[field(name = "type")] diff --git a/src/model/type.rs b/src/model/type.rs index a146a394..15066b84 100644 --- a/src/model/type.rs +++ b/src/model/type.rs @@ -1,6 +1,5 @@ use crate::model::{__EnumValue, __Field, __InputValue, __TypeKind}; use crate::registry; -use crate::registry::{Type, TypeName}; use async_graphql_derive::Object; enum TypeDetail<'a> { @@ -23,16 +22,16 @@ impl<'a> __Type<'a> { } pub fn new(registry: &'a registry::Registry, type_name: &str) -> __Type<'a> { - match TypeName::create(type_name) { - TypeName::NonNull(ty) => __Type { + match registry::TypeName::create(type_name) { + registry::TypeName::NonNull(ty) => __Type { registry, detail: TypeDetail::NonNull(ty.to_string()), }, - TypeName::List(ty) => __Type { + registry::TypeName::List(ty) => __Type { registry, detail: TypeDetail::List(ty.to_string()), }, - TypeName::Named(ty) => __Type { + registry::TypeName::Named(ty) => __Type { registry, detail: TypeDetail::Simple(®istry.types[ty]), }, @@ -99,19 +98,19 @@ impl<'a> __Type<'a> { ) -> Option>> { if let TypeDetail::Simple(ty) = &self.detail { ty.fields().and_then(|fields| { - Some( - fields - .values() - .filter(|field| { - (include_deprecated || field.deprecation.is_none()) - && !field.name.starts_with("__") - }) - .map(|field| __Field { - registry: self.registry, - field, - }) - .collect(), - ) + let mut fields = fields + .values() + .filter(|field| { + (include_deprecated || field.deprecation.is_none()) + && !field.name.starts_with("__") + }) + .map(|field| __Field { + registry: self.registry, + field, + }) + .collect::>(); + fields.sort_by(|a, b| a.field.name.cmp(&b.field.name)); + Some(fields) }) } else { None @@ -120,11 +119,11 @@ impl<'a> __Type<'a> { #[field] async fn interfaces(&self) -> Option>> { - if let TypeDetail::Simple(Type::Object { name, .. }) = &self.detail { + if let TypeDetail::Simple(registry::Type::Object { name, .. }) = &self.detail { Some( self.registry .implements - .get(*name) + .get(name) .unwrap_or(&Default::default()) .iter() .map(|ty| __Type::new(self.registry, ty)) @@ -137,14 +136,16 @@ impl<'a> __Type<'a> { #[field] async fn possible_types(&self) -> Option>> { - if let TypeDetail::Simple(Type::Interface { possible_types, .. }) = &self.detail { + if let TypeDetail::Simple(registry::Type::Interface { possible_types, .. }) = &self.detail { Some( possible_types .iter() .map(|ty| __Type::new(self.registry, ty)) .collect(), ) - } else if let TypeDetail::Simple(Type::Union { possible_types, .. }) = &self.detail { + } else if let TypeDetail::Simple(registry::Type::Union { possible_types, .. }) = + &self.detail + { Some( possible_types .iter() @@ -161,7 +162,7 @@ impl<'a> __Type<'a> { &self, #[arg(default = "false")] include_deprecated: bool, ) -> Option>> { - if let TypeDetail::Simple(Type::Enum { enum_values, .. }) = &self.detail { + if let TypeDetail::Simple(registry::Type::Enum { enum_values, .. }) = &self.detail { Some( enum_values .values() @@ -179,7 +180,7 @@ impl<'a> __Type<'a> { #[field] async fn input_fields(&self) -> Option>> { - if let TypeDetail::Simple(Type::InputObject { input_fields, .. }) = &self.detail { + if let TypeDetail::Simple(registry::Type::InputObject { input_fields, .. }) = &self.detail { Some( input_fields .iter() diff --git a/src/query.rs b/src/query.rs index 1d0234eb..ca39519d 100644 --- a/src/query.rs +++ b/src/query.rs @@ -2,8 +2,8 @@ use crate::context::Data; use crate::registry::Registry; use crate::types::QueryRoot; use crate::validation::check_rules; -use crate::{ContextBase, GQLOutputValue, Result}; -use crate::{GQLObject, QueryError, QueryParseError, Variables}; +use crate::{ContextBase, OutputValueType, Result}; +use crate::{ObjectType, QueryError, QueryParseError, Variables}; use bytes::Bytes; use graphql_parser::parse_query; use graphql_parser::query::{ @@ -112,8 +112,8 @@ impl<'a, Query, Mutation> QueryBuilder<'a, Query, Mutation> { /// Execute the query. pub async fn execute(self) -> Result where - Query: GQLObject + Send + Sync, - Mutation: GQLObject + Send + Sync, + Query: ObjectType + Send + Sync, + Mutation: ObjectType + Send + Sync, { self.prepare()?.execute().await } @@ -159,8 +159,8 @@ impl<'a, Query, Mutation> PreparedQuery<'a, Query, Mutation> { /// Execute the query. pub async fn execute(self) -> Result where - Query: GQLObject + Send + Sync, - Mutation: GQLObject + Send + Sync, + Query: ObjectType + Send + Sync, + Mutation: ObjectType + Send + Sync, { let ctx = ContextBase { item: &self.selection_set, @@ -172,8 +172,8 @@ impl<'a, Query, Mutation> PreparedQuery<'a, Query, Mutation> { }; match self.root { - Root::Query(query) => return GQLOutputValue::resolve(query, &ctx).await, - Root::Mutation(mutation) => return GQLOutputValue::resolve(mutation, &ctx).await, + Root::Query(query) => return OutputValueType::resolve(query, &ctx).await, + Root::Mutation(mutation) => return OutputValueType::resolve(mutation, &ctx).await, } } } diff --git a/src/registry.rs b/src/registry.rs index 6bb9268b..a9cab5bc 100644 --- a/src/registry.rs +++ b/src/registry.rs @@ -1,4 +1,4 @@ -use crate::{model, GQLType, Value}; +use crate::{model, Value}; use graphql_parser::query::Type as ParsedType; use std::collections::{HashMap, HashSet}; @@ -52,6 +52,7 @@ impl<'a> TypeName<'a> { } } +#[derive(Clone)] pub struct InputValue { pub name: &'static str, pub description: Option<&'static str>, @@ -59,14 +60,16 @@ pub struct InputValue { pub default_value: Option<&'static str>, } +#[derive(Clone)] pub struct Field { - pub name: &'static str, + pub name: String, pub description: Option<&'static str>, pub args: HashMap<&'static str, InputValue>, pub ty: String, pub deprecation: Option<&'static str>, } +#[derive(Clone)] pub struct EnumValue { pub name: &'static str, pub description: Option<&'static str>, @@ -80,28 +83,28 @@ pub enum Type { is_valid: fn(value: &Value) -> bool, }, Object { - name: &'static str, + name: String, description: Option<&'static str>, - fields: HashMap<&'static str, Field>, + fields: HashMap, }, Interface { - name: &'static str, + name: String, description: Option<&'static str>, - fields: HashMap<&'static str, Field>, + fields: HashMap, possible_types: HashSet, }, Union { - name: &'static str, + name: String, description: Option<&'static str>, possible_types: HashSet, }, Enum { - name: &'static str, + name: String, description: Option<&'static str>, enum_values: HashMap<&'static str, EnumValue>, }, InputObject { - name: &'static str, + name: String, description: Option<&'static str>, input_fields: Vec, }, @@ -112,7 +115,7 @@ impl Type { self.fields().and_then(|fields| fields.get(name)) } - pub fn fields(&self) -> Option<&HashMap<&'static str, Field>> { + pub fn fields(&self) -> Option<&HashMap> { match self { Type::Object { fields, .. } => Some(&fields), Type::Interface { fields, .. } => Some(&fields), @@ -183,13 +186,16 @@ pub struct Registry { } impl Registry { - pub fn create_type Type>(&mut self, mut f: F) -> String { + pub fn create_type Type>( + &mut self, + mut f: F, + ) -> String { let name = T::type_name(); if !self.types.contains_key(name.as_ref()) { self.types.insert( name.to_string(), Type::Object { - name: "", + name: "".to_string(), description: None, fields: Default::default(), }, @@ -197,9 +203,9 @@ impl Registry { let mut ty = f(self); if let Type::Object { fields, .. } = &mut ty { fields.insert( - "__typename", + "__typename".to_string(), Field { - name: "__typename", + name: "__typename".to_string(), description: None, args: Default::default(), ty: "String!".to_string(), diff --git a/src/resolver.rs b/src/resolver.rs index 0be5a4e9..bcb794b2 100644 --- a/src/resolver.rs +++ b/src/resolver.rs @@ -1,4 +1,4 @@ -use crate::{ContextSelectionSet, ErrorWithPosition, GQLObject, QueryError, Result}; +use crate::{ContextSelectionSet, ErrorWithPosition, ObjectType, QueryError, Result}; use graphql_parser::query::{Selection, TypeCondition}; use std::future::Future; use std::pin::Pin; @@ -9,7 +9,7 @@ struct Resolver<'a, T> { result: &'a mut serde_json::Map, } -impl<'a, T: GQLObject + Send + Sync> Resolver<'a, T> { +impl<'a, T: ObjectType + Send + Sync> Resolver<'a, T> { pub fn resolve(&'a mut self) -> Pin> + 'a + Send>> { Box::pin(async move { if self.ctx.items.is_empty() { @@ -83,7 +83,22 @@ impl<'a, T: GQLObject + Send + Sync> Resolver<'a, T> { } } -pub async fn do_resolve<'a, T: GQLObject + Send + Sync>( +pub async fn do_resolve<'a, T: ObjectType + Send + Sync>( + ctx: &'a ContextSelectionSet<'a>, + root: &'a T, +) -> Result { + let mut result = serde_json::Map::::new(); + Resolver { + ctx, + obj: root, + result: &mut result, + } + .resolve() + .await?; + Ok(result.into()) +} + +pub async fn do_resolve_values<'a, T: ObjectType + Send + Sync>( ctx: &'a ContextSelectionSet<'a>, root: &'a T, result: &mut serde_json::Map, @@ -91,7 +106,7 @@ pub async fn do_resolve<'a, T: GQLObject + Send + Sync>( Resolver { ctx, obj: root, - result, + result: result, } .resolve() .await?; diff --git a/src/scalars/bool.rs b/src/scalars/bool.rs index 2a840643..e8c27cf0 100644 --- a/src/scalars/bool.rs +++ b/src/scalars/bool.rs @@ -1,6 +1,6 @@ -use crate::{impl_scalar_internal, GQLScalar, Result, Value}; +use crate::{impl_scalar_internal, Result, Scalar, Value}; -impl GQLScalar for bool { +impl Scalar for bool { fn type_name() -> &'static str { "Boolean" } diff --git a/src/scalars/datetime.rs b/src/scalars/datetime.rs index 457357de..b5e0b908 100644 --- a/src/scalars/datetime.rs +++ b/src/scalars/datetime.rs @@ -1,7 +1,7 @@ -use crate::{impl_scalar_internal, GQLScalar, Result, Value}; +use crate::{impl_scalar_internal, Result, Scalar, Value}; use chrono::{DateTime, TimeZone, Utc}; -impl GQLScalar for DateTime { +impl Scalar for DateTime { fn type_name() -> &'static str { "DateTime" } diff --git a/src/scalars/floats.rs b/src/scalars/floats.rs index 8fced574..3c622c38 100644 --- a/src/scalars/floats.rs +++ b/src/scalars/floats.rs @@ -1,9 +1,9 @@ -use crate::{impl_scalar_internal, GQLScalar, Result, Value}; +use crate::{impl_scalar_internal, Result, Scalar, Value}; macro_rules! impl_float_scalars { ($($ty:ty),*) => { $( - impl GQLScalar for $ty { + impl Scalar for $ty { fn type_name() -> &'static str { "Float" } diff --git a/src/scalars/id.rs b/src/scalars/id.rs index fd025729..c2391595 100644 --- a/src/scalars/id.rs +++ b/src/scalars/id.rs @@ -1,4 +1,4 @@ -use crate::{impl_scalar_internal, GQLScalar, Result, Value}; +use crate::{impl_scalar_internal, Result, Scalar, Value}; use std::ops::{Deref, DerefMut}; /// ID scalar @@ -39,7 +39,7 @@ impl From for ID { } } -impl GQLScalar for ID { +impl Scalar for ID { fn type_name() -> &'static str { "ID" } diff --git a/src/scalars/integers.rs b/src/scalars/integers.rs index 003440d5..48fdd1b5 100644 --- a/src/scalars/integers.rs +++ b/src/scalars/integers.rs @@ -1,9 +1,9 @@ -use crate::{impl_scalar_internal, GQLScalar, Result, Value}; +use crate::{impl_scalar_internal, Result, Scalar, Value}; macro_rules! impl_integer_scalars { ($($ty:ty),*) => { $( - impl GQLScalar for $ty { + impl Scalar for $ty { fn type_name() -> &'static str { "Int" } diff --git a/src/scalars/mod.rs b/src/scalars/mod.rs index 5e784800..c48f4ae1 100644 --- a/src/scalars/mod.rs +++ b/src/scalars/mod.rs @@ -14,43 +14,43 @@ pub use id::ID; #[cfg(test)] mod tests { use super::ID; - use crate::GQLType; + use crate::Type; use chrono::{DateTime, Utc}; use uuid::Uuid; #[test] fn test_scalar_type() { - assert_eq!(::type_name(), "Boolean"); - assert_eq!(::qualified_type_name(), "Boolean!"); + assert_eq!(::type_name(), "Boolean"); + assert_eq!(::qualified_type_name(), "Boolean!"); - assert_eq!(::type_name(), "Int"); - assert_eq!(::qualified_type_name(), "Int!"); + assert_eq!(::type_name(), "Int"); + assert_eq!(::qualified_type_name(), "Int!"); - assert_eq!(::type_name(), "Float"); - assert_eq!(::qualified_type_name(), "Float!"); + assert_eq!(::type_name(), "Float"); + assert_eq!(::qualified_type_name(), "Float!"); - assert_eq!(<&str as GQLType>::type_name(), "String"); - assert_eq!(<&str as GQLType>::qualified_type_name(), "String!"); + assert_eq!(<&str as Type>::type_name(), "String"); + assert_eq!(<&str as Type>::qualified_type_name(), "String!"); - assert_eq!(::type_name(), "String"); - assert_eq!(::qualified_type_name(), "String!"); + assert_eq!(::type_name(), "String"); + assert_eq!(::qualified_type_name(), "String!"); - assert_eq!(::type_name(), "ID"); - assert_eq!(::qualified_type_name(), "ID!"); + assert_eq!(::type_name(), "ID"); + assert_eq!(::qualified_type_name(), "ID!"); #[cfg(feature = "chrono")] { - assert_eq!( as GQLType>::type_name(), "DateTime"); + assert_eq!( as Type>::type_name(), "DateTime"); assert_eq!( - as GQLType>::qualified_type_name(), + as Type>::qualified_type_name(), "DateTime!" ); } #[cfg(feature = "uuid")] { - assert_eq!(::type_name(), "UUID"); - assert_eq!(::qualified_type_name(), "UUID!"); + assert_eq!(::type_name(), "UUID"); + assert_eq!(::qualified_type_name(), "UUID!"); } } } diff --git a/src/scalars/string.rs b/src/scalars/string.rs index ee089d0d..a0c5615b 100644 --- a/src/scalars/string.rs +++ b/src/scalars/string.rs @@ -1,12 +1,12 @@ use crate::{ - impl_scalar_internal, registry, ContextSelectionSet, GQLOutputValue, GQLScalar, GQLType, - Result, Value, + impl_scalar_internal, registry, ContextSelectionSet, OutputValueType, Result, Scalar, Type, + Value, }; use std::borrow::Cow; const STRING_DESC:&'static str = "The `String` scalar type represents textual data, represented as UTF-8 character sequences. The String type is most often used by GraphQL to represent free-form human-readable text."; -impl GQLScalar for String { +impl Scalar for String { fn type_name() -> &'static str { "String" } @@ -36,7 +36,7 @@ impl GQLScalar for String { impl_scalar_internal!(String); -impl<'a> GQLType for &'a str { +impl<'a> Type for &'a str { fn type_name() -> Cow<'static, str> { Cow::Borrowed("String") } @@ -54,7 +54,7 @@ impl<'a> GQLType for &'a str { } #[async_trait::async_trait] -impl<'a> GQLOutputValue for &'a str { +impl<'a> OutputValueType for &'a str { async fn resolve(value: &Self, _: &ContextSelectionSet<'_>) -> Result { Ok(value.to_string().into()) } diff --git a/src/scalars/uuid.rs b/src/scalars/uuid.rs index 39f8c114..98ee4b0a 100644 --- a/src/scalars/uuid.rs +++ b/src/scalars/uuid.rs @@ -1,7 +1,7 @@ -use crate::{impl_scalar_internal, GQLScalar, Result, Value}; +use crate::{impl_scalar_internal, Result, Scalar, Value}; use uuid::Uuid; -impl GQLScalar for Uuid { +impl Scalar for Uuid { fn type_name() -> &'static str { "UUID" } diff --git a/src/schema.rs b/src/schema.rs index 6f5efd11..5fbb188b 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -3,7 +3,7 @@ use crate::model::__DirectiveLocation; use crate::query::QueryBuilder; use crate::registry::{Directive, InputValue, Registry}; use crate::types::QueryRoot; -use crate::{GQLObject, GQLSubscription, GQLType, SubscribeBuilder}; +use crate::{ObjectType, SubscribeBuilder, SubscriptionType, Type}; use std::any::Any; use std::collections::HashMap; @@ -16,13 +16,14 @@ pub struct Schema { pub(crate) data: Data, } -impl +impl Schema { /// Create a schema. /// /// The root object for the query and Mutation needs to be specified. - /// If there is no mutation, you can use `GQLEmptyMutation`. + /// If there is no mutation, you can use `EmptyMutation`. + /// If there is no subscription, you can use `EmptySubscription`. pub fn new(query: Query, mutation: Mutation, subscription: Subscription) -> Self { let mut registry = Registry { types: Default::default(), diff --git a/src/subscription.rs b/src/subscription.rs index 35f7bc62..b0d7f2ad 100644 --- a/src/subscription.rs +++ b/src/subscription.rs @@ -1,8 +1,7 @@ use crate::registry::Registry; use crate::validation::check_rules; use crate::{ - ContextBase, ContextSelectionSet, GQLType, QueryError, QueryParseError, Result, Schema, - Variables, + ContextBase, ContextSelectionSet, QueryError, QueryParseError, Result, Schema, Type, Variables, }; use graphql_parser::parse_query; use graphql_parser::query::{ @@ -26,7 +25,7 @@ impl Subscribe { msg: &(dyn Any + Send + Sync), ) -> Result> where - Subscription: GQLSubscription + Sync + Send + 'static, + Subscription: SubscriptionType + Sync + Send + 'static, { let ctx = ContextBase::<()> { item: (), @@ -42,8 +41,8 @@ impl Subscribe { /// Represents a GraphQL subscription object #[async_trait::async_trait] -pub trait GQLSubscription: GQLType { - /// This function returns true of type `GQLEmptySubscription` only +pub trait SubscriptionType: Type { + /// This function returns true of type `EmptySubscription` only #[doc(hidden)] fn is_empty() -> bool { return false; @@ -89,7 +88,7 @@ pub trait GQLSubscription: GQLType { ) -> Result>; } -fn create_types( +fn create_types( ctx: &ContextSelectionSet<'_>, fragments: &HashMap, types: &mut HashMap, @@ -131,6 +130,7 @@ fn create_types( Ok(()) } +/// Subscribe builder pub struct SubscribeBuilder<'a, Subscription> { pub(crate) subscription: &'a Subscription, pub(crate) registry: &'a Registry, @@ -141,7 +141,7 @@ pub struct SubscribeBuilder<'a, Subscription> { impl<'a, Subscription> SubscribeBuilder<'a, Subscription> where - Subscription: GQLSubscription, + Subscription: SubscriptionType, { /// Specify the operation name. pub fn operator_name(self, name: &'a str) -> Self { diff --git a/src/types/connection/connection.rs b/src/types/connection/connection.rs new file mode 100644 index 00000000..c7b13495 --- /dev/null +++ b/src/types/connection/connection.rs @@ -0,0 +1,189 @@ +use crate::types::connection::edge::Edge; +use crate::types::connection::page_info::PageInfo; +use crate::{ + do_resolve, registry, Context, ContextSelectionSet, ErrorWithPosition, ObjectType, + OutputValueType, QueryError, Result, Type, +}; +use graphql_parser::query::Field; +use inflector::Inflector; +use std::borrow::Cow; +use std::collections::HashMap; + +/// Connection type. +/// +/// Connection is the result of a query for `DataSource`, +/// If the `T` type is `OutputValueType`, you can return the value as a field function directly, +/// otherwise you can use the `Connection::map` function to convert to a type that implements `OutputValueType`. +/// `E` is an extension object type that extends the edge fields. +pub struct Connection { + total_count: Option, + page_info: PageInfo, + nodes: Vec<(String, E, T)>, +} + +impl Connection { + pub fn new( + total_count: Option, + has_previous_page: bool, + has_next_page: bool, + nodes: Vec<(String, E, T)>, + ) -> Self { + Connection { + total_count, + page_info: PageInfo { + has_previous_page, + has_next_page, + start_cursor: nodes.first().map(|(cursor, _, _)| cursor.clone()), + end_cursor: nodes.last().map(|(cursor, _, _)| cursor.clone()), + }, + nodes, + } + } + + /// Convert node type. + pub fn map(self, mut f: F) -> Connection + where + F: FnMut(T) -> O, + { + Connection { + total_count: self.total_count, + page_info: self.page_info, + nodes: self + .nodes + .into_iter() + .map(|(cursor, edge_type, node)| (cursor, edge_type, f(node))) + .collect(), + } + } +} + +impl Type for Connection { + fn type_name() -> Cow<'static, str> { + Cow::Owned(format!("{}Connection", T::type_name())) + } + + fn create_type_info(registry: &mut registry::Registry) -> String { + registry.create_type::(|registry| registry::Type::Object { + name: Self::type_name().to_string(), + description: None, + fields: { + let mut fields = HashMap::new(); + + fields.insert( + "pageInfo".to_string(), + registry::Field { + name: "pageInfo".to_string(), + description: Some("Information to aid in pagination."), + args: Default::default(), + ty: PageInfo::create_type_info(registry), + deprecation: None, + }, + ); + + fields.insert( + "edges".to_string(), + registry::Field { + name: "edges".to_string(), + description: Some("A list of edges."), + args: Default::default(), + ty: >>> as Type>::create_type_info(registry), + deprecation: None, + }, + ); + + fields.insert( + "totalCount".to_string(), + registry::Field { + name: "totalCount".to_string(), + description: Some(r#"A count of the total number of objects in this connection, ignoring pagination. This allows a client to fetch the first five objects by passing "5" as the argument to "first", then fetch the total count so it could display "5 of 83", for example."#), + args: Default::default(), + ty: Option::::create_type_info(registry), + deprecation: None, + }, + ); + + let elements_name = T::type_name().to_plural().to_camel_case(); + fields.insert(elements_name.clone(),registry::Field{ + name: elements_name, + description: Some(r#"A list of all of the objects returned in the connection. This is a convenience field provided for quickly exploring the API; rather than querying for "{ edges { node } }" when no edge data is needed, this field can be be used instead. Note that when clients like Relay need to fetch the "cursor" field on the edge to enable efficient pagination, this shortcut cannot be used, and the full "{ edges { node } }" version should be used instead."#), + args: Default::default(), + ty: Vec::::type_name().to_string(), + deprecation: None + }); + + fields + }, + }) + } +} + +#[async_trait::async_trait] +impl ObjectType + for Connection +{ + async fn resolve_field(&self, ctx: &Context<'_>, field: &Field) -> Result { + if field.name.as_str() == "pageInfo" { + let ctx_obj = ctx.with_item(&field.selection_set); + let page_info = &self.page_info; + return OutputValueType::resolve(page_info, &ctx_obj) + .await + .map_err(|err| err.with_position(field.position).into()); + } else if field.name.as_str() == "edges" { + let ctx_obj = ctx.with_item(&field.selection_set); + let edges = self + .nodes + .iter() + .map(|(cursor, extra_type, node)| Edge { + cursor, + extra_type, + node, + }) + .collect::>(); + return OutputValueType::resolve(&edges, &ctx_obj) + .await + .map_err(|err| err.with_position(field.position).into()); + } else if field.name.as_str() == "totalCount" { + return Ok(self + .total_count + .map(|n| (n as i32).into()) + .unwrap_or_else(|| serde_json::Value::Null)); + } else if field.name.as_str() == T::type_name().to_plural().to_camel_case() { + let ctx_obj = ctx.with_item(&field.selection_set); + let items = self + .nodes + .iter() + .map(|(_, _, item)| item) + .collect::>(); + return OutputValueType::resolve(&items, &ctx_obj) + .await + .map_err(|err| err.with_position(field.position).into()); + } + + anyhow::bail!(QueryError::FieldNotFound { + field_name: field.name.clone(), + object: Connection::::type_name().to_string(), + } + .with_position(field.position)) + } + + async fn resolve_inline_fragment( + &self, + name: &str, + _ctx: &ContextSelectionSet<'_>, + _result: &mut serde_json::Map, + ) -> Result<()> { + anyhow::bail!(QueryError::UnrecognizedInlineFragment { + object: Connection::::type_name().to_string(), + name: name.to_string(), + }); + } +} + +#[async_trait::async_trait] +impl OutputValueType + for Connection +{ + async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result { + do_resolve(ctx, value).await + } +} diff --git a/src/types/connection/edge.rs b/src/types/connection/edge.rs new file mode 100644 index 00000000..33af2cc9 --- /dev/null +++ b/src/types/connection/edge.rs @@ -0,0 +1,113 @@ +use crate::{ + do_resolve, registry, Context, ContextSelectionSet, ErrorWithPosition, ObjectType, + OutputValueType, QueryError, Result, Type, +}; +use graphql_parser::query::Field; +use std::borrow::Cow; +use std::collections::HashMap; + +pub struct Edge<'a, T, E> { + pub cursor: &'a str, + pub node: &'a T, + pub extra_type: &'a E, +} + +impl<'a, T, E> Type for Edge<'a, T, E> +where + T: OutputValueType + Send + Sync + 'a, + E: ObjectType + Sync + Send + 'a, +{ + fn type_name() -> Cow<'static, str> { + Cow::Owned(format!("{}Edge", T::type_name())) + } + + fn create_type_info(registry: &mut registry::Registry) -> String { + registry.create_type::(|registry| { + E::create_type_info(registry); + let extra_fields = if let Some(registry::Type::Object { fields, .. }) = + registry.types.get_mut(E::type_name().as_ref()) + { + fields.clone() + } else { + unreachable!() + }; + registry.types.remove(E::type_name().as_ref()); + + registry::Type::Object { + name: Self::type_name().to_string(), + description: Some("An edge in a connection."), + fields: { + let mut fields = HashMap::new(); + + fields.insert( + "node".to_string(), + registry::Field { + name: "node".to_string(), + description: Some("The item at the end of the edge"), + args: Default::default(), + ty: T::create_type_info(registry), + deprecation: None, + }, + ); + + fields.insert( + "cursor".to_string(), + registry::Field { + name: "cursor".to_string(), + description: Some("A cursor for use in pagination"), + args: Default::default(), + ty: String::create_type_info(registry), + deprecation: None, + }, + ); + + fields.extend(extra_fields); + fields + }, + } + }) + } +} + +#[async_trait::async_trait] +impl<'a, T, E> ObjectType for Edge<'a, T, E> +where + T: OutputValueType + Send + Sync + 'a, + E: ObjectType + Sync + Send + 'a, +{ + async fn resolve_field(&self, ctx: &Context<'_>, field: &Field) -> Result { + if field.name.as_str() == "node" { + let ctx_obj = ctx.with_item(&field.selection_set); + return OutputValueType::resolve(self.node, &ctx_obj) + .await + .map_err(|err| err.with_position(field.position).into()); + } else if field.name.as_str() == "cursor" { + return Ok(self.cursor.into()); + } + + self.extra_type.resolve_field(ctx, field).await + } + + async fn resolve_inline_fragment( + &self, + name: &str, + _ctx: &ContextSelectionSet<'_>, + _result: &mut serde_json::Map, + ) -> Result<()> { + anyhow::bail!(QueryError::UnrecognizedInlineFragment { + object: as Type>::type_name().to_string(), + name: name.to_string(), + }); + } +} + +#[async_trait::async_trait] +impl<'a, T, E> OutputValueType for Edge<'a, T, E> +where + T: OutputValueType + Send + Sync + 'a, + E: ObjectType + Sync + Send + 'a, +{ + async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result { + do_resolve(ctx, value).await + } +} diff --git a/src/types/connection/mod.rs b/src/types/connection/mod.rs new file mode 100644 index 00000000..0d8084c2 --- /dev/null +++ b/src/types/connection/mod.rs @@ -0,0 +1,205 @@ +mod connection; +mod edge; +mod page_info; +mod slice; + +use crate::{Context, ObjectType, QueryError, Result}; + +pub use connection::Connection; + +/// Connection query operation. +pub enum QueryOperation<'a> { + Forward { + after: Option<&'a str>, + limit: usize, + }, + Backward { + before: Option<&'a str>, + limit: usize, + }, +} + +/// Empty edge extension object. +pub struct EmptyEdgeFields; + +#[async_graphql_derive::Object(internal)] +impl EmptyEdgeFields {} + +/// Data source of GraphQL Cursor Connections type. +/// +/// `Edge` is an extension object type that extends the edge fields, If you don't need it, you can use `EmptyEdgeFields`. +/// +/// # References +/// (GraphQL Cursor Connections Specification)[https://facebook.github.io/relay/graphql/connections.htm] +/// +/// # Examples +/// +/// ```rust +/// use async_graphql::*; +/// use byteorder::{ReadBytesExt, BE}; +/// +/// struct QueryRoot; +/// +/// struct DiffFields(i32); +/// +/// #[Object] +/// impl DiffFields { +/// #[field] +/// async fn diff(&self) -> i32 { +/// self.0 +/// } +/// } +/// +/// struct Numbers; +/// +/// #[async_trait::async_trait] +/// impl DataSource for Numbers { +/// type Element = i32; +/// type Edge = DiffFields; +/// +/// async fn query_operation(&self, operation: &QueryOperation<'_>) -> Result> { +/// let (start, end) = match operation { +/// QueryOperation::Forward {after, limit} => { +/// let start = after.and_then(|after| base64::decode(after).ok()) +/// .and_then(|data| data.as_slice().read_i32::().ok()) +/// .map(|idx| idx + 1) +/// .unwrap_or(0); +/// let end = start + *limit as i32; +/// (start, end) +/// } +/// QueryOperation::Backward {before, limit} => { +/// let end = before.and_then(|before| base64::decode(before).ok()) +/// .and_then(|data| data.as_slice().read_i32::().ok()) +/// .unwrap_or(0); +/// let start = end - *limit as i32; +/// (start, end) +/// } +/// }; +/// +/// let nodes = (start..end).into_iter().map(|n| (base64::encode(n.to_be_bytes()), DiffFields(n - 1000), n)).collect(); +/// Ok(Connection::new(None, true, true, nodes)) +/// } +/// } +/// +/// #[Object] +/// impl QueryRoot { +/// #[field] +/// async fn numbers(&self, ctx: &Context<'_>, +/// after: Option, +/// before: Option, +/// first: Option, +/// last: Option +/// ) -> Result> { +/// Numbers.query(ctx, after, before, first, last).await +/// } +/// } +/// +/// #[async_std::main] +/// async fn main() { +/// let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription); +/// +/// assert_eq!(schema.query("{ numbers(first: 2) { edges { node } } }").execute().await.unwrap(), serde_json::json!({ +/// "numbers": { +/// "edges": [ +/// {"node": 0}, +/// {"node": 1} +/// ] +/// }, +/// })); +/// +/// assert_eq!(schema.query("{ numbers(last: 2) { edges { node diff } } }").execute().await.unwrap(), serde_json::json!({ +/// "numbers": { +/// "edges": [ +/// {"node": -2, "diff": -1002}, +/// {"node": -1, "diff": -1001} +/// ] +/// }, +/// })); +/// } +/// ``` +#[async_trait::async_trait] +pub trait DataSource: Sync + Send { + type Element; + type Edge: ObjectType + Send + Sync; + + async fn query( + &self, + ctx: &Context<'_>, + after: Option, + before: Option, + first: Option, + last: Option, + ) -> Result> { + let operation = if let Some(after) = &after { + QueryOperation::Forward { + after: Some(after), + limit: match first { + Some(value) => { + if value < 0 { + return Err(QueryError::ArgumentMustBeNonNegative { + field_name: ctx.name.clone(), + } + .into()); + } else { + value as usize + } + } + None => 10, + }, + } + } else if let Some(before) = &before { + QueryOperation::Backward { + before: Some(before), + limit: match last { + Some(value) => { + if value < 0 { + return Err(QueryError::ArgumentMustBeNonNegative { + field_name: ctx.name.clone(), + } + .into()); + } else { + value as usize + } + } + None => 10, + }, + } + } else if let Some(first) = first { + QueryOperation::Forward { + after: None, + limit: if first < 0 { + return Err(QueryError::ArgumentMustBeNonNegative { + field_name: ctx.name.clone(), + } + .into()); + } else { + first as usize + }, + } + } else if let Some(last) = last { + QueryOperation::Backward { + before: None, + limit: if last < 0 { + return Err(QueryError::ArgumentMustBeNonNegative { + field_name: ctx.name.clone(), + } + .into()); + } else { + last as usize + }, + } + } else { + QueryOperation::Forward { + after: None, + limit: 10, + } + }; + + self.query_operation(&operation).await + } + + async fn query_operation( + &self, + operation: &QueryOperation<'_>, + ) -> Result>; +} diff --git a/src/types/connection/page_info.rs b/src/types/connection/page_info.rs new file mode 100644 index 00000000..141581e2 --- /dev/null +++ b/src/types/connection/page_info.rs @@ -0,0 +1,31 @@ +use async_graphql_derive::Object; + +pub struct PageInfo { + pub has_previous_page: bool, + pub has_next_page: bool, + pub start_cursor: Option, + pub end_cursor: Option, +} + +#[Object(internal)] +impl PageInfo { + #[field(desc = "When paginating backwards, are there more items?")] + async fn has_previous_page(&self) -> bool { + self.has_previous_page + } + + #[field(desc = "When paginating forwards, are there more items?")] + async fn has_next_page(&self) -> bool { + self.has_next_page + } + + #[field(desc = "When paginating backwards, the cursor to continue.")] + async fn start_cursor(&self) -> &Option { + &self.start_cursor + } + + #[field(desc = "When paginating forwards, the cursor to continue.")] + async fn end_cursor(&self) -> &Option { + &self.end_cursor + } +} diff --git a/src/types/connection/slice.rs b/src/types/connection/slice.rs new file mode 100644 index 00000000..5d60327c --- /dev/null +++ b/src/types/connection/slice.rs @@ -0,0 +1,46 @@ +use crate::types::connection::{EmptyEdgeFields, QueryOperation}; +use crate::{Connection, DataSource, Result}; +use byteorder::{ReadBytesExt, BE}; + +#[async_trait::async_trait] +impl<'a, T: Sync> DataSource for &'a [T] { + type Element = &'a T; + type Edge = EmptyEdgeFields; + + async fn query_operation( + &self, + operation: &QueryOperation<'_>, + ) -> Result> { + let (start, end) = match operation { + QueryOperation::Forward { after, limit } => { + let start = after + .and_then(|after| base64::decode(after).ok()) + .and_then(|data| data.as_slice().read_u32::().ok()) + .map(|idx| (idx + 1) as usize) + .unwrap_or(0); + let end = (start + *limit).min(self.len()); + (start, end) + } + QueryOperation::Backward { before, limit } => { + let end = before + .and_then(|before| base64::decode(before).ok()) + .and_then(|data| data.as_slice().read_u32::().ok()) + .map(|idx| idx as usize) + .unwrap_or(self.len()); + let start = if end < *limit { 0 } else { end - *limit }; + (start, end) + } + }; + + let mut nodes = Vec::with_capacity(end - start); + for (idx, item) in self[start..end].iter().enumerate() { + nodes.push(( + base64::encode((idx as u32).to_be_bytes()), + EmptyEdgeFields, + item, + )); + } + + Ok(Connection::new(None, start > 0, end < self.len(), nodes)) + } +} diff --git a/src/types/empty_mutation.rs b/src/types/empty_mutation.rs index 40b47188..e2048c5b 100644 --- a/src/types/empty_mutation.rs +++ b/src/types/empty_mutation.rs @@ -1,4 +1,6 @@ -use crate::{registry, Context, ContextSelectionSet, GQLObject, GQLType, QueryError, Result}; +use crate::{ + registry, Context, ContextSelectionSet, ObjectType, OutputValueType, QueryError, Result, Type, +}; use graphql_parser::query::Field; use serde_json::{Map, Value}; use std::borrow::Cow; @@ -18,19 +20,19 @@ use std::borrow::Cow; /// impl QueryRoot {} /// /// fn main() { -/// let schema = Schema::new(QueryRoot, GQLEmptyMutation, GQLEmptySubscription); +/// let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription); /// } /// ``` -pub struct GQLEmptyMutation; +pub struct EmptyMutation; -impl GQLType for GQLEmptyMutation { +impl Type for EmptyMutation { fn type_name() -> Cow<'static, str> { Cow::Borrowed("EmptyMutation") } fn create_type_info(registry: &mut registry::Registry) -> String { registry.create_type::(|_| registry::Type::Object { - name: "EmptyMutation", + name: "EmptyMutation".to_string(), description: None, fields: Default::default(), }) @@ -38,13 +40,13 @@ impl GQLType for GQLEmptyMutation { } #[async_trait::async_trait] -impl GQLObject for GQLEmptyMutation { +impl ObjectType for EmptyMutation { fn is_empty() -> bool { - return true; + true } async fn resolve_field(&self, _ctx: &Context<'_>, _name: &Field) -> Result { - return Err(QueryError::NotConfiguredMutations.into()); + unreachable!() } async fn resolve_inline_fragment( @@ -53,6 +55,13 @@ impl GQLObject for GQLEmptyMutation { _ctx: &ContextSelectionSet<'_>, _result: &mut Map, ) -> Result<()> { + unreachable!() + } +} + +#[async_trait::async_trait] +impl OutputValueType for EmptyMutation { + async fn resolve(_value: &Self, _ctx: &ContextSelectionSet<'_>) -> Result { return Err(QueryError::NotConfiguredMutations.into()); } } diff --git a/src/types/empty_subscription.rs b/src/types/empty_subscription.rs index 4a0218de..f25da6a0 100644 --- a/src/types/empty_subscription.rs +++ b/src/types/empty_subscription.rs @@ -1,4 +1,7 @@ -use crate::{registry, ContextBase, GQLSubscription, GQLType, QueryError, Result}; +use crate::{ + registry, ContextBase, ContextSelectionSet, OutputValueType, QueryError, Result, + SubscriptionType, Type, +}; use graphql_parser::query::Field; use serde_json::Value; use std::any::{Any, TypeId}; @@ -9,16 +12,16 @@ use std::collections::HashMap; /// Empty subscription /// /// Only the parameters used to construct the Schema, representing an unconfigured subscription. -pub struct GQLEmptySubscription; +pub struct EmptySubscription; -impl GQLType for GQLEmptySubscription { +impl Type for EmptySubscription { fn type_name() -> Cow<'static, str> { Cow::Borrowed("EmptyMutation") } fn create_type_info(registry: &mut registry::Registry) -> String { registry.create_type::(|_| registry::Type::Object { - name: "EmptySubscription", + name: "EmptySubscription".to_string(), description: None, fields: Default::default(), }) @@ -26,9 +29,13 @@ impl GQLType for GQLEmptySubscription { } #[async_trait::async_trait] -impl GQLSubscription for GQLEmptySubscription { +impl SubscriptionType for EmptySubscription { + fn is_empty() -> bool { + true + } + fn create_type(_field: &Field, _types: &mut HashMap) -> Result<()> { - return Err(QueryError::NotConfiguredSubscriptions.into()); + unreachable!() } async fn resolve( @@ -37,6 +44,13 @@ impl GQLSubscription for GQLEmptySubscription { _types: &HashMap, _msg: &(dyn Any + Send + Sync), ) -> Result> { + unreachable!() + } +} + +#[async_trait::async_trait] +impl OutputValueType for EmptySubscription { + async fn resolve(_value: &Self, _ctx: &ContextSelectionSet<'_>) -> Result { return Err(QueryError::NotConfiguredSubscriptions.into()); } } diff --git a/src/types/enum.rs b/src/types/enum.rs index d6e6e809..3f021e60 100644 --- a/src/types/enum.rs +++ b/src/types/enum.rs @@ -1,14 +1,14 @@ -use crate::{GQLType, Result}; +use crate::{Result, Type}; use graphql_parser::query::Value; -pub struct GQLEnumItem { +pub struct EnumItem { pub name: &'static str, pub value: T, } #[async_trait::async_trait] -pub trait GQLEnum: GQLType + Sized + Eq + Send + Copy + Sized + 'static { - fn items() -> &'static [GQLEnumItem]; +pub trait EnumType: Type + Sized + Eq + Send + Copy + Sized + 'static { + fn items() -> &'static [EnumItem]; fn parse_enum(value: &Value) -> Option { let value = match value { diff --git a/src/types/list.rs b/src/types/list.rs index 66b2fb92..9f203ff7 100644 --- a/src/types/list.rs +++ b/src/types/list.rs @@ -1,7 +1,7 @@ -use crate::{registry, ContextSelectionSet, GQLInputValue, GQLOutputValue, GQLType, Result, Value}; +use crate::{registry, ContextSelectionSet, InputValueType, OutputValueType, Result, Type, Value}; use std::borrow::Cow; -impl GQLType for Vec { +impl Type for Vec { fn type_name() -> Cow<'static, str> { Cow::Owned(format!("[{}]", T::qualified_type_name())) } @@ -16,13 +16,13 @@ impl GQLType for Vec { } } -impl GQLInputValue for Vec { +impl InputValueType for Vec { fn parse(value: &Value) -> Option { match value { Value::List(values) => { let mut result = Vec::new(); for value in values { - result.push(GQLInputValue::parse(value)?); + result.push(InputValueType::parse(value)?); } Some(result) } @@ -32,17 +32,17 @@ impl GQLInputValue for Vec { } #[async_trait::async_trait] -impl GQLOutputValue for Vec { +impl OutputValueType for Vec { async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result { let mut res = Vec::new(); for item in value { - res.push(GQLOutputValue::resolve(item, &ctx).await?); + res.push(OutputValueType::resolve(item, &ctx).await?); } Ok(res.into()) } } -impl GQLType for &[T] { +impl Type for &[T] { fn type_name() -> Cow<'static, str> { Cow::Owned(format!("[{}]", T::type_name())) } @@ -53,32 +53,11 @@ impl GQLType for &[T] { } #[async_trait::async_trait] -impl GQLOutputValue for &[T] { +impl OutputValueType for &[T] { async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result { let mut res = Vec::new(); for item in value.iter() { - res.push(GQLOutputValue::resolve(item, &ctx).await?); - } - Ok(res.into()) - } -} - -impl GQLType for &Vec { - fn type_name() -> Cow<'static, str> { - Cow::Owned(format!("[{}]", T::type_name())) - } - - fn create_type_info(registry: &mut registry::Registry) -> String { - T::create_type_info(registry) - } -} - -#[async_trait::async_trait] -impl GQLOutputValue for &Vec { - async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result { - let mut res = Vec::new(); - for item in value.iter() { - res.push(GQLOutputValue::resolve(item, &ctx).await?); + res.push(OutputValueType::resolve(item, &ctx).await?); } Ok(res.into()) } @@ -86,7 +65,7 @@ impl GQLOutputValue for &Vec { #[cfg(test)] mod tests { - use crate::GQLType; + use crate::Type; #[test] fn test_list_type() { diff --git a/src/types/mod.rs b/src/types/mod.rs index 75bd20a7..5696c142 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -1,3 +1,4 @@ +mod connection; mod empty_mutation; mod empty_subscription; mod r#enum; @@ -6,8 +7,9 @@ mod optional; mod query_root; mod upload; -pub use empty_mutation::GQLEmptyMutation; -pub use empty_subscription::GQLEmptySubscription; +pub use connection::{Connection, DataSource, EmptyEdgeFields, QueryOperation}; +pub use empty_mutation::EmptyMutation; +pub use empty_subscription::EmptySubscription; pub use query_root::QueryRoot; -pub use r#enum::{GQLEnum, GQLEnumItem}; +pub use r#enum::{EnumItem, EnumType}; pub use upload::Upload; diff --git a/src/types/optional.rs b/src/types/optional.rs index 7e2e55f5..f5a73644 100644 --- a/src/types/optional.rs +++ b/src/types/optional.rs @@ -1,7 +1,7 @@ -use crate::{registry, ContextSelectionSet, GQLInputValue, GQLOutputValue, GQLType, Result, Value}; +use crate::{registry, ContextSelectionSet, InputValueType, OutputValueType, Result, Type, Value}; use std::borrow::Cow; -impl GQLType for Option { +impl Type for Option { fn type_name() -> Cow<'static, str> { T::type_name() } @@ -16,7 +16,7 @@ impl GQLType for Option { } } -impl GQLInputValue for Option { +impl InputValueType for Option { fn parse(value: &Value) -> Option { match value { Value::Null => Some(None), @@ -26,38 +26,11 @@ impl GQLInputValue for Option { } #[async_trait::async_trait] -impl GQLOutputValue for Option { +impl OutputValueType for Option { async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result where { if let Some(inner) = value { - GQLOutputValue::resolve(inner, ctx).await - } else { - Ok(serde_json::Value::Null) - } - } -} - -impl GQLType for &Option { - fn type_name() -> Cow<'static, str> { - T::type_name() - } - - fn qualified_type_name() -> String { - T::type_name().to_string() - } - - fn create_type_info(registry: &mut registry::Registry) -> String { - T::create_type_info(registry); - T::type_name().to_string() - } -} - -#[async_trait::async_trait] -impl GQLOutputValue for &Option { - async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result where - { - if let Some(inner) = value { - GQLOutputValue::resolve(inner, ctx).await + OutputValueType::resolve(inner, ctx).await } else { Ok(serde_json::Value::Null) } @@ -66,7 +39,7 @@ impl GQLOutputValue for &Option { #[cfg(test)] mod tests { - use crate::GQLType; + use crate::Type; #[test] fn test_optional_type() { diff --git a/src/types/query_root.rs b/src/types/query_root.rs index 589a49ff..c8155c69 100644 --- a/src/types/query_root.rs +++ b/src/types/query_root.rs @@ -1,8 +1,7 @@ use crate::model::{__Schema, __Type}; -use crate::registry::Type; use crate::{ - registry, Context, ContextSelectionSet, ErrorWithPosition, GQLObject, GQLOutputValue, GQLType, - QueryError, Result, Value, + do_resolve, registry, Context, ContextSelectionSet, ErrorWithPosition, ObjectType, + OutputValueType, QueryError, Result, Type, Value, }; use graphql_parser::query::Field; use std::borrow::Cow; @@ -12,7 +11,7 @@ pub struct QueryRoot { pub inner: T, } -impl GQLType for QueryRoot { +impl Type for QueryRoot { fn type_name() -> Cow<'static, str> { T::type_name() } @@ -20,11 +19,13 @@ impl GQLType for QueryRoot { fn create_type_info(registry: &mut registry::Registry) -> String { let schema_type = __Schema::create_type_info(registry); let root = T::create_type_info(registry); - if let Some(Type::Object { fields, .. }) = registry.types.get_mut(T::type_name().as_ref()) { + if let Some(registry::Type::Object { fields, .. }) = + registry.types.get_mut(T::type_name().as_ref()) + { fields.insert( - "__schema", + "__schema".to_string(), registry::Field { - name: "__schema", + name: "__schema".to_string(), description: Some("Access the current type schema of this server."), args: Default::default(), ty: schema_type, @@ -33,9 +34,9 @@ impl GQLType for QueryRoot { ); fields.insert( - "__type", + "__type".to_string(), registry::Field { - name: "__type", + name: "__type".to_string(), description: Some("Request the type information of a single type."), args: { let mut args = HashMap::new(); @@ -60,11 +61,11 @@ impl GQLType for QueryRoot { } #[async_trait::async_trait] -impl GQLObject for QueryRoot { +impl ObjectType for QueryRoot { async fn resolve_field(&self, ctx: &Context<'_>, field: &Field) -> Result { if field.name.as_str() == "__schema" { let ctx_obj = ctx.with_item(&field.selection_set); - return GQLOutputValue::resolve( + return OutputValueType::resolve( &__Schema { registry: &ctx.registry, }, @@ -75,7 +76,7 @@ impl GQLObject for QueryRoot { } else if field.name.as_str() == "__type" { let type_name: String = ctx.param_value("name", || Value::Null)?; let ctx_obj = ctx.with_item(&field.selection_set); - return GQLOutputValue::resolve( + return OutputValueType::resolve( &ctx.registry .types .get(&type_name) @@ -101,3 +102,10 @@ impl GQLObject for QueryRoot { }); } } + +#[async_trait::async_trait] +impl OutputValueType for QueryRoot { + async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result { + do_resolve(ctx, value).await + } +} diff --git a/src/types/upload.rs b/src/types/upload.rs index feea22b0..fc5279be 100644 --- a/src/types/upload.rs +++ b/src/types/upload.rs @@ -1,4 +1,4 @@ -use crate::{registry, GQLInputValue, GQLType, Value}; +use crate::{registry, InputValueType, Type, Value}; use std::borrow::Cow; /// Upload file type @@ -10,7 +10,7 @@ pub struct Upload { pub content: Vec, } -impl<'a> GQLType for Upload { +impl<'a> Type for Upload { fn type_name() -> Cow<'static, str> { Cow::Borrowed("Upload") } @@ -27,7 +27,7 @@ impl<'a> GQLType for Upload { } } -impl<'a> GQLInputValue for Upload { +impl<'a> InputValueType for Upload { fn parse(value: &Value) -> Option { if let Value::String(s) = value { if s.starts_with("file:") { diff --git a/src/validation/context.rs b/src/validation/context.rs index 475d8ad3..251d96cb 100644 --- a/src/validation/context.rs +++ b/src/validation/context.rs @@ -1,18 +1,18 @@ use crate::error::RuleError; -use crate::registry::{Registry, Type}; +use crate::registry; use graphql_parser::query::{Definition, Document, FragmentDefinition}; use graphql_parser::Pos; use std::collections::HashMap; pub struct ValidatorContext<'a> { - pub registry: &'a Registry, + pub registry: &'a registry::Registry, pub errors: Vec, - type_stack: Vec<&'a Type>, + type_stack: Vec<&'a registry::Type>, fragments: HashMap<&'a str, &'a FragmentDefinition>, } impl<'a> ValidatorContext<'a> { - pub fn new(registry: &'a Registry, doc: &'a Document) -> Self { + pub fn new(registry: &'a registry::Registry, doc: &'a Document) -> Self { Self { registry, errors: Default::default(), @@ -39,17 +39,21 @@ impl<'a> ValidatorContext<'a> { self.errors.extend(errors); } - pub fn with_type)>(&mut self, ty: &'a Type, mut f: F) { + pub fn with_type)>( + &mut self, + ty: &'a registry::Type, + mut f: F, + ) { self.type_stack.push(ty); f(self); self.type_stack.pop(); } - pub fn parent_type(&self) -> Option<&'a Type> { + pub fn parent_type(&self) -> Option<&'a registry::Type> { self.type_stack.get(self.type_stack.len() - 2).map(|t| *t) } - pub fn current_type(&self) -> &'a Type { + pub fn current_type(&self) -> &'a registry::Type { self.type_stack.last().unwrap() } diff --git a/src/validation/rules/fields_on_correct_type.rs b/src/validation/rules/fields_on_correct_type.rs index 016f9e89..b410a024 100644 --- a/src/validation/rules/fields_on_correct_type.rs +++ b/src/validation/rules/fields_on_correct_type.rs @@ -1,4 +1,4 @@ -use crate::registry::Type; +use crate::registry; use crate::validation::context::ValidatorContext; use crate::validation::visitor::Visitor; use graphql_parser::query::Field; @@ -14,7 +14,7 @@ impl<'a> Visitor<'a> for FieldsOnCorrectType { .field_by_name(&field.name) .is_none() { - if let Some(Type::Union { .. }) = ctx.parent_type() { + if let Some(registry::Type::Union { .. }) = ctx.parent_type() { if field.name == "__typename" { return; } diff --git a/src/validation/utils.rs b/src/validation/utils.rs index 27fd6f29..b0733eae 100644 --- a/src/validation/utils.rs +++ b/src/validation/utils.rs @@ -1,35 +1,34 @@ -use crate::registry::{Registry, Type, TypeName}; -use crate::Value; +use crate::{registry, Value}; -pub fn is_valid_input_value(registry: &Registry, type_name: &str, value: &Value) -> bool { +pub fn is_valid_input_value(registry: ®istry::Registry, type_name: &str, value: &Value) -> bool { if let Value::Variable(_) = value { return true; } - match TypeName::create(type_name) { - TypeName::NonNull(type_name) => match value { + match registry::TypeName::create(type_name) { + registry::TypeName::NonNull(type_name) => match value { Value::Null => false, _ => is_valid_input_value(registry, type_name, value), }, - TypeName::List(type_name) => match value { + registry::TypeName::List(type_name) => match value { Value::List(elems) => elems .iter() .all(|elem| is_valid_input_value(registry, type_name, elem)), _ => false, }, - TypeName::Named(type_name) => { + registry::TypeName::Named(type_name) => { if let Value::Null = value { return true; } if let Some(ty) = registry.types.get(type_name) { match ty { - Type::Scalar { is_valid, .. } => is_valid(value), - Type::Enum { enum_values, .. } => match value { + registry::Type::Scalar { is_valid, .. } => is_valid(value), + registry::Type::Enum { enum_values, .. } => match value { Value::Enum(name) => enum_values.contains_key(name.as_str()), _ => false, }, - Type::InputObject { input_fields, .. } => match value { + registry::Type::InputObject { input_fields, .. } => match value { Value::Object(values) => { for field in input_fields { let value = values.get(field.name).unwrap_or(&Value::Null); diff --git a/tests/enum.rs b/tests/enum.rs index 722383fb..fe4a76a6 100644 --- a/tests/enum.rs +++ b/tests/enum.rs @@ -35,11 +35,7 @@ pub async fn test_enum_type() { } } - let schema = Schema::new( - Root { value: MyEnum::A }, - GQLEmptyMutation, - GQLEmptySubscription, - ); + let schema = Schema::new(Root { value: MyEnum::A }, EmptyMutation, EmptySubscription); let query = format!( r#"{{ value diff --git a/tests/input_object.rs b/tests/input_object.rs index 5b9df470..6191e9b9 100644 --- a/tests/input_object.rs +++ b/tests/input_object.rs @@ -72,7 +72,7 @@ pub async fn test_input_object_default_value() { } } - let schema = Schema::new(Root, GQLEmptyMutation, GQLEmptySubscription); + let schema = Schema::new(Root, EmptyMutation, EmptySubscription); let query = format!( r#"{{ a(input:{{e:777}}) {{ diff --git a/tests/list.rs b/tests/list.rs index bfca88be..5fa885f8 100644 --- a/tests/list.rs +++ b/tests/list.rs @@ -38,8 +38,8 @@ pub async fn test_list_type() { Root { value: vec![1, 2, 3, 4, 5], }, - GQLEmptyMutation, - GQLEmptySubscription, + EmptyMutation, + EmptySubscription, ); let json_value: serde_json::Value = vec![1, 2, 3, 4, 5].into(); let query = format!( diff --git a/tests/optional.rs b/tests/optional.rs index deeb5523..8674a23e 100644 --- a/tests/optional.rs +++ b/tests/optional.rs @@ -50,8 +50,8 @@ pub async fn test_optional_type() { value1: Some(10), value2: None, }, - GQLEmptyMutation, - GQLEmptySubscription, + EmptyMutation, + EmptySubscription, ); let query = format!( r#"{{ diff --git a/tests/scalars.rs b/tests/scalars.rs index 63df8bf0..2331d47b 100644 --- a/tests/scalars.rs +++ b/tests/scalars.rs @@ -31,7 +31,7 @@ macro_rules! test_scalars { } } - let schema = Schema::new(Root { value: $value }, GQLEmptyMutation, GQLEmptySubscription); + let schema = Schema::new(Root { value: $value }, EmptyMutation, EmptySubscription); let json_value: serde_json::Value = $value.into(); let query = format!("{{ value testArg(input: {0}) testInput(input: {{value: {0}}}) }}", json_value); assert_eq!(