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 std::time::Duration;

use super::types::{ConnectToMapping, PositiveU64, PositiveUsize, TlsVersion};
use crate::error::{AppError, AppResult, ConnectToPortKind, ValidationError};

pub(crate) fn parse_header(s: &str) -> Result<(String, String), ValidationError> {
    match s.split_once(':') {
        Some((key, value)) => Ok((key.trim().to_owned(), value.trim().to_owned())),
        None => Err(ValidationError::InvalidHeaderFormat {
            value: s.to_owned(),
        }),
    }
}

pub(super) fn parse_positive_u64(s: &str) -> AppResult<PositiveU64> {
    s.parse::<PositiveU64>().map_err(AppError::from)
}

pub(super) fn parse_positive_usize(s: &str) -> AppResult<PositiveUsize> {
    s.parse::<PositiveUsize>().map_err(AppError::from)
}

pub(super) fn parse_tls_version(s: &str) -> AppResult<TlsVersion> {
    s.parse::<TlsVersion>()
}

pub(crate) fn parse_bool_env(s: &str) -> AppResult<bool> {
    match s.trim().to_ascii_lowercase().as_str() {
        "1" | "true" | "yes" | "y" | "on" => Ok(true),
        "0" | "false" | "no" | "n" | "off" => Ok(false),
        _ => Err(AppError::validation(ValidationError::InvalidBoolean {
            value: s.to_owned(),
        })),
    }
}

pub(crate) fn parse_connect_to(s: &str) -> Result<ConnectToMapping, ValidationError> {
    let parts: Vec<&str> = s.split(':').collect();
    if parts.len() != 4 {
        return Err(ValidationError::InvalidConnectToFormat {
            value: s.to_owned(),
        });
    }
    let source_host = parts
        .first()
        .ok_or_else(|| ValidationError::InvalidConnectTo {
            value: s.to_owned(),
        })?
        .trim();
    let source_port: u16 = parts
        .get(1)
        .ok_or_else(|| ValidationError::InvalidConnectTo {
            value: s.to_owned(),
        })?
        .trim()
        .parse()
        .map_err(|err| ValidationError::InvalidConnectToPort {
            value: s.to_owned(),
            kind: ConnectToPortKind::Source,
            source: err,
        })?;
    let target_host = parts
        .get(2)
        .ok_or_else(|| ValidationError::InvalidConnectTo {
            value: s.to_owned(),
        })?
        .trim();
    let target_port: u16 = parts
        .get(3)
        .ok_or_else(|| ValidationError::InvalidConnectTo {
            value: s.to_owned(),
        })?
        .trim()
        .parse()
        .map_err(|err| ValidationError::InvalidConnectToPort {
            value: s.to_owned(),
            kind: ConnectToPortKind::Target,
            source: err,
        })?;
    if source_host.is_empty() || target_host.is_empty() {
        return Err(ValidationError::ConnectToHostEmpty {
            value: s.to_owned(),
        });
    }
    Ok(ConnectToMapping {
        source_host: source_host.to_owned(),
        source_port,
        target_host: target_host.to_owned(),
        target_port,
    })
}

pub(crate) fn parse_duration_arg(s: &str) -> AppResult<Duration> {
    let value = s.trim();
    if value.is_empty() {
        return Err(AppError::validation(ValidationError::DurationEmpty));
    }

    let mut digits_len = 0usize;
    for ch in value.chars() {
        if ch.is_ascii_digit() {
            digits_len = digits_len.saturating_add(1);
        } else {
            break;
        }
    }
    if digits_len == 0 {
        return Err(AppError::validation(
            ValidationError::InvalidDurationFormat {
                value: value.to_owned(),
            },
        ));
    }
    let (num_part, unit_part) = value.split_at(digits_len);
    let number: u64 = num_part.parse().map_err(|err| {
        AppError::validation(ValidationError::InvalidDurationNumber {
            value: value.to_owned(),
            source: err,
        })
    })?;

    let unit = if unit_part.is_empty() { "s" } else { unit_part };
    let duration = match unit {
        "ms" => Duration::from_millis(number),
        "s" => Duration::from_secs(number),
        "m" => {
            let secs = number
                .checked_mul(60)
                .ok_or_else(|| AppError::validation(ValidationError::DurationOverflow))?;
            Duration::from_secs(secs)
        }
        "h" => {
            let secs = number
                .checked_mul(60)
                .and_then(|seconds| seconds.checked_mul(60))
                .ok_or_else(|| AppError::validation(ValidationError::DurationOverflow))?;
            Duration::from_secs(secs)
        }
        _ => {
            return Err(AppError::validation(ValidationError::InvalidDurationUnit {
                unit: unit.to_owned(),
            }));
        }
    };

    if duration.as_millis() == 0 {
        return Err(AppError::validation(ValidationError::DurationZero));
    }

    Ok(duration)
}