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}
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}