use crate::canonical::py_float_repr;
use crate::value::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TemplateFailureCode {
UnboundParam,
ResultScopeInCore,
InvalidStringify,
}
impl TemplateFailureCode {
pub fn as_str(self) -> &'static str {
match self {
TemplateFailureCode::UnboundParam => "UNBOUND_PARAM",
TemplateFailureCode::ResultScopeInCore => "RESULT_SCOPE_IN_CORE",
TemplateFailureCode::InvalidStringify => "INVALID_STRINGIFY",
}
}
}
#[derive(Debug, Clone)]
pub struct TemplateFailure {
pub code: TemplateFailureCode,
pub message: String,
}
impl std::fmt::Display for TemplateFailure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.code.as_str(), self.message)
}
}
impl std::error::Error for TemplateFailure {}
fn fail<T>(code: TemplateFailureCode, message: impl Into<String>) -> Result<T, TemplateFailure> {
Err(TemplateFailure {
code,
message: message.into(),
})
}
pub fn render_template(
template: &str,
params: &[(String, Value)],
) -> Result<String, TemplateFailure> {
let mut out = String::with_capacity(template.len());
let chars: Vec<char> = template.chars().collect();
let mut i = 0;
while i < chars.len() {
let c = chars[i];
if c == '{' {
if let Some(close) = find_placeholder_close(&chars, i) {
let name: String = chars[i + 1..close].iter().collect();
out.push_str(&resolve(template, &name, params)?);
i = close + 1;
continue;
}
out.push(c);
i += 1;
} else {
out.push(c);
i += 1;
}
}
Ok(out)
}
fn find_placeholder_close(chars: &[char], open: usize) -> Option<usize> {
let mut j = open + 1;
let mut count = 0;
while j < chars.len() {
match chars[j] {
'{' => return None, '}' => {
if count == 0 {
return None; }
return Some(j);
}
_ => {
count += 1;
j += 1;
}
}
}
None
}
fn resolve(
template: &str,
name: &str,
params: &[(String, Value)],
) -> Result<String, TemplateFailure> {
if name.starts_with("result.") || name.starts_with("item.") {
return fail(
TemplateFailureCode::ResultScopeInCore,
format!("template references '{{{name}}}' resolved by a different layer, not core render_template"),
);
}
match params.iter().find(|(k, _)| k == name) {
Some((_, Value::Null)) | None => fail(
TemplateFailureCode::UnboundParam,
format!("template '{template}' references unbound parameter '{name}'"),
),
Some((_, v)) => stringify(v),
}
}
fn stringify(v: &Value) -> Result<String, TemplateFailure> {
match v {
Value::Str(s) => Ok(s.clone()),
Value::Bool(b) => Ok(if *b { "True".into() } else { "False".into() }),
Value::Int(i) => Ok(i.to_string()),
Value::Float(f) => py_float_repr(*f).map_err(|_| TemplateFailure {
code: TemplateFailureCode::InvalidStringify,
message: format!("cannot stringify non-finite float {f}"),
}),
other => fail(
TemplateFailureCode::InvalidStringify,
format!("cannot stringify {} into a template", other.type_name()),
),
}
}
pub fn resolve_partial(template: &str, params: &[(String, Value)]) -> String {
let chars: Vec<char> = template.chars().collect();
let mut out = String::with_capacity(template.len());
let mut i = 0;
while i < chars.len() {
if chars[i] == '{' {
if let Some(close) = find_placeholder_close(&chars, i) {
let name: String = chars[i + 1..close].iter().collect();
if name.starts_with("result.") || name.starts_with("item.") {
out.push_str(&chars[i..=close].iter().collect::<String>());
i = close + 1;
continue;
}
match params.iter().find(|(k, _)| k == &name) {
Some((_, v)) if !matches!(v, Value::Null) => {
if let Ok(s) = stringify(v) {
out.push_str(&s);
} else {
out.push_str(&chars[i..=close].iter().collect::<String>());
}
}
_ => out.push_str(&chars[i..=close].iter().collect::<String>()),
}
i = close + 1;
continue;
}
}
out.push(chars[i]);
i += 1;
}
out
}