Using Jinja on StackSpot
In this section, you will find information about using the Jinja language in StackSpot.
Jinja
The StackSpot engine uses the Jinja language to allow Plugins to generate files based on user input parameters when creating an app or applying a Plugin.
In addition to file generation, Jinja expressions can be used in Plugin configuration files, allowing great flexibility in defining settings based on user input.
For security reasons, Jinja runs in Sandbox mode, separated from your system. Depending on how you use expressions, some of them may not behave as expected.
For more information about restrictions, see the official Jinja documentation about Sandbox.
Plugins rendering cycle
Before using Jinja, it is essential to understand how the Plugin rendering cycle works in StackSpot. Whenever a Plugin is applied to a project, either by the stk create app or stk apply plugin commands, the following steps occur:
- The configuration file (
plugin.yaml) is loaded. - Hooks with trigger
before-inputare executed. - The inputs defined in the configuration are requested from the user.
- The
computed-inputs(computed values) are calculated. - Hooks with trigger
before-renderare executed. - Jinja templates contained in the Plugin's
templatesfolder are rendered. - Hooks with trigger
after-renderare executed. - Files are merged, if they exist in the target.
The input parameters entered by the user in step 3 are used as variables. They can be used in Jinja expressions both in the templates used for file generation and at specific points in the Plugin configuration.
Jinja basic concepts
The Jinja language, like most template languages, allows you to define, through specific tags in a text, expressions that are interpolated by the engine to render the final result.
Jinja markup in text can be:
{{ ... }}: for expressions that will be rendered.{% ... %}: for control structures (conditionals, loops).{# ... #}: for comments (not rendered in the final output).
Check out a simple example of a Jinja template:
<!DOCTYPE html>
<html lang="en">
<head>
<title>My Webpage</title>
</head>
<body>
{# print a Hello message for each name in names list #}
{% for name in names %}
<h1>Hello {{ name }}!</h1>
{% endfor %}
</body>
</html>
When the variable names has the value ['John', 'Mary'], the template rendering returns:
<h1>Hello John!</h1>
<h1>Hello Mary!</h1>
In short, when evaluated by StackSpot's template engine, Jinja expressions and control structures are evaluated to produce the final result, either when generating files or when configuring commands and Hooks in the Plugin configuration files.
Jinja expressions
Expressions must be enclosed within {{ ... }}. This indicates something that should be rendered by Jinja. Expressions can contain literal values, variables, calculations and complex expressions.
To learn more about all the Jinja expressions, see the official Jinja documentation about expressions.
Below is a summary of Jinja's most relevant points for a content creator at StackSpot.
Variables
The most common use of Jinja expressions in StackSpot is to use the value of a variable in the rendered files. The variables present in the Jinja context come from the inputs in the Plugin configuration files and are requested from the user when the Plugin is applied to a project.
For example, imagine that a Plugin has the following input defined in its YAML file:
inputs:
- label: Enter your name
type: text
name: name
The expression to render the name entered by the user in the input will be:
{{ name }}
When applying the Plugin, in the applied project, the input asks the user for their name. Generated files that contain the expression {{ name }} will have the name typed by the user rendered instead of the expression.
If the Jinja variable has a hyphen, for example my-var, it can be an input variable and must be accessed as {{ inputs["my-var"] }}.
If it does not have a hyphen, you can use {{ myvar }} or {{ inputs.myvar }}.
Filters
Variables can be transformed using filters. The syntax for applying a filter to a variable is:
{{ variable | filter }}
It is possible to apply more than one filter by chaining several pipes (|), as in the example below:
{{ variable | filter1 | filter2 }}
In the above example, the variable value is modified by filter1 and then modified by filter2.
Example of the default capitalize filter:
{{ name | capitalize }}
This filter converts the entered name so that the first letter is uppercase and the others are lowercase.
For more information on Jinja's default filters, see the official Jinja documentation about filters.
In addition to Jinja's standard filters, StackSpot provides some useful filters for Stack creators:
pascalcase: converts strings to PascalCase format.camelcase: converts strings to camelCase format.kebabcase: converts strings to kebab-case format.cobolcase: converts strings to COBOL-CASE format.snakecase: converts strings to snake_case format.macrocase: converts strings to MACRO_CASE format.group_id_folder: converts strings by replacing.with/. This filter is useful for building folder names from Java package names.regex_replace: returns a copy of the value with all occurrences of a substring replaced with a new one.from_json: converts a JSON string into a Python dictionary.
You can provide an optional third argument count to regex_replace. Only the first count occurrences will be replaced:
# {{ "string value" | regex_replace("substring pattern", "replacement string") }}
{{ "Hello World" | regex_replace("Hello", "Goodbye") }}
-> Goodbye World
# {{ "string value" | regex_replace("substring pattern", "replacement string", count) }}
{{ "aaathree" | regex_replace("a", "two", 2) }}
-> two, two, athree
You can use variables that contain the value to be replaced.
Consider that the variable hello_world has the value "dogs, cats, birds":
# {{ variable | regex_replace('substring pattern', 'replacement string') }}
{{ hello_world | regex_replace('birds', 'humans') }}
-> dogs, cats, humans
The variable hello_world now has the value "dogs, cats, humans".
It is possible to use regular expression groups in the replacement string:
Consider that the variable hello_world has the value "burger":
# {{ string value or variable | regex_replace(define group pattern, replacement string group) }}
{{ hello_world | regex_replace('((\w+\s*)+)', 'cheese\1y') }}
-> cheeseburger
The variable hello_world now has the value "cheeseburger".
Example of the from_json filter:
{
"input1": "value1"
}
{% set my_dict = json_input | from_json %}
{{ my_dict.input1 }}
Control structures
The StackSpot portal does not support using Jinja control structures in the web interface. Therefore, it is not guaranteed that all Jinja templates will be successfully executed in the browser.
The control structures present in Jinja are if/elif/else/endif and for/else/endfor.
Use the if/elif/else/endif structure
Commands of the if/elif/else/endif structure allow you to define blocks that will be rendered according to a condition. The example below illustrates the use of an if block to control the printing of part of a template:
{% if age >= 18 %}
Would you like a drink?
{% elif age >= 5 %}
Would you like a soft drink?
{% else %}
Would you like a juice?
{% endif %}
In the above example, logic is implemented to determine the message that will be printed according to the value of the age variable.
The comparison operators available in Jinja are:
==: compares the two operands, returningtrueif they are equal.!=: compares the two operands, returningtrueif they are different.>:trueif the operand on the left is larger than the operand on the right.>=:trueif the operand on the left is greater than or equal to the operand on the right.<:trueif the operand on the left is smaller than the operand on the right.<=:trueif the operand on the left is less than or equal to the operand on the right.
The following logical operators can be used in expressions:
and: returnstrueif both left and right expressions are true.or: returnstrueif one of the expressions is true.not: negates the result of an expression.(expr): groups logical expressions.
The
elifandelseblocks are optional, and you can use more than oneelifblock if needed.
Use the for/else/endfor structure
The for/else/endfor commands allow you to define a repeating block for each element in a list, as in the example below:
{% for name in names %}
Hello {{ name }}!
{% else %}
No names given!
{% endfor %}
In the example above, for each name in the names list, a line with a greeting for the entered name will be rendered.
If the list of names is empty, the content of the block after the else will be rendered.
The
elseblock is optional and can be omitted if not needed.
Using Jinja on StackSpot
Generation of files in Plugins
Jinja templates are text files that can contain expressions and variables interpolated by the engine to generate the final result. When creating a Plugin, a templates folder is created, which contains the Jinja templates used to generate files when the Plugin is used.
When a user applies a Plugin or creates a new app, the Plugin rendering cycle is executed. The Jinja templates present in each of the applied Plugins are interpolated using the inputs entered by the user as variables that can be used in Jinja expressions in those files.
Jinja expressions can also be used to define dynamic file and folder names based on user inputs.
To illustrate the use of Jinja in file generation, consider a Plugin with the following structure:
example-plugin/
├── plugin.yaml
└── templates
├── file.txt
├── example_folder
│ └── nested_file.txt
└── {{folder_name}}
└── {{file_name}}.txt
The Plugin configuration in the plugin.yaml file contains the definition of the inputs that will be requested from the user:
schema-version: v4
kind: plugin
metadata:
name: example-plugin
display-name: example-plugin
description: Example plugin
version: 0.0.1
spec:
type: app
compatibility:
- python
docs:
pt-br: docs/pt-br/docs.md
en-us: docs/en-us/docs.md
technologies:
- Api
inputs:
- label: Folder name
type: text
name: folder_name
- label: File name
type: text
name: file_name
- label: Content
type: text
name: content
In the example above, three inputs are requested: folder_name, file_name and content, all strings.
Notice that there is a folder called {{folder_name}} and a file called {{file_name}}.txt, which show how to use Jinja expressions to define folder and file names dynamically based on user inputs.
The contents of the {{file_name}}.txt file will be:
Content: {{content}}
The file.txt and nested_file.txt files are empty and only illustrate that the entire file structure contained in the templates folder is generated after the Plugin is applied and can have as many files and folders as required.
When applying the Plugin to an empty app folder, the STK CLI will ask for inputs as in the example below:
$ cd app
$ stk apply plugin -p ../example-plugin
? The current folder doesn't seem to be the part of a StackSpot project. Do you still want to proceed applying the plugin? Yes
? Folder name my-folder
? File name my-file
? Content my-content
- Plugin example-plugin applied.
And the file structure generated will be:
app
├── example_folder
│ └── nested_file.txt
├── example.txt
├── file.txt
├── my-folder
│ └── my-file.txt
└── stk.yaml
Note that the folder name and files containing Jinja expressions were interpolated with the values entered by the user in the inputs.
The contents of the my-file.txt file after rendering will be:
Content: my-content
Note that the contents of the file have been interpolated by the Jinja engine by replacing the expression {{content}} with the contents typed in by the user, my-content.
Jinja expressions in configuration files
In addition to file generation, Jinja expressions can be used in the configuration YAML of Plugins.
Jinja expressions on computed inputs and global computed inputs
Jinja expressions can be used to calculate the computed-inputs and global-computed-inputs of Plugins, as in the example below:
schema-version: v4
kind: plugin
metadata:
name: example-plugin
display-name: example-plugin
description: Example plugin
version: 0.0.1
spec:
type: app
compatibility:
- python
docs:
pt-br: docs/pt-br/docs.md
en-us: docs/en-us/docs.md
technologies:
- Api
inputs:
- label: Name
type: text
name: name
computed-inputs:
name_uppercase: "{{ name | upper }}"
global-computed-inputs:
name_lowercase: "{{ name | lower }}"
In the example above, the variables name_uppercase and name_lowercase are created using Jinja expressions that take the input name and apply filters to convert the typed name to uppercase and lowercase.
Jinja Expressions in Declarative Hooks
Jinja expressions can be used in some Declarative Hooks configurations and in the file snippets used by Hooks. The following are the attributes that accept Jinja expressions for each Hook type.
- Declarative Hook of type
run: Jinja expressions can be used in bothcommandandworkdir:
hooks:
- type: run
trigger: before-render
workdir: {{ folder_name }}/{{ nested_folder_name }}
command: echo "Hello {{ name }}!"
- Declarative Hook of type
edit: Jinja expressions can be used on thepath,changes.search.string,changes.search.patternattributes, at all points wherevalueis used, and on thenot-existswithin awhen:
- type: edit
trigger: after-render
path: src/{{ some_input }}.txt
changes:
- insert:
line: 0
value: "{{ another_input }}"
when:
not-exists: "{{ another_input }}"
- search:
string: "{{ example }}"
insert-before:
value: "using {{ value }}"
- search:
pattern: {{ pattern_to_search }}
replace-by:
value: bla
- Declarative hook of type
edit-xml: Jinja expressions can be used in thepathand anyvalue:
- type: edit-xml
trigger: after-render
path: {{ some_variable }}.xml
changes:
- xpath: .//dependency
append:
value: |
<a>{{ some_variable }}</a>
- Declarative Hook of type
edit-json: Jinja expressions can be used onpathand anyvalue:
- type: edit-json
trigger: after-render
path: using-template.json
encoding: ascii
changes:
- jsonpath: "$.scripts"
update:
value: |
{
"{{ json_tag }}": "{{ json_value }}"
}
- Declarative Hook of type
edit-yaml: Jinja expressions can be used onpathand anyvalue:
- type: edit-yaml
trigger: after-render
path: using-template.yaml
encoding: ascii
changes:
- yamlpath: "$.scripts"
update:
value: |
{{ json_tag }}: {{ json_body }}
Referencing Connection Interface in Terraform
V1
requires:
connection-interface:
- connection-interface-one
In Terraform, attributes are referenced with the Connection Interface, according to the model:
{{ connection_interface_one_CONNECTOR }}
{{ connection_interface_one_CONNECTOR_engine }}
{{ connection_interface_one_CONNECTOR_is_stackspot_fun }}
In V1, this model continues to work, but it is not recommended to use it in V2.
V2
Now it is possible to access the attributes of the Connection Interfaces using the connections object. You can also include an alias in the syntax. There are two ways to access the attributes:
-
Kebab case: access using
[...]and a string, similar to a dictionary:{{ connections["connection-interface-one"].engine }} -
Snake case: access the attributes by converting the Kebab to snake_case:
{{ connections.connection_interface_one.is_stackspot_fun }}
An extra option is to access by camelCase, if the alias uses this convention:
{{ connections["connectionInterfaceOne"].engine }}
Next step
Check the section about Metadata usage in Plugins and learn where and how to use metadata when creating your Plugins.