radicle_cli/terminal/
args.rs1use 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 #[error("help invoked")]
21 Help,
22 #[error("help manual invoked")]
24 HelpManual { name: &'static str },
25 #[error("usage invoked")]
27 Usage,
28 #[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 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}