add GraphQL Cursor Connections

This commit is contained in:
sunli 2020-03-19 17:20:12 +08:00
parent a656073d06
commit ac044d6d30
57 changed files with 1102 additions and 451 deletions

View File

@ -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 }

View File

@ -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)

View File

@ -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();

View File

@ -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<Query, Mutation, Subscription> {
impl<Query, Mutation, Subscription> HandlerBuilder<Query, Mutation, Subscription>
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<Query, Mutation, Subscription>) -> Self {
@ -152,9 +152,9 @@ async fn handle_request<Query, Mutation, Subscription>(
mut payload: Payload,
) -> actix_web::Result<HttpResponse>
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 {

View File

@ -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<Query, Mutation, Subscription> {
impl<Query, Mutation, Subscription> WsSession<Query, Mutation, Subscription>
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<Schema<Query, Mutation, Subscription>>) -> Self {
Self {
@ -39,17 +39,27 @@ where
subscribes: Default::default(),
}
}
fn hb(&self, ctx: &mut WebsocketContext<Self>) {
ctx.run_interval(Duration::new(1, 0), |act, ctx| {
if Instant::now().duration_since(act.hb) > Duration::new(10, 0) {
ctx.stop();
}
});
}
}
impl<Query, Mutation, Subscription> Actor for WsSession<Query, Mutation, Subscription>
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<Self>;
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<Query, Mutation, Subscription> StreamHandler<Result<Message, ProtocolError>>
for WsSession<Query, Mutation, Subscription>
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<Message, ProtocolError>, ctx: &mut Self::Context) {
let msg = match msg {
@ -164,9 +174,9 @@ where
impl<Query, Mutation, Subscription> Handler<PushMessage>
for WsSession<Query, Mutation, Subscription>
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<Self, std::result::Result<(), ()>>;

View File

@ -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<TokenStre
};
let gql_typename = enum_args.name.clone().unwrap_or_else(|| ident.to_string());
check_reserved_name(&gql_typename, enum_args.internal)?;
let desc = enum_args
.desc
.as_ref()
@ -55,7 +57,7 @@ pub fn generate(enum_args: &args::Enum, input: &DeriveInput) -> Result<TokenStre
.unwrap_or_else(|| quote! {None});
enum_items.push(&variant.ident);
items.push(quote! {
#crate_name::GQLEnumItem {
#crate_name::EnumItem {
name: #gql_item_name,
value: #ident::#item_ident,
}
@ -76,13 +78,13 @@ pub fn generate(enum_args: &args::Enum, input: &DeriveInput) -> Result<TokenStre
#(#enum_items),*
}
impl #crate_name::GQLEnum for #ident {
fn items() -> &'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<TokenStre
fn create_type_info(registry: &mut #crate_name::registry::Registry) -> String {
registry.create_type::<Self, _>(|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<TokenStre
}
}
impl #crate_name::GQLInputValue for #ident {
impl #crate_name::InputValueType for #ident {
fn parse(value: &#crate_name::Value) -> Option<Self> {
#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<serde_json::Value> {
#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::<Self, _>(|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<serde_json::Value> {
#crate_name::GQLEnum::resolve_enum(*value)
#crate_name::EnumType::resolve_enum(value)
}
}
};

View File

@ -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::<Self, _>(|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<Self> {
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())
}

View File

@ -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())
}

View File

@ -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::<Self, _>(|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())
}

View File

@ -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::<Self, _>(|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<std::any::TypeId, #crate_name::graphql_parser::query::Field>) -> #crate_name::Result<()> {
use #crate_name::ErrorWithPosition;
#(#create_types)*

View File

@ -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())
}

View File

@ -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<Value, ParseError> {
pub fn parse_value(s: &str) -> std::result::Result<Value, ParseError> {
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(())
}
}

View File

@ -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<starwars::QueryRoot, GQLEmptyMutation, GQLEmptySubscription>;
type StarWarsSchema = Schema<starwars::QueryRoot, EmptyMutation, EmptySubscription>;
async fn index(s: web::Data<StarWarsSchema>, req: web::Json<GQLRequest>) -> web::Json<GQLResponse> {
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))

View File

@ -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<usize> {
self.droid_data.get(id).cloned()
}
pub fn humans(&self) -> Vec<usize> {
self.human_data.values().cloned().collect()
}
pub fn droids(&self) -> Vec<usize> {
self.droid_data.values().cloned().collect()
}
}

View File

@ -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::<StarWars>().human(&id).map(|id| Human(id))
}
#[field]
async fn humans(
&self,
ctx: &Context<'_>,
after: Option<String>,
before: Option<String>,
first: Option<i32>,
last: Option<i32>,
) -> Result<Connection<Human, EmptyEdgeFields>> {
let humans = ctx
.data::<StarWars>()
.humans()
.iter()
.map(|id| *id)
.collect::<Vec<_>>();
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<Droid> {
ctx.data::<StarWars>().droid(&id).map(|id| Droid(id))
}
#[field]
async fn droids(
&self,
ctx: &Context<'_>,
after: Option<String>,
before: Option<String>,
first: Option<i32>,
last: Option<i32>,
) -> Result<Connection<Human, EmptyEdgeFields>> {
let droids = ctx
.data::<StarWars>()
.droids()
.iter()
.map(|id| *id)
.collect::<Vec<_>>();
droids
.as_slice()
.query(ctx, after, before, first, last)
.await
.map(|connection| connection.map(|id| Human(*id)))
}
}
#[async_graphql::Interface(

View File

@ -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<starwars::QueryRoot, GQLEmptyMutation, GQLEmptySubscription>;
type StarWarsSchema = Schema<starwars::QueryRoot, EmptyMutation, EmptySubscription>;
async fn index(mut request: Request<StarWarsSchema>) -> Response {
let gql_request: GQLRequest = request.body_json().await.unwrap();
@ -28,7 +28,7 @@ async fn gql_graphiql(_request: Request<StarWarsSchema>) -> 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);

View File

@ -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<Self>;
}
/// 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<serde_json::Value>;
}
/// 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<Self> {
<$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<serde_json::Value> {
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<Self> {
<$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<serde_json::Value> {
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<T: GQLObject + Send + Sync> GQLOutputValue for T {
async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result<serde_json::Value> {
let mut result = serde_json::Map::<String, serde_json::Value>::new();
crate::resolver::do_resolve(ctx, value, &mut result).await?;
Ok(result.into())
impl<T: Type + Send + Sync> 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<T: OutputValueType + Send + Sync> OutputValueType for &T {
async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result<serde_json::Value> {
T::resolve(*value, ctx).await
}
}

View File

@ -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<T: GQLInputValue, F: FnOnce() -> Value>(
pub fn param_value<T: InputValueType, F: FnOnce() -> 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(),

View File

@ -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 {

View File

@ -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<Query, Mutation, Subscription>,
) -> 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<Query, Mutation, Subscription>,
) -> Result<PreparedQuery<'a, Query, Mutation>>
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) {

View File

@ -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<T> = anyhow::Result<T>;
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<T>, such as `Vec<i32>`
/// - Slice<T>, such as `&[i32]`
/// - Option<T>, such as `Option<i32>`
/// - GQLObject and `&GQLObject`
/// - GQLEnum
/// - Object and &Object
/// - Enum
/// - Result<T, E>, such as `Result<i32, E>`
///
/// # 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 {

View File

@ -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::<Vec<_>>();
args.sort_by(|a, b| a.input_value.name.cmp(b.input_value.name));
args
}
#[field(name = "type")]

View File

@ -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(&registry.types[ty]),
},
@ -99,19 +98,19 @@ impl<'a> __Type<'a> {
) -> Option<Vec<__Field<'a>>> {
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::<Vec<_>>();
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<Vec<__Type<'a>>> {
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<Vec<__Type<'a>>> {
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<Vec<__EnumValue<'a>>> {
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<Vec<__InputValue<'a>>> {
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()

View File

@ -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<serde_json::Value>
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<serde_json::Value>
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,
}
}
}

View File

@ -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<String, Field>,
},
Interface {
name: &'static str,
name: String,
description: Option<&'static str>,
fields: HashMap<&'static str, Field>,
fields: HashMap<String, Field>,
possible_types: HashSet<String>,
},
Union {
name: &'static str,
name: String,
description: Option<&'static str>,
possible_types: HashSet<String>,
},
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<InputValue>,
},
@ -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<String, Field>> {
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<T: GQLType, F: FnMut(&mut Registry) -> Type>(&mut self, mut f: F) -> String {
pub fn create_type<T: crate::Type, F: FnMut(&mut Registry) -> 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(),

View File

@ -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<String, serde_json::Value>,
}
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<Box<dyn Future<Output = Result<()>> + '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<serde_json::Value> {
let mut result = serde_json::Map::<String, serde_json::Value>::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<String, serde_json::Value>,
@ -91,7 +106,7 @@ pub async fn do_resolve<'a, T: GQLObject + Send + Sync>(
Resolver {
ctx,
obj: root,
result,
result: result,
}
.resolve()
.await?;

View File

@ -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"
}

View File

@ -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<Utc> {
impl Scalar for DateTime<Utc> {
fn type_name() -> &'static str {
"DateTime"
}

View File

@ -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"
}

View File

@ -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<usize> for ID {
}
}
impl GQLScalar for ID {
impl Scalar for ID {
fn type_name() -> &'static str {
"ID"
}

View File

@ -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"
}

View File

@ -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!(<bool as GQLType>::type_name(), "Boolean");
assert_eq!(<bool as GQLType>::qualified_type_name(), "Boolean!");
assert_eq!(<bool as Type>::type_name(), "Boolean");
assert_eq!(<bool as Type>::qualified_type_name(), "Boolean!");
assert_eq!(<i32 as GQLType>::type_name(), "Int");
assert_eq!(<i32 as GQLType>::qualified_type_name(), "Int!");
assert_eq!(<i32 as Type>::type_name(), "Int");
assert_eq!(<i32 as Type>::qualified_type_name(), "Int!");
assert_eq!(<f32 as GQLType>::type_name(), "Float");
assert_eq!(<f32 as GQLType>::qualified_type_name(), "Float!");
assert_eq!(<f32 as Type>::type_name(), "Float");
assert_eq!(<f32 as Type>::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!(<String as GQLType>::type_name(), "String");
assert_eq!(<String as GQLType>::qualified_type_name(), "String!");
assert_eq!(<String as Type>::type_name(), "String");
assert_eq!(<String as Type>::qualified_type_name(), "String!");
assert_eq!(<ID as GQLType>::type_name(), "ID");
assert_eq!(<ID as GQLType>::qualified_type_name(), "ID!");
assert_eq!(<ID as Type>::type_name(), "ID");
assert_eq!(<ID as Type>::qualified_type_name(), "ID!");
#[cfg(feature = "chrono")]
{
assert_eq!(<DateTime::<Utc> as GQLType>::type_name(), "DateTime");
assert_eq!(<DateTime::<Utc> as Type>::type_name(), "DateTime");
assert_eq!(
<DateTime::<Utc> as GQLType>::qualified_type_name(),
<DateTime::<Utc> as Type>::qualified_type_name(),
"DateTime!"
);
}
#[cfg(feature = "uuid")]
{
assert_eq!(<Uuid as GQLType>::type_name(), "UUID");
assert_eq!(<Uuid as GQLType>::qualified_type_name(), "UUID!");
assert_eq!(<Uuid as Type>::type_name(), "UUID");
assert_eq!(<Uuid as Type>::qualified_type_name(), "UUID!");
}
}
}

View File

@ -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<serde_json::Value> {
Ok(value.to_string().into())
}

View File

@ -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"
}

View File

@ -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<Query, Mutation, Subscription> {
pub(crate) data: Data,
}
impl<Query: GQLObject, Mutation: GQLObject, Subscription: GQLSubscription>
impl<Query: ObjectType, Mutation: ObjectType, Subscription: SubscriptionType>
Schema<Query, Mutation, Subscription>
{
/// 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(),

View File

@ -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<Option<serde_json::Value>>
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<Option<serde_json::Value>>;
}
fn create_types<T: GQLSubscription>(
fn create_types<T: SubscriptionType>(
ctx: &ContextSelectionSet<'_>,
fragments: &HashMap<String, FragmentDefinition>,
types: &mut HashMap<TypeId, Field>,
@ -131,6 +130,7 @@ fn create_types<T: GQLSubscription>(
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 {

View File

@ -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<T, E: ObjectType + Sync + Send> {
total_count: Option<usize>,
page_info: PageInfo,
nodes: Vec<(String, E, T)>,
}
impl<T, E: ObjectType + Sync + Send> Connection<T, E> {
pub fn new(
total_count: Option<usize>,
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<O, F>(self, mut f: F) -> Connection<O, E>
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<T: OutputValueType + Send + Sync, E: ObjectType + Sync + Send> Type for Connection<T, E> {
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::<Self, _>(|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: <Option::<Vec<Option<Edge<T,E>>>> 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::<i32>::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::<T>::type_name().to_string(),
deprecation: None
});
fields
},
})
}
}
#[async_trait::async_trait]
impl<T: OutputValueType + Send + Sync, E: ObjectType + Sync + Send> ObjectType
for Connection<T, E>
{
async fn resolve_field(&self, ctx: &Context<'_>, field: &Field) -> Result<serde_json::Value> {
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::<Vec<_>>();
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::<Vec<_>>();
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::<T, E>::type_name().to_string(),
}
.with_position(field.position))
}
async fn resolve_inline_fragment(
&self,
name: &str,
_ctx: &ContextSelectionSet<'_>,
_result: &mut serde_json::Map<String, serde_json::Value>,
) -> Result<()> {
anyhow::bail!(QueryError::UnrecognizedInlineFragment {
object: Connection::<T, E>::type_name().to_string(),
name: name.to_string(),
});
}
}
#[async_trait::async_trait]
impl<T: OutputValueType + Send + Sync, E: ObjectType + Sync + Send> OutputValueType
for Connection<T, E>
{
async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result<serde_json::Value> {
do_resolve(ctx, value).await
}
}

View File

@ -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::<Self, _>(|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<serde_json::Value> {
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<String, serde_json::Value>,
) -> Result<()> {
anyhow::bail!(QueryError::UnrecognizedInlineFragment {
object: <Edge<T, E> 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<serde_json::Value> {
do_resolve(ctx, value).await
}
}

205
src/types/connection/mod.rs Normal file
View File

@ -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<Connection<Self::Element, Self::Edge>> {
/// 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::<BE>().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::<BE>().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<String>,
/// before: Option<String>,
/// first: Option<i32>,
/// last: Option<i32>
/// ) -> Result<Connection<i32, DiffFields>> {
/// 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<String>,
before: Option<String>,
first: Option<i32>,
last: Option<i32>,
) -> Result<Connection<Self::Element, Self::Edge>> {
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<Connection<Self::Element, Self::Edge>>;
}

View File

@ -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<String>,
pub end_cursor: Option<String>,
}
#[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<String> {
&self.start_cursor
}
#[field(desc = "When paginating forwards, the cursor to continue.")]
async fn end_cursor(&self) -> &Option<String> {
&self.end_cursor
}
}

View File

@ -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<Connection<Self::Element, Self::Edge>> {
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::<BE>().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::<BE>().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))
}
}

View File

@ -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::<Self, _>(|_| 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<serde_json::Value> {
return Err(QueryError::NotConfiguredMutations.into());
unreachable!()
}
async fn resolve_inline_fragment(
@ -53,6 +55,13 @@ impl GQLObject for GQLEmptyMutation {
_ctx: &ContextSelectionSet<'_>,
_result: &mut Map<String, Value>,
) -> Result<()> {
unreachable!()
}
}
#[async_trait::async_trait]
impl OutputValueType for EmptyMutation {
async fn resolve(_value: &Self, _ctx: &ContextSelectionSet<'_>) -> Result<serde_json::Value> {
return Err(QueryError::NotConfiguredMutations.into());
}
}

View File

@ -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::<Self, _>(|_| 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<TypeId, Field>) -> Result<()> {
return Err(QueryError::NotConfiguredSubscriptions.into());
unreachable!()
}
async fn resolve(
@ -37,6 +44,13 @@ impl GQLSubscription for GQLEmptySubscription {
_types: &HashMap<TypeId, Field, RandomState>,
_msg: &(dyn Any + Send + Sync),
) -> Result<Option<Value>> {
unreachable!()
}
}
#[async_trait::async_trait]
impl OutputValueType for EmptySubscription {
async fn resolve(_value: &Self, _ctx: &ContextSelectionSet<'_>) -> Result<serde_json::Value> {
return Err(QueryError::NotConfiguredSubscriptions.into());
}
}

View File

@ -1,14 +1,14 @@
use crate::{GQLType, Result};
use crate::{Result, Type};
use graphql_parser::query::Value;
pub struct GQLEnumItem<T> {
pub struct EnumItem<T> {
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<Self>];
pub trait EnumType: Type + Sized + Eq + Send + Copy + Sized + 'static {
fn items() -> &'static [EnumItem<Self>];
fn parse_enum(value: &Value) -> Option<Self> {
let value = match value {

View File

@ -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<T: GQLType> GQLType for Vec<T> {
impl<T: Type> Type for Vec<T> {
fn type_name() -> Cow<'static, str> {
Cow::Owned(format!("[{}]", T::qualified_type_name()))
}
@ -16,13 +16,13 @@ impl<T: GQLType> GQLType for Vec<T> {
}
}
impl<T: GQLInputValue> GQLInputValue for Vec<T> {
impl<T: InputValueType> InputValueType for Vec<T> {
fn parse(value: &Value) -> Option<Self> {
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<T: GQLInputValue> GQLInputValue for Vec<T> {
}
#[async_trait::async_trait]
impl<T: GQLOutputValue + Send + Sync> GQLOutputValue for Vec<T> {
impl<T: OutputValueType + Send + Sync> OutputValueType for Vec<T> {
async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result<serde_json::Value> {
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<T: GQLType> GQLType for &[T] {
impl<T: Type> Type for &[T] {
fn type_name() -> Cow<'static, str> {
Cow::Owned(format!("[{}]", T::type_name()))
}
@ -53,32 +53,11 @@ impl<T: GQLType> GQLType for &[T] {
}
#[async_trait::async_trait]
impl<T: GQLOutputValue + Send + Sync> GQLOutputValue for &[T] {
impl<T: OutputValueType + Send + Sync> OutputValueType for &[T] {
async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result<serde_json::Value> {
let mut res = Vec::new();
for item in value.iter() {
res.push(GQLOutputValue::resolve(item, &ctx).await?);
}
Ok(res.into())
}
}
impl<T: GQLType> GQLType for &Vec<T> {
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<T: GQLOutputValue + Send + Sync> GQLOutputValue for &Vec<T> {
async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result<serde_json::Value> {
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<T: GQLOutputValue + Send + Sync> GQLOutputValue for &Vec<T> {
#[cfg(test)]
mod tests {
use crate::GQLType;
use crate::Type;
#[test]
fn test_list_type() {

View File

@ -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;

View File

@ -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<T: GQLType> GQLType for Option<T> {
impl<T: Type> Type for Option<T> {
fn type_name() -> Cow<'static, str> {
T::type_name()
}
@ -16,7 +16,7 @@ impl<T: GQLType> GQLType for Option<T> {
}
}
impl<T: GQLInputValue> GQLInputValue for Option<T> {
impl<T: InputValueType> InputValueType for Option<T> {
fn parse(value: &Value) -> Option<Self> {
match value {
Value::Null => Some(None),
@ -26,38 +26,11 @@ impl<T: GQLInputValue> GQLInputValue for Option<T> {
}
#[async_trait::async_trait]
impl<T: GQLOutputValue + Sync> GQLOutputValue for Option<T> {
impl<T: OutputValueType + Sync> OutputValueType for Option<T> {
async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result<serde_json::Value> where
{
if let Some(inner) = value {
GQLOutputValue::resolve(inner, ctx).await
} else {
Ok(serde_json::Value::Null)
}
}
}
impl<T: GQLType> GQLType for &Option<T> {
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<T: GQLOutputValue + Sync> GQLOutputValue for &Option<T> {
async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result<serde_json::Value> 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<T: GQLOutputValue + Sync> GQLOutputValue for &Option<T> {
#[cfg(test)]
mod tests {
use crate::GQLType;
use crate::Type;
#[test]
fn test_optional_type() {

View File

@ -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<T> {
pub inner: T,
}
impl<T: GQLType> GQLType for QueryRoot<T> {
impl<T: Type> Type for QueryRoot<T> {
fn type_name() -> Cow<'static, str> {
T::type_name()
}
@ -20,11 +19,13 @@ impl<T: GQLType> GQLType for QueryRoot<T> {
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<T: GQLType> GQLType for QueryRoot<T> {
);
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<T: GQLType> GQLType for QueryRoot<T> {
}
#[async_trait::async_trait]
impl<T: GQLObject + Send + Sync> GQLObject for QueryRoot<T> {
impl<T: ObjectType + Send + Sync> ObjectType for QueryRoot<T> {
async fn resolve_field(&self, ctx: &Context<'_>, field: &Field) -> Result<serde_json::Value> {
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<T: GQLObject + Send + Sync> GQLObject for QueryRoot<T> {
} 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<T: GQLObject + Send + Sync> GQLObject for QueryRoot<T> {
});
}
}
#[async_trait::async_trait]
impl<T: ObjectType + Send + Sync> OutputValueType for QueryRoot<T> {
async fn resolve(value: &Self, ctx: &ContextSelectionSet<'_>) -> Result<serde_json::Value> {
do_resolve(ctx, value).await
}
}

View File

@ -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<u8>,
}
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<Self> {
if let Value::String(s) = value {
if s.starts_with("file:") {

View File

@ -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<RuleError>,
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<F: FnMut(&mut ValidatorContext<'a>)>(&mut self, ty: &'a Type, mut f: F) {
pub fn with_type<F: FnMut(&mut ValidatorContext<'a>)>(
&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()
}

View File

@ -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;
}

View File

@ -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: &registry::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);

View File

@ -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

View File

@ -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}}) {{

View File

@ -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!(

View File

@ -50,8 +50,8 @@ pub async fn test_optional_type() {
value1: Some(10),
value2: None,
},
GQLEmptyMutation,
GQLEmptySubscription,
EmptyMutation,
EmptySubscription,
);
let query = format!(
r#"{{

View File

@ -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!(