1use std::fmt;
4
5#[derive(Debug)]
6pub struct SeamError {
7 code: String,
8 message: String,
9 status: u16,
10}
11
12fn default_status(code: &str) -> u16 {
13 match code {
14 "VALIDATION_ERROR" => 400,
15 "UNAUTHORIZED" => 401,
16 "FORBIDDEN" => 403,
17 "NOT_FOUND" => 404,
18 "RATE_LIMITED" => 429,
19 "INTERNAL_ERROR" => 500,
20 _ => 500,
21 }
22}
23
24impl SeamError {
25 pub fn new(code: impl Into<String>, message: impl Into<String>, status: u16) -> Self {
26 Self { code: code.into(), message: message.into(), status }
27 }
28
29 pub fn with_code(code: impl Into<String>, message: impl Into<String>) -> Self {
30 let code = code.into();
31 let status = default_status(&code);
32 Self { code, message: message.into(), status }
33 }
34
35 pub fn validation(msg: impl Into<String>) -> Self {
36 Self::with_code("VALIDATION_ERROR", msg)
37 }
38
39 pub fn not_found(msg: impl Into<String>) -> Self {
40 Self::with_code("NOT_FOUND", msg)
41 }
42
43 pub fn internal(msg: impl Into<String>) -> Self {
44 Self::with_code("INTERNAL_ERROR", msg)
45 }
46
47 pub fn unauthorized(msg: impl Into<String>) -> Self {
48 Self::with_code("UNAUTHORIZED", msg)
49 }
50
51 pub fn forbidden(msg: impl Into<String>) -> Self {
52 Self::with_code("FORBIDDEN", msg)
53 }
54
55 pub fn rate_limited(msg: impl Into<String>) -> Self {
56 Self::with_code("RATE_LIMITED", msg)
57 }
58
59 pub fn code(&self) -> &str {
60 &self.code
61 }
62
63 pub fn message(&self) -> &str {
64 &self.message
65 }
66
67 pub fn status(&self) -> u16 {
68 self.status
69 }
70}
71
72impl fmt::Display for SeamError {
73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 write!(f, "{}: {}", self.code, self.message)
75 }
76}
77
78impl std::error::Error for SeamError {}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
85 fn default_status_known_codes() {
86 assert_eq!(default_status("VALIDATION_ERROR"), 400);
87 assert_eq!(default_status("UNAUTHORIZED"), 401);
88 assert_eq!(default_status("FORBIDDEN"), 403);
89 assert_eq!(default_status("NOT_FOUND"), 404);
90 assert_eq!(default_status("RATE_LIMITED"), 429);
91 assert_eq!(default_status("INTERNAL_ERROR"), 500);
92 }
93
94 #[test]
95 fn default_status_unknown_code() {
96 assert_eq!(default_status("CUSTOM_ERROR"), 500);
97 }
98
99 #[test]
100 fn new_explicit_status() {
101 let err = SeamError::new("RATE_LIMITED", "too fast", 429);
102 assert_eq!(err.code(), "RATE_LIMITED");
103 assert_eq!(err.message(), "too fast");
104 assert_eq!(err.status(), 429);
105 }
106
107 #[test]
108 fn with_code_auto_resolves_status() {
109 let err = SeamError::with_code("NOT_FOUND", "gone");
110 assert_eq!(err.status(), 404);
111 }
112
113 #[test]
114 fn convenience_constructors() {
115 assert_eq!(SeamError::validation("x").status(), 400);
116 assert_eq!(SeamError::not_found("x").status(), 404);
117 assert_eq!(SeamError::internal("x").status(), 500);
118 assert_eq!(SeamError::unauthorized("x").status(), 401);
119 assert_eq!(SeamError::forbidden("x").status(), 403);
120 assert_eq!(SeamError::rate_limited("x").status(), 429);
121 }
122
123 #[test]
124 fn display_format() {
125 let err = SeamError::not_found("missing");
126 assert_eq!(err.to_string(), "NOT_FOUND: missing");
127 }
128}