cynic/
result.rs

1/// The response to a GraphQl operation
2#[derive(Debug, Clone)]
3pub struct GraphQlResponse<T, ErrorExtensions = serde::de::IgnoredAny> {
4    /// The operation data (if the operation was successful)
5    pub data: Option<T>,
6
7    /// Any errors that occurred as part of this operation
8    pub errors: Option<Vec<GraphQlError<ErrorExtensions>>>,
9}
10
11/// A model describing an error which has taken place during execution.
12#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize, thiserror::Error)]
13#[error("{message}")]
14pub struct GraphQlError<Extensions = serde::de::IgnoredAny> {
15    /// A description of the error which has taken place.
16    pub message: String,
17    /// Optional description of the locations where the errors have taken place.
18    pub locations: Option<Vec<GraphQlErrorLocation>>,
19    /// Optional path to the response field which experienced the associated error.
20    pub path: Option<Vec<GraphQlErrorPathSegment>>,
21    /// Optional arbitrary extra data describing the error in more detail.
22    pub extensions: Option<Extensions>,
23}
24
25impl<ErrorExtensions> GraphQlError<ErrorExtensions> {
26    /// Construct a new instance.
27    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/// A line and column offset describing the location of an error within a GraphQL document.
43#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize)]
44pub struct GraphQlErrorLocation {
45    /// The line at which the associated error begins.
46    pub line: i32,
47    /// The column of the line at which the associated error begins.
48    pub column: i32,
49}
50
51/// A segment of a GraphQL error path.
52#[derive(Clone, Debug, Eq, PartialEq, serde::Deserialize)]
53#[serde(untagged)]
54pub enum GraphQlErrorPathSegment {
55    /// A path segment representing a field by name.
56    Field(String),
57    /// A path segment representing an index offset, zero-based.
58    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            /// The operation data (if the operation was successful)
75            data: Option<T>,
76
77            /// Any errors that occurred as part of this operation
78            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}