#![warn(missing_docs, missing_debug_implementations)]
#![cfg_attr(feature = "doc-cfg", feature(doc_cfg))]
pub mod error;
pub use error::Error;
mod map;
pub use map::*;
mod template;
pub use template::*;
mod features;
#[allow(unused_imports)] pub use features::*;
mod non_aliasing;
pub fn substitute<'a, M>(source: &str, variables: &'a M) -> Result<String, Error>
where
M: VariableMap<'a> + ?Sized,
M::Value: AsRef<str>,
{
let output = template::Template::from_str(source)?.expand(variables)?;
Ok(output)
}
pub fn substitute_bytes<'a, M>(source: &[u8], variables: &'a M) -> Result<Vec<u8>, Error>
where
M: VariableMap<'a> + ?Sized,
M::Value: AsRef<[u8]>,
{
let output = template::ByteTemplate::from_slice(source)?.expand(variables)?;
Ok(output)
}
#[cfg(test)]
#[rustfmt::skip]
mod test {
use super::*;
use assert2::{assert, check, let_assert};
use std::collections::BTreeMap;
#[test]
fn test_substitute() {
let mut map: BTreeMap<String, String> = BTreeMap::new();
map.insert("name".into(), "world".into());
check!(let Ok("Hello world!") = substitute("Hello $name!", &map).as_deref());
check!(let Ok("Hello world!") = substitute("Hello ${name}!", &map).as_deref());
check!(let Ok("Hello world!") = substitute("Hello ${name:not-world}!", &map).as_deref());
check!(let Ok("Hello world!") = substitute("Hello ${not_name:world}!", &map).as_deref());
let mut map: BTreeMap<&str, &str> = BTreeMap::new();
map.insert("name", "world");
check!(let Ok("Hello world!") = substitute("Hello $name!", &map).as_deref());
check!(let Ok("Hello world!") = substitute("Hello ${name}!", &map).as_deref());
check!(let Ok("Hello world!") = substitute("Hello ${name:not-world}!", &map).as_deref());
check!(let Ok("Hello world!") = substitute("Hello ${not_name:world}!", &map).as_deref());
}
#[test]
fn substitution_in_default_value() {
let mut map = BTreeMap::new();
map.insert("name", "world");
check!(let Ok("Hello cruel world!") = substitute("Hello ${not_name:cruel $name}!", &map).as_deref());
}
#[test]
fn recursive_substitution_in_default_value() {
let mut map = BTreeMap::new();
check!(let Ok("Hello cruel world!") = substitute("Hello ${a:cruel ${b:world}}!", &map).as_deref());
check!(let Ok("Hello cruel round world!") = substitute("Hello ${a:cruel ${b:round ${c:world}}}!", &map).as_deref());
map.insert("c", "planet");
check!(let Ok("Hello cruel round planet!") = substitute("Hello ${a:cruel ${b:round ${c:world}}}!", &map).as_deref());
map.insert("b", "sphere");
check!(let Ok("Hello cruel sphere!") = substitute("Hello ${a:cruel ${b:round ${c:world}}}!", &map).as_deref());
map.insert("a", "spaceship");
check!(let Ok("Hello spaceship!") = substitute("Hello ${a:cruel ${b:round ${c:world}}}!", &map).as_deref());
}
#[test]
fn test_substitute_bytes() {
let mut map: BTreeMap<String, Vec<u8>> = BTreeMap::new();
map.insert("name".into(), b"world"[..].into());
check!(let Ok(b"Hello world!") = substitute_bytes(b"Hello $name!", &map).as_deref());
check!(let Ok(b"Hello world!") = substitute_bytes(b"Hello ${name}!", &map).as_deref());
check!(let Ok(b"Hello world!") = substitute_bytes(b"Hello ${name:not-world}!", &map).as_deref());
check!(let Ok(b"Hello world!") = substitute_bytes(b"Hello ${not_name:world}!", &map).as_deref());
let mut map: BTreeMap<&str, &[u8]> = BTreeMap::new();
map.insert("name", b"world");
check!(let Ok(b"Hello world!") = substitute_bytes(b"Hello $name!", &map).as_deref());
check!(let Ok(b"Hello world!") = substitute_bytes(b"Hello ${name}!", &map).as_deref());
check!(let Ok(b"Hello world!") = substitute_bytes(b"Hello ${name:not-world}!", &map).as_deref());
check!(let Ok(b"Hello world!") = substitute_bytes(b"Hello ${not_name:world}!", &map).as_deref());
}
#[test]
fn test_invalid_escape_sequence() {
let map: BTreeMap<String, String> = BTreeMap::new();
let source = r"Hello \world!";
let_assert!(Err(e) = substitute(source, &map));
assert!(e.to_string() == r"Invalid escape sequence: \w");
#[rustfmt::skip]
assert!(e.source_highlighting(source) == concat!(
r" Hello \world!", "\n",
r" ^^", "\n",
));
let source = r"Hello \❤❤";
let_assert!(Err(e) = substitute(source, &map));
assert!(e.to_string() == r"Invalid escape sequence: \❤");
#[rustfmt::skip]
assert!(e.source_highlighting(source) == concat!(
r" Hello \❤❤", "\n",
r" ^^", "\n",
));
let source = r"Hello world!\";
let_assert!(Err(e) = substitute(source, &map));
assert!(e.to_string() == r"Invalid escape sequence: missing escape character");
#[rustfmt::skip]
assert!(e.source_highlighting(source) == concat!(
r" Hello world!\", "\n",
r" ^", "\n",
));
}
#[test]
fn test_missing_variable_name() {
let map: BTreeMap<String, String> = BTreeMap::new();
let source = r"Hello $!";
let_assert!(Err(e) = substitute(source, &map));
assert!(e.to_string() == r"Missing variable name");
#[rustfmt::skip]
assert!(e.source_highlighting(source) == concat!(
r" Hello $!", "\n",
r" ^", "\n",
));
let source = r"Hello ${}!";
let_assert!(Err(e) = substitute(source, &map));
assert!(e.to_string() == r"Missing variable name");
#[rustfmt::skip]
assert!(e.source_highlighting(source) == concat!(
r" Hello ${}!", "\n",
r" ^^", "\n",
));
let source = r"Hello ${:fallback}!";
let_assert!(Err(e) = substitute(source, &map));
assert!(e.to_string() == r"Missing variable name");
#[rustfmt::skip]
assert!(e.source_highlighting(source) == concat!(
r" Hello ${:fallback}!", "\n",
r" ^^", "\n",
));
let source = r"Hello $❤";
let_assert!(Err(e) = substitute(source, &map));
assert!(e.to_string() == r"Missing variable name");
#[rustfmt::skip]
assert!(e.source_highlighting(source) == concat!(
r" Hello $❤", "\n",
r" ^", "\n",
));
let source = r"Hello $";
let_assert!(Err(e) = substitute(source, &map));
assert!(e.to_string() == r"Missing variable name");
#[rustfmt::skip]
assert!(e.source_highlighting(source) == concat!(
r" Hello $", "\n",
r" ^", "\n",
));
}
#[test]
fn test_unexpected_character() {
let map: BTreeMap<String, String> = BTreeMap::new();
let source = "Hello ${name)!";
let_assert!(Err(e) = substitute(source, &map));
assert!(e.to_string() == "Unexpected character: ')', expected a closing brace ('}') or colon (':')");
#[rustfmt::skip]
assert!(e.source_highlighting(source) == concat!(
" Hello ${name)!\n",
" ^\n",
));
let source = "Hello ${name❤";
let_assert!(Err(e) = substitute(source, &map));
assert!(e.to_string() == "Unexpected character: '❤', expected a closing brace ('}') or colon (':')");
#[rustfmt::skip]
assert!(e.source_highlighting(source) == concat!(
" Hello ${name❤\n",
" ^\n",
));
let source = b"\xE2\x98Hello ${name\xE2\x98";
let_assert!(Err(e) = substitute_bytes(source, &map));
assert!(e.to_string() == "Unexpected character: '\\xE2', expected a closing brace ('}') or colon (':')");
}
#[test]
fn test_missing_closing_brace() {
let map: BTreeMap<String, String> = BTreeMap::new();
let source = "Hello ${name";
let_assert!(Err(e) = substitute(source, &map));
assert!(e.to_string() == "Missing closing brace");
#[rustfmt::skip]
assert!(e.source_highlighting(source) == concat!(
" Hello ${name\n",
" ^\n",
));
let source = "Hello ${name:fallback";
let_assert!(Err(e) = substitute(source, &map));
assert!(e.to_string() == "Missing closing brace");
#[rustfmt::skip]
assert!(e.source_highlighting(source) == concat!(
" Hello ${name:fallback\n",
" ^\n",
));
}
#[test]
fn test_substitute_no_such_variable() {
let map: BTreeMap<String, String> = BTreeMap::new();
let source = "Hello ${name}!";
let_assert!(Err(e) = substitute(source, &map));
assert!(e.to_string() == "No such variable: $name");
#[rustfmt::skip]
assert!(e.source_highlighting(source) == concat!(
" Hello ${name}!\n",
" ^^^^\n",
));
let source = "Hello $name!";
let_assert!(Err(e) = substitute(source, &map));
assert!(e.to_string() == "No such variable: $name");
#[rustfmt::skip]
assert!(e.source_highlighting(source) == concat!(
" Hello $name!\n",
" ^^^^\n",
));
}
#[test]
fn test_dyn_variable_map() {
let mut variables = BTreeMap::new();
variables.insert(String::from("aap"), String::from("noot"));
let variables: &dyn VariableMap<Value = &String> = &variables;
let_assert!(Ok(expanded) = substitute("one ${aap}", variables));
assert!(expanded == "one noot");
}
#[test]
fn test_unicode_invalid_escape_sequence() {
let mut variables = BTreeMap::new();
variables.insert(String::from("aap"), String::from("noot"));
let source = r"emoticon: \( ^▽^ )/";
let_assert!(Err(e) = substitute(source, &variables));
#[rustfmt::skip]
assert!(e.source_highlighting(source) == concat!(
r" emoticon: \( ^▽^ )/", "\n",
r" ^^^", "\n",
));
}
}