xs-http 0.1.0

Instant HTTP API testing suite - type-safe alternative to Postman/curl
Documentation
use crate::display;
use dialoguer::{theme::ColorfulTheme, Input, Select};
use x_http::error::Result;
use x_http::{Method, Request};

pub struct InteractiveSession;

impl InteractiveSession {
    pub fn run() -> Result<()> {
        println!("x-http Interactive Mode");
        println!("Press Ctrl+C to exit\n");

        loop {
            match Self::prompt_and_execute() {
                Ok(true) => {}
                Ok(false) => break,
                Err(e) => {
                    eprintln!("Error: {}", e);
                }
            }
        }

        Ok(())
    }

    fn prompt_and_execute() -> Result<bool> {
        let method = Self::prompt_method()?;
        let url = Self::prompt_url()?;
        let headers = Self::prompt_headers()?;
        let body = if matches!(method, Method::Post | Method::Put | Method::Patch) {
            Self::prompt_body()?
        } else {
            None
        };

        let mut request = Request::new(method, url);

        for (key, value) in headers {
            request = request.header(key, value);
        }

        if let Some((body_str, is_json)) = body {
            if is_json {
                let json_value: serde_json::Value = serde_json::from_str(&body_str)?;
                request = request.json(&json_value)?;
            } else {
                request = request.text(body_str);
            }
        }

        println!("\n⏳ Sending request...\n");

        let response = request.send()?;
        display::display_response(&response)?;

        let continue_prompt: bool = dialoguer::Confirm::new()
            .with_prompt("\nMake another request?")
            .default(true)
            .interact()?;

        Ok(continue_prompt)
    }

    fn prompt_method() -> Result<Method> {
        let methods = vec!["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"];
        let selection = Select::with_theme(&ColorfulTheme::default())
            .with_prompt("Select HTTP method")
            .items(&methods)
            .default(0)
            .interact()?;

        Ok(match selection {
            0 => Method::Get,
            1 => Method::Post,
            2 => Method::Put,
            3 => Method::Delete,
            4 => Method::Patch,
            5 => Method::Head,
            6 => Method::Options,
            _ => Method::Get,
        })
    }

    fn prompt_url() -> Result<String> {
        let url: String = Input::with_theme(&ColorfulTheme::default())
            .with_prompt("URL")
            .interact_text()?;
        Ok(url)
    }

    fn prompt_headers() -> Result<Vec<(String, String)>> {
        let mut headers = Vec::new();

        loop {
            let header: String = Input::with_theme(&ColorfulTheme::default())
                .with_prompt("Header (key:value, or press Enter to skip)")
                .allow_empty(true)
                .interact_text()?;

            if header.is_empty() {
                break;
            }

            if let Some((key, value)) = header.split_once(':') {
                headers.push((key.trim().to_string(), value.trim().to_string()));
            } else {
                eprintln!("Invalid header format. Use key:value");
            }
        }

        Ok(headers)
    }

    fn prompt_body() -> Result<Option<(String, bool)>> {
        let has_body: bool = dialoguer::Confirm::new()
            .with_prompt("Include request body?")
            .default(false)
            .interact()?;

        if !has_body {
            return Ok(None);
        }

        let is_json: bool = dialoguer::Confirm::new()
            .with_prompt("Is the body JSON?")
            .default(true)
            .interact()?;

        let body: String = Input::with_theme(&ColorfulTheme::default())
            .with_prompt(if is_json { "JSON body" } else { "Body" })
            .interact_text()?;

        Ok(Some((body, is_json)))
    }
}