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 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}