Skip to main content

es_entity/
error.rs

1//! Types for working with errors produced by es-entity.
2
3use thiserror::Error;
4
5/// Error type for entity hydration failures (reconstructing entities from events).
6#[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)]
25/// Extracts the conflicting value from a PostgreSQL constraint violation detail message.
26///
27/// PostgreSQL formats unique violation details as:
28/// `Key (column)=(value) already exists.`
29///
30/// Returns `None` if the detail is missing or doesn't match the expected format.
31pub fn parse_constraint_detail_value(detail: Option<&str>) -> Option<String> {
32    let detail = detail?;
33    let start = detail.find("=(")? + 2;
34    let end = detail.rfind(") already")?;
35    if start <= end {
36        Some(detail[start..end].to_string())
37    } else {
38        None
39    }
40}
41
42#[doc(hidden)]
43/// Extracts the conflicting value from a database error's constraint violation.
44///
45/// Downcasts to [`sqlx::postgres::PgDatabaseError`], reads its `detail()`,
46/// and parses the conflicting value.
47pub fn extract_constraint_value(db_err: &dyn sqlx::error::DatabaseError) -> Option<String> {
48    let pg_err = db_err.try_downcast_ref::<sqlx::postgres::PgDatabaseError>()?;
49    parse_constraint_detail_value(pg_err.detail())
50}
51
52#[doc(hidden)]
53/// Wrapper used by generated code to format not-found values.
54/// Prefers `Display` over `Debug` via inherent-vs-trait method resolution.
55pub struct NotFoundValue<'a, T: ?Sized>(pub &'a T);
56
57impl<T: std::fmt::Display + ?Sized> NotFoundValue<'_, T> {
58    pub fn to_not_found_value(&self) -> String {
59        self.0.to_string()
60    }
61}
62
63#[doc(hidden)]
64pub trait ToNotFoundValueFallback {
65    fn to_not_found_value(&self) -> String;
66}
67
68impl<T: std::fmt::Debug + ?Sized> ToNotFoundValueFallback for NotFoundValue<'_, T> {
69    fn to_not_found_value(&self) -> String {
70        format!("{:?}", self.0)
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn parse_simple_uuid_value() {
80        let detail = Some("Key (id)=(550e8400-e29b-41d4-a716-446655440000) already exists.");
81        assert_eq!(
82            parse_constraint_detail_value(detail),
83            Some("550e8400-e29b-41d4-a716-446655440000".to_string())
84        );
85    }
86
87    #[test]
88    fn parse_string_value() {
89        let detail = Some("Key (email)=(user@example.com) already exists.");
90        assert_eq!(
91            parse_constraint_detail_value(detail),
92            Some("user@example.com".to_string())
93        );
94    }
95
96    #[test]
97    fn parse_composite_key_value() {
98        let detail = Some("Key (tenant_id, email)=(abc, user@example.com) already exists.");
99        assert_eq!(
100            parse_constraint_detail_value(detail),
101            Some("abc, user@example.com".to_string())
102        );
103    }
104
105    #[test]
106    fn parse_none_detail() {
107        assert_eq!(parse_constraint_detail_value(None), None);
108    }
109
110    #[test]
111    fn parse_unexpected_format() {
112        let detail = Some("something unexpected");
113        assert_eq!(parse_constraint_detail_value(detail), None);
114    }
115
116    #[test]
117    fn parse_value_containing_parentheses() {
118        let detail = Some("Key (name)=(foo (bar)) already exists.");
119        assert_eq!(
120            parse_constraint_detail_value(detail),
121            Some("foo (bar)".to_string())
122        );
123    }
124
125    #[test]
126    fn parse_empty_value() {
127        let detail = Some("Key (col)=() already exists.");
128        assert_eq!(parse_constraint_detail_value(detail), Some("".to_string()));
129    }
130
131    #[test]
132    fn not_found_value_uses_display_when_available() {
133        #[allow(unused_imports)]
134        use crate::ToNotFoundValueFallback;
135
136        // String implements Display - should get clean output
137        let val = "hello";
138        assert_eq!(NotFoundValue(val).to_not_found_value(), "hello");
139
140        // i32 implements Display
141        let num = 42;
142        assert_eq!(NotFoundValue(&num).to_not_found_value(), "42");
143    }
144
145    #[test]
146    fn not_found_value_falls_back_to_debug() {
147        use crate::ToNotFoundValueFallback;
148
149        // A type with Debug but no Display
150        #[derive(Debug)]
151        #[allow(dead_code)]
152        struct OnlyDebug(i32);
153
154        let val = OnlyDebug(7);
155        assert_eq!(NotFoundValue(&val).to_not_found_value(), "OnlyDebug(7)");
156    }
157}