use core::fmt;
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(transparent)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct StatusCode(u16);
impl StatusCode {
pub const SUCCESS: Self = Self(1000);
pub const CLIENT_ERROR: Self = Self(2000);
pub const INVALID_PARAMETERS: Self = Self(2001);
pub const NOT_ENOUGH_INFORMATION: Self = Self(2002);
pub const UNKNOWN_LOCATION: Self = Self(2003);
pub const UNKNOWN_TOKEN: Self = Self(2004);
pub const SERVER_ERROR: Self = Self(3000);
pub const UNABLE_TO_USE_CLIENT_API: Self = Self(3001);
pub const UNSUPPORTED_VERSION: Self = Self(3002);
pub const NO_MATCHING_ENDPOINTS: Self = Self(3003);
pub const HUB_ERROR: Self = Self(4000);
pub const UNKNOWN_RECEIVER: Self = Self(4001);
pub const TIMEOUT_ON_FORWARDED_REQUEST: Self = Self(4002);
pub const CONNECTION_PROBLEM: Self = Self(4003);
#[must_use]
pub const fn new(code: u16) -> Self {
Self(code)
}
#[must_use]
pub const fn get(self) -> u16 {
self.0
}
#[must_use]
pub const fn class(self) -> StatusClass {
match self.0 {
1000..=1999 => StatusClass::Success,
2000..=2999 => StatusClass::ClientError,
3000..=3999 => StatusClass::ServerError,
4000..=4999 => StatusClass::HubError,
_ => StatusClass::Unknown,
}
}
#[must_use]
pub const fn is_success(self) -> bool {
matches!(self.class(), StatusClass::Success)
}
#[must_use]
pub const fn is_custom(self) -> bool {
!matches!(self.class(), StatusClass::Unknown) && self.0 % 1000 >= 900
}
#[must_use]
pub const fn description(self) -> Option<&'static str> {
Some(match self.0 {
1000 => "Generic success code",
2000 => "Generic client error",
2001 => "Invalid or missing parameters",
2002 => "Not enough information",
2003 => "Unknown Location",
2004 => "Unknown Token",
3000 => "Generic server error",
3001 => "Unable to use the client's API",
3002 => "Unsupported version",
3003 => "No matching endpoints or expected endpoints missing between parties",
4000 => "Generic error",
4001 => "Unknown receiver (TO address is unknown)",
4002 => "Timeout on forwarded request",
4003 => "Connection problem (receiving party is not connected)",
_ => return None,
})
}
}
impl fmt::Display for StatusCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.description() {
Some(d) => write!(f, "{} ({d})", self.0),
None => write!(f, "{}", self.0),
}
}
}
impl fmt::Debug for StatusCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "StatusCode({self})")
}
}
impl From<u16> for StatusCode {
fn from(value: u16) -> Self {
Self(value)
}
}
impl From<StatusCode> for u16 {
fn from(value: StatusCode) -> Self {
value.0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum StatusClass {
Success,
ClientError,
ServerError,
HubError,
Unknown,
}
impl StatusClass {
#[must_use]
pub const fn is_success(self) -> bool {
matches!(self, Self::Success)
}
#[must_use]
pub const fn is_client_fault(self) -> bool {
matches!(self, Self::ClientError)
}
}
impl fmt::Display for StatusClass {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
Self::Success => "success",
Self::ClientError => "client error",
Self::ServerError => "server error",
Self::HubError => "hub error",
Self::Unknown => "unknown status class",
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn classes_follow_the_ranges() {
assert_eq!(StatusCode::new(1000).class(), StatusClass::Success);
assert_eq!(StatusCode::new(1999).class(), StatusClass::Success);
assert_eq!(StatusCode::new(2001).class(), StatusClass::ClientError);
assert_eq!(StatusCode::new(3002).class(), StatusClass::ServerError);
assert_eq!(StatusCode::new(4003).class(), StatusClass::HubError);
assert_eq!(StatusCode::new(5000).class(), StatusClass::Unknown);
assert_eq!(StatusCode::new(999).class(), StatusClass::Unknown);
}
#[test]
fn custom_ranges_are_recognised() {
for code in [1900, 1999, 2900, 3950, 4999] {
assert!(StatusCode::new(code).is_custom(), "{code} is in a reserved custom range");
}
for code in [1000, 2001, 3003, 4000, 2899] {
assert!(!StatusCode::new(code).is_custom(), "{code} is a standard code");
}
assert!(!StatusCode::new(5900).is_custom(), "outside every defined class");
}
#[test]
fn serialises_as_a_bare_number() {
assert_eq!(serde_json::to_string(&StatusCode::SUCCESS).unwrap(), "1000");
let parsed: StatusCode = serde_json::from_str("2001").unwrap();
assert_eq!(parsed, StatusCode::INVALID_PARAMETERS);
}
#[test]
fn display_includes_the_spec_description() {
assert_eq!(StatusCode::INVALID_PARAMETERS.to_string(), "2001 (Invalid or missing parameters)");
assert_eq!(StatusCode::new(2901).to_string(), "2901");
}
}