diff --git a/Cargo.toml b/Cargo.toml index 34a2383..31cd07a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,9 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +testutil = [] + [dependencies] arbitrary = "1.2.0" proptest = "1.0.0" @@ -12,6 +15,8 @@ proptest = "1.0.0" [dev-dependencies] criterion = "0.4.0" rstest = "0.16.0" +# see https://github.com/rust-lang/cargo/issues/2911#issuecomment-749580481 +parser-combinator = { path = ".", features = ["testutil"] } [[bench]] name = "json-benchmark" diff --git a/src/lib.rs b/src/lib.rs index acb73d8..bdaf89c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,4 +4,7 @@ mod parser; pub mod primitives; pub mod sequence; +#[cfg(feature = "testutil")] +pub mod testutil; + pub use parser::{ParseResult, Parser, ParserInput, Representation}; diff --git a/src/testutil/mod.rs b/src/testutil/mod.rs new file mode 100644 index 0000000..f75a70b --- /dev/null +++ b/src/testutil/mod.rs @@ -0,0 +1,135 @@ +use crate::choice::choice; +use crate::combinators::repeated; +use crate::primitives::{any_char, literal, literal_char, one_of, pred}; +use crate::sequence::seq; +use crate::Parser; + +/* + * JSON BNF + * ::= + ::= | | | | | + ::= "[" [] {"," }* "]" + ::= "{" [] {"," }* "}" + ::= ":" +*/ +#[derive(Debug, Clone, PartialEq)] +pub enum JsonValue { + Null, + Bool(bool), + Str(String), + Num(f64), + Array(Vec), + Object(Vec<(String, JsonValue)>), +} + +pub trait JsonParser<'a, T>: Parser<&'a str, T, &'a str> {} +impl<'a, T, P> JsonParser<'a, T> for P where P: Parser<&'a str, T, &'a str> {} + +pub fn json_null<'a>() -> impl JsonParser<'a, JsonValue> { + literal("null").to(JsonValue::Null) +} + +pub fn json_bool<'a>() -> impl JsonParser<'a, JsonValue> { + choice(( + literal("true").to(JsonValue::Bool(true)), + literal("false").to(JsonValue::Bool(false)), + )) +} + +pub fn json_number<'a>() -> impl JsonParser<'a, JsonValue> { + fn digit<'a>() -> impl JsonParser<'a, &'a str> { + one_of("1234567890") + } + + fn digits<'a>() -> impl JsonParser<'a, Vec<&'a str>> { + repeated(digit()).at_least(1) + } + + let json_number_inner = choice(( + seq((digits(), literal(".").ignore_then(digits()).optional())).map( + |(mut digits, maybe_decimal)| { + if let Some(decimal_digits) = maybe_decimal { + digits.push("."); + digits.extend(decimal_digits.into_iter()); + } + digits.into_iter().collect::() + }, + ), + literal(".").ignore_then(digits()).map(|decimal_digits| { + let mut d = vec!["."]; + d.extend(decimal_digits.into_iter()); + d.into_iter().collect::() + }), + )) + .map(|digits| digits.parse::().unwrap()); + + literal("-") + .optional() + .then(json_number_inner) + .map(|(maybe_sign, mut val)| { + if maybe_sign.is_some() { + val *= -1.0; + } + JsonValue::Num(val) + }) +} + +pub fn json_string_raw<'a>() -> impl JsonParser<'a, String> { + seq(( + literal_char('"'), + repeated(pred(any_char, |ch| *ch != '"')), + literal_char('"'), + )) + .map(|(_, s, _)| s.iter().cloned().collect::()) +} + +pub fn json_string<'a>() -> impl JsonParser<'a, JsonValue> { + json_string_raw().map(JsonValue::Str) +} + +fn whitespace<'a>() -> impl JsonParser<'a, ()> { + repeated(choice(( + literal_char('\t'), + literal_char('\n'), + literal_char(' '), + ))) + .to(()) +} + +pub fn json_array<'a>() -> impl JsonParser<'a, JsonValue> { + move |input| { + let val = json_value().surrounded_by(whitespace()); + + repeated(val) + .separated_by(literal(","), false) + .delimited(literal_char('['), literal_char(']')) + .map(JsonValue::Array) + .parse(input) + } +} + +pub fn json_object<'a>() -> impl JsonParser<'a, JsonValue> { + move |input| { + let kv = json_string_raw() + .surrounded_by(whitespace()) + .then_ignore(literal_char(':')) + .then(json_value().surrounded_by(whitespace())); + + repeated(kv) + .separated_by(literal_char(','), false) + .delimited(literal_char('{'), literal_char('}')) + .map(JsonValue::Object) + .parse(input) + } +} + +pub fn json_value<'a>() -> impl JsonParser<'a, JsonValue> { + choice(( + json_null(), + json_bool(), + json_number(), + json_string(), + json_array(), + json_object(), + )) +} diff --git a/tests/json_parser.rs b/tests/json_parser.rs index 2b515d5..cb52c94 100644 --- a/tests/json_parser.rs +++ b/tests/json_parser.rs @@ -1,9 +1,8 @@ -use parser_combinator::choice::choice; -use parser_combinator::combinators::repeated; -use parser_combinator::primitives::{any_char, literal, literal_char, one_of, pred}; -use parser_combinator::sequence::seq; -use parser_combinator::Parser; -use parser_combinator::Representation; +use parser_combinator::primitives::literal; +use parser_combinator::testutil::{ + json_array, json_bool, json_null, json_number, json_object, json_string, JsonValue, +}; +use parser_combinator::{Parser, Representation}; use proptest::prelude::*; @@ -32,136 +31,6 @@ fn test_parsing() { assert_eq!(output.unwrap(), ("a", " yolo")); } -/* - * JSON BNF - * ::= - ::= | | | | | - ::= "[" [] {"," }* "]" - ::= "{" [] {"," }* "}" - ::= ":" -*/ -#[derive(Debug, Clone, PartialEq)] -enum JsonValue { - Null, - Bool(bool), - Str(String), - Num(f64), - Array(Vec), - Object(Vec<(String, JsonValue)>), -} - -trait JsonParser<'a, T>: Parser<&'a str, T, &'a str> {} -impl<'a, T, P> JsonParser<'a, T> for P where P: Parser<&'a str, T, &'a str> {} - -fn json_null<'a>() -> impl JsonParser<'a, JsonValue> { - literal("null").to(JsonValue::Null) -} - -fn json_bool<'a>() -> impl JsonParser<'a, JsonValue> { - choice(( - literal("true").to(JsonValue::Bool(true)), - literal("false").to(JsonValue::Bool(false)), - )) -} - -fn json_number<'a>() -> impl JsonParser<'a, JsonValue> { - fn digit<'a>() -> impl JsonParser<'a, &'a str> { - one_of("1234567890") - } - - fn digits<'a>() -> impl JsonParser<'a, Vec<&'a str>> { - repeated(digit()).at_least(1) - } - - let json_number_inner = choice(( - seq((digits(), literal(".").ignore_then(digits()).optional())).map( - |(mut digits, maybe_decimal)| { - if let Some(decimal_digits) = maybe_decimal { - digits.push("."); - digits.extend(decimal_digits.into_iter()); - } - digits.into_iter().collect::() - }, - ), - literal(".").ignore_then(digits()).map(|decimal_digits| { - let mut d = vec!["."]; - d.extend(decimal_digits.into_iter()); - d.into_iter().collect::() - }), - )) - .map(|digits| digits.parse::().unwrap()); - - literal("-") - .optional() - .then(json_number_inner) - .map(|(maybe_sign, mut val)| { - if maybe_sign.is_some() { - val *= -1.0; - } - JsonValue::Num(val) - }) -} - -fn json_string_raw<'a>() -> impl JsonParser<'a, String> { - seq(( - literal_char('"'), - repeated(pred(any_char, |ch| *ch != '"')), - literal_char('"'), - )) - .map(|(_, s, _)| s.iter().cloned().collect::()) -} - -fn json_string<'a>() -> impl JsonParser<'a, JsonValue> { - json_string_raw().map(JsonValue::Str) -} - -fn whitespace<'a>() -> impl JsonParser<'a, ()> { - repeated(choice(( - literal_char('\t'), - literal_char('\n'), - literal_char(' '), - ))) - .to(()) -} - -fn json_array<'a>() -> impl JsonParser<'a, JsonValue> { - move |input| { - let val = json_value().surrounded_by(whitespace()); - - repeated(val) - .separated_by(literal(","), false) - .delimited(literal_char('['), literal_char(']')) - .map(JsonValue::Array) - .parse(input) - } -} - -fn json_object<'a>() -> impl JsonParser<'a, JsonValue> { - move |input| { - let kv = json_string_raw() - .surrounded_by(whitespace()) - .then_ignore(literal_char(':')) - .then(json_value().surrounded_by(whitespace())); - - repeated(kv) - .separated_by(literal_char(','), false) - .delimited(literal_char('{'), literal_char('}')) - .map(JsonValue::Object) - .parse(input) - } -} - -fn json_value<'a>() -> impl JsonParser<'a, JsonValue> { - choice(( - json_null(), - json_bool(), - json_number(), - json_string(), - json_array(), - json_object(), - )) -} - #[test] fn parse_json_primitives() { assert_eq!(