Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update #62

Merged
merged 5 commits into from
Aug 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
855 changes: 634 additions & 221 deletions Cargo.lock

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
[package]
authors = ["Vincent Prouillet <hello@vincentprouillet.com>"]
description = "A simple way to get started with a project by scaffolding from a template powered by the Tera engine"
edition = "2018"
edition = "2021"
keywords = ["tera", "scaffolding", "templating", "generator", "boilerplate"]
license = "MIT"
name = "kickstart"
version = "0.3.0"
version = "0.4.0"

[dependencies]
clap = "2"
clap = { version = "4", features = ["derive"] }
glob = "0.3"
memchr = "2"
regex = "1"
serde = {version = "1", features = ["derive"]}
tera = "1"
heck = "0.4.0"
term = "0.7"
toml = "0.5"
toml = "0.7"
walkdir = "2"

[dev-dependencies]
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,12 @@ You can use these like any other filter, e.g. `{{variable_name | camel_case}}`.

## Changelog

### 0.3.0 (unreleased)
### 0.4.0 (unrelased)

- Add case conversion filter
- Update dependencies

### 0.3.0 (2021-07-10)

- Update dependencies

Expand Down
10 changes: 5 additions & 5 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,17 @@ stages:
strategy:
matrix:
windows-stable:
imageName: 'vs2017-win2016'
imageName: 'windows-2022'
rustup_toolchain: stable
mac-stable:
imageName: 'macos-10.14'
imageName: 'macos-13'
rustup_toolchain: stable
linux-stable:
imageName: 'ubuntu-latest'
rustup_toolchain: stable
linux-msrv:
imageName: 'ubuntu-latest'
rustup_toolchain: 1.40.0
rustup_toolchain: 1.65.0
pool:
vmImage: $(imageName)
steps:
Expand Down Expand Up @@ -52,11 +52,11 @@ stages:
strategy:
matrix:
windows-stable:
imageName: 'vs2017-win2016'
imageName: 'windows-2022'
rustup_toolchain: stable
target: 'x86_64-pc-windows-msvc'
mac-stable:
imageName: 'macos-10.14'
imageName: 'macos-13'
rustup_toolchain: stable
target: 'x86_64-apple-darwin'
linux-stable:
Expand Down
124 changes: 54 additions & 70 deletions src/bin/kickstart.rs
Original file line number Diff line number Diff line change
@@ -1,46 +1,42 @@
use std::env;
use std::error::Error;
use std::path::Path;
use std::path::PathBuf;

use clap::{crate_authors, crate_description, crate_version, App, AppSettings, Arg, SubCommand};
use clap::{Parser, Subcommand};

use kickstart::generation::Template;
use kickstart::terminal;
use kickstart::validate::validate_file;

pub fn build_cli() -> App<'static, 'static> {
App::new("kickstart")
.version(crate_version!())
.author(crate_authors!())
.about(crate_description!())
.setting(AppSettings::SubcommandsNegateReqs)
.arg(
Arg::with_name("template")
.required(true)
.help("Template to use: a local path or a HTTP url pointing to a Git repository"),
)
.arg(
Arg::with_name("output-dir")
.short("o")
.long("output-dir")
.takes_value(true)
.help("Where to output the project: defaults to the current directory"),
)
.arg(
Arg::with_name("sub-dir")
.short("s")
.long("sub-dir")
.takes_value(true)
.help("A subdirectory of the chosen template to use, to allow nested templates."),
)
.arg(
Arg::with_name("no-input")
.long("no-input")
.help("Do not prompt for parameters and only use the defaults from template.toml"),
)
.subcommands(vec![SubCommand::with_name("validate")
.about("Validates that a template.toml is valid")
.arg(Arg::with_name("path").required(true).help("The path to the template.toml"))])
#[derive(Parser)]
#[clap(version, author, about, subcommand_negates_reqs = true)]
pub struct Cli {
/// Template to use: a local path or a HTTP url pointing to a Git repository
#[clap(required = true)]
pub template: Option<String>,

/// Where to output the project: defaults to the current directory
#[clap(short = 'o', long, default_value = ".")]
pub output_dir: PathBuf,

/// A subdirectory of the chosen template to use, to allow nested templates.
#[clap(short = 's', long)]
pub sub_dir: Option<String>,

/// Do not prompt for parameters and only use the defaults from template.toml
#[clap(long, default_value_t = false)]
pub no_input: bool,

#[clap(subcommand)]
pub command: Option<Command>,
}

#[derive(Debug, Subcommand)]
pub enum Command {
/// Validates that a template.toml is valid
Validate {
/// The path to the template.toml
path: PathBuf,
},
}

fn bail(e: &dyn Error) -> ! {
Expand All @@ -54,44 +50,32 @@ fn bail(e: &dyn Error) -> ! {
}

fn main() {
let matches = build_cli().get_matches();
let cli = Cli::parse();

match matches.subcommand() {
("validate", Some(matches)) => {
let errs = match validate_file(matches.value_of("path").unwrap()) {
Ok(e) => e,
Err(e) => bail(&e),
};
if let Some(Command::Validate { path }) = cli.command {
let errs = match validate_file(path) {
Ok(e) => e,
Err(e) => bail(&e),
};

if !errs.is_empty() {
terminal::error("The template.toml is invalid:\n");
for err in errs {
terminal::error(&format!("- {}\n", err));
}
::std::process::exit(1);
} else {
terminal::success("The template.toml file is valid!\n");
if !errs.is_empty() {
terminal::error("The template.toml is invalid:\n");
for err in errs {
terminal::error(&format!("- {}\n", err));
}
::std::process::exit(1);
} else {
terminal::success("The template.toml file is valid!\n");
}
_ => {
// The actual generation call
let template_path = matches.value_of("template").unwrap();
let output_dir = matches
.value_of("output-dir")
.map(|p| Path::new(p).to_path_buf())
.unwrap_or_else(|| env::current_dir().unwrap());
let no_input = matches.is_present("no-input");
let sub_dir = matches.value_of("sub-dir");
} else {
let template = match Template::from_input(&cli.template.unwrap(), cli.sub_dir.as_deref()) {
Ok(t) => t,
Err(e) => bail(&e),
};

let template = match Template::from_input(template_path, sub_dir) {
Ok(t) => t,
Err(e) => bail(&e),
};

match template.generate(&output_dir, no_input) {
Ok(_) => terminal::success("\nEverything done, ready to go!\n"),
Err(e) => bail(&e),
};
}
match template.generate(&cli.output_dir, cli.no_input) {
Ok(_) => terminal::success("\nEverything done, ready to go!\n"),
Err(e) => bail(&e),
};
}
}
15 changes: 7 additions & 8 deletions src/definition.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
use std::collections::{HashMap};
use std::collections::HashMap;

use regex::{Regex, Match};
use serde::Deserialize;
use tera::{Context};
use tera::Context;
use toml::Value;

use crate::errors::{new_error, ErrorKind, Result};
use crate::prompt::{ask_bool, ask_choices, ask_integer, ask_string};
use crate::utils::{render_one_off_template};
use crate::utils::render_one_off_template;

/// A condition for a question to be asked
#[derive(Debug, Clone, PartialEq, Deserialize)]
Expand Down Expand Up @@ -122,10 +121,10 @@ impl TemplateDefinition {
context.insert(key, val);
}

let rendered_default = render_one_off_template(&s, &context, None);
let rendered_default = render_one_off_template(s, &context, None);
match rendered_default {
Err(e) => return Err(e),
Ok(v ) => v,
Ok(v) => v,
}
} else {
s.clone()
Expand Down Expand Up @@ -303,7 +302,7 @@ mod tests {
assert_eq!(tpl.variables.len(), 3);

let res = tpl.ask_questions(true);

assert!(res.is_ok());
let res = res.unwrap();

Expand All @@ -314,6 +313,6 @@ mod tests {
let got_value = res.get("manifest").unwrap();
let expected_value: String = String::from("my_project-other_project-manifest.md");

assert_eq!(got_value, &Value::String(expected_value))
assert_eq!(got_value, &Value::String(expected_value))
}
}
14 changes: 7 additions & 7 deletions src/filters.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,35 +15,35 @@ pub fn register_all_filters(tera: &mut Tera) {

pub fn upper_camel_case(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
let s = try_get_value!("upper_camel_case", "value", String, value);
Ok(to_value(&s.to_upper_camel_case()).unwrap())
Ok(to_value(s.to_upper_camel_case()).unwrap())
}

pub fn camel_case(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
let s = try_get_value!("camel_case", "value", String, value);
Ok(to_value(&s.to_lower_camel_case()).unwrap())
Ok(to_value(s.to_lower_camel_case()).unwrap())
}

pub fn snake_case(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
let s = try_get_value!("snake_case", "value", String, value);
Ok(to_value(&s.to_snake_case()).unwrap())
Ok(to_value(s.to_snake_case()).unwrap())
}

pub fn kebab_case(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
let s = try_get_value!("kebab_case", "value", String, value);
Ok(to_value(&s.to_kebab_case()).unwrap())
Ok(to_value(s.to_kebab_case()).unwrap())
}

pub fn shouty_snake_case(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
let s = try_get_value!("shouty_snake_case", "value", String, value);
Ok(to_value(&s.to_shouty_snake_case()).unwrap())
Ok(to_value(s.to_shouty_snake_case()).unwrap())
}

pub fn title_case(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
let s = try_get_value!("title_case", "value", String, value);
Ok(to_value(&s.to_title_case()).unwrap())
Ok(to_value(s.to_title_case()).unwrap())
}

pub fn shouty_kebab_case(value: &Value, _: &HashMap<String, Value>) -> Result<Value> {
let s = try_get_value!("shouty_kebab_case", "value", String, value);
Ok(to_value(&s.to_shouty_kebab_case()).unwrap())
Ok(to_value(s.to_shouty_kebab_case()).unwrap())
}
20 changes: 11 additions & 9 deletions src/generation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ use std::process::Command;
use std::str;

use glob::Pattern;
use tera::{Context};
use tera::Context;
use walkdir::WalkDir;

use crate::definition::TemplateDefinition;
use crate::errors::{map_io_err, new_error, ErrorKind, Result};
use crate::utils::{render_one_off_template, create_directory, get_source, is_binary, read_file, write_file, Source};
use crate::utils::{
create_directory, get_source, is_binary, read_file, render_one_off_template, write_file, Source,
};

/// The current template being generated
#[derive(Debug, PartialEq)]
Expand Down Expand Up @@ -46,7 +48,7 @@ impl Template {
// on some platforms:
// https://www.reddit.com/r/rust/comments/92mbk5/kickstart_a_scaffolding_tool_to_get_new_projects/e3ahegw
Command::new("git")
.args(&["clone", "--recurse-submodules", remote, &format!("{}", tmp.display())])
.args(["clone", "--recurse-submodules", remote, &format!("{}", tmp.display())])
.output()
.map_err(|err| new_error(ErrorKind::Git { err }))?;
Ok(Template::from_local(&tmp, sub_dir))
Expand Down Expand Up @@ -79,15 +81,15 @@ impl Template {

if !output_dir.exists() {
println!("Creating {:?}", output_dir);
create_directory(&output_dir)?;
create_directory(output_dir)?;
}

// Create the glob patterns of files to copy without rendering first, only once
let patterns: Vec<Pattern> =
definition.copy_without_render.iter().map(|s| Pattern::new(s).unwrap()).collect();

let start_path = if let Some(ref directory) = definition.directory {
self.path.join(&directory)
self.path.join(directory)
} else {
self.path.clone()
};
Expand Down Expand Up @@ -134,19 +136,19 @@ impl Template {
}

// Only pass non-binary files or the files not matching the copy_without_render patterns through Tera
let mut f = File::open(&entry.path())?;
let mut f = File::open(entry.path())?;
let mut buffer = Vec::new();
f.read_to_end(&mut buffer)?;

let no_render = patterns.iter().map(|p| p.matches_path(&real_path)).any(|x| x);

if no_render || is_binary(&buffer) {
map_io_err(fs::copy(&entry.path(), &real_path), entry.path())?;
map_io_err(fs::copy(entry.path(), &real_path), entry.path())?;
continue;
}

let contents = render_one_off_template(
&str::from_utf8(&buffer).unwrap(),
str::from_utf8(&buffer).unwrap(),
&context,
Some(entry.path().to_path_buf()),
)?;
Expand All @@ -158,7 +160,7 @@ impl Template {
if let Some(val) = variables.get(&cleanup.name) {
if *val == cleanup.value {
for p in &cleanup.paths {
let actual_path = render_one_off_template(&p, &context, None)?;
let actual_path = render_one_off_template(p, &context, None)?;
let path_to_delete = output_dir.join(actual_path);
if !path_to_delete.exists() {
continue;
Expand Down
Loading