1use std::error::Error;
4use std::fmt::{self, Display, Formatter};
5use std::sync::Arc;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
9#[non_exhaustive]
10pub enum ErrorCode {
11 InactiveEffect,
13 MissingService,
15 TypeMismatch,
17 DuplicateService,
19 AccessDenied,
21 InvalidConfig,
23 Plugin,
25 Event,
27 PropertyConflict,
29 Other,
31}
32
33impl ErrorCode {
34 pub const fn message(self) -> &'static str {
36 match self {
37 Self::InactiveEffect => "cannot create effect on inactive context",
38 Self::MissingService => "required service is unavailable",
39 Self::TypeMismatch => "stored value has an unexpected type",
40 Self::DuplicateService => "service has already been registered",
41 Self::AccessDenied => "service belongs to another fiber",
42 Self::InvalidConfig => "invalid config",
43 Self::Plugin => "plugin failed",
44 Self::Event => "event listener failed",
45 Self::PropertyConflict => "property is already declared",
46 Self::Other => "cordis error",
47 }
48 }
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct ValidationIssue {
54 pub message: String,
56 pub path: Vec<String>,
58}
59
60impl ValidationIssue {
61 pub fn new(message: impl Into<String>) -> Self {
63 Self {
64 message: message.into(),
65 path: Vec::new(),
66 }
67 }
68
69 pub fn at(mut self, path: impl IntoIterator<Item = impl Into<String>>) -> Self {
71 self.path = path.into_iter().map(Into::into).collect();
72 self
73 }
74}
75
76#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct ValidationError {
79 pub issues: Vec<ValidationIssue>,
81}
82
83impl ValidationError {
84 pub fn new(issues: impl IntoIterator<Item = ValidationIssue>) -> Self {
86 Self {
87 issues: issues.into_iter().collect(),
88 }
89 }
90}
91
92impl Display for ValidationError {
93 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
94 writeln!(f, "invalid config:")?;
95 for (index, issue) in self.issues.iter().enumerate() {
96 write!(f, " - {}", issue.message)?;
97 if !issue.path.is_empty() {
98 write!(f, " (at {})", issue.path.join("."))?;
99 }
100 if index + 1 < self.issues.len() {
101 writeln!(f)?;
102 }
103 }
104 Ok(())
105 }
106}
107
108impl Error for ValidationError {}
109
110#[derive(Debug, Clone)]
112pub struct CordisError {
113 code: ErrorCode,
114 message: String,
115 validation: Option<ValidationError>,
116 source: Option<Arc<dyn Error + Send + Sync + 'static>>,
117}
118
119impl CordisError {
120 pub fn new(code: ErrorCode) -> Self {
122 Self {
123 code,
124 message: code.message().to_owned(),
125 validation: None,
126 source: None,
127 }
128 }
129
130 pub fn with_message(code: ErrorCode, message: impl Into<String>) -> Self {
132 Self {
133 code,
134 message: message.into(),
135 validation: None,
136 source: None,
137 }
138 }
139
140 pub fn with_source(
142 code: ErrorCode,
143 message: impl Into<String>,
144 source: impl Error + Send + Sync + 'static,
145 ) -> Self {
146 Self {
147 code,
148 message: message.into(),
149 validation: None,
150 source: Some(Arc::new(source)),
151 }
152 }
153
154 pub fn validation(issues: impl IntoIterator<Item = ValidationIssue>) -> Self {
156 let validation = ValidationError::new(issues);
157 Self {
158 code: ErrorCode::InvalidConfig,
159 message: validation.to_string(),
160 validation: Some(validation),
161 source: None,
162 }
163 }
164
165 pub const fn code(&self) -> ErrorCode {
167 self.code
168 }
169
170 pub fn validation_error(&self) -> Option<&ValidationError> {
172 self.validation.as_ref()
173 }
174
175 pub fn context(mut self, context: impl AsRef<str>) -> Self {
177 self.message = format!("{}: {}", context.as_ref(), self.message);
178 self
179 }
180
181 pub fn caused_by(mut self, source: impl Error + Send + Sync + 'static) -> Self {
183 self.source = Some(Arc::new(source));
184 self
185 }
186}
187
188impl Display for CordisError {
189 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
190 f.write_str(&self.message)
191 }
192}
193
194impl Error for CordisError {
195 fn source(&self) -> Option<&(dyn Error + 'static)> {
196 self.source
197 .as_deref()
198 .map(|source| source as &(dyn Error + 'static))
199 }
200}
201
202impl From<ValidationError> for CordisError {
203 fn from(value: ValidationError) -> Self {
204 Self {
205 code: ErrorCode::InvalidConfig,
206 message: value.to_string(),
207 validation: Some(value),
208 source: None,
209 }
210 }
211}
212
213impl From<String> for CordisError {
214 fn from(value: String) -> Self {
215 Self::with_message(ErrorCode::Other, value)
216 }
217}
218
219impl From<&str> for CordisError {
220 fn from(value: &str) -> Self {
221 Self::with_message(ErrorCode::Other, value)
222 }
223}
224
225pub type Result<T, E = CordisError> = std::result::Result<T, E>;
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn source_chain_survives_construction_and_clone() {
234 let cause = std::io::Error::other("disk gone");
235 let error = CordisError::with_source(ErrorCode::Plugin, "plugin failed", cause);
236 let source = Error::source(&error).expect("source recorded");
237 assert_eq!(source.to_string(), "disk gone");
238 assert_eq!(error.code(), ErrorCode::Plugin);
239 assert_eq!(error.to_string(), "plugin failed");
240
241 let cloned = error.clone();
242 assert_eq!(
243 Error::source(&cloned).map(ToString::to_string).as_deref(),
244 Some("disk gone")
245 );
246
247 let attached = CordisError::new(ErrorCode::Event).caused_by(std::io::Error::other("inner"));
248 assert_eq!(
249 Error::source(&attached).map(ToString::to_string).as_deref(),
250 Some("inner")
251 );
252 }
253}