Skip to content

Commit

Permalink
Add parser tests and fix bugs
Browse files Browse the repository at this point in the history
  • Loading branch information
jpikl committed Jul 9, 2023
1 parent 7bc08a5 commit 118a9f1
Showing 1 changed file with 148 additions and 15 deletions.
163 changes: 148 additions & 15 deletions src/commands/x/pattern.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use owo_colors::OwoColorize;
use std::fmt::Debug;
use std::fmt::Display;
use std::fmt::Formatter;
use std::iter::Fuse;
Expand All @@ -15,7 +16,7 @@ const SINGLE_QUOTE: char = '\'';
const DOUBLE_QUOTE: char = '"';

const ESCAPED_LF: char = 'n';
const ESCAPED_CR: char = 'c';
const ESCAPED_CR: char = 'r';
const ESCAPED_TAB: char = 't';

pub type Result<T> = std::result::Result<T, Error>;
Expand Down Expand Up @@ -54,16 +55,16 @@ impl Display for Error {
}
}

#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub struct Pattern(Vec<Item>);

#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub enum Item {
Constant(String),
Expression(Vec<Command>),
}

#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq)]
pub struct Command {
pub name: String,
pub args: Vec<String>,
Expand All @@ -79,6 +80,43 @@ impl Pattern {
}
}

impl Display for Pattern {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
for item in &self.0 {
write!(f, "{}", item)?;
}
Ok(())
}
}

impl Display for Item {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Constant(value) => write!(f, "{}", value),
Self::Expression(commands) => {
write!(f, "{{")?;
for (i, command) in commands.iter().enumerate() {
if i > 0 {
write!(f, "|")?;
}
write!(f, "{}", command)?;
}
write!(f, "}}")
}
}
}
}

impl Display for Command {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "`{}`", self.name)?;
for arg in &self.args {
write!(f, " `{}`", arg)?;
}
Ok(())
}
}

pub struct Parser<'a> {
input: String,
iterator: Peekable<Fuse<Chars<'a>>>,
Expand Down Expand Up @@ -137,7 +175,7 @@ impl Parser<'_> {
self.consume(EXPR_START);
self.consume_whitespaces();

let mut commands = Vec::new();
let mut command = Vec::new();
let mut command_expected = false;

while let Some(char) = self.peek() {
Expand All @@ -154,7 +192,7 @@ impl Parser<'_> {
}
}
_ => {
commands.push(self.parse_command()?);
command.push(self.parse_command()?);
command_expected = self.try_consume(PIPE);

if command_expected {
Expand All @@ -167,7 +205,7 @@ impl Parser<'_> {
if command_expected {
Err(self.error(ErrorKind::MissingCommandAfter, self.position))
} else if self.try_consume(EXPR_END) {
Ok(commands)
Ok(command)
} else {
Err(self.error(ErrorKind::MissingExpressionEnd, start_position))
}
Expand All @@ -191,11 +229,18 @@ impl Parser<'_> {
}

fn parse_arg(&mut self) -> Result<String> {
match self.peek() {
Some(quote @ (SINGLE_QUOTE | DOUBLE_QUOTE)) => self.parse_quoted_arg(quote),
Some(_) => self.parse_unquote_arg(),
None => unreachable!("unexpected EOF when parsing arg"),
let mut arg = String::new();

while let Some(char) = self.peek() {
match char {
EXPR_START | PIPE | EXPR_END => break,
char if char.is_whitespace() => break,
char @ (SINGLE_QUOTE | DOUBLE_QUOTE) => arg.push_str(&self.parse_quoted_arg(char)?),
_ => arg.push_str(&self.parse_unquote_arg()?),
}
}

Ok(arg)
}

fn parse_quoted_arg(&mut self, quote: char) -> Result<String> {
Expand All @@ -209,7 +254,7 @@ impl Parser<'_> {
self.consume(quote);
return Ok(arg);
} else if char == self.escape {
arg.push(self.parse_escape_sequence(|_| false));
arg.push(self.parse_escape_sequence(|char| char == quote));
} else {
arg.push(self.consume(char));
}
Expand All @@ -223,8 +268,7 @@ impl Parser<'_> {

while let Some(char) = self.peek() {
match char {
EXPR_START | PIPE | EXPR_END => break,
char @ (SINGLE_QUOTE | DOUBLE_QUOTE) => arg.push_str(&self.parse_quoted_arg(char)?),
EXPR_START | PIPE | EXPR_END | SINGLE_QUOTE | DOUBLE_QUOTE => break,
char if char.is_whitespace() => break,
char if char == self.escape => {
arg.push(self.parse_escape_sequence(|char| match char {
Expand All @@ -240,7 +284,7 @@ impl Parser<'_> {
Ok(arg)
}

fn parse_escape_sequence(&mut self, is_escapable: fn(char) -> bool) -> char {
fn parse_escape_sequence(&mut self, is_escapable: impl Fn(char) -> bool) -> char {
self.consume(self.escape);

match self.peek() {
Expand Down Expand Up @@ -304,3 +348,92 @@ impl Parser<'_> {
}
}
}

#[cfg(test)]
mod tests {
use super::*;
use claims::assert_ok;
use rstest::rstest;

#[rstest]
// Constants and empty expressions
#[case("", "")]
#[case("c1", "c1")]
#[case("{}", "{}")]
#[case("{ }", "{}")]
#[case("{}c1{}", "{}c1{}")]
#[case("c1{}c2{}c3", "c1{}c2{}c3")]
#[case(" c1 { } c2 { } c3 ", " c1 {} c2 {} c3 ")]
// Command with args
#[case("{n}", "{`n`}")]
#[case("{n a}", "{`n` `a`}")]
#[case("{n a b}", "{`n` `a` `b`}")]
#[case("{name}", "{`name`}")]
#[case("{name arg}", "{`name` `arg`}")]
#[case("{name arg1 arg2}", "{`name` `arg1` `arg2`}")]
// Pipelines
#[case("{n1|n2}", "{`n1`|`n2`}")]
#[case("{n1|n2|n3}", "{`n1`|`n2`|`n3`}")]
#[case("{n1|n2 a21|n3 a31 a32}", "{`n1`|`n2` `a21`|`n3` `a31` `a32`}")]
// All together
#[case(
"c1{}c2{n1}c3{n2 a21 a22}c4{n3|n4 a41|n5 a51 a52}c5",
"c1{}c2{`n1`}c3{`n2` `a21` `a22`}c4{`n3`|`n4` `a41`|`n5` `a51` `a52`}c5"
)]
#[case(
" c1 {} c2 { n1 } c3 { n2 a21 a22 } c4 { n3 | n4 a41 | n5 a51 a52 } c5 ",
" c1 {} c2 {`n1`} c3 {`n2` `a21` `a22`} c4 {`n3`|`n4` `a41`|`n5` `a51` `a52`} c5 ",
)]
// Escaped - General
#[case("%%", "%")]
#[case("%n", "\n")]
#[case("%r", "\r")]
#[case("%t", "\t")]
// Unescaped - General
#[case("%", "%")]
// Escaped - Outside expression
#[case("%}", "}")]
#[case("%{", "{")]
// Unescaped - Outside expression
#[case("%'", "%'")]
#[case("%\"", "%\"")]
#[case("%|", "%|")]
// Escaped - Unquoted args
#[case(r#"{a% b}"#, r#"{`a b`}"#)]
#[case(r#"{a%'b}"#, r#"{`a'b`}"#)]
#[case(r#"{a%"b}"#, r#"{`a"b`}"#)]
#[case(r#"{a%|b}"#, r#"{`a|b`}"#)]
#[case(r#"{a%{b}"#, r#"{`a{b`}"#)]
#[case(r#"{a%}b}"#, r#"{`a}b`}"#)]
// Unescaped - Unquoted args
#[case(r#"{a%xb}"#, r#"{`a%xb`}"#)]
// Escaped - Single quoted args
#[case(r#"{'a%'b'}"#, r#"{`a'b`}"#)]
// Unescaped - Single quoted args
#[case(r#"{'a% b'}"#, r#"{`a% b`}"#)]
#[case(r#"{'a%"b'}"#, r#"{`a%"b`}"#)]
#[case(r#"{'a%|b'}"#, r#"{`a%|b`}"#)]
#[case(r#"{'a%{b'}"#, r#"{`a%{b`}"#)]
#[case(r#"{'a%}b'}"#, r#"{`a%}b`}"#)]
#[case(r#"{'a%xb'}"#, r#"{`a%xb`}"#)]
// Escaped - Double quoted args
#[case(r#"{"a%"b"}"#, r#"{`a"b`}"#)]
// Unescaped - Double quoted args
#[case(r#"{"a% b"}"#, r#"{`a% b`}"#)]
#[case(r#"{"a%'b"}"#, r#"{`a%'b`}"#)]
#[case(r#"{"a%|b"}"#, r#"{`a%|b`}"#)]
#[case(r#"{"a%{b"}"#, r#"{`a%{b`}"#)]
#[case(r#"{"a%}b"}"#, r#"{`a%}b`}"#)]
#[case(r#"{"a%xb"}"#, r#"{`a%xb`}"#)]
// Consecutive quoted joined args
#[case(r#"{a'b'"c"}"#, r#"{`abc`}"#)]
#[case(r#"{a"c"'b'}"#, r#"{`acb`}"#)]
#[case(r#"{'b'a"c"}"#, r#"{`bac`}"#)]
#[case(r#"{'b'"c"a}"#, r#"{`bca`}"#)]
#[case(r#"{"c"a'b'}"#, r#"{`cab`}"#)]
#[case(r#"{"c"'b'a}"#, r#"{`cba`}"#)]
fn parse(#[case] input: &str, #[case] normalized: &str) {
let pattern = assert_ok!(Pattern::from_str_with_escape(input, '%'));
assert_eq!(pattern.to_string(), normalized);
}
}

0 comments on commit 118a9f1

Please sign in to comment.