use fnv::FnvHashMap;
use smallvec::SmallVec;
use smartstring::{LazyCompact, SmartString};
use std::fmt;
use std::sync::Arc;
use thiserror::Error;
#[derive(Error, Debug, PartialEq, Eq)]
pub enum Error {
#[error("unknown key '{0}'")]
UnknownKey(SmartString<LazyCompact>),
#[error("no data for key '{0}'")]
NoData(SmartString<LazyCompact>),
#[error("imbalanced brackets in template")]
ImbalancedBrackets,
#[error("integer overflow/underflow")]
Overflow,
#[error("std::fmt::Write error")]
Write(#[from] std::fmt::Error),
}
pub type FormatterCallback<T> = Arc<dyn Fn(&T) -> Option<String> + Send + Sync>;
pub type FormatMap<T> = FnvHashMap<SmartString<LazyCompact>, FormatterCallback<T>>;
pub type FormatPieces<T> = SmallVec<[FormatPiece<T>; 256]>;
pub struct Formatter<T> {
pub key: SmartString<LazyCompact>,
pub cb: FormatterCallback<T>,
}
impl<T> PartialEq for Formatter<T> {
fn eq(&self, other: &Self) -> bool {
self.key == other.key
}
}
impl<T> Eq for Formatter<T> {}
impl<T> fmt::Debug for Formatter<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Formatter(key: {})", self.key)
}
}
#[derive(PartialEq, Eq, Debug)]
pub enum FormatPiece<T> {
Verbatim(SmartString<LazyCompact>),
Formatter(Formatter<T>),
}
pub trait ToFormatPieces<T> {
fn to_format_pieces<S: AsRef<str>>(&self, tmpl: S) -> Result<FormatPieces<T>, Error>;
}
impl<T> ToFormatPieces<T> for FormatMap<T> {
fn to_format_pieces<S: AsRef<str>>(&self, tmpl: S) -> Result<FormatPieces<T>, Error> {
let tmpl = tmpl.as_ref();
let chars = tmpl.char_indices();
let mut out = FormatPieces::with_capacity(tmpl.len());
let mut start_key_idx = 0;
let mut pending_escape = false;
let mut last_pushed_idx = 0;
macro_rules! push_verb {
($out:expr, $tmpl:expr, $range:expr) => {
let unpushed = unsafe { $tmpl.get_unchecked($range) };
$out.push(FormatPiece::Verbatim(unpushed.into()));
};
}
for (idx, cur) in chars {
match (cur, start_key_idx) {
('{', 0) => {
push_verb!(out, tmpl, last_pushed_idx..idx);
start_key_idx = idx.checked_add(1).ok_or(Error::Overflow)?;
}
('{', s) if idx.checked_sub(s).ok_or(Error::Overflow)? == 0 => {
start_key_idx = 0;
last_pushed_idx = idx;
}
('{', _) => return Err(Error::ImbalancedBrackets),
('}', 0) if !pending_escape => {
pending_escape = true;
push_verb!(out, tmpl, last_pushed_idx..idx);
}
('}', 0) if pending_escape => {
pending_escape = false;
last_pushed_idx = idx;
}
('}', s) => {
let key = unsafe { tmpl.get_unchecked(s..idx) };
let key = key.into();
match self.get(&key) {
Some(f) => {
out.push(FormatPiece::Formatter(Formatter { key, cb: f.clone() }));
}
None => return Err(Error::UnknownKey(key)),
};
start_key_idx = 0;
last_pushed_idx = idx.checked_add(1).ok_or(Error::Overflow)?;
}
_ => {
if pending_escape {
return Err(Error::ImbalancedBrackets);
}
}
}
}
if last_pushed_idx < tmpl.len() {
push_verb!(out, tmpl, last_pushed_idx..);
}
Ok(out)
}
}
pub trait Render<T> {
fn render(&self, data: &T) -> Result<String, Error>;
}
impl<T> Render<T> for FormatPieces<T> {
fn render(&self, data: &T) -> Result<String, Error> {
let mut out = String::with_capacity(self.len().checked_mul(16).ok_or(Error::Overflow)?);
for piece in self {
match piece {
FormatPiece::Verbatim(s) => out.push_str(s),
FormatPiece::Formatter(f) => {
out.push_str(&(f.cb)(data).ok_or_else(|| Error::NoData(f.key.clone()))?);
}
}
}
Ok(out)
}
}
#[macro_export]
macro_rules! fm {
(@single $($x:tt)*) => (());
(@count $($rest:expr),*) => (<[()]>::len(&[$(fm!(@single $rest)),*]));
($($key:expr => $value:expr,)+) => { fm!($($key => $value),+) };
($($key:expr => $value:expr),*) => {
{
let nr = fm!(@count $($key),*);
let mut map = $crate::FormatMap::with_capacity_and_hasher(nr, Default::default());
$(
let cb: $crate::FormatterCallback<_> = std::sync::Arc::new($value);
map.insert($key.into(), cb);
)*
map
}
};
}
#[cfg(test)]
mod lib_test;