Skip to main content

panopticon_core/
error.rs

1/// Errors produced while building or compiling a draft pipeline.
2///
3/// `DraftError` is returned by construction methods on `Pipeline<Draft>`
4/// and by `Pipeline::compile`. Callers should match on the variants
5/// when they want to surface specific failures (duplicate names,
6/// unresolved references) with custom diagnostics — for example when a
7/// loader is translating external configuration into a pipeline.
8#[derive(Debug, thiserror::Error, PartialEq)]
9pub enum DraftError {
10    /// A [`Pipeline::var`](crate::prelude::Pipeline#method.var) call used
11    /// a name already bound to another variable.
12    #[error("A variable named '{name}' already exists in the pipeline")]
13    DuplicateVariable {
14        /// The conflicting variable name.
15        name: String,
16    },
17
18    /// A [`Pipeline::step`](crate::prelude::Pipeline#method.step) call
19    /// used a name already bound to another step.
20    #[error("A step named '{name}' already exists in the pipeline")]
21    DuplicateStep {
22        /// The conflicting step name.
23        name: String,
24    },
25
26    /// A `Pipeline::returns` call used a name already bound to another
27    /// return block.
28    #[error("A return named '{name}' already exists in the pipeline")]
29    DuplicateReturn {
30        /// The conflicting return-block name.
31        name: String,
32    },
33
34    /// Compile-time simulation found a step parameter referencing a
35    /// store entry that is not produced by any preceding step.
36    #[error(
37        "Step '{step}' references '{reference}' which is not available at that point in the pipeline"
38    )]
39    UnresolvedReference {
40        /// The step whose parameters contained the bad reference.
41        step: String,
42        /// The dotted store path that failed to resolve.
43        reference: String,
44    },
45
46    /// Compile-time simulation found a return-block parameter
47    /// referencing a store entry that is not available at the end of
48    /// the pipeline.
49    #[error(
50        "Return '{returns}' references '{reference}' which is not available at the end of the pipeline"
51    )]
52    UnresolvedReturn {
53        /// The return block whose parameters contained the bad
54        /// reference.
55        returns: String,
56        /// The dotted store path that failed to resolve.
57        reference: String,
58    },
59
60    /// Operation metadata failed validation when the operation was
61    /// registered — typically a [`NameSpec::DerivedFrom`](crate::extend::NameSpec)
62    /// referencing a missing input or an input of the wrong type.
63    #[error("Invalid operation metadata for '{operation}': {reason}")]
64    InvalidMetadata {
65        /// The operation whose metadata failed validation.
66        operation: &'static str,
67        /// Human-readable explanation of the failure.
68        reason: String,
69    },
70
71    /// A step declared an extension requirement that is not registered
72    /// on the pipeline.
73    #[error(
74        "Step '{step}' (operation '{operation}') requires extension '{extension}' which is not registered on the pipeline"
75    )]
76    MissingExtension {
77        /// The step whose operation requires the extension.
78        step: String,
79        /// The operation name from its metadata.
80        operation: &'static str,
81        /// The extension name that should be registered via
82        /// [`Pipeline::extension`](crate::prelude::Pipeline#method.extension).
83        extension: String,
84    },
85}
86
87/// Errors produced while executing a running pipeline.
88///
89/// Returned by `Pipeline::wait` when the worker thread reports failure,
90/// and surfaced through `OperationError` variants in custom operations.
91/// Iteration and guard failures wrap the underlying cause in the
92/// [`Iteration`](Self::Iteration) and [`Guard`](Self::Guard) variants so
93/// the trace preserves the scope in which the failure occurred.
94#[derive(Debug, thiserror::Error, PartialEq)]
95pub enum OperationError {
96    /// Parameter resolution tried to look up a dotted store path that
97    /// did not exist at runtime.
98    #[error("Reference '{reference}' not found in store")]
99    ReferenceNotFound {
100        /// The dotted store path that failed to resolve.
101        reference: String,
102    },
103
104    /// A [`Param::Template`](crate::prelude::Param::Template) contained
105    /// a part that resolved to an array or map instead of a scalar.
106    #[error("Template parts must resolve to scalar values")]
107    InvalidTemplatePart,
108
109    /// Resolving a parameter into the runtime store failed — typically
110    /// because the target key was already occupied.
111    #[error("Failed to resolve parameter '{parameter}': {reason}")]
112    ParameterResolutionFailed {
113        /// The parameter name being resolved.
114        parameter: String,
115        /// Human-readable explanation from the underlying store error.
116        reason: String,
117    },
118
119    /// A custom operation called `Context::input` with a name that was
120    /// not declared in its [`OperationMetadata`](crate::extend::OperationMetadata).
121    #[error("Operation '{operation}' has no declared input '{input}'")]
122    UndeclaredInput {
123        /// The operation name from its metadata.
124        operation: &'static str,
125        /// The undeclared input name that was requested.
126        input: String,
127    },
128
129    /// A custom operation called `Context::set_static_output` with a
130    /// name not present as a [`NameSpec::Static`](crate::extend::NameSpec)
131    /// output in its metadata.
132    #[error("Operation '{operation}' has no declared static output '{output}'")]
133    UndeclaredOutput {
134        /// The operation name from its metadata.
135        operation: &'static str,
136        /// The undeclared static output name.
137        output: String,
138    },
139
140    /// A custom operation called `Context::set_derived_output` with an
141    /// input name not referenced by any derived output in its metadata.
142    #[error("Operation '{operation}' has no declared derived output from input '{input}'")]
143    UndeclaredDerivedOutput {
144        /// The operation name from its metadata.
145        operation: &'static str,
146        /// The input name the derived output should have come from.
147        input: String,
148    },
149
150    /// An iteration node resolved its [`IterSource`](crate::prelude::IterSource)
151    /// reference to nothing at runtime.
152    #[error("Iteration '{iter_name}': source '{source_ref}' not found")]
153    IterSourceNotFound {
154        /// The iteration node's name.
155        iter_name: String,
156        /// The store path that failed to resolve.
157        source_ref: String,
158    },
159
160    /// An iteration source resolved to an entry of the wrong shape —
161    /// an array was expected but a map (or scalar) was found, or vice
162    /// versa.
163    #[error("Iteration '{iter_name}': source '{source_ref}' is not {expected}")]
164    IterSourceTypeMismatch {
165        /// The iteration node's name.
166        iter_name: String,
167        /// The store path whose entry had the wrong shape.
168        source_ref: String,
169        /// The shape the iteration expected (`"array"` or `"map"`).
170        expected: &'static str,
171    },
172
173    /// A step inside an iteration body failed. Wraps the underlying
174    /// error so the outer cause includes the iteration name, current
175    /// index/key, and nesting depth.
176    #[error("In iteration '{iter_name}' at index {index}: {source}")]
177    Iteration {
178        /// The enclosing iteration's name.
179        iter_name: String,
180        /// The current index or key as rendered by
181        /// [`IterIndex::fmt`](crate::extend::IterIndex).
182        index: String,
183        /// Nesting depth of the iteration (outermost is `0`).
184        depth: usize,
185        /// The underlying error from the inner step.
186        #[source]
187        source: Box<OperationError>,
188    },
189
190    /// A traversal of a [`StoreEntry`](crate::prelude::StoreEntry) failed.
191    #[error("Access error: {0}")]
192    AccessError(#[from] AccessError),
193
194    /// A [`Store`](crate::prelude::Store) operation failed at runtime.
195    #[error("Store error: {0}")]
196    StoreError(#[from] StoreError),
197
198    /// The runner tried to look up the parameters for a step or return
199    /// block that was never recorded at draft time — this indicates a
200    /// bug in the caller and is effectively unreachable through normal
201    /// construction APIs.
202    #[error("No parameters found for '{name}'")]
203    MissingParameters {
204        /// The missing step or return-block name.
205        name: String,
206    },
207
208    /// A hook interceptor returned [`HookAction::Abort`](crate::extend::HookAction::Abort).
209    #[error("Hook '{hook}' aborted execution: {reason}")]
210    HookAbort {
211        /// The hook that aborted the pipeline.
212        hook: String,
213        /// The reason returned by the interceptor.
214        reason: String,
215    },
216
217    /// `Pipeline::cancel` was observed between steps and the worker
218    /// exited early.
219    #[error("Operation cancelled")]
220    Cancelled,
221
222    /// The worker thread panicked. `Pipeline::wait` turns the panic
223    /// into this variant rather than unwinding into the caller.
224    #[error("Execution thread panicked")]
225    ThreadPanic,
226
227    /// `Context::extension` could not find the requested extension —
228    /// either the operation did not declare it, or no extension was
229    /// registered under that name and type.
230    #[error("Extension '{extension}' not found for operation '{operation}'")]
231    ExtensionNotFound {
232        /// The operation requesting the extension.
233        operation: String,
234        /// The extension name that could not be resolved.
235        extension: String,
236    },
237
238    /// A [`GuardSource`](crate::prelude::GuardSource) resolved to an
239    /// entry that was not a [`Value::Boolean`](crate::prelude::Value::Boolean).
240    #[error("Guard '{guard_name}': reference '{reference}' is not a boolean")]
241    GuardTypeMismatch {
242        /// The guard node's name.
243        guard_name: String,
244        /// The store path whose entry was not a boolean.
245        reference: String,
246    },
247
248    /// A step inside a guard body failed. Wraps the underlying error so
249    /// the outer cause includes the guard name.
250    #[error("In guard '{guard_name}': {source}")]
251    Guard {
252        /// The enclosing guard's name.
253        guard_name: String,
254        /// The underlying error from the inner step.
255        #[source]
256        source: Box<OperationError>,
257    },
258
259    /// A custom operation emitted an ad-hoc error via
260    /// [`Context::error`](crate::extend::Context) or the
261    /// [`op_error!`](crate::op_error) macro.
262    #[error("[{operation}] {message}")]
263    Custom {
264        /// The operation name (from metadata) that emitted the error.
265        operation: String,
266        /// The operation-supplied message.
267        message: String,
268    },
269}
270
271/// Errors produced by [`Store`](crate::prelude::Store) key management.
272#[derive(Debug, thiserror::Error, PartialEq)]
273pub enum StoreError {
274    /// A call to [`Store::insert`](crate::prelude::Store::insert) or a
275    /// `merge` collided with an existing entry under the same name.
276    #[error("An entry with the name '{0}' already exists in the store.")]
277    EntryAlreadyExists(String),
278    /// A call to [`Store::get`](crate::prelude::Store::get) did not find
279    /// the requested name.
280    #[error("No entry found in the store with the name '{0}'.")]
281    EntryNotFound(String),
282}
283
284/// Errors produced while narrowing or traversing a
285/// [`StoreEntry`](crate::prelude::StoreEntry) or
286/// [`Value`](crate::prelude::Value).
287#[derive(Debug, thiserror::Error, PartialEq)]
288pub enum AccessError {
289    /// A nested lookup could not find the given key.
290    #[error("Entry '{0}' not found")]
291    NotFound(String),
292
293    /// A map lookup failed — reserved for contexts where a bare key is
294    /// the most useful piece of information.
295    #[error("Key '{0}' not found")]
296    KeyNotFound(String),
297
298    /// An array index was out of range.
299    #[error("Index {0} out of bounds")]
300    IndexOutOfBounds(usize),
301
302    /// A `var` accessor was called on an array or map entry. The inner
303    /// `&'static str` is the name of the actual variant for diagnostics.
304    #[error("Expected var, found {0}")]
305    NotAVar(&'static str),
306
307    /// An `array` accessor was called on a var or map entry.
308    #[error("Expected array, found {0}")]
309    NotAnArray(&'static str),
310
311    /// A `map` accessor was called on a var or array entry.
312    #[error("Expected map, found {0}")]
313    NotAMap(&'static str),
314
315    /// A typed [`Value`](crate::prelude::Value) accessor (`as_text`,
316    /// `as_integer`, etc.) was called on the wrong variant.
317    #[error("Type mismatch: expected {expected}, found {found}")]
318    TypeMismatch {
319        /// The type name the caller asked for.
320        expected: &'static str,
321        /// The type name the value actually had.
322        found: &'static str,
323    },
324}
325
326/// Builds an [`OperationError::Custom`] from format arguments, tagged
327/// with the operation name taken from a [`Context`](crate::extend::Context).
328///
329/// Thin wrapper over `Context::error` that lets custom operations use
330/// `format!`-style syntax without the `format!` call. The first
331/// argument is the context (`$ctx`); the rest are passed verbatim to
332/// `format!`.
333///
334/// ```no_run
335/// use panopticon_core::extend::*;
336///
337/// fn execute(ctx: &mut Context) -> Result<(), OperationError> {
338///     let key: i64 = 42;
339///     Err(op_error!(ctx, "key {} out of range", key))
340/// }
341/// ```
342#[macro_export]
343macro_rules! op_error {
344    ($ctx:expr, $($arg:tt)*) => {
345        $ctx.error(format!($($arg)*))
346    };
347}