#![cfg_attr(not(feature = "std"), no_std)]
#![deny(missing_docs)]
#![cfg_attr(docsrs, feature(doc_cfg))]
#![cfg_attr(
feature = "std",
doc = " \
- **std** (enabled by default, enabled in this build): remove for `#![no_std]` operation. \
Implies *alloc*.\
"
)]
#![cfg_attr(
not(feature = "std"),
doc = " \
- **std** (enabled by default, *disabled* in this build): remove for `#![no_std]` operation. \
Implies *alloc*.\
"
)]
#![cfg_attr(
feature = "alloc",
doc = " \
- **alloc** (enabled by default via *std*, enabled in this build):\
"
)]
#![cfg_attr(
not(feature = "alloc"),
doc = " \
- **alloc** (enabled by default via *std*, disabled in this build):\
"
)]
use core::fmt;
use core::iter::FusedIterator;
use core::ops::Range;
#[cfg(feature = "alloc")]
extern crate alloc;
#[cfg(feature = "alloc")]
use alloc::string::String;
#[cfg(feature = "alloc")]
use alloc::collections::BTreeMap;
#[cfg(feature = "alloc")]
use core::borrow::Borrow;
#[cfg(feature = "std")]
use std::collections::HashMap;
#[cfg(feature = "std")]
use std::hash::Hash;
#[derive(Debug, PartialEq, Eq)]
pub enum Error<'a, E> {
UnclosedRegion {
source: &'a str,
range: Range<usize>,
},
UnexpectedClosingBrace {
index: usize,
},
UnexpectedOpeningBrace {
index: usize,
},
BadReplacement {
key: &'a str,
range: Range<usize>,
error: E,
},
WriteFailed(fmt::Error),
}
impl<'a, E> From<fmt::Error> for Error<'a, E> {
fn from(e: fmt::Error) -> Self {
Error::WriteFailed(e)
}
}
impl<'a, E> fmt::Display for Error<'a, E>
where
E: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Error::UnclosedRegion { source, .. } => {
write!(f, "Unclosed template region at \"{}\"", source)
}
Error::UnexpectedClosingBrace { index } => {
write!(f, "Unexpected closing brace at index {}", index)
}
Error::UnexpectedOpeningBrace { index } => {
write!(
f,
"Unexpected curly brace within template region at index {}",
index
)
}
Error::BadReplacement { key, error, .. } => {
write!(f, "Error in template string at \"{{{}}}\": {}", key, error)
}
Error::WriteFailed(fmt::Error) => f.write_str("Error in writing output"),
}
}
}
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
impl<'a, E> std::error::Error for Error<'a, E>
where
E: std::error::Error + 'static,
{
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::BadReplacement { error, .. } => Some(error),
Error::WriteFailed(error) => Some(error),
_ => None,
}
}
}
pub trait Filler<W, E>
where
W: fmt::Write,
{
fn fill(&mut self, output: &mut W, key: &str) -> Result<(), E>;
}
impl<F, W, E> Filler<W, E> for F
where
F: FnMut(&mut W, &str) -> Result<(), E>,
W: fmt::Write,
{
fn fill(&mut self, output: &mut W, key: &str) -> Result<(), E> {
self(output, key)
}
}
#[cfg_attr(not(feature = "std"), allow(rustdoc::broken_intra_doc_links))]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SimpleFillerError {
NoSuchKey,
WriteFailed(fmt::Error),
}
impl From<fmt::Error> for SimpleFillerError {
fn from(e: fmt::Error) -> Self {
SimpleFillerError::WriteFailed(e)
}
}
impl fmt::Display for SimpleFillerError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
SimpleFillerError::NoSuchKey => f.write_str("no such key"),
SimpleFillerError::WriteFailed(fmt::Error) => f.write_str("write failed"),
}
}
}
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
impl std::error::Error for SimpleFillerError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
SimpleFillerError::WriteFailed(error) => Some(error),
_ => None,
}
}
}
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
impl<K, V, W> Filler<W, SimpleFillerError> for &HashMap<K, V>
where
K: Borrow<str> + Eq + Hash,
V: AsRef<str>,
W: fmt::Write,
{
fn fill(&mut self, output: &mut W, key: &str) -> Result<(), SimpleFillerError> {
self.get(key)
.ok_or(SimpleFillerError::NoSuchKey)
.and_then(|value| output.write_str(value.as_ref()).map_err(Into::into))
}
}
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
impl<K, V, W> Filler<W, SimpleFillerError> for &BTreeMap<K, V>
where
K: Borrow<str> + Ord,
V: AsRef<str>,
W: fmt::Write,
{
fn fill(&mut self, output: &mut W, key: &str) -> Result<(), SimpleFillerError> {
self.get(key)
.ok_or(SimpleFillerError::NoSuchKey)
.and_then(|value| output.write_str(value.as_ref()).map_err(Into::into))
}
}
pub trait StrExt {
#[cfg_attr(feature = "std", doc = " Example, using a hash map:")]
#[cfg_attr(
not(feature = "std"),
doc = " Example, using a hash map (requires the *std* feature):"
)]
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
fn fill_to_string<F, E>(&self, filler: F) -> Result<String, Error<E>>
where
F: Filler<String, E>,
{
let mut out = String::new();
self.fill_into(&mut out, filler).map(|()| out)
}
fn fill_into<F, W, E>(&self, output: &mut W, filler: F) -> Result<(), Error<E>>
where
F: Filler<W, E>,
W: fmt::Write;
}
impl StrExt for str {
#[inline]
fn fill_into<F, W, E>(&self, output: &mut W, filler: F) -> Result<(), Error<E>>
where
F: Filler<W, E>,
W: fmt::Write,
{
fill(self, filler, output)
}
}
pub fn fill<'a, F, W, E>(
mut template: &'a str,
mut filler: F,
output: &mut W,
) -> Result<(), Error<'a, E>>
where
F: Filler<W, E>,
W: fmt::Write,
{
let mut index = 0;
loop {
if let Some(i) = template.find(|c| c == '{' || c == '}') {
#[allow(clippy::wildcard_in_or_patterns)]
match template.as_bytes()[i] {
c @ b'}' | c @ b'{' if template.as_bytes().get(i + 1) == Some(&c) => {
output.write_str(&template[0..i + 1])?;
template = &template[i + 2..];
index += i + 2;
}
b'}' => return Err(Error::UnexpectedClosingBrace { index: index + i }),
b'{' | _ => {
output.write_str(&template[0..i])?;
template = &template[i..];
index += i;
if let Some(i) = template[1..].find(|c| c == '{' || c == '}') {
match template.as_bytes()[i + 1] {
b'}' => {
if let Err(e) = filler.fill(output, &template[1..i + 1]) {
return Err(Error::BadReplacement {
key: &template[1..i + 1],
range: (index + 1)..(index + i + 1),
error: e,
});
}
template = &template[i + 2..];
index += i + 2;
}
b'{' | _ => {
return Err(Error::UnexpectedOpeningBrace {
index: index + i + 1,
})
}
}
} else {
return Err(Error::UnclosedRegion {
source: template,
range: index..(index + template.len()),
});
}
}
}
} else {
output.write_str(template)?;
break;
}
}
Ok(())
}
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
pub fn fill_to_string<F, E>(template: &str, filler: F) -> Result<String, Error<E>>
where
F: Filler<String, E>,
{
let mut out = String::new();
fill(template, filler, &mut out).map(|()| out)
}
pub fn split_on(string: &str, c: char) -> (&str, Option<&str>) {
match string.find(c) {
Some(i) => (&string[..i], Some(&string[i + c.len_utf8()..])),
None => (string, None),
}
}
#[derive(Clone, Copy, Debug)]
pub struct Separators {
pub between_key_and_properties: char,
pub between_properties: char,
pub between_property_name_and_value: char,
}
pub fn split_propertied(
s: &str,
separators: Separators,
) -> (
&str,
impl Iterator<Item = (&str, Option<&str>)>
+ DoubleEndedIterator
+ FusedIterator
+ Clone
+ fmt::Debug,
) {
let (key, properties) = split_on(s, separators.between_key_and_properties);
let properties = properties
.map(|properties| properties.split(separators.between_properties))
.unwrap_or_else(|| {
let mut dummy = "".split(' ');
dummy.next();
dummy
})
.map(move |word| split_on(word, separators.between_property_name_and_value));
(key, properties)
}
#[cfg(test)]
mod tests {
#[allow(unused_imports)]
use super::*;
#[cfg(feature = "alloc")]
macro_rules! test {
($name:ident, $filler:expr) => {
#[test]
fn $name() {
let filler = $filler;
assert_eq!(
"Hello, {}!".fill_to_string(&filler).as_ref().map(|s| &**s),
Ok("Hello, (this space intentionally left blank)!"),
);
assert_eq!(
"Hello, {name}!"
.fill_to_string(&filler)
.as_ref()
.map(|s| &**s),
Ok("Hello, world!"),
);
assert_eq!(
"Hello, {you}!".fill_to_string(&filler),
Err(Error::BadReplacement {
key: "you",
range: 8..11,
error: SimpleFillerError::NoSuchKey,
}),
);
assert_eq!(
"I like {keys with SPACES!? 😱}"
.fill_to_string(&filler)
.as_ref()
.map(|s| &**s),
Ok("I like identifier-only keys 👌"),
);
}
};
}
#[cfg(feature = "alloc")]
test!(closure_filler, |out: &mut String, key: &str| {
use core::fmt::Write;
out.write_str(match key {
"" => "(this space intentionally left blank)",
"name" => "world",
"keys with SPACES!? 😱" => "identifier-only keys 👌",
_ => return Err(SimpleFillerError::NoSuchKey),
})
.map_err(Into::into)
});
#[cfg(feature = "std")]
test!(hash_map_fillter, {
[
("", "(this space intentionally left blank)"),
("name", "world"),
("keys with SPACES!? 😱", "identifier-only keys 👌"),
]
.into_iter()
.collect::<HashMap<_, _>>()
});
#[cfg(feature = "alloc")]
test!(btree_map_fillter, {
[
("", "(this space intentionally left blank)"),
("name", "world"),
("keys with SPACES!? 😱", "identifier-only keys 👌"),
]
.into_iter()
.collect::<BTreeMap<_, _>>()
});
#[test]
#[cfg(feature = "alloc")]
fn fill_errors() {
let c = |_: &mut String, _: &str| -> Result<(), ()> { Ok(()) };
assert_eq!(
fill_to_string("Hello, {thing", c),
Err(Error::UnclosedRegion {
source: "{thing",
range: 7..13
})
);
assert_eq!(
fill_to_string("{}/{x}/{xx}/{xxx}/{{/}}/{thing", c),
Err(Error::UnclosedRegion {
source: "{thing",
range: 24..30
})
);
assert_eq!(
fill_to_string("Hello, }thing", c),
Err(Error::UnexpectedClosingBrace { index: 7 })
);
assert_eq!(
fill_to_string("{}/{x}/{xx}/{xxx}/{{/}}/}thing", c),
Err(Error::UnexpectedClosingBrace { index: 24 })
);
assert_eq!(
fill_to_string("Hello, {thi{{ng}", c),
Err(Error::UnexpectedOpeningBrace { index: 11 })
);
assert_eq!(
fill_to_string("{}/{x}/{xx}/{xxx}/{{/}}/{x{", c),
Err(Error::UnexpectedOpeningBrace { index: 26 })
);
assert_eq!(
fill_to_string("Hello, {thi}}ng}", c),
Err(Error::UnexpectedClosingBrace { index: 12 })
);
assert_eq!(
fill_to_string("{}/{x}/{xx}/{xxx}/{{/}}/}", c),
Err(Error::UnexpectedClosingBrace { index: 24 })
);
}
#[test]
#[cfg(feature = "alloc")]
fn do_not_do_this_at_home_kids() {
let s = "Don’t{␡}{}{^H} do this at home, {who}!".fill_to_string(
|output: &mut String, key: &str| {
match key {
"␡" | "" | "^H" => {
output.pop();
}
"who" => {
output.push_str("kids");
}
_ => return Err(()),
}
Ok(())
},
);
assert_eq!(s.unwrap(), "Do do this at home, kids!");
let s = "Don’t yell at {who}!{←make ASCII uppercase} (Please.)".fill_to_string(
|output: &mut String, key: &str| {
match key {
"←make ASCII uppercase" => {
output.make_ascii_uppercase();
}
"who" => {
output.push_str("me");
}
_ => return Err(()),
}
Ok(())
},
);
assert_eq!(s.unwrap(), "DON’T YELL AT ME! (Please.)");
}
}