1use std::collections::HashMap;
2use std::str::FromStr;
3
4use anyhow::Result;
5use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
6use reqwest::Method;
7use serde::Deserialize;
8
9use crate::path_utils::{CanonicalizedPathBuf, RelativePath};
10use crate::request::body::{Body, MultipartPart};
11use crate::request::Request;
12use crate::ResponseHandler;
13
14#[derive(Debug, Deserialize)]
15struct StructuredRequestSource {
16 method: String,
17 url: String,
18 headers: Option<HashMap<String, String>>,
19 response_handler: Option<StructuredResponseHandler>,
20 body: Option<StructuredBody>,
21}
22
23#[derive(Debug, Deserialize)]
24#[serde(untagged)]
25enum StructuredBody {
26 Plain(String),
27 Mutlipart(Vec<StructuredMultipartPart>),
28}
29
30#[derive(Debug, Deserialize)]
31#[serde(untagged)]
32enum StructuredMultipartPart {
33 Text {
34 name: String,
35 text: String,
36 mime: Option<String>,
37 },
38 File {
39 name: String,
40 filepath: String,
41 mime: Option<String>,
42 },
43}
44
45impl StructuredMultipartPart {
46 fn into_part(self, reference_location: &CanonicalizedPathBuf) -> Result<MultipartPart> {
47 Ok(match self {
48 StructuredMultipartPart::Text {
49 name,
50 text,
51 mime: mime_str,
52 } => MultipartPart::Text {
53 name,
54 text,
55 mime_str,
56 },
57 StructuredMultipartPart::File {
58 name,
59 filepath: file_path,
60 mime: mime_str,
61 } => MultipartPart::File {
62 name,
63 file_path: reference_location.get_dependency_path(&file_path)?,
64 mime_str,
65 },
66 })
67 }
68}
69
70impl StructuredBody {
71 pub fn into_body(self, reference_location: &CanonicalizedPathBuf) -> Result<Body> {
72 match self {
73 StructuredBody::Plain(text) => Ok(Body::Plain(text.to_string())),
74 StructuredBody::Mutlipart(parts) => Ok(Body::Multipart(
75 parts
76 .into_iter()
77 .map(|it| it.into_part(reference_location))
78 .collect::<Result<Vec<MultipartPart>>>()?,
79 )),
80 }
81 }
82}
83
84#[derive(Debug, Deserialize)]
85struct StructuredResponseHandler {
86 pub json: Option<String>,
87 pub deno: Option<String>,
88}
89
90impl StructuredResponseHandler {
91 pub fn response_handler(self) -> Option<ResponseHandler> {
92 if let Some(json) = self.json {
93 Some(ResponseHandler::Json { json_path: json })
94 } else {
95 self.deno
96 .map(|code| ResponseHandler::Deno { program: code })
97 }
98 }
99}
100
101impl TryFrom<(&CanonicalizedPathBuf, StructuredRequestSource)> for Request {
102 type Error = anyhow::Error;
103
104 fn try_from(
105 arg: (&CanonicalizedPathBuf, StructuredRequestSource),
106 ) -> std::result::Result<Self, Self::Error> {
107 let reference_location = arg.0;
108 let value = arg.1;
109
110 let headers = match value.headers {
111 Some(headers) => {
112 let mut tmp = HeaderMap::new();
113 for (name, value) in headers {
114 tmp.append(HeaderName::from_str(&name)?, HeaderValue::from_str(&value)?);
115 }
116 Ok::<HeaderMap, anyhow::Error>(tmp)
117 }
118 None => Ok(HeaderMap::new()),
119 }?;
120
121 Ok(Request {
122 method: Method::from_str(&value.method)?,
123 url: value.url.to_string(),
124 headers,
125 body: value
126 .body
127 .map(|it| it.into_body(reference_location))
128 .unwrap_or(Ok(Body::Plain("".to_string())))?,
129 response_handler: value
130 .response_handler
131 .and_then(StructuredResponseHandler::response_handler),
132 })
133 }
134}
135
136pub fn parse_request_from_json(
137 reference_location: &CanonicalizedPathBuf,
138 text: &str,
139) -> Result<Request> {
140 let structured = serde_json::from_str(text)?;
141 Request::try_from((reference_location, structured))
142}
143
144pub fn parse_request_from_yaml(
145 reference_location: &CanonicalizedPathBuf,
146 text: &str,
147) -> Result<Request> {
148 let structured = serde_yaml::from_str(text)?;
149 Request::try_from((reference_location, structured))
150}
151
152#[cfg(test)]
153mod tests {
154 use indoc::indoc;
155 use reqwest::header::{HeaderMap, HeaderName, HeaderValue};
156
157 use crate::request::body::MultipartPart;
158 use crate::test_utils::root;
159 use crate::ResponseHandler;
160
161 use super::*;
162
163 #[test]
164 fn should_parse_minimal_json_request() -> Result<()> {
165 let result = parse_request_from_json(
166 &root(),
167 indoc! {r#"
168 {
169 "method": "POST",
170 "url": "http://localhost/foo"
171 }
172 "#},
173 )?;
174
175 assert_eq!(
176 result,
177 Request {
178 method: Method::POST,
179 url: "http://localhost/foo".to_string(),
180 headers: HeaderMap::new(),
181 body: Body::Plain("".to_string()),
182 response_handler: None
183 }
184 );
185
186 Ok(())
187 }
188
189 #[test]
190 fn should_parse_json_request_with_headers() -> Result<()> {
191 let result = parse_request_from_json(
192 &root(),
193 indoc! {r#"
194 {
195 "method": "POST",
196 "url": "http://localhost/foo",
197 "headers": {
198 "accept": "application/json"
199 }
200 }
201 "#},
202 )?;
203
204 let headers = {
205 let mut tmp = HeaderMap::new();
206 tmp.append(
207 HeaderName::from_str("accept").unwrap(),
208 HeaderValue::from_str("application/json").unwrap(),
209 );
210 tmp
211 };
212
213 assert_eq!(
214 result,
215 Request {
216 method: Method::POST,
217 url: "http://localhost/foo".to_string(),
218 headers,
219 body: Body::Plain("".to_string()),
220 response_handler: None
221 }
222 );
223
224 Ok(())
225 }
226
227 #[test]
228 fn should_parse_json_request_with_json_response_handler() -> Result<()> {
229 let result = parse_request_from_json(
230 &root(),
231 indoc! {r#"
232 {
233 "method": "POST",
234 "url": "http://localhost/foo",
235 "response_handler": {
236 "json": "$.data"
237 }
238 }
239 "#},
240 )?;
241
242 assert_eq!(
243 result,
244 Request {
245 method: Method::POST,
246 url: "http://localhost/foo".to_string(),
247 headers: HeaderMap::new(),
248 body: Body::Plain("".to_string()),
249 response_handler: Some(ResponseHandler::Json {
250 json_path: "$.data".to_string()
251 }),
252 }
253 );
254
255 Ok(())
256 }
257
258 #[test]
259 fn should_parse_json_request_with_deno_response_handler() -> Result<()> {
260 let result = parse_request_from_json(
261 &root(),
262 indoc! {r#"
263 {
264 "method": "POST",
265 "url": "http://localhost/foo",
266 "response_handler": {
267 "deno": "setResult('ok!');"
268 }
269 }
270 "#},
271 )?;
272
273 assert_eq!(
274 result,
275 Request {
276 method: Method::POST,
277 url: "http://localhost/foo".to_string(),
278 headers: HeaderMap::new(),
279 body: Body::Plain("".to_string()),
280 response_handler: Some(ResponseHandler::Deno {
281 program: "setResult('ok!');".to_string()
282 }),
283 }
284 );
285
286 Ok(())
287 }
288
289 #[test]
290 fn should_parse_json_request_with_plain_body() -> Result<()> {
291 let result = parse_request_from_json(
292 &root(),
293 indoc! {r#"
294 {
295 "method": "POST",
296 "url": "http://localhost/foo",
297 "body": "plain body"
298 }
299 "#},
300 )?;
301
302 assert_eq!(
303 result,
304 Request {
305 method: Method::POST,
306 url: "http://localhost/foo".to_string(),
307 headers: HeaderMap::new(),
308 body: Body::Plain("plain body".to_string()),
309 response_handler: None
310 }
311 );
312
313 Ok(())
314 }
315
316 #[test]
317 fn should_parse_json_request_with_multipart_body() -> Result<()> {
318 let result = parse_request_from_json(
319 &root(),
320 indoc! {r#"
321 {
322 "method": "POST",
323 "url": "http://localhost/foo",
324 "body": [
325 {
326 "name": "textpart1",
327 "text": "text for part 1"
328 },
329 {
330 "name": "textpart2",
331 "text": "text for part 2",
332 "mime": "text/plain"
333 },
334 {
335 "name": "filepart1",
336 "filepath": "resources/image.jpg"
337 },
338 {
339 "name": "filepart2",
340 "filepath": "resources/image.jpg",
341 "mime": "image/png"
342 }
343 ]
344 }
345 "#},
346 )?;
347
348 assert_eq!(
349 result,
350 Request {
351 method: Method::POST,
352 url: "http://localhost/foo".to_string(),
353 headers: HeaderMap::new(),
354 body: Body::Multipart(vec![
355 MultipartPart::Text {
356 name: "textpart1".to_string(),
357 text: "text for part 1".to_string(),
358 mime_str: None,
359 },
360 MultipartPart::Text {
361 name: "textpart2".to_string(),
362 text: "text for part 2".to_string(),
363 mime_str: Some("text/plain".to_string()),
364 },
365 MultipartPart::File {
366 name: "filepart1".to_string(),
367 file_path: root().join("resources/image.jpg"),
368 mime_str: None,
369 },
370 MultipartPart::File {
371 name: "filepart2".to_string(),
372 file_path: root().join("resources/image.jpg"),
373 mime_str: Some("image/png".to_string()),
374 },
375 ]),
376 response_handler: None
377 }
378 );
379
380 Ok(())
381 }
382
383 #[test]
384 fn should_parse_full_yaml_request() -> Result<()> {
385 let result = parse_request_from_yaml(
386 &root(),
387 indoc! {r#"
388 method: POST
389 url: http://localhost/foo
390 body: hello there
391 "#},
392 )?;
393
394 assert_eq!(
395 result,
396 Request {
397 method: Method::POST,
398 url: "http://localhost/foo".to_string(),
399 headers: HeaderMap::new(),
400 body: Body::Plain("hello there".to_string()),
401 response_handler: None
402 }
403 );
404
405 Ok(())
406 }
407
408 #[test]
409 fn should_parse_full_yaml_multipart_request() -> Result<()> {
410 let result = parse_request_from_yaml(
411 &root(),
412 indoc! {r#"
413 method: POST
414 url: http://localhost/foo
415 headers:
416 accept: application/json
417 body:
418 - name: textpart1
419 text: text for part 1
420 - name: textpart2
421 text: text for part 2
422 mime: text/plain
423 - name: filepart1
424 filepath: resources/image.jpg
425 - name: filepart2
426 filepath: resources/image.jpg
427 mime: image/png
428 "#},
429 )?;
430
431 let headers = {
432 let mut tmp = HeaderMap::new();
433 tmp.append(
434 HeaderName::from_str("accept").unwrap(),
435 HeaderValue::from_str("application/json").unwrap(),
436 );
437 tmp
438 };
439
440 assert_eq!(
441 result,
442 Request {
443 method: Method::POST,
444 url: "http://localhost/foo".to_string(),
445 headers,
446 body: Body::Multipart(vec![
447 MultipartPart::Text {
448 name: "textpart1".to_string(),
449 text: "text for part 1".to_string(),
450 mime_str: None,
451 },
452 MultipartPart::Text {
453 name: "textpart2".to_string(),
454 text: "text for part 2".to_string(),
455 mime_str: Some("text/plain".to_string()),
456 },
457 MultipartPart::File {
458 name: "filepart1".to_string(),
459 file_path: root().join("resources/image.jpg"),
460 mime_str: None,
461 },
462 MultipartPart::File {
463 name: "filepart2".to_string(),
464 file_path: root().join("resources/image.jpg"),
465 mime_str: Some("image/png".to_string()),
466 },
467 ]),
468 response_handler: None
469 }
470 );
471
472 Ok(())
473 }
474}