use serde::{Deserialize, Serialize};
use serde_json::Value;
use super::rest::{ClientError, RestClient};
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GraphqlError {
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub locations: Option<Vec<GraphqlErrorLocation>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub extensions: Option<Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GraphqlErrorLocation {
pub line: u32,
pub column: u32,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct GraphqlResponse<T> {
pub data: Option<T>,
pub errors: Option<Vec<GraphqlError>>,
}
#[derive(Clone, Debug)]
pub struct GraphqlClient {
rest: RestClient,
}
impl GraphqlClient {
pub(crate) fn new(rest: RestClient) -> Self {
Self { rest }
}
pub async fn query<T, V>(&self, query: &str, variables: Option<V>) -> Result<GraphqlResponse<T>, ClientError>
where
T: for<'de> Deserialize<'de>,
V: Serialize,
{
self.execute(query, None::<String>, variables).await
}
pub async fn mutation<T, V>(
&self,
mutation: &str,
variables: Option<V>,
) -> Result<GraphqlResponse<T>, ClientError>
where
T: for<'de> Deserialize<'de>,
V: Serialize,
{
self.execute(mutation, None::<String>, variables).await
}
pub async fn execute<T, V>(
&self,
query: &str,
operation_name: Option<impl Into<String>>,
variables: Option<V>,
) -> Result<GraphqlResponse<T>, ClientError>
where
T: for<'de> Deserialize<'de>,
V: Serialize,
{
let body = GraphqlRequestBody {
query: query.to_string(),
variables: variables.map(|v| serde_json::to_value(v).unwrap_or_default()),
operation_name: operation_name.map(Into::into),
};
self.rest.post_json("/graphql", &body).await
}
}
#[derive(Serialize)]
struct GraphqlRequestBody {
query: String,
#[serde(skip_serializing_if = "Option::is_none")]
variables: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
operation_name: Option<String>,
}
#[cfg(test)]
mod tests {
use bytes::Bytes;
use http::{HeaderMap, Request, Response};
use serde_json::json;
use crate::client::builder::{BoxedClientService, Client};
fn graphql_mock_client(response_body: Bytes) -> Client {
let service = tower::service_fn(move |_req: Request<Bytes>| {
let body = response_body.clone();
async move {
Ok::<_, crate::BoxError>(
Response::builder()
.header(http::header::CONTENT_TYPE, "application/json")
.body(body)
.unwrap(),
)
}
});
Client::from_service(
BoxedClientService::new(service),
reqwest::Url::parse("http://example.com").unwrap(),
HeaderMap::new(),
)
}
#[tokio::test]
async fn test_graphql_query_success() {
let body = Bytes::from_static(br#"{"data":{"foo":"bar"}}"#);
let client = graphql_mock_client(body);
let resp = client
.graphql()
.query::<serde_json::Value, serde_json::Value>("query GetFoo { foo }", None)
.await
.unwrap();
assert_eq!(resp.data.unwrap()["foo"], "bar");
assert!(resp.errors.is_none());
}
#[tokio::test]
async fn test_graphql_query_errors() {
let body = Bytes::from_static(
br#"{"errors":[{"message":"unknown query","locations":[{"line":1,"column":1}]}]}"#,
);
let client = graphql_mock_client(body);
let resp = client
.graphql()
.query::<serde_json::Value, serde_json::Value>("query Bad { }", None)
.await
.unwrap();
assert!(resp.data.is_none());
let errors = resp.errors.unwrap();
assert_eq!(errors[0].message, "unknown query");
assert_eq!(errors[0].locations.as_ref().unwrap()[0].line, 1);
}
#[tokio::test]
async fn test_graphql_mutation_and_variables() {
let body = Bytes::from_static(br#"{"data":{"create":true}}"#);
let client = graphql_mock_client(body);
let resp = client
.graphql()
.mutation::<serde_json::Value, serde_json::Value>(
"mutation Create { create }",
Some(json!({"input": "x"})),
)
.await
.unwrap();
assert_eq!(resp.data.unwrap()["create"], true);
}
#[tokio::test]
async fn test_graphql_execute_with_operation_name() {
let body = Bytes::from_static(br#"{"data":{"result":42}}"#);
let client = graphql_mock_client(body);
let resp = client
.graphql()
.execute::<serde_json::Value, serde_json::Value>(
"query Named { result }",
Some("Named"),
Some(json!({})),
)
.await
.unwrap();
assert_eq!(resp.data.unwrap()["result"], 42);
}
}