nautilus-network 0.65.0

Network communication machinery for the Nautilus trading engine
Documentation
// -------------------------------------------------------------------------------------------------
//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
//  https://nautechsystems.io
//
//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
//  You may not use this file except in compliance with the License.
//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.
// -------------------------------------------------------------------------------------------------

//! HTTP types including status codes, methods, and responses.

use std::{collections::HashMap, hash::Hash};

use bytes::Bytes;
use http::{Method, StatusCode, status::InvalidStatusCode};

/// An HTTP status code.
///
/// Wraps [`http::StatusCode`] to reuse its validation and convenience methods.
#[derive(Clone, Debug)]
pub struct HttpStatus {
    inner: StatusCode,
}

impl HttpStatus {
    /// Creates an [`HttpStatus`] from a [`StatusCode`].
    #[must_use]
    pub const fn new(code: StatusCode) -> Self {
        Self { inner: code }
    }

    /// Returns the status code as a `u16` (e.g., `200` for OK).
    #[inline]
    #[must_use]
    pub const fn as_u16(&self) -> u16 {
        self.inner.as_u16()
    }

    /// Returns the three-digit ASCII representation of this status (e.g., `"200"`).
    #[inline]
    #[must_use]
    pub fn as_str(&self) -> &str {
        self.inner.as_str()
    }

    /// Checks if this status is in the 1xx (informational) range.
    #[inline]
    #[must_use]
    pub fn is_informational(&self) -> bool {
        self.inner.is_informational()
    }

    /// Checks if this status is in the 2xx (success) range.
    #[inline]
    #[must_use]
    pub fn is_success(&self) -> bool {
        self.inner.is_success()
    }

    /// Checks if this status is in the 3xx (redirection) range.
    #[inline]
    #[must_use]
    pub fn is_redirection(&self) -> bool {
        self.inner.is_redirection()
    }

    /// Checks if this status is in the 4xx (client error) range.
    #[inline]
    #[must_use]
    pub fn is_client_error(&self) -> bool {
        self.inner.is_client_error()
    }

    /// Checks if this status is in the 5xx (server error) range.
    #[inline]
    #[must_use]
    pub fn is_server_error(&self) -> bool {
        self.inner.is_server_error()
    }
}

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

    /// Attempts to construct a [`HttpStatus`] from a `u16`.
    ///
    /// # Errors
    ///
    /// Returns an error if the code is not in the valid `100..999` range.
    fn try_from(code: u16) -> Result<Self, Self::Error> {
        Ok(Self {
            inner: StatusCode::from_u16(code)?,
        })
    }
}

/// An HTTP method supported by [`super::HttpClient`].
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum HttpMethod {
    GET,
    POST,
    PUT,
    DELETE,
    PATCH,
}

impl From<HttpMethod> for Method {
    fn from(value: HttpMethod) -> Self {
        match value {
            HttpMethod::GET => Self::GET,
            HttpMethod::POST => Self::POST,
            HttpMethod::PUT => Self::PUT,
            HttpMethod::DELETE => Self::DELETE,
            HttpMethod::PATCH => Self::PATCH,
        }
    }
}

/// The status, selected headers, and raw body returned by an HTTP request.
#[derive(Clone, Debug)]
pub struct HttpResponse {
    /// The HTTP status code.
    pub status: HttpStatus,
    /// The response headers as a map of key-value pairs.
    pub headers: HashMap<String, String>,
    /// The raw response body.
    pub body: Bytes,
}

#[cfg(test)]
mod tests {
    use rstest::rstest;

    use super::*;

    #[rstest]
    #[case(100, [true, false, false, false, false])]
    #[case(199, [true, false, false, false, false])]
    #[case(200, [false, true, false, false, false])]
    #[case(299, [false, true, false, false, false])]
    #[case(300, [false, false, true, false, false])]
    #[case(399, [false, false, true, false, false])]
    #[case(400, [false, false, false, true, false])]
    #[case(499, [false, false, false, true, false])]
    #[case(500, [false, false, false, false, true])]
    #[case(599, [false, false, false, false, true])]
    #[case(600, [false; 5])]
    #[case(999, [false; 5])]
    fn status_classifies_boundaries(#[case] code: u16, #[case] expected: [bool; 5]) {
        let status = HttpStatus::try_from(code).unwrap();

        assert_eq!(status.as_u16(), code);
        assert_eq!(status.as_str(), code.to_string());
        assert_eq!(
            [
                status.is_informational(),
                status.is_success(),
                status.is_redirection(),
                status.is_client_error(),
                status.is_server_error()
            ],
            expected,
        );
    }

    #[rstest]
    #[case(0)]
    #[case(99)]
    #[case(1000)]
    #[case(u16::MAX)]
    fn status_rejects_invalid_code(#[case] code: u16) {
        let error = HttpStatus::try_from(code).unwrap_err();

        assert_eq!(error.to_string(), "invalid status code");
    }

    #[rstest]
    #[case(HttpMethod::GET, Method::GET)]
    #[case(HttpMethod::POST, Method::POST)]
    #[case(HttpMethod::PUT, Method::PUT)]
    #[case(HttpMethod::DELETE, Method::DELETE)]
    #[case(HttpMethod::PATCH, Method::PATCH)]
    fn method_conversion_preserves_verb(#[case] input: HttpMethod, #[case] expected: Method) {
        assert_eq!(Method::from(input), expected);
    }
}