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::collections::BTreeMap;
use std::time::{SystemTime, UNIX_EPOCH};

use reqwest::Url;

use crate::{
    args::{Scenario, ScenarioStep},
    error::{AppError, AppResult, HttpError},
};

pub(super) fn resolve_step_url(
    scenario: &Scenario,
    step: &ScenarioStep,
    vars: &BTreeMap<String, String>,
) -> AppResult<Url> {
    if let Some(url) = step.url.as_ref() {
        let rendered = render_template(url, vars);
        return Url::parse(&rendered).map_err(|err| {
            AppError::http(HttpError::InvalidScenarioUrl {
                url: rendered,
                source: err,
            })
        });
    }

    let path = step
        .path
        .as_ref()
        .ok_or_else(|| AppError::http(HttpError::ScenarioStepMissingUrlOrPath))?;
    let base_url = scenario
        .base_url
        .as_ref()
        .ok_or_else(|| AppError::http(HttpError::ScenarioBaseUrlRequired))?;
    let rendered_path = render_template(path, vars);
    let base = Url::parse(base_url).map_err(|err| {
        AppError::http(HttpError::InvalidScenarioBaseUrl {
            url: base_url.to_owned(),
            source: err,
        })
    })?;
    let joined = base.join(&rendered_path).map_err(|err| {
        AppError::http(HttpError::JoinUrlFailed {
            url: rendered_path,
            source: err,
        })
    })?;
    Ok(joined)
}

pub(super) fn build_template_vars(
    scenario: &Scenario,
    step: &ScenarioStep,
    seq: u64,
    step_index: usize,
) -> BTreeMap<String, String> {
    let mut vars = scenario.vars.clone();
    for (key, value) in &step.vars {
        vars.insert(key.clone(), value.clone());
    }

    let now_ms = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map_or(0, |duration| duration.as_millis());
    vars.insert("seq".to_owned(), seq.to_string());
    vars.insert("step".to_owned(), step_index.saturating_add(1).to_string());
    vars.insert("timestamp_ms".to_owned(), now_ms.to_string());
    vars.insert("timestamp_s".to_owned(), (now_ms / 1000).to_string());

    vars
}

pub(crate) fn render_template(input: &str, vars: &BTreeMap<String, String>) -> String {
    let mut rest = input;
    let mut output = String::with_capacity(input.len());

    loop {
        let start = match rest.find("{{") {
            Some(start) => start,
            None => {
                output.push_str(rest);
                break;
            }
        };
        let (before, after_start) = rest.split_at(start);
        output.push_str(before);
        let after = match after_start.strip_prefix("{{") {
            Some(after) => after,
            None => {
                output.push_str(after_start);
                break;
            }
        };
        let end = match after.find("}}") {
            Some(end) => end,
            None => {
                output.push_str("{{");
                output.push_str(after);
                break;
            }
        };
        let (key_part, after_end) = after.split_at(end);
        let key = key_part.trim();
        if let Some(value) = vars.get(key) {
            output.push_str(value);
        } else {
            output.push_str("{{");
            output.push_str(key);
            output.push_str("}}");
        }
        rest = match after_end.strip_prefix("}}") {
            Some(remaining) => remaining,
            None => {
                output.push_str(after_end);
                break;
            }
        };
    }

    output
}

pub(super) fn step_label(step: &ScenarioStep, step_index: usize) -> String {
    step.name
        .clone()
        .unwrap_or_else(|| format!("step {}", step_index.saturating_add(1)))
}