dothttp 0.10.0

dothttp is a text-based scriptable HTTP client. It is a fork for dot-http. It is a simple language that resembles the actual HTTP protocol but with additional features to make it practical for someone who builds and tests APIs.
Documentation
pub mod print;

mod ci;
#[cfg(test)]
mod tests;

use std::fmt;

use color_eyre::eyre::anyhow;

pub use self::{ci::CiOutput, print::FormattedOutput};
use crate::{
    http::{Method, Request, Response},
    script_engine::report::TestsReport,
    Result,
};

#[derive(Debug, Eq, PartialEq, Clone)]
pub enum FormatItem {
    FirstLine,
    Headers,
    Body,
    Tests,
    Name,
    Chars(String),
}

pub fn parse_format(format: &str) -> Result<Vec<FormatItem>> {
    let mut result = Vec::new();
    let mut marker = false;
    let mut buff = String::new();
    for ch in format.chars() {
        if marker {
            marker = false;
            let action = match ch {
                '%' => None,
                'R' => Some(FormatItem::FirstLine),
                'H' => Some(FormatItem::Headers),
                'B' => Some(FormatItem::Body),
                'T' => Some(FormatItem::Tests),
                'N' => Some(FormatItem::Name),
                _ => return Err(anyhow!("Invalid formatting character '{}'", ch)),
            };
            if let Some(a) = action {
                if !buff.is_empty() {
                    result.push(FormatItem::Chars(buff));
                    buff = String::new();
                }
                result.push(a);
            } else {
                buff.push(ch);
            }
        } else if ch == '%' {
            marker = true;
        } else {
            buff.push(ch);
        }
    }
    if !buff.is_empty() {
        result.push(FormatItem::Chars(buff));
    }
    Ok(result)
}

fn prettify_response_body(body: &str) -> String {
    match serde_json::from_str(body) {
        Ok(serde_json::Value::Object(response_body)) => {
            serde_json::to_string_pretty(&response_body).unwrap()
        }
        _ => String::from(body),
    }
}

pub trait Output {
    fn response(&mut self, response: &Response, tests: &TestsReport) -> Result<()>;
    fn request(&mut self, request: &Request, request_name: &str) -> Result<()>;
    fn tests(&mut self, tests: Vec<(String, String, TestsReport)>) -> Result<()>;

    fn exit_code(&mut self) -> std::process::ExitCode {
        std::process::ExitCode::SUCCESS
    }
}

impl Output for Box<dyn Output> {
    fn response(&mut self, response: &Response, tests: &TestsReport) -> Result<()> {
        (**self).response(response, tests)
    }

    fn request(&mut self, request: &Request, request_name: &str) -> Result<()> {
        (**self).request(request, request_name)
    }

    fn tests(&mut self, tests: Vec<(String, String, TestsReport)>) -> Result<()> {
        (**self).tests(tests)
    }

    fn exit_code(&mut self) -> std::process::ExitCode {
        (**self).exit_code()
    }
}

impl fmt::Display for Method {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let method = match *self {
            Method::Get => "GET",
            Method::Post => "POST",
            Method::Delete => "DELETE",
            Method::Put => "PUT",
            Method::Patch => "PATCH",
            Method::Options => "OPTIONS",
        };
        f.write_str(method)
    }
}