Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(transform): Add Number Minimum Transform #196

Merged
merged 3 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions build/config/substation.libsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,18 @@
type: type,
settings: std.prune(std.mergePatch(default, $.helpers.abbv(settings))),
},
min(settings={}): $.transform.number.minimum(settings=settings),
minimum(settings={}): {
local type = 'number_minimum',
local default = {
id: $.helpers.id(type, settings),
object: $.config.object,
value: null,
},

type: type,
settings: std.prune(std.mergePatch(default, $.helpers.abbv(settings))),
},
math: {
default: {
object: $.config.object,
Expand Down Expand Up @@ -1364,6 +1376,13 @@
$.tf.str.append({ suffix: '\n' }),
],
},
num: $.pattern.transform.number,
number: {
clamp(source_key, target_key, min, max): [
$.tf.number.maximum({ object: { source_key: source_key, target_key: target_key }, value: min }),
$.tf.number.minimum({ object: { source_key: target_key, target_key: target_key }, value: max }),
],
},
},
},
// Utility functions that can be used in conditions and transforms.
Expand Down
11 changes: 11 additions & 0 deletions examples/config/transform/number/clamp/config.jsonnet
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// This example uses the `number.clamp` pattern to return a value that is
// constrained to a range, where the range is defined by two constants.
local sub = import '../../../../../build/config/substation.libsonnet';

{
concurrency: 1,
// Use `null` for object keys to operate on the entire message.
transforms: sub.pattern.tf.num.clamp(null, null, 0, 100) + [
sub.tf.send.stdout(),
],
}
3 changes: 3 additions & 0 deletions examples/config/transform/number/clamp/data.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
-1
101
50
3 changes: 3 additions & 0 deletions examples/config/transform/number/clamp/stdout.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
100
0
50
11 changes: 11 additions & 0 deletions examples/config/transform/number/min/config.jsonnet
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// This example uses the `number_minimum` transform to return the smaller
// of two values, where one value is a constant and the other is a message.
local sub = import '../../../../../build/config/substation.libsonnet';

{
concurrency: 1,
transforms: [
sub.tf.num.min({ value: 0 }),
sub.tf.send.stdout(),
],
}
4 changes: 4 additions & 0 deletions examples/config/transform/number/min/data.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
0
-1
-1.1
10
4 changes: 4 additions & 0 deletions examples/config/transform/number/min/stdout.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
0
-1
-1.1
0
76 changes: 76 additions & 0 deletions transform/number_minimum.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package transform

import (
"context"
"encoding/json"
"fmt"
"math"

"github.com/brexhq/substation/config"
iconfig "github.com/brexhq/substation/internal/config"
"github.com/brexhq/substation/message"
)

func newNumberMinimum(_ context.Context, cfg config.Config) (*numberMinimum, error) {
conf := numberValConfig{}
if err := iconfig.Decode(cfg.Settings, &conf); err != nil {
return nil, fmt.Errorf("transform number_minimum: %v", err)
}

if conf.ID == "" {
conf.ID = "number_minimum"
}

if err := conf.Validate(); err != nil {
return nil, fmt.Errorf("transform %s: %v", conf.ID, err)
}

tf := numberMinimum{
conf: conf,
isObject: conf.Object.SourceKey != "" && conf.Object.TargetKey != "",
}

return &tf, nil
}

type numberMinimum struct {
conf numberValConfig
isObject bool
}

func (tf *numberMinimum) Transform(ctx context.Context, msg *message.Message) ([]*message.Message, error) {
if msg.IsControl() {
return []*message.Message{msg}, nil
}

var value message.Value
if tf.isObject {
value = msg.GetValue(tf.conf.Object.SourceKey)
} else {
value = bytesToValue(msg.Data())
}

if !value.Exists() {
return []*message.Message{msg}, nil
}

flo64 := math.Min(value.Float(), tf.conf.Value)

if !tf.isObject {
s := numberFloat64ToString(flo64)
msg.SetData([]byte(s))

return []*message.Message{msg}, nil
}

if err := msg.SetValue(tf.conf.Object.TargetKey, flo64); err != nil {
return nil, fmt.Errorf("transform %s: %v", tf.conf.ID, err)
}

return []*message.Message{msg}, nil
}

func (tf *numberMinimum) String() string {
b, _ := json.Marshal(tf.conf)
return string(b)
}
158 changes: 158 additions & 0 deletions transform/number_minimum_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
package transform

import (
"context"
"reflect"
"testing"

"github.com/brexhq/substation/config"
"github.com/brexhq/substation/message"
)

var _ Transformer = &numberMinimum{}

var numberMinimumTests = []struct {
name string
cfg config.Config
test []byte
expected [][]byte
}{
// data tests
{
"data",
config.Config{
Settings: map[string]interface{}{
"value": 1,
},
},
[]byte(`0`),
[][]byte{
[]byte(`0`),
},
},
{
"data",
config.Config{
Settings: map[string]interface{}{
"value": -1,
},
},
[]byte(`0`),
[][]byte{
[]byte(`-1`),
},
},
{
"data",
config.Config{
Settings: map[string]interface{}{
"value": -1.1,
},
},
[]byte(`0.1`),
[][]byte{
[]byte(`-1.1`),
},
},
// object tests
{
"object",
config.Config{
Settings: map[string]interface{}{
"object": map[string]interface{}{
"source_key": "a",
"target_key": "a",
},
"value": 1,
},
},
[]byte(`{"a":0}`),
[][]byte{
[]byte(`{"a":0}`),
},
},
{
"object",
config.Config{
Settings: map[string]interface{}{
"object": map[string]interface{}{
"source_key": "a",
"target_key": "a",
},
"value": -1,
},
},
[]byte(`{"a":0}`),
[][]byte{
[]byte(`{"a":-1}`),
},
},
{
"object",
config.Config{
Settings: map[string]interface{}{
"object": map[string]interface{}{
"source_key": "a",
"target_key": "a",
},
"value": -1.1,
},
},
[]byte(`{"a":0.1}`),
[][]byte{
[]byte(`{"a":-1.1}`),
},
},
}

func TestNumberMinimum(t *testing.T) {
ctx := context.TODO()
for _, test := range numberMinimumTests {
t.Run(test.name, func(t *testing.T) {
tf, err := newNumberMinimum(ctx, test.cfg)
if err != nil {
t.Fatal(err)
}

msg := message.New().SetData(test.test)
result, err := tf.Transform(ctx, msg)
if err != nil {
t.Error(err)
}

var data [][]byte
for _, c := range result {
data = append(data, c.Data())
}

if !reflect.DeepEqual(data, test.expected) {
t.Errorf("expected %s, got %s", test.expected, data)
}
})
}
}

func benchmarkNumberMinimum(b *testing.B, tf *numberMinimum, data []byte) {
ctx := context.TODO()
msg := message.New().SetData(data)
b.ResetTimer()

for i := 0; i < b.N; i++ {
_, _ = tf.Transform(ctx, msg)
}
}

func BenchmarkNumberMinimum(b *testing.B) {
for _, test := range numberMinimumTests {
tf, err := newNumberMinimum(context.TODO(), test.cfg)
if err != nil {
b.Fatal(err)
}

b.Run(test.name,
func(b *testing.B) {
benchmarkNumberMinimum(b, tf, test.test)
},
)
}
}
2 changes: 2 additions & 0 deletions transform/transform.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ func New(ctx context.Context, cfg config.Config) (Transformer, error) { //nolint
// Number transforms.
case "number_maximum":
return newNumberMaximum(ctx, cfg)
case "number_minimum":
return newNumberMinimum(ctx, cfg)
case "number_math_addition":
return newNumberMathAddition(ctx, cfg)
case "number_math_division":
Expand Down
Loading