Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions pkg/stream_transform/stream_transform_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,63 @@ func TestMeaningfulXMLStreamTransform(t *testing.T) {
}
}

func TestToJsonNaiveXMLTransform(t *testing.T) {
// Test the naive XML-to-JSON passthrough using toJson
input := `<?xml version="1.0" encoding="UTF-8"?>
<root xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<name>Test</name>
<count>42</count>
<items>
<item>one</item>
<item>two</item>
</items>
</root>`
tmpl := `{{ toJson . }}`
tfmFactory := NewStreamTransformerFactory(GolangTemplateXMLV2, tmpl)
if !tfmFactory.IsTransformable() {
t.Fatalf("failed to create transformer factory: is not transformable")
}
tfm, err := tfmFactory.GetTransformer(input)
if err != nil {
t.Fatalf("failed to create transformer: %v", err)
}
if err := tfm.Transform(); err != nil {
t.Fatalf("failed to transform: %v", err)
}
tfmOut := tfm.GetOutStream()
outputBytes, _ := io.ReadAll(tfmOut)
outputStr := string(outputBytes)

// The mxj library converts XML to a map, so we expect a JSON object
// with the root element as the top-level key
expected := `{"root":{"-xmlns":"http://s3.amazonaws.com/doc/2006-03-01/","count":42,"items":{"item":["one","two"]},"name":"Test"}}`
if outputStr != expected {
t.Fatalf("unexpected output:\ngot: '%s'\nwant: '%s'", outputStr, expected)
}
}

func TestObsoleteToJsonFails(t *testing.T) {
// Test the naive XML-to-JSON passthrough using toJson
input := `<?xml version="1.0" encoding="UTF-8"?>
<root>
<name>Test</name>
<count>42</count>
<items>
<item>one</item>
<item>two</item>
</items>
</root>`
tmpl := `{{ toJson . }}`
tfmFactory := NewStreamTransformerFactory(GolangTemplateXMLV1, tmpl)
if !tfmFactory.IsTransformable() {
t.Fatalf("failed to create transformer factory: is not transformable")
}
_, err := tfmFactory.GetTransformer(input)
if err == nil {
t.Fatalf("expected error when creating transformer but got none")
}
}

func TestOpensslCertTextStreamTransform(t *testing.T) {
input := openSSLCertTextOutput
t.Log("TestOpensslCertTextStreamTransform")
Expand Down
41 changes: 34 additions & 7 deletions pkg/stream_transform/template_stream_transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,20 @@ import (
"encoding/json"
"fmt"
"io"
"regexp"
"text/template"

"github.com/clbanning/mxj/v2"
"golang.org/x/mod/semver"
)

var (
tmplSemVerRegexp = regexp.MustCompile(`^(.+)_(v\d+\.\d+\.\d+)$`)
)

const (
GolangTemplateXMLV1 = "golang_template_mxj_v0.1.0"
GolangTemplateXMLV2 = "golang_template_mxj_v0.2.0"
GolangTemplateJSONV1 = "golang_template_json_v0.1.0"
GolangTemplateTextV1 = "golang_template_text_v0.1.0"
GolangTemplateUnspecifiedV1 = "golang_template_v0.1.0"
Expand All @@ -36,7 +43,7 @@ func NewStreamTransformerFactory(tplType string, tplStr string) StreamTransforme

func (stf *streamTransformerFactory) IsTransformable() bool {
switch stf.tplType {
case GolangTemplateXMLV1:
case GolangTemplateXMLV1, GolangTemplateXMLV2:
return true
case GolangTemplateJSONV1:
return true
Expand All @@ -51,20 +58,20 @@ func (stf *streamTransformerFactory) IsTransformable() bool {

func (stf *streamTransformerFactory) GetTransformer(input string) (StreamTransformer, error) {
switch stf.tplType {
case GolangTemplateXMLV1:
case GolangTemplateXMLV1, GolangTemplateXMLV2:
inStream := newXMLBestEffortReader(bytes.NewBufferString(input))
outStream := bytes.NewBuffer(nil)
tfm, err := newTemplateStreamTransformer(stf.tplStr, inStream, outStream)
tfm, err := newTemplateStreamTransformer(stf.tplType, stf.tplStr, inStream, outStream)
return tfm, err
case GolangTemplateJSONV1:
inStream := newJSONReader(bytes.NewBufferString(input))
outStream := bytes.NewBuffer(nil)
tfm, err := newTemplateStreamTransformer(stf.tplStr, inStream, outStream)
tfm, err := newTemplateStreamTransformer(stf.tplType, stf.tplStr, inStream, outStream)
return tfm, err
case GolangTemplateTextV1, GolangTemplateUnspecifiedV1:
inStream := newTextReader(bytes.NewBufferString(input))
outStream := bytes.NewBuffer(nil)
tfm, err := newTemplateStreamTransformer(stf.tplStr, inStream, outStream)
tfm, err := newTemplateStreamTransformer(stf.tplType, stf.tplStr, inStream, outStream)
return tfm, err
default:
return nil, fmt.Errorf("unsupported template type: %s", stf.tplType)
Expand Down Expand Up @@ -134,6 +141,16 @@ func getXPathAllOuter(xml string, path string) ([]string, error) {
return ss.GetAllFull(xml, path)
}

// toJson marshals any value to a JSON string.
// This enables simple XML-to-JSON transforms like: {{ toJson . }}
func toJson(v interface{}) (string, error) {
b, err := json.Marshal(v)
if err != nil {
return "", err
}
return string(b), nil
}

type ObjectReader interface {
Read() (interface{}, error)
}
Expand Down Expand Up @@ -224,11 +241,17 @@ type templateStreamTransfomer struct {
// }

func newTemplateStreamTransformer(
tmplType string,
tplStr string,
inStream ObjectReader,
outStream io.ReadWriter,
) (StreamTransformer, error) {
tpl, tplErr := template.New("__stream_tfm__").Funcs(template.FuncMap{
tmplSemVerMatches := tmplSemVerRegexp.FindStringSubmatch(tmplType)
if len(tmplSemVerMatches) != 3 {
return nil, fmt.Errorf("invalid template type format: %s", tmplType)
}
tmplSemVer := tmplSemVerMatches[2]
tmplFuncMap := template.FuncMap{
"separator": separator,
"jsonMapFromString": jsonMapFromString,
"getXPath": getXPathInner,
Expand All @@ -238,7 +261,11 @@ func newTemplateStreamTransformer(
"safeIndex": safeIndex,
"toBool": toBool,
"toInt": toInt,
}).Parse(tplStr)
}
if semver.Compare(tmplSemVer, "v0.2.0") >= 0 {
tmplFuncMap["toJson"] = toJson
}
tpl, tplErr := template.New("__stream_tfm__").Funcs(tmplFuncMap).Parse(tplStr)
if tplErr != nil {
return nil, tplErr
}
Expand Down
Loading