From 7ad405f388515aca1e31c1bd68c4d2c5fc825761 Mon Sep 17 00:00:00 2001 From: vedoa <> Date: Sat, 29 Nov 2025 21:05:22 +0100 Subject: [PATCH 1/5] Add support for derived variables #79 --- examples/derived/template.toml | 15 ++++++++ examples/derived/{{package_path}}/Main.java | 7 ++++ src/definition.rs | 39 +++++++++++++++++++++ src/main.rs | 6 ++++ 4 files changed, 67 insertions(+) create mode 100644 examples/derived/template.toml create mode 100644 examples/derived/{{package_path}}/Main.java diff --git a/examples/derived/template.toml b/examples/derived/template.toml new file mode 100644 index 0000000..a78e31c --- /dev/null +++ b/examples/derived/template.toml @@ -0,0 +1,15 @@ +name = "Java domain" +description = "Dynamic folder structure" +kickstart_version = 1 + +[[variables]] +name = "package" +default = "my.domain.test" +prompt = "Enter your package name (dot-separated):" +validation = "^[a-z]+(\\.[a-z][a-z0-9_]*)*$" + +[[variables]] +name = "package_path" +default = "{{ package | replace(from='.', to='/')}}" +prompt = "Will not be asked since this is a derived variable" +derived = true diff --git a/examples/derived/{{package_path}}/Main.java b/examples/derived/{{package_path}}/Main.java new file mode 100644 index 0000000..14d1670 --- /dev/null +++ b/examples/derived/{{package_path}}/Main.java @@ -0,0 +1,7 @@ +package {{package}}; + +public class Main { + public static void main(String[] args) { + System.out.println("Hello, World!"); + } +} diff --git a/src/definition.rs b/src/definition.rs index a788eae..03ca22e 100644 --- a/src/definition.rs +++ b/src/definition.rs @@ -42,6 +42,8 @@ pub struct Variable { pub validation: Option, /// Only ask this variable if that condition is true pub only_if: Option, + /// Whether this variable is derived and should not be prompted + pub derived: Option, } /// A hook is a file that will get executed @@ -421,4 +423,41 @@ mod tests { assert_eq!(got_value, &Value::String(expected_value)) } + + #[test] + fn can_handle_derived_variable_with_no_input() { + let tpl: TemplateDefinition = toml::from_str( + r#" + name = "Test template" + description = "Testing derived variable behavior" + kickstart_version = 1 + + [[variables]] + name = "project_name" + default = "My project" + prompt = "What's the name of your project?" + + [[variables]] + name = "slug" + default = "{{project_name | slugify}}" + prompt = "Slug for the project" + derived = true + "#, + ) + .unwrap(); + + assert_eq!(tpl.variables.len(), 2); + + let res = tpl.default_values(); + assert!(res.is_ok()); + let res = res.unwrap(); + + // Check that both variables exist + assert!(res.contains_key("project_name")); + assert!(res.contains_key("slug")); + + // Check that slug was rendered from project_name + let expected_slug = Value::String("my-project".to_string()); + assert_eq!(res.get("slug"), Some(&expected_slug)); + } } diff --git a/src/main.rs b/src/main.rs index 82ff73f..e8c2c9d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -54,6 +54,12 @@ fn ask_questions(template: &Template, no_input: bool) -> Result Date: Wed, 3 Dec 2025 21:12:57 +0100 Subject: [PATCH 2/5] Make prompt optional but required if derived not set to true. Add exmaple to github actions. --- .github/workflows/ci.yaml | 3 +- examples/derived/template.toml | 1 - src/definition.rs | 62 ++++++++++++++++++++++++++++++++-- src/main.rs | 9 ++--- 4 files changed, 66 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e8f2032..7e7cf89 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -44,4 +44,5 @@ jobs: cargo run --features=cli -- examples/default-from-variable/ --no-input -o default cargo run --features=cli -- examples/slugify/ --no-input -o slugify cargo run --features=cli -- examples/super-basic/ --no-input -o super-basic - cargo run --features=cli -- examples/with-directory/ --no-input -o with-directory \ No newline at end of file + cargo run --features=cli -- examples/with-directory/ --no-input -o with-directory + cargo run --features=cli -- examples/derived/ --no-input -o derived \ No newline at end of file diff --git a/examples/derived/template.toml b/examples/derived/template.toml index a78e31c..3d35fae 100644 --- a/examples/derived/template.toml +++ b/examples/derived/template.toml @@ -11,5 +11,4 @@ validation = "^[a-z]+(\\.[a-z][a-z0-9_]*)*$" [[variables]] name = "package_path" default = "{{ package | replace(from='.', to='/')}}" -prompt = "Will not be asked since this is a derived variable" derived = true diff --git a/src/definition.rs b/src/definition.rs index 03ca22e..8c6d8f5 100644 --- a/src/definition.rs +++ b/src/definition.rs @@ -35,7 +35,7 @@ pub struct Variable { /// A default value is required. It can be a Tera expression if it is a string. pub(crate) default: Value, /// The text asked to the user - pub prompt: String, + pub prompt: Option, /// Only for questions with choices pub choices: Option>, /// A regex pattern to validate the input. Only used where the value is meant to be a string. @@ -134,6 +134,22 @@ impl TemplateDefinition { } for var in &self.variables { + if var.prompt.is_none() && !var.derived.unwrap_or(false) { + errs.push(format!( + "Variable `{}` must have either a prompt or be marked as derived", + var.name + )); + } + + if let Some(ref prompt) = var.prompt { + if prompt.trim().is_empty() { + errs.push(format!( + "Variable `{}` has an empty prompt, which is not allowed", + var.name + )); + } + } + let type_str = var.default.type_str(); types.insert(var.name.to_string(), type_str); @@ -425,7 +441,7 @@ mod tests { } #[test] - fn can_handle_derived_variable_with_no_input() { + fn can_handle_derived_variable() { let tpl: TemplateDefinition = toml::from_str( r#" name = "Test template" @@ -440,7 +456,6 @@ mod tests { [[variables]] name = "slug" default = "{{project_name | slugify}}" - prompt = "Slug for the project" derived = true "#, ) @@ -460,4 +475,45 @@ mod tests { let expected_slug = Value::String("my-project".to_string()); assert_eq!(res.get("slug"), Some(&expected_slug)); } + + #[test] + fn fails_if_prompt_and_derived_missing() { + let tpl: TemplateDefinition = toml::from_str( + r#" + name = "Test template" + kickstart_version = 1 + + [[variables]] + name = "broken_var" + default = "some_value" + "#, + ) + .unwrap(); + + let errs = tpl.validate(); + assert!(!errs.is_empty()); + assert!(errs + .iter() + .any(|e| e.contains("must have either a prompt or be marked as derived"))); + } + + #[test] + fn fails_if_prompt_is_empty() { + let tpl: TemplateDefinition = toml::from_str( + r#" + name = "Test template" + kickstart_version = 1 + + [[variables]] + name = "broken_var" + default = "some_value" + prompt = "" + "#, + ) + .unwrap(); + + let errs = tpl.validate(); + assert!(!errs.is_empty()); + assert!(errs.iter().any(|e| e.contains("empty prompt"))); + } } diff --git a/src/main.rs b/src/main.rs index e8c2c9d..090991b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -64,26 +64,27 @@ fn ask_questions(template: &Template, no_input: bool) -> Result { - let res = if no_input { b } else { ask_bool(&var.prompt, b)? }; + let res = if no_input { b } else { ask_bool(prompt_text, b)? }; vals.insert(var.name.clone(), Value::Boolean(res)); continue; } Value::String(s) => { - let res = if no_input { s } else { ask_string(&var.prompt, &s, &var.validation)? }; + let res = if no_input { s } else { ask_string(prompt_text, &s, &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)? }; + let res = if no_input { i } else { ask_integer(prompt_text, i)? }; vals.insert(var.name.clone(), Value::Integer(res)); continue; } From 6f9ef982b9cd8873737c921759cac35f02e6fe5e Mon Sep 17 00:00:00 2001 From: vedoa <> Date: Fri, 5 Dec 2025 17:10:00 +0100 Subject: [PATCH 3/5] Update README --- README.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 759418d..9c5d94f 100644 --- a/README.md +++ b/README.md @@ -141,6 +141,11 @@ default = "my-project" prompt = "What is the name of this project?" validation = "^([a-zA-Z][a-zA-Z0-9_-]+)$" +[[variables]] +name = "slug" +default = "{{ project_name | slugify }}" +derived = true + [[variables]] name = "database" default = "postgres" @@ -204,10 +209,11 @@ A variable has the following required fields: - `name`: the name of the variable in Tera context - `default`: the default value for that question, `kickstart` uses that to deduce the type of that value (only string, bool and integer are currently supported). You can use previous variables in the default, eg `"{{ project_name | lower }}"` will replace `project_name` with the value of the variable. -- `prompt`: the text to display to the user +- `prompt`: the text to display to the user unless the variable is derived -And three more optional fields: +and four more optional fields: +- `derived`: set to `true` if the variable should not prompt the user and instead be computed from default - `choices`: a list of potential values, `kickstart` will make the user pick one - `only_if`: this question will only be asked if the variable `name` has the value `value` - `validation`: a Regex pattern to check when getting a string value @@ -230,6 +236,11 @@ You can use these like any other filter, e.g. `{{variable_name | camel_case}}`. ## Changelog +### 0.x.x (2025-xx-xx) + +- New `derived = true` flag allows variables to be computed from default without prompting the user +- `prompt` remains required for non-derived variables, but is optional when `derived` is set to true + ### 0.5.0 (2024-12-13) - The `sub-dir` parameter has been renamed to `directory` in the CLI From 63a81dec2fd2353a839e12ec59be2b40db21c4f4 Mon Sep 17 00:00:00 2001 From: vedoa <> Date: Mon, 8 Dec 2025 16:29:53 +0100 Subject: [PATCH 4/5] Add version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9c5d94f..5466b87 100644 --- a/README.md +++ b/README.md @@ -236,7 +236,7 @@ You can use these like any other filter, e.g. `{{variable_name | camel_case}}`. ## Changelog -### 0.x.x (2025-xx-xx) +### 0.5.1 (2025-08-12) - New `derived = true` flag allows variables to be computed from default without prompting the user - `prompt` remains required for non-derived variables, but is optional when `derived` is set to true From bac35c1f143822d1f10c6f05b45408f64a28762a Mon Sep 17 00:00:00 2001 From: vedoa <> Date: Mon, 8 Dec 2025 16:32:33 +0100 Subject: [PATCH 5/5] Proper format for date --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5466b87..14d754c 100644 --- a/README.md +++ b/README.md @@ -236,7 +236,7 @@ You can use these like any other filter, e.g. `{{variable_name | camel_case}}`. ## Changelog -### 0.5.1 (2025-08-12) +### 0.5.1 (2025-12-08) - New `derived = true` flag allows variables to be computed from default without prompting the user - `prompt` remains required for non-derived variables, but is optional when `derived` is set to true