mcserver 0.1.7

A command line interface tool which simplifies minecraft server management with zellij and mcrcon
use anyhow::{Context, Result, anyhow};
use quote::quote;
use serde::Deserialize;
use std::{
    env, fs,
    path::{Path, PathBuf},
};

mod config_defs {
    include!("src/config_defs.rs");

    use proc_macro2::TokenStream;
    use quote::{ToTokens, quote};

    impl ToTokens for StaticConfig<String> {
        fn to_tokens(&self, tokens: &mut TokenStream) {
            let contact = &self.contact;
            let dynamic_config_path = &self.dynamic_config_path;
            tokens.extend(quote! {
                StaticConfig {
                    contact: #contact,
                    dynamic_config_path: #dynamic_config_path,
                }
            });
        }
    }

    impl ToTokens for Password {
        fn to_tokens(&self, tokens: &mut TokenStream) {
            let password = &self.0;
            tokens.extend(quote! {#password})
        }
    }

    impl ToTokens for RconConfig {
        fn to_tokens(&self, tokens: &mut TokenStream) {
            let server_address = match self.server_address.as_ref() {
                Some(server_address) => quote! {
                    Some(#server_address.to_string())
                },
                None => quote! { None },
            };

            let port = match self.port {
                Some(port) => quote! { Some(#port) },
                None => quote! { None },
            };

            let password = match self.password.as_ref() {
                Some(password) => quote! { Some(Password(#password.to_string())) },
                None => quote! { None },
            };

            tokens.extend(quote! {
                RconConfig {
                    server_address: #server_address,
                    port: #port,
                    password: #password,
                }
            })
        }
    }

    impl ToTokens for DynamicConfig {
        fn to_tokens(&self, tokens: &mut TokenStream) {
            let default_java_args = &self.default_java_args;
            let nogui = &self.nogui;
            let servers_directory = &self.servers_directory;
            let default_server = &self.default_server;

            let key_value_pairs = self.rcon.iter().map(|(k, v)| {
                quote! { ( #k.to_string(), #v )}
            });

            tokens.extend(quote! {
                DynamicConfig {
                    default_java_args: #default_java_args.to_string(),
                    nogui: #nogui,
                    servers_directory: #servers_directory.to_string(),
                    default_server: #default_server.to_string(),
                    rcon: std::collections::HashMap::from([
                        #(#key_value_pairs),*
                    ]),
                }
            });
        }
    }
}

use config_defs::{DynamicConfig, StaticConfig};

#[derive(Debug, Deserialize)]
struct Config {
    static_config: StaticConfig<String>,
    default_dynamic_config: DynamicConfig,
}

macro_rules! warning {
    ($($arg:tt)*) => {
        println!("cargo:warning={}", format!($($arg)*))
    }
}

macro_rules! build_log {
    ($($arg:tt)*) => {
        #[cfg(feature = "build-logging")]
        warning!($($arg)*)
    }
}

fn handle_static_config(cargo_manifest_dir: &Path) -> Result<Option<StaticConfig<String>>> {
    let static_config_path = cargo_manifest_dir.join("static_config.toml");

    if !static_config_path.exists() {
        build_log!("The static configuration file does not exist");
        return Ok(None);
    }

    build_log!("Static configuration file found");

    if !static_config_path.is_file() {
        warning!("The static configuration given is not a file");
        return Ok(None);
    }

    build_log!("Static configuration is a file");

    let result = toml::from_str(
        &fs::read_to_string(static_config_path).context("Failed to read configuration file")?,
    );

    match result {
        Ok(static_config) => {
            build_log!("Static configuration read");
            Ok(static_config)
        }
        Err(err) => {
            warning!("Failed to parse static configuration: {err}");
            Ok(None)
        }
    }
}

fn main() -> Result<()> {
    build_log!("Build script running...");
    println!("cargo:rerun-if-changed=");

    let out_dir = PathBuf::new().join(env::var("OUT_DIR")?);
    build_log!("Out directory: {out_dir:?}");

    let cargo_manifest_dir = PathBuf::new().join(env::var("CARGO_MANIFEST_DIR")?);
    build_log!("Cargo manifest dir: {cargo_manifest_dir:?}");

    let cfg_generation_file = &out_dir.join("generated_cfg.rs");
    let config_template_path = &cargo_manifest_dir.join("config_template.toml");

    if !config_template_path.exists() {
        build_log!("Config path ({config_template_path:?}) does not exist");
        return Err(anyhow!("Configuration template does not exist"));
    }

    build_log!("Configuration path exists ({config_template_path:?})");

    if !config_template_path.is_file() {
        build_log!("Configuration template should be a file",);
        return Err(anyhow!("Invalid configuration template"));
    }

    let config: Config = toml::from_str(
        &fs::read_to_string(config_template_path).context("Failed to read configuration file")?,
    )
    .context("Failed to parse configuration file")?;

    let static_config = match handle_static_config(&cargo_manifest_dir)? {
        Some(static_config) => {
            build_log!("Using static configuration");
            static_config
        }
        None => {
            build_log!("Static configuration is not being used");
            config.static_config
        }
    };

    let default_dynamic_config = config.default_dynamic_config;

    let tokens = quote! {
        mod generated_cfg {
            use crate::config_defs::{StaticConfig, Password, RconConfig, DynamicConfig};
            use std::sync::OnceLock;

            pub const STATIC_CONFIG: StaticConfig = #static_config;
            pub static DEFAULT_DYNAMIC_CONFIG: OnceLock<DynamicConfig> = OnceLock::new();

            pub fn get_default_dynamic_config() -> &'static DynamicConfig {
                DEFAULT_DYNAMIC_CONFIG.get_or_init(||
                    #default_dynamic_config
                )
            }
        }
    };

    fs::write(cfg_generation_file, tokens.to_string())?;

    let expanded_dynamic_config_dir = shellexpand::full(&static_config.dynamic_config_path)?;
    let dynamic_config_template_path = Path::new(&*expanded_dynamic_config_dir).join("config.toml");

    if dynamic_config_template_path.exists() {
        if !dynamic_config_template_path.is_file() {
            build_log!(
                "There is something at the path where the dynamic configuration is supposed to exist; this will cause problems in the future"
            );
        } else {
            build_log!("Dynamic configuration found");
        }
    } else {
        fs::create_dir_all(expanded_dynamic_config_dir.to_string())?;
        fs::write(
            &dynamic_config_template_path,
            toml::to_string(&default_dynamic_config)
                .context("Failed to serialize dynamic configuration")?,
        )
        .with_context(|| {
            format!("Failed to write to the dynamic configuration path ({dynamic_config_template_path:?})")
        })?;
    }

    build_log!("Configuration has been generated");

    Ok(())
}