rested 0.11.0

Language/Interpreter for easily defining and running requests to an http server.
Documentation
pub use crate::parser::ast::RequestMethod;

use crate::{
    error::ColoredMetaError,
    error_meta::ToContextualError,
    interpreter::{
        ir::{self, *},
        ureq_runner::UreqRun,
    },
};
use string_utils::*;

use std::error::Error;

use tracing::{error, info};

#[derive(Debug)]
pub enum RunResponse {
    Success(String),
    Failure(String),
}

impl<'source> ir::Program<'source> {
    pub fn run_ureq(
        self,
        request_names: Option<&[String]>,
    ) -> Vec<(request_id::RequestId, RunResponse)> {
        Runner::new(self, Box::new(UreqRun)).run(request_names)
    }
}

use colored::Colorize;
pub trait RunStrategy {
    fn run_request(&mut self, request: &Request) -> std::result::Result<String, Box<dyn Error>>;
}

struct Runner<'source> {
    program: ir::Program<'source>,
    strategy: Box<dyn RunStrategy>,
}

impl<'source> Runner<'source> {
    pub fn new(program: ir::Program<'source>, strategy: Box<dyn RunStrategy>) -> Self {
        Self { program, strategy }
    }

    pub fn run(
        &mut self,
        request_names: Option<&[String]>,
    ) -> Vec<(request_id::RequestId, RunResponse)> {
        let requests = self.program.items.iter().filter(|r| {
            match (&request_names, r.name.as_deref().unwrap_or(&r.request.url)) {
                (None, _) => true,
                (Some(desired), name) => desired.iter().any(|n| n == name),
            }
        });

        let mut responses = Vec::with_capacity(request_names.map(|names| names.len()).unwrap_or(2));

        for item in requests {
            let request_id = request_id::RequestId::from(item);
            let RequestItem {
                span,
                request,
                dbg,
                log_destination,
                ..
            } = item;

            info!(
                "sending {} request to {}",
                request.method.to_string().yellow().bold(),
                request.url.bold()
            );

            if *dbg {
                eprintln!("{}", &format!("{:#?}", request));
            }

            let res = match self.strategy.run_request(request) {
                Ok(res) => res,
                Err(error) => {
                    let err = &error::RunError(error.to_string())
                        .to_contextual_error(*span, self.program.source);
                    let err = ColoredMetaError(err);
                    error!("{err:#}");
                    responses.push((request_id, RunResponse::Failure(format!("{err:#}"))));
                    continue;
                }
            };

            if let Some(log_destination) = log_destination {
                match log_destination {
                    LogDestination::File(file_path) => match log(&res, file_path) {
                        Ok(_) => {
                            info!("{}", format!("saved response to {:?}", file_path).blue());
                        }
                        Err(error) => {
                            error!(
                                "{:#}",
                                ColoredMetaError(
                                    &error::RunError(error.to_string())
                                        .to_contextual_error(*span, self.program.source)
                                )
                            )
                        }
                    },
                }
            }

            println!("{res}");

            responses.push((request_id, RunResponse::Success(res)));
        }

        return responses;
    }
}

mod error {
    use std::error::Error;

    use crate::error_meta::ToContextualError;

    #[derive(Debug, Clone)]
    pub struct RunError(pub String);

    impl Error for RunError {}
    impl std::fmt::Display for RunError {
        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
            self.0.fmt(f)
        }
    }

    impl ToContextualError for RunError {}
}

mod string_utils {
    use std::{
        fs,
        io::{self, Write},
    };

    pub fn log(content: &str, to_file: &std::path::PathBuf) -> std::io::Result<()> {
        if let Some(dir_path) = to_file.parent() {
            fs::create_dir_all(dir_path)?
        };

        let file = fs::File::options()
            .truncate(true)
            .write(true)
            .create(true)
            .open(to_file)?;

        let mut w = io::BufWriter::new(file);

        write!(w, "{content}")
    }
}

pub mod request_id {
    use std::str::FromStr;

    use anyhow::Context;

    use crate::interpreter::ir;

    #[derive(Debug)]
    pub struct RequestId {
        pub method: String,
        pub url_or_name: String,
    }

    impl From<&ir::RequestItem> for RequestId {
        fn from(r: &ir::RequestItem) -> Self {
            let (m, n) = match r.name.clone() {
                Some(name) => (r.request.method.to_string(), name),
                None => (r.request.method.to_string(), r.request.url.clone()),
            };

            return RequestId {
                method: m,
                url_or_name: n,
            };
        }
    }

    impl FromStr for RequestId {
        type Err = anyhow::Error;

        fn from_str(s: &str) -> Result<Self, Self::Err> {
            let mut split = s.split("::");

            let m = split
                .next()
                .context("can't get a prompt entry from an empty string")?;
            let n = split
                .next()
                .context("failed to get url or name from string")?;

            return Ok(RequestId {
                method: m.to_owned(),
                url_or_name: n.to_owned(),
            });
        }
    }

    impl RequestId {
        pub fn as_string(&self) -> String {
            return format!("{}::{}", self.method, self.url_or_name);
        }
    }
}