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#[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 }
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#[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 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#[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#[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 pub fn reason(&self) -> &TemplateErrorReason {
259 &self.reason
260 }
261
262 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 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}