keyvalues_parser/text/
parse.rs

1// Due to Pest generating variants that are all uppercase
2#![allow(renamed_and_removed_lints)]
3#![allow(clippy::unknown_clippy_lints)]
4#![allow(clippy::upper_case_acronyms)]
5
6use pest::{iterators::Pair as PestPair, Parser};
7use pest_derive::Parser;
8
9use std::borrow::Cow;
10
11use crate::{error::Result, Obj, PartialVdf as Vdf, Value};
12
13macro_rules! common_parsing {
14    ($parser:ty, $rule:ty, $parse_escaped:expr) => {
15        /// Attempts to parse VDF text to a [`Vdf`]
16        pub fn parse(s: &str) -> Result<Vdf<'_>> {
17            let mut full_grammar = <$parser>::parse(<$rule>::vdf, s)?;
18
19            // There can be multiple base macros before the initial pair
20            let mut bases = Vec::new();
21            loop {
22                let pair = full_grammar.next().unwrap();
23                if let <$rule>::base_macro = pair.as_rule() {
24                    let base_path_string = pair.into_inner().next().unwrap();
25                    let base_path = match base_path_string.as_rule() {
26                        <$rule>::quoted_raw_string => base_path_string.into_inner().next().unwrap(),
27                        <$rule>::unquoted_string => base_path_string,
28                        _ => unreachable!("Prevented by grammar"),
29                    }
30                    .as_str();
31                    bases.push(Cow::from(base_path));
32                } else {
33                    let (key, value) = parse_pair(pair);
34                    return Ok(Vdf { key, value, bases });
35                }
36            }
37        }
38
39        // impl<'a> From<PestPair<'a, $rule>> for Vdf<'a> {
40        //     fn from(full_grammar: PestPair<'a, $rule>) -> Self {
41        //         let (key, value) = parse_pair(full_grammar);
42        //         Self::new(key, value)
43        //     }
44        // }
45
46        fn parse_pair(grammar_pair: PestPair<'_, $rule>) -> (Cow<'_, str>, Value<'_>) {
47            // Structure: pair
48            //            \ key   <- Desired
49            //            \ value <- Desired
50            if let <$rule>::pair = grammar_pair.as_rule() {
51                // Parse out the key and value
52                let mut grammar_pair_innards = grammar_pair.into_inner();
53                let grammar_string = grammar_pair_innards.next().unwrap();
54                let key = parse_string(grammar_string);
55
56                let grammar_value = grammar_pair_innards.next().unwrap();
57                let value = Value::from(grammar_value);
58
59                (key, value)
60            } else {
61                unreachable!("Prevented by grammar");
62            }
63        }
64
65        fn parse_string(grammar_string: PestPair<'_, $rule>) -> Cow<'_, str> {
66            match grammar_string.as_rule() {
67                // Structure: quoted_string
68                //            \ "
69                //            \ quoted_inner <- Desired
70                //            \ "
71                <$rule>::quoted_string => {
72                    let quoted_inner = grammar_string.into_inner().next().unwrap();
73                    if $parse_escaped {
74                        parse_escaped_string(quoted_inner)
75                    } else {
76                        Cow::from(quoted_inner.as_str())
77                    }
78                }
79                // Structure: unquoted_string <- Desired
80                <$rule>::unquoted_string => {
81                    let s = grammar_string.as_str();
82                    Cow::from(s)
83                }
84                _ => unreachable!("Prevented by grammar"),
85            }
86        }
87
88        // Note: there can be a slight performance win here by having the grammar skip capturing
89        // quoted_inner and instead just slice off the starting and ending '"', but I'm going to pass since
90        // it seems like a hack for a ~4% improvement
91        fn parse_escaped_string(inner: PestPair<'_, $rule>) -> Cow<'_, str> {
92            let s = inner.as_str();
93
94            if s.contains('\\') {
95                // Escaped version won't be quite as long, but it will likely be close
96                let mut escaped = String::with_capacity(s.len());
97                let mut it = s.chars();
98
99                while let Some(ch) = it.next() {
100                    if ch == '\\' {
101                        // Character is escaped so check the next character to figure out the full
102                        // character
103                        match it.next() {
104                            Some('n') => escaped.push('\n'),
105                            Some('r') => escaped.push('\r'),
106                            Some('t') => escaped.push('\t'),
107                            Some('\\') => escaped.push('\\'),
108                            Some('\"') => escaped.push('\"'),
109                            _ => unreachable!("Prevented by grammar"),
110                        }
111                    } else {
112                        escaped.push(ch)
113                    }
114                }
115
116                Cow::from(escaped)
117            } else {
118                Cow::from(s)
119            }
120        }
121
122        impl<'a> From<PestPair<'a, $rule>> for Value<'a> {
123            fn from(grammar_value: PestPair<'a, $rule>) -> Self {
124                // Structure: value is ( obj | quoted_string | unquoted_string )
125                match grammar_value.as_rule() {
126                    // Structure: ( quoted_string | unquoted_string )
127                    <$rule>::quoted_string | <$rule>::unquoted_string => {
128                        Self::Str(parse_string(grammar_value))
129                    }
130                    // Structure: obj
131                    //            \ pair* <- Desired
132                    <$rule>::obj => {
133                        let mut obj = Obj::new();
134                        for grammar_pair in grammar_value.into_inner() {
135                            let (key, value) = parse_pair(grammar_pair);
136                            let entry = obj.entry(key).or_default();
137                            (*entry).push(value);
138                        }
139
140                        Self::Obj(obj)
141                    }
142                    _ => unreachable!("Prevented by grammar"),
143                }
144            }
145        }
146    };
147}
148
149pub use escaped::{parse as escaped_parse, PestError as EscapedPestError};
150pub use raw::{parse as raw_parse, PestError as RawPestError};
151
152impl<'a> Vdf<'a> {
153    /// Attempts to parse VDF text to a [`Vdf`]
154    pub fn parse(s: &'a str) -> Result<Self> {
155        escaped_parse(s)
156    }
157
158    pub fn parse_raw(s: &'a str) -> Result<Self> {
159        raw_parse(s)
160    }
161}
162
163impl<'a> crate::Vdf<'a> {
164    /// Attempts to parse VDF text to a [`Vdf`]
165    pub fn parse(s: &'a str) -> Result<Self> {
166        Ok(crate::Vdf::from(Vdf::parse(s)?))
167    }
168
169    pub fn parse_raw(s: &'a str) -> Result<Self> {
170        Ok(crate::Vdf::from(Vdf::parse_raw(s)?))
171    }
172}
173
174mod escaped {
175    use super::*;
176
177    #[derive(Parser)]
178    #[grammar = "grammars/escaped.pest"]
179    struct EscapedParser;
180
181    pub type PestError = pest::error::Error<Rule>;
182
183    common_parsing!(EscapedParser, Rule, true);
184}
185
186mod raw {
187    use super::*;
188
189    #[derive(Parser)]
190    #[grammar = "grammars/raw.pest"]
191    struct RawParser;
192
193    pub type PestError = pest::error::Error<Rule>;
194
195    common_parsing!(RawParser, Rule, false);
196}