fhttp_core/parsers/
parsing_gql.rs

1use std::str::FromStr;
2
3use anyhow::{anyhow, Context, Result};
4use pest::iterators::Pair;
5use pest::Parser;
6use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
7use reqwest::Method;
8use serde_json::map::Map;
9use serde_json::Value;
10
11use crate::parsers::gql_parser::{RequestParser, Rule};
12use crate::parsers::{fileupload_regex, Request};
13use crate::postprocessing::response_handler::ResponseHandler;
14use crate::request::body::Body;
15
16pub fn parse_gql_str<T: AsRef<str>>(source: T) -> Result<Request> {
17    let file = RequestParser::parse(Rule::file, source.as_ref())
18        .expect("unsuccessful parse") // unwrap the parse result
19        .next()
20        .unwrap(); // get and unwrap the `file` rule; never fails
21
22    let mut method = Method::GET;
23    let mut url = String::new();
24    let mut headers = HeaderMap::new();
25    let mut query = String::new();
26    let mut response_handler: Option<ResponseHandler> = None;
27    let mut variables: Option<String> = None;
28
29    for element in file.into_inner() {
30        match element.as_rule() {
31            Rule::first_line => parse_first_line(element, &mut method, &mut url)?,
32            Rule::header_line => parse_header_line(&mut headers, element)?,
33            Rule::query => query.push_str(element.as_str().trim()),
34            Rule::variables => variables = Some(element.as_str().trim().to_owned()),
35            Rule::response_handler_json => {
36                parse_json_response_handler(&mut response_handler, element)
37            }
38            Rule::response_handler_deno => {
39                parse_deno_response_handler(&mut response_handler, element)
40            }
41            _ => (),
42        }
43    }
44
45    let variables = match variables {
46        None => Value::Object(Map::new()),
47        Some(ref variables) => serde_json::from_str(variables)
48            .context("Error parsing variables section, seems to be invalid JSON?")?,
49    };
50
51    disallow_file_uploads(&query)?;
52
53    let mut map = Map::new();
54    map.insert("query".into(), Value::String(query));
55    map.insert("variables".into(), variables);
56
57    let body = Value::Object(map);
58    let body = serde_json::to_string(&body).unwrap();
59    let body = Body::Plain(body);
60
61    Ok(Request {
62        method,
63        url,
64        headers: ensure_content_type_json(headers),
65        body,
66        response_handler,
67    })
68}
69
70fn parse_first_line(element: Pair<Rule>, method: &mut Method, url: &mut String) -> Result<()> {
71    for field in element.into_inner() {
72        match field.as_rule() {
73            Rule::method => {
74                *method = Method::from_str(field.as_str())
75                    .with_context(|| format!("invalid method '{}'", field.as_str()))?
76            }
77            Rule::url => url.push_str(field.as_str()),
78            _ => unreachable!(),
79        }
80    }
81
82    Ok(())
83}
84
85fn parse_header_line(headers: &mut HeaderMap, element: Pair<Rule>) -> Result<()> {
86    let mut name = String::new();
87    let mut value = String::new();
88
89    for part in element.into_inner() {
90        match part.as_rule() {
91            Rule::header_name => name.push_str(part.as_str()),
92            Rule::header_value => value.push_str(part.as_str()),
93            _ => unreachable!(),
94        }
95    }
96
97    headers.insert(
98        HeaderName::from_str(&name).with_context(|| format!("invalid header name: '{}'", &name))?,
99        HeaderValue::from_str(&value)
100            .with_context(|| format!("invalid header value: '{}'", &value))?,
101    );
102
103    Ok(())
104}
105
106fn parse_json_response_handler(
107    response_handler: &mut Option<ResponseHandler>,
108    element: Pair<Rule>,
109) {
110    element.into_inner().for_each(|exp| match exp.as_rule() {
111        Rule::response_handler_exp => {
112            *response_handler = Some(ResponseHandler::Json {
113                json_path: exp.as_str().trim().to_owned(),
114            });
115        }
116        _ => unreachable!(),
117    });
118}
119
120fn parse_deno_response_handler(
121    response_handler: &mut Option<ResponseHandler>,
122    element: Pair<Rule>,
123) {
124    element.into_inner().for_each(|exp| match exp.as_rule() {
125        Rule::response_handler_exp => {
126            *response_handler = Some(ResponseHandler::Deno {
127                program: exp.as_str().trim().to_owned(),
128            });
129        }
130        _ => unreachable!(),
131    });
132}
133
134fn ensure_content_type_json(mut map: HeaderMap) -> HeaderMap {
135    map.entry("content-type")
136        .or_insert(HeaderValue::from_static("application/json"));
137
138    map
139}
140
141fn disallow_file_uploads(body: &str) -> Result<()> {
142    let captures = fileupload_regex().captures_iter(body);
143
144    match captures.count() {
145        0 => Ok(()),
146        _ => Err(anyhow!("file uploads are not allowed in graphql requests")),
147    }
148}
149
150#[cfg(test)]
151mod parse_gql_requests {
152    use indoc::indoc;
153    use serde_json::json;
154
155    use super::*;
156
157    #[test]
158    fn should_parse_headers_and_query() -> Result<()> {
159        let result = parse_gql_str(indoc!(
160            r##"
161            GET http://localhost:9000/foo
162            content-type: application/json; charset=UTF-8
163            accept: application/xml
164            com.header.name: com.header.value
165
166            query
167        "##
168        ))?;
169
170        assert_eq!(
171            result,
172            Request::basic("GET", "http://localhost:9000/foo")
173                .add_header("content-type", "application/json; charset=UTF-8")
174                .add_header("accept", "application/xml")
175                .add_header("com.header.name", "com.header.value")
176                .gql_body(json!({
177                    "query": "query",
178                    "variables": {}
179                }))
180        );
181
182        Ok(())
183    }
184
185    #[test]
186    fn should_allow_overriding_content_type() -> Result<()> {
187        let result = parse_gql_str(indoc!(
188            r##"
189            GET http://localhost:9000/foo
190            content-type: application/xml
191
192            query
193        "##
194        ))?;
195
196        assert_eq!(
197            result,
198            Request::basic("GET", "http://localhost:9000/foo")
199                .add_header("content-type", "application/xml")
200                .gql_body(json!({
201                    "query": "query",
202                    "variables": {}
203                }))
204        );
205
206        Ok(())
207    }
208
209    #[test]
210    fn should_parse_query_and_response_handler() -> Result<()> {
211        let result = parse_gql_str(indoc!(
212            r##"
213            DELETE http://localhost:9000/foo
214
215            query
216            query
217
218            > {%
219                json $.data
220            %}
221        "##
222        ))?;
223
224        assert_eq!(
225            result,
226            Request::basic("DELETE", "http://localhost:9000/foo")
227                .add_header("content-type", "application/json")
228                .gql_body(json!({
229                    "query": "query\nquery",
230                    "variables": {}
231                }))
232                .response_handler_json("$.data")
233        );
234
235        Ok(())
236    }
237
238    #[test]
239    fn should_parse_with_deno_response_handler() -> Result<()> {
240        let result = parse_gql_str(indoc!(
241            r##"
242            DELETE http://localhost:9000/foo
243
244            query
245            query
246
247            > {%
248                deno
249                if (status === 200) {
250                    setResult('ok');
251                } else {
252                    setResult('not ok');
253                }
254            %}
255        "##
256        ))?;
257
258        assert_eq!(
259            result,
260            Request::basic("DELETE", "http://localhost:9000/foo")
261                .add_header("content-type", "application/json")
262                .gql_body(json!({
263                    "query": "query\nquery",
264                    "variables": {}
265                }))
266                .response_handler_deno(
267                    r#"if (status === 200) {
268        setResult('ok');
269    } else {
270        setResult('not ok');
271    }"#
272                )
273        );
274
275        Ok(())
276    }
277
278    #[test]
279    fn should_parse_query_and_variables() -> Result<()> {
280        let result = parse_gql_str(indoc!(
281            r##"
282            GET http://localhost:9000/foo
283
284            query
285
286            {
287                "foo": "bar"
288            }
289        "##
290        ))?;
291
292        assert_eq!(
293            result,
294            Request::basic("GET", "http://localhost:9000/foo")
295                .add_header("content-type", "application/json")
296                .gql_body(json!({
297                    "query": "query",
298                    "variables": {
299                        "foo": "bar"
300                    }
301                }))
302        );
303
304        Ok(())
305    }
306
307    #[test]
308    fn should_parse_query_variables_and_response_handler() -> Result<()> {
309        let result = parse_gql_str(indoc!(
310            r##"
311            DELETE http://localhost:9000/foo
312
313            query
314            query
315
316            { "foo": "bar" }
317
318            > {%
319                json $.data
320            %}
321        "##
322        ))?;
323
324        assert_eq!(
325            result,
326            Request::basic("DELETE", "http://localhost:9000/foo")
327                .add_header("content-type", "application/json")
328                .body("query\nquery")
329                .response_handler_json("$.data")
330                .gql_body(json!({
331                    "query": "query\nquery",
332                    "variables": {
333                        "foo": "bar"
334                    }
335                }))
336        );
337
338        Ok(())
339    }
340
341    #[test]
342    fn should_tolerate_more_space_between_headers_and_query() -> Result<()> {
343        let result = parse_gql_str(indoc!(
344            r##"
345            DELETE http://localhost:9000/foo
346            foo: bar
347
348
349
350            query
351        "##
352        ))?;
353
354        assert_eq!(
355            result,
356            Request::basic("DELETE", "http://localhost:9000/foo")
357                .add_header("content-type", "application/json")
358                .add_header("foo", "bar")
359                .gql_body(json!({
360                    "query": "query",
361                    "variables": {}
362                }))
363        );
364
365        Ok(())
366    }
367
368    #[test]
369    fn should_tolerate_more_space_between_query_and_response_handler() -> Result<()> {
370        let result = parse_gql_str(indoc!(
371            r##"
372            DELETE http://localhost:9000/foo
373
374            query
375
376
377
378            > {% json foo %}
379        "##
380        ))?;
381
382        assert_eq!(
383            result,
384            Request::basic("DELETE", "http://localhost:9000/foo")
385                .add_header("content-type", "application/json")
386                .gql_body(json!({
387                    "query": "query",
388                    "variables": {}
389                }))
390                .response_handler_json("foo")
391        );
392
393        Ok(())
394    }
395
396    #[test]
397    fn should_tolerate_trailing_newlines_with_query() -> Result<()> {
398        let result = parse_gql_str(indoc!(
399            r##"
400            GET http://localhost:9000/foo
401            content-type: application/json; charset=UTF-8
402            accept: application/xml
403
404            query
405
406
407        "##
408        ))?;
409
410        assert_eq!(
411            result,
412            Request::basic("GET", "http://localhost:9000/foo")
413                .add_header("content-type", "application/json; charset=UTF-8")
414                .add_header("accept", "application/xml")
415                .gql_body(json!({
416                    "query": "query",
417                    "variables": {}
418                }))
419        );
420
421        Ok(())
422    }
423
424    #[test]
425    fn should_tolerate_trailing_newlines_with_query_and_response_handler() -> Result<()> {
426        let result = parse_gql_str(indoc!(
427            r##"
428            GET http://localhost:9000/foo
429            content-type: application/json; charset=UTF-8
430            accept: application/xml
431
432            query
433
434            > {% json handler %}
435
436
437
438        "##
439        ))?;
440
441        assert_eq!(
442            result,
443            Request::basic("GET", "http://localhost:9000/foo")
444                .add_header("content-type", "application/json; charset=UTF-8")
445                .add_header("accept", "application/xml")
446                .gql_body(json!({
447                    "query": "query",
448                    "variables": {}
449                }))
450                .response_handler_json("handler")
451        );
452
453        Ok(())
454    }
455
456    #[test]
457    fn should_allow_commenting_out_headers() -> Result<()> {
458        let result = parse_gql_str(indoc!(
459            r##"
460            GET http://localhost:9000/foo
461            # accept: application/xml
462
463            query
464        "##
465        ))?;
466
467        assert_eq!(
468            result,
469            Request::basic("GET", "http://localhost:9000/foo")
470                .add_header("content-type", "application/json")
471                .gql_body(json!({
472                    "query": "query",
473                    "variables": {}
474                }))
475        );
476
477        Ok(())
478    }
479
480    #[test]
481    fn should_not_allow_using_file_uploads_in_gql_files() -> Result<()> {
482        let result = parse_gql_str(indoc!(
483            r##"
484            GET http://localhost:9000/foo
485
486            ${file("partname", "../resources/it/profiles.json")}
487            ${file("file", "../resources/it/profiles2.json")}
488        "##
489        ));
490
491        assert_err!(result, "file uploads are not allowed in graphql requests");
492
493        Ok(())
494    }
495}