Skip to content

Commit c376ade

Browse files
author
Vincent Prouillet
committedFeb 15, 2018
Initial commit
0 parents  commit c376ade

15 files changed

+323
-0
lines changed
 

‎.editorconfig

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[*.rs]
2+
end_of_line = lf
3+
charset = utf-8
4+
indent_style = space
5+
indent_size = 4

‎.gitignore

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/*.iml
2+
/.idea
3+
target/
4+
Cargo.lock
5+
notes
6+
out.html
7+
*.bench
8+
node_modules
9+
yarn.lock
10+
public/
11+
*.css
12+
Hello/
13+
clicli/

‎Cargo.toml

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
name = "kickstart"
3+
version = "0.0.1"
4+
description = "A simple way to get started with a project"
5+
authors = ["Vincent Prouillet <prouillet.vincent@gmail.com>"]
6+
license = "MIT"
7+
8+
[[bin]]
9+
name = "kickstart"
10+
11+
[dependencies]
12+
clap = "2"
13+
tera = "0.11"
14+
toml = "0.4"
15+
walkdir = "2"

‎LICENSE

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2015 Vincent Prouillet
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.
22+

‎README.md

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# kickstart
2+
3+
A very WIP equivalent of [cookiecutter](https://github.com/audreyr/cookiecutter)
4+
in Rust.
5+
6+
## Run on examples
7+
8+
- cargo run -- examples/super-basic
9+
- cargo run -- examples/rust-cli -o clicli
10+
11+
## TODO
12+
13+
- Use template from git urls

‎examples/rust-cli/Cargo.toml

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[package]
2+
name = "{{bin_name}}"
3+
version = "0.1.0"
4+
authors = ["{{author_name}} <{{author_mail}}>"]
5+
description = "{{description}}"
6+
7+
[dependencies]
8+
{%- if use_clap %}
9+
clap = "2"
10+
{% endif -%}

‎examples/rust-cli/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# {{ bin_name | capitalize }}

‎examples/rust-cli/src/main.rs

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{% if use_clap %}
2+
#[macro_use]
3+
extern crate clap;
4+
{% endif %}
5+
6+
fn main() {
7+
{% if use_clap -%}
8+
let app = App::new("{{bin_name}}")
9+
.version(crate_version!())
10+
.author(crate_authors!())
11+
.about(crate_description!());
12+
13+
let matches = app.get_matches();
14+
{%- else -%}
15+
println!("Hello, world!");
16+
{%- endif %}
17+
}

‎examples/rust-cli/template.toml

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[bin_name]
2+
default = "Bin"
3+
question = "Binary name?"
4+
5+
[description]
6+
default = "Some CLI app"
7+
question = "A description for that binary?"
8+
9+
[author_name]
10+
default = "Vincent"
11+
question = "Your name?"
12+
13+
[author_mail]
14+
default = "spam@spam.com"
15+
question = "Your email?"
16+
17+
[use_clap]
18+
default = true
19+
question = "Do you want clap set-up?"

‎examples/super-basic/template.toml

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
[directory_name]
2+
default = "Hello"
3+
question = "Directory name?"
4+
5+
[file_name]
6+
default = "Howdy"
7+
question = "File name?"
8+
9+
[greeting_recipient]
10+
default = "Vincent"
11+
question = "Who am I greeting?"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
print("Hello, {{greeting_recipient}}!")

‎src/cli.rs

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
use clap::{App, Arg};
2+
3+
pub fn build_cli() -> App<'static, 'static> {
4+
App::new("kickstart")
5+
.version(crate_version!())
6+
.author(crate_authors!())
7+
.about(crate_description!())
8+
.arg(
9+
Arg::with_name("template")
10+
.required(true)
11+
.help("Template to use: a local path or a url")
12+
)
13+
.arg(
14+
Arg::with_name("output_dir")
15+
.short("o")
16+
.long("output_dir")
17+
.takes_value(true)
18+
.help("Where to output the project: defaults to the current directory")
19+
)
20+
.subcommands(vec![])
21+
}

‎src/main.rs

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#[macro_use]
2+
extern crate clap;
3+
extern crate tera;
4+
extern crate toml;
5+
extern crate walkdir;
6+
7+
use std::env;
8+
use std::path::Path;
9+
10+
mod cli;
11+
mod template;
12+
mod prompt;
13+
14+
use template::Template;
15+
16+
17+
fn main() {
18+
let matches = cli::build_cli().get_matches();
19+
let template_path = matches.value_of("template").unwrap();
20+
let output_dir = matches.value_of("output_dir")
21+
.map(|p| Path::new(p).to_path_buf())
22+
.unwrap_or_else(|| env::current_dir().unwrap());
23+
let template = Template::from_cli(template_path);
24+
template.generate(output_dir);
25+
}

‎src/prompt.rs

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
use std::io::{self, Write, BufRead};
2+
3+
4+
/// Wait for user input and return what they typed
5+
fn read_line() -> String {
6+
let stdin = io::stdin();
7+
let stdin = stdin.lock();
8+
let mut lines = stdin.lines();
9+
lines
10+
.next()
11+
.and_then(|l| l.ok())
12+
.unwrap()
13+
}
14+
15+
/// Ask a yes/no question to the user
16+
pub fn ask_bool(question: &str, default: bool) -> bool {
17+
print!("{} {}: ", question, if default { "[Y/n]" } else { "[y/N]" });
18+
let _ = io::stdout().flush();
19+
let input = read_line();
20+
21+
match &*input {
22+
"y" | "Y" | "yes" | "YES" | "true" => true,
23+
"n" | "N" | "no" | "NO" | "false" => false,
24+
"" => default,
25+
_ => {
26+
println!("Invalid choice: '{}'", input);
27+
ask_bool(question, default)
28+
},
29+
}
30+
}
31+
32+
/// Ask a question to the user where they can write any string
33+
pub fn ask_string(question: &str, default: &str) -> String {
34+
print!("{} ({}): ", question, default);
35+
let _ = io::stdout().flush();
36+
let input = read_line();
37+
38+
match &*input {
39+
"" => default.to_string(),
40+
_ => input,
41+
}
42+
}

‎src/template.rs

+108
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
use std::fs::{File, create_dir_all};
2+
use std::io::prelude::*;
3+
use std::path::{Path, PathBuf};
4+
5+
use toml::{self, Value as TomlValue};
6+
use tera::{Tera, Context};
7+
use walkdir::WalkDir;
8+
9+
use prompt::{ask_string, ask_bool};
10+
11+
12+
// TODO: error handling
13+
pub fn read_file(p: &Path) -> String {
14+
let mut f = File::open(p).expect("file not found");
15+
16+
let mut contents = String::new();
17+
f.read_to_string(&mut contents)
18+
.expect("something went wrong reading the file");
19+
20+
contents
21+
}
22+
23+
pub fn write_file(p: &Path, contents: &str) {
24+
let mut f = File::create(p).expect("Unable to create file");
25+
f.write_all(contents.as_bytes()).expect("Unable to write data");
26+
}
27+
28+
pub fn create_directory(path: &Path) {
29+
if !path.exists() {
30+
create_dir_all(path).unwrap();
31+
}
32+
}
33+
34+
#[derive(Debug, PartialEq)]
35+
pub struct Template {
36+
path: PathBuf,
37+
name: String,
38+
}
39+
40+
impl Template {
41+
pub fn from_cli(path: &str) -> Template {
42+
// TODO: templates can be from git as well, not only local
43+
let path = Path::new(path);
44+
let name = path.file_name().unwrap().to_string_lossy();
45+
46+
Template {
47+
path: path.to_path_buf(),
48+
name: name.to_string(),
49+
}
50+
}
51+
52+
pub fn download_from_vcs(&self) {
53+
// TODO
54+
}
55+
56+
fn ask_questions(&self, conf: TomlValue) -> Context {
57+
let table = conf.as_table().unwrap();
58+
let mut context = Context::new();
59+
60+
for (key, data) in table {
61+
let question = data["question"].as_str().unwrap();
62+
if let Some(b) = data["default"].as_bool() {
63+
let res = ask_bool(question, b);
64+
context.add(key, &res);
65+
66+
} else {
67+
let res = ask_string(question, data["default"].as_str().unwrap());
68+
context.add(key, &res);
69+
};
70+
}
71+
72+
context
73+
}
74+
75+
// TODO: error handling
76+
pub fn generate(&self, output_dir: PathBuf) {
77+
// Get the variables from the user first
78+
let conf_path = self.path.join("template.toml");
79+
if !conf_path.exists() {
80+
panic!("template.toml missing")
81+
}
82+
let conf: TomlValue = toml::from_str(&read_file(&conf_path)).unwrap();
83+
let context = self.ask_questions(conf);
84+
85+
if !output_dir.exists() {
86+
create_directory(&output_dir);
87+
}
88+
89+
// And now generate the files (in current directory for now)
90+
for entry in WalkDir::new(&self.path).into_iter().filter_map(|e| e.ok()) {
91+
// Skip root folder and the template.toml
92+
if entry.path() == self.path || entry.path() == conf_path {
93+
continue;
94+
}
95+
let path = entry.path().strip_prefix(&self.path).unwrap();
96+
// TODO: only render tpl if {{..}}
97+
let tpl = Tera::one_off(&format!("{}", path.display()), &context, false).unwrap();
98+
let real_path = Path::new(&tpl);
99+
100+
if entry.path().is_dir() {
101+
create_directory(&output_dir.join(real_path));
102+
} else {
103+
let contents = Tera::one_off(&read_file(&entry.path()), &context, false).unwrap();
104+
write_file(&output_dir.join(real_path), &contents);
105+
}
106+
}
107+
}
108+
}

0 commit comments

Comments
 (0)
Please sign in to comment.