radicle_cli/commands/
config.rs

1#![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
213/// Print a JSON Value.
214fn 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}