use std::io;
use std::str::FromStr;
use clap::{Parser, Subcommand};
use serde_json as json;
use thiserror::Error;
use radicle::cob::{Title, TypeNameParse};
use radicle::identity::doc::update::EditVisibility;
use radicle::identity::doc::update::PayloadUpsert;
use radicle::identity::doc::PayloadId;
use radicle::prelude::{Did, RepoId};
use crate::git::Rev;
use crate::terminal::Interactive;
const ABOUT: &str = "Manage repository identities";
const LONG_ABOUT: &str = r#"
The `id` command is used to manage and propose changes to the
identity of a Radicle repository.
See the rad-id(1) man page for more information.
"#;
#[derive(Debug, Error)]
pub enum PayloadUpsertParseError {
#[error("could not parse payload id: {0}")]
IdParse(#[from] TypeNameParse),
#[error("could not parse json value: {0}")]
Value(#[from] json::Error),
}
pub(super) fn parse_many_upserts(
values: &[String],
) -> impl Iterator<Item = Result<PayloadUpsert, PayloadUpsertParseError>> + use<'_> {
let chunks = values.chunks_exact(3);
assert!(chunks.remainder().is_empty());
chunks.map(|chunk| {
#[allow(clippy::indexing_slicing)]
Ok(PayloadUpsert {
id: PayloadId::from_str(&chunk[0])?,
key: chunk[1].to_owned(),
value: json::from_str(&chunk[2].to_owned())?,
})
})
}
#[derive(Clone, Debug)]
struct EditVisibilityParser;
impl clap::builder::TypedValueParser for EditVisibilityParser {
type Value = EditVisibility;
fn parse_ref(
&self,
cmd: &clap::Command,
arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> Result<Self::Value, clap::Error> {
<EditVisibility as std::str::FromStr>::from_str.parse_ref(cmd, arg, value)
}
fn possible_values(
&self,
) -> Option<Box<dyn Iterator<Item = clap::builder::PossibleValue> + '_>> {
use clap::builder::PossibleValue;
Some(Box::new(
[PossibleValue::new("private"), PossibleValue::new("public")].into_iter(),
))
}
}
#[derive(Debug, Parser)]
#[command(about = ABOUT, long_about = LONG_ABOUT, disable_version_flag = true)]
pub struct Args {
#[command(subcommand)]
pub(super) command: Option<Command>,
#[arg(long)]
#[arg(value_name = "RID", global = true)]
pub(super) repo: Option<RepoId>,
#[arg(long)]
#[arg(global = true)]
no_confirm: bool,
#[arg(long, short)]
#[arg(global = true)]
pub(super) quiet: bool,
}
impl Args {
pub(super) fn interactive(&self) -> Interactive {
if self.no_confirm {
Interactive::No
} else {
Interactive::new(io::stdout())
}
}
}
#[derive(Subcommand, Debug)]
pub(super) enum Command {
#[clap(alias("a"))]
Accept {
#[arg(value_name = "REVISION_ID")]
revision: Rev,
},
#[clap(alias("r"))]
Reject {
#[arg(value_name = "REVISION_ID")]
revision: Rev,
},
#[clap(alias("e"))]
Edit {
#[arg(value_name = "REVISION_ID")]
revision: Rev,
#[arg(long)]
title: Option<Title>,
#[arg(long)]
description: Option<String>,
},
#[clap(alias("u"))]
Update {
#[arg(long)]
title: Option<Title>,
#[arg(long)]
description: Option<String>,
#[arg(long, short)]
#[arg(value_name = "DID")]
#[arg(action = clap::ArgAction::Append)]
delegate: Vec<Did>,
#[arg(long, short)]
#[arg(value_name = "DID")]
#[arg(action = clap::ArgAction::Append)]
rescind: Vec<Did>,
#[arg(long)]
threshold: Option<usize>,
#[arg(long)]
#[arg(value_parser = EditVisibilityParser)]
visibility: Option<EditVisibility>,
#[arg(long)]
#[arg(value_name = "DID")]
#[arg(action = clap::ArgAction::Append)]
allow: Vec<Did>,
#[arg(long)]
#[arg(value_name = "DID")]
#[arg(action = clap::ArgAction::Append)]
disallow: Vec<Did>,
#[arg(long)]
#[arg(value_names = ["TYPE", "KEY", "VALUE"], num_args = 3)]
payload: Vec<String>,
#[arg(long)]
edit: bool,
},
#[clap(alias("l"))]
List,
#[clap(alias("s"))]
Show {
#[arg(value_name = "REVISION_ID")]
revision: Rev,
},
#[clap(alias("d"))]
Redact {
#[arg(value_name = "REVISION_ID")]
revision: Rev,
},
}
#[cfg(test)]
mod test {
use super::{parse_many_upserts, Args};
use clap::error::ErrorKind;
use clap::Parser;
#[test]
fn should_parse_single_payload() {
let args = Args::try_parse_from(["id", "update", "--payload", "key", "name", "value"]);
assert!(args.is_ok())
}
#[test]
fn should_not_parse_single_payload() {
let err = Args::try_parse_from(["id", "update", "--payload", "key", "name"]).unwrap_err();
assert_eq!(err.kind(), ErrorKind::WrongNumberOfValues);
}
#[test]
fn should_parse_multiple_payloads() {
let args = Args::try_parse_from([
"id",
"update",
"--payload",
"key_1",
"name_1",
"value_1",
"--payload",
"key_2",
"name_2",
"value_2",
]);
assert!(args.is_ok())
}
#[test]
fn should_not_parse_single_payloads() {
let err = Args::try_parse_from([
"id",
"update",
"--payload",
"key_1",
"name_1",
"value_1",
"--payload",
"key_2",
"name_2",
])
.unwrap_err();
assert_eq!(err.kind(), ErrorKind::WrongNumberOfValues);
}
#[test]
fn should_not_clobber_payload_args() {
let err = Args::try_parse_from([
"id",
"update",
"--payload",
"key_1",
"name_1",
"--payload", "key_2",
"name_2",
"value_2",
])
.unwrap_err();
assert_eq!(err.kind(), ErrorKind::WrongNumberOfValues);
}
#[test]
fn should_parse_into_payload() {
let payload: Result<Vec<_>, _> = parse_many_upserts(&[
"xyz.radicle.project".to_string(),
"name".to_string(),
"{}".to_string(),
])
.collect();
assert!(payload.is_ok())
}
#[test]
#[should_panic(expected = "assertion failed: chunks.remainder().is_empty()")]
fn should_not_parse_into_payload() {
let _: Result<Vec<_>, _> =
parse_many_upserts(&["xyz.radicle.project".to_string(), "name".to_string()]).collect();
}
}