init commit

This commit is contained in:
sunli 2020-03-01 18:54:34 +08:00
commit 5030a28361
18 changed files with 1384 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/target
Cargo.lock
.idea
.DS_Store

29
Cargo.toml Normal file
View File

@ -0,0 +1,29 @@
[package]
name = "async-graphql"
version = "0.1.0"
authors = ["sunli <scott_s829@163.com>"]
edition = "2018"
[features]
default = ["chrono", "uuid"]
[dependencies]
async-graphql-derive = { path = "async-graphql-derive" }
graphql-parser = "0.2.3"
anyhow = "1.0.26"
thiserror = "1.0.11"
async-trait = "0.1.24"
serde = "1.0.104"
serde_derive = "1.0.104"
serde_json = "1.0.48"
fnv = "1.0.6"
chrono = { version = "0.4.10", optional = true }
uuid = { version = "0.8.1", optional = true }
[dev-dependencies]
async-std = { version = "1.5.0", features = ["attributes"] }
[workspace]
members = [
"async-graphql-derive"
]

View File

@ -0,0 +1,13 @@
[package]
name = "async-graphql-derive"
version = "0.1.0"
authors = ["sunli <scott_s829@163.com>"]
edition = "2018"
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1.0.6"
syn = { version = "1.0.13", features = ["full"] }
quote = "1.0.2"

View File

@ -0,0 +1,317 @@
use syn::{Attribute, AttributeArgs, Error, Meta, NestedMeta, Result, Type};
#[derive(Debug)]
pub struct Object {
pub name: Option<String>,
pub desc: Option<String>,
}
impl Object {
pub fn parse(args: AttributeArgs) -> Result<Self> {
let mut name = None;
let mut desc = None;
for arg in args {
match arg {
NestedMeta::Meta(Meta::NameValue(nv)) => {
if nv.path.is_ident("name") {
if let syn::Lit::Str(lit) = nv.lit {
name = Some(lit.value());
} else {
return Err(Error::new_spanned(
&nv.lit,
"Attribute 'name' should be a string.",
));
}
} else if nv.path.is_ident("desc") {
if let syn::Lit::Str(lit) = nv.lit {
desc = Some(lit.value());
} else {
return Err(Error::new_spanned(
&nv.lit,
"Attribute 'desc' should be a string.",
));
}
}
}
_ => {}
}
}
Ok(Self { name, desc })
}
}
#[derive(Debug)]
pub struct Argument {
pub name: String,
pub desc: Option<String>,
pub ty: Type,
}
#[derive(Debug)]
pub struct Field {
pub name: Option<String>,
pub desc: Option<String>,
pub is_attr: bool,
pub arguments: Vec<Argument>,
}
impl Field {
pub fn parse(attrs: &[Attribute]) -> Result<Option<Self>> {
let mut is_field = false;
let mut name = None;
let mut desc = None;
let mut is_attr = false;
let mut arguments = Vec::new();
for attr in attrs {
if attr.path.is_ident("field") {
is_field = true;
if let Ok(Meta::List(args)) = attr.parse_meta() {
for meta in args.nested {
match meta {
NestedMeta::Meta(Meta::Path(p)) if p.is_ident("attr") => {
is_attr = true;
}
NestedMeta::Meta(Meta::NameValue(nv)) => {
if nv.path.is_ident("name") {
if let syn::Lit::Str(lit) = nv.lit {
name = Some(lit.value());
} else {
return Err(Error::new_spanned(
&nv.lit,
"Attribute 'name' should be a string.",
));
}
} else if nv.path.is_ident("desc") {
if let syn::Lit::Str(lit) = nv.lit {
desc = Some(lit.value());
} else {
return Err(Error::new_spanned(
&nv.lit,
"Attribute 'desc' should be a string.",
));
}
}
}
NestedMeta::Meta(Meta::List(ls)) => {
if ls.path.is_ident("arg") {
let mut name = None;
let mut desc = None;
let mut ty = None;
for meta in &ls.nested {
if let NestedMeta::Meta(Meta::NameValue(nv)) = meta {
if nv.path.is_ident("name") {
if let syn::Lit::Str(lit) = &nv.lit {
name = Some(lit.value());
} else {
return Err(Error::new_spanned(
&nv.lit,
"Attribute 'name' should be a string.",
));
}
} else if nv.path.is_ident("desc") {
if let syn::Lit::Str(lit) = &nv.lit {
desc = Some(lit.value());
} else {
return Err(Error::new_spanned(
&nv.lit,
"Attribute 'desc' should be a string.",
));
}
} else if nv.path.is_ident("type") {
if let syn::Lit::Str(lit) = &nv.lit {
if let Ok(ty2) =
syn::parse_str::<syn::Type>(&lit.value())
{
ty = Some(ty2);
} else {
return Err(Error::new_spanned(
&lit,
"expect type",
));
}
} else {
return Err(Error::new_spanned(
&nv.lit,
"Attribute 'type' should be a string.",
));
}
}
}
}
if name.is_none() {
return Err(Error::new_spanned(ls, "missing name."));
}
if ty.is_none() {
return Err(Error::new_spanned(ls, "missing type."));
}
arguments.push(Argument {
name: name.unwrap(),
desc,
ty: ty.unwrap(),
});
}
}
_ => {}
}
}
}
}
}
if is_field {
Ok(Some(Self {
name,
desc,
is_attr,
arguments,
}))
} else {
Ok(None)
}
}
}
#[derive(Debug)]
pub struct Enum {
pub name: Option<String>,
pub desc: Option<String>,
}
impl Enum {
pub fn parse(args: AttributeArgs) -> Result<Self> {
let mut name = None;
let mut desc = None;
for arg in args {
match arg {
NestedMeta::Meta(Meta::NameValue(nv)) => {
if nv.path.is_ident("name") {
if let syn::Lit::Str(lit) = nv.lit {
name = Some(lit.value());
} else {
return Err(Error::new_spanned(
&nv.lit,
"Attribute 'name' should be a string.",
));
}
} else if nv.path.is_ident("desc") {
if let syn::Lit::Str(lit) = nv.lit {
desc = Some(lit.value());
} else {
return Err(Error::new_spanned(
&nv.lit,
"Attribute 'desc' should be a string.",
));
}
}
}
_ => {}
}
}
Ok(Self { name, desc })
}
}
#[derive(Debug)]
pub struct EnumItem {
pub name: Option<String>,
pub desc: Option<String>,
}
impl EnumItem {
pub fn parse(attrs: &[Attribute]) -> Result<Self> {
let mut name = None;
let mut desc = None;
for attr in attrs {
if attr.path.is_ident("item") {
if let Ok(Meta::List(args)) = attr.parse_meta() {
for meta in args.nested {
match meta {
NestedMeta::Meta(Meta::NameValue(nv)) => {
if nv.path.is_ident("name") {
if let syn::Lit::Str(lit) = nv.lit {
name = Some(lit.value());
} else {
return Err(Error::new_spanned(
&nv.lit,
"Attribute 'name' should be a string.",
));
}
} else if nv.path.is_ident("desc") {
if let syn::Lit::Str(lit) = nv.lit {
desc = Some(lit.value());
} else {
return Err(Error::new_spanned(
&nv.lit,
"Attribute 'desc' should be a string.",
));
}
}
}
_ => {}
}
}
}
}
}
Ok(Self { name, desc })
}
}
#[derive(Debug)]
pub struct InputField {
pub name: Option<String>,
pub desc: Option<String>,
}
impl InputField {
pub fn parse(attrs: &[Attribute]) -> Result<Self> {
let mut name = None;
let mut desc = None;
for attr in attrs {
if attr.path.is_ident("field") {
if let Ok(Meta::List(args)) = attr.parse_meta() {
for meta in args.nested {
match meta {
NestedMeta::Meta(Meta::NameValue(nv)) => {
if nv.path.is_ident("name") {
if let syn::Lit::Str(lit) = nv.lit {
name = Some(lit.value());
} else {
return Err(Error::new_spanned(
&nv.lit,
"Attribute 'name' should be a string.",
));
}
} else if nv.path.is_ident("desc") {
if let syn::Lit::Str(lit) = nv.lit {
desc = Some(lit.value());
} else {
return Err(Error::new_spanned(
&nv.lit,
"Attribute 'desc' should be a string.",
));
}
}
}
_ => {}
}
}
}
}
}
Ok(Self { name, desc })
}
}

View File

@ -0,0 +1,83 @@
use crate::args;
use proc_macro::TokenStream;
use quote::quote;
use syn::{Data, DeriveInput, Error, Result};
pub fn generate(enum_args: &args::Enum, input: &DeriveInput) -> Result<TokenStream> {
let attrs = &input.attrs;
let vis = &input.vis;
let ident = &input.ident;
let e = match &input.data {
Data::Enum(e) => e,
_ => return Err(Error::new_spanned(input, "It should be a enum")),
};
let gql_typename = enum_args.name.clone().unwrap_or_else(|| ident.to_string());
let mut enum_items = Vec::new();
let mut items = Vec::new();
for variant in &e.variants {
if !variant.fields.is_empty() {
return Err(Error::new_spanned(
&variant,
format!(
"Invalid enum variant {}.\nGraphQL enums may only contain unit variants.",
variant.ident
),
));
}
let item_ident = &variant.ident;
let mut item_args = args::EnumItem::parse(&variant.attrs)?;
let gql_item_name = item_args
.name
.take()
.unwrap_or_else(|| variant.ident.to_string());
enum_items.push(variant);
let desc = match item_args.desc.take() {
Some(desc) => quote! { Some(#desc) },
None => quote! { None },
};
items.push(quote! {
async_graphql::GQLEnumItem {
name: #gql_item_name,
desc: #desc,
value: #ident::#item_ident,
}
});
}
let expanded = quote! {
#(#attrs)*
#[derive(Copy, Clone, Eq, PartialEq)]
#vis enum #ident {
#(#enum_items),*
}
impl async_graphql::GQLEnum for #ident {
fn items() -> &'static [async_graphql::GQLEnumItem<#ident>] {
&[#(#items),*]
}
}
impl async_graphql::GQLType for #ident {
fn type_name() -> String {
#gql_typename.to_string()
}
}
impl async_graphql::GQLInputValue for #ident {
fn parse(value: graphql_parser::query::Value) -> Result<Self> {
Self::parse_enum(value)
}
}
#[async_graphql::async_trait::async_trait]
impl async_graphql::GQLOutputValue for #ident {
async fn resolve(self, _: &ContextSelectionSet<'_>) -> Result<serde_json::Value> {
self.resolve_enum()
}
}
};
Ok(expanded.into())
}

View File

@ -0,0 +1,40 @@
use crate::args;
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::quote;
use syn::{Data, DeriveInput, Error, Ident, Result};
pub fn generate(object_args: &args::Object, input: &DeriveInput) -> Result<TokenStream> {
let attrs = &input.attrs;
let vis = &input.vis;
let ident = &input.ident;
let s = match &input.data {
Data::Struct(s) => s,
_ => return Err(Error::new_spanned(input, "It should be a struct.")),
};
let gql_typename = object_args
.name
.clone()
.unwrap_or_else(|| ident.to_string());
for field in &s.fields {
let field_args = args::Field::parse(&field.attrs)?;
let ident = field.ident.as_ref().unwrap();
}
let expanded = quote! {
#input
impl async_graphql::Type for #ident {
fn type_name() -> String {
#gql_typename.to_string()
}
}
impl async_graphql::GQLInputObject for #ident {
}
};
Ok(expanded.into())
}

View File

@ -0,0 +1,52 @@
extern crate proc_macro;
mod args;
mod r#enum;
mod input_object;
mod object;
use proc_macro::TokenStream;
use syn::parse_macro_input;
use syn::{AttributeArgs, DeriveInput};
#[proc_macro_attribute]
#[allow(non_snake_case)]
pub fn Object(args: TokenStream, input: TokenStream) -> TokenStream {
let object_args = match args::Object::parse(parse_macro_input!(args as AttributeArgs)) {
Ok(object_args) => object_args,
Err(err) => return err.to_compile_error().into(),
};
let input = parse_macro_input!(input as DeriveInput);
match object::generate(&object_args, &input) {
Ok(expanded) => expanded,
Err(err) => err.to_compile_error().into(),
}
}
#[proc_macro_attribute]
#[allow(non_snake_case)]
pub fn Enum(args: TokenStream, input: TokenStream) -> TokenStream {
let enum_args = match args::Enum::parse(parse_macro_input!(args as AttributeArgs)) {
Ok(enum_args) => enum_args,
Err(err) => return err.to_compile_error().into(),
};
let input = parse_macro_input!(input as DeriveInput);
match r#enum::generate(&enum_args, &input) {
Ok(expanded) => expanded,
Err(err) => err.to_compile_error().into(),
}
}
#[proc_macro_attribute]
#[allow(non_snake_case)]
pub fn InputObject(args: TokenStream, input: TokenStream) -> TokenStream {
let object_args = match args::Object::parse(parse_macro_input!(args as AttributeArgs)) {
Ok(enum_args) => enum_args,
Err(err) => return err.to_compile_error().into(),
};
let input = parse_macro_input!(input as DeriveInput);
match input_object::generate(&object_args, &input) {
Ok(expanded) => expanded,
Err(err) => err.to_compile_error().into(),
}
}

View File

@ -0,0 +1,128 @@
use crate::args;
use proc_macro::TokenStream;
use proc_macro2::Span;
use quote::quote;
use syn::{Data, DeriveInput, Error, Ident, Result};
pub fn generate(object_args: &args::Object, input: &DeriveInput) -> Result<TokenStream> {
let attrs = &input.attrs;
let vis = &input.vis;
let ident = &input.ident;
let s = match &input.data {
Data::Struct(s) => s,
_ => return Err(Error::new_spanned(input, "It should be a struct.")),
};
let gql_typename = object_args
.name
.clone()
.unwrap_or_else(|| ident.to_string());
let trait_ident = Ident::new(&format!("{}Fields", ident.to_string()), Span::call_site());
let mut new_fields = Vec::new();
let mut trait_fns = Vec::new();
let mut resolvers = Vec::new();
for field in &s.fields {
if let Some(field_args) = args::Field::parse(&field.attrs)? {
// is field
let vis = &field.vis;
let ty = &field.ty;
let ident = field.ident.as_ref().unwrap();
let field_name = ident.to_string();
if field_args.is_attr {
new_fields.push(quote! { #vis #ident: #ty });
}
let mut decl_params = Vec::new();
let mut get_params = Vec::new();
let mut use_params = Vec::new();
for arg in &field_args.arguments {
let name = Ident::new(&arg.name, Span::call_site());
let ty = &arg.ty;
let name_str = name.to_string();
decl_params.push(quote! { #name: #ty });
get_params.push(quote! {
let #name: #ty = ctx_field.param_value(#name_str)?;
});
use_params.push(quote! { #name });
}
trait_fns.push(quote! {
async fn #ident(&self, ctx: &async_graphql::ContextField<'_>, #(#decl_params),*) -> async_graphql::Result<#ty>;
});
resolvers.push(quote! {
if field.name.as_str() == #field_name {
#(#get_params)*
let obj = #trait_ident::#ident(&self, &ctx_field, #(#use_params),*).await.
map_err(|err| err.with_position(field.position))?;
let ctx_obj = ctx_field.with_item(&field.selection_set);
let value = obj.resolve(&ctx_obj).await.
map_err(|err| err.with_position(field.position))?;
result.insert(#field_name.to_string(), value.into());
continue;
}
});
} else {
new_fields.push(quote! { #field });
}
}
let expanded = quote! {
#(#attrs)*
#vis struct #ident {
#(#new_fields),*
}
#[async_graphql::async_trait::async_trait]
#vis trait #trait_ident {
#(#trait_fns)*
}
impl async_graphql::GQLType for #ident {
fn type_name() -> String {
#gql_typename.to_string()
}
}
#[async_graphql::async_trait::async_trait]
impl async_graphql::GQLOutputValue for #ident {
async fn resolve(self, ctx: &async_graphql::ContextSelectionSet<'_>) -> async_graphql::Result<async_graphql::serde_json::Value> {
use async_graphql::GQLErrorWithPosition;
if ctx.items.is_empty() {
async_graphql::anyhow::bail!(async_graphql::GQLQueryError::MustHaveSubFields {
object: #gql_typename,
}.with_position(ctx.span.0));
}
let mut result = async_graphql::serde_json::Map::<String, async_graphql::serde_json::Value>::new();
for selection in &ctx.items {
match selection {
async_graphql::graphql_parser::query::Selection::Field(field) => {
let ctx_field = ctx.with_item(field);
if field.name.as_str() == "__typename" {
result.insert("__typename".to_string(), #gql_typename.into());
continue;
}
#(#resolvers)*
async_graphql::anyhow::bail!(async_graphql::GQLQueryError::FieldNotFound {
field_name: field.name.clone(),
object: #gql_typename,
}.with_position(field.position));
}
_ => {}
}
}
Ok(async_graphql::serde_json::Value::Object(result))
}
}
impl async_graphql::GQLObject for #ident {}
};
Ok(expanded.into())
}

68
examples/helloworld.rs Normal file
View File

@ -0,0 +1,68 @@
use async_graphql::*;
#[async_graphql::Enum(name = "haha", desc = "hehe")]
enum MyEnum {
A,
B,
}
#[async_graphql::Object(name = "haha", desc = "hehe")]
struct MyObj {
#[field(
attr,
name = "a",
arg(name = "a1", type = "i32"),
arg(name = "a2", type = "f32")
)]
a: i32,
#[field(desc = "hehe")]
b: i32,
#[field(arg(name = "input", type = "MyEnum"))]
c: MyEnum,
#[field]
child: ChildObj,
}
#[async_graphql::Object]
struct ChildObj {
#[field(attr)]
value: f32,
}
#[async_trait::async_trait]
impl MyObjFields for MyObj {
async fn a(&self, ctx: &ContextField<'_>, a1: i32, a2: f32) -> Result<i32> {
Ok(a1 + a2 as i32)
}
async fn b(&self, ctx: &ContextField<'_>) -> Result<i32> {
Ok(999)
}
async fn c(&self, ctx: &ContextField<'_>, input: MyEnum) -> Result<MyEnum> {
Ok(input)
}
async fn child(&self, ctx: &ContextField<'_>) -> async_graphql::Result<ChildObj> {
Ok(ChildObj { value: 10.0 })
}
}
#[async_trait::async_trait]
impl ChildObjFields for ChildObj {
async fn value(&self, ctx: &ContextField<'_>) -> Result<f32> {
Ok(self.value)
}
}
#[async_std::main]
async fn main() {
let res = GQLQueryBuilder::new(MyObj { a: 10 }, GQLEmptyMutation, "{ b c(input:B) }")
.execute()
.await
.unwrap();
serde_json::to_writer_pretty(std::io::stdout(), &res).unwrap();
}

85
src/context.rs Normal file
View File

@ -0,0 +1,85 @@
use crate::{GQLInputValue, Result};
use fnv::FnvHasher;
use graphql_parser::query::{Field, SelectionSet, Value};
use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::hash::BuildHasherDefault;
use std::ops::{Deref, DerefMut};
#[derive(Serialize, Deserialize, Default)]
pub struct Variables(HashMap<String, serde_json::Value>);
impl Deref for Variables {
type Target = HashMap<String, serde_json::Value>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Variables {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
#[derive(Default)]
pub struct Data(HashMap<TypeId, Box<dyn Any + Sync + Send>, BuildHasherDefault<FnvHasher>>);
impl Data {
pub fn insert<D: Any + Send + Sync>(&mut self, data: D) {
self.0.insert(TypeId::of::<D>(), Box::new(data));
}
pub fn remove<D: Any + Send + Sync>(&mut self) {
self.0.remove(&TypeId::of::<D>());
}
}
pub type ContextSelectionSet<'a> = Context<'a, &'a SelectionSet>;
pub type ContextField<'a> = Context<'a, &'a Field>;
pub struct Context<'a, T> {
pub(crate) item: T,
pub(crate) data: Option<&'a Data>,
pub(crate) variables: Option<&'a Variables>,
}
impl<'a, T> Deref for Context<'a, T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.item
}
}
impl<'a, T> Context<'a, T> {
pub fn with_item<R>(&self, item: R) -> Context<'a, R> {
Context {
item,
data: self.data,
variables: self.variables,
}
}
pub fn data<D: Any + Send + Sync>(&self) -> Option<&D> {
self.data.and_then(|data| {
data.0
.get(&TypeId::of::<D>())
.and_then(|d| d.downcast_ref::<D>())
})
}
}
impl<'a> Context<'a, &'a Field> {
pub fn param_value<T: GQLInputValue>(&self, name: &str) -> Result<T> {
let value = self
.arguments
.iter()
.find(|(n, _)| n == name)
.map(|(_, v)| v)
.cloned()
.unwrap_or(Value::Null);
GQLInputValue::parse(value)
}
}

25
src/datetime.rs Normal file
View File

@ -0,0 +1,25 @@
use crate::{GQLQueryError, GQLScalar, Result, Value};
use chrono::{DateTime, TimeZone, Utc};
impl GQLScalar for DateTime<Utc> {
fn type_name() -> &'static str {
"DateTime"
}
fn parse(value: Value) -> Result<Self> {
match value {
Value::String(s) => Ok(Utc.datetime_from_str(&s, "%+")?),
_ => {
return Err(GQLQueryError::ExpectedType {
expect: Self::type_name().to_string(),
actual: value,
}
.into())
}
}
}
fn into_json(self) -> Result<serde_json::Value> {
Ok(self.to_rfc3339().into())
}
}

50
src/enum.rs Normal file
View File

@ -0,0 +1,50 @@
use crate::{GQLQueryError, GQLType, Result};
use graphql_parser::query::Value;
#[doc(hidden)]
pub struct GQLEnumItem<T> {
pub name: &'static str,
pub desc: Option<&'static str>,
pub value: T,
}
#[doc(hidden)]
#[async_trait::async_trait]
pub trait GQLEnum: GQLType + Sized + Eq + Send + Copy + Sized + 'static {
fn items() -> &'static [GQLEnumItem<Self>];
fn parse_enum(value: Value) -> Result<Self> {
match value {
Value::Enum(s) => {
let items = Self::items();
for item in items {
if item.name == s {
return Ok(item.value);
}
}
Err(GQLQueryError::InvalidEnumValue {
enum_type: Self::type_name(),
value: s,
}
.into())
}
_ => {
return Err(GQLQueryError::ExpectedType {
expect: Self::type_name(),
actual: value,
}
.into())
}
}
}
fn resolve_enum(self) -> Result<serde_json::Value> {
let items = Self::items();
for item in items {
if item.value == self {
return Ok(item.name.clone().into());
}
}
unreachable!()
}
}

77
src/error.rs Normal file
View File

@ -0,0 +1,77 @@
use crate::Error;
use graphql_parser::query::Value;
use graphql_parser::Pos;
use std::fmt::{Debug, Display, Formatter};
#[derive(Debug, Error)]
#[error("{0}")]
pub struct GQLQueryParseError(pub(crate) String);
#[derive(Debug, Error)]
pub enum GQLQueryError {
#[error("Not supported.")]
NotSupported,
#[error("Expected type \"{expect}\", found {actual}.")]
ExpectedType { expect: String, actual: Value },
#[error("Cannot query field \"{field_name}\" on type \"{object}\".")]
FieldNotFound {
field_name: String,
object: &'static str,
},
#[error("Unknown operation named \"{name}\"")]
UnknownOperationNamed { name: String },
#[error("Type \"{object}\" must have a selection of subfields.")]
MustHaveSubFields { object: &'static str },
#[error("Schema is not configured for mutations.")]
NotConfiguredMutations,
#[error("Invalid value for enum \"{enum_type}\"")]
InvalidEnumValue { enum_type: String, value: String },
}
pub trait GQLErrorWithPosition {
type Result;
fn with_position(self, position: Pos) -> GQLPositionError;
}
impl<T: Into<Error>> GQLErrorWithPosition for T {
type Result = GQLPositionError;
fn with_position(self, position: Pos) -> GQLPositionError {
GQLPositionError {
position,
inner: self.into(),
}
}
}
#[derive(Debug, Error)]
pub struct GQLPositionError {
pub position: Pos,
pub inner: Error,
}
impl GQLPositionError {
pub fn new(position: Pos, inner: Error) -> Self {
Self { position, inner }
}
pub fn into_inner(self) -> Error {
self.inner
}
}
impl Display for GQLPositionError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}:{}: {}",
self.position.line, self.position.column, self.inner
)
}
}

35
src/lib.rs Normal file
View File

@ -0,0 +1,35 @@
#[macro_use]
extern crate thiserror;
#[macro_use]
extern crate serde_derive;
mod context;
mod r#enum;
mod error;
mod query;
mod scalar;
mod r#type;
#[cfg(feature = "chrono")]
mod datetime;
#[cfg(feature = "uuid")]
mod uuid;
pub use anyhow;
pub use async_trait;
pub use graphql_parser;
pub use serde_json;
pub use async_graphql_derive::{Enum, InputObject, Object};
pub use context::{Context, ContextField, ContextSelectionSet, Data, Variables};
pub use error::{GQLErrorWithPosition, GQLPositionError, GQLQueryError, GQLQueryParseError};
pub use graphql_parser::query::Value;
pub use query::GQLQueryBuilder;
pub use r#enum::{GQLEnum, GQLEnumItem};
pub use r#type::{
GQLEmptyMutation, GQLInputObject, GQLInputValue, GQLObject, GQLOutputValue, GQLType,
};
pub use scalar::GQLScalar;
pub type Result<T> = anyhow::Result<T>;
pub type Error = anyhow::Error;

111
src/query.rs Normal file
View File

@ -0,0 +1,111 @@
use crate::{
Context, Data, GQLErrorWithPosition, GQLObject, GQLQueryError, GQLQueryParseError, Result,
Variables,
};
use graphql_parser::parse_query;
use graphql_parser::query::{Definition, OperationDefinition};
pub struct GQLQueryBuilder<'a, Query, Mutation> {
query: Query,
mutation: Mutation,
query_source: &'a str,
operation_name: Option<&'a str>,
variables: Option<&'a Variables>,
data: Option<&'a Data>,
}
impl<'a, Query, Mutation> GQLQueryBuilder<'a, Query, Mutation> {
pub fn new(query: Query, mutation: Mutation, query_source: &'a str) -> Self {
Self {
query,
mutation,
query_source,
operation_name: None,
variables: None,
data: None,
}
}
pub fn operator_name(self, name: &'a str) -> Self {
GQLQueryBuilder {
operation_name: Some(name),
..self
}
}
pub fn variables(self, vars: &'a Variables) -> Self {
GQLQueryBuilder {
variables: Some(vars),
..self
}
}
pub fn data(self, data: &'a Data) -> Self {
GQLQueryBuilder {
data: Some(data),
..self
}
}
pub async fn execute(self) -> Result<serde_json::Value>
where
Query: GQLObject,
Mutation: GQLObject,
{
let document =
parse_query(self.query_source).map_err(|err| GQLQueryParseError(err.to_string()))?;
for definition in &document.definitions {
match definition {
Definition::Operation(OperationDefinition::SelectionSet(selection_set)) => {
if self.operation_name.is_none() {
let ctx = Context {
item: selection_set,
data: self.data.as_deref(),
variables: self.variables.as_deref(),
};
return self.query.resolve(&ctx).await;
}
}
Definition::Operation(OperationDefinition::Query(query)) => {
if self.operation_name.is_none()
|| self.operation_name == query.name.as_ref().map(|s| s.as_str())
{
let ctx = Context {
item: &query.selection_set,
data: self.data.as_deref(),
variables: self.variables.as_deref(),
};
return self.query.resolve(&ctx).await;
}
}
Definition::Operation(OperationDefinition::Mutation(mutation)) => {
if self.operation_name.is_none()
|| self.operation_name == mutation.name.as_ref().map(|s| s.as_str())
{
let ctx = Context {
item: &mutation.selection_set,
data: self.data.as_deref(),
variables: self.variables.as_deref(),
};
return self.mutation.resolve(&ctx).await;
}
}
Definition::Operation(OperationDefinition::Subscription(subscription)) => {
anyhow::bail!(GQLQueryError::NotSupported.with_position(subscription.position));
}
Definition::Fragment(fragment) => {
anyhow::bail!(GQLQueryError::NotSupported.with_position(fragment.position));
}
}
}
if let Some(operation_name) = self.operation_name {
anyhow::bail!(GQLQueryError::UnknownOperationNamed {
name: operation_name.to_string()
});
}
Ok(serde_json::Value::Null)
}
}

137
src/scalar.rs Normal file
View File

@ -0,0 +1,137 @@
use crate::r#type::{GQLInputValue, GQLOutputValue, GQLType};
use crate::{ContextSelectionSet, GQLQueryError, Result};
use graphql_parser::query::Value;
pub trait GQLScalar: Sized + Send {
fn type_name() -> &'static str;
fn parse(value: Value) -> Result<Self>;
fn into_json(self) -> Result<serde_json::Value>;
}
impl<T: GQLScalar> GQLType for T {
fn type_name() -> String {
T::type_name().to_string()
}
}
impl<T: GQLScalar> GQLInputValue for T {
fn parse(value: Value) -> Result<Self> {
T::parse(value)
}
}
#[async_trait::async_trait]
impl<T: GQLScalar> GQLOutputValue for T {
async fn resolve(self, _: &ContextSelectionSet<'_>) -> Result<serde_json::Value> {
T::into_json(self)
}
}
macro_rules! impl_integer_scalars {
($($ty:ty),*) => {
$(
impl GQLScalar for $ty {
fn type_name() -> &'static str {
"Int!"
}
fn parse(value: Value) -> Result<Self> {
match value {
Value::Int(n) => Ok(n.as_i64().unwrap() as Self),
_ => {
return Err(GQLQueryError::ExpectedType {
expect: <Self as GQLScalar>::type_name().to_string(),
actual: value,
}
.into())
}
}
}
fn into_json(self) -> Result<serde_json::Value> {
Ok(self.into())
}
}
)*
};
}
impl_integer_scalars!(i8, i16, i32, i64, u8, u16, u32, u64);
macro_rules! impl_float_scalars {
($($ty:ty),*) => {
$(
impl GQLScalar for $ty {
fn type_name() -> &'static str {
"Float!"
}
fn parse(value: Value) -> Result<Self> {
match value {
Value::Int(n) => Ok(n.as_i64().unwrap() as Self),
Value::Float(n) => Ok(n as Self),
_ => {
return Err(GQLQueryError::ExpectedType {
expect: <Self as GQLScalar>::type_name().to_string(),
actual: value,
}
.into())
}
}
}
fn into_json(self) -> Result<serde_json::Value> {
Ok(self.into())
}
}
)*
};
}
impl_float_scalars!(f32, f64);
impl GQLScalar for String {
fn type_name() -> &'static str {
"String!"
}
fn parse(value: Value) -> Result<Self> {
match value {
Value::String(s) => Ok(s),
_ => {
return Err(GQLQueryError::ExpectedType {
expect: <Self as GQLScalar>::type_name().to_string(),
actual: value,
}
.into())
}
}
}
fn into_json(self) -> Result<serde_json::Value> {
Ok(self.into())
}
}
impl GQLScalar for bool {
fn type_name() -> &'static str {
"Boolean!"
}
fn parse(value: Value) -> Result<Self> {
match value {
Value::Boolean(n) => Ok(n),
_ => {
return Err(GQLQueryError::ExpectedType {
expect: <Self as GQLScalar>::type_name().to_string(),
actual: value,
}
.into())
}
}
}
fn into_json(self) -> Result<serde_json::Value> {
Ok(self.into())
}
}

105
src/type.rs Normal file
View File

@ -0,0 +1,105 @@
use crate::{ContextSelectionSet, GQLErrorWithPosition, GQLQueryError, Result};
use graphql_parser::query::Value;
#[doc(hidden)]
pub trait GQLType {
fn type_name() -> String;
}
#[doc(hidden)]
pub trait GQLInputValue: GQLType + Sized {
fn parse(value: Value) -> Result<Self>;
}
#[doc(hidden)]
#[async_trait::async_trait]
pub trait GQLOutputValue: GQLType {
async fn resolve(self, ctx: &ContextSelectionSet<'_>) -> Result<serde_json::Value>;
}
impl<T: GQLType> GQLType for Vec<T> {
fn type_name() -> String {
format!("[{}]!", T::type_name()).into()
}
}
impl<T: GQLInputValue> GQLInputValue for Vec<T> {
fn parse(value: Value) -> Result<Self> {
match value {
Value::List(values) => {
let mut result = Vec::new();
for value in values {
result.push(GQLInputValue::parse(value)?);
}
Ok(result)
}
_ => {
return Err(GQLQueryError::ExpectedType {
expect: Self::type_name(),
actual: value,
}
.into())
}
}
}
}
#[async_trait::async_trait]
impl<T: GQLOutputValue + Send> GQLOutputValue for Vec<T> {
async fn resolve(self, ctx: &ContextSelectionSet<'_>) -> Result<serde_json::Value> {
let mut res = Vec::new();
for item in self {
res.push(item.resolve(ctx).await?);
}
Ok(res.into())
}
}
impl<T: GQLType> GQLType for Option<T> {
fn type_name() -> String {
format!("{}", T::type_name().trim_end_matches("!")).into()
}
}
impl<T: GQLInputValue> GQLInputValue for Option<T> {
fn parse(value: Value) -> Result<Self> {
match value {
Value::Null => Ok(None),
_ => Ok(Some(GQLInputValue::parse(value)?)),
}
}
}
#[async_trait::async_trait]
impl<T: GQLOutputValue + Send> GQLOutputValue for Option<T> {
async fn resolve(self, ctx: &ContextSelectionSet<'_>) -> Result<serde_json::Value> {
if let Some(inner) = self {
inner.resolve(ctx).await
} else {
Ok(serde_json::Value::Null)
}
}
}
#[doc(hidden)]
pub trait GQLObject: GQLType + GQLOutputValue {}
pub struct GQLEmptyMutation;
impl GQLType for GQLEmptyMutation {
fn type_name() -> String {
"EmptyMutation".to_string()
}
}
#[async_trait::async_trait]
impl GQLOutputValue for GQLEmptyMutation {
async fn resolve(self, ctx: &ContextSelectionSet<'_>) -> Result<serde_json::Value> {
anyhow::bail!(GQLQueryError::NotConfiguredMutations.with_position(ctx.item.span.0));
}
}
impl GQLObject for GQLEmptyMutation {}
#[doc(hidden)]
pub trait GQLInputObject: GQLType + GQLInputValue {}

25
src/uuid.rs Normal file
View File

@ -0,0 +1,25 @@
use crate::{GQLQueryError, GQLScalar, Result, Value};
use uuid::Uuid;
impl GQLScalar for Uuid {
fn type_name() -> &'static str {
"UUID"
}
fn parse(value: Value) -> Result<Self> {
match value {
Value::String(s) => Ok(Uuid::parse_str(&s)?),
_ => {
return Err(GQLQueryError::ExpectedType {
expect: Self::type_name().to_string(),
actual: value,
}
.into())
}
}
}
fn into_json(self) -> Result<serde_json::Value> {
Ok(self.to_string().into())
}
}