fhttp_core/request_sources/
structured_request_source.rs

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}