reqmd_ast 0.2.1

ast library interface for reqmd
Documentation
use super::HttpData;
use crate::Error;
use ::nom::branch::alt;
use ::nom::bytes::complete::{is_a, tag};
use ::nom::character::complete::{alpha1, alphanumeric1, newline, space1};
use ::nom::combinator::{opt, recognize};
use ::nom::multi::{fold_many0, many1_count};
use ::nom::sequence::{preceded, separated_pair, terminated};
use ::nom::{Finish, IResult, Parser};
use ::reqmd_http as http;

pub(super) fn parse(input: &str) -> Result<HttpData, Error> {
    let (_, request) = http_data(input)
        .finish()
        .map_err(|err| Error::Parse(format!("HttpData parsing error: {}", err)))?;
    Ok(request)
}

fn http_data(input: &str) -> IResult<&str, HttpData> {
    let (input, method) = http_method(input)?;
    let (input, _) = space1(input)?;
    let (input, path) = http_path(input)?;
    let (input, _) = opt(preceded(newline, space1)).parse(input)?;
    let (input, query) = opt(http_query_string).parse(input)?;
    let (input, _) = opt(newline).parse(input)?;
    let (input, headers) = opt(http_headers).parse(input)?;
    let query = query.unwrap_or_default();
    let headers = headers.unwrap_or_default();

    Ok((
        input,
        HttpData {
            method,
            path,
            query,
            headers,
            ..Default::default()
        },
    ))
}

fn http_method(input: &str) -> IResult<&str, http::Method> {
    alpha1
        .map_res(|s: &str| s.parse::<http::Method>())
        .parse(input)
}

fn http_path(input: &str) -> IResult<&str, http::Path> {
    recognize(many1_count(alt((
        alphanumeric1,
        is_a("-._~!$&'()*+,;=:/@"),
    ))))
    .map(http::Path::from)
    .parse(input)
}

fn http_query_string(input: &str) -> IResult<&str, http::QueryString> {
    fn char_group(input: &str) -> IResult<&str, String> {
        recognize(many1_count(alt((
            alphanumeric1,
            is_a("@!\"'$%^*_-+()<>[]{}/|;`."),
            preceded(
                tag("\\"),
                alt((tag(" "), tag("="), tag("&"), tag("?"), tag("\\"))),
            ),
        ))))
        .map(String::from)
        .parse(input)
    }

    fn target_value(input: &str) -> IResult<&str, String> {
        alt((
            terminated(char_group, tag("&")),
            terminated(char_group, preceded(newline, preceded(space1, tag("&")))),
            char_group,
        ))
        .parse(input)
    }

    preceded(
        tag("?"),
        fold_many0(
            separated_pair(char_group, tag("="), target_value),
            http::QueryString::default,
            |mut query_string, (left, right)| {
                query_string.add(left, right);
                query_string
            },
        ),
    )
    .parse(input)
}

fn http_headers(input: &str) -> IResult<&str, http::Headers> {
    fn header_line(input: &str) -> IResult<&str, (&str, &str)> {
        separated_pair(
            recognize(many1_count(alt((alphanumeric1, is_a("-_"))))),
            tag(": "),
            recognize(many1_count(alt((
                alphanumeric1,
                is_a(" @!:\"#$%^&*()_-+={}[]|;'<>,.?/`~"),
            )))),
        )
        .parse(input)
    }

    fold_many0(
        alt((terminated(header_line, newline), header_line)),
        http::Headers::default,
        |mut headers, (key, value)| {
            headers.add(key, value);
            headers
        },
    )
    .parse(input)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[rstest::rstest]
    fn parse_method_verb() {
        let result = http_method("POST /api/v1/widgets").finish();
        let (rest, verb) = result.unwrap();
        assert_eq!(verb, http::Method::Post);
        assert_eq!(rest, " /api/v1/widgets");
    }

    #[rstest::rstest]
    fn parse_path() {
        let result = http_path("/api/v1/widgets?rofl=copter").finish();
        let (rest, path) = result.unwrap();
        assert_eq!(path.as_str(), "/api/v1/widgets");
        assert_eq!(rest, "?rofl=copter");
    }

    #[rstest::rstest]
    fn parse_path_with_special_characters() {
        let result = http_path("/api/v1/widg-ets._~!$&'()*+,;=:/@").finish();
        let (rest, path) = result.unwrap();
        assert_eq!(path.as_str(), "/api/v1/widg-ets._~!$&'()*+,;=:/@");
        assert_eq!(rest, "");
    }

    #[rstest::rstest]
    fn single_line_query_string() {
        let result = http_query_string("?foo=bar").finish();
        let (rest, query) = result.unwrap();
        assert_eq!(query.first("foo"), Some("bar"));
        assert_eq!("", rest);
    }

    #[rstest::rstest]
    fn query_string_with_dollar_signs() {
        let result = http_query_string("?price=$19.99&$ENV=true").finish();
        let (rest, query) = result.unwrap();
        assert_eq!(query.first("price"), Some("$19.99"));
        assert_eq!(query.first("$ENV"), Some("true"));
        assert_eq!("", rest);
    }

    #[rstest::rstest]
    fn multi_line_query_string() {
        let input = r#"?foo=bar
                       &biz=baz
                       &rofl=copter!"#;
        let result = http_query_string(input).finish();
        let (rest, query) = result.unwrap();
        assert_eq!(query.first("foo"), Some("bar"));
        assert_eq!(query.first("biz"), Some("baz"));
        assert_eq!(query.first("rofl"), Some("copter!"));
        assert_eq!("", rest);
    }

    #[rstest::rstest]
    fn single_line_header() {
        let input = "Content-Type: application/json";
        let result = http_headers(input).finish();
        let (rest, headers) = result.unwrap();
        assert_eq!(headers.first("Content-Type"), Some("application/json"));
        assert_eq!("", rest);
    }

    #[rstest::rstest]
    fn header_with_dollar_signs() {
        let input = "X-Api-Key: $API_KEY";
        let result = http_headers(input).finish();
        let (rest, headers) = result.unwrap();
        assert_eq!(headers.first("X-Api-Key"), Some("$API_KEY"));
        assert_eq!("", rest);
    }

    #[rstest::rstest]
    fn multi_line_headers() {
        let input = [
            "Content-Type: application/json",
            "Authorization: Bearer token",
        ]
        .join("\n");
        let result = http_headers(&input).finish();
        let (rest, headers) = result.unwrap();
        assert_eq!(headers.first("Content-Type"), Some("application/json"));
        assert_eq!(headers.first("Authorization"), Some("Bearer token"));
        assert_eq!(rest, "");
    }

    #[rstest::rstest]
    fn full_request_parsing_single_line_query_params() {
        let input = [
            "POST /api/v1/widgets?foo=bar&biz=baz",
            "Content-Type: application/json",
            "Authorization: Bearer token",
        ]
        .join("\n");

        let (rest, request) = http_data(&input).finish().unwrap();
        assert_eq!(request.method, http::Method::Post);
        assert_eq!(request.path.as_str(), "/api/v1/widgets");
        assert_eq!(request.query.first("foo"), Some("bar"));
        assert_eq!(request.query.first("biz"), Some("baz"));
        assert_eq!(request.headers.first("Authorization"), Some("Bearer token"));
        assert_eq!(
            request.headers.first("Content-Type"),
            Some("application/json")
        );
        assert_eq!(rest, "");
    }

    #[rstest::rstest]
    fn full_request_parsing_multi_line_query_params() {
        let input = [
            "GET /api/v1/widgets?foo=bar",
            "                   &biz=baz",
            "Content-Type: application/json",
            "Authorization: Bearer token",
        ]
        .join("\n");

        let (rest, request) = http_data(&input).finish().unwrap();
        assert_eq!(request.method, http::Method::Get);
        assert_eq!(request.path.as_str(), "/api/v1/widgets");
        assert_eq!(request.query.first("foo"), Some("bar"));
        assert_eq!(request.query.first("biz"), Some("baz"));
        assert_eq!(request.headers.first("Authorization"), Some("Bearer token"));
        assert_eq!(
            request.headers.first("Content-Type"),
            Some("application/json")
        );
        assert_eq!(rest, "");
    }

    #[rstest::rstest]
    fn full_request_parsing_multi_next_line_query_params() {
        let input = [
            "GET /api/v1/widgets",
            "    ?foo=bar",
            "    &biz=baz",
            "Content-Type: application/json",
            "Authorization: Bearer token",
        ]
        .join("\n");

        let (rest, request) = http_data(&input).finish().unwrap();
        assert_eq!(request.method, http::Method::Get);
        assert_eq!(request.path.as_str(), "/api/v1/widgets");
        assert_eq!(request.query.first("foo"), Some("bar"));
        assert_eq!(request.query.first("biz"), Some("baz"));
        assert_eq!(request.headers.first("Authorization"), Some("Bearer token"));
        assert_eq!(
            request.headers.first("Content-Type"),
            Some("application/json")
        );
        assert_eq!(rest, "");
    }

    #[rstest::rstest]
    fn full_request_parsing_no_headers() {
        let input = "DELETE /api/v1/widgets?foo=bar&biz=baz";
        let (rest, request) = http_data(input).finish().unwrap();
        assert_eq!(request.method, http::Method::Delete);
        assert_eq!(request.path.as_str(), "/api/v1/widgets");
        assert_eq!(request.query.first("foo"), Some("bar"));
        assert_eq!(request.query.first("biz"), Some("baz"));
        assert!(request.headers.is_empty());
        assert_eq!(rest, "");
    }

    #[rstest::rstest]
    fn full_request_parsing_no_query_params() {
        let input = [
            "PUT /api/v1/widgets",
            "Content-Type: application/json",
            "Authorization: Bearer token",
        ]
        .join("\n");
        let (rest, request) = http_data(&input).finish().unwrap();
        assert_eq!(rest, "");
        assert_eq!(request.method, http::Method::Put);
        assert_eq!(request.path.as_str(), "/api/v1/widgets");
        assert!(request.query.is_empty());
        assert_eq!(request.headers.first("Authorization"), Some("Bearer token"));
        assert_eq!(
            request.headers.first("Content-Type"),
            Some("application/json")
        );
    }

    #[rstest::rstest]
    fn full_request_parsing_no_query_params_or_header() {
        let input = "GET /api/v1/widgets";
        let (rest, request) = http_data(input).finish().unwrap();
        assert_eq!(request.method, http::Method::Get);
        assert_eq!(request.path.as_str(), "/api/v1/widgets");
        assert!(request.query.is_empty());
        assert!(request.headers.is_empty());
        assert_eq!(rest, "");
    }
}