gtmpl_ng/
error.rs

1use crate::node::{ChainNode, CommandNode, Nodes, PipeNode};
2use gtmpl_value::{FuncError, Value};
3use std::{fmt, num::ParseIntError, string::FromUtf8Error};
4use thiserror::Error;
5
6#[derive(Debug, Clone)]
7pub struct ErrorContext {
8    pub name: String,
9    pub line: usize,
10    pub col: usize,
11    pub len: usize,
12}
13
14impl fmt::Display for ErrorContext {
15    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
16        write!(f, "{}:{}:{}:{}", self.name, self.line, self.col, self.len)
17    }
18}
19
20/// A structured error with location and optional cause chain
21#[derive(Debug)]
22pub struct StructuredError {
23    pub context: ErrorContext,
24    pub message: String,
25    pub cause: Option<Box<StructuredError>>,
26}
27
28impl StructuredError {
29    pub fn new(
30        name: impl ToString,
31        line: usize,
32        col: usize,
33        len: usize,
34        message: impl ToString,
35    ) -> Self {
36        Self {
37            context: ErrorContext {
38                name: name.to_string(),
39                line,
40                col,
41                len,
42            },
43            message: message.to_string(),
44            cause: None,
45        }
46    }
47
48    pub fn with_cause(mut self, cause: StructuredError) -> Self {
49        self.cause = Some(Box::new(cause));
50        self
51    }
52
53    /// Iterate through the error chain
54    pub fn chain(&self) -> ErrorChainIter<'_> {
55        ErrorChainIter {
56            current: Some(self),
57        }
58    }
59}
60
61pub struct ErrorChainIter<'a> {
62    current: Option<&'a StructuredError>,
63}
64
65impl<'a> Iterator for ErrorChainIter<'a> {
66    type Item = &'a StructuredError;
67
68    fn next(&mut self) -> Option<Self::Item> {
69        let current = self.current?;
70        self.current = current.cause.as_deref();
71        Some(current)
72    }
73}
74
75impl fmt::Display for StructuredError {
76    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
77        write!(f, "template: {}:{}", self.context, self.message)?;
78        if let Some(ref cause) = self.cause {
79            write!(f, "\n  caused by: {}", cause)?;
80        }
81        Ok(())
82    }
83}
84
85#[derive(Error, Debug)]
86pub enum ParseError {
87    #[error("unexpected {0} in define clause")]
88    UnexpectedInDefineClause(Nodes),
89    #[error("unexpected end")]
90    UnexpectedEnd,
91    #[error("template: {0}:{1}")]
92    WithContext(ErrorContext, String),
93    #[error("no tree")]
94    NoTree,
95    #[error(transparent)]
96    NodeError(#[from] NodeError),
97    #[error("enable gtmpl_dynamic_template to use a pipeline as name")]
98    NoDynamicTemplate,
99    #[error("unable to parse string: {0}")]
100    UnableToParseString(String),
101}
102
103impl ParseError {
104    pub fn with_context(
105        name: impl ToString,
106        line: usize,
107        col: usize,
108        len: usize,
109        msg: impl ToString,
110    ) -> Self {
111        Self::WithContext(
112            ErrorContext {
113                name: name.to_string(),
114                line,
115                col,
116                len,
117            },
118            msg.to_string(),
119        )
120    }
121}
122
123#[derive(Error, Debug)]
124pub enum NodeError {
125    #[error("unable to unquote")]
126    UnquoteError,
127    #[error("NaN")]
128    NaN,
129    #[error("not a tree node")]
130    NaTN,
131}
132
133#[derive(Error, Debug)]
134pub enum PrintError {
135    #[error("unable to process verb: {0}")]
136    UnableToProcessVerb(String),
137    #[error("{0:X} is not a valid char")]
138    NotAValidChar(i128),
139    #[error("unable to format {0} as {1}")]
140    UnableToFormat(Value, char),
141    #[error("unable to terminate format arg: {0}")]
142    UnableToTerminateFormatArg(String),
143    #[error("missing ] in {0}")]
144    MissingClosingBracket(String),
145    #[error("unable to parse index: {0}")]
146    UnableToParseIndex(ParseIntError),
147    #[error("unable to parse width: {0}")]
148    UnableToParseWidth(ParseIntError),
149    #[error("width after index (e.g. %[3]2d)")]
150    WithAfterIndex,
151    #[error("precision after index (e.g. %[3].2d)")]
152    PrecisionAfterIndex,
153}
154
155#[derive(Error, Debug)]
156pub enum ExecError {
157    #[error("{0}")]
158    Structured(StructuredError),
159    #[error("{0} is an incomplete or empty template")]
160    IncompleteTemplate(String),
161    #[error("{0}")]
162    IOError(#[from] std::io::Error),
163    #[error("unknown node: {0}")]
164    UnknownNode(Nodes),
165    #[error("expected if or with node, got {0}")]
166    ExpectedIfOrWith(Nodes),
167    #[error("unable to convert output to uft-8: {0}")]
168    Utf8ConversionFailed(FromUtf8Error),
169    #[error("empty var stack")]
170    EmptyStack,
171    #[error("var context smaller than {0}")]
172    VarContextToSmall(usize),
173    #[error("invalid range {0:?}")]
174    InvalidRange(Value),
175    #[error("pipeline must yield a String")]
176    PipelineMustYieldString,
177    #[error("template {0} not defined")]
178    TemplateNotDefined(String),
179    #[error("exceeded max template depth")]
180    MaxTemplateDepth,
181    #[error("error evaluating pipe: {0}")]
182    ErrorEvaluatingPipe(PipeNode),
183    #[error("no arguments for command node: {0}")]
184    NoArgsForCommandNode(CommandNode),
185    #[error("cannot evaluate command: {0}")]
186    CannotEvaluateCommand(Nodes),
187    #[error("field chain without fields :/")]
188    FieldChainWithoutFields,
189    #[error("{0} has arguments but cannot be invoked as function")]
190    NotAFunctionButArguments(String),
191    #[error("no fields in eval_chain_node")]
192    NoFieldsInEvalChainNode,
193    #[error("indirection through explicit nul in {0}")]
194    NullInChain(ChainNode),
195    #[error("cannot handle {0} as argument")]
196    InvalidArgument(Nodes),
197    #[error("{0} is not a defined function")]
198    UndefinedFunction(String),
199    #[error(transparent)]
200    FuncError(#[from] FuncError),
201    #[error("can't give argument to non-function {0}")]
202    ArgumentForNonFunction(Nodes),
203    #[error("only maps and objects have fields")]
204    OnlyMapsAndObjectsHaveFields,
205    #[error("no field `{0}` in {1}")]
206    NoFieldFor(String, Value),
207    #[error("variable {0} not found")]
208    VariableNotFound(String),
209}
210
211impl ExecError {
212    /// Wrap an error with location context, preserving the error chain.
213    /// The innermost (original) error location stays as primary,
214    /// outer contexts are added to the cause chain.
215    pub fn with_context(
216        name: impl ToString,
217        line: usize,
218        col: usize,
219        len: usize,
220        error: ExecError,
221    ) -> Self {
222        let new_context = ErrorContext {
223            name: name.to_string(),
224            line,
225            col,
226            len,
227        };
228
229        match error {
230            // If already structured, keep original context as primary,
231            // add new context to the cause chain
232            ExecError::Structured(mut inner) => {
233                let outer_cause = StructuredError {
234                    context: new_context,
235                    message: "while evaluating".to_string(),
236                    cause: inner.cause.take(),
237                };
238                inner.cause = Some(Box::new(outer_cause));
239                ExecError::Structured(inner)
240            }
241            // Convert other errors to structured with this context
242            other => {
243                let structured = StructuredError {
244                    context: new_context,
245                    message: other.to_string(),
246                    cause: None,
247                };
248                ExecError::Structured(structured)
249            }
250        }
251    }
252
253    /// Get the structured error if this is one
254    pub fn as_structured(&self) -> Option<&StructuredError> {
255        match self {
256            ExecError::Structured(s) => Some(s),
257            _ => None,
258        }
259    }
260}
261
262#[derive(Error, Debug)]
263pub enum TemplateError {
264    #[error(transparent)]
265    ExecError(#[from] ExecError),
266    #[error(transparent)]
267    ParseError(#[from] ParseError),
268}