-
Notifications
You must be signed in to change notification settings - Fork 25.5k
Description
The problem
Today our only choice for a templating language is Mustache which, while fine for very simple templates, does not work well in the JSON context where we mostly use it (eg search templates, watches etc).
For instance, if you want to return an array of terms in a query, you can't do:
"terms": "{{ #toJson }} my_terms {{ /toJson }}"
because that would render as:
"terms": "[\"term_one\",\"term_two\"]"
Instead, the whole template has to be passed as one big string in order to remove the double quotes:
"terms": {{ #toJson }} my_terms {{ /toJson }}
Dealing with big queries as a string instead of as a JSON structure is complicated, finicky, and error prone.
On top of that, Mustache's limited support for defaults, conditionals, etc make anything but variable substitution difficult and hacky. Just see https://www.elastic.co/guide/en/elasticsearch/reference/5.4/search-template.html#_conditional_clauses as an example of how ugly it can get.
Making it JSON friendly
Instead, we need a templating solution where JSON is parsed as JSON, and any string element is treated as a potential template, the result of which can be any JSON-compatible data structure.
For instance:
"<< my_undefined_val >>" → null
"<< my_bool >>" → true
"<< my_num >>" → 5.4
"<< my_string >>" → "some string"
"<< my_array >>" → [ "foo", "bar" ]
"<< my_map >>" → { "foo": { "bar": "baz" }}
Additionally, templates can be embedded in strings in combination with non-templated strings:
"query": "status:<< status >> title:( << title >>)"
→
"query": "status:active title:( some search keywords)"
Any JSON-compatible data structure could be stringified to its JSON equivalent when used as an embedded template, eg:
"msg": "Here are the broken nodes: << array_of_nodes >>"
→
"msg": "Here are the broken nodes: [\"first\", \"second\"]"
Templates in JSON keys would always be stringified.
Using Painless
Painless is a fast, safe, and powerful scripting language with strong typing; in other words, parameters retain their types instead of just being considered as strings, in the way Mustache does.
Painless can be used as is with a wrapper implementing the stringification mentioned above. By far the majority of templates use simple value substitution:
{
"template": {
"lang": "painless",
"params": {
"statuses": [
"active",
"pending"
]
},
"inline": {
"query": {
"terms": "<< params.statuses >>"
}
}
}
}
Default values:
"range": {
"age": {
"gte": "<< params.min_age ? params.min_age : 21 >>",
"lte": "<< params.max_age ? params.max_age : 95 >>",
}
}
Conditionals:
"must": [
"<< if (params.only_active) { return [ 'term': [ 'active' : true ]] }; return; >>",
...
]
String manipulation:
"emails": "<< params.emails.join(', ') >>"
It could also be extended to make common templating use cases more succinct, eg
- easier default values for undefined variables (eg
params.num || 10
- easier
map
andfilter
functions for list manipulation - addition of functions to escape URI or HTML etc
- Support for JSONpath or similar to do, eg:
def new_nodes = jpath(params.nodes_info, '$.nodes[?(@.version > \"5.2.0\")].name' );
Of course, these additions would be backwards compatible, so old templates will keep working while new templates can take advantage of any new syntax.