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