Skip to main content

miniconf_parser/
parser.rs

1use pest::Parser;
2use pest::iterators::Pair;
3use pest_derive::Parser;
4
5use crate::ast::{Document, Entry, Value};
6use crate::error::{MiniConfError, ParseErrorKind};
7
8const ROOT_SECTION: &str = "root";
9
10#[derive(Parser)]
11#[grammar = "grammar.pest"]
12/// Generated pest parser for MiniConf.
13pub struct MiniConfParser;
14
15/// Parses MiniConf input into a [`Document`].
16pub fn parse_document(source: &str) -> Result<Document, MiniConfError> {
17    let mut document = Document::new();
18    document.ensure_section_mut(ROOT_SECTION);
19    let mut current_section = ROOT_SECTION.to_string();
20
21    let pairs = MiniConfParser::parse(Rule::text, source)?
22        .next()
23        .expect("text rule must produce a pair")
24        .into_inner();
25
26    for pair in pairs {
27        match pair.as_rule() {
28            Rule::section_header => {
29                let name = pair.into_inner().next().unwrap().as_str().to_string();
30                current_section = name;
31                document.ensure_section_mut(&current_section);
32            }
33            Rule::key_value => {
34                handle_key_value(&mut document, &current_section, pair)?;
35            }
36            Rule::comment_line | Rule::EOI => {
37                // comments and explicit end markers are intentionally ignored
38            }
39            _ => {}
40        }
41    }
42
43    Ok(document)
44}
45
46fn handle_key_value(
47    document: &mut Document,
48    current_section: &str,
49    pair: Pair<'_, Rule>,
50) -> Result<(), MiniConfError> {
51    let line = line_of(&pair);
52    let mut inner = pair.into_inner();
53    let key = inner.next().expect("key present").as_str().to_string();
54    let value_pair = inner
55        .find(|p| matches!(p.as_rule(), Rule::value))
56        .expect("value present");
57    let value = parse_value(value_pair)?;
58
59    let section = document.ensure_section_mut(current_section);
60    if section.entries.iter().any(|entry| entry.key == key) {
61        return Err(MiniConfError::semantic(
62            ParseErrorKind::DuplicateKey,
63            line,
64            format!("key `{key}` already defined in [{current_section}]"),
65        ));
66    }
67
68    section.entries.push(Entry::new(key, value, line));
69    Ok(())
70}
71
72fn parse_value(pair: Pair<'_, Rule>) -> Result<Value, MiniConfError> {
73    match pair.as_rule() {
74        Rule::value => {
75            let inner = pair.into_inner().next().expect("value inner");
76            parse_value(inner)
77        }
78        Rule::quoted_string => Ok(Value::String(unescape(pair.as_str()))),
79        Rule::bare_string => Ok(Value::String(pair.as_str().to_string())),
80        Rule::number => {
81            let line = line_of(&pair);
82            match pair.as_str().parse::<f64>() {
83                Ok(num) => Ok(Value::Number(num)),
84                Err(err) => Err(MiniConfError::semantic(
85                    ParseErrorKind::InvalidValue,
86                    line,
87                    format!("invalid number: {err}"),
88                )),
89            }
90        }
91        Rule::boolean => Ok(Value::Bool(matches!(pair.as_str(), "true" | "yes"))),
92        Rule::array => parse_array(pair),
93        other => unreachable!("unexpected value rule: {other:?}"),
94    }
95}
96
97fn parse_array(pair: Pair<'_, Rule>) -> Result<Value, MiniConfError> {
98    let mut values = Vec::new();
99    for inner in pair.into_inner() {
100        if matches!(inner.as_rule(), Rule::value) {
101            values.push(parse_value(inner)?);
102        }
103    }
104    Ok(Value::Array(values))
105}
106
107fn unescape(raw: &str) -> String {
108    let body = &raw[1..raw.len() - 1];
109    let mut buf = String::with_capacity(body.len());
110    let mut chars = body.chars();
111    while let Some(ch) = chars.next() {
112        if ch == '\\' {
113            if let Some(next) = chars.next() {
114                match next {
115                    '"' => buf.push('"'),
116                    'n' => buf.push('\n'),
117                    't' => buf.push('\t'),
118                    '\\' => buf.push('\\'),
119                    other => buf.push(other),
120                }
121            }
122        } else {
123            buf.push(ch);
124        }
125    }
126    buf
127}
128
129fn line_of(pair: &Pair<'_, Rule>) -> usize {
130    pair.as_span().start_pos().line_col().0
131}