Skip to main content

seam_server/
errors.rs

1/* packages/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    "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}