use std::collections::HashMap;
use anyhow::{anyhow, bail};
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub enum Value {
#[default]
None,
Literal(String),
Var(String),
}
impl Value {
pub fn substitute_tokens(string: &str, vars: &impl VariableStore) -> String {
enum State {
Escaped,
Dollar,
OpenBracket,
Name,
}
let mut state = State::Dollar;
let mut out = String::new();
let mut name = String::new();
for c in string.chars() {
state = match state {
State::Escaped => {
out.push(c);
State::Dollar
}
State::Dollar => match c {
'$' => State::OpenBracket,
'\\' => State::Escaped,
_ => {
out.push(c);
state
}
},
State::OpenBracket => {
if c == '{' {
State::Name
} else {
out.push(c);
State::Dollar
}
}
State::Name => {
if c == '}' {
if let Some(var) = vars.get_var(&name) {
out.push_str(var);
}
name.clear();
State::Dollar
} else {
name.push(c);
state
}
}
}
}
out
}
pub fn get(&self, vars: &impl VariableStore) -> anyhow::Result<String> {
match self {
Self::None => bail!("Empty value"),
Self::Literal(val) => Ok(Self::substitute_tokens(val, vars)),
Self::Var(name) => vars
.get_var(name)
.map(str::to_string)
.ok_or(anyhow!("Variable {name} is not defined")),
}
}
pub fn is_some(&self) -> bool {
!matches!(self, Self::None)
}
pub fn get_as_option(&self, vars: &impl VariableStore) -> anyhow::Result<Option<String>> {
match self {
Self::None => Ok(None),
_ => Ok(Some(self.get(vars)?)),
}
}
}
pub trait VariableStore {
fn get_var(&self, var: &str) -> Option<&str>;
fn set_var(&mut self, var: String, val: String);
fn try_set_var(&mut self, var: String, val: String) -> anyhow::Result<()> {
if is_reserved_constant_var(&var) {
bail!("Tried to set the value of a reserved constant variable");
}
self.set_var(var, val);
Ok(())
}
fn set_reserved_constants(&mut self, constants: ReservedConstantVariables) {
self.set_var(
CONSTANT_VAR_MC_VERSION.to_string(),
constants.mc_version.to_string(),
);
}
fn var_exists(&self, var: &str) -> bool {
self.get_var(var).is_some()
}
}
#[derive(Debug, Default, Clone)]
pub struct HashMapVariableStore(HashMap<String, String>);
impl HashMapVariableStore {
pub fn new() -> Self {
Self(HashMap::new())
}
}
impl VariableStore for HashMapVariableStore {
fn get_var(&self, var: &str) -> Option<&str> {
self.0.get(var).map(String::as_str)
}
fn set_var(&mut self, var: String, val: String) {
self.0.insert(var, val);
}
fn var_exists(&self, var: &str) -> bool {
self.0.contains_key(var)
}
}
pub const CONSTANT_VAR_MC_VERSION: &str = "MINECRAFT_VERSION";
pub const RESERVED_CONSTANT_VARS: [&str; 1] = [CONSTANT_VAR_MC_VERSION];
pub fn is_reserved_constant_var(var: &str) -> bool {
RESERVED_CONSTANT_VARS.contains(&var)
}
pub struct ReservedConstantVariables<'a> {
pub mc_version: &'a str,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_value_substitution() {
let vars = {
let mut vars = HashMapVariableStore::new();
vars.set_var("bar".into(), "foo".into());
vars.set_var("hello".into(), "who".into());
vars
};
let string = "One ${bar} skip a ${hello}";
let string = Value::substitute_tokens(string, &vars);
assert_eq!(string, "One foo skip a who");
}
}