refactor envelope with clap derive api

This commit is contained in:
Clément DOUIN
2023-12-06 23:12:06 +01:00
parent 4a77253c1d
commit 2c33dd2f9f
14 changed files with 304 additions and 642 deletions
+9
View File
@@ -1,4 +1,5 @@
use clap::Parser;
use email::account::config::DEFAULT_INBOX_FOLDER;
/// The folder name argument parser
#[derive(Debug, Parser)]
@@ -7,3 +8,11 @@ pub struct FolderNameArg {
#[arg(name = "folder-name", value_name = "FOLDER")]
pub name: String,
}
/// The optional folder name argument parser
#[derive(Debug, Parser)]
pub struct FolderNameOptionalArg {
/// The name of the folder
#[arg(name = "folder-name", value_name = "FOLDER", default_value = DEFAULT_INBOX_FOLDER)]
pub name: String,
}
-241
View File
@@ -1,241 +0,0 @@
//! Folder CLI module.
//!
//! This module provides subcommands, arguments and a command matcher
//! related to the folder domain.
use std::collections::HashSet;
use anyhow::Result;
use clap::{self, Arg, ArgAction, ArgMatches, Command};
use log::{debug, info};
use crate::ui::table;
const ARG_ALL: &str = "all";
const ARG_EXCLUDE: &str = "exclude";
const ARG_INCLUDE: &str = "include";
const ARG_GLOBAL_SOURCE: &str = "global-source";
const ARG_SOURCE: &str = "source";
const ARG_TARGET: &str = "target";
const CMD_CREATE: &str = "create";
const CMD_DELETE: &str = "delete";
const CMD_EXPUNGE: &str = "expunge";
const CMD_FOLDER: &str = "folder";
const CMD_LIST: &str = "list";
/// Represents the folder commands.
#[derive(Debug, PartialEq, Eq)]
pub enum Cmd {
Create,
List(table::args::MaxTableWidth),
Expunge,
Delete,
}
/// Represents the folder command matcher.
pub fn matches(m: &ArgMatches) -> Result<Option<Cmd>> {
let cmd = if let Some(m) = m.subcommand_matches(CMD_FOLDER) {
if let Some(_) = m.subcommand_matches(CMD_EXPUNGE) {
info!("expunge folder subcommand matched");
Some(Cmd::Expunge)
} else if let Some(_) = m.subcommand_matches(CMD_CREATE) {
debug!("create folder command matched");
Some(Cmd::Create)
} else if let Some(m) = m.subcommand_matches(CMD_LIST) {
debug!("list folders command matched");
let max_table_width = table::args::parse_max_width(m);
Some(Cmd::List(max_table_width))
} else if let Some(_) = m.subcommand_matches(CMD_DELETE) {
debug!("delete folder command matched");
Some(Cmd::Delete)
} else {
info!("no folder subcommand matched, falling back to subcommand list");
Some(Cmd::List(None))
}
} else {
None
};
Ok(cmd)
}
/// Represents the folder subcommand.
pub fn subcmd() -> Command {
Command::new(CMD_FOLDER)
.about("Subcommand to manage folders")
.long_about("Subcommand to manage folders like list, expunge or delete")
.subcommands([
Command::new(CMD_EXPUNGE).about("Delete emails marked for deletion"),
Command::new(CMD_CREATE)
.aliases(["add", "new"])
.about("Create a new folder"),
Command::new(CMD_LIST)
.about("List folders")
.arg(table::args::max_width()),
Command::new(CMD_DELETE)
.aliases(["remove", "rm"])
.about("Delete a folder with all its emails"),
])
}
/// Represents the source folder argument.
pub fn global_args() -> impl IntoIterator<Item = Arg> {
[Arg::new(ARG_GLOBAL_SOURCE)
.help("Override the default INBOX folder")
.long_help(
"Override the default INBOX folder.
The given folder will be used by default for all other commands (when
applicable).",
)
.long("folder")
.short('f')
.global(true)
.value_name("name")]
}
pub fn parse_global_source_arg(matches: &ArgMatches) -> Option<&str> {
matches
.get_one::<String>(ARG_GLOBAL_SOURCE)
.map(String::as_str)
}
pub fn source_arg(help: &'static str) -> Arg {
Arg::new(ARG_SOURCE).help(help).value_name("name")
}
pub fn parse_source_arg(matches: &ArgMatches) -> Option<&str> {
matches.get_one::<String>(ARG_SOURCE).map(String::as_str)
}
/// Represents the all folders argument.
pub fn all_arg(help: &'static str) -> Arg {
Arg::new(ARG_ALL)
.help(help)
.long("all-folders")
.alias("all")
.short('A')
.action(ArgAction::SetTrue)
.conflicts_with(ARG_SOURCE)
.conflicts_with(ARG_INCLUDE)
.conflicts_with(ARG_EXCLUDE)
}
/// Represents the folders to include argument.
pub fn include_arg(help: &'static str) -> Arg {
Arg::new(ARG_INCLUDE)
.help(help)
.long("include-folder")
.alias("only")
.short('F')
.value_name("FOLDER")
.num_args(1..)
.action(ArgAction::Append)
.conflicts_with(ARG_SOURCE)
.conflicts_with(ARG_ALL)
.conflicts_with(ARG_EXCLUDE)
}
/// Represents the folders to exclude argument.
pub fn exclude_arg(help: &'static str) -> Arg {
Arg::new(ARG_EXCLUDE)
.help(help)
.long("exclude-folder")
.alias("except")
.short('x')
.value_name("FOLDER")
.num_args(1..)
.action(ArgAction::Append)
.conflicts_with(ARG_SOURCE)
.conflicts_with(ARG_ALL)
.conflicts_with(ARG_INCLUDE)
}
/// Represents the folders to exclude argument parser.
pub fn parse_exclude_arg(m: &ArgMatches) -> HashSet<String> {
m.get_many::<String>(ARG_EXCLUDE)
.unwrap_or_default()
.map(ToOwned::to_owned)
.collect()
}
/// Represents the target folder argument.
pub fn target_arg() -> Arg {
Arg::new(ARG_TARGET)
.help("Specifies the target folder")
.value_name("TARGET")
.required(true)
}
/// Represents the target folder argument parser.
pub fn parse_target_arg(matches: &ArgMatches) -> &str {
matches.get_one::<String>(ARG_TARGET).unwrap().as_str()
}
#[cfg(test)]
mod tests {
use clap::{error::ErrorKind, Command};
use super::*;
#[test]
fn it_should_match_cmds() {
let arg = Command::new("himalaya")
.subcommand(subcmd())
.get_matches_from(&["himalaya", "folders"]);
assert_eq!(Some(Cmd::List(None)), matches(&arg).unwrap());
let arg = Command::new("himalaya")
.subcommand(subcmd())
.get_matches_from(&["himalaya", "folders", "list", "--max-width", "20"]);
assert_eq!(Some(Cmd::List(Some(20))), matches(&arg).unwrap());
}
#[test]
fn it_should_match_source_arg() {
macro_rules! get_matches_from {
($($arg:expr),*) => {
Command::new("himalaya")
.arg(source_arg())
.get_matches_from(&["himalaya", $($arg,)*])
};
}
let app = get_matches_from![];
assert_eq!(None, app.get_one::<String>(ARG_SOURCE).map(String::as_str));
let app = get_matches_from!["-f", "SOURCE"];
assert_eq!(
Some("SOURCE"),
app.get_one::<String>(ARG_SOURCE).map(String::as_str)
);
let app = get_matches_from!["--folder", "SOURCE"];
assert_eq!(
Some("SOURCE"),
app.get_one::<String>(ARG_SOURCE).map(String::as_str)
);
}
#[test]
fn it_should_match_target_arg() {
macro_rules! get_matches_from {
($($arg:expr),*) => {
Command::new("himalaya")
.arg(target_arg())
.try_get_matches_from_mut(&["himalaya", $($arg,)*])
};
}
let app = get_matches_from![];
assert_eq!(ErrorKind::MissingRequiredArgument, app.unwrap_err().kind());
let app = get_matches_from!["TARGET"];
assert_eq!(
Some("TARGET"),
app.unwrap()
.get_one::<String>(ARG_TARGET)
.map(String::as_str)
);
}
}
+191
View File
@@ -49,3 +49,194 @@ impl FolderSubcommand {
}
}
}
#[cfg(test)]
mod tests {
use async_trait::async_trait;
use email::{
account::config::AccountConfig,
backend::Backend,
envelope::{Envelope, Envelopes},
flag::Flags,
folder::{Folder, Folders},
message::Messages,
};
use std::{any::Any, fmt::Debug, io};
use termcolor::ColorSpec;
use crate::printer::{Print, PrintTable, WriteColor};
use super::*;
#[tokio::test]
async fn it_should_list_mboxes() {
#[derive(Debug, Default, Clone)]
struct StringWriter {
content: String,
}
impl io::Write for StringWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.content
.push_str(&String::from_utf8(buf.to_vec()).unwrap());
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
self.content = String::default();
Ok(())
}
}
impl termcolor::WriteColor for StringWriter {
fn supports_color(&self) -> bool {
false
}
fn set_color(&mut self, _spec: &ColorSpec) -> io::Result<()> {
io::Result::Ok(())
}
fn reset(&mut self) -> io::Result<()> {
io::Result::Ok(())
}
}
impl WriteColor for StringWriter {}
#[derive(Debug, Default)]
struct PrinterServiceTest {
pub writer: StringWriter,
}
impl Printer for PrinterServiceTest {
fn print_table<T: Debug + PrintTable + erased_serde::Serialize + ?Sized>(
&mut self,
data: Box<T>,
opts: PrintTableOpts,
) -> anyhow::Result<()> {
data.print_table(&mut self.writer, opts)?;
Ok(())
}
fn print_log<T: Debug + Print>(&mut self, _data: T) -> anyhow::Result<()> {
unimplemented!()
}
fn print<T: Debug + Print + serde::Serialize>(
&mut self,
_data: T,
) -> anyhow::Result<()> {
unimplemented!()
}
fn is_json(&self) -> bool {
unimplemented!()
}
}
struct TestBackend;
#[async_trait]
impl Backend for TestBackend {
fn name(&self) -> String {
unimplemented!();
}
async fn add_folder(&mut self, _: &str) -> email::Result<()> {
unimplemented!();
}
async fn list_folders(&mut self) -> email::Result<Folders> {
Ok(Folders::from_iter([
Folder {
name: "INBOX".into(),
desc: "desc".into(),
},
Folder {
name: "Sent".into(),
desc: "desc".into(),
},
]))
}
async fn expunge_folder(&mut self, _: &str) -> email::Result<()> {
unimplemented!();
}
async fn purge_folder(&mut self, _: &str) -> email::Result<()> {
unimplemented!();
}
async fn delete_folder(&mut self, _: &str) -> email::Result<()> {
unimplemented!();
}
async fn get_envelope(&mut self, _: &str, _: &str) -> email::Result<Envelope> {
unimplemented!();
}
async fn list_envelopes(
&mut self,
_: &str,
_: usize,
_: usize,
) -> email::Result<Envelopes> {
unimplemented!()
}
async fn search_envelopes(
&mut self,
_: &str,
_: &str,
_: &str,
_: usize,
_: usize,
) -> email::Result<Envelopes> {
unimplemented!()
}
async fn add_email(&mut self, _: &str, _: &[u8], _: &Flags) -> email::Result<String> {
unimplemented!()
}
async fn get_emails(&mut self, _: &str, _: Vec<&str>) -> email::Result<Messages> {
unimplemented!()
}
async fn preview_emails(&mut self, _: &str, _: Vec<&str>) -> email::Result<Messages> {
unimplemented!()
}
async fn copy_emails(&mut self, _: &str, _: &str, _: Vec<&str>) -> email::Result<()> {
unimplemented!()
}
async fn move_emails(&mut self, _: &str, _: &str, _: Vec<&str>) -> email::Result<()> {
unimplemented!()
}
async fn delete_emails(&mut self, _: &str, _: Vec<&str>) -> email::Result<()> {
unimplemented!()
}
async fn add_flags(&mut self, _: &str, _: Vec<&str>, _: &Flags) -> email::Result<()> {
unimplemented!()
}
async fn set_flags(&mut self, _: &str, _: Vec<&str>, _: &Flags) -> email::Result<()> {
unimplemented!()
}
async fn remove_flags(
&mut self,
_: &str,
_: Vec<&str>,
_: &Flags,
) -> email::Result<()> {
unimplemented!()
}
fn as_any(&self) -> &dyn Any {
unimplemented!()
}
}
let account_config = AccountConfig::default();
let mut printer = PrinterServiceTest::default();
let mut backend = TestBackend {};
assert!(list(&account_config, &mut printer, &mut backend, None)
.await
.is_ok());
assert_eq!(
concat![
"\n",
"NAME │DESC \n",
"INBOX │desc \n",
"Sent │desc \n",
"\n"
],
printer.writer.content
);
}
}
-247
View File
@@ -1,247 +0,0 @@
//! Folder handling module.
//!
//! This module gathers all folder actions triggered by the CLI.
use anyhow::Result;
use dialoguer::Confirm;
use email::account::config::AccountConfig;
use std::process;
use crate::{
backend::Backend,
printer::{PrintTableOpts, Printer},
};
use super::Folders;
pub async fn expunge<P: Printer>(printer: &mut P, backend: &Backend, folder: &str) -> Result<()> {
backend.expunge_folder(folder).await?;
printer.print(format!("Folder {folder} successfully expunged!"))
}
pub async fn list<P: Printer>(
config: &AccountConfig,
printer: &mut P,
backend: &Backend,
max_width: Option<usize>,
) -> Result<()> {
let folders: Folders = backend.list_folders().await?.into();
printer.print_table(
// TODO: remove Box
Box::new(folders),
PrintTableOpts {
format: &config.email_reading_format,
max_width,
},
)
}
pub async fn create<P: Printer>(printer: &mut P, backend: &Backend, folder: &str) -> Result<()> {
backend.add_folder(folder).await?;
printer.print("Folder successfully created!")
}
pub async fn delete<P: Printer>(printer: &mut P, backend: &Backend, folder: &str) -> Result<()> {
if let Some(false) | None = Confirm::new()
.with_prompt(format!("Confirm deletion of folder {folder}?"))
.default(false)
.report(false)
.interact_opt()?
{
process::exit(0);
};
backend.delete_folder(folder).await?;
printer.print("Folder successfully deleted!")
}
#[cfg(test)]
mod tests {
use async_trait::async_trait;
use email::{
account::config::AccountConfig,
backend::Backend,
envelope::{Envelope, Envelopes},
flag::Flags,
folder::{Folder, Folders},
message::Messages,
};
use std::{any::Any, fmt::Debug, io};
use termcolor::ColorSpec;
use crate::printer::{Print, PrintTable, WriteColor};
use super::*;
#[tokio::test]
async fn it_should_list_mboxes() {
#[derive(Debug, Default, Clone)]
struct StringWriter {
content: String,
}
impl io::Write for StringWriter {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.content
.push_str(&String::from_utf8(buf.to_vec()).unwrap());
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
self.content = String::default();
Ok(())
}
}
impl termcolor::WriteColor for StringWriter {
fn supports_color(&self) -> bool {
false
}
fn set_color(&mut self, _spec: &ColorSpec) -> io::Result<()> {
io::Result::Ok(())
}
fn reset(&mut self) -> io::Result<()> {
io::Result::Ok(())
}
}
impl WriteColor for StringWriter {}
#[derive(Debug, Default)]
struct PrinterServiceTest {
pub writer: StringWriter,
}
impl Printer for PrinterServiceTest {
fn print_table<T: Debug + PrintTable + erased_serde::Serialize + ?Sized>(
&mut self,
data: Box<T>,
opts: PrintTableOpts,
) -> anyhow::Result<()> {
data.print_table(&mut self.writer, opts)?;
Ok(())
}
fn print_log<T: Debug + Print>(&mut self, _data: T) -> anyhow::Result<()> {
unimplemented!()
}
fn print<T: Debug + Print + serde::Serialize>(
&mut self,
_data: T,
) -> anyhow::Result<()> {
unimplemented!()
}
fn is_json(&self) -> bool {
unimplemented!()
}
}
struct TestBackend;
#[async_trait]
impl Backend for TestBackend {
fn name(&self) -> String {
unimplemented!();
}
async fn add_folder(&mut self, _: &str) -> email::Result<()> {
unimplemented!();
}
async fn list_folders(&mut self) -> email::Result<Folders> {
Ok(Folders::from_iter([
Folder {
name: "INBOX".into(),
desc: "desc".into(),
},
Folder {
name: "Sent".into(),
desc: "desc".into(),
},
]))
}
async fn expunge_folder(&mut self, _: &str) -> email::Result<()> {
unimplemented!();
}
async fn purge_folder(&mut self, _: &str) -> email::Result<()> {
unimplemented!();
}
async fn delete_folder(&mut self, _: &str) -> email::Result<()> {
unimplemented!();
}
async fn get_envelope(&mut self, _: &str, _: &str) -> email::Result<Envelope> {
unimplemented!();
}
async fn list_envelopes(
&mut self,
_: &str,
_: usize,
_: usize,
) -> email::Result<Envelopes> {
unimplemented!()
}
async fn search_envelopes(
&mut self,
_: &str,
_: &str,
_: &str,
_: usize,
_: usize,
) -> email::Result<Envelopes> {
unimplemented!()
}
async fn add_email(&mut self, _: &str, _: &[u8], _: &Flags) -> email::Result<String> {
unimplemented!()
}
async fn get_emails(&mut self, _: &str, _: Vec<&str>) -> email::Result<Messages> {
unimplemented!()
}
async fn preview_emails(&mut self, _: &str, _: Vec<&str>) -> email::Result<Messages> {
unimplemented!()
}
async fn copy_emails(&mut self, _: &str, _: &str, _: Vec<&str>) -> email::Result<()> {
unimplemented!()
}
async fn move_emails(&mut self, _: &str, _: &str, _: Vec<&str>) -> email::Result<()> {
unimplemented!()
}
async fn delete_emails(&mut self, _: &str, _: Vec<&str>) -> email::Result<()> {
unimplemented!()
}
async fn add_flags(&mut self, _: &str, _: Vec<&str>, _: &Flags) -> email::Result<()> {
unimplemented!()
}
async fn set_flags(&mut self, _: &str, _: Vec<&str>, _: &Flags) -> email::Result<()> {
unimplemented!()
}
async fn remove_flags(
&mut self,
_: &str,
_: Vec<&str>,
_: &Flags,
) -> email::Result<()> {
unimplemented!()
}
fn as_any(&self) -> &dyn Any {
unimplemented!()
}
}
let account_config = AccountConfig::default();
let mut printer = PrinterServiceTest::default();
let mut backend = TestBackend {};
assert!(list(&account_config, &mut printer, &mut backend, None)
.await
.is_ok());
assert_eq!(
concat![
"\n",
"NAME │DESC \n",
"INBOX │desc \n",
"Sent │desc \n",
"\n"
],
printer.writer.content
);
}
}
-2
View File
@@ -1,8 +1,6 @@
pub mod arg;
pub mod args;
pub mod command;
pub mod config;
pub mod handlers;
use anyhow::Result;
use serde::Serialize;