From cef79652260fdcc84d1ffeb1c05a7fdcd1a5e9b0 Mon Sep 17 00:00:00 2001
From: Justin Ehlert <justinehlert@flywheel.io>
Date: Wed, 13 Dec 2017 10:09:52 -0600
Subject: [PATCH] Simplify template invocation

This is done by reducing duplication of template arguments, and
formalized $include/$template into the flattenSwagger step.
---
 swagger/Gruntfile.js                          |  13 +-
 swagger/paths/collections.yaml                |  74 +---------
 swagger/paths/groups.yaml                     |  45 +++---
 swagger/support/swagger-resolver.js           | 136 ++++++++++++++++++
 swagger/support/tasks/flatten-swagger.js      |  23 ++-
 swagger/support/tasks/resolve-templates.js    |  94 ------------
 swagger/templates/analyses-list.yaml          |   2 +-
 ...analysis-files-create-ticket-filename.yaml |   2 +-
 swagger/templates/analysis-files.yaml         |   2 +-
 swagger/templates/analysis-item.yaml          |   2 +-
 swagger/templates/analysis-notes-item.yaml    |   2 +-
 swagger/templates/analysis-notes.yaml         |   2 +-
 swagger/templates/file-item.yaml              |   2 +-
 swagger/templates/file-list-upload.yaml       |   2 +-
 swagger/templates/notes-note.yaml             |   2 +-
 swagger/templates/notes.yaml                  |   2 +-
 swagger/templates/packfile-end.yaml           |   2 +-
 swagger/templates/packfile-start.yaml         |   2 +-
 swagger/templates/packfile.yaml               |   2 +-
 swagger/templates/permissions-user.yaml       |   2 +-
 swagger/templates/permissions.yaml            |   2 +-
 swagger/templates/tags-tag.yaml               |   2 +-
 swagger/templates/tags.yaml                   |   2 +-
 23 files changed, 195 insertions(+), 224 deletions(-)
 create mode 100644 swagger/support/swagger-resolver.js
 delete mode 100644 swagger/support/tasks/resolve-templates.js

diff --git a/swagger/Gruntfile.js b/swagger/Gruntfile.js
index 714414c0..33f8bf0c 100644
--- a/swagger/Gruntfile.js
+++ b/swagger/Gruntfile.js
@@ -76,22 +76,12 @@ module.exports = function(grunt) {
 			}
 		},
 
-		/**
-		 * Resolve templates within swagger
-		 */
-		resolveTemplates: {
-			core: {
-				src: 'build/swagger-flat.json',
-				dest: 'build/swagger-int.json'
-			}
-		},
-
 		/**
 		 * Resolve schema links in the swagger documentation
 		 */
 		resolveSchemaLinks: {
 			core: {
-				src: 'build/swagger-int.json',
+				src: 'build/swagger-flat.json',
 				dest: 'build/swagger-ui.json'
 			}
 		},
@@ -136,7 +126,6 @@ module.exports = function(grunt) {
 	grunt.registerTask('build-schema', [
 		'copy:schema', 
 		'flattenSwagger',
-		'resolveTemplates',
 		'resolveSchemaLinks'
 	]);
 
diff --git a/swagger/paths/collections.yaml b/swagger/paths/collections.yaml
index b92831fb..bbc1f303 100644
--- a/swagger/paths/collections.yaml
+++ b/swagger/paths/collections.yaml
@@ -1,4 +1,9 @@
 # Global template arguments for collection
+$template_arguments:
+  resource: collection
+  tag: collections
+  parameter: CollectionId
+
 /collections:
   get:
     summary: List all collections.
@@ -130,114 +135,45 @@
 # ===== Tags =====
 /collections/{CollectionId}/tags:
   $template: templates/tags.yaml
-  arguments:
-    resource: collection
-    tag: collections
-    parameter: CollectionId
 /collections/{CollectionId}/tags/{TagValue}:
   $template: templates/tags-tag.yaml
-  arguments:
-    resource: collection
-    tag: collections
-    parameter: CollectionId
 
 # ===== Packfile =====
 /collections/{CollectionId}/packfile-start:
   $template: templates/packfile-start.yaml
-  arguments:
-    resource: collection
-    tag: collections
-    parameter: CollectionId
 /collections/{CollectionId}/packfile:
   $template: templates/packfile.yaml
-  arguments:
-    resource: collection
-    tag: collections
-    parameter: CollectionId
 /collections/{CollectionId}/packfile-end:
   $template: templates/packfile-end.yaml
-  arguments:
-    resource: collection
-    tag: collections
-    parameter: CollectionId
 
 # ===== Files =====
 /collections/{CollectionId}/files:
   $template: templates/file-list-upload.yaml
-  arguments:
-    resource: collection
-    tag: collections
-    parameter: CollectionId
 /collections/{CollectionId}/files/{FileName}:
   $template: templates/file-item.yaml
-  arguments:
-    resource: collection
-    tag: collections
-    parameter: CollectionId
 
 # ===== Permissions =====
 /collections/{CollectionId}/permissions:
   $template: templates/permissions.yaml
-  arguments:
-    resource: collection
-    tag: collections
-    parameter: CollectionId
 /collections/{CollectionId}/permissions/{UserId}:
   $template: templates/permissions-user.yaml
-  arguments:
-    resource: collection
-    tag: collections
-    parameter: CollectionId
 
 # ===== Notes =====
 /collections/{CollectionId}/notes:
   $template: templates/notes.yaml
-  arguments:
-    resource: collection
-    tag: collections
-    parameter: CollectionId
 /collections/{CollectionId}/notes/{NoteId}:
   $template: templates/notes-note.yaml
-  arguments:
-    resource: collection
-    tag: collections
-    parameter: CollectionId
 
 # ===== Analyses =====
 /collections/{CollectionId}/analyses:
   $template: templates/analyses-list.yaml
-  arguments:
-    resource: collection
-    tag: collections
-    parameter: CollectionId
 /collections/{CollectionId}/analyses/{AnalysisId}:
   $template: templates/analysis-item.yaml
-  arguments:
-    resource: collection
-    tag: collections
-    parameter: CollectionId
 /collections/{CollectionId}/analyses/{AnalysisId}/files:
   $template: templates/analysis-files.yaml
-  arguments:
-    resource: collection
-    tag: collections
-    parameter: CollectionId
 /collections/{CollectionId}/analyses/{AnalysisId}/files/{Filename}:
   $template: templates/analysis-files-create-ticket-filename.yaml
-  arguments:
-    resource: collection
-    tag: collections
-    parameter: CollectionId
 /collections/{CollectionId}/analyses/{AnalysisId}/notes:
   $template: templates/analysis-notes.yaml
-  arguments:
-    resource: collection
-    tag: collections
-    parameter: CollectionId
 /collections/{CollectionId}/analyses/{AnalysisId}/notes/{NoteId}:
   $template: templates/analysis-notes-item.yaml
-  arguments:
-    resource: collection
-    tag: collections
-    parameter: CollectionId
-    
\ No newline at end of file
diff --git a/swagger/paths/groups.yaml b/swagger/paths/groups.yaml
index 4c70539e..5576cb6a 100644
--- a/swagger/paths/groups.yaml
+++ b/swagger/paths/groups.yaml
@@ -1,3 +1,8 @@
+$template_arguments:
+  resource: group
+  tag: groups
+  parameter: GroupId
+
 /groups:
   get:
     summary: List all groups
@@ -70,34 +75,6 @@
       '200':
         $ref: '#/responses/200:deleted-with-count'
 
-# ===== Permissions =====
-/groups/{GroupId}/permissions:
-  $template: templates/permissions.yaml
-  arguments:
-    resource: group
-    tag: groups
-    parameter: GroupId
-/groups/{GroupId}/permissions/{UserId}:
-  $template: templates/permissions-user.yaml
-  arguments:
-    resource: group
-    tag: groups
-    parameter: GroupId
-
-# ===== Tags =====
-/groups/{GroupId}/tags:
-  $template: templates/tags.yaml
-  arguments:
-    resource: group
-    tag: groups
-    parameter: GroupId
-/groups/{GroupId}/tags/{TagValue}:
-  $template: templates/tags-tag.yaml
-  arguments:
-    resource: group
-    tag: groups
-    parameter: GroupId
-
 /groups/{GroupId}/projects:
   parameters:
     - required: true
@@ -117,3 +94,15 @@
         examples:
           response:
             $ref: examples/output/project-list.json
+
+# ===== Permissions =====
+/groups/{GroupId}/permissions:
+  $template: templates/permissions.yaml
+/groups/{GroupId}/permissions/{UserId}:
+  $template: templates/permissions-user.yaml
+
+# ===== Tags =====
+/groups/{GroupId}/tags:
+  $template: templates/tags.yaml
+/groups/{GroupId}/tags/{TagValue}:
+  $template: templates/tags-tag.yaml
diff --git a/swagger/support/swagger-resolver.js b/swagger/support/swagger-resolver.js
new file mode 100644
index 00000000..d40b2727
--- /dev/null
+++ b/swagger/support/swagger-resolver.js
@@ -0,0 +1,136 @@
+'use strict';
+
+var _ = require('lodash');
+var path = require('path');
+var fs = require('fs');
+var process = require('process');
+var yaml = require('js-yaml');
+var Mustache = require('mustache');
+
+var walk = require('./walk');
+
+// Throws if args are missing
+function validateTemplateArgs(tmplpath, template, args) {
+	var i, param;
+	if( !template || !template.parameters ) {
+		return;
+	}
+	for( i = 0; i < template.parameters.length; i++ ) {
+		param = template.parameters[i];
+		if( !param.name ) {
+			throw 'Template "' + tmplpath + '" parameter does not have a name!';
+		}
+		if( _.isNil(args[param.name]) ) {
+			throw 'Template "' + tmplpath + '" invocation is missing parameter: ' + param.name;
+		}
+	}
+}
+
+/**
+ * @class SwaggerResolver
+ * Performs stateful resolution of $include and $template directives
+ * in YAML files.
+ * @param {object} options The optional configuration
+ * @param {string} options.path The root path to resolve templates and includes from. (Defaults to cwd)
+ * @param {function} options.log The optional logging function
+ */
+var SwaggerResolver = function(options) {
+	options = options||{};
+	this.path = options.path||process.cwd();
+	this.templates = {};
+	this.log = options.log||function() {
+		console.log.apply(console, arguments);
+	};
+};
+
+/**
+ * Recursively resolve any includes and templates in the data.
+ * @param {object} data The data object
+ * @param {string} data.text The text data, in string format. (Either JSON or YAML)
+ */
+SwaggerResolver.prototype.resolveContent = function(data) {
+	// Reset template arguments for this run
+	// Possible issue: templates including templates will smash this, can we make it
+	// more stack oriented?
+	this.templateArguments = null;
+
+	// Do conversion first
+	var obj = yaml.safeLoad(data.text);
+	return this.resolveObject(obj);
+};
+
+SwaggerResolver.prototype.resolveObject = function(obj) {
+	// Perform a deep-walk of the object, replacing 
+	return walk(obj, this.visit.bind(this));
+};
+
+SwaggerResolver.prototype.loadFile = function(relpath) {
+	var abspath = path.join(this.path, relpath);
+	if( !fs.existsSync(abspath) ) {
+		throw 'File does not exist: ' + abspath;
+	}
+	return yaml.safeLoad(fs.readFileSync(abspath).toString());
+};
+
+SwaggerResolver.prototype.visit = function(obj) {
+	// obj will have $template or $include, not both
+	if( obj.hasOwnProperty('$include') ) {
+		obj = this.resolveIncludes(obj);
+
+		// Recursively resolve content
+		obj = this.resolveObject(obj);
+	} else if( obj.hasOwnProperty('$template') ) {
+		obj = this.resolveTemplate(obj);
+
+		// Recursively resolve content
+		obj = this.resolveObject(obj);
+	} else if( obj.hasOwnProperty('$template_arguments') ) {
+		this.templateArguments = obj['$template_arguments'];
+		delete obj['$template_arguments'];
+	}
+
+	return obj;
+};
+
+SwaggerResolver.prototype.resolveIncludes = function(obj) {
+	var i, includes, inc;
+
+	includes = obj['$include'];
+	if( typeof includes === 'string' ) {
+		includes = [includes];
+	}
+
+	delete obj['$include'];
+	for( i = 0; i < includes.length; i++ ) {
+		// Load the include file
+		inc = this.loadFile(includes[i]);
+		// And merge its contents into obj
+		_.extend(obj, inc);
+	}
+	
+	return obj;
+};
+
+SwaggerResolver.prototype.resolveTemplate = function(obj) {
+	var tmplpath, tmpl, args, text;
+
+	tmplpath = obj['$template'];
+	tmpl = this.templates[tmplpath];
+
+	if( !tmpl ) {
+		tmpl = this.loadFile(tmplpath);
+		this.templates[tmplpath] = tmpl;
+		Mustache.parse(tmpl.template);
+	}
+
+	// Validate arguments?
+	args = obj['arguments']||this.templateArguments||{};
+	validateTemplateArgs(tmplpath, tmpl, args);
+
+	// Render the template, and parse
+	text = Mustache.render(tmpl.template, args);
+	return yaml.safeLoad(text);
+};
+
+module.exports = SwaggerResolver;
+
diff --git a/swagger/support/tasks/flatten-swagger.js b/swagger/support/tasks/flatten-swagger.js
index bf2c9578..a1fe41be 100644
--- a/swagger/support/tasks/flatten-swagger.js
+++ b/swagger/support/tasks/flatten-swagger.js
@@ -6,10 +6,7 @@ module.exports = function(grunt) {
 	var yaml = require('js-yaml');
 	var resolve = require('json-refs').resolveRefs;
 
-	// Use YAML to load nested content
-	function resolveContent(res, callback) {
-		callback(undefined, yaml.safeLoad(res.text));
-	}
+	var SwaggerResolver = require('../swagger-resolver');
 
 	/**
 	 * This task flattens the nested swagger yaml into a single flat file.
@@ -23,6 +20,15 @@ module.exports = function(grunt) {
 	grunt.registerMultiTask('flattenSwagger', 'Resolve references in swagger YAML files', function() {
 		var apiFile = this.data.apiFile||'swagger.yml';
 		var destFile = this.data.dest||'swagger.json';
+		var resolver = new SwaggerResolver({
+			log: function() {
+				grunt.log.writeln.apply(grunt.log, arguments);
+			}
+		});
+
+		function resolveContent(res, callback) {
+			callback(undefined, resolver.resolveContent(res));
+		}
 
 		// See: http://azimi.me/2015/07/16/split-swagger-into-smaller-files.html
 		// and the corresponding repo: https://github.com/mohsen1/multi-file-swagger-example
@@ -36,6 +42,15 @@ module.exports = function(grunt) {
 		});
 
 		var root = yaml.safeLoad(fs.readFileSync(apiFile).toString());
+
+		// Resolve any top-level includes or templates
+		try {
+			root = resolver.resolveObject(root);
+		} catch (e) {
+			grunt.log.writeln("Could not resolve root:", e);
+			return false;
+		}
+
 		var resolveOpts = {
 			filter: ['relative'],
 			loaderOptions: {
diff --git a/swagger/support/tasks/resolve-templates.js b/swagger/support/tasks/resolve-templates.js
deleted file mode 100644
index 0f0c3848..00000000
--- a/swagger/support/tasks/resolve-templates.js
+++ /dev/null
@@ -1,94 +0,0 @@
-'use strict';
-
-module.exports = function(grunt) {
-	var path = require('path');
-	var fs = require('fs');
-	var process = require('process');
-	var yaml = require('js-yaml');
-	var mustache = require('mustache');
-	var _ = require('lodash');
-	var walk = require('../walk');
-
-	function resolveTemplatesFunc(templates) {
-		var loaded_templates = {};
-		return function(obj) {
-			return walk(obj, function(obj) {
-				var tmplpath, tmpl;
-
-				if( obj.hasOwnProperty('$template') ) {
-					tmplpath = path.join(templates, obj['$template']);
-					if( !loaded_templates[tmplpath] ) {
-						if( !fs.existsSync(tmplpath) ) {
-							throw 'Template file does not exist: ' + tmplpath;
-						}
-						loaded_templates[tmplpath] = yaml.safeLoad(fs.readFileSync(tmplpath).toString());
-					}
-
-					tmpl = _.cloneDeep(loaded_templates[tmplpath]['template']);
-					obj = walk(tmpl, resolveParamsFunc(obj['arguments']));
-				}
-
-				return obj;
-			});
-		};
-	}
-
-	function resolveIncludesFunc(templates) {
-		return function(obj) {
-			return walk(obj, function(obj) {
-				var i, includes, incpath, inc;
-
-				if( obj.hasOwnProperty('$include') ) {
-					includes = obj['$include'];
-					delete obj['$include'];
-					for( i = 0; i < includes.length; i++ ) {
-						incpath = path.join(templates, includes[i]);
-						if( !fs.existsSync(incpath) ) {
-							throw 'Included file does not exist: ' + incpath;
-						}
-						inc = yaml.safeLoad(fs.readFileSync(incpath).toString());
-						_.extend(obj, inc);
-					}
-				}
-
-				return obj;
-			});
-		};
-	}
-
-	function resolveParamsFunc(args) {
-		return function(obj) {
-			if( typeof obj === 'string' ) {
-				return mustache.render(obj, args||{});
-			}
-			return obj;
-		};
-	}
-
-	grunt.registerMultiTask('resolveTemplates', 'Resolve templates in swagger JSON file', function() {
-		var src = this.data.src;
-		var dest = this.data.dest;
-		var templates = this.data.templates||process.cwd();
-		var resolveTemplates = resolveTemplatesFunc(templates);
-		var resolveIncludes = resolveIncludesFunc(templates);
-
-		if(!fs.existsSync(src)) {
-			grunt.log.writeln('Could not find:', src);
-			return false;
-		}
-
-		var root = JSON.parse(fs.readFileSync(src).toString());
-		try {
-			root = resolveIncludes(root);
-			root = resolveTemplates(root);
-		} catch( e ) {
-			grunt.log.writeln('Error resolving templates:', e);
-			return false;
-		}
-
-		var data = JSON.stringify(root, null, 2);
-		fs.writeFileSync(dest, data);
-	});
-};
-
-
diff --git a/swagger/templates/analyses-list.yaml b/swagger/templates/analyses-list.yaml
index c7d37e93..ad44c1a1 100644
--- a/swagger/templates/analyses-list.yaml
+++ b/swagger/templates/analyses-list.yaml
@@ -5,7 +5,7 @@ parameters:
     type: string
   - name: tag
     type: string
-template:
+template: |
   parameters:
     - required: true
       type: string
diff --git a/swagger/templates/analysis-files-create-ticket-filename.yaml b/swagger/templates/analysis-files-create-ticket-filename.yaml
index b1ada829..7f90a951 100644
--- a/swagger/templates/analysis-files-create-ticket-filename.yaml
+++ b/swagger/templates/analysis-files-create-ticket-filename.yaml
@@ -5,7 +5,7 @@ parameters:
     type: string
   - name: tag
     type: string
-template:
+template: |
   parameters:
     - required: true
       type: string
diff --git a/swagger/templates/analysis-files.yaml b/swagger/templates/analysis-files.yaml
index bcdc793c..1577e15e 100644
--- a/swagger/templates/analysis-files.yaml
+++ b/swagger/templates/analysis-files.yaml
@@ -5,7 +5,7 @@ parameters:
     type: string
   - name: tag
     type: string
-template:
+template: |
   parameters:
     - required: true
       type: string
diff --git a/swagger/templates/analysis-item.yaml b/swagger/templates/analysis-item.yaml
index d187a0e5..68cd6020 100644
--- a/swagger/templates/analysis-item.yaml
+++ b/swagger/templates/analysis-item.yaml
@@ -5,7 +5,7 @@ parameters:
     type: string
   - name: tag
     type: string
-template:
+template: |
     parameters:
       - required: true
         type: string
diff --git a/swagger/templates/analysis-notes-item.yaml b/swagger/templates/analysis-notes-item.yaml
index 62e26f90..f4a87a72 100644
--- a/swagger/templates/analysis-notes-item.yaml
+++ b/swagger/templates/analysis-notes-item.yaml
@@ -5,7 +5,7 @@ parameters:
     type: string
   - name: tag
     type: string
-template:
+template: |
   parameters:
     - required: true
       type: string
diff --git a/swagger/templates/analysis-notes.yaml b/swagger/templates/analysis-notes.yaml
index bbb5867e..2627f2ed 100644
--- a/swagger/templates/analysis-notes.yaml
+++ b/swagger/templates/analysis-notes.yaml
@@ -5,7 +5,7 @@ parameters:
     type: string
   - name: tag
     type: string
-template:
+template: |
   parameters:
     - required: true
       type: string
diff --git a/swagger/templates/file-item.yaml b/swagger/templates/file-item.yaml
index 54873dfa..304791fa 100644
--- a/swagger/templates/file-item.yaml
+++ b/swagger/templates/file-item.yaml
@@ -5,7 +5,7 @@ parameters:
     type: string
   - name: tag
     type: string
-template:
+template: |
   parameters:
     - required: true
       type: string
diff --git a/swagger/templates/file-list-upload.yaml b/swagger/templates/file-list-upload.yaml
index 8b1be9a0..57a33187 100644
--- a/swagger/templates/file-list-upload.yaml
+++ b/swagger/templates/file-list-upload.yaml
@@ -5,7 +5,7 @@ parameters:
     type: string
   - name: tag
     type: string
-template:
+template: |
   parameters:
     - required: true
       type: string
diff --git a/swagger/templates/notes-note.yaml b/swagger/templates/notes-note.yaml
index a00f2bc2..4fdfd3b3 100644
--- a/swagger/templates/notes-note.yaml
+++ b/swagger/templates/notes-note.yaml
@@ -5,7 +5,7 @@ parameters:
     type: string
   - name: tag
     type: string
-template:
+template: |
   parameters:
     - required: true
       type: string
diff --git a/swagger/templates/notes.yaml b/swagger/templates/notes.yaml
index 540b1255..1c4936b0 100644
--- a/swagger/templates/notes.yaml
+++ b/swagger/templates/notes.yaml
@@ -5,7 +5,7 @@ parameters:
     type: string
   - name: tag
     type: string
-template:
+template: |
   parameters:
     - required: true
       type: string
diff --git a/swagger/templates/packfile-end.yaml b/swagger/templates/packfile-end.yaml
index fedd50cd..0b662d20 100644
--- a/swagger/templates/packfile-end.yaml
+++ b/swagger/templates/packfile-end.yaml
@@ -5,7 +5,7 @@ parameters:
     type: string
   - name: tag
     type: string
-template:
+template: |
   parameters:
     - required: true
       type: string
diff --git a/swagger/templates/packfile-start.yaml b/swagger/templates/packfile-start.yaml
index 25da57db..d8b5ebfc 100644
--- a/swagger/templates/packfile-start.yaml
+++ b/swagger/templates/packfile-start.yaml
@@ -5,7 +5,7 @@ parameters:
     type: string
   - name: tag
     type: string
-template:
+template: |
   parameters:
     - required: true
       type: string
diff --git a/swagger/templates/packfile.yaml b/swagger/templates/packfile.yaml
index bd2851f3..d302d75c 100644
--- a/swagger/templates/packfile.yaml
+++ b/swagger/templates/packfile.yaml
@@ -5,7 +5,7 @@ parameters:
     type: string
   - name: tag
     type: string
-template:
+template: |
   parameters:
     - required: true
       type: string
diff --git a/swagger/templates/permissions-user.yaml b/swagger/templates/permissions-user.yaml
index 0a0875c4..f8ab3862 100644
--- a/swagger/templates/permissions-user.yaml
+++ b/swagger/templates/permissions-user.yaml
@@ -5,7 +5,7 @@ parameters:
     type: string
   - name: tag
     type: string
-template:
+template: |
   parameters:
     - required: true
       type: string
diff --git a/swagger/templates/permissions.yaml b/swagger/templates/permissions.yaml
index 995d8746..ff9ef38c 100644
--- a/swagger/templates/permissions.yaml
+++ b/swagger/templates/permissions.yaml
@@ -5,7 +5,7 @@ parameters:
     type: string
   - name: tag
     type: string
-template:
+template: |
   parameters:
     - required: true
       type: string
diff --git a/swagger/templates/tags-tag.yaml b/swagger/templates/tags-tag.yaml
index c868f4b5..d12ad63f 100644
--- a/swagger/templates/tags-tag.yaml
+++ b/swagger/templates/tags-tag.yaml
@@ -5,7 +5,7 @@ parameters:
     type: string
   - name: tag
     type: string
-template:
+template: |
     parameters:
       - required: true
         type: string
diff --git a/swagger/templates/tags.yaml b/swagger/templates/tags.yaml
index b1a96893..d396ec79 100644
--- a/swagger/templates/tags.yaml
+++ b/swagger/templates/tags.yaml
@@ -5,7 +5,7 @@ parameters:
     type: string
   - name: tag
     type: string
-template:
+template: |
   parameters:
     - required: true
       type: string
-- 
GitLab