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}