use crate::error::error_ctx::render_ftl_stack_section;
use crate::error::{ErrorCtx, FlowKind};
use crate::span::Span;
pub type Result<T> = std::result::Result<T, TemplateError>;
#[derive(Debug)]
pub enum TemplateError {
InvalidReference { name: String, ctx: Box<ErrorCtx> },
TypeMismatch {
expected: &'static str,
actual: String,
ctx: Box<ErrorCtx>,
},
Misc { message: String },
Parse {
template: String,
line: u32,
col: u32,
message: String,
},
Stop { message: Option<String> },
Flow(FlowKind),
NotFound { name: String },
Io(std::io::Error),
Model { message: String },
}
impl TemplateError {
pub fn with_stack(self, stack: Vec<crate::error::StackFrame>) -> Self {
crate::core::template_exception::with_stack(self, stack)
}
pub fn invalid_reference(name: impl Into<String>) -> Self {
crate::core::invalid_reference_exception::new_instance(name)
}
pub fn invalid_reference_at(name: impl Into<String>, span: Span) -> Self {
crate::core::invalid_reference_exception::new_instance_at(name, span)
}
pub fn with_dot_tip(self) -> Self {
crate::core::invalid_reference_exception::with_dot_tip(self)
}
pub fn type_mismatch(expected: &'static str, actual: impl Into<String>) -> Self {
crate::core::unexpected_type_exception::new_type_mismatch(expected, actual)
}
pub fn type_mismatch_at(expected: &'static str, actual: impl Into<String>, span: Span) -> Self {
crate::core::unexpected_type_exception::new_type_mismatch_at(expected, actual, span)
}
pub fn with_blame(self, node_type_symbol: &str, role: &str, blamed_expr: &str) -> Self {
crate::core::unexpected_type_exception::with_blame(
self,
node_type_symbol,
role,
blamed_expr,
)
}
pub fn with_blame_at(
self,
node_type_symbol: &str,
role: &str,
blamed_expr: &str,
template_name: &str,
span: Span,
) -> Self {
crate::core::unexpected_type_exception::with_blame_at(
self,
node_type_symbol,
role,
blamed_expr,
template_name,
span,
)
}
pub fn with_assignment_target(self, target: &str) -> Self {
crate::core::unexpected_type_exception::with_assignment_target(self, target)
}
pub fn with_tip(self, tip: &str) -> Self {
crate::core::unexpected_type_exception::with_tip(self, tip)
}
pub fn with_location(mut self, template_name: &str, span: Span) -> Self {
match &mut self {
TemplateError::InvalidReference { ctx, .. }
| TemplateError::TypeMismatch { ctx, .. } => {
if ctx.span.line == 0 {
ctx.span = span;
}
if ctx.template_name.is_none() {
ctx.template_name = Some(template_name.to_string());
}
}
_ => {}
}
self
}
pub fn has_location(&self) -> bool {
match self {
TemplateError::InvalidReference { ctx, .. }
| TemplateError::TypeMismatch { ctx, .. } => {
ctx.span.line > 0 || ctx.template_name.is_some()
}
_ => false,
}
}
pub fn with_expected_phrase(self, phrase: &str) -> Self {
crate::core::unexpected_type_exception::with_expected_phrase(self, phrase)
}
pub fn misc(message: impl Into<String>) -> Self {
crate::core::_misc_template_exception::new(message)
}
pub fn to_user_message(&self) -> String {
match self {
TemplateError::InvalidReference { name, ctx } => {
let mut s = format!(
"The following has evaluated to null or missing:\n==> {name}{}",
ctx.blamed_location()
);
s.push_str(&render_tips(
ctx.extra_tip
.iter()
.map(|t| t.as_str())
.chain([INVALID_REFERENCE_TIP]),
));
if let Some(sec) = render_ftl_stack_section(&ctx.instruction_stack) {
s.push_str(&sec);
}
s
}
TemplateError::TypeMismatch {
expected,
actual,
ctx,
} => {
let mut s = String::new();
if let Some(b) = &ctx.blamer {
s.push_str(b);
}
let expected_phrase = ctx
.expected_phrase
.clone()
.unwrap_or_else(|| expected_phrase_for(expected));
s.push_str(&format!("Expected {expected_phrase}, but "));
match &ctx.assignment_target {
Some(t) => {
s.push_str(&format!("assignment target variable {t} has evaluated to "))
}
None => s.push_str("this has evaluated to "),
}
s.push_str(&a_or_an(actual));
if ctx.blamed_expr.is_some() {
s.push(':');
} else {
s.push('.');
}
if let Some(blamed) = &ctx.blamed_expr {
s.push_str(&format!("\n==> {blamed}{}", ctx.blamed_location()));
}
if let Some(tip) = &ctx.extra_tip {
s.push_str(&render_tips([tip.as_str()]));
}
if let Some(sec) = render_ftl_stack_section(&ctx.instruction_stack) {
s.push_str(&sec);
}
s
}
TemplateError::Misc { message } => message.clone(),
TemplateError::Parse {
template,
line,
col,
message,
} => format!(
"Syntax error in template \"{template}\" in line {line}, column {col}:\n{message}"
),
TemplateError::Stop { message } => match message {
Some(m) => m.clone(),
None => "[No error description was available.]".to_string(),
},
TemplateError::Flow(kind) => match kind {
FlowKind::Break => "break is illegal outside a loop".to_string(),
FlowKind::Continue => "continue is illegal outside a loop".to_string(),
},
TemplateError::NotFound { name } => {
format!("Template not found for name \"{name}\".")
}
TemplateError::Io(e) => e.to_string(),
TemplateError::Model { message } => message.clone(),
}
}
}
pub(crate) const INVALID_REFERENCE_TIP: &str =
crate::core::invalid_reference_exception::INVALID_REFERENCE_TIP;
fn expected_phrase_for(expected: &'static str) -> String {
crate::core::unexpected_type_exception::expected_phrase_for(expected)
}
fn a_or_an(type_name: &str) -> String {
crate::core::unexpected_type_exception::a_or_an(type_name)
}
fn render_tips<'a>(tips: impl IntoIterator<Item = &'a str>) -> String {
let mut s = String::new();
let mut first = true;
for tip in tips {
if first {
s.push_str("\n\n");
first = false;
} else {
s.push('\n');
}
s.push_str("----\nTip: ");
s.push_str(tip);
}
if !first {
s.push_str("\n----");
}
s
}
impl std::fmt::Display for TemplateError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.to_user_message())
}
}
impl std::error::Error for TemplateError {}
impl From<std::io::Error> for TemplateError {
fn from(e: std::io::Error) -> Self {
TemplateError::Io(e)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn invalid_reference_message_matches_java() {
let mut e = TemplateError::invalid_reference_at("user.name", Span::new(1, 3));
if let TemplateError::InvalidReference { ctx, .. } = &mut e {
ctx.template_name = Some("t.ftl".to_string());
}
let msg = e.to_user_message();
assert!(
msg.starts_with(
"The following has evaluated to null or missing:\n==> user.name [in template"
),
"{msg}"
);
assert!(
msg.contains("\n\n----\nTip: If the failing expression is known to legally refer"),
"{msg}"
);
assert!(
msg.ends_with("(myOptionalVar.foo)!myDefault, (myOptionalVar.foo)??\n----"),
"{msg}"
);
}
#[test]
fn type_mismatch_blame_matches_java() {
let mut e = TemplateError::type_mismatch_at("number", "string", Span::new(1, 7))
.with_blame("-", "right-hand operand", "s");
if let TemplateError::TypeMismatch { ctx, .. } = &mut e {
ctx.template_name = Some("t.ftl".to_string());
}
assert_eq!(
e.to_user_message(),
"For \"-\" right-hand operand: Expected a number, but this has evaluated to a string:\n==> s [in template \"t.ftl\" at line 1, column 7]"
);
}
#[test]
fn type_mismatch_assignment_target() {
let e = TemplateError::type_mismatch("number", "string").with_assignment_target("x");
assert!(matches!(&e, TemplateError::TypeMismatch { ctx, .. } if ctx.blamed_expr.is_none()));
assert_eq!(
e.to_user_message(),
"Expected a number, but assignment target variable \"x\" has evaluated to a string."
);
}
#[test]
fn flow_kind_display() {
assert_eq!(
TemplateError::Flow(FlowKind::Break).to_user_message(),
"break is illegal outside a loop"
);
assert_eq!(
TemplateError::Flow(FlowKind::Continue).to_user_message(),
"continue is illegal outside a loop"
);
}
}