use nom::{ branch::alt, bytes::complete::tag, character::complete::{ alphanumeric1, i8, multispace0, multispace1, newline, none_of, not_line_ending, space1, }, combinator::{all_consuming, map, opt, recognize, rest}, error::{context, convert_error, VerboseError}, multi::{many0, many1}, number::complete::float, sequence::{preceded, separated_pair, terminated}, Err as NomErr, IResult, Parser, }; #[derive(PartialEq, Debug)] pub enum Command { Exit, SetPos(f32, f32), GetPos, Move(Direction, i8), Stop, Park, SetConf(String, String), Reset, GetInfo, DumpState, DumpCaps, SendCmd(String), } #[derive(PartialEq, Debug)] pub enum Direction { UP, DOWN, CW, CCW, } fn exit(input: &str) -> IResult<&str, Command, VerboseError<&str>> { map(alt((tag("q"), tag("Q"))), |_| Command::Exit).parse(input) } fn float_pair(input: &str) -> IResult<&str, (f32, f32), VerboseError<&str>> { context("float_pair", separated_pair(float, multispace1, float)).parse(input) } fn set_pos(input: &str) -> IResult<&str, Command, VerboseError<&str>> { context( "set_pos", map( preceded( terminated(alt((tag("set_pos"), tag("P"))), multispace1), float_pair, ), |pair: (f32, f32)| Command::SetPos(pair.0, pair.1), ), ) .parse(input) } fn get_pos(input: &str) -> IResult<&str, Command, VerboseError<&str>> { map(alt((tag("get_pos"), tag("p"))), |_| Command::GetPos).parse(input) } fn direction(input: &str) -> IResult<&str, Direction, VerboseError<&str>> { context( "direction", alt(( map(alt((tag("UP"), tag("2"))), |_| Direction::UP), map(alt((tag("DOWN"), tag("4"))), |_| Direction::DOWN), map(alt((tag("CCW"), tag("LEFT"), tag("8"))), |_| Direction::CCW), map(alt((tag("CW"), tag("RIGHT"), tag("16"))), |_| Direction::CW), )), ) .parse(input) } fn move_parameters(input: &str) -> IResult<&str, (Direction, i8), VerboseError<&str>> { context( "move_parameters", separated_pair(direction, multispace1, i8), ) .parse(input) } fn move_command(input: &str) -> IResult<&str, Command, VerboseError<&str>> { context( "move", map( preceded( terminated(alt((tag("move"), tag("M"))), multispace1), move_parameters, ), |pair: (Direction, i8)| Command::Move(pair.0, pair.1), ), ) .parse(input) } fn stop(input: &str) -> IResult<&str, Command, VerboseError<&str>> { map(alt((tag("stop"), tag("S"))), |_| Command::Stop).parse(input) } fn park(input: &str) -> IResult<&str, Command, VerboseError<&str>> { map(alt((tag("park"), tag("K"))), |_| Command::Park).parse(input) } fn string_pair(input: &str) -> IResult<&str, (&str, &str), VerboseError<&str>> { // TODO: find out if alphanumeric1 is enough. Might need -_ and others context( "string_pair", separated_pair(alphanumeric1, multispace1, alphanumeric1), ) .parse(input) } fn set_conf(input: &str) -> IResult<&str, Command, VerboseError<&str>> { context( "set_comf", map( preceded( terminated(alt((tag("set_conf"), tag("C"))), multispace1), string_pair, ), |pair: (&str, &str)| Command::SetConf(pair.0.to_owned(), pair.1.to_owned()), ), ) .parse(input) } fn reset(input: &str) -> IResult<&str, Command, VerboseError<&str>> { map(alt((tag("reset"), tag("R"))), |_| Command::Reset).parse(input) } fn get_info(input: &str) -> IResult<&str, Command, VerboseError<&str>> { map(alt((tag("get_info"), tag("_"))), |_| Command::GetInfo).parse(input) } fn dump_state(input: &str) -> IResult<&str, Command, VerboseError<&str>> { map(tag("dump_state"), |_| Command::DumpState).parse(input) } fn dump_caps(input: &str) -> IResult<&str, Command, VerboseError<&str>> { map(alt((tag("dump_caps"), tag("1"))), |_| Command::DumpCaps).parse(input) } fn send_cmd(input: &str) -> IResult<&str, Command, VerboseError<&str>> { context( "send_cmd", map( preceded( terminated(alt((tag("send_cmd"), tag("w"))), space1), many1(none_of("#\n")), ), |rest: Vec| Command::SendCmd(rest.iter().collect()), ), ) .parse(input) } fn command(input: &str) -> IResult<&str, Command, VerboseError<&str>> { context( "command", alt(( exit, set_pos, park, get_pos, move_command, stop, set_conf, reset, get_info, dump_state, dump_caps, send_cmd, )), ) .parse(input) } fn comment(input: &str) -> IResult<&str, &str, VerboseError<&str>> { context( "comment", recognize(preceded(multispace0, preceded(tag("#"), not_line_ending))), ) .parse(input) } fn line(input: &str) -> IResult<&str, Command, VerboseError<&str>> { context( "line", all_consuming(terminated(terminated(command, opt(comment)), newline)), ) .parse(input) } pub fn parse_command(input: &str) -> Result { let result = line(input); match result { Ok(("", cmd)) => Ok(cmd), Ok((rest, _)) => Err("Unable to parse rest".to_owned()), Err(err) => match err { NomErr::Incomplete(_) => Err("Command was incomplete".to_owned()), NomErr::Error(err) | NomErr::Failure(err) => Err(convert_error(input, err)), }, } } #[cfg(test)] mod tests { use super::*; use nom::Err as NomErr; fn assert_command(input: &str, expected: Command) { let result = line(input); match result { Ok(("", cmd)) => assert_eq!(cmd, expected), Err(err) => match err { NomErr::Incomplete(_) => panic!("Command was incomplete"), NomErr::Error(err) | NomErr::Failure(err) => { panic!("{}", convert_error(input, err)) } }, _ => {} } } #[test] fn test_exit() { assert_command("Q\n", Command::Exit); assert_command("q\n", Command::Exit); } #[test] fn test_set_pos() { assert_command("set_pos 180.0 10.0\n", Command::SetPos(180.0, 10.0)); assert_command("P 180.0 10.0\n", Command::SetPos(180.0, 10.0)); } #[test] fn test_get_pos() { assert_command("get_pos\n", Command::GetPos); assert_command("p\n", Command::GetPos); } #[test] fn test_move() { assert_command("M UP 100\n", Command::Move(Direction::UP, 100)); assert_command("M DOWN -1\n", Command::Move(Direction::DOWN, -1)); assert_command("M CCW 42\n", Command::Move(Direction::CCW, 42)); assert_command("M LEFT 42\n", Command::Move(Direction::CCW, 42)); assert_command("M CW 42\n", Command::Move(Direction::CW, 42)); assert_command("M RIGHT 42\n", Command::Move(Direction::CW, 42)); assert_command("move UP 100\n", Command::Move(Direction::UP, 100)); } #[test] fn test_stop() { assert_command("stop\n", Command::Stop); assert_command("S\n", Command::Stop); } #[test] fn test_park() { assert_command("park\n", Command::Park); assert_command("K\n", Command::Park); } #[test] fn test_reset() { assert_command("reset\n", Command::Reset); assert_command("R\n", Command::Reset); } #[test] fn test_get_info() { assert_command("get_info\n", Command::GetInfo); assert_command("_\n", Command::GetInfo); } #[test] fn test_dump_state() { assert_command("dump_state\n", Command::DumpState); } #[test] fn test_dump_caps() { assert_command("dump_caps\n", Command::DumpCaps); assert_command("1\n", Command::DumpCaps); } #[test] fn test_set_conf() { assert_command( "set_conf foo bar\n", Command::SetConf("foo".to_owned(), "bar".to_owned()), ); assert_command( "C foo bar\n", Command::SetConf("foo".to_owned(), "bar".to_owned()), ); } #[test] fn test_send_cmd() { assert_command("send_cmd foo bar\n", Command::SendCmd("foo bar".to_owned())); assert_command("w foo bar\n", Command::SendCmd("foo bar".to_owned())); } #[test] fn test_comments() { assert_command( "set_pos 180.0 10.0 # fooo bar lol test\n", Command::SetPos(180.0, 10.0), ); assert_command( "P 180.0 10.0 # fooo bar lol test\n", Command::SetPos(180.0, 10.0), ); // TODO: figure out if the trailing space is a problem assert_command( "send_cmd foo bar # this is a comment\n", Command::SendCmd("foo bar ".to_owned()), ); } }