mcserver 0.3.6

Minecraft Multiplexer
use std::{
    fmt::{self, Display},
    path::Path,
};

use miette::{Context, IntoDiagnostic};
use tokio::fs::{self, ReadDir};

use crate::{
    consts::METADATA_DIRECTORY,
    error::{Error, display_path},
};

#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct ServerGroup {
    pub name: String,
    pub children: Vec<ServerEntry>,
    pub expanded: bool,
}

impl ServerGroup {
    pub fn new(name: String, children: Vec<ServerEntry>, expanded: bool) -> Self {
        Self {
            name,
            children,
            expanded,
        }
    }
}

impl Display for ServerGroup {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        writeln!(f, "{}", self.name)?;
        fmt_entries(&self.children, f, "", 3)
    }
}

#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct ServerObject {
    pub name: String,
    pub online: bool,
}

#[derive(Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum ServerEntry {
    Group(ServerGroup),
    Object(ServerObject),
}

impl ServerEntry {
    fn group(name: String, children: Vec<ServerEntry>) -> Self {
        Self::Group(ServerGroup::new(name, children, false))
    }

    fn object(name: String, online: bool) -> Self {
        Self::Object(ServerObject { name, online })
    }
}

impl Display for ServerEntry {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ServerEntry::Group(server_group) => write!(f, "{server_group}"),
            ServerEntry::Object(server_object) => write!(f, "{}", server_object.name),
        }
    }
}

pub fn fmt_entries(
    entries: &[ServerEntry],
    f: &mut fmt::Formatter,
    base_indentation: &str,
    indentation_width: usize,
) -> fmt::Result {
    for (idx, entry) in entries.iter().enumerate() {
        match entry {
            ServerEntry::Group(server_group) => {
                if idx == entries.len().saturating_sub(1) {
                    writeln!(f, "{base_indentation}└╴{}", server_group.name)?;
                    fmt_entries(
                        &server_group.children,
                        f,
                        &format!("{base_indentation}{}", " ".repeat(indentation_width)),
                        indentation_width,
                    )
                } else {
                    writeln!(f, "{base_indentation}├╴{}", server_group.name)?;
                    fmt_entries(
                        &server_group.children,
                        f,
                        &format!(
                            "{base_indentation}│{}",
                            " ".repeat(indentation_width.saturating_sub(1))
                        ),
                        indentation_width,
                    )
                }
            }
            ServerEntry::Object(server_object) => {
                if idx == entries.len().saturating_sub(1) {
                    writeln!(f, "{base_indentation}└╴{}", server_object.name)
                } else {
                    writeln!(f, "{base_indentation}├╴{}", server_object.name)
                }
            }
        }?;
    }

    Ok(())
}

pub async fn make_read_dir(dir: &Path) -> miette::Result<ReadDir> {
    fs::read_dir(dir)
        .await
        .into_diagnostic()
        .wrap_err_with(|| format!("Failed to read directory `{}`", display_path(dir)))
}

#[async_recursion::async_recursion]
pub async fn read(dir: &Path) -> miette::Result<Vec<ServerEntry>> {
    let mut children: Vec<ServerEntry> = vec![];
    let mut read_dir = make_read_dir(dir).await?;

    while let Some(entry) = read_dir
        .next_entry()
        .await
        .into_diagnostic()
        .wrap_err_with(|| {
            format!(
                "Failed to read entry from directory `{}`",
                display_path(dir)
            )
        })?
    {
        let entry_path = entry.path();

        let relative_path = entry_path
            .strip_prefix(dir)
            .into_diagnostic()
            .wrap_err_with(|| {
                format!(
                    "Failed to strip `{}` from `{}`",
                    display_path(dir),
                    display_path(&entry_path)
                )
            })?
            .to_path_buf()
            .into_string()
            .map_err(Error::NonUtf8Path)
            .into_diagnostic()?;

        if fs::try_exists(entry_path.join(METADATA_DIRECTORY))
            .await
            .into_diagnostic()
            .wrap_err("Failed to check if directory exists")?
        {
            children.push(ServerEntry::object(relative_path, false));
        } else {
            children.push(ServerEntry::group(relative_path, read(&entry_path).await?));
        }
    }

    children.sort_unstable();

    Ok(children)
}

#[cfg(test)]
mod test {
    use std::path::PathBuf;

    use tempfile::TempDir;

    use super::*;

    fn tempdir_init() -> miette::Result<TempDir> {
        tempfile::tempdir()
            .into_diagnostic()
            .wrap_err("Failed to create temporary directory")
    }

    async fn new_dir(parent: &Path, name: impl AsRef<Path>) -> miette::Result<PathBuf> {
        let dir = parent.join(name);

        fs::create_dir(&dir)
            .await
            .into_diagnostic()
            .wrap_err_with(|| format!("Failed to create directory `{}`", display_path(&dir)))?;

        Ok(dir)
    }

    async fn new_server(
        parent: &Path,
        name: impl AsRef<Path>,
    ) -> miette::Result<(PathBuf, PathBuf)> {
        let server_dir = new_dir(parent, name).await?;
        let metadata_dir = new_dir(&server_dir, METADATA_DIRECTORY).await?;

        Ok((server_dir, metadata_dir))
    }

    #[tokio::test]
    async fn entries() -> miette::Result<()> {
        let temp_dir = tempdir_init()?;
        let servers_directory = temp_dir.path();

        for i in 0..2 {
            let group = new_dir(servers_directory, format!("group-{i}")).await?;

            for j in 0..2 {
                new_server(&group, format!("server-{j}-group{i}")).await?;

                let subgroup =
                    new_dir(servers_directory, format!("subgroup-{j}-group-{i}")).await?;

                for k in 0..2 {
                    new_server(&subgroup, format!("server-{k}-subgroup-{j}-group-{i}")).await?;
                }
            }
        }

        let output = format!(
            "{}",
            ServerGroup::new("root".to_string(), read(servers_directory).await?, false)
        );

        let expected = "\
root
├╴group-0
│  ├╴server-0-group0
│  └╴server-1-group0
├╴group-1
│  ├╴server-0-group1
│  └╴server-1-group1
├╴subgroup-0-group-0
│  ├╴server-0-subgroup-0-group-0
│  └╴server-1-subgroup-0-group-0
├╴subgroup-0-group-1
│  ├╴server-0-subgroup-0-group-1
│  └╴server-1-subgroup-0-group-1
├╴subgroup-1-group-0
│  ├╴server-0-subgroup-1-group-0
│  └╴server-1-subgroup-1-group-0
└╴subgroup-1-group-1
   ├╴server-0-subgroup-1-group-1
   └╴server-1-subgroup-1-group-1
";

        assert_eq!(output, expected);

        Ok(())
    }
}