1use crate::consts;
2use std::error::Error;
3use std::fmt;
4
5use serde::Serialize;
6use serde::ser::SerializeStruct;
7use serde_json::Map;
8use serde_json::Value;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
11#[serde(rename_all = "snake_case")]
12pub enum CliErrorKind {
13 Usage,
14 Config,
15 Capability,
16 BackendFailure,
17 BackendProtocol,
18 Internal,
19}
20
21impl CliErrorKind {
22 pub const fn code(self) -> &'static str {
23 match self {
24 Self::Usage => "CLI.USAGE_ERROR",
25 Self::Config => "CLI.CONFIG_ERROR",
26 Self::Capability => "CLI.CAPABILITY_ERROR",
27 Self::BackendFailure => "CLI.BACKEND_EXEC_FAILURE",
28 Self::BackendProtocol => "CLI.BACKEND_PROTOCOL_ERROR",
29 Self::Internal => "CLI.INTERNAL_ERROR",
30 }
31 }
32
33 pub const fn exit_code(self) -> u8 {
34 match self {
35 Self::Usage => 2,
36 Self::Config => 3,
37 Self::Capability => 4,
38 Self::BackendFailure => 5,
39 Self::BackendProtocol => 6,
40 Self::Internal => 1,
41 }
42 }
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct CliError {
47 pub(crate) kind: CliErrorKind,
48 pub message: String,
49 pub details: Map<String, Value>,
50 pub cause: Option<String>,
51 pub suggested_action: Option<String>,
52 source: Option<CliErrorSource>,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq)]
56struct CliErrorSource(String);
57
58impl fmt::Display for CliErrorSource {
59 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
60 formatter.write_str(&self.0)
61 }
62}
63
64impl Error for CliErrorSource {}
65
66impl Serialize for CliError {
67 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
68 where
69 S: serde::Serializer,
70 {
71 let field_count = 3
72 + usize::from(!self.details.is_empty())
73 + usize::from(self.cause.is_some())
74 + usize::from(self.suggested_action.is_some());
75 let mut state = serializer.serialize_struct("CliError", field_count)?;
76 state.serialize_field(consts::FIELD_KIND, &self.kind)?;
77 state.serialize_field(consts::FIELD_CODE, self.code())?;
78 state.serialize_field(consts::FIELD_MESSAGE, &self.message)?;
79 if !self.details.is_empty() {
80 state.serialize_field(consts::FIELD_DETAILS, &self.details)?;
81 }
82 if let Some(cause) = self.cause.as_ref() {
83 state.serialize_field(consts::FIELD_CAUSE, cause)?;
84 }
85 if let Some(suggested_action) = self.suggested_action.as_ref() {
86 state.serialize_field(consts::FIELD_SUGGESTED_ACTION, suggested_action)?;
87 }
88 state.end()
89 }
90}
91
92impl fmt::Display for CliError {
93 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
94 formatter.write_str(&self.message)
95 }
96}
97
98impl Error for CliError {
99 fn source(&self) -> Option<&(dyn Error + 'static)> {
100 self.source
101 .as_ref()
102 .map(|source| source as &(dyn Error + 'static))
103 }
104}
105
106impl CliError {
107 pub fn usage(message: impl Into<String>) -> Self {
108 Self::new(CliErrorKind::Usage, message)
109 }
110
111 pub fn config(message: impl Into<String>) -> Self {
112 Self::new(CliErrorKind::Config, message)
113 }
114
115 pub fn capability(message: impl Into<String>) -> Self {
116 Self::new(CliErrorKind::Capability, message)
117 }
118
119 pub fn backend_failure(message: impl Into<String>) -> Self {
120 Self::new(CliErrorKind::BackendFailure, message)
121 }
122
123 pub fn backend_protocol(message: impl Into<String>) -> Self {
124 Self::new(CliErrorKind::BackendProtocol, message)
125 }
126
127 pub fn internal(message: impl Into<String>) -> Self {
128 Self::new(CliErrorKind::Internal, message)
129 }
130
131 pub(crate) fn new(kind: CliErrorKind, message: impl Into<String>) -> Self {
132 Self {
133 kind,
134 message: message.into(),
135 details: Map::new(),
136 cause: None,
137 suggested_action: None,
138 source: None,
139 }
140 }
141
142 pub fn with_detail(mut self, key: impl Into<String>, value: Value) -> Self {
143 self.details.insert(key.into(), value);
144 self
145 }
146
147 pub fn with_suggested_action(mut self, suggested_action: impl Into<String>) -> Self {
148 self.suggested_action = Some(suggested_action.into());
149 self
150 }
151
152 pub fn with_cause(mut self, cause: impl Into<String>) -> Self {
153 let cause = cause.into();
154 self.cause = Some(cause.clone());
155 self.source = Some(CliErrorSource(cause));
156 self
157 }
158
159 pub fn with_source<E>(mut self, source: E) -> Self
160 where
161 E: fmt::Display,
162 {
163 let cause = source.to_string();
165 self.cause = Some(cause.clone());
166 self.source = Some(CliErrorSource(cause));
167 self
168 }
169
170 pub fn kind_label(&self) -> &'static str {
171 match self.kind {
172 CliErrorKind::Usage => "usage",
173 CliErrorKind::Config => "config",
174 CliErrorKind::Capability => "capability",
175 CliErrorKind::BackendFailure => "backend_failure",
176 CliErrorKind::BackendProtocol => "backend_protocol",
177 CliErrorKind::Internal => "internal",
178 }
179 }
180
181 pub const fn exit_code(&self) -> u8 {
182 self.kind.exit_code()
183 }
184
185 pub const fn code(&self) -> &'static str {
186 self.kind.code()
187 }
188}