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}