Skip to content

Commit

Permalink
new: Add variable interpolation support to templates. (moonrepo#303)
Browse files Browse the repository at this point in the history
* Pull in tera.

* Start on variables.

* Test variables.

* Add path interpolation.

* Test rendering.

* Rework path interpolation.

* Update config docs.

* More docs and var work.

* Polish.
  • Loading branch information
milesj committed Sep 14, 2022
1 parent 982c4a9 commit 0a69832
Show file tree
Hide file tree
Showing 33 changed files with 871 additions and 100 deletions.
5 changes: 5 additions & 0 deletions .moon/workspace.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ node:
typescript:
syncProjectReferences: true

generator:
templates:
- './templates'
- './tests/fixtures/generator/templates'

runner:
logRunningCommand: true

Expand Down
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions crates/cli/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,12 @@ pub enum Commands {
#[clap(help = "Destination path, relative from the current working directory")]
dest: Option<String>,

#[clap(
long,
help = "Use the default value of all variables instead of prompting"
)]
defaults: bool,

#[clap(long, help = "Run entire generator process without writing files")]
dry_run: bool,

Expand Down
146 changes: 139 additions & 7 deletions crates/cli/src/commands/generate.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use crate::helpers::load_workspace;
use console::Term;
use dialoguer::{Confirm, Input};
use moon_generator::{FileState, Generator};
use dialoguer::{theme::Theme, Confirm, Input, MultiSelect, Select};
use moon_config::TemplateVariable;
use moon_error::MoonError;
use moon_generator::{FileState, Generator, GeneratorError, Template, TemplateContext};
use moon_logger::color;
use moon_terminal::create_theme;
use moon_utils::path;
Expand All @@ -10,12 +12,139 @@ use std::path::PathBuf;

#[derive(Debug)]
pub struct GenerateOptions {
pub defaults: bool,
pub dest: Option<String>,
pub dry_run: bool,
pub force: bool,
pub template: bool,
}

fn gather_variables(
template: &Template,
theme: &dyn Theme,
options: &GenerateOptions,
) -> Result<TemplateContext, GeneratorError> {
let mut context = TemplateContext::new();
let error_handler = |e| GeneratorError::Moon(MoonError::Io(e));

for (name, config) in &template.config.variables {
match config {
TemplateVariable::Boolean(var) => {
if options.defaults || var.prompt.is_none() {
context.insert(name, &var.default);
} else {
let value = Confirm::with_theme(theme)
.default(var.default)
.with_prompt(var.prompt.as_ref().unwrap())
.show_default(true)
.interact()
.map_err(error_handler)?;

context.insert(name, &value);
}
}
TemplateVariable::Enum(var) => {
let default_index = var
.values
.iter()
.position(|i| i == &var.default)
.unwrap_or_default();

match (options.defaults, var.multiple.unwrap_or_default()) {
(true, true) => {
context.insert(name, &[&var.values[default_index]]);
}
(true, false) => {
context.insert(name, &var.values[default_index]);
}
(false, true) => {
let indexes = MultiSelect::with_theme(theme)
.with_prompt(&var.prompt)
.items(&var.values)
.defaults(
&var.values
.iter()
.enumerate()
.map(|(i, _)| i == default_index)
.collect::<Vec<bool>>(),
)
.interact()
.map_err(error_handler)?;

context.insert(
name,
&indexes
.iter()
.map(|i| var.values[*i].clone())
.collect::<Vec<String>>(),
);
}
(false, false) => {
let index = Select::with_theme(theme)
.with_prompt(&var.prompt)
.default(default_index)
.items(&var.values)
.interact()
.map_err(error_handler)?;

context.insert(name, &var.values[index]);
}
};
}
TemplateVariable::Number(var) => {
let required = var.required.unwrap_or_default();

if options.defaults || var.prompt.is_none() {
context.insert(name, &var.default);
} else {
let value: i32 = Input::with_theme(theme)
.default(var.default)
.with_prompt(var.prompt.as_ref().unwrap())
.allow_empty(false)
.show_default(true)
.validate_with(|input: &i32| -> Result<(), &str> {
if required && *input == 0 {
Err("a non-zero value is required")
} else {
Ok(())
}
})
.interact_text()
.map_err(error_handler)?;

context.insert(name, &value);
}
}
TemplateVariable::String(var) => {
let required = var.required.unwrap_or_default();

if options.defaults || var.prompt.is_none() {
context.insert(name, &var.default);
} else {
let value: String = Input::with_theme(theme)
.default(var.default.clone())
.with_prompt(var.prompt.as_ref().unwrap())
.allow_empty(false)
.show_default(!var.default.is_empty())
.validate_with(|input: &String| -> Result<(), &str> {
if required && input.is_empty() {
Err("a value is required")
} else {
Ok(())
}
})
.interact_text()
.map_err(error_handler)?;

context.insert(name, &value);
}
}
}
}

Ok(context)
}

pub async fn generate(
name: &str,
options: GenerateOptions,
Expand Down Expand Up @@ -57,17 +186,20 @@ pub async fn generate(
term.flush()?;

// Determine the destination path
let relative_dest = match options.dest {
Some(d) => d,
let relative_dest = match &options.dest {
Some(d) => d.clone(),
None => Input::with_theme(&theme)
.with_prompt("Where to generate code to?")
.allow_empty(false)
.interact_text()?,
};
let dest = path::normalize(cwd.join(&relative_dest));

// Gather variables and build context
let context = gather_variables(&template, &theme, &options)?;

// Load template files and determine when to overwrite
template.load_files(&dest).await?;
template.load_files(&dest, &context).await?;

for file in &mut template.files {
if file.dest_path.exists()
Expand All @@ -85,7 +217,7 @@ pub async fn generate(

// Generate the files in the destination and print the results
if !options.dry_run {
generator.generate(&template.files).await?;
generator.generate(&template, &context).await?;
}

term.write_line("")?;
Expand All @@ -106,7 +238,7 @@ pub async fn generate(
},
color::muted_light(
PathBuf::from(&relative_dest)
.join(file.path)
.join(&file.name)
.to_string_lossy()
)
))?;
Expand Down
2 changes: 2 additions & 0 deletions crates/cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,15 @@ pub async fn run_cli() {
Commands::Generate {
name,
dest,
defaults,
dry_run,
force,
template,
} => {
generate(
name,
GenerateOptions {
defaults: *defaults,
dest: dest.clone(),
dry_run: *dry_run,
force: *force,
Expand Down
65 changes: 62 additions & 3 deletions crates/cli/tests/generate_test.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
use insta::assert_snapshot;
use moon_utils::test::{create_moon_command, create_sandbox, get_assert_output};
use predicates::prelude::*;
use std::fs;

fn get_path_safe_output(assert: &assert_cmd::assert::Assert) -> String {
let output = get_assert_output(&assert);

output.replace('\\', "/")
get_assert_output(assert).replace('\\', "/")
}

#[test]
Expand Down Expand Up @@ -86,3 +85,63 @@ fn overwrites_existing_files_when_forced() {
assert!(fixture.path().join("test/folder/nested-file.ts").exists());
assert!(!fixture.path().join("test/template.yml").exists());
}

#[test]
fn overwrites_existing_files_when_interpolated_path() {
let fixture = create_sandbox("generator");

create_moon_command(fixture.path())
.arg("generate")
.arg("vars")
.arg("./test")
.arg("--defaults")
.assert();

let assert = create_moon_command(fixture.path())
.arg("generate")
.arg("vars")
.arg("./test")
.arg("--defaults")
.arg("--force")
.assert();

assert_snapshot!(get_path_safe_output(&assert));

// file-[stringNotEmpty]-[number].txt
assert!(fixture.path().join("./test/file-default-0.txt").exists());
}

#[test]
fn renders_and_interpolates_templates() {
let fixture = create_sandbox("generator");

let assert = create_moon_command(fixture.path())
.arg("generate")
.arg("vars")
.arg("./test")
.arg("--defaults")
.assert();

assert.success();

assert_snapshot!(fs::read_to_string(fixture.path().join("./test/expressions.txt")).unwrap());
assert_snapshot!(fs::read_to_string(fixture.path().join("./test/control.txt")).unwrap());
}

#[test]
fn interpolates_destination_path() {
let fixture = create_sandbox("generator");

let assert = create_moon_command(fixture.path())
.arg("generate")
.arg("vars")
.arg("./test")
.arg("--defaults")
.assert();

// Verify output paths are correct
assert_snapshot!(get_path_safe_output(&assert));

// file-[stringNotEmpty]-[number].txt
assert!(fixture.path().join("./test/file-default-0.txt").exists());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
source: crates/cli/tests/generate_test.rs
assertion_line: 143
expression: get_path_safe_output(&assert)
---

Variable testing
A template for testing all variable config combinations.


created --> ./test/control.txt
created --> ./test/expressions.txt
created --> ./test/file-default-0.txt



Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
source: crates/cli/tests/generate_test.rs
assertion_line: 108
expression: get_path_safe_output(&assert)
---

Variable testing
A template for testing all variable config combinations.


replaced -> ./test/control.txt
replaced -> ./test/expressions.txt
replaced -> ./test/file-default-0.txt



Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
source: crates/cli/tests/generate_test.rs
assertion_line: 103
expression: "fs::read_to_string(fixture.path().join(\"./test/control.txt\")).unwrap()"
---

Should show


Looping multenum:

1. b


Including partial:
THIS WAS INCLUDED


Filters:
DEFAULT
246
1

Loading

0 comments on commit 0a69832

Please sign in to comment.