handlebars/
error.rs

1use std::error::Error as StdError;
2use std::fmt::{self, Write};
3use std::io::Error as IOError;
4use std::string::FromUtf8Error;
5
6use serde_json::error::Error as SerdeError;
7use thiserror::Error;
8
9#[cfg(feature = "dir_source")]
10use walkdir::Error as WalkdirError;
11
12#[cfg(feature = "script_helper")]
13use rhai::{EvalAltResult, ParseError};
14
15/// Error when rendering data on template.
16#[derive(Debug)]
17pub struct RenderError {
18    pub template_name: Option<String>,
19    pub line_no: Option<usize>,
20    pub column_no: Option<usize>,
21    reason: Box<RenderErrorReason>,
22    unimplemented: bool,
23    // backtrace: Backtrace,
24}
25
26impl fmt::Display for RenderError {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
28        let desc = self.reason.to_string();
29
30        match (self.line_no, self.column_no) {
31            (Some(line), Some(col)) => write!(
32                f,
33                "Error rendering \"{}\" line {}, col {}: {}",
34                self.template_name.as_deref().unwrap_or("Unnamed template"),
35                line,
36                col,
37                desc
38            ),
39            _ => write!(f, "{}", desc),
40        }
41    }
42}
43
44impl From<IOError> for RenderError {
45    fn from(e: IOError) -> RenderError {
46        RenderErrorReason::IOError(e).into()
47    }
48}
49
50impl From<FromUtf8Error> for RenderError {
51    fn from(e: FromUtf8Error) -> Self {
52        RenderErrorReason::Utf8Error(e).into()
53    }
54}
55
56impl From<TemplateError> for RenderError {
57    fn from(e: TemplateError) -> Self {
58        RenderErrorReason::TemplateError(e).into()
59    }
60}
61
62/// Template rendering error
63#[derive(Debug, Error)]
64pub enum RenderErrorReason {
65    #[error("Template not found {0}")]
66    TemplateNotFound(String),
67    #[error("Failed to parse template {0}")]
68    TemplateError(
69        #[from]
70        #[source]
71        TemplateError,
72    ),
73    #[error("Failed to access variable in strict mode {0:?}")]
74    MissingVariable(Option<String>),
75    #[error("Partial not found {0}")]
76    PartialNotFound(String),
77    #[error("Helper not found {0}")]
78    HelperNotFound(String),
79    #[error("Helper/Decorator {0} param at index {1} required but not found")]
80    ParamNotFoundForIndex(&'static str, usize),
81    #[error("Helper/Decorator {0} param with name {1} required but not found")]
82    ParamNotFoundForName(&'static str, String),
83    #[error("Helper/Decorator {0} param with name {1} type mismatch for {2}")]
84    ParamTypeMismatchForName(&'static str, String, String),
85    #[error("Helper/Decorator {0} hash with name {1} type mismatch for {2}")]
86    HashTypeMismatchForName(&'static str, String, String),
87    #[error("Decorator not found {0}")]
88    DecoratorNotFound(String),
89    #[error("Can not include current template in partial")]
90    CannotIncludeSelf,
91    #[error("Invalid logging level: {0}")]
92    InvalidLoggingLevel(String),
93    #[error("Invalid param type, {0} expected")]
94    InvalidParamType(&'static str),
95    #[error("Block content required")]
96    BlockContentRequired,
97    #[error("Invalid json path {0}")]
98    InvalidJsonPath(String),
99    #[error("Cannot access array/vector with string index, {0}")]
100    InvalidJsonIndex(String),
101    #[error("Failed to access JSON data: {0}")]
102    SerdeError(
103        #[from]
104        #[source]
105        SerdeError,
106    ),
107    #[error("IO Error: {0}")]
108    IOError(
109        #[from]
110        #[source]
111        IOError,
112    ),
113    #[error("FromUtf8Error: {0}")]
114    Utf8Error(
115        #[from]
116        #[source]
117        FromUtf8Error,
118    ),
119    #[error("Nested error: {0}")]
120    NestedError(#[source] Box<dyn StdError + Send + Sync + 'static>),
121    #[cfg(feature = "script_helper")]
122    #[error("Cannot convert data to Rhai dynamic: {0}")]
123    ScriptValueError(
124        #[from]
125        #[source]
126        Box<EvalAltResult>,
127    ),
128    #[cfg(feature = "script_helper")]
129    #[error("Failed to load rhai script: {0}")]
130    ScriptLoadError(
131        #[from]
132        #[source]
133        ScriptError,
134    ),
135    #[error("Unimplemented")]
136    Unimplemented,
137    #[error("{0}")]
138    Other(String),
139}
140
141impl From<RenderErrorReason> for RenderError {
142    fn from(e: RenderErrorReason) -> RenderError {
143        RenderError {
144            template_name: None,
145            line_no: None,
146            column_no: None,
147            reason: Box::new(e),
148            unimplemented: false,
149        }
150    }
151}
152
153impl RenderError {
154    #[deprecated(since = "5.0.0", note = "Use RenderErrorReason instead")]
155    pub fn new<T: AsRef<str>>(desc: T) -> RenderError {
156        RenderErrorReason::Other(desc.as_ref().to_string()).into()
157    }
158
159    pub fn strict_error(path: Option<&String>) -> RenderError {
160        RenderErrorReason::MissingVariable(path.map(|p| p.to_owned())).into()
161    }
162
163    #[deprecated(since = "5.0.0", note = "Use RenderErrorReason::NestedError instead")]
164    pub fn from_error<E>(_error_info: &str, cause: E) -> RenderError
165    where
166        E: StdError + Send + Sync + 'static,
167    {
168        RenderErrorReason::NestedError(Box::new(cause)).into()
169    }
170
171    #[inline]
172    pub(crate) fn is_unimplemented(&self) -> bool {
173        matches!(*self.reason, RenderErrorReason::Unimplemented)
174    }
175
176    /// Get `RenderErrorReason` for this error
177    pub fn reason(&self) -> &RenderErrorReason {
178        self.reason.as_ref()
179    }
180}
181
182impl StdError for RenderError {
183    fn source(&self) -> Option<&(dyn StdError + 'static)> {
184        Some(self.reason())
185    }
186}
187
188/// Template parsing error
189#[derive(Debug, Error)]
190pub enum TemplateErrorReason {
191    #[error("helper {0:?} was opened, but {1:?} is closing")]
192    MismatchingClosedHelper(String, String),
193    #[error("decorator {0:?} was opened, but {1:?} is closing")]
194    MismatchingClosedDecorator(String, String),
195    #[error("invalid handlebars syntax.")]
196    InvalidSyntax,
197    #[error("invalid parameter {0:?}")]
198    InvalidParam(String),
199    #[error("nested subexpression is not supported")]
200    NestedSubexpression,
201    #[error("Template \"{1}\": {0}")]
202    IoError(IOError, String),
203    #[cfg(feature = "dir_source")]
204    #[error("Walk dir error: {err}")]
205    WalkdirError {
206        #[from]
207        err: WalkdirError,
208    },
209}
210
211/// Error on parsing template.
212#[derive(Debug, Error)]
213pub struct TemplateError {
214    reason: Box<TemplateErrorReason>,
215    template_name: Option<String>,
216    line_no: Option<usize>,
217    column_no: Option<usize>,
218    segment: Option<String>,
219}
220
221impl TemplateError {
222    #[allow(deprecated)]
223    pub fn of(e: TemplateErrorReason) -> TemplateError {
224        TemplateError {
225            reason: Box::new(e),
226            template_name: None,
227            line_no: None,
228            column_no: None,
229            segment: None,
230        }
231    }
232
233    pub fn at(mut self, template_str: &str, line_no: usize, column_no: usize) -> TemplateError {
234        self.line_no = Some(line_no);
235        self.column_no = Some(column_no);
236        self.segment = Some(template_segment(template_str, line_no, column_no));
237        self
238    }
239
240    pub fn in_template(mut self, name: String) -> TemplateError {
241        self.template_name = Some(name);
242        self
243    }
244
245    /// Get underlying reason for the error
246    pub fn reason(&self) -> &TemplateErrorReason {
247        &self.reason
248    }
249
250    /// Get the line number and column number of this error
251    pub fn pos(&self) -> Option<(usize, usize)> {
252        match (self.line_no, self.column_no) {
253            (Some(line_no), Some(column_no)) => Some((line_no, column_no)),
254            _ => None,
255        }
256    }
257
258    /// Get template name of this error
259    /// Returns `None` when the template has no associated name
260    pub fn name(&self) -> Option<&String> {
261        self.template_name.as_ref()
262    }
263}
264
265impl From<(IOError, String)> for TemplateError {
266    fn from(err_info: (IOError, String)) -> TemplateError {
267        let (e, name) = err_info;
268        TemplateError::of(TemplateErrorReason::IoError(e, name))
269    }
270}
271
272#[cfg(feature = "dir_source")]
273impl From<WalkdirError> for TemplateError {
274    fn from(e: WalkdirError) -> TemplateError {
275        TemplateError::of(TemplateErrorReason::from(e))
276    }
277}
278
279fn template_segment(template_str: &str, line: usize, col: usize) -> String {
280    let range = 3;
281    let line_start = if line >= range { line - range } else { 0 };
282    let line_end = line + range;
283
284    let mut buf = String::new();
285    for (line_count, line_content) in template_str.lines().enumerate() {
286        if line_count >= line_start && line_count <= line_end {
287            let _ = writeln!(&mut buf, "{line_count:4} | {line_content}");
288            if line_count == line - 1 {
289                buf.push_str("     |");
290                for c in 0..line_content.len() {
291                    if c != col {
292                        buf.push('-');
293                    } else {
294                        buf.push('^');
295                    }
296                }
297                buf.push('\n');
298            }
299        }
300    }
301
302    buf
303}
304
305impl fmt::Display for TemplateError {
306    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
307        match (self.line_no, self.column_no, &self.segment) {
308            (Some(line), Some(col), Some(seg)) => writeln!(
309                f,
310                "Template error: {}\n    --> Template error in \"{}\":{}:{}\n     |\n{}     |\n     = reason: {}",
311                self.reason(),
312                self.template_name
313                    .as_ref()
314                    .unwrap_or(&"Unnamed template".to_owned()),
315                line,
316                col,
317                seg,
318                self.reason()
319            ),
320            _ => write!(f, "{}", self.reason()),
321        }
322    }
323}
324
325#[cfg(feature = "script_helper")]
326#[derive(Debug, Error)]
327pub enum ScriptError {
328    #[error(transparent)]
329    IoError(#[from] IOError),
330
331    #[error(transparent)]
332    ParseError(#[from] ParseError),
333}
334
335#[cfg(test)]
336mod test {
337    use super::*;
338
339    #[test]
340    fn test_source_error() {
341        let reason = RenderErrorReason::TemplateNotFound("unnamed".to_owned());
342        let render_error = RenderError::from(reason);
343
344        let reason2 = render_error.source().unwrap();
345        assert!(matches!(
346            reason2.downcast_ref::<RenderErrorReason>().unwrap(),
347            RenderErrorReason::TemplateNotFound(_)
348        ));
349    }
350}