Skip to main content

serde_structprop/
parse.rs

1//! Parser for the structprop format.
2//!
3//! This module contains the [`Value`] type that represents a parsed structprop
4//! document and the [`parse()`] function that converts a raw `&str` into a
5//! [`Value::Object`] tree.
6//!
7//! # Grammar (informal)
8//!
9//! ```text
10//! document   = assignment*
11//! assignment = TERM '=' value
12//!            | TERM '{' assignment* '}'
13//! value      = TERM
14//!            | '{' (TERM | '{' assignment* '}')* '}'
15//! ```
16
17use crate::error::{Error, Result};
18use crate::lexer::{tokenize, Token};
19use indexmap::IndexMap;
20
21// ---------------------------------------------------------------------------
22// Public types
23// ---------------------------------------------------------------------------
24
25/// A node in the structprop value tree produced by [`parse()`].
26///
27/// The tree maps directly onto structprop's three syntactic forms:
28///
29/// | Structprop syntax | Variant |
30/// |---|---|
31/// | `key = value` | [`Value::Scalar`] |
32/// | `key = { a b c }` | [`Value::Array`] |
33/// | `key { … }` | [`Value::Object`] |
34///
35/// Scalar strings are stored verbatim; numeric or boolean coercion is
36/// performed lazily via the [`Value::as_bool`], [`Value::as_i64`], and
37/// [`Value::as_f64`] helpers.
38#[derive(Debug, Clone, PartialEq)]
39pub enum Value {
40    /// A bare or quoted string token, stored as-is (no coercion applied).
41    ///
42    /// Use [`Value::as_bool`], [`Value::as_i64`], or [`Value::as_f64`] to
43    /// attempt type coercion, or [`Value::is_null`] to test for `null`.
44    Scalar(String),
45
46    /// An ordered list of values, corresponding to `key = { … }` syntax.
47    ///
48    /// Array items may themselves be [`Value::Scalar`]s or
49    /// [`Value::Object`]s (the latter written as `{ key = val … }` inside the
50    /// outer braces).
51    Array(Vec<Value>),
52
53    /// An ordered map from string keys to values, corresponding to either a
54    /// `key { … }` block or the implicit top-level document object.
55    ///
56    /// Key insertion order is preserved via [`IndexMap`].
57    Object(IndexMap<String, Value>),
58}
59
60// ---------------------------------------------------------------------------
61// Public entry point
62// ---------------------------------------------------------------------------
63
64/// Parse a structprop document from `input` and return the top-level
65/// [`Value::Object`].
66///
67/// # Errors
68///
69/// Returns [`Error::Parse`] if the input contains unexpected tokens or
70/// violates the structprop grammar.
71///
72/// # Examples
73///
74/// ```
75/// use serde_structprop::parse::{parse, Value};
76///
77/// let v = parse("port = 8080\n").unwrap();
78/// if let Value::Object(map) = v {
79///     assert_eq!(map["port"].as_i64(), Some(8080));
80/// }
81/// ```
82pub fn parse(input: &str) -> Result<Value> {
83    let tokens = tokenize(input);
84    let mut pos = 0usize;
85    let map = parse_object(&tokens, &mut pos, /*top_level=*/ true)?;
86    Ok(Value::Object(map))
87}
88
89// ---------------------------------------------------------------------------
90// Internal parser helpers
91// ---------------------------------------------------------------------------
92
93/// Return a reference to the token at `pos` without advancing, defaulting to
94/// [`Token::Eof`] when `pos` is out of bounds.
95fn peek(tokens: &[Token], pos: usize) -> &Token {
96    tokens.get(pos).unwrap_or(&Token::Eof)
97}
98
99/// Advance the position cursor by one.
100fn advance(pos: &mut usize) {
101    *pos += 1;
102}
103
104/// Consume the next token, asserting it is a [`Token::Term`], and return its
105/// string value.
106///
107/// # Errors
108///
109/// Returns [`Error::Parse`] if the next token is not a term.
110fn expect_term(tokens: &[Token], pos: &mut usize) -> Result<String> {
111    match tokens.get(*pos) {
112        Some(Token::Term(s)) => {
113            let s = s.clone();
114            advance(pos);
115            Ok(s)
116        }
117        other => Err(Error::Parse(format!("expected term, got {other:?}"))),
118    }
119}
120
121/// Parse a sequence of assignments into an [`IndexMap`].
122///
123/// * If `top_level` is `true`, parsing stops at [`Token::Eof`].
124/// * If `top_level` is `false`, parsing stops at `}` (which is consumed).
125///
126/// # Errors
127///
128/// Returns [`Error::Parse`] on malformed input.
129fn parse_object(
130    tokens: &[Token],
131    pos: &mut usize,
132    top_level: bool,
133) -> Result<IndexMap<String, Value>> {
134    let mut map = IndexMap::new();
135
136    loop {
137        match peek(tokens, *pos) {
138            Token::Eof => {
139                if top_level {
140                    break;
141                }
142                return Err(Error::Parse("unexpected EOF inside object".to_owned()));
143            }
144            Token::Close => {
145                if top_level {
146                    return Err(Error::Parse("unexpected '}'".to_owned()));
147                }
148                advance(pos); // consume '}'
149                break;
150            }
151            Token::Term(_) => {
152                let key = expect_term(tokens, pos)?;
153                match peek(tokens, *pos) {
154                    Token::Eq => {
155                        advance(pos); // consume '='
156                        let val = parse_value(tokens, pos)?;
157                        if map.contains_key(&key) {
158                            return Err(Error::Parse(format!("duplicate key '{key}'")));
159                        }
160                        map.insert(key, val);
161                    }
162                    Token::Open => {
163                        advance(pos); // consume '{'
164                        let sub = parse_object(tokens, pos, /*top_level=*/ false)?;
165                        if map.contains_key(&key) {
166                            return Err(Error::Parse(format!("duplicate key '{key}'")));
167                        }
168                        map.insert(key, Value::Object(sub));
169                    }
170                    other => {
171                        return Err(Error::Parse(format!(
172                            "expected '=' or '{{' after key '{key}', got {other:?}"
173                        )));
174                    }
175                }
176            }
177            other => {
178                return Err(Error::Parse(format!("unexpected token {other:?}")));
179            }
180        }
181    }
182
183    Ok(map)
184}
185
186/// Parse a single value: either a scalar term or a `{ … }` block.
187///
188/// # Errors
189///
190/// Returns [`Error::Parse`] on unexpected tokens.
191fn parse_value(tokens: &[Token], pos: &mut usize) -> Result<Value> {
192    match peek(tokens, *pos) {
193        Token::Open => {
194            advance(pos); // consume '{'
195            parse_array_or_object_list(tokens, pos)
196        }
197        Token::Term(_) => {
198            let s = expect_term(tokens, pos)?;
199            Ok(Value::Scalar(s))
200        }
201        other => Err(Error::Parse(format!("expected value, got {other:?}"))),
202    }
203}
204
205/// Parse the body of a `{ … }` block that follows `=`.
206///
207/// The block may contain:
208/// - A list of scalar terms → [`Value::Array`] of [`Value::Scalar`]s.
209/// - A list of `{ … }` sub-objects → [`Value::Array`] of [`Value::Object`]s.
210/// - A mix of both.
211///
212/// # Errors
213///
214/// Returns [`Error::Parse`] on unexpected tokens or premature EOF.
215fn parse_array_or_object_list(tokens: &[Token], pos: &mut usize) -> Result<Value> {
216    let mut items: Vec<Value> = Vec::new();
217
218    loop {
219        match peek(tokens, *pos) {
220            Token::Close => {
221                advance(pos); // consume '}'
222                break;
223            }
224            Token::Eof => {
225                return Err(Error::Parse("unexpected EOF inside array".to_owned()));
226            }
227            Token::Open => {
228                // A nested object literal inside an array: { key = val … }
229                advance(pos); // consume '{'
230                let sub = parse_object(tokens, pos, /*top_level=*/ false)?;
231                items.push(Value::Object(sub));
232            }
233            Token::Term(_) => {
234                let s = expect_term(tokens, pos)?;
235                items.push(Value::Scalar(s));
236            }
237            other @ Token::Eq => {
238                return Err(Error::Parse(format!(
239                    "unexpected token in array: {other:?}"
240                )));
241            }
242        }
243    }
244
245    Ok(Value::Array(items))
246}
247
248// ---------------------------------------------------------------------------
249// Scalar coercion helpers
250// ---------------------------------------------------------------------------
251
252impl Value {
253    /// Try to interpret this [`Value::Scalar`] as a `bool`.
254    ///
255    /// Returns `Some(true)` for the literal string `"true"`, `Some(false)` for
256    /// `"false"`, and `None` for any other value or non-scalar variant.
257    ///
258    /// This mirrors the Python implementation's `json.loads` coercion.
259    #[must_use]
260    pub fn as_bool(&self) -> Option<bool> {
261        if let Value::Scalar(s) = self {
262            match s.as_str() {
263                "true" => Some(true),
264                "false" => Some(false),
265                _ => None,
266            }
267        } else {
268            None
269        }
270    }
271
272    /// Try to interpret this [`Value::Scalar`] as an `i64`.
273    ///
274    /// Returns `Some(n)` if the string parses as a signed 64-bit integer, or
275    /// `None` otherwise.
276    #[must_use]
277    pub fn as_i64(&self) -> Option<i64> {
278        if let Value::Scalar(s) = self {
279            s.parse().ok()
280        } else {
281            None
282        }
283    }
284
285    /// Try to interpret this [`Value::Scalar`] as an `f64`.
286    ///
287    /// Returns `Some(n)` if the string parses as a 64-bit float, or `None`
288    /// otherwise.
289    #[must_use]
290    pub fn as_f64(&self) -> Option<f64> {
291        if let Value::Scalar(s) = self {
292            s.parse().ok()
293        } else {
294            None
295        }
296    }
297
298    /// Returns `true` if this value is the scalar string `"null"`.
299    ///
300    /// Used by the deserializer to map structprop's `null` token to
301    /// [`Option::None`].
302    #[must_use]
303    pub fn is_null(&self) -> bool {
304        matches!(self, Value::Scalar(s) if s == "null")
305    }
306
307    /// Returns a short human-readable name for the variant, used in error
308    /// messages.
309    #[must_use]
310    pub fn type_name(&self) -> &'static str {
311        match self {
312            Value::Scalar(_) => "scalar",
313            Value::Array(_) => "array",
314            Value::Object(_) => "object",
315        }
316    }
317}
318
319// ---------------------------------------------------------------------------
320// Tests
321// ---------------------------------------------------------------------------
322
323#[cfg(test)]
324mod tests {
325    use super::*;
326
327    #[test]
328    fn simple_kv() {
329        let v = parse("key = value\n").unwrap();
330        if let Value::Object(map) = v {
331            assert_eq!(map["key"], Value::Scalar("value".into()));
332        } else {
333            panic!("expected object");
334        }
335    }
336
337    #[test]
338    fn nested_object() {
339        let input = "db {\n  host = localhost\n  port = 5432\n}\n";
340        let v = parse(input).unwrap();
341        if let Value::Object(map) = v {
342            if let Value::Object(db) = &map["db"] {
343                assert_eq!(db["host"], Value::Scalar("localhost".into()));
344                assert_eq!(db["port"], Value::Scalar("5432".into()));
345            } else {
346                panic!("expected nested object");
347            }
348        } else {
349            panic!("expected object");
350        }
351    }
352
353    #[test]
354    fn array_of_scalars() {
355        let input = "tables = { Table1 Table2 }\n";
356        let v = parse(input).unwrap();
357        if let Value::Object(map) = v {
358            assert_eq!(
359                map["tables"],
360                Value::Array(vec![
361                    Value::Scalar("Table1".into()),
362                    Value::Scalar("Table2".into()),
363                ])
364            );
365        } else {
366            panic!("expected object");
367        }
368    }
369
370    #[test]
371    fn number_scalar() {
372        let v = parse("port = 8080\n").unwrap();
373        if let Value::Object(map) = v {
374            assert_eq!(map["port"].as_i64(), Some(8080));
375        }
376    }
377
378    #[test]
379    fn bool_scalar() {
380        let v = parse("enabled = true\n").unwrap();
381        if let Value::Object(map) = v {
382            assert_eq!(map["enabled"].as_bool(), Some(true));
383        }
384    }
385}