1use std::fmt;
4use std::sync::Arc;
5
6use thiserror::Error;
7
8pub type SharedError = Arc<dyn std::error::Error + Send + Sync + 'static>;
10
11#[derive(Debug)]
13struct DisplayError(String);
14
15impl fmt::Display for DisplayError {
16 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
17 f.write_str(&self.0)
18 }
19}
20
21impl std::error::Error for DisplayError {}
22
23pub type Result<T> = std::result::Result<T, PhotonError>;
25
26#[derive(Debug, Clone, Error)]
28pub enum PhotonError {
29 #[error("topic not found: {0}")]
31 TopicNotFound(String),
32
33 #[error("subscription not found: {0}")]
35 SubscriptionNotFound(String),
36
37 #[error("event not found: {0}")]
39 EventNotFound(String),
40
41 #[error("invalid topic name: {0}")]
43 InvalidTopicName(String),
44
45 #[error("payload error: {0}")]
47 PayloadError(String),
48
49 #[error("schema mismatch: {0}")]
51 SchemaMismatch(String),
52
53 #[error("topic already exists: {0}")]
55 TopicAlreadyExists(String),
56
57 #[error("subscription name required for durable subscriptions")]
59 SubscriptionNameRequired,
60
61 #[deprecated(note = "use PhotonError::persistence(...) for sourced failures")]
65 #[error("persistence error: {0}")]
66 PersistenceError(String),
67
68 #[error("persistence error: {context}")]
70 Persistence {
71 context: String,
73 #[source]
75 source: SharedError,
76 },
77
78 #[error("identity error: {0}")]
84 Identity(String),
85
86 #[error("internal error: {0}")]
88 Internal(String),
89
90 #[error("internal error: {context}")]
92 Caused {
93 context: String,
95 #[source]
97 source: SharedError,
98 },
99}
100
101impl PhotonError {
102 pub fn caused(
107 context: impl Into<String>,
108 err: impl fmt::Display + Send + Sync + 'static,
109 ) -> Self {
110 Self::Caused {
111 context: context.into(),
112 source: Arc::new(DisplayError(err.to_string())),
113 }
114 }
115
116 pub fn caused_error(
118 context: impl Into<String>,
119 err: impl std::error::Error + Send + Sync + 'static,
120 ) -> Self {
121 Self::Caused {
122 context: context.into(),
123 source: Arc::new(err),
124 }
125 }
126
127 pub fn persistence(
129 context: impl Into<String>,
130 err: impl std::error::Error + Send + Sync + 'static,
131 ) -> Self {
132 Self::Persistence {
133 context: context.into(),
134 source: Arc::new(err),
135 }
136 }
137}
138
139impl From<serde_json::Error> for PhotonError {
140 fn from(err: serde_json::Error) -> Self {
141 Self::PayloadError(err.to_string())
142 }
143}
144
145impl From<photon_core::IdentityError> for PhotonError {
146 fn from(err: photon_core::IdentityError) -> Self {
147 match err {
148 photon_core::IdentityError::InvalidActor(msg)
149 | photon_core::IdentityError::Factory(msg) => Self::Identity(msg),
150 }
151 }
152}
153
154impl From<anyhow::Error> for PhotonError {
155 fn from(err: anyhow::Error) -> Self {
156 Self::Caused {
158 context: err.to_string(),
159 source: Arc::new(DisplayError(format!("{err:#}"))),
160 }
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use std::error::Error as _;
167
168 use super::*;
169
170 #[derive(Debug, thiserror::Error)]
171 #[error("disk offline")]
172 struct DiskError;
173
174 struct BadTag;
177
178 impl fmt::Display for BadTag {
179 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180 f.write_str("bad auth tag")
181 }
182 }
183
184 #[test]
185 fn caused_wraps_display_only_source() {
186 let err = PhotonError::caused("decrypt failed", BadTag);
187 assert_eq!(err.to_string(), "internal error: decrypt failed");
188 let source = err.source().expect("caused keeps a source");
189 assert_eq!(source.to_string(), "bad auth tag");
190 }
191
192 #[test]
193 fn caused_error_preserves_source_chain() {
194 let err = PhotonError::caused_error("flush failed", DiskError);
195 assert_eq!(err.to_string(), "internal error: flush failed");
196 let source = err.source().expect("caused_error keeps a source");
199 assert_eq!(source.to_string(), "disk offline");
200 }
201
202 #[test]
203 fn persistence_reports_context_and_source() {
204 let err = PhotonError::persistence("sqlite decode", DiskError);
205 assert_eq!(err.to_string(), "persistence error: sqlite decode");
206 let source = err.source().expect("persistence keeps a source");
207 assert_eq!(source.to_string(), "disk offline");
208 }
209
210 #[test]
211 fn anyhow_conversion_keeps_full_chain() {
212 let err = anyhow::Error::new(DiskError).context("flush checkpoint");
213 let err = PhotonError::from(err);
214 assert_eq!(err.to_string(), "internal error: flush checkpoint");
215 let source = err.source().expect("anyhow conversion keeps a source");
216 let chain = source.to_string();
217 assert!(chain.contains("flush checkpoint"), "chain: {chain}");
218 assert!(chain.contains("disk offline"), "chain: {chain}");
219 }
220
221 #[test]
222 fn serde_json_conversion_maps_to_payload_error() {
223 let err = serde_json::from_str::<serde_json::Value>("{").unwrap_err();
224 let err = PhotonError::from(err);
225 assert!(matches!(err, PhotonError::PayloadError(_)));
226 }
227
228 #[test]
229 fn caused_errors_stay_clone() {
230 let err = PhotonError::caused("original", BadTag);
231 let clone = err.clone();
232 assert_eq!(err.to_string(), clone.to_string());
233 assert_eq!(
234 err.source().map(ToString::to_string),
235 clone.source().map(ToString::to_string)
236 );
237 }
238}