improvements, add example, readme

master
Ondřej Hruška 4 years ago
parent e4d855fecc
commit c20e16d632
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 1
      .gitignore
  2. 27
      Cargo.toml
  3. 59
      README.md
  4. 5
      examples/foo.json
  5. 41
      examples/minimal.rs
  6. 159
      examples/rotn.rs
  7. 35
      src/lib.rs

1
.gitignore vendored

@ -1,2 +1,3 @@
/target
Cargo.lock
.idea

@ -1,17 +1,26 @@
[package]
name = "appbase"
name = "clappconfig"
version = "0.1.0"
authors = ["Ondřej Hruška <ondra@ondrovo.com>"]
edition = "2018"
publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
license = "MIT"
description = "Clap-based app config boilerplate: set up logging, load config, parse args"
repository = "https://git.ondrovo.com/packages/cli-app-base"
readme = "README.md"
keywords = ["clap", "cli", "logging", "boilerplate"]
categories = [
"command-line-interface"
]
[dependencies]
log = "0.4"
env_logger = "0.7.1"
env_logger = "0.7"
failure = "0.1"
serde = "1.0.106"
serde_json = "1.0.51"
clap = "2.33.0"
json5 = "0.2.7"
serde = "1.0"
serde_json = "1.0"
clap = "2.33"
json5 = "0.2"
[dev-dependencies]
smart-default = "0.6"
serde_derive = "1.0"

@ -0,0 +1,59 @@
# clappconfig - CLI app config boilerplate
```none
Clap
App
Config
-----------
clappconfig
```
This crate provides a simple CLI app boilerplate that takes care of
the most repetitive init tasks:
- Load JSON5 config from a custom or default path (or use `Default::default()`)
- Set up logging, with CLI and config-based overrides
- Optionally print the loaded or default config
- Show help / version on demand, courtesy of `clap`
- Optional start-up banner print
Supports custom `clap` arguments as well.
-> The repository is open to improvement PRs.
## Logging
- uses `env_logger` by default
- level can be set in the config file
- log level can be overridden by the `--log` CLI flag
- log level can be increased by repeated use of `-v` or `--verbose`
## Example
See the examples directory.
The example called "rotn" implements rot13 as a command-line tool.
```none
$ cargo run --example complete -- -h
Rot-N 0.1.0 by Ondřej Hruška <ondra@ondrovo.com>
USAGE:
rotn [FLAGS] [OPTIONS] --input <FILE>
FLAGS:
--default-config Print the default config JSON for reference (or to be piped to a file)
--dump-config Print the loaded config struct
-h, --help Prints help information
-V, --version Prints version information
-v, --verbose Increase logging verbosity (repeat to increase)
OPTIONS:
-c, --config <FILE> Sets a custom config file (JSON5)
-i, --input <FILE> Input file
--log <LEVEL> Set logging verbosity (error,warning,info,debug,trace)
-n, --shift <N> Positive or negative shift, default 13
```
To get rot13 of this README, run `cargo run --example rotn -- -iREADME.md`.

@ -0,0 +1,5 @@
// This is a JSON5 file with config for the "minimal.rs" example
{
// a foo
"foo": 1234,
}

@ -0,0 +1,41 @@
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate log;
use clap::ArgMatches;
use clappconfig::AppConfig;
use failure::Fallible;
#[derive(Serialize, Deserialize, Debug, Default)]
#[serde(default)]
struct Config {
foo: u32,
bar: u32,
}
impl AppConfig for Config {
type Init = Config;
fn logging(&self) -> &str {
"info"
}
fn configure<'a>(self, _clap: &ArgMatches<'a>) -> Fallible<Config> {
Ok(self)
}
}
fn main() -> Fallible<()> {
let cfg = Config::init("Foo", "examples/foo.json", None)?;
trace!("Trace message");
debug!("Debug message");
info!("Info message");
warn!("Warn message");
error!("Error message");
println!("Welcome to foo, config:\n{:#?}", cfg);
Ok(())
}

@ -0,0 +1,159 @@
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate failure;
#[macro_use]
extern crate log;
use clap::ArgMatches;
use clappconfig::AppConfig;
use failure::Fallible;
use smart_default::SmartDefault;
use std::collections::HashMap;
use std::fs::OpenOptions;
use std::io::Read;
use std::path::PathBuf;
use std::str::FromStr;
#[derive(Serialize, Deserialize, Debug, SmartDefault)]
#[serde(deny_unknown_fields)]
#[serde(default)]
struct Config {
#[default = "info"]
logging: String,
log_modules: HashMap<String, String>,
#[default = 13]
offset: i32,
}
struct Main {
config: Config,
input_file: PathBuf,
}
impl AppConfig for Config {
type Init = Main;
fn logging(&self) -> &str {
&self.logging
}
fn logging_suppress_mods(&self) -> Option<Vec<&str>> {
Some(vec![
"tokio_reactor",
"tokio_io",
"hyper",
"reqwest",
"mio",
"want",
])
}
/// Print startup banner.
/// May be overridden to customize or disable it.
fn print_banner(_name: &str, _version: &str) {
// Disable
}
/// Log messages printed before logging is set up
/// May be overridden to customize or disable it.
fn pre_log_println(_message: String) {
// Disable
}
/// Get log module levels to use (take priority over the main log level)
fn logging_mod_levels(&self) -> Option<&HashMap<String, String>> {
Some(&self.log_modules)
}
fn add_args<'a: 'b, 'b>(clap: clap::App<'a, 'b>) -> clap::App<'a, 'b> {
// Default impl
clap.arg(
clap::Arg::with_name("input")
.short("i")
.long("input")
.value_name("FILE")
.required_unless("default-config")
.help("Input file")
.takes_value(true),
)
.arg(
clap::Arg::with_name("offset")
.short("n")
.long("shift")
.value_name("N")
.help("Positive or negative shift, default 13")
.takes_value(true),
)
}
fn configure<'a>(mut self, clap: &ArgMatches<'a>) -> Fallible<Self::Init> {
let input = clap.value_of("input").unwrap().to_string();
debug!("Input: {}", input);
if input.is_empty() {
bail!("Input is required!");
}
let pb = PathBuf::from_str(&input)?.canonicalize()?;
debug!("Input path: {}", pb.display());
if !pb.is_file() {
return Err(format_err!("Input is not a file!"));
}
// Set offset by arg
if let Some(v) = clap.value_of("offset") {
self.offset = v.parse()?;
}
debug!("Rot raw: {}", self.offset);
self.offset = i32::rem_euclid(self.offset, 26);
debug!("Rot normalized: {}", self.offset);
Ok(Main {
config: self,
input_file: pb,
})
}
}
fn rot_n(string: String, rot: u8) -> String {
assert!(rot < 26);
const LOWER: &[u8] = b"abcdefghihjlmnopqrstuvwxyz";
const UPPER: &[u8] = b"ABCDEFGHIHJLMNOPQRSTUVWXYZ";
string
.chars()
.map(|c| match c {
'A'..='Z' => UPPER[((c as u8 - 'A' as u8 + rot) % 26) as usize] as char,
'a'..='z' => LOWER[((c as u8 - 'a' as u8 + rot) % 26) as usize] as char,
_ => c,
})
.collect()
}
fn main() -> Fallible<()> {
// Using custom version (default is CARGO_PKG_VERSION)
let version = format!(
"{} by {}",
env!("CARGO_PKG_VERSION"),
env!("CARGO_PKG_AUTHORS")
);
let main = Config::init("Rot-N", "rotn_config.json", Some(&version))?;
let mut f = OpenOptions::new().read(true).open(main.input_file)?;
let mut buf = String::new();
f.read_to_string(&mut buf)?;
println!("{}", rot_n(buf, main.config.offset as u8));
Ok(())
}

@ -18,7 +18,19 @@ pub trait AppConfig: Sized + Serialize + DeserializeOwned + Debug + Default {
type Init;
/// Get log level
fn logging(&self) -> &String;
fn logging(&self) -> &str;
/// Print startup banner.
/// May be overridden to customize or disable it.
fn print_banner(name: &str, version: &str) {
println!("{} {}\nRun with -h for help", name, version);
}
/// Log messages printed before logging is set up
/// May be overridden to customize or disable it.
fn pre_log_println(message: String) {
println!("{}", message);
}
/// Get names of library modules to suppress from log output (limit to warn or error)
fn logging_suppress_mods(&self) -> Option<Vec<&str>> {
@ -103,25 +115,26 @@ pub trait AppConfig: Sized + Serialize + DeserializeOwned + Debug + Default {
/* Load config */
let cfg_file = argv.value_of("config").unwrap_or(cfg_file_name);
println!("{} {}\nRun with -h for help", name, version);
Self::print_banner(name, version);
let filepath = PathBuf::from_str(cfg_file)?;
let config: Self = if filepath.is_file() {
println!("Load config from \"{}\"", cfg_file);
let path = filepath.canonicalize()?;
Self::pre_log_println(format!("Loading config from: {}", path.display()));
let buf = read_file(filepath)?;
let buf = read_file(path)?;
json5::from_str(&buf)?
} else {
println!(
Self::pre_log_println(format!(
"Config file \"{}\" does not exist, using defaults.",
cfg_file
);
));
Self::default()
};
let mut level = config.logging().as_str();
let mut level = config.logging();
if !LOG_LEVELS.contains(&level) {
bail!("Invalid default log level: {}", level);
@ -163,8 +176,8 @@ pub trait AppConfig: Sized + Serialize + DeserializeOwned + Debug + Default {
println!();
builder.format_timestamp_millis();
let mut per_mod = vec![];
let mut per_mod = vec![];
if let Some(mod_levels) = config.logging_mod_levels() {
for (module, lvl) in mod_levels {
per_mod.push((module, log::LevelFilter::from_str(lvl)?));
@ -187,11 +200,11 @@ pub trait AppConfig: Sized + Serialize + DeserializeOwned + Debug + Default {
continue 'sup;
}
}
builder.filter_module(lib, sup_lvl);
}
}
for (module, lvl) in per_mod.into_iter() {
builder.filter_module(module, lvl);
}

Loading…
Cancel
Save