Skip to main content

reqmd_ast/
http_data.rs

1use crate::{BodyData, Error, Position, parsing::ParseContext};
2use ::markdown::mdast::{self, Node};
3use ::reqmd_http as http;
4
5/// # HTTP Data
6///
7/// Data extracted from a markdown document `code`
8/// block that is tagged with the language of `http`.
9/// This format is mostly the same as a raw http
10/// request.
11///
12#[derive(Debug, Clone, PartialEq, Eq, Default)]
13#[cfg_attr(feature = "serde", derive(::serde::Serialize, ::serde::Deserialize))]
14#[cfg_attr(feature = "serde", serde(default))]
15pub struct HttpData {
16    /// The heading in the markdown document found directly
17    /// above the http code block.  The `#` and leading
18    /// whitespace are trimmed.
19    pub title: Option<String>,
20
21    /// Any text found between the heading and the http code
22    /// block is treated as a description.
23    pub description: Option<String>,
24
25    /// Parsed method from the beginning of the http
26    /// code block.
27    pub method: http::Method,
28
29    /// just the path portion of the request line
30    pub path: http::Path,
31
32    /// data dextracted from the query string of the
33    /// request line.
34    pub query: http::QueryString,
35
36    /// data extracted from the headers of the http code block
37    pub headers: http::Headers,
38
39    /// the next code block immediately following the http code
40    /// block is treated as the body of the request ( if any ).
41    pub body: BodyData,
42
43    /// the range from which all of this data was extracted
44    pub position: Position,
45}
46
47impl HttpData {
48    pub(crate) fn try_collect(ctx: &ParseContext<'_>) -> Result<Vec<Self>, Error> {
49        let mut data_set = Vec::new();
50        let mut iter = ctx.root.children.iter().peekable();
51        let mut prior_heading = None;
52
53        while let Some(child) = iter.next() {
54            match child {
55                Node::Code(block) if is_http_code(block) => {
56                    let mut data = parser::parse(&block.value)?;
57                    data.position = Position::try_from(block)?;
58
59                    if let Some(Node::Code(block)) = iter.peek()
60                        && !is_http_code(block)
61                    {
62                        iter.next(); // consuming peeked block for body
63                        data.body = BodyData::from(block.clone());
64                        data.position.extend(&Position::try_from(block)?);
65                    }
66
67                    if let Some(heading) = prior_heading.take() {
68                        let position = Position::try_from(heading)?;
69                        let title = position
70                            .find_substring(ctx.input)?
71                            .trim_start_matches('#')
72                            .trim();
73                        data.title = Some(title.into());
74
75                        if let Some(range) = position.range_between(&data.position) {
76                            let desc = ctx.input[range].trim();
77                            if !desc.is_empty() {
78                                data.description = Some(desc.into());
79                            }
80                        }
81
82                        data.position.extend(&position);
83                    }
84
85                    data_set.push(data);
86                }
87                Node::Heading(heading) => {
88                    prior_heading = Some(heading);
89                }
90                _ => continue,
91            }
92        }
93
94        Ok(data_set)
95    }
96}
97
98fn is_http_code(code: &mdast::Code) -> bool {
99    code.lang.as_deref() == Some("http")
100}
101
102mod parser;
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use crate::Point;
108    use crate::support::fixtures::post_widget_parse_context as ctx;
109
110    #[rstest::rstest]
111    fn collect_from_mdast_works(ctx: ParseContext<'static>) {
112        let data = HttpData::try_collect(&ctx).unwrap();
113        assert_eq!(data.len(), 1);
114        let http = data.first().unwrap();
115        dbg!(&http);
116        assert_eq!(http.method, http::Method::Post);
117        assert_eq!(http.path, http::Path::from("/widgets"));
118        assert_eq!(http.query.first("foo"), Some("bar"));
119        assert_eq!(http.query.first("rofl"), Some("copter"));
120        assert_eq!(http.headers.first("authorization"), Some("Bearer abcd1234"));
121        assert_eq!(http.body.lang.as_deref(), Some("json"));
122        assert_eq!(http.body.meta.as_deref(), Some("http-body"));
123        assert_eq!(http.title.as_deref(), Some("Post Widgets"));
124        assert_eq!(
125            http.description.as_deref(),
126            Some("I've often wondered what this text is called")
127        );
128        assert_eq!(
129            http.body.position.as_ref(),
130            Some(&Position {
131                start: Point {
132                    line: 19,
133                    column: 1,
134                    offset: 232
135                },
136                end: Point {
137                    line: 24,
138                    column: 4,
139                    offset: 305
140                },
141            })
142        );
143        assert_eq!(
144            http.body.content.text(),
145            Some("{\n  \"name\": \"XFox\",\n  \"desc\": \"Wonderful widget!\"\n}")
146        );
147        assert_eq!(
148            http.position,
149            Position {
150                start: Point {
151                    line: 8,
152                    column: 1,
153                    offset: 80
154                },
155                end: Point {
156                    line: 24,
157                    column: 4,
158                    offset: 305
159                },
160            }
161        );
162    }
163}