pub const GRAPHQL_RESPONSE_MEDIA_TYPE: &str = "application/graphql-response+json";
pub const GRAPHQL_LEGACY_RESPONSE_MEDIA_TYPE: &str = "application/json";
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct Error {
pub message: String,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub locations: Vec<ErrorLocation>,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub path: Vec<String>,
}
impl Error {
pub fn from_static(message: &'static str) -> Self {
Self {
message: message.to_string(),
locations: vec![],
path: vec![],
}
}
}
pub trait IntoError {
fn into_error(self) -> Error;
}
impl IntoError for Error {
#[inline]
fn into_error(self) -> Error {
self
}
}
impl<T> IntoError for T
where
T: std::error::Error,
{
fn into_error(self) -> Error {
Error {
message: self.to_string(),
locations: vec![],
path: vec![],
}
}
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct ErrorLocation {
pub line: usize,
pub column: usize,
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
pub struct ResponseBody<T> {
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<T>,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub errors: Vec<Error>,
}
impl<T> ResponseBody<T> {
pub fn from_data(data: T) -> Self {
Self {
data: Some(data),
errors: vec![],
}
}
pub fn from_error(error: impl IntoError) -> Self {
Self {
data: None,
errors: vec![error.into_error()],
}
}
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use super::{Error, IntoError, ResponseBody};
fn deserialize_response_body<T>(response_body: &str) -> serde_json::Result<ResponseBody<T>>
where
T: serde::de::DeserializeOwned,
{
serde_json::from_str(response_body)
}
fn serialize_response_data_body<T>(data: T) -> String
where
T: serde::ser::Serialize,
{
let response_body = ResponseBody::from_data(data);
serde_json::to_string(&response_body).unwrap()
}
#[test]
fn serialize_response_with_string_data() {
let data = "test data";
let response_body = serialize_response_data_body(data);
assert_matches!(deserialize_response_body::<String>(&response_body), Ok(resp_body) => {
assert_eq!(resp_body.data, Some("test data".to_string()));
assert_eq!(resp_body.errors.len(), 0);
});
}
#[derive(Debug, serde::Deserialize, serde::Serialize)]
struct Data {
field: String,
}
#[test]
fn serialize_response_with_struct_data() {
let data = Data {
field: "test data".to_string(),
};
let response_body = serialize_response_data_body(data);
assert_matches!(deserialize_response_body::<Data>(&response_body), Ok(resp_body) => {
assert_eq!(resp_body.errors.len(), 0);
assert_matches!(resp_body.data, Some(data) => {
assert_eq!(data.field, "test data");
});
});
}
fn serialize_error_response_body(error: impl IntoError) -> String {
let response_body = ResponseBody::<()>::from_error(error);
serde_json::to_string(&response_body).unwrap()
}
#[test]
fn serialize_response_with_error_from_static() {
let error = Error::from_static("test error message");
let response_body = serialize_error_response_body(error);
assert_matches!(deserialize_response_body::<()>(&response_body), Ok(resp_body) => {
assert_eq!(resp_body.data, None);
assert_eq!(resp_body.errors.len(), 1);
assert_eq!(resp_body.errors[0].message, "test error message");
});
}
#[derive(Debug, thiserror::Error)]
#[error("test error: {cause}")]
struct TestError {
cause: String,
}
#[test]
fn serialize_response_with_struct_implementing_error_trait() {
let error = TestError {
cause: "test message".to_string(),
};
let response_body = serialize_error_response_body(error);
assert_matches!(deserialize_response_body::<()>(&response_body), Ok(resp_body) => {
assert_eq!(resp_body.data, None);
assert_eq!(resp_body.errors.len(), 1);
assert_eq!(resp_body.errors[0].message, "test error: test message");
});
}
}