Repository: asyncapi/generator (js)

Issue:
Enable template developer to declare default value for a parameter in the configuration file
#### Reason/Context

Discussion started here https://github.com/asyncapi/generator-hooks/pull/1#discussion_r424954487 when we were about to merge new hooks handling and ability to reuse hooks across templates and use parameters inside hooks.

Why `parameters[param].default` in config would be useful?
- much clearer for the template user do see directly what are the default values for parameters
- template developers, when they use some generic hook and its parameter, could specify a default value for the parameter that suits the purpose of their template

#### Description

Add support for `parameters[param].default` in the `.tp-config.json` file


Pull request description:
feat: add default value for template parameters
**Description**

- Now you as template author could define a default value for template properties. If user pass parameter value in start command, for sure, it will be used as value of parameter. 
- Tested with https://github.com/asyncapi/java-spring-template/pull/37

**Related issue(s)**
Resolves #339 
<!-- If you refer to a particular issue, provide its number, othewise, remove this section.
For example, `Resolves #123`, `Fixes #43`, or `See also #33`. The 3rd option will not automatically close the issue after the merge. -->

Patch:
diff --git a/docs/authoring.md b/docs/authoring.md
index 438916b..a40c2a4 100644
--- a/docs/authoring.md
+++ b/docs/authoring.md
@@ -183,6 +183,7 @@ The `.tp-config.json` file contains a JSON object that may have the following in
 |`supportedProtocols`| [String] | A list with all the protocols this template supports.
 |`parameters`| Object[String, Object] | An object with all the parameters that can be passed when generating the template. When using the command line, it's done by indicating `--param name=value` or `-p name=value`.
 |`parameters[param].description`| String | A user-friendly description about the parameter.
+|`parameters[param].default`| Any | Default value of the parameter if not specified. Shouldn't be used for mandatory `required=true` parameters.
 |`parameters[param].required`| Boolean | Whether the parameter is required or not.
 |`conditionalFiles`| Object[String, Object] | An object containing all the file paths that should be conditionally rendered. Each key represents a file path and each value must be an object with the keys `subject` and `validation`.
 |`conditionalFiles[filePath].subject`| String | The `subject` is a [JMESPath](http://jmespath.org/) query to grab the value you want to apply the condition to. It queries an object with the whole AsyncAPI document and, when specified, the given server. The object looks like this: `{ asyncapi: { ... }, server: { ... } }`.
@@ -201,6 +202,11 @@ The `.tp-config.json` file contains a JSON object that may have the following in
     "server": {
       "description": "The server you want to use in the code.",
       "required": true
+    },
+    "dummyParameter": {
+      "description": "Example of parameter with default value.",
+      "default": "just a string",
+      "required": false
     }
   },
   "conditionalFiles": {
diff --git a/lib/generator.js b/lib/generator.js
index d44dca7..40c0687 100644
--- a/lib/generator.js
+++ b/lib/generator.js
@@ -642,9 +642,29 @@ class Generator {
     } catch (e) {
       this.templateConfig = {};
     }
+    await this.loadDefaultValues();
     await this.validateTemplateConfig();
   }
 
+  /**
+   * Loads default values of parameters from template config. If value was already set as parameter it will not be
+   * overrided.
+   * @private
+   */
+  async loadDefaultValues() {
+    const parameters = this.templateConfig.parameters;
+    const defaultValues = Object.keys(parameters || {}).filter(key => parameters[key].default);
+
+    defaultValues.filter(dv => this.templateParams[dv] === undefined).forEach(dv =>
+      Object.defineProperty(this.templateParams, dv, {
+        enumerable: true,
+        get() {
+          return parameters[dv].default;
+        }
+      })
+    );
+  }
+
   /**
    * Validates the template configuration.
    *
@@ -660,16 +680,11 @@ class Generator {
       throw new Error(`This template is not compatible with the current version of the generator (${packageJson.version}). This template is compatible with the following version range: ${generator}.`);
     }
 
-    const requiredParams = [];
-    Object.keys(parameters || {}).forEach(key => {
-      if (parameters[key].required === true) requiredParams.push(key);
-    });
+    const requiredParams = Object.keys(parameters || {}).filter(key => parameters[key].required === true);
 
-    if (Array.isArray(requiredParams)) {
-      const missingParams = requiredParams.filter(rp => this.templateParams[rp] === undefined);
-      if (missingParams.length) {
-        throw new Error(`This template requires the following missing params: ${missingParams}.`);
-      }
+    const missingParams = requiredParams.filter(rp => this.templateParams[rp] === undefined);
+    if (missingParams.length) {
+      throw new Error(`This template requires the following missing params: ${missingParams}.`);
     }
 
     if (typeof conditionalFiles === 'object') {
diff --git a/package-lock.json b/package-lock.json
index 8b5ba97..5e26f49 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,6 +1,6 @@
 {
   "name": "@asyncapi/generator",
-  "version": "0.46.0",
+  "version": "0.46.1",
   "lockfileVersion": 1,
   "requires": true,
   "dependencies": {
diff --git a/package.json b/package.json
index ed95bc1..df9e801 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@asyncapi/generator",
-  "version": "0.46.0",
+  "version": "0.46.1",
   "description": "The AsyncAPI generator. It can generate documentation, code, anything!",
   "main": "./lib/generator.js",
   "bin": {


Test patch:
diff --git a/test/generator.test.js b/test/generator.test.js
index fd30263..a60a7cc 100644
--- a/test/generator.test.js
+++ b/test/generator.test.js
@@ -383,4 +383,87 @@ describe('Generator', () => {
       expect(content).toBe(utils.__files[filePath]);
     });
   });
+
+  describe('#loadDefaultValues', () => {
+    it('default value of parameter is set', async () => {
+      const gen = new Generator('testTemplate', __dirname, {
+        templateParams: {
+          test: true
+        }
+      });
+      gen.templateConfig  = {
+        parameters: {
+          paramWithDefault: {
+            description: 'Parameter with default value',
+            default: 'default',
+            required: false
+          },
+          paramWithoutDefault: {
+            description: 'Parameter without default value',
+            required: false
+          },
+          test: {
+            description: 'test',
+            required: false
+          }
+        }
+      };
+
+      await gen.loadDefaultValues();
+
+      expect(gen.templateParams).toStrictEqual({
+        test: true,
+        paramWithDefault: 'default'
+      });
+    });
+
+    it('default value of parameter is not override user value', async () => {
+      const gen = new Generator('testTemplate', __dirname, {
+        templateParams: {
+          test: true
+        }
+      });
+      gen.templateConfig = {
+        parameters: {
+          test: {
+            description: 'Parameter with default value',
+            default: false,
+            required: false
+          }
+        }
+      };
+
+      await gen.loadDefaultValues();
+
+      expect(gen.templateParams).toStrictEqual({
+        test: true
+      });
+    });
+
+    it('no default values', async () => {
+      const gen = new Generator('testTemplate', __dirname, {
+        templateParams: {
+          test: true
+        }
+      });
+      gen.templateConfig = {
+        parameters: {
+          test: {
+            description: 'Parameter with default value',
+            required: false
+          },
+          anotherParam: {
+            description: 'Yeat another param',
+            required: false
+          }
+        }
+      };
+
+      await gen.loadDefaultValues();
+
+      expect(gen.templateParams).toStrictEqual({
+        test: true
+      });
+    });
+  });
 });