master
Ondřej Hruška 4 years ago
commit 02ec18a88f
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 2
      .gitignore
  2. 16
      Cargo.toml
  3. 210
      src/lib.rs

2
.gitignore vendored

@ -0,0 +1,2 @@
/target
Cargo.lock

@ -0,0 +1,16 @@
[package]
name = "cli-app-base"
version = "0.1.0"
authors = ["Ondřej Hruška <ondra@ondrovo.com>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
log = "0.4"
env_logger = "0.7.1"
failure = "0.1"
serde = "1.0.106"
serde_json = "1.0.51"
clap = "2.33.0"
json5 = "0.2.7"

@ -0,0 +1,210 @@
#[macro_use]
extern crate log;
use failure::{bail, Fallible};
use std::collections::HashMap;
use std::str::FromStr;
use serde::{Serialize};
use std::fmt::Debug;
use std::path::{Path, PathBuf};
use std::fs::File;
use std::io::Read;
use serde::de::DeserializeOwned;
pub const LOG_LEVELS: [&str; 5] = ["error", "warn", "info", "debug", "trace"];
/// 3rd-party libraries that produce log spam - we set these to a fixed higher level
/// to allow using e.g. TRACE without drowning our custom messages
pub const SPAMMY_LIBS: [&str; 6] = [
"tokio_reactor",
"hyper",
"reqwest",
"mio",
"want",
"rumqtt",
];
/// Implement this for the main config struct
pub trait BoilerplateCfg {
type Init;
/// Get log level
fn logging(&self) -> &String;
/// Get names of library modules to suppress from log output (limit to warn or error)
fn logging_suppress_mods(&self) -> Option<Vec<&str>> {
Some(Vec::from(&SPAMMY_LIBS[..]))
}
/// Get log module levels to use (take priority over the main log level)
fn logging_mod_levels(&self) -> Option<HashMap<String, String>> {
None
}
/// Add args to later use in the `configure` method.
fn add_args<'a: 'b, 'b>(clap: clap::App<'a, 'b>) -> clap::App<'a, 'b> {
// Default impl
clap
}
/// Configure the config object using args.
/// Logging has already been inited and configured.
fn configure<'a>(self, _clap: &clap::ArgMatches<'a>) -> Fallible<Self::Init>;
}
pub trait Boilerplate : BoilerplateCfg + Sized {
fn init(name: &str, cfg_file_name: &str, version : Option<String>) -> Fallible<Self::Init>;
}
fn read_file<P: AsRef<Path>>(path: P) -> Fallible<String> {
let path = path.as_ref();
let mut file = File::open(path)?;
let mut buf = String::new();
file.read_to_string(&mut buf)?;
Ok(buf)
}
impl<CFG : BoilerplateCfg + Serialize + DeserializeOwned + Debug + Default> Boilerplate for CFG {
/// Initialize the app
fn init(name: &str, cfg_file_name: &str, version : Option<String>) -> Fallible<CFG::Init>
{
let version : String = version.unwrap_or_else(|| env!("CARGO_PKG_VERSION").into());
let clap =
clap::App::new(name)
.arg(clap::Arg::with_name("config")
.short("c").long("config")
.value_name("FILE")
.help("Sets a custom config file (JSON5)")
.takes_value(true))
.arg(clap::Arg::with_name("dump-config")
.long("dump-config")
.takes_value(false)
.help("Print the loaded config struct"))
.arg(clap::Arg::with_name("default-config")
.long("default-config")
.takes_value(false)
.help("Print the default config JSON for reference (or to be piped to a file)"))
.arg(clap::Arg::with_name("verbose")
.short("v").long("verbose").multiple(true)
.help("Increase logging verbosity (repeat to increase)"))
.arg(clap::Arg::with_name("log-level")
.long("log")
.takes_value(true).value_name("LEVEL")
.validator(|s| {
if LOG_LEVELS.contains(&s.as_str()) { return Ok(()); }
Err(format!("Bad log level: {}", s))
})
.help("Set logging verbosity (error,warning,info,debug,trace)"));
let clap = Self::add_args(clap);
// this must be done after `add_args` or all hell breaks loose around lifetimes
let clap = clap
.version(version.as_str());
let argv = clap.get_matches();
if argv.is_present("default-config") {
println!("{}", serde_json::to_string_pretty(&CFG::default()).unwrap());
std::process::exit(0);
}
/* Load config */
let cfg_file = argv.value_of("config").unwrap_or(cfg_file_name);
println!("{} {}\nRun with -h for help", name, version);
let filepath = PathBuf::from_str(cfg_file)?;
let config: CFG = if filepath.is_file() {
println!("Load config from \"{}\"", cfg_file);
let buf = read_file(filepath)?;
json5::from_str(&buf)?
} else {
println!("Config file \"{}\" does not exist, using defaults.", cfg_file);
CFG::default()
};
let mut level = config.logging().as_str();
if !LOG_LEVELS.contains(&level) {
bail!("Invalid default log level: {}", level);
}
/* env RUST_LOG overrides default if set, but can be changed by CLI args */
let env_level = option_env!("RUST_LOG").unwrap_or("");
if !env_level.is_empty() {
level = env_level;
if !LOG_LEVELS.contains(&level) {
bail!("Invalid env log level: {}", level);
}
}
/* Explicitly requested level */
if let Some(l) = argv.value_of("log-level") {
level = l; // validated by clap
}
/* Verbosity increased */
if argv.is_present("verbose") {
// bump verbosity if -v's are present
let pos = LOG_LEVELS
.iter()
.position(|x| x == &level)
.unwrap();
level = match LOG_LEVELS
.iter()
.nth(pos + argv.occurrences_of("verbose") as usize)
{
Some(new_level) => new_level,
None => "trace",
};
}
let env = env_logger::Env::default().default_filter_or(level);
let mut builder = env_logger::Builder::from_env(env);
// add a newline
println!();
builder.format_timestamp_millis();
if let Some(suppress_mods) = config.logging_suppress_mods() {
// set logging level for suppressed libs
let mut sup_lvl = log::LevelFilter::Info;
if level == "warn" {
sup_lvl = log::LevelFilter::Warn;
} else if level == "error" {
sup_lvl = log::LevelFilter::Error;
}
for lib in suppress_mods {
builder.filter_module(lib, sup_lvl);
}
}
if let Some(mod_levels) = config.logging_mod_levels() {
for (module, lvl) in mod_levels {
let lvl = log::LevelFilter::from_str(lvl.as_ref())?;
builder.filter_module(module.as_ref(), lvl);
}
}
builder.init();
if argv.is_present("dump-config") {
debug!("Loaded config: {:#?}", config);
}
config.configure(&argv)
}
}
Loading…
Cancel
Save