use clap::Parser;
use std::{
path::{Path, PathBuf},
str::FromStr,
};
use thiserror::Error;
#[derive(Parser, Clone, Debug, PartialEq, Eq)]
#[command(name = "")]
pub enum ShackleCommand {
Init(InitArgs),
List(ListArgs),
SetDescription(SetDescriptionArgs),
SetBranch(SetBranchArgs),
Delete(DeleteArgs),
Housekeeping(HousekeepingArgs),
Exit,
#[command(hide = true)]
GitUploadPack(GitUploadPackArgs),
#[command(hide = true)]
GitReceivePack(GitReceivePackArgs),
}
#[derive(Parser, Clone, Debug, PartialEq, Eq)]
pub struct InitArgs {
#[arg(long)]
pub group: Option<String>,
#[arg(long)]
pub description: Option<String>,
#[arg(long, default_value = "main")]
pub branch: String,
#[arg(long)]
pub mirror: Option<String>,
pub repo_name: String,
}
#[derive(Parser, Clone, Debug, PartialEq, Eq)]
pub struct ListArgs {
#[arg(short, long)]
pub verbose: bool,
}
#[derive(Parser, Clone, Debug, PartialEq, Eq)]
pub struct SetDescriptionArgs {
#[arg(value_parser = RelativePathParser)]
pub directory: PathBuf,
pub description: String,
}
#[derive(Parser, Clone, Debug, PartialEq, Eq)]
pub struct SetBranchArgs {
#[arg(value_parser = RelativePathParser)]
pub directory: PathBuf,
pub branch: String,
}
#[derive(Parser, Clone, Debug, PartialEq, Eq)]
pub struct DeleteArgs {
#[arg(value_parser = RelativePathParser)]
pub directory: PathBuf,
}
#[derive(Parser, Clone, Debug, PartialEq, Eq)]
pub struct HousekeepingArgs {
pub directory: Option<PathBuf>,
}
#[derive(Parser, Clone, Debug, PartialEq, Eq)]
pub struct GitUploadPackArgs {
#[arg(long, default_value_t = true)]
pub strict: bool,
#[arg(long)]
pub no_strict: bool,
#[arg(long)]
pub timeout: Option<u32>,
#[arg(long)]
pub stateless_rpc: bool,
#[arg(long)]
pub advertise_refs: bool,
#[arg(value_parser = RelativePathParser)]
pub directory: PathBuf,
}
#[derive(Parser, Clone, Debug, PartialEq, Eq)]
pub struct GitReceivePackArgs {
#[arg(value_parser = RelativePathParser)]
pub directory: PathBuf,
}
#[derive(Error, Debug)]
pub enum ParserError {
#[error(transparent)]
ClapError(#[from] clap::error::Error),
#[error("`{0}`")]
LexerError(String),
}
impl FromStr for ShackleCommand {
type Err = ParserError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let trimmed = s.trim();
let lexed = shlex::split(trimmed);
match lexed {
None => Err(ParserError::LexerError("Incomplete input".to_string())),
Some(lexed) => {
let parsed =
ShackleCommand::try_parse_from(["".to_owned()].into_iter().chain(lexed))?;
Ok(parsed)
}
}
}
}
#[derive(Clone)]
struct RelativePathParser;
impl clap::builder::TypedValueParser for RelativePathParser {
type Value = std::path::PathBuf;
fn parse_ref(
&self,
cmd: &clap::Command,
arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
clap::builder::TypedValueParser::parse(self, cmd, arg, value.to_owned())
}
fn parse(
&self,
cmd: &clap::Command,
arg: Option<&clap::Arg>,
value: std::ffi::OsString,
) -> Result<Self::Value, clap::Error> {
let raw = clap::builder::PathBufValueParser::default().parse(cmd, arg, value)?;
Ok(raw
.strip_prefix(Path::new("/~"))
.or_else(|_| raw.strip_prefix(Path::new("~")))
.map(|m| m.to_owned())
.unwrap_or(raw))
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn it_parses_exit_correctly() {
assert_eq!(
"exit".parse::<ShackleCommand>().unwrap(),
ShackleCommand::Exit
);
}
#[test]
fn it_parses_git_upload_pack_correctly() {
assert_eq!(
"git-upload-pack --stateless-rpc foobar.git"
.parse::<ShackleCommand>()
.unwrap(),
ShackleCommand::GitUploadPack(GitUploadPackArgs {
strict: true,
no_strict: false,
timeout: None,
stateless_rpc: true,
advertise_refs: false,
directory: PathBuf::from("foobar.git"),
})
);
}
}