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

use color_eyre::eyre::{anyhow, Context};
use serde_json::Value;

pub trait EnvironmentProvider {
    fn snapshot(&self) -> Value;
    fn save(&mut self, snapshot: &Value) -> io::Result<()>;
}

#[derive(Debug, Clone)]
pub struct StaticEnvironmentProvider {
    env: Value,
    snapshot: Option<Value>,
}

impl StaticEnvironmentProvider {
    pub fn new(env: Value) -> Self {
        Self {
            env,
            snapshot: None,
        }
    }

    pub fn snapshot(&self) -> &serde_json::Map<String, Value> {
        self.snapshot
            .as_ref()
            .expect("snapshot to be set")
            .as_object()
            .expect("snapshot to be object")
    }
}

impl EnvironmentProvider for StaticEnvironmentProvider {
    fn snapshot(&self) -> Value {
        self.env.clone()
    }

    fn save(&mut self, snapshot: &Value) -> io::Result<()> {
        self.snapshot = Some(snapshot.clone());
        Ok(())
    }
}

#[derive(Debug, Clone)]
pub struct EnvironmentFileProvider {
    snapshot: serde_json::Map<String, Value>,
    snapshot_path: PathBuf,
}

impl EnvironmentFileProvider {
    pub fn open(
        environment_name: &str,
        environment_path: impl AsRef<Path>,
        snapshot_path: impl AsRef<Path>,
    ) -> crate::Result<Self> {
        let Value::Object(mut environment) =
            read_json_content(environment_path.as_ref()).context("environment deserialization")?
        else {
            return Err(anyhow!("Expected environment file to be a map"));
        };

        let Value::Object(mut environment) = environment
            .remove(environment_name)
            .unwrap_or_else(|| serde_json::json!({}))
        else {
            return Err(anyhow!("Expected selected environment to be a map"));
        };

        let Value::Object(mut snapshot) =
            read_json_content(snapshot_path.as_ref()).context("snapshot deserialization")?
        else {
            return Err(anyhow!("Expected snapshot file to be a map"));
        };

        snapshot.append(&mut environment);

        Ok(Self {
            snapshot,
            snapshot_path: snapshot_path.as_ref().to_owned(),
        })
    }
}

impl EnvironmentProvider for EnvironmentFileProvider {
    fn snapshot(&self) -> Value {
        Value::Object(self.snapshot.clone())
    }

    fn save(&mut self, snapshot: &Value) -> io::Result<()> {
        fs::write(&self.snapshot_path, serde_json::to_string_pretty(snapshot)?)?;

        Ok(())
    }
}

fn read_json_content(path: &Path) -> crate::Result<Value> {
    match fs::read(path) {
        Ok(data) => Ok(serde_json::from_slice(&data).context("json deserialization")?),
        Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(serde_json::json!({})),
        Err(e) => Err(anyhow!("IO Error: {e}")),
    }
}