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/// Wrapper used by generated code to format not-found values.
44/// Prefers `Display` over `Debug` via inherent-vs-trait method resolution.
45pub struct NotFoundValue<'a, T: ?Sized>(pub &'a T);
46
47impl<T: std::fmt::Display + ?Sized> NotFoundValue<'_, T> {
48    pub fn to_not_found_value(&self) -> String {
49        self.0.to_string()
50    }
51}
52
53#[doc(hidden)]
54pub trait ToNotFoundValueFallback {
55    fn to_not_found_value(&self) -> String;
56}
57
58impl<T: std::fmt::Debug + ?Sized> ToNotFoundValueFallback for NotFoundValue<'_, T> {
59    fn to_not_found_value(&self) -> String {
60        format!("{:?}", self.0)
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn parse_simple_uuid_value() {
70        let detail = Some("Key (id)=(550e8400-e29b-41d4-a716-446655440000) already exists.");
71        assert_eq!(
72            parse_constraint_detail_value(detail),
73            Some("550e8400-e29b-41d4-a716-446655440000".to_string())
74        );
75    }
76
77    #[test]
78    fn parse_string_value() {
79        let detail = Some("Key (email)=(user@example.com) already exists.");
80        assert_eq!(
81            parse_constraint_detail_value(detail),
82            Some("user@example.com".to_string())
83        );
84    }
85
86    #[test]
87    fn parse_composite_key_value() {
88        let detail = Some("Key (tenant_id, email)=(abc, user@example.com) already exists.");
89        assert_eq!(
90            parse_constraint_detail_value(detail),
91            Some("abc, user@example.com".to_string())
92        );
93    }
94
95    #[test]
96    fn parse_none_detail() {
97        assert_eq!(parse_constraint_detail_value(None), None);
98    }
99
100    #[test]
101    fn parse_unexpected_format() {
102        let detail = Some("something unexpected");
103        assert_eq!(parse_constraint_detail_value(detail), None);
104    }
105
106    #[test]
107    fn parse_value_containing_parentheses() {
108        let detail = Some("Key (name)=(foo (bar)) already exists.");
109        assert_eq!(
110            parse_constraint_detail_value(detail),
111            Some("foo (bar)".to_string())
112        );
113    }
114
115    #[test]
116    fn parse_empty_value() {
117        let detail = Some("Key (col)=() already exists.");
118        assert_eq!(parse_constraint_detail_value(detail), Some("".to_string()));
119    }
120
121    #[test]
122    fn not_found_value_uses_display_when_available() {
123        #[allow(unused_imports)]
124        use crate::ToNotFoundValueFallback;
125
126        // String implements Display - should get clean output
127        let val = "hello";
128        assert_eq!(NotFoundValue(val).to_not_found_value(), "hello");
129
130        // i32 implements Display
131        let num = 42;
132        assert_eq!(NotFoundValue(&num).to_not_found_value(), "42");
133    }
134
135    #[test]
136    fn not_found_value_falls_back_to_debug() {
137        use crate::ToNotFoundValueFallback;
138
139        // A type with Debug but no Display
140        #[derive(Debug)]
141        #[allow(dead_code)]
142        struct OnlyDebug(i32);
143
144        let val = OnlyDebug(7);
145        assert_eq!(NotFoundValue(&val).to_not_found_value(), "OnlyDebug(7)");
146    }
147}