use bytes::Bytes;
use derivative::Derivative;
use serde::Deserialize;
use serde::Serialize;
use serde::de::DeserializeSeed;
use serde::de::Error;
use serde_json_bytes::ByteString;
use serde_json_bytes::Map as JsonMap;
use crate::configuration::BatchingMode;
use crate::json_ext::Object;
use crate::json_ext::Value;
#[derive(Clone, Derivative, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
#[derivative(Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub struct Request {
#[serde(skip_serializing_if = "Option::is_none", default)]
pub query: Option<String>,
#[serde(skip_serializing_if = "Option::is_none", default)]
pub operation_name: Option<String>,
#[serde(
skip_serializing_if = "Object::is_empty",
default,
deserialize_with = "deserialize_null_default"
)]
pub variables: Object,
#[serde(
skip_serializing_if = "Object::is_empty",
default,
deserialize_with = "deserialize_null_default"
)]
pub extensions: Object,
}
fn deserialize_null_default<'de, D, T: Default + Deserialize<'de>>(
deserializer: D,
) -> Result<T, D::Error>
where
D: serde::Deserializer<'de>,
{
<Option<T>>::deserialize(deserializer).map(|x| x.unwrap_or_default())
}
fn as_optional_object<E: Error>(value: Value) -> Result<Object, E> {
use serde::de::Unexpected;
let exp = "a map or null";
match value {
Value::Object(object) => Ok(object),
Value::Null => Ok(Object::default()),
Value::Bool(value) => Err(E::invalid_type(Unexpected::Bool(value), &exp)),
Value::Number(_) => Err(E::invalid_type(Unexpected::Other("a number"), &exp)),
Value::String(value) => Err(E::invalid_type(Unexpected::Str(value.as_str()), &exp)),
Value::Array(_) => Err(E::invalid_type(Unexpected::Seq, &exp)),
}
}
#[buildstructor::buildstructor]
impl Request {
#[builder(visibility = "pub")]
fn new(
query: Option<String>,
operation_name: Option<String>,
variables: JsonMap<ByteString, Value>,
extensions: JsonMap<ByteString, Value>,
) -> Self {
Self {
query,
operation_name,
variables,
extensions,
}
}
#[builder(visibility = "pub")]
fn fake_new(
query: Option<String>,
operation_name: Option<String>,
variables: JsonMap<ByteString, Value>,
extensions: JsonMap<ByteString, Value>,
) -> Self {
Self {
query,
operation_name,
variables,
extensions,
}
}
pub fn deserialize_from_bytes(data: &Bytes) -> Result<Self, serde_json::Error> {
let seed = RequestFromBytesSeed(data);
let mut de = serde_json::Deserializer::from_slice(data);
seed.deserialize(&mut de)
}
pub(crate) fn batch_from_urlencoded_query(
url_encoded_query: String,
) -> Result<Vec<Request>, serde_json::Error> {
let value: Value = serde_urlencoded::from_bytes(url_encoded_query.as_bytes())
.map_err(serde_json::Error::custom)?;
Request::process_query_values(value)
}
pub(crate) fn batch_from_bytes(bytes: &[u8]) -> Result<Vec<Request>, serde_json::Error> {
let value: Value = serde_json::from_slice(bytes).map_err(serde_json::Error::custom)?;
Request::process_batch_values(value)
}
fn allocate_result_array(value: &Value) -> Vec<Request> {
match value.as_array() {
Some(array) => Vec::with_capacity(array.len()),
None => Vec::with_capacity(1),
}
}
fn process_batch_values(value: Value) -> Result<Vec<Request>, serde_json::Error> {
let mut result = Request::allocate_result_array(&value);
if let Value::Array(entries) = value {
u64_histogram!(
"apollo.router.operations.batching.size",
"Number of queries contained within each query batch",
entries.len() as u64,
mode = BatchingMode::BatchHttpLink.to_string() );
u64_counter!(
"apollo.router.operations.batching",
"Total requests with batched operations",
1,
mode = BatchingMode::BatchHttpLink.to_string() );
for entry in entries {
let bytes = serde_json::to_vec(&entry)?;
result.push(Request::deserialize_from_bytes(&bytes.into())?);
}
} else {
let bytes = serde_json::to_vec(&value)?;
result.push(Request::deserialize_from_bytes(&bytes.into())?);
}
Ok(result)
}
fn process_query_values(value: Value) -> Result<Vec<Request>, serde_json::Error> {
let mut result = Request::allocate_result_array(&value);
if let Value::Array(entries) = value {
u64_histogram!(
"apollo.router.operations.batching.size",
"Number of queries contained within each query batch",
entries.len() as u64,
mode = BatchingMode::BatchHttpLink.to_string() );
u64_counter!(
"apollo.router.operations.batching",
"Total requests with batched operations",
1,
mode = BatchingMode::BatchHttpLink.to_string() );
for entry in entries {
result.push(Request::process_value(&entry)?);
}
} else {
result.push(Request::process_value(&value)?)
}
Ok(result)
}
fn process_value(value: &Value) -> Result<Request, serde_json::Error> {
let operation_name = value.get("operationName").and_then(Value::as_str);
let query = value.get("query").and_then(Value::as_str).map(String::from);
let variables: Object = value
.get("variables")
.and_then(Value::as_str)
.map(serde_json::from_str)
.transpose()?
.unwrap_or_default();
let extensions: Object = value
.get("extensions")
.and_then(Value::as_str)
.map(serde_json::from_str)
.transpose()?
.unwrap_or_default();
let request = Self::builder()
.and_query(query)
.variables(variables)
.and_operation_name(operation_name)
.extensions(extensions)
.build();
Ok(request)
}
pub fn from_urlencoded_query(url_encoded_query: String) -> Result<Request, serde_json::Error> {
let urldecoded: Value = serde_urlencoded::from_bytes(url_encoded_query.as_bytes())
.map_err(serde_json::Error::custom)?;
Request::process_value(&urldecoded)
}
}
struct RequestFromBytesSeed<'data>(&'data Bytes);
impl<'de> DeserializeSeed<'de> for RequestFromBytesSeed<'_> {
type Value = Request;
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(serde::Deserialize)]
#[serde(field_identifier, rename_all = "camelCase")]
enum Field {
Query,
OperationName,
Variables,
Extensions,
#[serde(other)]
Other,
}
const FIELDS: &[&str] = &["query", "operationName", "variables", "extensions"];
struct RequestVisitor<'data>(&'data Bytes);
impl<'de> serde::de::Visitor<'de> for RequestVisitor<'_> {
type Value = Request;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a GraphQL request")
}
fn visit_map<V>(self, mut map: V) -> Result<Request, V::Error>
where
V: serde::de::MapAccess<'de>,
{
let mut query = None;
let mut operation_name = None;
let mut variables = None;
let mut extensions = None;
while let Some(key) = map.next_key()? {
match key {
Field::Query => {
if query.is_some() {
return Err(Error::duplicate_field("query"));
}
query = Some(map.next_value()?);
}
Field::OperationName => {
if operation_name.is_some() {
return Err(Error::duplicate_field("operationName"));
}
operation_name = Some(map.next_value()?);
}
Field::Variables => {
if variables.is_some() {
return Err(Error::duplicate_field("variables"));
}
let seed = serde_json_bytes::value::BytesSeed::new(self.0);
let value = map.next_value_seed(seed)?;
variables = Some(as_optional_object(value)?);
}
Field::Extensions => {
if extensions.is_some() {
return Err(Error::duplicate_field("extensions"));
}
let seed = serde_json_bytes::value::BytesSeed::new(self.0);
let value = map.next_value_seed(seed)?;
extensions = Some(as_optional_object(value)?);
}
Field::Other => {
let _: serde::de::IgnoredAny = map.next_value()?;
}
}
}
Ok(Request {
query: query.unwrap_or_default(),
operation_name: operation_name.unwrap_or_default(),
variables: variables.unwrap_or_default(),
extensions: extensions.unwrap_or_default(),
})
}
}
deserializer.deserialize_struct("Request", FIELDS, RequestVisitor(self.0))
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use serde_json_bytes::json as bjson;
use test_log::test;
use super::*;
#[test]
fn test_request() {
let data = json!(
{
"query": "query aTest($arg1: String!) { test(who: $arg1) }",
"operationName": "aTest",
"variables": { "arg1": "me" },
"extensions": {"extension": 1}
});
let result = check_deserialization(data);
assert_eq!(
result,
Request::builder()
.query("query aTest($arg1: String!) { test(who: $arg1) }".to_owned())
.operation_name("aTest")
.variables(bjson!({ "arg1": "me" }).as_object().unwrap().clone())
.extensions(bjson!({"extension": 1}).as_object().cloned().unwrap())
.build()
);
}
#[test]
fn test_no_variables() {
let result = check_deserialization(json!(
{
"query": "query aTest($arg1: String!) { test(who: $arg1) }",
"operationName": "aTest",
"extensions": {"extension": 1}
}));
assert_eq!(
result,
Request::builder()
.query("query aTest($arg1: String!) { test(who: $arg1) }".to_owned())
.operation_name("aTest")
.extensions(bjson!({"extension": 1}).as_object().cloned().unwrap())
.build()
);
}
#[test]
fn test_variables_is_null() {
let result = check_deserialization(json!(
{
"query": "query aTest($arg1: String!) { test(who: $arg1) }",
"operationName": "aTest",
"variables": null,
"extensions": {"extension": 1}
}));
assert_eq!(
result,
Request::builder()
.query("query aTest($arg1: String!) { test(who: $arg1) }")
.operation_name("aTest")
.extensions(bjson!({"extension": 1}).as_object().cloned().unwrap())
.build()
);
}
#[test]
fn from_urlencoded_query_works() {
let query_string = "query=%7B+topProducts+%7B+upc+name+reviews+%7B+id+product+%7B+name+%7D+author+%7B+id+name+%7D+%7D+%7D+%7D&extensions=%7B+%22persistedQuery%22+%3A+%7B+%22version%22+%3A+1%2C+%22sha256Hash%22+%3A+%2220a101de18d4a9331bfc4ccdfef33cc735876a689490433570f17bdd4c0bad3f%22+%7D+%7D".to_string();
let expected_result = check_deserialization(json!(
{
"query": "{ topProducts { upc name reviews { id product { name } author { id name } } } }",
"extensions": {
"persistedQuery": {
"version": 1,
"sha256Hash": "20a101de18d4a9331bfc4ccdfef33cc735876a689490433570f17bdd4c0bad3f"
}
}
}));
let req = Request::from_urlencoded_query(query_string).unwrap();
assert_eq!(expected_result, req);
}
#[test]
fn from_urlencoded_query_with_variables_works() {
let query_string = "query=%7B+topProducts+%7B+upc+name+reviews+%7B+id+product+%7B+name+%7D+author+%7B+id+name+%7D+%7D+%7D+%7D&variables=%7B%22date%22%3A%222022-01-01T00%3A00%3A00%2B00%3A00%22%7D&extensions=%7B+%22persistedQuery%22+%3A+%7B+%22version%22+%3A+1%2C+%22sha256Hash%22+%3A+%2220a101de18d4a9331bfc4ccdfef33cc735876a689490433570f17bdd4c0bad3f%22+%7D+%7D".to_string();
let expected_result = check_deserialization(json!(
{
"query": "{ topProducts { upc name reviews { id product { name } author { id name } } } }",
"variables": {"date": "2022-01-01T00:00:00+00:00"},
"extensions": {
"persistedQuery": {
"version": 1,
"sha256Hash": "20a101de18d4a9331bfc4ccdfef33cc735876a689490433570f17bdd4c0bad3f"
}
}
}));
let req = Request::from_urlencoded_query(query_string).unwrap();
assert_eq!(expected_result, req);
}
#[test]
fn null_extensions() {
let expected_result = check_deserialization(json!(
{
"query": "{ topProducts { upc name reviews { id product { name } author { id name } } } }",
"variables": {"date": "2022-01-01T00:00:00+00:00"},
"extensions": null
}));
insta::assert_yaml_snapshot!(expected_result);
}
#[test]
fn missing_extensions() {
let expected_result = check_deserialization(json!(
{
"query": "{ topProducts { upc name reviews { id product { name } author { id name } } } }",
"variables": {"date": "2022-01-01T00:00:00+00:00"},
}));
insta::assert_yaml_snapshot!(expected_result);
}
#[test]
fn extensions() {
let expected_result = check_deserialization(json!(
{
"query": "{ topProducts { upc name reviews { id product { name } author { id name } } } }",
"variables": {"date": "2022-01-01T00:00:00+00:00"},
"extensions": {
"something_simple": "else",
"something_complex": {
"nested": "value"
}
}
}));
insta::assert_yaml_snapshot!(expected_result);
}
fn check_deserialization(request: serde_json::Value) -> Request {
let string = serde_json::to_string(&request).expect("could not serialize request");
let string_deserialized =
serde_json::from_str(&string).expect("could not deserialize string");
let bytes = Bytes::copy_from_slice(string.as_bytes());
let bytes_deserialized =
Request::deserialize_from_bytes(&bytes).expect("could not deserialize from bytes");
assert_eq!(
string_deserialized, bytes_deserialized,
"string and bytes deserialization did not match"
);
string_deserialized
}
}