1use thiserror::Error;
4
5use crate::redact::redact_credentials_in_text;
6
7#[derive(Debug, Error)]
8pub enum Error {
9 #[error("Database error: {message}")]
10 Database {
11 message: String,
12 #[source]
13 source: Option<Box<dyn std::error::Error + Send + Sync>>,
14 },
15
16 #[error("Not found: {0}")]
17 NotFound(String),
18
19 #[error("Validation error: {0}")]
20 Validation(String),
21
22 #[error("Privacy policy violation: {0}")]
23 Privacy(String),
24
25 #[error("Internal error: {0}")]
26 Internal(String),
27
28 #[error("Serialization error: {message}")]
29 Serialization {
30 message: String,
31 #[source]
32 source: Option<serde_json::Error>,
33 },
34
35 #[error("Pending deletion: {0}")]
36 PendingDeletion(String),
37
38 #[error("Identity error: {0}")]
39 Identity(String),
40}
41
42impl From<serde_json::Error> for Error {
43 fn from(e: serde_json::Error) -> Self {
44 Self::serialization(e)
45 }
46}
47
48impl Error {
49 #[must_use]
51 pub fn database(msg: impl AsRef<str>) -> Self {
52 Self::Database {
53 message: redact_credentials_in_text(msg.as_ref()),
54 source: None,
55 }
56 }
57
58 #[must_use]
60 pub fn database_with_source(
61 msg: impl AsRef<str>,
62 source: impl std::error::Error + Send + Sync + 'static,
63 ) -> Self {
64 Self::Database {
65 message: redact_credentials_in_text(msg.as_ref()),
66 source: Some(Box::new(source)),
67 }
68 }
69
70 #[must_use]
72 pub fn serialization(e: serde_json::Error) -> Self {
73 Self::Serialization {
74 message: e.to_string(),
75 source: Some(e),
76 }
77 }
78
79 #[must_use]
81 pub fn serialization_msg(msg: impl Into<String>) -> Self {
82 Self::Serialization {
83 message: msg.into(),
84 source: None,
85 }
86 }
87
88 pub fn is_retryable_transaction_contention(&self) -> bool {
90 match self {
91 Error::Database { message, .. } => {
92 let s = message.to_lowercase();
93 s.contains("read or write conflict")
94 || s.contains("can be retried")
95 || (s.contains("failed transaction") && s.contains("conflict"))
96 }
97 _ => false,
98 }
99 }
100}
101
102impl From<&str> for Error {
103 fn from(s: &str) -> Self {
104 Error::Validation(s.to_string())
105 }
106}
107
108pub type Result<T> = std::result::Result<T, Error>;
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn database_redacts_url_userinfo() {
116 let err = Error::database("connect failed: postgres://user:secret@host/db");
117 let s = err.to_string();
118 assert!(s.contains("postgres://***@host/db"));
119 assert!(!s.contains("secret"));
120 }
121
122 #[test]
123 fn serialization_preserves_source() {
124 let raw = serde_json::from_str::<u32>("not-json").expect_err("bad json");
125 let err = Error::from(raw);
126 let Error::Serialization {
127 source: Some(_), ..
128 } = err
129 else {
130 panic!("expected Serialization with source: {err:?}");
131 };
132 }
133}