From df6ef4d1b69a064ae3f6cd74076d98f5cd12b2ca Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 02:12:04 +0000 Subject: [PATCH 1/3] Initial plan From 0e9e777621b22905d4c636308222389e3021c563 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 02:16:40 +0000 Subject: [PATCH 2/3] feat: add uuid, now(format), trimPrefix, trimSuffix template functions Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> --- module/pipeline_template.go | 46 +++++++++++-- module/pipeline_template_test.go | 114 +++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+), 4 deletions(-) diff --git a/module/pipeline_template.go b/module/pipeline_template.go index a83dde7c..0cdfb7bf 100644 --- a/module/pipeline_template.go +++ b/module/pipeline_template.go @@ -94,16 +94,46 @@ func (te *TemplateEngine) resolveValue(v any, pc *PipelineContext) (any, error) } } +// timeLayouts maps common Go time constant names to their layout strings. +var timeLayouts = map[string]string{ + "ANSIC": time.ANSIC, + "UnixDate": time.UnixDate, + "RubyDate": time.RubyDate, + "RFC822": time.RFC822, + "RFC822Z": time.RFC822Z, + "RFC850": time.RFC850, + "RFC1123": time.RFC1123, + "RFC1123Z": time.RFC1123Z, + "RFC3339": time.RFC3339, + "RFC3339Nano": time.RFC3339Nano, + "Kitchen": time.Kitchen, + "Stamp": time.Stamp, + "StampMilli": time.StampMilli, + "StampMicro": time.StampMicro, + "StampNano": time.StampNano, + "DateTime": time.DateTime, + "DateOnly": time.DateOnly, + "TimeOnly": time.TimeOnly, +} + // templateFuncMap returns the function map available in pipeline templates. func templateFuncMap() template.FuncMap { return template.FuncMap{ - // uuidv4 generates a new UUID v4 string. + // uuid generates a new UUID v4 string. + "uuid": func() string { + return uuid.New().String() + }, + // uuidv4 generates a new UUID v4 string (alias for uuid). "uuidv4": func() string { return uuid.New().String() }, - // now returns the current time in RFC3339 format (UTC). - "now": func() string { - return time.Now().UTC().Format(time.RFC3339) + // now returns the current UTC time formatted with the given Go time layout + // string or named constant (e.g. "RFC3339", "2006-01-02"). + "now": func(layout string) string { + if l, ok := timeLayouts[layout]; ok { + layout = l + } + return time.Now().UTC().Format(layout) }, // lower converts a string to lowercase. "lower": strings.ToLower, @@ -117,6 +147,14 @@ func templateFuncMap() template.FuncMap { } return val }, + // trimPrefix removes the given prefix from a string if present. + "trimPrefix": func(prefix, s string) string { + return strings.TrimPrefix(s, prefix) + }, + // trimSuffix removes the given suffix from a string if present. + "trimSuffix": func(suffix, s string) string { + return strings.TrimSuffix(s, suffix) + }, // json marshals a value to a JSON string. "json": func(v any) string { b, err := json.Marshal(v) diff --git a/module/pipeline_template_test.go b/module/pipeline_template_test.go index afd865c9..cabe78f3 100644 --- a/module/pipeline_template_test.go +++ b/module/pipeline_template_test.go @@ -218,6 +218,120 @@ func TestTemplateEngine_ResolveMap_ErrorPropagation(t *testing.T) { } } +func TestTemplateEngine_FuncUUID(t *testing.T) { + te := NewTemplateEngine() + pc := NewPipelineContext(nil, nil) + + result, err := te.Resolve("{{ uuid }}", pc) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // UUID v4 has the form xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx + if len(result) != 36 { + t.Errorf("expected UUID of length 36, got %q (len=%d)", result, len(result)) + } + if result[14] != '4' { + t.Errorf("expected UUID version 4, got %q", result) + } +} + +func TestTemplateEngine_FuncNowRFC3339(t *testing.T) { + te := NewTemplateEngine() + pc := NewPipelineContext(nil, nil) + + result, err := te.Resolve(`{{ now "RFC3339" }}`, pc) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(result) == 0 { + t.Error("expected non-empty timestamp") + } + // RFC3339 strings contain 'T' and 'Z' or offset + if !strings.Contains(result, "T") { + t.Errorf("expected RFC3339 timestamp, got %q", result) + } +} + +func TestTemplateEngine_FuncNowRawLayout(t *testing.T) { + te := NewTemplateEngine() + pc := NewPipelineContext(nil, nil) + + result, err := te.Resolve(`{{ now "2006-01-02" }}`, pc) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // Should be a date of the form YYYY-MM-DD + if len(result) != 10 { + t.Errorf("expected date of length 10, got %q", result) + } +} + +func TestTemplateEngine_FuncTrimPrefix(t *testing.T) { + te := NewTemplateEngine() + pc := NewPipelineContext(map[string]any{"phone": "+15551234567"}, nil) + + result, err := te.Resolve(`{{ .phone | trimPrefix "+" }}`, pc) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result != "15551234567" { + t.Errorf("expected '15551234567', got %q", result) + } +} + +func TestTemplateEngine_FuncTrimPrefixNotPresent(t *testing.T) { + te := NewTemplateEngine() + pc := NewPipelineContext(map[string]any{"val": "hello"}, nil) + + result, err := te.Resolve(`{{ .val | trimPrefix "world" }}`, pc) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result != "hello" { + t.Errorf("expected 'hello', got %q", result) + } +} + +func TestTemplateEngine_FuncTrimSuffix(t *testing.T) { + te := NewTemplateEngine() + pc := NewPipelineContext(map[string]any{"val": "hello.txt"}, nil) + + result, err := te.Resolve(`{{ .val | trimSuffix ".txt" }}`, pc) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result != "hello" { + t.Errorf("expected 'hello', got %q", result) + } +} + +func TestTemplateEngine_FuncTrimSuffixNotPresent(t *testing.T) { + te := NewTemplateEngine() + pc := NewPipelineContext(map[string]any{"val": "hello"}, nil) + + result, err := te.Resolve(`{{ .val | trimSuffix ".txt" }}`, pc) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result != "hello" { + t.Errorf("expected 'hello', got %q", result) + } +} + +func TestTemplateEngine_FuncDefault(t *testing.T) { + te := NewTemplateEngine() + pc := NewPipelineContext(nil, nil) + pc.MergeStepOutput("parse", map[string]any{"query_params": map[string]any{}}) + + result, err := te.Resolve(`{{ .steps.parse.query_params.page_size | default "25" }}`, pc) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if result != "25" { + t.Errorf("expected '25', got %q", result) + } +} + func TestTemplateEngine_ResolveMap_DoesNotMutateInput(t *testing.T) { te := NewTemplateEngine() pc := NewPipelineContext(map[string]any{"x": "resolved"}, nil) From a0d369638b5a087fc38ca591b386fcf516bc0b3b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 27 Feb 2026 02:51:43 +0000 Subject: [PATCH 3/3] fix: make now() backward compatible with zero-argument form Co-authored-by: intel352 <77607+intel352@users.noreply.github.com> --- module/pipeline_template.go | 12 +++++++++--- module/pipeline_template_test.go | 13 +++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/module/pipeline_template.go b/module/pipeline_template.go index 0cdfb7bf..c66ed3f3 100644 --- a/module/pipeline_template.go +++ b/module/pipeline_template.go @@ -129,9 +129,15 @@ func templateFuncMap() template.FuncMap { }, // now returns the current UTC time formatted with the given Go time layout // string or named constant (e.g. "RFC3339", "2006-01-02"). - "now": func(layout string) string { - if l, ok := timeLayouts[layout]; ok { - layout = l + // When called with no argument it defaults to RFC3339. + "now": func(args ...string) string { + layout := time.RFC3339 + if len(args) > 0 && args[0] != "" { + if l, ok := timeLayouts[args[0]]; ok { + layout = l + } else { + layout = args[0] + } } return time.Now().UTC().Format(layout) }, diff --git a/module/pipeline_template_test.go b/module/pipeline_template_test.go index cabe78f3..029c179b 100644 --- a/module/pipeline_template_test.go +++ b/module/pipeline_template_test.go @@ -235,6 +235,19 @@ func TestTemplateEngine_FuncUUID(t *testing.T) { } } +func TestTemplateEngine_FuncNowNoArgs(t *testing.T) { + te := NewTemplateEngine() + pc := NewPipelineContext(nil, nil) + + result, err := te.Resolve(`{{ now }}`, pc) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(result, "T") { + t.Errorf("expected RFC3339 timestamp from no-arg now, got %q", result) + } +} + func TestTemplateEngine_FuncNowRFC3339(t *testing.T) { te := NewTemplateEngine() pc := NewPipelineContext(nil, nil)