diff --git a/build/config/substation.libsonnet b/build/config/substation.libsonnet index 2a388fa3..90c6be88 100644 --- a/build/config/substation.libsonnet +++ b/build/config/substation.libsonnet @@ -25,8 +25,15 @@ num: $.condition.number, number: { default: { - object: $.config.object, - value: null, + object: $.config.object, + value: null, + }, + lt(settings={}): $.condition.number.less_than(settings=settings), + less_than(settings={}): { + local default = $.condition.number.default, + + type: 'number_less_than', + settings: std.prune(std.mergePatch(default, $.helpers.abbv(settings))), }, gt(settings={}): $.condition.number.greater_than(settings=settings), greater_than(settings={}): { diff --git a/build/config/substation_test.jsonnet b/build/config/substation_test.jsonnet index 27bf51d9..78eb7566 100644 --- a/build/config/substation_test.jsonnet +++ b/build/config/substation_test.jsonnet @@ -9,6 +9,7 @@ local inspector = sub.condition.format.json(); { condition: { number: { + less_than: sub.condition.number.less_than({obj: {src: src}, value: 1}), greater_than: sub.condition.number.greater_than({obj: {src: src}, value: 1}), } }, diff --git a/condition/condition.go b/condition/condition.go index 59b5b0a9..60215390 100644 --- a/condition/condition.go +++ b/condition/condition.go @@ -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_less_than": + return newNumberLessThan(ctx, cfg) case "number_greater_than": return newNumberGreaterThan(ctx, cfg) case "number_bitwise_and": diff --git a/condition/number_less_than.go b/condition/number_less_than.go new file mode 100644 index 00000000..602a0f6a --- /dev/null +++ b/condition/number_less_than.go @@ -0,0 +1,51 @@ +package condition + +import ( + "context" + "encoding/json" + "strconv" + + "github.com/brexhq/substation/config" + "github.com/brexhq/substation/message" +) + +func newNumberLessThan(_ context.Context, cfg config.Config) (*numberLessThan, error) { + conf := numberConfig{} + if err := conf.Decode(cfg.Settings); err != nil { + return nil, err + } + insp := numberLessThan{ + conf: conf, + } + return &insp, nil +} + +type numberLessThan struct { + conf numberConfig +} + +func (insp *numberLessThan) 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 *numberLessThan) match(f float64) bool { + return f < c.conf.Value +} + +func (c *numberLessThan) String() string { + b, _ := json.Marshal(c.conf) + return string(b) +} diff --git a/condition/number_less_than_test.go b/condition/number_less_than_test.go new file mode 100644 index 00000000..ec584511 --- /dev/null +++ b/condition/number_less_than_test.go @@ -0,0 +1,158 @@ +package condition + +import ( + "context" + "testing" + + "github.com/brexhq/substation/config" + "github.com/brexhq/substation/message" +) + +var _ inspector = &numberLessThan{} + +var numberLessThanTests = []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": 14, + }, + }, + []byte(`{"foo":10}`), + true, + }, + { + "fail", + config.Config{ + Settings: map[string]interface{}{ + "value": 1, + }, + }, + []byte(`10`), + false, + }, + { + "pass", + config.Config{ + Settings: map[string]interface{}{ + "object": map[string]interface{}{ + "source_key": "foo", + }, + "value": 10, + }, + }, + []byte(`{"foo":1}`), + true, + }, + { + "fail", + config.Config{ + Settings: map[string]interface{}{ + "value": 5, + }, + }, + []byte(`15`), + false, + }, + // Floats + { + "fail", + config.Config{ + Settings: map[string]interface{}{ + "value": 1, + }, + }, + []byte(`1.5`), + false, + }, + { + "fail", + config.Config{ + Settings: map[string]interface{}{ + "value": 0.1, + }, + }, + []byte(`1.5`), + false, + }, + { + "pass", + config.Config{ + Settings: map[string]interface{}{ + "object": map[string]interface{}{ + "source_key": "foo", + }, + "value": 1.5, + }, + }, + []byte(`{"foo":1.1}`), + true, + }, + { + "pass", + config.Config{ + Settings: map[string]interface{}{ + "value": 1.5, + }, + }, + []byte(`1`), + true, + }, +} + +func TestNumberLessThan(t *testing.T) { + ctx := context.TODO() + + for _, test := range numberLessThanTests { + t.Run(test.name, func(t *testing.T) { + message := message.New().SetData(test.test) + insp, err := newNumberLessThan(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 benchmarkNumberLessThan(b *testing.B, insp *numberLessThan, message *message.Message) { + ctx := context.TODO() + for i := 0; i < b.N; i++ { + _, _ = insp.Inspect(ctx, message) + } +} + +func BenchmarkNumberLessThan(b *testing.B) { + for _, test := range numberLessThanTests { + insp, err := newNumberLessThan(context.TODO(), test.cfg) + if err != nil { + b.Fatal(err) + } + + b.Run(test.name, + func(b *testing.B) { + message := message.New().SetData(test.test) + benchmarkNumberLessThan(b, insp, message) + }, + ) + } +}