Skip to content

Commit

Permalink
Introduce a StatusCode type to represent statuses
Browse files Browse the repository at this point in the history
This introduces a `StatusCode` type modeled after HTTP status codes but
adapted to known NATS constants.
  • Loading branch information
caspervonb committed Jun 3, 2022
1 parent 90e8f96 commit fdaf332
Show file tree
Hide file tree
Showing 4 changed files with 253 additions and 19 deletions.
3 changes: 2 additions & 1 deletion async-nats/src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ use bytes::{Buf, BytesMut};
use tokio::io;

use crate::header::{HeaderMap, HeaderName, HeaderValue};
use crate::status::StatusCode;
use crate::{ClientOp, ServerError, ServerOp};

/// Supertrait enabling trait object for containing both TLS and non TLS `TcpStream` connection.
Expand Down Expand Up @@ -218,7 +219,7 @@ impl Connection {
}

let mut headers = HeaderMap::new();
let mut maybe_status: Option<NonZeroU16> = None;
let mut maybe_status: Option<StatusCode> = None;
let mut maybe_description: Option<String> = None;
if let Some(slice) = version_line.get("NATS/1.0".len()..).map(|s| s.trim()) {
match slice.split_once(' ') {
Expand Down
8 changes: 5 additions & 3 deletions async-nats/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,11 @@ pub use options::{AuthError, ConnectOptions};
pub mod header;
pub mod jetstream;
pub mod message;
pub mod status;
mod tls;

pub use message::Message;
pub use status::StatusCode;

/// Information sent by the server back to this client
/// during initial connection, and possibly again later.
Expand Down Expand Up @@ -230,7 +232,7 @@ pub(crate) enum ServerOp {
reply: Option<String>,
payload: Bytes,
headers: Option<HeaderMap>,
status: Option<NonZeroU16>,
status: Option<StatusCode>,
description: Option<String>,
},
}
Expand Down Expand Up @@ -811,7 +813,7 @@ impl Client {
self.flush().await?;
match sub.next().await {
Some(message) => {
if message.is_no_responders() {
if message.status == Some(StatusCode::NO_RESPONDERS) {
return Err(Box::new(std::io::Error::new(
ErrorKind::NotFound,
"nats: no responders",
Expand Down Expand Up @@ -839,7 +841,7 @@ impl Client {
self.flush().await?;
match sub.next().await {
Some(message) => {
if message.is_no_responders() {
if message.status == Some(StatusCode::NO_RESPONDERS) {
return Err(Box::new(std::io::Error::new(
ErrorKind::NotFound,
"nats: no responders",
Expand Down
17 changes: 2 additions & 15 deletions async-nats/src/message.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::header::HeaderMap;
use crate::status::StatusCode;
use bytes::Bytes;
use std::num::NonZeroU16;

Expand All @@ -8,20 +9,6 @@ pub struct Message {
pub reply: Option<String>,
pub payload: Bytes,
pub headers: Option<HeaderMap>,
pub(crate) status: Option<NonZeroU16>,
pub status: Option<StatusCode>,
pub(crate) description: Option<String>,
}

impl Message {
pub(crate) fn is_no_responders(&self) -> bool {
if !self.payload.is_empty() {
return false;
}
if self.status == NonZeroU16::new(NO_RESPONDERS) {
return true;
}
false
}
}

const NO_RESPONDERS: u16 = 503;
244 changes: 244 additions & 0 deletions async-nats/src/status.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
// Heavily borrowed from the http crate, would re-export since it is already a dependency
// but we have our own range of constants.
use std::convert::TryFrom;
use std::error::Error;
use std::fmt;
use std::num::NonZeroU16;
use std::str::FromStr;

/// A possible error value when converting a `StatusCode` from a `u16` or `&str`
///
/// This error indicates that the supplied input was not a valid number, was less
/// than 100, or was greater than 999.
pub struct InvalidStatusCode {}

impl fmt::Debug for InvalidStatusCode {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("InvalidStatusCode").finish()
}
}

impl fmt::Display for InvalidStatusCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("invalid status code")
}
}

impl Error for InvalidStatusCode {}

impl InvalidStatusCode {
fn new() -> InvalidStatusCode {
InvalidStatusCode {}
}
}

/// An NATS status code.
///
/// Constants are provided for known status codes.
///
/// Status code values in the range 100-999 (inclusive) are supported by this
/// type. Values in the range 100-599 are semantically classified by the most
/// significant digit. See [`StatusCode::is_success`], etc.
///
/// # Examples
///
/// ```
/// use async_nats::StatusCode;
///
/// assert_eq!(StatusCode::OK.as_u16(), 200);
/// assert!(StatusCode::OK.is_informational());
/// ```
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct StatusCode(NonZeroU16);

impl StatusCode {
/// Converts a u16 to a status code.
///
/// The function validates the correctness of the supplied u16. It must be
/// greater or equal to 100 and less than 1000.
///
/// # Example
///
/// ```
/// use async_nats::status::StatusCode;
///
/// let ok = StatusCode::from_u16(200).unwrap();
/// assert_eq!(ok, StatusCode::OK);
///
/// let err = StatusCode::from_u16(99);
/// assert!(err.is_err());
///
/// let err = StatusCode::from_u16(1000);
/// assert!(err.is_err());
/// ```
#[inline]
pub fn from_u16(src: u16) -> Result<StatusCode, InvalidStatusCode> {
if !(100..1000).contains(&src) {
return Err(InvalidStatusCode::new());
}

NonZeroU16::new(src)
.map(StatusCode)
.ok_or_else(InvalidStatusCode::new)
}

/// Converts a &[u8] to a status code
pub fn from_bytes(src: &[u8]) -> Result<StatusCode, InvalidStatusCode> {
if src.len() != 3 {
return Err(InvalidStatusCode::new());
}

let a = src[0].wrapping_sub(b'0') as u16;
let b = src[1].wrapping_sub(b'0') as u16;
let c = src[2].wrapping_sub(b'0') as u16;

if a == 0 || a > 9 || b > 9 || c > 9 {
return Err(InvalidStatusCode::new());
}

let status = (a * 100) + (b * 10) + c;
NonZeroU16::new(status)
.map(StatusCode)
.ok_or_else(InvalidStatusCode::new)
}

/// Returns the `u16` corresponding to this `StatusCode`.
///
/// # Example
///
/// ```
/// let status = async_nats::StatusCode::OK;
/// assert_eq!(status.as_u16(), 200);
/// ```
#[inline]
pub fn as_u16(&self) -> u16 {
(*self).into()
}

/// Check if status is within 100-199.
#[inline]
pub fn is_informational(&self) -> bool {
(100..200).contains(&self.0.get())
}

/// Check if status is within 200-299.
#[inline]
pub fn is_success(&self) -> bool {
(200..300).contains(&self.0.get())
}

/// Check if status is within 300-399.
#[inline]
pub fn is_redirection(&self) -> bool {
(300..400).contains(&self.0.get())
}

/// Check if status is within 400-499.
#[inline]
pub fn is_client_error(&self) -> bool {
(400..500).contains(&self.0.get())
}

/// Check if status is within 500-599.
#[inline]
pub fn is_server_error(&self) -> bool {
(500..600).contains(&self.0.get())
}
}

impl fmt::Debug for StatusCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.0, f)
}
}

/// Formats the status code.
///
/// # Example
///
/// ```
/// # use async_nats::StatusCode;
/// assert_eq!(format!("{}", StatusCode::OK), "200");
/// ```
impl fmt::Display for StatusCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// TODO(caspervonb) display a canonical statically known reason / human readable description of the status
write!(f, "{}", u16::from(*self))
}
}

impl Default for StatusCode {
#[inline]
fn default() -> StatusCode {
StatusCode::OK
}
}

impl PartialEq<u16> for StatusCode {
#[inline]
fn eq(&self, other: &u16) -> bool {
self.as_u16() == *other
}
}

impl PartialEq<StatusCode> for u16 {
#[inline]
fn eq(&self, other: &StatusCode) -> bool {
*self == other.as_u16()
}
}

impl From<StatusCode> for u16 {
#[inline]
fn from(status: StatusCode) -> u16 {
status.0.get()
}
}

impl FromStr for StatusCode {
type Err = InvalidStatusCode;

fn from_str(s: &str) -> Result<StatusCode, InvalidStatusCode> {
StatusCode::from_bytes(s.as_ref())
}
}

impl<'a> From<&'a StatusCode> for StatusCode {
#[inline]
fn from(t: &'a StatusCode) -> Self {
*t
}
}

impl<'a> TryFrom<&'a [u8]> for StatusCode {
type Error = InvalidStatusCode;

#[inline]
fn try_from(t: &'a [u8]) -> Result<Self, Self::Error> {
StatusCode::from_bytes(t)
}
}

impl<'a> TryFrom<&'a str> for StatusCode {
type Error = InvalidStatusCode;

#[inline]
fn try_from(t: &'a str) -> Result<Self, Self::Error> {
t.parse()
}
}

impl TryFrom<u16> for StatusCode {
type Error = InvalidStatusCode;

#[inline]
fn try_from(t: u16) -> Result<Self, Self::Error> {
StatusCode::from_u16(t)
}
}

impl StatusCode {
pub const OK: StatusCode = StatusCode(unsafe { NonZeroU16::new_unchecked(200) });
pub const NOT_FOUND: StatusCode = StatusCode(unsafe { NonZeroU16::new_unchecked(404) });
pub const NO_RESPONDERS: StatusCode = StatusCode(unsafe { NonZeroU16::new_unchecked(503) });
}

0 comments on commit fdaf332

Please sign in to comment.