Some improvements.

This commit is contained in:
Sunli 2020-10-10 10:32:43 +08:00
parent 7fe59d9f90
commit 87ba51fdd0
57 changed files with 532 additions and 523 deletions

View File

@ -150,8 +150,8 @@ pub fn generate(enum_args: &args::Enum) -> GeneratorResult<TokenStream> {
#[#crate_name::async_trait::async_trait]
impl #crate_name::OutputValueType for #ident {
async fn resolve(&self, _: &#crate_name::ContextSelectionSet<'_>, _field: &#crate_name::Positioned<#crate_name::parser::types::Field>) -> #crate_name::ServerResult<#crate_name::serde_json::Value> {
Ok(#crate_name::resolver_utils::enum_value(*self).into_json().unwrap())
async fn resolve(&self, _: &#crate_name::ContextSelectionSet<'_>, _field: &#crate_name::Positioned<#crate_name::parser::types::Field>) -> #crate_name::ServerResult<#crate_name::Value> {
Ok(#crate_name::resolver_utils::enum_value(*self))
}
}

View File

@ -125,7 +125,7 @@ pub fn generate(object_args: &args::InputObject) -> GeneratorResult<TokenStream>
put_fields.push(quote! {
map.insert(
#crate_name::parser::types::Name::new_unchecked(#name.to_owned()),
#crate_name::parser::types::Name::new(#name),
#crate_name::InputValueType::to_value(&self.#ident)
);
});

View File

@ -312,7 +312,7 @@ pub fn generate(interface_args: &args::Interface) -> GeneratorResult<TokenStream
#[allow(clippy::all, clippy::pedantic)]
#[#crate_name::async_trait::async_trait]
impl #generics #crate_name::resolver_utils::ContainerType for #ident #generics {
async fn resolve_field(&self, ctx: &#crate_name::Context<'_>) -> #crate_name::ServerResult<Option<#crate_name::serde_json::Value>> {
async fn resolve_field(&self, ctx: &#crate_name::Context<'_>) -> #crate_name::ServerResult<Option<#crate_name::Value>> {
#(#resolvers)*
Ok(None)
}
@ -327,7 +327,7 @@ pub fn generate(interface_args: &args::Interface) -> GeneratorResult<TokenStream
#[allow(clippy::all, clippy::pedantic)]
#[#crate_name::async_trait::async_trait]
impl #generics #crate_name::OutputValueType for #ident #generics {
async fn resolve(&self, ctx: &#crate_name::ContextSelectionSet<'_>, _field: &#crate_name::Positioned<#crate_name::parser::types::Field>) -> #crate_name::ServerResult<#crate_name::serde_json::Value> {
async fn resolve(&self, ctx: &#crate_name::ContextSelectionSet<'_>, _field: &#crate_name::Positioned<#crate_name::parser::types::Field>) -> #crate_name::ServerResult<#crate_name::Value> {
#crate_name::resolver_utils::resolve_container(ctx, self).await
}
}

View File

@ -90,7 +90,7 @@ pub fn generate(object_args: &args::MergedObject) -> GeneratorResult<TokenStream
#[allow(clippy::all, clippy::pedantic)]
#[#crate_name::async_trait::async_trait]
impl #crate_name::resolver_utils::ContainerType for #ident {
async fn resolve_field(&self, ctx: &#crate_name::Context<'_>) -> #crate_name::ServerResult<::std::option::Option<#crate_name::serde_json::Value>> {
async fn resolve_field(&self, ctx: &#crate_name::Context<'_>) -> #crate_name::ServerResult<::std::option::Option<#crate_name::Value>> {
#create_merged_obj.resolve_field(ctx).await
}
}
@ -98,7 +98,7 @@ pub fn generate(object_args: &args::MergedObject) -> GeneratorResult<TokenStream
#[allow(clippy::all, clippy::pedantic)]
#[#crate_name::async_trait::async_trait]
impl #crate_name::OutputValueType for #ident {
async fn resolve(&self, ctx: &#crate_name::ContextSelectionSet<'_>, _field: &#crate_name::Positioned<#crate_name::parser::types::Field>) -> #crate_name::ServerResult<#crate_name::serde_json::Value> {
async fn resolve(&self, ctx: &#crate_name::ContextSelectionSet<'_>, _field: &#crate_name::Positioned<#crate_name::parser::types::Field>) -> #crate_name::ServerResult<#crate_name::Value> {
#crate_name::resolver_utils::resolve_container(ctx, self).await
}
}

View File

@ -80,7 +80,7 @@ pub fn generate(object_args: &args::MergedSubscription) -> GeneratorResult<Token
fn create_field_stream<'a>(
&'a self,
ctx: &'a #crate_name::Context<'a>
) -> Option<::std::pin::Pin<::std::boxed::Box<dyn #crate_name::futures::Stream<Item = #crate_name::ServerResult<#crate_name::serde_json::Value>> + ::std::marker::Send + 'a>>> {
) -> Option<::std::pin::Pin<::std::boxed::Box<dyn #crate_name::futures::Stream<Item = #crate_name::ServerResult<#crate_name::Value>> + ::std::marker::Send + 'a>>> {
None #create_field_stream
}
}

View File

@ -490,12 +490,12 @@ pub fn generate(
#[allow(unused_braces, unused_variables, unused_parens, unused_mut)]
#[#crate_name::async_trait::async_trait]
impl#generics #crate_name::resolver_utils::ContainerType for #self_ty #where_clause {
async fn resolve_field(&self, ctx: &#crate_name::Context<'_>) -> #crate_name::ServerResult<::std::option::Option<#crate_name::serde_json::Value>> {
async fn resolve_field(&self, ctx: &#crate_name::Context<'_>) -> #crate_name::ServerResult<::std::option::Option<#crate_name::Value>> {
#(#resolvers)*
Ok(None)
}
async fn find_entity(&self, ctx: &#crate_name::Context<'_>, params: &#crate_name::Value) -> #crate_name::ServerResult<::std::option::Option<#crate_name::serde_json::Value>> {
async fn find_entity(&self, ctx: &#crate_name::Context<'_>, params: &#crate_name::Value) -> #crate_name::ServerResult<::std::option::Option<#crate_name::Value>> {
let params = match params {
#crate_name::Value::Object(params) => params,
_ => return Ok(None),
@ -516,7 +516,7 @@ pub fn generate(
#[allow(clippy::all, clippy::pedantic)]
#[#crate_name::async_trait::async_trait]
impl #generics #crate_name::OutputValueType for #self_ty #where_clause {
async fn resolve(&self, ctx: &#crate_name::ContextSelectionSet<'_>, _field: &#crate_name::Positioned<#crate_name::parser::types::Field>) -> #crate_name::ServerResult<#crate_name::serde_json::Value> {
async fn resolve(&self, ctx: &#crate_name::ContextSelectionSet<'_>, _field: &#crate_name::Positioned<#crate_name::parser::types::Field>) -> #crate_name::ServerResult<#crate_name::Value> {
#crate_name::resolver_utils::resolve_container(ctx, self).await
}
}

View File

@ -64,8 +64,8 @@ pub fn generate(
&self,
_: &#crate_name::ContextSelectionSet<'_>,
_field: &#crate_name::Positioned<#crate_name::parser::types::Field>
) -> #crate_name::ServerResult<#crate_name::serde_json::Value> {
Ok(#crate_name::ScalarType::to_value(self).into_json().unwrap())
) -> #crate_name::ServerResult<#crate_name::Value> {
Ok(#crate_name::ScalarType::to_value(self))
}
}
};

View File

@ -171,7 +171,7 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult<TokenStream
#[#crate_name::async_trait::async_trait]
impl #generics #crate_name::resolver_utils::ContainerType for #ident #generics #where_clause {
async fn resolve_field(&self, ctx: &#crate_name::Context<'_>) -> #crate_name::ServerResult<::std::option::Option<#crate_name::serde_json::Value>> {
async fn resolve_field(&self, ctx: &#crate_name::Context<'_>) -> #crate_name::ServerResult<::std::option::Option<#crate_name::Value>> {
#(#resolvers)*
Ok(None)
}
@ -181,7 +181,7 @@ pub fn generate(object_args: &args::SimpleObject) -> GeneratorResult<TokenStream
#[#crate_name::async_trait::async_trait]
impl #generics #crate_name::OutputValueType for #ident #generics #where_clause {
async fn resolve(&self, ctx: &#crate_name::ContextSelectionSet<'_>, _field: &#crate_name::Positioned<#crate_name::parser::types::Field>) -> #crate_name::ServerResult<#crate_name::serde_json::Value> {
async fn resolve(&self, ctx: &#crate_name::ContextSelectionSet<'_>, _field: &#crate_name::Positioned<#crate_name::parser::types::Field>) -> #crate_name::ServerResult<#crate_name::Value> {
#crate_name::resolver_utils::resolve_container(ctx, self).await
}
}

View File

@ -381,7 +381,7 @@ pub fn generate(
fn create_field_stream<'a>(
&'a self,
ctx: &'a #crate_name::Context<'a>,
) -> ::std::option::Option<::std::pin::Pin<::std::boxed::Box<dyn #crate_name::futures::Stream<Item = #crate_name::ServerResult<#crate_name::serde_json::Value>> + Send + 'a>>> {
) -> ::std::option::Option<::std::pin::Pin<::std::boxed::Box<dyn #crate_name::futures::Stream<Item = #crate_name::ServerResult<#crate_name::Value>> + Send + 'a>>> {
#(#create_stream)*
None
}

View File

@ -160,7 +160,7 @@ pub fn generate(union_args: &args::Union) -> GeneratorResult<TokenStream> {
#[#crate_name::async_trait::async_trait]
impl #generics #crate_name::resolver_utils::ContainerType for #ident #generics {
async fn resolve_field(&self, ctx: &#crate_name::Context<'_>) -> #crate_name::ServerResult<::std::option::Option<#crate_name::serde_json::Value>> {
async fn resolve_field(&self, ctx: &#crate_name::Context<'_>) -> #crate_name::ServerResult<::std::option::Option<#crate_name::Value>> {
Ok(None)
}
@ -174,7 +174,7 @@ pub fn generate(union_args: &args::Union) -> GeneratorResult<TokenStream> {
#[allow(clippy::all, clippy::pedantic)]
#[#crate_name::async_trait::async_trait]
impl #generics #crate_name::OutputValueType for #ident #generics {
async fn resolve(&self, ctx: &#crate_name::ContextSelectionSet<'_>, _field: &#crate_name::Positioned<#crate_name::parser::types::Field>) -> #crate_name::ServerResult<#crate_name::serde_json::Value> {
async fn resolve(&self, ctx: &#crate_name::ContextSelectionSet<'_>, _field: &#crate_name::Positioned<#crate_name::parser::types::Field>) -> #crate_name::ServerResult<#crate_name::Value> {
#crate_name::resolver_utils::resolve_container(ctx, self).await
}
}

View File

@ -10,16 +10,14 @@ I would recommend on checking out this [async-graphql example](https://github.co
## General Concept
In `async-graphql` all user-facing errors are cast to the `Error` type which by default provides
the error message exposed by `std::fmt::Display`. However `Error` also provides an additional
field `Option<serde_json::Value>` which - if given some valid `serde_json::Map` - will be exposed as the extensions key to any error.
the error message exposed by `std::fmt::Display`. However, `Error` actually provides an additional information that can extend the error.
A resolver looks like this:
```rust
async fn parse_with_extensions(&self) -> Result<i32> {
let my_extension = json!({ "details": "CAN_NOT_FETCH" });
Err(Error::new("MyMessage").extend_with(|| my_extension))
}
async fn parse_with_extensions(&self) -> Result<i32, Error> {
Err(Error::new("MyMessage").extend_with(|_, e| e.set("details", "CAN_NOT_FETCH")))
}
```
may then return a response like this:
@ -53,8 +51,8 @@ use std::num::ParseIntError;
async fn parse_with_extensions(&self) -> Result<i32> {
Ok("234a"
.parse()
.map_err(|err: ParseIntError| err.extend_with(|_err| json!({"code": 404})))?)
}
.map_err(|err: ParseIntError| err.extend_with(|_err, e| e.set("code", 404)))?)
}
```
### Implementing ErrorExtensions for custom errors.
@ -62,7 +60,8 @@ If you find yourself attaching extensions to your errors all over the place you
implementing the trait on your custom error type directly.
```rust
use thiserror::Error;
#[macro_use]
extern crate thiserror;
#[derive(Debug, Error)]
pub enum MyError {
@ -79,13 +78,11 @@ pub enum MyError {
impl ErrorExtensions for MyError {
// lets define our base extensions
fn extend(&self) -> Error {
Error::new(format!("{}", self)).extend_with(|err|
Error::new(format!("{}", self)).extend_with(|err, e|
match self {
MyError::NotFound => json!({"code": "NOT_FOUND"}),
MyError::ServerError(reason) => json!({ "reason": reason }),
MyError::ErrorWithoutExtensions => {
json!("This will be ignored since it does not represent an object.")
}
MyError::NotFound => e.set("code", "NOT_FOUND"),
MyError::ServerError(reason) => e.set("reason", reason),
MyError::ErrorWithoutExtensions => {}
})
}
}
@ -98,7 +95,7 @@ Or further extend your error through `extend_with`.
async fn parse_with_extensions_result(&self) -> Result<i32> {
// Err(MyError::NotFound.extend())
// OR
Err(MyError::NotFound.extend_with(|_| json!({ "on_the_fly": "some_more_info" })))
Err(MyError::NotFound.extend_with(|_, e| e.set("on_the_fly", "some_more_info")))
}
```
@ -126,9 +123,8 @@ use async_graphql::*;
async fn parse_with_extensions(&self) -> Result<i32> {
Ok("234a"
.parse()
.extend_err(|_| json!({"code": 404}))?)
}
.extend_err(|_, e| e.set("code", 404))?)
}
```
### Chained extensions
Since `ErrorExtensions` and `ResultExt` are implemented for any type `&E where E: std::fmt::Display`
@ -141,10 +137,10 @@ async fn parse_with_extensions(&self) -> Result<i32> {
match "234a".parse() {
Ok(n) => Ok(n),
Err(e) => Err(e
.extend_with(|_| json!({"code": 404}))
.extend_with(|_| json!({"details": "some more info.."}))
.extend_with(|_, e| e.set("code", 404))
.extend_with(|_, e| e.set("details", "some more info.."))
// keys may also overwrite previous keys...
.extend_with(|_| json!({"code": 500}))),
.extend_with(|_, e| e.set("code", 500))),
}
}
```
@ -177,7 +173,7 @@ The disadvantage is that the below code does **NOT** compile:
async fn parse_with_extensions_result(&self) -> Result<i32> {
// the trait `error::ErrorExtensions` is not implemented
// for `std::num::ParseIntError`
"234a".parse().extend_err(|_| json!({"code": 404}))
"234a".parse().extend_err(|_, e| e.set("code", 404))
}
```
@ -188,7 +184,7 @@ async fn parse_with_extensions_result(&self) -> Result<i32> {
// does work because ErrorExtensions is implemented for &ParseIntError
"234a"
.parse()
.map_err(|ref e: ParseIntError| e.extend_with(|_| json!({"code": 404})))
.map_err(|ref e: ParseIntError| e.extend_with(|_, e| e.set("code", 404)))
}
```

View File

@ -12,16 +12,14 @@
## 一般概念
在`Async-graphql`中,所有面向用户的错误都强制转换为`Error`类型,默认情况下会提供
由`std:::fmt::Display`暴露的错误消息。但是,`Error`实际上提供了一个额外的
字段`Option<serde_json::Value>`,如果它是一个`serde_json::Map`,则可以扩展错误的信息。
由`std:::fmt::Display`暴露的错误消息。但是,`Error`实际上提供了一个额外的可以扩展错误的信息。
Resolver函数类似这样
```rust
async fn parse_with_extensions(&self) -> Result<i32, Error> {
let my_extension = json!({ "details": "CAN_NOT_FETCH" });
Err(Error::new("MyMessage").extend_with(|_| my_extension))
}
Err(Error::new("MyMessage").extend_with(|_, e| e.set("details", "CAN_NOT_FETCH")))
}
```
然后可以返回如下响应:
@ -55,8 +53,8 @@ use std::num::ParseIntError;
async fn parse_with_extensions(&self) -> Result<i32> {
Ok("234a"
.parse()
.map_err(|err: ParseIntError| err.extend_with(|_err| json!({"code": 404})))?)
}
.map_err(|err: ParseIntError| err.extend_with(|_err, e| e.set("code", 404)))?)
}
```
### 为自定义错误实现ErrorExtensions
@ -83,13 +81,11 @@ pub enum MyError {
impl ErrorExtensions for MyError {
// lets define our base extensions
fn extend(&self) -> Error {
Error::new(format!("{}", self)).extend_with(|err|
Error::new(format!("{}", self)).extend_with(|err, e|
match self {
MyError::NotFound => json!({"code": "NOT_FOUND"}),
MyError::ServerError(reason) => json!({ "reason": reason }),
MyError::ErrorWithoutExtensions => {
json!("This will be ignored since it does not represent an object.")
}
MyError::NotFound => e.set("code", "NOT_FOUND"),
MyError::ServerError(reason) => e.set("reason", reason),
MyError::ErrorWithoutExtensions => {}
})
}
}
@ -101,7 +97,7 @@ impl ErrorExtensions for MyError {
async fn parse_with_extensions_result(&self) -> Result<i32> {
// Err(MyError::NotFound.extend())
// OR
Err(MyError::NotFound.extend_with(|_| json!({ "on_the_fly": "some_more_info" })))
Err(MyError::NotFound.extend_with(|_, e| e.set("on_the_fly", "some_more_info")))
}
```
@ -129,9 +125,8 @@ use async_graphql::*;
async fn parse_with_extensions(&self) -> Result<i32> {
Ok("234a"
.parse()
.extend_err(|_| json!({"code": 404}))?)
}
.extend_err(|_, e| e.set("code", 404))?)
}
```
### 链式调用
@ -144,10 +139,10 @@ async fn parse_with_extensions(&self) -> Result<i32> {
match "234a".parse() {
Ok(n) => Ok(n),
Err(e) => Err(e
.extend_with(|_| json!({"code": 404}))
.extend_with(|_| json!({"details": "some more info.."}))
.extend_with(|_, e| e.set("code", 404))
.extend_with(|_, e| e.set("details", "some more info.."))
// keys may also overwrite previous keys...
.extend_with(|_| json!({"code": 500}))),
.extend_with(|_, e| e.set("code", 500))),
}
}
```
@ -182,7 +177,7 @@ Rust的稳定版本还未提供特化功能这就是为什么`ErrorExtensions
async fn parse_with_extensions_result(&self) -> Result<i32> {
// the trait `error::ErrorExtensions` is not implemented
// for `std::num::ParseIntError`
"234a".parse().extend_err(|_| json!({"code": 404}))
"234a".parse().extend_err(|_, e| e.set("code", 404))
}
```
@ -193,7 +188,7 @@ async fn parse_with_extensions_result(&self) -> Result<i32> {
// does work because ErrorExtensions is implemented for &ParseIntError
"234a"
.parse()
.map_err(|ref e: ParseIntError| e.extend_with(|_| json!({"code": 404})))
.map_err(|ref e: ParseIntError| e.extend_with(|_, e| e.set("code", 404)))
}
```

View File

@ -188,17 +188,24 @@ fn upload() -> Result<()> {
struct MutationRoot;
#[Object]
impl MutationRoot {
async fn single_upload(&self, file: Upload) -> FileInfo {
println!("single_upload: filename={}", file.filename());
println!("single_upload: content_type={:?}", file.content_type());
async fn single_upload(&self, ctx: &Context<'_>, file: Upload) -> FileInfo {
let upload_value = file.value(ctx).unwrap();
println!("single_upload: filename={}", upload_value.filename);
println!(
"single_upload: content_type={:?}",
upload_value.content_type
);
let file_info = FileInfo {
filename: file.filename().into(),
mime_type: file.content_type().map(ToString::to_string),
filename: upload_value.filename.clone(),
mime_type: upload_value.content_type.clone(),
};
let mut content = String::new();
file.into_read().read_to_string(&mut content).unwrap();
upload_value
.into_read()
.read_to_string(&mut content)
.unwrap();
assert_eq!(content, "test".to_owned());
file_info

View File

@ -309,8 +309,5 @@ fn parse_arguments(
fn parse_name(pair: Pair<Rule>, pc: &mut PositionCalculator) -> Result<Positioned<Name>> {
debug_assert_eq!(pair.as_rule(), Rule::name);
Ok(Positioned::new(
Name::new_unchecked(pair.as_str().to_owned()),
pc.step(&pair),
))
Ok(Positioned::new(Name::new(pair.as_str()), pc.step(&pair)))
}

View File

@ -7,19 +7,20 @@
//! This follows the [June 2018 edition of the GraphQL spec](https://spec.graphql.org/June2018/).
use crate::pos::Positioned;
use serde::de::{Deserializer, Error as _, Unexpected};
use serde::de::Deserializer;
use serde::ser::{Error as _, Serializer};
use serde::{Deserialize, Serialize};
use std::borrow::Borrow;
use std::borrow::{Borrow, Cow};
use std::collections::{hash_map, BTreeMap, HashMap};
use std::convert::{TryFrom, TryInto};
use std::fmt::{self, Display, Formatter, Write};
use std::fs::File;
use std::ops::Deref;
pub use executable::*;
pub use serde_json::Number;
pub use service::*;
use std::iter::FromIterator;
use std::sync::Arc;
mod executable;
mod service;
@ -72,7 +73,7 @@ impl Type {
base: if let Some(ty) = ty.strip_prefix('[') {
BaseType::List(Box::new(Self::new(ty.strip_suffix(']')?)?))
} else {
BaseType::Named(Name::new(ty.to_owned()).ok()?)
BaseType::Named(Name::new(ty))
},
nullable,
})
@ -132,9 +133,127 @@ pub enum ConstValue {
List(Vec<ConstValue>),
/// An object. This is a map of keys to values.
Object(BTreeMap<Name, ConstValue>),
/// An uploaded file.
#[serde(serialize_with = "fail_serialize_upload", skip_deserializing)]
Upload(UploadValue),
}
impl From<()> for ConstValue {
fn from((): ()) -> Self {
ConstValue::Null
}
}
macro_rules! from_integer {
($($ty:ident),*) => {
$(
impl From<$ty> for ConstValue {
fn from(n: $ty) -> Self {
ConstValue::Number(n.into())
}
}
)*
};
}
from_integer!(i8, i16, i32, i64, isize, u8, u16, u32, u64, usize);
impl From<f32> for ConstValue {
fn from(f: f32) -> Self {
From::from(f as f64)
}
}
impl From<f64> for ConstValue {
fn from(f: f64) -> Self {
Number::from_f64(f).map_or(ConstValue::Null, ConstValue::Number)
}
}
impl From<bool> for ConstValue {
fn from(value: bool) -> Self {
ConstValue::Boolean(value)
}
}
impl From<String> for ConstValue {
fn from(value: String) -> Self {
ConstValue::String(value)
}
}
impl<'a> From<&'a str> for ConstValue {
fn from(value: &'a str) -> Self {
ConstValue::String(value.into())
}
}
impl<'a> From<Cow<'a, str>> for ConstValue {
fn from(f: Cow<'a, str>) -> Self {
ConstValue::String(f.into_owned())
}
}
impl<T: Into<ConstValue>> FromIterator<T> for ConstValue {
fn from_iter<I: IntoIterator<Item = T>>(iter: I) -> Self {
ConstValue::List(iter.into_iter().map(Into::into).collect())
}
}
impl<'a, T: Clone + Into<ConstValue>> From<&'a [T]> for ConstValue {
fn from(f: &'a [T]) -> Self {
ConstValue::List(f.iter().cloned().map(Into::into).collect())
}
}
impl<T: Into<ConstValue>> From<Vec<T>> for ConstValue {
fn from(f: Vec<T>) -> Self {
ConstValue::List(f.into_iter().map(Into::into).collect())
}
}
impl From<BTreeMap<Name, ConstValue>> for ConstValue {
fn from(f: BTreeMap<Name, ConstValue>) -> Self {
ConstValue::Object(f)
}
}
impl PartialEq<serde_json::Value> for ConstValue {
fn eq(&self, other: &serde_json::Value) -> bool {
match (self, other) {
(ConstValue::Null, serde_json::Value::Null) => true,
(ConstValue::Number(a), serde_json::Value::Number(b)) => a == b,
(ConstValue::String(a), serde_json::Value::String(b)) => a == b,
(ConstValue::Boolean(a), serde_json::Value::Bool(b)) => a == b,
(ConstValue::Enum(a), serde_json::Value::String(b)) => a == b,
(ConstValue::List(a), serde_json::Value::Array(b)) => {
if a.len() != b.len() {
return false;
}
a.iter().zip(b.iter()).all(|(a, b)| a == b)
}
(ConstValue::Object(a), serde_json::Value::Object(b)) => {
if a.len() != b.len() {
return false;
}
for (a_key, a_value) in a.iter() {
if let Some(b_value) = b.get(a_key.as_str()) {
if b_value != a_value {
return false;
}
} else {
return false;
}
}
true
}
_ => false,
}
}
}
impl PartialEq<ConstValue> for serde_json::Value {
fn eq(&self, other: &ConstValue) -> bool {
other == self
}
}
impl ConstValue {
@ -155,7 +274,6 @@ impl ConstValue {
.map(|(key, value)| (key, value.into_value()))
.collect(),
),
Self::Upload(upload) => Value::Upload(upload),
}
}
@ -191,7 +309,7 @@ impl Display for ConstValue {
Self::String(val) => write_quoted(val, f),
Self::Boolean(true) => f.write_str("true"),
Self::Boolean(false) => f.write_str("false"),
Self::Null | Self::Upload(_) => f.write_str("null"),
Self::Null => f.write_str("null"),
Self::Enum(name) => f.write_str(name),
Self::List(items) => write_list(items, f),
Self::Object(map) => write_object(map, f),
@ -205,6 +323,7 @@ impl TryFrom<serde_json::Value> for ConstValue {
Self::deserialize(value)
}
}
impl TryFrom<ConstValue> for serde_json::Value {
type Error = serde_json::Error;
fn try_from(value: ConstValue) -> Result<Self, Self::Error> {
@ -241,9 +360,6 @@ pub enum Value {
List(Vec<Value>),
/// An object. This is a map of keys to values.
Object(BTreeMap<Name, Value>),
/// An uploaded file.
#[serde(serialize_with = "fail_serialize_upload", skip_deserializing)]
Upload(UploadValue),
}
impl Value {
@ -277,7 +393,6 @@ impl Value {
.map(|(key, value)| Ok((key, value.into_const_with_mut(f)?)))
.collect::<Result<_, _>>()?,
),
Self::Upload(upload) => ConstValue::Upload(upload),
})
}
@ -322,7 +437,7 @@ impl Display for Value {
Self::String(val) => write_quoted(val, f),
Self::Boolean(true) => f.write_str("true"),
Self::Boolean(false) => f.write_str("false"),
Self::Null | Self::Upload(_) => f.write_str("null"),
Self::Null => f.write_str("null"),
Self::Enum(name) => f.write_str(name),
Self::List(items) => write_list(items, f),
Self::Object(map) => write_object(map, f),
@ -352,9 +467,6 @@ impl TryFrom<Value> for serde_json::Value {
fn fail_serialize_variable<S: Serializer>(_: &str, _: S) -> Result<S::Ok, S::Error> {
Err(S::Error::custom("cannot serialize variable"))
}
fn fail_serialize_upload<S: Serializer>(_: &UploadValue, _: S) -> Result<S::Ok, S::Error> {
Err(S::Error::custom("cannot serialize uploaded file"))
}
fn write_quoted(s: &str, f: &mut Formatter<'_>) -> fmt::Result {
f.write_char('"')?;
@ -390,51 +502,6 @@ fn write_object<K: Display, V: Display>(
f.write_char('}')
}
/// A file upload value.
pub struct UploadValue {
/// The name of the file.
pub filename: String,
/// The content type of the file.
pub content_type: Option<String>,
/// The file data.
pub content: File,
}
impl UploadValue {
/// Attempt to clone the upload value. This type's `Clone` implementation simply calls this and
/// panics on failure.
///
/// # Errors
///
/// Fails if cloning the inner `File` fails.
pub fn try_clone(&self) -> std::io::Result<Self> {
Ok(Self {
filename: self.filename.clone(),
content_type: self.content_type.clone(),
content: self.content.try_clone()?,
})
}
}
impl Clone for UploadValue {
fn clone(&self) -> Self {
self.try_clone().unwrap()
}
}
impl fmt::Debug for UploadValue {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "Upload({})", self.filename)
}
}
impl PartialEq for UploadValue {
fn eq(&self, other: &Self) -> bool {
self.filename == other.filename
}
}
impl Eq for UploadValue {}
/// A const GraphQL directive, such as `@deprecated(reason: "Use the other field)`. This differs
/// from [`Directive`](struct.Directive.html) in that it uses [`ConstValue`](enum.ConstValue.html)
/// instead of [`Value`](enum.Value.html).
@ -509,48 +576,22 @@ impl Directive {
}
}
/// A GraphQL name. This is a newtype wrapper around a string with the addition guarantee that it
/// is a valid GraphQL name (follows the regex `[_A-Za-z][_0-9A-Za-z]*`).
/// A GraphQL name.
///
/// [Reference](https://spec.graphql.org/June2018/#Name).
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(transparent)]
pub struct Name(String);
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Name(Arc<str>);
impl Serialize for Name {
fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
serializer.serialize_str(&self.0)
}
}
impl Name {
/// Check whether the name is valid (follows the regex `[_A-Za-z][_0-9A-Za-z]*`).
#[must_use]
pub fn is_valid(name: &str) -> bool {
let mut bytes = name.bytes();
bytes
.next()
.map_or(false, |c| c.is_ascii_alphabetic() || c == b'_')
&& bytes.all(|c| c.is_ascii_alphanumeric() || c == b'_')
}
/// Create a new name without checking whether it is valid or not. This will always check in
/// debug mode.
///
/// This function is not `unsafe` because an invalid name does not cause UB, but care should be
/// taken to make sure it is a valid name.
#[must_use]
pub fn new_unchecked(name: String) -> Self {
debug_assert!(Self::is_valid(&name));
Self(name)
}
/// Create a new name, checking whether it is valid. Returns ownership of the string if it
/// fails.
///
/// # Errors
///
/// Fails if the name is not a valid name.
pub fn new(name: String) -> Result<Self, String> {
if Self::is_valid(&name) {
Ok(Self(name))
} else {
Err(name)
}
/// Create a new name.
pub fn new(name: &str) -> Self {
Self(name.into())
}
/// Get the name as a string.
@ -558,12 +599,6 @@ impl Name {
pub fn as_str(&self) -> &str {
&self.0
}
/// Convert the name to a `String`.
#[must_use]
pub fn into_string(self) -> String {
self.0
}
}
impl AsRef<str> for Name {
@ -578,12 +613,6 @@ impl Borrow<str> for Name {
}
}
impl From<Name> for String {
fn from(name: Name) -> Self {
name.0
}
}
impl Deref for Name {
type Target = str;
@ -600,17 +629,17 @@ impl Display for Name {
impl PartialEq<String> for Name {
fn eq(&self, other: &String) -> bool {
self.0 == *other
self.as_str() == other
}
}
impl PartialEq<str> for Name {
fn eq(&self, other: &str) -> bool {
self.0 == other
self.as_str() == other
}
}
impl PartialEq<Name> for String {
fn eq(&self, other: &Name) -> bool {
other == self
self == other.as_str()
}
}
impl PartialEq<Name> for str {
@ -631,18 +660,8 @@ impl<'a> PartialEq<Name> for &'a str {
impl<'de> Deserialize<'de> for Name {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
Self::new(String::deserialize(deserializer)?)
.map_err(|s| D::Error::invalid_value(Unexpected::Str(&s), &"a GraphQL name"))
Ok(Self(
String::deserialize(deserializer)?.into_boxed_str().into(),
))
}
}
#[cfg(test)]
#[test]
fn test_valid_names() {
assert!(Name::is_valid("valid_name"));
assert!(Name::is_valid("numbers123_456_789abc"));
assert!(Name::is_valid("MiXeD_CaSe"));
assert!(Name::is_valid("_"));
assert!(!Name::is_valid("invalid name"));
assert!(!Name::is_valid("123and_text"));
}

View File

@ -41,12 +41,12 @@ pub trait InputValueType: Type + Sized {
/// Represents a GraphQL output value
#[async_trait::async_trait]
pub trait OutputValueType: Type {
/// Resolve an output value to `serde_json::Value`.
/// Resolve an output value to `async_graphql::Value`.
async fn resolve(
&self,
ctx: &ContextSelectionSet<'_>,
field: &Positioned<Field>,
) -> ServerResult<serde_json::Value>;
) -> ServerResult<Value>;
}
impl<T: Type + Send + Sync> Type for &T {
@ -66,7 +66,7 @@ impl<T: OutputValueType + Send + Sync> OutputValueType for &T {
&self,
ctx: &ContextSelectionSet<'_>,
field: &Positioned<Field>,
) -> ServerResult<serde_json::Value> {
) -> ServerResult<Value> {
T::resolve(*self, ctx, field).await
}
}
@ -91,7 +91,7 @@ impl<T: OutputValueType + Sync> OutputValueType for Result<T> {
&self,
ctx: &ContextSelectionSet<'_>,
field: &Positioned<Field>,
) -> ServerResult<serde_json::Value> {
) -> ServerResult<Value> {
match self {
Ok(value) => Ok(value.resolve(ctx, field).await?),
Err(err) => Err(err.clone().into_server_error().at(field.pos)),

View File

@ -5,7 +5,8 @@ use crate::parser::types::{
};
use crate::schema::SchemaEnv;
use crate::{
Error, InputValueType, Lookahead, Pos, Positioned, Result, ServerError, ServerResult, Value,
Error, InputValueType, Lookahead, Pos, Positioned, Result, ServerError, ServerResult,
UploadValue, Value,
};
use fnv::FnvHashMap;
use serde::ser::{SerializeSeq, Serializer};
@ -248,6 +249,7 @@ pub struct QueryEnvInner {
pub variables: Variables,
pub operation: Positioned<OperationDefinition>,
pub fragments: HashMap<Name, Positioned<FragmentDefinition>>,
pub uploads: Vec<UploadValue>,
pub ctx_data: Arc<Data>,
}

View File

@ -1,9 +1,22 @@
use crate::{parser, InputValueType, Pos, Value};
use serde::Serialize;
use std::collections::BTreeMap;
use std::fmt::{self, Debug, Display, Formatter};
use std::marker::PhantomData;
use thiserror::Error;
/// Extensions to the error.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default)]
#[serde(transparent)]
pub struct ErrorExtensionValues(BTreeMap<String, Value>);
impl ErrorExtensionValues {
/// Set an extension value.
pub fn set(&mut self, name: impl AsRef<str>, value: impl Into<Value>) {
self.0.insert(name.as_ref().to_string(), value.into());
}
}
/// An error in a GraphQL server.
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ServerError {
@ -16,8 +29,15 @@ pub struct ServerError {
#[serde(skip_serializing_if = "Vec::is_empty")]
pub path: Vec<PathSegment>,
/// Extensions to the error.
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<serde_json::Map<String, serde_json::Value>>,
#[serde(skip_serializing_if = "error_extensions_is_empty")]
pub extensions: Option<ErrorExtensionValues>,
}
fn error_extensions_is_empty(values: &Option<ErrorExtensionValues>) -> bool {
match values {
Some(values) => values.0.is_empty(),
None => true,
}
}
impl ServerError {
@ -152,8 +172,8 @@ pub struct Error {
/// The error message.
pub message: String,
/// Extensions to the error.
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<serde_json::Map<String, serde_json::Value>>,
#[serde(skip_serializing_if = "error_extensions_is_empty")]
pub extensions: Option<ErrorExtensionValues>,
}
impl Error {
@ -189,55 +209,6 @@ impl<T: Display> From<T> for Error {
/// An alias for `Result<T, Error>`.
pub type Result<T, E = Error> = std::result::Result<T, E>;
/*
/// Extend errors with additional information.
///
/// This trait is implemented for `Error` and `Result<T>`.
pub trait ExtendError {
/// Extend the error with the extensions.
///
/// The value must be a map otherwise this function will panic. It takes a value for the
/// ergonomics of being able to use serde_json's `json!` macro.
///
/// If the error already contains extensions they are appended on.
fn extend(self, extensions: serde_json::Value) -> Self;
/// Extend the error with a callback to make the extensions.
fn extend_with(self, f: impl FnOnce(&Error) -> serde_json::Value) -> Self;
}
impl ExtendError for Error {
fn extend(self, extensions: serde_json::Value) -> Self {
let mut extensions = match extensions {
serde_json::Value::Object(map) => map,
_ => panic!("Extend must be called with a map"),
};
Self {
extensions: Some(match self.extensions {
Some(mut existing) => {
existing.append(&mut extensions);
existing
}
None => extensions,
}),
..self
}
}
fn extend_with(self, f: impl FnOnce(&Error) -> serde_json::Value) -> Self {
let ext = f(&self);
self.extend(ext)
}
}
impl<T> ExtendError for Result<T> {
fn extend(self, extensions: serde_json::Value) -> Self {
self.map_err(|e| e.extend(extensions))
}
fn extend_with(self, f: impl FnOnce(&Error) -> serde_json::Value) -> Self {
self.map_err(|e| e.extend_with(f))
}
}*/
/// An error parsing the request.
#[derive(Debug, Error)]
#[non_exhaustive]
@ -305,24 +276,14 @@ pub trait ErrorExtensions: Sized {
/// Add extensions to the error, using a callback to make the extensions.
fn extend_with<C>(self, cb: C) -> Error
where
C: FnOnce(&Self) -> serde_json::Value,
C: FnOnce(&Self, &mut ErrorExtensionValues),
{
let message = self.extend().message;
match cb(&self) {
serde_json::Value::Object(cb_res) => {
let extensions = match self.extend().extensions {
Some(mut extensions) => {
extensions.extend(cb_res);
extensions
}
None => cb_res.into_iter().collect(),
};
Error {
message,
extensions: Some(extensions),
}
}
_ => panic!("Extend must be called with a map"),
let mut extensions = self.extend().extensions.unwrap_or_default();
cb(&self, &mut extensions);
Error {
message,
extensions: Some(extensions),
}
}
}
@ -349,7 +310,7 @@ pub trait ResultExt<T, E>: Sized {
/// Extend the error value of the result with the callback.
fn extend_err<C>(self, cb: C) -> Result<T>
where
C: FnOnce(&E) -> serde_json::Value;
C: FnOnce(&E, &mut ErrorExtensionValues);
/// Extend the result to a `Result`.
fn extend(self) -> Result<T>;
@ -363,10 +324,10 @@ where
{
fn extend_err<C>(self, cb: C) -> Result<T>
where
C: FnOnce(&E) -> serde_json::Value,
C: FnOnce(&E, &mut ErrorExtensionValues),
{
match self {
Err(err) => Err(err.extend_with(|e| cb(e))),
Err(err) => Err(err.extend_with(|e, ee| cb(e, ee))),
Ok(value) => Ok(value),
}
}

View File

@ -1,9 +1,10 @@
use crate::extensions::{Extension, ExtensionContext, ExtensionFactory, ResolveInfo};
use crate::Variables;
use crate::{Value, Variables};
use chrono::{DateTime, Utc};
use serde::ser::SerializeMap;
use serde::{Serialize, Serializer};
use std::collections::BTreeMap;
use std::convert::TryInto;
use std::ops::Deref;
struct PendingResolve {
@ -116,10 +117,11 @@ impl Extension for ApolloTracingExtension {
}
}
fn result(&mut self, _ctx: &ExtensionContext<'_>) -> Option<serde_json::Value> {
fn result(&mut self, _ctx: &ExtensionContext<'_>) -> Option<Value> {
self.resolves
.sort_by(|a, b| a.start_offset.cmp(&b.start_offset));
Some(serde_json::json!({
serde_json::json!({
"version": 1,
"startTime": self.start_time.to_rfc3339(),
"endTime": self.end_time.to_rfc3339(),
@ -127,6 +129,8 @@ impl Extension for ApolloTracingExtension {
"execution": {
"resolvers": self.resolves
}
}))
})
.try_into()
.ok()
}
}

View File

@ -18,10 +18,10 @@ pub use self::apollo_tracing::ApolloTracing;
pub use self::logger::Logger;
#[cfg(feature = "tracing")]
pub use self::tracing::Tracing;
use crate::parser::types::ExecutableDocument;
use crate::Error;
use serde_json::Value;
use crate::parser::types::{ExecutableDocument, Name};
use crate::{Error, Value};
use std::any::{Any, TypeId};
use std::collections::BTreeMap;
pub(crate) type BoxExtension = Box<dyn Extension>;
@ -141,7 +141,7 @@ pub trait Extension: Sync + Send + 'static {
fn error(&mut self, ctx: &ExtensionContext<'_>, err: &ServerError) {}
/// Get the results
fn result(&mut self, ctx: &ExtensionContext<'_>) -> Option<serde_json::Value> {
fn result(&mut self, ctx: &ExtensionContext<'_>) -> Option<Value> {
None
}
}
@ -236,16 +236,16 @@ impl Extension for Extensions {
.iter_mut()
.filter_map(|e| {
if let Some(name) = e.name() {
e.result(ctx).map(|res| (name.to_string(), res))
e.result(ctx).map(|res| (Name::new(name), res))
} else {
None
}
})
.collect::<serde_json::Map<_, _>>();
.collect::<BTreeMap<_, _>>();
if value.is_empty() {
None
} else {
Some(value.into())
Some(Value::Object(value))
}
} else {
None

View File

@ -1,4 +1,4 @@
use crate::{BatchRequest, ParseRequestError};
use crate::{BatchRequest, ParseRequestError, UploadValue};
use bytes::Bytes;
use futures::io::AsyncRead;
use futures::stream::Stream;
@ -101,15 +101,16 @@ pub(super) async fn receive_batch_multipart(
for (name, filename, content_type, file) in files {
if let Some(var_paths) = map.remove(&name) {
let upload = UploadValue {
filename,
content_type,
content: file,
};
for var_path in var_paths {
match &mut request {
BatchRequest::Single(request) => {
request.set_upload(
&var_path,
filename.clone(),
content_type.clone(),
file.try_clone().unwrap(),
);
request.set_upload(&var_path, upload.try_clone()?);
}
BatchRequest::Batch(requests) => {
let mut s = var_path.splitn(2, '.');
@ -118,12 +119,7 @@ pub(super) async fn receive_batch_multipart(
if let (Some(idx), Some(path)) = (idx, path) {
if let Some(request) = requests.get_mut(idx) {
request.set_upload(
path,
filename.clone(),
content_type.clone(),
file.try_clone().unwrap(),
);
request.set_upload(path, upload.try_clone()?);
}
}
}

View File

@ -162,8 +162,8 @@ pub use context::{
Context, ContextBase, Data, QueryEnv, QueryPathNode, QueryPathSegment, ResolveId, Variables,
};
pub use error::{
Error, ErrorExtensions, InputValueError, InputValueResult, ParseRequestError, PathSegment,
Result, ResultExt, ServerError, ServerResult,
Error, ErrorExtensionValues, ErrorExtensions, InputValueError, InputValueResult,
ParseRequestError, PathSegment, Result, ResultExt, ServerError, ServerResult,
};
pub use look_ahead::Lookahead;
pub use parser::types::{ConstValue as Value, Number};

View File

@ -1,10 +1,8 @@
use crate::parser::types::UploadValue;
use crate::{Data, ParseRequestError, Value, Variables};
use crate::{Data, ParseRequestError, UploadValue, Value, Variables};
use serde::{Deserialize, Deserializer};
use std::any::Any;
use std::collections::HashMap;
use std::fmt::{self, Debug, Formatter};
use std::fs::File;
/// GraphQL request.
///
@ -25,6 +23,10 @@ pub struct Request {
#[serde(default, deserialize_with = "deserialize_variables")]
pub variables: Variables,
/// Uploads
#[serde(skip)]
pub uploads: Vec<UploadValue>,
/// The data of the request that can be accessed through `Context::data`.
///
/// **This data is only valid for this request**
@ -49,6 +51,7 @@ impl Request {
query: query.into(),
operation_name: None,
variables: Variables::default(),
uploads: Vec::default(),
data: Data::default(),
extensions: Default::default(),
}
@ -79,22 +82,13 @@ impl Request {
/// `variables.files.2.content` is equivalent to the Rust code
/// `request.variables["files"][2]["content"]`. If no variable exists at the path this function
/// won't do anything.
pub fn set_upload(
&mut self,
var_path: &str,
filename: String,
content_type: Option<String>,
content: File,
) {
pub fn set_upload(&mut self, var_path: &str, upload: UploadValue) {
let variable = match self.variables.variable_path(var_path) {
Some(variable) => variable,
None => return,
};
*variable = Value::Upload(UploadValue {
filename,
content_type,
content,
});
self.uploads.push(upload);
*variable = Value::String(format!("#__graphql_file__:{}", self.uploads.len() - 1));
}
}

View File

@ -1,9 +1,10 @@
use crate::extensions::{ErrorLogger, Extension, ExtensionContext, ResolveInfo};
use crate::parser::types::Selection;
use crate::parser::types::{Name, Selection};
use crate::registry::MetaType;
use crate::{
Context, ContextSelectionSet, OutputValueType, PathSegment, ServerError, ServerResult, Value,
};
use std::collections::BTreeMap;
use std::future::Future;
use std::pin::Pin;
@ -19,10 +20,10 @@ pub trait ContainerType: OutputValueType {
false
}
/// Resolves a field value and outputs it as a json value `serde_json::Value`.
/// Resolves a field value and outputs it as a json value `async_graphql::Value`.
///
/// If the field was not found returns None.
async fn resolve_field(&self, ctx: &Context<'_>) -> ServerResult<Option<serde_json::Value>>;
async fn resolve_field(&self, ctx: &Context<'_>) -> ServerResult<Option<Value>>;
/// Collect all the fields of the container that are queried in the selection set.
///
@ -42,18 +43,14 @@ pub trait ContainerType: OutputValueType {
/// Find the GraphQL entity with the given name from the parameter.
///
/// Objects should override this in case they are the query root.
async fn find_entity(
&self,
_: &Context<'_>,
_params: &Value,
) -> ServerResult<Option<serde_json::Value>> {
async fn find_entity(&self, _: &Context<'_>, _params: &Value) -> ServerResult<Option<Value>> {
Ok(None)
}
}
#[async_trait::async_trait]
impl<T: ContainerType + Send + Sync> ContainerType for &T {
async fn resolve_field(&self, ctx: &Context<'_>) -> ServerResult<Option<serde_json::Value>> {
async fn resolve_field(&self, ctx: &Context<'_>) -> ServerResult<Option<Value>> {
T::resolve_field(*self, ctx).await
}
}
@ -64,55 +61,54 @@ impl<T: ContainerType + Send + Sync> ContainerType for &T {
pub async fn resolve_container<'a, T: ContainerType + Send + Sync>(
ctx: &ContextSelectionSet<'a>,
root: &'a T,
) -> ServerResult<serde_json::Value> {
) -> ServerResult<Value> {
let mut fields = Fields(Vec::new());
fields.add_set(ctx, root)?;
let futures = fields.0;
let res = futures::future::try_join_all(futures).await?;
let mut map = serde_json::Map::new();
let mut map = BTreeMap::new();
for (name, value) in res {
if let serde_json::Value::Object(b) = value {
if let Some(serde_json::Value::Object(a)) = map.get_mut(&name) {
if let Value::Object(b) = value {
if let Some(Value::Object(a)) = map.get_mut(&name) {
a.extend(b);
} else {
map.insert(name, b.into());
map.insert(name, Value::Object(b));
}
} else {
map.insert(name, value);
}
}
Ok(map.into())
Ok(Value::Object(map))
}
/// Resolve an container by executing each of the fields serially.
pub async fn resolve_container_serial<'a, T: ContainerType + Send + Sync>(
ctx: &ContextSelectionSet<'a>,
root: &'a T,
) -> ServerResult<serde_json::Value> {
) -> ServerResult<Value> {
let mut fields = Fields(Vec::new());
fields.add_set(ctx, root)?;
let futures = fields.0;
let mut map = serde_json::Map::new();
let mut map = BTreeMap::new();
for field in futures {
let (name, value) = field.await?;
if let serde_json::Value::Object(b) = value {
if let Some(serde_json::Value::Object(a)) = map.get_mut(&name) {
if let Value::Object(b) = value {
if let Some(Value::Object(a)) = map.get_mut(&name) {
a.extend(b);
} else {
map.insert(name, b.into());
map.insert(name, Value::Object(b));
}
} else {
map.insert(name, value);
}
}
Ok(map.into())
Ok(Value::Object(map))
}
type BoxFieldFuture<'a> =
Pin<Box<dyn Future<Output = ServerResult<(String, serde_json::Value)>> + 'a + Send>>;
type BoxFieldFuture<'a> = Pin<Box<dyn Future<Output = ServerResult<(Name, Value)>> + 'a + Send>>;
/// A set of fields on an container that are being selected.
pub struct Fields<'a>(Vec<BoxFieldFuture<'a>>);
@ -134,17 +130,11 @@ impl<'a> Fields<'a> {
if field.node.name.node == "__typename" {
// Get the typename
let ctx_field = ctx.with_field(field);
let field_name = ctx_field
.item
.node
.response_key()
.node
.clone()
.into_string();
let field_name = ctx_field.item.node.response_key().node.clone();
let typename = root.introspection_type_name().into_owned();
self.0.push(Box::pin(async move {
Ok((field_name, serde_json::Value::String(typename)))
Ok((field_name, Value::String(typename)))
}));
continue;
}
@ -164,13 +154,7 @@ impl<'a> Fields<'a> {
let ctx = ctx.clone();
async move {
let ctx_field = ctx.with_field(field);
let field_name = ctx_field
.item
.node
.response_key()
.node
.clone()
.into_string();
let field_name = ctx_field.item.node.response_key().node.clone();
let ctx_extension = ExtensionContext {
schema_data: &ctx.schema_env.data,
query_data: &ctx.query_env.ctx_data,
@ -196,7 +180,7 @@ impl<'a> Fields<'a> {
T::type_name()
))
.at(ctx_field.item.pos)
.path(PathSegment::Field(field_name)));
.path(PathSegment::Field(field_name.to_string())));
}
},
};
@ -209,7 +193,7 @@ impl<'a> Fields<'a> {
let res = match root.resolve_field(&ctx_field).await {
Ok(value) => Ok((field_name, value.unwrap())),
Err(e) => Err(e.path(PathSegment::Field(field_name))),
Err(e) => Err(e.path(PathSegment::Field(field_name.to_string()))),
}
.log_error(&ctx_extension, &ctx_field.query_env.extensions)?;

View File

@ -42,5 +42,5 @@ pub fn parse_enum<T: EnumType + InputValueType>(value: Value) -> InputValueResul
/// This can be used to implement `InputValueType::to_value` or `OutputValueType::resolve`.
pub fn enum_value<T: EnumType>(value: T) -> Value {
let item = T::items().iter().find(|item| item.value == value).unwrap();
Value::Enum(Name::new_unchecked(item.name.to_owned()))
Value::Enum(Name::new(item.name))
}

View File

@ -1,13 +1,15 @@
use crate::extensions::{ErrorLogger, Extension, ExtensionContext, ResolveInfo};
use crate::parser::types::Field;
use crate::{ContextSelectionSet, OutputValueType, PathSegment, Positioned, ServerResult, Type};
use crate::{
ContextSelectionSet, OutputValueType, PathSegment, Positioned, ServerResult, Type, Value,
};
/// Resolve an list by executing each of the items concurrently.
pub async fn resolve_list<'a, T: OutputValueType + Send + Sync + 'a>(
ctx: &ContextSelectionSet<'a>,
field: &Positioned<Field>,
iter: impl IntoIterator<Item = T>,
) -> ServerResult<serde_json::Value> {
) -> ServerResult<Value> {
let mut futures = Vec::new();
for (idx, item) in iter.into_iter().enumerate() {
@ -45,5 +47,5 @@ pub async fn resolve_list<'a, T: OutputValueType + Send + Sync + 'a>(
});
}
Ok(futures::future::try_join_all(futures).await?.into())
Ok(Value::List(futures::future::try_join_all(futures).await?))
}

View File

@ -1,15 +1,15 @@
use crate::{CacheControl, Result, ServerError};
use crate::{CacheControl, Result, ServerError, Value};
use serde::Serialize;
/// Query response
#[derive(Debug, Default, Serialize)]
pub struct Response {
/// Data of query result
pub data: serde_json::Value,
pub data: Value,
/// Extensions result
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<serde_json::Value>,
pub extensions: Option<Value>,
/// Cache control value
#[serde(skip)]
@ -23,7 +23,7 @@ pub struct Response {
impl Response {
/// Create a new successful response with the data.
#[must_use]
pub fn new(data: impl Into<serde_json::Value>) -> Self {
pub fn new(data: impl Into<Value>) -> Self {
Self {
data: data.into(),
..Default::default()
@ -41,7 +41,7 @@ impl Response {
/// Set the extensions result of the response.
#[must_use]
pub fn extensions(self, extensions: Option<serde_json::Value>) -> Self {
pub fn extensions(self, extensions: Option<Value>) -> Self {
Self { extensions, ..self }
}
@ -127,15 +127,15 @@ mod tests {
#[test]
fn test_batch_response_single() {
let resp = BatchResponse::Single(Response::new(serde_json::Value::Bool(true)));
let resp = BatchResponse::Single(Response::new(Value::Boolean(true)));
assert_eq!(serde_json::to_string(&resp).unwrap(), r#"{"data":true}"#);
}
#[test]
fn test_batch_response_batch() {
let resp = BatchResponse::Batch(vec![
Response::new(serde_json::Value::Bool(true)),
Response::new(serde_json::Value::String("1".to_string())),
Response::new(Value::Boolean(true)),
Response::new(Value::String("1".to_string())),
]);
assert_eq!(
serde_json::to_string(&resp).unwrap(),

View File

@ -10,12 +10,13 @@ use crate::types::QueryRoot;
use crate::validation::{check_rules, CheckResult, ValidationMode};
use crate::{
BatchRequest, BatchResponse, CacheControl, ContextBase, ObjectType, QueryEnv, Request,
Response, ServerError, SubscriptionType, Type, ID,
Response, ServerError, SubscriptionType, Type, Value, ID,
};
use futures::stream::{self, Stream, StreamExt};
use indexmap::map::IndexMap;
use itertools::Itertools;
use std::any::Any;
use std::collections::BTreeMap;
use std::ops::Deref;
use std::sync::atomic::AtomicUsize;
use std::sync::Arc;
@ -420,6 +421,7 @@ where
variables: request.variables,
operation,
fragments: document.fragments,
uploads: request.uploads,
ctx_data: Arc::new(request.data),
};
Ok((env, cache_control))
@ -544,11 +546,11 @@ where
let is_err = data.is_err();
let extensions = env.extensions.lock().result(&ctx_extension);
yield match data {
Ok((name, value)) => Response::new(
serde_json::json!({
name: value,
})
),
Ok((name, value)) => {
let mut map = BTreeMap::new();
map.insert(name, value);
Response::new(Value::Object(map))
},
Err(e) => Response::from_errors(vec![e]),
}.extensions(extensions);
if is_err {

View File

@ -1,5 +1,5 @@
use crate::parser::types::{Selection, TypeCondition};
use crate::{Context, ContextSelectionSet, PathSegment, ServerError, ServerResult, Type};
use crate::parser::types::{Name, Selection, TypeCondition};
use crate::{Context, ContextSelectionSet, PathSegment, ServerError, ServerResult, Type, Value};
use futures::{Stream, StreamExt};
use std::pin::Pin;
@ -15,11 +15,10 @@ pub trait SubscriptionType: Type {
fn create_field_stream<'a>(
&'a self,
ctx: &'a Context<'a>,
) -> Option<Pin<Box<dyn Stream<Item = ServerResult<serde_json::Value>> + Send + 'a>>>;
) -> Option<Pin<Box<dyn Stream<Item = ServerResult<Value>> + Send + 'a>>>;
}
type BoxFieldStream<'a> =
Pin<Box<dyn Stream<Item = ServerResult<(String, serde_json::Value)>> + 'a + Send>>;
type BoxFieldStream<'a> = Pin<Box<dyn Stream<Item = ServerResult<(Name, Value)>> + 'a + Send>>;
pub(crate) fn collect_subscription_streams<'a, T: SubscriptionType + Send + Sync + 'static>(
ctx: &ContextSelectionSet<'a>,
@ -40,21 +39,20 @@ pub(crate) fn collect_subscription_streams<'a, T: SubscriptionType + Send + Sync
.node
.response_key()
.node
.as_str();
.clone();
let stream = root.create_field_stream(&ctx);
if let Some(mut stream) = stream {
while let Some(item) = stream.next().await {
yield match item {
Ok(value) => Ok((field_name.to_owned(), value)),
Err(e) => Err(e.path(PathSegment::Field(field_name.to_owned()))),
Err(e) => Err(e.path(PathSegment::Field(field_name.to_string()))),
};
}
} else {
yield Err(ServerError::new(format!(r#"Cannot query field "{}" on type "{}"."#, field_name, T::type_name()))
.at(ctx.item.pos)
.path(PathSegment::Field(field_name.to_owned())));
.path(PathSegment::Field(field_name.to_string())));
return;
}
}
@ -103,7 +101,7 @@ impl<T: SubscriptionType + Send + Sync> SubscriptionType for &T {
fn create_field_stream<'a>(
&'a self,
ctx: &'a Context<'a>,
) -> Option<Pin<Box<dyn Stream<Item = ServerResult<serde_json::Value>> + Send + 'a>>> {
) -> Option<Pin<Box<dyn Stream<Item = ServerResult<Value>> + Send + 'a>>> {
T::create_field_stream(*self, ctx)
}
}

View File

@ -5,7 +5,7 @@ use crate::resolver_utils::{resolve_container, ContainerType};
use crate::types::connection::{CursorType, EmptyFields};
use crate::{
registry, Context, ContextSelectionSet, ObjectType, OutputValueType, Positioned, Result,
ServerResult, Type,
ServerResult, Type, Value,
};
use futures::{Stream, StreamExt, TryStreamExt};
use indexmap::map::IndexMap;
@ -196,7 +196,7 @@ where
EC: ObjectType + Sync + Send,
EE: ObjectType + Sync + Send,
{
async fn resolve_field(&self, ctx: &Context<'_>) -> ServerResult<Option<serde_json::Value>> {
async fn resolve_field(&self, ctx: &Context<'_>) -> ServerResult<Option<Value>> {
if ctx.item.node.name.node == "pageInfo" {
let page_info = PageInfo {
has_previous_page: self.has_previous_page,
@ -231,7 +231,7 @@ where
&self,
ctx: &ContextSelectionSet<'_>,
_field: &Positioned<Field>,
) -> ServerResult<serde_json::Value> {
) -> ServerResult<Value> {
resolve_container(ctx, self).await
}
}

View File

@ -4,7 +4,7 @@ use crate::resolver_utils::{resolve_container, ContainerType};
use crate::types::connection::CursorType;
use crate::{
registry, Context, ContextSelectionSet, ObjectType, OutputValueType, Positioned, ServerResult,
Type,
Type, Value,
};
use indexmap::map::IndexMap;
use std::borrow::Cow;
@ -113,14 +113,14 @@ where
T: OutputValueType + Send + Sync,
E: ObjectType + Sync + Send,
{
async fn resolve_field(&self, ctx: &Context<'_>) -> ServerResult<Option<serde_json::Value>> {
async fn resolve_field(&self, ctx: &Context<'_>) -> ServerResult<Option<Value>> {
if ctx.item.node.name.node == "node" {
let ctx_obj = ctx.with_selection_set(&ctx.item.node.selection_set);
return OutputValueType::resolve(&self.node, &ctx_obj, ctx.item)
.await
.map(Some);
} else if ctx.item.node.name.node == "cursor" {
return Ok(Some(self.cursor.encode_cursor().into()));
return Ok(Some(Value::String(self.cursor.encode_cursor())));
}
self.additional_fields.resolve_field(ctx).await
@ -138,7 +138,7 @@ where
&self,
ctx: &ContextSelectionSet<'_>,
_field: &Positioned<Field>,
) -> ServerResult<serde_json::Value> {
) -> ServerResult<Value> {
resolve_container(ctx, self).await
}
}

View File

@ -2,7 +2,7 @@ use crate::parser::types::Field;
use crate::resolver_utils::ContainerType;
use crate::{
registry, Context, ContextSelectionSet, ObjectType, OutputValueType, Positioned, ServerError,
ServerResult, Type,
ServerResult, Type, Value,
};
use std::borrow::Cow;
@ -48,7 +48,7 @@ impl ContainerType for EmptyMutation {
true
}
async fn resolve_field(&self, _ctx: &Context<'_>) -> ServerResult<Option<serde_json::Value>> {
async fn resolve_field(&self, _ctx: &Context<'_>) -> ServerResult<Option<Value>> {
unreachable!()
}
}
@ -59,7 +59,7 @@ impl OutputValueType for EmptyMutation {
&self,
_ctx: &ContextSelectionSet<'_>,
field: &Positioned<Field>,
) -> ServerResult<serde_json::Value> {
) -> ServerResult<Value> {
Err(ServerError::new("Schema is not configured for mutations.").at(field.pos))
}
}

View File

@ -1,4 +1,4 @@
use crate::{registry, Context, ServerError, ServerResult, SubscriptionType, Type};
use crate::{registry, Context, ServerError, ServerResult, SubscriptionType, Type, Value};
use futures::{stream, Stream};
use std::borrow::Cow;
use std::pin::Pin;
@ -34,7 +34,7 @@ impl SubscriptionType for EmptySubscription {
fn create_field_stream<'a>(
&'a self,
ctx: &'a Context<'a>,
) -> Option<Pin<Box<dyn Stream<Item = ServerResult<serde_json::Value>> + Send + 'a>>>
) -> Option<Pin<Box<dyn Stream<Item = ServerResult<Value>> + Send + 'a>>>
where
Self: Send + Sync + 'static + Sized,
{

View File

@ -1,4 +1,4 @@
use crate::{InputValueError, InputValueResult, Scalar, ScalarType, Value};
use crate::{InputValueError, InputValueResult, Number, Scalar, ScalarType, Value};
/// The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point).
#[Scalar(internal, name = "Float")]
@ -21,7 +21,7 @@ impl ScalarType for f32 {
}
fn to_value(&self) -> Value {
Value::Number(serde_json::Number::from_f64(*self as f64).unwrap())
Value::Number(Number::from_f64(*self as f64).unwrap())
}
}

View File

@ -14,7 +14,7 @@ where
match value {
Value::Object(map) => map
.into_iter()
.map(|(name, value)| Ok((name.into_string(), T::parse(Some(value))?)))
.map(|(name, value)| Ok((name.to_string(), T::parse(Some(value))?)))
.collect::<Result<_, _>>()
.map_err(InputValueError::propagate),
_ => Err(InputValueError::expected_type(value)),
@ -24,9 +24,7 @@ where
fn to_value(&self) -> Value {
let mut map = BTreeMap::new();
for (name, value) in self {
if let Ok(name) = Name::new(name.clone()) {
map.insert(name, value.to_value());
}
map.insert(Name::new(name), value.to_value());
}
Value::Object(map)
}

View File

@ -14,7 +14,7 @@ where
match value {
Value::Object(map) => map
.into_iter()
.map(|(name, value)| Ok((name.into_string(), T::parse(Some(value))?)))
.map(|(name, value)| Ok((name.to_string(), T::parse(Some(value))?)))
.collect::<Result<_, _>>()
.map_err(InputValueError::propagate),
_ => Err(InputValueError::expected_type(value)),
@ -24,9 +24,7 @@ where
fn to_value(&self) -> Value {
let mut map = BTreeMap::new();
for (name, value) in self {
if let Ok(name) = Name::new(name.clone()) {
map.insert(name, value.to_value());
}
map.insert(Name::new(name), value.to_value());
}
Value::Object(map)
}

View File

@ -51,7 +51,7 @@ impl<T: OutputValueType + Send + Sync + Ord> OutputValueType for BTreeSet<T> {
&self,
ctx: &ContextSelectionSet<'_>,
field: &Positioned<Field>,
) -> ServerResult<serde_json::Value> {
) -> ServerResult<Value> {
resolve_list(ctx, field, self).await
}
}

View File

@ -53,7 +53,7 @@ impl<T: OutputValueType + Send + Sync + Hash + Eq> OutputValueType for HashSet<T
&self,
ctx: &ContextSelectionSet<'_>,
field: &Positioned<Field>,
) -> ServerResult<serde_json::Value> {
) -> ServerResult<Value> {
resolve_list(ctx, field, self).await
}
}

View File

@ -51,7 +51,7 @@ impl<T: OutputValueType + Send + Sync> OutputValueType for LinkedList<T> {
&self,
ctx: &ContextSelectionSet<'_>,
field: &Positioned<Field>,
) -> ServerResult<serde_json::Value> {
) -> ServerResult<Value> {
resolve_list(ctx, field, self).await
}
}

View File

@ -1,6 +1,8 @@
use crate::parser::types::Field;
use crate::resolver_utils::resolve_list;
use crate::{registry, ContextSelectionSet, OutputValueType, Positioned, ServerResult, Type};
use crate::{
registry, ContextSelectionSet, OutputValueType, Positioned, ServerResult, Type, Value,
};
use std::borrow::Cow;
impl<'a, T: Type + 'a> Type for &'a [T] {
@ -24,7 +26,7 @@ impl<T: OutputValueType + Send + Sync> OutputValueType for &[T] {
&self,
ctx: &ContextSelectionSet<'_>,
field: &Positioned<Field>,
) -> ServerResult<serde_json::Value> {
) -> ServerResult<Value> {
resolve_list(ctx, field, self.iter()).await
}
}

View File

@ -46,7 +46,7 @@ impl<T: OutputValueType + Send + Sync> OutputValueType for Vec<T> {
&self,
ctx: &ContextSelectionSet<'_>,
field: &Positioned<Field>,
) -> ServerResult<serde_json::Value> {
) -> ServerResult<Value> {
resolve_list(ctx, field, self).await
}
}

View File

@ -51,7 +51,7 @@ impl<T: OutputValueType + Send + Sync> OutputValueType for VecDeque<T> {
&self,
ctx: &ContextSelectionSet<'_>,
field: &Positioned<Field>,
) -> ServerResult<serde_json::Value> {
) -> ServerResult<Value> {
resolve_list(ctx, field, self).await
}
}

View File

@ -44,11 +44,11 @@ impl<T: OutputValueType + Sync> OutputValueType for Option<T> {
&self,
ctx: &ContextSelectionSet<'_>,
field: &Positioned<Field>,
) -> ServerResult<serde_json::Value> {
) -> ServerResult<Value> {
if let Some(inner) = self {
OutputValueType::resolve(inner, ctx, field).await
} else {
Ok(serde_json::Value::Null)
Ok(Value::Null)
}
}
}

View File

@ -43,7 +43,7 @@ impl<'a> OutputValueType for &'a str {
&self,
_: &ContextSelectionSet<'_>,
_field: &Positioned<Field>,
) -> ServerResult<serde_json::Value> {
Ok((*self).into())
) -> ServerResult<Value> {
Ok(Value::String((*self).to_string()))
}
}

View File

@ -7,6 +7,7 @@ use crate::{
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::borrow::Cow;
use std::convert::TryInto;
use std::ops::{Deref, DerefMut};
/// A scalar that can represent any JSON value.
@ -95,8 +96,11 @@ impl<T: Serialize + Send + Sync> OutputValueType for OutputJson<T> {
&self,
_ctx: &ContextSelectionSet<'_>,
_field: &Positioned<Field>,
) -> ServerResult<serde_json::Value> {
Ok(serde_json::to_value(&self.0).unwrap_or_else(|_| serde_json::Value::Null))
) -> ServerResult<Value> {
Ok(serde_json::to_value(&self.0)
.ok()
.and_then(|json| json.try_into().ok())
.unwrap_or_else(|| Value::Null))
}
}

View File

@ -3,7 +3,7 @@ use crate::registry::{MetaType, Registry};
use crate::resolver_utils::resolve_container;
use crate::{
CacheControl, ContainerType, Context, ContextSelectionSet, ObjectType, OutputValueType,
Positioned, ServerResult, SimpleObject, Type,
Positioned, ServerResult, SimpleObject, Type, Value,
};
use indexmap::IndexMap;
use std::borrow::Cow;
@ -61,7 +61,7 @@ where
A: ObjectType + Send + Sync,
B: ObjectType + Send + Sync,
{
async fn resolve_field(&self, ctx: &Context<'_>) -> ServerResult<Option<serde_json::Value>> {
async fn resolve_field(&self, ctx: &Context<'_>) -> ServerResult<Option<Value>> {
match self.0.resolve_field(ctx).await {
Ok(Some(value)) => Ok(Some(value)),
Ok(None) => self.1.resolve_field(ctx).await,
@ -80,7 +80,7 @@ where
&self,
ctx: &ContextSelectionSet<'_>,
_field: &Positioned<Field>,
) -> ServerResult<serde_json::Value> {
) -> ServerResult<Value> {
resolve_container(ctx, self).await
}
}

View File

@ -25,6 +25,6 @@ pub use maybe_undefined::MaybeUndefined;
pub use merged_object::{MergedObject, MergedObjectTail};
#[cfg(feature = "string_number")]
pub use string_number::StringNumber;
pub use upload::Upload;
pub use upload::{Upload, UploadValue};
pub(crate) use query_root::QueryRoot;

View File

@ -3,7 +3,7 @@ use crate::parser::types::Field;
use crate::resolver_utils::{resolve_container, ContainerType};
use crate::{
registry, Any, Context, ContextSelectionSet, ObjectType, OutputValueType, Positioned,
ServerError, ServerResult, SimpleObject, Type,
ServerError, ServerResult, SimpleObject, Type, Value,
};
use indexmap::map::IndexMap;
@ -81,7 +81,7 @@ impl<T: Type> Type for QueryRoot<T> {
#[async_trait::async_trait]
impl<T: ObjectType + Send + Sync> ContainerType for QueryRoot<T> {
async fn resolve_field(&self, ctx: &Context<'_>) -> ServerResult<Option<serde_json::Value>> {
async fn resolve_field(&self, ctx: &Context<'_>) -> ServerResult<Option<Value>> {
if ctx.item.node.name.node == "__schema" {
if self.disable_introspection {
return Ok(None);
@ -122,7 +122,7 @@ impl<T: ObjectType + Send + Sync> ContainerType for QueryRoot<T> {
.ok_or_else(|| ServerError::new("Entity not found.").at(ctx.item.pos))?,
);
}
return Ok(Some(res.into()));
return Ok(Some(Value::List(res)));
} else if ctx.item.node.name.node == "_service" {
let ctx_obj = ctx.with_selection_set(&ctx.item.node.selection_set);
return OutputValueType::resolve(
@ -146,7 +146,7 @@ impl<T: ObjectType + Send + Sync> OutputValueType for QueryRoot<T> {
&self,
ctx: &ContextSelectionSet<'_>,
_field: &Positioned<Field>,
) -> ServerResult<serde_json::Value> {
) -> ServerResult<Value> {
resolve_container(ctx, self).await
}
}

View File

@ -1,8 +1,54 @@
use crate::parser::types::UploadValue;
use crate::{registry, InputValueError, InputValueResult, InputValueType, Type, Value};
use crate::{registry, Context, InputValueError, InputValueResult, InputValueType, Type, Value};
use futures::AsyncRead;
use std::borrow::Cow;
use std::fs::File;
use std::io::Read;
/// A file upload value.
pub struct UploadValue {
/// The name of the file.
pub filename: String,
/// The content type of the file.
pub content_type: Option<String>,
/// The file data.
pub content: File,
}
impl UploadValue {
/// Attempt to clone the upload value. This type's `Clone` implementation simply calls this and
/// panics on failure.
///
/// # Errors
///
/// Fails if cloning the inner `File` fails.
pub fn try_clone(&self) -> std::io::Result<Self> {
Ok(Self {
filename: self.filename.clone(),
content_type: self.content_type.clone(),
content: self.content.try_clone()?,
})
}
/// Convert to a `Read`.
///
/// **Note**: this is a *synchronous/blocking* reader.
pub fn into_read(self) -> impl Read + Sync + Send + 'static {
self.content
}
#[cfg(feature = "unblock")]
#[cfg_attr(feature = "nightly", doc(cfg(feature = "unblock")))]
/// Convert to a `AsyncRead`.
pub fn into_async_read(self) -> impl AsyncRead + Sync + Send + 'static {
blocking::Unblock::new(self.content)
}
/// Returns the size of the file, in bytes.
pub fn size(&self) -> std::io::Result<u64> {
self.content.metadata().map(|meta| meta.len())
}
}
/// Uploaded file
///
/// **Reference:** <https://github.com/jaydenseric/graphql-multipart-request-spec>
@ -23,8 +69,8 @@ use std::io::Read;
///
/// #[Object]
/// impl MutationRoot {
/// async fn upload(&self, file: Upload) -> bool {
/// println!("upload: filename={}", file.filename());
/// async fn upload(&self, ctx: &Context<'_>, file: Upload) -> bool {
/// println!("upload: filename={}", file.value(ctx).unwrap().filename);
/// true
/// }
/// }
@ -42,36 +88,12 @@ use std::io::Read;
/// --form 'map={ "0": ["variables.file"] }' \
/// --form '0=@myFile.txt'
/// ```
pub struct Upload(UploadValue);
pub struct Upload(usize);
impl Upload {
/// Filename
pub fn filename(&self) -> &str {
self.0.filename.as_str()
}
/// Content type, such as `application/json`, `image/jpg` ...
pub fn content_type(&self) -> Option<&str> {
self.0.content_type.as_deref()
}
/// Returns the size of the file, in bytes.
pub fn size(&self) -> std::io::Result<u64> {
self.0.content.metadata().map(|meta| meta.len())
}
/// Convert to a `Read`.
///
/// **Note**: this is a *synchronous/blocking* reader.
pub fn into_read(self) -> impl Read + Sync + Send + 'static {
self.0.content
}
#[cfg(feature = "unblock")]
#[cfg_attr(feature = "nightly", doc(cfg(feature = "unblock")))]
/// Convert to a `AsyncRead`.
pub fn into_async_read(self) -> impl futures::AsyncRead + Sync + Send + 'static {
blocking::Unblock::new(self.0.content)
/// Get the upload value.
pub fn value(&self, ctx: &Context<'_>) -> std::io::Result<UploadValue> {
ctx.query_env.uploads[self.0].try_clone()
}
}
@ -84,19 +106,21 @@ impl Type for Upload {
registry.create_type::<Self, _>(|_| registry::MetaType::Scalar {
name: Self::type_name().to_string(),
description: None,
is_valid: |value| matches!(value, Value::Upload(_)),
is_valid: |value| matches!(value, Value::String(_)),
})
}
}
impl InputValueType for Upload {
fn parse(value: Option<Value>) -> InputValueResult<Self> {
const PREFIX: &str = "#__graphql_file__:";
let value = value.unwrap_or_default();
if let Value::Upload(upload) = value {
Ok(Upload(upload))
} else {
Err(InputValueError::expected_type(value))
if let Value::String(s) = &value {
if s.starts_with(PREFIX) {
return Ok(Upload(s[PREFIX.len()..].parse::<usize>().unwrap()));
}
}
Err(InputValueError::expected_type(value))
}
fn to_value(&self) -> Value {

View File

@ -7,26 +7,20 @@ pub async fn test_error_extensions() {
#[Object]
impl Query {
async fn extend_err(&self) -> Result<i32> {
Err("my error".extend_with(|err| {
serde_json::json!({
"msg":err,
"code":100,
})
Err("my error".extend_with(|err, e| {
e.set("msg", err.to_string());
e.set("code", 100);
}))
}
async fn extend_result(&self) -> Result<i32> {
Err(Error::from("my error"))
.extend_err(|_| {
serde_json::json!({
"msg": "my error",
"code": 100
})
.extend_err(|_, e| {
e.set("msg", "my error");
e.set("code", 100);
})
.extend_err(|_| {
serde_json::json!({
"code2": 20
})
.extend_err(|_, e| {
e.set("code2", 20);
})
}
}

View File

@ -126,18 +126,19 @@ pub async fn test_field_features() {
stream
.next()
.await
.map(|resp| resp.into_result().unwrap().data),
Some(serde_json::json!({
.map(|resp| resp.into_result().unwrap().data)
.unwrap(),
serde_json::json!({
"values": 10
}))
})
);
let mut stream = schema.execute_stream("subscription { valuesBson }").boxed();
assert_eq!(
stream.next().await.map(|resp| resp.data),
Some(serde_json::json!({
stream.next().await.map(|resp| resp.data).unwrap(),
serde_json::json!({
"valuesBson": 10
}))
})
);
assert_eq!(

View File

@ -1689,10 +1689,9 @@ pub async fn test_input_validator_variable() {
let validator_length = 6;
for case in &test_cases {
let mut variables = Variables::default();
variables.0.insert(
Name::new("id".to_owned()).unwrap(),
Value::String(case.to_string()),
);
variables
.0
.insert(Name::new("id"), Value::String(case.to_string()));
let field_query = "query($id: String!) {fieldParameter(id: $id)}";
let object_query = "query($id: String!) {inputObject(input: {id: $id})}";

View File

@ -86,11 +86,11 @@ pub async fn test_list_type() {
);
let mut res = schema.execute(&query).await.data;
if let serde_json::Value::Object(obj) = &mut res {
if let Value::Object(obj) = &mut res {
if let Some(value_hash_set) = obj.get_mut("valueHashSet") {
if let serde_json::Value::Array(array) = value_hash_set {
if let Value::List(array) = value_hash_set {
array.sort_by(|a, b| {
if let (serde_json::Value::Number(a), serde_json::Value::Number(b)) = (a, b) {
if let (Value::Number(a), Value::Number(b)) = (a, b) {
if let (Some(a), Some(b)) = (a.as_i64(), b.as_i64()) {
return a.cmp(&b);
}

View File

@ -196,10 +196,10 @@ pub async fn test_merged_subscription() {
.boxed();
for i in 0i32..10 {
assert_eq!(
Some(serde_json::json!({
serde_json::json!({
"events1": i,
})),
stream.next().await
}),
stream.next().await.unwrap()
);
}
assert!(stream.next().await.is_none());
@ -212,10 +212,10 @@ pub async fn test_merged_subscription() {
.boxed();
for i in 10i32..20 {
assert_eq!(
Some(serde_json::json!({
serde_json::json!({
"events2": i,
})),
stream.next().await
}),
stream.next().await.unwrap()
);
}
assert!(stream.next().await.is_none());

View File

@ -68,8 +68,8 @@ pub async fn test_input_value_custom_error() {
.boxed();
for i in 0..10 {
assert_eq!(
Some(Ok(serde_json::json!({ "type": i }))),
stream.next().await
serde_json::json!({ "type": i }),
stream.next().await.unwrap().unwrap()
);
}
assert!(stream.next().await.is_none());

View File

@ -36,8 +36,8 @@ pub async fn test_subscription() {
.boxed();
for i in 10..20 {
assert_eq!(
Some(serde_json::json!({ "values": i })),
stream.next().await
serde_json::json!({ "values": i }),
stream.next().await.unwrap()
);
}
assert!(stream.next().await.is_none());
@ -50,8 +50,8 @@ pub async fn test_subscription() {
.boxed();
for i in 10..20 {
assert_eq!(
Some(serde_json::json!({ "events": {"a": i, "b": i * 10} })),
stream.next().await
serde_json::json!({ "events": {"a": i, "b": i * 10} }),
stream.next().await.unwrap()
);
}
assert!(stream.next().await.is_none());
@ -96,12 +96,12 @@ pub async fn test_subscription_with_ctx_data() {
.map(|resp| resp.data)
.boxed();
assert_eq!(
Some(serde_json::json!({ "values": 100 })),
stream.next().await
serde_json::json!({ "values": 100 }),
stream.next().await.unwrap()
);
assert_eq!(
Some(serde_json::json!({ "objects": { "value": 100 } })),
stream.next().await
serde_json::json!({ "objects": { "value": 100 } }),
stream.next().await.unwrap()
);
assert!(stream.next().await.is_none());
}
@ -138,8 +138,8 @@ pub async fn test_subscription_with_token() {
.map(|resp| resp.into_result().unwrap().data)
.boxed();
assert_eq!(
Some(serde_json::json!({ "values": 100 })),
stream.next().await
serde_json::json!({ "values": 100 }),
stream.next().await.unwrap()
);
assert!(stream.next().await.is_none());
}
@ -197,8 +197,8 @@ pub async fn test_subscription_inline_fragment() {
.boxed();
for i in 10..20 {
assert_eq!(
Some(serde_json::json!({ "events": {"a": i, "b": i * 10} })),
stream.next().await
serde_json::json!({ "events": {"a": i, "b": i * 10} }),
stream.next().await.unwrap()
);
}
assert!(stream.next().await.is_none());
@ -252,8 +252,8 @@ pub async fn test_subscription_fragment() {
.boxed();
for i in 10i32..20 {
assert_eq!(
Some(serde_json::json!({ "events": {"a": i, "b": i * 10} })),
stream.next().await
serde_json::json!({ "events": {"a": i, "b": i * 10} }),
stream.next().await.unwrap()
);
}
assert!(stream.next().await.is_none());
@ -308,8 +308,8 @@ pub async fn test_subscription_fragment2() {
.boxed();
for i in 10..20 {
assert_eq!(
Some(serde_json::json!({ "events": {"a": i, "b": i * 10} })),
stream.next().await
serde_json::json!({ "events": {"a": i, "b": i * 10} }),
stream.next().await.unwrap()
);
}
assert!(stream.next().await.is_none());
@ -354,8 +354,8 @@ pub async fn test_subscription_error() {
.boxed();
for i in 0i32..5 {
assert_eq!(
Some(Ok(serde_json::json!({ "events": { "value": i } }))),
stream.next().await
serde_json::json!({ "events": { "value": i } }),
stream.next().await.unwrap().unwrap()
);
}
assert_eq!(
@ -405,8 +405,8 @@ pub async fn test_subscription_fieldresult() {
.boxed();
for i in 0i32..5 {
assert_eq!(
Some(Ok(serde_json::json!({ "values": i }))),
stream.next().await
serde_json::json!({ "values": i }),
stream.next().await.unwrap().unwrap()
);
}
assert_eq!(

View File

@ -1,4 +1,5 @@
use async_graphql::*;
use std::collections::HashMap;
#[async_std::test]
pub async fn test_variables() {
@ -271,3 +272,34 @@ pub async fn test_variables_enum() {
})
);
}
#[async_std::test]
pub async fn test_variables_json() {
struct QueryRoot;
#[Object]
impl QueryRoot {
pub async fn value(&self, value: Json<HashMap<String, i32>>) -> i32 {
*value.get("a-b").unwrap()
}
}
let schema = Schema::new(QueryRoot, EmptyMutation, EmptySubscription);
let query = Request::new(
r#"
query QueryWithVariables($value: JSON) {
value(value: $value)
}
"#,
)
.variables(Variables::from_json(serde_json::json!({
"value": { "a-b": 123 },
})));
assert_eq!(
schema.execute(query).await.into_result().unwrap().data,
serde_json::json!({
"value": 123,
})
);
}