Skip to content

Commit

Permalink
feat(builtins): add a new builtin function strings.render_template to…
Browse files Browse the repository at this point in the history
… render templated strings

This adds support for rendering of templated strings utilizing Golang's text/template library.
For a given templated string and key/value mapping of template var inputs, this builtin will
inject the values into the template where they are referenced by key.

Fixes #6371
Signed-off-by: Rohan Vasavada <rohanvasavada@gmail.com>
  • Loading branch information
RDVasavada authored and ashutosh-narkar committed Nov 17, 2023
1 parent 31b1633 commit d46bc9d
Show file tree
Hide file tree
Showing 5 changed files with 160 additions and 0 deletions.
15 changes: 15 additions & 0 deletions ast/builtins.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ var DefaultBuiltins = [...]*Builtin{
TrimSpace,
Sprintf,
StringReverse,
RenderTemplate,

// Numbers
NumbersRange,
Expand Down Expand Up @@ -1317,6 +1318,20 @@ var StringReverse = &Builtin{
Categories: stringsCat,
}

var RenderTemplate = &Builtin{
Name: "strings.render_template",
Description: `Renders a templated string with given template variables injected. For a given templated string and key/value mapping, values will be injected into the template where they are referenced by key.
For examples of templating syntax, see https://pkg.go.dev/text/template`,
Decl: types.NewFunction(
types.Args(
types.Named("value", types.S).Description("a templated string"),
types.Named("vars", types.NewObject(nil, types.NewDynamicProperty(types.S, types.A))).Description("a mapping of template variable keys to values"),
),
types.Named("result", types.S).Description("rendered template with template variables injected"),
),
Categories: stringsCat,
}

/**
* Numbers
*/
Expand Down
26 changes: 26 additions & 0 deletions builtin_metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@
"startswith",
"strings.any_prefix_match",
"strings.any_suffix_match",
"strings.render_template",
"strings.replace_n",
"strings.reverse",
"substring",
Expand Down Expand Up @@ -15898,6 +15899,31 @@
},
"wasm": false
},
"strings.render_template": {
"args": [
{
"description": "a templated string",
"name": "value",
"type": "string"
},
{
"description": "a mapping of template variable keys to values",
"name": "vars",
"type": "object[string: any]"
}
],
"available": [
"edge"
],
"description": "Renders a templated string with given template variables injected. For a given templated string and key/value mapping, values will be injected into the template where they are referenced by key.\n\tFor examples of templating syntax, see https://pkg.go.dev/text/template",
"introduced": "edge",
"result": {
"description": "rendered template with template variables injected",
"name": "result",
"type": "string"
},
"wasm": false
},
"strings.replace_n": {
"args": [
{
Expand Down
25 changes: 25 additions & 0 deletions capabilities.json
Original file line number Diff line number Diff line change
Expand Up @@ -3880,6 +3880,31 @@
"type": "function"
}
},
{
"name": "strings.render_template",
"decl": {
"args": [
{
"type": "string"
},
{
"dynamic": {
"key": {
"type": "string"
},
"value": {
"type": "any"
}
},
"type": "object"
}
],
"result": {
"type": "string"
},
"type": "function"
}
},
{
"name": "strings.replace_n",
"decl": {
Expand Down
49 changes: 49 additions & 0 deletions test/cases/testdata/rendertemplate/rendertemplate.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
cases:
- note: rendertemplate/simple
query: data.test.p = x
modules:
- |
package test
template_string = `{{.test}}`
template_vars = {`test`: `hello world`}
p = strings.render_template(template_string, template_vars)
want_result:
- x: 'hello world'

- note: rendertemplate/simpleint
query: data.test.p = x
modules:
- |
package test
template_string = `{{.test}}`
template_vars = {`test`: 2023}
p = strings.render_template(template_string, template_vars)
want_result:
- x: '2023'

- note: rendertemplate/complex
query: data.test.p = x
modules:
- |
package test
template_string = `{{range $i, $name := .hellonames}}{{if $i}},{{end}}hello {{$name}}{{end}}`
template_vars = {`hellonames`: [`rohan`, `john doe`]}
p = strings.render_template(template_string, template_vars)
want_result:
- x: 'hello rohan,hello john doe'

- note: rendertemplate/missingkey
query: data.test.p = x
modules:
- |
package test
template_string = `{{.testvarnotprovided}}`
template_vars = {`test`: `hello world`}
p = strings.render_template(template_string, template_vars)
want_error_code: eval_builtin_error
strict_error: true

45 changes: 45 additions & 0 deletions topdown/template.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package topdown

import (
"bytes"
"text/template"

"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/topdown/builtins"
)

func renderTemplate(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
preContentTerm, err := builtins.StringOperand(operands[0].Value, 1)
if err != nil {
return err
}

templateVariablesTerm, err := builtins.ObjectOperand(operands[1].Value, 2)
if err != nil {
return err
}

var templateVariables map[string]interface{}

if err := ast.As(templateVariablesTerm, &templateVariables); err != nil {
return err
}

tmpl, err := template.New("template").Parse(string(preContentTerm))
if err != nil {
return err
}

// Do not attempt to render if template variable keys are missing
tmpl.Option("missingkey=error")
var buf bytes.Buffer
if err := tmpl.Execute(&buf, templateVariables); err != nil {
return err
}

return iter(ast.StringTerm(buf.String()))
}

func init() {
RegisterBuiltinFunc(ast.RenderTemplate.Name, renderTemplate)
}

0 comments on commit d46bc9d

Please sign in to comment.