Repository: cargo-generate/cargo-generate
Pull request #1201 (2024-06-07 02:21:53)

feat(rhai-hooks): add a date API that provides year, month and day access
Most license files start with a copyright statement with a year but `cargo-generate` hooks or template parameters do not provide access to that information . The only alternatives would be using the `system::command` API but that isn't something that is very portable and prompting the user for the year which is less than ideal. This PR adds a new API `system::date` which returns an object with 3 properties, `year`, `month`, `day`. This would allow for generating a license from a rhai hook:

```rhai
file::write(license.txt, `Copyright ${system::date().year} ${authors}
...
`);

```

I've also added docs for the `system` module, though I don't know if they cover the details outlined in #616.

sassman: thanks for the PR!

Please make sure you have formatted the new code correctly: `cargo fmt`.
This is why the build fails.

https://github.com/cargo-generate/cargo-generate/actions/runs/9410733469/job/25922949844?pr=1201#step:5:19
FreeMasen: > Please make sure you have formatted the new code correctly: `cargo fmt`.
> This is why the build fails.

Sorry about that, the updates should now match the project style 
FreeMasen: I'll try and refactor the clippy lints this weekend

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 51727da..9b29fb7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -206,6 +206,7 @@ dependencies = [
  "serde",
  "tempfile",
  "thiserror",
+ "time",
  "toml",
  "url",
  "walkdir",
diff --git a/Cargo.toml b/Cargo.toml
index 2ef346f..77f0dc7 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -22,6 +22,7 @@ walkdir = "~2.5"
 remove_dir_all = "~0.8"
 ignore = "~0.4"
 anyhow = "~1.0"
+time = "~0.3"
 toml = { version = "~0.8", features = ["preserve_order"] }
 thiserror = "~1.0"
 home = "~0.5"
diff --git a/guide/src/templates/scripting.rhai-extensions.md b/guide/src/templates/scripting.rhai-extensions.md
index 4638c03..688c44b 100644
--- a/guide/src/templates/scripting.rhai-extensions.md
+++ b/guide/src/templates/scripting.rhai-extensions.md
@@ -62,6 +62,26 @@ Besides the basic [`Rhai`] features, these are the modules/behaviors defined:
 
   Create/overwrite a file inside the template folder, each entry in the array on a new line
 
+### System
+
+* **`system::command(cmd: &str, args: Array) -> value`**
+
+  Execute a command on the system generating the project from a template.
+
+  The user will be prompted with 
+
+  ```
+  The template is requesting to run the following command. Do you agree?
+  <command> <args>
+  ```
+
+  unless the user uses the flag `--allow-commands`. If the user attempts to use the
+  `--silent` flag without the `--allow-commands` flag will fail.
+
+* **`system::date() -> Date`**
+  
+  Get the date in UTC from the system as an object with the properties `year`, `month`, and `day`.
+
 ### Other
 
 * **`abort(reason: &str)`**: Aborts `cargo-generate` with a script error.
diff --git a/guide/src/templates/template_defined_placeholders.md b/guide/src/templates/template_defined_placeholders.md
index f514b82..3c0986e 100644
--- a/guide/src/templates/template_defined_placeholders.md
+++ b/guide/src/templates/template_defined_placeholders.md
@@ -16,7 +16,7 @@ placeholder_name = { prompt = "Enter your name", choices = ["Alice", "Bob"], def
 - `choices` (optional): A list of predefined choices for the placeholder value.
 - `default` (optional): The default value for the placeholder if no user input is provided.
 - `regex` (optional and only for string-like types): The entered value is validated against this regex. 
-- `type`: The data type of the placeholder value (e.g., `"string"`, `"text"`, `"editor"`, `"boolean"`).
+- `type`: The data type of the placeholder value (e.g., `"string"`, `"text"`, `"editor"`, `"bool"`).
 
 ## Prompt, Choices, and Default Values
 
@@ -31,7 +31,7 @@ placeholder_name = { prompt = "Enter your name", choices = ["Alice", "Bob"], def
 - `"string"`: Represents a string value.
 - `"text"`: Represents a multiline string value. (terminated by hitting <CTRL-D>)
 - `"editor"`: Represents a multiline string value, collected from the user by a real terminal editor.
-- `"boolean"`: Represents a boolean value (`true` or `false`).
+- `"bool"`: Represents a boolean value (`true` or `false`).
 
 ## Example
 
@@ -40,7 +40,7 @@ Consider the following `cargo-generate.toml` file:
 ```toml
 [placeholders]
 project_name = { prompt = "Enter project name", default = "my_project", type = "string" }
-use_git = { prompt = "Initialize Git repository?", choices = ["Yes", "No"], default = "Yes", type = "boolean" }
+use_git = { prompt = "Initialize Git repository?", choices = ["Yes", "No"], default = "Yes", type = "bool" }
 phone_number = { prompt = "What's your phone number?", type = "string", regex = "^[0-9]+$" }
 ```
 
diff --git a/src/hooks/system_mod.rs b/src/hooks/system_mod.rs
index 54c38b8..b3f1451 100644
--- a/src/hooks/system_mod.rs
+++ b/src/hooks/system_mod.rs
@@ -1,5 +1,6 @@
 use rhai::{Dynamic, Module};
-use std::process::Command;
+use std::{collections::BTreeMap, process::Command};
+use time::OffsetDateTime;
 
 use crate::{
     interactive::prompt_and_check_variable,
@@ -16,6 +17,7 @@ pub fn create_module(allow_commands: bool, silent: bool) -> Module {
     module.set_native_fn("command", move |name: &str, commands_args: rhai::Array| {
         run_command(name, commands_args, allow_commands, silent)
     });
+    module.set_native_fn("date", get_utc_date);
 
     module
 }
@@ -88,3 +90,24 @@ fn run_command(
         Err(e) => Err(format!("System command `{full_command}` failed to execute: {e}").into()),
     }
 }
+
+fn get_utc_date() -> HookResult<Dynamic> {
+    Ok(construct_date_map(OffsetDateTime::now_utc()))
+}
+
+fn construct_date_map(dt: OffsetDateTime) -> Dynamic {
+    let mut value = BTreeMap::new();
+    value.insert(
+        "year".parse().unwrap(),
+        Dynamic::from_int(i64::from(dt.year())),
+    );
+    value.insert(
+        "month".parse().unwrap(),
+        Dynamic::from_int(dt.month() as i64),
+    );
+    value.insert(
+        "day".parse().unwrap(),
+        Dynamic::from_int(i64::from(dt.day())),
+    );
+    Dynamic::from_map(value)
+}
diff --git a/src/project_variables.rs b/src/project_variables.rs
index f016968..769862d 100644
--- a/src/project_variables.rs
+++ b/src/project_variables.rs
@@ -105,7 +105,7 @@ pub enum ConversionError {
         choices: Vec<String>,
     },
     #[error(
-        "invalid type for variable `{var_name}`: `{value}` possible values are `bool` and `string`"
+        "invalid type for variable `{var_name}`: `{value}` possible values are `bool`, `string`, `text` and `editor`"
     )]
     InvalidVariableType { var_name: String, value: String },
     #[error("{var_type} type does not support `choices` field")]


Test patch:
diff --git a/tests/integration/hooks_and_rhai.rs b/tests/integration/hooks_and_rhai.rs
index e1be0ac..3b68924 100644
--- a/tests/integration/hooks_and_rhai.rs
+++ b/tests/integration/hooks_and_rhai.rs
@@ -451,6 +451,54 @@ fn rhai_filter_invokes_rhai_script() {
         .contains("content from RHAI"));
 }
 
+#[test]
+fn date_works() {
+    let template = tempdir()
+        .file(
+            "init.rhai",
+            indoc! {r#"
+                let dt = system::date();
+                variable::set("year", `${dt.year}`);
+                variable::set("month", `${dt.month}`);
+                variable::set("day", `${dt.day}`);
+            "#},
+        )
+        .file(
+            "cargo-generate.toml",
+            indoc! {r#"
+                [hooks]
+                init = ["init.rhai"]
+            "#},
+        )
+        .file(
+            "generated.txt",
+            indoc! {r#"
+                {{year}}-{{month}}-{{day}}
+        "#},
+        )
+        .init_git()
+        .build();
+    let date = time::OffsetDateTime::now_utc();
+    let dir = tempdir().build();
+
+    binary()
+        .arg_git(template.path())
+        .arg_name("foo")
+        .flag_init()
+        .current_dir(dir.path())
+        .assert()
+        .success()
+        .stdout(predicates::str::contains("Done!").from_utf8());
+
+    assert!(dir.exists("generated.txt"), "generated.txt didn't exist");
+    let content = dir.read("generated.txt");
+    let expected = format!("{}-{}-{}", date.year(), u8::from(date.month()), date.day());
+    assert!(
+        content.contains(&expected),
+        "generated.txt didn't include `{expected}`:\n`{content}`"
+    );
+}
+
 #[test]
 fn missing_rhai_filter_fails_prints_warnings() {
     let template = tempdir()