Skip to main content

seam_server/
errors.rs

1/* src/server/core/rust/src/errors.rs */
2
3use 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}