strest 0.1.10

Blazing-fast async HTTP load tester in Rust - lock-free design, real-time stats, distributed runs, and optional chart exports for high-load API testing.
Documentation
use clap::ValueEnum;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::num::{NonZeroU64, NonZeroUsize};
use std::time::Duration;

use crate::error::{AppError, ValidationError};

#[derive(Debug, Clone, Copy, ValueEnum, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum HttpMethod {
    Get,
    Post,
    Patch,
    Put,
    Delete,
}

#[derive(Debug, Clone, Copy, ValueEnum, Deserialize, Serialize, PartialEq, Eq)]
pub enum HttpVersion {
    #[serde(rename = "0.9")]
    #[value(name = "0.9")]
    V0_9,
    #[serde(rename = "1.0")]
    #[value(name = "1.0")]
    V1_0,
    #[serde(rename = "1.1")]
    #[value(name = "1.1")]
    V1_1,
    #[serde(rename = "2")]
    #[value(name = "2")]
    V2,
    #[serde(rename = "3")]
    #[value(name = "3")]
    V3,
}

impl std::str::FromStr for HttpVersion {
    type Err = AppError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let normalized = s.trim();
        match normalized {
            "0.9" => Ok(HttpVersion::V0_9),
            "1.0" => Ok(HttpVersion::V1_0),
            "1.1" => Ok(HttpVersion::V1_1),
            "2" => Ok(HttpVersion::V2),
            "3" => Ok(HttpVersion::V3),
            _ => Err(AppError::validation(ValidationError::InvalidHttpVersion {
                value: s.to_owned(),
            })),
        }
    }
}

#[derive(Debug, Clone, Copy, ValueEnum, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum OutputFormat {
    Text,
    Json,
    Jsonl,
    Csv,
    Quiet,
}

#[derive(Debug, Clone, Copy, ValueEnum, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum TimeUnit {
    Ns,
    Us,
    Ms,
    S,
    M,
    H,
}

#[derive(Debug, Clone, Copy, ValueEnum, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum ControllerMode {
    Auto,
    Manual,
}

impl std::str::FromStr for ControllerMode {
    type Err = AppError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let normalized = s.trim().to_ascii_lowercase();
        match normalized.as_str() {
            "auto" => Ok(ControllerMode::Auto),
            "manual" => Ok(ControllerMode::Manual),
            _ => Err(AppError::validation(
                ValidationError::InvalidControllerMode {
                    value: s.to_owned(),
                },
            )),
        }
    }
}

#[derive(Debug, Clone, Copy, ValueEnum, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum Protocol {
    Http,
    GrpcUnary,
    GrpcStreaming,
    Websocket,
    Tcp,
    Udp,
    Quic,
    Mqtt,
    Enet,
    Kcp,
    Raknet,
}

impl Protocol {
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Protocol::Http => "http",
            Protocol::GrpcUnary => "grpc-unary",
            Protocol::GrpcStreaming => "grpc-streaming",
            Protocol::Websocket => "websocket",
            Protocol::Tcp => "tcp",
            Protocol::Udp => "udp",
            Protocol::Quic => "quic",
            Protocol::Mqtt => "mqtt",
            Protocol::Enet => "enet",
            Protocol::Kcp => "kcp",
            Protocol::Raknet => "raknet",
        }
    }

    #[must_use]
    pub const fn to_domain(self) -> crate::domain::run::ProtocolKind {
        match self {
            Protocol::Http => crate::domain::run::ProtocolKind::Http,
            Protocol::GrpcUnary => crate::domain::run::ProtocolKind::GrpcUnary,
            Protocol::GrpcStreaming => crate::domain::run::ProtocolKind::GrpcStreaming,
            Protocol::Websocket => crate::domain::run::ProtocolKind::Websocket,
            Protocol::Tcp => crate::domain::run::ProtocolKind::Tcp,
            Protocol::Udp => crate::domain::run::ProtocolKind::Udp,
            Protocol::Quic => crate::domain::run::ProtocolKind::Quic,
            Protocol::Mqtt => crate::domain::run::ProtocolKind::Mqtt,
            Protocol::Enet => crate::domain::run::ProtocolKind::Enet,
            Protocol::Kcp => crate::domain::run::ProtocolKind::Kcp,
            Protocol::Raknet => crate::domain::run::ProtocolKind::Raknet,
        }
    }
}

#[derive(Debug, Clone, Copy, ValueEnum, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum LoadMode {
    Arrival,
    Step,
    Ramp,
    Jitter,
    Burst,
    Soak,
}

impl LoadMode {
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            LoadMode::Arrival => "arrival",
            LoadMode::Step => "step",
            LoadMode::Ramp => "ramp",
            LoadMode::Jitter => "jitter",
            LoadMode::Burst => "burst",
            LoadMode::Soak => "soak",
        }
    }

    #[must_use]
    pub const fn to_domain(self) -> crate::domain::run::LoadMode {
        match self {
            LoadMode::Arrival => crate::domain::run::LoadMode::Arrival,
            LoadMode::Step => crate::domain::run::LoadMode::Step,
            LoadMode::Ramp => crate::domain::run::LoadMode::Ramp,
            LoadMode::Jitter => crate::domain::run::LoadMode::Jitter,
            LoadMode::Burst => crate::domain::run::LoadMode::Burst,
            LoadMode::Soak => crate::domain::run::LoadMode::Soak,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TlsVersion {
    V1_0,
    V1_1,
    V1_2,
    V1_3,
}

impl std::str::FromStr for TlsVersion {
    type Err = AppError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let normalized = s.trim().to_ascii_lowercase();
        match normalized.as_str() {
            "1.0" | "tls1.0" | "tls1" | "v1.0" => Ok(TlsVersion::V1_0),
            "1.1" | "tls1.1" | "v1.1" => Ok(TlsVersion::V1_1),
            "1.2" | "tls1.2" | "v1.2" => Ok(TlsVersion::V1_2),
            "1.3" | "tls1.3" | "v1.3" => Ok(TlsVersion::V1_3),
            _ => Err(AppError::validation(ValidationError::InvalidTlsVersion {
                value: s.to_owned(),
            })),
        }
    }
}

impl<'de> Deserialize<'de> for TlsVersion {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let value = String::deserialize(deserializer)?;
        value
            .parse::<TlsVersion>()
            .map_err(serde::de::Error::custom)
    }
}

impl Serialize for TlsVersion {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: serde::Serializer,
    {
        let value = match self {
            TlsVersion::V1_0 => "1.0",
            TlsVersion::V1_1 => "1.1",
            TlsVersion::V1_2 => "1.2",
            TlsVersion::V1_3 => "1.3",
        };
        serializer.serialize_str(value)
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PositiveU64(NonZeroU64);

impl PositiveU64 {
    #[must_use]
    pub const fn get(self) -> u64 {
        self.0.get()
    }
}

impl TryFrom<u64> for PositiveU64 {
    type Error = ValidationError;

    fn try_from(value: u64) -> Result<Self, Self::Error> {
        NonZeroU64::new(value)
            .map(PositiveU64)
            .ok_or_else(|| ValidationError::ValueTooSmall { min: 1 })
    }
}

impl std::str::FromStr for PositiveU64 {
    type Err = ValidationError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let value: u64 = s
            .parse()
            .map_err(|err| ValidationError::InvalidNumber { source: err })?;
        PositiveU64::try_from(value)
    }
}

impl From<PositiveU64> for u64 {
    fn from(value: PositiveU64) -> Self {
        value.get()
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PositiveUsize(NonZeroUsize);

impl PositiveUsize {
    #[must_use]
    pub const fn get(self) -> usize {
        self.0.get()
    }
}

impl TryFrom<usize> for PositiveUsize {
    type Error = ValidationError;

    fn try_from(value: usize) -> Result<Self, Self::Error> {
        NonZeroUsize::new(value)
            .map(PositiveUsize)
            .ok_or_else(|| ValidationError::ValueTooSmall { min: 1 })
    }
}

impl std::str::FromStr for PositiveUsize {
    type Err = ValidationError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let value: usize = s
            .parse()
            .map_err(|err| ValidationError::InvalidNumber { source: err })?;
        PositiveUsize::try_from(value)
    }
}

impl From<PositiveUsize> for usize {
    fn from(value: PositiveUsize) -> Self {
        value.get()
    }
}

#[derive(Debug, Clone)]
pub struct LoadProfile {
    pub initial_rpm: u64,
    pub stages: Vec<LoadStage>,
}

#[derive(Debug, Clone)]
pub struct LoadStage {
    pub duration: Duration,
    pub target_rpm: u64,
}

#[derive(Debug, Clone)]
pub struct Scenario {
    pub base_url: Option<String>,
    pub vars: BTreeMap<String, String>,
    pub steps: Vec<ScenarioStep>,
}

#[derive(Debug, Clone)]
pub struct ScenarioStep {
    pub name: Option<String>,
    pub method: HttpMethod,
    pub url: Option<String>,
    pub path: Option<String>,
    pub headers: Vec<(String, String)>,
    pub body: Option<String>,
    pub assert_status: Option<u16>,
    pub assert_body_contains: Option<String>,
    pub think_time: Option<Duration>,
    pub vars: BTreeMap<String, String>,
}

#[derive(Debug, Clone)]
pub struct ConnectToMapping {
    pub source_host: String,
    pub source_port: u16,
    pub target_host: String,
    pub target_port: u16,
}