create-roblox-project 0.1.0

Generate initial file structure of Roblox projects
Documentation
use git2::SubmoduleUpdateOptions;
use crate::Error;

use std::fmt::{self, Display, Formatter};
use std::path::{Path, PathBuf};

use futures::future::try_join_all;
use git2::Repository;
use serde_json::{Value, to_string_pretty};
use tokio::fs;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WorkItem {
    Empty,
    NewFile {
        path: PathBuf,
        content: String
    },
    GitInit,
    GitAddSubmodule {
        path: PathBuf,
        url: String,
    },
}

const MAX_FILE_CONTENT_DISPLAY: usize = 60;
const FILE_CONTENT_DISPLAY: usize = 40;

impl Display for WorkItem {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        match self {
            Self::Empty => {
                write!(f, "empty work item")
            }
            Self::NewFile { path, content } => {
                let content = if content.len() > MAX_FILE_CONTENT_DISPLAY {
                    content.get(0..FILE_CONTENT_DISPLAY)
                        .unwrap_or_else(|| {
                            (0..FILE_CONTENT_DISPLAY).into_iter()
                                .rev()
                                .find(|index| content.is_char_boundary(*index))
                                .and_then(|index| content.get(0..index))
                                .unwrap_or("")
                        })
                } else {
                    content
                };
                write!(f, "new file {}: {}", path.display(), content)
            }
            Self::GitAddSubmodule { path, url } => {
                write!(f, "add git submodule {} ({})", path.display(), url)
            }
            Self::GitInit => {
                write!(f, "init git repository")
            }
        }
    }
}

impl WorkItem {
    pub fn write_string<T: Into<PathBuf>, U: Into<String>>(path: T, content: U) -> Self {
        Self::NewFile {
            path: path.into(),
            content: content.into(),
        }
    }

    pub fn write_multi_line_string<T: Into<PathBuf>, U>(path: T, lines: U) -> Self
        where U: IntoIterator, U::Item: Into<String>

    {
        let content_lines: Vec<String> = lines.into_iter()
            .map(Into::into)
            .collect();
        Self::NewFile {
            path: path.into(),
            content: content_lines.join("\n"),
        }
    }

    pub fn write_json<T: Into<PathBuf>>(path: T, value: Value) -> Self {
        Self::NewFile {
            path: path.into(),
            content: to_string_pretty(&value)
                .expect("value should serialize to JSON"),
        }
    }

    pub fn add_git_submodule<T: Into<PathBuf>, U: Into<String>>(path: T, url: U) -> Self {
        Self::GitAddSubmodule {
            path: path.into(),
            url: url.into(),
        }
    }

    pub async fn execute(&self, project_root: &Path) -> Result<(), Error> {
        match self {
            Self::Empty => Ok(()),
            Self::NewFile { path, content } => {
                if let Some(parent) = path.parent() {
                    let directory = project_root.join(parent);
                    log::trace!("creating parent directory: {}", parent.display());
                    fs::create_dir_all(&directory).await
                        .map_err(|io_error| {
                            Error::CannotCreateDirectory {
                                path: directory,
                                reason: format!("{}", io_error),
                            }
                        })?;
                        log::trace!("parent directory {} created", parent.display());
                    }

                log::debug!("writing file at {}", path.display());
                fs::write(project_root.join(path), content).await
                    .map_err(|io_error| {
                        Error::CannotWriteFile {
                            path: path.clone(),
                            reason: format!("{}", io_error),
                        }
                    })?;
                log::trace!("file {} written", path.display());
                Ok(())
            }
            Self::GitInit => {
                log::debug!("init git repository at {}", project_root.display());
                Repository::init(&project_root)
                    .map(|_| ())
                    .map_err(|git_error| {
                        Error::GitError {
                            reason: format!("{}", git_error),
                        }
                    })?;
                log::trace!("repository at {} initialized", project_root.display());
                Ok(())
            }
            Self::GitAddSubmodule { path, url } => {
                log::debug!("adding submodule at {} ({})", path.display(), url);
                Repository::open(&project_root)
                    .and_then(|repository| {
                        repository.submodule(url, path, true)
                            .and_then(|mut submodule| {
                                let mut options = SubmoduleUpdateOptions::new();
                                submodule.clone(Some(&mut options))
                                    .and_then(|_| {
                                        submodule.add_finalize()
                                    })
                            })
                    })
                    .map_err(|git_error| {
                        Error::GitError {
                            reason: format!("{}", git_error),
                        }
                    })?;
                Ok(())
            }
        }
    }
}

impl Default for WorkItem {
    fn default() -> Self {
        Self::Empty
    }
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Work {
    primary_work: Vec<WorkItem>,
    secondary_work: Vec<WorkItem>,
}

impl From<WorkItem> for Work {
    fn from(item: WorkItem) -> Self {
        let mut work = Self::new();
        work.add_item(item);
        work
    }
}

impl Work {
    pub fn new() -> Self {
        Self {
            primary_work: Vec::new(),
            secondary_work: Vec::new(),
        }
    }

    pub fn merge(&mut self, mut work: Self) -> &mut Self {
        self.primary_work.extend(work.primary_work.drain(..));
        self.secondary_work.extend(work.secondary_work.drain(..));
        self
    }

    pub fn add_item(&mut self, item: WorkItem) -> &mut Self {
        self.secondary_work.push(item);
        self
    }

    pub fn add_items<I>(&mut self, items: I) -> &mut Self
        where I: IntoIterator<Item=WorkItem>
    {
        self.secondary_work.extend(items);
        self
    }

    pub fn add_primary_item(&mut self, item: WorkItem) -> &mut Self {
        self.secondary_work.push(item);
        self
    }

    #[tokio::main]
    pub async fn execute(&self, project_root: &Path) -> Result<(), String> {
        Self::execute_work(project_root, &self.primary_work).await?;
        Self::execute_work(project_root, &self.secondary_work).await
    }

    async fn execute_work(project_root: &Path, work: &Vec<WorkItem>) -> Result<(), String> {
        let work_futures: Vec<_> = work.iter()
            .map(|work_item| {
                work_item.execute(project_root)
            })
            .collect();
        try_join_all(work_futures).await
            .map(|_| ())
            .map_err(|error| format!("{}", error))
    }
}