1#![allow(missing_docs)] use std::time::Instant;
3
4use apollo_compiler::response::ExecutionResponse;
5use bytes::Bytes;
6use displaydoc::Display;
7use serde::Deserialize;
8use serde::Serialize;
9use serde_json_bytes::ByteString;
10use serde_json_bytes::Map;
11
12use crate::error::Error;
13use crate::graphql::IntoGraphQLErrors;
14use crate::json_ext::Object;
15use crate::json_ext::Path;
16use crate::json_ext::Value;
17
18#[derive(thiserror::Error, Display, Debug, Eq, PartialEq)]
19#[error("GraphQL response was malformed: {reason}")]
20pub(crate) struct MalformedResponseError {
21 pub(crate) reason: String,
23}
24
25#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Default)]
28#[serde(rename_all = "camelCase")]
29#[non_exhaustive]
30pub struct Response {
31 #[serde(skip_serializing_if = "Option::is_none", default)]
33 pub label: Option<String>,
34
35 #[serde(skip_serializing_if = "Option::is_none", default)]
37 pub data: Option<Value>,
38
39 #[serde(skip_serializing_if = "Option::is_none", default)]
41 pub path: Option<Path>,
42
43 #[serde(skip_serializing_if = "Vec::is_empty", default)]
45 pub errors: Vec<Error>,
46
47 #[serde(skip_serializing_if = "Object::is_empty", default)]
49 pub extensions: Object,
50
51 #[serde(skip_serializing_if = "Option::is_none", default)]
52 pub has_next: Option<bool>,
53
54 #[serde(skip, default)]
55 pub subscribed: Option<bool>,
56
57 #[serde(skip, default)]
59 pub created_at: Option<Instant>,
60
61 #[serde(skip_serializing_if = "Vec::is_empty", default)]
62 pub incremental: Vec<IncrementalResponse>,
63}
64
65#[buildstructor::buildstructor]
66impl Response {
67 #[builder(visibility = "pub")]
69 fn new(
70 label: Option<String>,
71 data: Option<Value>,
72 path: Option<Path>,
73 errors: Vec<Error>,
74 extensions: Map<ByteString, Value>,
75 _subselection: Option<String>,
76 has_next: Option<bool>,
77 subscribed: Option<bool>,
78 incremental: Vec<IncrementalResponse>,
79 created_at: Option<Instant>,
80 ) -> Self {
81 Self {
82 label,
83 data,
84 path,
85 errors,
86 extensions,
87 has_next,
88 subscribed,
89 incremental,
90 created_at,
91 }
92 }
93
94 pub fn is_primary(&self) -> bool {
96 self.path.is_none()
97 }
98
99 pub fn append_errors(&mut self, errors: &mut Vec<Error>) {
101 self.errors.append(errors)
102 }
103
104 pub(crate) fn from_bytes(b: Bytes) -> Result<Response, MalformedResponseError> {
108 let value = Value::from_bytes(b).map_err(|error| MalformedResponseError {
109 reason: error.to_string(),
110 })?;
111 Response::from_value(value)
112 }
113
114 pub(crate) fn from_value(value: Value) -> Result<Response, MalformedResponseError> {
115 let mut object = ensure_object!(value).map_err(|error| MalformedResponseError {
116 reason: error.to_string(),
117 })?;
118 let data = object.remove("data");
119 let errors = extract_key_value_from_object!(object, "errors", Value::Array(v) => v)
120 .map_err(|err| MalformedResponseError {
121 reason: err.to_string(),
122 })?
123 .into_iter()
124 .flatten()
125 .map(Error::from_value)
126 .collect::<Result<Vec<Error>, MalformedResponseError>>()?;
127 let extensions =
128 extract_key_value_from_object!(object, "extensions", Value::Object(o) => o)
129 .map_err(|err| MalformedResponseError {
130 reason: err.to_string(),
131 })?
132 .unwrap_or_default();
133 let label = extract_key_value_from_object!(object, "label", Value::String(s) => s)
134 .map_err(|err| MalformedResponseError {
135 reason: err.to_string(),
136 })?
137 .map(|s| s.as_str().to_string());
138 let path = extract_key_value_from_object!(object, "path")
139 .map(serde_json_bytes::from_value)
140 .transpose()
141 .map_err(|err| MalformedResponseError {
142 reason: err.to_string(),
143 })?;
144 let has_next = extract_key_value_from_object!(object, "hasNext", Value::Bool(b) => b)
145 .map_err(|err| MalformedResponseError {
146 reason: err.to_string(),
147 })?;
148 let incremental =
149 extract_key_value_from_object!(object, "incremental", Value::Array(a) => a).map_err(
150 |err| MalformedResponseError {
151 reason: err.to_string(),
152 },
153 )?;
154 let incremental: Vec<IncrementalResponse> = match incremental {
155 Some(v) => v
156 .into_iter()
157 .map(serde_json_bytes::from_value)
158 .collect::<Result<Vec<IncrementalResponse>, _>>()
159 .map_err(|err| MalformedResponseError {
160 reason: err.to_string(),
161 })?,
162 None => vec![],
163 };
164 if data.is_none() && errors.is_empty() {
168 return Err(MalformedResponseError {
169 reason: "graphql response without data must contain at least one error".to_string(),
170 });
171 }
172
173 Ok(Response {
174 label,
175 data,
176 path,
177 errors,
178 extensions,
179 has_next,
180 subscribed: None,
181 incremental,
182 created_at: None,
183 })
184 }
185}
186
187#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Default)]
190#[serde(rename_all = "camelCase")]
191#[non_exhaustive]
192pub struct IncrementalResponse {
193 #[serde(skip_serializing_if = "Option::is_none", default)]
195 pub label: Option<String>,
196
197 #[serde(skip_serializing_if = "Option::is_none", default)]
199 pub data: Option<Value>,
200
201 #[serde(skip_serializing_if = "Option::is_none", default)]
203 pub path: Option<Path>,
204
205 #[serde(skip_serializing_if = "Vec::is_empty", default)]
207 pub errors: Vec<Error>,
208
209 #[serde(skip_serializing_if = "Object::is_empty", default)]
211 pub extensions: Object,
212}
213
214#[buildstructor::buildstructor]
215impl IncrementalResponse {
216 #[builder(visibility = "pub")]
218 fn new(
219 label: Option<String>,
220 data: Option<Value>,
221 path: Option<Path>,
222 errors: Vec<Error>,
223 extensions: Map<ByteString, Value>,
224 ) -> Self {
225 Self {
226 label,
227 data,
228 path,
229 errors,
230 extensions,
231 }
232 }
233
234 pub fn append_errors(&mut self, errors: &mut Vec<Error>) {
236 self.errors.append(errors)
237 }
238}
239
240impl From<ExecutionResponse> for Response {
241 fn from(response: ExecutionResponse) -> Response {
242 let ExecutionResponse { errors, data } = response;
243 Self {
244 errors: errors.into_graphql_errors().unwrap(),
245 data: data.map(serde_json_bytes::Value::Object),
246 extensions: Default::default(),
247 label: None,
248 path: None,
249 has_next: None,
250 subscribed: None,
251 created_at: None,
252 incremental: Vec::new(),
253 }
254 }
255}
256
257#[cfg(test)]
258mod tests {
259 use serde_json::json;
260 use serde_json_bytes::json as bjson;
261 use uuid::Uuid;
262
263 use super::*;
264 use crate::assert_response_eq_ignoring_error_id;
265 use crate::graphql;
266 use crate::graphql::Error;
267 use crate::graphql::Location;
268 use crate::graphql::Response;
269
270 #[test]
271 fn test_append_errors_path_fallback_and_override() {
272 let uuid1 = Uuid::new_v4();
273 let uuid2 = Uuid::new_v4();
274 let expected_errors = vec![
275 Error::builder()
276 .message("Something terrible happened!")
277 .path(Path::from("here"))
278 .apollo_id(uuid1)
279 .build(),
280 Error::builder()
281 .message("I mean for real")
282 .apollo_id(uuid2)
283 .build(),
284 ];
285
286 let mut errors_to_append = vec![
287 Error::builder()
288 .message("Something terrible happened!")
289 .path(Path::from("here"))
290 .apollo_id(uuid1)
291 .build(),
292 Error::builder()
293 .message("I mean for real")
294 .apollo_id(uuid2)
295 .build(),
296 ];
297
298 let mut response = Response::builder().build();
299 response.append_errors(&mut errors_to_append);
300 assert_eq!(response.errors, expected_errors);
301 }
302
303 #[test]
304 fn test_response() {
305 let result = serde_json::from_str::<Response>(
306 json!(
307 {
308 "errors": [
309 {
310 "message": "Name for character with ID 1002 could not be fetched.",
311 "locations": [{ "line": 6, "column": 7 }],
312 "path": ["hero", "heroFriends", 1, "name"],
313 "extensions": {
314 "error-extension": 5,
315 }
316 }
317 ],
318 "data": {
319 "hero": {
320 "name": "R2-D2",
321 "heroFriends": [
322 {
323 "id": "1000",
324 "name": "Luke Skywalker"
325 },
326 {
327 "id": "1002",
328 "name": null
329 },
330 {
331 "id": "1003",
332 "name": "Leia Organa"
333 }
334 ]
335 }
336 },
337 "extensions": {
338 "response-extension": 3,
339 }
340 })
341 .to_string()
342 .as_str(),
343 );
344 let response = result.unwrap();
345 assert_response_eq_ignoring_error_id!(
346 response,
347 Response::builder()
348 .data(json!({
349 "hero": {
350 "name": "R2-D2",
351 "heroFriends": [
352 {
353 "id": "1000",
354 "name": "Luke Skywalker"
355 },
356 {
357 "id": "1002",
358 "name": null
359 },
360 {
361 "id": "1003",
362 "name": "Leia Organa"
363 }
364 ]
365 }
366 }))
367 .errors(vec![
368 Error::builder()
369 .message("Name for character with ID 1002 could not be fetched.")
370 .locations(vec!(Location { line: 6, column: 7 }))
371 .path(Path::from("hero/heroFriends/1/name"))
372 .extensions(
373 bjson!({ "error-extension": 5, })
374 .as_object()
375 .cloned()
376 .unwrap()
377 )
378 .build()
379 ])
380 .extensions(
381 bjson!({
382 "response-extension": 3,
383 })
384 .as_object()
385 .cloned()
386 .unwrap()
387 )
388 .build()
389 );
390 }
391
392 #[test]
393 fn test_patch_response() {
394 let result = serde_json::from_str::<Response>(
395 json!(
396 {
397 "label": "part",
398 "hasNext": true,
399 "path": ["hero", "heroFriends", 1, "name"],
400 "errors": [
401 {
402 "message": "Name for character with ID 1002 could not be fetched.",
403 "locations": [{ "line": 6, "column": 7 }],
404 "path": ["hero", "heroFriends", 1, "name"],
405 "extensions": {
406 "error-extension": 5,
407 }
408 }
409 ],
410 "data": {
411 "hero": {
412 "name": "R2-D2",
413 "heroFriends": [
414 {
415 "id": "1000",
416 "name": "Luke Skywalker"
417 },
418 {
419 "id": "1002",
420 "name": null
421 },
422 {
423 "id": "1003",
424 "name": "Leia Organa"
425 }
426 ]
427 }
428 },
429 "extensions": {
430 "response-extension": 3,
431 }
432 })
433 .to_string()
434 .as_str(),
435 );
436 let response = result.unwrap();
437 assert_response_eq_ignoring_error_id!(
438 response,
439 Response::builder()
440 .label("part".to_owned())
441 .data(json!({
442 "hero": {
443 "name": "R2-D2",
444 "heroFriends": [
445 {
446 "id": "1000",
447 "name": "Luke Skywalker"
448 },
449 {
450 "id": "1002",
451 "name": null
452 },
453 {
454 "id": "1003",
455 "name": "Leia Organa"
456 }
457 ]
458 }
459 }))
460 .path(Path::from("hero/heroFriends/1/name"))
461 .errors(vec![
462 Error::builder()
463 .message("Name for character with ID 1002 could not be fetched.")
464 .locations(vec!(Location { line: 6, column: 7 }))
465 .path(Path::from("hero/heroFriends/1/name"))
466 .extensions(
467 bjson!({ "error-extension": 5, })
468 .as_object()
469 .cloned()
470 .unwrap()
471 )
472 .build()
473 ])
474 .extensions(
475 bjson!({
476 "response-extension": 3,
477 })
478 .as_object()
479 .cloned()
480 .unwrap()
481 )
482 .has_next(true)
483 .build()
484 );
485 }
486
487 #[test]
488 fn test_no_data_and_no_errors() {
489 let response = Response::from_bytes("{\"errors\":null}".into());
490 assert_eq!(
491 response.expect_err("no data and no errors"),
492 MalformedResponseError {
493 reason: "graphql response without data must contain at least one error".to_string(),
494 }
495 );
496 }
497
498 #[test]
499 fn test_data_null() {
500 let response = Response::from_bytes("{\"data\":null}".into()).unwrap();
501 assert_eq!(
502 response,
503 Response::builder().data(Some(Value::Null)).build(),
504 );
505 }
506}