Skip to content

Commit

Permalink
Implement min/max using text/template lt func
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewkroh committed Jul 19, 2023
1 parent 4836ff9 commit 4cdc09c
Show file tree
Hide file tree
Showing 4 changed files with 160 additions and 134 deletions.
8 changes: 2 additions & 6 deletions x-pack/filebeat/docs/inputs/input-httpjson.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -214,12 +214,8 @@ Some built-in helper functions are provided to work with the input state inside
- `hmacBase64`: calculates the hmac signature of a list of strings concatenated together. Returns a base64 encoded signature. Supports sha1 or sha256. Example `[[hmac "sha256" "secret" "string1" "string2" (formatDate (now) "RFC1123")]]`
- `hmac`: calculates the hmac signature of a list of strings concatenated together. Returns a hex encoded signature. Supports sha1 or sha256. Example `[[hmac "sha256" "secret" "string1" "string2" (formatDate (now) "RFC1123")]]`
- `join`: joins a list using the specified separator. Example: `[[join .body.arr ","]]`
- `maxInt`: returns the maximum value of a list of signed integers where at least one value is required. If no values are provided, it returns an error.
- `minInt`: returns the minimum value of a list of signed integers where at least one value is required. If no values are provided, it returns an error.
- `maxUint`: returns the maximum value of a list of unsigned integers where at least one value is required. If no values are provided, it returns an error.
- `minUint`: returns the minimum value of a list of unsigned integers where at least one value is required. If no values are provided, it returns an error.
- `maxFloat`: returns the maximum value of a list of floating point numbers where at least one value is required. If no values are provided, it returns an error.
- `minFloat`: returns the minimum value of a list of floating point numbers where at least one value is required. If no values are provided, it returns an error.
- `max`: returns the maximum of two values.
- `min`: returns the minimum of two values.
- `mul`: multiplies two integers.
- `now`: returns the current `time.Time` object in UTC. Optionally, it can receive a `time.Duration` as a parameter. Example: `[[now (parseDuration "-1h")]]` returns the time at 1 hour before now.
- `parseDate`: parses a date string and returns a `time.Time` in UTC. By default the expected layout is `RFC3339` but optionally can accept any of the Golang predefined layouts or a custom one. Example: `[[ parseDate "2020-11-05T12:25:32Z" ]]`, `[[ parseDate "2020-11-05T12:25:32.1234567Z" "RFC3339Nano" ]]`, `[[ (parseDate "Thu Nov 5 12:25:32 +0000 2020" "Mon Jan _2 15:04:05 -0700 2006").UTC ]]`.
Expand Down
107 changes: 107 additions & 0 deletions x-pack/filebeat/input/httpjson/texttemplate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
// or more contributor license agreements. Licensed under the Elastic License;
// you may not use this file except in compliance with the Elastic License.

package httpjson

import (
"errors"
"reflect"
)

// These functions come from Go's text/template/funcs.go (1.19).
//
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

var (
errBadComparisonType = errors.New("invalid type for comparison")
errBadComparison = errors.New("incompatible types for comparison")
)

type kind int

const (
invalidKind kind = iota
boolKind
complexKind
intKind
floatKind
stringKind
uintKind
)

func basicKind(v reflect.Value) (kind, error) {
switch v.Kind() {
case reflect.Bool:
return boolKind, nil
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return intKind, nil
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return uintKind, nil
case reflect.Float32, reflect.Float64:
return floatKind, nil
case reflect.Complex64, reflect.Complex128:
return complexKind, nil
case reflect.String:
return stringKind, nil
}
return invalidKind, errBadComparisonType
}

// indirectInterface returns the concrete value in an interface value,
// or else the zero reflect.Value.
// That is, if v represents the interface value x, the result is the same as reflect.ValueOf(x):
// the fact that x was an interface value is forgotten.
func indirectInterface(v reflect.Value) reflect.Value {
if v.Kind() != reflect.Interface {
return v
}
if v.IsNil() {
return reflect.Value{}
}
return v.Elem()
}

// lt evaluates the comparison a < b.
func lt(arg1, arg2 reflect.Value) (bool, error) {
arg1 = indirectInterface(arg1)
k1, err := basicKind(arg1)
if err != nil {
return false, err
}
arg2 = indirectInterface(arg2)
k2, err := basicKind(arg2)
if err != nil {
return false, err
}
truth := false
if k1 != k2 {
// Special case: Can compare integer values regardless of type's sign.
switch {
case k1 == intKind && k2 == uintKind:
truth = arg1.Int() < 0 || uint64(arg1.Int()) < arg2.Uint()
case k1 == uintKind && k2 == intKind:
truth = arg2.Int() >= 0 && arg1.Uint() < uint64(arg2.Int())
default:
return false, errBadComparison
}
} else {
switch k1 {
case boolKind, complexKind:
return false, errBadComparisonType
case floatKind:
truth = arg1.Float() < arg2.Float()
case intKind:
truth = arg1.Int() < arg2.Int()
case stringKind:
truth = arg1.String() < arg2.String()
case uintKind:
truth = arg1.Uint() < arg2.Uint()
default:
panic("invalid kind")
}
}
return truth, nil
}
51 changes: 22 additions & 29 deletions x-pack/filebeat/input/httpjson/value_tpl.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,6 @@ type valueTpl struct {
*template.Template
}

// number is a custom interface type that can be used to represent a number in a template.
type number interface{ ~int | ~uint | ~float64 }

func (t *valueTpl) Unpack(in string) error {
tpl, err := template.New("").
Option("missingkey=error").
Expand All @@ -69,12 +66,8 @@ func (t *valueTpl) Unpack(in string) error {
"hmacBase64": hmacStringBase64,
"join": join,
"toJSON": toJSON,
"maxInt": max[int],
"minInt": min[int],
"maxUint": max[uint],
"minUint": min[uint],
"maxFloat": max[float64],
"minFloat": min[float64],
"max": max,
"min": min,
"mul": mul,
"now": now,
"parseDate": parseDate,
Expand Down Expand Up @@ -304,30 +297,30 @@ func div(a, b int64) int64 {
return a / b
}

// min returns the minimum value of a list of numbers.
func min[T number](head T, tail ...T) T {
for _, t := range tail {
if t != t {
return t
}
if t < head {
head = t
}
func min(arg1, arg2 reflect.Value) (interface{}, error) {
lessThan, err := lt(arg1, arg2)
if err != nil {
return nil, err
}

// arg1 is < arg2.
if lessThan {
return arg1.Interface(), nil
}
return head
return arg2.Interface(), nil
}

// max returns the maximum value of a list of numbers.
func max[T number](head T, tail ...T) T {
for _, t := range tail {
if t != t {
return t
}
if t > head {
head = t
}
func max(arg1, arg2 reflect.Value) (interface{}, error) {
lessThan, err := lt(arg1, arg2)
if err != nil {
return nil, err
}

// arg1 is < arg2.
if lessThan {
return arg2.Interface(), nil
}
return head
return arg1.Interface(), nil
}

func base64Encode(values ...string) string {
Expand Down
128 changes: 29 additions & 99 deletions x-pack/filebeat/input/httpjson/value_tpl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,144 +357,74 @@ func TestValueTpl(t *testing.T) {
expectedVal: "4",
},
{
name: "func maxInt",
value: `[[maxInt 1 4]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedVal: "4",
},
{
name: "func maxInt with list",
value: `[[maxInt 1 4 5 3 2]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedVal: "5",
},
{
name: "func maxInt with year expression",
value: `[[maxInt (now.Year) 2023]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedVal: "2023",
},
{
name: "func maxInt with single value",
value: `[[maxInt 2023]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedVal: "2023",
},
{
name: "func maxInt with no arguments",
value: `[[maxInt]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedError: `template: :1:2: executing "" at <maxInt>: wrong number of args for maxInt: want at least 1 got 0`,
},
{
name: "func maxInt with unknown type in list",
value: `[[maxInt 1 "b" -2]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedError: `template: :1:11: executing "" at <"b">: expected integer; found "b"`,
},
{
name: "func maxUint",
value: `[[maxUint 18446744073709551615 18446744073709551614]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedVal: "18446744073709551615",
},
{
name: "func minUint",
value: `[[minUint 18446744073709551615 18446744073709551614]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedVal: "18446744073709551614",
},
{
name: "func minInt",
value: `[[minInt 1 4]]`,
name: "func min int",
value: `[[min 4 1]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedVal: "1",
},
{
name: "func minInt with list",
value: `[[minInt 4 6 2 1 3 7]]`,
name: "func max int",
value: `[[max 4 1]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedVal: "1",
},
{
name: "func minInt with unknown type in list",
value: `[[minInt 1 "b" -2]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedError: `template: :1:11: executing "" at <"b">: expected integer; found "b"`,
expectedVal: "4",
},
{
name: "func minInt with year expression",
value: `[[ minInt (now.Year) 2023 ]]`,
name: "func max float",
value: `[[max 1.23 4.666]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedVal: "2023",
expectedVal: "4.666",
},
{
name: "func minInt with single value",
value: `[[ minInt 2023 ]]`,
name: "func min float",
value: `[[min 1.23 4.666]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedVal: "2023",
},
{
name: "func minInt with no arguments",
value: `[[minInt]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedError: `template: :1:2: executing "" at <minInt>: wrong number of args for minInt: want at least 1 got 0`,
expectedVal: "1.23",
},
{
name: "func maxFloat",
value: `[[maxFloat 1.23 4.666]]`,
name: "func min string",
value: `[[min "a" "b"]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedVal: "4.666",
expectedVal: "a",
},
{
name: "func maxFloat with list",
value: `[[maxFloat 1.23 4.667 4.666]]`,
name: "func max string",
value: `[[max "a" "b"]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedVal: "4.667",
expectedVal: "b",
},
{
name: "func maxFloat with full range",
value: `[[maxFloat 1.7976931348623157e+308 -1.7976931348623157e+308]]`,
name: "func min int64 unix seconds",
value: `[[ min (now.Unix) 1689771139 ]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedVal: "1.7976931348623157e+308",
expectedVal: "1689771139",
},
{
name: "func minFloat",
value: `[[minFloat 1.23 4.666]]`,
name: "func min int year",
value: `[[ min (now.Year) 2020 ]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedVal: "1.23",
expectedVal: "2020",
},
{
name: "func minFloat with list",
value: `[[minFloat -2.23 1.23 4.667 4.666]]`,
name: "func max duration",
value: `[[ max (parseDuration "59m") (parseDuration "1h") ]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedVal: "-2.23",
expectedVal: "1h0m0s",
},
{
name: "func minFloat with full range",
value: `[[minFloat 1.7976931348623157e+308 -1.7976931348623157e+308]]`,
name: "func min int ",
value: `[[ min (now.Year) 2020 ]]`,
paramCtx: emptyTransformContext(),
paramTr: transformable{},
expectedVal: "-1.7976931348623157e+308",
expectedVal: "2020",
},
{
name: "func sha1 hmac Hex",
Expand Down

0 comments on commit 4cdc09c

Please sign in to comment.