Skip to main content

ergo_runtime/common/
errors.rs

1//! common::errors
2//!
3//! Purpose:
4//! - Define the kernel-owned validation error taxonomy shared by compute-style
5//!   primitive registration and catalog assembly.
6//!
7//! Owns:
8//! - `ValidationError` as the typed authority for common compute registration
9//!   failures.
10//! - The `Display`/`Error` surface higher layers rely on for chaining instead of
11//!   flattening these errors into strings.
12//!
13//! Does not own:
14//! - Primitive-family wrapper errors in catalog assembly.
15//! - Host or product-facing error descriptors.
16//!
17//! Connects to:
18//! - `catalog.rs`, which wraps these failures in `CoreRegistrationError`.
19//! - Compute validation callers that need one authoritative error surface.
20//!
21//! Safety notes:
22//! - `Display` intentionally stays aligned with `ErrorInfo` summary/rule-id so
23//!   registration diagnostics share one semantic authority.
24
25use std::borrow::Cow;
26use std::fmt;
27
28use crate::common::value::{PrimitiveKind, ValueType};
29use crate::common::{doc_anchor_for_rule, ErrorInfo, Phase};
30
31#[derive(Debug, Clone, PartialEq)]
32#[non_exhaustive]
33pub enum ValidationError {
34    InvalidId {
35        id: String,
36    },
37    InvalidVersion {
38        version: String,
39    },
40    WrongKind {
41        expected: PrimitiveKind,
42        got: PrimitiveKind,
43    },
44    /// X.7: Compute primitives must declare at least one input.
45    NoInputsDeclared {
46        primitive: String,
47    },
48    NoOutputsDeclared {
49        primitive: String,
50    },
51    SideEffectsNotAllowed,
52    NonDeterministicExecution,
53    NonDeterministicErrors {
54        primitive: String,
55    },
56    InvalidCadence {
57        primitive: String,
58    },
59    InvalidInputCardinality {
60        primitive: String,
61        input: String,
62        got: String,
63    },
64    DuplicateId(String),
65    DuplicateInput {
66        name: String,
67        first_index: usize,
68        second_index: usize,
69    },
70    DuplicateOutput {
71        name: String,
72        first_index: usize,
73        second_index: usize,
74    },
75    InvalidInputType {
76        input: String,
77        expected: ValueType,
78        got: ValueType,
79    },
80    InvalidOutputType {
81        output: String,
82        expected: ValueType,
83        got: ValueType,
84    },
85    MissingDeclaredOutput {
86        primitive: String,
87        output: String,
88    },
89    InvalidParameterType {
90        parameter: String,
91        expected: ValueType,
92        got: ValueType,
93    },
94    StateNotResettable {
95        primitive: String,
96    },
97    MissingOutput {
98        node: String,
99        output: String,
100    },
101    /// X.10: Compute parameters must not be Series type.
102    UnsupportedParameterType {
103        primitive: String,
104        version: String,
105        parameter: String,
106        got: ValueType,
107    },
108}
109
110impl ErrorInfo for ValidationError {
111    fn rule_id(&self) -> &'static str {
112        match self {
113            Self::InvalidId { .. } => "CMP-1",
114            Self::InvalidVersion { .. } => "CMP-2",
115            Self::WrongKind { .. } => "CMP-3",
116            Self::NoInputsDeclared { .. } => "CMP-4",
117            Self::DuplicateInput { .. } => "CMP-5",
118            Self::NoOutputsDeclared { .. } => "CMP-6",
119            Self::DuplicateOutput { .. } => "CMP-7",
120            Self::SideEffectsNotAllowed => "CMP-8",
121            Self::StateNotResettable { .. } => "CMP-9",
122            Self::NonDeterministicErrors { .. } => "CMP-10",
123            Self::InvalidInputType { .. } => "CMP-13",
124            Self::InvalidInputCardinality { .. } => "CMP-14",
125            Self::UnsupportedParameterType { .. } => "CMP-15",
126            Self::InvalidCadence { .. } => "CMP-16",
127            Self::NonDeterministicExecution => "CMP-17",
128            Self::DuplicateId(_) => "CMP-18",
129            Self::MissingDeclaredOutput { .. } => "CMP-11",
130            Self::MissingOutput { .. } => "CMP-11",
131            Self::InvalidOutputType { .. } => "CMP-20",
132            Self::InvalidParameterType { .. } => "CMP-19",
133        }
134    }
135
136    fn phase(&self) -> Phase {
137        Phase::Registration
138    }
139
140    fn doc_anchor(&self) -> &'static str {
141        doc_anchor_for_rule(self.rule_id())
142    }
143
144    fn summary(&self) -> Cow<'static, str> {
145        match self {
146            Self::InvalidId { id } => Cow::Owned(format!("Invalid compute ID: '{}'", id)),
147            Self::InvalidVersion { version } => {
148                Cow::Owned(format!("Invalid version: '{}'", version))
149            }
150            Self::WrongKind { expected, got } => Cow::Owned(format!(
151                "Wrong kind: expected {:?}, got {:?}",
152                expected, got
153            )),
154            Self::NoInputsDeclared { .. } => Cow::Borrowed("Compute has no inputs"),
155            Self::NoOutputsDeclared { .. } => Cow::Borrowed("Compute has no outputs"),
156            Self::SideEffectsNotAllowed => Cow::Borrowed("Compute has side effects"),
157            Self::NonDeterministicExecution => {
158                Cow::Borrowed("Compute execution must be deterministic")
159            }
160            Self::NonDeterministicErrors { .. } => {
161                Cow::Borrowed("Compute errors must be deterministic when allowed")
162            }
163            Self::InvalidCadence { .. } => Cow::Borrowed("Compute cadence must be continuous"),
164            Self::InvalidInputCardinality { input, got, .. } => Cow::Owned(format!(
165                "Input '{}' has invalid cardinality '{}'",
166                input, got
167            )),
168            Self::DuplicateId(_) => Cow::Borrowed("Duplicate compute ID: already registered"),
169            Self::DuplicateInput { name, .. } => {
170                Cow::Owned(format!("Duplicate input name: '{}'", name))
171            }
172            Self::DuplicateOutput { name, .. } => {
173                Cow::Owned(format!("Duplicate output name: '{}'", name))
174            }
175            Self::InvalidInputType {
176                input,
177                expected,
178                got,
179            } => Cow::Owned(format!(
180                "Input '{}' has invalid type: expected {:?}, got {:?}",
181                input, expected, got
182            )),
183            Self::InvalidOutputType {
184                output,
185                expected,
186                got,
187            } => Cow::Owned(format!(
188                "Output '{}' has invalid type: expected {:?}, got {:?}",
189                output, expected, got
190            )),
191            Self::MissingDeclaredOutput { primitive, output } => Cow::Owned(format!(
192                "Missing declared output '{}' for primitive '{}'",
193                output, primitive
194            )),
195            Self::InvalidParameterType {
196                parameter,
197                expected,
198                got,
199            } => Cow::Owned(format!(
200                "Parameter '{}' has invalid type: expected {:?}, got {:?}",
201                parameter, expected, got
202            )),
203            Self::StateNotResettable { .. } => {
204                Cow::Borrowed("State must be resettable when allowed")
205            }
206            Self::MissingOutput { node, output } => {
207                Cow::Owned(format!("Missing output '{}' on node '{}'", output, node))
208            }
209            Self::UnsupportedParameterType { parameter, got, .. } => Cow::Owned(format!(
210                "Parameter '{}' has unsupported type {:?}",
211                parameter, got
212            )),
213        }
214    }
215
216    fn path(&self) -> Option<Cow<'static, str>> {
217        match self {
218            Self::InvalidId { .. } => Some(Cow::Borrowed("$.id")),
219            Self::InvalidVersion { .. } => Some(Cow::Borrowed("$.version")),
220            Self::WrongKind { .. } => Some(Cow::Borrowed("$.kind")),
221            Self::NoInputsDeclared { .. } => Some(Cow::Borrowed("$.inputs")),
222            Self::NoOutputsDeclared { .. } => Some(Cow::Borrowed("$.outputs")),
223            Self::DuplicateId(_) => Some(Cow::Borrowed("$.id")),
224            Self::DuplicateInput { second_index, .. } => {
225                Some(Cow::Owned(format!("$.inputs[{}].name", second_index)))
226            }
227            Self::DuplicateOutput { second_index, .. } => {
228                Some(Cow::Owned(format!("$.outputs[{}].name", second_index)))
229            }
230            Self::InvalidInputType { .. } => Some(Cow::Borrowed("$.inputs[].type")),
231            Self::InvalidOutputType { .. } => Some(Cow::Borrowed("$.outputs[].type")),
232            Self::InvalidInputCardinality { .. } => Some(Cow::Borrowed("$.inputs[].cardinality")),
233            Self::SideEffectsNotAllowed => Some(Cow::Borrowed("$.side_effects")),
234            Self::NonDeterministicExecution => Some(Cow::Borrowed("$.execution.deterministic")),
235            Self::NonDeterministicErrors { .. } => Some(Cow::Borrowed("$.errors.deterministic")),
236            Self::InvalidCadence { .. } => Some(Cow::Borrowed("$.execution.cadence")),
237            Self::UnsupportedParameterType { .. } => Some(Cow::Borrowed("$.parameters[].type")),
238            Self::InvalidParameterType { .. } => Some(Cow::Borrowed("$.parameters[].default")),
239            Self::StateNotResettable { .. } => Some(Cow::Borrowed("$.state.resettable")),
240            _ => None,
241        }
242    }
243
244    fn fix(&self) -> Option<Cow<'static, str>> {
245        match self {
246            Self::InvalidId { .. } => Some(Cow::Borrowed(
247                "ID must start with lowercase letter and contain only lowercase letters, digits, and underscores",
248            )),
249            Self::DuplicateId(_) => Some(Cow::Borrowed("Choose a unique ID not already registered")),
250            Self::InvalidVersion { .. } => Some(Cow::Borrowed(
251                "Version must be valid semver (e.g., '1.0.0')",
252            )),
253            Self::WrongKind { .. } => Some(Cow::Borrowed("Set kind: compute")),
254            Self::NoInputsDeclared { .. } => Some(Cow::Borrowed("Add at least one input")),
255            Self::NoOutputsDeclared { .. } => Some(Cow::Borrowed("Add at least one output")),
256            Self::SideEffectsNotAllowed => Some(Cow::Borrowed("Set side_effects: false")),
257            Self::NonDeterministicExecution => {
258                Some(Cow::Borrowed("Set execution.deterministic: true"))
259            }
260            Self::NonDeterministicErrors { .. } => Some(Cow::Borrowed(
261                "Set errors.deterministic: true or errors.allowed: false",
262            )),
263            Self::InvalidCadence { .. } => Some(Cow::Borrowed("Set cadence: continuous")),
264            Self::InvalidInputCardinality { .. } => {
265                Some(Cow::Borrowed("Set input cardinality to single"))
266            }
267            Self::DuplicateInput { name, .. } => Some(Cow::Owned(format!(
268                "Rename input '{}' to a unique value",
269                name
270            ))),
271            Self::DuplicateOutput { name, .. } => Some(Cow::Owned(format!(
272                "Rename output '{}' to a unique value",
273                name
274            ))),
275            Self::InvalidOutputType { .. } => Some(Cow::Borrowed(
276                "Use a valid output type: number, bool, series, or string",
277            )),
278            Self::UnsupportedParameterType { parameter, .. } => Some(Cow::Owned(format!(
279                "Change parameter '{}' type to int, number, or bool",
280                parameter
281            ))),
282            Self::InvalidParameterType { parameter, .. } => Some(Cow::Owned(format!(
283                "Change parameter '{}' default value to match the declared type",
284                parameter
285            ))),
286            Self::StateNotResettable { .. } => {
287                Some(Cow::Borrowed("Set state.resettable: true"))
288            }
289            _ => None,
290        }
291    }
292}
293
294impl fmt::Display for ValidationError {
295    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
296        write!(f, "{} ({})", self.summary(), self.rule_id())
297    }
298}
299
300impl std::error::Error for ValidationError {}
301
302#[cfg(test)]
303mod tests {
304    use super::ValidationError;
305    use crate::common::value::ValueType;
306    use crate::common::ErrorInfo;
307
308    #[test]
309    fn cmp_15_remains_unsupported_parameter_type() {
310        let err = ValidationError::UnsupportedParameterType {
311            primitive: "p".to_string(),
312            version: "1.0.0".to_string(),
313            parameter: "x".to_string(),
314            got: ValueType::String,
315        };
316
317        assert_eq!(err.rule_id(), "CMP-15");
318        assert_eq!(
319            err.doc_anchor(),
320            "docs/primitives/compute.md#4-enforcement-mapping"
321        );
322        assert_eq!(err.path().as_deref(), Some("$.parameters[].type"));
323        assert_eq!(
324            err.fix().as_deref(),
325            Some("Change parameter 'x' type to int, number, or bool")
326        );
327    }
328
329    #[test]
330    fn cmp_19_reserved_for_invalid_parameter_type() {
331        let err = ValidationError::InvalidParameterType {
332            parameter: "x".to_string(),
333            expected: ValueType::Number,
334            got: ValueType::String,
335        };
336
337        assert_eq!(err.rule_id(), "CMP-19");
338        assert_eq!(
339            err.doc_anchor(),
340            "docs/primitives/compute.md#4-enforcement-mapping"
341        );
342        assert_eq!(err.path().as_deref(), Some("$.parameters[].default"));
343        assert_eq!(
344            err.fix().as_deref(),
345            Some("Change parameter 'x' default value to match the declared type")
346        );
347    }
348
349    #[test]
350    fn cmp_20_reserved_for_invalid_output_type() {
351        let err = ValidationError::InvalidOutputType {
352            output: "out".to_string(),
353            expected: ValueType::Number,
354            got: ValueType::String,
355        };
356
357        assert_eq!(err.rule_id(), "CMP-20");
358        assert_eq!(
359            err.doc_anchor(),
360            "docs/primitives/compute.md#4-enforcement-mapping"
361        );
362        assert_eq!(err.path().as_deref(), Some("$.outputs[].type"));
363        assert_eq!(
364            err.fix().as_deref(),
365            Some("Use a valid output type: number, bool, series, or string")
366        );
367    }
368}