Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
remote-helper: create initial command parser
Adrian Duke committed 2 months ago
commit d67e01474cd7d78307570a2ca1f95454e2dd62e6
parent d1d7a751d024be66f2d4f20c1012687937eadc9d
1 file changed +190 -0
added crates/radicle-remote-helper/src/protocol.rs
@@ -0,0 +1,190 @@
+
use thiserror::Error;
+

+
#[derive(Debug, Error)]
+
pub enum Error {
+
    #[error("invalid command `{0}`")]
+
    InvalidCommand(String),
+
}
+

+
#[derive(Debug, PartialEq, Eq)]
+
pub enum Command {
+
    Capabilities,
+
    List,
+
    ListForPush,
+
    Fetch { oid: String, refstr: String },
+
    Push(String),
+
    Option { key: String, value: Option<String> },
+
}
+

+
#[derive(Debug, PartialEq, Eq)]
+
pub enum Line {
+
    Valid(Command),
+
    Blank,
+
}
+

+
impl Command {
+
    pub 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())),
+
        }
+
    }
+
}
+

+
#[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());
+
    }
+
}