use std::fmt;
use std::fs;
use std::path::PathBuf;
use std::str::FromStr;
use thiserror::Error;
use clap::{Parser, Subcommand};
use radicle::cob;
use radicle::git;
use radicle::prelude::*;
use radicle::storage;
use crate::git::Rev;
#[derive(Parser, Debug)]
#[command(disable_version_flag = true)]
pub struct Args {
#[command(subcommand)]
pub(super) command: Command,
}
#[derive(Subcommand, Debug)]
pub(super) enum Command {
Create(#[clap(flatten)] Create),
List {
#[arg(long, short, value_name = "RID")]
repo: RepoId,
#[arg(long = "type", short, value_name = "TYPENAME")]
type_name: cob::TypeName,
},
Log {
#[arg(long, short, value_name = "RID")]
repo: RepoId,
#[arg(long = "type", short, value_name = "TYPENAME")]
type_name: cob::TypeName,
#[arg(long, short, value_name = "OID")]
object: Rev,
#[arg(long, default_value_t = Format::Pretty, value_parser = FormatParser)]
format: Format,
#[arg(long, value_name = "OID")]
from: Option<Rev>,
#[arg(long, value_name = "OID")]
until: Option<Rev>,
},
Migrate,
Show {
#[arg(long, short, value_name = "RID")]
repo: RepoId,
#[arg(long = "type", short, value_name = "TYPENAME")]
type_name: cob::TypeName,
#[arg(long = "object", short, value_name = "OID", action = clap::ArgAction::Append, required = true)]
objects: Vec<Rev>,
#[arg(long, default_value_t = Format::Json, value_parser = FormatParser)]
format: Format,
},
Update(#[clap(flatten)] Update),
}
#[derive(Parser, Debug)]
pub(super) struct Operation {
#[arg(long, short)]
pub(super) message: String,
#[arg(long = "embed-file", value_names = ["NAME", "PATH"], num_args = 2)]
pub(super) embed_files: Vec<String>,
#[arg(long = "embed-hash", value_names = ["NAME", "OID"], num_args = 2)]
pub(super) embed_hashes: Vec<String>,
#[arg(value_name = "FILENAME")]
pub(super) actions: PathBuf,
}
#[derive(Parser, Debug)]
pub(super) struct Create {
#[arg(long, short, value_name = "RID")]
pub(super) repo: RepoId,
#[arg(long = "type", short, value_name = "TYPENAME")]
pub(super) type_name: FilteredTypeName,
#[clap(flatten)]
pub(super) operation: Operation,
}
#[derive(Parser, Debug)]
pub(super) struct Update {
#[arg(long, short)]
pub(super) repo: RepoId,
#[arg(long = "type", short, value_name = "TYPENAME")]
pub(super) type_name: FilteredTypeName,
#[arg(long, short, value_name = "OID")]
pub(super) object: Rev,
#[arg(long, default_value_t = Format::Json, value_parser = FormatParser)]
pub(super) format: Format,
#[clap(flatten)]
pub(super) operation: Operation,
}
#[derive(Clone, Debug)]
pub(super) struct Embed {
name: String,
content: EmbedContent,
}
impl Embed {
pub(super) fn try_into_bytes(
self,
repo: &storage::git::Repository,
) -> anyhow::Result<cob::Embed<cob::Uri>> {
Ok(match self.content {
EmbedContent::Hash(hash) => cob::Embed {
name: self.name,
content: hash.resolve::<git::Oid>(&repo.backend)?.into(),
},
EmbedContent::Path(path) => {
cob::Embed::store(self.name, &fs::read(path)?, &repo.backend)?
}
})
}
}
#[derive(Clone, Debug)]
pub(super) enum EmbedContent {
Path(PathBuf),
Hash(Rev),
}
impl From<PathBuf> for EmbedContent {
fn from(path: PathBuf) -> Self {
EmbedContent::Path(path)
}
}
impl From<Rev> for EmbedContent {
fn from(rev: Rev) -> Self {
EmbedContent::Hash(rev)
}
}
pub(super) fn parse_many_embeds<T>(values: &[String]) -> impl Iterator<Item = Embed> + use<'_, T>
where
T: From<String>,
EmbedContent: From<T>,
{
let chunks = values.chunks_exact(2);
assert!(chunks.remainder().is_empty());
chunks.map(|chunk| {
#[allow(clippy::indexing_slicing)]
Embed {
name: chunk[0].to_string(),
content: EmbedContent::from(T::from(chunk[1].clone())),
}
})
}
#[derive(Clone, Debug, PartialEq)]
pub(super) enum Format {
Json,
Pretty,
}
impl fmt::Display for Format {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Format::Json => f.write_str("json"),
Format::Pretty => f.write_str("pretty"),
}
}
}
#[non_exhaustive]
#[derive(Debug, Error)]
#[error("invalid format value: {0:?}")]
pub struct FormatParseError(String);
impl FromStr for Format {
type Err = FormatParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"json" => Ok(Self::Json),
"pretty" => Ok(Self::Pretty),
_ => Err(FormatParseError(s.to_string())),
}
}
}
#[derive(Clone, Debug)]
struct FormatParser;
impl clap::builder::TypedValueParser for FormatParser {
type Value = Format;
fn parse_ref(
&self,
cmd: &clap::Command,
arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
use clap::error::ErrorKind;
let format = <Format as std::str::FromStr>::from_str.parse_ref(cmd, arg, value)?;
match cmd.get_name() {
"show" | "update" if format == Format::Pretty => Err(clap::Error::raw(
ErrorKind::ValueValidation,
format!("output format `{format}` is not allowed in this command"),
)
.with_cmd(cmd)),
_ => Ok(format),
}
}
fn possible_values(
&self,
) -> Option<Box<dyn Iterator<Item = clap::builder::PossibleValue> + '_>> {
use clap::builder::PossibleValue;
Some(Box::new(
[PossibleValue::new("json"), PossibleValue::new("pretty")].into_iter(),
))
}
}
#[derive(Clone, Debug)]
pub(super) enum FilteredTypeName {
Issue,
Patch,
Identity,
Other(cob::TypeName),
}
impl AsRef<cob::TypeName> for FilteredTypeName {
fn as_ref(&self) -> &cob::TypeName {
match self {
FilteredTypeName::Issue => &cob::issue::TYPENAME,
FilteredTypeName::Patch => &cob::patch::TYPENAME,
FilteredTypeName::Identity => &cob::identity::TYPENAME,
FilteredTypeName::Other(value) => value,
}
}
}
impl From<cob::TypeName> for FilteredTypeName {
fn from(value: cob::TypeName) -> Self {
if value == *cob::issue::TYPENAME {
FilteredTypeName::Issue
} else if value == *cob::patch::TYPENAME {
FilteredTypeName::Patch
} else if value == *cob::identity::TYPENAME {
FilteredTypeName::Identity
} else {
FilteredTypeName::Other(value)
}
}
}
impl std::fmt::Display for FilteredTypeName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.as_ref().fmt(f)
}
}
impl std::str::FromStr for FilteredTypeName {
type Err = cob::TypeNameParse;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Self::from(s.parse::<cob::TypeName>()?))
}
}
#[cfg(test)]
mod test {
use super::Args;
use clap::error::ErrorKind;
use clap::Parser;
const ARGS: &[&str] = &[
"--repo",
"rad:z3Tr6bC7ctEg2EHmLvknUr29mEDLH",
"--type",
"xyz.radicle.issue",
"--object",
"f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354",
];
#[test]
fn should_allow_log_json_format() {
let args = Args::try_parse_from(
["cob", "log", "--format", "json"]
.iter()
.chain(ARGS.iter())
.collect::<Vec<_>>(),
);
assert!(args.is_ok())
}
#[test]
fn should_allow_log_pretty_format() {
let args = Args::try_parse_from(
["cob", "log", "--format", "pretty"]
.iter()
.chain(ARGS.iter())
.collect::<Vec<_>>(),
);
assert!(args.is_ok())
}
#[test]
fn should_allow_show_json_format() {
let args = Args::try_parse_from(
["cob", "show", "--format", "json"]
.iter()
.chain(ARGS.iter())
.collect::<Vec<_>>(),
);
assert!(args.is_ok())
}
#[test]
fn should_allow_update_json_format() {
let args = Args::try_parse_from(
[
"cob",
"update",
"--format",
"json",
"--message",
"",
"/dev/null",
]
.iter()
.chain(ARGS.iter())
.collect::<Vec<_>>(),
);
println!("{args:?}");
assert!(args.is_ok())
}
#[test]
fn should_not_allow_show_pretty_format() {
let err = Args::try_parse_from(["cob", "show", "--format", "pretty"]).unwrap_err();
assert_eq!(err.kind(), ErrorKind::ValueValidation);
}
#[test]
fn should_not_allow_update_pretty_format() {
let err = Args::try_parse_from(["cob", "update", "--format", "pretty"]).unwrap_err();
assert_eq!(err.kind(), ErrorKind::ValueValidation);
}
}