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
use std::{fmt, io::Write, process::ExitCode};

use crate::{
    http,
    output::{prettify_response_body, FormatItem, Output},
    script_engine::report::{TestResult, TestsReport},
    Result,
};

pub struct FormattedOutput<W1: Write, W2: Write> {
    writer: W1,
    writer_err: W2,
    request_format: Vec<FormatItem>,
    response_format: Vec<FormatItem>,
    error: bool,
}

impl<W1: Write, W2: Write> FormattedOutput<W1, W2> {
    pub fn new(
        writer: W1,
        writer_err: W2,
        request_format: Vec<FormatItem>,
        response_format: Vec<FormatItem>,
    ) -> FormattedOutput<W1, W2> {
        FormattedOutput {
            writer,
            writer_err,
            request_format,
            response_format,
            error: false,
        }
    }

    pub fn into_writers(self) -> (W1, W2) {
        (self.writer, self.writer_err)
    }
}

fn format_headers(headers: &[(String, String)]) -> String {
    headers.iter().fold(String::new(), |mut acc, (key, value)| {
        use std::fmt::Write;
        writeln!(&mut acc, "{}: {}", key, value).unwrap();
        acc
    })
}

fn format_body(body: &Option<String>) -> String {
    match body {
        Some(body) => prettify_response_body(body),
        None => String::from(""),
    }
}

fn format_tests(report: &TestsReport) -> String {
    use std::fmt::Write;

    let mut output = String::new();
    if report.is_empty() {
        return output;
    }

    for (test, result) in report.all() {
        write!(&mut output, "Test `{test}`: ").unwrap();
        match result {
            TestResult::Error { error } => writeln!(&mut output, "FAILED with {error}").unwrap(),
            TestResult::Success => writeln!(&mut output, "OK").unwrap(),
        }
    }

    output
}

impl<W1: Write, W2: Write> Output for FormattedOutput<W1, W2> {
    fn response(&mut self, response: &http::Response, tests: &TestsReport) -> Result<()> {
        if self.response_format.is_empty() {
            return Ok(());
        }

        let http::Response {
            headers,
            version,
            status,
            body,
            ..
        } = response;

        for format_item in &self.response_format {
            let to_write = match format_item {
                FormatItem::FirstLine => format!("{} {}", version, status),
                FormatItem::Headers => format_headers(headers),
                FormatItem::Body => format_body(body),
                FormatItem::Chars(s) => s.clone(),
                FormatItem::Tests => format_tests(tests),
                FormatItem::Name => continue,
            };

            if to_write.is_empty() {
                continue;
            }

            write!(self.writer, "{to_write}")?;
        }

        self.error = self.error || tests.failed().next().is_some();

        Ok(())
    }

    fn request(&mut self, request: &http::Request, request_name: &str) -> Result<()> {
        if self.request_format.is_empty() {
            return Ok(());
        }
        let http::Request {
            method,
            target,
            headers,
            body,
            ..
        } = request;

        for format_item in &self.request_format {
            let to_write = match format_item {
                FormatItem::FirstLine => format!("{} {}", method, target),
                FormatItem::Headers => format_headers(headers),
                FormatItem::Body => format_body(body),
                FormatItem::Chars(s) => s.clone(),
                FormatItem::Tests => continue,
                FormatItem::Name => format!("[{request_name}]"),
            };

            if to_write.is_empty() {
                continue;
            }

            write!(self.writer, "{to_write}")?;
        }
        Ok(())
    }

    fn tests(&mut self, tests: Vec<(String, String, TestsReport)>) -> Result<()> {
        if !self.error {
            return Ok(());
        }

        let mut index = 1;

        writeln!(self.writer_err, "RUN FAILED")?;

        for (file, name, tests) in tests {
            for (test, result) in tests.failed() {
                let TestResult::Error { error } = result else {
                    panic!("tests.failed() should return only failed tests")
                };
                writeln!(
                    self.writer_err,
                    "{index}. Test `{test}` in `[{file} / {name}]` FAILED with {error}"
                )?;
                index += 1;
            }
        }

        Ok(())
    }

    fn exit_code(&mut self) -> ExitCode {
        if self.error {
            ExitCode::FAILURE
        } else {
            ExitCode::SUCCESS
        }
    }
}

impl fmt::Display for http::Version {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            http::Version::Http09 => write!(f, "HTTP/0.9"),
            http::Version::Http10 => write!(f, "HTTP/1.0"),
            http::Version::Http11 => write!(f, "HTTP/1.1"),
            http::Version::Http2 => write!(f, "HTTP/2"),
            http::Version::Http3 => write!(f, "HTTP/3"),
        }
    }
}