init commit
This commit is contained in:
commit
5030a28361
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
/target
|
||||||
|
Cargo.lock
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
29
Cargo.toml
Normal file
29
Cargo.toml
Normal 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"
|
||||||
|
]
|
13
async-graphql-derive/Cargo.toml
Normal file
13
async-graphql-derive/Cargo.toml
Normal 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"
|
317
async-graphql-derive/src/args.rs
Normal file
317
async-graphql-derive/src/args.rs
Normal 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 })
|
||||||
|
}
|
||||||
|
}
|
83
async-graphql-derive/src/enum.rs
Normal file
83
async-graphql-derive/src/enum.rs
Normal 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())
|
||||||
|
}
|
40
async-graphql-derive/src/input_object.rs
Normal file
40
async-graphql-derive/src/input_object.rs
Normal 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())
|
||||||
|
}
|
52
async-graphql-derive/src/lib.rs
Normal file
52
async-graphql-derive/src/lib.rs
Normal 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(),
|
||||||
|
}
|
||||||
|
}
|
128
async-graphql-derive/src/object.rs
Normal file
128
async-graphql-derive/src/object.rs
Normal 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
68
examples/helloworld.rs
Normal 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
85
src/context.rs
Normal 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
25
src/datetime.rs
Normal 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
50
src/enum.rs
Normal 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
77
src/error.rs
Normal 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
35
src/lib.rs
Normal 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
111
src/query.rs
Normal 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
137
src/scalar.rs
Normal 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
105
src/type.rs
Normal 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
25
src/uuid.rs
Normal 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())
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user