//! This is a complete example, showing how to use the boilerplate in a real-world application #[macro_use] extern crate serde_derive; #[macro_use] pub extern crate log; use clap::ArgMatches; use clappconfig::AppConfig; 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, #[default = 13] offset: i32, } /// This is a struct created by the `Config::configure()` method /// (and returned by the `Config::init()`) struct Main { config: Config, input_file: PathBuf, } impl AppConfig for Config { type Init = Main; /// Logging from the config struct fn logging(&self) -> &str { &self.logging } /// Exclude some spammy libs from logs fn logging_suppress_mods(&self) -> Option> { Some(vec![ // we do not actually use these here, this is an example "tokio_reactor", "tokio_io", "hyper", "reqwest", "mio", "want", ]) } /// We do not want a banner fn print_banner(_name: &str, _version: &str) { // Disable } /// Suppress stderr logging on startup fn pre_log_println(_message: String) { // Disable } /// Get log module levels to use /// (take priority over the main log level & suppressing) fn logging_mod_levels(&self) -> Option<&HashMap> { // we let user configure these Some(&self.log_modules) } /// Add our custom args 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") // Work-around for default-config acting as short-circuit (similar to --version and --help) .required_unless("default-config") .help("Input file to rotate") .takes_value(true), ) .arg( clap::Arg::with_name("offset") .short("n") .long("shift") .value_name("N") .help("Positive or negative rotation, default 13") .takes_value(true), ) } /// Here finalize configuration and parse our custom arguments. /// /// Sometimes these go into `Config`, then we can use `type Init = Config;` /// /// Here we don't want to have the input path in config, so a different struct is used. /// /// This can also be solved using `#[serde(skip)]` on the field, but sometimes that is not /// possible. fn configure<'a>(mut self, clap: &ArgMatches<'a>) -> anyhow::Result { let input = clap.value_of("input").unwrap().to_string(); // Logging is initialized now - feel free to use it debug!("Input: {}", input); if input.is_empty() { anyhow::bail!("Input is required!"); } // `?` can be used to abort let pb = PathBuf::from_str(&input)?.canonicalize()?; debug!("Input path: {}", pb.display()); if !pb.is_file() { anyhow::bail!("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); assert!(self.offset < 26); debug!("Rot normalized: {}", self.offset); // We construct the `Init` struct here Ok(Main { config: self, input_file: pb, }) } } fn main() -> anyhow::Result<()> { // 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", &version)?; // rot13 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(()) } /// Rotate a string fn rot_n(string: String, rot: u8) -> String { assert!(rot < 26); const LOWER: &[u8] = b"abcdefghijklmnopqrstuvwxyz"; const UPPER: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ"; 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() }