mise 2024.1.16

The front-end to your dev env
use std::collections::HashMap;
use std::env::{join_paths, split_paths};
use std::fmt::{Display, Formatter};
use std::io::Write;
use std::path::{Path, PathBuf};

use base64::prelude::*;
use flate2::write::{ZlibDecoder, ZlibEncoder};
use flate2::Compression;
use itertools::Itertools;
use miette::{IntoDiagnostic, Result};
use serde_derive::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
pub struct DirenvDiff {
    #[serde(default, rename = "p")]
    pub old: HashMap<String, String>,
    #[serde(default, rename = "n")]
    pub new: HashMap<String, String>,
}

impl DirenvDiff {
    pub fn parse(input: &str) -> Result<DirenvDiff> {
        // let bytes = BASE64_URL_SAFE.decode(input)?;
        // let uncompressed = inflate_bytes_zlib(&bytes).unwrap();
        // Ok(serde_json::from_slice(&uncompressed[..])?)
        let mut writer = Vec::new();
        let mut decoder = ZlibDecoder::new(writer);
        let bytes = BASE64_URL_SAFE.decode(input).into_diagnostic()?;
        decoder.write_all(&bytes[..]).into_diagnostic()?;
        writer = decoder.finish().into_diagnostic()?;
        serde_json::from_slice(&writer[..]).into_diagnostic()
    }

    pub fn new_path(&self) -> Vec<PathBuf> {
        let path = self.new.get("PATH");
        match path {
            Some(path) => split_paths(path).collect(),
            None => vec![],
        }
    }

    pub fn old_path(&self) -> Vec<PathBuf> {
        let path = self.old.get("PATH");
        match path {
            Some(path) => split_paths(path).collect(),
            None => vec![],
        }
    }

    /// this adds a directory to both the old and new path in DIRENV_DIFF
    /// the purpose is to trick direnv into thinking that this path has always been there
    /// that way it does not remove it when it modifies PATH
    /// it returns the old and new paths as vectors
    pub fn add_path_to_old_and_new(&mut self, path: &Path) -> Result<(Vec<PathBuf>, Vec<PathBuf>)> {
        let mut old = self.old_path();
        let mut new = self.new_path();

        old.insert(0, path.into());
        new.insert(0, path.into());

        self.old.insert(
            "PATH".into(),
            join_paths(&old).into_diagnostic()?.into_string().unwrap(),
        );
        self.new.insert(
            "PATH".into(),
            join_paths(&new).into_diagnostic()?.into_string().unwrap(),
        );

        Ok((old, new))
    }

    pub fn remove_path_from_old_and_new(
        &mut self,
        path: &Path,
    ) -> Result<(Vec<PathBuf>, Vec<PathBuf>)> {
        let mut old = self.old_path();
        let mut new = self.new_path();

        // remove the path from both old and new but only once
        old.iter().position(|p| p == path).map(|i| old.remove(i));
        new.iter().position(|p| p == path).map(|i| new.remove(i));

        self.old.insert(
            "PATH".into(),
            join_paths(&old).into_diagnostic()?.into_string().unwrap(),
        );
        self.new.insert(
            "PATH".into(),
            join_paths(&new).into_diagnostic()?.into_string().unwrap(),
        );

        Ok((old, new))
    }

    pub fn dump(&self) -> Result<String> {
        let mut gz = ZlibEncoder::new(Vec::new(), Compression::fast());
        gz.write_all(&serde_json::to_vec(self).into_diagnostic()?)
            .into_diagnostic()?;
        Ok(BASE64_URL_SAFE.encode(gz.finish().into_diagnostic()?))
    }
}

impl Display for DirenvDiff {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        let print_sorted = |hashmap: &HashMap<String, String>| {
            hashmap
                .iter()
                .map(|(k, v)| format!("{k}={v}"))
                .sorted()
                .collect::<Vec<_>>()
        };
        f.debug_struct("DirenvDiff")
            .field("old", &print_sorted(&self.old))
            .field("new", &print_sorted(&self.new))
            .finish()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_parse() {
        let input = r#"eJys0c1yojAAwPF3ybmWaLB-zPSAGCqIQCGgeGGIELDlM2BEOr77zs7szr7AXv-H3-X_Axqw_gGabYM1qPk1A88XUP1OW93FVhBtdReswURq-FXEfSqJmEusLpKUdxLspALRJY1Yt2Bifk8aLhf5iiZIhhDCjEtE6svmteGuSJVHAV7-qppuYrAG_0WVXtNK8Ms__KgQdYc9sAapMXRj1-9XW8VX7A16UA4NPIs9xCK5WO51XnvfwWBT1R9N7zIcHvvJbZF5g8pk0V2c5CboIw8_NjOUWDK5qcxIcaFrp3anhwdr5FeKJmfd9stgqvuVZqcXsXHYJ-kSGWpoxyZLzf0a0LUcMgv17exenXXunfOTZZfybiVmb9OAhjDtHEcOk0lrRWG84OrRobW6IgGGZqwelglTq8UmJrbP9p0x9pTW5t3L21P1mZfL7_pMtIW599v-Cx_dmzEdCcZ1TAzkz7dvfO4QAefO6Y4VxYmijzgP_Oz9Hbz8uU5jDp7PXwEAAP__wB6qKg=="#;
        let diff = DirenvDiff::parse(input).unwrap();
        assert_display_snapshot!(diff);
    }

    #[test]
    fn test_dump() {
        let diff = DirenvDiff {
            old: HashMap::from([("a".to_string(), "b".to_string())]),
            new: HashMap::from([("c".to_string(), "d".to_string())]),
        };
        let output = diff.dump().unwrap();
        assert_display_snapshot!(&output);
        let diff = DirenvDiff::parse(&output).unwrap();
        assert_display_snapshot!(diff);
    }

    #[test]
    fn test_add_path_to_old_and_new() {
        let mut diff = DirenvDiff {
            old: HashMap::from([("PATH".to_string(), "/foo:/tmp:/bar:/old".to_string())]),
            new: HashMap::from([("PATH".to_string(), "/foo:/bar:/new".to_string())]),
        };
        let path = PathBuf::from("/tmp");
        diff.add_path_to_old_and_new(&path).unwrap();
        assert_display_snapshot!(diff.old.get("PATH").unwrap());
        assert_display_snapshot!(diff.new.get("PATH").unwrap());
    }

    #[test]
    fn test_null_path() {
        let mut diff = DirenvDiff {
            old: HashMap::from([]),
            new: HashMap::from([]),
        };
        let path = PathBuf::from("/tmp");
        diff.add_path_to_old_and_new(&path).unwrap();
        assert_display_snapshot!(diff.old.get("PATH").unwrap());
        assert_display_snapshot!(diff.new.get("PATH").unwrap());
    }
}