handlebars/
error.rs

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