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| {
109 let mut reason = error.to_string();
110
111 if error.classify() == serde_json::error::Category::Syntax
120 && reason.contains("unexpected end of hex escape")
121 {
122 reason.push_str("; the response contains an unpaired Unicode surrogate");
123 }
124 MalformedResponseError { reason }
125 })?;
126 Response::from_value(value)
127 }
128
129 pub(crate) fn from_value(value: Value) -> Result<Response, MalformedResponseError> {
130 let mut object = ensure_object!(value).map_err(|error| MalformedResponseError {
131 reason: error.to_string(),
132 })?;
133 let data = object.remove("data");
134 let errors = extract_key_value_from_object!(object, "errors", Value::Array(v) => v)
135 .map_err(|err| MalformedResponseError {
136 reason: err.to_string(),
137 })?
138 .into_iter()
139 .flatten()
140 .map(Error::from_value)
141 .collect::<Result<Vec<Error>, MalformedResponseError>>()?;
142 let extensions =
143 extract_key_value_from_object!(object, "extensions", Value::Object(o) => o)
144 .map_err(|err| MalformedResponseError {
145 reason: err.to_string(),
146 })?
147 .unwrap_or_default();
148 let label = extract_key_value_from_object!(object, "label", Value::String(s) => s)
149 .map_err(|err| MalformedResponseError {
150 reason: err.to_string(),
151 })?
152 .map(|s| s.as_str().to_string());
153 let path = extract_key_value_from_object!(object, "path")
154 .map(serde_json_bytes::from_value)
155 .transpose()
156 .map_err(|err| MalformedResponseError {
157 reason: err.to_string(),
158 })?;
159 let has_next = extract_key_value_from_object!(object, "hasNext", Value::Bool(b) => b)
160 .map_err(|err| MalformedResponseError {
161 reason: err.to_string(),
162 })?;
163 let incremental =
164 extract_key_value_from_object!(object, "incremental", Value::Array(a) => a).map_err(
165 |err| MalformedResponseError {
166 reason: err.to_string(),
167 },
168 )?;
169 let incremental: Vec<IncrementalResponse> = match incremental {
170 Some(v) => v
171 .into_iter()
172 .map(serde_json_bytes::from_value)
173 .collect::<Result<Vec<IncrementalResponse>, _>>()
174 .map_err(|err| MalformedResponseError {
175 reason: err.to_string(),
176 })?,
177 None => vec![],
178 };
179 if data.is_none() && errors.is_empty() {
183 return Err(MalformedResponseError {
184 reason: "graphql response without data must contain at least one error".to_string(),
185 });
186 }
187
188 Ok(Response {
189 label,
190 data,
191 path,
192 errors,
193 extensions,
194 has_next,
195 subscribed: None,
196 incremental,
197 created_at: None,
198 })
199 }
200}
201
202#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Default)]
205#[serde(rename_all = "camelCase")]
206#[non_exhaustive]
207pub struct IncrementalResponse {
208 #[serde(skip_serializing_if = "Option::is_none", default)]
210 pub label: Option<String>,
211
212 #[serde(skip_serializing_if = "Option::is_none", default)]
214 pub data: Option<Value>,
215
216 #[serde(skip_serializing_if = "Option::is_none", default)]
218 pub path: Option<Path>,
219
220 #[serde(skip_serializing_if = "Vec::is_empty", default)]
222 pub errors: Vec<Error>,
223
224 #[serde(skip_serializing_if = "Object::is_empty", default)]
226 pub extensions: Object,
227}
228
229#[buildstructor::buildstructor]
230impl IncrementalResponse {
231 #[builder(visibility = "pub")]
233 fn new(
234 label: Option<String>,
235 data: Option<Value>,
236 path: Option<Path>,
237 errors: Vec<Error>,
238 extensions: Map<ByteString, Value>,
239 ) -> Self {
240 Self {
241 label,
242 data,
243 path,
244 errors,
245 extensions,
246 }
247 }
248
249 pub fn append_errors(&mut self, errors: &mut Vec<Error>) {
251 self.errors.append(errors)
252 }
253}
254
255impl From<ExecutionResponse> for Response {
256 fn from(response: ExecutionResponse) -> Response {
257 let ExecutionResponse { errors, data } = response;
258 Self {
259 errors: errors.into_graphql_errors().unwrap(),
260 data: data.map(serde_json_bytes::Value::Object),
261 extensions: Default::default(),
262 label: None,
263 path: None,
264 has_next: None,
265 subscribed: None,
266 created_at: None,
267 incremental: Vec::new(),
268 }
269 }
270}
271
272impl Response {
273 pub(crate) fn all_errors(&self) -> impl Iterator<Item = &Error> {
274 self.errors
275 .iter()
276 .chain(self.incremental.iter().flat_map(|r| r.errors.iter()))
277 }
278
279 pub(crate) fn contains_errors(&self) -> bool {
280 self.all_errors().next().is_some()
281 }
282}
283
284#[cfg(test)]
285impl Response {
286 pub(crate) fn errors_with_code<'a>(&'a self, code: &'a str) -> impl Iterator<Item = &'a Error> {
287 self.errors
288 .iter()
289 .filter(move |err| err.extension_code().is_some_and(|c| c == code))
290 }
291
292 pub(crate) fn contains_error_code(&self, code: &str) -> bool {
293 self.errors_with_code(code).next().is_some()
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use serde_json::json;
300 use serde_json_bytes::json as bjson;
301 use uuid::Uuid;
302
303 use super::*;
304 use crate::assert_response_eq_ignoring_error_id;
305 use crate::graphql;
306 use crate::graphql::Error;
307 use crate::graphql::Location;
308 use crate::graphql::Response;
309
310 #[test]
311 fn test_append_errors_path_fallback_and_override() {
312 let uuid1 = Uuid::new_v4();
313 let uuid2 = Uuid::new_v4();
314 let expected_errors = vec![
315 Error::builder()
316 .message("Something terrible happened!")
317 .path(Path::from("here"))
318 .apollo_id(uuid1)
319 .build(),
320 Error::builder()
321 .message("I mean for real")
322 .apollo_id(uuid2)
323 .build(),
324 ];
325
326 let mut errors_to_append = vec![
327 Error::builder()
328 .message("Something terrible happened!")
329 .path(Path::from("here"))
330 .apollo_id(uuid1)
331 .build(),
332 Error::builder()
333 .message("I mean for real")
334 .apollo_id(uuid2)
335 .build(),
336 ];
337
338 let mut response = Response::builder().build();
339 response.append_errors(&mut errors_to_append);
340 assert_eq!(response.errors, expected_errors);
341 }
342
343 #[test]
344 fn test_response() {
345 let result = serde_json::from_str::<Response>(
346 json!(
347 {
348 "errors": [
349 {
350 "message": "Name for character with ID 1002 could not be fetched.",
351 "locations": [{ "line": 6, "column": 7 }],
352 "path": ["hero", "heroFriends", 1, "name"],
353 "extensions": {
354 "error-extension": 5,
355 }
356 }
357 ],
358 "data": {
359 "hero": {
360 "name": "R2-D2",
361 "heroFriends": [
362 {
363 "id": "1000",
364 "name": "Luke Skywalker"
365 },
366 {
367 "id": "1002",
368 "name": null
369 },
370 {
371 "id": "1003",
372 "name": "Leia Organa"
373 }
374 ]
375 }
376 },
377 "extensions": {
378 "response-extension": 3,
379 }
380 })
381 .to_string()
382 .as_str(),
383 );
384 let response = result.unwrap();
385 assert_response_eq_ignoring_error_id!(
386 response,
387 Response::builder()
388 .data(json!({
389 "hero": {
390 "name": "R2-D2",
391 "heroFriends": [
392 {
393 "id": "1000",
394 "name": "Luke Skywalker"
395 },
396 {
397 "id": "1002",
398 "name": null
399 },
400 {
401 "id": "1003",
402 "name": "Leia Organa"
403 }
404 ]
405 }
406 }))
407 .errors(vec![
408 Error::builder()
409 .message("Name for character with ID 1002 could not be fetched.")
410 .locations(vec!(Location { line: 6, column: 7 }))
411 .path(Path::from("hero/heroFriends/1/name"))
412 .extensions(
413 bjson!({ "error-extension": 5, })
414 .as_object()
415 .cloned()
416 .unwrap()
417 )
418 .build()
419 ])
420 .extensions(
421 bjson!({
422 "response-extension": 3,
423 })
424 .as_object()
425 .cloned()
426 .unwrap()
427 )
428 .build()
429 );
430 }
431
432 #[test]
433 fn test_patch_response() {
434 let result = serde_json::from_str::<Response>(
435 json!(
436 {
437 "label": "part",
438 "hasNext": true,
439 "path": ["hero", "heroFriends", 1, "name"],
440 "errors": [
441 {
442 "message": "Name for character with ID 1002 could not be fetched.",
443 "locations": [{ "line": 6, "column": 7 }],
444 "path": ["hero", "heroFriends", 1, "name"],
445 "extensions": {
446 "error-extension": 5,
447 }
448 }
449 ],
450 "data": {
451 "hero": {
452 "name": "R2-D2",
453 "heroFriends": [
454 {
455 "id": "1000",
456 "name": "Luke Skywalker"
457 },
458 {
459 "id": "1002",
460 "name": null
461 },
462 {
463 "id": "1003",
464 "name": "Leia Organa"
465 }
466 ]
467 }
468 },
469 "extensions": {
470 "response-extension": 3,
471 }
472 })
473 .to_string()
474 .as_str(),
475 );
476 let response = result.unwrap();
477 assert_response_eq_ignoring_error_id!(
478 response,
479 Response::builder()
480 .label("part".to_owned())
481 .data(json!({
482 "hero": {
483 "name": "R2-D2",
484 "heroFriends": [
485 {
486 "id": "1000",
487 "name": "Luke Skywalker"
488 },
489 {
490 "id": "1002",
491 "name": null
492 },
493 {
494 "id": "1003",
495 "name": "Leia Organa"
496 }
497 ]
498 }
499 }))
500 .path(Path::from("hero/heroFriends/1/name"))
501 .errors(vec![
502 Error::builder()
503 .message("Name for character with ID 1002 could not be fetched.")
504 .locations(vec!(Location { line: 6, column: 7 }))
505 .path(Path::from("hero/heroFriends/1/name"))
506 .extensions(
507 bjson!({ "error-extension": 5, })
508 .as_object()
509 .cloned()
510 .unwrap()
511 )
512 .build()
513 ])
514 .extensions(
515 bjson!({
516 "response-extension": 3,
517 })
518 .as_object()
519 .cloned()
520 .unwrap()
521 )
522 .has_next(true)
523 .build()
524 );
525 }
526
527 #[test]
528 fn test_no_data_and_no_errors() {
529 let response = Response::from_bytes("{\"errors\":null}".into());
530 assert_eq!(
531 response.expect_err("no data and no errors"),
532 MalformedResponseError {
533 reason: "graphql response without data must contain at least one error".to_string(),
534 }
535 );
536 }
537
538 #[test]
539 fn test_data_null() {
540 let response = Response::from_bytes("{\"data\":null}".into()).unwrap();
541 assert_eq!(
542 response,
543 Response::builder().data(Some(Value::Null)).build(),
544 );
545 }
546
547 mod unicode {
554 use rstest::rstest;
555
556 use super::*;
557
558 #[rstest]
560 #[case::raw_utf8("{ \"data\": { \"greeting\": \"hello ๐ฐ๐\" } }", bjson!({ "greeting": "hello ๐ฐ๐" }))]
562 #[case::surrogate_pairs(r#"{"data":{"greeting":"hello \uD83D\uDCB0\uD83D\uDC95"}}"#, bjson!({ "greeting": "hello ๐ฐ๐" }))]
565 #[case::bmp_and_surrogate_pair(r#"{"data":{"greeting":"\u2764 \uD83D\uDE00"}}"#, bjson!({ "greeting": "โค ๐" }))]
567 fn valid_emoji(#[case] json: &str, #[case] expected: Value) {
568 let resp = Response::from_bytes(Bytes::copy_from_slice(json.as_bytes())).unwrap();
569 assert_eq!(resp.data, Some(expected));
570 }
571
572 #[rstest]
574 #[case::lone_surrogate_space(r#"{"data":{"greeting":"hello \uD83D end"}}"#)]
576 #[case::lone_surrogate_non_u_escape(r#"{"data":{"greeting":"hello \uD83D\n end"}}"#)]
578 fn lone_surrogate_rejected(#[case] json: &str) {
579 let err = Response::from_bytes(Bytes::copy_from_slice(json.as_bytes())).unwrap_err();
580 assert!(
581 err.reason.contains("unexpected end of hex escape"),
582 "expected base serde_json error, got: {err}"
583 );
584 assert!(
585 err.reason.contains("unpaired Unicode surrogate"),
586 "expected surrogate hint in error, got: {err}"
587 );
588 }
589 }
590}