thegraph_graphql_http/http/response.rs
1/// The preferred type for GraphQL-over-HTTP server responses. As specified in the section
2/// [4.1 Media Types](https://graphql.github.io/graphql-over-http/draft/#sec-Media-Types) of the
3/// GraphQL-over-HTTP specification.
4pub const GRAPHQL_RESPONSE_MEDIA_TYPE: &str = "application/graphql-response+json";
5
6/// The legacy type for GraphQL-over-HTTP server responses. As specified in the section
7/// [4.1 Media Types](https://graphql.github.io/graphql-over-http/draft/#sec-Media-Types) of the
8/// GraphQL-over-HTTP specification.
9pub const GRAPHQL_LEGACY_RESPONSE_MEDIA_TYPE: &str = "application/json";
10
11/// The response error type for GraphQL-over-HTTP server responses. As specified in the section
12/// [7.1.2 Errors](https://spec.graphql.org/draft/#sec-Errors) and the
13/// [Error Result Format](https://spec.graphql.org/draft/#sec-Errors.Error-Result-Format) subsection
14/// of the GraphQL specification.
15#[derive(Debug, serde::Deserialize, serde::Serialize)]
16pub struct Error {
17 /// A short, human-readable description of the problem.
18 ///
19 /// From the [Error Result Format](https://spec.graphql.org/draft/#sec-Errors.Error-Result-Format)
20 /// subsection of the GraphQL specification:
21 ///
22 /// > Every error MUST contain an entry with the key `message` with a string description of the
23 /// > error intended for the developer as a guide to understand and correct the error.
24 pub message: String,
25
26 /// A list of locations describing the beginning of the associated syntax element causing the
27 /// error.
28 ///
29 /// From the [Error Result Format](https://spec.graphql.org/draft/#sec-Errors.Error-Result-Format)
30 /// subsection of the GraphQL specification:
31 ///
32 /// > If an error can be associated to a particular point in the requested GraphQL document, it
33 /// > SHOULD contain an entry with the key `locations` with a list of locations, where each
34 /// > location is a map with the keys `line` and `column`, both positive numbers starting from
35 /// > `1` which describe the beginning of an associated syntax element.
36 #[serde(default)]
37 #[serde(skip_serializing_if = "Vec::is_empty")]
38 pub locations: Vec<ErrorLocation>,
39
40 /// A list of path segments starting at the root of the response and ending with the field
41 /// associated with the error.
42 ///
43 /// From the [Error Result Format](https://spec.graphql.org/draft/#sec-Errors.Error-Result-Format)
44 /// subsection of the GraphQL specification:
45 ///
46 /// > If an error can be associated to a particular field in the GraphQL result, it must contain
47 /// > an entry with the key `path` that details the path of the response field which experienced
48 /// > the error. This allows clients to identify whether a `null` result is intentional or
49 /// > caused by a runtime error.
50 /// >
51 /// > This field should be a list of path segments starting at the root of the response and
52 /// > ending with the field associated with the error. Path segments that represent fields
53 /// > should be strings, and path segments that represent list indices should be 0-indexed
54 /// > integers. If the error happens in an aliased field, the path to the error should use the
55 /// > aliased name, since it represents a path in the response, not in the request.
56 #[serde(default)]
57 #[serde(skip_serializing_if = "Vec::is_empty")]
58 pub path: Vec<String>,
59}
60
61impl Error {
62 /// Convert a static string into an [`Error`].
63 pub fn from_static(message: &'static str) -> Self {
64 Self {
65 message: message.to_string(),
66 locations: vec![],
67 path: vec![],
68 }
69 }
70}
71
72/// A trait for types that can be converted into [`Error`], a GraphQL HTTP Response error.
73pub trait IntoError {
74 /// Convert the type into [`Error`].
75 fn into_error(self) -> Error;
76}
77
78impl IntoError for Error {
79 #[inline]
80 fn into_error(self) -> Error {
81 self
82 }
83}
84
85impl<T> IntoError for T
86where
87 T: std::error::Error,
88{
89 fn into_error(self) -> Error {
90 Error {
91 message: self.to_string(),
92 locations: vec![],
93 path: vec![],
94 }
95 }
96}
97
98/// A location describing the beginning of the associated syntax element causing the error.
99#[derive(Debug, serde::Deserialize, serde::Serialize)]
100pub struct ErrorLocation {
101 pub line: usize,
102 pub column: usize,
103}
104
105/// A response to a GraphQL request.
106///
107/// As specified in the section [7. Response](https://spec.graphql.org/draft/#sec-Response) of the
108/// GraphQL specification.
109#[derive(Debug, serde::Deserialize, serde::Serialize)]
110pub struct ResponseBody<T> {
111 /// The response will be the result of the execution of the requested operation.
112 ///
113 /// If the operation was a query, this output will be an object of the query root operation
114 /// type; if the operation was a mutation, this output will be an object of the mutation root
115 /// operation type.
116 ///
117 /// If an error was raised before execution begins, the data entry should not be present in the
118 /// result; If an error was raised during the execution that prevented a valid response, the
119 /// data entry in the response should be `null`. In both cases the field will be set to `None`.
120 #[serde(skip_serializing_if = "Option::is_none")]
121 pub data: Option<T>,
122
123 /// The errors entry in the response is a non-empty list of [`Error`] raised during the request,
124 /// where each error is a map of data described by the error result specified in the section
125 /// [7.1.2. Errors](https://spec.graphql.org/draft/#sec-Errors) of the GraphQL specification.
126 #[serde(default)]
127 #[serde(skip_serializing_if = "Vec::is_empty")]
128 pub errors: Vec<Error>,
129}
130
131impl<T> ResponseBody<T> {
132 /// Create a new response body with the given data.
133 pub fn from_data(data: T) -> Self {
134 Self {
135 data: Some(data),
136 errors: vec![],
137 }
138 }
139
140 /// Create a new response body with the given error.
141 pub fn from_error(error: impl IntoError) -> Self {
142 Self {
143 data: None,
144 errors: vec![error.into_error()],
145 }
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use assert_matches::assert_matches;
152
153 use super::{Error, IntoError, ResponseBody};
154
155 /// Deserialize the given string as a GraphQL response body.
156 fn deserialize_response_body<T>(response_body: &str) -> serde_json::Result<ResponseBody<T>>
157 where
158 T: serde::de::DeserializeOwned,
159 {
160 serde_json::from_str(response_body)
161 }
162
163 /// Serialize the given data as a GraphQL response body.
164 fn serialize_response_data_body<T>(data: T) -> String
165 where
166 T: serde::ser::Serialize,
167 {
168 let response_body = ResponseBody::from_data(data);
169 serde_json::to_string(&response_body).unwrap()
170 }
171
172 /// Ensure that the response body is correctly serialized when the data is a string.
173 #[test]
174 fn serialize_response_with_string_data() {
175 //* Given
176 let data = "test data";
177
178 //* When
179 let response_body = serialize_response_data_body(data);
180
181 //* Then
182 // Ensure that the response body is a valid GraphQL response.
183 assert_matches!(deserialize_response_body::<String>(&response_body), Ok(resp_body) => {
184 // The data should be returned
185 assert_eq!(resp_body.data, Some("test data".to_string()));
186
187 // There should be no errors
188 assert_eq!(resp_body.errors.len(), 0);
189 });
190 }
191
192 /// Test data type implementing the serde traits.
193 ///
194 /// See [`serialize_response_with_struct_data`] test.
195 #[derive(Debug, serde::Deserialize, serde::Serialize)]
196 struct Data {
197 field: String,
198 }
199
200 /// Ensure that the response body is correctly serialized when the data is a struct.
201 #[test]
202 fn serialize_response_with_struct_data() {
203 //* Given
204 let data = Data {
205 field: "test data".to_string(),
206 };
207
208 //* When
209 let response_body = serialize_response_data_body(data);
210
211 //* Then
212 // Ensure that the response body is a valid GraphQL response.
213 assert_matches!(deserialize_response_body::<Data>(&response_body), Ok(resp_body) => {
214 // There should be no errors
215 assert_eq!(resp_body.errors.len(), 0);
216
217 // The data should be returned
218 assert_matches!(resp_body.data, Some(data) => {
219 assert_eq!(data.field, "test data");
220 });
221 });
222 }
223
224 /// Serialize the given error as a GraphQL error response body.
225 fn serialize_error_response_body(error: impl IntoError) -> String {
226 let response_body = ResponseBody::<()>::from_error(error);
227 serde_json::to_string(&response_body).unwrap()
228 }
229
230 /// Ensure that the error response body is correctly serialized when the error is a string.
231 #[test]
232 fn serialize_response_with_error_from_static() {
233 //* Given
234 let error = Error::from_static("test error message");
235
236 //* When
237 let response_body = serialize_error_response_body(error);
238
239 //* Then
240 // Ensure that the response body is a valid GraphQL error response.
241 assert_matches!(deserialize_response_body::<()>(&response_body), Ok(resp_body) => {
242 // No data should be returned
243 assert_eq!(resp_body.data, None);
244
245 // There should be one error
246 assert_eq!(resp_body.errors.len(), 1);
247 assert_eq!(resp_body.errors[0].message, "test error message");
248 });
249 }
250
251 /// Test error type implementing the `std::error::Error` trait.
252 ///
253 /// Se [`serialize_response_with_struct_implementing_error_trait`] test.
254 #[derive(Debug, thiserror::Error)]
255 #[error("test error: {cause}")]
256 struct TestError {
257 cause: String,
258 }
259
260 /// Ensure that the error response body is correctly serialized when the error is an object
261 /// implementing the `std::error::Error` trait.
262 #[test]
263 fn serialize_response_with_struct_implementing_error_trait() {
264 //* Given
265 let error = TestError {
266 cause: "test message".to_string(),
267 };
268
269 //* When
270 let response_body = serialize_error_response_body(error);
271
272 //* Then
273 // Ensure that the response body is a valid GraphQL error response.
274 assert_matches!(deserialize_response_body::<()>(&response_body), Ok(resp_body) => {
275 // No data should be returned
276 assert_eq!(resp_body.data, None);
277
278 // There should be one error
279 assert_eq!(resp_body.errors.len(), 1);
280 assert_eq!(resp_body.errors[0].message, "test error: test message");
281 });
282 }
283}