Skip to content

Commit

Permalink
Hooks implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
Keats committed Dec 5, 2024
1 parent 9c345e8 commit d1c806c
Show file tree
Hide file tree
Showing 8 changed files with 315 additions and 118 deletions.
3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@ heck = "0.5"
term = "1"
toml = "0.8"
walkdir = "2"

[dev-dependencies]
tempfile = "3"


[badges]
maintenance = {status = "actively-developed"}
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,25 @@ cleanup = [
{ name = "auth_method", value = "none", paths = ["{{ project_name }}/docs/auth.md"]},
]

# A list of hooks we can run at various stages of the template.
# This will execute the given files in the given order and they will be templated with access to all the variables.
# Hooks can also be run conditionally depending on a variable value.
# If a hook is meant to fail, make sure to exit with a non 0 error code.
# The files need to be executable, no restrictions otherwise. It can be python, bash, bat etc.

# pre-gen hooks are run after all the questions have been answered. This can be used for example to do more complex
# validations
pre_gen_hooks = [
{ name = "validate", path = "validate_vars.py" },
]

# post-gen hooks are run after the generation is done. This can be used for additional cleanup or running other things
# like `git init`, install git hooks, downloading dependencies etc
post_gen_hooks = [
{ name = "finish setup", path = "finish_setup.sh" },
{ name = "install frontend dependencies", path = "install_spa_deps.sh", only_if = { name = "spa", value = true} },
]

# A list of variables, the schema is explained in detail below
[[variables]]
name = "project_name"
Expand Down
157 changes: 131 additions & 26 deletions src/bin/kickstart.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
use std::collections::HashMap;
use std::error::Error;
use std::path::PathBuf;

use clap::{Parser, Subcommand};
use tera::Context;
use toml::Value;

use kickstart::generation::Template;
use kickstart::terminal;
use kickstart::validate::validate_file;
use kickstart::errors::{new_error, ErrorKind, Result};
use kickstart::prompt::{ask_bool, ask_choices, ask_integer, ask_string};
use kickstart::utils::render_one_off_template;
use kickstart::validate_file;
use kickstart::Template;
use kickstart::TemplateDefinition;
use kickstart::{terminal, HookFile};

#[derive(Parser)]
#[clap(version, author, about, subcommand_negates_reqs = true)]
Expand Down Expand Up @@ -41,44 +48,142 @@ pub enum Command {
},
}

/// Ask all the questions of that template and return the answers.
/// If `no_input` is `true`, it will automatically pick the defaults without
/// prompting the user
fn ask_questions(
definition: &TemplateDefinition,
no_input: bool,
) -> Result<HashMap<String, Value>> {
let mut vals = HashMap::new();

for var in &definition.variables {
// Skip the question if the value is different from the condition
if let Some(ref cond) = var.only_if {
if let Some(val) = vals.get(&cond.name) {
if *val != cond.value {
continue;
}
} else {
// Not having it means we didn't even ask the question
continue;
}
}

if let Some(ref choices) = var.choices {
let res = if no_input {
var.default.clone()
} else {
ask_choices(&var.prompt, &var.default, choices)?
};
vals.insert(var.name.clone(), res);
continue;
}

match &var.default {
Value::Boolean(b) => {
let res = if no_input { *b } else { ask_bool(&var.prompt, *b)? };
vals.insert(var.name.clone(), Value::Boolean(res));
continue;
}
Value::String(s) => {
let default_value = if s.contains("{{") && s.contains("}}") {
let mut context = Context::new();
for (key, val) in &vals {
context.insert(key, val);
}

let rendered_default = render_one_off_template(s, &context, None);
match rendered_default {
Err(e) => return Err(e),
Ok(v) => v,
}
} else {
s.clone()
};

let res = if no_input {
default_value
} else {
ask_string(&var.prompt, &default_value, &var.validation)?
};

vals.insert(var.name.clone(), Value::String(res));
continue;
}
Value::Integer(i) => {
let res = if no_input { *i } else { ask_integer(&var.prompt, *i)? };
vals.insert(var.name.clone(), Value::Integer(res));
continue;
}
_ => return Err(new_error(ErrorKind::InvalidTemplate)),
}
}

Ok(vals)
}

fn bail(e: &dyn Error) -> ! {
terminal::error(&format!("Error: {}", e));
let mut cause = e.source();
while let Some(e) = cause {
terminal::error(&format!("Reason: {}", e));
terminal::error(&format!("\nReason: {}", e));
cause = e.source();
}
::std::process::exit(1)
}

macro_rules! bail_if_err {
($expr:expr) => {{
match $expr {
Ok(v) => v,
Err(e) => bail(&e),
}
}};
}

fn execute_hook(hook: &HookFile) -> Result<()> {
todo!("write me")
}

fn main() {
let cli = Cli::parse();

if let Some(Command::Validate { path }) = cli.command {
let errs = match validate_file(path) {
Ok(e) => e,
Err(e) => bail(&e),
};
match cli.command {
Some(Command::Validate { path }) => {
let errs = bail_if_err!(validate_file(path));

if !errs.is_empty() {
terminal::error("The template.toml is invalid:\n");
for err in errs {
terminal::error(&format!("- {}\n", err));
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");
}
::std::process::exit(1);
} else {
terminal::success("The template.toml file is valid!\n");
}
} else {
let template = match Template::from_input(&cli.template.unwrap(), cli.directory.as_deref())
{
Ok(t) => t,
Err(e) => bail(&e),
};
None => {
let template = bail_if_err!(Template::from_input(
&cli.template.unwrap(),
cli.directory.as_deref()
));

match template.generate(&cli.output_dir, cli.no_input) {
Ok(_) => terminal::success("\nEverything done, ready to go!\n"),
Err(e) => bail(&e),
};
// 1. ask questions
let variables = bail_if_err!(ask_questions(&template.definition, cli.no_input));

// 2. run pre-gen hooks
let pre_gen_hooks = bail_if_err!(template.get_pre_gen_hooks());
if !pre_gen_hooks.is_empty() {}

// 3. generate
bail_if_err!(template.generate(&cli.output_dir, &variables));

// 4. run post-gen hooks
let post_gen_hooks = bail_if_err!(template.get_post_gen_hooks());
if !post_gen_hooks.is_empty() {}

terminal::success("\nEverything done, ready to go!\n");
}
}
}
Loading

0 comments on commit d1c806c

Please sign in to comment.