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") .next()
20 .unwrap(); 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}