1#[derive(Debug, Clone)]
3pub struct GraphQlResponse<T, ErrorExtensions = serde::de::IgnoredAny> {
4 pub data: Option<T>,
6
7 pub errors: Option<Vec<GraphQlError<ErrorExtensions>>>,
9}
10
11#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, thiserror::Error)]
13#[error("{message}")]
14pub struct GraphQlError<Extensions = serde::de::IgnoredAny> {
15 pub message: String,
17 pub locations: Option<Vec<GraphQlErrorLocation>>,
19 pub path: Option<Vec<GraphQlErrorPathSegment>>,
21 pub extensions: Option<Extensions>,
23}
24
25impl<ErrorExtensions> GraphQlError<ErrorExtensions> {
26 pub fn new(
28 message: String,
29 locations: Option<Vec<GraphQlErrorLocation>>,
30 path: Option<Vec<GraphQlErrorPathSegment>>,
31 extensions: Option<ErrorExtensions>,
32 ) -> Self {
33 GraphQlError {
34 message,
35 locations,
36 path,
37 extensions,
38 }
39 }
40}
41
42#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize)]
44pub struct GraphQlErrorLocation {
45 pub line: i32,
47 pub column: i32,
49}
50
51#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize)]
53#[serde(untagged)]
54pub enum GraphQlErrorPathSegment {
55 Field(String),
57 Index(i32),
59}
60
61impl<'de, T, ErrorExtensions> serde::Deserialize<'de> for GraphQlResponse<T, ErrorExtensions>
62where
63 T: serde::Deserialize<'de>,
64 ErrorExtensions: serde::Deserialize<'de>,
65{
66 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
67 where
68 D: serde::Deserializer<'de>,
69 {
70 use serde::de::Error;
71
72 #[derive(serde::Deserialize)]
73 struct ResponseDeser<T, ErrorExtensions> {
74 data: Option<T>,
76
77 errors: Option<Vec<GraphQlError<ErrorExtensions>>>,
79 }
80
81 let ResponseDeser { data, errors } = ResponseDeser::deserialize(deserializer)?;
82
83 if data.is_none() && errors.is_none() {
84 return Err(D::Error::custom(
85 "Either data or errors must be present in a GraphQL response",
86 ));
87 }
88
89 Ok(GraphQlResponse { data, errors })
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use serde_json::json;
96
97 use super::*;
98
99 #[test]
100 fn test_default_graphql_response_ignores_extensions() {
101 let response = json!({
102 "data": null,
103 "errors": [{
104 "message": "hello",
105 "locations": null,
106 "path": null,
107 "extensions": {"some": "string"}
108 }]
109 });
110 insta::assert_debug_snapshot!(serde_json::from_value::<GraphQlResponse<()>>(response).unwrap(), @r###"
111 GraphQlResponse {
112 data: None,
113 errors: Some(
114 [
115 GraphQlError {
116 message: "hello",
117 locations: None,
118 path: None,
119 extensions: Some(
120 IgnoredAny,
121 ),
122 },
123 ],
124 ),
125 }
126 "###);
127 }
128
129 #[test]
130 fn test_graphql_response_fails_on_completely_invalid_response() {
131 let response = json!({
132 "message": "This endpoint requires you to be authenticated.",
133 });
134 serde_json::from_value::<GraphQlResponse<()>>(response).unwrap_err();
135 }
136}