feat: an Argument type that evaluates a template against the Request

Added a new "source": "template" argument type, that evaluates a Go text/template against a context containing the request Body, Query, Payload and Headers, enabling much richer mapping from request attributes to argument parameters.
This commit is contained in:
Ian Roberts 2024-10-20 22:07:34 +01:00
parent 9f725b2cb0
commit 9892bc678b
2 changed files with 138 additions and 63 deletions

View file

@ -41,6 +41,7 @@ const (
SourceEntirePayload string = "entire-payload"
SourceEntireQuery string = "entire-query"
SourceEntireHeaders string = "entire-headers"
SourceTemplate string = "template"
)
const (
@ -438,6 +439,76 @@ type Argument struct {
Name string `json:"name,omitempty"`
EnvName string `json:"envname,omitempty"`
Base64Decode bool `json:"base64decode,omitempty"`
// if the Argument is SourceTemplate, this will be the compiled template,
// otherwise it will be nil
template *template.Template
}
// UnmarshalJSON parses an Argument in the normal way, and then allows the
// newly-loaded Argument to do any necessary post-processing.
func (ha *Argument) UnmarshalJSON(text []byte) error {
// First unmarshal as normal, skipping the custom unmarshaller
type jsonArgument Argument
if err := json.Unmarshal(text, (*jsonArgument)(ha)); err != nil {
return err
}
return ha.postProcess()
}
// postProcess does the necessary post-unmarshal processing for this argument.
// If the argument is a SourceTemplate it compiles the template string into an
// executable template. This method is idempotent, i.e. it is safe to call
// more than once on the same Argument
func (ha *Argument) postProcess() error {
if ha.Source == SourceTemplate && ha.template == nil {
// now compile the template
var err error
ha.template, err = template.New("argument").Option("missingkey=zero").Parse(ha.Name)
return err
}
return nil
}
// templateContext is the context passed as "." to the template executed when
// getting an Argument of type SourceTemplate
type templateContext struct {
ID string
ContentType string
Body []byte
Headers map[string]interface{}
Query map[string]interface{}
Payload map[string]interface{}
Method string
RemoteAddr string
}
// BodyText is a convenience to access the request Body as a string. This means
// you can just say {{ .BodyText }} instead of having to do a trick like
// {{ printf "%s" .Body }}
func (ctx *templateContext) BodyText() string {
return string(ctx.Body)
}
// GetHeader is a function to fetch a specific item out of the headers map
// by its case insensitive name. The header name is converted to canonical form
// before being looked up in the header map, e.g. {{ .GetHeader "x-request-id" }}
func (ctx *templateContext) GetHeader(name string) interface{} {
return ctx.Headers[textproto.CanonicalMIMEHeaderKey(name)]
}
func (ha *Argument) runTemplate(r *Request) (string, error) {
w := &strings.Builder{}
ctx := &templateContext{
r.ID, r.ContentType, r.Body, r.Headers, r.Query, r.Payload, r.RawRequest.Method, r.RawRequest.RemoteAddr,
}
err := ha.template.Execute(w, ctx)
if err == nil {
return w.String(), nil
}
return "", err
}
// Get Argument method returns the value for the Argument's key name
@ -500,6 +571,9 @@ func (ha *Argument) Get(r *Request) (string, error) {
}
return string(res), nil
case SourceTemplate:
return ha.runTemplate(r)
}
if source != nil {