Merge branch 'actix-web-v4-beta' into upgrade-actix-web
This commit is contained in:
commit
f872dcda8d
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
|
@ -16,8 +16,8 @@ jobs:
|
|||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- { rust: stable, os: ubuntu-latest }
|
||||
- { rust: 1.54.0, os: ubuntu-latest }
|
||||
# - { rust: stable, os: ubuntu-latest }
|
||||
- { rust: 1.56.1, os: ubuntu-latest }
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
|
5
.github/workflows/code-coverage.yml
vendored
5
.github/workflows/code-coverage.yml
vendored
|
@ -10,6 +10,11 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Install stable toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: 1.56.1
|
||||
override: true
|
||||
- name: Run cargo-tarpaulin
|
||||
uses: actions-rs/tarpaulin@v0.1
|
||||
with:
|
||||
|
|
|
@ -1,2 +1,2 @@
|
|||
edition = "2018"
|
||||
edition = "2021"
|
||||
newline_style = "unix"
|
||||
|
|
32
CHANGELOG.md
32
CHANGELOG.md
|
@ -4,6 +4,38 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Add `chrono::Duration` custom scalar. [#689](https://github.com/async-graphql/async-graphql/pull/689)
|
||||
- Implement `From<Option<Option<T>>>` for `MaybeUndefined<T>`.
|
||||
- Add `MaybeUndefined::as_opt_ref`, `MaybeUndefined::as_opt_deref`, `MaybeUndefined::map`, `MaybeUndefined::map_value`, `MaybeUndefined::contains`, `MaybeUndefined::contains_value`, and `MaybeUndefined::transpose` methods.
|
||||
- Made `MaybeUndefined::is_undefined`, `MaybeUndefined::is_null`, `MaybeUndefined::is_value`, `MaybeUndefined::value` and `MaybeUndefined::as_opt_ref` const.
|
||||
- Add `ResolverError` type. [#671](https://github.com/async-graphql/async-graphql/issues/671)
|
||||
- [async-graphql-axum] Bump axum from `0.2.5` to `0.3`.
|
||||
- [async-graphql-poem] Export the HTTP headers in the `Context`.
|
||||
|
||||
## [2.11.0] 2021-11-03
|
||||
|
||||
- Use Rust `2021` edition.
|
||||
- Subscription typename - [GraphQL - October 2021] [#681](https://github.com/async-graphql/async-graphql/issues/681)
|
||||
- Allow directive on variable definition - [GraphQL - October 2021] [#678](https://github.com/async-graphql/async-graphql/issues/678)
|
||||
- Specified By - [GraphQL - October 2021] [#677](https://github.com/async-graphql/async-graphql/issues/677)
|
||||
- Add `specified_by_url` for `Tz`, `DateTime<Tz>`, `Url`, `Uuid` and `Upload` scalars.
|
||||
- Number value literal lookahead restrictions - [GraphQL - October 2021] [#685](https://github.com/async-graphql/async-graphql/issues/685)
|
||||
|
||||
## [2.10.8] 2021-10-26
|
||||
|
||||
- [async-graphql-poem] Bump poem to `1.0.13`.
|
||||
|
||||
## [2.10.6] 2021-10-26
|
||||
|
||||
- Add derived for object & simple object & complex object. [#667](https://github.com/async-graphql/async-graphql/pull/667) [#670](https://github.com/async-graphql/async-graphql/pull/670)
|
||||
- Respect query object field order. [#612](https://github.com/async-graphql/async-graphql/issues/612)
|
||||
|
||||
## [2.10.5] 2021-10-22
|
||||
|
||||
- Bump poem from `0.6.6` to `1.0.7`.
|
||||
|
||||
## [2.10.4] 2021-10-22
|
||||
|
||||
- Implement `Default` for ID #659.
|
||||
|
|
12
Cargo.toml
12
Cargo.toml
|
@ -1,8 +1,8 @@
|
|||
[package]
|
||||
name = "async-graphql"
|
||||
version = "2.10.4"
|
||||
version = "2.11.0"
|
||||
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
description = "A GraphQL server library implemented in Rust"
|
||||
license = "MIT/Apache-2.0"
|
||||
documentation = "https://docs.rs/async-graphql/"
|
||||
|
@ -22,11 +22,12 @@ dataloader = ["futures-timer", "futures-channel", "lru"]
|
|||
tracing = ["tracinglib", "tracing-futures"]
|
||||
decimal = ["rust_decimal"]
|
||||
cbor = ["serde_cbor"]
|
||||
chrono-duration = ["chrono", "iso8601-duration"]
|
||||
|
||||
[dependencies]
|
||||
async-graphql-derive = { path = "derive", version = "=2.10.4" }
|
||||
async-graphql-value = { path = "value", version = "=2.10.4" }
|
||||
async-graphql-parser = { path = "parser", version = "=2.10.4" }
|
||||
async-graphql-derive = { path = "derive", version = "=2.11.0" }
|
||||
async-graphql-value = { path = "value", version = "=2.11.0" }
|
||||
async-graphql-parser = { path = "parser", version = "=2.11.0" }
|
||||
|
||||
async-stream = "0.3.0"
|
||||
async-trait = "0.1.48"
|
||||
|
@ -50,6 +51,7 @@ mime = "0.3.15"
|
|||
bson = { version = "2.0.0", optional = true, features = ["chrono-0_4"] }
|
||||
chrono = { version = "0.4.19", optional = true }
|
||||
chrono-tz = { version = "0.5.3", optional = true }
|
||||
iso8601-duration = { version = "0.1.0", optional = true }
|
||||
log = { version = "0.4.14", optional = true }
|
||||
secrecy = { version = "0.7.0", optional = true }
|
||||
tracinglib = { version = "0.1.25", optional = true, package = "tracing" }
|
||||
|
|
|
@ -34,7 +34,7 @@
|
|||
* [Docs](https://docs.rs/async-graphql)
|
||||
* [GitHub repository](https://github.com/async-graphql/async-graphql)
|
||||
* [Cargo package](https://crates.io/crates/async-graphql)
|
||||
* Minimum supported Rust version: 1.54.0 or later
|
||||
* Minimum supported Rust version: 1.56.1 or later
|
||||
|
||||
## Safety
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
[package]
|
||||
name = "async-graphql-derive"
|
||||
version = "2.10.4"
|
||||
version = "2.11.0"
|
||||
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
description = "Macros for async-graphql"
|
||||
license = "MIT/Apache-2.0"
|
||||
documentation = "https://docs.rs/async-graphql/"
|
||||
|
@ -15,7 +15,7 @@ categories = ["network-programming", "asynchronous"]
|
|||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
async-graphql-parser = { path = "../parser", version = "=2.10.4" }
|
||||
async-graphql-parser = { path = "../parser", version = "=2.11.0" }
|
||||
proc-macro2 = "1.0.24"
|
||||
syn = { version = "1.0.64", features = ["full", "extra-traits", "visit-mut", "visit"] }
|
||||
quote = "1.0.9"
|
||||
|
|
|
@ -6,7 +6,7 @@ use syn::{
|
|||
Attribute, Generics, Ident, Lit, LitBool, LitStr, Meta, NestedMeta, Path, Type, Visibility,
|
||||
};
|
||||
|
||||
#[derive(FromMeta)]
|
||||
#[derive(FromMeta, Clone)]
|
||||
#[darling(default)]
|
||||
pub struct CacheControl {
|
||||
public: bool,
|
||||
|
@ -46,7 +46,7 @@ impl FromMeta for DefaultValue {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Visible {
|
||||
None,
|
||||
HiddenAlways,
|
||||
|
@ -86,7 +86,7 @@ pub struct ConcreteType {
|
|||
pub params: PathList,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Deprecation {
|
||||
NoDeprecated,
|
||||
Deprecated { reason: Option<String> },
|
||||
|
@ -115,7 +115,7 @@ impl FromMeta for Deprecation {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(FromField)]
|
||||
#[derive(FromField, Clone)]
|
||||
#[darling(attributes(graphql), forward_attrs(doc))]
|
||||
pub struct SimpleObjectField {
|
||||
pub ident: Option<Ident>,
|
||||
|
@ -143,6 +143,8 @@ pub struct SimpleObjectField {
|
|||
pub guard: Option<Meta>,
|
||||
#[darling(default)]
|
||||
pub visible: Option<Visible>,
|
||||
#[darling(default, multiple)]
|
||||
pub derived: Vec<DerivedField>,
|
||||
}
|
||||
|
||||
#[derive(FromDeriveInput)]
|
||||
|
@ -243,6 +245,19 @@ pub struct ObjectField {
|
|||
pub guard: Option<Meta>,
|
||||
pub visible: Option<Visible>,
|
||||
pub complexity: Option<ComplexityType>,
|
||||
#[darling(default, multiple)]
|
||||
pub derived: Vec<DerivedField>,
|
||||
}
|
||||
|
||||
#[derive(FromMeta, Default, Clone)]
|
||||
#[darling(default)]
|
||||
/// Derivied fields arguments: are used to generate derivied fields.
|
||||
pub struct DerivedField {
|
||||
pub name: Option<Ident>,
|
||||
pub into: Option<String>,
|
||||
pub with: Option<Path>,
|
||||
#[darling(default)]
|
||||
pub owned: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(FromDeriveInput)]
|
||||
|
@ -431,6 +446,7 @@ pub struct Scalar {
|
|||
pub name: Option<String>,
|
||||
pub use_type_description: bool,
|
||||
pub visible: Option<Visible>,
|
||||
pub specified_by_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(FromMeta, Default)]
|
||||
|
@ -637,6 +653,8 @@ pub struct NewType {
|
|||
pub name: NewTypeName,
|
||||
#[darling(default)]
|
||||
pub visible: Option<Visible>,
|
||||
#[darling(default)]
|
||||
pub specified_by_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(FromMeta, Default)]
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
use proc_macro::TokenStream;
|
||||
use proc_macro2::Ident;
|
||||
use quote::quote;
|
||||
use std::iter::FromIterator;
|
||||
use std::str::FromStr;
|
||||
use syn::ext::IdentExt;
|
||||
use syn::{Block, Error, ImplItem, ItemImpl, ReturnType};
|
||||
use syn::{
|
||||
punctuated::Punctuated, Block, Error, FnArg, ImplItem, ItemImpl, Pat, ReturnType, Token, Type,
|
||||
TypeReference,
|
||||
};
|
||||
|
||||
use crate::args::{self, ComplexityType, RenameRuleExt, RenameTarget};
|
||||
use crate::output_type::OutputType;
|
||||
|
@ -23,6 +29,91 @@ pub fn generate(
|
|||
let mut resolvers = Vec::new();
|
||||
let mut schema_fields = Vec::new();
|
||||
|
||||
// Computation of the derivated fields
|
||||
let mut derived_impls = vec![];
|
||||
for item in &mut item_impl.items {
|
||||
if let ImplItem::Method(method) = item {
|
||||
let method_args: args::ObjectField =
|
||||
parse_graphql_attrs(&method.attrs)?.unwrap_or_default();
|
||||
|
||||
for derived in method_args.derived {
|
||||
if derived.name.is_some() && derived.into.is_some() {
|
||||
let base_function_name = &method.sig.ident;
|
||||
let name = derived.name.unwrap();
|
||||
let with = derived.with;
|
||||
let into = Type::Verbatim(
|
||||
proc_macro2::TokenStream::from_str(&derived.into.unwrap()).unwrap(),
|
||||
);
|
||||
|
||||
let mut new_impl = method.clone();
|
||||
new_impl.sig.ident = name;
|
||||
new_impl.sig.output =
|
||||
syn::parse2::<ReturnType>(quote! { -> #crate_name::Result<#into> })
|
||||
.expect("invalid result type");
|
||||
|
||||
let should_create_context = new_impl
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.nth(1)
|
||||
.map(|x| {
|
||||
if let FnArg::Typed(pat) = x {
|
||||
if let Type::Reference(TypeReference { elem, .. }) = &*pat.ty {
|
||||
if let Type::Path(path) = elem.as_ref() {
|
||||
return path.path.segments.last().unwrap().ident
|
||||
!= "Context";
|
||||
}
|
||||
}
|
||||
};
|
||||
true
|
||||
})
|
||||
.unwrap_or(true);
|
||||
|
||||
if should_create_context {
|
||||
let arg_ctx = syn::parse2::<FnArg>(quote! { ctx: &Context<'_> })
|
||||
.expect("invalid arg type");
|
||||
new_impl.sig.inputs.insert(1, arg_ctx);
|
||||
}
|
||||
|
||||
let other_atts: Punctuated<Ident, Token![,]> = Punctuated::from_iter(
|
||||
new_impl
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.filter_map(|x| match x {
|
||||
FnArg::Typed(pat) => match &*pat.pat {
|
||||
Pat::Ident(ident) => Some(Ok(ident.ident.clone())),
|
||||
_ => Some(Err(Error::new_spanned(
|
||||
&pat,
|
||||
"Must be a simple argument",
|
||||
))),
|
||||
},
|
||||
FnArg::Receiver(_) => None,
|
||||
})
|
||||
.collect::<Result<Vec<Ident>, Error>>()?
|
||||
.into_iter(),
|
||||
);
|
||||
|
||||
let new_block = match with {
|
||||
Some(with) => quote!({
|
||||
::std::result::Result::Ok(#with(#self_ty::#base_function_name(&self, #other_atts).await?))
|
||||
}),
|
||||
None => quote!({
|
||||
{
|
||||
::std::result::Result::Ok(#self_ty::#base_function_name(&self, #other_atts).await?.into())
|
||||
}
|
||||
}),
|
||||
};
|
||||
|
||||
new_impl.block = syn::parse2::<Block>(new_block).expect("invalid block");
|
||||
|
||||
derived_impls.push(ImplItem::Method(new_impl));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
item_impl.items.append(&mut derived_impls);
|
||||
|
||||
for item in &mut item_impl.items {
|
||||
if let ImplItem::Method(method) = item {
|
||||
let method_args: args::ObjectField =
|
||||
|
|
|
@ -71,13 +71,9 @@ pub fn generate(enum_args: &args::Enum) -> GeneratorResult<TokenStream> {
|
|||
}
|
||||
|
||||
let remote_conversion = if let Some(remote) = &enum_args.remote {
|
||||
let remote_ty = if let Ok(ty) = syn::parse_str::<syn::Type>(remote) {
|
||||
ty
|
||||
} else {
|
||||
return Err(
|
||||
Error::new_spanned(remote, format!("Invalid remote type: '{}'", remote)).into(),
|
||||
);
|
||||
};
|
||||
let remote_ty = syn::parse_str::<syn::Type>(remote).map_err(|_| {
|
||||
Error::new_spanned(remote, format!("Invalid remote type: '{}'", remote))
|
||||
})?;
|
||||
|
||||
let local_to_remote_items = enum_items.iter().map(|item| {
|
||||
quote! {
|
||||
|
|
|
@ -231,7 +231,7 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult<TokenStream>
|
|||
}
|
||||
|
||||
fn to_value(&self) -> #crate_name::Value {
|
||||
let mut map = ::std::collections::BTreeMap::new();
|
||||
let mut map = #crate_name::indexmap::IndexMap::new();
|
||||
#(#put_fields)*
|
||||
#crate_name::Value::Object(map)
|
||||
}
|
||||
|
@ -272,7 +272,7 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult<TokenStream>
|
|||
}
|
||||
|
||||
fn __internal_to_value(&self) -> #crate_name::Value where Self: #crate_name::InputType {
|
||||
let mut map = ::std::collections::BTreeMap::new();
|
||||
let mut map = #crate_name::indexmap::IndexMap::new();
|
||||
#(#put_fields)*
|
||||
#crate_name::Value::Object(map)
|
||||
}
|
||||
|
|
|
@ -90,6 +90,7 @@ pub fn generate(object_args: &args::MergedObject) -> GeneratorResult<TokenStream
|
|||
extends: #extends,
|
||||
keys: ::std::option::Option::None,
|
||||
visible: #visible,
|
||||
is_subscription: false,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -72,6 +72,7 @@ pub fn generate(object_args: &args::MergedSubscription) -> GeneratorResult<Token
|
|||
extends: #extends,
|
||||
keys: ::std::option::Option::None,
|
||||
visible: #visible,
|
||||
is_subscription: true,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -38,12 +38,18 @@ pub fn generate(newtype_args: &args::NewType) -> GeneratorResult<TokenStream> {
|
|||
None => quote! { <#inner_ty as #crate_name::Type>::type_name() },
|
||||
};
|
||||
let create_type_info = if let Some(name) = &gql_typename {
|
||||
let specified_by_url = match &newtype_args.specified_by_url {
|
||||
Some(specified_by_url) => quote! { ::std::option::Option::Some(#specified_by_url) },
|
||||
None => quote! { ::std::option::Option::None },
|
||||
};
|
||||
|
||||
quote! {
|
||||
registry.create_type::<#ident, _>(|_| #crate_name::registry::MetaType::Scalar {
|
||||
name: ::std::borrow::ToOwned::to_owned(#name),
|
||||
description: #desc,
|
||||
is_valid: |value| <#ident as #crate_name::ScalarType>::is_valid(value),
|
||||
visible: #visible,
|
||||
specified_by_url: #specified_by_url,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
use proc_macro::TokenStream;
|
||||
use proc_macro2::Ident;
|
||||
use quote::quote;
|
||||
use std::iter::FromIterator;
|
||||
use std::str::FromStr;
|
||||
use syn::ext::IdentExt;
|
||||
use syn::{Block, Error, ImplItem, ItemImpl, ReturnType};
|
||||
use syn::{
|
||||
punctuated::Punctuated, Block, Error, FnArg, ImplItem, ItemImpl, Pat, ReturnType, Token, Type,
|
||||
TypeReference,
|
||||
};
|
||||
|
||||
use crate::args::{self, ComplexityType, RenameRuleExt, RenameTarget};
|
||||
use crate::output_type::OutputType;
|
||||
|
@ -38,6 +44,91 @@ pub fn generate(
|
|||
let mut add_keys = Vec::new();
|
||||
let mut create_entity_types = Vec::new();
|
||||
|
||||
// Computation of the derivated fields
|
||||
let mut derived_impls = vec![];
|
||||
for item in &mut item_impl.items {
|
||||
if let ImplItem::Method(method) = item {
|
||||
let method_args: args::ObjectField =
|
||||
parse_graphql_attrs(&method.attrs)?.unwrap_or_default();
|
||||
|
||||
for derived in method_args.derived {
|
||||
if derived.name.is_some() && derived.into.is_some() {
|
||||
let base_function_name = &method.sig.ident;
|
||||
let name = derived.name.unwrap();
|
||||
let with = derived.with;
|
||||
let into = Type::Verbatim(
|
||||
proc_macro2::TokenStream::from_str(&derived.into.unwrap()).unwrap(),
|
||||
);
|
||||
|
||||
let mut new_impl = method.clone();
|
||||
new_impl.sig.ident = name;
|
||||
new_impl.sig.output =
|
||||
syn::parse2::<ReturnType>(quote! { -> #crate_name::Result<#into> })
|
||||
.expect("invalid result type");
|
||||
|
||||
let should_create_context = new_impl
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.nth(1)
|
||||
.map(|x| {
|
||||
if let FnArg::Typed(pat) = x {
|
||||
if let Type::Reference(TypeReference { elem, .. }) = &*pat.ty {
|
||||
if let Type::Path(path) = elem.as_ref() {
|
||||
return path.path.segments.last().unwrap().ident
|
||||
!= "Context";
|
||||
}
|
||||
}
|
||||
};
|
||||
true
|
||||
})
|
||||
.unwrap_or(true);
|
||||
|
||||
if should_create_context {
|
||||
let arg_ctx = syn::parse2::<FnArg>(quote! { ctx: &Context<'_> })
|
||||
.expect("invalid arg type");
|
||||
new_impl.sig.inputs.insert(1, arg_ctx);
|
||||
}
|
||||
|
||||
let other_atts: Punctuated<Ident, Token![,]> = Punctuated::from_iter(
|
||||
new_impl
|
||||
.sig
|
||||
.inputs
|
||||
.iter()
|
||||
.filter_map(|x| match x {
|
||||
FnArg::Typed(pat) => match &*pat.pat {
|
||||
Pat::Ident(ident) => Some(Ok(ident.ident.clone())),
|
||||
_ => Some(Err(Error::new_spanned(
|
||||
&pat,
|
||||
"Must be a simple argument",
|
||||
))),
|
||||
},
|
||||
FnArg::Receiver(_) => None,
|
||||
})
|
||||
.collect::<Result<Vec<Ident>, Error>>()?
|
||||
.into_iter(),
|
||||
);
|
||||
|
||||
let new_block = match with {
|
||||
Some(with) => quote!({
|
||||
::std::result::Result::Ok(#with(#self_ty::#base_function_name(&self, #other_atts).await?))
|
||||
}),
|
||||
None => quote!({
|
||||
{
|
||||
::std::result::Result::Ok(#self_ty::#base_function_name(&self, #other_atts).await?.into())
|
||||
}
|
||||
}),
|
||||
};
|
||||
|
||||
new_impl.block = syn::parse2::<Block>(new_block).expect("invalid block");
|
||||
|
||||
derived_impls.push(ImplItem::Method(new_impl));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
item_impl.items.append(&mut derived_impls);
|
||||
|
||||
for item in &mut item_impl.items {
|
||||
if let ImplItem::Method(method) = item {
|
||||
let method_args: args::ObjectField =
|
||||
|
@ -472,6 +563,7 @@ pub fn generate(
|
|||
extends: #extends,
|
||||
keys: ::std::option::Option::None,
|
||||
visible: #visible,
|
||||
is_subscription: false,
|
||||
});
|
||||
#(#create_entity_types)*
|
||||
#(#add_keys)*
|
||||
|
@ -539,6 +631,7 @@ pub fn generate(
|
|||
extends: #extends,
|
||||
keys: ::std::option::Option::None,
|
||||
visible: #visible,
|
||||
is_subscription: false,
|
||||
});
|
||||
#(#create_entity_types)*
|
||||
#(#add_keys)*
|
||||
|
|
|
@ -30,6 +30,11 @@ pub fn generate(
|
|||
let generic = &item_impl.generics;
|
||||
let where_clause = &item_impl.generics.where_clause;
|
||||
let visible = visible_fn(&scalar_args.visible);
|
||||
let specified_by_url = match &scalar_args.specified_by_url {
|
||||
Some(specified_by_url) => quote! { ::std::option::Option::Some(#specified_by_url) },
|
||||
None => quote! { ::std::option::Option::None },
|
||||
};
|
||||
|
||||
let expanded = quote! {
|
||||
#item_impl
|
||||
|
||||
|
@ -45,6 +50,7 @@ pub fn generate(
|
|||
description: #desc,
|
||||
is_valid: |value| <#self_ty as #crate_name::ScalarType>::is_valid(value),
|
||||
visible: #visible,
|
||||
specified_by_url: #specified_by_url,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,28 @@
|
|||
use darling::ast::Data;
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use std::str::FromStr;
|
||||
use syn::ext::IdentExt;
|
||||
use syn::Error;
|
||||
use syn::{Error, Ident, Path, Type};
|
||||
|
||||
use crate::args::{self, RenameRuleExt, RenameTarget};
|
||||
use crate::args::{self, RenameRuleExt, RenameTarget, SimpleObjectField};
|
||||
use crate::utils::{
|
||||
gen_deprecation, generate_guards, get_crate_name, get_rustdoc, visible_fn, GeneratorResult,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
struct DerivedFieldMetadata {
|
||||
ident: Ident,
|
||||
into: Type,
|
||||
owned: Option<bool>,
|
||||
with: Option<Path>,
|
||||
}
|
||||
|
||||
struct SimpleObjectFieldGenerator<'a> {
|
||||
field: &'a SimpleObjectField,
|
||||
derived: Option<DerivedFieldMetadata>,
|
||||
}
|
||||
|
||||
pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult<TokenStream> {
|
||||
let crate_name = get_crate_name(object_args.internal);
|
||||
let ident = &object_args.ident;
|
||||
|
@ -37,15 +51,62 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult<TokenStream
|
|||
let mut resolvers = Vec::new();
|
||||
let mut schema_fields = Vec::new();
|
||||
|
||||
let mut processed_fields: Vec<SimpleObjectFieldGenerator> = vec![];
|
||||
|
||||
// Before processing the fields, we generate the derivated fields
|
||||
for field in &s.fields {
|
||||
processed_fields.push(SimpleObjectFieldGenerator {
|
||||
field,
|
||||
derived: None,
|
||||
});
|
||||
|
||||
for derived in &field.derived {
|
||||
if derived.name.is_some() && derived.into.is_some() {
|
||||
let name = derived.name.clone().unwrap();
|
||||
let into = match syn::parse2::<Type>(
|
||||
proc_macro2::TokenStream::from_str(&derived.into.clone().unwrap()).unwrap(),
|
||||
) {
|
||||
Ok(e) => e,
|
||||
_ => {
|
||||
return Err(Error::new_spanned(
|
||||
&name,
|
||||
"derived into must be a valid type.",
|
||||
)
|
||||
.into());
|
||||
}
|
||||
};
|
||||
|
||||
let derived = DerivedFieldMetadata {
|
||||
ident: name,
|
||||
into,
|
||||
owned: derived.owned,
|
||||
with: derived.with.clone(),
|
||||
};
|
||||
|
||||
processed_fields.push(SimpleObjectFieldGenerator {
|
||||
field,
|
||||
derived: Some(derived),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for SimpleObjectFieldGenerator { field, derived } in &processed_fields {
|
||||
if field.skip {
|
||||
continue;
|
||||
}
|
||||
let ident = match &field.ident {
|
||||
|
||||
let base_ident = match &field.ident {
|
||||
Some(ident) => ident,
|
||||
None => return Err(Error::new_spanned(&ident, "All fields must be named.").into()),
|
||||
};
|
||||
|
||||
let ident = if let Some(derived) = derived {
|
||||
&derived.ident
|
||||
} else {
|
||||
base_ident
|
||||
};
|
||||
|
||||
let field_name = field.name.clone().unwrap_or_else(|| {
|
||||
object_args
|
||||
.rename_fields
|
||||
|
@ -65,7 +126,18 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult<TokenStream
|
|||
None => quote! { ::std::option::Option::None },
|
||||
};
|
||||
let vis = &field.vis;
|
||||
let ty = &field.ty;
|
||||
|
||||
let ty = if let Some(derived) = derived {
|
||||
&derived.into
|
||||
} else {
|
||||
&field.ty
|
||||
};
|
||||
|
||||
let owned = if let Some(derived) = derived {
|
||||
derived.owned.unwrap_or(field.owned)
|
||||
} else {
|
||||
field.owned
|
||||
};
|
||||
|
||||
let cache_control = {
|
||||
let public = field.cache_control.is_public();
|
||||
|
@ -104,23 +176,41 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult<TokenStream
|
|||
|guard| quote! { #guard.check(ctx).await.map_err(|err| err.into_server_error(ctx.item.pos))?; },
|
||||
);
|
||||
|
||||
getters.push(if !field.owned {
|
||||
let with_function = derived.as_ref().and_then(|x| x.with.as_ref());
|
||||
|
||||
let mut block = match !owned {
|
||||
true => quote! {
|
||||
&self.#base_ident
|
||||
},
|
||||
false => quote! {
|
||||
::std::clone::Clone::clone(&self.#base_ident)
|
||||
},
|
||||
};
|
||||
|
||||
block = match (derived, with_function) {
|
||||
(Some(_), Some(with)) => quote! {
|
||||
#with(#block)
|
||||
},
|
||||
(Some(_), None) => quote! {
|
||||
::std::convert::Into::into(#block)
|
||||
},
|
||||
(_, _) => block,
|
||||
};
|
||||
|
||||
let ty = match !owned {
|
||||
true => quote! { &#ty },
|
||||
false => quote! { #ty },
|
||||
};
|
||||
|
||||
getters.push(
|
||||
quote! {
|
||||
#[inline]
|
||||
#[allow(missing_docs)]
|
||||
#vis async fn #ident(&self, ctx: &#crate_name::Context<'_>) -> #crate_name::Result<&#ty> {
|
||||
::std::result::Result::Ok(&self.#ident)
|
||||
#vis async fn #ident(&self, ctx: &#crate_name::Context<'_>) -> #crate_name::Result<#ty> {
|
||||
::std::result::Result::Ok(#block)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
#[inline]
|
||||
#[allow(missing_docs)]
|
||||
#vis async fn #ident(&self, ctx: &#crate_name::Context<'_>) -> #crate_name::Result<#ty> {
|
||||
::std::result::Result::Ok(::std::clone::Clone::clone(&self.#ident))
|
||||
}
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
resolvers.push(quote! {
|
||||
if ctx.item.node.name.node == #field_name {
|
||||
|
@ -203,6 +293,7 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult<TokenStream
|
|||
extends: #extends,
|
||||
keys: ::std::option::Option::None,
|
||||
visible: #visible,
|
||||
is_subscription: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -253,6 +344,7 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult<TokenStream
|
|||
extends: #extends,
|
||||
keys: ::std::option::Option::None,
|
||||
visible: #visible,
|
||||
is_subscription: false,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -375,7 +375,7 @@ pub fn generate(
|
|||
};
|
||||
#crate_name::futures_util::pin_mut!(resolve_fut);
|
||||
let mut resp = query_env.extensions.resolve(ri, &mut resolve_fut).await.map(|value| {
|
||||
let mut map = ::std::collections::BTreeMap::new();
|
||||
let mut map = #crate_name::indexmap::IndexMap::new();
|
||||
map.insert(::std::clone::Clone::clone(&field_name), value.unwrap_or_default());
|
||||
#crate_name::Response::new(#crate_name::Value::Object(map))
|
||||
})
|
||||
|
@ -456,6 +456,7 @@ pub fn generate(
|
|||
extends: #extends,
|
||||
keys: ::std::option::Option::None,
|
||||
visible: ::std::option::Option::None,
|
||||
is_subscription: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
- [Context](context.md)
|
||||
- [Error handling](error_handling.md)
|
||||
- [Merging Objects / Subscriptions](merging_objects.md)
|
||||
- [Derived fields](derived_fields.md)
|
||||
- [Complex Object](define_complex_object.md)
|
||||
- [Enum](define_enum.md)
|
||||
- [Interface](define_interface.md)
|
||||
- [Union](define_union.md)
|
||||
|
@ -26,6 +28,9 @@
|
|||
- [Apollo Tracing](apollo_tracing.md)
|
||||
- [Query complexity and depth](depth_and_complexity.md)
|
||||
- [Hide content in introspection](visibility.md)
|
||||
- [Extensions](extensions.md)
|
||||
- [How extensions are working](extensions_inner_working.md)
|
||||
- [Available extensions](extensions_available.md)
|
||||
- [Integrations](integrations.md)
|
||||
- [Tide](integrations_to_tide.md)
|
||||
- [Warp](integrations_to_warp.md)
|
||||
|
@ -33,5 +38,4 @@
|
|||
- [Advanced topics](advanced_topics.md)
|
||||
- [Custom scalars](custom_scalars.md)
|
||||
- [Optimizing N+1 queries](dataloader.md)
|
||||
- [Custom extensions](custom_extensions.md)
|
||||
- [Apollo Federation](apollo_federation.md)
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
# Custom extensions
|
||||
|
||||
A GraphQL extension object can receive events in various stages of a query's execution, and you can collect various kinds of data to be returned in the query results.
|
||||
|
||||
You can use `async_graphql::Extension` to define an extension object, and your application must call `Schema::extension` when your `Schema` is created.
|
||||
|
||||
You can refer to [Apollo Tracing](https://github.com/async-graphql/async-graphql/blob/master/src/extensions/tracing.rs) to implement your own extension types.
|
103
docs/en/src/derived_fields.md
Normal file
103
docs/en/src/derived_fields.md
Normal file
|
@ -0,0 +1,103 @@
|
|||
# Derived fields
|
||||
|
||||
When you are working on a GraphQL project, you usually have to explain and share how your scalars should
|
||||
be interpreted by your consumers. Sometimes, you event want to have the same data and the same logic exposing
|
||||
the data in another type.
|
||||
|
||||
Within `async-graphql` you can create derived fields for objects to generate derived fields.
|
||||
|
||||
Consider you want to create a `Date` scalar, to represent an event of time.
|
||||
How will you represent and format this date? You could create a scalar `Date` where you specified it's the RFCXXX
|
||||
implemented to format it.
|
||||
|
||||
With derived fields there is a simple way to support multiple representation of a `Date` easily:
|
||||
|
||||
```rust
|
||||
struct DateRFC3339(chrono::DateTime);
|
||||
struct DateRFC2822(chrono::DateTime);
|
||||
|
||||
#[Scalar]
|
||||
impl ScalarType for DateRFC3339 {
|
||||
fn parse(value: Value) -> InputValueResult { ... }
|
||||
|
||||
fn to_value(&self) -> Value {
|
||||
Value::String(self.0.to_rfc3339())
|
||||
}
|
||||
}
|
||||
|
||||
#[Scalar]
|
||||
impl ScalarType for DateRFC2822 {
|
||||
fn parse(value: Value) -> InputValueResult { ... }
|
||||
|
||||
fn to_value(&self) -> Value {
|
||||
Value::String(self.0.to_rfc2822())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DateRFC2822> for DateRFC3339 {
|
||||
fn from(value: DateRFC2822) -> Self {
|
||||
DateRFC3339(value.0)
|
||||
}
|
||||
}
|
||||
|
||||
struct Query;
|
||||
|
||||
#[Object]
|
||||
impl Query {
|
||||
#[graphql(derived(name = "date_rfc3339", into = "DateRFC3339"))]
|
||||
async fn duration_rfc2822(&self, arg: String) -> DateRFC2822 {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
It will render a GraphQL like:
|
||||
|
||||
```graphql
|
||||
type Query {
|
||||
duration_rfc2822(arg: String): DateRFC2822!
|
||||
duration_rfc3339(arg: String): DateRFC3339!
|
||||
}
|
||||
```
|
||||
|
||||
## Wrapper types
|
||||
|
||||
A derived field won't be able to manage everythings easily: without the specialization from the Rust language, you won't be able to implement specialized trait like:
|
||||
```
|
||||
impl From<Vec<U>> for Vec<T> {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
So you wouldn't be able to generate derived fields for existing wrapper type structures like `Vec` or `Option`. But when you implement a `From<U> for T` you should be able to derived a `From<Vec<U>> for Vec<T>` and a `From<Option<U>> for Option<T>`.
|
||||
We included a `with` parameter to help you define a function to call instead of using the `Into` trait implementation between wrapper structures.
|
||||
|
||||
|
||||
### Example
|
||||
|
||||
```rust
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
struct ValueDerived(String);
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
struct ValueDerived2(String);
|
||||
|
||||
scalar!(ValueDerived);
|
||||
scalar!(ValueDerived2);
|
||||
|
||||
impl From<ValueDerived> for ValueDerived2 {
|
||||
fn from(value: ValueDerived) -> Self {
|
||||
ValueDerived2(value.0)
|
||||
}
|
||||
}
|
||||
|
||||
fn option_to_option<T, U: From<T>>(value: Option<T>) -> Option<U> {
|
||||
value.map(|x| x.into())
|
||||
}
|
||||
|
||||
#[derive(SimpleObject)]
|
||||
struct TestObj {
|
||||
#[graphql(derived(owned, name = "value2", into = "Option<ValueDerived2>", with = "option_to_option"))]
|
||||
pub value1: Option<ValueDerived>,
|
||||
}
|
||||
```
|
3
docs/en/src/extensions.md
Normal file
3
docs/en/src/extensions.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Extensions
|
||||
|
||||
`async-graphql` has the capability to be extended with extensions without having to modify the original source code. A lot of features can be added this way, and a lot of extensions already exists.
|
54
docs/en/src/extensions_available.md
Normal file
54
docs/en/src/extensions_available.md
Normal file
|
@ -0,0 +1,54 @@
|
|||
# Extensions available
|
||||
|
||||
There are a lot of available extensions in the `async-graphql` to empower your GraphQL Server, some of these documentations are documented here.
|
||||
|
||||
## Analyzer
|
||||
*Available in the repository*
|
||||
|
||||
The `analyzer` extension will output a field containing `complexity` and `depth` in the response extension field of each query.
|
||||
|
||||
|
||||
## Apollo Persisted Queries
|
||||
*Available in the repository*
|
||||
|
||||
To improve network performance for large queries, you can enable this Persisted Queries extension. With this extension enabled, each unique query is associated to a unique identifier, so clients can send this identifier instead of the corresponding query string to reduce requests sizes.
|
||||
|
||||
This extension doesn't force you to use some cache strategy, you can choose the caching strategy you want, you'll just have to implement the `CacheStorage` trait:
|
||||
```rust
|
||||
#[async_trait::async_trait]
|
||||
pub trait CacheStorage: Send + Sync + Clone + 'static {
|
||||
/// Load the query by `key`.
|
||||
async fn get(&self, key: String) -> Option<String>;
|
||||
/// Save the query by `key`.
|
||||
async fn set(&self, key: String, query: String);
|
||||
}
|
||||
```
|
||||
|
||||
### References
|
||||
|
||||
[Apollo doc - Persisted Queries](https://www.apollographql.com/docs/react/api/link/persisted-queries/)
|
||||
|
||||
## Apollo Tracing
|
||||
*Available in the repository*
|
||||
|
||||
Apollo Tracing is an extension which includes analytics data for your queries. This extension works to follow the old and now deprecated [Apollo Tracing Spec](https://github.com/apollographql/apollo-tracing). If you want to check the newer Apollo Reporting Protocol, it's implemented by [async-graphql Apollo studio extension](https://github.com/async-graphql/async_graphql_apollo_studio_extension) for Apollo Studio.
|
||||
|
||||
## Apollo Studio
|
||||
*Available at [async-graphql/async_graphql_apollo_studio_extension](https://github.com/async-graphql/async_graphql_apollo_studio_extension)*
|
||||
|
||||
Apollo Studio is a cloud platform that helps you build, validate, and secure your organization's graph (description from the official documentation). It's a service allowing you to monitor & work with your team around your GraphQL Schema. `async-graphql` provides an extension implementing the official [Apollo Specification](https://www.apollographql.com/docs/studio/setup-analytics/#third-party-support) available at [async-graphql-extension-apollo-tracing](https://github.com/async-graphql/async_graphql_apollo_studio_extension) and [Crates.io](https://crates.io/crates/async-graphql-extension-apollo-tracing).
|
||||
|
||||
## Logger
|
||||
*Available in the repository*
|
||||
|
||||
Logger is a simple extension allowing you to add some logging feature to `async-graphql`. It's also a good example to learn how to create your own extension.
|
||||
|
||||
## OpenTelemetry
|
||||
*Available in the repository*
|
||||
|
||||
OpenTelemetry is an extension providing an integration with the [opentelemetry crate](https://crates.io/crates/opentelemetry) to allow your application to capture distributed traces and metrics from `async-grraphql`.
|
||||
|
||||
## Tracing
|
||||
*Available in the repository*
|
||||
|
||||
Tracing is a simple extension allowing you to add some tracing feature to `async-graphql`. A little like the `Logger` extension.
|
162
docs/en/src/extensions_inner_working.md
Normal file
162
docs/en/src/extensions_inner_working.md
Normal file
|
@ -0,0 +1,162 @@
|
|||
# How extensions are defined
|
||||
|
||||
An `async-graphql` extension is defined by implementing the trait `Extension` associated. The `Extension` trait allow you to insert custom code to some several steps used to respond to GraphQL's queries through `async-graphql`. With `Extensions`, your application can hook into the GraphQL's requests lifecycle to add behaviors about incoming requests or outgoing response.
|
||||
|
||||
`Extensions` are a lot like middleware from other frameworks, be careful when using those: when you use an extension **it'll be run for every GraphQL requests**.
|
||||
|
||||
Across every step, you'll have the `ExtensionContext` supplied with data about your current request execution. Feel free to check how it's constructed in the code, documentation about it will soon come.
|
||||
|
||||
## A word about middleware
|
||||
|
||||
For those who don't know, let's dig deeper into what is a middleware:
|
||||
|
||||
```rust
|
||||
async fn middleware(&self, ctx: &ExtensionContext<'_>, next: NextMiddleware<'_>) -> MiddlewareResult {
|
||||
// Logic to your middleware.
|
||||
|
||||
/*
|
||||
* Final step to your middleware, we call the next function which will trigger
|
||||
* the execution of the next middleware. It's like a `callback` in JavaScript.
|
||||
*/
|
||||
next.run(ctx).await
|
||||
}
|
||||
```
|
||||
|
||||
As you have seen, a `Middleware` is only a function calling the next function at the end, but we could also do a middleware with the `next` function at the start. This is where it's becoming tricky: depending on where you put your logics and where is the `next` call, your logic won't have the same execution order.
|
||||
|
||||
|
||||
Depending on your logic code, you'll want to process it before or after the `next` call. If you need more information about middlewares, there are a lot of things in the web.
|
||||
|
||||
## Processing of a query
|
||||
|
||||
There are several steps to go to process a query to completion, you'll be able to create extension based on these hooks.
|
||||
|
||||
### request
|
||||
|
||||
First, when we receive a request, if it's not a subscription, the first function to be called will be `request`, it's the first step, it's the function called at the incoming request, and it's also the function which will output the response to the user.
|
||||
|
||||
Default implementation for `request`:
|
||||
|
||||
```rust
|
||||
async fn request(&self, ctx: &ExtensionContext<'_>, next: NextRequest<'_>) -> Response {
|
||||
next.run(ctx).await
|
||||
}
|
||||
```
|
||||
|
||||
Depending on where you put your logic code, it'll be executed at the beginning or at the ending of the query being processed.
|
||||
|
||||
|
||||
```rust
|
||||
async fn request(&self, ctx: &ExtensionContext<'_>, next: NextRequest<'_>) -> Response {
|
||||
// The code here will be run before the prepare_request is executed.
|
||||
let result = next.run(ctx).await;
|
||||
// The code after the completion of this futue will be after the processing, just before sending the result to the user.
|
||||
result
|
||||
}
|
||||
```
|
||||
|
||||
### prepare_request
|
||||
|
||||
Just after the `request`, we will have the `prepare_request` lifecycle, which will be hooked.
|
||||
|
||||
```rust
|
||||
async fn prepare_request(
|
||||
&self,
|
||||
ctx: &ExtensionContext<'_>,
|
||||
request: Request,
|
||||
next: NextPrepareRequest<'_>,
|
||||
) -> ServerResult<Request> {
|
||||
// The code here will be un before the prepare_request is executed, just after the request lifecycle hook.
|
||||
let result = next.run(ctx, request).await;
|
||||
// The code here will be run just after the prepare_request
|
||||
result
|
||||
}
|
||||
```
|
||||
|
||||
### parse_query
|
||||
|
||||
The `parse_query` will create a GraphQL `ExecutableDocument` on your query, it'll check if the query is valid for the GraphQL Spec. Usually the implemented spec in `async-graphql` tends to be the last stable one (October2021).
|
||||
|
||||
```rust
|
||||
/// Called at parse query.
|
||||
async fn parse_query(
|
||||
&self,
|
||||
ctx: &ExtensionContext<'_>,
|
||||
// The raw query
|
||||
query: &str,
|
||||
// The variables
|
||||
variables: &Variables,
|
||||
next: NextParseQuery<'_>,
|
||||
) -> ServerResult<ExecutableDocument> {
|
||||
next.run(ctx, query, variables).await
|
||||
}
|
||||
```
|
||||
|
||||
### validation
|
||||
|
||||
The `validation` step will check (depending on your `validation_mode`) rules the query should abide to and give the client data about why the query is not valid.
|
||||
|
||||
```rust
|
||||
/// Called at validation query.
|
||||
async fn validation(
|
||||
&self,
|
||||
ctx: &ExtensionContext<'_>,
|
||||
next: NextValidation<'_>,
|
||||
) -> Result<ValidationResult, Vec<ServerError>> {
|
||||
next.run(ctx).await
|
||||
}
|
||||
```
|
||||
|
||||
### execute
|
||||
|
||||
The `execution` step is a huge one, it'll start the execution of the query by calling each resolver concurrently for a `Query` and serially for a `Mutation`.
|
||||
|
||||
```rust
|
||||
/// Called at execute query.
|
||||
async fn execute(
|
||||
&self,
|
||||
ctx: &ExtensionContext<'_>,
|
||||
operation_name: Option<&str>,
|
||||
next: NextExecute<'_>,
|
||||
) -> Response {
|
||||
// Befoe starting resolving the whole query
|
||||
let result = next.run(ctx, operation_name).await;
|
||||
// After resolving the whole query
|
||||
result
|
||||
}
|
||||
````
|
||||
|
||||
### resolve
|
||||
|
||||
The `resolve` step is launched for each field.
|
||||
|
||||
```rust
|
||||
/// Called at resolve field.
|
||||
async fn resolve(
|
||||
&self,
|
||||
ctx: &ExtensionContext<'_>,
|
||||
info: ResolveInfo<'_>,
|
||||
next: NextResolve<'_>,
|
||||
) -> ServerResult<Option<Value>> {
|
||||
// Logic before resolving the field
|
||||
let result = next.run(ctx, info).await;
|
||||
// Logic after resolving the field
|
||||
result
|
||||
}
|
||||
```
|
||||
|
||||
### subscribe
|
||||
|
||||
The `subscribe` lifecycle has the same behavior as the `request` but for a `Subscritpion`.
|
||||
|
||||
```rust
|
||||
/// Called at subscribe request.
|
||||
fn subscribe<'s>(
|
||||
&self,
|
||||
ctx: &ExtensionContext<'_>,
|
||||
stream: BoxStream<'s, Response>,
|
||||
next: NextSubscribe<'_>,
|
||||
) -> BoxStream<'s, Response> {
|
||||
next.run(ctx, stream)
|
||||
}
|
||||
```
|
2
examples
2
examples
|
@ -1 +1 @@
|
|||
Subproject commit df8cfdff35d25d1cef3c9fda4d98e639a55fffc2
|
||||
Subproject commit 8641a72c8ccb78e924e2550cd1fbfa2e62319174
|
|
@ -1,8 +1,8 @@
|
|||
[package]
|
||||
name = "async-graphql-actix-web"
|
||||
version = "2.10.4"
|
||||
version = "2.11.0"
|
||||
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
description = "async-graphql for actix-web"
|
||||
license = "MIT/Apache-2.0"
|
||||
documentation = "https://docs.rs/async-graphql-actix-web/"
|
||||
|
@ -12,8 +12,7 @@ keywords = ["futures", "async", "graphql"]
|
|||
categories = ["network-programming", "asynchronous"]
|
||||
|
||||
[dependencies]
|
||||
async-graphql = { path = "../..", version = "=2.10.4" }
|
||||
|
||||
async-graphql = { path = "../..", version = "=2.11.0" }
|
||||
actix = "0.12.0"
|
||||
actix-http = "3.0.0-beta.11"
|
||||
actix-web = { version = "4.0.0-beta.10", default-features = false }
|
||||
|
@ -22,6 +21,7 @@ async-channel = "1.6.1"
|
|||
futures-util = { version = "0.3.17", default-features = false }
|
||||
serde_json = "1.0.64"
|
||||
serde_urlencoded = "0.7.0"
|
||||
futures-channel = "0.3.13"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "2.2.0"
|
||||
|
|
|
@ -174,4 +174,4 @@ impl Responder for Response {
|
|||
}
|
||||
res.body(serde_json::to_string(&self.0).unwrap())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -212,4 +212,4 @@ where
|
|||
.spawn(ctx)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -241,4 +241,4 @@ async fn test_count() {
|
|||
.into()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
[package]
|
||||
name = "async-graphql-axum"
|
||||
version = "2.10.4"
|
||||
version = "2.11.0"
|
||||
authors = ["sunli <scott_s829@163.com>"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
description = "async-graphql for axum"
|
||||
license = "MIT/Apache-2.0"
|
||||
documentation = "https://docs.rs/async-graphql-axum/"
|
||||
|
@ -12,10 +12,10 @@ keywords = ["futures", "async", "graphql", "axum"]
|
|||
categories = ["network-programming", "asynchronous"]
|
||||
|
||||
[dependencies]
|
||||
async-graphql = { path = "../..", version = "=2.10.4" }
|
||||
async-graphql = { path = "../..", version = "=2.11.0" }
|
||||
|
||||
async-trait = "0.1.51"
|
||||
axum = { version = "0.2.5", features = ["ws", "headers"] }
|
||||
axum = { version = "0.3", features = ["ws", "headers"] }
|
||||
bytes = "1.0.1"
|
||||
headers = "0.3.4"
|
||||
http = "0.2.4"
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
use std::convert::TryFrom;
|
||||
|
||||
use axum::body::Body;
|
||||
use axum::response::IntoResponse;
|
||||
use headers::HeaderName;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
[package]
|
||||
name = "async-graphql-poem"
|
||||
version = "2.10.4"
|
||||
version = "2.11.0"
|
||||
authors = ["sunli <scott_s829@163.com>"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
description = "async-graphql for poem"
|
||||
license = "MIT/Apache-2.0"
|
||||
documentation = "https://docs.rs/async-graphql-poem/"
|
||||
|
@ -11,11 +11,10 @@ repository = "https://github.com/async-graphql/async-graphql"
|
|||
keywords = ["futures", "async", "graphql", "poem"]
|
||||
categories = ["network-programming", "asynchronous"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
async-graphql = { path = "../..", version = "=2.10.4" }
|
||||
async-graphql = { path = "../..", version = "=2.11.0" }
|
||||
|
||||
poem = { version = "0.6.6", features = ["websocket"] }
|
||||
poem = { version = "1.0.19", features = ["websocket"] }
|
||||
futures-util = { version = "0.3.13", default-features = false }
|
||||
serde_json = "1.0.66"
|
||||
tokio-util = { version = "0.6.7", features = ["compat"] }
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use async_graphql::http::MultipartOptions;
|
||||
use poem::error::BadRequest;
|
||||
use poem::http::{header, Method};
|
||||
use poem::web::Query;
|
||||
use poem::{async_trait, Error, FromRequest, Request, RequestBody, Result};
|
||||
|
@ -12,7 +13,7 @@ use tokio_util::compat::TokioAsyncReadCompatExt;
|
|||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use poem::{handler, RouteMethod, route, EndpointExt};
|
||||
/// use poem::{handler, Route, post, EndpointExt};
|
||||
/// use poem::web::{Json, Data};
|
||||
/// use poem::middleware::AddData;
|
||||
/// use async_graphql_poem::GraphQLRequest;
|
||||
|
@ -35,7 +36,7 @@ use tokio_util::compat::TokioAsyncReadCompatExt;
|
|||
/// }
|
||||
///
|
||||
/// let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
|
||||
/// let app = route().at("/", RouteMethod::new().post(index.with(AddData::new(schema))));
|
||||
/// let app = Route::new().at("/", post(index.with(AddData::new(schema))));
|
||||
/// ```
|
||||
pub struct GraphQLRequest(pub async_graphql::Request);
|
||||
|
||||
|
@ -49,7 +50,7 @@ impl<'a> FromRequest<'a> for GraphQLRequest {
|
|||
.await?
|
||||
.0
|
||||
.into_single()
|
||||
.map_err(Error::bad_request)?,
|
||||
.map_err(BadRequest)?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
@ -63,10 +64,7 @@ impl<'a> FromRequest<'a> for GraphQLBatchRequest {
|
|||
|
||||
async fn from_request(req: &'a Request, body: &mut RequestBody) -> Result<Self> {
|
||||
if req.method() == Method::GET {
|
||||
let req = Query::from_request(req, body)
|
||||
.await
|
||||
.map_err(Error::bad_request)?
|
||||
.0;
|
||||
let req = Query::from_request(req, body).await?.0;
|
||||
Ok(Self(async_graphql::BatchRequest::Single(req)))
|
||||
} else {
|
||||
let content_type = req
|
||||
|
@ -80,8 +78,7 @@ impl<'a> FromRequest<'a> for GraphQLBatchRequest {
|
|||
body.take()?.into_async_read().compat(),
|
||||
MultipartOptions::default(),
|
||||
)
|
||||
.await
|
||||
.map_err(Error::bad_request)?,
|
||||
.await?,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,8 +4,10 @@
|
|||
|
||||
mod extractor;
|
||||
mod query;
|
||||
mod response;
|
||||
mod subscription;
|
||||
|
||||
pub use extractor::{GraphQLBatchRequest, GraphQLRequest};
|
||||
pub use query::GraphQL;
|
||||
pub use response::{GraphQLBatchResponse, GraphQLResponse};
|
||||
pub use subscription::GraphQLSubscription;
|
||||
|
|
|
@ -1,15 +1,14 @@
|
|||
use async_graphql::{BatchResponse as GraphQLBatchResponse, ObjectType, Schema, SubscriptionType};
|
||||
use poem::web::Json;
|
||||
use async_graphql::{ObjectType, Schema, SubscriptionType};
|
||||
use poem::{async_trait, Endpoint, FromRequest, Request, Result};
|
||||
|
||||
use crate::GraphQLBatchRequest;
|
||||
use crate::{GraphQLBatchRequest, GraphQLBatchResponse};
|
||||
|
||||
/// A GraphQL query endpoint.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use poem::{route, RouteMethod};
|
||||
/// use poem::{Route, post};
|
||||
/// use async_graphql_poem::GraphQL;
|
||||
/// use async_graphql::{EmptyMutation, EmptySubscription, Object, Schema};
|
||||
///
|
||||
|
@ -25,7 +24,7 @@ use crate::GraphQLBatchRequest;
|
|||
/// type MySchema = Schema<Query, EmptyMutation, EmptySubscription>;
|
||||
///
|
||||
/// let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
|
||||
/// let app = route().at("/", RouteMethod::new().post(GraphQL::new(schema)));
|
||||
/// let app = Route::new().at("/", post(GraphQL::new(schema)));
|
||||
/// ```
|
||||
pub struct GraphQL<Query, Mutation, Subscription> {
|
||||
schema: Schema<Query, Mutation, Subscription>,
|
||||
|
@ -45,11 +44,11 @@ where
|
|||
Mutation: ObjectType + 'static,
|
||||
Subscription: SubscriptionType + 'static,
|
||||
{
|
||||
type Output = Result<Json<GraphQLBatchResponse>>;
|
||||
type Output = Result<GraphQLBatchResponse>;
|
||||
|
||||
async fn call(&self, req: Request) -> Self::Output {
|
||||
let (req, mut body) = req.split();
|
||||
let req = GraphQLBatchRequest::from_request(&req, &mut body).await?;
|
||||
Ok(Json(self.schema.execute_batch(req.0).await))
|
||||
Ok(GraphQLBatchResponse(self.schema.execute_batch(req.0).await))
|
||||
}
|
||||
}
|
||||
|
|
50
integrations/poem/src/response.rs
Normal file
50
integrations/poem/src/response.rs
Normal file
|
@ -0,0 +1,50 @@
|
|||
use poem::http::header::HeaderName;
|
||||
use poem::web::Json;
|
||||
use poem::{IntoResponse, Response};
|
||||
|
||||
/// Response for `async_graphql::Request`.
|
||||
pub struct GraphQLResponse(pub async_graphql::Response);
|
||||
|
||||
impl From<async_graphql::Response> for GraphQLResponse {
|
||||
fn from(resp: async_graphql::Response) -> Self {
|
||||
Self(resp)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for GraphQLResponse {
|
||||
fn into_response(self) -> Response {
|
||||
GraphQLBatchResponse(self.0.into()).into_response()
|
||||
}
|
||||
}
|
||||
|
||||
/// Response for `async_graphql::BatchRequest`.
|
||||
pub struct GraphQLBatchResponse(pub async_graphql::BatchResponse);
|
||||
|
||||
impl From<async_graphql::BatchResponse> for GraphQLBatchResponse {
|
||||
fn from(resp: async_graphql::BatchResponse) -> Self {
|
||||
Self(resp)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for GraphQLBatchResponse {
|
||||
fn into_response(self) -> Response {
|
||||
let mut resp = Json(&self.0).into_response();
|
||||
|
||||
if self.0.is_ok() {
|
||||
if let Some(cache_control) = self.0.cache_control().value() {
|
||||
if let Ok(value) = cache_control.try_into() {
|
||||
resp.headers_mut().insert("cache-control", value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (name, value) in self.0.http_headers() {
|
||||
if let (Ok(name), Ok(value)) = (TryInto::<HeaderName>::try_into(name), value.try_into())
|
||||
{
|
||||
resp.headers_mut().append(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
resp
|
||||
}
|
||||
}
|
|
@ -12,7 +12,7 @@ use poem::{http, Endpoint, FromRequest, IntoResponse, Request, Response, Result}
|
|||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use poem::{route, RouteMethod};
|
||||
/// use poem::{Route, get};
|
||||
/// use async_graphql_poem::GraphQLSubscription;
|
||||
/// use async_graphql::{EmptyMutation, Object, Schema, Subscription};
|
||||
/// use futures_util::{Stream, stream};
|
||||
|
@ -38,7 +38,7 @@ use poem::{http, Endpoint, FromRequest, IntoResponse, Request, Response, Result}
|
|||
/// type MySchema = Schema<Query, EmptyMutation, Subscription>;
|
||||
///
|
||||
/// let schema = Schema::new(Query, EmptyMutation, Subscription);
|
||||
/// let app = route().at("/ws", RouteMethod::new().get(GraphQLSubscription::new(schema)));
|
||||
/// let app = Route::new().at("/ws", get(GraphQLSubscription::new(schema)));
|
||||
/// ```
|
||||
pub struct GraphQLSubscription<Query, Mutation, Subscription, F> {
|
||||
schema: Schema<Query, Mutation, Subscription>,
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
[package]
|
||||
name = "async-graphql-rocket"
|
||||
version = "2.10.4"
|
||||
version = "2.11.0"
|
||||
authors = ["Daniel Wiesenberg <daniel@simplificAR.io>"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
description = "async-graphql for Rocket.rs"
|
||||
license = "MIT/Apache-2.0"
|
||||
documentation = "https://docs.rs/async-graphql/"
|
||||
|
@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql", "rocket"]
|
|||
categories = ["network-programming", "asynchronous"]
|
||||
|
||||
[dependencies]
|
||||
async-graphql = { path = "../..", version = "=2.10.4" }
|
||||
async-graphql = { path = "../..", version = "=2.11.0" }
|
||||
|
||||
rocket = { version = "0.5.0-rc.1", default-features = false }
|
||||
serde = "1.0.126"
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
[package]
|
||||
name = "async-graphql-tide"
|
||||
version = "2.10.4"
|
||||
version = "2.11.0"
|
||||
authors = ["vkill <vkill.net@gmail.com>"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
description = "async-graphql for tide"
|
||||
license = "MIT/Apache-2.0"
|
||||
documentation = "https://docs.rs/async-graphql-tide/"
|
||||
|
@ -16,7 +16,7 @@ default = ["websocket"]
|
|||
websocket = ["tide-websockets"]
|
||||
|
||||
[dependencies]
|
||||
async-graphql = { path = "../..", version = "=2.10.4" }
|
||||
async-graphql = { path = "../..", version = "=2.11.0" }
|
||||
async-trait = "0.1.48"
|
||||
futures-util = "0.3.13"
|
||||
serde_json = "1.0.64"
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
[package]
|
||||
name = "async-graphql-warp"
|
||||
version = "2.10.4"
|
||||
version = "2.11.0"
|
||||
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
description = "async-graphql for warp"
|
||||
license = "MIT/Apache-2.0"
|
||||
documentation = "https://docs.rs/async-graphql-warp/"
|
||||
|
@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql"]
|
|||
categories = ["network-programming", "asynchronous"]
|
||||
|
||||
[dependencies]
|
||||
async-graphql = { path = "../..", version = "=2.10.4" }
|
||||
async-graphql = { path = "../..", version = "=2.11.0" }
|
||||
|
||||
warp = { version = "0.3.0", default-features = false, features = ["websocket"] }
|
||||
futures-util = { version = "0.3.13", default-features = false, features = ["sink"] }
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
use std::convert::TryInto;
|
||||
use std::io;
|
||||
use std::io::ErrorKind;
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
[package]
|
||||
name = "async-graphql-parser"
|
||||
version = "2.10.4"
|
||||
version = "2.11.0"
|
||||
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
description = "GraphQL query parser for async-graphql"
|
||||
license = "MIT/Apache-2.0"
|
||||
documentation = "https://docs.rs/async-graphql/"
|
||||
|
@ -12,7 +12,7 @@ keywords = ["futures", "async", "graphql"]
|
|||
categories = ["network-programming", "asynchronous"]
|
||||
|
||||
[dependencies]
|
||||
async-graphql-value = { path = "../value", version = "=2.10.4" }
|
||||
async-graphql-value = { path = "../value", version = "=2.11.0" }
|
||||
pest = "2.1.3"
|
||||
pest_derive = "2.1.0"
|
||||
serde_json = "1.0.64"
|
||||
|
|
|
@ -10,7 +10,7 @@ executable_definition = { operation_definition | fragment_definition }
|
|||
operation_definition = { named_operation_definition | selection_set }
|
||||
named_operation_definition = { operation_type ~ name? ~ variable_definitions? ~ directives? ~ selection_set }
|
||||
variable_definitions = { "(" ~ variable_definition* ~ ")" }
|
||||
variable_definition = { variable ~ ":" ~ type_ ~ default_value? }
|
||||
variable_definition = { variable ~ ":" ~ type_ ~ directives? ~ default_value? }
|
||||
|
||||
selection_set = { "{" ~ selection+ ~ "}" }
|
||||
selection = { field | inline_fragment | fragment_spread }
|
||||
|
@ -86,6 +86,7 @@ directive_location = {
|
|||
| "FRAGMENT_DEFINITION"
|
||||
| "FRAGMENT_SPREAD"
|
||||
| "INLINE_FRAGMENT"
|
||||
| "VARIABLE_DEFINITION"
|
||||
| "SCHEMA"
|
||||
| "SCALAR"
|
||||
| "OBJECT"
|
||||
|
@ -115,7 +116,7 @@ value = { variable | number | string | boolean | null | enum_value |
|
|||
|
||||
variable = { "$" ~ name }
|
||||
|
||||
number = @{ float | int }
|
||||
number = @{ (float | int) ~ !name_start }
|
||||
float = { int ~ ((fractional ~ exponent) | fractional | exponent) }
|
||||
fractional = { "." ~ ASCII_DIGIT+ }
|
||||
exponent = { ("E" | "e") ~ ("+" | "-")? ~ ASCII_DIGIT+ }
|
||||
|
@ -164,4 +165,5 @@ arguments = { "(" ~ argument+ ~ ")" }
|
|||
const_argument = { name ~ ":" ~ const_value }
|
||||
argument = { name ~ ":" ~ value }
|
||||
|
||||
name = @{ (ASCII_ALPHA | "_") ~ (ASCII_ALPHA | ASCII_DIGIT | "_")* }
|
||||
name_start = @{ (ASCII_ALPHA | "_") }
|
||||
name = @{ name_start ~ (ASCII_ALPHA | ASCII_DIGIT | "_")* }
|
||||
|
|
|
@ -208,6 +208,8 @@ fn parse_variable_definition(
|
|||
|
||||
let variable = parse_variable(pairs.next().unwrap(), pc)?;
|
||||
let var_type = parse_type(pairs.next().unwrap(), pc)?;
|
||||
|
||||
let directives = parse_opt_directives(&mut pairs, pc)?;
|
||||
let default_value = parse_if_rule(&mut pairs, Rule::default_value, |pair| {
|
||||
parse_default_value(pair, pc)
|
||||
})?;
|
||||
|
@ -218,6 +220,7 @@ fn parse_variable_definition(
|
|||
VariableDefinition {
|
||||
name: variable,
|
||||
var_type,
|
||||
directives,
|
||||
default_value,
|
||||
},
|
||||
pos,
|
||||
|
|
|
@ -312,3 +312,21 @@ fn parse_name(pair: Pair<Rule>, pc: &mut PositionCalculator) -> Result<Positione
|
|||
debug_assert_eq!(pair.as_rule(), Rule::name);
|
||||
Ok(Positioned::new(Name::new(pair.as_str()), pc.step(&pair)))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_number_lookahead_restrictions() {
|
||||
GraphQLParser::parse(Rule::const_list, "[123 abc]").unwrap();
|
||||
GraphQLParser::parse(Rule::const_list, "[123.0123 abc]").unwrap();
|
||||
GraphQLParser::parse(Rule::const_list, "[123.0123e7 abc]").unwrap();
|
||||
GraphQLParser::parse(Rule::const_list, "[123.0123e77 abc]").unwrap();
|
||||
|
||||
assert!(GraphQLParser::parse(Rule::const_list, "[123abc]").is_err());
|
||||
assert!(GraphQLParser::parse(Rule::const_list, "[123.0123abc]").is_err());
|
||||
assert!(GraphQLParser::parse(Rule::const_list, "[123.0123e7abc]").is_err());
|
||||
assert!(GraphQLParser::parse(Rule::const_list, "[123.0123e77abc]").is_err());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -323,6 +323,7 @@ fn parse_directive_definition(
|
|||
"FRAGMENT_DEFINITION" => DirectiveLocation::FragmentDefinition,
|
||||
"FRAGMENT_SPREAD" => DirectiveLocation::FragmentSpread,
|
||||
"INLINE_FRAGMENT" => DirectiveLocation::InlineFragment,
|
||||
"VARIABLE_DEFINITION" => DirectiveLocation::VariableDefinition,
|
||||
"SCHEMA" => DirectiveLocation::Schema,
|
||||
"SCALAR" => DirectiveLocation::Scalar,
|
||||
"OBJECT" => DirectiveLocation::Object,
|
||||
|
|
|
@ -10,7 +10,7 @@ use std::str::Chars;
|
|||
/// Original position of an element in source code.
|
||||
///
|
||||
/// You can serialize and deserialize it to the GraphQL `locations` format
|
||||
/// ([reference](https://spec.graphql.org/June2018/#sec-Errors)).
|
||||
/// ([reference](https://spec.graphql.org/October2021/#sec-Errors)).
|
||||
#[derive(PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Default, Hash, Serialize, Deserialize)]
|
||||
pub struct Pos {
|
||||
/// One-based line number.
|
||||
|
|
|
@ -5,7 +5,7 @@ use async_graphql_value::{ConstValue, Name, Value};
|
|||
|
||||
/// An executable GraphQL file or request string.
|
||||
///
|
||||
/// [Reference](https://spec.graphql.org/June2018/#ExecutableDocument).
|
||||
/// [Reference](https://spec.graphql.org/October2021/#ExecutableDocument).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExecutableDocument {
|
||||
/// The operations of the document.
|
||||
|
@ -93,7 +93,7 @@ enum OperationsIterInner<'a> {
|
|||
|
||||
/// A GraphQL operation, such as `mutation($content:String!) { makePost(content: $content) { id } }`.
|
||||
///
|
||||
/// [Reference](https://spec.graphql.org/June2018/#OperationDefinition).
|
||||
/// [Reference](https://spec.graphql.org/October2021/#OperationDefinition).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct OperationDefinition {
|
||||
/// The type of operation.
|
||||
|
@ -108,13 +108,15 @@ pub struct OperationDefinition {
|
|||
|
||||
/// A variable definition inside a list of variable definitions, for example `$name:String!`.
|
||||
///
|
||||
/// [Reference](https://spec.graphql.org/June2018/#VariableDefinition).
|
||||
/// [Reference](https://spec.graphql.org/October2021/#VariableDefinition).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VariableDefinition {
|
||||
/// The name of the variable, without the preceding `$`.
|
||||
pub name: Positioned<Name>,
|
||||
/// The type of the variable.
|
||||
pub var_type: Positioned<Type>,
|
||||
/// The variable's directives.
|
||||
pub directives: Vec<Positioned<Directive>>,
|
||||
/// The optional default value of the variable.
|
||||
pub default_value: Option<Positioned<ConstValue>>,
|
||||
}
|
||||
|
@ -139,7 +141,7 @@ impl VariableDefinition {
|
|||
|
||||
/// A set of fields to be selected, for example `{ name age }`.
|
||||
///
|
||||
/// [Reference](https://spec.graphql.org/June2018/#SelectionSet).
|
||||
/// [Reference](https://spec.graphql.org/October2021/#SelectionSet).
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct SelectionSet {
|
||||
/// The fields to be selected.
|
||||
|
@ -148,7 +150,7 @@ pub struct SelectionSet {
|
|||
|
||||
/// A part of an object to be selected; a single field, a fragment spread or an inline fragment.
|
||||
///
|
||||
/// [Reference](https://spec.graphql.org/June2018/#Selection).
|
||||
/// [Reference](https://spec.graphql.org/October2021/#Selection).
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Selection {
|
||||
/// Select a single field, such as `name` or `weightKilos: weight(unit: KILOGRAMS)`.
|
||||
|
@ -182,7 +184,7 @@ impl Selection {
|
|||
|
||||
/// A field being selected on an object, such as `name` or `weightKilos: weight(unit: KILOGRAMS)`.
|
||||
///
|
||||
/// [Reference](https://spec.graphql.org/June2018/#Field).
|
||||
/// [Reference](https://spec.graphql.org/October2021/#Field).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Field {
|
||||
/// The optional field alias.
|
||||
|
@ -217,7 +219,7 @@ impl Field {
|
|||
|
||||
/// A fragment selector, such as `... userFields`.
|
||||
///
|
||||
/// [Reference](https://spec.graphql.org/June2018/#FragmentSpread).
|
||||
/// [Reference](https://spec.graphql.org/October2021/#FragmentSpread).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FragmentSpread {
|
||||
/// The name of the fragment being selected.
|
||||
|
@ -228,7 +230,7 @@ pub struct FragmentSpread {
|
|||
|
||||
/// An inline fragment selector, such as `... on User { name }`.
|
||||
///
|
||||
/// [Reference](https://spec.graphql.org/June2018/#InlineFragment).
|
||||
/// [Reference](https://spec.graphql.org/October2021/#InlineFragment).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InlineFragment {
|
||||
/// The type condition.
|
||||
|
@ -241,7 +243,7 @@ pub struct InlineFragment {
|
|||
|
||||
/// The definition of a fragment, such as `fragment userFields on User { name age }`.
|
||||
///
|
||||
/// [Reference](https://spec.graphql.org/June2018/#FragmentDefinition).
|
||||
/// [Reference](https://spec.graphql.org/October2021/#FragmentDefinition).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FragmentDefinition {
|
||||
/// The type this fragment operates on.
|
||||
|
@ -254,7 +256,7 @@ pub struct FragmentDefinition {
|
|||
|
||||
/// A type a fragment can apply to (`on` followed by the type).
|
||||
///
|
||||
/// [Reference](https://spec.graphql.org/June2018/#TypeCondition).
|
||||
/// [Reference](https://spec.graphql.org/October2021/#TypeCondition).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TypeCondition {
|
||||
/// The type this fragment applies to.
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
//! [`ServiceDocument`](struct.ServiceDocument.html), representing an executable GraphQL query and a
|
||||
//! GraphQL service respectively.
|
||||
//!
|
||||
//! This follows the [June 2018 edition of the GraphQL spec](https://spec.graphql.org/June2018/).
|
||||
//! This follows the [June 2018 edition of the GraphQL spec](https://spec.graphql.org/October2021/).
|
||||
|
||||
mod executable;
|
||||
mod service;
|
||||
|
@ -19,7 +19,7 @@ pub use service::*;
|
|||
|
||||
/// The type of an operation; `query`, `mutation` or `subscription`.
|
||||
///
|
||||
/// [Reference](https://spec.graphql.org/June2018/#OperationType).
|
||||
/// [Reference](https://spec.graphql.org/October2021/#OperationType).
|
||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||
pub enum OperationType {
|
||||
/// A query.
|
||||
|
@ -42,7 +42,7 @@ impl Display for OperationType {
|
|||
|
||||
/// A GraphQL type, for example `String` or `[String!]!`.
|
||||
///
|
||||
/// [Reference](https://spec.graphql.org/June2018/#Type).
|
||||
/// [Reference](https://spec.graphql.org/October2021/#Type).
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct Type {
|
||||
/// The base type.
|
||||
|
@ -105,7 +105,7 @@ impl Display for BaseType {
|
|||
/// from [`Directive`](struct.Directive.html) in that it uses [`ConstValue`](enum.ConstValue.html)
|
||||
/// instead of [`Value`](enum.Value.html).
|
||||
///
|
||||
/// [Reference](https://spec.graphql.org/June2018/#Directive).
|
||||
/// [Reference](https://spec.graphql.org/October2021/#Directive).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ConstDirective {
|
||||
/// The name of the directive.
|
||||
|
@ -140,7 +140,7 @@ impl ConstDirective {
|
|||
|
||||
/// A GraphQL directive, such as `@deprecated(reason: "Use the other field")`.
|
||||
///
|
||||
/// [Reference](https://spec.graphql.org/June2018/#Directive).
|
||||
/// [Reference](https://spec.graphql.org/October2021/#Directive).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Directive {
|
||||
/// The name of the directive.
|
||||
|
|
|
@ -5,7 +5,7 @@ use async_graphql_value::Name;
|
|||
|
||||
/// A GraphQL file or request string defining a GraphQL service.
|
||||
///
|
||||
/// [Reference](https://spec.graphql.org/June2018/#Document).
|
||||
/// [Reference](https://spec.graphql.org/October2021/#Document).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ServiceDocument {
|
||||
/// The definitions of this document.
|
||||
|
@ -14,8 +14,8 @@ pub struct ServiceDocument {
|
|||
|
||||
/// A definition concerning the type system of a GraphQL service.
|
||||
///
|
||||
/// [Reference](https://spec.graphql.org/June2018/#TypeSystemDefinition). This enum also covers
|
||||
/// [extensions](https://spec.graphql.org/June2018/#TypeSystemExtension).
|
||||
/// [Reference](https://spec.graphql.org/October2021/#TypeSystemDefinition). This enum also covers
|
||||
/// [extensions](https://spec.graphql.org/October2021/#TypeSystemExtension).
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum TypeSystemDefinition {
|
||||
/// The definition of the schema of the service.
|
||||
|
@ -28,8 +28,8 @@ pub enum TypeSystemDefinition {
|
|||
|
||||
/// The definition of the schema in a GraphQL service.
|
||||
///
|
||||
/// [Reference](https://spec.graphql.org/June2018/#SchemaDefinition). This also covers
|
||||
/// [extensions](https://spec.graphql.org/June2018/#SchemaExtension).
|
||||
/// [Reference](https://spec.graphql.org/October2021/#SchemaDefinition). This also covers
|
||||
/// [extensions](https://spec.graphql.org/October2021/#SchemaExtension).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SchemaDefinition {
|
||||
/// Whether the schema is an extension of another schema.
|
||||
|
@ -46,8 +46,8 @@ pub struct SchemaDefinition {
|
|||
|
||||
/// The definition of a type in a GraphQL service.
|
||||
///
|
||||
/// [Reference](https://spec.graphql.org/June2018/#TypeDefinition). This also covers
|
||||
/// [extensions](https://spec.graphql.org/June2018/#TypeExtension).
|
||||
/// [Reference](https://spec.graphql.org/October2021/#TypeDefinition). This also covers
|
||||
/// [extensions](https://spec.graphql.org/October2021/#TypeExtension).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TypeDefinition {
|
||||
/// Whether the type is an extension of another type.
|
||||
|
@ -81,7 +81,7 @@ pub enum TypeKind {
|
|||
|
||||
/// The definition of an object type.
|
||||
///
|
||||
/// [Reference](https://spec.graphql.org/June2018/#ObjectType).
|
||||
/// [Reference](https://spec.graphql.org/October2021/#ObjectType).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ObjectType {
|
||||
/// The interfaces implemented by the object.
|
||||
|
@ -92,7 +92,7 @@ pub struct ObjectType {
|
|||
|
||||
/// The definition of a field inside an object or interface.
|
||||
///
|
||||
/// [Reference](https://spec.graphql.org/June2018/#FieldDefinition).
|
||||
/// [Reference](https://spec.graphql.org/October2021/#FieldDefinition).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FieldDefinition {
|
||||
/// The description of the field.
|
||||
|
@ -109,7 +109,7 @@ pub struct FieldDefinition {
|
|||
|
||||
/// The definition of an interface type.
|
||||
///
|
||||
/// [Reference](https://spec.graphql.org/June2018/#InterfaceType).
|
||||
/// [Reference](https://spec.graphql.org/October2021/#InterfaceType).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InterfaceType {
|
||||
/// The interfaces implemented by the interface.
|
||||
|
@ -120,7 +120,7 @@ pub struct InterfaceType {
|
|||
|
||||
/// The definition of a union type.
|
||||
///
|
||||
/// [Reference](https://spec.graphql.org/June2018/#UnionType).
|
||||
/// [Reference](https://spec.graphql.org/October2021/#UnionType).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct UnionType {
|
||||
/// The member types of the union.
|
||||
|
@ -129,7 +129,7 @@ pub struct UnionType {
|
|||
|
||||
/// The definition of an enum.
|
||||
///
|
||||
/// [Reference](https://spec.graphql.org/June2018/#EnumType).
|
||||
/// [Reference](https://spec.graphql.org/October2021/#EnumType).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EnumType {
|
||||
/// The possible values of the enum.
|
||||
|
@ -138,7 +138,7 @@ pub struct EnumType {
|
|||
|
||||
/// The definition of a value inside an enum.
|
||||
///
|
||||
/// [Reference](https://spec.graphql.org/June2018/#EnumValueDefinition).
|
||||
/// [Reference](https://spec.graphql.org/October2021/#EnumValueDefinition).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EnumValueDefinition {
|
||||
/// The description of the argument.
|
||||
|
@ -151,7 +151,7 @@ pub struct EnumValueDefinition {
|
|||
|
||||
/// The definition of an input object.
|
||||
///
|
||||
/// [Reference](https://spec.graphql.org/June2018/#InputObjectType).
|
||||
/// [Reference](https://spec.graphql.org/October2021/#InputObjectType).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InputObjectType {
|
||||
/// The fields of the input object.
|
||||
|
@ -160,7 +160,7 @@ pub struct InputObjectType {
|
|||
|
||||
/// The definition of an input value inside the arguments of a field.
|
||||
///
|
||||
/// [Reference](https://spec.graphql.org/June2018/#InputValueDefinition).
|
||||
/// [Reference](https://spec.graphql.org/October2021/#InputValueDefinition).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct InputValueDefinition {
|
||||
/// The description of the argument.
|
||||
|
@ -177,7 +177,7 @@ pub struct InputValueDefinition {
|
|||
|
||||
/// The definition of a directive in a service.
|
||||
///
|
||||
/// [Reference](https://spec.graphql.org/June2018/#DirectiveDefinition).
|
||||
/// [Reference](https://spec.graphql.org/October2021/#DirectiveDefinition).
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DirectiveDefinition {
|
||||
/// The description of the directive.
|
||||
|
@ -192,7 +192,7 @@ pub struct DirectiveDefinition {
|
|||
|
||||
/// Where a directive can apply to.
|
||||
///
|
||||
/// [Reference](https://spec.graphql.org/June2018/#DirectiveLocation).
|
||||
/// [Reference](https://spec.graphql.org/October2021/#DirectiveLocation).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DirectiveLocation {
|
||||
/// A [query](enum.OperationType.html#variant.Query) [operation](struct.OperationDefinition.html).
|
||||
|
@ -233,4 +233,6 @@ pub enum DirectiveLocation {
|
|||
/// An [input value definition](struct.InputValueDefinition.html) on an input object but not a
|
||||
/// field.
|
||||
InputFieldDefinition,
|
||||
/// An [variable definition](struct.VariableDefinition.html).
|
||||
VariableDefinition,
|
||||
}
|
||||
|
|
3
parser/tests/executables/variable_directive.graphql
Normal file
3
parser/tests/executables/variable_directive.graphql
Normal file
|
@ -0,0 +1,3 @@
|
|||
query Foo($a: Int @directive = 10, $b: Int @directive) {
|
||||
value
|
||||
}
|
|
@ -4,3 +4,5 @@ directive @test1(service: String!) on FIELD_DEFINITION
|
|||
directive @test2(service: String!) on FIELD
|
||||
directive @test3(service: String!) on ENUM_VALUE
|
||||
directive @test4(service: String!) on ENUM
|
||||
|
||||
directive @test5(service: String!) on VARIABLE_DEFINITION
|
||||
|
|
140
src/error.rs
140
src/error.rs
|
@ -1,6 +1,9 @@
|
|||
use std::any::Any;
|
||||
use std::collections::BTreeMap;
|
||||
use std::error::Error as StdError;
|
||||
use std::fmt::{self, Debug, Display, Formatter};
|
||||
use std::marker::PhantomData;
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
|
@ -20,10 +23,13 @@ impl ErrorExtensionValues {
|
|||
}
|
||||
|
||||
/// An error in a GraphQL server.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
pub struct ServerError {
|
||||
/// An explanatory message of the error.
|
||||
pub message: String,
|
||||
/// The source of the error, comes from [`ResolverError<T>`].
|
||||
#[serde(skip)]
|
||||
pub source: Option<Arc<dyn Any + Send + Sync>>,
|
||||
/// Where the error occurred.
|
||||
#[serde(skip_serializing_if = "Vec::is_empty", default)]
|
||||
pub locations: Vec<Pos>,
|
||||
|
@ -39,17 +45,55 @@ fn error_extensions_is_empty(values: &Option<ErrorExtensionValues>) -> bool {
|
|||
values.as_ref().map_or(true, |values| values.0.is_empty())
|
||||
}
|
||||
|
||||
impl Debug for ServerError {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("ServerError")
|
||||
.field("message", &self.message)
|
||||
.field("locations", &self.locations)
|
||||
.field("path", &self.path)
|
||||
.field("extensions", &self.extensions)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for ServerError {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.message.eq(&other.message)
|
||||
&& self.locations.eq(&other.locations)
|
||||
&& self.path.eq(&other.path)
|
||||
&& self.extensions.eq(&other.extensions)
|
||||
}
|
||||
}
|
||||
|
||||
impl ServerError {
|
||||
/// Create a new server error with the message.
|
||||
pub fn new(message: impl Into<String>, pos: Option<Pos>) -> Self {
|
||||
Self {
|
||||
message: message.into(),
|
||||
source: None,
|
||||
locations: pos.map(|pos| vec![pos]).unwrap_or_default(),
|
||||
path: Vec::new(),
|
||||
extensions: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the source of the error.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use std::string::FromUtf8Error;
|
||||
/// use async_graphql::{ResolverError, Error, ServerError, Pos};
|
||||
///
|
||||
/// let bytes = vec![0, 159];
|
||||
/// let err: Error = String::from_utf8(bytes).map_err(ResolverError::new).unwrap_err().into();
|
||||
/// let server_err: ServerError = err.into_server_error(Pos { line: 1, column: 1 });
|
||||
/// assert!(server_err.source::<FromUtf8Error>().is_some());
|
||||
/// ```
|
||||
pub fn source<T: Any + Send + Sync>(&self) -> Option<&T> {
|
||||
self.source.as_ref().map(|err| err.downcast_ref()).flatten()
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn with_path(self, path: Vec<PathSegment>) -> Self {
|
||||
Self { path, ..self }
|
||||
|
@ -72,6 +116,7 @@ impl From<parser::Error> for ServerError {
|
|||
fn from(e: parser::Error) -> Self {
|
||||
Self {
|
||||
message: e.to_string(),
|
||||
source: None,
|
||||
locations: e.positions().collect(),
|
||||
path: Vec::new(),
|
||||
extensions: None,
|
||||
|
@ -160,20 +205,39 @@ impl<T: InputType, E: Display> From<E> for InputValueError<T> {
|
|||
pub type InputValueResult<T> = Result<T, InputValueError<T>>;
|
||||
|
||||
/// An error with a message and optional extensions.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
|
||||
#[derive(Clone, Serialize)]
|
||||
pub struct Error {
|
||||
/// The error message.
|
||||
pub message: String,
|
||||
/// The source of the error, comes from [`ResolverError<T>`].
|
||||
#[serde(skip)]
|
||||
pub source: Option<Arc<dyn Any + Send + Sync>>,
|
||||
/// Extensions to the error.
|
||||
#[serde(skip_serializing_if = "error_extensions_is_empty")]
|
||||
pub extensions: Option<ErrorExtensionValues>,
|
||||
}
|
||||
|
||||
impl Debug for Error {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Error")
|
||||
.field("message", &self.message)
|
||||
.field("extensions", &self.extensions)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Error {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.message.eq(&other.message) && self.extensions.eq(&other.extensions)
|
||||
}
|
||||
}
|
||||
|
||||
impl Error {
|
||||
/// Create an error from the given error message.
|
||||
pub fn new(message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
message: message.into(),
|
||||
source: None,
|
||||
extensions: None,
|
||||
}
|
||||
}
|
||||
|
@ -183,6 +247,7 @@ impl Error {
|
|||
pub fn into_server_error(self, pos: Pos) -> ServerError {
|
||||
ServerError {
|
||||
message: self.message,
|
||||
source: self.source,
|
||||
locations: vec![pos],
|
||||
path: Vec::new(),
|
||||
extensions: self.extensions,
|
||||
|
@ -190,10 +255,21 @@ impl Error {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T: Display> From<T> for Error {
|
||||
impl<T: Display + Send + Sync + 'static> From<T> for Error {
|
||||
fn from(e: T) -> Self {
|
||||
Self {
|
||||
message: e.to_string(),
|
||||
source: None,
|
||||
extensions: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ResolverError> for Error {
|
||||
fn from(e: ResolverError) -> Self {
|
||||
Self {
|
||||
message: e.message,
|
||||
source: Some(e.error),
|
||||
extensions: None,
|
||||
}
|
||||
}
|
||||
|
@ -267,40 +343,58 @@ impl From<mime::FromStrError> for ParseRequestError {
|
|||
/// An error which can be extended into a `Error`.
|
||||
pub trait ErrorExtensions: Sized {
|
||||
/// Convert the error to a `Error`.
|
||||
fn extend(&self) -> Error;
|
||||
fn extend(self) -> Error;
|
||||
|
||||
/// Add extensions to the error, using a callback to make the extensions.
|
||||
fn extend_with<C>(self, cb: C) -> Error
|
||||
where
|
||||
C: FnOnce(&Self, &mut ErrorExtensionValues),
|
||||
{
|
||||
let message = self.extend().message;
|
||||
let mut extensions = self.extend().extensions.unwrap_or_default();
|
||||
cb(&self, &mut extensions);
|
||||
let mut new_extensions = Default::default();
|
||||
cb(&self, &mut new_extensions);
|
||||
|
||||
let Error {
|
||||
message,
|
||||
source,
|
||||
extensions,
|
||||
} = self.extend();
|
||||
|
||||
let mut extensions = extensions.unwrap_or_default();
|
||||
extensions.0.extend(new_extensions.0);
|
||||
|
||||
Error {
|
||||
message,
|
||||
source,
|
||||
extensions: Some(extensions),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorExtensions for Error {
|
||||
fn extend(&self) -> Error {
|
||||
self.clone()
|
||||
fn extend(self) -> Error {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// implementing for &E instead of E gives the user the possibility to implement for E which does
|
||||
// not conflict with this implementation acting as a fallback.
|
||||
impl<E: std::fmt::Display> ErrorExtensions for &E {
|
||||
fn extend(&self) -> Error {
|
||||
impl<E: Display> ErrorExtensions for &E {
|
||||
fn extend(self) -> Error {
|
||||
Error {
|
||||
message: self.to_string(),
|
||||
source: None,
|
||||
extensions: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorExtensions for ResolverError {
|
||||
#[inline]
|
||||
fn extend(self) -> Error {
|
||||
Error::from(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Extend a `Result`'s error value with [`ErrorExtensions`](trait.ErrorExtensions.html).
|
||||
pub trait ResultExt<T, E>: Sized {
|
||||
/// Extend the error value of the result with the callback.
|
||||
|
@ -335,3 +429,27 @@ where
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around a dynamic error type for resolver.
|
||||
#[derive(Debug)]
|
||||
pub struct ResolverError {
|
||||
message: String,
|
||||
error: Arc<dyn Any + Send + Sync>,
|
||||
}
|
||||
|
||||
impl<T: StdError + Send + Sync + 'static> From<T> for ResolverError {
|
||||
fn from(err: T) -> Self {
|
||||
Self {
|
||||
message: err.to_string(),
|
||||
error: Arc::new(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResolverError {
|
||||
/// Create a new failure.
|
||||
#[inline]
|
||||
pub fn new<T: StdError + Send + Sync + 'static>(err: T) -> Self {
|
||||
From::from(err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -624,7 +624,7 @@ impl<'a> GraphQLPlaygroundConfig<'a> {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::collections::BTreeMap;
|
||||
use indexmap::IndexMap;
|
||||
|
||||
#[test]
|
||||
fn test_with_setting_can_use_any_json_value() {
|
||||
|
@ -634,7 +634,7 @@ mod tests {
|
|||
.with_setting("number", 10)
|
||||
.with_setting("null", Value::Null)
|
||||
.with_setting("array", Vec::from([1, 2, 3]))
|
||||
.with_setting("object", BTreeMap::new());
|
||||
.with_setting("object", IndexMap::new());
|
||||
|
||||
let json = serde_json::to_value(settings).unwrap();
|
||||
let settings = json["settings"].as_object().unwrap();
|
||||
|
|
39
src/lib.rs
39
src/lib.rs
|
@ -34,7 +34,7 @@
|
|||
//! * [Docs](https://docs.rs/async-graphql)
|
||||
//! * [GitHub repository](https://github.com/async-graphql/async-graphql)
|
||||
//! * [Cargo package](https://crates.io/crates/async-graphql)
|
||||
//! * Minimum supported Rust version: 1.51 or later
|
||||
//! * Minimum supported Rust version: 1.56.1 or later
|
||||
//!
|
||||
//! ## Features
|
||||
//!
|
||||
|
@ -210,7 +210,7 @@ pub use base::{
|
|||
};
|
||||
pub use error::{
|
||||
Error, ErrorExtensionValues, ErrorExtensions, InputValueError, InputValueResult,
|
||||
ParseRequestError, PathSegment, Result, ResultExt, ServerError, ServerResult,
|
||||
ParseRequestError, PathSegment, ResolverError, Result, ResultExt, ServerError, ServerResult,
|
||||
};
|
||||
pub use look_ahead::Lookahead;
|
||||
pub use registry::CacheControl;
|
||||
|
@ -279,6 +279,7 @@ pub type FieldResult<T> = Result<T>;
|
|||
/// | default | Use `Default::default` for default value | none | Y |
|
||||
/// | default | Argument default value | literal | Y |
|
||||
/// | default_with | Expression to generate default value | code string | Y |
|
||||
/// | derived | Generate derived fields *[See also the Book](https://async-graphql.github.io/async-graphql/en/derived_fields.html).* | object | Y |
|
||||
/// | validator | Input value validator | [`InputValueValidator`](validators/trait.InputValueValidator.html) | Y |
|
||||
/// | complexity | Custom field complexity. *[See also the Book](https://async-graphql.github.io/async-graphql/en/depth_and_complexity.html).* | bool | Y |
|
||||
/// | complexity | Custom field complexity. | string | Y |
|
||||
|
@ -288,6 +289,14 @@ pub type FieldResult<T> = Result<T>;
|
|||
/// | serial | Resolve each field sequentially. | bool | Y |
|
||||
/// | key | Is entity key(for Federation) | bool | Y |
|
||||
///
|
||||
/// # Derived argument parameters
|
||||
///
|
||||
/// | Attribute | description | Type | Optional |
|
||||
/// |--------------|------------------------------------------|------------ |----------|
|
||||
/// | name | Generated derived field name | string | N |
|
||||
/// | into | Type to derived an into | string | Y |
|
||||
/// | with | Function to apply to manage advanced use cases | string| Y |
|
||||
///
|
||||
/// # Valid field return types
|
||||
///
|
||||
/// - Scalar values, such as `i32` and `bool`. `usize`, `isize`, `u128` and `i128` are not
|
||||
|
@ -443,6 +452,7 @@ pub use async_graphql_derive::Object;
|
|||
/// | name | Field name | string | Y |
|
||||
/// | deprecation | Field deprecated | bool | Y |
|
||||
/// | deprecation | Field deprecation reason | string | Y |
|
||||
/// | derived | Generate derived fields *[See also the Book](https://async-graphql.github.io/async-graphql/en/derived_fields.html).* | object | Y |
|
||||
/// | owned | Field resolver return a ownedship value | bool | Y |
|
||||
/// | cache_control | Field cache control | [`CacheControl`](struct.CacheControl.html) | Y |
|
||||
/// | external | Mark a field as owned by another service. This allows service A to use fields from service B while also knowing at runtime the types of that field. | bool | Y |
|
||||
|
@ -452,6 +462,16 @@ pub use async_graphql_derive::Object;
|
|||
/// | visible | If `false`, it will not be displayed in introspection. *[See also the Book](https://async-graphql.github.io/async-graphql/en/visibility.html).* | bool | Y |
|
||||
/// | visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y |
|
||||
///
|
||||
/// # Derived argument parameters
|
||||
///
|
||||
/// | Attribute | description | Type | Optional |
|
||||
/// |--------------|------------------------------------------|------------ |----------|
|
||||
/// | name | Generated derived field name | string | N |
|
||||
/// | into | Type to derived an into | string | Y |
|
||||
/// | owned | Field resolver return a ownedship value | bool | Y |
|
||||
/// | with | Function to apply to manage advanced use cases | string| Y |
|
||||
///
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
|
@ -498,6 +518,7 @@ pub use async_graphql_derive::SimpleObject;
|
|||
/// | name | Field name | string | Y |
|
||||
/// | deprecation | Field deprecated | bool | Y |
|
||||
/// | deprecation | Field deprecation reason | string | Y |
|
||||
/// | derived | Generate derived fields *[See also the Book](https://async-graphql.github.io/async-graphql/en/derived_fields.html).* | object | Y |
|
||||
/// | cache_control | Field cache control | [`CacheControl`](struct.CacheControl.html) | Y |
|
||||
/// | external | Mark a field as owned by another service. This allows service A to use fields from service B while also knowing at runtime the types of that field. | bool | Y |
|
||||
/// | provides | Annotate the expected returned fieldset from a field on a base type that is guaranteed to be selectable by the gateway. | string | Y |
|
||||
|
@ -507,6 +528,14 @@ pub use async_graphql_derive::SimpleObject;
|
|||
/// | visible | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y |
|
||||
/// | secret | Mark this field as a secret, it will not output the actual value in the log. | bool | Y |
|
||||
///
|
||||
/// # Derived argument parameters
|
||||
///
|
||||
/// | Attribute | description | Type | Optional |
|
||||
/// |--------------|------------------------------------------|------------ |----------|
|
||||
/// | name | Generated derived field name | string | N |
|
||||
/// | into | Type to derived an into | string | Y |
|
||||
/// | with | Function to apply to manage advanced use cases | string| Y |
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
|
@ -958,6 +987,7 @@ pub use async_graphql_derive::Subscription;
|
|||
/// | Attribute | description | Type | Optional |
|
||||
/// |-------------|---------------------------|----------|----------|
|
||||
/// | name | Scalar name | string | Y |
|
||||
/// | specified_by_url | Provide a specification URL for this scalar type, it must link to a human-readable specification of the data format, serialization and coercion rules for this scalar. | string | Y |
|
||||
///
|
||||
pub use async_graphql_derive::Scalar;
|
||||
|
||||
|
@ -971,8 +1001,9 @@ pub use async_graphql_derive::Scalar;
|
|||
/// |-------------|---------------------------|----------|----------|
|
||||
/// | name | If this attribute is provided then define a new scalar, otherwise it is just a transparent proxy for the internal scalar. | string | Y |
|
||||
/// | name | If this attribute is provided then define a new scalar, otherwise it is just a transparent proxy for the internal scalar. | bool | Y |
|
||||
/// | visible(Only valid for new scalars.) | If `false`, it will not be displayed in introspection. *[See also the Book](https://async-graphql.github.io/async-graphql/en/visibility.html).* | bool | Y |
|
||||
/// | visible(Only valid for new scalars.) | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y |
|
||||
/// | visible(Only valid for new scalars) | If `false`, it will not be displayed in introspection. *[See also the Book](https://async-graphql.github.io/async-graphql/en/visibility.html).* | bool | Y |
|
||||
/// | visible(Only valid for new scalars) | Call the specified function. If the return value is `false`, it will not be displayed in introspection. | string | Y |
|
||||
/// | specified_by_url(Only valid for new scalars) | Provide a specification URL for this scalar type, it must link to a human-readable specification of the data format, serialization and coercion rules for this scalar. | string | Y |
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
use std::collections::HashMap;
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use crate::parser::types::{Field, FragmentDefinition, Selection, SelectionSet};
|
||||
use crate::Context;
|
||||
|
|
|
@ -218,4 +218,16 @@ impl<'a> __Type<'a> {
|
|||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[graphql(name = "specifiedByURL")]
|
||||
async fn specified_by_url(&self) -> Option<&'a str> {
|
||||
if let TypeDetail::Named(registry::MetaType::Scalar {
|
||||
specified_by_url, ..
|
||||
}) = &self.detail
|
||||
{
|
||||
*specified_by_url
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -189,6 +189,7 @@ pub enum MetaType {
|
|||
description: Option<&'static str>,
|
||||
is_valid: fn(value: &Value) -> bool,
|
||||
visible: Option<MetaVisibleFn>,
|
||||
specified_by_url: Option<&'static str>,
|
||||
},
|
||||
Object {
|
||||
name: String,
|
||||
|
@ -198,6 +199,7 @@ pub enum MetaType {
|
|||
extends: bool,
|
||||
keys: Option<Vec<String>>,
|
||||
visible: Option<MetaVisibleFn>,
|
||||
is_subscription: bool,
|
||||
},
|
||||
Interface {
|
||||
name: String,
|
||||
|
@ -374,6 +376,7 @@ impl Registry {
|
|||
extends: false,
|
||||
keys: None,
|
||||
visible: None,
|
||||
is_subscription: false,
|
||||
},
|
||||
);
|
||||
let ty = f(self);
|
||||
|
@ -507,6 +510,7 @@ impl Registry {
|
|||
extends: false,
|
||||
keys: None,
|
||||
visible: None,
|
||||
is_subscription: false,
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryFrom;
|
||||
use std::fmt::{self, Debug, Formatter};
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
|
||||
use indexmap::IndexMap;
|
||||
|
||||
use crate::extensions::ResolveInfo;
|
||||
use crate::parser::types::Selection;
|
||||
use crate::registry::MetaType;
|
||||
|
@ -74,7 +75,7 @@ pub async fn resolve_container_serial<'a, T: ContainerType + ?Sized>(
|
|||
resolve_container_inner(ctx, root, false).await
|
||||
}
|
||||
|
||||
fn insert_value(target: &mut BTreeMap<Name, Value>, name: Name, value: Value) {
|
||||
fn insert_value(target: &mut IndexMap<Name, Value>, name: Name, value: Value) {
|
||||
if let Some(prev_value) = target.get_mut(&name) {
|
||||
if let Value::Object(target_map) = prev_value {
|
||||
if let Value::Object(obj) = value {
|
||||
|
@ -118,7 +119,7 @@ async fn resolve_container_inner<'a, T: ContainerType + ?Sized>(
|
|||
results
|
||||
};
|
||||
|
||||
let mut map = BTreeMap::new();
|
||||
let mut map = IndexMap::new();
|
||||
for (name, value) in res {
|
||||
insert_value(&mut map, name, value);
|
||||
}
|
||||
|
|
|
@ -68,6 +68,9 @@ pub trait ScalarType: Sized + Send {
|
|||
/// // Rename to `MV` and add description.
|
||||
/// // scalar!(MyValue, "MV", "This is my value");
|
||||
///
|
||||
/// // Rename to `MV`, add description and specifiedByURL.
|
||||
/// // scalar!(MyValue, "MV", "This is my value", "https://tools.ietf.org/html/rfc4122");
|
||||
///
|
||||
/// struct Query;
|
||||
///
|
||||
/// #[Object]
|
||||
|
@ -92,23 +95,47 @@ pub trait ScalarType: Sized + Send {
|
|||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! scalar {
|
||||
($ty:ty, $name:literal, $desc:literal, $specified_by_url:literal) => {
|
||||
$crate::scalar_internal!(
|
||||
$ty,
|
||||
$name,
|
||||
::std::option::Option::Some($desc),
|
||||
::std::option::Option::Some($specified_by_url)
|
||||
);
|
||||
};
|
||||
|
||||
($ty:ty, $name:literal, $desc:literal) => {
|
||||
$crate::scalar_internal!($ty, $name, ::std::option::Option::Some($desc));
|
||||
$crate::scalar_internal!(
|
||||
$ty,
|
||||
$name,
|
||||
::std::option::Option::Some($desc),
|
||||
::std::option::Option::None
|
||||
);
|
||||
};
|
||||
|
||||
($ty:ty, $name:literal) => {
|
||||
$crate::scalar_internal!($ty, $name, ::std::option::Option::None);
|
||||
$crate::scalar_internal!(
|
||||
$ty,
|
||||
$name,
|
||||
::std::option::Option::None,
|
||||
::std::option::Option::None
|
||||
);
|
||||
};
|
||||
|
||||
($ty:ty) => {
|
||||
$crate::scalar_internal!($ty, ::std::stringify!($ty), ::std::option::Option::None);
|
||||
$crate::scalar_internal!(
|
||||
$ty,
|
||||
::std::stringify!($ty),
|
||||
::std::option::Option::None,
|
||||
::std::option::Option::None
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
#[doc(hidden)]
|
||||
macro_rules! scalar_internal {
|
||||
($ty:ty, $name:expr, $desc:expr) => {
|
||||
($ty:ty, $name:expr, $desc:expr, $specified_by_url:expr) => {
|
||||
impl $crate::Type for $ty {
|
||||
fn type_name() -> ::std::borrow::Cow<'static, ::std::primitive::str> {
|
||||
::std::borrow::Cow::Borrowed($name)
|
||||
|
@ -122,6 +149,7 @@ macro_rules! scalar_internal {
|
|||
description: $desc,
|
||||
is_valid: |value| <$ty as $crate::ScalarType>::is_valid(value),
|
||||
visible: ::std::option::Option::None,
|
||||
specified_by_url: $specified_by_url,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -98,6 +98,7 @@ impl Response {
|
|||
}
|
||||
|
||||
/// Response for batchable queries
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum BatchResponse {
|
||||
|
|
|
@ -190,6 +190,7 @@ where
|
|||
extends: false,
|
||||
keys: None,
|
||||
visible: None,
|
||||
is_subscription: false,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -107,6 +107,7 @@ where
|
|||
extends: false,
|
||||
keys: None,
|
||||
visible: None,
|
||||
is_subscription: false,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -45,6 +45,7 @@ impl Type for EmptyMutation {
|
|||
extends: false,
|
||||
keys: None,
|
||||
visible: None,
|
||||
is_subscription: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ impl Type for EmptySubscription {
|
|||
extends: false,
|
||||
keys: None,
|
||||
visible: None,
|
||||
is_subscription: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
6
src/types/external/chrono_tz.rs
vendored
6
src/types/external/chrono_tz.rs
vendored
|
@ -2,7 +2,11 @@ use chrono_tz::Tz;
|
|||
|
||||
use crate::{InputValueError, InputValueResult, Scalar, ScalarType, Value};
|
||||
|
||||
#[Scalar(internal, name = "TimeZone")]
|
||||
#[Scalar(
|
||||
internal,
|
||||
name = "TimeZone",
|
||||
specified_by_url = "http://www.iana.org/time-zones"
|
||||
)]
|
||||
impl ScalarType for Tz {
|
||||
fn parse(value: Value) -> InputValueResult<Self> {
|
||||
match value {
|
||||
|
|
18
src/types/external/datetime.rs
vendored
18
src/types/external/datetime.rs
vendored
|
@ -5,7 +5,11 @@ use crate::{InputValueError, InputValueResult, Scalar, ScalarType, Value};
|
|||
/// Implement the DateTime<FixedOffset> scalar
|
||||
///
|
||||
/// The input/output is a string in RFC3339 format.
|
||||
#[Scalar(internal, name = "DateTime")]
|
||||
#[Scalar(
|
||||
internal,
|
||||
name = "DateTime",
|
||||
specified_by_url = "https://datatracker.ietf.org/doc/html/rfc3339"
|
||||
)]
|
||||
impl ScalarType for DateTime<FixedOffset> {
|
||||
fn parse(value: Value) -> InputValueResult<Self> {
|
||||
match &value {
|
||||
|
@ -22,7 +26,11 @@ impl ScalarType for DateTime<FixedOffset> {
|
|||
/// Implement the DateTime<Local> scalar
|
||||
///
|
||||
/// The input/output is a string in RFC3339 format.
|
||||
#[Scalar(internal, name = "DateTime")]
|
||||
#[Scalar(
|
||||
internal,
|
||||
name = "DateTime",
|
||||
specified_by_url = "https://datatracker.ietf.org/doc/html/rfc3339"
|
||||
)]
|
||||
impl ScalarType for DateTime<Local> {
|
||||
fn parse(value: Value) -> InputValueResult<Self> {
|
||||
match &value {
|
||||
|
@ -39,7 +47,11 @@ impl ScalarType for DateTime<Local> {
|
|||
/// Implement the DateTime<Utc> scalar
|
||||
///
|
||||
/// The input/output is a string in RFC3339 format.
|
||||
#[Scalar(internal, name = "DateTime")]
|
||||
#[Scalar(
|
||||
internal,
|
||||
name = "DateTime",
|
||||
specified_by_url = "https://datatracker.ietf.org/doc/html/rfc3339"
|
||||
)]
|
||||
impl ScalarType for DateTime<Utc> {
|
||||
fn parse(value: Value) -> InputValueResult<Self> {
|
||||
match &value {
|
||||
|
|
25
src/types/external/duration.rs
vendored
Normal file
25
src/types/external/duration.rs
vendored
Normal file
|
@ -0,0 +1,25 @@
|
|||
use chrono::Duration;
|
||||
use iso8601_duration as iso8601;
|
||||
|
||||
use crate::{InputValueError, InputValueResult, Scalar, ScalarType, Value};
|
||||
|
||||
/// Implement the Duration scalar
|
||||
///
|
||||
/// The input/output is a string in ISO8601 format.
|
||||
#[Scalar(
|
||||
internal,
|
||||
name = "Duration",
|
||||
specified_by_url = "https://en.wikipedia.org/wiki/ISO_8601#Durations"
|
||||
)]
|
||||
impl ScalarType for Duration {
|
||||
fn parse(value: Value) -> InputValueResult<Self> {
|
||||
match &value {
|
||||
Value::String(s) => Ok(Duration::from_std(iso8601::Duration::parse(s)?.to_std())?),
|
||||
_ => Err(InputValueError::expected_type(value)),
|
||||
}
|
||||
}
|
||||
|
||||
fn to_value(&self) -> Value {
|
||||
Value::String(self.to_string())
|
||||
}
|
||||
}
|
4
src/types/external/json_object/btreemap.rs
vendored
4
src/types/external/json_object/btreemap.rs
vendored
|
@ -2,6 +2,8 @@ use std::collections::BTreeMap;
|
|||
use std::fmt::Display;
|
||||
use std::str::FromStr;
|
||||
|
||||
use indexmap::IndexMap;
|
||||
|
||||
use crate::{
|
||||
InputType, InputValueError, InputValueResult, Name, OutputType, Scalar, ScalarType, Value,
|
||||
};
|
||||
|
@ -33,7 +35,7 @@ where
|
|||
}
|
||||
|
||||
fn to_value(&self) -> Value {
|
||||
let mut map = BTreeMap::new();
|
||||
let mut map = IndexMap::new();
|
||||
for (name, value) in self {
|
||||
map.insert(Name::new(name.to_string()), value.to_value());
|
||||
}
|
||||
|
|
6
src/types/external/json_object/hashmap.rs
vendored
6
src/types/external/json_object/hashmap.rs
vendored
|
@ -1,8 +1,10 @@
|
|||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
use std::hash::Hash;
|
||||
use std::str::FromStr;
|
||||
|
||||
use indexmap::IndexMap;
|
||||
|
||||
use crate::{
|
||||
InputType, InputValueError, InputValueResult, Name, OutputType, Scalar, ScalarType, Value,
|
||||
};
|
||||
|
@ -34,7 +36,7 @@ where
|
|||
}
|
||||
|
||||
fn to_value(&self) -> Value {
|
||||
let mut map = BTreeMap::new();
|
||||
let mut map = IndexMap::new();
|
||||
for (name, value) in self {
|
||||
map.insert(Name::new(name.to_string()), value.to_value());
|
||||
}
|
||||
|
|
1
src/types/external/list/array.rs
vendored
1
src/types/external/list/array.rs
vendored
|
@ -1,5 +1,4 @@
|
|||
use std::borrow::Cow;
|
||||
use std::convert::TryInto;
|
||||
|
||||
use crate::parser::types::Field;
|
||||
use crate::resolver_utils::resolve_list;
|
||||
|
|
2
src/types/external/mod.rs
vendored
2
src/types/external/mod.rs
vendored
|
@ -20,6 +20,8 @@ mod chrono_tz;
|
|||
mod datetime;
|
||||
#[cfg(feature = "decimal")]
|
||||
mod decimal;
|
||||
#[cfg(feature = "chrono-duration")]
|
||||
mod duration;
|
||||
#[cfg(feature = "chrono")]
|
||||
mod naive_time;
|
||||
#[cfg(feature = "secrecy")]
|
||||
|
|
2
src/types/external/url.rs
vendored
2
src/types/external/url.rs
vendored
|
@ -2,7 +2,7 @@ use url::Url;
|
|||
|
||||
use crate::{InputValueError, InputValueResult, Scalar, ScalarType, Value};
|
||||
|
||||
#[Scalar(internal)]
|
||||
#[Scalar(internal, specified_by_url = "http://url.spec.whatwg.org/")]
|
||||
/// URL is a String implementing the [URL Standard](http://url.spec.whatwg.org/)
|
||||
impl ScalarType for Url {
|
||||
fn parse(value: Value) -> InputValueResult<Self> {
|
||||
|
|
6
src/types/external/uuid.rs
vendored
6
src/types/external/uuid.rs
vendored
|
@ -2,7 +2,11 @@ use uuid::Uuid;
|
|||
|
||||
use crate::{InputValueError, InputValueResult, Scalar, ScalarType, Value};
|
||||
|
||||
#[Scalar(internal, name = "UUID")]
|
||||
#[Scalar(
|
||||
internal,
|
||||
name = "UUID",
|
||||
specified_by_url = "http://tools.ietf.org/html/rfc4122"
|
||||
)]
|
||||
/// A UUID is a unique 128-bit number, stored as 16 octets. UUIDs are parsed as Strings
|
||||
/// within GraphQL. UUIDs are used to assign unique identifiers to entities without requiring a central
|
||||
/// allocating authority.
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
use std::convert::TryFrom;
|
||||
use std::num::ParseIntError;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
|
|
|
@ -85,6 +85,7 @@ impl<T> Type for OutputJson<T> {
|
|||
description: None,
|
||||
is_valid: |_| true,
|
||||
visible: None,
|
||||
specified_by_url: None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use std::borrow::Cow;
|
||||
use std::ops::Deref;
|
||||
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
|
@ -6,7 +7,7 @@ use crate::{registry, InputType, InputValueError, InputValueResult, Type, Value}
|
|||
|
||||
/// Similar to `Option`, but it has three states, `undefined`, `null` and `x`.
|
||||
///
|
||||
/// **Reference:** <https://spec.graphql.org/June2018/#sec-Null-Value>
|
||||
/// **Reference:** <https://spec.graphql.org/October2021/#sec-Null-Value>
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
|
@ -61,34 +62,34 @@ impl<T> Default for MaybeUndefined<T> {
|
|||
}
|
||||
|
||||
impl<T> MaybeUndefined<T> {
|
||||
/// Returns true if the MaybeUndefined<T> is undefined.
|
||||
/// Returns true if the `MaybeUndefined<T>` is undefined.
|
||||
#[inline]
|
||||
pub fn is_undefined(&self) -> bool {
|
||||
pub const fn is_undefined(&self) -> bool {
|
||||
matches!(self, MaybeUndefined::Undefined)
|
||||
}
|
||||
|
||||
/// Returns true if the MaybeUndefined<T> is null.
|
||||
/// Returns true if the `MaybeUndefined<T>` is null.
|
||||
#[inline]
|
||||
pub fn is_null(&self) -> bool {
|
||||
pub const fn is_null(&self) -> bool {
|
||||
matches!(self, MaybeUndefined::Null)
|
||||
}
|
||||
|
||||
/// Returns true if the MaybeUndefined<T> is value.
|
||||
/// Returns true if the `MaybeUndefined<T>` contains value.
|
||||
#[inline]
|
||||
pub fn is_value(&self) -> bool {
|
||||
pub const fn is_value(&self) -> bool {
|
||||
matches!(self, MaybeUndefined::Value(_))
|
||||
}
|
||||
|
||||
/// Borrow the value, returns `None` if the value is `undefined` or `null`, otherwise returns `Some(T)`.
|
||||
/// Borrow the value, returns `None` if the the `MaybeUndefined<T>` is `undefined` or `null`, otherwise returns `Some(T)`.
|
||||
#[inline]
|
||||
pub fn value(&self) -> Option<&T> {
|
||||
pub const fn value(&self) -> Option<&T> {
|
||||
match self {
|
||||
MaybeUndefined::Value(value) => Some(value),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert MaybeUndefined<T> to Option<T>.
|
||||
/// Converts the `MaybeUndefined<T>` to `Option<T>`.
|
||||
#[inline]
|
||||
pub fn take(self) -> Option<T> {
|
||||
match self {
|
||||
|
@ -96,6 +97,81 @@ impl<T> MaybeUndefined<T> {
|
|||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the `MaybeUndefined<T>` to `Option<Option<T>>`.
|
||||
#[inline]
|
||||
pub const fn as_opt_ref(&self) -> Option<Option<&T>> {
|
||||
match self {
|
||||
MaybeUndefined::Undefined => None,
|
||||
MaybeUndefined::Null => Some(None),
|
||||
MaybeUndefined::Value(value) => Some(Some(value)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the `MaybeUndefined<T>` to `Option<Option<&U>>`.
|
||||
#[inline]
|
||||
pub fn as_opt_deref<U>(&self) -> Option<Option<&U>>
|
||||
where
|
||||
U: ?Sized,
|
||||
T: Deref<Target = U>,
|
||||
{
|
||||
match self {
|
||||
MaybeUndefined::Undefined => None,
|
||||
MaybeUndefined::Null => Some(None),
|
||||
MaybeUndefined::Value(value) => Some(Some(value.deref())),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the `MaybeUndefined<T>` contains the given value.
|
||||
#[inline]
|
||||
pub fn contains_value<U>(&self, x: &U) -> bool
|
||||
where
|
||||
U: PartialEq<T>,
|
||||
{
|
||||
match self {
|
||||
MaybeUndefined::Value(y) => x == y,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if the `MaybeUndefined<T>` contains the given nullable value.
|
||||
#[inline]
|
||||
pub fn contains<U>(&self, x: &Option<U>) -> bool
|
||||
where
|
||||
U: PartialEq<T>,
|
||||
{
|
||||
match self {
|
||||
MaybeUndefined::Value(y) => matches!(x, Some(v) if v == y),
|
||||
MaybeUndefined::Null => matches!(x, None),
|
||||
MaybeUndefined::Undefined => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Maps a `MaybeUndefined<T>` to `MaybeUndefined<U>` by applying a function to the contained nullable value
|
||||
#[inline]
|
||||
pub fn map<U, F: FnOnce(Option<T>) -> Option<U>>(self, f: F) -> MaybeUndefined<U> {
|
||||
match self {
|
||||
MaybeUndefined::Value(v) => match f(Some(v)) {
|
||||
Some(v) => MaybeUndefined::Value(v),
|
||||
None => MaybeUndefined::Null,
|
||||
},
|
||||
MaybeUndefined::Null => match f(None) {
|
||||
Some(v) => MaybeUndefined::Value(v),
|
||||
None => MaybeUndefined::Null,
|
||||
},
|
||||
MaybeUndefined::Undefined => MaybeUndefined::Undefined,
|
||||
}
|
||||
}
|
||||
|
||||
/// Maps a `MaybeUndefined<T>` to `MaybeUndefined<U>` by applying a function to the contained value
|
||||
#[inline]
|
||||
pub fn map_value<U, F: FnOnce(T) -> U>(self, f: F) -> MaybeUndefined<U> {
|
||||
match self {
|
||||
MaybeUndefined::Value(v) => MaybeUndefined::Value(f(v)),
|
||||
MaybeUndefined::Null => MaybeUndefined::Null,
|
||||
MaybeUndefined::Undefined => MaybeUndefined::Undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Type> Type for MaybeUndefined<T> {
|
||||
|
@ -132,6 +208,24 @@ impl<T: InputType> InputType for MaybeUndefined<T> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T, E> MaybeUndefined<Result<T, E>> {
|
||||
/// Transposes a `MaybeUndefined` of a [`Result`] into a [`Result`] of a `MaybeUndefined`.
|
||||
///
|
||||
/// [`MaybeUndefined::Undefined`] will be mapped to [`Ok`]`(`[`MaybeUndefined::Undefined`]`)`.
|
||||
/// [`MaybeUndefined::Null`] will be mapped to [`Ok`]`(`[`MaybeUndefined::Null`]`)`.
|
||||
/// [`MaybeUndefined::Value`]`(`[`Ok`]`(_))` and [`MaybeUndefined::Value`]`(`[`Err`]`(_))` will be mapped to
|
||||
/// [`Ok`]`(`[`MaybeUndefined::Value`]`(_))` and [`Err`]`(_)`.
|
||||
#[inline]
|
||||
pub fn transpose(self) -> Result<MaybeUndefined<T>, E> {
|
||||
match self {
|
||||
MaybeUndefined::Undefined => Ok(MaybeUndefined::Undefined),
|
||||
MaybeUndefined::Null => Ok(MaybeUndefined::Null),
|
||||
MaybeUndefined::Value(Ok(v)) => Ok(MaybeUndefined::Value(v)),
|
||||
MaybeUndefined::Value(Err(e)) => Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Serialize> Serialize for MaybeUndefined<T> {
|
||||
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
|
||||
match self {
|
||||
|
@ -166,6 +260,16 @@ impl<T> From<MaybeUndefined<T>> for Option<Option<T>> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<T> From<Option<Option<T>>> for MaybeUndefined<T> {
|
||||
fn from(value: Option<Option<T>>) -> Self {
|
||||
match value {
|
||||
Some(Some(value)) => Self::Value(value),
|
||||
Some(None) => Self::Null,
|
||||
None => Self::Undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::*;
|
||||
|
@ -260,4 +364,117 @@ mod tests {
|
|||
Some(Some(42))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_as_opt_ref() {
|
||||
let mut value: MaybeUndefined<String>;
|
||||
let mut r: Option<Option<&String>>;
|
||||
|
||||
value = MaybeUndefined::Undefined;
|
||||
r = value.as_opt_ref();
|
||||
assert_eq!(r, None);
|
||||
|
||||
value = MaybeUndefined::Null;
|
||||
r = value.as_opt_ref();
|
||||
assert_eq!(r, Some(None));
|
||||
|
||||
value = MaybeUndefined::Value("abc".to_string());
|
||||
r = value.as_opt_ref();
|
||||
assert_eq!(r, Some(Some(&"abc".to_string())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_as_opt_deref() {
|
||||
let mut value: MaybeUndefined<String>;
|
||||
let mut r: Option<Option<&str>>;
|
||||
|
||||
value = MaybeUndefined::Undefined;
|
||||
r = value.as_opt_deref();
|
||||
assert_eq!(r, None);
|
||||
|
||||
value = MaybeUndefined::Null;
|
||||
r = value.as_opt_deref();
|
||||
assert_eq!(r, Some(None));
|
||||
|
||||
value = MaybeUndefined::Value("abc".to_string());
|
||||
r = value.as_opt_deref();
|
||||
assert_eq!(r, Some(Some("abc")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_contains_value() {
|
||||
let test = "abc";
|
||||
|
||||
let mut value: MaybeUndefined<String> = MaybeUndefined::Undefined;
|
||||
assert!(!value.contains_value(&test));
|
||||
|
||||
value = MaybeUndefined::Null;
|
||||
assert!(!value.contains_value(&test));
|
||||
|
||||
value = MaybeUndefined::Value("abc".to_string());
|
||||
assert!(value.contains_value(&test));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_contains() {
|
||||
let test = Some("abc");
|
||||
let none: Option<&str> = None;
|
||||
|
||||
let mut value: MaybeUndefined<String> = MaybeUndefined::Undefined;
|
||||
assert!(!value.contains(&test));
|
||||
assert!(!value.contains(&none));
|
||||
|
||||
value = MaybeUndefined::Null;
|
||||
assert!(!value.contains(&test));
|
||||
assert!(value.contains(&none));
|
||||
|
||||
value = MaybeUndefined::Value("abc".to_string());
|
||||
assert!(value.contains(&test));
|
||||
assert!(!value.contains(&none));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_map_value() {
|
||||
let mut value: MaybeUndefined<i32> = MaybeUndefined::Undefined;
|
||||
assert_eq!(value.map_value(|v| v > 2), MaybeUndefined::Undefined);
|
||||
|
||||
value = MaybeUndefined::Null;
|
||||
assert_eq!(value.map_value(|v| v > 2), MaybeUndefined::Null);
|
||||
|
||||
value = MaybeUndefined::Value(5);
|
||||
assert_eq!(value.map_value(|v| v > 2), MaybeUndefined::Value(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_map() {
|
||||
let mut value: MaybeUndefined<i32> = MaybeUndefined::Undefined;
|
||||
assert_eq!(value.map(|v| Some(v.is_some())), MaybeUndefined::Undefined);
|
||||
|
||||
value = MaybeUndefined::Null;
|
||||
assert_eq!(
|
||||
value.map(|v| Some(v.is_some())),
|
||||
MaybeUndefined::Value(false)
|
||||
);
|
||||
|
||||
value = MaybeUndefined::Value(5);
|
||||
assert_eq!(
|
||||
value.map(|v| Some(v.is_some())),
|
||||
MaybeUndefined::Value(true)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_transpose() {
|
||||
let mut value: MaybeUndefined<Result<i32, &'static str>> = MaybeUndefined::Undefined;
|
||||
assert_eq!(value.transpose(), Ok(MaybeUndefined::Undefined));
|
||||
|
||||
value = MaybeUndefined::Null;
|
||||
assert_eq!(value.transpose(), Ok(MaybeUndefined::Null));
|
||||
|
||||
value = MaybeUndefined::Value(Ok(5));
|
||||
assert_eq!(value.transpose(), Ok(MaybeUndefined::Value(5)));
|
||||
|
||||
value = MaybeUndefined::Value(Err("eror"));
|
||||
assert_eq!(value.transpose(), Err("eror"));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@ impl<A: Type, B: Type> Type for MergedObject<A, B> {
|
|||
extends: false,
|
||||
keys: None,
|
||||
visible: None,
|
||||
is_subscription: false,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -111,6 +111,7 @@ impl Type for Upload {
|
|||
description: None,
|
||||
is_valid: |value| matches!(value, Value::String(_)),
|
||||
visible: None,
|
||||
specified_by_url: Some("https://github.com/jaydenseric/graphql-multipart-request-spec"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -329,4 +329,9 @@ mod tests {
|
|||
"#,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typename_in_subscription_root() {
|
||||
expect_fails_rule!(factory, "subscription { __typename }");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::futures_util::Stream;
|
||||
use crate::parser::types::ExecutableDocument;
|
||||
use crate::validation::visitor::{visit, RuleError, Visitor, VisitorContext};
|
||||
use crate::*;
|
||||
|
@ -345,8 +346,17 @@ impl MutationRoot {
|
|||
}
|
||||
}
|
||||
|
||||
static TEST_HARNESS: Lazy<Schema<QueryRoot, MutationRoot, EmptySubscription>> =
|
||||
Lazy::new(|| Schema::new(QueryRoot, MutationRoot, EmptySubscription));
|
||||
pub struct SubscriptionRoot;
|
||||
|
||||
#[Subscription(internal)]
|
||||
impl SubscriptionRoot {
|
||||
async fn values(&self) -> impl Stream<Item = i32> {
|
||||
futures_util::stream::once(async move { 10 })
|
||||
}
|
||||
}
|
||||
|
||||
static TEST_HARNESS: Lazy<Schema<QueryRoot, MutationRoot, SubscriptionRoot>> =
|
||||
Lazy::new(|| Schema::new(QueryRoot, MutationRoot, SubscriptionRoot));
|
||||
|
||||
pub(crate) fn validate<'a, V, F>(
|
||||
doc: &'a ExecutableDocument,
|
||||
|
|
|
@ -611,6 +611,17 @@ fn visit_selection<'a, V: Visitor<'a>>(
|
|||
visit_field(v, ctx, field);
|
||||
},
|
||||
);
|
||||
} else if ctx.current_type().map(|ty| match ty {
|
||||
MetaType::Object {
|
||||
is_subscription, ..
|
||||
} => *is_subscription,
|
||||
_ => false,
|
||||
}) == Some(true)
|
||||
{
|
||||
ctx.report_error(
|
||||
vec![field.pos],
|
||||
"Unknown field \"__typename\" on type \"Subscription\".",
|
||||
);
|
||||
}
|
||||
}
|
||||
Selection::FragmentSpread(fragment_spread) => {
|
||||
|
@ -840,6 +851,7 @@ impl From<RuleError> for ServerError {
|
|||
fn from(e: RuleError) -> Self {
|
||||
Self {
|
||||
message: e.message,
|
||||
source: None,
|
||||
locations: e.locations,
|
||||
path: Vec::new(),
|
||||
extensions: e.extensions,
|
||||
|
|
382
tests/derived_field.rs
Normal file
382
tests/derived_field.rs
Normal file
|
@ -0,0 +1,382 @@
|
|||
use async_graphql::*;
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn test_derived_field_object() {
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
struct Query;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct ValueDerived(String);
|
||||
|
||||
scalar!(ValueDerived);
|
||||
|
||||
impl From<i32> for ValueDerived {
|
||||
fn from(value: i32) -> Self {
|
||||
ValueDerived(format!("{}", value))
|
||||
}
|
||||
}
|
||||
|
||||
#[Object]
|
||||
impl Query {
|
||||
#[graphql(derived(name = "value2", into = "ValueDerived"))]
|
||||
async fn value1(&self, #[graphql(default = 100)] input: i32) -> i32 {
|
||||
input
|
||||
}
|
||||
}
|
||||
|
||||
let query = "{ value1 value2 }";
|
||||
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
|
||||
assert_eq!(
|
||||
schema.execute(query).await.data,
|
||||
value!({
|
||||
"value1": 100,
|
||||
"value2": "100",
|
||||
})
|
||||
);
|
||||
|
||||
let query = "{ value1(input: 1) value2(input: 2) }";
|
||||
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
|
||||
assert_eq!(
|
||||
schema.execute(query).await.data,
|
||||
value!({
|
||||
"value1": 1,
|
||||
"value2": "2",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn test_derived_field_object_with() {
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
struct Query;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct ValueDerived(String);
|
||||
|
||||
scalar!(ValueDerived);
|
||||
|
||||
impl From<i32> for ValueDerived {
|
||||
fn from(value: i32) -> Self {
|
||||
ValueDerived(format!("{}", value))
|
||||
}
|
||||
}
|
||||
|
||||
fn option_to_option<T, U: From<T>>(value: Option<T>) -> Option<U> {
|
||||
value.map(|x| x.into())
|
||||
}
|
||||
|
||||
#[Object]
|
||||
impl Query {
|
||||
#[graphql(derived(
|
||||
name = "value2",
|
||||
into = "Option<ValueDerived>",
|
||||
with = "option_to_option"
|
||||
))]
|
||||
async fn value1(&self, #[graphql(default = 100)] input: i32) -> Option<i32> {
|
||||
Some(input)
|
||||
}
|
||||
}
|
||||
|
||||
let query = "{ value1 value2 }";
|
||||
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
|
||||
assert_eq!(
|
||||
schema.execute(query).await.data,
|
||||
value!({
|
||||
"value1": 100,
|
||||
"value2": "100",
|
||||
})
|
||||
);
|
||||
|
||||
let query = "{ value1(input: 1) value2(input: 2) }";
|
||||
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
|
||||
assert_eq!(
|
||||
schema.execute(query).await.data,
|
||||
value!({
|
||||
"value1": 1,
|
||||
"value2": "2",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn test_derived_field_simple_object() {
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
struct Query;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct ValueDerived(String);
|
||||
|
||||
scalar!(ValueDerived);
|
||||
|
||||
impl From<i32> for ValueDerived {
|
||||
fn from(value: i32) -> Self {
|
||||
ValueDerived(format!("{}", value))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(SimpleObject)]
|
||||
struct TestObj {
|
||||
#[graphql(owned, derived(name = "value2", into = "ValueDerived"))]
|
||||
pub value1: i32,
|
||||
}
|
||||
|
||||
#[Object]
|
||||
impl Query {
|
||||
async fn test(&self, #[graphql(default = 100)] input: i32) -> TestObj {
|
||||
TestObj { value1: input }
|
||||
}
|
||||
}
|
||||
|
||||
let query = "{ test { value1 value2 } }";
|
||||
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
|
||||
assert_eq!(
|
||||
schema.execute(query).await.data,
|
||||
value!({
|
||||
"test": {
|
||||
"value1": 100,
|
||||
"value2": "100",
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
let query = "{ test(input: 2) { value1 value2 }}";
|
||||
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
|
||||
dbg!(schema.execute(query).await);
|
||||
assert_eq!(
|
||||
schema.execute(query).await.data,
|
||||
value!({
|
||||
"test": {
|
||||
"value1": 2,
|
||||
"value2": "2",
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn test_derived_field_simple_object_option() {
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
struct Query;
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
struct ValueDerived(String);
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone)]
|
||||
struct ValueDerived2(String);
|
||||
|
||||
scalar!(ValueDerived);
|
||||
scalar!(ValueDerived2);
|
||||
|
||||
impl From<ValueDerived> for ValueDerived2 {
|
||||
fn from(value: ValueDerived) -> Self {
|
||||
ValueDerived2(value.0)
|
||||
}
|
||||
}
|
||||
|
||||
fn option_to_option<T, U: From<T>>(value: Option<T>) -> Option<U> {
|
||||
value.map(|x| x.into())
|
||||
}
|
||||
|
||||
fn vec_to_vec<T, U: From<T>>(value: Vec<T>) -> Vec<U> {
|
||||
value.into_iter().map(|x| x.into()).collect()
|
||||
}
|
||||
|
||||
fn vecopt_to_vecopt<T, U: From<T>>(value: Vec<Option<T>>) -> Vec<Option<U>> {
|
||||
value.into_iter().map(|x| x.map(|opt| opt.into())).collect()
|
||||
}
|
||||
|
||||
fn optvec_to_optvec<T, U: From<T>>(value: Option<Vec<T>>) -> Option<Vec<U>> {
|
||||
value.map(|x| x.into_iter().map(|y| y.into()).collect())
|
||||
}
|
||||
|
||||
#[derive(SimpleObject)]
|
||||
struct TestObj {
|
||||
#[graphql(derived(
|
||||
owned,
|
||||
name = "value2",
|
||||
into = "Option<ValueDerived2>",
|
||||
with = "option_to_option"
|
||||
))]
|
||||
pub value1: Option<ValueDerived>,
|
||||
#[graphql(derived(
|
||||
owned,
|
||||
name = "value_vec_2",
|
||||
into = "Vec<ValueDerived2>",
|
||||
with = "vec_to_vec"
|
||||
))]
|
||||
pub value_vec_1: Vec<ValueDerived>,
|
||||
#[graphql(derived(
|
||||
owned,
|
||||
name = "value_opt_vec_2",
|
||||
into = "Option<Vec<ValueDerived2>>",
|
||||
with = "optvec_to_optvec"
|
||||
))]
|
||||
pub value_opt_vec_1: Option<Vec<ValueDerived>>,
|
||||
#[graphql(derived(
|
||||
owned,
|
||||
name = "value_vec_opt_2",
|
||||
into = "Vec<Option<ValueDerived2>>",
|
||||
with = "vecopt_to_vecopt"
|
||||
))]
|
||||
pub value_vec_opt_1: Vec<Option<ValueDerived>>,
|
||||
}
|
||||
|
||||
#[Object]
|
||||
impl Query {
|
||||
async fn test(&self) -> TestObj {
|
||||
TestObj {
|
||||
value1: Some(ValueDerived("Test".to_string())),
|
||||
value_vec_1: vec![ValueDerived("Test".to_string())],
|
||||
value_opt_vec_1: Some(vec![ValueDerived("Test".to_string())]),
|
||||
value_vec_opt_1: vec![Some(ValueDerived("Test".to_string()))],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let query = "{ test { value1 value2 valueVec1 valueVec2 valueOptVec1 valueOptVec2 } }";
|
||||
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
|
||||
assert_eq!(
|
||||
schema.execute(query).await.data,
|
||||
value!({
|
||||
"test": {
|
||||
"value1": "Test",
|
||||
"value2": "Test",
|
||||
"valueVec1": vec!["Test"],
|
||||
"valueVec2": vec!["Test"],
|
||||
"valueOptVec1": vec!["Test"],
|
||||
"valueOptVec2": vec!["Test"],
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn test_derived_field_complex_object() {
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(SimpleObject)]
|
||||
#[graphql(complex)]
|
||||
struct MyObj {
|
||||
a: i32,
|
||||
#[graphql(owned, derived(name = "f", into = "ValueDerived"))]
|
||||
b: i32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct ValueDerived(String);
|
||||
|
||||
scalar!(ValueDerived);
|
||||
|
||||
impl From<i32> for ValueDerived {
|
||||
fn from(value: i32) -> Self {
|
||||
ValueDerived(format!("{}", value))
|
||||
}
|
||||
}
|
||||
|
||||
#[ComplexObject]
|
||||
impl MyObj {
|
||||
async fn c(&self) -> i32 {
|
||||
self.a + self.b
|
||||
}
|
||||
|
||||
#[graphql(derived(name = "e", into = "ValueDerived"))]
|
||||
async fn d(&self, v: i32) -> i32 {
|
||||
self.a + self.b + v
|
||||
}
|
||||
}
|
||||
|
||||
struct Query;
|
||||
|
||||
#[Object]
|
||||
impl Query {
|
||||
async fn obj(&self) -> MyObj {
|
||||
MyObj { a: 10, b: 20 }
|
||||
}
|
||||
}
|
||||
|
||||
let query = "{ obj { a b c d(v:100) e(v: 200) f } }";
|
||||
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
|
||||
dbg!(schema.execute(query).await);
|
||||
assert_eq!(
|
||||
schema.execute(query).await.data,
|
||||
value!({
|
||||
"obj": {
|
||||
"a": 10,
|
||||
"b": 20,
|
||||
"c": 30,
|
||||
"d": 130,
|
||||
"e": "230",
|
||||
"f": "20",
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn test_derived_field_complex_object_derived() {
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(SimpleObject)]
|
||||
#[graphql(complex)]
|
||||
struct MyObj {
|
||||
a: i32,
|
||||
#[graphql(owned, derived(name = "f", into = "ValueDerived"))]
|
||||
b: i32,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct ValueDerived(String);
|
||||
|
||||
scalar!(ValueDerived);
|
||||
|
||||
impl From<i32> for ValueDerived {
|
||||
fn from(value: i32) -> Self {
|
||||
ValueDerived(format!("{}", value))
|
||||
}
|
||||
}
|
||||
|
||||
fn option_to_option<T, U: From<T>>(value: Option<T>) -> Option<U> {
|
||||
value.map(|x| x.into())
|
||||
}
|
||||
|
||||
#[ComplexObject]
|
||||
impl MyObj {
|
||||
async fn c(&self) -> i32 {
|
||||
self.a + self.b
|
||||
}
|
||||
|
||||
#[graphql(derived(name = "e", into = "Option<ValueDerived>", with = "option_to_option"))]
|
||||
async fn d(&self, v: i32) -> Option<i32> {
|
||||
Some(self.a + self.b + v)
|
||||
}
|
||||
}
|
||||
|
||||
struct Query;
|
||||
|
||||
#[Object]
|
||||
impl Query {
|
||||
async fn obj(&self) -> MyObj {
|
||||
MyObj { a: 10, b: 20 }
|
||||
}
|
||||
}
|
||||
|
||||
let query = "{ obj { a b c d(v:100) e(v: 200) f } }";
|
||||
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
|
||||
assert_eq!(
|
||||
schema.execute(query).await.data,
|
||||
value!({
|
||||
"obj": {
|
||||
"a": 10,
|
||||
"b": 20,
|
||||
"c": 30,
|
||||
"d": 130,
|
||||
"e": "230",
|
||||
"f": "20",
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
|
@ -75,3 +75,112 @@ pub async fn test_error_extensions() {
|
|||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn test_failure() {
|
||||
#[derive(thiserror::Error, Debug, PartialEq)]
|
||||
enum MyError {
|
||||
#[error("error1")]
|
||||
Error1,
|
||||
|
||||
#[error("error2")]
|
||||
Error2,
|
||||
}
|
||||
|
||||
struct Query;
|
||||
|
||||
#[Object]
|
||||
impl Query {
|
||||
async fn failure(&self) -> Result<i32> {
|
||||
Err(ResolverError::new(MyError::Error1).into())
|
||||
}
|
||||
|
||||
async fn failure2(&self) -> Result<i32> {
|
||||
Err(ResolverError::new(MyError::Error2))?;
|
||||
Ok(1)
|
||||
}
|
||||
|
||||
async fn failure3(&self) -> Result<i32> {
|
||||
Err(ResolverError::new(MyError::Error1)
|
||||
.extend_with(|_, values| values.set("a", 1))
|
||||
.extend_with(|_, values| values.set("b", 2)))?;
|
||||
Ok(1)
|
||||
}
|
||||
|
||||
async fn failure4(&self) -> Result<i32> {
|
||||
Err(ResolverError::new(MyError::Error2))
|
||||
.extend_err(|_, values| values.set("a", 1))
|
||||
.extend_err(|_, values| values.set("b", 2))?;
|
||||
Ok(1)
|
||||
}
|
||||
}
|
||||
|
||||
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
|
||||
let err = schema
|
||||
.execute("{ failure }")
|
||||
.await
|
||||
.into_result()
|
||||
.unwrap_err()
|
||||
.remove(0);
|
||||
assert_eq!(err.source::<MyError>().unwrap(), &MyError::Error1);
|
||||
|
||||
let err = schema
|
||||
.execute("{ failure2 }")
|
||||
.await
|
||||
.into_result()
|
||||
.unwrap_err()
|
||||
.remove(0);
|
||||
assert_eq!(err.source::<MyError>().unwrap(), &MyError::Error2);
|
||||
|
||||
let err = schema
|
||||
.execute("{ failure3 }")
|
||||
.await
|
||||
.into_result()
|
||||
.unwrap_err()
|
||||
.remove(0);
|
||||
assert_eq!(err.source::<MyError>().unwrap(), &MyError::Error1);
|
||||
assert_eq!(
|
||||
err.extensions,
|
||||
Some({
|
||||
let mut values = ErrorExtensionValues::default();
|
||||
values.set("a", 1);
|
||||
values.set("b", 2);
|
||||
values
|
||||
})
|
||||
);
|
||||
|
||||
let err = schema
|
||||
.execute("{ failure4 }")
|
||||
.await
|
||||
.into_result()
|
||||
.unwrap_err()
|
||||
.remove(0);
|
||||
assert_eq!(err.source::<MyError>().unwrap(), &MyError::Error2);
|
||||
assert_eq!(
|
||||
err.extensions,
|
||||
Some({
|
||||
let mut values = ErrorExtensionValues::default();
|
||||
values.set("a", 1);
|
||||
values.set("b", 2);
|
||||
values
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn test_failure2() {
|
||||
#[derive(thiserror::Error, Debug, PartialEq)]
|
||||
enum MyError {
|
||||
#[error("error1")]
|
||||
Error1,
|
||||
}
|
||||
|
||||
struct Query;
|
||||
|
||||
#[Object]
|
||||
impl Query {
|
||||
async fn failure(&self) -> Result<i32, ResolverError> {
|
||||
Err(MyError::Error1)?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -242,6 +242,7 @@ pub async fn test_find_entity_with_context() {
|
|||
schema.execute(query).await.into_result().unwrap_err(),
|
||||
vec![ServerError {
|
||||
message: "Not found".to_string(),
|
||||
source: None,
|
||||
locations: vec![Pos {
|
||||
line: 2,
|
||||
column: 13
|
||||
|
|
|
@ -85,6 +85,7 @@ pub async fn test_field_features() {
|
|||
vec![ServerError {
|
||||
message: r#"Unknown field "valueAbc" on type "QueryRoot". Did you mean "value"?"#
|
||||
.to_owned(),
|
||||
source: None,
|
||||
locations: vec![Pos { column: 3, line: 1 }],
|
||||
path: Vec::new(),
|
||||
extensions: None,
|
||||
|
@ -113,6 +114,7 @@ pub async fn test_field_features() {
|
|||
vec![ServerError {
|
||||
message: r#"Unknown field "valueAbc" on type "MyObj". Did you mean "value"?"#
|
||||
.to_owned(),
|
||||
source: None,
|
||||
locations: vec![Pos { column: 9, line: 1 }],
|
||||
path: Vec::new(),
|
||||
extensions: None,
|
||||
|
@ -148,6 +150,7 @@ pub async fn test_field_features() {
|
|||
.errors,
|
||||
vec![ServerError {
|
||||
message: r#"Unknown field "valuesAbc" on type "SubscriptionRoot". Did you mean "values", "valuesBson"?"#.to_owned(),
|
||||
source: None,
|
||||
locations: vec![Pos {
|
||||
column: 16,
|
||||
line: 1
|
||||
|
|
|
@ -99,6 +99,7 @@ pub async fn test_guard_simple_rule() {
|
|||
.unwrap_err(),
|
||||
vec![ServerError {
|
||||
message: "Forbidden".to_string(),
|
||||
source: None,
|
||||
locations: vec![Pos { line: 1, column: 3 }],
|
||||
path: vec![PathSegment::Field("value".to_owned())],
|
||||
extensions: None,
|
||||
|
@ -127,6 +128,7 @@ pub async fn test_guard_simple_rule() {
|
|||
.errors,
|
||||
vec![ServerError {
|
||||
message: "Forbidden".to_string(),
|
||||
source: None,
|
||||
locations: vec![Pos {
|
||||
line: 1,
|
||||
column: 16
|
||||
|
@ -176,6 +178,7 @@ pub async fn test_guard_and_operator() {
|
|||
.unwrap_err(),
|
||||
vec![ServerError {
|
||||
message: "Forbidden".to_string(),
|
||||
source: None,
|
||||
locations: vec![Pos { line: 1, column: 3 }],
|
||||
path: vec![PathSegment::Field("value".to_owned())],
|
||||
extensions: None,
|
||||
|
@ -195,6 +198,7 @@ pub async fn test_guard_and_operator() {
|
|||
.unwrap_err(),
|
||||
vec![ServerError {
|
||||
message: "Forbidden".to_string(),
|
||||
source: None,
|
||||
locations: vec![Pos { line: 1, column: 3 }],
|
||||
path: vec![PathSegment::Field("value".to_owned())],
|
||||
extensions: None,
|
||||
|
@ -214,6 +218,7 @@ pub async fn test_guard_and_operator() {
|
|||
.unwrap_err(),
|
||||
vec![ServerError {
|
||||
message: "Forbidden".to_string(),
|
||||
source: None,
|
||||
locations: vec![Pos { line: 1, column: 3 }],
|
||||
path: vec![PathSegment::Field("value".to_owned())],
|
||||
extensions: None,
|
||||
|
@ -283,6 +288,7 @@ pub async fn test_guard_or_operator() {
|
|||
.unwrap_err(),
|
||||
vec![ServerError {
|
||||
message: "Forbidden".to_string(),
|
||||
source: None,
|
||||
locations: vec![Pos { line: 1, column: 3 }],
|
||||
path: vec![PathSegment::Field("value".to_owned())],
|
||||
extensions: None,
|
||||
|
@ -332,6 +338,7 @@ pub async fn test_guard_chain_operator() {
|
|||
.unwrap_err(),
|
||||
vec![ServerError {
|
||||
message: "Forbidden".to_string(),
|
||||
source: None,
|
||||
locations: vec![Pos { line: 1, column: 3 }],
|
||||
path: vec![PathSegment::Field("value".to_owned())],
|
||||
extensions: None,
|
||||
|
@ -352,6 +359,7 @@ pub async fn test_guard_chain_operator() {
|
|||
.unwrap_err(),
|
||||
vec![ServerError {
|
||||
message: "Forbidden".to_string(),
|
||||
source: None,
|
||||
locations: vec![Pos { line: 1, column: 3 }],
|
||||
path: vec![PathSegment::Field("value".to_owned())],
|
||||
extensions: None,
|
||||
|
@ -372,6 +380,7 @@ pub async fn test_guard_chain_operator() {
|
|||
.unwrap_err(),
|
||||
vec![ServerError {
|
||||
message: "Forbidden".to_string(),
|
||||
source: None,
|
||||
locations: vec![Pos { line: 1, column: 3 }],
|
||||
path: vec![PathSegment::Field("value".to_owned())],
|
||||
extensions: None,
|
||||
|
@ -392,6 +401,7 @@ pub async fn test_guard_chain_operator() {
|
|||
.unwrap_err(),
|
||||
vec![ServerError {
|
||||
message: "Forbidden".to_string(),
|
||||
source: None,
|
||||
locations: vec![Pos { line: 1, column: 3 }],
|
||||
path: vec![PathSegment::Field("value".to_owned())],
|
||||
extensions: None,
|
||||
|
@ -483,6 +493,7 @@ pub async fn test_guard_race_operator() {
|
|||
.unwrap_err(),
|
||||
vec![ServerError {
|
||||
message: "Forbidden".to_string(),
|
||||
source: None,
|
||||
locations: vec![Pos { line: 1, column: 3 }],
|
||||
path: vec![PathSegment::Field("value".to_owned())],
|
||||
extensions: None,
|
||||
|
@ -538,6 +549,7 @@ pub async fn test_guard_use_params() {
|
|||
.unwrap_err(),
|
||||
vec![ServerError {
|
||||
message: "Forbidden".to_string(),
|
||||
source: None,
|
||||
locations: vec![Pos { line: 1, column: 3 }],
|
||||
path: vec![PathSegment::Field("get".to_owned())],
|
||||
extensions: None,
|
||||
|
|
|
@ -67,6 +67,7 @@ pub async fn test_input_validator_string_min_length() {
|
|||
.expect_err(&should_fail_msg),
|
||||
vec![ServerError {
|
||||
message: field_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 17
|
||||
|
@ -84,6 +85,7 @@ pub async fn test_input_validator_string_min_length() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: object_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 14
|
||||
|
@ -177,6 +179,7 @@ pub async fn test_input_validator_string_max_length() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: field_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 17
|
||||
|
@ -194,6 +197,7 @@ pub async fn test_input_validator_string_max_length() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: object_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 14
|
||||
|
@ -286,6 +290,7 @@ pub async fn test_input_validator_chars_min_length() {
|
|||
.expect_err(&should_fail_msg),
|
||||
vec![ServerError {
|
||||
message: field_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 17
|
||||
|
@ -303,6 +308,7 @@ pub async fn test_input_validator_chars_min_length() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: object_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 14
|
||||
|
@ -397,6 +403,7 @@ pub async fn test_input_validator_chars_max_length() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: field_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 17
|
||||
|
@ -414,6 +421,7 @@ pub async fn test_input_validator_chars_max_length() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: object_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 14
|
||||
|
@ -534,6 +542,7 @@ pub async fn test_input_validator_string_email() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: field_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 17
|
||||
|
@ -552,6 +561,7 @@ pub async fn test_input_validator_string_email() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: object_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 14
|
||||
|
@ -682,6 +692,7 @@ pub async fn test_input_validator_string_mac() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: field_error_msg.clone(),
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 17
|
||||
|
@ -700,6 +711,7 @@ pub async fn test_input_validator_string_mac() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: object_error_msg.clone(),
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 14
|
||||
|
@ -717,6 +729,7 @@ pub async fn test_input_validator_string_mac() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: field_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 17
|
||||
|
@ -735,6 +748,7 @@ pub async fn test_input_validator_string_mac() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: object_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 14
|
||||
|
@ -792,6 +806,7 @@ pub async fn test_input_validator_string_mac() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: field_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 17
|
||||
|
@ -810,6 +825,7 @@ pub async fn test_input_validator_string_mac() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: object_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 14
|
||||
|
@ -851,6 +867,7 @@ pub async fn test_input_validator_string_mac() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: field_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 17
|
||||
|
@ -869,6 +886,7 @@ pub async fn test_input_validator_string_mac() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: object_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 14
|
||||
|
@ -929,6 +947,7 @@ pub async fn test_input_validator_int_range() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: field_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 17
|
||||
|
@ -946,6 +965,7 @@ pub async fn test_input_validator_int_range() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: object_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 14
|
||||
|
@ -1034,6 +1054,7 @@ pub async fn test_input_validator_int_less_than() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: field_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 17
|
||||
|
@ -1051,6 +1072,7 @@ pub async fn test_input_validator_int_less_than() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: object_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 14
|
||||
|
@ -1141,6 +1163,7 @@ pub async fn test_input_validator_int_greater_than() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: field_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 17
|
||||
|
@ -1158,6 +1181,7 @@ pub async fn test_input_validator_int_greater_than() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: object_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 14
|
||||
|
@ -1241,6 +1265,7 @@ pub async fn test_input_validator_int_nonzero() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: field_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 17
|
||||
|
@ -1258,6 +1283,7 @@ pub async fn test_input_validator_int_nonzero() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: object_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 14
|
||||
|
@ -1345,6 +1371,7 @@ pub async fn test_input_validator_int_equal() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: field_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 17
|
||||
|
@ -1362,6 +1389,7 @@ pub async fn test_input_validator_int_equal() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: object_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 14
|
||||
|
@ -1461,6 +1489,7 @@ pub async fn test_input_validator_list_max_length() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: field_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 17
|
||||
|
@ -1478,6 +1507,7 @@ pub async fn test_input_validator_list_max_length() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: object_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 14
|
||||
|
@ -1577,6 +1607,7 @@ pub async fn test_input_validator_list_min_length() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: field_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 17
|
||||
|
@ -1594,6 +1625,7 @@ pub async fn test_input_validator_list_min_length() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: object_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 14
|
||||
|
@ -1701,6 +1733,7 @@ pub async fn test_input_validator_operator_or() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: field_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 17
|
||||
|
@ -1718,6 +1751,7 @@ pub async fn test_input_validator_operator_or() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: object_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 14
|
||||
|
@ -1818,6 +1852,7 @@ pub async fn test_input_validator_operator_and() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: field_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 17
|
||||
|
@ -1835,6 +1870,7 @@ pub async fn test_input_validator_operator_and() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: object_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 14
|
||||
|
@ -1938,6 +1974,7 @@ pub async fn test_input_validator_variable() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: field_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 37
|
||||
|
@ -1955,6 +1992,7 @@ pub async fn test_input_validator_variable() {
|
|||
.expect_err(&should_fail_msg[..]),
|
||||
vec![ServerError {
|
||||
message: object_error_msg,
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 34
|
||||
|
@ -2041,6 +2079,7 @@ pub async fn test_custom_input_validator_with_extensions() {
|
|||
.expect_err(should_fail_msg),
|
||||
vec![ServerError {
|
||||
message: field_error_msg.into(),
|
||||
source: None,
|
||||
locations: vec!(Pos {
|
||||
line: 1,
|
||||
column: 17
|
||||
|
|
|
@ -18,6 +18,7 @@ pub async fn test_input_value_custom_error() {
|
|||
vec![ServerError {
|
||||
message: "Failed to parse \"Int\": Only integers from -128 to 127 are accepted."
|
||||
.to_owned(),
|
||||
source: None,
|
||||
locations: vec![Pos {
|
||||
line: 1,
|
||||
column: 14,
|
||||
|
|
|
@ -167,6 +167,7 @@ pub async fn test_array_type() {
|
|||
vec![ServerError {
|
||||
message: r#"Failed to parse "[Int!]": Expected input type "[Int; 6]", found [Int; 5]."#
|
||||
.to_owned(),
|
||||
source: None,
|
||||
locations: vec![Pos {
|
||||
line: 1,
|
||||
column: 22,
|
||||
|
|
60
tests/preserve_order.rs
Normal file
60
tests/preserve_order.rs
Normal file
|
@ -0,0 +1,60 @@
|
|||
use async_graphql::*;
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn test_preserve_order() {
|
||||
#[derive(SimpleObject)]
|
||||
struct Root {
|
||||
a: i32,
|
||||
b: i32,
|
||||
c: i32,
|
||||
}
|
||||
|
||||
let schema = Schema::new(Root { a: 1, b: 2, c: 3 }, EmptyMutation, EmptySubscription);
|
||||
assert_eq!(
|
||||
schema
|
||||
.execute("{ a c b }")
|
||||
.await
|
||||
.into_result()
|
||||
.unwrap()
|
||||
.data,
|
||||
value!({
|
||||
"a": 1, "c": 3, "b": 2
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_string(
|
||||
&schema
|
||||
.execute("{ a c b }")
|
||||
.await
|
||||
.into_result()
|
||||
.unwrap()
|
||||
.data
|
||||
)
|
||||
.unwrap(),
|
||||
r#"{"a":1,"c":3,"b":2}"#
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
schema
|
||||
.execute("{ c b a }")
|
||||
.await
|
||||
.into_result()
|
||||
.unwrap()
|
||||
.data,
|
||||
value!({
|
||||
"c": 3, "b": 2, "a": 1
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_string(
|
||||
&schema
|
||||
.execute("{ c b a }")
|
||||
.await
|
||||
.into_result()
|
||||
.unwrap()
|
||||
.data
|
||||
)
|
||||
.unwrap(),
|
||||
r#"{"c":3,"b":2,"a":1}"#
|
||||
);
|
||||
}
|
|
@ -34,12 +34,14 @@ pub async fn test_fieldresult() {
|
|||
errors: vec![
|
||||
ServerError {
|
||||
message: "TestError".to_string(),
|
||||
source: None,
|
||||
locations: vec![Pos { line: 1, column: 3 }],
|
||||
path: vec![PathSegment::Field("error1".to_owned())],
|
||||
extensions: None,
|
||||
},
|
||||
ServerError {
|
||||
message: "TestError".to_string(),
|
||||
source: None,
|
||||
locations: vec![Pos {
|
||||
line: 1,
|
||||
column: 19,
|
||||
|
@ -60,6 +62,7 @@ pub async fn test_fieldresult() {
|
|||
.unwrap_err(),
|
||||
vec![ServerError {
|
||||
message: "TestError".to_string(),
|
||||
source: None,
|
||||
locations: vec![Pos { line: 1, column: 3 }],
|
||||
path: vec![PathSegment::Field("optError".to_owned())],
|
||||
extensions: None,
|
||||
|
@ -74,6 +77,7 @@ pub async fn test_fieldresult() {
|
|||
.unwrap_err(),
|
||||
vec![ServerError {
|
||||
message: "TestError".to_string(),
|
||||
source: None,
|
||||
locations: vec![Pos { line: 1, column: 3 }],
|
||||
path: vec![
|
||||
PathSegment::Field("vecError".to_owned()),
|
||||
|
@ -188,6 +192,7 @@ pub async fn test_error_propagation() {
|
|||
cache_control: Default::default(),
|
||||
errors: vec![ServerError {
|
||||
message: "myerror".to_string(),
|
||||
source: None,
|
||||
locations: vec![Pos {
|
||||
line: 1,
|
||||
column: 20,
|
||||
|
@ -215,6 +220,7 @@ pub async fn test_error_propagation() {
|
|||
cache_control: Default::default(),
|
||||
errors: vec![ServerError {
|
||||
message: "myerror".to_string(),
|
||||
source: None,
|
||||
locations: vec![Pos {
|
||||
line: 1,
|
||||
column: 23,
|
||||
|
@ -238,6 +244,7 @@ pub async fn test_error_propagation() {
|
|||
cache_control: Default::default(),
|
||||
errors: vec![ServerError {
|
||||
message: "myerror".to_string(),
|
||||
source: None,
|
||||
locations: vec![Pos {
|
||||
line: 1,
|
||||
column: 23,
|
||||
|
@ -267,6 +274,7 @@ pub async fn test_error_propagation() {
|
|||
cache_control: Default::default(),
|
||||
errors: vec![ServerError {
|
||||
message: "myerror".to_string(),
|
||||
source: None,
|
||||
locations: vec![Pos {
|
||||
line: 1,
|
||||
column: 23,
|
||||
|
|
|
@ -11,7 +11,12 @@ mod test_mod {
|
|||
|
||||
#[tokio::test]
|
||||
pub async fn test_scalar_macro() {
|
||||
scalar!(test_mod::MyValue, "MV", "DESC");
|
||||
scalar!(
|
||||
test_mod::MyValue,
|
||||
"MV",
|
||||
"DESC",
|
||||
"https://tools.ietf.org/html/rfc4122"
|
||||
);
|
||||
|
||||
struct Query;
|
||||
|
||||
|
@ -26,7 +31,7 @@ pub async fn test_scalar_macro() {
|
|||
let schema = Schema::new(Query, EmptyMutation, EmptySubscription);
|
||||
assert_eq!(
|
||||
schema
|
||||
.execute(r#"{ __type(name:"MV") { name description } }"#)
|
||||
.execute(r#"{ __type(name:"MV") { name description specifiedByURL } }"#)
|
||||
.await
|
||||
.into_result()
|
||||
.unwrap()
|
||||
|
@ -35,6 +40,7 @@ pub async fn test_scalar_macro() {
|
|||
"__type": {
|
||||
"name": "MV",
|
||||
"description": "DESC",
|
||||
"specifiedByURL": "https://tools.ietf.org/html/rfc4122",
|
||||
}
|
||||
})
|
||||
);
|
||||
|
|
|
@ -345,6 +345,7 @@ pub async fn test_subscription_error() {
|
|||
stream.next().await,
|
||||
Some(Err(vec![ServerError {
|
||||
message: "TestError".to_string(),
|
||||
source: None,
|
||||
locations: vec![Pos {
|
||||
line: 1,
|
||||
column: 25
|
||||
|
@ -390,6 +391,7 @@ pub async fn test_subscription_fieldresult() {
|
|||
cache_control: Default::default(),
|
||||
errors: vec![ServerError {
|
||||
message: "StreamErr".to_string(),
|
||||
source: None,
|
||||
locations: vec![Pos {
|
||||
line: 1,
|
||||
column: 16
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
[package]
|
||||
name = "async-graphql-value"
|
||||
version = "2.10.4"
|
||||
version = "2.11.0"
|
||||
authors = ["sunli <scott_s829@163.com>", "Koxiaet"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
description = "GraphQL value for async-graphql"
|
||||
license = "MIT/Apache-2.0"
|
||||
documentation = "https://docs.rs/async-graphql/"
|
||||
|
@ -15,3 +15,4 @@ categories = ["network-programming", "asynchronous"]
|
|||
serde_json = "1.0.64"
|
||||
serde = { version = "1.0.125", features = ["derive"] }
|
||||
bytes = { version = "1.0.1", features = ["serde"] }
|
||||
indexmap = { version = "1.7.0", features = ["serde"] }
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::{fmt, vec};
|
||||
|
||||
use indexmap::IndexMap;
|
||||
|
||||
use crate::{ConstValue, Name};
|
||||
|
||||
use serde::de::{
|
||||
|
@ -78,7 +79,7 @@ where
|
|||
}
|
||||
|
||||
fn visit_object<'de, V>(
|
||||
object: BTreeMap<Name, ConstValue>,
|
||||
object: IndexMap<Name, ConstValue>,
|
||||
visitor: V,
|
||||
) -> Result<V::Value, DeserializerError>
|
||||
where
|
||||
|
@ -365,13 +366,13 @@ impl<'de> SeqAccess<'de> for SeqDeserializer {
|
|||
}
|
||||
|
||||
struct MapDeserializer {
|
||||
iter: <BTreeMap<Name, ConstValue> as IntoIterator>::IntoIter,
|
||||
iter: <IndexMap<Name, ConstValue> as IntoIterator>::IntoIter,
|
||||
value: Option<ConstValue>,
|
||||
}
|
||||
|
||||
impl MapDeserializer {
|
||||
#[inline]
|
||||
fn new(map: BTreeMap<Name, ConstValue>) -> Self {
|
||||
fn new(map: IndexMap<Name, ConstValue>) -> Self {
|
||||
MapDeserializer {
|
||||
iter: map.into_iter(),
|
||||
value: None,
|
||||
|
|
|
@ -10,17 +10,17 @@ mod value_serde;
|
|||
mod variables;
|
||||
|
||||
use std::borrow::{Borrow, Cow};
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::fmt::{self, Display, Formatter, Write};
|
||||
use std::iter::FromIterator;
|
||||
use std::ops::Deref;
|
||||
use std::sync::Arc;
|
||||
|
||||
use bytes::Bytes;
|
||||
use indexmap::IndexMap;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
|
||||
pub use deserializer::{from_value, DeserializerError};
|
||||
#[doc(hidden)]
|
||||
pub use indexmap;
|
||||
pub use serde_json::Number;
|
||||
pub use serializer::{to_value, SerializerError};
|
||||
|
||||
|
@ -139,7 +139,7 @@ pub enum ConstValue {
|
|||
/// A list of values.
|
||||
List(Vec<ConstValue>),
|
||||
/// An object. This is a map of keys to values.
|
||||
Object(BTreeMap<Name, ConstValue>),
|
||||
Object(IndexMap<Name, ConstValue>),
|
||||
}
|
||||
|
||||
impl PartialEq for ConstValue {
|
||||
|
@ -254,8 +254,8 @@ impl<T: Into<ConstValue>> From<Vec<T>> for ConstValue {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<BTreeMap<Name, ConstValue>> for ConstValue {
|
||||
fn from(f: BTreeMap<Name, ConstValue>) -> Self {
|
||||
impl From<IndexMap<Name, ConstValue>> for ConstValue {
|
||||
fn from(f: IndexMap<Name, ConstValue>) -> Self {
|
||||
ConstValue::Object(f)
|
||||
}
|
||||
}
|
||||
|
@ -364,7 +364,7 @@ pub enum Value {
|
|||
/// A list of values.
|
||||
List(Vec<Value>),
|
||||
/// An object. This is a map of keys to values.
|
||||
Object(BTreeMap<Name, Value>),
|
||||
Object(IndexMap<Name, Value>),
|
||||
}
|
||||
|
||||
impl Value {
|
||||
|
|
|
@ -191,7 +191,7 @@ macro_rules! value_internal {
|
|||
|
||||
({ $($tt:tt)+ }) => {
|
||||
$crate::ConstValue::Object({
|
||||
let mut object = std::collections::BTreeMap::new();
|
||||
let mut object = $crate::indexmap::IndexMap::new();
|
||||
$crate::value_internal!(@object object () ($($tt)+) ($($tt)+));
|
||||
object
|
||||
})
|
||||
|
@ -227,7 +227,7 @@ macro_rules! value_expect_expr_comma {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{ConstValue, Name};
|
||||
use std::collections::BTreeMap;
|
||||
use indexmap::IndexMap;
|
||||
|
||||
#[test]
|
||||
fn test_macro() {
|
||||
|
@ -254,7 +254,7 @@ mod tests {
|
|||
)
|
||||
);
|
||||
assert_eq!(value!({"a": 10, "b": true}), {
|
||||
let mut map = BTreeMap::new();
|
||||
let mut map = IndexMap::new();
|
||||
map.insert(Name::new("a"), ConstValue::Number(10.into()));
|
||||
map.insert(Name::new("b"), ConstValue::Boolean(true));
|
||||
ConstValue::Object(map)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::error::Error;
|
||||
use std::fmt;
|
||||
|
||||
use indexmap::IndexMap;
|
||||
use serde::ser::{self, Impossible};
|
||||
use serde::Serialize;
|
||||
|
||||
|
@ -180,7 +180,7 @@ impl ser::Serializer for Serializer {
|
|||
T: ser::Serialize,
|
||||
{
|
||||
value.serialize(self).map(|v| {
|
||||
let mut map = BTreeMap::new();
|
||||
let mut map = IndexMap::new();
|
||||
map.insert(Name::new(variant), v);
|
||||
ConstValue::Object(map)
|
||||
})
|
||||
|
@ -222,7 +222,7 @@ impl ser::Serializer for Serializer {
|
|||
#[inline]
|
||||
fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
|
||||
Ok(SerializeMap {
|
||||
map: BTreeMap::new(),
|
||||
map: IndexMap::new(),
|
||||
key: None,
|
||||
})
|
||||
}
|
||||
|
@ -233,7 +233,7 @@ impl ser::Serializer for Serializer {
|
|||
_name: &'static str,
|
||||
_len: usize,
|
||||
) -> Result<Self::SerializeStruct, Self::Error> {
|
||||
Ok(SerializeStruct(BTreeMap::new()))
|
||||
Ok(SerializeStruct(IndexMap::new()))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
@ -244,7 +244,7 @@ impl ser::Serializer for Serializer {
|
|||
variant: &'static str,
|
||||
_len: usize,
|
||||
) -> Result<Self::SerializeStructVariant, Self::Error> {
|
||||
Ok(SerializeStructVariant(Name::new(variant), BTreeMap::new()))
|
||||
Ok(SerializeStructVariant(Name::new(variant), IndexMap::new()))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
@ -337,14 +337,14 @@ impl ser::SerializeTupleVariant for SerializeTupleVariant {
|
|||
|
||||
#[inline]
|
||||
fn end(self) -> Result<Self::Ok, Self::Error> {
|
||||
let mut map = BTreeMap::new();
|
||||
let mut map = IndexMap::new();
|
||||
map.insert(self.0, ConstValue::List(self.1));
|
||||
Ok(ConstValue::Object(map))
|
||||
}
|
||||
}
|
||||
|
||||
struct SerializeMap {
|
||||
map: BTreeMap<Name, ConstValue>,
|
||||
map: IndexMap<Name, ConstValue>,
|
||||
key: Option<Name>,
|
||||
}
|
||||
|
||||
|
@ -378,7 +378,7 @@ impl ser::SerializeMap for SerializeMap {
|
|||
}
|
||||
}
|
||||
|
||||
struct SerializeStruct(BTreeMap<Name, ConstValue>);
|
||||
struct SerializeStruct(IndexMap<Name, ConstValue>);
|
||||
|
||||
impl ser::SerializeStruct for SerializeStruct {
|
||||
type Ok = ConstValue;
|
||||
|
@ -405,7 +405,7 @@ impl ser::SerializeStruct for SerializeStruct {
|
|||
}
|
||||
}
|
||||
|
||||
struct SerializeStructVariant(Name, BTreeMap<Name, ConstValue>);
|
||||
struct SerializeStructVariant(Name, IndexMap<Name, ConstValue>);
|
||||
|
||||
impl ser::SerializeStructVariant for SerializeStructVariant {
|
||||
type Ok = ConstValue;
|
||||
|
@ -428,7 +428,7 @@ impl ser::SerializeStructVariant for SerializeStructVariant {
|
|||
|
||||
#[inline]
|
||||
fn end(self) -> Result<Self::Ok, Self::Error> {
|
||||
let mut map = BTreeMap::new();
|
||||
let mut map = IndexMap::new();
|
||||
map.insert(self.0, ConstValue::Object(self.1));
|
||||
Ok(ConstValue::Object(map))
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user