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::{fs, path::Path};

use color_eyre::eyre::Context;

use crate::{
    parser::{parse, File, RequestScript},
    Result,
};

#[derive(Debug, Copy, Clone)]
pub struct SourceItem<'a> {
    pub name: &'a str,
    pub index: usize,
    pub script: &'a RequestScript,
}

impl SourceItem<'_> {
    pub fn source_name(&self) -> &str {
        self.name
    }

    pub fn request_name(&self) -> String {
        if let Some(name) = &self.script.name {
            name.clone()
        } else {
            format!("#{}", self.index + 1)
        }
    }
}

pub trait SourceProvider {
    fn requests(&mut self) -> impl Iterator<Item = SourceItem>;
}

pub struct FileSourceProvider {
    file: File,
    name: String,
    request_index: Option<usize>,
}

impl FileSourceProvider {
    pub fn new(file: impl AsRef<Path>, request_index: Option<usize>) -> Result<Self> {
        let name = file.as_ref().display().to_string();

        let file_contents = fs::read_to_string(&file)
            .with_context(|| format!("Failed opening script file: `{}`", name))?;

        let file = parse(file.as_ref().to_path_buf(), file_contents.as_str())
            .with_context(|| format!("Failed parsing file: `{}`", name))?;

        Ok(Self {
            file,
            name,
            request_index,
        })
    }
}

impl SourceProvider for FileSourceProvider {
    fn requests(&mut self) -> impl Iterator<Item = SourceItem> {
        self.file
            .request_scripts(self.request_index)
            .map(|(index, script)| SourceItem {
                name: &self.name,
                index,
                script,
            })
    }
}

pub struct FilesSourceProvider(Vec<FileSourceProvider>);

impl FilesSourceProvider {
    pub fn from_list<T>(files: impl IntoIterator<Item = T>) -> Result<Self>
    where
        T: AsRef<str>,
    {
        let mut inner = vec![];
        for file in files {
            let (path, index) = if file.as_ref().contains('#') {
                file.as_ref().split_once('#').unwrap()
            } else {
                (file.as_ref(), "")
            };
            let request = index.parse().ok();
            inner.push(FileSourceProvider::new(path, request)?);
        }

        Ok(Self(inner))
    }
}

impl SourceProvider for FilesSourceProvider {
    fn requests(&mut self) -> impl Iterator<Item = SourceItem> {
        self.0.iter_mut().flat_map(|it| it.requests())
    }
}