add new command

This commit is contained in:
Michael Angelo Calimlim 2020-11-22 11:42:07 +08:00
parent bb08c87b66
commit c6ffd9a871
9 changed files with 278 additions and 1 deletions

View file

@ -7,3 +7,8 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = "3.0.0-beta.2"
[dev-dependencies]
assert_cmd = "1.0"
predicates = "1.0"

View file

@ -1 +1,17 @@
# ftw
## Commands
### new
creates a new godot-rust project
```shell
$ ftw new my-awesome-game # creates a godot-rust project using the default template
$ ftw new my-awesome-game default # same as above
$ ftw new my-awesome-game /path/to/some/template # creates a godot-rust project using a custom template
```

45
src/ftw_command.rs Normal file
View file

@ -0,0 +1,45 @@
use crate::ftw_template::FtwTemplate;
use crate::process_command::ProcessCommand;
use crate::traits::Processor;
use crate::traits::ToGitUrl;
use crate::type_alias::Commands;
use crate::type_alias::ProjectName;
use std::fs::OpenOptions;
use std::io::prelude::*;
pub enum FtwCommand {
New {
project_name: ProjectName,
template: FtwTemplate,
},
}
impl Processor for FtwCommand {
fn process(&self) {
match self {
FtwCommand::New {
project_name,
template,
} => {
let git_url = &template.to_git_url();
let gitignore_path: String = format!("{}/.gitignore", project_name);
let commands: Commands = vec![vec![
"cargo",
"generate",
"--name",
project_name,
"--git",
git_url,
]];
(ProcessCommand { commands: commands }).process();
let mut gitignore_file = OpenOptions::new()
.append(true)
.open(gitignore_path)
.unwrap();
if let Err(e) = writeln!(gitignore_file, "godot/export_presets.cfg") {
eprintln!("Couldn't write to file: {}", e);
}
}
}
}
}

30
src/ftw_template.rs Normal file
View file

@ -0,0 +1,30 @@
use crate::traits::ToGitUrl;
use crate::type_alias::GitUrl;
use std::str::FromStr;
pub enum FtwTemplate {
Default,
Custom { git_url: GitUrl },
}
impl FromStr for FtwTemplate {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"default" => Ok(FtwTemplate::Default),
git_url => Ok(FtwTemplate::Custom {
git_url: git_url.to_string(),
}),
}
}
}
impl ToGitUrl for FtwTemplate {
fn to_git_url(&self) -> GitUrl {
match self {
FtwTemplate::Default => "https://github.com/godot-rust/godot-rust-template",
FtwTemplate::Custom { git_url } => git_url,
}
.to_string()
}
}

View file

@ -1,3 +1,41 @@
mod ftw_command;
mod ftw_template;
mod process_command;
mod traits;
mod type_alias;
use crate::ftw_command::FtwCommand;
use crate::ftw_template::FtwTemplate;
use crate::traits::Processor;
use clap::{clap_app, crate_authors, crate_version};
fn main() {
println!("Hello, world!");
let version = crate_version!();
let author = crate_authors!("\n");
let matches = clap_app!(ftw =>
(version: version)
(author: author)
(about: "manage your godot-rust project")
(@subcommand new =>
(about: "create a new godot-rust project directory")
(@arg project_name: +required "set the name of your project")
(@arg template: !required "set the template to be used in your project")))
.get_matches();
let command: FtwCommand = match matches.subcommand() {
Some(("new", args)) => {
let template: FtwTemplate = match args
.value_of("template")
.and_then(|template| Some(template))
{
Some(template) => template.parse().unwrap(),
None => FtwTemplate::Default,
};
FtwCommand::New {
project_name: args.value_of("project_name").unwrap().to_string(),
template: template,
}
}
_ => panic!("this should not happen!"),
};
command.process();
}

28
src/process_command.rs Normal file
View file

@ -0,0 +1,28 @@
use crate::traits::Processor;
use crate::type_alias::Commands;
use std::io::{self, Write};
use std::process::Command;
pub struct ProcessCommand<'a> {
pub commands: Commands<'a>,
}
impl Processor for ProcessCommand<'_> {
fn process(&self) {
for xs in &self.commands {
let out = match xs.split_at(1) {
(&[cmd], args) => args
.iter()
.fold(&mut Command::new(cmd), |s, i| s.arg(i))
.output()
.expect("failed to execute process"),
_ => panic!("this should not happen"),
};
if out.status.success() {
io::stdout().write_all(&out.stdout).unwrap();
} else {
io::stderr().write_all(&out.stderr).unwrap();
}
}
}
}

9
src/traits.rs Normal file
View file

@ -0,0 +1,9 @@
use crate::type_alias::GitUrl;
pub trait Processor {
fn process(&self);
}
pub trait ToGitUrl {
fn to_git_url(&self) -> GitUrl;
}

3
src/type_alias.rs Normal file
View file

@ -0,0 +1,3 @@
pub type Commands<'a> = Vec<Vec<&'a str>>;
pub type GitUrl = String;
pub type ProjectName = String;

103
tests/test_main.rs Normal file
View file

@ -0,0 +1,103 @@
use assert_cmd::prelude::*;
use predicates::prelude::*;
use std::env;
use std::fs::{remove_dir_all, File};
use std::io::prelude::*;
use std::path::Path;
use std::process::Command;
const GAME: &str = "my-awesome-game";
fn ftw() -> std::process::Command {
Command::cargo_bin("ftw").unwrap()
}
fn setup() {}
fn teardown() {
remove_dir_all(GAME);
}
fn test_generated_project() {
assert!(Path::new(GAME).exists());
let gitignore_file_path = format!("{}/.gitignore", GAME);
let mut gitignore_file = File::open(gitignore_file_path).unwrap();
let mut gitignore_file_contents = String::new();
gitignore_file
.read_to_string(&mut gitignore_file_contents)
.unwrap();
assert!(gitignore_file_contents.contains("export_presets.cfg"));
}
#[test]
fn test_ftw_new() -> Result<(), Box<dyn std::error::Error>> {
setup();
let current_path = env::current_dir()?;
ftw()
.arg("new")
.arg(GAME)
.assert()
.success()
.stdout(predicate::str::contains(format!(
"Done! New project created {}/{}",
current_path.display(),
GAME
)));
test_generated_project();
teardown();
Ok(())
}
#[test]
fn test_ftw_new_failure() -> Result<(), Box<dyn std::error::Error>> {
setup();
ftw()
.arg("new")
.assert()
.failure()
.stderr(predicate::str::contains(
"The following required arguments were not provided:\n <project_name>\n",
));
teardown();
Ok(())
}
#[test]
fn test_ftw_new_with_default_template() -> Result<(), Box<dyn std::error::Error>> {
setup();
let current_path = env::current_dir()?;
ftw()
.arg("new")
.arg(GAME)
.arg("default")
.assert()
.success()
.stdout(predicate::str::contains(format!(
"Done! New project created {}/{}",
current_path.display(),
GAME
)));
test_generated_project();
teardown();
Ok(())
}
#[test]
fn test_ftw_new_with_custom_template() -> Result<(), Box<dyn std::error::Error>> {
setup();
let current_path = env::current_dir()?;
ftw()
.arg("new")
.arg(GAME)
.arg("../ftw")
.assert()
.success()
.stdout(predicate::str::contains(format!(
"Done! New project created {}/{}",
current_path.display(),
GAME
)));
test_generated_project();
teardown();
Ok(())
}