keyvalues_parser/text/parse/
mod.rs

1use pest::iterators::Pair as PestPair;
2
3use std::borrow::Cow;
4
5use crate::{error::Result, Obj, PartialVdf as Vdf, Value};
6
7// the parser code is formatted using `prettyplease` which differs slightly from `rustfmt`. this
8// indirection is enough to keep `rustfmt` from formatting the generated code until either the
9// `ignore` config option is stable for `rustfmt` _or_ we switch to a different parser library with
10// a less hacky setup :D
11mod escaped {
12    include!("escaped.rs");
13}
14mod raw {
15    include!("raw.rs");
16}
17
18// unfortunate hack to re-use most of the code that consumes the pest parser produced by our two
19// separate grammars :/
20macro_rules! common_parsing {
21    ($parser:ty, $rule:ty, $parse_escaped:expr) => {
22        /// Attempts to parse VDF text to a [`Vdf`]
23        pub fn parse(s: &str) -> Result<Vdf<'_>> {
24            let mut full_grammar = <$parser>::parse(<$rule>::vdf, s)?;
25
26            // There can be multiple base macros before the initial pair
27            let mut bases = Vec::new();
28            loop {
29                let pair = full_grammar.next().unwrap();
30                if let <$rule>::base_macro = pair.as_rule() {
31                    let base_path_string = pair.into_inner().next().unwrap();
32                    let base_path = match base_path_string.as_rule() {
33                        <$rule>::quoted_raw_string => base_path_string.into_inner().next().unwrap(),
34                        <$rule>::unquoted_string => base_path_string,
35                        _ => unreachable!("Prevented by grammar"),
36                    }
37                    .as_str();
38                    bases.push(Cow::from(base_path));
39                } else {
40                    let (key, value) = parse_pair(pair);
41                    return Ok(Vdf { key, value, bases });
42                }
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
149// expose ^^ macro to the rest of the crate
150pub(crate) use common_parsing;
151
152pub use escaped::{parse as escaped_parse, PestError as EscapedPestError};
153pub use raw::{parse as raw_parse, PestError as RawPestError};
154
155impl<'a> Vdf<'a> {
156    /// Attempts to parse VDF text to a [`Vdf`]
157    pub fn parse(s: &'a str) -> Result<Self> {
158        escaped_parse(s)
159    }
160
161    pub fn parse_raw(s: &'a str) -> Result<Self> {
162        raw_parse(s)
163    }
164}
165
166impl<'a> crate::Vdf<'a> {
167    /// Attempts to parse VDF text to a [`Vdf`]
168    pub fn parse(s: &'a str) -> Result<Self> {
169        Ok(crate::Vdf::from(Vdf::parse(s)?))
170    }
171
172    pub fn parse_raw(s: &'a str) -> Result<Self> {
173        Ok(crate::Vdf::from(Vdf::parse_raw(s)?))
174    }
175}