use crate::error::{Error, Result};
use crate::lexer::{tokenize, Token};
use indexmap::IndexMap;
#[derive(Debug, Clone, PartialEq)]
pub enum Value {
Scalar(String),
Array(Vec<Value>),
Object(IndexMap<String, Value>),
}
pub fn parse(input: &str) -> Result<Value> {
let tokens = tokenize(input)?;
let mut pos = 0usize;
let map = parse_object(&tokens, &mut pos, true)?;
Ok(Value::Object(map))
}
fn peek(tokens: &[(Token, u32)], pos: usize) -> &Token {
tokens.get(pos).map_or(&Token::Eof, |(tok, _)| tok)
}
fn token_display(tok: Option<&Token>) -> String {
match tok {
Some(Token::Term(s)) => format!("'{s}'"),
Some(Token::Eq) => "'='".to_owned(),
Some(Token::Open) => "'{{'".to_owned(),
Some(Token::Close) => "'}}'".to_owned(),
Some(Token::Eof) | None => "end of input".to_owned(),
}
}
fn line_at(tokens: &[(Token, u32)], pos: usize) -> u32 {
tokens.get(pos).map_or(0, |&(_, line)| line)
}
fn advance(pos: &mut usize) {
*pos += 1;
}
fn expect_term(tokens: &[(Token, u32)], pos: &mut usize) -> Result<String> {
let line = line_at(tokens, *pos);
match tokens.get(*pos) {
Some((Token::Term(s), _)) => {
let s = s.clone();
advance(pos);
Ok(s)
}
other => {
let tok = other.map(|(t, _)| t);
Err(Error::Parse(format!(
"line {line}: expected a key or value, got {}",
token_display(tok)
)))
}
}
}
fn parse_object(
tokens: &[(Token, u32)],
pos: &mut usize,
top_level: bool,
) -> Result<IndexMap<String, Value>> {
let mut map = IndexMap::new();
loop {
let line = line_at(tokens, *pos);
match peek(tokens, *pos) {
Token::Eof => {
if top_level {
break;
}
return Err(Error::Parse(format!(
"line {line}: unexpected EOF inside object"
)));
}
Token::Close => {
if top_level {
return Err(Error::Parse(format!("line {line}: unexpected '}}'")));
}
advance(pos); break;
}
Token::Term(_) => {
let key = expect_term(tokens, pos)?;
let after_line = line_at(tokens, *pos);
match peek(tokens, *pos) {
Token::Eq => {
advance(pos); let val = parse_value(tokens, pos)?;
if map.contains_key(&key) {
return Err(Error::Parse(format!(
"line {after_line}: duplicate key '{key}'"
)));
}
map.insert(key, val);
}
Token::Open => {
advance(pos); let sub = parse_object(tokens, pos, false)?;
if map.contains_key(&key) {
return Err(Error::Parse(format!(
"line {after_line}: duplicate key '{key}'"
)));
}
map.insert(key, Value::Object(sub));
}
other => {
return Err(Error::Parse(format!(
"line {after_line}: expected '=' or '{{' after key '{key}', got {}",
token_display(Some(other))
)));
}
}
}
other => {
return Err(Error::Parse(format!(
"line {line}: unexpected {}",
token_display(Some(other))
)));
}
}
}
Ok(map)
}
fn parse_value(tokens: &[(Token, u32)], pos: &mut usize) -> Result<Value> {
let line = line_at(tokens, *pos);
match peek(tokens, *pos) {
Token::Open => {
advance(pos); parse_array_or_object_list(tokens, pos)
}
Token::Term(_) => {
let s = expect_term(tokens, pos)?;
Ok(Value::Scalar(s))
}
other => Err(Error::Parse(format!(
"line {line}: expected a value, got {}",
token_display(Some(other))
))),
}
}
fn parse_array_or_object_list(tokens: &[(Token, u32)], pos: &mut usize) -> Result<Value> {
let mut items: Vec<Value> = Vec::new();
loop {
let line = line_at(tokens, *pos);
match peek(tokens, *pos) {
Token::Close => {
advance(pos); break;
}
Token::Eof => {
return Err(Error::Parse(format!(
"line {line}: unexpected EOF inside array"
)));
}
Token::Open => {
advance(pos); let sub = parse_object(tokens, pos, false)?;
items.push(Value::Object(sub));
}
Token::Term(_) => {
if matches!(tokens.get(*pos + 1), Some((Token::Eq, _))) {
let key = match tokens.get(*pos) {
Some((Token::Term(s), _)) => s.clone(),
_ => "?".to_owned(),
};
return Err(Error::Parse(format!(
"line {line}: '{key} = ...' is not valid inside an array; \
wrap it in braces for a nested object: '{{ {key} = ... }}'"
)));
}
let s = expect_term(tokens, pos)?;
items.push(Value::Scalar(s));
}
Token::Eq => {
return Err(Error::Parse(format!(
"line {line}: unexpected '=' inside array"
)));
}
}
}
Ok(Value::Array(items))
}
impl Value {
#[must_use]
pub fn as_bool(&self) -> Option<bool> {
if let Value::Scalar(s) = self {
match s.as_str() {
"true" => Some(true),
"false" => Some(false),
_ => None,
}
} else {
None
}
}
#[must_use]
pub fn as_i64(&self) -> Option<i64> {
if let Value::Scalar(s) = self {
s.parse().ok()
} else {
None
}
}
#[must_use]
pub fn as_f64(&self) -> Option<f64> {
if let Value::Scalar(s) = self {
s.parse().ok()
} else {
None
}
}
#[must_use]
pub fn is_null(&self) -> bool {
matches!(self, Value::Scalar(s) if s == "null")
}
#[must_use]
pub fn as_str(&self) -> Option<&str> {
if let Value::Scalar(s) = self {
Some(s)
} else {
None
}
}
#[must_use]
pub fn type_name(&self) -> &'static str {
match self {
Value::Scalar(_) => "scalar",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn simple_kv() {
let v = parse("key = value\n").unwrap();
if let Value::Object(map) = v {
assert_eq!(map["key"], Value::Scalar("value".into()));
} else {
panic!("expected object");
}
}
#[test]
fn nested_object() {
let input = "db {\n host = localhost\n port = 5432\n}\n";
let v = parse(input).unwrap();
if let Value::Object(map) = v {
if let Value::Object(db) = &map["db"] {
assert_eq!(db["host"], Value::Scalar("localhost".into()));
assert_eq!(db["port"], Value::Scalar("5432".into()));
} else {
panic!("expected nested object");
}
} else {
panic!("expected object");
}
}
#[test]
fn array_of_scalars() {
let input = "tables = { Table1 Table2 }\n";
let v = parse(input).unwrap();
if let Value::Object(map) = v {
assert_eq!(
map["tables"],
Value::Array(vec![
Value::Scalar("Table1".into()),
Value::Scalar("Table2".into()),
])
);
} else {
panic!("expected object");
}
}
#[test]
fn number_scalar() {
let v = parse("port = 8080\n").unwrap();
if let Value::Object(map) = v {
assert_eq!(map["port"].as_i64(), Some(8080));
}
}
#[test]
fn bool_scalar() {
let v = parse("enabled = true\n").unwrap();
if let Value::Object(map) = v {
assert_eq!(map["enabled"].as_bool(), Some(true));
}
}
#[test]
fn error_includes_line_number() {
let input = "good = ok\nbad = {\n";
let err = parse(input).unwrap_err().to_string();
assert!(
err.contains("line "),
"expected a line number in error: {err}"
);
}
#[test]
fn kv_inside_array_suggests_fix() {
let input = "list = {\n subkey = nested\n}\n";
let err = parse(input).unwrap_err().to_string();
assert!(
err.contains("'subkey = ...' is not valid inside an array"),
"expected actionable hint in error: {err}"
);
assert!(
err.contains("{ subkey = ... }"),
"expected brace-wrap hint in error: {err}"
);
}
#[test]
fn token_display_uses_human_readable_names() {
let input = "= value\n";
let err = parse(input).unwrap_err().to_string();
assert!(
err.contains("'='") || err.contains("end of input"),
"error should use human-readable token names: {err}"
);
}
}