Commit 1a78a5f9 authored by Yann Hodique's avatar Yann Hodique Committed by GitHub

feat(kurtosis-devnet): add template expansion helper (#13490)

We need a few things:
- some convenience functions for expanding user functions
- optional data to drive the template
- strict expansion (missingkley=error)
parent 0067de05
package tmpl
import (
"fmt"
"io"
"text/template"
)
// TemplateFunc represents a function that can be used in templates
type TemplateFunc func(string) (string, error)
// TemplateContext contains data and functions to be passed to templates
type TemplateContext struct {
Data interface{}
Functions map[string]TemplateFunc
}
type TemplateContextOptions func(*TemplateContext)
func WithFunction(name string, fn TemplateFunc) TemplateContextOptions {
return func(ctx *TemplateContext) {
ctx.Functions[name] = fn
}
}
func WithData(data interface{}) TemplateContextOptions {
return func(ctx *TemplateContext) {
ctx.Data = data
}
}
// NewTemplateContext creates a new TemplateContext with default functions
func NewTemplateContext(opts ...TemplateContextOptions) *TemplateContext {
ctx := &TemplateContext{
Functions: make(map[string]TemplateFunc),
}
for _, opt := range opts {
opt(ctx)
}
return ctx
}
// InstantiateTemplate reads a template from the reader, executes it with the context,
// and writes the result to the writer
func (ctx *TemplateContext) InstantiateTemplate(reader io.Reader, writer io.Writer) error {
// Read template content
templateBytes, err := io.ReadAll(reader)
if err != nil {
return fmt.Errorf("failed to read template: %w", err)
}
// Convert TemplateFunc map to FuncMap
funcMap := template.FuncMap{}
for name, fn := range ctx.Functions {
funcMap[name] = fn
}
// Create template with helper functions and option to error on missing fields
tmpl := template.New("template").
Funcs(funcMap).
Option("missingkey=error")
// Parse template
tmpl, err = tmpl.Parse(string(templateBytes))
if err != nil {
return fmt.Errorf("failed to parse template: %w", err)
}
// Execute template with context
if err := tmpl.Execute(writer, ctx.Data); err != nil {
return fmt.Errorf("failed to execute template: %w", err)
}
return nil
}
package tmpl
import (
"bytes"
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func TestNewTemplateContext(t *testing.T) {
t.Run("creates empty context", func(t *testing.T) {
ctx := NewTemplateContext()
require.Nil(t, ctx.Data, "expected nil Data in new context")
require.Empty(t, ctx.Functions, "expected empty Functions map in new context")
})
t.Run("adds data with WithData option", func(t *testing.T) {
data := map[string]string{"key": "value"}
ctx := NewTemplateContext(WithData(data))
require.NotNil(t, ctx.Data, "expected non-nil Data in context")
d, ok := ctx.Data.(map[string]string)
require.True(t, ok)
require.Equal(t, "value", d["key"])
})
t.Run("adds function with WithFunction option", func(t *testing.T) {
fn := func(s string) (string, error) { return s + "test", nil }
ctx := NewTemplateContext(WithFunction("testfn", fn))
require.Len(t, ctx.Functions, 1, "expected one function in context")
_, ok := ctx.Functions["testfn"]
require.True(t, ok, "function not added with correct name")
})
}
func TestInstantiateTemplate(t *testing.T) {
t.Run("simple template substitution", func(t *testing.T) {
data := map[string]string{"name": "world"}
ctx := NewTemplateContext(WithData(data))
input := strings.NewReader("Hello {{.name}}!")
var output bytes.Buffer
err := ctx.InstantiateTemplate(input, &output)
require.NoError(t, err)
expected := "Hello world!"
require.Equal(t, expected, output.String())
})
t.Run("template with custom function", func(t *testing.T) {
upper := func(s string) (string, error) { return strings.ToUpper(s), nil }
ctx := NewTemplateContext(
WithData(map[string]string{"name": "world"}),
WithFunction("upper", upper),
)
input := strings.NewReader("Hello {{upper .name}}!")
var output bytes.Buffer
err := ctx.InstantiateTemplate(input, &output)
require.NoError(t, err)
expected := "Hello WORLD!"
require.Equal(t, expected, output.String())
})
t.Run("invalid template syntax", func(t *testing.T) {
ctx := NewTemplateContext()
input := strings.NewReader("Hello {{.name")
var output bytes.Buffer
err := ctx.InstantiateTemplate(input, &output)
require.Error(t, err, "expected error for invalid template syntax")
})
t.Run("missing data field", func(t *testing.T) {
ctx := NewTemplateContext()
input := strings.NewReader("Hello {{.name}}!")
var output bytes.Buffer
err := ctx.InstantiateTemplate(input, &output)
require.Error(t, err, "expected error for missing data field")
})
t.Run("multiple functions and data fields", func(t *testing.T) {
upper := func(s string) (string, error) { return strings.ToUpper(s), nil }
lower := func(s string) (string, error) { return strings.ToLower(s), nil }
data := map[string]string{
"greeting": "Hello",
"name": "World",
}
ctx := NewTemplateContext(
WithData(data),
WithFunction("upper", upper),
WithFunction("lower", lower),
)
input := strings.NewReader("{{upper .greeting}} {{lower .name}}!")
var output bytes.Buffer
err := ctx.InstantiateTemplate(input, &output)
require.NoError(t, err)
expected := "HELLO world!"
require.Equal(t, expected, output.String())
})
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment