securitydept_utils/
error.rs1use 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}