kodumaro-http-cli 0.5.5

Kodumaro HTTP CLI inspired by HTTPie
Documentation
use std::io::Write;

use crossterm::style::{Color, Print, ResetColor, SetForegroundColor, SetStyle};
use eyre::Result;
use futures::StreamExt;
use indicatif::{ProgressBar, ProgressState, ProgressStyle};
use reqwest::{header, Body};
use reqwest::{redirect::Policy, Request, RequestBuilder};
use serde_json::Value;
use tokio::fs::File;
use tokio_util::io::ReaderStream;

use crate::buffer::Buffer;
use crate::cli::utils;
use crate::output::format_by_ext;
use crate::styles::*;

use super::http_request::HttpRequest;
use super::payload::Payload;

pub trait Connector {

    #[allow(async_fn_in_trait)]
    async fn start(&self) -> Result<()>;
}

impl Connector for HttpRequest {

    async fn start(&self) -> Result<()> {
        let request: Request = self.into();
        let content_type = request.headers()
            .get(header::CONTENT_TYPE)
            .and_then(|v| v.to_str().ok())
            .unwrap_or("application/octect-stream")
            .to_string();
        let policy: Policy = self.into();
        let client = reqwest::Client::builder()
            .danger_accept_invalid_certs(!self.verify)
            .redirect(policy)
            .build()?;

        let builder = RequestBuilder::from_parts(client.clone(), request);
        let mut stdout = std::io::stdout();
        let mut stderr = std::io::stderr();

        if self.verbose {
            if let Some(builder) = builder.try_clone() {
                let request = builder.build()?;

                if !self.verify {
                    crossterm::execute!(
                        stderr,
                        SetStyle(*STATUS_FAILURE_STYLE),
                        Print("Skipping SSL verification"),
                        SetStyle(*DEFAULT_STYLE),
                        Print("\n"),
                    )?;
                }

                crossterm::execute!(
                    stderr,
                    SetStyle(*METHOD_STYLE),
                    Print(request.method()),
                    Print(" "),
                    SetStyle(*DEFAULT_STYLE),
                    SetStyle(*URL_STYLE),
                    Print(request.url().to_string()),
                    SetStyle(*DEFAULT_STYLE),
                    Print("\n"),
                )?;
                for (name, value) in request.headers().iter() {
                    let value = value.to_str()?;
                    crossterm::execute!(
                        stderr,
                        SetStyle(*HEADER_NAME_STYLE),
                        Print(name),
                        Print(": "),
                        SetStyle(*DEFAULT_STYLE),
                        SetStyle(*HEADER_VALUE_STYLE),
                        Print(value),
                        SetStyle(*DEFAULT_STYLE),
                        Print("\n"),
                    )?;
                }

                eprintln!();
                match &self.payload {
                    Payload::Content(content) => {
                        format_by_ext(
                            &content,
                            utils::extension_from_mime(&content_type),
                            &mut stderr,
                        )?;
                        eprintln!();
                    }
                    Payload::FileUpload(filename) => {
                        crossterm::execute!(
                            stderr,
                            SetStyle(*DEFAULT_STYLE),
                            Print("Content from file: "),
                            SetStyle(*STATUS_OTHER_STYLE),
                            Print(filename),
                            SetStyle(*DEFAULT_STYLE),
                            Print("\n"),
                        )?;
                    }
                    Payload::NoContent => (),
                }
            }
        }
        if self.dry_run {
            if let Some(output) = &self.output {
                crossterm::execute!(
                    stderr,
                    SetStyle(*DEFAULT_STYLE),
                    Print("save output into "),
                    SetStyle(*HEADER_VALUE_STYLE),  /* XXX: create dedicated style */
                    Print(output),
                    SetStyle(*DEFAULT_STYLE),
                    Print("\n"),
                )?;
            }
            return Ok(());
        }

        let builder = match &self.payload {
            Payload::Content(content) if content_type.contains("json") => {
                let content: Value = serde_json::from_str(content)?;
              builder.json(&content)
            }
            Payload::Content(content) => builder.body(content.to_owned()),
            Payload::FileUpload(filename) => {
                let file = File::open(filename).await?;
                let stream = ReaderStream::new(file);
                builder.body(Body::wrap_stream(stream))
            }
            Payload::NoContent => builder,
        };

        let response = builder.send().await?;
        let status = response.status();

        if self.verbose {
            draw_line(&mut stderr)?;

            match status.as_u16() / 100 {
                2 => crossterm::execute!(
                    stderr,
                    SetStyle(*STATUS_SUCCESS_STYLE),
                    Print(status),
                    SetStyle(*DEFAULT_STYLE),
                    Print("\n"),
                )?,
                4|5 => crossterm::execute!(
                    stderr,
                    SetStyle(*STATUS_FAILURE_STYLE),
                    Print(status),
                    SetStyle(*DEFAULT_STYLE),
                    Print("\n"),
                )?,
                _ => crossterm::execute!(
                    stderr,
                    SetStyle(*STATUS_OTHER_STYLE),
                    Print(status),
                    SetStyle(*DEFAULT_STYLE),
                    Print("\n"),
                )?,
            }
            for (name, value) in response.headers().iter() {
                let value = value.to_str()?;
                crossterm::execute!(
                    stderr,
                    SetStyle(*HEADER_NAME_STYLE),
                    Print(name),
                    Print(": "),
                    SetStyle(*DEFAULT_STYLE),
                    SetStyle(*HEADER_VALUE_STYLE),
                    Print(value),
                    SetStyle(*DEFAULT_STYLE),
                    Print("\n"),
                )?;
            }
        }
        eprintln!();

        if self.fail {
            let code = status.as_u16();
            if (400..=599).contains(&code) {
                crossterm::execute!(
                    stderr,
                    SetStyle(*STATUS_FAILURE_STYLE),
                    Print(status),
                    SetStyle(*DEFAULT_STYLE),
                    Print("\n"),
                )?;

                let mut stream = response.bytes_stream();
                while let Some(item) = stream.next().await {
                    let chunk = item?;
                    let chunk = chunk.into_iter().collect::<Vec<u8>>();
                    stderr.write_all(chunk.as_slice())?;
                    stderr.flush()?;
                }
                stderr.write(&[10u8])?;
                std::process::exit(code as i32 / 100);
            }
        }

        let total_size: u64 = response.content_length().unwrap_or(0);
        let pb = ProgressBar::new(total_size);
        pb.set_style(
            ProgressStyle::with_template(
                "{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({eta})"
            )?.with_key(
                "eta",
                |state: &ProgressState, w: &mut dyn ::std::fmt::Write|
                    write!(w, "{:.1}s", state.eta().as_secs_f64()).unwrap()
            )
        );

        let content_type = response.headers()
            .get(reqwest::header::CONTENT_TYPE)
            .map(|value| value.to_str().unwrap_or_default())
            .unwrap_or("text/plain")
            .to_string();

        let mut out: Box<dyn Write> = match &self.output {
            Some(file) => Box::new(std::fs::File::create(file)?),
            None => Box::new(Buffer::new(
                &mut stdout,
                self.url.path().to_lowercase(),
                content_type,
            )),
        };

        let mut downloaded: u64 = 0;
        let mut stream = response.bytes_stream();

        while let Some(item) = stream.next().await {
            let chunk = item?;
            let chunk = chunk.into_iter().collect::<Vec<u8>>();
            out.write_all(chunk.as_slice())?;
            downloaded = total_size.min(downloaded + chunk.len() as u64);
            pb.set_position(downloaded);
        }
        out.flush()?;
        pb.finish_and_clear();

        Ok(())
    }
}

fn draw_line(writer: &mut impl Write) -> Result<()> {
    let width = match crossterm::terminal::size() {
        Ok((width, _)) => width,
        Err(_) => 80,
    };
    let line = "".repeat(width as usize);
    crossterm::execute!(
        writer,
        SetForegroundColor(Color::Black),
        Print(line),
        ResetColor,
    )?;
    Ok(())
}