1use 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 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 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}