gqlrequest/
lib.rs

1use eyre::Result;
2use serde::{Deserialize, Serialize};
3use serde_json::value::Value;
4use std::collections::HashMap;
5
6/// Request for GraphQL to create JSON requets structure
7///
8/// ```json
9/// {
10///     "operationName": "createBook",
11///     "variables": {
12///         "book": {
13///             "title": "Rocket Engineering",
14///         }
15///     },
16///     "query": "mutation createBook($book: createBook!) {\n  createBook(book: $book) {\n    title\n }\n}\n"
17/// }
18/// ```
19#[derive(Debug, Clone, Serialize)]
20#[serde(rename_all = "camelCase")]
21pub struct GqlRequest {
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub operation_name: Option<String>,
24    #[serde(skip_serializing_if = "HashMap::is_empty")]
25    pub variables: HashMap<String, Value>,
26    pub query: String,
27}
28
29impl GqlRequest {
30    /// Cretas new request with only one query
31    pub fn new(query: &str) -> Self {
32        GqlRequest {
33            operation_name: None,
34            variables: HashMap::new(),
35            query: query.to_string(),
36        }
37    }
38
39    /// Crete new request for GraphQL with anonymous query/mutation
40    /// ```json, no_run
41    /// {
42    ///     query: "info()"
43    ///     variables: "book": { "title": "Rocket Engineering" }
44    /// }
45    pub fn new_with_variable<T: Serialize>(query: &str, variable: &str, object: &T) -> Self {
46        GqlRequest {
47            operation_name: None,
48            variables: [(variable.to_string(), serde_json::json!(object))]
49                .iter()
50                .cloned()
51                .collect(),
52            query: query.to_string(),
53        }
54    }
55
56    /// Create new request with opetaion name
57    /// ```json, no_run
58    /// {
59    ///     query: ""
60    /// }
61    pub fn new_with_op(operation_name: &str, query: &str) -> Self {
62        GqlRequest {
63            operation_name: Some(operation_name.to_string()),
64            variables: HashMap::new(),
65            query: query.to_string(),
66        }
67    }
68    pub fn add_variable<T: Serialize>(&mut self, name: &str, object: &T) -> Result<()> {
69        if self.operation_name.is_none() && !self.variables.is_empty() {
70            Err(eyre::eyre!(
71                "Not possible to add variable when using anonymous query/mutation"
72            ))
73        } else {
74            let json = serde_json::json!(object);
75            self.variables.insert(name.to_string(), json);
76            Ok(())
77        }
78    }
79}
80
81#[derive(Debug, Deserialize)]
82pub struct GqlResponse<T> {
83    pub data: Option<T>,
84    pub errors: Option<Vec<ErrorMsg>>,
85}
86
87#[derive(Debug, Deserialize)]
88pub struct ErrorMsg {
89    pub message: String,
90    pub locations: Vec<Location>,
91    pub path: Option<Vec<Value>>,
92    pub extensions: Option<Value>,
93}
94
95#[derive(Debug, Deserialize)]
96pub struct Location {
97    pub line: i32,
98    pub column: i32,
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn variable_add_test() {
107        #[derive(Serialize)]
108        struct TestQuery {
109            pub title: String,
110        }
111        let test = TestQuery {
112            title: "Rocket Engineering".to_string(),
113        };
114
115        let mut request = GqlRequest::new_with_variable("", "test", &test);
116        assert!(request.add_variable("test", &test).is_err())
117    }
118
119    #[test]
120    fn empty_variables_test() {
121        let query = "{ apiVersion }";
122        let expected_body = serde_json::json!({
123            "query": query,
124        });
125
126        let request = GqlRequest::new("{ apiVersion }");
127        let request = serde_json::json!(&request);
128        assert_eq!(request, expected_body);
129    }
130
131    #[test]
132    fn request_test() {
133        #[derive(Serialize)]
134        struct TestQuery {
135            pub title: String,
136        }
137        let exp_data = r#"
138        {
139            "operationName": "createBook",
140            "variables": {
141                "book": {
142                    "title": "Rocket Engineering"
143                }
144            },
145            "query": "mutation createBook($book: createBook!) { createBook(book: $book) { title }}"
146        }
147        "#;
148
149        let test_query = TestQuery {
150            title: "Rocket Engineering".to_string(),
151        };
152        let op_name = "createBook";
153        let query = "mutation createBook($book: createBook!) { createBook(book: $book) { title }}";
154
155        let mut gql_request = GqlRequest::new_with_op(op_name, query);
156        gql_request.add_variable("book", &test_query).unwrap();
157
158        let request = serde_json::json!(gql_request);
159        let expected: serde_json::Value = serde_json::from_str(exp_data).unwrap();
160
161        assert_eq!(request["operationName"], expected["operationName"]);
162        assert_eq!(request, expected);
163    }
164
165    #[test]
166    fn request_anonymous_test() {
167        #[derive(Serialize)]
168        struct TestQuery {
169            pub title: String,
170        }
171        let exp_data = r#"
172        {
173            "variables": {
174                "book": {
175                    "title": "Rocket Engineering"
176                }
177            },
178            "query": "mutation ($book: createBook!) { createBook(book: $book) { title }}"
179        }
180        "#;
181
182        let test_query = TestQuery {
183            title: "Rocket Engineering".to_string(),
184        };
185        let query = "mutation ($book: createBook!) { createBook(book: $book) { title }}";
186        let gql_request = GqlRequest::new_with_variable(query, "book", &test_query);
187
188        let request = serde_json::json!(gql_request);
189        let expected: serde_json::Value = serde_json::from_str(exp_data).unwrap();
190
191        assert_eq!(request, expected);
192    }
193
194    #[test]
195    fn response_test() {
196        let expected = r#"{"data":{"sensor":{"createdAt":"2020-09-15T07:08:54.668686+00:00","id":"59de6057-e913-45e3-95b1-e628741443fd","location":null,"macaddress":"DC:A6:32:0B:62:37","name":"unnamed-59de6057-e913-45e3-95b1-e628741443fd","updatedAt":"2020-09-15T07:08:54.668686+00:00"}}}"#;
197
198        #[derive(Debug, Deserialize)]
199        #[serde(rename_all = "camelCase")]
200        pub struct Sensor {
201            pub name: String,
202            pub location: Option<String>,
203            pub macaddress: String,
204            pub created_at: String,
205            pub updated_at: String,
206        }
207
208        #[derive(Debug, Deserialize)]
209        #[serde(rename_all = "camelCase")]
210        pub struct SensorData {
211            pub sensor: Sensor,
212        }
213
214        let response: GqlResponse<SensorData> = serde_json::from_str(expected).unwrap();
215
216        let data = response.data.unwrap();
217
218        assert_eq!(
219            data.sensor.name,
220            "unnamed-59de6057-e913-45e3-95b1-e628741443fd"
221        );
222    }
223
224    /// Error taken from: https://lucasconstantino.github.io/graphiql-online/
225    #[test]
226    fn error_response_ext_test() {
227        let expected = r#"{ "errors": [ { "message": "Cannot query field \"named\" on type \"Country\". Did you mean \"name\"?", "locations": [ { "line": 34, "column": 5 } ], "extensions": { "code": "GRAPHQL_VALIDATION_FAILED" } } ] }"#;
228
229        #[derive(Debug, Deserialize)]
230        #[serde(rename_all = "camelCase")]
231        struct Country {
232            #[allow(dead_code)]
233            name: String,
234        }
235
236        let response: GqlResponse<Country> = serde_json::from_str(expected).unwrap();
237
238        assert!(response.data.is_none());
239        assert!(response.errors.is_some());
240
241        let errors = response.errors.unwrap();
242
243        assert_eq!(errors.len(), 1);
244
245        let error = errors.first().unwrap();
246        assert_eq!(
247            error.message,
248            r#"Cannot query field "named" on type "Country". Did you mean "name"?"#
249        );
250        assert_eq!(error.locations.len(), 1);
251        let location = error.locations.first().unwrap();
252        assert_eq!(location.line, 34);
253        assert_eq!(location.column, 5);
254    }
255
256    /// Error taken from: https://lucasconstantino.github.io/graphiql-online/
257    #[test]
258    fn error_response_path_test() {
259        let expected = r#"{ "data": null, "errors": [ { "message": "Failed to parse \"UUID\": invalid length: expected one of [36, 32], found 7", "locations": [ { "line": 2, "column": 14 } ], "path": [ "sensor" ] } ] }"#;
260
261        #[derive(Debug, Deserialize)]
262        #[serde(rename_all = "camelCase")]
263        struct Country {
264            #[allow(dead_code)]
265            name: String,
266        }
267
268        let response: GqlResponse<Country> = serde_json::from_str(expected).unwrap();
269
270        assert!(response.data.is_none());
271        assert!(response.errors.is_some());
272
273        let errors = response.errors.unwrap();
274
275        assert_eq!(errors.len(), 1);
276
277        let error = errors.first().unwrap();
278        assert_eq!(
279            error.message,
280            r#"Failed to parse "UUID": invalid length: expected one of [36, 32], found 7"#
281        );
282        assert_eq!(error.locations.len(), 1);
283        let location = error.locations.first().unwrap();
284        assert_eq!(location.line, 2);
285        assert_eq!(location.column, 14);
286
287        assert!(error.path.is_some());
288    }
289}