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, ConfigError, 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
153                .get_mut(&key)
154                .ok_or_else(|| ConfigError::Custom(format!("{key} does not exist")))?;
155            print_value(value)?;
156        }
157        Operation::Set(key, value) => {
158            let mut temp_config = RawConfig::from_file(&path)?;
159            let value = temp_config.set(&key.into(), value.into())?;
160            temp_config.write(&path)?;
161            print_value(&value)?;
162        }
163        Operation::Push(key, value) => {
164            let mut temp_config = RawConfig::from_file(&path)?;
165            let value = temp_config.push(&key.into(), value.into())?;
166            temp_config.write(&path)?;
167            print_value(&value)?;
168        }
169        Operation::Remove(key, value) => {
170            let mut temp_config = RawConfig::from_file(&path)?;
171            let value = temp_config.remove(&key.into(), value.into())?;
172            temp_config.write(&path)?;
173            print_value(&value)?;
174        }
175        Operation::Unset(key) => {
176            let mut temp_config = RawConfig::from_file(&path)?;
177            let value = temp_config.unset(&key.into())?;
178            temp_config.write(&path)?;
179            print_value(&value)?;
180        }
181        Operation::Init => {
182            if path.try_exists()? {
183                anyhow::bail!("configuration file already exists at `{}`", path.display());
184            }
185            Config::init(
186                options.alias.ok_or(anyhow!(
187                    "an alias must be provided to initialize a new configuration"
188                ))?,
189                &path,
190            )?;
191            term::success!(
192                "Initialized new Radicle configuration at {}",
193                path.display()
194            );
195        }
196        Operation::Edit => match term::editor::Editor::new(&path)?.extension("json").edit()? {
197            Some(_) => {
198                term::success!("Successfully made changes to the configuration at {path:?}")
199            }
200            None => term::info!("No changes were made to the configuration at {path:?}"),
201        },
202    }
203
204    Ok(())
205}
206
207/// Print a JSON Value.
208fn print_value(value: &serde_json::Value) -> anyhow::Result<()> {
209    match value {
210        serde_json::Value::Null => {}
211        serde_json::Value::Bool(b) => term::print(b),
212        serde_json::Value::Array(a) => a.iter().try_for_each(print_value)?,
213        serde_json::Value::Number(n) => term::print(n),
214        serde_json::Value::String(s) => term::print(s),
215        serde_json::Value::Object(o) => {
216            term::json::to_pretty(&o, Path::new("config.json"))?.print()
217        }
218    }
219    Ok(())
220}