http-diff 0.0.5

http-diff - CLI tool to verify consistency across web server versions. Ideal for large-scale refactors, sanity tests and maintaining data integrity across versions.
use super::super::utils::flatten_variables_map;
use crate::actions::AppAction;
use anyhow::Result;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::{broadcast, Semaphore};
use uuid::Uuid;

use super::super::config::Configuration;
use super::super::config::{DomainVariant, EndpointConfiguration};
use super::super::request::Request;
use super::super::types::{
    AppError, HeadersMap, HttpMethod, PathVariableValue,
    PlaceholderToValueMap, VariableGenerator, VariablesMap,
};
use super::super::utils::{
    get_placeholders_from_string, replace_placeholder_with_value,
};
use super::job::Job;

pub fn map_configuration_to_jobs(
    configuration: &Configuration,
    app_actions_sender: broadcast::Sender<AppAction>,
    requests_semaphore: Arc<Semaphore>,
    threads_semaphore: Arc<Semaphore>,
) -> Result<Vec<Job>, AppError> {
    let mut endpoints: Vec<Job> = Vec::new();

    for endpoint_config in &configuration.endpoints {
        let placeholders =
            get_placeholders_from_string(&endpoint_config.endpoint);

        let mut endpoint_variable_lookup: VariablesMap = HashMap::new();

        if let Some(global_optional_variables) = &configuration.variables {
            endpoint_variable_lookup.extend(global_optional_variables.clone());
        }

        if let Some(endpoint_optional_variables) = &endpoint_config.variables {
            endpoint_variable_lookup
                .extend(endpoint_optional_variables.clone());
        }

        let endpoint_placeholders_with_variables: Vec<&String> = placeholders
            .iter()
            .filter(|&placeholder| {
                endpoint_variable_lookup.contains_key(placeholder)
            })
            .collect();

        endpoint_variable_lookup.retain(|key, _| placeholders.contains(key));

        if endpoint_placeholders_with_variables.is_empty() {
            let new_job = map_job_with_no_variables(
                &configuration.domains,
                endpoint_config,
                app_actions_sender.clone(),
                requests_semaphore.clone(),
                threads_semaphore.clone(),
            )?;

            endpoints.push(new_job);
        } else {
            let variable_map_combinations =
                flatten_variables_map(endpoint_variable_lookup);

            for variables_combination in variable_map_combinations {
                let new_job = map_job_with_variables(
                    &variables_combination,
                    &endpoint_placeholders_with_variables,
                    &configuration.domains,
                    endpoint_config,
                    app_actions_sender.clone(),
                    requests_semaphore.clone(),
                    threads_semaphore.clone(),
                )?;

                endpoints.push(new_job);
            }
        }
    }

    Ok(endpoints)
}

fn map_job_with_no_variables(
    domains: &Vec<DomainVariant>,
    endpoint_config: &EndpointConfiguration,
    app_actions_sender: broadcast::Sender<AppAction>,
    requests_semaphore: Arc<Semaphore>,
    threads_semaphore: Arc<Semaphore>,
) -> Result<Job, AppError> {
    let mut jobs: Vec<Request> = Vec::new();

    for domain_variant in domains {
        let (domain, domain_headers) = match domain_variant {
            DomainVariant::Url(domain) => (domain.clone(), None),
            DomainVariant::UrlWithHeaders(domain_config) => {
                (domain_config.domain.clone(), domain_config.headers.clone())
            }
        };

        let uri = domain.join(&endpoint_config.endpoint).map_err(|_| {
            let error_message = format!(
                "{} with {}",
                domain.to_string(),
                &endpoint_config.endpoint
            );
            AppError::FailedToParseConfig(error_message)
        })?;

        let headers_mapped = build_endpoint_headers(
            domain_headers,
            endpoint_config.headers.clone(),
        );

        let http_method = endpoint_config
            .http_method
            .clone()
            .unwrap_or_else(|| HttpMethod::GET);

        let new_job = Request::new(
            &uri,
            &http_method,
            headers_mapped,
            endpoint_config.body.clone(),
        );

        jobs.push(new_job);
    }

    Ok(Job::new(
        jobs,
        &endpoint_config.endpoint,
        app_actions_sender,
        requests_semaphore,
        threads_semaphore,
        &endpoint_config.response_processor,
        &endpoint_config.request_builder,
    ))
}

fn map_job_with_variables(
    variables_combination: &PlaceholderToValueMap,
    endpoint_placeholders_with_variables: &Vec<&String>,
    domains: &Vec<DomainVariant>,
    endpoint_config: &EndpointConfiguration,
    app_actions_sender: broadcast::Sender<AppAction>,
    requests_semaphore: Arc<Semaphore>,
    threads_semaphore: Arc<Semaphore>,
) -> Result<Job, AppError> {
    let mut jobs: Vec<Request> = Vec::new();

    let mut formatted_string = endpoint_config.endpoint.clone();

    for placeholder in endpoint_placeholders_with_variables {
        let value = variables_combination.get(*placeholder);
        if value.is_none() {
            continue;
        }

        let value_for_replacement = match value.unwrap() {
            PathVariableValue::Generator(generator_type) => {
                match generator_type {
                    VariableGenerator::UUID => Uuid::new_v4().to_string(),
                }
            }
            PathVariableValue::String(string_value) => string_value.clone(),
            PathVariableValue::Int(int_value) => int_value.to_string(),
        };

        formatted_string = replace_placeholder_with_value(
            &formatted_string,
            placeholder,
            &value_for_replacement,
        );
    }
    for domain_variant in domains {
        let (domain, domain_headers) = match domain_variant {
            DomainVariant::Url(domain) => (domain.clone(), None),
            DomainVariant::UrlWithHeaders(domain_config) => {
                (domain_config.domain.clone(), domain_config.headers.clone())
            }
        };

        let uri = domain.join(&formatted_string).map_err(|_| {
            let error_message =
                format!("{} with {}", domain.to_string(), &formatted_string);
            AppError::FailedToParseConfig(error_message)
        })?;

        let headers_mapped = build_endpoint_headers(
            domain_headers,
            endpoint_config.headers.clone(),
        );

        let http_method = endpoint_config
            .http_method
            .clone()
            .unwrap_or_else(|| HttpMethod::GET);

        let new_job = Request::new(
            &uri,
            &http_method,
            headers_mapped,
            endpoint_config.body.clone(),
        );

        jobs.push(new_job);
    }
    Ok(Job::new(
        jobs,
        &formatted_string,
        app_actions_sender.clone(),
        requests_semaphore.clone(),
        threads_semaphore.clone(),
        &endpoint_config.response_processor,
        &endpoint_config.request_builder,
    ))
}

fn build_endpoint_headers(
    domain: Option<HeadersMap>,
    endpoint: Option<HeadersMap>,
) -> Option<HeadersMap> {
    let headers_mapped = match (domain, endpoint) {
        (Some(mut domain_headers), Some(endpoint_headers)) => {
            domain_headers.extend(endpoint_headers);

            Some(domain_headers)
        }
        (None, Some(endpoint_headers)) => Some(endpoint_headers),
        (Some(domain_headers), None) => Some(domain_headers),
        (None, None) => None,
    };

    headers_mapped
}