cli: Fix `rad config` bugs

The command was unable to deal with an invalid config. We fix this by
introducing a way to load the user's home without loading the config.
This commit is contained in:
cloudhead 2024-02-26 12:10:34 +01:00
parent b0fbbeed6b
commit d86d99e002
No known key found for this signature in database
3 changed files with 55 additions and 27 deletions

View File

@ -2,8 +2,11 @@
use std::ffi::OsString; use std::ffi::OsString;
use std::path::Path; use std::path::Path;
use std::process; use std::process;
use std::str::FromStr;
use anyhow::anyhow; use anyhow::anyhow;
use radicle::node::Alias;
use radicle::profile::Config;
use crate::terminal as term; use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help}; use crate::terminal::args::{Args, Error, Help};
@ -18,11 +21,10 @@ Usage
rad config [<option>...] rad config [<option>...]
rad config show [<option>...] rad config show [<option>...]
rad config init [<option>...] rad config init --alias <alias> [<option>...]
rad config edit [<option>...] rad config edit [<option>...]
rad config get <key> [<option>...] rad config get <key> [<option>...]
If no argument is specified, prints the current radicle configuration as JSON. If no argument is specified, prints the current radicle configuration as JSON.
To initialize a new configuration file, use `rad config init`. To initialize a new configuration file, use `rad config init`.
@ -44,6 +46,7 @@ enum Operation {
pub struct Options { pub struct Options {
op: Operation, op: Operation,
alias: Option<Alias>,
} }
impl Args for Options { impl Args for Options {
@ -52,6 +55,7 @@ impl Args for Options {
let mut parser = lexopt::Parser::from_args(args); let mut parser = lexopt::Parser::from_args(args);
let mut op: Option<Operation> = None; let mut op: Option<Operation> = None;
let mut alias = None;
#[allow(clippy::never_loop)] #[allow(clippy::never_loop)]
while let Some(arg) = parser.next()? { while let Some(arg) = parser.next()? {
@ -59,6 +63,13 @@ impl Args for Options {
Long("help") | Short('h') => { Long("help") | Short('h') => {
return Err(Error::Help.into()); return Err(Error::Help.into());
} }
Long("alias") => {
let value = parser.value()?;
let input = value.to_string_lossy();
let input = Alias::from_str(&input)?;
alias = Some(input);
}
Value(val) if op.is_none() => match val.to_string_lossy().as_ref() { Value(val) if op.is_none() => match val.to_string_lossy().as_ref() {
"show" => op = Some(Operation::Show), "show" => op = Some(Operation::Show),
"edit" => op = Some(Operation::Edit), "edit" => op = Some(Operation::Edit),
@ -78,6 +89,7 @@ impl Args for Options {
Ok(( Ok((
Options { Options {
op: op.unwrap_or_default(), op: op.unwrap_or_default(),
alias,
}, },
vec![], vec![],
)) ))
@ -85,14 +97,16 @@ impl Args for Options {
} }
pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let profile = ctx.profile()?; let home = ctx.home()?;
let path = profile.home.config(); let path = home.config();
match options.op { match options.op {
Operation::Show => { Operation::Show => {
let profile = ctx.profile()?;
term::json::to_pretty(&profile.config, path.as_path())?.print(); term::json::to_pretty(&profile.config, path.as_path())?.print();
} }
Operation::Get(key) => { Operation::Get(key) => {
let profile = ctx.profile()?;
let data = serde_json::to_value(profile.config)?; let data = serde_json::to_value(profile.config)?;
if let Some(value) = get_value(&data, &key) { if let Some(value) = get_value(&data, &key) {
print_value(value)?; print_value(value)?;
@ -102,7 +116,16 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
if path.try_exists()? { if path.try_exists()? {
anyhow::bail!("configuration file already exists at `{}`", path.display()); anyhow::bail!("configuration file already exists at `{}`", path.display());
} }
profile.config.write(&path)?; Config::init(
options.alias.ok_or(anyhow!(
"an alias must be provided to initialize a new configuration"
))?,
&path,
)?;
term::success!(
"Initialized new Radicle configuration at {}",
path.display()
);
} }
Operation::Edit => { Operation::Edit => {
let Some(cmd) = term::editor::default_editor() else { let Some(cmd) = term::editor::default_editor() else {

View File

@ -75,7 +75,7 @@ fn print_help() -> anyhow::Result<()> {
println!("{DESCRIPTION}"); println!("{DESCRIPTION}");
println!(); println!();
rad_help::run(Default::default(), term::profile) rad_help::run(Default::default(), term::DefaultContext)
} }
fn run(command: Command) -> Result<(), Option<anyhow::Error>> { fn run(command: Command) -> Result<(), Option<anyhow::Error>> {

View File

@ -14,7 +14,7 @@ use std::process;
pub use radicle_term::*; pub use radicle_term::*;
use radicle::profile::Profile; use radicle::profile::{Home, Profile};
use crate::terminal; use crate::terminal;
@ -22,20 +22,17 @@ use crate::terminal;
pub trait Context { pub trait Context {
/// Return the currently active profile, or an error if no profile is active. /// Return the currently active profile, or an error if no profile is active.
fn profile(&self) -> Result<Profile, anyhow::Error>; fn profile(&self) -> Result<Profile, anyhow::Error>;
/// Return the Radicle home.
fn home(&self) -> Result<Home, std::io::Error>;
} }
impl Context for Profile { impl Context for Profile {
fn profile(&self) -> Result<Profile, anyhow::Error> { fn profile(&self) -> Result<Profile, anyhow::Error> {
Ok(self.clone()) Ok(self.clone())
} }
}
impl<F> Context for F fn home(&self) -> Result<Home, std::io::Error> {
where Ok(self.home.clone())
F: Fn() -> Result<Profile, anyhow::Error>,
{
fn profile(&self) -> Result<Profile, anyhow::Error> {
self()
} }
} }
@ -57,7 +54,7 @@ where
pub fn run_command<A, C>(help: Help, cmd: C) -> ! pub fn run_command<A, C>(help: Help, cmd: C) -> !
where where
A: Args, A: Args,
C: Command<A, fn() -> anyhow::Result<Profile>>, C: Command<A, DefaultContext>,
{ {
let args = std::env::args_os().skip(1).collect(); let args = std::env::args_os().skip(1).collect();
@ -67,7 +64,7 @@ where
pub fn run_command_args<A, C>(help: Help, cmd: C, args: Vec<OsString>) -> ! pub fn run_command_args<A, C>(help: Help, cmd: C, args: Vec<OsString>) -> !
where where
A: Args, A: Args,
C: Command<A, fn() -> anyhow::Result<Profile>>, C: Command<A, DefaultContext>,
{ {
use io as term; use io as term;
@ -108,7 +105,7 @@ where
} }
}; };
match cmd.run(options, self::profile) { match cmd.run(options, DefaultContext) {
Ok(()) => process::exit(0), Ok(()) => process::exit(0),
Err(err) => { Err(err) => {
terminal::fail(help.name, &err); terminal::fail(help.name, &err);
@ -117,17 +114,25 @@ where
} }
} }
/// Get the default profile. Fails if there is no profile. /// Gets the default profile. Fails if there is no profile.
pub fn profile() -> Result<Profile, anyhow::Error> { pub struct DefaultContext;
match Profile::load() {
Ok(profile) => Ok(profile), impl Context for DefaultContext {
Err(radicle::profile::Error::NotFound(path)) => Err(args::Error::WithHint { fn home(&self) -> Result<Home, std::io::Error> {
err: anyhow::anyhow!("Radicle profile not found in '{}'.", path.display()), radicle::profile::home()
hint: "To setup your radicle profile, run `rad auth`.", }
fn profile(&self) -> Result<Profile, anyhow::Error> {
match Profile::load() {
Ok(profile) => Ok(profile),
Err(radicle::profile::Error::NotFound(path)) => Err(args::Error::WithHint {
err: anyhow::anyhow!("Radicle profile not found in '{}'.", path.display()),
hint: "To setup your radicle profile, run `rad auth`.",
}
.into()),
Err(radicle::profile::Error::Config(e)) => Err(e.into()),
Err(e) => Err(anyhow::anyhow!("Could not load radicle profile: {e}")),
} }
.into()),
Err(radicle::profile::Error::Config(e)) => Err(e.into()),
Err(e) => Err(anyhow::anyhow!("Could not load radicle profile: {e}")),
} }
} }