ash_core/
errors.rs

1//! Error types for ASH protocol.
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5
6/// Error codes for ASH protocol.
7///
8/// These codes are stable and should not change between versions.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
11pub enum AshErrorCode {
12    /// Context not found in store
13    InvalidContext,
14    /// Context has expired
15    ContextExpired,
16    /// Context was already consumed (replay detected)
17    ReplayDetected,
18    /// Proof does not match expected value
19    IntegrityFailed,
20    /// Binding does not match expected endpoint
21    EndpointMismatch,
22    /// Mode requirements not met
23    ModeViolation,
24    /// Content type not supported
25    UnsupportedContentType,
26    /// Request format is invalid
27    MalformedRequest,
28    /// Payload cannot be canonicalized
29    CanonicalizationFailed,
30}
31
32impl AshErrorCode {
33    /// Get the recommended HTTP status code for this error.
34    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    /// Get the error code as a string.
49    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/// Main error type for ASH operations.
71///
72/// Error messages are designed to be safe for logging and client responses.
73/// They never contain sensitive data like payloads, proofs, or canonical strings.
74#[derive(Debug, Clone)]
75pub struct AshError {
76    /// Error code
77    code: AshErrorCode,
78    /// Human-readable message (safe for logging)
79    message: String,
80}
81
82impl AshError {
83    /// Create a new AshError.
84    pub fn new(code: AshErrorCode, message: impl Into<String>) -> Self {
85        Self {
86            code,
87            message: message.into(),
88        }
89    }
90
91    /// Get the error code.
92    pub fn code(&self) -> AshErrorCode {
93        self.code
94    }
95
96    /// Get the error message.
97    pub fn message(&self) -> &str {
98        &self.message
99    }
100
101    /// Get the recommended HTTP status code.
102    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
115/// Convenience functions for creating common errors.
116impl AshError {
117    /// Context not found.
118    pub fn invalid_context() -> Self {
119        Self::new(AshErrorCode::InvalidContext, "Context not found")
120    }
121
122    /// Context expired.
123    pub fn context_expired() -> Self {
124        Self::new(AshErrorCode::ContextExpired, "Context has expired")
125    }
126
127    /// Replay detected.
128    pub fn replay_detected() -> Self {
129        Self::new(AshErrorCode::ReplayDetected, "Context already consumed")
130    }
131
132    /// Integrity check failed.
133    pub fn integrity_failed() -> Self {
134        Self::new(AshErrorCode::IntegrityFailed, "Proof verification failed")
135    }
136
137    /// Endpoint mismatch.
138    pub fn endpoint_mismatch() -> Self {
139        Self::new(
140            AshErrorCode::EndpointMismatch,
141            "Binding does not match endpoint",
142        )
143    }
144
145    /// Canonicalization failed.
146    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}