use thiserror::Error; #[derive(Debug, Error)] pub(super) enum Error { #[error("invalid command `{0}`")] InvalidCommand(String), } #[derive(Debug, PartialEq, Eq)] pub(super) enum Command { Capabilities, List, ListForPush, Fetch { oid: String, refstr: String }, Push(String), Option { key: String, value: Option }, } #[derive(Debug, PartialEq, Eq)] pub(super) enum Line { Valid(Command), Blank, } impl Command { pub(super) fn parse_line(line: &str) -> Result { let line = line.trim(); if line.is_empty() { return Ok(Line::Blank); } // Split the command verb from the rest of the line. let (cmd, args) = line.split_once(' ').unwrap_or((line, "")); let args = args.trim(); match cmd { "capabilities" => Ok(Line::Valid(Command::Capabilities)), "list" => { if args == "for-push" { Ok(Line::Valid(Command::ListForPush)) } else if args.is_empty() { Ok(Line::Valid(Command::List)) } else { Err(Error::InvalidCommand(line.to_owned())) } } "fetch" => { // fetch // Use split_whitespace to handle multiple spaces between OID and Ref, // which is permitted. let mut parts = args.split_whitespace(); let oid = parts .next() .ok_or_else(|| Error::InvalidCommand(line.to_owned()))?; let refstr = parts .next() .ok_or_else(|| Error::InvalidCommand(line.to_owned()))?; Ok(Line::Valid(Command::Fetch { oid: oid.to_owned(), refstr: refstr.to_owned(), })) } "push" => Ok(Line::Valid(Command::Push(args.to_owned()))), "option" => { // option [value] // Use split_once to preserve whitespace in the value. let (key, val) = args.split_once(' ').unwrap_or((args, "")); let value = if val.is_empty() { None } else { Some(val.to_owned()) }; Ok(Line::Valid(Command::Option { key: key.to_owned(), value, })) } _ => Err(Error::InvalidCommand(line.to_owned())), } } } mod io { use std::io::{self, prelude::*}; use super::*; pub(crate) struct LineReader { inner: io::BufReader, } impl LineReader { pub(crate) fn new(reader: R) -> Self { Self { inner: io::BufReader::new(reader), } } pub(crate) fn read_line(&mut self) -> io::Result> { let mut line = String::new(); if self.inner.read_line(&mut line)? == 0 { // EOF reached return Ok(Ok(Line::Blank)); } Ok(Command::parse_line(&line)) } } impl Iterator for LineReader { type Item = io::Result>; fn next(&mut self) -> Option { match self.read_line() { Ok(line) => Some(Ok(line)), Err(e) => Some(Err(e)), } } } } pub(crate) use io::*; #[cfg(test)] mod tests { use super::*; #[test] fn test_capabilities() { assert_eq!( Command::parse_line("capabilities").unwrap(), Line::Valid(Command::Capabilities) ); } #[test] fn test_list() { assert_eq!( Command::parse_line("list").unwrap(), Line::Valid(Command::List) ); } #[test] fn test_list_for_push() { assert_eq!( Command::parse_line("list for-push").unwrap(), Line::Valid(Command::ListForPush) ); } #[test] fn test_fetch() { assert_eq!( Command::parse_line("fetch oid ref").unwrap(), Line::Valid(Command::Fetch { oid: "oid".to_owned(), refstr: "ref".to_owned() }) ); } #[test] fn test_fetch_whitespace() { assert_eq!( Command::parse_line("fetch oid ref").unwrap(), Line::Valid(Command::Fetch { oid: "oid".to_owned(), refstr: "ref".to_owned() }) ); } #[test] fn test_push() { assert_eq!( Command::parse_line("push src:dst").unwrap(), Line::Valid(Command::Push("src:dst".to_owned())) ); } #[test] fn test_push_force() { assert_eq!( Command::parse_line("push +src:dst").unwrap(), Line::Valid(Command::Push("+src:dst".to_owned())) ); } #[test] fn test_push_delete() { assert_eq!( Command::parse_line("push :dst").unwrap(), Line::Valid(Command::Push(":dst".to_owned())) ); } #[test] fn test_option() { assert_eq!( Command::parse_line("option verbosity 2").unwrap(), Line::Valid(Command::Option { key: "verbosity".to_owned(), value: Some("2".to_owned()) }) ); } #[test] fn test_option_whitespace_preservation() { assert_eq!( Command::parse_line("option patch.message Fix: whitespace").unwrap(), Line::Valid(Command::Option { key: "patch.message".to_owned(), value: Some("Fix: whitespace".to_owned()) }) ); } #[test] fn test_empty() { assert_eq!(Command::parse_line("").unwrap(), Line::Blank); assert_eq!(Command::parse_line(" ").unwrap(), Line::Blank); } #[test] fn test_invalid() { assert!(Command::parse_line("invalid command").is_err()); assert!(Command::parse_line("list invalid").is_err()); } }