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}
46
47impl ErrorCode {
48    /// Stable process exit code for this failure category.
49    pub fn exit_code(self) -> i32 {
50        match self {
51            Self::ParseError => 2,
52            Self::IoError => 3,
53            Self::ValidationError => 4,
54            Self::StateError => 5,
55            Self::WindowCreateError
56            | Self::HostError
57            | Self::HostViewerError
58            | Self::UiNotFound
59            | Self::UnsupportedType => 6,
60            Self::EventLoopError | Self::HostBindError => 7,
61            Self::InternalError => 8,
62            Self::WorkflowError => 9,
63        }
64    }
65
66    /// Wire slug historically emitted in the `error` field (REQ-0051–0073, REQ-0078).
67    pub fn error_slug(self) -> &'static str {
68        match self {
69            Self::ParseError => "parse",
70            Self::IoError => "io",
71            Self::ValidationError => "validation",
72            Self::StateError => "state",
73            Self::WindowCreateError => "window_create",
74            Self::EventLoopError => "event_loop",
75            Self::InternalError => "internal",
76            Self::HostError | Self::UiNotFound | Self::UnsupportedType => "host_error",
77            Self::HostBindError => "host_bind",
78            Self::HostViewerError => "host_viewer",
79            Self::WorkflowError => "workflow",
80        }
81    }
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn error_code_serde_is_screaming_snake() {
90        let cases = [
91            (ErrorCode::ParseError, "PARSE_ERROR"),
92            (ErrorCode::IoError, "IO_ERROR"),
93            (ErrorCode::ValidationError, "VALIDATION_ERROR"),
94            (ErrorCode::StateError, "STATE_ERROR"),
95            (ErrorCode::WindowCreateError, "WINDOW_CREATE_ERROR"),
96            (ErrorCode::EventLoopError, "EVENT_LOOP_ERROR"),
97            (ErrorCode::InternalError, "INTERNAL_ERROR"),
98            (ErrorCode::HostError, "HOST_ERROR"),
99            (ErrorCode::HostBindError, "HOST_BIND_ERROR"),
100            (ErrorCode::HostViewerError, "HOST_VIEWER_ERROR"),
101            (ErrorCode::UiNotFound, "UI_NOT_FOUND"),
102            (ErrorCode::UnsupportedType, "UNSUPPORTED_TYPE"),
103            (ErrorCode::WorkflowError, "WORKFLOW_ERROR"),
104        ];
105        for (code, expected) in cases {
106            let json = serde_json::to_string(&code).expect("serialize");
107            assert_eq!(json, format!("\"{expected}\""));
108            let round: ErrorCode = serde_json::from_str(&json).expect("deserialize");
109            assert_eq!(round, code);
110        }
111    }
112}