use crate::discover_author::discover_author;
use crate::work::{Work, WorkItem};
use std::collections::HashSet;
use std::path::PathBuf;
use chrono::{Datelike, Utc};
use serde_json::json;
use inflector::cases::kebabcase::to_kebab_case;
static SCRIPTS_DIRECTORY: &'static str = "scripts";
static MODULES_DIRECTORY: &'static str = "modules";
static REMODEL_DIRECTORY: &'static str = ".remodel";
static TESTEZ_DIRECTORY: &'static str = "testez";
static LUA_TEST_RUNNER_FILE: &'static str = "run-tests.lua";
static REMODEL_REMOVE_TEST_FILE: &'static str = "remove-tests.lua";
static PROJECT_VAR: &'static str = "{{PROJECT_NAME}}";
static AUTHOR_VAR: &'static str = "{{AUTHOR}}";
static YEAR_VAR: &'static str = "{{YEAR}}";
static BUILD_FILE_EXTENSION: &'static str = "{{FILE_EXTENSION}}";
static GIT_IGNORE: &'static str = include_str!("templates/gitignore.txt");
static LUA_TEST_RUNNER: &'static str = include_str!("templates/run-tests.lua");
static BUILD_SCRIPT: &'static str = include_str!("templates/build-assets.sh");
static BUILD_AND_RUN_SCRIPT: &'static str = include_str!("templates/build-and-run-tests.sh");
static TEST_RUNNER_PATH_VAR: &'static str = "{{TEST_RUNNER_PATH}}";
static SELENE_TESTEZ_CONFIG: &'static str = include_str!("templates/testez.toml");
static REMODEL_REMOVE_TESTS: &'static str = include_str!("templates/remove-tests.lua");
static MIT_LICENSE: &'static str = include_str!("templates/licenses/MIT.txt");
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Feature {
Rojo,
Selene,
TestEZ,
Foreman,
Scripts,
Remodel,
MitLicense,
}
pub static DEFAULT_FEATURES: [Feature; 6] = [
Feature::Rojo,
Feature::Selene,
Feature::TestEZ,
Feature::Foreman,
Feature::Scripts,
Feature::Remodel,
];
pub static ALL_FEATURES: [Feature; 7] = [
Feature::Rojo,
Feature::Selene,
Feature::TestEZ,
Feature::Foreman,
Feature::Scripts,
Feature::Remodel,
Feature::MitLicense,
];
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum ProjectType {
Library,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum Tool {
Remodel,
Rojo,
RunInRoblox,
Selene,
}
impl Tool {
fn to_foreman_dependency(&self) -> String {
let (name, source, version) = self.get_info();
format!(
r#"{} = {{ source = "{}", version = "{}" }}"#,
name,
source,
version,
)
}
fn get_info(&self) -> (&'static str, &'static str, &'static str) {
let name = match self {
Self::Remodel => "remodel",
Self::Rojo => "rojo",
Self::RunInRoblox => "run-in-roblox",
Self::Selene => "selene",
};
let source = match self {
Self::Remodel => "rojo-rbx/remodel",
Self::Rojo => "rojo-rbx/rojo",
Self::RunInRoblox => "rojo-rbx/run-in-roblox",
Self::Selene => "Kampfkarren/selene",
};
let version = match self {
Self::Remodel => "0.7.1",
Self::Rojo => "6.0.2",
Self::RunInRoblox => "0.3.0",
Self::Selene => "0.11.0",
};
(name, source, version)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Project {
project_type: ProjectType,
name: String,
kebab_name: String,
file_extension: String,
author: String,
features: HashSet<Feature>,
}
impl Project {
pub fn new_library<T: Into<String>>(name: T) -> Self {
let name = name.into();
let kebab_name = to_kebab_case(&name);
let features = DEFAULT_FEATURES.iter()
.map(Clone::clone)
.collect();
let author = discover_author()
.map(|(name, email)| {
if let Some(email) = email {
format!("{} <{}>", name, email)
} else {
name
}
})
.unwrap_or("<author>".to_owned());
Self {
project_type: ProjectType::Library,
name,
kebab_name,
file_extension: "rbxm".to_owned(),
author,
features,
}
}
pub fn get_directory_name(&self) -> &String {
&self.kebab_name
}
pub fn with_feature(&mut self, feature: Feature) -> &mut Self {
self.features.insert(feature);
self
}
pub fn remove_feature(&mut self, feature: &Feature) -> &mut Self {
self.features.remove(feature);
self
}
pub fn create(&self) -> Work {
let mut work: Work = self.get_default_work();
match self.project_type {
ProjectType::Library => {
work.add_item(WorkItem::write_string("src/init.lua", "return {}\n"));
}
}
work = self.features.iter()
.map(|feature| self.get_feature_work(feature))
.fold(work, |mut work, other_work| {
work.merge(other_work);
work
});
if self.has_feature(Feature::Rojo) {
work.merge(self.build_rojo_config());
}
work
}
#[inline]
fn has_feature(&self, feature: Feature) -> bool {
self.features.contains(&feature)
}
fn get_default_work(&self) -> Work {
let mut work = Work::new();
work.add_primary_item(WorkItem::GitInit);
work.add_item(WorkItem::write_string(".gitignore", GIT_IGNORE));
work.add_item(
WorkItem::write_string("README.md", format!("# {}\n", self.name))
);
work
}
fn get_feature_work(&self, feature: &Feature) -> Work {
match feature {
Feature::Rojo => {
self.build_rojo_config()
},
Feature::TestEZ => {
WorkItem::add_git_submodule(
PathBuf::from(MODULES_DIRECTORY).join(TESTEZ_DIRECTORY),
"https://github.com/Roblox/testez.git",
).into()
}
Feature::Selene => {
self.build_selene_config()
}
Feature::Scripts => {
self.build_scripts()
}
Feature::Foreman => {
self.build_foreman_config()
}
Feature::Remodel => {
self.build_remodel_config()
}
Feature::MitLicense => {
self.build_mit_license()
}
}
}
fn build_rojo_config(&self) -> Work {
let mut work = Work::new();
work.add_item(WorkItem::write_json(
"default.project.json",
json!({
"name": self.name,
"tree": {
"$path": "src",
},
}),
));
if self.has_feature(Feature::TestEZ) {
let test_place_work = WorkItem::write_json(
"test-place.project.json",
json!({
"name": format!("Test {}", self.name),
"tree": {
"$className": "DataModel",
"HttpService": {
"$className": "HttpService",
"$properties": {
"HttpEnabled": true,
},
},
"ReplicatedStorage": {
"$className": "ReplicatedStorage",
&self.name: {
"$path": "default.project.json",
},
"TestEZ": {
"$path": format!("{}/{}", MODULES_DIRECTORY, TESTEZ_DIRECTORY),
},
},
},
}),
);
work.add_item(test_place_work);
}
work
}
fn build_scripts(&self) -> Work {
let mut work = Work::new();
if self.has_feature(Feature::Rojo) {
let scripts = PathBuf::from(SCRIPTS_DIRECTORY);
let mut build_script = BUILD_SCRIPT.replace(PROJECT_VAR, &self.kebab_name)
.replace(BUILD_FILE_EXTENSION, &self.file_extension);
if self.has_feature(Feature::Remodel) {
build_script.push_str(&format!(
"remodel run {}\n",
REMODEL_REMOVE_TEST_FILE,
));
}
work.add_item(WorkItem::write_string(
scripts.join("build-assets.sh"),
build_script,
));
if self.has_feature(Feature::TestEZ) {
work.add_item(
WorkItem::write_string(
scripts.join(LUA_TEST_RUNNER_FILE),
LUA_TEST_RUNNER.replace(PROJECT_VAR, &self.name),
)
);
work.add_item(
WorkItem::write_string(
scripts.join("build-and-run-tests.sh"),
BUILD_AND_RUN_SCRIPT.replace(
TEST_RUNNER_PATH_VAR,
&format!("{}/{}", SCRIPTS_DIRECTORY, LUA_TEST_RUNNER_FILE),
),
)
);
}
}
work
}
fn build_selene_config(&self) -> Work {
let has_testez = self.has_feature(Feature::TestEZ);
let mut work: Work = WorkItem::write_string(
"selene.toml",
format!(
r#"std = "roblox{}""#,
if has_testez { "+testez" } else { "" },
),
).into();
if has_testez {
work.add_item(WorkItem::write_string("testez.toml", SELENE_TESTEZ_CONFIG));
}
work
}
fn get_tools(&self) -> Vec<Tool> {
let mut tools = Vec::new();
if self.has_feature(Feature::Rojo) {
tools.push(Tool::Rojo);
if self.has_feature(Feature::TestEZ)
&& self.has_feature(Feature::Scripts)
{
tools.push(Tool::RunInRoblox);
}
}
if self.has_feature(Feature::Selene) {
tools.push(Tool::Selene);
}
if self.has_feature(Feature::Remodel) {
tools.push(Tool::Remodel);
}
tools
}
fn build_foreman_config(&self) -> Work {
let mut tools: Vec<String> = self.get_tools().iter()
.map(|tool| tool.to_foreman_dependency())
.collect();
tools.sort();
let mut lines = vec![
"[tools]".to_owned(),
];
lines.extend(tools);
WorkItem::write_multi_line_string("foreman.toml", lines)
.into()
}
fn build_remodel_config(&self) -> Work {
WorkItem::write_string(
PathBuf::from(REMODEL_DIRECTORY).join(REMODEL_REMOVE_TEST_FILE),
REMODEL_REMOVE_TESTS,
).into()
}
fn build_mit_license(&self) -> Work {
WorkItem::write_string(
"LICENSE.txt",
MIT_LICENSE.replace(YEAR_VAR, &Utc::now().year().to_string())
.replace(AUTHOR_VAR, &self.author)
).into()
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn build_script_template_has_project_variable() {
let some_name = "fooProject";
assert!(BUILD_SCRIPT.find(some_name).is_none());
let script = BUILD_SCRIPT.replace(PROJECT_VAR, &some_name);
assert_ne!(script, BUILD_SCRIPT);
assert!(script.find(some_name).is_some())
}
#[test]
fn build_script_template_has_file_extension_variable() {
let extension = "rbxm";
assert!(BUILD_SCRIPT.find(extension).is_none());
let script = BUILD_SCRIPT.replace(BUILD_FILE_EXTENSION, &extension);
assert_ne!(script, BUILD_SCRIPT);
assert!(script.find(extension).is_some())
}
#[test]
fn lua_test_runner_template_has_correct_variable() {
let some_name = "fooProject";
assert!(LUA_TEST_RUNNER.find(some_name).is_none());
let runner = LUA_TEST_RUNNER.replace(PROJECT_VAR, &some_name);
assert_ne!(runner, LUA_TEST_RUNNER);
assert!(runner.find(some_name).is_some())
}
#[test]
fn build_and_run_tests_script_template_has_test_runner_variable() {
let some_runner = "run.lua";
assert!(BUILD_AND_RUN_SCRIPT.find(some_runner).is_none());
let script = BUILD_AND_RUN_SCRIPT.replace(TEST_RUNNER_PATH_VAR, &some_runner);
assert_ne!(script, BUILD_AND_RUN_SCRIPT);
assert!(script.find(some_runner).is_some())
}
#[test]
fn mit_license_template_has_author_variable() {
let author = "spongebob";
assert!(MIT_LICENSE.find(author).is_none());
let license = MIT_LICENSE.replace(AUTHOR_VAR, &author);
assert_ne!(license, MIT_LICENSE);
assert!(license.find(author).is_some())
}
#[test]
fn mit_license_template_has_year_variable() {
let year = "2089";
assert!(MIT_LICENSE.find(year).is_none());
let license = MIT_LICENSE.replace(YEAR_VAR, &year);
assert_ne!(license, MIT_LICENSE);
assert!(license.find(year).is_some())
}
}