diff --git a/src/error.rs b/src/error.rs index dfa3de3..47a539e 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,7 +1,7 @@ use nom::error::{ContextError, ParseError}; use thiserror::Error; -#[derive(Debug, Clone, Error)] +#[derive(Debug, Clone, Eq, PartialEq, Error)] #[error("Error parsing document. {kind}")] pub struct Error { pub input: String, @@ -9,7 +9,7 @@ pub struct Error { pub kind: ErrorKind, } -#[derive(Debug, Clone, Error)] +#[derive(Debug, Clone, Eq, PartialEq, Error)] pub enum ErrorKind { #[error("Failed to parse {0} component of semver string.")] Context(&'static str), @@ -19,7 +19,7 @@ pub enum ErrorKind { Other, } -#[derive(Debug)] +#[derive(Debug, Clone, Eq, PartialEq)] pub(crate) struct NodeParseError { pub(crate) input: I, pub(crate) context: Option<&'static str>, diff --git a/src/node.rs b/src/node.rs index 8e6d80b..d868e9c 100644 --- a/src/node.rs +++ b/src/node.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub struct Node { pub name: String, pub values: Vec, @@ -8,5 +8,11 @@ pub struct Node { pub children: Vec, } -#[derive(Debug, Clone)] -pub enum NodeValue {} +#[derive(Debug, Clone, PartialEq)] +pub enum NodeValue { + Int(i64), + Float(f64), + String(String), + Boolean(bool), + Null, +} diff --git a/src/parser.rs b/src/parser.rs index 58df343..3788920 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,14 +1,14 @@ use std::collections::HashMap; use nom::branch::alt; -use nom::bytes::complete::tag; -use nom::character::complete::{alpha1, alphanumeric1, one_of}; -use nom::combinator::{map, not, opt, recognize}; +use nom::bytes::complete::{is_not, tag, take_until}; +use nom::character::complete::{alpha1, alphanumeric1, anychar, one_of}; +use nom::combinator::{eof, map, opt, recognize, value}; use nom::multi::{many0, many1}; use nom::sequence::{delimited, pair, preceded}; use nom::IResult; -use crate::error::{ErrorKind, NodeParseError}; +use crate::error::NodeParseError; use crate::node::{Node, NodeValue}; /// `document := linespace* (node (newline document)?)?` @@ -71,19 +71,72 @@ fn identifier(input: &str) -> IResult<&str, &str, NodeParseError<&str>> { } fn node_arg(input: &str) -> IResult<&str, NodeArg, NodeParseError<&str>> { - todo!() + alt(( + map(property, |(key, val)| NodeArg::Property(key, val)), + map(node_value, NodeArg::Value), + ))(input) } -fn node_children(input: &str) -> IResult<&str, Vec, NodeParseError<&str>> { +/// `prop := identifier '=' value` +fn property(input: &str) -> IResult<&str, (&str, NodeValue), NodeParseError<&str>> { + let (input, key) = identifier(input)?; + let (input, _) = tag("=")(input)?; + let (input, val) = node_value(input)?; + Ok((input, (key, val))) +} + +/// `value := string | raw_string | number | boolean | 'null'` +fn node_value(input: &str) -> IResult<&str, NodeValue, NodeParseError<&str>> { + alt(( + map(string, |s| NodeValue::String(s.into())), + map(raw_string, |s| NodeValue::String(s.into())), + number, + boolean, + value(NodeValue::Null, tag("null")), + ))(input) +} + +fn node_children(_input: &str) -> IResult<&str, Vec, NodeParseError<&str>> { todo!() } // TODO: This should be much more specific about what escapes are allowed. /// `string := '"' ('\\' ["\\] | [^"])* '"'` -fn string(input: &str) -> IResult<&str, &str, NodeParseError<&str>> { +fn string(_input: &str) -> IResult<&str, &str, NodeParseError<&str>> { todo!() } +/// `raw-string := 'r' raw-string-hash` +fn raw_string(input: &str) -> IResult<&str, &str, NodeParseError<&str>> { + preceded(tag("r"), raw_string_hash)(input) +} + +/// `raw-string-hash := '#' raw-string-hash '#' | raw-string-quotes` +fn raw_string_hash(input: &str) -> IResult<&str, &str, NodeParseError<&str>> { + alt(( + delimited(tag("#"), raw_string_hash, tag("#")), + raw_string_quotes, + ))(input) +} + +/// `raw-string-quotes := '"' .* '"'` +fn raw_string_quotes(input: &str) -> IResult<&str, &str, NodeParseError<&str>> { + delimited(tag("\""), recognize(many0(anychar)), tag("\""))(input) +} + +/// `number := decimal | hex | octal | binary` +fn number(_input: &str) -> IResult<&str, NodeValue, NodeParseError<&str>> { + todo!() +} + +/// `boolean := 'true' | 'false'` +fn boolean(input: &str) -> IResult<&str, NodeValue, NodeParseError<&str>> { + alt(( + value(NodeValue::Boolean(true), tag("true")), + value(NodeValue::Boolean(false), tag("false")), + ))(input) +} + /// `node-space := ws* escline ws* | ws+` fn node_space(input: &str) -> IResult<&str, (), NodeParseError<&str>> { alt(( @@ -92,37 +145,107 @@ fn node_space(input: &str) -> IResult<&str, (), NodeParseError<&str>> { ))(input) } -/// `single-line-comment := '//' ('\r' [^\n] | [^\r\n])* newline` -fn single_line_comment(input: &str) -> IResult<&str, &str, NodeParseError<&str>> { +/// `single-line-comment := '//' ('\r' [^\n] | [^\r\n])* (newline | eof)` +fn single_line_comment(input: &str) -> IResult<&str, (), NodeParseError<&str>> { let (input, _) = tag("//")(input)?; - let (input, comment) = recognize(many0(alt(( - preceded(tag("\r"), not(tag("\n"))), - not(tag("\r\n")), - ))))(input)?; - let (input, _) = newline(input)?; - Ok((input, comment)) + let (input, _) = alt((take_until("\r\n"), is_not("\n")))(input)?; + let (input, _) = alt((newline, eof))(input)?; + Ok((input, ())) } /// `multi-line-comment := '/*' ('*' [^\/] | [^*])* '*/'` -fn multi_line_comment(input: &str) -> IResult<&str, &str, NodeParseError<&str>> { - delimited(tag("/*"), recognize(many0(not(tag("*/")))), tag("*/"))(input) +fn multi_line_comment(input: &str) -> IResult<&str, (), NodeParseError<&str>> { + delimited(tag("/*"), value((), take_until("*/")), tag("*/"))(input) } /// `escline := '\\' ws* (single-line-comment | newline)` fn escline(input: &str) -> IResult<&str, (), NodeParseError<&str>> { let (input, _) = tag("\\")(input)?; let (input, _) = many0(whitespace)(input)?; - let (input, _) = alt((single_line_comment, newline))(input)?; + let (input, _) = alt((recognize(single_line_comment), newline))(input)?; Ok((input, ())) } /// `ws := bom | ' ' | '\t' | multi-line-comment` fn whitespace(input: &str) -> IResult<&str, &str, NodeParseError<&str>> { // TODO: bom? - alt((/*bom,*/ tag(" "), tag("\t"), multi_line_comment))(input) + alt(( + /*bom,*/ tag(" "), + tag("\t"), + recognize(multi_line_comment), + ))(input) } /// `newline := ('\r' '\n') | '\n'` fn newline(input: &str) -> IResult<&str, &str, NodeParseError<&str>> { alt((tag("\r\n"), tag("\n")))(input) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_boolean() { + assert_eq!(boolean("true"), Ok(("", NodeValue::Boolean(true)))); + assert_eq!(boolean("false"), Ok(("", NodeValue::Boolean(false)))); + assert!(boolean("blah").is_err()); + } + + #[test] + fn test_node_space() { + assert_eq!(node_space(" "), Ok(("", ()))); + assert_eq!(node_space("\t "), Ok(("", ()))); + assert_eq!(node_space("\t \\ // hello\n "), Ok(("", ()))); + assert!(node_space("blah").is_err()); + } + + #[test] + fn test_single_line_comment() { + assert_eq!(single_line_comment("//hello"), Ok(("", ()))); + assert_eq!(single_line_comment("// \thello"), Ok(("", ()))); + assert_eq!(single_line_comment("//hello\n"), Ok(("", ()))); + assert_eq!(single_line_comment("//hello\r\n"), Ok(("", ()))); + assert_eq!(single_line_comment("//hello\n\r"), Ok(("\r", ()))); + assert_eq!(single_line_comment("//hello\rworld"), Ok(("", ()))); + } + + #[test] + fn test_multi_line_comment() { + assert_eq!(multi_line_comment("/*hello*/"), Ok(("", ()))); + assert_eq!(multi_line_comment("/*hello*/\n"), Ok(("\n", ()))); + assert_eq!(multi_line_comment("/*\nhello\r\n*/"), Ok(("", ()))); + assert_eq!(multi_line_comment("/*\nhello** /\n*/"), Ok(("", ()))); + assert_eq!(multi_line_comment("/**\nhello** /\n*/"), Ok(("", ()))); + assert_eq!(multi_line_comment("/*hello*/world"), Ok(("world", ()))); + } + + #[test] + fn test_escline() { + assert_eq!(escline("\\\nfoo"), Ok(("foo", ()))); + assert_eq!(escline("\\\n foo"), Ok((" foo", ()))); + assert_eq!(escline("\\ \t \nfoo"), Ok(("foo", ()))); + assert_eq!(escline("\\ // test \nfoo"), Ok(("foo", ()))); + assert_eq!(escline("\\ // test \n foo"), Ok((" foo", ()))); + } + + #[test] + fn test_whitespace() { + assert_eq!(whitespace(" "), Ok(("", " "))); + assert_eq!(whitespace("\t"), Ok(("", "\t"))); + assert_eq!( + whitespace("/* \nfoo\r\n */ etc"), + Ok((" etc", "/* \nfoo\r\n */")) + ); + assert!(whitespace("hi").is_err()) + } + + #[test] + fn test_newline() { + assert_eq!(newline("\n"), Ok(("", "\n"))); + assert_eq!(newline("\r\n"), Ok(("", "\r\n"))); + assert_eq!(newline("\n\n"), Ok(("\n", "\n"))); + assert!(newline("\r").is_err()); + assert!(newline("blah").is_err()); + } +}