Skip to content

Commit c192777

Browse files
feat(parser): add a new high level parser api
1 parent 3edb914 commit c192777

File tree

6 files changed

+152
-61
lines changed

6 files changed

+152
-61
lines changed

keyvalues-parser/README.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,8 @@ const LOGIN_USERS_VDF: &str = r#"
4141
}
4242
"#;
4343

44-
use keyvalues_parser::Vdf;
45-
46-
fn main() -> Result<(), Box<dyn std::error::Error>> {
47-
let vdf = Vdf::parse(LOGIN_USERS_VDF)?;
44+
fn main() -> keyvalues_parser::error::Result<()> {
45+
let vdf = keyvalues_parser::parse(LOGIN_USERS_VDF)?;
4846
assert_eq!(
4947
"12345678901234567",
5048
vdf.value.unwrap_obj().keys().next().unwrap(),

keyvalues-parser/examples/parse_mutate_render.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use keyvalues_parser::Vdf;
1+
use keyvalues_parser::{parse, Vdf};
22

33
use std::{fs, path::Path};
44

@@ -34,7 +34,7 @@ fn update_version(controller_mappings: &mut Vdf, new_version: String) -> Option<
3434

3535
fn main() -> Result<(), Box<dyn std::error::Error>> {
3636
let vdf_text = read_asset_file("parse_mutate_render.vdf")?;
37-
let mut controller_mappings = Vdf::parse(&vdf_text)?;
37+
let mut controller_mappings = Vdf::from(parse(&vdf_text)?);
3838

3939
// Reading information from VDF:
4040
// This involves a lot of `Option`s so it's moved inside a function

keyvalues-parser/src/lib.rs

Lines changed: 89 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,75 @@ pub mod text;
1919
/// `pest` re-exported for your convenience :)
2020
pub use pest;
2121

22+
/// Parse a KeyValues document to a loosely typed representation
23+
///
24+
/// This is shorthand for parsing a document with default settings aka `Parser::new().parse(text)`
25+
pub fn parse<'text>(text: &'text str) -> error::Result<PartialVdf<'text>> {
26+
Parser::new().parse(text)
27+
}
28+
29+
/// A configurable KeyValues parser allowing for adjusting settings before parsing
30+
#[derive(Clone, Debug, Default)]
31+
pub struct Parser {
32+
literal_special_chars: bool,
33+
}
34+
35+
impl Parser {
36+
/// Constructs a default parser
37+
///
38+
/// Currently this consists of:
39+
///
40+
/// | Toggle | Description |
41+
/// | :---: | :--- |
42+
/// | [`Parser::literal_special_chars()`] | Whether to interpret `\` in strings as the start of an escaped special character, or a literal `\` |
43+
pub const fn new() -> Self {
44+
// same as Default, but const 😏
45+
Self {
46+
literal_special_chars: false,
47+
}
48+
}
49+
50+
/// Toggle how to interpret `\` in strings
51+
///
52+
/// By default (`false`) the parser will interpret backslashes (`\`) in strings as the start of
53+
/// an escaped special character (e.g. `\\` -> `\`, `\"` -> `"`). When `true` the parser will
54+
/// instead interpret backslashes (`\`) as a literal backslash. Commonly seen with
55+
/// windows-paths, for instance
56+
pub const fn literal_special_chars(mut self, yes: bool) -> Self {
57+
self.literal_special_chars = yes;
58+
self
59+
}
60+
61+
/// Parse a KeyValues document to a loosely typed representation
62+
///
63+
/// # Example
64+
///
65+
/// ```
66+
/// use keyvalues_parser::Parser;
67+
/// let vdf = Parser::new()
68+
/// .literal_special_chars(true)
69+
/// .parse(r"InstallDir C:\You\Later")
70+
/// .unwrap();
71+
/// assert_eq!(vdf.value.unwrap_str(), r"C:\You\Later");
72+
/// ```
73+
pub fn parse<'text>(&self, vdf: &'text str) -> error::Result<PartialVdf<'text>> {
74+
if self.literal_special_chars {
75+
#[expect(deprecated)] // deprecated for thee, but not for me!
76+
text::parse::raw_parse(vdf)
77+
} else {
78+
#[expect(deprecated)] // deprecated for thee, but not for me!
79+
text::parse::escaped_parse(vdf)
80+
}
81+
}
82+
}
83+
84+
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
85+
pub struct TopLevelVdf<'text> {
86+
pub key: Key<'text>,
87+
pub value: Value<'text>,
88+
pub bases: Vec<Cow<'text, str>>,
89+
}
90+
2291
/// A Key is simply an alias for `Cow<str>`
2392
pub type Key<'text> = Cow<'text, str>;
2493

@@ -28,8 +97,8 @@ pub type Key<'text> = Cow<'text, str>;
2897
///
2998
/// ## Parse
3099
///
31-
/// `Vdf`s will generally be created through the use of [`Vdf::parse()`] which takes a string
32-
/// representing VDF text and attempts to parse it to a `Vdf` representation.
100+
/// `Vdf`s will generally be created through the use of [`parse()`] or [`Parser::parse()`] which
101+
/// takes a string representing VDF text and attempts to parse it to a `Vdf` representation.
33102
///
34103
/// ## Mutate
35104
///
@@ -43,8 +112,6 @@ pub type Key<'text> = Cow<'text, str>;
43112
/// ## Example
44113
///
45114
/// ```
46-
/// use keyvalues_parser::Vdf;
47-
///
48115
/// // Parse
49116
/// let vdf_text = r#"
50117
/// "Outer Key"
@@ -55,7 +122,7 @@ pub type Key<'text> = Cow<'text, str>;
55122
/// }
56123
/// }
57124
/// "#;
58-
/// let mut parsed = Vdf::parse(vdf_text)?;
125+
/// let mut parsed = keyvalues_parser::parse(vdf_text)?;
59126
///
60127
/// // Mutate: i.e. remove the last "Inner Key" pair
61128
/// parsed
@@ -80,24 +147,6 @@ pub struct Vdf<'text> {
80147
pub value: Value<'text>,
81148
}
82149

83-
impl<'text> From<PartialVdf<'text>> for Vdf<'text> {
84-
fn from(partial: PartialVdf<'text>) -> Self {
85-
Self {
86-
key: partial.key,
87-
value: partial.value,
88-
}
89-
}
90-
}
91-
92-
// TODO: Just store a `Vdf` internally?
93-
// TODO: don't expose these publicly?
94-
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
95-
pub struct PartialVdf<'text> {
96-
pub key: Key<'text>,
97-
pub value: Value<'text>,
98-
pub bases: Vec<Cow<'text, str>>,
99-
}
100-
101150
impl<'text> Vdf<'text> {
102151
/// Creates a [`Vdf`] using a provided key and value
103152
///
@@ -115,6 +164,23 @@ impl<'text> Vdf<'text> {
115164
}
116165
}
117166

167+
impl<'text> From<PartialVdf<'text>> for Vdf<'text> {
168+
fn from(partial: PartialVdf<'text>) -> Self {
169+
Self {
170+
key: partial.key,
171+
value: partial.value,
172+
}
173+
}
174+
}
175+
176+
// TODO: Just store a `Vdf` internally?
177+
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
178+
pub struct PartialVdf<'text> {
179+
pub key: Key<'text>,
180+
pub value: Value<'text>,
181+
pub bases: Vec<Cow<'text, str>>,
182+
}
183+
118184
// TODO: why is this type alias a thing if it's not private but the usage of it inside `Obj` is?
119185
type ObjInner<'text> = BTreeMap<Key<'text>, Vec<Value<'text>>>;
120186
type ObjInnerPair<'text> = (Key<'text>, Vec<Value<'text>>);

keyvalues-parser/src/text/parse/escaped.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,32 @@ type ParseResult<'a> = pest::ParseResult<BoxedState<'a>>;
66

77
common_parsing!(pest_parse, Rule, true);
88

9+
// NOTE(cosmic): duplicate of `super::raw::parse()`, but with a different deprecation method
10+
/// Attempts to parse VDF text to a [`Vdf`]
11+
#[deprecated(since = "0.2.3", note = "Moved to `keyvalues_parser::parse()`")]
12+
pub fn parse(s: &str) -> Result<PartialVdf<'_>> {
13+
let mut full_grammar = pest_parse(s)?;
14+
15+
// There can be multiple base macros before the initial pair
16+
let mut bases = Vec::new();
17+
loop {
18+
let pair = full_grammar.next().unwrap();
19+
if let Rule::base_macro = pair.as_rule() {
20+
let base_path_string = pair.into_inner().next().unwrap();
21+
let base_path = match base_path_string.as_rule() {
22+
Rule::quoted_raw_string => base_path_string.into_inner().next().unwrap(),
23+
Rule::unquoted_string => base_path_string,
24+
_ => unreachable!("Prevented by grammar"),
25+
}
26+
.as_str();
27+
bases.push(Cow::from(base_path));
28+
} else {
29+
let (key, value) = parse_pair(pair);
30+
return Ok(PartialVdf { key, value, bases });
31+
}
32+
}
33+
}
34+
935
#[allow(non_camel_case_types, clippy::upper_case_acronyms)]
1036
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
1137
pub enum Rule {

keyvalues-parser/src/text/parse/mod.rs

Lines changed: 4 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,11 @@ use pest::{iterators::Pair as PestPair, Atomicity, RuleType};
99
mod escaped;
1010
mod raw;
1111

12+
#[expect(deprecated)]
1213
pub use escaped::parse as escaped_parse;
13-
#[deprecated(
14-
since = "0.2.3",
15-
note = "Moved to `keyvalues_parser::error::EscapedPestError`"
16-
)]
1714
pub use escaped::PestError as EscapedPestError;
15+
#[expect(deprecated)]
1816
pub use raw::parse as raw_parse;
19-
#[deprecated(
20-
since = "0.2.3",
21-
note = "Moved to `keyvalues_parser::error::RawPestError`"
22-
)]
2317
pub use raw::PestError as RawPestError;
2418

2519
type BoxedState<'a, R> = Box<pest::ParserState<'a, R>>;
@@ -74,30 +68,6 @@ fn skip<R: RuleType>(s: BoxedState<'_, R>) -> ParseResult<'_, R> {
7468
// separate grammars :/
7569
macro_rules! common_parsing {
7670
($parse_fn:ident, $rule:ty, $parse_escaped:expr) => {
77-
/// Attempts to parse VDF text to a [`Vdf`]
78-
pub fn parse(s: &str) -> Result<PartialVdf<'_>> {
79-
let mut full_grammar = $parse_fn(s)?;
80-
81-
// There can be multiple base macros before the initial pair
82-
let mut bases = Vec::new();
83-
loop {
84-
let pair = full_grammar.next().unwrap();
85-
if let <$rule>::base_macro = pair.as_rule() {
86-
let base_path_string = pair.into_inner().next().unwrap();
87-
let base_path = match base_path_string.as_rule() {
88-
<$rule>::quoted_raw_string => base_path_string.into_inner().next().unwrap(),
89-
<$rule>::unquoted_string => base_path_string,
90-
_ => unreachable!("Prevented by grammar"),
91-
}
92-
.as_str();
93-
bases.push(Cow::from(base_path));
94-
} else {
95-
let (key, value) = parse_pair(pair);
96-
return Ok(PartialVdf { key, value, bases });
97-
}
98-
}
99-
}
100-
10171
fn parse_pair(grammar_pair: PestPair<'_, $rule>) -> (Cow<'_, str>, Value<'_>) {
10272
// Structure: pair
10373
// \ key <- Desired
@@ -218,10 +188,12 @@ impl<'a> Vdf<'a> {
218188
impl<'a> PartialVdf<'a> {
219189
/// Attempts to parse VDF text to a [`Vdf`]
220190
pub fn parse(s: &'a str) -> Result<Self> {
191+
#[expect(deprecated)]
221192
escaped_parse(s)
222193
}
223194

224195
pub fn parse_raw(s: &'a str) -> Result<Self> {
196+
#[expect(deprecated)]
225197
raw_parse(s)
226198
}
227199
}

keyvalues-parser/src/text/parse/raw.rs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,35 @@ type ParseResult<'a> = pest::ParseResult<BoxedState<'a>>;
66

77
common_parsing!(pest_parse, Rule, false);
88

9+
// NOTE(cosmic): duplicate of `super::escaped::parse()`, but with a different deprecation method
10+
/// Attempts to parse VDF text to a [`Vdf`]
11+
#[deprecated(
12+
since = "0.2.3",
13+
note = "Please use `Parser::new().literal_special_chars(true).parse()` instead"
14+
)]
15+
pub fn parse(s: &str) -> Result<PartialVdf<'_>> {
16+
let mut full_grammar = pest_parse(s)?;
17+
18+
// There can be multiple base macros before the initial pair
19+
let mut bases = Vec::new();
20+
loop {
21+
let pair = full_grammar.next().unwrap();
22+
if let Rule::base_macro = pair.as_rule() {
23+
let base_path_string = pair.into_inner().next().unwrap();
24+
let base_path = match base_path_string.as_rule() {
25+
Rule::quoted_raw_string => base_path_string.into_inner().next().unwrap(),
26+
Rule::unquoted_string => base_path_string,
27+
_ => unreachable!("Prevented by grammar"),
28+
}
29+
.as_str();
30+
bases.push(Cow::from(base_path));
31+
} else {
32+
let (key, value) = parse_pair(pair);
33+
return Ok(PartialVdf { key, value, bases });
34+
}
35+
}
36+
}
37+
938
#[allow(non_camel_case_types, clippy::upper_case_acronyms)]
1039
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
1140
pub enum Rule {

0 commit comments

Comments
 (0)