use std::any::Any; use std::collections::HashMap; use std::fmt::{self, Debug, Formatter}; use serde::{Deserialize, Deserializer, Serialize}; use crate::parser::parse_query; use crate::parser::types::ExecutableDocument; use crate::{Data, ParseRequestError, ServerError, UploadValue, Value, Variables}; /// 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`). #[non_exhaustive] #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Request { /// The query source of the request. #[serde(default)] 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, /// Uploads sent with the request. #[serde(skip)] pub uploads: Vec, /// 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, /// The extensions config of the request. #[serde(default)] pub extensions: HashMap, /// Disable introspection queries for this request. #[serde(skip)] pub disable_introspection: bool, #[serde(skip)] pub(crate) parsed_query: Option, } impl Request { /// Create a request object with query source. pub fn new(query: impl Into) -> Self { Self { query: query.into(), operation_name: None, variables: Variables::default(), uploads: Vec::default(), data: Data::default(), extensions: Default::default(), disable_introspection: false, parsed_query: None, } } /// Specify the operation name of the request. #[must_use] pub fn operation_name>(self, name: T) -> Self { Self { operation_name: Some(name.into()), ..self } } /// Specify the variables. #[must_use] pub fn variables(self, variables: Variables) -> Self { Self { variables, ..self } } /// Insert some data for this request. #[must_use] pub fn data(mut self, data: D) -> Self { self.data.insert(data); self } /// Disable introspection queries for this request. #[must_use] pub fn disable_introspection(mut self) -> Self { self.disable_introspection = true; self } #[inline] /// Performs parsing of query ahead of execution. /// /// This effectively allows to inspect query information, before passing request to schema for /// execution as long as query is valid. pub fn parsed_query(&mut self) -> Result<&ExecutableDocument, ServerError> { if self.parsed_query.is_none() { match parse_query(&self.query) { Ok(parsed) => self.parsed_query = Some(parsed), Err(error) => return Err(error.into()), } } //forbid_unsafe effectively bans optimize away else branch here so use unwrap //but this unwrap never panics Ok(self.parsed_query.as_ref().unwrap()) } /// 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, upload: UploadValue) { fn variable_path<'a>(variables: &'a mut Variables, path: &str) -> Option<&'a mut Value> { let mut parts = path.strip_prefix("variables.")?.split('.'); let initial = variables.get_mut(parts.next().unwrap())?; parts.try_fold(initial, |current, part| match current { Value::List(list) => part .parse::() .ok() .and_then(|idx| usize::try_from(idx).ok()) .and_then(move |idx| list.get_mut(idx)), Value::Object(obj) => obj.get_mut(part), _ => None, }) } let variable = match variable_path(&mut self.variables, var_path) { Some(variable) => variable, None => return, }; self.uploads.push(upload); *variable = Value::String(format!("#__graphql_file__:{}", self.uploads.len() - 1)); } } impl> From for Request { fn from(query: T) -> Self { Self::new(query) } } impl Debug for Request { fn fmt(&self, f: &mut Formatter) -> fmt::Result { f.debug_struct("Request") .field("query", &self.query) .field("operation_name", &self.operation_name) .field("variables", &self.variables) .field("extensions", &self.extensions) .finish() } } /// Batch support for GraphQL requests, which is either a single query, or an array of queries /// /// **Reference:** #[derive(Debug, Deserialize)] #[serde(untagged)] #[allow(clippy::large_enum_variant)] //Request is at fault pub enum BatchRequest { /// Single query Single(Request), /// Non-empty array of queries #[serde(deserialize_with = "deserialize_non_empty_vec")] Batch(Vec), } impl BatchRequest { /// Attempt to convert the batch request into a single request. /// /// # Errors /// /// Fails if the batch request is a list of requests with a message saying that batch requests /// aren't supported. pub fn into_single(self) -> Result { match self { Self::Single(req) => Ok(req), Self::Batch(_) => Err(ParseRequestError::UnsupportedBatch), } } /// Returns an iterator over the requests. pub fn iter(&self) -> impl Iterator { match self { BatchRequest::Single(request) => { Box::new(std::iter::once(request)) as Box> } BatchRequest::Batch(requests) => Box::new(requests.iter()), } } /// Returns an iterator that allows modifying each request. pub fn iter_mut(&mut self) -> impl Iterator { match self { BatchRequest::Single(request) => { Box::new(std::iter::once(request)) as Box> } BatchRequest::Batch(requests) => Box::new(requests.iter_mut()), } } /// Specify the variables for each requests. #[must_use] pub fn variables(mut self, variables: Variables) -> Self { for request in self.iter_mut() { request.variables = variables.clone(); } self } /// Insert some data for for each requests. #[must_use] pub fn data(mut self, data: D) -> Self { for request in self.iter_mut() { request.data.insert(data.clone()); } self } /// Disable introspection queries for for each requests. #[must_use] pub fn disable_introspection(mut self) -> Self { for request in self.iter_mut() { request.disable_introspection = true; } self } } fn deserialize_non_empty_vec<'de, D, T>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, T: Deserialize<'de>, { use serde::de::Error as _; let v = >::deserialize(deserializer)?; if v.is_empty() { Err(D::Error::invalid_length(0, &"a non-empty sequence")) } else { Ok(v) } } impl From for BatchRequest { fn from(r: Request) -> Self { BatchRequest::Single(r) } } impl From> for BatchRequest { fn from(r: Vec) -> Self { BatchRequest::Batch(r) } } #[cfg(test)] mod tests { use crate::*; #[test] fn test_request() { let request: Request = from_value(value! ({ "query": "{ a b c }" })) .unwrap(); assert!(request.variables.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 = from_value(value! ({ "query": "{ a b c }", "operationName": "a" })) .unwrap(); assert!(request.variables.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 = from_value(value! ({ "query": "{ a b c }", "variables": { "v1": 100, "v2": [1, 2, 3], "v3": "str", } })) .unwrap(); assert_eq!( request.variables.into_value(), value!({ "v1": 100, "v2": [1, 2, 3], "v3": "str", }) ); assert!(request.operation_name.is_none()); assert_eq!(request.query, "{ a b c }"); } #[test] fn test_deserialize_request_with_null_variables() { let request: Request = from_value(value! ({ "query": "{ a b c }", "variables": null })) .unwrap(); assert!(request.operation_name.is_none()); assert!(request.variables.is_empty()); } #[test] fn test_batch_request_single() { let request: BatchRequest = from_value(value! ({ "query": "{ a b c }" })) .unwrap(); if let BatchRequest::Single(request) = request { assert!(request.variables.is_empty()); assert!(request.operation_name.is_none()); assert_eq!(request.query, "{ a b c }"); } else { unreachable!() } } #[test] fn test_batch_request_batch() { let request: BatchRequest = from_value(value!([ { "query": "{ a b c }" }, { "query": "{ d e }" } ])) .unwrap(); if let BatchRequest::Batch(requests) = request { assert!(requests[0].variables.is_empty()); assert!(requests[0].operation_name.is_none()); assert_eq!(requests[0].query, "{ a b c }"); assert!(requests[1].variables.is_empty()); assert!(requests[1].operation_name.is_none()); assert_eq!(requests[1].query, "{ d e }"); } else { unreachable!() } } }