cli/block: Use clap

This commit is contained in:
Lorenz Leutgeb 2025-09-28 19:02:48 +02:00 committed by Erik Kundt
parent ed5a68c1d7
commit 6d698bb794
4 changed files with 94 additions and 81 deletions

View File

@ -1,96 +1,24 @@
use std::ffi::OsString; mod args;
use radicle::node::policy::Policy; use radicle::node::policy::Policy;
use radicle::prelude::{NodeId, RepoId};
use crate::terminal as term; use crate::terminal as term;
use crate::terminal::args;
use crate::terminal::args::{Args, Error, Help};
pub const HELP: Help = Help { use args::Target;
name: "block",
description: "Block repositories or nodes from being seeded or followed",
version: env!("RADICLE_VERSION"),
usage: r#"
Usage
rad block <rid> [<option>...] pub use args::Args;
rad block <nid> [<option>...] pub(crate) use args::ABOUT;
Blocks a repository from being seeded or a node from being followed. pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
Options
--help Print help
"#,
};
enum Target {
Node(NodeId),
Repo(RepoId),
}
impl std::fmt::Display for Target {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Node(nid) => nid.fmt(f),
Self::Repo(rid) => rid.fmt(f),
}
}
}
pub struct Options {
target: Target,
}
impl Args for Options {
fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
use lexopt::prelude::*;
let mut parser = lexopt::Parser::from_args(args);
let mut target = None;
while let Some(arg) = parser.next()? {
match arg {
Long("help") | Short('h') => {
return Err(Error::Help.into());
}
Value(val) if target.is_none() => {
if let Ok(rid) = args::rid(&val) {
target = Some(Target::Repo(rid));
} else if let Ok(nid) = args::nid(&val) {
target = Some(Target::Node(nid));
} else {
anyhow::bail!(
"invalid repository or node specified, see `rad block --help`"
)
}
}
_ => anyhow::bail!(arg.unexpected()),
}
}
Ok((
Options {
target: target.ok_or(anyhow::anyhow!(
"a repository or node to block must be specified, see `rad block --help`"
))?,
},
vec![],
))
}
}
pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let profile = ctx.profile()?; let profile = ctx.profile()?;
let mut policies = profile.policies_mut()?; let mut policies = profile.policies_mut()?;
let updated = match options.target { let updated = match args.target {
Target::Node(nid) => policies.set_follow_policy(&nid, Policy::Block)?, Target::Node(nid) => policies.set_follow_policy(&nid, Policy::Block)?,
Target::Repo(rid) => policies.set_seed_policy(&rid, Policy::Block)?, Target::Repo(rid) => policies.set_seed_policy(&rid, Policy::Block)?,
}; };
if updated { if updated {
term::success!("Policy for {} set to 'block'", options.target); term::success!("Policy for {} set to 'block'", args.target);
} }
Ok(()) Ok(())
} }

View File

@ -0,0 +1,79 @@
use clap::Parser;
use thiserror::Error;
use radicle::prelude::{NodeId, RepoId};
pub(crate) const ABOUT: &str = "Block repositories or nodes from being seeded or followed";
#[derive(Clone, Debug)]
pub(super) enum Target {
Node(NodeId),
Repo(RepoId),
}
#[derive(Debug, Error)]
#[error("invalid repository or node specified (RID parsing failed with: '{repo}', NID parsing failed with: '{node}'))")]
pub(super) struct ParseTargetError {
repo: radicle::identity::IdError,
node: radicle::crypto::PublicKeyError,
}
impl std::str::FromStr for Target {
type Err = ParseTargetError;
fn from_str(val: &str) -> Result<Self, Self::Err> {
val.parse::<RepoId>().map(Target::Repo).or_else(|repo| {
val.parse::<NodeId>()
.map(Target::Node)
.map_err(|node| ParseTargetError { repo, node })
})
}
}
impl std::fmt::Display for Target {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Node(nid) => nid.fmt(f),
Self::Repo(rid) => rid.fmt(f),
}
}
}
#[derive(Parser, Debug)]
#[command(about = ABOUT, disable_version_flag = true)]
pub struct Args {
/// A Repository ID or Node ID to block from seeding or following (respectively)
///
/// Example values:
/// - z6MkiswaKJ85vafhffCGBu2gdBsYoDAyHVBWRxL3j297fwS9 (Node ID)
/// - rad:z3Tr6bC7ctEg2EHmLvknUr29mEDLH (Repository ID)
#[arg(value_name = "RID|NID", verbatim_doc_comment)]
pub(super) target: Target,
}
#[cfg(test)]
mod test {
use clap::error::ErrorKind;
use clap::Parser;
use super::Args;
#[test]
fn should_parse_nid() {
let args =
Args::try_parse_from(["block", "z6MkiswaKJ85vafhffCGBu2gdBsYoDAyHVBWRxL3j297fwS9"]);
assert!(args.is_ok())
}
#[test]
fn should_parse_rid() {
let args = Args::try_parse_from(["block", "rad:z3Tr6bC7ctEg2EHmLvknUr29mEDLH"]);
assert!(args.is_ok())
}
#[test]
fn should_not_parse() {
let err = Args::try_parse_from(["block", "bee"]).unwrap_err();
assert_eq!(err.kind(), ErrorKind::ValueValidation);
}
}

View File

@ -38,7 +38,10 @@ impl CommandItem {
const COMMANDS: &[CommandItem] = &[ const COMMANDS: &[CommandItem] = &[
CommandItem::Lexopt(crate::commands::auth::HELP), CommandItem::Lexopt(crate::commands::auth::HELP),
CommandItem::Lexopt(crate::commands::block::HELP), CommandItem::Clap {
name: "block",
about: crate::commands::block::ABOUT,
},
CommandItem::Lexopt(crate::commands::checkout::HELP), CommandItem::Lexopt(crate::commands::checkout::HELP),
CommandItem::Lexopt(crate::commands::clone::HELP), CommandItem::Lexopt(crate::commands::clone::HELP),
CommandItem::Lexopt(crate::commands::config::HELP), CommandItem::Lexopt(crate::commands::config::HELP),

View File

@ -45,6 +45,7 @@ struct CliArgs {
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug)]
enum Commands { enum Commands {
Block(block::Args),
Clean(clean::Args), Clean(clean::Args),
Issue(issue::Args), Issue(issue::Args),
Path(path::Args), Path(path::Args),
@ -178,7 +179,9 @@ pub(crate) fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyho
term::run_command_args::<auth::Options, _>(auth::HELP, auth::run, args.to_vec()); term::run_command_args::<auth::Options, _>(auth::HELP, auth::run, args.to_vec());
} }
"block" => { "block" => {
term::run_command_args::<block::Options, _>(block::HELP, block::run, args.to_vec()); if let Some(Commands::Block(args)) = CliArgs::parse().command {
term::run_command_fn(block::run, args);
}
} }
"checkout" => { "checkout" => {
term::run_command_args::<checkout::Options, _>( term::run_command_args::<checkout::Options, _>(