create custom CError trait, using it for errors
The name chosen here is perhaps not ideal, and if you can think of a better one that should probably be quickly used instead. This is one option for C exit-code handling which is a lot cleaner than the existing implementation in my opinion. Much of the cleanup also comes from the enum error type in config. This enum's implementation could be a lot cleaner with something like the 'thiserror' crate: the message method could simply utilize that crate's derived Display implementation. Regarding `unwrap_or_else(|e| e.exit())`, it would have been ideal to simply handle panics with a handler so it could simply be `unwrap()`, but since panics tend to only pass the error string to the handler, an exit method seems like the best solution. Because this enforces consistency of adding the program invokation before every error message, it has an unfortunate side-effect of the usage text getting that information duplicated which may be seen as undesirable. There are some workarounds to this, but I've deferred deciding how best to do that (if it even is desirable to do so).
This commit is contained in:
parent
a0453f1cc3
commit
3eac2d2eab
102
src/config.rs
102
src/config.rs
|
@ -20,7 +20,7 @@
|
|||
|
||||
use std::{
|
||||
fs::File,
|
||||
io::Read,
|
||||
io::{Read, self},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
|
@ -31,6 +31,8 @@ use yacexits::{
|
|||
EX_UNAVAILABLE,
|
||||
};
|
||||
|
||||
use crate::error::CError;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Config {
|
||||
pub hopfiles: Vec<String>,
|
||||
|
@ -42,56 +44,64 @@ pub struct Sources {
|
|||
pub modrinth: Vec<String>,
|
||||
}
|
||||
|
||||
pub fn get_config(dirs: BaseDirectories) -> Result<PathBuf, (String, u32)> {
|
||||
match dirs.place_config_file("config.toml") {
|
||||
Ok(file) => Ok(file),
|
||||
Err(_) => {
|
||||
Err((
|
||||
format!("Unable to create configuration file."),
|
||||
EX_UNAVAILABLE,
|
||||
))
|
||||
},
|
||||
}
|
||||
pub enum ConfigError {
|
||||
CreateFailed(io::Error),
|
||||
OpenError(io::Error),
|
||||
ReadError(io::Error),
|
||||
FormatError(std::string::FromUtf8Error),
|
||||
ParseError(toml::de::Error),
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn read_config(config_path: PathBuf) -> Result<Self, (String, u32)> {
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
|
||||
let mut config_file = match File::open(&config_path) {
|
||||
Ok(file) => file,
|
||||
Err(_) => {
|
||||
return Err((
|
||||
format!("{}: Permission denied.", config_path.display()),
|
||||
EX_UNAVAILABLE,
|
||||
));
|
||||
impl CError for ConfigError {
|
||||
fn message(&self) -> String {
|
||||
match self {
|
||||
Self::CreateFailed(err) => {
|
||||
format!("Unable to create configuration file: {}", err)
|
||||
},
|
||||
};
|
||||
|
||||
if let Some(err) = config_file.read_to_end(&mut buf).err() {
|
||||
return Err((format!("{:?}", err), EX_DATAERR));
|
||||
};
|
||||
|
||||
let toml = match String::from_utf8(buf) {
|
||||
Ok(contents) => contents,
|
||||
Err(err) => {
|
||||
return Err((
|
||||
format!("{:?}", err),
|
||||
EX_DATAERR,
|
||||
));
|
||||
Self::OpenError(err) => {
|
||||
format!("Unable to open configuration file: {}", err)
|
||||
},
|
||||
};
|
||||
|
||||
match toml::from_str(&toml) {
|
||||
Ok(val) => Ok(val),
|
||||
Err(_) => {
|
||||
Err((
|
||||
format!(
|
||||
"{}: Invalid configuration file.", config_path.display()
|
||||
),
|
||||
EX_DATAERR,
|
||||
))
|
||||
Self::ReadError(err) => {
|
||||
format!("Error while reading configuration file: {}", err)
|
||||
},
|
||||
Self::FormatError(err) => {
|
||||
format!("Configuration file is not valid utf-8: {}", err)
|
||||
},
|
||||
Self::ParseError(err) => {
|
||||
format!("Unable to parse configuration file: {}", err)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn code(&self) -> u32 {
|
||||
match self {
|
||||
Self::CreateFailed(_) => EX_UNAVAILABLE,
|
||||
Self::OpenError(_) => EX_UNAVAILABLE,
|
||||
Self::ReadError(_) => EX_DATAERR,
|
||||
Self::FormatError(_) => EX_DATAERR,
|
||||
Self::ParseError(_) => EX_DATAERR,
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_config(dirs: BaseDirectories) -> Result<PathBuf, ConfigError> {
|
||||
dirs.place_config_file("config.toml").map_err(ConfigError::CreateFailed)
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn read_config(config_path: PathBuf) -> Result<Self, ConfigError> {
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
|
||||
let mut config_file = File::open(&config_path)
|
||||
.map_err(ConfigError::OpenError)?;
|
||||
|
||||
config_file.read_to_end(&mut buf)
|
||||
.map_err(ConfigError::ReadError)?;
|
||||
|
||||
let toml = String::from_utf8(buf)
|
||||
.map_err(ConfigError::FormatError)?;
|
||||
|
||||
toml::from_str(&toml).map_err(ConfigError::ParseError)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
use yacexits::*;
|
||||
|
||||
pub trait CError {
|
||||
fn code(&self) -> u32;
|
||||
|
||||
fn message(&self) -> String;
|
||||
|
||||
fn exit(&self) -> ! {
|
||||
eprintln!("{}: {}", program_invokation(), self.message());
|
||||
exit(self.code());
|
||||
}
|
||||
}
|
||||
|
||||
fn program_invokation() -> String {
|
||||
// TODO: ideally this would be argv[0] from main.
|
||||
// This could be done with a const OnceCell, but I'm not sure I like that solution.
|
||||
// Using std, we can do this though:
|
||||
std::env::args().next()
|
||||
// with a fallback to the program name
|
||||
.unwrap_or_else(|| env!("CARGO_PKG_NAME").to_owned())
|
||||
}
|
||||
|
||||
impl<'l> CError for arg::ParseKind<'l> {
|
||||
fn message(&self) -> String {
|
||||
format!(
|
||||
"Usage: {}{}",
|
||||
program_invokation(), // argv[0],
|
||||
" [-v] add | get | init | list | remove | update\n\n".to_owned() +
|
||||
"add [-m version] [-f hopfiles...] packages...\n" +
|
||||
"get [-n] [-d directory] [-m versions...] [-t types...] packages\n" +
|
||||
"init [-f hopfiles...] version type\n" +
|
||||
"list [[-f hopfiles...] | [-m versions...] [-t types...]]\n" +
|
||||
"remove [[-f hopfiles...] | type version]] packages...\n" +
|
||||
"update [[-f hopfiles... | [-m versions...] [-t types...]]",
|
||||
)
|
||||
}
|
||||
|
||||
fn code(&self) -> u32 { EX_USAGE }
|
||||
}
|
||||
|
||||
impl CError for xdg::BaseDirectoriesError {
|
||||
fn message(&self) -> String {
|
||||
format!("Unable to open configuration file: {}", self)
|
||||
}
|
||||
|
||||
fn code(&self) -> u32 { EX_UNAVAILABLE }
|
||||
}
|
54
src/main.rs
54
src/main.rs
|
@ -26,18 +26,18 @@ mod args;
|
|||
mod client;
|
||||
mod config;
|
||||
mod hopfile;
|
||||
mod error;
|
||||
|
||||
use api::*;
|
||||
use args::*;
|
||||
use client::*;
|
||||
use config::*;
|
||||
use hopfile::*;
|
||||
use error::*;
|
||||
|
||||
use yacexits::{
|
||||
exit,
|
||||
EX_SOFTWARE,
|
||||
EX_UNAVAILABLE,
|
||||
EX_USAGE,
|
||||
};
|
||||
|
||||
struct AppContext {
|
||||
|
@ -50,55 +50,19 @@ struct AppContext {
|
|||
async fn rust_main(arguments: c_main::Args) {
|
||||
let argv: Vec<&str> = arguments.into_iter().collect();
|
||||
|
||||
let usage_info = format!(
|
||||
"Usage: {}{}",
|
||||
argv[0],
|
||||
" [-v] add | get | init | list | remove | update\n\n".to_owned() +
|
||||
"add [-m version] [-f hopfiles...] packages...\n" +
|
||||
"get [-n] [-d directory] [-m versions...] [-t types...] packages\n" +
|
||||
"init [-f hopfiles...] version type\n" +
|
||||
"list [[-f hopfiles...] | [-m versions...] [-t types...]]\n" +
|
||||
"remove [[-f hopfiles...] | type version]] packages...\n" +
|
||||
"update [[-f hopfiles... | [-m versions...] [-t types...]]",
|
||||
);
|
||||
|
||||
|
||||
let args = Arguments::from_args(
|
||||
argv
|
||||
.clone()
|
||||
.into_iter()
|
||||
).unwrap_or_else(|_| {
|
||||
eprintln!("{}", usage_info);
|
||||
exit(EX_USAGE);
|
||||
});
|
||||
).unwrap_or_else(|e| e.exit());
|
||||
|
||||
let xdg_basedirs = xdg::BaseDirectories::with_prefix("hopper");
|
||||
|
||||
// this might be cursed; I haven’t decided
|
||||
let config = match get_config(
|
||||
xdg_basedirs.unwrap_or_else(|_| {
|
||||
eprintln!(
|
||||
"{}: Unable to open configuration file: Permission denied.",
|
||||
argv[0],
|
||||
);
|
||||
exit(EX_UNAVAILABLE);
|
||||
})
|
||||
) {
|
||||
Ok(path) => {
|
||||
match Config::read_config(path) {
|
||||
Ok(file) => file,
|
||||
Err((err, code)) => {
|
||||
eprintln!("{}: {}", argv[0], err);
|
||||
exit(code);
|
||||
},
|
||||
}
|
||||
},
|
||||
Err((err, code)) => {
|
||||
eprintln!("{}: {}", argv[0], err);
|
||||
exit(code);
|
||||
},
|
||||
};
|
||||
let xdg_basedirs = xdg::BaseDirectories::with_prefix("hopper")
|
||||
.unwrap_or_else(|e| e.exit());
|
||||
|
||||
let config = get_config(xdg_basedirs)
|
||||
.and_then(Config::read_config)
|
||||
.unwrap_or_else(|e| e.exit());
|
||||
|
||||
let ctx = AppContext { args, config };
|
||||
|
||||
match ctx.args.sub {
|
||||
|
|
Loading…
Reference in New Issue