use std::{
collections::HashMap,
convert::TryFrom,
fmt::Display,
fs::File,
io::{Read, Write},
path::Path,
str::FromStr,
};
use csv::Trim;
use serde::*;
use serde_with::{serde_as, TryFromInto};
use thiserror::Error;
use EmoteMapperError::*;
pub struct EmoteMapper {
map: HashMap<String, CodePoint>,
emote_width: usize,
}
impl Default for EmoteMapper {
fn default() -> Self {
Self {
map: Default::default(),
emote_width: 3,
}
}
}
impl EmoteMapper {
pub fn emote_width(mut self, emote_width: usize) -> Self {
self.emote_width = emote_width;
self
}
pub fn to_char(&self, emote_name: &str) -> Option<char> {
self.map.get(emote_name).copied().map(char::from)
}
pub fn to_string(&self, emote_name: &str) -> Option<String> {
self.to_char(emote_name)
.map(|c| format!("{}{}", c, " ".repeat(self.emote_width - 1)))
}
}
/// Parsing
impl EmoteMapper {
pub fn from_reader(rdr: impl Read) -> Result<Self, EmoteMapperError> {
let mut rdr = csv::ReaderBuilder::new()
.has_headers(false)
.trim(Trim::All)
.from_reader(rdr);
let map = rdr
.deserialize::<Mapping>()
.map(|r| {
let r: Mapping = r.map_err(StringParsingError)?;
Ok((r.name, r.code_point))
})
.collect::<Result<_, EmoteMapperError>>()?;
Ok(EmoteMapper {
map,
..EmoteMapper::default()
})
}
pub fn from_path(p: &Path) -> Result<Self, EmoteMapperError> {
Self::from_reader(File::open(p).map_err(IOError)?)
}
}
impl FromStr for EmoteMapper {
type Err = EmoteMapperError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::from_reader(s.as_bytes())
}
}
/// Saving
impl EmoteMapper {
pub fn to_writer(self, wtr: impl Write) -> Result<(), EmoteMapperError> {
let mut wtr = csv::WriterBuilder::new()
.has_headers(false)
.from_writer(wtr);
for mapping in self.map.into_iter().map(Mapping::from) {
wtr.serialize(mapping).unwrap();
}
wtr.flush().map_err(IOError)
}
pub fn to_path(self, p: &Path) -> Result<(), EmoteMapperError> {
self.to_writer(File::open(p).map_err(IOError)?)
}
}
impl From<EmoteMapper> for String {
fn from(val: EmoteMapper) -> Self {
let mut s = "".to_string();
{
val.to_writer(unsafe { s.as_mut_vec() })
.expect("Writing to String should not fail");
}
s
}
}
#[test]
fn from_into_str() {
let str = "testEmote,F4000\n";
let em = EmoteMapper::from_str(str).unwrap();
assert_eq!(em.map.get("testEmote").unwrap().0, '\u{F4000}');
assert_eq!(String::from(em), str)
}
#[serde_as]
#[derive(Debug, Deserialize, Serialize)]
struct Mapping {
name: String,
#[serde_as(as = "TryFromInto<String>")]
code_point: CodePoint,
}
impl From<(String, CodePoint)> for Mapping {
fn from((name, code_point): (String, CodePoint)) -> Self {
Mapping { name, code_point }
}
}
#[derive(Error, Debug)]
pub enum EmoteMapperError {
#[error("codepoint is expected to be a hex number: `{0}`")]
InvalidCodepoint(String),
#[error("Parsing from string failed with:\n{0}")]
StringParsingError(csv::Error),
#[error("Failed due to IO-Error:\n{0}")]
IOError(std::io::Error),
}
#[derive(Clone, Copy, Debug)]
struct CodePoint(char);
impl From<CodePoint> for char {
fn from(cp: CodePoint) -> Self {
cp.0
}
}
impl Display for CodePoint {
fn fmt(&self, f: &mut __private::Formatter<'_>) -> std::fmt::Result {
char::from(*self).fmt(f)
}
}
impl From<CodePoint> for String {
fn from(val: CodePoint) -> Self {
format!("{:X}", val.0 as u32)
}
}
impl TryFrom<String> for CodePoint {
type Error = EmoteMapperError;
fn try_from(s: String) -> Result<Self, Self::Error> {
Ok(CodePoint(
char::from_u32(
u32::from_str_radix(&s, 16).map_err(|_| InvalidCodepoint(s.to_string()))?,
)
.ok_or_else(|| InvalidCodepoint(s.to_string()))?,
))