Skip to content

Commit

Permalink
feat: add bitmath inspector (#128)
Browse files Browse the repository at this point in the history
* feat: add number inspector

* test: add missing unit tests for negation, and data processing

* fix: looser interpretation when processing data

* refactor: replace number inspector with bitmath

* fix: update bitmath comments/errors
  • Loading branch information
shellcromancer committed Aug 9, 2023
1 parent 667ceb3 commit 4721ffa
Show file tree
Hide file tree
Showing 4 changed files with 306 additions and 0 deletions.
13 changes: 13 additions & 0 deletions build/config/substation.libsonnet
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
defaults: {
inspector: {
bitmath: {
options: { type: null, value: null },
},
content: {
options: { type: null },
},
Expand Down Expand Up @@ -209,6 +212,16 @@
},
inspector: {
settings: { key: null, negate: null },
bitmath(options=$.defaults.inspector.bitmath.options,
settings=$.interfaces.inspector.settings): {
local opt = std.mergePatch($.defaults.inspector.bitmath.options, options),

assert $.helpers.inspector.validate(settings) : 'invalid inspector settings',
local s = std.mergePatch($.interfaces.inspector.settings, settings),

type: 'bitmath',
settings: std.mergePatch({ options: opt }, s),
},
condition(options=null,
settings=$.interfaces.inspector.settings): {
assert options != null : 'invalid inspector options',
Expand Down
103 changes: 103 additions & 0 deletions condition/bitmath.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package condition

import (
"context"
"fmt"
"strconv"

"golang.org/x/exp/slices"

"github.com/brexhq/substation/config"
"github.com/brexhq/substation/internal/errors"
)

// bitmath evaluates data using bitwith math operations.
//
// This inspector supports the data and object handling patterns.
type inspBitmath struct {
condition
Options inspBitmathOptions `json:"options"`
}

type inspBitmathOptions struct {
// Type is the bitmath evaluation Type used during inspection.
//
// Must be one of:
//
// - and
//
// - or
//
// - not
//
// - xor
Type string `json:"type"`
// Value is the number that is used for comparison during inspection.
Value int64 `json:"value"`
}

// Creates a new bitmath inspector.
func newInspBitmath(_ context.Context, cfg config.Config) (c inspBitmath, err error) {
if err = config.Decode(cfg.Settings, &c); err != nil {
return inspBitmath{}, err
}

// validate option.type
if !slices.Contains(
[]string{
"and",
"or",
"not",
"xor",
},
c.Options.Type) {
return inspBitmath{}, fmt.Errorf("condition: bitmath: type %q: %v", c.Options.Type, errors.ErrInvalidOption)
}

return c, nil
}

func (c inspBitmath) String() string {
return toString(c)
}

// Inspect evaluates encapsulated data with the bitmath inspector.
func (c inspBitmath) Inspect(ctx context.Context, capsule config.Capsule) (output bool, err error) {
var check int64
if c.Key == "" {
check, err = strconv.ParseInt(string(capsule.Data()), 10, 64)
if err != nil {
return false, fmt.Errorf("condition: bitmath: invalid data: %v", err)
}
} else {
check = capsule.Get(c.Key).Int()
}

var matched bool
switch c.Options.Type {
case "and":
if check&c.Options.Value != 0 {
matched = true
}
case "or":
if check|c.Options.Value != 0 {
matched = true
}
case "not":
if ^check != 0 {
matched = true
}
case "xor":
if check^c.Options.Value != 0 {
matched = true
}
default:
return false, fmt.Errorf("condition: bitmath: type %s: %v", c.Options.Type, errors.ErrInvalidOption)
}

if c.Negate {
return !matched, nil
}

return matched, nil
}
188 changes: 188 additions & 0 deletions condition/bitmath_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package condition

import (
"context"
"testing"

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

var _ Inspector = inspBitmath{}

var bitmathTests = []struct {
name string
cfg config.Config
test []byte
expected bool
}{
{
"pass xor",
config.Config{
Type: "bitmath",
Settings: map[string]interface{}{
"key": "foo",
"options": map[string]interface{}{
"type": "xor",
"value": 3,
},
},
},
[]byte(`{"foo":"0"}`),
true,
},
{
"fail xor",
config.Config{
Type: "bitmath",
Settings: map[string]interface{}{
"key": "foo",
"options": map[string]interface{}{
"type": "xor",
"value": 42,
},
},
},
[]byte(`{"foo":"42"}`),
false,
},
{
"!fail xor",
config.Config{
Type: "bitmath",
Settings: map[string]interface{}{
"key": "foo",
"negate": true,
"options": map[string]interface{}{
"type": "xor",
"value": 42,
},
},
},
[]byte(`{"foo":"42"}`),
true,
},
{
"pass or",
config.Config{
Type: "bitmath",
Settings: map[string]interface{}{
"key": "foo",
"options": map[string]interface{}{
"type": "or",
"value": -1,
},
},
},
[]byte(`{"foo":"0"}`),
true,
},
{
"!pass or",
config.Config{
Type: "bitmath",
Settings: map[string]interface{}{
"key": "foo",
"negate": true,
"options": map[string]interface{}{
"type": "or",
"value": 1,
},
},
},
[]byte(`{"foo":"0"}`),
false,
},
{
"pass and",
config.Config{
Type: "bitmath",
Settings: map[string]interface{}{
"key": "foo",
"options": map[string]interface{}{
"type": "and",
"value": 0x0001,
},
},
},
[]byte(`{"foo":"570506001"}`),
true,
},
{
"fail and",
config.Config{
Type: "bitmath",
Settings: map[string]interface{}{
"key": "foo",
"options": map[string]interface{}{
"type": "and",
"value": 0x0002,
},
},
},
[]byte(`{"foo":"570506001"}`),
false,
},
{
"pass data",
config.Config{
Type: "bitmath",
Settings: map[string]interface{}{
"options": map[string]interface{}{
"type": "or",
"value": 1,
},
},
},
[]byte(`0001`),
true,
},
}

func TestBitmath(t *testing.T) {
ctx := context.TODO()
capsule := config.NewCapsule()

for _, test := range bitmathTests {
t.Run(test.name, func(t *testing.T) {
capsule.SetData(test.test)

insp, err := newInspBitmath(ctx, test.cfg)
if err != nil {
t.Fatal(err)
}

check, err := insp.Inspect(ctx, capsule)
if err != nil {
t.Error(err)
}

if test.expected != check {
t.Errorf("expected %v, got %v", test.expected, check)
}
})
}
}

func benchmarkBitmathByte(b *testing.B, inspector inspBitmath, capsule config.Capsule) {
ctx := context.TODO()
for i := 0; i < b.N; i++ {
_, _ = inspector.Inspect(ctx, capsule)
}
}

func BenchmarkBitmathByte(b *testing.B) {
capsule := config.NewCapsule()
for _, test := range bitmathTests {
insp, err := newInspBitmath(context.TODO(), test.cfg)
if err != nil {
b.Fatal(err)
}

b.Run(test.name,
func(b *testing.B) {
capsule.SetData(test.test)
benchmarkBitmathByte(b, insp, capsule)
},
)
}
}
2 changes: 2 additions & 0 deletions condition/condition.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ type Inspector interface {
// NewInspector returns a configured Inspector from an Inspector configuration.
func NewInspector(ctx context.Context, cfg config.Config) (Inspector, error) {
switch cfg.Type {
case "bitmath":
return newInspBitmath(ctx, cfg)
case "condition":
return newInspCondition(ctx, cfg)
case "content":
Expand Down

0 comments on commit 4721ffa

Please sign in to comment.