231 lines
6.1 KiB
Rust
231 lines
6.1 KiB
Rust
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<String> },
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq)]
|
|
pub(super) enum Line {
|
|
Valid(Command),
|
|
Blank,
|
|
}
|
|
|
|
impl Command {
|
|
pub(super) fn parse_line(line: &str) -> Result<Line, Error> {
|
|
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 <oid> <name>
|
|
// 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 <key> [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<R: Read> {
|
|
inner: io::BufReader<R>,
|
|
}
|
|
|
|
impl<R: Read> LineReader<R> {
|
|
pub(crate) fn new(reader: R) -> Self {
|
|
Self {
|
|
inner: io::BufReader::new(reader),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn read_line(&mut self) -> io::Result<Result<Line, Error>> {
|
|
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<R: Read> Iterator for LineReader<R> {
|
|
type Item = io::Result<Result<Line, Error>>;
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
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());
|
|
}
|
|
}
|