Replace http::GQLRequest with Request

This commit is contained in:
Koxiaet 2020-09-12 17:07:46 +01:00
parent 79bcf5f710
commit dcc7d1be14
12 changed files with 154 additions and 136 deletions

View File

@ -41,10 +41,10 @@ impl FromRequest for GQLRequest {
let config = req.app_data::<Self::Config>().cloned().unwrap_or_default();
if req.method() == Method::GET {
let res = web::Query::<async_graphql::http::GQLRequest>::from_query(req.query_string());
let res = web::Query::<async_graphql::Request>::from_query(req.query_string());
Box::pin(async move {
let gql_request = res?;
Ok(GQLRequest(gql_request.into_inner().into()))
Ok(GQLRequest(gql_request.into_inner()))
})
} else {
let content_type = req

View File

@ -199,7 +199,7 @@ impl<'q> FromQuery<'q> for GQLRequest {
let decoded = value.url_decode().map_err(|e| e.to_string())?;
let json_value = serde_json::from_str::<serde_json::Value>(&decoded)
.map_err(|e| e.to_string())?;
variables = Variables::parse_from_json(json_value).into();
variables = Variables::from_json(json_value).into();
}
}
_ => {

View File

@ -5,7 +5,7 @@
#![allow(clippy::needless_doctest_main)]
#![forbid(unsafe_code)]
use async_graphql::http::{GQLRequest, MultipartOptions};
use async_graphql::http::MultipartOptions;
use async_graphql::{resolver_utils::ObjectType, Schema, SubscriptionType};
use async_trait::async_trait;
use std::str::FromStr;
@ -103,7 +103,7 @@ impl<State: Clone + Send + Sync + 'static> RequestExt<State> for Request<State>
opts: MultipartOptions,
) -> tide::Result<async_graphql::Request> {
if self.method() == Method::Get {
Ok(self.query::<GQLRequest>()?.into())
Ok(self.query::<async_graphql::Request>()?)
} else {
let content_type = self
.header(&headers::CONTENT_TYPE)

View File

@ -5,8 +5,10 @@
#![allow(clippy::needless_doctest_main)]
#![forbid(unsafe_code)]
use async_graphql::http::{GQLRequest, MultipartOptions};
use async_graphql::{resolver_utils::ObjectType, Data, FieldResult, Schema, SubscriptionType};
use async_graphql::http::MultipartOptions;
use async_graphql::{
resolver_utils::ObjectType, Data, FieldResult, Request, Schema, SubscriptionType,
};
use futures::io::ErrorKind;
use futures::{select, TryStreamExt};
use futures::{SinkExt, StreamExt};
@ -112,9 +114,9 @@ where
opts: Arc<MultipartOptions>,
schema| async move {
if method == Method::GET {
let request: GQLRequest = serde_urlencoded::from_str(&query)
let request: Request = serde_urlencoded::from_str(&query)
.map_err(|err| warp::reject::custom(BadRequest(err.into())))?;
Ok::<_, Rejection>((schema, async_graphql::Request::from(request)))
Ok::<_, Rejection>((schema, request))
} else {
let request = async_graphql::http::receive_body(
content_type,

View File

@ -6,18 +6,19 @@ use crate::parser::types::{
use crate::schema::SchemaEnv;
use crate::{FieldResult, InputValueType, Lookahead, Pos, Positioned, QueryError, Result, Value};
use fnv::FnvHashMap;
use serde::ser::SerializeSeq;
use serde::{Serialize, Serializer};
use serde::ser::{SerializeSeq, Serializer};
use serde::{Deserialize, Serialize};
use std::any::{Any, TypeId};
use std::collections::BTreeMap;
use std::convert::TryFrom;
use std::fmt::{self, Display, Formatter};
use std::fmt::{self, Debug, Display, Formatter};
use std::ops::Deref;
use std::sync::atomic::AtomicUsize;
use std::sync::Arc;
/// Variables of a query.
#[derive(Debug, Clone, Default, Serialize)]
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
#[serde(transparent)]
pub struct Variables(pub BTreeMap<Name, Value>);
impl Display for Variables {
@ -31,18 +32,34 @@ impl Display for Variables {
}
impl Variables {
/// Parse variables from JSON object.
/// Get the variables from a GraphQL value.
///
/// If the value is not a map, or the keys of map are not valid GraphQL names, then an empty
/// `Variables` instance will be returned.
pub fn parse_from_json(value: serde_json::Value) -> Self {
if let Ok(Value::Object(obj)) = Value::from_json(value) {
Self(obj)
} else {
Default::default()
/// If the value is not a map, then no variables will be returned.
#[must_use]
pub fn from_value(value: Value) -> Self {
match value {
Value::Object(obj) => Self(obj),
_ => Self::default(),
}
}
/// Get the values from a JSON value.
///
/// If the value is not a map or the keys of a map are not valid GraphQL names, then no
/// variables will be returned.
#[must_use]
pub fn from_json(value: serde_json::Value) -> Self {
Value::from_json(value)
.map(Self::from_value)
.unwrap_or_default()
}
/// Get the variables as a GraphQL value.
#[must_use]
pub fn into_value(self) -> Value {
Value::Object(self.0)
}
pub(crate) fn variable_path(&mut self, path: &str) -> Option<&mut Value> {
let mut parts = path.strip_prefix("variables.")?.split('.');
@ -60,17 +77,31 @@ impl Variables {
}
}
impl From<Variables> for Value {
fn from(variables: Variables) -> Self {
variables.into_value()
}
}
/// Schema/Context data.
///
/// This is a type map, allowing you to store anything inside it.
#[derive(Default)]
pub struct Data(FnvHashMap<TypeId, Box<dyn Any + Sync + Send>>);
impl Data {
#[allow(missing_docs)]
/// Insert data.
pub fn insert<D: Any + Send + Sync>(&mut self, data: D) {
self.0.insert(TypeId::of::<D>(), Box::new(data));
}
}
impl Debug for Data {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.debug_tuple("Data").finish()
}
}
/// Context for `SelectionSet`
pub type ContextSelectionSet<'a> = ContextBase<'a, &'a Positioned<SelectionSet>>;

View File

@ -8,38 +8,9 @@ pub use graphiql_source::graphiql_source;
pub use multipart::{receive_multipart, MultipartOptions};
pub use playground_source::{playground_source, GraphQLPlaygroundConfig};
use crate::{Data, ParseRequestError, Request, Variables};
use crate::{ParseRequestError, Request};
use futures::io::AsyncRead;
use futures::AsyncReadExt;
use serde::Deserialize;
/// Deserializable GraphQL Request object
#[derive(Deserialize, Clone, PartialEq, Debug)]
pub struct GQLRequest {
/// Query source
pub query: String,
/// Operation name for this query
#[serde(rename = "operationName")]
pub operation_name: Option<String>,
/// Variables for this query
pub variables: Option<serde_json::Value>,
}
impl From<GQLRequest> for Request {
fn from(request: GQLRequest) -> Self {
Self {
query: request.query,
operation_name: request.operation_name,
variables: request
.variables
.map(Variables::parse_from_json)
.unwrap_or_default(),
ctx_data: Data::default(),
}
}
}
/// Receive a GraphQL request from a content type and body.
pub async fn receive_body(
@ -54,60 +25,6 @@ pub async fn receive_body(
body.read_to_end(&mut data)
.await
.map_err(ParseRequestError::Io)?;
Ok(serde_json::from_slice::<GQLRequest>(&data)
.map_err(ParseRequestError::InvalidRequest)?
.into())
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_request() {
let request: GQLRequest = serde_json::from_value(json! ({
"query": "{ a b c }"
}))
.unwrap();
assert!(request.variables.is_none());
assert!(request.operation_name.is_none());
assert_eq!(request.query, "{ a b c }");
}
#[test]
fn test_request_with_operation_name() {
let request: GQLRequest = serde_json::from_value(json! ({
"query": "{ a b c }",
"operationName": "a"
}))
.unwrap();
assert!(request.variables.is_none());
assert_eq!(request.operation_name.as_deref(), Some("a"));
assert_eq!(request.query, "{ a b c }");
}
#[test]
fn test_request_with_variables() {
let request: GQLRequest = serde_json::from_value(json! ({
"query": "{ a b c }",
"variables": {
"v1": 100,
"v2": [1, 2, 3],
"v3": "str",
}
}))
.unwrap();
assert_eq!(
request.variables,
Some(json!({
"v1": 100,
"v2": [1, 2, 3],
"v3": "str",
}))
);
assert!(request.operation_name.is_none());
assert_eq!(request.query, "{ a b c }");
Ok(serde_json::from_slice::<Request>(&data).map_err(ParseRequestError::InvalidRequest)?)
}
}

View File

@ -1,4 +1,3 @@
use crate::http::GQLRequest;
use crate::{ParseRequestError, Request};
use bytes::Bytes;
use futures::io::AsyncRead;
@ -66,9 +65,8 @@ pub async fn receive_multipart(
Some("operations") => {
let request_str = field.text().await?;
request = Some(
serde_json::from_str::<GQLRequest>(&request_str)
.map_err(ParseRequestError::InvalidRequest)?
.into(),
serde_json::from_str::<Request>(&request_str)
.map_err(ParseRequestError::InvalidRequest)?,
);
}
Some("map") => {

View File

@ -1,14 +1,29 @@
use crate::parser::types::UploadValue;
use crate::{Data, Value, Variables};
use serde::Deserialize;
use std::any::Any;
use std::fs::File;
/// GraphQL query request
/// GraphQL request.
///
/// This can be deserialized from a structure of the query string, the operation name and the
/// variables. The names are all in `camelCase` (e.g. `operationName`).
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Request {
pub(crate) query: String,
pub(crate) operation_name: Option<String>,
pub(crate) variables: Variables,
pub(crate) ctx_data: Data,
/// The query source of the request.
pub query: String,
/// The operation name of the request.
#[serde(default, rename = "operationName")]
pub operation_name: Option<String>,
/// The variables of the request.
#[serde(default)]
pub variables: Variables,
/// The data of the request that can be accessed through `Context::data`.
///
/// **This data is only valid for this request**
#[serde(skip)]
pub data: Data,
}
impl Request {
@ -18,11 +33,11 @@ impl Request {
query: query.into(),
operation_name: None,
variables: Variables::default(),
ctx_data: Data::default(),
data: Data::default(),
}
}
/// Specify the operation name.
/// Specify the operation name of the request.
pub fn operation_name<T: Into<String>>(self, name: T) -> Self {
Self {
operation_name: Some(name.into()),
@ -35,15 +50,18 @@ impl Request {
Self { variables, ..self }
}
/// Add a context data that can be accessed in the `Context`, you access it with `Context::data`.
///
/// **This data is only valid for this query**
/// Insert some data for this request.
pub fn data<D: Any + Send + Sync>(mut self, data: D) -> Self {
self.ctx_data.insert(data);
self.data.insert(data);
self
}
/// Set uploaded file path
/// Set a variable to an upload value.
///
/// `var_path` is a dot-separated path to the item that begins with `variables`, for example
/// `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,
@ -68,3 +86,55 @@ impl<T: Into<String>> From<T> for Request {
Self::new(query)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_request() {
let request: Request = serde_json::from_value(json! ({
"query": "{ a b c }"
}))
.unwrap();
assert!(request.variables.0.is_empty());
assert!(request.operation_name.is_none());
assert_eq!(request.query, "{ a b c }");
}
#[test]
fn test_request_with_operation_name() {
let request: Request = serde_json::from_value(json! ({
"query": "{ a b c }",
"operationName": "a"
}))
.unwrap();
assert!(request.variables.0.is_empty());
assert_eq!(request.operation_name.as_deref(), Some("a"));
assert_eq!(request.query, "{ a b c }");
}
#[test]
fn test_request_with_variables() {
let request: Request = serde_json::from_value(json! ({
"query": "{ a b c }",
"variables": {
"v1": 100,
"v2": [1, 2, 3],
"v3": "str",
}
}))
.unwrap();
assert_eq!(
request.variables.into_value().into_json().unwrap(),
json!({
"v1": 100,
"v2": [1, 2, 3],
"v3": "str",
})
);
assert!(request.operation_name.is_none());
assert_eq!(request.query, "{ a b c }");
}
}

View File

@ -251,7 +251,7 @@ impl<'a> Fields<'a> {
let introspection_type_name = root.introspection_type_name();
if type_condition.map_or(false, |condition| {
let applies_concrete_object = type_condition.map_or(false, |condition| {
introspection_type_name == condition
|| ctx
.schema_env
@ -259,7 +259,8 @@ impl<'a> Fields<'a> {
.implements
.get(&*introspection_type_name)
.map_or(false, |interfaces| interfaces.contains(condition))
}) {
});
if applies_concrete_object {
// The fragment applies to the concrete object type.
// TODO: This solution isn't ideal. If there are two interfaces InterfaceA

View File

@ -442,7 +442,7 @@ where
let (document, cache_control, extensions) =
try_query_result!(self.prepare_request(&request));
let mut resp = self
.execute_once(document, extensions, request.variables, request.ctx_data)
.execute_once(document, extensions, request.variables, request.data)
.await;
resp.cache_control = cache_control;
resp
@ -466,7 +466,7 @@ where
if document.operation.node.ty != OperationType::Subscription {
let mut resp = schema
.execute_once(document, extensions, request.variables, request.ctx_data)
.execute_once(document, extensions, request.variables, request.data)
.await;
resp.cache_control = cache_control;
yield resp;
@ -509,7 +509,7 @@ where
/// Execute an GraphQL subscription.
pub fn execute_stream(&self, request: impl Into<Request>) -> impl Stream<Item = Response> {
let mut request = request.into();
let ctx_data = std::mem::take(&mut request.ctx_data);
let ctx_data = std::mem::take(&mut request.data);
self.execute_stream_with_ctx_data(request, Arc::new(ctx_data))
}
}

View File

@ -1,7 +1,7 @@
//! WebSocket transport for subscription
use crate::resolver_utils::ObjectType;
use crate::{http, Data, FieldResult, Request, Response, Schema, SubscriptionType};
use crate::{Data, FieldResult, Request, Response, Schema, SubscriptionType};
use futures::channel::mpsc;
use futures::task::{Context, Poll};
use futures::{Future, Stream, StreamExt};
@ -229,8 +229,7 @@ where
}
"start" => {
if let (Some(id), Some(payload)) = (msg.id, msg.payload) {
if let Ok(request) = serde_json::from_value::<http::GQLRequest>(payload) {
let request = Request::from(request);
if let Ok(request) = serde_json::from_value::<Request>(payload) {
let stream = schema
.execute_stream_with_ctx_data(request, ctx.ctx_data.clone())
.boxed();

View File

@ -24,7 +24,7 @@ pub async fn test_variables() {
}
"#,
)
.variables(Variables::parse_from_json(serde_json::json!({
.variables(Variables::from_json(serde_json::json!({
"intVal": 10,
"intListVal": [1, 2, 3, 4, 5],
})));
@ -117,7 +117,7 @@ pub async fn test_variable_null() {
}
"#,
)
.variables(Variables::parse_from_json(serde_json::json!({
.variables(Variables::from_json(serde_json::json!({
"intVal": null,
})));
let resp = schema.execute(query).await;
@ -168,7 +168,7 @@ pub async fn test_variable_in_input_object() {
}"#;
let resp = schema
.execute(
Request::new(query).variables(Variables::parse_from_json(serde_json::json!({
Request::new(query).variables(Variables::from_json(serde_json::json!({
"value": 10,
}))),
)
@ -189,7 +189,7 @@ pub async fn test_variable_in_input_object() {
}"#;
let resp = schema
.execute(
Request::new(query).variables(Variables::parse_from_json(serde_json::json!({
Request::new(query).variables(Variables::from_json(serde_json::json!({
"value": 3,
}))),
)
@ -210,7 +210,7 @@ pub async fn test_variable_in_input_object() {
}"#;
let resp = schema
.execute(
Request::new(query).variables(Variables::parse_from_json(serde_json::json!({
Request::new(query).variables(Variables::from_json(serde_json::json!({
"value": 10,
}))),
)