diff --git a/integrations/actix-web/src/lib.rs b/integrations/actix-web/src/lib.rs index 55dc7eaf..a71144db 100644 --- a/integrations/actix-web/src/lib.rs +++ b/integrations/actix-web/src/lib.rs @@ -41,10 +41,10 @@ impl FromRequest for GQLRequest { let config = req.app_data::().cloned().unwrap_or_default(); if req.method() == Method::GET { - let res = web::Query::::from_query(req.query_string()); + let res = web::Query::::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 diff --git a/integrations/rocket/src/lib.rs b/integrations/rocket/src/lib.rs index aab84043..bce9b6b7 100644 --- a/integrations/rocket/src/lib.rs +++ b/integrations/rocket/src/lib.rs @@ -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::(&decoded) .map_err(|e| e.to_string())?; - variables = Variables::parse_from_json(json_value).into(); + variables = Variables::from_json(json_value).into(); } } _ => { diff --git a/integrations/tide/src/lib.rs b/integrations/tide/src/lib.rs index f1279e81..a50278d9 100644 --- a/integrations/tide/src/lib.rs +++ b/integrations/tide/src/lib.rs @@ -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 RequestExt for Request opts: MultipartOptions, ) -> tide::Result { if self.method() == Method::Get { - Ok(self.query::()?.into()) + Ok(self.query::()?) } else { let content_type = self .header(&headers::CONTENT_TYPE) diff --git a/integrations/warp/src/lib.rs b/integrations/warp/src/lib.rs index e29287fe..4a3c3cf0 100644 --- a/integrations/warp/src/lib.rs +++ b/integrations/warp/src/lib.rs @@ -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, 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, diff --git a/src/context.rs b/src/context.rs index 3b5c81c6..99a339db 100644 --- a/src/context.rs +++ b/src/context.rs @@ -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); 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 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>); impl Data { - #[allow(missing_docs)] + /// Insert data. pub fn insert(&mut self, data: D) { self.0.insert(TypeId::of::(), 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>; diff --git a/src/http/mod.rs b/src/http/mod.rs index 2f7128b0..5db2ce5a 100644 --- a/src/http/mod.rs +++ b/src/http/mod.rs @@ -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, - - /// Variables for this query - pub variables: Option, -} - -impl From 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::(&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::(&data).map_err(ParseRequestError::InvalidRequest)?) } } diff --git a/src/http/multipart.rs b/src/http/multipart.rs index 8038ebde..51a53b81 100644 --- a/src/http/multipart.rs +++ b/src/http/multipart.rs @@ -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::(&request_str) - .map_err(ParseRequestError::InvalidRequest)? - .into(), + serde_json::from_str::(&request_str) + .map_err(ParseRequestError::InvalidRequest)?, ); } Some("map") => { diff --git a/src/request.rs b/src/request.rs index e8f0c594..a5bfe3c4 100644 --- a/src/request.rs +++ b/src/request.rs @@ -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, - 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, + /// 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>(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(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> From 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 }"); + } +} diff --git a/src/resolver_utils/object.rs b/src/resolver_utils/object.rs index bf201c9e..d6bb1c40 100644 --- a/src/resolver_utils/object.rs +++ b/src/resolver_utils/object.rs @@ -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 diff --git a/src/schema.rs b/src/schema.rs index aa17c01f..38bcec75 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -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) -> impl Stream { 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)) } } diff --git a/src/subscription/transports/websocket/mod.rs b/src/subscription/transports/websocket/mod.rs index bfbb7068..ac6ecefe 100644 --- a/src/subscription/transports/websocket/mod.rs +++ b/src/subscription/transports/websocket/mod.rs @@ -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::(payload) { - let request = Request::from(request); + if let Ok(request) = serde_json::from_value::(payload) { let stream = schema .execute_stream_with_ctx_data(request, ctx.ctx_data.clone()) .boxed(); diff --git a/tests/variables.rs b/tests/variables.rs index 89b50b21..9473351c 100644 --- a/tests/variables.rs +++ b/tests/variables.rs @@ -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, }))), )