Skip to content

Commit

Permalink
More Implicit JSON Serialization (#20)
Browse files Browse the repository at this point in the history
Just taking a string on to the end of a template is very naive, it's
entrely possible some clever user will put an action in each if/else
clause and we'll miss one.  Instead walk the parse tree and augment
actions that don't set variables.  A more acceptable solution than #19
proposed.
  • Loading branch information
spjmurray authored Apr 22, 2020
1 parent 7ee3089 commit 8a289ad
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 14 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,17 @@ Dynamic attributes are best thought of as functions: they accept input arguments
Dynamic attributes do not have any side effects.

In general, dynamic attributes use the Go language https://golang.org/pkg/text/template/[template^] library with a few specializations.
All pipelines and functions are fully supported.
Actions involving control flow (e.g. `if` and `range`) are not supported at present.
Arguments are confined to scalar values only (typically `int`, `string` and `bool`) and undefined values (`nil`).
Control flow is allowed, however, each dynamic attribute should execute at one action in order to generate a value.
Dot is not defined initially, all data must be accessed though accessor functions.
Iteration is not supported.

=== Dynamic Attribute Typing

The key thing to note is that Go language templating operates on text.
Text has no concept of type (other than being a string) therefore all dynamic attributes must be serialized to JSON in order to preserve type information and allow the Service Broker to make the correct decisions.
The JSON serialization function is added implicitly, by the Service Broker, to all pipelines.
The JSON serialization function is added implicitly, by the Service Broker, to all action pipelines that would usually generate template output.
All dynamic attributes are initially defined as strings, however the attribute itself takes on the type of the value returned by attribute template processing.

Internally, the templating engine treats all data as abstract values.
Functions, however, may require a parameter to be of a specific type.
The templating engine will attempt to cast from an abstract value to a concrete data type where required by a function argument.
If this conversion fails, an error is raised.

==== Optional Attributes

All attribute templates are optional by default.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,8 +115,7 @@ The result type will be any type.
== `json`

The `json` function serializes its input as a JSON string.
All templates implicitly append the JSON function to their pipelines as this is required by the Service Broker for all operations.
This is only performed for simple, single pipeline actions, therefore complex action involving control flow are not supported at present.
All action pipelines that generate output are implicitly appended with the JSON function.

[source]
----
Expand Down
51 changes: 48 additions & 3 deletions pkg/provisioners/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"math/big"
"strings"
"text/template"
"text/template/parse"
"time"

"github.com/couchbase/service-broker/pkg/errors"
Expand Down Expand Up @@ -295,6 +296,50 @@ const (
templateSuffix = "}}"
)

// jsonify is a command to be appended to actions in the template
// parse tree.
var jsonify = &parse.CommandNode{
NodeType: parse.NodeCommand,
Args: []parse.Node{
parse.NewIdentifier("json"),
},
}

// transformActionsToJSON walks the parse tree and finds any actions that
// would usually generate text output. These are appened with a JSON function
// that turns the abstract type into a JSON string, preserving type for later
// decoding and patching into the resource structure.
func transformActionsToJSON(n parse.Node) {
if n == nil {
return
}

switch node := n.(type) {
case *parse.ActionNode:
if node.Pipe != nil && len(node.Pipe.Decl) == 0 {
node.Pipe.Cmds = append(node.Pipe.Cmds, jsonify)
}
case *parse.BranchNode:
transformActionsToJSON(node.List)
transformActionsToJSON(node.ElseList)
case *parse.CommandNode:
for _, arg := range node.Args {
transformActionsToJSON(arg)
}
case *parse.IfNode:
transformActionsToJSON(node.BranchNode.List)
transformActionsToJSON(node.BranchNode.ElseList)
case *parse.ListNode:
for _, item := range node.Nodes {
transformActionsToJSON(item)
}
case *parse.PipeNode:
for _, cmd := range node.Cmds {
transformActionsToJSON(cmd)
}
}
}

// renderTemplateString takes a string and returns either the literal value if it's
// not a template or the object returned after template rendering.
func renderTemplateString(str string, entry *registry.Entry) (interface{}, error) {
Expand All @@ -309,9 +354,6 @@ func renderTemplateString(str string, entry *registry.Entry) (interface{}, error

glog.V(log.LevelDebug).Infof("resolving dynamic attribute %s", str)

// Implictly add in a JSON transformation to preserve type and structure.
str = fmt.Sprintf("%s| json }}", str[:len(str)-len(templateSuffix)])

funcs := map[string]interface{}{
"registry": templateFunctionRegistry(entry),
"parameter": templateFunctionParameter(entry),
Expand All @@ -330,6 +372,9 @@ func renderTemplateString(str string, entry *registry.Entry) (interface{}, error
return nil, err
}

// Implictly add in a JSON transformation to preserve type and structure.
transformActionsToJSON(tmpl.Root)

buf := &bytes.Buffer{}
if err := tmpl.Execute(buf, nil); err != nil {
return nil, errors.NewConfigurationError("dynamic attribute resolution failed: %v", err)
Expand Down

0 comments on commit 8a289ad

Please sign in to comment.