curl_parser_ruben4ick/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use pest::Parser;
4use pest::error::Error as PestError;
5use pest_derive::Parser;
6use std::collections::HashMap;
7use std::fmt::{Display, Formatter, Result as FmtResult};
8use thiserror::Error;
9
10#[derive(Parser)]
11#[grammar = "./curl_grammar.pest"]
12pub struct CurlParser;
13
14#[derive(Debug, Error)]
15pub enum ParseError {
16    #[error("parse error: {0}")]
17    Pest(#[from] Box<PestError<Rule>>),
18
19    #[error("missing URL in curl command")]
20    MissingUrl,
21
22    #[error("missing value for flag {0}")]
23    MissingValue(&'static str),
24}
25
26#[derive(Debug)]
27pub struct CurlRequest {
28    pub method: String,
29    pub url: String,
30    pub headers: HashMap<String, String>,
31    pub body: String,
32}
33
34impl Display for CurlRequest {
35    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
36        writeln!(f, "method: {}", self.method)?;
37        writeln!(f, "url: {}", self.url)?;
38        writeln!(f, "headers:")?;
39        for (k, v) in &self.headers {
40            writeln!(f, "  {}: {}", k, v)?;
41        }
42        writeln!(f, "body:\n{}", self.body)
43    }
44}
45
46impl CurlRequest {
47    pub fn parse_input(input: &str) -> Result<Self, ParseError> {
48        let pairs =
49            CurlParser::parse(Rule::curl, input).map_err(|e| ParseError::Pest(Box::new(e)))?;
50
51        let mut method = String::new();
52        let mut url = String::new();
53        let mut headers = HashMap::new();
54        let mut body = String::new();
55
56        for pair in pairs {
57            if pair.as_rule() != Rule::curl {
58                continue;
59            }
60            for p in pair.into_inner() {
61                match p.as_rule() {
62                    Rule::url => {
63                        if url.is_empty() {
64                            url = strip_quotes(p.as_str()).to_string();
65                        }
66                    }
67                    Rule::method_flag => {
68                        let v = p
69                            .into_inner()
70                            .next()
71                            .ok_or(ParseError::MissingValue("-X/--request"))?;
72                        method = strip_quotes(v.as_str()).to_uppercase();
73                    }
74                    Rule::header_flag => {
75                        let v = p
76                            .into_inner()
77                            .next()
78                            .ok_or(ParseError::MissingValue("-H/--header"))?;
79                        if let Some((k, v)) = split_header(strip_quotes(v.as_str())) {
80                            headers.insert(k, v);
81                        }
82                    }
83                    Rule::data_flag => {
84                        let v = p
85                            .into_inner()
86                            .next()
87                            .ok_or(ParseError::MissingValue("-d/--data"))?;
88                        body = strip_quotes(v.as_str()).to_string();
89                    }
90                    _ => {}
91                }
92            }
93        }
94
95        if url.is_empty() {
96            return Err(ParseError::MissingUrl);
97        }
98
99        Ok(CurlRequest {
100            method,
101            url,
102            headers,
103            body,
104        })
105    }
106}
107
108pub fn parse(input: &str) -> Result<CurlRequest, ParseError> {
109    CurlRequest::parse_input(input)
110}
111
112fn strip_quotes(s: &str) -> &str {
113    if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
114        &s[1..s.len() - 1]
115    } else {
116        s
117    }
118}
119
120fn split_header(raw: &str) -> Option<(String, String)> {
121    let (k, v) = raw.split_once(':')?;
122    Some((k.trim().to_string(), v.trim().to_string()))
123}