selene-core 0.3.0

selene-core is the backend for Selene, a local-first music player
Documentation
use std::{fs, io};

use serde::{Deserialize, Serialize};
use toml_edit::{DocumentMut, Item, Table};

use crate::config_dir;

pub mod common;

#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
    #[error("IoError: {0}")]
    Io(#[from] std::io::Error),

    #[error("No home directory was found for the current user")]
    NoHomeDir,

    #[error("TomlError: {0}")]
    TomlEdit(#[from] toml_edit::TomlError),

    #[error("TomlError: {0}")]
    Toml(#[from] toml::de::Error),
}

pub trait Config: Sized + Serialize + for<'de> Deserialize<'de> + Default {
    /// The name of the config file, excluding its extension
    const CONFIG_FILE_NAME: &'static str;

    /// Loads [`Self`] from the system config directory using [`Self::CONFIG_FILE_NAME`], creating a default config if one does not already exist
    fn load() -> Result<Self, ConfigError> {
        let mut file = config_dir().join(Self::CONFIG_FILE_NAME);
        file.add_extension("toml");

        let config = match std::fs::read_to_string(&file) {
            Ok(toml) => toml::from_str(&toml)?,
            Err(err) if matches![err.kind(), io::ErrorKind::NotFound] => {
                let config = Self::default();
                config.save()?;
                config
            }
            Err(err) => return Err(err.into()),
        };

        Ok(config)
    }

    /// Saves [`Self`] to the system config directory using [`Self::CONFIG_FILE_NAME`]
    ///
    /// # Errors
    ///
    /// Errors when the program cannot create necessary directories or write to file
    /// See [`std::fs::create_dir_all()`] and [`std::fs::write()`] for more information
    fn save(&self) -> Result<(), ConfigError> {
        let config_dir = config_dir();
        let mut file = config_dir.join(Self::CONFIG_FILE_NAME);
        file.add_extension("toml");

        let config_doc = toml_edit::ser::to_document(self).unwrap();
        let mut disk_doc: DocumentMut = if file.exists() {
            fs::read_to_string(&file)?.parse()?
        } else {
            DocumentMut::new()
        };

        write_needed_config_values(disk_doc.as_table_mut(), config_doc.as_table());

        fs::create_dir_all(config_dir)?;
        fs::write(file, disk_doc.to_string())?;

        Ok(())
    }
}

fn write_needed_config_values(disk: &mut Table, current_table: &Table) {
    for (key, value) in current_table {
        match value {
            Item::Table(table) => {
                let disk_table = disk
                    .entry(key)
                    .or_insert(Item::Table(Table::new()))
                    .as_table_mut()
                    .unwrap();
                write_needed_config_values(disk_table, table);
            }
            _ => {
                disk.insert(key, value.clone());
            }
        }
    }
}