Skip to main content

sc_lint/
error.rs

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        // source erased to String — CliError is a display type, not a re-throw carrier
164        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}