Skip to main content

ergo_adapter/
errors.rs

1//! errors.rs — Adapter error types
2//!
3//! Purpose:
4//! - Defines error types for adapter manifest validation, event
5//!   binding, fixture construction, and composition failures.
6//!
7//! Owns:
8//! - `InvalidAdapter`, `InvalidAdapterComposition`, `InvalidBinding`
9
10use std::borrow::Cow;
11use std::fmt;
12
13use ergo_runtime::common::{doc_anchor_for_rule, ErrorInfo, Phase};
14#[derive(Debug)]
15#[non_exhaustive]
16pub enum InvalidAdapter {
17    InvalidId {
18        id: String,
19    },
20    InvalidVersion {
21        version: String,
22    },
23    InvalidRuntimeCompatibility {
24        version: String,
25    },
26    IncompatibleRuntime {
27        required: String,
28        actual: String,
29    },
30    ProvidesNothing,
31    DuplicateContextKey {
32        name: String,
33        first_index: usize,
34        second_index: usize,
35    },
36    InvalidContextKeyType {
37        name: String,
38        got: String,
39        index: usize,
40    },
41    DuplicateEventKind {
42        name: String,
43        index: usize,
44    },
45    InvalidPayloadSchema {
46        event: String,
47        error: String,
48        index: usize,
49    },
50    NoCaptureFormat,
51    InvalidCaptureField {
52        field: String,
53        index: usize,
54    },
55    MissingWritableFlag {
56        key: String,
57        index: usize,
58    },
59    DuplicateEffectName {
60        name: String,
61        index: usize,
62    },
63    InvalidEffectSchema {
64        effect: String,
65        error: String,
66        index: usize,
67    },
68    WritableWithoutSetContext {
69        keys: Vec<String>,
70    },
71    WritableKeyNotCaptured {
72        key: String,
73        index: usize,
74    },
75    SetContextNotCaptured,
76    WritableKeyRequired {
77        key: String,
78        index: usize,
79    },
80    RequiredEventFieldNotProvided {
81        event: String,
82        field: String,
83        event_index: usize,
84    },
85    RequiredEventFieldTypeMismatch {
86        event: String,
87        field: String,
88        expected: String,
89        got: String,
90        event_index: usize,
91    },
92    EventPayloadSchemaNotObject {
93        event: String,
94        event_index: usize,
95    },
96    UnsupportedEventFieldType {
97        event: String,
98        field: String,
99        detail: String,
100        event_index: usize,
101    },
102}
103
104impl ErrorInfo for InvalidAdapter {
105    fn rule_id(&self) -> &'static str {
106        match self {
107            Self::InvalidId { .. } => "ADP-1",
108            Self::InvalidVersion { .. } => "ADP-2",
109            Self::InvalidRuntimeCompatibility { .. } => "ADP-3",
110            Self::IncompatibleRuntime { .. } => "ADP-3",
111            Self::ProvidesNothing => "ADP-4",
112            Self::DuplicateContextKey { .. } => "ADP-5",
113            Self::InvalidContextKeyType { .. } => "ADP-6",
114            Self::DuplicateEventKind { .. } => "ADP-7",
115            Self::InvalidPayloadSchema { .. } => "ADP-8",
116            Self::NoCaptureFormat => "ADP-9",
117            Self::InvalidCaptureField { .. } => "ADP-10",
118            Self::MissingWritableFlag { .. } => "ADP-11",
119            Self::DuplicateEffectName { .. } => "ADP-12",
120            Self::InvalidEffectSchema { .. } => "ADP-13",
121            Self::WritableWithoutSetContext { .. } => "ADP-14",
122            Self::WritableKeyNotCaptured { .. } => "ADP-15",
123            Self::SetContextNotCaptured => "ADP-16",
124            Self::WritableKeyRequired { .. } => "ADP-17",
125            Self::RequiredEventFieldNotProvided { .. } => "ADP-18",
126            Self::RequiredEventFieldTypeMismatch { .. } => "ADP-18",
127            Self::EventPayloadSchemaNotObject { .. } => "ADP-19",
128            Self::UnsupportedEventFieldType { .. } => "ADP-19",
129        }
130    }
131
132    fn phase(&self) -> Phase {
133        Phase::Registration
134    }
135
136    fn doc_anchor(&self) -> &'static str {
137        doc_anchor_for_rule(self.rule_id())
138    }
139
140    fn summary(&self) -> Cow<'static, str> {
141        match self {
142            Self::InvalidId { id } => Cow::Owned(format!("Invalid adapter ID: '{}'", id)),
143            Self::InvalidVersion { version } => {
144                Cow::Owned(format!("Invalid version: '{}'", version))
145            }
146            Self::InvalidRuntimeCompatibility { version } => Cow::Owned(format!(
147                "Invalid runtime_compatibility: '{}' (must be semver)",
148                version
149            )),
150            Self::IncompatibleRuntime { required, actual } => {
151                Cow::Owned(format!("runtime {} < required {}", actual, required))
152            }
153            Self::ProvidesNothing => Cow::Borrowed("Adapter provides no context keys or events"),
154            Self::DuplicateContextKey { name, .. } => {
155                Cow::Owned(format!("Duplicate context key: '{}'", name))
156            }
157            Self::InvalidContextKeyType { name, got, .. } => {
158                Cow::Owned(format!("Context key '{}' has invalid type '{}'", name, got))
159            }
160            Self::DuplicateEventKind { name, .. } => {
161                Cow::Owned(format!("Duplicate event kind: '{}'", name))
162            }
163            Self::InvalidPayloadSchema { event, error, .. } => Cow::Owned(format!(
164                "Invalid payload schema for event '{}': {}",
165                event, error
166            )),
167            Self::NoCaptureFormat => Cow::Borrowed("Capture format_version is empty or invalid"),
168            Self::InvalidCaptureField { field, .. } => Cow::Owned(format!(
169                "Capture field '{}' is not in CaptureFieldSet",
170                field
171            )),
172            Self::MissingWritableFlag { key, .. } => Cow::Owned(format!(
173                "Context key '{}' is missing the 'writable' field",
174                key
175            )),
176            Self::DuplicateEffectName { name, .. } => {
177                Cow::Owned(format!("Duplicate effect name: '{}'", name))
178            }
179            Self::InvalidEffectSchema { effect, error, .. } => Cow::Owned(format!(
180                "Invalid payload schema for effect '{}': {}",
181                effect, error
182            )),
183            Self::WritableWithoutSetContext { keys } => Cow::Owned(format!(
184                "Writable keys {:?} declared but adapter has no set_context effect",
185                keys
186            )),
187            Self::WritableKeyNotCaptured { key, .. } => Cow::Owned(format!(
188                "Writable key '{}' must be captured for replay determinism",
189                key
190            )),
191            Self::SetContextNotCaptured => {
192                Cow::Borrowed("set_context effect must be captured when writable keys exist")
193            }
194            Self::WritableKeyRequired { key, .. } => {
195                Cow::Owned(format!("Writable key '{}' cannot have required: true", key))
196            }
197            Self::RequiredEventFieldNotProvided { event, field, .. } => Cow::Owned(format!(
198                "Required event field '{}.{}' is not declared in adapter context_keys",
199                event, field
200            )),
201            Self::RequiredEventFieldTypeMismatch {
202                event,
203                field,
204                expected,
205                got,
206                ..
207            } => Cow::Owned(format!(
208                "Required event field '{}.{}' type mismatch: context key is '{}', schema requires '{}'",
209                event, field, got, expected
210            )),
211            Self::EventPayloadSchemaNotObject { event, .. } => Cow::Owned(format!(
212                "Event '{}' payload_schema must be an object schema for context materialization",
213                event
214            )),
215            Self::UnsupportedEventFieldType {
216                event, field, detail, ..
217            } => Cow::Owned(format!(
218                "Event field '{}.{}' uses unsupported materialization type: {}",
219                event, field, detail
220            )),
221        }
222    }
223
224    fn path(&self) -> Option<Cow<'static, str>> {
225        match self {
226            Self::InvalidId { .. } => Some(Cow::Borrowed("$.id")),
227            Self::InvalidVersion { .. } => Some(Cow::Borrowed("$.version")),
228            Self::InvalidRuntimeCompatibility { .. } => {
229                Some(Cow::Borrowed("$.runtime_compatibility"))
230            }
231            Self::IncompatibleRuntime { .. } => Some(Cow::Borrowed("$.runtime_compatibility")),
232            Self::ProvidesNothing => None,
233            Self::DuplicateContextKey { second_index, .. } => {
234                Some(Cow::Owned(format!("$.context_keys[{}].name", second_index)))
235            }
236            Self::InvalidContextKeyType { index, .. } => {
237                Some(Cow::Owned(format!("$.context_keys[{}].type", index)))
238            }
239            Self::DuplicateEventKind { index, .. } => {
240                Some(Cow::Owned(format!("$.event_kinds[{}].name", index)))
241            }
242            Self::InvalidPayloadSchema { index, .. } => Some(Cow::Owned(format!(
243                "$.event_kinds[{}].payload_schema",
244                index
245            ))),
246            Self::NoCaptureFormat => Some(Cow::Borrowed("$.capture.format_version")),
247            Self::InvalidCaptureField { index, .. } => {
248                Some(Cow::Owned(format!("$.capture.fields[{}]", index)))
249            }
250            Self::MissingWritableFlag { index, .. } => {
251                Some(Cow::Owned(format!("$.context_keys[{}].writable", index)))
252            }
253            Self::DuplicateEffectName { index, .. } => {
254                Some(Cow::Owned(format!("$.accepts.effects[{}].name", index)))
255            }
256            Self::InvalidEffectSchema { index, .. } => Some(Cow::Owned(format!(
257                "$.accepts.effects[{}].payload_schema",
258                index
259            ))),
260            Self::WritableWithoutSetContext { .. } => Some(Cow::Borrowed("$.accepts.effects")),
261            Self::WritableKeyNotCaptured { index, .. } => {
262                Some(Cow::Owned(format!("$.context_keys[{}]", index)))
263            }
264            Self::SetContextNotCaptured => Some(Cow::Borrowed("$.capture.fields")),
265            Self::WritableKeyRequired { index, .. } => {
266                Some(Cow::Owned(format!("$.context_keys[{}]", index)))
267            }
268            Self::RequiredEventFieldNotProvided {
269                event_index, field, ..
270            } => Some(Cow::Owned(format!(
271                "$.event_kinds[{}].payload_schema.properties.{}",
272                event_index, field
273            ))),
274            Self::RequiredEventFieldTypeMismatch {
275                event_index, field, ..
276            } => Some(Cow::Owned(format!(
277                "$.event_kinds[{}].payload_schema.properties.{}",
278                event_index, field
279            ))),
280            Self::EventPayloadSchemaNotObject { event_index, .. } => Some(Cow::Owned(format!(
281                "$.event_kinds[{}].payload_schema",
282                event_index
283            ))),
284            Self::UnsupportedEventFieldType {
285                event_index, field, ..
286            } => Some(Cow::Owned(format!(
287                "$.event_kinds[{}].payload_schema.properties.{}",
288                event_index, field
289            ))),
290        }
291    }
292
293    fn fix(&self) -> Option<Cow<'static, str>> {
294        match self {
295            Self::InvalidId { .. } => Some(Cow::Borrowed(
296                "ID must start with lowercase letter, contain only lowercase letters, digits, and underscores (no hyphens)",
297            )),
298            Self::InvalidVersion { .. } => Some(Cow::Borrowed(
299                "Version must be valid semver (e.g., '1.0.0')",
300            )),
301            Self::InvalidRuntimeCompatibility { .. } => Some(Cow::Borrowed(
302                "runtime_compatibility must be valid semver (e.g., '0.1.0')",
303            )),
304            Self::IncompatibleRuntime { required, .. } => {
305                Some(Cow::Owned(format!(
306                    "upgrade runtime to {} or higher",
307                    required
308                )))
309            }
310            Self::ProvidesNothing => Some(Cow::Borrowed(
311                "Add at least one context_key or event_kind",
312            )),
313            Self::DuplicateContextKey { name, .. } => Some(Cow::Owned(format!(
314                "Rename '{}' to a unique value",
315                name
316            ))),
317            Self::InvalidContextKeyType { got, .. } => Some(Cow::Owned(format!(
318                "Type '{}' is not valid; use Number, Bool, String, or Series",
319                got
320            ))),
321            Self::DuplicateEventKind { name, .. } => Some(Cow::Owned(format!(
322                "Rename event kind '{}' to a unique value",
323                name
324            ))),
325            Self::InvalidPayloadSchema { .. } => Some(Cow::Borrowed(
326                "Provide a valid JSON Schema (Draft 2020-12)",
327            )),
328            Self::NoCaptureFormat => Some(Cow::Borrowed(
329                "Set capture.format_version to a non-empty string",
330            )),
331            Self::InvalidCaptureField { field, .. } => Some(Cow::Owned(format!(
332                "'{}' is not in CaptureFieldSet; valid selectors: event.<kind>, meta.adapter_id, meta.adapter_version, meta.timestamp",
333                field
334            ))),
335            Self::MissingWritableFlag { key, .. } => Some(Cow::Owned(format!(
336                "Add 'writable: true' or 'writable: false' to context key '{}'",
337                key
338            ))),
339            Self::DuplicateEffectName { name, .. } => Some(Cow::Owned(format!(
340                "Rename effect '{}' to a unique value",
341                name
342            ))),
343            Self::InvalidEffectSchema { .. } => Some(Cow::Borrowed(
344                "Provide a valid JSON Schema (Draft 2020-12)",
345            )),
346            Self::WritableWithoutSetContext { .. } => Some(Cow::Borrowed(
347                "Add 'set_context' to accepts.effects when using writable keys",
348            )),
349            // ADP-15: Deferred until REP-SCOPE expansion
350            Self::WritableKeyNotCaptured { .. } => None,
351            // ADP-16: Deferred until REP-SCOPE expansion
352            Self::SetContextNotCaptured => None,
353            Self::WritableKeyRequired { key, .. } => Some(Cow::Owned(format!(
354                "Set 'required: false' on writable key '{}'",
355                key
356            ))),
357            Self::RequiredEventFieldNotProvided { field, .. } => Some(Cow::Owned(format!(
358                "Add context key '{}' to context_keys with matching type",
359                field
360            ))),
361            Self::RequiredEventFieldTypeMismatch {
362                field, expected, ..
363            } => Some(Cow::Owned(format!(
364                "Change context key '{}' type to '{}'",
365                field, expected
366            ))),
367            Self::EventPayloadSchemaNotObject { .. } => Some(Cow::Borrowed(
368                "Set event payload_schema.type to 'object' and declare properties",
369            )),
370            Self::UnsupportedEventFieldType { .. } => Some(Cow::Borrowed(
371                "Use field types that map to runtime values: number/integer, boolean, string, or array of numbers",
372            )),
373        }
374    }
375}
376
377impl fmt::Display for InvalidAdapter {
378    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
379        write!(f, "[{}] {}", self.rule_id(), self.summary())
380    }
381}
382
383impl std::error::Error for InvalidAdapter {}