1use thiserror::Error;
4
5#[derive(Error, Debug)]
7pub enum EntityHydrationError {
8 #[error("EntityHydrationError - UninitializedFieldError: {0}")]
9 UninitializedFieldError(#[from] derive_builder::UninitializedFieldError),
10 #[error("EntityHydrationError - Deserialization: {0}")]
11 EventDeserialization(#[from] serde_json::Error),
12}
13
14#[derive(Error, Debug)]
15#[error("CursorDestructureError: couldn't turn {0} into {1}")]
16pub struct CursorDestructureError(&'static str, &'static str);
17
18impl From<(&'static str, &'static str)> for CursorDestructureError {
19 fn from((name, variant): (&'static str, &'static str)) -> Self {
20 Self(name, variant)
21 }
22}
23
24#[doc(hidden)]
25pub fn parse_constraint_detail_value(detail: Option<&str>) -> Option<String> {
37 let detail = detail?;
38 let start = detail.find("=(")? + 2;
39 let end = detail.rfind(") already")?;
40 if start <= end {
41 Some(detail[start..end].to_string())
42 } else {
43 None
44 }
45}
46
47#[doc(hidden)]
48pub fn extract_constraint_value(db_err: &dyn sqlx::error::DatabaseError) -> Option<String> {
56 let pg_err = db_err.try_downcast_ref::<sqlx::postgres::PgDatabaseError>()?;
57 parse_constraint_detail_value(pg_err.detail())
58}
59
60#[doc(hidden)]
61pub fn extract_events_pkey_id_value(db_err: &dyn sqlx::error::DatabaseError) -> Option<String> {
72 events_pkey_id_from_value(extract_constraint_value(db_err)?)
73}
74
75fn events_pkey_id_from_value(value: String) -> Option<String> {
76 match value.rsplit_once(", ") {
77 Some((id, _sequence)) => Some(id.to_string()),
78 None => Some(value),
79 }
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86pub enum ConstraintKind {
87 Unique,
88 ForeignKey,
89 Check,
90}
91
92#[doc(hidden)]
93pub fn is_classified_constraint_violation(db_err: &dyn sqlx::error::DatabaseError) -> bool {
98 db_err.is_unique_violation() || db_err.is_foreign_key_violation() || db_err.is_check_violation()
99}
100
101#[doc(hidden)]
102pub struct NotFoundValue<'a, T: ?Sized>(pub &'a T);
105
106impl<T: std::fmt::Display + ?Sized> NotFoundValue<'_, T> {
107 pub fn to_not_found_value(&self) -> String {
108 self.0.to_string()
109 }
110}
111
112#[doc(hidden)]
113pub trait ToNotFoundValueFallback {
114 fn to_not_found_value(&self) -> String;
115}
116
117impl<T: std::fmt::Debug + ?Sized> ToNotFoundValueFallback for NotFoundValue<'_, T> {
118 fn to_not_found_value(&self) -> String {
119 format!("{:?}", self.0)
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126 use proptest::prelude::*;
127
128 #[test]
129 fn parse_simple_uuid_value() {
130 let detail = Some("Key (id)=(550e8400-e29b-41d4-a716-446655440000) already exists.");
131 assert_eq!(
132 parse_constraint_detail_value(detail),
133 Some("550e8400-e29b-41d4-a716-446655440000".to_string())
134 );
135 }
136
137 #[test]
138 fn parse_string_value() {
139 let detail = Some("Key (email)=(user@example.com) already exists.");
140 assert_eq!(
141 parse_constraint_detail_value(detail),
142 Some("user@example.com".to_string())
143 );
144 }
145
146 #[test]
147 fn parse_composite_key_value() {
148 let detail = Some("Key (tenant_id, email)=(abc, user@example.com) already exists.");
149 assert_eq!(
150 parse_constraint_detail_value(detail),
151 Some("abc, user@example.com".to_string())
152 );
153 }
154
155 #[test]
156 fn parse_none_detail() {
157 assert_eq!(parse_constraint_detail_value(None), None);
158 }
159
160 #[test]
161 fn parse_unexpected_format() {
162 let detail = Some("something unexpected");
163 assert_eq!(parse_constraint_detail_value(detail), None);
164 }
165
166 #[test]
167 fn parse_value_containing_parentheses() {
168 let detail = Some("Key (name)=(foo (bar)) already exists.");
169 assert_eq!(
170 parse_constraint_detail_value(detail),
171 Some("foo (bar)".to_string())
172 );
173 }
174
175 #[test]
176 fn events_pkey_id_strips_the_sequence() {
177 let value = parse_constraint_detail_value(Some(
178 "Key (id, sequence)=(550e8400-e29b-41d4-a716-446655440000, 1) already exists.",
179 ))
180 .unwrap();
181 assert_eq!(
182 events_pkey_id_from_value(value),
183 Some("550e8400-e29b-41d4-a716-446655440000".to_string())
184 );
185 }
186
187 #[test]
188 fn events_pkey_id_keeps_commas_inside_the_id() {
189 assert_eq!(
190 events_pkey_id_from_value("a, b, 3".to_string()),
191 Some("a, b".to_string())
192 );
193 }
194
195 #[test]
196 fn events_pkey_id_without_separator_returns_value_as_is() {
197 assert_eq!(
198 events_pkey_id_from_value("plain".to_string()),
199 Some("plain".to_string())
200 );
201 }
202
203 #[test]
204 fn parse_empty_value() {
205 let detail = Some("Key (col)=() already exists.");
206 assert_eq!(parse_constraint_detail_value(detail), Some("".to_string()));
207 }
208
209 #[test]
210 fn not_found_value_uses_display_when_available() {
211 #[allow(unused_imports)]
212 use crate::ToNotFoundValueFallback;
213
214 let val = "hello";
216 assert_eq!(NotFoundValue(val).to_not_found_value(), "hello");
217
218 let num = 42;
220 assert_eq!(NotFoundValue(&num).to_not_found_value(), "42");
221 }
222
223 #[test]
224 fn not_found_value_falls_back_to_debug() {
225 use crate::ToNotFoundValueFallback;
226
227 #[derive(Debug)]
229 #[allow(dead_code)]
230 struct OnlyDebug(i32);
231
232 let val = OnlyDebug(7);
233 assert_eq!(NotFoundValue(&val).to_not_found_value(), "OnlyDebug(7)");
234 }
235
236 proptest! {
237 #[test]
241 fn constraint_detail_never_panics_and_is_honest(detail in ".*") {
242 match parse_constraint_detail_value(Some(&detail)) {
243 None => {}
244 Some(v) => {
245 prop_assert!(detail.contains(&v));
247 let start = detail.find("=(").expect("start marker present") + 2;
249 let end = detail
250 .rfind(") already")
251 .expect("end marker present");
252 prop_assert!(start <= end);
253 prop_assert_eq!(&detail[start..end], v.as_str());
254 }
255 }
256 }
257
258 #[test]
259 fn constraint_detail_none_input_returns_none(_ in Just(())) {
260 prop_assert_eq!(parse_constraint_detail_value(None), None);
261 }
262 }
263}