Skip to content

Commit

Permalink
extend templating with useful functions
Browse files Browse the repository at this point in the history
Signed-off-by: Adam Ludes <[email protected]>
  • Loading branch information
deerbone committed Oct 16, 2024
1 parent 2fa497a commit 3ad1631
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 13 deletions.
35 changes: 31 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,8 @@ Here is an example that includes query parameters gopherColor and gopherAge in t

Templates can also include data from the request body, allowing you to create dynamic responses based on the request data. Currently only JSON bodies are supported. The request also needs to have the correct content type set (Content-Type: application/json).

This example also showcases the functions `timeNow`, `timeUTC`, `timeAdd`, `timeFormat`, `jsonMarshal` and `stringsJoin` that are available for use in templates.

Here is an example that includes the request body in the response:

````json
Expand Down Expand Up @@ -651,38 +653,63 @@ Here is an example that includes the request body in the response:
"data": {
"type": "{{ .RequestBody.data.type }}",
"id": "{{ .PathParams.GopherID }}",
"timestamp": "{{ timeFormat (timeUTC (timeNow)) "2006-01-02 15:04" }}",
"birthday": "{{ timeFormat (timeAdd (timeNow) "24h") "2006-01-02" }}",
"attributes": {
"name": "{{ .RequestBody.data.attributes.name }}",
"color": "{{ stringsJoin .QueryParams.gopherColor "," }}",
"age": {{ index .QueryParams.gopherAge 0 }}
}
},
"friends": {{ jsonMarshal .RequestBody.data.friends }}
}
}
````
````json
// request body to POST /gophers/bca49e8a-82dd-4c5d-b886-13a6ceb3744b?gopherColor=Blue&gopherColor=Purple&gopherAge=42
{
"data": {
"type": "gophers",
"attributes": {
"name": "Natalissa",
}
"name": "Natalissa"
},
"friends": [
{
"name": "Zebediah",
"color": "Purple",
"age": 55
}
]
}
}
// response
{
"data": {
"type": "gophers",
"id": "bca49e8a-82dd-4c5d-b886-13a6ceb3744b",
"timestamp": "2006-01-02 15:04",
"birthday": "2006-01-03",
"attributes": {
"name": "Natalissa",
"color": "Blue,Purple",
"age": 42
}
},
"friends": [{"age":55,"color":"Purple","name":"Zebediah"}]
}
}
````

#### Available custom templating functions

These functions aren't part of the standard Go template functions, but are available for use in Killgrave templates:

- `timeNow`: Returns the current time (in RFC3339 format).
- `timeUTC`: Returns the current time in UTC (in RFC3339 format).
- `timeAdd`: Adds a duration to a time.Time object. Uses the [Go ParseDuration format](https://pkg.go.dev/time#ParseDuration).
- `timeFormat`: Formats a RFC3339 string using the provided layout. Uses the [Go time package layout](https://pkg.go.dev/time#pkg-constants).
- `jsonMarshal`: Marshals an object to a JSON string.
- `stringsJoin`: Concatenates an array of strings using a separator.


## Contributing
[Contributions](CONTRIBUTING.md) are more than welcome, if you are interested please follow our guidelines to help you get started.
Expand Down
41 changes: 39 additions & 2 deletions internal/server/http/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,44 @@ func applyTemplate(i Imposter, bodyBytes []byte, r *http.Request) ([]byte, error
}

tmpl, err := template.New("body").
Funcs(template.FuncMap{"stringsJoin": strings.Join}).
Funcs(template.FuncMap{
"stringsJoin": strings.Join,
"jsonMarshal": func(v interface{}) (string, error) {
b, err := json.Marshal(v)
if err != nil {
return "", err
}
return string(b), nil
},
"timeNow": func() string {
return time.Now().Format(time.RFC3339)
},
"timeUTC": func(t string) (string, error) {
parsedTime, err := time.Parse(time.RFC3339, t)
if err != nil {
return "", fmt.Errorf("error parsing time: %v", err)
}
return parsedTime.UTC().Format(time.RFC3339), nil
},
"timeAdd": func(t string, d string) (string, error) {
parsedTime, err := time.Parse(time.RFC3339, t)
if err != nil {
return "", fmt.Errorf("error parsing time: %v", err)
}
duration, err := time.ParseDuration(d)
if err != nil {
return "", fmt.Errorf("error parsing duration: %v", err)
}
return parsedTime.Add(duration).Format(time.RFC3339), nil
},
"timeFormat": func(t string, layout string) (string, error) {
parsedTime, err := time.Parse(time.RFC3339, t)
if err != nil {
return "", fmt.Errorf("error parsing time: %v", err)
}
return parsedTime.Format(layout), nil
},
}).
Parse(bodyStr)
if err != nil {
return nil, fmt.Errorf("error parsing template: %w", err)
Expand Down Expand Up @@ -111,7 +148,7 @@ func applyTemplate(i Imposter, bodyBytes []byte, r *http.Request) ([]byte, error

func extractBody(r *http.Request) (map[string]interface{}, error) {
body := make(map[string]interface{})
if r.Body == nil {
if r.Body == http.NoBody {
return body, nil
}

Expand Down
24 changes: 18 additions & 6 deletions internal/server/http/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"net/http/httptest"
"os"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -72,12 +73,19 @@ func TestImposterHandler(t *testing.T) {
func TestImposterHandlerTemplating(t *testing.T) {
bodyRequest := []byte(`{
"data": {
"type": "gophers",
"attributes": {
"name": "Natalissa"
}
"type": "gophers",
"attributes": {
"name": "Natalissa"
},
"friends": [
{
"name": "Zebediah",
"color": "Purple",
"age": 55
}
]
}
}`)
}`)
var headers = make(map[string]string)
headers["Content-Type"] = "application/json"

Expand All @@ -100,11 +108,14 @@ func TestImposterHandlerTemplating(t *testing.T) {
"data": {
"type": "gophers",
"id": "bca49e8a-82dd-4c5d-b886-13a6ceb3744b",
"timestamp": "` + time.Now().UTC().Format("2006-01-02 15:04") + `",
"birthday": "` + time.Now().UTC().Add(time.Hour*24).Format("2006-01-02") + `",
"attributes": {
"name": "Natalissa",
"color": "Blue,Purple",
"age": 42
}
},
"friends": [{"age":55,"color":"Purple","name":"Zebediah"}]
}
}
`
Expand All @@ -127,6 +138,7 @@ func TestImposterHandlerTemplating(t *testing.T) {
req.Header.Set("Content-Type", "application/json")

rec := httptest.NewRecorder()
tt.imposter.PopulateBodyData()
handler := ImposterHandler(tt.imposter)

handler.ServeHTTP(rec, req)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
"data": {
"type": "{{ .RequestBody.data.type }}",
"id": "{{ .PathParams.GopherID }}",
"timestamp": "{{ timeFormat (timeUTC (timeNow)) "2006-01-02 15:04" }}",
"birthday": "{{ timeFormat (timeAdd (timeUTC (timeNow)) "24h") "2006-01-02" }}",
"attributes": {
"name": "{{ .RequestBody.data.attributes.name }}",
"color": "{{ stringsJoin .QueryParams.gopherColor "," }}",
"age": {{ index .QueryParams.gopherAge 0 }}
}
},
"friends": {{ jsonMarshal .RequestBody.data.friends }}
}
}

0 comments on commit 3ad1631

Please sign in to comment.