rotz 1.2.1

Fully cross platform dotfile manager written in rust.
use std::{collections::HashMap, fmt::Debug, fs, path::PathBuf};

use miette::Diagnostic;
use serde::{Deserialize, Serialize};
use tap::Pipe;
#[cfg(feature = "profiling")]
use tracing::instrument;

use crate::{FILE_EXTENSIONS, FileFormat, PROJECT_DIRS, helpers};

#[derive(thiserror::Error, Diagnostic, Debug)]
pub(crate) enum Error {
  #[error("Could not read state file")]
  #[diagnostic(code(state::read))]
  Reading(#[source] std::io::Error),

  #[error("Could not write state file")]
  #[diagnostic(code(state::write))]
  Writing(#[source] std::io::Error),

  #[error("Could not serialize state")]
  #[diagnostic(code(state::serialize))]
  Serializing(
    #[source]
    #[diagnostic_source]
    helpers::ParseError,
  ),

  #[error("Could not deserialize state")]
  #[diagnostic(code(state::deserialize))]
  Deserializing(
    #[source]
    #[diagnostic_source]
    helpers::ParseError,
  ),
}

#[derive(Serialize, Deserialize, Default, Debug)]
#[serde(transparent)]
pub(crate) struct Linked(pub HashMap<String, HashMap<PathBuf, PathBuf>>);

impl From<HashMap<String, HashMap<PathBuf, PathBuf>>> for Linked {
  fn from(value: HashMap<String, HashMap<PathBuf, PathBuf>>) -> Self {
    Self(value)
  }
}

#[derive(Serialize, Deserialize, Default, Debug)]
pub(crate) struct State {
  pub linked: Linked,
}

impl State {
  #[cfg_attr(feature = "profiling", instrument)]
  pub fn read() -> Result<State, Error> {
    let state_file = helpers::get_file_with_format(PROJECT_DIRS.data_local_dir(), "state");

    if let Some((state_file, format)) = state_file {
      deserialize_state(&fs::read_to_string(state_file).map_err(Error::Reading)?, format).map_err(Error::Deserializing)?
    } else {
      State::default()
    }
    .pipe(Ok)
  }

  #[cfg_attr(feature = "profiling", instrument)]
  pub fn write(&self) -> Result<(), Error> {
    let state_file =
      helpers::get_file_with_format(PROJECT_DIRS.data_local_dir(), "state").unwrap_or_else(|| (PROJECT_DIRS.data_local_dir().join(format!("state.{}", FILE_EXTENSIONS[0].0)), FILE_EXTENSIONS[0].1));

    fs::create_dir_all(PROJECT_DIRS.data_local_dir()).map_err(Error::Writing)?;
    fs::write(state_file.0, serialize_state(self, state_file.1).map_err(Error::Serializing)?).map_err(Error::Writing)
  }
}

#[cfg_attr(feature = "profiling", instrument)]
fn deserialize_state(state: &str, format: FileFormat) -> Result<State, helpers::ParseError> {
  Ok(match format {
    #[cfg(feature = "yaml")]
    FileFormat::Yaml => serde_yaml::from_str(state)?,
    #[cfg(feature = "toml")]
    FileFormat::Toml => serde_toml::from_str(state)?,
    #[cfg(feature = "json")]
    FileFormat::Json => serde_json::from_str(state)?,
  })
}

#[cfg_attr(feature = "profiling", instrument)]
fn serialize_state(state: &(impl Serialize + Debug), format: FileFormat) -> Result<String, helpers::ParseError> {
  Ok(match format {
    #[cfg(feature = "yaml")]
    FileFormat::Yaml => serde_yaml::to_string(state)?,
    #[cfg(feature = "toml")]
    FileFormat::Toml => serde_toml::to_string(state)?,
    #[cfg(feature = "json")]
    FileFormat::Json => serde_json::to_string(state)?,
  })
}