From f26051685ceedad1d9a5cec8bc40e14c437a9502 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20DOUIN?= Date: Fri, 9 Apr 2021 00:15:16 +0200 Subject: [PATCH] refactor output system (#74) --- CHANGELOG.md | 4 + Cargo.lock | 65 +++++++++++++++- Cargo.toml | 2 + README.md | 7 +- src/main.rs | 22 +++++- src/msg/cli.rs | 190 +++++++++++++++++++++++++++------------------- src/output/cli.rs | 13 ++-- src/output/fmt.rs | 28 +++++++ src/output/log.rs | 86 +++++++++++++++++++++ 9 files changed, 325 insertions(+), 92 deletions(-) create mode 100644 src/output/fmt.rs create mode 100644 src/output/log.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f09d4f..98338e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Wiki entry for Gmail users [#58] - Info logs for copy/move/delete cmd + silent mode [#74] +### Changed + +- Refactor output system + log levels [#74] + ## [0.2.3] - 2021-04-08 ### Added diff --git a/Cargo.lock b/Cargo.lock index 7eb1302..9e1fdc4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,6 +30,17 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.0.1" @@ -194,6 +205,19 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "env_logger" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17392a012ea30ef05a610aa97dfb49496e71c9f676b27879922ea5bdf60d9d3f" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + [[package]] name = "error-chain" version = "0.12.4" @@ -271,14 +295,25 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +[[package]] +name = "hermit-abi" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" +dependencies = [ + "libc", +] + [[package]] name = "himalaya" version = "0.2.3" dependencies = [ "clap", + "env_logger", "error-chain", "imap", "lettre", + "log", "mailparse", "native-tls", "rfc2047-decoder", @@ -325,6 +360,12 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyperx" version = "1.3.0" @@ -475,11 +516,11 @@ dependencies = [ [[package]] name = "log" -version = "0.4.11" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" dependencies = [ - "cfg-if 0.1.10", + "cfg-if 1.0.0", ] [[package]] @@ -1033,6 +1074,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "termcolor" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4" +dependencies = [ + "winapi-util", +] + [[package]] name = "terminal_size" version = "0.1.15" @@ -1197,6 +1247,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" diff --git a/Cargo.toml b/Cargo.toml index 7d23800..ac7270f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,9 +7,11 @@ edition = "2018" [dependencies] clap = {version = "2.33.3", default-features = false, features = ["suggestions"]} +env_logger = "0.8.3" error-chain = "0.12.4" imap = "2.4.0" lettre = "0.10.0-beta.3" +log = "0.4.14" mailparse = "0.13.1" native-tls = "0.2" rfc2047-decoder = "0.1.2" diff --git a/README.md b/README.md index d864a82..d255b62 100644 --- a/README.md +++ b/README.md @@ -87,17 +87,18 @@ soywod 📫 Minimalist CLI email client USAGE: - himalaya [FLAGS] [OPTIONS] [SUBCOMMAND] + himalaya [OPTIONS] [SUBCOMMAND] FLAGS: -h, --help Prints help information - -s, --silent Disables any output -V, --version Prints version information OPTIONS: -a, --account Selects a specific account + -l, --log Defines the logs level [default: info] [possible values: error, warn, info, debug, + trace] -m, --mailbox Selects a specific mailbox [default: INBOX] - -o, --output Defines the output format [default: plain] [possible values: plain, json] + -o, --output Defines the output format [default: plain] [possible values: plain, json] SUBCOMMANDS: attachments Downloads all message attachments diff --git a/src/main.rs b/src/main.rs index 8785e99..291d56b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -7,6 +7,8 @@ mod config { } mod output { pub(crate) mod cli; + pub(crate) mod fmt; + pub(crate) mod log; pub(crate) mod utils; } mod imap { @@ -28,6 +30,7 @@ mod mbox { use clap; use error_chain::error_chain; +use log::{debug, error}; use std::env; use crate::{ @@ -36,7 +39,11 @@ use crate::{ imap::cli::{imap_matches, imap_subcmds}, mbox::cli::{mbox_matches, mbox_source_arg, mbox_subcmds}, msg::cli::{msg_matches, msg_subcmds}, - output::cli::output_args, + output::{ + cli::output_args, + fmt::OutputFmt, + log::{init as init_logger, LogLevel}, + }, }; error_chain! { @@ -45,6 +52,7 @@ error_chain! { ImapCli(crate::imap::cli::Error, crate::imap::cli::ErrorKind); MboxCli(crate::mbox::cli::Error, crate::mbox::cli::ErrorKind); MsgCli(crate::msg::cli::Error, crate::msg::cli::ErrorKind); + OutputLog(crate::output::log::Error, crate::output::log::ErrorKind); } } @@ -62,6 +70,14 @@ fn run() -> Result<()> { .subcommands(msg_subcmds()) .get_matches(); + let output_fmt: OutputFmt = matches.value_of("output").unwrap().into(); + let log_level: LogLevel = matches.value_of("log").unwrap().into(); + + init_logger(&output_fmt, &log_level)?; + debug!("Logger initialized"); + debug!("Output format: {}", &output_fmt); + debug!("Log level: {}", &log_level); + loop { if mbox_matches(&matches)? { break; @@ -88,8 +104,8 @@ fn main() { match errs.next() { None => (), Some(err) => { - eprintln!("{}", err); - errs.for_each(|err| eprintln!(" ↳ {}", err)); + error!("{}", err); + errs.for_each(|err| error!(" ↳ {}", err)); } } } diff --git a/src/msg/cli.rs b/src/msg/cli.rs index 66c242a..56fa2af 100644 --- a/src/msg/cli.rs +++ b/src/msg/cli.rs @@ -1,5 +1,6 @@ use clap::{self, App, Arg, ArgMatches, SubCommand}; use error_chain::error_chain; +use log::{debug, error, info}; use std::{fs, ops::Deref}; use crate::{ @@ -9,7 +10,6 @@ use crate::{ input, mbox::cli::mbox_target_arg, msg::model::{Attachments, Msg, Msgs, ReadableMsg}, - output::utils::{print, Info}, smtp, }; @@ -19,7 +19,6 @@ error_chain! { Imap(crate::imap::model::Error, crate::imap::model::ErrorKind); Input(crate::input::Error, crate::input::ErrorKind); MsgModel(crate::msg::model::Error, crate::msg::model::ErrorKind); - OutputUtils(crate::output::utils::Error, crate::output::utils::ErrorKind); Smtp(crate::smtp::Error, crate::smtp::ErrorKind); } } @@ -162,25 +161,33 @@ pub fn msg_subcmds<'a>() -> Vec> { pub fn msg_matches(matches: &ArgMatches) -> Result<()> { let config = Config::new_from_file()?; let account = config.find_account_by_name(matches.value_of("account"))?; - let output_fmt = matches.value_of("output").unwrap(); - let silent = matches.is_present("silent"); let mbox = matches.value_of("mailbox").unwrap(); if let Some(matches) = matches.subcommand_matches("list") { + debug!("Subcommand matched: list"); + let mut imap_conn = ImapConnector::new(&account)?; let page_size: usize = matches.value_of("page-size").unwrap().parse().unwrap(); + debug!("Page size: {}", &page_size); let page: usize = matches.value_of("page").unwrap().parse().unwrap(); + debug!("Page: {}", &page); + let msgs = imap_conn.list_msgs(&mbox, &page_size, &page)?; let msgs = Msgs::from(&msgs); - print(&output_fmt, &silent, msgs)?; + info!("{}", msgs); + imap_conn.logout(); return Ok(()); } if let Some(matches) = matches.subcommand_matches("search") { + debug!("Subcommand matched: search"); + let mut imap_conn = ImapConnector::new(&account)?; let page_size: usize = matches.value_of("page-size").unwrap().parse().unwrap(); + debug!("Page size: {}", &page_size); let page: usize = matches.value_of("page").unwrap().parse().unwrap(); + debug!("Page: {}", &page); let query = matches .values_of("query") .unwrap_or_default() @@ -205,62 +212,61 @@ pub fn msg_matches(matches: &ArgMatches) -> Result<()> { }) .1 .join(" "); + debug!("Query: {}", &query); + let msgs = imap_conn.search_msgs(&mbox, &query, &page_size, &page)?; let msgs = Msgs::from(&msgs); - print(&output_fmt, &silent, msgs)?; + info!("{}", msgs); + imap_conn.logout(); return Ok(()); } if let Some(matches) = matches.subcommand_matches("read") { + debug!("Subcommand matched: read"); + let mut imap_conn = ImapConnector::new(&account)?; let uid = matches.value_of("uid").unwrap(); + debug!("UID: {}", &uid); let mime = format!("text/{}", matches.value_of("mime-type").unwrap()); + debug!("MIME: {}", &mime); + let msg = imap_conn.read_msg(&mbox, &uid)?; let msg = ReadableMsg::from_bytes(&mime, &msg)?; - print(&output_fmt, &silent, msg)?; + info!("{}", msg); + imap_conn.logout(); return Ok(()); } if let Some(matches) = matches.subcommand_matches("attachments") { + debug!("Subcommand matched: attachments"); + let mut imap_conn = ImapConnector::new(&account)?; let uid = matches.value_of("uid").unwrap(); + debug!("UID: {}", &uid); let msg = imap_conn.read_msg(&mbox, &uid)?; let attachments = Attachments::from_bytes(&msg)?; + debug!( + "{} attachment(s) found for message {}", + attachments.0.len(), + uid + ); + attachments.0.iter().for_each(|attachment| { + let filepath = config.downloads_filepath(&account, &attachment.filename); + debug!("Downloading {}…", &attachment.filename); + fs::write(filepath, &attachment.raw).unwrap() + }); + info!("{} attachment(s) successfully downloaded", &uid); - match output_fmt { - "plain" => { - println!( - "{} attachment(s) found for message {}", - attachments.0.len(), - uid - ); - - attachments.0.iter().for_each(|attachment| { - let filepath = config.downloads_filepath(&account, &attachment.filename); - println!("Downloading {}…", &attachment.filename); - fs::write(filepath, &attachment.raw).unwrap() - }); - - println!("Done!"); - } - "json" => { - attachments.0.iter().for_each(|attachment| { - let filepath = config.downloads_filepath(&account, &attachment.filename); - fs::write(filepath, &attachment.raw).unwrap() - }); - - print!("{{}}"); - } - _ => (), - } imap_conn.logout(); return Ok(()); } if let Some(matches) = matches.subcommand_matches("write") { + debug!("Subcommand matched: write"); + let mut imap_conn = ImapConnector::new(&account)?; let attachments = matches .values_of("attachments") @@ -276,17 +282,17 @@ pub fn msg_matches(matches: &ArgMatches) -> Result<()> { match input::post_edit_choice() { Ok(choice) => match choice { input::Choice::Send => { - println!("Sending…"); + debug!("Sending message…"); let msg = msg.to_sendable_msg()?; smtp::send(&account, &msg)?; imap_conn.append_msg("Sent", &msg.formatted(), &[Flag::Seen])?; - println!("Done!"); + info!("Message successfully sent"); break; } input::Choice::Draft => { - println!("Saving to draft…"); + debug!("Saving to draft…"); imap_conn.append_msg("Drafts", &msg.to_vec()?, &[Flag::Seen])?; - println!("Done!"); + info!("Message successfully saved to Drafts"); break; } input::Choice::Edit => { @@ -295,7 +301,7 @@ pub fn msg_matches(matches: &ArgMatches) -> Result<()> { } input::Choice::Quit => break, }, - Err(err) => eprintln!("{}", err), + Err(err) => error!("{}", err), } } imap_conn.logout(); @@ -303,30 +309,44 @@ pub fn msg_matches(matches: &ArgMatches) -> Result<()> { } if let Some(matches) = matches.subcommand_matches("template") { + debug!("Subcommand matched: template"); + if let Some(_) = matches.subcommand_matches("new") { + debug!("Subcommand matched: new"); + let tpl = Msg::build_new_tpl(&config, &account)?; - print(&output_fmt, &silent, &tpl)?; + info!("{}", tpl); } if let Some(matches) = matches.subcommand_matches("reply") { + debug!("Subcommand matched: reply"); + let mut imap_conn = ImapConnector::new(&account)?; let uid = matches.value_of("uid").unwrap(); + debug!("UID: {}", &uid); + let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); let tpl = if matches.is_present("reply-all") { msg.build_reply_all_tpl(&config, &account)? } else { msg.build_reply_tpl(&config, &account)? }; - print(&output_fmt, &silent, &tpl)?; + info!("{}", tpl); + imap_conn.logout(); } if let Some(matches) = matches.subcommand_matches("forward") { + debug!("Subcommand matched: forward"); + let mut imap_conn = ImapConnector::new(&account)?; let uid = matches.value_of("uid").unwrap(); + debug!("UID: {}", &uid); + let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); let tpl = msg.build_forward_tpl(&config, &account)?; - print(&output_fmt, &silent, &tpl)?; + info!("{}", tpl); + imap_conn.logout(); } @@ -334,6 +354,8 @@ pub fn msg_matches(matches: &ArgMatches) -> Result<()> { } if let Some(matches) = matches.subcommand_matches("reply") { + debug!("Subcommand matched: reply"); + let mut imap_conn = ImapConnector::new(&account)?; let attachments = matches .values_of("attachments") @@ -341,6 +363,7 @@ pub fn msg_matches(matches: &ArgMatches) -> Result<()> { .map(String::from) .collect::>(); let uid = matches.value_of("uid").unwrap(); + debug!("UID: {}", &uid); let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); let tpl = if matches.is_present("reply-all") { @@ -357,17 +380,17 @@ pub fn msg_matches(matches: &ArgMatches) -> Result<()> { match input::post_edit_choice() { Ok(choice) => match choice { input::Choice::Send => { - println!("Sending…"); + debug!("Sending message…"); smtp::send(&account, &msg.to_sendable_msg()?)?; imap_conn.append_msg("Sent", &msg.to_vec()?, &[Flag::Seen])?; imap_conn.add_flags(mbox, uid, "\\Answered")?; - println!("Done!"); + info!("Message successfully sent"); break; } input::Choice::Draft => { - println!("Saving to draft…"); + debug!("Saving draft message…"); imap_conn.append_msg("Drafts", &msg.to_vec()?, &[Flag::Seen])?; - println!("Done!"); + info!("Message successfully saved to Drafts"); break; } input::Choice::Edit => { @@ -376,7 +399,7 @@ pub fn msg_matches(matches: &ArgMatches) -> Result<()> { } input::Choice::Quit => break, }, - Err(err) => eprintln!("{}", err), + Err(err) => error!("{}", err), } } @@ -385,6 +408,8 @@ pub fn msg_matches(matches: &ArgMatches) -> Result<()> { } if let Some(matches) = matches.subcommand_matches("forward") { + debug!("Subcommand matched: forward"); + let mut imap_conn = ImapConnector::new(&account)?; let attachments = matches .values_of("attachments") @@ -392,6 +417,7 @@ pub fn msg_matches(matches: &ArgMatches) -> Result<()> { .map(String::from) .collect::>(); let uid = matches.value_of("uid").unwrap(); + debug!("UID: {}", &uid); let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); let tpl = msg.build_forward_tpl(&config, &account)?; @@ -403,16 +429,16 @@ pub fn msg_matches(matches: &ArgMatches) -> Result<()> { match input::post_edit_choice() { Ok(choice) => match choice { input::Choice::Send => { - println!("Sending…"); + debug!("Sending message…"); smtp::send(&account, &msg.to_sendable_msg()?)?; imap_conn.append_msg("Sent", &msg.to_vec()?, &[Flag::Seen])?; - println!("Done!"); + info!("Message successfully sent"); break; } input::Choice::Draft => { - println!("Saving to draft…"); + debug!("Saving draft message…"); imap_conn.append_msg("Drafts", &msg.to_vec()?, &[Flag::Seen])?; - println!("Done!"); + info!("Message successfully saved to Drafts"); break; } input::Choice::Edit => { @@ -421,7 +447,7 @@ pub fn msg_matches(matches: &ArgMatches) -> Result<()> { } input::Choice::Quit => break, }, - Err(err) => eprintln!("{}", err), + Err(err) => error!("{}", err), } } @@ -430,63 +456,67 @@ pub fn msg_matches(matches: &ArgMatches) -> Result<()> { } if let Some(matches) = matches.subcommand_matches("copy") { + debug!("Subcommand matched: copy"); + let mut imap_conn = ImapConnector::new(&account)?; let uid = matches.value_of("uid").unwrap(); + debug!("UID: {}", &uid); let target = matches.value_of("target").unwrap(); + debug!("Target: {}", &target); + let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); let mut flags = msg.flags.deref().to_vec(); flags.push(Flag::Seen); imap_conn.append_msg(target, &msg.raw, &flags)?; - imap_conn.logout(); + info!( + "Message {} successfully copied to folder `{}`", + &uid, &target + ); - print( - &output_fmt, - &silent, - Info(format!( - "Message {} successfully copied to folder `{}`", - &uid, &target - )), - )?; + imap_conn.logout(); return Ok(()); } if let Some(matches) = matches.subcommand_matches("move") { + debug!("Subcommand matched: move"); + let mut imap_conn = ImapConnector::new(&account)?; let uid = matches.value_of("uid").unwrap(); + debug!("UID: {}", &uid); let target = matches.value_of("target").unwrap(); + debug!("Target: {}", &target); + let msg = Msg::from(imap_conn.read_msg(&mbox, &uid)?); let mut flags = msg.flags.deref().to_vec(); flags.push(Flag::Seen); imap_conn.append_msg(target, &msg.raw, msg.flags.deref())?; imap_conn.add_flags(mbox, uid, "\\Seen \\Deleted")?; - imap_conn.logout(); + info!( + "Message {} successfully moved to folder `{}`", + &uid, &target + ); - print( - &output_fmt, - &silent, - Info(format!( - "Message {} successfully moved to folder `{}`", - &uid, &target - )), - )?; + imap_conn.logout(); return Ok(()); } if let Some(matches) = matches.subcommand_matches("delete") { + debug!("Subcommand matched: delete"); + let mut imap_conn = ImapConnector::new(&account)?; let uid = matches.value_of("uid").unwrap(); - imap_conn.add_flags(mbox, uid, "\\Seen \\Deleted")?; - imap_conn.logout(); + debug!("UID: {}", &uid); - print( - &output_fmt, - &silent, - Info(format!("Message {} successfully deleted", &uid)), - )?; + imap_conn.add_flags(mbox, uid, "\\Seen \\Deleted")?; + info!("Message {} successfully deleted", &uid); + + imap_conn.logout(); return Ok(()); } if let Some(matches) = matches.subcommand_matches("send") { + debug!("Subcommand matched: send"); + let mut imap_conn = ImapConnector::new(&account)?; let msg = matches.value_of("message").unwrap(); let msg = Msg::from(msg.to_string()); @@ -498,6 +528,8 @@ pub fn msg_matches(matches: &ArgMatches) -> Result<()> { } if let Some(matches) = matches.subcommand_matches("save") { + debug!("Subcommand matched: save"); + let mut imap_conn = ImapConnector::new(&account)?; let msg = matches.value_of("message").unwrap(); let msg = Msg::from(msg.to_string()); @@ -506,11 +538,13 @@ pub fn msg_matches(matches: &ArgMatches) -> Result<()> { return Ok(()); } - // Default case: list first page messages + debug!("Default subcommand matched: list"); + let mut imap_conn = ImapConnector::new(&account)?; let msgs = imap_conn.list_msgs(&mbox, &10, &0)?; let msgs = Msgs::from(&msgs); - print(&output_fmt, &silent, msgs)?; + info!("{}", &msgs); + imap_conn.logout(); Ok(()) } diff --git a/src/output/cli.rs b/src/output/cli.rs index 4cc6fdc..f2fc58b 100644 --- a/src/output/cli.rs +++ b/src/output/cli.rs @@ -6,12 +6,15 @@ pub fn output_args<'a>() -> Vec> { .long("output") .short("o") .help("Defines the output format") - .value_name("STRING") + .value_name("FMT") .possible_values(&["plain", "json"]) .default_value("plain"), - Arg::with_name("silent") - .long("silent") - .short("s") - .help("Disables any output"), + Arg::with_name("log") + .long("log") + .short("l") + .help("Defines the logs level") + .value_name("LEVEL") + .possible_values(&["error", "warn", "info", "debug", "trace"]) + .default_value("info"), ] } diff --git a/src/output/fmt.rs b/src/output/fmt.rs new file mode 100644 index 0000000..c80e283 --- /dev/null +++ b/src/output/fmt.rs @@ -0,0 +1,28 @@ +use std::fmt; + +pub enum OutputFmt { + Json, + Plain, +} + +impl From<&str> for OutputFmt { + fn from(s: &str) -> Self { + match s { + "json" => Self::Json, + "plain" | _ => Self::Plain, + } + } +} + +impl fmt::Display for OutputFmt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match *self { + OutputFmt::Json => "JSON", + OutputFmt::Plain => "PLAIN", + }, + ) + } +} diff --git a/src/output/log.rs b/src/output/log.rs new file mode 100644 index 0000000..5c07760 --- /dev/null +++ b/src/output/log.rs @@ -0,0 +1,86 @@ +use error_chain::error_chain; +use log::{Level, LevelFilter, Metadata, Record}; +use std::fmt; + +use super::fmt::OutputFmt; + +error_chain! {} + +// Log level struct + +pub struct LogLevel(pub LevelFilter); + +impl From<&str> for LogLevel { + fn from(s: &str) -> Self { + match s { + "error" => Self(LevelFilter::Error), + "warn" => Self(LevelFilter::Warn), + "debug" => Self(LevelFilter::Debug), + "trace" => Self(LevelFilter::Trace), + "info" | _ => Self(LevelFilter::Info), + } + } +} + +impl fmt::Display for LogLevel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0.to_string()) + } +} + +// Plain logger + +struct PlainLogger; + +impl log::Log for PlainLogger { + fn enabled(&self, metadata: &Metadata) -> bool { + metadata.level() <= Level::Trace + } + + fn log(&self, record: &Record) { + if self.enabled(record.metadata()) { + if let Level::Error = record.level() { + eprintln!("{}", record.args()); + } else { + println!("{}", record.args()); + } + } + } + + fn flush(&self) {} +} + +// JSON logger + +struct JsonLogger; + +impl log::Log for JsonLogger { + fn enabled(&self, metadata: &Metadata) -> bool { + [Level::Error, Level::Info].contains(&metadata.level()) + } + + fn log(&self, record: &Record) { + if self.enabled(record.metadata()) { + if let Level::Error = record.level() { + eprintln!("{}", record.args()); + } else { + // Should be safe enough to `.unwrap()` since it's + // formatted by Himalaya itself + print!("{}", serde_json::to_string(record.args()).unwrap()); + } + } + } + + fn flush(&self) {} +} + +// Init + +pub fn init(fmt: &OutputFmt, level: &LogLevel) -> Result<()> { + log::set_boxed_logger(match fmt { + &OutputFmt::Json => Box::new(JsonLogger), + &OutputFmt::Plain => Box::new(PlainLogger), + }) + .map(|()| log::set_max_level(level.0)) + .chain_err(|| "Could not init logger") +}