mod error;
mod impl_check;
use crate::error::GoldrustError;
use derive_more::Display;
use serde::{Deserialize, Serialize};
use std::fmt::Formatter;
use std::fs::OpenOptions;
use std::path::{Path, PathBuf};
assert_impl_commons_without_default!(ResponseSource);
#[macro_export]
macro_rules! goldrust {
($file_extension:expr) => {
Goldrust::new(
{
fn f() {}
fn type_name_of_val<T>(_: T) -> &'static str {
std::any::type_name::<T>()
}
let mut name = type_name_of_val(f).strip_suffix("::f").unwrap_or("");
while let Some(rest) = name.strip_suffix("::{{closure}}") {
name = rest;
}
&name.replace("::", "-")
},
String::from($file_extension),
)
};
($file_extension:expr, $function_id:expr) => {
Goldrust::new($function_id, String::from($file_extension))
};
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Serialize, Deserialize, Display)]
#[display("{update_golden_files}, {golden_file_path:?}, {response_source}, {save_check}")]
pub struct Goldrust {
update_golden_files: bool,
pub golden_file_path: PathBuf,
pub file_extension: String,
pub response_source: ResponseSource,
pub save_check: bool,
}
assert_impl_commons_without_default!(Goldrust);
impl Goldrust {
#[tracing::instrument]
pub fn new(function_name: &str, file_extension: String) -> Self {
let golden_file_dir =
std::env::var("GOLDRUST_DIR").unwrap_or("tests/resources/golden".to_string());
let golden_file_path =
Path::new(&golden_file_dir).join(format!("{}.{}", function_name, file_extension));
let allow_external_api_call: bool = std::env::var("GOLDRUST_ALLOW_EXTERNAL_API_CALL")
.unwrap_or("false".to_string())
.parse()
.expect("GOLDRUST_ALLOW_EXTERNAL_API_CALL must be parseable as a boolean");
let update_golden_files: bool = std::env::var("GOLDRUST_UPDATE_GOLDEN_FILES")
.unwrap_or("false".to_string())
.parse()
.expect("GOLDRUST_UPDATE_GOLDEN_FILES must be a boolean");
let save_check = !update_golden_files;
let response_source = response_source(
allow_external_api_call,
update_golden_files,
golden_file_path.as_ref(),
);
Self {
update_golden_files,
golden_file_path,
file_extension,
response_source,
save_check,
}
}
#[tracing::instrument(skip(self, content))]
pub fn save(&mut self, content: Content) -> Result<(), GoldrustError> {
self.save_check = true;
if !self.update_golden_files {
tracing::debug!("Golden files should not be updated, skipping save");
return Ok(());
}
match content {
Content::Json(content) => {
let file = OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(&self.golden_file_path)
.inspect_err(
|_e| tracing::error!(?self.golden_file_path, "Error opening file"),
)?;
let file_fmt = format!("{:?}", self.golden_file_path);
serde_json::to_writer_pretty(file, &content).inspect_err(|_e| {
tracing::error!(file = file_fmt, "Error writing content to file")
})?;
}
#[cfg(feature = "image")]
Content::Image(content) => {
content.save(&self.golden_file_path)?;
}
};
tracing::debug!(?self.golden_file_path, "Saved content to golden file");
Ok(())
}
}
#[tracing::instrument]
fn response_source(
allow_external_api_call: bool,
update_golden_files: bool,
golden_file_path: &Path,
) -> ResponseSource {
let golden_file_exists = golden_file_path.exists();
let response_source: ResponseSource = match (
allow_external_api_call,
update_golden_files,
golden_file_exists,
) {
(false, true, _) => {
panic!("Cannot update golden files without allowing external API calls")
}
(false, false, false) => {
panic!("Cannot test without allowing external API calls when golden files do not exist, create file: {}", golden_file_path.display())
}
(false, false, true) => {
tracing::debug!("Use local golden files without making external API calls");
ResponseSource::Local
}
(true, false, false) => {
tracing::debug!("Use external API without updating golden files");
ResponseSource::External
}
(true, false, true) => {
tracing::debug!("Use local golden files without making external API calls, even though external API calls are allowed");
ResponseSource::Local
}
(true, true, _) => {
tracing::debug!("Use external API calls and update golden files");
ResponseSource::External
}
};
response_source
}
impl Drop for Goldrust {
fn drop(&mut self) {
if !self.save_check {
tracing::error!("Should save item to golden file.\nEven if you've called the `save` methods, it might not be executing due to prior early returns, etc.");
}
}
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Debug, Serialize, Deserialize, Display)]
#[display("{_variant}")]
pub enum ResponseSource {
Local,
External,
}
#[derive(Clone, PartialEq, Debug)]
pub enum Content {
Json(serde_json::Value),
#[cfg(feature = "image")]
Image(image::DynamicImage),
}
impl std::fmt::Display for Content {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Content::Json(_) => write!(f, "Json"),
#[cfg(feature = "image")]
Content::Image(_) => write!(f, "Image"),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn display_goldrust() {
let goldrust = goldrust!("json");
assert_eq!(
format!("{}", goldrust),
format!(
"{}, {:?}, {}, {}",
goldrust.update_golden_files,
goldrust.golden_file_path,
goldrust.response_source,
goldrust.save_check
)
);
}
}