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        return Err(anyhow::anyhow!(
89            "unexpected argument `{}`",
90            arg.to_string_lossy()
91        ));
92    }
93    Ok(())
94}
95
96pub fn refstring(flag: &str, value: OsString) -> anyhow::Result<RefString> {
97    RefString::try_from(
98        value
99            .into_string()
100            .map_err(|_| anyhow!("the value specified for '--{}' is not valid UTF-8", flag))?,
101    )
102    .map_err(|_| {
103        anyhow!(
104            "the value specified for '--{}' is not a valid ref string",
105            flag
106        )
107    })
108}
109
110pub fn did(val: &OsString) -> anyhow::Result<Did> {
111    let val = val.to_string_lossy();
112    let Ok(peer) = Did::from_str(&val) else {
113        if crypto::PublicKey::from_str(&val).is_ok() {
114            return Err(anyhow!("expected DID, did you mean 'did:key:{val}'?"));
115        } else {
116            return Err(anyhow!("invalid DID '{}', expected 'did:key'", val));
117        }
118    };
119    Ok(peer)
120}
121
122pub fn nid(val: &OsString) -> anyhow::Result<NodeId> {
123    let val = val.to_string_lossy();
124    NodeId::from_str(&val).map_err(|_| anyhow!("invalid Node ID '{}'", val))
125}
126
127pub fn rid(val: &OsString) -> anyhow::Result<RepoId> {
128    let val = val.to_string_lossy();
129    RepoId::from_str(&val).map_err(|_| anyhow!("invalid Repository ID '{}'", val))
130}
131
132pub fn pubkey(val: &OsString) -> anyhow::Result<NodeId> {
133    let Ok(did) = did(val) else {
134        let nid = nid(val)?;
135        return Ok(nid);
136    };
137    Ok(did.as_key().to_owned())
138}
139
140pub fn socket_addr(val: &OsString) -> anyhow::Result<SocketAddr> {
141    let val = val.to_string_lossy();
142    SocketAddr::from_str(&val).map_err(|_| anyhow!("invalid socket address '{}'", val))
143}
144
145pub fn addr(val: &OsString) -> anyhow::Result<Address> {
146    let val = val.to_string_lossy();
147    Address::from_str(&val).map_err(|_| anyhow!("invalid address '{}'", val))
148}
149
150pub fn number(val: &OsString) -> anyhow::Result<usize> {
151    let val = val.to_string_lossy();
152    usize::from_str(&val).map_err(|_| anyhow!("invalid number '{}'", val))
153}
154
155pub fn seconds(val: &OsString) -> anyhow::Result<time::Duration> {
156    let val = val.to_string_lossy();
157    let secs = u64::from_str(&val).map_err(|_| anyhow!("invalid number of seconds '{}'", val))?;
158
159    Ok(time::Duration::from_secs(secs))
160}
161
162pub fn milliseconds(val: &OsString) -> anyhow::Result<time::Duration> {
163    let val = val.to_string_lossy();
164    let secs =
165        u64::from_str(&val).map_err(|_| anyhow!("invalid number of milliseconds '{}'", val))?;
166
167    Ok(time::Duration::from_millis(secs))
168}
169
170pub fn string(val: &OsString) -> String {
171    val.to_string_lossy().to_string()
172}
173
174pub fn rev(val: &OsString) -> anyhow::Result<Rev> {
175    let s = val.to_str().ok_or(anyhow!("invalid git rev {val:?}"))?;
176    Ok(Rev::from(s.to_owned()))
177}
178
179pub fn oid(val: &OsString) -> anyhow::Result<Oid> {
180    let s = string(val);
181    let o = radicle::git::Oid::from_str(&s).map_err(|_| anyhow!("invalid git oid '{s}'"))?;
182
183    Ok(o)
184}
185
186pub fn alias(val: &OsString) -> anyhow::Result<Alias> {
187    let val = val.as_os_str();
188    let val = val
189        .to_str()
190        .ok_or_else(|| anyhow!("alias must be valid UTF-8"))?;
191
192    Alias::from_str(val).map_err(|e| e.into())
193}
194
195pub fn issue(val: &OsString) -> anyhow::Result<issue::IssueId> {
196    let val = val.to_string_lossy();
197    issue::IssueId::from_str(&val).map_err(|_| anyhow!("invalid Issue ID '{}'", val))
198}
199
200pub fn patch(val: &OsString) -> anyhow::Result<patch::PatchId> {
201    let val = val.to_string_lossy();
202    patch::PatchId::from_str(&val).map_err(|_| anyhow!("invalid Patch ID '{}'", val))
203}
204
205pub fn cob(val: &OsString) -> anyhow::Result<cob::ObjectId> {
206    let val = val.to_string_lossy();
207    cob::ObjectId::from_str(&val).map_err(|_| anyhow!("invalid Object ID '{}'", val))
208}