use std::collections::BTreeSet;
use std::ffi::OsString;
use std::fmt::{self, Display};
use std::fs::File;
use std::io::{self, Write};
use std::ops::{Deref, DerefMut};
use std::path::{Path, PathBuf};
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("invalid flag name: {0}")]
InvalidFlagName(String),
#[error("I/O error: {0}")]
IOError(#[from] std::io::Error),
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum Flag {
Seen,
Answered,
Forwarded,
Flagged,
Deleted,
Draft,
Important,
MDNSent,
Junk,
NotJunk,
Phishing,
Custom(String),
}
impl TryFrom<&str> for Flag {
type Error = Error;
fn try_from(flag: &str) -> Result<Self, Error> {
Ok(match flag {
"$seen" => Flag::Seen,
"$answered" => Flag::Answered,
"$Forwarded" => Flag::Forwarded,
"$flagged" => Flag::Flagged,
"$Deleted" => Flag::Deleted,
"$draft" => Flag::Draft,
"$Important" => Flag::Important,
"$MDNSent" => Flag::MDNSent,
"$Junk" => Flag::Junk,
"$NotJunk" => Flag::NotJunk,
"$Phishing" => Flag::Phishing,
_ => {
if flag.is_empty() || flag.contains('\n') {
return Err(Error::InvalidFlagName(flag.to_string()));
}
Flag::Custom(flag.to_string())
}
})
}
}
impl Display for Flag {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
Flag::Seen => "$seen",
Flag::Answered => "$answered",
Flag::Forwarded => "$Forwarded",
Flag::Flagged => "$flagged",
Flag::Deleted => "$Deleted",
Flag::Draft => "$draft",
Flag::Important => "$Important",
Flag::MDNSent => "$MDNSent",
Flag::Junk => "$Junk",
Flag::NotJunk => "$NotJunk",
Flag::Phishing => "$Phishing",
Flag::Custom(c) => c,
};
write!(f, "{}", s)
}
}
#[derive(Debug, Eq, PartialEq, PartialOrd)]
pub struct Flags(BTreeSet<Flag>);
impl Flags {
pub fn new() -> Self {
Flags(BTreeSet::new())
}
pub fn parse_string(s: &str) -> Result<Self, Error> {
if s.is_empty() {
return Ok(Flags::new());
}
s.trim_end().split('\n').map(Flag::try_from).collect()
}
pub fn parse_file(f: File) -> Result<Self, Error> {
let s = io::read_to_string(&f)?;
Flags::parse_string(&s)
}
pub fn write_string(&self) -> String {
let mut result = String::new();
for flag in &self.0 {
result.push_str(&format!("{}\n", flag));
}
result
}
#[allow(clippy::write_with_newline)]
pub fn write_file(&self, f: &mut impl Write) -> io::Result<()> {
for flag in &self.0 {
write!(f, "{}\n", flag)?;
}
f.flush()
}
}
impl Default for Flags {
fn default() -> Self {
Self::new()
}
}
impl Deref for Flags {
type Target = BTreeSet<Flag>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Flags {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<const N: usize> std::convert::From<[Flag; N]> for Flags {
fn from(arr: [Flag; N]) -> Self {
Flags(BTreeSet::from(arr))
}
}
impl std::convert::From<&BTreeSet<Flag>> for Flags {
fn from(set: &BTreeSet<Flag>) -> Self {
let mut c = BTreeSet::new();
for i in set {
c.insert(i.clone());
}
Flags(c)
}
}
impl FromIterator<Flag> for Flags {
fn from_iter<T: IntoIterator<Item = Flag>>(iter: T) -> Self {
let mut set = BTreeSet::new();
for i in iter {
set.insert(i);
}
Flags(set)
}
}
impl IntoIterator for Flags {
type Item = Flag;
type IntoIter = <BTreeSet<Flag> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl fmt::Display for Flags {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut first = true;
write!(f, "(")?;
for flag in &self.0 {
if !first {
write!(f, ", {}", flag)?;
} else {
write!(f, "{}", flag)?;
first = false;
}
}
write!(f, ")")?;
Ok(())
}
}
pub(crate) fn flags_path_for(dir: impl AsRef<Path>, id: &str) -> PathBuf {
PathBuf::from_iter([
dir.as_ref().as_os_str(),
&OsString::from(".meta"),
&OsString::from(format!("{}.flags", id)),
])
}
#[cfg(test)]
mod tests {
use tempfile::NamedTempFile;
use super::*;
#[test]
fn test_flag() {
assert_eq!(Flag::try_from("$seen").unwrap(), Flag::Seen);
assert_eq!(Flag::try_from("$Forwarded").unwrap(), Flag::Forwarded);
assert_eq!(
Flag::try_from("myflag").unwrap(),
Flag::Custom(String::from("myflag"))
);
assert!(Flag::try_from("my\nflag").is_err());
assert!(Flag::try_from("").is_err());
}
#[test]
fn test_flags() {
let set = Flags::from([Flag::Answered]);
assert_eq!(set.to_string(), String::from("($answered)"));
let set = Flags::from([Flag::Forwarded, Flag::Answered]);
assert_eq!(set.to_string(), String::from("($answered, $Forwarded)"));
let set = Flags::from([
Flag::Custom(String::from("myflag")),
Flag::Forwarded,
Flag::Answered,
]);
assert_eq!(
set.to_string(),
String::from("($answered, $Forwarded, myflag)")
);
}
#[test]
fn test_serialize_string() {
let set = Flags::from([
Flag::Custom(String::from("myflag")),
Flag::Forwarded,
Flag::Answered,
]);
assert_eq!(
set.to_string(),
String::from("($answered, $Forwarded, myflag)")
);
let ser = set.write_string();
let set2 = Flags::parse_string(&ser).unwrap();
assert_eq!(set, set2);
}
#[test]
fn test_serialize_file() {
let set = Flags::from([
Flag::Custom(String::from("myflag")),
Flag::Forwarded,
Flag::Answered,
]);
assert_eq!(
set.to_string(),
String::from("($answered, $Forwarded, myflag)")
);
let mut f = NamedTempFile::new().unwrap();
set.write_file(&mut f).unwrap();
let r = File::open(f.path()).unwrap();
let set2 = Flags::parse_file(r).unwrap();
assert_eq!(set, set2);
}
}