hen 0.8.1

Run API collections from the command line.
use http::Method;
use reqwest::header::HeaderMap;
use std::{collections::HashMap, path::PathBuf};

use crate::parser::{self, context};

#[derive(Debug, Clone)]
pub enum FormDataType {
    Text(String),
    File(PathBuf),
}

#[derive(Debug, Clone)]
pub struct Request {
    pub description: String,
    pub method: Method,
    pub url: String,
    pub headers: HashMap<String, String>,
    pub query_params: HashMap<String, String>,
    pub form_data: HashMap<String, FormDataType>,
    pub body: Option<String>,
    pub body_content_type: Option<String>,
    pub callback_src: Vec<String>,
    pub working_dir: PathBuf,
}

impl Request {
    // builds a reqwest request then sends it.
    pub async fn exec(&self) -> Result<String, Box<dyn std::error::Error>> {
        // assemble the request using reqwest
        let client = reqwest::Client::new();

        let mut request =
            client.request(self.method.clone(), context::inject_from_prompt(&self.url));

        // assemble a HeaderMap
        let mut header_map = HeaderMap::new();

        // if self.body_content_type is set, add it to the headers
        if let Some(content_type) = &self.body_content_type {
            header_map.insert(
                reqwest::header::CONTENT_TYPE,
                context::inject_from_prompt(content_type)
                    .parse::<reqwest::header::HeaderValue>()
                    .unwrap(),
            );
        }

        for (key, value) in &self.headers {
            header_map.insert(
                key.parse::<reqwest::header::HeaderName>()
                    .expect(format!("Invalid header name: {}", key).as_str()),
                context::inject_from_prompt(value)
                    .parse::<reqwest::header::HeaderValue>()
                    .unwrap(),
            );
        }

        // assemble multipart form data
        let mut form = reqwest::multipart::Form::new();
        for (key, value) in &self.form_data {
            // form = form.text(key.clone(), value.clone());
            match value {
                FormDataType::Text(text) => {
                    form = form.text(key.clone(), context::inject_from_prompt(text));
                }
                FormDataType::File(filepath) => {
                    let file = reqwest::multipart::Part::bytes(std::fs::read(filepath)?)
                        .file_name(filepath.file_name().unwrap().to_str().unwrap().to_string());
                    form = form.part(key.clone(), file);
                }
            }
        }

        // inject from prompt for query params
        let mut query_params: HashMap<String, String> = HashMap::new();
        self.query_params.iter().for_each(|(key, value)| {
            query_params.insert(key.clone(), context::inject_from_prompt(value));
        });

        request = match self.form_data.len() > 0 {
            true => request
                .headers(header_map)
                .query(&query_params)
                .multipart(form),
            false => request.headers(header_map).query(&query_params),
        };

        if let Some(body) = &self.body {
            request = request.body(context::inject_from_prompt(body));
        }

        log::debug!("REQUEST\n{:#?}", request);

        let resp = request.send().await?;

        log::debug!("RESPONSE\n{:#?}", resp);

        let status_code = resp.status().as_str().to_string();
        let resp_text = resp.text().await.unwrap();

        // execute the callback if it is set
        let mut callback_resp: Vec<String> = vec![];

        if self.callback_src.len() > 0 {
            // create a hash map with the response data as RESPONSE
            // status code as CODE
            let mut env: HashMap<String, String> = HashMap::new();
            let sanitized_response = resp_text.replace('\0', "");
            env.insert("RESPONSE".to_string(), sanitized_response);
            env.insert("STATUS".to_string(), status_code);
            env.insert("DESCRIPTION".to_string(), self.description.clone());

            // for each source in the callback_src, evaluate it
            for src in &self.callback_src {
                callback_resp.push(parser::eval_shell_script(
                    src,
                    &self.working_dir,
                    Some(env.clone()),
                ));
            }

            log::debug!("CALLBACK RESPONSE\n{:#?}", callback_resp);
        }

        // return the callback text if it is set, else, return the response text
        if callback_resp.len() > 0 {
            Ok(callback_resp.join("\n"))
        } else {
            Ok(resp_text)
        }
    }

    pub fn as_curl(&self) -> String {
        let mut curl = String::new();

        // collect the headers
        let headers = self
            .headers
            .iter()
            .map(|(key, value)| format!("-H \'{}: {}\'", key, value))
            .collect::<Vec<String>>()
            .join(" ");

        // if self.body_content_type is set, add it as a header
        let headers = match &self.body_content_type {
            Some(content_type) => format!("-H \'Content-Type: {}\' {}", content_type, headers),
            None => headers,
        };

        // collect the query params
        let query_params = self
            .query_params
            .iter()
            .map(|(key, value)| format!("{}={}", key, value))
            .collect::<Vec<String>>()
            .join("&");

        // append the '?' to the query params if it is not empty
        let query_params = match query_params.len() {
            0 => "".to_string(),
            _ => format!("?{}", query_params),
        };

        // collect the form data
        let form_data = self
            .form_data
            .iter()
            .map(|(key, value)| match value {
                FormDataType::Text(text) => format!("-F \'{}={}\'", key, text),
                FormDataType::File(filepath) => format!("-F \'{}=@{}\'", key, filepath.display()),
            })
            .collect::<Vec<String>>()
            .join(" ");

        // collect the body
        let body = match &self.body {
            Some(body) => format!("-d \' {}\'", body),
            None => "".to_string(),
        };

        // assemble the curl command
        curl.push_str(&format!(
            "curl -X {} \'{}{}\' {} {} {}",
            self.method, self.url, query_params, headers, form_data, body
        ));
        return curl;
    }
}