From e49900a3a769dba0e3a75caa61201c60f4d80bf5 Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Sun, 8 Mar 2026 15:28:05 -0500 Subject: [PATCH] add processor to scale int values --- internal/processor/int-scale.go | 70 +++++++++ internal/processor/test/int-scale_test.go | 170 ++++++++++++++++++++++ schema/processors.schema.json | 55 +++++-- 3 files changed, 285 insertions(+), 10 deletions(-) create mode 100644 internal/processor/int-scale.go create mode 100644 internal/processor/test/int-scale_test.go diff --git a/internal/processor/int-scale.go b/internal/processor/int-scale.go new file mode 100644 index 0000000..615fb34 --- /dev/null +++ b/internal/processor/int-scale.go @@ -0,0 +1,70 @@ +package processor + +import ( + "context" + "errors" + "fmt" + + "github.com/jwetzell/showbridge-go/internal/config" +) + +type IntScale struct { + OutMin int + OutMax int + InMin int + InMax int + config config.ProcessorConfig +} + +func (ir *IntScale) Process(ctx context.Context, payload any) (any, error) { + payloadInt, ok := GetAnyAs[int](payload) + if !ok { + return nil, errors.New("int.scale can only process an int") + } + + payloadInt = (payloadInt-ir.InMin)*(ir.OutMax-ir.OutMin)/(ir.InMax-ir.InMin) + ir.OutMin + return payloadInt, nil +} + +func (ir *IntScale) Type() string { + return ir.config.Type +} + +func init() { + RegisterProcessor(ProcessorRegistration{ + Type: "int.scale", + New: func(config config.ProcessorConfig) (Processor, error) { + params := config.Params + + inMinInt, err := params.GetInt("inMin") + if err != nil { + return nil, fmt.Errorf("int.scale inMin error: %w", err) + } + + inMaxInt, err := params.GetInt("inMax") + if err != nil { + return nil, fmt.Errorf("int.scale inMax error: %w", err) + } + + if inMaxInt < inMinInt { + return nil, errors.New("int.scale inMax must be greater than inMin") + } + + outMinInt, err := params.GetInt("outMin") + if err != nil { + return nil, fmt.Errorf("int.scale outMin error: %w", err) + } + + outMaxInt, err := params.GetInt("outMax") + if err != nil { + return nil, fmt.Errorf("int.scale outMax error: %w", err) + } + + if outMaxInt < outMinInt { + return nil, errors.New("int.scale outMax must be greater than outMin") + } + + return &IntScale{config: config, InMin: inMinInt, InMax: inMaxInt, OutMin: outMinInt, OutMax: outMaxInt}, nil + }, + }) +} diff --git a/internal/processor/test/int-scale_test.go b/internal/processor/test/int-scale_test.go new file mode 100644 index 0000000..6ab44a6 --- /dev/null +++ b/internal/processor/test/int-scale_test.go @@ -0,0 +1,170 @@ +package processor_test + +import ( + "testing" + + "github.com/jwetzell/showbridge-go/internal/config" + "github.com/jwetzell/showbridge-go/internal/processor" +) + +func TestIntScaleFromRegistry(t *testing.T) { + registration, ok := processor.ProcessorRegistry["int.scale"] + if !ok { + t.Fatalf("int.scale processor not registered") + } + + processorInstance, err := registration.New(config.ProcessorConfig{ + Type: "int.scale", + Params: map[string]any{ + "inMin": 0, + "inMax": 10, + "outMin": 0, + "outMax": 127, + }, + }) + + if err != nil { + t.Fatalf("failed to create int.scale processor: %s", err) + } + + if processorInstance.Type() != "int.scale" { + t.Fatalf("int.scale processor has wrong type: %s", processorInstance.Type()) + } +} + +func TestGoodIntScale(t *testing.T) { + tests := []struct { + name string + payload any + params map[string]any + expected int + }{ + { + name: "0-10 -> 0-127", + params: map[string]any{ + "inMin": 0, + "inMax": 10, + "outMin": 0, + "outMax": 127, + }, + payload: 5, + expected: 63, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + registration, ok := processor.ProcessorRegistry["int.scale"] + if !ok { + t.Fatalf("int.scale processor not registered") + } + + processorInstance, err := registration.New(config.ProcessorConfig{ + Type: "int.scale", + Params: test.params, + }) + + if err != nil { + t.Fatalf("int.scale failed to create processor: %s", err) + } + + got, err := processorInstance.Process(t.Context(), test.payload) + if err != nil { + t.Fatalf("int.scale processing failed: %s", err) + } + + gotInt, ok := got.(int) + if !ok { + t.Fatalf("int.scale returned a %T payload: %s", got, got) + } + + if gotInt != test.expected { + t.Fatalf("int.scale got %d, expected %d", gotInt, test.expected) + } + }) + } +} + +func TestBadIntScale(t *testing.T) { + tests := []struct { + name string + params map[string]any + payload any + errorString string + }{ + { + name: "no inMin param", + payload: "hello", + params: map[string]any{"inMax": 10, "outMin": 0, "outMax": 127}, + errorString: "int.scale inMin error: not found", + }, + { + name: "no inMax param", + payload: "hello", + params: map[string]any{"inMin": 0, "outMin": 0, "outMax": 127}, + errorString: "int.scale inMax error: not found", + }, + { + name: "no outMin param", + payload: "hello", + params: map[string]any{"inMin": 0, "inMax": 10, "outMax": 127}, + errorString: "int.scale outMin error: not found", + }, + { + name: "no outMax param", + payload: "hello", + params: map[string]any{"inMin": 0, "inMax": 10, "outMin": 0}, + errorString: "int.scale outMax error: not found", + }, + { + name: "inMin param not a number", + payload: "hello", + params: map[string]any{"inMin": "0", "max": 10, "outMin": 0, "outMax": 127}, + errorString: "int.scale inMin error: not a number", + }, + { + name: "inMax param not a number", + payload: "hello", + params: map[string]any{"inMin": 0, "inMax": "10", "outMin": 0, "outMax": 127}, + errorString: "int.scale inMax error: not a number", + }, + { + name: "inMax less than inMin", + payload: "hello", + params: map[string]any{"inMin": 10, "inMax": 0, "outMin": 0, "outMax": 127}, + errorString: "int.scale inMax must be greater than inMin", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + + registration, ok := processor.ProcessorRegistry["int.scale"] + if !ok { + t.Fatalf("int.scale processor not registered") + } + + processorInstance, err := registration.New(config.ProcessorConfig{ + Type: "int.scale", + Params: test.params, + }) + + if err != nil { + if test.errorString != err.Error() { + t.Fatalf("int.scale got error '%s', expected '%s'", err.Error(), test.errorString) + } + return + } + + got, err := processorInstance.Process(t.Context(), test.payload) + + if err == nil { + t.Fatalf("int.scale expected to fail but got payload: %s", got) + } + + if err.Error() != test.errorString { + t.Fatalf("int.scale got error '%s', expected '%s'", err.Error(), test.errorString) + } + }) + } +} diff --git a/schema/processors.schema.json b/schema/processors.schema.json index 85157ca..c254326 100644 --- a/schema/processors.schema.json +++ b/schema/processors.schema.json @@ -117,7 +117,7 @@ "type": "string" }, "posX": { - "title": "Position X", + "title": "Position X", "type": "string" }, "posY": { @@ -190,7 +190,7 @@ "type": "object", "properties": { "method": { - "title": "HTTP Method", + "title": "HTTP Method", "type": "string", "enum": ["GET", "POST"] }, @@ -245,7 +245,7 @@ "type": "object", "properties": { "base": { - "title": "Base", + "title": "Base", "type": "integer", "enum": [0,2, 8, 10, 16], "default": 10 @@ -255,7 +255,7 @@ "type": "integer", "enum": [0, 8, 16, 32, 64], "default": 64 - } + } }, "additionalProperties": false } @@ -275,11 +275,11 @@ "type": "object", "properties": { "min": { - "title": "Minimum", + "title": "Minimum", "type": "integer" }, "max": { - "title": "Maximum", + "title": "Maximum", "type": "integer" } }, @@ -290,6 +290,41 @@ "required": ["type", "params"], "additionalProperties": false }, + { + "type": "object", + "title": "Scale Int", + "properties": { + "type": { + "type": "string", + "const": "int.scale" + }, + "params": { + "type": "object", + "properties": { + "inMin": { + "title": "Input Minimum", + "type": "integer" + }, + "inMax": { + "title": "Input Maximum", + "type": "integer" + }, + "outMin": { + "title": "Output Minimum", + "type": "integer" + }, + "outMax": { + "title": "Output Maximum", + "type": "integer" + } + }, + "required": ["inMin", "inMax", "outMin", "outMax"], + "additionalProperties": false + } + }, + "required": ["type", "params"], + "additionalProperties": false + }, { "type": "object", "title": "Decode JSON", @@ -334,7 +369,7 @@ "enum": ["NoteOn", "noteon", "note_on"] }, "channel": { - "title": "Channel", + "title": "Channel", "type": "string" }, "note": { @@ -471,7 +506,7 @@ "type": "object", "properties": { "topic": { - "title": "Topic", + "title": "Topic", "type": "string" }, "qos": { @@ -518,7 +553,7 @@ "type": "object", "properties": { "subject": { - "title": "Subject", + "title": "Subject", "type": "string" }, "payload": { @@ -549,7 +584,7 @@ "type": "string" }, "args": { - "title": "Arguments", + "title": "Arguments", "type": "array", "items": { "type": "string"