mcserver 0.2.0

A command line interface which simplifies minecraft server management with zellij and mcrcon
Documentation
use crate::{
    error::{Error, Result},
    server,
};
use serde::Deserialize;
use std::{
    cell::UnsafeCell,
    collections::HashMap,
    env,
    ffi::OsStr,
    fmt::{self, Debug, Formatter},
    fs, io,
    path::{Path, PathBuf},
};

const DEFAULT_CONFIG: &str = include_str!(concat!(env!("OUT_DIR"), "/generated_config.toml"));

const CONFIG_DIRECTORY_NAME: &str = "mcserver";
const CONFIG_FILE_NAME: &str = "config.toml";

#[derive(Clone, Deserialize, PartialEq)]
pub struct Password(pub String);

impl AsRef<OsStr> for Password {
    fn as_ref(&self) -> &OsStr {
        OsStr::new(&self.0)
    }
}

impl Debug for Password {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(f, "(hidden)")
    }
}

#[derive(Clone, Debug, Deserialize, PartialEq)]
pub struct RconConfig {
    pub server_address: Option<String>,
    pub port: Option<u16>,
    pub password: Option<Password>,
}

#[derive(Debug, Deserialize)]
#[serde(from = "String")]
pub struct ServersDirectory {
    raw_string: String,
    expanded: UnsafeCell<Option<PathBuf>>,
}

impl From<String> for ServersDirectory {
    fn from(value: String) -> Self {
        Self {
            raw_string: value,
            expanded: UnsafeCell::new(None),
        }
    }
}

impl ServersDirectory {
    pub fn expand(&self) -> Result<&Path> {
        unsafe {
            if let Some(expanded) = &*(self.expanded.get()) {
                return Ok(expanded);
            }

            let expanded_path = PathBuf::from(shellexpand::full(&self.raw_string)?.into_owned());
            Ok((*self.expanded.get()).insert(expanded_path))
        }
    }
}

#[derive(Debug, Deserialize)]
pub struct Config {
    pub default_java_args: String,
    pub contact: String,
    pub nogui: bool,
    pub servers_directory: ServersDirectory,
    pub default_server: Option<String>,
    pub rcon: Option<HashMap<String, RconConfig>>,
}

pub fn get_directory() -> Result<PathBuf> {
    let config_dir = dirs::config_dir()
        .ok_or(Error::UnresolvedConfigDirectory)?
        .join(CONFIG_DIRECTORY_NAME);

    Ok(config_dir)
}

pub fn load_or_create(config_directory: impl AsRef<Path>) -> Result<Config> {
    let config_file_path = config_directory.as_ref().join(CONFIG_FILE_NAME);

    match fs::read_to_string(&config_file_path) {
        Ok(config_str) => Ok(toml::from_str(&config_str)?),
        Err(err) => {
            if err.kind() == io::ErrorKind::NotFound {
                fs::create_dir_all(&config_directory)?;
                fs::write(&config_file_path, DEFAULT_CONFIG)?;
                Ok(toml::from_str(DEFAULT_CONFIG)?)
            } else {
                Err(Error::Io(err))
            }
        }
    }
}

pub fn get_current_server_directory(servers_dir: impl AsRef<Path>) -> Result<String> {
    let mut server_path = env::current_dir()?;

    loop {
        if fs::exists(server_path.join(server::METADATA_DIRECTORY_NAME))? {
            break;
        }

        server_path = server_path
            .parent()
            .ok_or(Error::InvalidServersDirectory)?
            .to_path_buf();
    }

    let server = server_path
        .strip_prefix(servers_dir)
        .map_err(|_| Error::InvalidServersDirectory)?
        .to_string_lossy()
        .into_owned();

    Ok(server)
}

pub fn server_or_current<S>(server: S, config: &Config) -> Result<String>
where
    S: Into<String> + for<'a> PartialEq<&'a str>,
{
    if server == "." {
        get_current_server_directory(config.servers_directory.expand()?)
    } else {
        Ok(server.into())
    }
}

pub fn handle_server_arg(server: Option<String>, config: &Config) -> Result<String> {
    match server {
        Some(server) => server_or_current(server, config),
        None => match &config.default_server {
            Some(server) => Ok(server.clone()),
            None => Err(Error::NoDefaultServer),
        },
    }
}

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

    #[test]
    fn paths() {
        let expected_config_dir =
            PathBuf::from(shellexpand::tilde("~/.config/mcserver/").into_owned());

        assert_eq!(
            &get_directory().expect("Failed to get config directory"),
            &expected_config_dir
        );
    }

    #[test]
    fn something() {}
}