radicle_cli/commands/
config.rs1#![allow(clippy::or_fun_call)]
2use std::ffi::OsString;
3use std::path::Path;
4use std::str::FromStr;
5
6use anyhow::anyhow;
7use radicle::node::Alias;
8use radicle::profile::{config, Config, ConfigPath, RawConfig};
9
10use crate::terminal as term;
11use crate::terminal::args::{Args, Error, Help};
12use crate::terminal::Element as _;
13
14pub const HELP: Help = Help {
15 name: "config",
16 description: "Manage your local Radicle configuration",
17 version: env!("RADICLE_VERSION"),
18 usage: r#"
19Usage
20
21 rad config [<option>...]
22 rad config show [<option>...]
23 rad config init --alias <alias> [<option>...]
24 rad config edit [<option>...]
25 rad config get <key> [<option>...]
26 rad config schema [<option>...]
27 rad config set <key> <value> [<option>...]
28 rad config unset <key> [<option>...]
29 rad config push <key> <value> [<option>...]
30 rad config remove <key> <value> [<option>...]
31
32 If no argument is specified, prints the current radicle configuration as JSON.
33 To initialize a new configuration file, use `rad config init`.
34
35Options
36
37 --help Print help
38
39"#,
40};
41
42#[derive(Default)]
43enum Operation {
44 #[default]
45 Show,
46 Get(String),
47 Schema,
48 Set(String, String),
49 Push(String, String),
50 Remove(String, String),
51 Unset(String),
52 Init,
53 Edit,
54}
55
56pub struct Options {
57 op: Operation,
58 alias: Option<Alias>,
59}
60
61impl Args for Options {
62 fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
63 use lexopt::prelude::*;
64
65 let mut parser = lexopt::Parser::from_args(args);
66 let mut op: Option<Operation> = None;
67 let mut alias = None;
68
69 #[allow(clippy::never_loop)]
70 while let Some(arg) = parser.next()? {
71 match arg {
72 Long("help") | Short('h') => {
73 return Err(Error::Help.into());
74 }
75 Long("alias") => {
76 let value = parser.value()?;
77 let input = value.to_string_lossy();
78 let input = Alias::from_str(&input)?;
79
80 alias = Some(input);
81 }
82 Value(val) if op.is_none() => match val.to_string_lossy().as_ref() {
83 "show" => op = Some(Operation::Show),
84 "schema" => op = Some(Operation::Schema),
85 "edit" => op = Some(Operation::Edit),
86 "init" => op = Some(Operation::Init),
87 "get" => {
88 let key = parser.value()?;
89 let key = key.to_string_lossy();
90 op = Some(Operation::Get(key.to_string()));
91 }
92 "set" => {
93 let key = parser.value()?;
94 let key = key.to_string_lossy();
95 let value = parser.value()?;
96 let value = value.to_string_lossy();
97
98 op = Some(Operation::Set(key.to_string(), value.to_string()));
99 }
100 "push" => {
101 let key = parser.value()?;
102 let key = key.to_string_lossy();
103 let value = parser.value()?;
104 let value = value.to_string_lossy();
105
106 op = Some(Operation::Push(key.to_string(), value.to_string()));
107 }
108 "remove" => {
109 let key = parser.value()?;
110 let key = key.to_string_lossy();
111 let value = parser.value()?;
112 let value = value.to_string_lossy();
113
114 op = Some(Operation::Remove(key.to_string(), value.to_string()));
115 }
116 "unset" => {
117 let key = parser.value()?;
118 let key = key.to_string_lossy();
119 op = Some(Operation::Unset(key.to_string()));
120 }
121 unknown => anyhow::bail!("unknown operation '{unknown}'"),
122 },
123 _ => return Err(anyhow!(arg.unexpected())),
124 }
125 }
126
127 Ok((
128 Options {
129 op: op.unwrap_or_default(),
130 alias,
131 },
132 vec![],
133 ))
134 }
135}
136
137pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
138 let home = ctx.home()?;
139 let path = home.config();
140
141 match options.op {
142 Operation::Show => {
143 let profile = ctx.profile()?;
144 term::json::to_pretty(&profile.config, path.as_path())?.print();
145 }
146 Operation::Schema => {
147 term::json::to_pretty(&schemars::schema_for!(Config), path.as_path())?.print();
148 }
149 Operation::Get(key) => {
150 let mut temp_config = RawConfig::from_file(&path)?;
151 let key: ConfigPath = key.into();
152 let value = temp_config.get_mut(&key).ok_or_else(|| {
153 anyhow::anyhow!("{key} does not exist in configuration found at {path:?}")
154 })?;
155 print_value(value)?;
156 }
157 Operation::Set(key, value) => {
158 let value = modify(path, |tmp| tmp.set(&key.into(), value.into()))?;
159 print_value(&value)?;
160 }
161 Operation::Push(key, value) => {
162 let value = modify(path, |tmp| tmp.push(&key.into(), value.into()))?;
163 print_value(&value)?;
164 }
165 Operation::Remove(key, value) => {
166 let value = modify(path, |tmp| tmp.remove(&key.into(), value.into()))?;
167 print_value(&value)?;
168 }
169 Operation::Unset(key) => {
170 let value = modify(path, |tmp| tmp.unset(&key.into()))?;
171 print_value(&value)?;
172 }
173 Operation::Init => {
174 if path.try_exists()? {
175 anyhow::bail!("configuration file already exists at `{}`", path.display());
176 }
177 Config::init(
178 options.alias.ok_or(anyhow!(
179 "an alias must be provided to initialize a new configuration"
180 ))?,
181 &path,
182 )?;
183 term::success!(
184 "Initialized new Radicle configuration at {}",
185 path.display()
186 );
187 }
188 Operation::Edit => match term::editor::Editor::new(&path)?.extension("json").edit()? {
189 Some(_) => {
190 term::success!("Successfully made changes to the configuration at {path:?}")
191 }
192 None => term::info!("No changes were made to the configuration at {path:?}"),
193 },
194 }
195
196 Ok(())
197}
198
199fn modify<P, M>(path: P, modification: M) -> anyhow::Result<serde_json::Value>
200where
201 P: AsRef<Path>,
202 M: FnOnce(&mut RawConfig) -> Result<serde_json::Value, config::ModifyError>,
203{
204 let path = path.as_ref();
205 let mut temp_config = RawConfig::from_file(path)?;
206 let value = modification(&mut temp_config).map_err(|err| {
207 anyhow::anyhow!("failed to modify configuration found at {path:?} due to {err}")
208 })?;
209 temp_config.write(path)?;
210 Ok(value)
211}
212
213fn print_value(value: &serde_json::Value) -> anyhow::Result<()> {
215 match value {
216 serde_json::Value::Null => {}
217 serde_json::Value::Bool(b) => term::print(b),
218 serde_json::Value::Array(a) => a.iter().try_for_each(print_value)?,
219 serde_json::Value::Number(n) => term::print(n),
220 serde_json::Value::String(s) => term::print(s),
221 serde_json::Value::Object(o) => {
222 term::json::to_pretty(&o, Path::new("config.json"))?.print()
223 }
224 }
225 Ok(())
226}