Merge branch 'upload-stream'
This commit is contained in:
commit
2d1eb41598
|
@ -35,7 +35,7 @@ chrono = "0.4.10"
|
|||
slab = "0.4.2"
|
||||
once_cell = "1.3.1"
|
||||
itertools = "0.9.0"
|
||||
tempdir = "0.3.7"
|
||||
tempfile = "3.1.0"
|
||||
httparse = "1.3.4"
|
||||
mime = "0.3.16"
|
||||
http = "0.2.1"
|
||||
|
|
|
@ -156,7 +156,7 @@ pub fn generate(enum_args: &args::Enum, input: &DeriveInput) -> Result<TokenStre
|
|||
}
|
||||
|
||||
impl #crate_name::InputValueType for #ident {
|
||||
fn parse(value: &#crate_name::Value) -> #crate_name::InputValueResult<Self> {
|
||||
fn parse(value: #crate_name::Value) -> #crate_name::InputValueResult<Self> {
|
||||
#crate_name::EnumType::parse_enum(value)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -81,17 +81,17 @@ pub fn generate(object_args: &args::InputObject, input: &DeriveInput) -> Result<
|
|||
get_fields.push(quote! {
|
||||
let #ident:#ty = {
|
||||
match obj.get(#name) {
|
||||
Some(value) => #crate_name::InputValueType::parse(value)?,
|
||||
Some(value) => #crate_name::InputValueType::parse(value.clone())?,
|
||||
None => {
|
||||
let default = #default_repr;
|
||||
#crate_name::InputValueType::parse(&default)?
|
||||
#crate_name::InputValueType::parse(default)?
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
} else {
|
||||
get_fields.push(quote! {
|
||||
let #ident:#ty = #crate_name::InputValueType::parse(obj.get(#name).unwrap_or(&#crate_name::Value::Null))?;
|
||||
let #ident:#ty = #crate_name::InputValueType::parse(obj.get(#name).cloned().unwrap_or(#crate_name::Value::Null))?;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -129,14 +129,14 @@ pub fn generate(object_args: &args::InputObject, input: &DeriveInput) -> Result<
|
|||
}
|
||||
|
||||
impl #crate_name::InputValueType for #ident {
|
||||
fn parse(value: &#crate_name::Value) -> #crate_name::InputValueResult<Self> {
|
||||
fn parse(value: #crate_name::Value) -> #crate_name::InputValueResult<Self> {
|
||||
use #crate_name::Type;
|
||||
|
||||
if let #crate_name::Value::Object(obj) = value {
|
||||
if let #crate_name::Value::Object(obj) = &value {
|
||||
#(#get_fields)*
|
||||
Ok(Self { #(#fields),* })
|
||||
} else {
|
||||
Err(#crate_name::InputValueError::ExpectedType)
|
||||
Err(#crate_name::InputValueError::ExpectedType(value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,7 +69,7 @@ pub fn generate(interface_args: &args::Interface, input: &DeriveInput) -> Result
|
|||
};
|
||||
if let Type::Path(p) = &field.ty {
|
||||
// This validates that the field type wasn't already used
|
||||
if enum_items.insert(p) == false {
|
||||
if !enum_items.insert(p) {
|
||||
return Err(Error::new_spanned(
|
||||
field,
|
||||
"This type already used in another variant",
|
||||
|
|
|
@ -165,7 +165,7 @@ pub fn Scalar(args: TokenStream, input: TokenStream) -> TokenStream {
|
|||
}
|
||||
|
||||
impl #generic #crate_name::InputValueType for #self_ty #where_clause {
|
||||
fn parse(value: &#crate_name::Value) -> #crate_name::InputValueResult<Self> {
|
||||
fn parse(value: #crate_name::Value) -> #crate_name::InputValueResult<Self> {
|
||||
<#self_ty as #crate_name::ScalarType>::parse(value)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -126,7 +126,7 @@ pub fn generate(object_args: &args::Object, item_impl: &mut ItemImpl) -> Result<
|
|||
});
|
||||
key_getter.push(quote! {
|
||||
params.get(#name).and_then(|value| {
|
||||
let value: Option<#ty> = #crate_name::InputValueType::parse(value).ok();
|
||||
let value: Option<#ty> = #crate_name::InputValueType::parse(value.clone()).ok();
|
||||
value
|
||||
})
|
||||
});
|
||||
|
|
|
@ -61,7 +61,7 @@ pub fn generate(union_args: &args::Interface, input: &DeriveInput) -> Result<Tok
|
|||
};
|
||||
if let Type::Path(p) = &field.ty {
|
||||
// This validates that the field type wasn't already used
|
||||
if enum_items.insert(p) == false {
|
||||
if !enum_items.insert(p) {
|
||||
return Err(Error::new_spanned(
|
||||
field,
|
||||
"This type already used in another variant",
|
||||
|
|
|
@ -58,6 +58,7 @@ pub fn build_value_repr(crate_name: &TokenStream, value: &Value) -> TokenStream
|
|||
}
|
||||
}
|
||||
}
|
||||
Value::Upload(_) => quote! { #crate_name::Value::Null },
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,4 +10,4 @@ mod value;
|
|||
|
||||
pub use pos::{Pos, Positioned};
|
||||
pub use query_parser::{parse_query, parse_value, Error, Result};
|
||||
pub use value::Value;
|
||||
pub use value::{UploadValue, Value};
|
||||
|
|
|
@ -1,5 +1,29 @@
|
|||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::fmt::Formatter;
|
||||
use std::fs::File;
|
||||
|
||||
pub struct UploadValue {
|
||||
pub filename: String,
|
||||
pub content_type: Option<String>,
|
||||
pub content: File,
|
||||
}
|
||||
|
||||
impl fmt::Debug for UploadValue {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "Upload({})", self.filename)
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for UploadValue {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
filename: self.filename.clone(),
|
||||
content_type: self.content_type.clone(),
|
||||
content: self.content.try_clone().unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a GraphQL value
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -14,6 +38,7 @@ pub enum Value {
|
|||
Enum(String),
|
||||
List(Vec<Value>),
|
||||
Object(BTreeMap<String, Value>),
|
||||
Upload(UploadValue),
|
||||
}
|
||||
|
||||
impl PartialEq for Value {
|
||||
|
@ -54,6 +79,7 @@ impl PartialEq for Value {
|
|||
}
|
||||
true
|
||||
}
|
||||
(Upload(a), Upload(b)) => a.filename == b.filename,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
@ -112,6 +138,7 @@ impl fmt::Display for Value {
|
|||
}
|
||||
write!(f, "}}")
|
||||
}
|
||||
Value::Upload(_) => write!(f, "null"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -136,6 +163,7 @@ impl From<Value> for serde_json::Value {
|
|||
.map(|(name, value)| (name, value.into()))
|
||||
.collect(),
|
||||
),
|
||||
Value::Upload(_) => serde_json::Value::Null,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
mod test_utils;
|
||||
use serde_json::json;
|
||||
use smol::{Task, Timer};
|
||||
use std::io::Read;
|
||||
use std::time::Duration;
|
||||
use tide::Request;
|
||||
|
||||
|
@ -167,21 +168,18 @@ fn upload() -> Result<()> {
|
|||
#[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);
|
||||
println!("single_upload: path={:?}", file.path);
|
||||
|
||||
let file_path = file.path.clone();
|
||||
let content = Task::blocking(async move { std::fs::read_to_string(file_path) })
|
||||
.await
|
||||
.ok();
|
||||
assert_eq!(content, Some("test\r\n".to_owned()));
|
||||
println!("single_upload: filename={}", file.filename());
|
||||
println!("single_upload: content_type={:?}", file.content_type());
|
||||
|
||||
let file_info = FileInfo {
|
||||
filename: file.filename,
|
||||
mime_type: file.content_type,
|
||||
filename: file.filename().into(),
|
||||
mime_type: file.content_type().map(ToString::to_string),
|
||||
};
|
||||
|
||||
let mut content = String::new();
|
||||
file.into_read().read_to_string(&mut content).ok();
|
||||
assert_eq!(content, "test\r\n".to_owned());
|
||||
|
||||
file_info
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ impl ScalarType for StringNumber {
|
|||
"StringNumber"
|
||||
}
|
||||
|
||||
fn parse(value: &Value) -> InputValueResult<Self> {
|
||||
fn parse(value: Value) -> InputValueResult<Self> {
|
||||
if let Value::String(value) = value {
|
||||
// Parse the integer value
|
||||
value.parse().map(StringNumber)?
|
||||
|
|
|
@ -19,7 +19,7 @@ impl ScalarType for StringNumber {
|
|||
"StringNumber"
|
||||
}
|
||||
|
||||
fn parse(value: &Value) -> InputValueResult<Self> {
|
||||
fn parse(value: Value) -> InputValueResult<Self> {
|
||||
if let Value::String(value) = value {
|
||||
// 解析整数
|
||||
value.parse().map(StringNumber)?
|
||||
|
|
19
src/base.rs
19
src/base.rs
|
@ -52,7 +52,7 @@ pub trait Type {
|
|||
/// Represents a GraphQL input value
|
||||
pub trait InputValueType: Type + Sized {
|
||||
/// Parse from `Value`
|
||||
fn parse(value: &Value) -> InputValueResult<Self>;
|
||||
fn parse(value: Value) -> InputValueResult<Self>;
|
||||
}
|
||||
|
||||
/// Represents a GraphQL output value
|
||||
|
@ -128,11 +128,11 @@ pub trait InputObjectType: InputValueType {}
|
|||
/// "MyInt"
|
||||
/// }
|
||||
///
|
||||
/// fn parse(value: &Value) -> InputValueResult<Self> {
|
||||
/// fn parse(value: Value) -> InputValueResult<Self> {
|
||||
/// if let Value::Int(n) = value {
|
||||
/// Ok(MyInt(*n as i32))
|
||||
/// Ok(MyInt(n as i32))
|
||||
/// } else {
|
||||
/// Err(InputValueError::ExpectedType)
|
||||
/// Err(InputValueError::ExpectedType(value))
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
|
@ -151,16 +151,13 @@ pub trait ScalarType: Sized + Send {
|
|||
}
|
||||
|
||||
/// Parse a scalar value, return `Some(Self)` if successful, otherwise return `None`.
|
||||
fn parse(value: &Value) -> InputValueResult<Self>;
|
||||
fn parse(value: Value) -> InputValueResult<Self>;
|
||||
|
||||
/// Checks for a valid scalar value.
|
||||
///
|
||||
/// The default implementation is to try to parse it, and in some cases you can implement this on your own to improve performance.
|
||||
fn is_valid(value: &Value) -> bool {
|
||||
match Self::parse(value) {
|
||||
Ok(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
/// Implementing this function can find incorrect input values during the verification phase, which can improve performance.
|
||||
fn is_valid(_value: &Value) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Convert the scalar value to json value.
|
||||
|
|
|
@ -3,11 +3,12 @@ use crate::parser::ast::{Directive, Field, FragmentDefinition, SelectionSet, Var
|
|||
use crate::registry::Registry;
|
||||
use crate::{InputValueType, QueryError, Result, Schema, Type};
|
||||
use crate::{Pos, Positioned, Value};
|
||||
use async_graphql_parser::UploadValue;
|
||||
use fnv::FnvHashMap;
|
||||
use std::any::{Any, TypeId};
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::fs::File;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
@ -56,9 +57,9 @@ impl Variables {
|
|||
pub(crate) fn set_upload(
|
||||
&mut self,
|
||||
var_path: &str,
|
||||
filename: &str,
|
||||
content_type: Option<&str>,
|
||||
path: &Path,
|
||||
filename: String,
|
||||
content_type: Option<String>,
|
||||
content: File,
|
||||
) {
|
||||
let mut it = var_path.split('.').peekable();
|
||||
|
||||
|
@ -76,7 +77,11 @@ impl Variables {
|
|||
if let Value::List(ls) = current {
|
||||
if let Some(value) = ls.get_mut(idx as usize) {
|
||||
if !has_next {
|
||||
*value = Value::String(file_string(filename, content_type, path));
|
||||
*value = Value::Upload(UploadValue {
|
||||
filename,
|
||||
content_type,
|
||||
content,
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
current = value;
|
||||
|
@ -88,7 +93,11 @@ impl Variables {
|
|||
} else if let Value::Object(obj) = current {
|
||||
if let Some(value) = obj.get_mut(s) {
|
||||
if !has_next {
|
||||
*value = Value::String(file_string(filename, content_type, path));
|
||||
*value = Value::Upload(UploadValue {
|
||||
filename,
|
||||
content_type,
|
||||
content,
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
current = value;
|
||||
|
@ -101,14 +110,6 @@ impl Variables {
|
|||
}
|
||||
}
|
||||
|
||||
fn file_string(filename: &str, content_type: Option<&str>, path: &Path) -> String {
|
||||
if let Some(content_type) = content_type {
|
||||
format!("file:{}:{}|", filename, content_type) + &path.display().to_string()
|
||||
} else {
|
||||
format!("file:{}|", filename) + &path.display().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
/// Schema/Context data
|
||||
pub struct Data(FnvHashMap<TypeId, Box<dyn Any + Sync + Send>>);
|
||||
|
@ -411,16 +412,12 @@ impl<'a, T> ContextBase<'a, T> {
|
|||
if directive.name.as_str() == "skip" {
|
||||
if let Some(value) = directive.get_argument("if") {
|
||||
match InputValueType::parse(
|
||||
&self.resolve_input_value(value.clone_inner(), value.position())?,
|
||||
self.resolve_input_value(value.clone_inner(), value.position())?,
|
||||
) {
|
||||
Ok(true) => return Ok(true),
|
||||
Ok(false) => {}
|
||||
Err(err) => {
|
||||
return Err(err.into_error(
|
||||
value.pos,
|
||||
bool::qualified_type_name(),
|
||||
value.clone_inner(),
|
||||
))
|
||||
return Err(err.into_error(value.pos, bool::qualified_type_name()))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -434,16 +431,12 @@ impl<'a, T> ContextBase<'a, T> {
|
|||
} else if directive.name.as_str() == "include" {
|
||||
if let Some(value) = directive.get_argument("if") {
|
||||
match InputValueType::parse(
|
||||
&self.resolve_input_value(value.clone_inner(), value.position())?,
|
||||
self.resolve_input_value(value.clone_inner(), value.position())?,
|
||||
) {
|
||||
Ok(false) => return Ok(true),
|
||||
Ok(true) => {}
|
||||
Err(err) => {
|
||||
return Err(err.into_error(
|
||||
value.pos,
|
||||
bool::qualified_type_name(),
|
||||
value.clone_inner(),
|
||||
))
|
||||
return Err(err.into_error(value.pos, bool::qualified_type_name()))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@ -499,18 +492,18 @@ impl<'a> ContextBase<'a, &'a Positioned<Field>> {
|
|||
Some(value) => {
|
||||
let pos = value.position();
|
||||
let value = self.resolve_input_value(value.into_inner(), pos)?;
|
||||
match InputValueType::parse(&value) {
|
||||
match InputValueType::parse(value) {
|
||||
Ok(res) => Ok(res),
|
||||
Err(err) => Err(err.into_error(pos, T::qualified_type_name(), value)),
|
||||
Err(err) => Err(err.into_error(pos, T::qualified_type_name())),
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let value = default();
|
||||
match InputValueType::parse(&value) {
|
||||
match InputValueType::parse(value) {
|
||||
Ok(res) => Ok(res),
|
||||
Err(err) => {
|
||||
// The default value has no valid location.
|
||||
Err(err.into_error(Pos::default(), T::qualified_type_name(), value))
|
||||
Err(err.into_error(Pos::default(), T::qualified_type_name()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ pub enum InputValueError {
|
|||
Custom(String),
|
||||
|
||||
/// The type of input value does not match the expectation.
|
||||
ExpectedType,
|
||||
ExpectedType(Value),
|
||||
}
|
||||
|
||||
impl<T: Display> From<T> for InputValueError {
|
||||
|
@ -19,14 +19,14 @@ impl<T: Display> From<T> for InputValueError {
|
|||
|
||||
impl InputValueError {
|
||||
#[allow(missing_docs)]
|
||||
pub fn into_error(self, pos: Pos, expected_type: String, value: Value) -> Error {
|
||||
pub fn into_error(self, pos: Pos, expected_type: String) -> Error {
|
||||
match self {
|
||||
InputValueError::Custom(reason) => Error::Query {
|
||||
pos,
|
||||
path: None,
|
||||
err: QueryError::ParseInputValue { reason },
|
||||
},
|
||||
InputValueError::ExpectedType => Error::Query {
|
||||
InputValueError::ExpectedType(value) => Error::Query {
|
||||
pos,
|
||||
path: None,
|
||||
err: QueryError::ExpectedInputType {
|
||||
|
|
|
@ -32,7 +32,6 @@ where
|
|||
let mut multipart = Multipart::parse(
|
||||
self.1,
|
||||
boundary.as_str(),
|
||||
opts.temp_dir.as_deref(),
|
||||
opts.max_file_size,
|
||||
opts.max_num_files,
|
||||
)
|
||||
|
@ -59,14 +58,14 @@ where
|
|||
if let Some(name) = &part.name {
|
||||
if let Some(var_paths) = map.remove(name) {
|
||||
for var_path in var_paths {
|
||||
if let (Some(filename), PartData::File(path)) =
|
||||
if let (Some(filename), PartData::File(content)) =
|
||||
(&part.filename, &part.data)
|
||||
{
|
||||
builder.set_upload(
|
||||
&var_path,
|
||||
&filename,
|
||||
part.content_type.as_deref(),
|
||||
path,
|
||||
filename.clone(),
|
||||
part.content_type.clone(),
|
||||
content.try_clone().unwrap(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -78,10 +77,6 @@ where
|
|||
return Err(ParseRequestError::MissingFiles);
|
||||
}
|
||||
|
||||
if let Some(temp_dir) = multipart.temp_dir {
|
||||
builder.set_files_holder(temp_dir);
|
||||
}
|
||||
|
||||
Ok(builder)
|
||||
} else {
|
||||
let mut data = Vec::new();
|
||||
|
|
|
@ -5,16 +5,14 @@ use futures::{AsyncBufRead, AsyncRead};
|
|||
use http::{header::HeaderName, HeaderMap, HeaderValue};
|
||||
use itertools::Itertools;
|
||||
use std::fs::File;
|
||||
use std::io::{Cursor, Read, Write};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::io::{Cursor, Read, Seek, SeekFrom, Write};
|
||||
use std::str::FromStr;
|
||||
use tempdir::TempDir;
|
||||
|
||||
const MAX_HEADERS: usize = 16;
|
||||
|
||||
pub enum PartData {
|
||||
Bytes(Vec<u8>),
|
||||
File(PathBuf),
|
||||
File(File),
|
||||
}
|
||||
|
||||
pub struct Part {
|
||||
|
@ -26,10 +24,10 @@ pub struct Part {
|
|||
}
|
||||
|
||||
impl Part {
|
||||
pub fn create_reader<'a>(&'a self) -> Result<Box<dyn Read + 'a>, std::io::Error> {
|
||||
let reader: Box<dyn Read> = match &self.data {
|
||||
pub fn create_reader(self) -> Result<Box<dyn Read>, std::io::Error> {
|
||||
let reader: Box<dyn Read> = match self.data {
|
||||
PartData::Bytes(bytes) => Box::new(Cursor::new(bytes)),
|
||||
PartData::File(path) => Box::new(File::open(path)?),
|
||||
PartData::File(content) => Box::new(content),
|
||||
};
|
||||
Ok(reader)
|
||||
}
|
||||
|
@ -55,7 +53,6 @@ impl ContentDisposition {
|
|||
}
|
||||
|
||||
pub struct Multipart {
|
||||
pub temp_dir: Option<TempDir>,
|
||||
pub parts: Vec<Part>,
|
||||
}
|
||||
|
||||
|
@ -63,12 +60,10 @@ impl Multipart {
|
|||
pub async fn parse<R: AsyncRead + Unpin>(
|
||||
reader: R,
|
||||
boundary: &str,
|
||||
temp_dir_in: Option<&Path>,
|
||||
max_file_size: Option<usize>,
|
||||
max_num_files: Option<usize>,
|
||||
) -> Result<Multipart, ParseRequestError> {
|
||||
let mut reader = BufReader::new(reader);
|
||||
let mut temp_dir = None;
|
||||
let mut parts = Vec::new();
|
||||
let boundary = format!("--{}", boundary);
|
||||
let max_num_files = max_num_files.unwrap_or(std::usize::MAX);
|
||||
|
@ -79,17 +74,7 @@ impl Multipart {
|
|||
reader.except_token(boundary.as_bytes()).await?;
|
||||
reader.except_token(b"\r\n").await?;
|
||||
let headers = Self::parse_headers(&mut reader).await?;
|
||||
parts.push(
|
||||
Self::parse_body(
|
||||
&mut reader,
|
||||
&headers,
|
||||
&mut temp_dir,
|
||||
temp_dir_in,
|
||||
max_file_size,
|
||||
&boundary,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
parts.push(Self::parse_body(&mut reader, &headers, max_file_size, &boundary).await?);
|
||||
Multipart::check_max_num_files(&mut parts, max_num_files, &mut current_num_files)?;
|
||||
|
||||
// next parts
|
||||
|
@ -100,21 +85,11 @@ impl Multipart {
|
|||
}
|
||||
|
||||
let headers = Self::parse_headers(&mut reader).await?;
|
||||
parts.push(
|
||||
Self::parse_body(
|
||||
&mut reader,
|
||||
&headers,
|
||||
&mut temp_dir,
|
||||
temp_dir_in,
|
||||
max_file_size,
|
||||
&boundary,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
parts.push(Self::parse_body(&mut reader, &headers, max_file_size, &boundary).await?);
|
||||
Multipart::check_max_num_files(&mut parts, max_num_files, &mut current_num_files)?;
|
||||
}
|
||||
|
||||
Ok(Multipart { temp_dir, parts })
|
||||
Ok(Multipart { parts })
|
||||
}
|
||||
|
||||
fn check_max_num_files(
|
||||
|
@ -171,8 +146,6 @@ impl Multipart {
|
|||
async fn parse_body<R: AsyncBufRead + Unpin>(
|
||||
mut reader: R,
|
||||
headers: &HeaderMap,
|
||||
temp_dir: &mut Option<TempDir>,
|
||||
temp_dir_in: Option<&Path>,
|
||||
max_file_size: usize,
|
||||
boundary: &str,
|
||||
) -> Result<Part, ParseRequestError> {
|
||||
|
@ -193,17 +166,9 @@ impl Multipart {
|
|||
let mut state = ReadUntilState::default();
|
||||
let mut total_size = 0;
|
||||
|
||||
let part_data = if let Some(filename) = &content_disposition.filename {
|
||||
if temp_dir.is_none() {
|
||||
if let Some(temp_dir_in) = temp_dir_in {
|
||||
*temp_dir = Some(TempDir::new_in(temp_dir_in, "async-graphql")?);
|
||||
} else {
|
||||
*temp_dir = Some(TempDir::new("async-graphql")?);
|
||||
}
|
||||
}
|
||||
let temp_dir = temp_dir.as_mut().unwrap();
|
||||
let path = temp_dir.path().join(filename);
|
||||
let mut file = File::create(&path)?;
|
||||
let part_data = if content_disposition.filename.is_some() {
|
||||
// Create a temporary file.
|
||||
let mut file = tempfile::tempfile()?;
|
||||
|
||||
loop {
|
||||
let (size, found) = reader
|
||||
|
@ -218,7 +183,8 @@ impl Multipart {
|
|||
break;
|
||||
}
|
||||
}
|
||||
PartData::File(path)
|
||||
file.seek(SeekFrom::Start(0))?;
|
||||
PartData::File(file)
|
||||
} else {
|
||||
let mut body = Vec::new();
|
||||
|
||||
|
@ -273,10 +239,9 @@ mod tests {
|
|||
Content-Type: text/plain; charset=utf-8\r\n\r\n\
|
||||
data\
|
||||
--abbc761f78ff4d7cb7573b5a23f96ef0--\r\n";
|
||||
let multipart =
|
||||
Multipart::parse(data, "abbc761f78ff4d7cb7573b5a23f96ef0", None, None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let multipart = Multipart::parse(data, "abbc761f78ff4d7cb7573b5a23f96ef0", None, None)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(multipart.parts.len(), 2);
|
||||
|
||||
let part_1 = &multipart.parts[0];
|
||||
|
@ -307,35 +272,23 @@ mod tests {
|
|||
data\
|
||||
--abbc761f78ff4d7cb7573b5a23f96ef0--\r\n";
|
||||
|
||||
assert!(Multipart::parse(
|
||||
data,
|
||||
"abbc761f78ff4d7cb7573b5a23f96ef0",
|
||||
None,
|
||||
Some(5),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.is_ok());
|
||||
assert!(
|
||||
Multipart::parse(data, "abbc761f78ff4d7cb7573b5a23f96ef0", Some(5), None,)
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
assert!(Multipart::parse(
|
||||
data,
|
||||
"abbc761f78ff4d7cb7573b5a23f96ef0",
|
||||
None,
|
||||
Some(6),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.is_ok());
|
||||
assert!(
|
||||
Multipart::parse(data, "abbc761f78ff4d7cb7573b5a23f96ef0", Some(6), None,)
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
assert!(Multipart::parse(
|
||||
data,
|
||||
"abbc761f78ff4d7cb7573b5a23f96ef0",
|
||||
None,
|
||||
Some(4),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
.is_err());
|
||||
assert!(
|
||||
Multipart::parse(data, "abbc761f78ff4d7cb7573b5a23f96ef0", Some(4), None,)
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
|
@ -352,34 +305,22 @@ mod tests {
|
|||
data\
|
||||
--abbc761f78ff4d7cb7573b5a23f96ef0--\r\n";
|
||||
|
||||
assert!(Multipart::parse(
|
||||
data,
|
||||
"abbc761f78ff4d7cb7573b5a23f96ef0",
|
||||
None,
|
||||
None,
|
||||
Some(1)
|
||||
)
|
||||
.await
|
||||
.is_err());
|
||||
assert!(
|
||||
Multipart::parse(data, "abbc761f78ff4d7cb7573b5a23f96ef0", None, Some(1))
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
|
||||
assert!(Multipart::parse(
|
||||
data,
|
||||
"abbc761f78ff4d7cb7573b5a23f96ef0",
|
||||
None,
|
||||
None,
|
||||
Some(2)
|
||||
)
|
||||
.await
|
||||
.is_ok());
|
||||
assert!(
|
||||
Multipart::parse(data, "abbc761f78ff4d7cb7573b5a23f96ef0", None, Some(2))
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
|
||||
assert!(Multipart::parse(
|
||||
data,
|
||||
"abbc761f78ff4d7cb7573b5a23f96ef0",
|
||||
None,
|
||||
None,
|
||||
Some(3)
|
||||
)
|
||||
.await
|
||||
.is_ok());
|
||||
assert!(
|
||||
Multipart::parse(data, "abbc761f78ff4d7cb7573b5a23f96ef0", None, Some(3))
|
||||
.await
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
19
src/query.rs
19
src/query.rs
|
@ -14,9 +14,9 @@ use crate::{
|
|||
use itertools::Itertools;
|
||||
use std::any::Any;
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs::File;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use tempdir::TempDir;
|
||||
|
||||
/// IntoQueryBuilder options
|
||||
#[derive(Default, Clone)]
|
||||
|
@ -65,7 +65,6 @@ pub struct QueryBuilder {
|
|||
pub(crate) operation_name: Option<String>,
|
||||
pub(crate) variables: Variables,
|
||||
pub(crate) ctx_data: Option<Data>,
|
||||
pub(crate) files_holder: Option<TempDir>,
|
||||
}
|
||||
|
||||
impl QueryBuilder {
|
||||
|
@ -76,7 +75,6 @@ impl QueryBuilder {
|
|||
operation_name: None,
|
||||
variables: Default::default(),
|
||||
ctx_data: None,
|
||||
files_holder: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -107,21 +105,16 @@ impl QueryBuilder {
|
|||
self
|
||||
}
|
||||
|
||||
/// Set file holder
|
||||
pub fn set_files_holder(&mut self, files_holder: TempDir) {
|
||||
self.files_holder = Some(files_holder);
|
||||
}
|
||||
|
||||
/// Set uploaded file path
|
||||
pub fn set_upload(
|
||||
&mut self,
|
||||
var_path: &str,
|
||||
filename: &str,
|
||||
content_type: Option<&str>,
|
||||
path: &Path,
|
||||
filename: String,
|
||||
content_type: Option<String>,
|
||||
content: File,
|
||||
) {
|
||||
self.variables
|
||||
.set_upload(var_path, filename, content_type, path);
|
||||
.set_upload(var_path, filename, content_type, content);
|
||||
}
|
||||
|
||||
/// Execute the query.
|
||||
|
|
|
@ -18,8 +18,8 @@ impl ScalarType for Any {
|
|||
Some("The `_Any` scalar is used to pass representations of entities from external services into the root `_entities` field for execution.")
|
||||
}
|
||||
|
||||
fn parse(value: &Value) -> InputValueResult<Self> {
|
||||
Ok(Self(value.clone()))
|
||||
fn parse(value: Value) -> InputValueResult<Self> {
|
||||
Ok(Self(value))
|
||||
}
|
||||
|
||||
fn is_valid(_value: &Value) -> bool {
|
||||
|
|
|
@ -11,10 +11,17 @@ impl ScalarType for bool {
|
|||
Some("The `Boolean` scalar type represents `true` or `false`.")
|
||||
}
|
||||
|
||||
fn parse(value: &Value) -> InputValueResult<Self> {
|
||||
fn parse(value: Value) -> InputValueResult<Self> {
|
||||
match value {
|
||||
Value::Boolean(n) => Ok(*n),
|
||||
_ => Err(InputValueError::ExpectedType),
|
||||
Value::Boolean(n) => Ok(n),
|
||||
_ => Err(InputValueError::ExpectedType(value)),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_valid(value: &Value) -> bool {
|
||||
match value {
|
||||
Value::Boolean(_) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,10 +9,10 @@ impl ScalarType for ObjectId {
|
|||
"ObjectId"
|
||||
}
|
||||
|
||||
fn parse(value: &Value) -> InputValueResult<Self> {
|
||||
fn parse(value: Value) -> InputValueResult<Self> {
|
||||
match value {
|
||||
Value::String(s) => Ok(ObjectId::with_string(&s)?),
|
||||
_ => Err(InputValueError::ExpectedType),
|
||||
_ => Err(InputValueError::ExpectedType(value)),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -27,7 +27,7 @@ impl ScalarType for UtcDateTime {
|
|||
"DateTime"
|
||||
}
|
||||
|
||||
fn parse(value: &Value) -> InputValueResult<Self> {
|
||||
fn parse(value: Value) -> InputValueResult<Self> {
|
||||
DateTime::<Utc>::parse(value).map(UtcDateTime::from)
|
||||
}
|
||||
|
||||
|
|
|
@ -9,10 +9,10 @@ impl ScalarType for Tz {
|
|||
"TimeZone"
|
||||
}
|
||||
|
||||
fn parse(value: &Value) -> InputValueResult<Self> {
|
||||
fn parse(value: Value) -> InputValueResult<Self> {
|
||||
match value {
|
||||
Value::String(s) => Ok(Tz::from_str(&s)?),
|
||||
_ => Err(InputValueError::ExpectedType),
|
||||
_ => Err(InputValueError::ExpectedType(value)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,10 +11,10 @@ impl ScalarType for DateTime<Utc> {
|
|||
"DateTime"
|
||||
}
|
||||
|
||||
fn parse(value: &Value) -> InputValueResult<Self> {
|
||||
fn parse(value: Value) -> InputValueResult<Self> {
|
||||
match value {
|
||||
Value::String(s) => Ok(Utc.datetime_from_str(&s, "%+")?),
|
||||
_ => Err(InputValueError::ExpectedType),
|
||||
_ => Err(InputValueError::ExpectedType(value)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,11 +14,11 @@ macro_rules! impl_float_scalars {
|
|||
Some("The `Float` scalar type represents signed double-precision fractional values as specified by [IEEE 754](https://en.wikipedia.org/wiki/IEEE_floating_point).")
|
||||
}
|
||||
|
||||
fn parse(value: &Value) -> InputValueResult<Self> {
|
||||
fn parse(value: Value) -> InputValueResult<Self> {
|
||||
match value {
|
||||
Value::Int(n) => Ok(*n as Self),
|
||||
Value::Float(n) => Ok(*n as Self),
|
||||
_ => Err(InputValueError::ExpectedType)
|
||||
Value::Int(n) => Ok(n as Self),
|
||||
Value::Float(n) => Ok(n as Self),
|
||||
_ => Err(InputValueError::ExpectedType(value))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -77,11 +77,11 @@ impl ScalarType for ID {
|
|||
"ID"
|
||||
}
|
||||
|
||||
fn parse(value: &Value) -> InputValueResult<Self> {
|
||||
fn parse(value: Value) -> InputValueResult<Self> {
|
||||
match value {
|
||||
Value::Int(n) => Ok(ID(n.to_string())),
|
||||
Value::String(s) => Ok(ID(s.clone())),
|
||||
_ => Err(InputValueError::ExpectedType),
|
||||
Value::String(s) => Ok(ID(s)),
|
||||
_ => Err(InputValueError::ExpectedType(value)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -14,10 +14,10 @@ macro_rules! impl_integer_scalars {
|
|||
Some("The `Int` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.")
|
||||
}
|
||||
|
||||
fn parse(value: &Value) -> InputValueResult<Self> {
|
||||
fn parse(value: Value) -> InputValueResult<Self> {
|
||||
match value {
|
||||
Value::Int(n) => Ok(*n as Self),
|
||||
_ => Err(InputValueError::ExpectedType)
|
||||
Value::Int(n) => Ok(n as Self),
|
||||
_ => Err(InputValueError::ExpectedType(value))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -51,11 +51,11 @@ macro_rules! impl_int64_scalars {
|
|||
Some("The `Int64` scalar type represents non-fractional signed whole numeric values. Int can represent values between -(2^64) and 2^64 - 1.")
|
||||
}
|
||||
|
||||
fn parse(value: &Value) -> InputValueResult<Self> {
|
||||
fn parse(value: Value) -> InputValueResult<Self> {
|
||||
match value {
|
||||
Value::Int(n) => Ok(*n as Self),
|
||||
Value::Int(n) => Ok(n as Self),
|
||||
Value::String(s) => Ok(s.parse()?),
|
||||
_ => Err(InputValueError::ExpectedType)
|
||||
_ => Err(InputValueError::ExpectedType(value))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,8 +28,8 @@ impl<T: DeserializeOwned + Serialize + Send + Sync> ScalarType for Json<T> {
|
|||
"JSON"
|
||||
}
|
||||
|
||||
fn parse(value: &Value) -> InputValueResult<Self> {
|
||||
Ok(serde_json::from_value(value.clone().into()).map(Json)?)
|
||||
fn parse(value: Value) -> InputValueResult<Self> {
|
||||
Ok(serde_json::from_value(value.into()).map(Json)?)
|
||||
}
|
||||
|
||||
fn to_json(&self) -> Result<serde_json::Value> {
|
||||
|
|
|
@ -18,10 +18,10 @@ impl ScalarType for String {
|
|||
Some(STRING_DESC)
|
||||
}
|
||||
|
||||
fn parse(value: &Value) -> InputValueResult<Self> {
|
||||
fn parse(value: Value) -> InputValueResult<Self> {
|
||||
match value {
|
||||
Value::String(s) => Ok(s.clone()),
|
||||
_ => Err(InputValueError::ExpectedType),
|
||||
Value::String(s) => Ok(s),
|
||||
_ => Err(InputValueError::ExpectedType(value)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,10 +8,10 @@ impl ScalarType for Url {
|
|||
"Url"
|
||||
}
|
||||
|
||||
fn parse(value: &Value) -> InputValueResult<Self> {
|
||||
fn parse(value: Value) -> InputValueResult<Self> {
|
||||
match value {
|
||||
Value::String(s) => Ok(Url::parse(s)?),
|
||||
_ => Err(InputValueError::ExpectedType),
|
||||
Value::String(s) => Ok(Url::parse(&s)?),
|
||||
_ => Err(InputValueError::ExpectedType(value)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,10 +8,10 @@ impl ScalarType for Uuid {
|
|||
"UUID"
|
||||
}
|
||||
|
||||
fn parse(value: &Value) -> InputValueResult<Self> {
|
||||
fn parse(value: Value) -> InputValueResult<Self> {
|
||||
match value {
|
||||
Value::String(s) => Ok(Uuid::parse_str(&s)?),
|
||||
_ => Err(InputValueError::ExpectedType),
|
||||
_ => Err(InputValueError::ExpectedType(value)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -44,10 +44,10 @@ impl ScalarType for Cursor {
|
|||
"Cursor"
|
||||
}
|
||||
|
||||
fn parse(value: &Value) -> InputValueResult<Self> {
|
||||
fn parse(value: Value) -> InputValueResult<Self> {
|
||||
match value {
|
||||
Value::String(s) => Ok(Cursor(s.into())),
|
||||
_ => Err(InputValueError::ExpectedType),
|
||||
Value::String(s) => Ok(Cursor(s)),
|
||||
_ => Err(InputValueError::ExpectedType(value)),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,11 +11,11 @@ pub struct EnumItem<T> {
|
|||
pub trait EnumType: Type + Sized + Eq + Send + Copy + Sized + 'static {
|
||||
fn items() -> &'static [EnumItem<Self>];
|
||||
|
||||
fn parse_enum(value: &Value) -> InputValueResult<Self> {
|
||||
fn parse_enum(value: Value) -> InputValueResult<Self> {
|
||||
let value = match value {
|
||||
Value::Enum(s) => s.as_str(),
|
||||
Value::String(s) => s.as_str(),
|
||||
_ => return Err(InputValueError::ExpectedType),
|
||||
Value::Enum(s) => s,
|
||||
Value::String(s) => s,
|
||||
_ => return Err(InputValueError::ExpectedType(value)),
|
||||
};
|
||||
|
||||
let items = Self::items();
|
||||
|
|
|
@ -20,7 +20,7 @@ impl<T: Type> Type for Vec<T> {
|
|||
}
|
||||
|
||||
impl<T: InputValueType> InputValueType for Vec<T> {
|
||||
fn parse(value: &Value) -> InputValueResult<Self> {
|
||||
fn parse(value: Value) -> InputValueResult<Self> {
|
||||
match value {
|
||||
Value::List(values) => {
|
||||
let mut result = Vec::new();
|
||||
|
|
|
@ -20,7 +20,7 @@ impl<T: Type> Type for Option<T> {
|
|||
}
|
||||
|
||||
impl<T: InputValueType> InputValueType for Option<T> {
|
||||
fn parse(value: &Value) -> InputValueResult<Self> {
|
||||
fn parse(value: Value) -> InputValueResult<Self> {
|
||||
match value {
|
||||
Value::Null => Ok(None),
|
||||
_ => Ok(Some(T::parse(value)?)),
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
use crate::{registry, InputValueError, InputValueResult, InputValueType, Type, Value};
|
||||
use async_graphql_parser::UploadValue;
|
||||
use std::borrow::Cow;
|
||||
use std::path::PathBuf;
|
||||
use std::io::Read;
|
||||
|
||||
/// Uploaded file
|
||||
///
|
||||
|
@ -23,7 +24,7 @@ use std::path::PathBuf;
|
|||
/// #[async_graphql::Object]
|
||||
/// impl MutationRoot {
|
||||
/// async fn upload(&self, file: Upload) -> bool {
|
||||
/// println!("upload: filename={}", file.filename);
|
||||
/// println!("upload: filename={}", file.filename());
|
||||
/// true
|
||||
/// }
|
||||
/// }
|
||||
|
@ -41,15 +42,23 @@ use std::path::PathBuf;
|
|||
/// --form 'map={ "0": ["variables.file"] }' \
|
||||
/// --form '0=@myFile.txt'
|
||||
/// ```
|
||||
pub struct Upload {
|
||||
pub struct Upload(UploadValue);
|
||||
|
||||
impl Upload {
|
||||
/// Filename
|
||||
pub filename: String,
|
||||
pub fn filename(&self) -> &str {
|
||||
self.0.filename.as_str()
|
||||
}
|
||||
|
||||
/// Content type, such as `application/json`, `image/jpg` ...
|
||||
pub content_type: Option<String>,
|
||||
pub fn content_type(&self) -> Option<&str> {
|
||||
self.0.content_type.as_deref()
|
||||
}
|
||||
|
||||
/// Temporary file path
|
||||
pub path: PathBuf,
|
||||
/// Convert to a read
|
||||
pub fn into_read(self) -> impl Read + Sync + Send + 'static {
|
||||
self.0.content
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Type for Upload {
|
||||
|
@ -70,31 +79,11 @@ impl<'a> Type for Upload {
|
|||
}
|
||||
|
||||
impl<'a> InputValueType for Upload {
|
||||
fn parse(value: &Value) -> InputValueResult<Self> {
|
||||
if let Value::String(s) = value {
|
||||
if s.starts_with("file:") {
|
||||
let s = &s[5..];
|
||||
if let Some(idx) = s.find('|') {
|
||||
let name_and_type = &s[..idx];
|
||||
let path = &s[idx + 1..];
|
||||
if let Some(type_idx) = name_and_type.find(':') {
|
||||
let name = &name_and_type[..type_idx];
|
||||
let mime_type = &name_and_type[type_idx + 1..];
|
||||
return Ok(Self {
|
||||
filename: name.to_string(),
|
||||
content_type: Some(mime_type.to_string()),
|
||||
path: PathBuf::from(path),
|
||||
});
|
||||
} else {
|
||||
return Ok(Self {
|
||||
filename: name_and_type.to_string(),
|
||||
content_type: None,
|
||||
path: PathBuf::from(path),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
fn parse(value: Value) -> InputValueResult<Self> {
|
||||
if let Value::Upload(upload) = value {
|
||||
Ok(Upload(upload))
|
||||
} else {
|
||||
Err(InputValueError::ExpectedType(value))
|
||||
}
|
||||
Err(InputValueError::ExpectedType)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user