1use serde::{Deserialize, Serialize};
4use std::fmt;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
11pub enum AshErrorCode {
12 InvalidContext,
14 ContextExpired,
16 ReplayDetected,
18 IntegrityFailed,
20 EndpointMismatch,
22 ModeViolation,
24 UnsupportedContentType,
26 MalformedRequest,
28 CanonicalizationFailed,
30}
31
32impl AshErrorCode {
33 pub fn http_status(&self) -> u16 {
35 match self {
36 AshErrorCode::InvalidContext => 400,
37 AshErrorCode::ContextExpired => 410,
38 AshErrorCode::ReplayDetected => 409,
39 AshErrorCode::IntegrityFailed => 400,
40 AshErrorCode::EndpointMismatch => 400,
41 AshErrorCode::ModeViolation => 400,
42 AshErrorCode::UnsupportedContentType => 400,
43 AshErrorCode::MalformedRequest => 400,
44 AshErrorCode::CanonicalizationFailed => 400,
45 }
46 }
47
48 pub fn as_str(&self) -> &'static str {
50 match self {
51 AshErrorCode::InvalidContext => "ASH_INVALID_CONTEXT",
52 AshErrorCode::ContextExpired => "ASH_CONTEXT_EXPIRED",
53 AshErrorCode::ReplayDetected => "ASH_REPLAY_DETECTED",
54 AshErrorCode::IntegrityFailed => "ASH_INTEGRITY_FAILED",
55 AshErrorCode::EndpointMismatch => "ASH_ENDPOINT_MISMATCH",
56 AshErrorCode::ModeViolation => "ASH_MODE_VIOLATION",
57 AshErrorCode::UnsupportedContentType => "ASH_UNSUPPORTED_CONTENT_TYPE",
58 AshErrorCode::MalformedRequest => "ASH_MALFORMED_REQUEST",
59 AshErrorCode::CanonicalizationFailed => "ASH_CANONICALIZATION_FAILED",
60 }
61 }
62}
63
64impl fmt::Display for AshErrorCode {
65 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66 write!(f, "{}", self.as_str())
67 }
68}
69
70#[derive(Debug, Clone)]
75pub struct AshError {
76 code: AshErrorCode,
78 message: String,
80}
81
82impl AshError {
83 pub fn new(code: AshErrorCode, message: impl Into<String>) -> Self {
85 Self {
86 code,
87 message: message.into(),
88 }
89 }
90
91 pub fn code(&self) -> AshErrorCode {
93 self.code
94 }
95
96 pub fn message(&self) -> &str {
98 &self.message
99 }
100
101 pub fn http_status(&self) -> u16 {
103 self.code.http_status()
104 }
105}
106
107impl fmt::Display for AshError {
108 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109 write!(f, "{}: {}", self.code, self.message)
110 }
111}
112
113impl std::error::Error for AshError {}
114
115impl AshError {
117 pub fn invalid_context() -> Self {
119 Self::new(AshErrorCode::InvalidContext, "Context not found")
120 }
121
122 pub fn context_expired() -> Self {
124 Self::new(AshErrorCode::ContextExpired, "Context has expired")
125 }
126
127 pub fn replay_detected() -> Self {
129 Self::new(AshErrorCode::ReplayDetected, "Context already consumed")
130 }
131
132 pub fn integrity_failed() -> Self {
134 Self::new(AshErrorCode::IntegrityFailed, "Proof verification failed")
135 }
136
137 pub fn endpoint_mismatch() -> Self {
139 Self::new(
140 AshErrorCode::EndpointMismatch,
141 "Binding does not match endpoint",
142 )
143 }
144
145 pub fn canonicalization_failed(reason: &str) -> Self {
147 Self::new(
148 AshErrorCode::CanonicalizationFailed,
149 format!("Failed to canonicalize payload: {}", reason),
150 )
151 }
152}
153
154#[cfg(test)]
155mod tests {
156 use super::*;
157
158 #[test]
159 fn test_error_code_http_status() {
160 assert_eq!(AshErrorCode::InvalidContext.http_status(), 400);
161 assert_eq!(AshErrorCode::ContextExpired.http_status(), 410);
162 assert_eq!(AshErrorCode::ReplayDetected.http_status(), 409);
163 }
164
165 #[test]
166 fn test_error_code_as_str() {
167 assert_eq!(AshErrorCode::InvalidContext.as_str(), "ASH_INVALID_CONTEXT");
168 assert_eq!(AshErrorCode::ReplayDetected.as_str(), "ASH_REPLAY_DETECTED");
169 }
170
171 #[test]
172 fn test_error_display() {
173 let err = AshError::invalid_context();
174 assert_eq!(err.to_string(), "ASH_INVALID_CONTEXT: Context not found");
175 }
176
177 #[test]
178 fn test_error_convenience_functions() {
179 assert_eq!(
180 AshError::invalid_context().code(),
181 AshErrorCode::InvalidContext
182 );
183 assert_eq!(
184 AshError::context_expired().code(),
185 AshErrorCode::ContextExpired
186 );
187 assert_eq!(
188 AshError::replay_detected().code(),
189 AshErrorCode::ReplayDetected
190 );
191 }
192}