Skip to content

Commit be277f7

Browse files
authored
Templating functions - setHeader, initArray, variadic concat (#1201)
* Add templating functions for setHeader, initArray and concat * fix doc
1 parent 26b5a4a commit be277f7

File tree

5 files changed

+156
-10
lines changed

5 files changed

+156
-10
lines changed

core/templating/template_helpers.go

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,20 @@ func (t templateHelpers) split(target, separator string) []string {
116116
return strings.Split(target, separator)
117117
}
118118

119-
func (t templateHelpers) concat(val1, val2 string) string {
120-
return val1 + val2
119+
// Concatenates any number of arguments together as strings.
120+
func (t templateHelpers) concat(args ...interface{}) string {
121+
var parts []string
122+
for _, arg := range args {
123+
// If arg is a slice, flatten it
124+
if s, ok := arg.([]interface{}); ok {
125+
for _, v := range s {
126+
parts = append(parts, fmt.Sprint(v))
127+
}
128+
} else {
129+
parts = append(parts, fmt.Sprint(arg))
130+
}
131+
}
132+
return strings.Join(parts, "")
121133
}
122134

123135
func (t templateHelpers) isNumeric(stringToCheck string) bool {
@@ -498,6 +510,24 @@ func (t templateHelpers) setStatusCode(statusCode string, options *raymond.Optio
498510
return ""
499511
}
500512

513+
func (t templateHelpers) setHeader(headerName string, headerValue string, options *raymond.Options) string {
514+
if headerName == "" {
515+
log.Error("header name cannot be empty")
516+
return ""
517+
}
518+
internalVars := options.ValueFromAllCtx("InternalVars").(map[string]interface{})
519+
var headers map[string][]string
520+
if h, ok := internalVars["setHeaders"]; ok {
521+
headers = h.(map[string][]string)
522+
} else {
523+
headers = make(map[string][]string)
524+
}
525+
// Replace or add the header
526+
headers[headerName] = []string{headerValue}
527+
internalVars["setHeaders"] = headers
528+
return ""
529+
}
530+
501531
func (t templateHelpers) sum(numbers []string, format string) string {
502532
return sumNumbers(numbers, format)
503533
}
@@ -548,6 +578,13 @@ func (t templateHelpers) addToArray(key string, value string, output bool, optio
548578
}
549579
}
550580

581+
// Initializes (clears) an array in the template context under the given key.
582+
func (t templateHelpers) initArray(key string, options *raymond.Options) string {
583+
arrayData := options.ValueFromAllCtx("Kvs").(map[string]interface{})
584+
arrayData[key] = []string{}
585+
return ""
586+
}
587+
551588
func (t templateHelpers) getArray(key string, options *raymond.Options) []string {
552589
arrayData := options.ValueFromAllCtx("Kvs").(map[string]interface{})
553590
if array, ok := arrayData[key]; ok {

core/templating/template_helpers_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,18 @@ import (
77
. "github.com/onsi/gomega"
88
)
99

10+
// mockRaymondOptions is a minimal mock for raymond.Options for testing
11+
type mockRaymondOptions struct {
12+
internalVars map[string]interface{}
13+
}
14+
15+
func (m *mockRaymondOptions) ValueFromAllCtx(key string) interface{} {
16+
if key == "InternalVars" {
17+
return m.internalVars
18+
}
19+
return nil
20+
}
21+
1022
func testNow() time.Time {
1123
parsedTime, _ := time.Parse("2006-01-02T15:04:05Z", "2018-01-01T00:00:00Z")
1224
return parsedTime
@@ -93,13 +105,22 @@ func Test_split(t *testing.T) {
93105
}
94106

95107
func Test_concat(t *testing.T) {
108+
96109
RegisterTestingT(t)
97110

98111
unit := templateHelpers{}
99112

100113
Expect(unit.concat("one", " two")).To(Equal("one two"))
101114
}
102115

116+
func Test_concatWithManyStrings(t *testing.T) {
117+
RegisterTestingT(t)
118+
119+
unit := templateHelpers{}
120+
121+
Expect(unit.concat("one", " two", " three", " four")).To(Equal("one two three four"))
122+
}
123+
103124
func Test_length(t *testing.T) {
104125
RegisterTestingT(t)
105126

core/templating/templating.go

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ type Request struct {
4141
Host string
4242
}
4343

44-
4544
type Templator struct {
4645
SupportedMethodMap map[string]interface{}
4746
TemplateHelper templateHelpers
@@ -62,7 +61,7 @@ func NewEnrichedTemplator(journal *journal.Journal) *Templator {
6261
now: time.Now,
6362
fakerSource: gofakeit.New(0),
6463
TemplateDataSource: templateDataSource,
65-
journal: journal,
64+
journal: journal,
6665
}
6766

6867
helperMethodMap["now"] = t.nowHelper
@@ -105,11 +104,13 @@ func NewEnrichedTemplator(journal *journal.Journal) *Templator {
105104
helperMethodMap["journal"] = t.parseJournalBasedOnIndex
106105
helperMethodMap["hasJournalKey"] = t.hasJournalKey
107106
helperMethodMap["setStatusCode"] = t.setStatusCode
107+
helperMethodMap["setHeader"] = t.setHeader
108108
helperMethodMap["sum"] = t.sum
109109
helperMethodMap["add"] = t.add
110110
helperMethodMap["subtract"] = t.subtract
111111
helperMethodMap["multiply"] = t.multiply
112112
helperMethodMap["divide"] = t.divide
113+
helperMethodMap["initArray"] = t.initArray
113114
helperMethodMap["addToArray"] = t.addToArray
114115
helperMethodMap["getArray"] = t.getArray
115116
helperMethodMap["putValue"] = t.putValue
@@ -146,11 +147,20 @@ func (t *Templator) RenderTemplate(tpl *raymond.Template, requestDetails *models
146147

147148
ctx := t.NewTemplatingData(requestDetails, literals, vars, state)
148149
result, err := tpl.Exec(ctx)
149-
if err == nil {
150-
statusCode, ok := ctx.InternalVars["statusCode"]
151-
if ok && response != nil {
150+
if err == nil && response != nil {
151+
// Set status code if present
152+
if statusCode, ok := ctx.InternalVars["statusCode"]; ok {
152153
response.Status = statusCode.(int)
153154
}
155+
// Set headers if present
156+
if setHeaders, ok := ctx.InternalVars["setHeaders"]; ok {
157+
if response.Headers == nil {
158+
response.Headers = make(map[string][]string)
159+
}
160+
for k, v := range setHeaders.(map[string][]string) {
161+
response.Headers[k] = v
162+
}
163+
}
154164
}
155165
return result, err
156166
}

core/templating/templating_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,18 @@ func Test_ApplyTemplate_MatchingRowsCsvAndReturnMatchedString(t *testing.T) {
8080
Expect(template).To(Equal(`Test2`))
8181
}
8282

83+
func Test_ApplyTemplate_InitArray_ClearsArray(t *testing.T) {
84+
RegisterTestingT(t)
85+
86+
template, err := ApplyTemplate(
87+
&models.RequestDetails{},
88+
make(map[string]string),
89+
`{{addToArray 'myArray' 'one' false}}{{addToArray 'myArray' 'two' false}}{{addToArray 'myArray' 'three' false}}{{addToArray 'myArray' 'four' false}}{{addToArray 'myArray' 'five' false}}{{initArray 'myArray'}}{{addToArray 'myArray' 'six' false}}{{#each (getArray 'myArray')}}{{this}}{{/each}}`,
90+
)
91+
92+
Expect(err).To(BeNil())
93+
Expect(template).To(Equal("six"))
94+
}
8395
func Test_ApplyTemplate_MatchingRowsCsvMissingDataSource(t *testing.T) {
8496
RegisterTestingT(t)
8597

@@ -917,6 +929,22 @@ func Test_ApplyTemplate_setStatusCode_should_handle_nil_response(t *testing.T) {
917929
Expect(template).To(Equal(""))
918930
}
919931

932+
func Test_ApplyTemplate_setHeader(t *testing.T) {
933+
RegisterTestingT(t)
934+
935+
templator := templating.NewTemplator()
936+
937+
template, err := templator.ParseTemplate(`{{ setHeader "X-Test-Header" "HeaderValue" }}`)
938+
Expect(err).To(BeNil())
939+
940+
response := &models.ResponseDetails{Headers: map[string][]string{}}
941+
result, err := templator.RenderTemplate(template, &models.RequestDetails{}, response, &models.Literals{}, &models.Variables{}, make(map[string]string))
942+
943+
Expect(err).To(BeNil())
944+
Expect(result).To(Equal(""))
945+
Expect(response.Headers).To(HaveKeyWithValue("X-Test-Header", []string{"HeaderValue"}))
946+
}
947+
920948
func toInterfaceSlice(arguments []string) []interface{} {
921949
argumentsArray := make([]interface{}, len(arguments))
922950

docs/pages/keyconcepts/templating/templating.rst

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ Currently, you can get the following data from request to the response via templ
4949
Helper Methods
5050
--------------
5151

52-
Additional data can come from helper methods. These are the ones Hoverfly currently support:
52+
Additional data can come from helper methods. These are the ones Hoverfly currently supports:
5353

5454
+-----------------------------------------------------------+-----------------------------------------------------------+-----------------------------------------+
5555
| Description | Example | Result |
@@ -529,14 +529,19 @@ In this case, you can use the internal key value data store. The following helpe
529529
+----------------------------+--------------------------------------------+-----------------------+
530530
| Get an entry | ``{{ getValue 'id' }}`` | 123 |
531531
+----------------------------+--------------------------------------------+-----------------------+
532-
| Add a value to an arra | ``{{ addToArray 'names' 'John' true }}`` | John |
532+
| Add a value to an array | ``{{ addToArray 'names' 'John' true }}`` | John |
533+
| Get an array | ``{{ getArray 'names' }}`` | ["John"] |
534+
| Clear an array | ``{{ initArray 'names' }}`` | [] |
533535
+----------------------------+--------------------------------------------+-----------------------+
534536
| Get an array | ``{{ getArray 'names' }}`` | []string{"John" |
535537
+----------------------------+--------------------------------------------+-----------------------+
536538

539+
537540
``addToArray`` will create a new array if one doesn't exist. The boolean argument in ``putValue`` and ``addToArray``
538541
is used to control whether the set value is returned.
539542

543+
``initArray`` will clear an existing array (set it to empty) or create a new empty array if it does not exist. This is useful for resetting arrays inside loops or before reusing them in a template.
544+
540545
.. note::
541546

542547
Each templating session has its own key value store, which means all the data you set will be cleared after the current response is rendered.
@@ -601,7 +606,7 @@ You can use the following helper methods to join, split or replace string values
601606
+-----------------------------------------------------------+-----------------------------------------------------------+-----------------------------------------+
602607
| Description | Example | Result |
603608
+===========================================================+===========================================================+=========================================+
604-
| String concatenate | ``{{ concat 'bee' 'hive' }}`` | beehive |
609+
| String concatenate | ``{{ concat 'bee' 'hive' 'buzz' }}`` | beehivebuzz |
605610
+-----------------------------------------------------------+-----------------------------------------------------------+-----------------------------------------+
606611
| String splitting | ``{{ split 'bee,hive' ',' }}`` | []string{"bee", "hive"} |
607612
+-----------------------------------------------------------+-----------------------------------------------------------+-----------------------------------------+
@@ -654,6 +659,51 @@ To learn about more advanced templating functionality, such as looping and condi
654659

655660
Global Literals and Variables
656661
-----------------------------
662+
Setting properties on the response
663+
----------------------------------
664+
665+
Hoverfly provides helper functions to set properties on the HTTP response directly from your templates. This allows you to dynamically control the status code and headers based on request data or logic in your template.
666+
667+
Setting the Status Code
668+
~~~~~~~~~~~~~~~~~~~~~~~
669+
You can set the HTTP status code of the response using the ``setStatusCode`` helper. This is useful for conditional logic, such as returning a 404 if a resource is not found, or a 200 if an operation succeeds.
670+
671+
.. code:: handlebars
672+
673+
{{ setStatusCode 404 }}
674+
675+
You can use this helper inside conditional blocks:
676+
677+
.. code:: handlebars
678+
679+
{{#equal (csvDeleteRows 'pets' 'category' 'cats' true) '0'}}
680+
{{ setStatusCode 404 }}
681+
{"Message":"Error no cats found"}
682+
{{else}}
683+
{{ setStatusCode 200 }}
684+
{"Message":"All cats deleted"}
685+
{{/equal}}
686+
687+
If you provide an invalid status code (e.g., outside the range 100-599), it will be ignored.
688+
689+
Setting Response Headers
690+
~~~~~~~~~~~~~~~~~~~~~~~~
691+
You can set or override HTTP response headers using the ``setHeader`` helper. This is useful for adding custom headers, controlling caching, or setting content types dynamically.
692+
693+
.. code:: handlebars
694+
695+
{{ setHeader "X-Custom-Header" "HeaderValue" }}
696+
697+
You can use this helper multiple times to set different headers, or inside conditional blocks to set headers based on logic:
698+
699+
.. code:: handlebars
700+
701+
{{ setHeader "Content-Type" "application/json" }}
702+
{{ setHeader "X-Request-Id" (randomUuid) }}
703+
704+
If the header already exists, it will be overwritten with the new value.
705+
706+
Both helpers do not output anything to the template result; they only affect the response properties.
657707
You can define global literals and variables for templated response. This comes in handy when you
658708
have a lot of templated responses that share the same constant values or helper methods.
659709

0 commit comments

Comments
 (0)