Skip to main content

wyvern_schema/
error_code.rs

1//! Stable machine-readable error codes for stderr JSON.
2//!
3//! # Stability
4//!
5//! Once published, [`ErrorCode`] variants must not be removed or renamed.
6//! New variants may be added in a non-breaking way. Serde emits
7//! `SCREAMING_SNAKE_CASE` strings (e.g. `PARSE_ERROR`).
8
9use serde::{Deserialize, Serialize};
10
11/// Stable error codes for scripting consumers of Wyvern stderr JSON.
12///
13/// These codes are **additive** alongside the historical `error` slug field
14/// (`parse`, `validation`, …). Consumers that only check `error` remain valid;
15/// new consumers should prefer `code` for stable branching.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
17#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
18pub enum ErrorCode {
19    /// JSON text could not be parsed (CLI load stage).
20    ParseError,
21    /// File or stdin read failed.
22    IoError,
23    /// Schema or field-level validation failure.
24    ValidationError,
25    /// Mode/lifecycle state failure (e.g. action outside `--interactive`).
26    StateError,
27    /// Native window or webview construction failed (legacy wry stack).
28    WindowCreateError,
29    /// Event loop creation or run failed (legacy wry stack).
30    EventLoopError,
31    /// Stdout/stderr JSON serialization failed at the CLI emit boundary (REQ-0078).
32    InternalError,
33    /// Generic host failure (`host_error` / exit 6).
34    HostError,
35    /// TCP bind failed (`host_bind` / exit 7).
36    HostBindError,
37    /// Viewer launch / discovery failed (`host_viewer` / exit 6).
38    HostViewerError,
39    /// Packaged UI root or template missing (`host_error` / exit 6).
40    UiNotFound,
41    /// Dialog type not yet on the host matrix (`host_error` / exit 6).
42    UnsupportedType,
43    /// Workflow pre/post or `next_wizard` chain failure (`workflow` / exit 9).
44    WorkflowError,
45    /// Headless idle timeout with no harness result (`host_error` / exit 6).
46    SessionTimeoutError,
47}
48
49impl ErrorCode {
50    /// Stable process exit code for this failure category.
51    pub fn exit_code(self) -> i32 {
52        match self {
53            Self::ParseError => 2,
54            Self::IoError => 3,
55            Self::ValidationError => 4,
56            Self::StateError => 5,
57            Self::WindowCreateError
58            | Self::HostError
59            | Self::HostViewerError
60            | Self::UiNotFound
61            | Self::UnsupportedType
62            | Self::SessionTimeoutError => 6,
63            Self::EventLoopError | Self::HostBindError => 7,
64            Self::InternalError => 8,
65            Self::WorkflowError => 9,
66        }
67    }
68
69    /// Wire slug historically emitted in the `error` field (REQ-0051–0073, REQ-0078).
70    pub fn error_slug(self) -> &'static str {
71        match self {
72            Self::ParseError => "parse",
73            Self::IoError => "io",
74            Self::ValidationError => "validation",
75            Self::StateError => "state",
76            Self::WindowCreateError => "window_create",
77            Self::EventLoopError => "event_loop",
78            Self::InternalError => "internal",
79            Self::HostError
80            | Self::UiNotFound
81            | Self::UnsupportedType
82            | Self::SessionTimeoutError => "host_error",
83            Self::HostBindError => "host_bind",
84            Self::HostViewerError => "host_viewer",
85            Self::WorkflowError => "workflow",
86        }
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93
94    #[test]
95    fn error_code_serde_is_screaming_snake() {
96        let cases = [
97            (ErrorCode::ParseError, "PARSE_ERROR"),
98            (ErrorCode::IoError, "IO_ERROR"),
99            (ErrorCode::ValidationError, "VALIDATION_ERROR"),
100            (ErrorCode::StateError, "STATE_ERROR"),
101            (ErrorCode::WindowCreateError, "WINDOW_CREATE_ERROR"),
102            (ErrorCode::EventLoopError, "EVENT_LOOP_ERROR"),
103            (ErrorCode::InternalError, "INTERNAL_ERROR"),
104            (ErrorCode::HostError, "HOST_ERROR"),
105            (ErrorCode::HostBindError, "HOST_BIND_ERROR"),
106            (ErrorCode::HostViewerError, "HOST_VIEWER_ERROR"),
107            (ErrorCode::UiNotFound, "UI_NOT_FOUND"),
108            (ErrorCode::UnsupportedType, "UNSUPPORTED_TYPE"),
109            (ErrorCode::WorkflowError, "WORKFLOW_ERROR"),
110            (ErrorCode::SessionTimeoutError, "SESSION_TIMEOUT_ERROR"),
111        ];
112        for (code, expected) in cases {
113            let json = serde_json::to_string(&code).expect("serialize");
114            assert_eq!(json, format!("\"{expected}\""));
115            let round: ErrorCode = serde_json::from_str(&json).expect("deserialize");
116            assert_eq!(round, code);
117        }
118    }
119}