Skip to main content

securitydept_utils/
error.rs

1use std::borrow::Cow;
2
3use serde::Serialize;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
6#[serde(rename_all = "snake_case")]
7pub enum UserRecovery {
8    None,
9    Retry,
10    RestartFlow,
11    Reauthenticate,
12    ContactSupport,
13}
14
15#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
16pub struct ErrorPresentation {
17    pub code: &'static str,
18    pub message: Cow<'static, str>,
19    pub recovery: UserRecovery,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
23#[serde(rename_all = "snake_case")]
24pub enum ServerErrorKind {
25    InvalidRequest,
26    Unauthenticated,
27    Unauthorized,
28    Conflict,
29    Unavailable,
30    Internal,
31}
32
33impl ServerErrorKind {
34    pub const fn from_http_status(status: u16) -> Self {
35        match status {
36            400 | 404 | 422 => Self::InvalidRequest,
37            401 => Self::Unauthenticated,
38            403 => Self::Unauthorized,
39            409 => Self::Conflict,
40            503 => Self::Unavailable,
41            _ if status >= 500 => Self::Internal,
42            _ => Self::InvalidRequest,
43        }
44    }
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
48pub struct ServerErrorDescriptor {
49    pub kind: ServerErrorKind,
50    pub code: &'static str,
51    pub message: Cow<'static, str>,
52    pub recovery: UserRecovery,
53    pub retryable: bool,
54    pub presentation: ErrorPresentation,
55}
56
57impl ServerErrorDescriptor {
58    pub fn new(kind: ServerErrorKind, presentation: ErrorPresentation) -> Self {
59        let retryable = presentation.recovery == UserRecovery::Retry;
60        Self {
61            kind,
62            code: presentation.code,
63            message: presentation.message.clone(),
64            recovery: presentation.recovery,
65            retryable,
66            presentation,
67        }
68    }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
72pub struct ServerErrorEnvelope {
73    pub success: bool,
74    pub status: u16,
75    pub error: ServerErrorDescriptor,
76}
77
78impl ServerErrorEnvelope {
79    pub fn new(status: u16, error: ServerErrorDescriptor) -> Self {
80        Self {
81            success: false,
82            status,
83            error,
84        }
85    }
86}
87
88impl ErrorPresentation {
89    pub fn new(
90        code: &'static str,
91        message: impl Into<Cow<'static, str>>,
92        recovery: UserRecovery,
93    ) -> Self {
94        Self {
95            code,
96            message: message.into(),
97            recovery,
98        }
99    }
100}
101
102pub trait ToErrorPresentation {
103    fn to_error_presentation(&self) -> ErrorPresentation;
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn server_error_kind_derives_from_status() {
112        assert_eq!(
113            ServerErrorKind::from_http_status(401),
114            ServerErrorKind::Unauthenticated
115        );
116        assert_eq!(
117            ServerErrorKind::from_http_status(409),
118            ServerErrorKind::Conflict
119        );
120        assert_eq!(
121            ServerErrorKind::from_http_status(503),
122            ServerErrorKind::Unavailable
123        );
124    }
125
126    #[test]
127    fn server_error_descriptor_preserves_dual_layer_fields() {
128        let descriptor = ServerErrorDescriptor::new(
129            ServerErrorKind::InvalidRequest,
130            ErrorPresentation::new(
131                "token_set_frontend.redirect_uri_invalid",
132                "The redirect URL is invalid.",
133                UserRecovery::RestartFlow,
134            ),
135        );
136
137        assert_eq!(descriptor.kind, ServerErrorKind::InvalidRequest);
138        assert_eq!(descriptor.code, "token_set_frontend.redirect_uri_invalid");
139        assert_eq!(descriptor.recovery, UserRecovery::RestartFlow);
140        assert_eq!(descriptor.presentation.code, descriptor.code);
141        assert!(!descriptor.retryable);
142    }
143}