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