radicle_cli/terminal/
args.rs

1use std::ffi::OsString;
2use std::net::SocketAddr;
3use std::str::FromStr;
4use std::time;
5
6use anyhow::anyhow;
7
8use radicle::cob::{self, issue, patch};
9use radicle::crypto;
10use radicle::git::{Oid, RefString};
11use radicle::node::{Address, Alias};
12use radicle::prelude::{Did, NodeId, RepoId};
13
14use crate::git::Rev;
15use crate::terminal as term;
16
17#[derive(thiserror::Error, Debug)]
18pub enum Error {
19    /// If this error is returned from argument parsing, help is displayed.
20    #[error("help invoked")]
21    Help,
22    /// If this error is returned from argument parsing, the manual page is displayed.
23    #[error("help manual invoked")]
24    HelpManual { name: &'static str },
25    /// If this error is returned from argument parsing, usage is displayed.
26    #[error("usage invoked")]
27    Usage,
28    /// An error with a hint.
29    #[error("{err}")]
30    WithHint {
31        err: anyhow::Error,
32        hint: &'static str,
33    },
34}
35
36pub struct Help {
37    pub name: &'static str,
38    pub description: &'static str,
39    pub version: &'static str,
40    pub usage: &'static str,
41}
42
43impl Help {
44    /// Print help to stdout.
45    pub fn print(&self) {
46        term::help(self.name, self.version, self.description, self.usage);
47    }
48}
49
50pub trait Args: Sized {
51    fn from_env() -> anyhow::Result<Self> {
52        let args: Vec<_> = std::env::args_os().skip(1).collect();
53
54        match Self::from_args(args) {
55            Ok((opts, unparsed)) => {
56                self::finish(unparsed)?;
57
58                Ok(opts)
59            }
60            Err(err) => Err(err),
61        }
62    }
63
64    fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)>;
65}
66
67pub fn parse_value<T: FromStr>(flag: &str, value: OsString) -> anyhow::Result<T>
68where
69    <T as FromStr>::Err: std::error::Error,
70{
71    value
72        .into_string()
73        .map_err(|_| anyhow!("the value specified for '--{}' is not valid UTF-8", flag))?
74        .parse()
75        .map_err(|e| anyhow!("invalid value specified for '--{}' ({})", flag, e))
76}
77
78pub fn format(arg: lexopt::Arg) -> OsString {
79    match arg {
80        lexopt::Arg::Long(flag) => format!("--{flag}").into(),
81        lexopt::Arg::Short(flag) => format!("-{flag}").into(),
82        lexopt::Arg::Value(val) => val,
83    }
84}
85
86pub fn finish(unparsed: Vec<OsString>) -> anyhow::Result<()> {
87    if let Some(arg) = unparsed.first() {
88        anyhow::bail!("unexpected argument `{}`", arg.to_string_lossy())
89    }
90    Ok(())
91}
92
93pub fn refstring(flag: &str, value: OsString) -> anyhow::Result<RefString> {
94    RefString::try_from(
95        value
96            .into_string()
97            .map_err(|_| anyhow!("the value specified for '--{}' is not valid UTF-8", flag))?,
98    )
99    .map_err(|_| {
100        anyhow!(
101            "the value specified for '--{}' is not a valid ref string",
102            flag
103        )
104    })
105}
106
107pub fn did(val: &OsString) -> anyhow::Result<Did> {
108    let val = val.to_string_lossy();
109    let Ok(peer) = Did::from_str(&val) else {
110        if crypto::PublicKey::from_str(&val).is_ok() {
111            return Err(anyhow!("expected DID, did you mean 'did:key:{val}'?"));
112        } else {
113            return Err(anyhow!("invalid DID '{}', expected 'did:key'", val));
114        }
115    };
116    Ok(peer)
117}
118
119pub fn nid(val: &OsString) -> anyhow::Result<NodeId> {
120    let val = val.to_string_lossy();
121    NodeId::from_str(&val).map_err(|_| anyhow!("invalid Node ID '{}'", val))
122}
123
124pub fn rid(val: &OsString) -> anyhow::Result<RepoId> {
125    let val = val.to_string_lossy();
126    RepoId::from_str(&val).map_err(|_| anyhow!("invalid Repository ID '{}'", val))
127}
128
129pub fn pubkey(val: &OsString) -> anyhow::Result<NodeId> {
130    let Ok(did) = did(val) else {
131        let nid = nid(val)?;
132        return Ok(nid);
133    };
134    Ok(did.as_key().to_owned())
135}
136
137pub fn socket_addr(val: &OsString) -> anyhow::Result<SocketAddr> {
138    let val = val.to_string_lossy();
139    SocketAddr::from_str(&val).map_err(|_| anyhow!("invalid socket address '{}'", val))
140}
141
142pub fn addr(val: &OsString) -> anyhow::Result<Address> {
143    let val = val.to_string_lossy();
144    Address::from_str(&val).map_err(|_| anyhow!("invalid address '{}'", val))
145}
146
147pub fn number(val: &OsString) -> anyhow::Result<usize> {
148    let val = val.to_string_lossy();
149    usize::from_str(&val).map_err(|_| anyhow!("invalid number '{}'", val))
150}
151
152pub fn seconds(val: &OsString) -> anyhow::Result<time::Duration> {
153    let val = val.to_string_lossy();
154    let secs = u64::from_str(&val).map_err(|_| anyhow!("invalid number of seconds '{}'", val))?;
155
156    Ok(time::Duration::from_secs(secs))
157}
158
159pub fn milliseconds(val: &OsString) -> anyhow::Result<time::Duration> {
160    let val = val.to_string_lossy();
161    let secs =
162        u64::from_str(&val).map_err(|_| anyhow!("invalid number of milliseconds '{}'", val))?;
163
164    Ok(time::Duration::from_millis(secs))
165}
166
167pub fn string(val: &OsString) -> String {
168    val.to_string_lossy().to_string()
169}
170
171pub fn rev(val: &OsString) -> anyhow::Result<Rev> {
172    let s = val.to_str().ok_or(anyhow!("invalid git rev {val:?}"))?;
173    Ok(Rev::from(s.to_owned()))
174}
175
176pub fn oid(val: &OsString) -> anyhow::Result<Oid> {
177    let s = string(val);
178    let o = radicle::git::Oid::from_str(&s).map_err(|_| anyhow!("invalid git oid '{s}'"))?;
179
180    Ok(o)
181}
182
183pub fn alias(val: &OsString) -> anyhow::Result<Alias> {
184    let val = val.as_os_str();
185    let val = val
186        .to_str()
187        .ok_or_else(|| anyhow!("alias must be valid UTF-8"))?;
188
189    Alias::from_str(val).map_err(|e| e.into())
190}
191
192pub fn issue(val: &OsString) -> anyhow::Result<issue::IssueId> {
193    let val = val.to_string_lossy();
194    issue::IssueId::from_str(&val).map_err(|_| anyhow!("invalid Issue ID '{}'", val))
195}
196
197pub fn patch(val: &OsString) -> anyhow::Result<patch::PatchId> {
198    let val = val.to_string_lossy();
199    patch::PatchId::from_str(&val).map_err(|_| anyhow!("invalid Patch ID '{}'", val))
200}
201
202pub fn cob(val: &OsString) -> anyhow::Result<cob::ObjectId> {
203    let val = val.to_string_lossy();
204    cob::ObjectId::from_str(&val).map_err(|_| anyhow!("invalid Object ID '{}'", val))
205}