From 79d1ebb277dd98f430694a37b7564b3e1e12c829 Mon Sep 17 00:00:00 2001 From: Richard Kosegi Date: Thu, 22 Aug 2024 21:59:05 +0200 Subject: [PATCH] Pipeline: add template functions to serialize DOM to various formats Signed-off-by: Richard Kosegi --- pipeline/executor.go | 2 +- pipeline/export_op.go | 2 +- pipeline/template_engine.go | 19 ++++++++------ pipeline/template_engine_funcs.go | 30 ++++++++++++++++------ pipeline/template_engine_test.go | 42 +++++++++++++++++++++++++++++++ 5 files changed, 77 insertions(+), 18 deletions(-) diff --git a/pipeline/executor.go b/pipeline/executor.go index aca18d9..a8947a8 100644 --- a/pipeline/executor.go +++ b/pipeline/executor.go @@ -47,7 +47,7 @@ func (ac actContext) Executor() Executor { return ac.e } func (ac actContext) TemplateEngine() TemplateEngine { return ac.t } func (ac actContext) Logger() Logger { return ac.l } func (ac actContext) Snapshot() map[string]interface{} { - return dom.DefaultNodeMappingFn(ac.Data()).(map[string]interface{}) + return dom.DefaultNodeEncoderFn(ac.Data()).(map[string]interface{}) } func (p *exec) newCtx(a Action) *actContext { diff --git a/pipeline/export_op.go b/pipeline/export_op.go index aff28fb..54f6c46 100644 --- a/pipeline/export_op.go +++ b/pipeline/export_op.go @@ -95,7 +95,7 @@ func (e *ExportOp) Do(ctx ActionContext) (err error) { } return enc(f, d.(dom.Leaf).Value()) } - return enc(f, dom.DefaultNodeMappingFn(d.(dom.Container))) + return enc(f, dom.DefaultNodeEncoderFn(d.(dom.Container))) } func (e *ExportOp) CloneWith(ctx ActionContext) Action { diff --git a/pipeline/template_engine.go b/pipeline/template_engine.go index 9632ddd..0119ce4 100644 --- a/pipeline/template_engine.go +++ b/pipeline/template_engine.go @@ -30,14 +30,17 @@ type templateEngine struct { func renderTemplate(tmplStr string, data interface{}, fm template.FuncMap) (string, error) { tmpl := template.New("tmpl").Funcs(fm) tmpl.Funcs(template.FuncMap{ - "tpl": tplFunc(tmpl), - "toYaml": toYamlFunc, - "isEmpty": isEmptyFunc, - "unflatten": unflattenFunc, - "fileExists": fileExistsFunc, - "mergeFiles": mergeFilesFunc, - "isDir": isDirFunc, - "glob": globFunc, + "tpl": tplFunc(tmpl), + "toYaml": toYamlFunc, + "isEmpty": isEmptyFunc, + "unflatten": unflattenFunc, + "fileExists": fileExistsFunc, + "mergeFiles": mergeFilesFunc, + "isDir": isDirFunc, + "glob": globFunc, + "dom2yaml": dom2yamlFunc, + "dom2json": dom2jsonFunc, + "dom2properties": dom2propertiesFunc, }) _, err := tmpl.Parse(tmplStr) if err != nil { diff --git a/pipeline/template_engine_funcs.go b/pipeline/template_engine_funcs.go index 51fabab..f9e7829 100644 --- a/pipeline/template_engine_funcs.go +++ b/pipeline/template_engine_funcs.go @@ -17,9 +17,9 @@ limitations under the License. package pipeline import ( - "fmt" "github.com/rkosegi/yaml-toolkit/analytics" "github.com/rkosegi/yaml-toolkit/dom" + "github.com/rkosegi/yaml-toolkit/props" "github.com/rkosegi/yaml-toolkit/utils" "os" "path/filepath" @@ -89,18 +89,32 @@ func globFunc(pattern string) ([]string, error) { return filepath.Glob(pattern) } -// mergeFilesFunc merges 0 or more files into single map[string]interface{} -func mergeFilesFunc(files []string) (map[string]interface{}, error) { +// mergeFilesFunc merges 0 or more files into dom.Container +func mergeFilesFunc(files []string) (dom.Container, error) { ds := analytics.NewDocumentSet() - result := make(map[string]interface{}) for _, f := range files { err := ds.AddDocumentFromFile(f, analytics.DefaultFileDecoderProvider(f)) if err != nil { return nil, err } } - for k, v := range ds.AsOne().Merged(dom.ListsMergeAppend()).Flatten() { - result[k] = fmt.Sprintf("%v", v.Value()) - } - return result, nil + return ds.AsOne().Merged(dom.ListsMergeAppend()), nil +} + +func dom2str(c dom.Container, encFn dom.EncoderFunc) (string, error) { + var buf strings.Builder + err := c.Serialize(&buf, dom.DefaultNodeEncoderFn, encFn) + return buf.String(), err +} + +func dom2yamlFunc(c dom.Container) (string, error) { + return dom2str(c, dom.DefaultYamlEncoder) +} + +func dom2jsonFunc(c dom.Container) (string, error) { + return dom2str(c, dom.DefaultJsonEncoder) +} + +func dom2propertiesFunc(c dom.Container) (string, error) { + return dom2str(c, props.EncoderFn) } diff --git a/pipeline/template_engine_test.go b/pipeline/template_engine_test.go index c30c15b..2bf8cc9 100644 --- a/pipeline/template_engine_test.go +++ b/pipeline/template_engine_test.go @@ -189,3 +189,45 @@ func TestTemplateFuncGlob(t *testing.T) { assert.NoError(t, err) assert.Equal(t, 1, len(files)) } + +func TestTemplateFuncDom2Yaml(t *testing.T) { + type testCase struct { + format string + exp string + } + d, err := os.MkdirTemp("", "yt*") + assert.NoError(t, err) + removeDirsLater(t, d) + f1, err := os.CreateTemp(d, "yt*.yaml") + assert.NoError(t, err) + f2, err := os.CreateTemp(d, "yt*.yaml") + assert.NoError(t, err) + _, err = f1.Write([]byte("a: 1")) + assert.NoError(t, err) + _, err = f2.Write([]byte("b: 2\n")) + assert.NoError(t, err) + for _, tc := range []testCase{ + { + format: "properties", + exp: "a=1", + }, + { + format: "yaml", + exp: "a: 1", + }, + { + format: "json", + exp: `"a": 1`, + }, + } { + var res string + res, err = renderTemplate(`{{ mergeFiles ( glob ( printf "%s/*.yaml" .Temp ) ) | dom2`+tc.format+` | trim }}`, + map[string]interface{}{ + "Temp": d, + }, sprig.HermeticTxtFuncMap()) + t.Logf("Merged content using format '%s':\n%s", tc.format, res) + assert.NoError(t, err) + assert.Contains(t, res, tc.exp) + } + removeFilesLater(t, f1, f2) +}