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
272#[cfg(test)]
273impl Response {
274 pub(crate) fn errors_with_code<'a>(&'a self, code: &'a str) -> impl Iterator<Item = &'a Error> {
275 self.errors
276 .iter()
277 .filter(move |err| err.extension_code().is_some_and(|c| c == code))
278 }
279
280 pub(crate) fn contains_error_code(&self, code: &str) -> bool {
281 self.errors_with_code(code).next().is_some()
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use serde_json::json;
288 use serde_json_bytes::json as bjson;
289 use uuid::Uuid;
290
291 use super::*;
292 use crate::assert_response_eq_ignoring_error_id;
293 use crate::graphql;
294 use crate::graphql::Error;
295 use crate::graphql::Location;
296 use crate::graphql::Response;
297
298 #[test]
299 fn test_append_errors_path_fallback_and_override() {
300 let uuid1 = Uuid::new_v4();
301 let uuid2 = Uuid::new_v4();
302 let expected_errors = vec![
303 Error::builder()
304 .message("Something terrible happened!")
305 .path(Path::from("here"))
306 .apollo_id(uuid1)
307 .build(),
308 Error::builder()
309 .message("I mean for real")
310 .apollo_id(uuid2)
311 .build(),
312 ];
313
314 let mut errors_to_append = 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 response = Response::builder().build();
327 response.append_errors(&mut errors_to_append);
328 assert_eq!(response.errors, expected_errors);
329 }
330
331 #[test]
332 fn test_response() {
333 let result = serde_json::from_str::<Response>(
334 json!(
335 {
336 "errors": [
337 {
338 "message": "Name for character with ID 1002 could not be fetched.",
339 "locations": [{ "line": 6, "column": 7 }],
340 "path": ["hero", "heroFriends", 1, "name"],
341 "extensions": {
342 "error-extension": 5,
343 }
344 }
345 ],
346 "data": {
347 "hero": {
348 "name": "R2-D2",
349 "heroFriends": [
350 {
351 "id": "1000",
352 "name": "Luke Skywalker"
353 },
354 {
355 "id": "1002",
356 "name": null
357 },
358 {
359 "id": "1003",
360 "name": "Leia Organa"
361 }
362 ]
363 }
364 },
365 "extensions": {
366 "response-extension": 3,
367 }
368 })
369 .to_string()
370 .as_str(),
371 );
372 let response = result.unwrap();
373 assert_response_eq_ignoring_error_id!(
374 response,
375 Response::builder()
376 .data(json!({
377 "hero": {
378 "name": "R2-D2",
379 "heroFriends": [
380 {
381 "id": "1000",
382 "name": "Luke Skywalker"
383 },
384 {
385 "id": "1002",
386 "name": null
387 },
388 {
389 "id": "1003",
390 "name": "Leia Organa"
391 }
392 ]
393 }
394 }))
395 .errors(vec![
396 Error::builder()
397 .message("Name for character with ID 1002 could not be fetched.")
398 .locations(vec!(Location { line: 6, column: 7 }))
399 .path(Path::from("hero/heroFriends/1/name"))
400 .extensions(
401 bjson!({ "error-extension": 5, })
402 .as_object()
403 .cloned()
404 .unwrap()
405 )
406 .build()
407 ])
408 .extensions(
409 bjson!({
410 "response-extension": 3,
411 })
412 .as_object()
413 .cloned()
414 .unwrap()
415 )
416 .build()
417 );
418 }
419
420 #[test]
421 fn test_patch_response() {
422 let result = serde_json::from_str::<Response>(
423 json!(
424 {
425 "label": "part",
426 "hasNext": true,
427 "path": ["hero", "heroFriends", 1, "name"],
428 "errors": [
429 {
430 "message": "Name for character with ID 1002 could not be fetched.",
431 "locations": [{ "line": 6, "column": 7 }],
432 "path": ["hero", "heroFriends", 1, "name"],
433 "extensions": {
434 "error-extension": 5,
435 }
436 }
437 ],
438 "data": {
439 "hero": {
440 "name": "R2-D2",
441 "heroFriends": [
442 {
443 "id": "1000",
444 "name": "Luke Skywalker"
445 },
446 {
447 "id": "1002",
448 "name": null
449 },
450 {
451 "id": "1003",
452 "name": "Leia Organa"
453 }
454 ]
455 }
456 },
457 "extensions": {
458 "response-extension": 3,
459 }
460 })
461 .to_string()
462 .as_str(),
463 );
464 let response = result.unwrap();
465 assert_response_eq_ignoring_error_id!(
466 response,
467 Response::builder()
468 .label("part".to_owned())
469 .data(json!({
470 "hero": {
471 "name": "R2-D2",
472 "heroFriends": [
473 {
474 "id": "1000",
475 "name": "Luke Skywalker"
476 },
477 {
478 "id": "1002",
479 "name": null
480 },
481 {
482 "id": "1003",
483 "name": "Leia Organa"
484 }
485 ]
486 }
487 }))
488 .path(Path::from("hero/heroFriends/1/name"))
489 .errors(vec![
490 Error::builder()
491 .message("Name for character with ID 1002 could not be fetched.")
492 .locations(vec!(Location { line: 6, column: 7 }))
493 .path(Path::from("hero/heroFriends/1/name"))
494 .extensions(
495 bjson!({ "error-extension": 5, })
496 .as_object()
497 .cloned()
498 .unwrap()
499 )
500 .build()
501 ])
502 .extensions(
503 bjson!({
504 "response-extension": 3,
505 })
506 .as_object()
507 .cloned()
508 .unwrap()
509 )
510 .has_next(true)
511 .build()
512 );
513 }
514
515 #[test]
516 fn test_no_data_and_no_errors() {
517 let response = Response::from_bytes("{\"errors\":null}".into());
518 assert_eq!(
519 response.expect_err("no data and no errors"),
520 MalformedResponseError {
521 reason: "graphql response without data must contain at least one error".to_string(),
522 }
523 );
524 }
525
526 #[test]
527 fn test_data_null() {
528 let response = Response::from_bytes("{\"data\":null}".into()).unwrap();
529 assert_eq!(
530 response,
531 Response::builder().data(Some(Value::Null)).build(),
532 );
533 }
534
535 mod unicode {
542 use rstest::rstest;
543
544 use super::*;
545
546 #[rstest]
548 #[case::raw_utf8("{ \"data\": { \"greeting\": \"hello ๐ฐ๐\" } }", bjson!({ "greeting": "hello ๐ฐ๐" }))]
550 #[case::surrogate_pairs(r#"{"data":{"greeting":"hello \uD83D\uDCB0\uD83D\uDC95"}}"#, bjson!({ "greeting": "hello ๐ฐ๐" }))]
553 #[case::bmp_and_surrogate_pair(r#"{"data":{"greeting":"\u2764 \uD83D\uDE00"}}"#, bjson!({ "greeting": "โค ๐" }))]
555 fn valid_emoji(#[case] json: &str, #[case] expected: Value) {
556 let resp = Response::from_bytes(Bytes::copy_from_slice(json.as_bytes())).unwrap();
557 assert_eq!(resp.data, Some(expected));
558 }
559
560 #[rstest]
562 #[case::lone_surrogate_space(r#"{"data":{"greeting":"hello \uD83D end"}}"#)]
564 #[case::lone_surrogate_non_u_escape(r#"{"data":{"greeting":"hello \uD83D\n end"}}"#)]
566 fn lone_surrogate_rejected(#[case] json: &str) {
567 let err = Response::from_bytes(Bytes::copy_from_slice(json.as_bytes())).unwrap_err();
568 assert!(
569 err.reason.contains("unexpected end of hex escape"),
570 "expected base serde_json error, got: {err}"
571 );
572 assert!(
573 err.reason.contains("unpaired Unicode surrogate"),
574 "expected surrogate hint in error, got: {err}"
575 );
576 }
577 }
578}