diff --git a/build/config/substation.libsonnet b/build/config/substation.libsonnet index 8184568b..2a388fa3 100644 --- a/build/config/substation.libsonnet +++ b/build/config/substation.libsonnet @@ -24,6 +24,17 @@ }, num: $.condition.number, number: { + default: { + object: $.config.object, + value: null, + }, + gt(settings={}): $.condition.number.greater_than(settings=settings), + greater_than(settings={}): { + local default = $.condition.number.default, + + type: 'number_greater_than', + settings: std.prune(std.mergePatch(default, $.helpers.abbv(settings))), + }, bitwise: { and(settings={}): { local default = { diff --git a/build/config/substation_test.jsonnet b/build/config/substation_test.jsonnet index 62cd70a2..27bf51d9 100644 --- a/build/config/substation_test.jsonnet +++ b/build/config/substation_test.jsonnet @@ -7,6 +7,11 @@ local transform = sub.transform.object.copy(settings={ obj: { src: src, trg: trg local inspector = sub.condition.format.json(); { + condition: { + number: { + greater_than: sub.condition.number.greater_than({obj: {src: src}, value: 1}), + } + }, transform: { send: { http: { diff --git a/condition/condition.go b/condition/condition.go index 9c686492..59b5b0a9 100644 --- a/condition/condition.go +++ b/condition/condition.go @@ -24,7 +24,7 @@ type inspector interface { } // newInspector returns a configured Inspector from an Inspector configuration. -func newInspector(ctx context.Context, cfg config.Config) (inspector, error) { //nolint: cyclop // ignore cyclomatic complexity +func newInspector(ctx context.Context, cfg config.Config) (inspector, error) { //nolint: cyclop, gocyclo // ignore cyclomatic complexity switch cfg.Type { // Format inspectors. case "format_mime": @@ -58,6 +58,8 @@ func newInspector(ctx context.Context, cfg config.Config) (inspector, error) { / case "network_ip_valid": return newNetworkIPValid(ctx, cfg) // Number inspectors. + case "number_greater_than": + return newNumberGreaterThan(ctx, cfg) case "number_bitwise_and": return newNumberBitwiseAND(ctx, cfg) case "number_bitwise_or": diff --git a/condition/number.go b/condition/number.go index 7538b015..979c171c 100644 --- a/condition/number.go +++ b/condition/number.go @@ -49,3 +49,14 @@ func numberLengthMeasurement(b []byte, measurement string) int { return len(b) } } + +type numberConfig struct { + // Value used for comparison during inspection. + Value float64 `json:"value"` + + Object iconfig.Object `json:"object"` +} + +func (c *numberConfig) Decode(in interface{}) error { + return iconfig.Decode(in, c) +} diff --git a/condition/number_greater_than.go b/condition/number_greater_than.go new file mode 100644 index 00000000..fe95f8a0 --- /dev/null +++ b/condition/number_greater_than.go @@ -0,0 +1,54 @@ +package condition + +import ( + "context" + "encoding/json" + "strconv" + + "github.com/brexhq/substation/config" + "github.com/brexhq/substation/message" +) + +func newNumberGreaterThan(_ context.Context, cfg config.Config) (*numberGreaterThan, error) { + conf := numberConfig{} + if err := conf.Decode(cfg.Settings); err != nil { + return nil, err + } + + insp := numberGreaterThan{ + conf: conf, + } + + return &insp, nil +} + +type numberGreaterThan struct { + conf numberConfig +} + +func (insp *numberGreaterThan) Inspect(ctx context.Context, msg *message.Message) (output bool, err error) { + if msg.IsControl() { + return false, nil + } + + if insp.conf.Object.SourceKey == "" { + f, err := strconv.ParseFloat(string(msg.Data()), 64) + if err != nil { + return false, err + } + + return insp.match(f), nil + } + + v := msg.GetValue(insp.conf.Object.SourceKey) + return insp.match(v.Float()), nil +} + +func (c *numberGreaterThan) match(f float64) bool { + return f > c.conf.Value +} + +func (c *numberGreaterThan) String() string { + b, _ := json.Marshal(c.conf) + return string(b) +} diff --git a/condition/number_greater_than_test.go b/condition/number_greater_than_test.go new file mode 100644 index 00000000..0b2b0179 --- /dev/null +++ b/condition/number_greater_than_test.go @@ -0,0 +1,158 @@ +package condition + +import ( + "context" + "testing" + + "github.com/brexhq/substation/config" + "github.com/brexhq/substation/message" +) + +var _ inspector = &numberGreaterThan{} + +var numberGreaterThanTests = []struct { + name string + cfg config.Config + test []byte + expected bool +}{ + // Integers + { + "pass", + config.Config{ + Settings: map[string]interface{}{ + "object": map[string]interface{}{ + "source_key": "foo", + }, + "value": 1, + }, + }, + []byte(`{"foo":10}`), + true, + }, + { + "pass", + config.Config{ + Settings: map[string]interface{}{ + "value": 1, + }, + }, + []byte(`10`), + true, + }, + { + "fail", + config.Config{ + Settings: map[string]interface{}{ + "object": map[string]interface{}{ + "source_key": "foo", + }, + "value": 10, + }, + }, + []byte(`{"foo":1}`), + false, + }, + { + "fail", + config.Config{ + Settings: map[string]interface{}{ + "value": 10, + }, + }, + []byte(`1`), + false, + }, + // Floats + { + "pass", + config.Config{ + Settings: map[string]interface{}{ + "value": 1, + }, + }, + []byte(`1.5`), + true, + }, + { + "pass", + config.Config{ + Settings: map[string]interface{}{ + "value": 1.1, + }, + }, + []byte(`1.5`), + true, + }, + { + "fail", + config.Config{ + Settings: map[string]interface{}{ + "object": map[string]interface{}{ + "source_key": "foo", + }, + "value": 1.5, + }, + }, + []byte(`{"foo":1.1}`), + false, + }, + { + "fail", + config.Config{ + Settings: map[string]interface{}{ + "value": 1.5, + }, + }, + []byte(`1`), + false, + }, +} + +func TestNumberGreaterThan(t *testing.T) { + ctx := context.TODO() + + for _, test := range numberGreaterThanTests { + t.Run(test.name, func(t *testing.T) { + message := message.New().SetData(test.test) + insp, err := newNumberGreaterThan(ctx, test.cfg) + if err != nil { + t.Fatal(err) + } + + check, err := insp.Inspect(ctx, message) + if err != nil { + t.Error(err) + } + + if test.expected != check { + t.Errorf("expected %v, got %v", test.expected, check) + t.Errorf("settings: %+v", test.cfg) + t.Errorf("test: %+v", string(test.test)) + } + }) + } +} + +func benchmarkNumberGreaterThan(b *testing.B, insp *numberGreaterThan, message *message.Message) { + ctx := context.TODO() + for i := 0; i < b.N; i++ { + _, _ = insp.Inspect(ctx, message) + } +} + +func BenchmarkNumberGreaterThan(b *testing.B) { + for _, test := range numberGreaterThanTests { + insp, err := newNumberGreaterThan(context.TODO(), test.cfg) + if err != nil { + b.Fatal(err) + } + + b.Run(test.name, + func(b *testing.B) { + message := message.New().SetData(test.test) + benchmarkNumberGreaterThan(b, insp, message) + }, + ) + } +}