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}