1use std::io;
2use std::str::FromStr;
3
4use clap::{Parser, Subcommand};
5
6use serde_json as json;
7
8use thiserror::Error;
9
10use radicle::cob::{Title, TypeNameParse};
11use radicle::identity::doc::update::EditVisibility;
12use radicle::identity::doc::update::PayloadUpsert;
13use radicle::identity::doc::PayloadId;
14use radicle::prelude::{Did, RepoId};
15
16use crate::git::Rev;
17
18use crate::terminal::Interactive;
19
20const ABOUT: &str = "Manage repository identities";
21const LONG_ABOUT: &str = r#"
22The `id` command is used to manage and propose changes to the
23identity of a Radicle repository.
24
25See the rad-id(1) man page for more information.
26"#;
27
28#[derive(Debug, Error)]
29pub enum PayloadUpsertParseError {
30 #[error("could not parse payload id: {0}")]
31 IdParse(#[from] TypeNameParse),
32 #[error("could not parse json value: {0}")]
33 Value(#[from] json::Error),
34}
35
36pub(super) fn parse_many_upserts(
48 values: &[String],
49) -> impl Iterator<Item = Result<PayloadUpsert, PayloadUpsertParseError>> + use<'_> {
50 let chunks = values.chunks_exact(3);
53
54 assert!(chunks.remainder().is_empty());
55
56 chunks.map(|chunk| {
57 #[allow(clippy::indexing_slicing)]
59 Ok(PayloadUpsert {
60 id: PayloadId::from_str(&chunk[0])?,
61 key: chunk[1].to_owned(),
62 value: json::from_str(&chunk[2].to_owned())?,
63 })
64 })
65}
66
67#[derive(Clone, Debug)]
68struct EditVisibilityParser;
69
70impl clap::builder::TypedValueParser for EditVisibilityParser {
71 type Value = EditVisibility;
72
73 fn parse_ref(
74 &self,
75 cmd: &clap::Command,
76 arg: Option<&clap::Arg>,
77 value: &std::ffi::OsStr,
78 ) -> Result<Self::Value, clap::Error> {
79 <EditVisibility as std::str::FromStr>::from_str.parse_ref(cmd, arg, value)
80 }
81
82 fn possible_values(
83 &self,
84 ) -> Option<Box<dyn Iterator<Item = clap::builder::PossibleValue> + '_>> {
85 use clap::builder::PossibleValue;
86 Some(Box::new(
87 [PossibleValue::new("private"), PossibleValue::new("public")].into_iter(),
88 ))
89 }
90}
91
92#[derive(Debug, Parser)]
93#[command(about = ABOUT, long_about = LONG_ABOUT, disable_version_flag = true)]
94pub struct Args {
95 #[command(subcommand)]
96 pub(super) command: Option<Command>,
97
98 #[arg(long)]
102 #[arg(value_name = "RID", global = true)]
103 pub(super) repo: Option<RepoId>,
104
105 #[arg(long)]
107 #[arg(global = true)]
108 no_confirm: bool,
109
110 #[arg(long, short)]
112 #[arg(global = true)]
113 pub(super) quiet: bool,
114}
115
116impl Args {
117 pub(super) fn interactive(&self) -> Interactive {
118 if self.no_confirm {
119 Interactive::No
120 } else {
121 Interactive::new(io::stdout())
122 }
123 }
124}
125
126#[derive(Subcommand, Debug)]
127pub(super) enum Command {
128 #[clap(alias("a"))]
130 Accept {
131 #[arg(value_name = "REVISION_ID")]
133 revision: Rev,
134 },
135
136 #[clap(alias("r"))]
138 Reject {
139 #[arg(value_name = "REVISION_ID")]
141 revision: Rev,
142 },
143
144 #[clap(alias("e"))]
146 Edit {
147 #[arg(value_name = "REVISION_ID")]
149 revision: Rev,
150
151 #[arg(long)]
153 title: Option<Title>,
154
155 #[arg(long)]
157 description: Option<String>,
158 },
159
160 #[clap(alias("u"))]
162 Update {
163 #[arg(long)]
165 title: Option<Title>,
166
167 #[arg(long)]
169 description: Option<String>,
170
171 #[arg(long, short)]
173 #[arg(value_name = "DID")]
174 #[arg(action = clap::ArgAction::Append)]
175 delegate: Vec<Did>,
176
177 #[arg(long, short)]
179 #[arg(value_name = "DID")]
180 #[arg(action = clap::ArgAction::Append)]
181 rescind: Vec<Did>,
182
183 #[arg(long)]
185 threshold: Option<usize>,
186
187 #[arg(long)]
189 #[arg(value_parser = EditVisibilityParser)]
190 visibility: Option<EditVisibility>,
191
192 #[arg(long)]
194 #[arg(value_name = "DID")]
195 #[arg(action = clap::ArgAction::Append)]
196 allow: Vec<Did>,
197
198 #[arg(long)]
200 #[arg(value_name = "DID")]
201 #[arg(action = clap::ArgAction::Append)]
202 disallow: Vec<Did>,
203
204 #[arg(long)]
212 #[arg(value_names = ["TYPE", "KEY", "VALUE"], num_args = 3)]
213 payload: Vec<String>,
214
215 #[arg(long)]
217 edit: bool,
218 },
219
220 #[clap(alias("l"))]
222 List,
223
224 #[clap(alias("s"))]
226 Show {
227 #[arg(value_name = "REVISION_ID")]
229 revision: Rev,
230 },
231
232 #[clap(alias("d"))]
234 Redact {
235 #[arg(value_name = "REVISION_ID")]
237 revision: Rev,
238 },
239}
240
241#[cfg(test)]
242mod test {
243 use super::{parse_many_upserts, Args};
244 use clap::error::ErrorKind;
245 use clap::Parser;
246
247 #[test]
248 fn should_parse_single_payload() {
249 let args = Args::try_parse_from(["id", "update", "--payload", "key", "name", "value"]);
250 assert!(args.is_ok())
251 }
252
253 #[test]
254 fn should_not_parse_single_payload() {
255 let err = Args::try_parse_from(["id", "update", "--payload", "key", "name"]).unwrap_err();
256 assert_eq!(err.kind(), ErrorKind::WrongNumberOfValues);
257 }
258
259 #[test]
260 fn should_parse_multiple_payloads() {
261 let args = Args::try_parse_from([
262 "id",
263 "update",
264 "--payload",
265 "key_1",
266 "name_1",
267 "value_1",
268 "--payload",
269 "key_2",
270 "name_2",
271 "value_2",
272 ]);
273 assert!(args.is_ok())
274 }
275
276 #[test]
277 fn should_not_parse_single_payloads() {
278 let err = Args::try_parse_from([
279 "id",
280 "update",
281 "--payload",
282 "key_1",
283 "name_1",
284 "value_1",
285 "--payload",
286 "key_2",
287 "name_2",
288 ])
289 .unwrap_err();
290 assert_eq!(err.kind(), ErrorKind::WrongNumberOfValues);
291 }
292
293 #[test]
294 fn should_not_clobber_payload_args() {
295 let err = Args::try_parse_from([
296 "id",
297 "update",
298 "--payload",
299 "key_1",
300 "name_1",
301 "--payload", "key_2",
303 "name_2",
304 "value_2",
305 ])
306 .unwrap_err();
307 assert_eq!(err.kind(), ErrorKind::WrongNumberOfValues);
308 }
309
310 #[test]
311 fn should_parse_into_payload() {
312 let payload: Result<Vec<_>, _> = parse_many_upserts(&[
313 "xyz.radicle.project".to_string(),
314 "name".to_string(),
315 "{}".to_string(),
316 ])
317 .collect();
318 assert!(payload.is_ok())
319 }
320
321 #[test]
322 #[should_panic(expected = "assertion failed: chunks.remainder().is_empty()")]
323 fn should_not_parse_into_payload() {
324 let _: Result<Vec<_>, _> =
325 parse_many_upserts(&["xyz.radicle.project".to_string(), "name".to_string()]).collect();
326 }
327}