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