Skip to main content

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