1 Commits

Author SHA1 Message Date
Joel Wetzell
68a943e141 add option to save recording of call for sip.call.server 2026-03-04 12:46:25 -06:00
62 changed files with 1473 additions and 660 deletions

View File

@@ -1,4 +1,4 @@
ARG GO_VERSION=1.26.0 ARG GO_VERSION=1.25.5
FROM golang:${GO_VERSION}-alpine AS build FROM golang:${GO_VERSION}-alpine AS build
RUN apk --no-cache add ca-certificates tzdata RUN apk --no-cache add ca-certificates tzdata
WORKDIR /build WORKDIR /build

View File

@@ -13,8 +13,6 @@ routes:
processors: processors:
- type: osc.message.create - type: osc.message.create
params: params:
address: "{{.Payload.URL.Path}}" address: "{{.URL.Path}}"
- type: osc.message.encode - type: osc.message.encode
- type: router.output output: udp
params:
module: udp

2
go.mod
View File

@@ -1,6 +1,6 @@
module github.com/jwetzell/showbridge-go module github.com/jwetzell/showbridge-go
go 1.26.0 go 1.25.5
require ( require (
github.com/eclipse/paho.mqtt.golang v1.5.1 github.com/eclipse/paho.mqtt.golang v1.5.1

View File

@@ -1,22 +1,7 @@
package common package common
import "context"
type contextKey string type contextKey string
const RouterContextKey contextKey = contextKey("router") const RouterContextKey contextKey = contextKey("router")
const SourceContextKey contextKey = contextKey("source") const SourceContextKey contextKey = contextKey("source")
const ModulesContextKey contextKey = contextKey("modules") const ModulesContextKey contextKey = contextKey("modules")
const SenderContextKey contextKey = contextKey("sender")
type RouteIO interface {
HandleInput(ctx context.Context, sourceId string, payload any) (bool, []RouteIOError)
HandleOutput(ctx context.Context, destinationId string, payload any) error
}
type RouteIOError struct {
Index int
OutputError error
ProcessError error
InputError error
}

View File

@@ -273,6 +273,7 @@ type ModuleConfig struct {
type RouteConfig struct { type RouteConfig struct {
Input string `json:"input"` Input string `json:"input"`
Processors []ProcessorConfig `json:"processors"` Processors []ProcessorConfig `json:"processors"`
Output string `json:"output"`
} }
type ProcessorConfig struct { type ProcessorConfig struct {

View File

@@ -0,0 +1,95 @@
package module
import (
"context"
"errors"
"log/slog"
"net/http"
"time"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
"github.com/jwetzell/showbridge-go/internal/route"
)
type HTTPClient struct {
config config.ModuleConfig
ctx context.Context
client *http.Client
router route.RouteIO
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
RegisterModule(ModuleRegistration{
Type: "http.client",
New: func(config config.ModuleConfig) (Module, error) {
return &HTTPClient{config: config, logger: CreateLogger(config)}, nil
},
})
}
func (hc *HTTPClient) Id() string {
return hc.config.Id
}
func (hc *HTTPClient) Type() string {
return hc.config.Type
}
func (hc *HTTPClient) Start(ctx context.Context) error {
hc.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok {
return errors.New("http.client unable to get router from context")
}
hc.router = router
moduleContext, cancel := context.WithCancel(ctx)
hc.ctx = moduleContext
hc.cancel = cancel
hc.client = &http.Client{
Timeout: 10 * time.Second,
}
<-hc.ctx.Done()
hc.logger.Debug("done")
return nil
}
func (hc *HTTPClient) Output(ctx context.Context, payload any) error {
payloadRequest, ok := processor.GetAnyAs[*http.Request](payload)
if !ok {
return errors.New("http.client is only able to output an http.Request")
}
if hc.client == nil {
return errors.New("http.client client is nil")
}
response, err := hc.client.Do(payloadRequest)
if err != nil {
return err
}
if hc.router != nil {
hc.router.HandleInput(hc.ctx, hc.Id(), response)
}
return nil
}
func (hc *HTTPClient) Stop() {
hc.cancel()
}
func (hc *HTTPClient) Get(key string) (any, error) {
return nil, errors.New("http.client does not support Get")
}

View File

@@ -6,19 +6,19 @@ import (
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
"net"
"net/http" "net/http"
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor" "github.com/jwetzell/showbridge-go/internal/processor"
"github.com/jwetzell/showbridge-go/internal/route"
) )
type HTTPServer struct { type HTTPServer struct {
config config.ModuleConfig config config.ModuleConfig
Port uint16 Port uint16
ctx context.Context ctx context.Context
router common.RouteIO router route.RouteIO
logger *slog.Logger logger *slog.Logger
cancel context.CancelFunc cancel context.CancelFunc
} }
@@ -84,10 +84,6 @@ func (hs *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
} }
if hs.router != nil { if hs.router != nil {
inputContext := context.WithValue(hs.ctx, httpServerContextKey("responseWriter"), &responseWriter) inputContext := context.WithValue(hs.ctx, httpServerContextKey("responseWriter"), &responseWriter)
senderAddr, err := net.ResolveTCPAddr("tcp", r.RemoteAddr)
if err == nil {
inputContext = context.WithValue(inputContext, common.SenderContextKey, senderAddr)
}
aRouteFound, routingErrors := hs.router.HandleInput(inputContext, hs.Id(), r) aRouteFound, routingErrors := hs.router.HandleInput(inputContext, hs.Id(), r)
if !responseWriter.done { if !responseWriter.done {
if aRouteFound { if aRouteFound {
@@ -147,7 +143,7 @@ func (hs *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func (hs *HTTPServer) Start(ctx context.Context) error { func (hs *HTTPServer) Start(ctx context.Context) error {
hs.logger.Debug("running") hs.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO) router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok { if !ok {
return errors.New("http.server unable to get router from context") return errors.New("http.server unable to get router from context")

View File

@@ -10,6 +10,7 @@ import (
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/route"
"gitlab.com/gomidi/midi/v2" "gitlab.com/gomidi/midi/v2"
_ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv" _ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
) )
@@ -17,7 +18,7 @@ import (
type MIDIInput struct { type MIDIInput struct {
config config.ModuleConfig config config.ModuleConfig
ctx context.Context ctx context.Context
router common.RouteIO router route.RouteIO
Port string Port string
SendFunc func(midi.Message) error SendFunc func(midi.Message) error
logger *slog.Logger logger *slog.Logger
@@ -50,7 +51,7 @@ func (mi *MIDIInput) Type() string {
func (mi *MIDIInput) Start(ctx context.Context) error { func (mi *MIDIInput) Start(ctx context.Context) error {
mi.logger.Debug("running") mi.logger.Debug("running")
defer midi.CloseDriver() defer midi.CloseDriver()
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO) router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok { if !ok {
return errors.New("midi.input unable to get router from context") return errors.New("midi.input unable to get router from context")

View File

@@ -11,6 +11,7 @@ import (
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor" "github.com/jwetzell/showbridge-go/internal/processor"
"github.com/jwetzell/showbridge-go/internal/route"
"gitlab.com/gomidi/midi/v2" "gitlab.com/gomidi/midi/v2"
_ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv" _ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
) )
@@ -18,7 +19,7 @@ import (
type MIDIOutput struct { type MIDIOutput struct {
config config.ModuleConfig config config.ModuleConfig
ctx context.Context ctx context.Context
router common.RouteIO router route.RouteIO
Port string Port string
SendFunc func(midi.Message) error SendFunc func(midi.Message) error
logger *slog.Logger logger *slog.Logger
@@ -52,7 +53,7 @@ func (mo *MIDIOutput) Type() string {
func (mo *MIDIOutput) Start(ctx context.Context) error { func (mo *MIDIOutput) Start(ctx context.Context) error {
mo.logger.Debug("running") mo.logger.Debug("running")
defer midi.CloseDriver() defer midi.CloseDriver()
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO) router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok { if !ok {
return errors.New("midi.output unable to get router from context") return errors.New("midi.output unable to get router from context")

View File

@@ -10,12 +10,13 @@ import (
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor" "github.com/jwetzell/showbridge-go/internal/processor"
"github.com/jwetzell/showbridge-go/internal/route"
) )
type MQTTClient struct { type MQTTClient struct {
config config.ModuleConfig config config.ModuleConfig
ctx context.Context ctx context.Context
router common.RouteIO router route.RouteIO
Broker string Broker string
ClientID string ClientID string
Topic string Topic string
@@ -62,7 +63,7 @@ func (mc *MQTTClient) Type() string {
func (mc *MQTTClient) Start(ctx context.Context) error { func (mc *MQTTClient) Start(ctx context.Context) error {
mc.logger.Debug("running") mc.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO) router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok { if !ok {
return errors.New("mqtt.client unable to get router from context") return errors.New("mqtt.client unable to get router from context")

View File

@@ -8,13 +8,14 @@ import (
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor" "github.com/jwetzell/showbridge-go/internal/processor"
"github.com/jwetzell/showbridge-go/internal/route"
"github.com/nats-io/nats.go" "github.com/nats-io/nats.go"
) )
type NATSClient struct { type NATSClient struct {
config config.ModuleConfig config config.ModuleConfig
ctx context.Context ctx context.Context
router common.RouteIO router route.RouteIO
URL string URL string
Subject string Subject string
client *nats.Conn client *nats.Conn
@@ -53,7 +54,7 @@ func (nc *NATSClient) Type() string {
func (nc *NATSClient) Start(ctx context.Context) error { func (nc *NATSClient) Start(ctx context.Context) error {
nc.logger.Debug("running") nc.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO) router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok { if !ok {
return errors.New("nats.client unable to get router from context") return errors.New("nats.client unable to get router from context")

View File

@@ -10,6 +10,7 @@ import (
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/route"
"github.com/nats-io/nats-server/v2/server" "github.com/nats-io/nats-server/v2/server"
) )
@@ -18,7 +19,7 @@ type NATSServer struct {
ctx context.Context ctx context.Context
Ip string Ip string
Port int Port int
router common.RouteIO router route.RouteIO
server *server.Server server *server.Server
logger *slog.Logger logger *slog.Logger
cancel context.CancelFunc cancel context.CancelFunc
@@ -66,7 +67,7 @@ func (ns *NATSServer) Type() string {
func (ns *NATSServer) Start(ctx context.Context) error { func (ns *NATSServer) Start(ctx context.Context) error {
ns.logger.Debug("running") ns.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO) router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok { if !ok {
return errors.New("nats.server unable to get router from context") return errors.New("nats.server unable to get router from context")
@@ -103,7 +104,7 @@ func (ns *NATSServer) Start(ctx context.Context) error {
} }
func (ns *NATSServer) Output(ctx context.Context, payload any) error { func (ns *NATSServer) Output(ctx context.Context, payload any) error {
return errors.New("nats.server does not support output") return errors.ErrUnsupported
} }
func (ns *NATSServer) Stop() { func (ns *NATSServer) Stop() {

View File

@@ -11,13 +11,14 @@ import (
"github.com/jwetzell/psn-go" "github.com/jwetzell/psn-go"
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/route"
) )
type PSNClient struct { type PSNClient struct {
config config.ModuleConfig config config.ModuleConfig
conn *net.UDPConn conn *net.UDPConn
ctx context.Context ctx context.Context
router common.RouteIO router route.RouteIO
decoder *psn.Decoder decoder *psn.Decoder
logger *slog.Logger logger *slog.Logger
cancel context.CancelFunc cancel context.CancelFunc
@@ -43,7 +44,7 @@ func (pc *PSNClient) Type() string {
func (pc *PSNClient) Start(ctx context.Context) error { func (pc *PSNClient) Start(ctx context.Context) error {
pc.logger.Debug("running") pc.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO) router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok { if !ok {
return errors.New("psn.client unable to get router from context") return errors.New("psn.client unable to get router from context")

View File

@@ -13,13 +13,14 @@ import (
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/framer" "github.com/jwetzell/showbridge-go/internal/framer"
"github.com/jwetzell/showbridge-go/internal/processor" "github.com/jwetzell/showbridge-go/internal/processor"
"github.com/jwetzell/showbridge-go/internal/route"
"go.bug.st/serial" "go.bug.st/serial"
) )
type SerialClient struct { type SerialClient struct {
config config.ModuleConfig config config.ModuleConfig
ctx context.Context ctx context.Context
router common.RouteIO router route.RouteIO
Port string Port string
Framer framer.Framer Framer framer.Framer
Mode *serial.Mode Mode *serial.Mode
@@ -85,7 +86,7 @@ func (sc *SerialClient) SetupPort() error {
func (sc *SerialClient) Start(ctx context.Context) error { func (sc *SerialClient) Start(ctx context.Context) error {
sc.logger.Debug("running") sc.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO) router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok { if !ok {
return errors.New("serial.client unable to get router from context") return errors.New("serial.client unable to get router from context")

View File

@@ -7,6 +7,7 @@ import (
"io" "io"
"log/slog" "log/slog"
"os" "os"
"path"
"sync" "sync"
"time" "time"
@@ -17,16 +18,18 @@ import (
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor" "github.com/jwetzell/showbridge-go/internal/processor"
"github.com/jwetzell/showbridge-go/internal/route"
) )
type SIPCallServer struct { type SIPCallServer struct {
config config.ModuleConfig config config.ModuleConfig
ctx context.Context ctx context.Context
router common.RouteIO router route.RouteIO
IP string IP string
Port int Port int
Transport string Transport string
UserAgent string UserAgent string
RecordingPath string
dg *diago.Diago dg *diago.Diago
logger *slog.Logger logger *slog.Logger
cancel context.CancelFunc cancel context.CancelFunc
@@ -85,7 +88,19 @@ func init() {
} }
} }
return &SIPCallServer{config: moduleConfig, IP: ipString, Port: int(portNum), Transport: transportString, UserAgent: userAgentString, logger: CreateLogger(moduleConfig)}, nil recordingPathString := ""
recordingPath, ok := params["recordingPath"]
if ok {
specificRecordingPath, ok := recordingPath.(string)
if !ok {
return nil, errors.New("sip.call.server recordingPath must be a string")
}
recordingPathString = specificRecordingPath
}
return &SIPCallServer{config: moduleConfig, IP: ipString, Port: int(portNum), Transport: transportString, UserAgent: userAgentString, RecordingPath: recordingPathString, logger: CreateLogger(moduleConfig)}, nil
}, },
}) })
} }
@@ -100,7 +115,7 @@ func (scs *SIPCallServer) Type() string {
func (scs *SIPCallServer) Start(ctx context.Context) error { func (scs *SIPCallServer) Start(ctx context.Context) error {
scs.logger.Debug("running") scs.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO) router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok { if !ok {
return errors.New("sip.call.server unable to get router from context") return errors.New("sip.call.server unable to get router from context")
@@ -150,6 +165,41 @@ func (scs *SIPCallServer) HandleCall(inDialog *diago.DialogServerSession) {
dialogContext := context.WithValue(scs.ctx, sipCallContextKey("call"), &SIPCall{ dialogContext := context.WithValue(scs.ctx, sipCallContextKey("call"), &SIPCall{
inDialog: inDialog, inDialog: inDialog,
}) })
if scs.RecordingPath != "" {
filename := path.Join(scs.RecordingPath, fmt.Sprintf("%s-%d.wav", inDialog.ToUser(), time.Now().Unix()))
wavFile, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0755)
if err != nil {
scs.logger.Error("error creating recording file", "error", err, "filename", filename)
} else {
recorder, err := inDialog.AudioStereoRecordingCreate(wavFile)
if err != nil {
scs.logger.Error("error creating recording", "error", err)
wavFile.Close()
} else {
go func() {
<-inDialog.Context().Done()
err := recorder.Close()
if err != nil {
scs.logger.Error("error closing recording", "error", err)
}
wavFile.Close()
scs.logger.Debug("finished recording", "filename", filename)
}()
go func() {
bytes, err := media.Copy(recorder.AudioReader(), recorder.AudioWriter())
fmt.Println("recorded bytes", bytes)
if err != nil {
if !errors.Is(err, io.EOF) {
scs.logger.Error("error while recording", "error", err)
}
}
}()
}
}
}
scs.router.HandleInput(dialogContext, scs.Id(), SIPCallMessage{ scs.router.HandleInput(dialogContext, scs.Id(), SIPCallMessage{
To: inDialog.ToUser(), To: inDialog.ToUser(),
}) })

View File

@@ -18,12 +18,13 @@ import (
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor" "github.com/jwetzell/showbridge-go/internal/processor"
"github.com/jwetzell/showbridge-go/internal/route"
) )
type SIPDTMFServer struct { type SIPDTMFServer struct {
config config.ModuleConfig config config.ModuleConfig
ctx context.Context ctx context.Context
router common.RouteIO router route.RouteIO
IP string IP string
Port int Port int
Transport string Transport string
@@ -113,7 +114,7 @@ func (sds *SIPDTMFServer) Type() string {
func (sds *SIPDTMFServer) Start(ctx context.Context) error { func (sds *SIPDTMFServer) Start(ctx context.Context) error {
sds.logger.Debug("running") sds.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO) router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok { if !ok {
return errors.New("sip.dtmf.server unable to get router from context") return errors.New("sip.dtmf.server unable to get router from context")

View File

@@ -12,6 +12,7 @@ import (
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/framer" "github.com/jwetzell/showbridge-go/internal/framer"
"github.com/jwetzell/showbridge-go/internal/processor" "github.com/jwetzell/showbridge-go/internal/processor"
"github.com/jwetzell/showbridge-go/internal/route"
) )
type TCPClient struct { type TCPClient struct {
@@ -19,7 +20,7 @@ type TCPClient struct {
framer framer.Framer framer framer.Framer
conn *net.TCPConn conn *net.TCPConn
ctx context.Context ctx context.Context
router common.RouteIO router route.RouteIO
Addr *net.TCPAddr Addr *net.TCPAddr
logger *slog.Logger logger *slog.Logger
cancel context.CancelFunc cancel context.CancelFunc
@@ -70,7 +71,7 @@ func (tc *TCPClient) Type() string {
func (tc *TCPClient) Start(ctx context.Context) error { func (tc *TCPClient) Start(ctx context.Context) error {
tc.logger.Debug("running") tc.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO) router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok { if !ok {
return errors.New("net.tcp.client unable to get router from context") return errors.New("net.tcp.client unable to get router from context")

View File

@@ -15,6 +15,7 @@ import (
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/framer" "github.com/jwetzell/showbridge-go/internal/framer"
"github.com/jwetzell/showbridge-go/internal/processor" "github.com/jwetzell/showbridge-go/internal/processor"
"github.com/jwetzell/showbridge-go/internal/route"
) )
type TCPServer struct { type TCPServer struct {
@@ -22,7 +23,7 @@ type TCPServer struct {
Addr *net.TCPAddr Addr *net.TCPAddr
Framer framer.Framer Framer framer.Framer
ctx context.Context ctx context.Context
router common.RouteIO router route.RouteIO
quit chan interface{} quit chan interface{}
wg sync.WaitGroup wg sync.WaitGroup
connections []*net.TCPConn connections []*net.TCPConn
@@ -141,13 +142,7 @@ ClientRead:
messages := ts.Framer.Decode(buffer[0:byteCount]) messages := ts.Framer.Decode(buffer[0:byteCount])
for _, message := range messages { for _, message := range messages {
if ts.router != nil { if ts.router != nil {
senderAddr, ok := client.RemoteAddr().(*net.TCPAddr)
if ok {
senderCtx := context.WithValue(ts.ctx, common.SenderContextKey, senderAddr)
ts.router.HandleInput(senderCtx, ts.Id(), message)
} else {
ts.router.HandleInput(ts.ctx, ts.Id(), message) ts.router.HandleInput(ts.ctx, ts.Id(), message)
}
} else { } else {
ts.logger.Error("input received but no router is configured") ts.logger.Error("input received but no router is configured")
} }
@@ -160,7 +155,7 @@ ClientRead:
func (ts *TCPServer) Start(ctx context.Context) error { func (ts *TCPServer) Start(ctx context.Context) error {
ts.logger.Debug("running") ts.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO) router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok { if !ok {
return errors.New("net.tcp.server unable to get router from context") return errors.New("net.tcp.server unable to get router from context")

View File

@@ -0,0 +1,73 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestHTTPClientFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["http.client"]
if !ok {
t.Fatalf("http.client module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "http.client",
})
if err != nil {
t.Fatalf("failed to create http.client module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("http.client module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "http.client" {
t.Fatalf("http.client module has wrong type: %s", moduleInstance.Type())
}
}
func TestBadHTTPClient(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["http.client"]
if !ok {
t.Fatalf("http.client module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "http.client",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("http.client got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("http.client expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("http.client got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -9,13 +9,14 @@ import (
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/route"
) )
type TimeInterval struct { type TimeInterval struct {
config config.ModuleConfig config config.ModuleConfig
Duration uint32 Duration uint32
ctx context.Context ctx context.Context
router common.RouteIO router route.RouteIO
ticker *time.Ticker ticker *time.Ticker
logger *slog.Logger logger *slog.Logger
cancel context.CancelFunc cancel context.CancelFunc
@@ -46,7 +47,7 @@ func (i *TimeInterval) Type() string {
func (i *TimeInterval) Start(ctx context.Context) error { func (i *TimeInterval) Start(ctx context.Context) error {
i.logger.Debug("running") i.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO) router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok { if !ok {
return errors.New("time.interval unable to get router from context") return errors.New("time.interval unable to get router from context")

View File

@@ -9,13 +9,14 @@ import (
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/route"
) )
type TimeTimer struct { type TimeTimer struct {
config config.ModuleConfig config config.ModuleConfig
Duration uint32 Duration uint32
ctx context.Context ctx context.Context
router common.RouteIO router route.RouteIO
timer *time.Timer timer *time.Timer
logger *slog.Logger logger *slog.Logger
cancel context.CancelFunc cancel context.CancelFunc
@@ -47,7 +48,7 @@ func (t *TimeTimer) Type() string {
func (t *TimeTimer) Start(ctx context.Context) error { func (t *TimeTimer) Start(ctx context.Context) error {
t.logger.Debug("running") t.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO) router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok { if !ok {
return errors.New("net.tcp.client unable to get router from context") return errors.New("net.tcp.client unable to get router from context")

View File

@@ -10,6 +10,7 @@ import (
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor" "github.com/jwetzell/showbridge-go/internal/processor"
"github.com/jwetzell/showbridge-go/internal/route"
) )
type UDPClient struct { type UDPClient struct {
@@ -18,7 +19,7 @@ type UDPClient struct {
Port uint16 Port uint16
conn *net.UDPConn conn *net.UDPConn
ctx context.Context ctx context.Context
router common.RouteIO router route.RouteIO
logger *slog.Logger logger *slog.Logger
cancel context.CancelFunc cancel context.CancelFunc
} }
@@ -63,7 +64,7 @@ func (uc *UDPClient) SetupConn() error {
func (uc *UDPClient) Start(ctx context.Context) error { func (uc *UDPClient) Start(ctx context.Context) error {
uc.logger.Debug("running") uc.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO) router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok { if !ok {
return errors.New("net.udp.client unable to get router from context") return errors.New("net.udp.client unable to get router from context")

View File

@@ -11,13 +11,14 @@ import (
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor" "github.com/jwetzell/showbridge-go/internal/processor"
"github.com/jwetzell/showbridge-go/internal/route"
) )
type UDPMulticast struct { type UDPMulticast struct {
config config.ModuleConfig config config.ModuleConfig
conn *net.UDPConn conn *net.UDPConn
ctx context.Context ctx context.Context
router common.RouteIO router route.RouteIO
Addr *net.UDPAddr Addr *net.UDPAddr
logger *slog.Logger logger *slog.Logger
cancel context.CancelFunc cancel context.CancelFunc
@@ -57,7 +58,7 @@ func (um *UDPMulticast) Type() string {
func (um *UDPMulticast) Start(ctx context.Context) error { func (um *UDPMulticast) Start(ctx context.Context) error {
um.logger.Debug("running") um.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO) router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok { if !ok {
return errors.New("net.udp.multicast unable to get router from context") return errors.New("net.udp.multicast unable to get router from context")

View File

@@ -10,6 +10,7 @@ import (
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/route"
) )
type UDPServer struct { type UDPServer struct {
@@ -17,7 +18,7 @@ type UDPServer struct {
BufferSize int BufferSize int
config config.ModuleConfig config config.ModuleConfig
ctx context.Context ctx context.Context
router common.RouteIO router route.RouteIO
logger *slog.Logger logger *slog.Logger
cancel context.CancelFunc cancel context.CancelFunc
} }
@@ -69,7 +70,7 @@ func (us *UDPServer) Type() string {
func (us *UDPServer) Start(ctx context.Context) error { func (us *UDPServer) Start(ctx context.Context) error {
us.logger.Debug("running") us.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO) router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok { if !ok {
return errors.New("net.udp.server unable to get router from context") return errors.New("net.udp.server unable to get router from context")
@@ -96,7 +97,7 @@ func (us *UDPServer) Start(ctx context.Context) error {
default: default:
listener.SetDeadline(time.Now().Add(time.Millisecond * 200)) listener.SetDeadline(time.Now().Add(time.Millisecond * 200))
numBytes, senderAddr, err := listener.ReadFromUDP(buffer) numBytes, _, err := listener.ReadFromUDP(buffer)
if err != nil { if err != nil {
//NOTE(jwetzell) we hit deadline //NOTE(jwetzell) we hit deadline
if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() { if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
@@ -106,8 +107,7 @@ func (us *UDPServer) Start(ctx context.Context) error {
} }
message := buffer[:numBytes] message := buffer[:numBytes]
if us.router != nil { if us.router != nil {
senderCtx := context.WithValue(us.ctx, common.SenderContextKey, senderAddr) us.router.HandleInput(us.ctx, us.Id(), message)
us.router.HandleInput(senderCtx, us.Id(), message)
} else { } else {
us.logger.Error("input received but no router is configured") us.logger.Error("input received but no router is configured")
} }

View File

@@ -0,0 +1,48 @@
package processor
import (
"context"
"fmt"
"github.com/jwetzell/artnet-go"
"github.com/jwetzell/showbridge-go/internal/config"
)
type ArtNetPacketFilter struct {
config config.ProcessorConfig
OpCode uint16
}
func (apf *ArtNetPacketFilter) Process(ctx context.Context, payload any) (any, error) {
payloadPacket, ok := GetAnyAs[artnet.ArtNetPacket](payload)
if !ok {
return nil, fmt.Errorf("artnet.packet.filter processor only accepts an ArtNetPacket")
}
if payloadPacket.GetOpCode() != apf.OpCode {
return nil, nil
}
return payloadPacket, nil
}
func (apf *ArtNetPacketFilter) Type() string {
return apf.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "artnet.packet.filter",
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
opCodeNum, err := params.GetInt("opCode")
if err != nil {
return nil, fmt.Errorf("artnet.packet.filter opCode error: %w", err)
}
return &ArtNetPacketFilter{config: config, OpCode: uint16(opCodeNum)}, nil
},
})
}

View File

@@ -15,11 +15,23 @@ type FilterExpr struct {
Program *vm.Program Program *vm.Program
} }
func (fe *FilterExpr) Process(ctx context.Context, payload any) (any, error) { func SafeExprEnv(payload any) any {
exprEnv := ExprEnv{
Payload: payload,
}
exprEnv := GetEnvData(ctx, payload) return exprEnv
}
output, err := expr.Run(fe.Program, exprEnv) type ExprEnv struct {
Payload any
}
func (se *FilterExpr) Process(ctx context.Context, payload any) (any, error) {
exprEnv := SafeExprEnv(payload)
output, err := expr.Run(se.Program, exprEnv)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -35,8 +47,8 @@ func (fe *FilterExpr) Process(ctx context.Context, payload any) (any, error) {
return payload, nil return payload, nil
} }
func (fe *FilterExpr) Type() string { func (se *FilterExpr) Type() string {
return fe.config.Type return se.config.Type
} }
func init() { func init() {

View File

@@ -12,7 +12,7 @@ type FreeDDecode struct {
config config.ProcessorConfig config config.ProcessorConfig
} }
func (fd *FreeDDecode) Process(ctx context.Context, payload any) (any, error) { func (fdd *FreeDDecode) Process(ctx context.Context, payload any) (any, error) {
payloadBytes, ok := GetAnyAs[[]byte](payload) payloadBytes, ok := GetAnyAs[[]byte](payload)
if !ok { if !ok {
@@ -26,8 +26,8 @@ func (fd *FreeDDecode) Process(ctx context.Context, payload any) (any, error) {
return payloadMessage, nil return payloadMessage, nil
} }
func (fd *FreeDDecode) Type() string { func (fdd *FreeDDecode) Type() string {
return fd.config.Type return fdd.config.Type
} }
func init() { func init() {

View File

@@ -12,7 +12,7 @@ type FreeDEncode struct {
config config.ProcessorConfig config config.ProcessorConfig
} }
func (fe *FreeDEncode) Process(ctx context.Context, payload any) (any, error) { func (fde *FreeDEncode) Process(ctx context.Context, payload any) (any, error) {
payloadPosition, ok := GetAnyAs[freeD.FreeDPosition](payload) payloadPosition, ok := GetAnyAs[freeD.FreeDPosition](payload)
if !ok { if !ok {
@@ -23,8 +23,8 @@ func (fe *FreeDEncode) Process(ctx context.Context, payload any) (any, error) {
return payloadBytes, nil return payloadBytes, nil
} }
func (fe *FreeDEncode) Type() string { func (fde *FreeDEncode) Type() string {
return fe.config.Type return fde.config.Type
} }
func init() { func init() {

View File

@@ -0,0 +1,70 @@
package processor
import (
"bytes"
"context"
"fmt"
"net/http"
"text/template"
"github.com/jwetzell/showbridge-go/internal/config"
)
type HTTPRequestCreate struct {
config config.ProcessorConfig
Method string
URL *template.Template
}
func (hrc *HTTPRequestCreate) Process(ctx context.Context, payload any) (any, error) {
templateData := GetTemplateData(ctx, payload)
var urlBuffer bytes.Buffer
err := hrc.URL.Execute(&urlBuffer, templateData)
if err != nil {
return nil, err
}
urlString := urlBuffer.String()
//TODO(jwetzell): support body
request, err := http.NewRequest(hrc.Method, urlString, bytes.NewBuffer([]byte{}))
if err != nil {
return nil, err
}
return request, nil
}
func (hrc *HTTPRequestCreate) Type() string {
return hrc.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "http.request.create",
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
methodString, err := params.GetString("method")
if err != nil {
return nil, fmt.Errorf("http.request.create method error: %w", err)
}
urlString, err := params.GetString("url")
if err != nil {
return nil, fmt.Errorf("http.request.create url error: %w", err)
}
urlTemplate, err := template.New("url").Parse(urlString)
if err != nil {
return nil, err
}
return &HTTPRequestCreate{config: config, URL: urlTemplate, Method: methodString}, nil
},
})
}

View File

@@ -1,93 +0,0 @@
package processor
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"text/template"
"time"
"github.com/jwetzell/showbridge-go/internal/config"
)
type HTTPRequestDo struct {
config config.ProcessorConfig
client *http.Client
Method string
URL *template.Template
}
func (hrd *HTTPRequestDo) Process(ctx context.Context, payload any) (any, error) {
templateData := GetTemplateData(ctx, payload)
var urlBuffer bytes.Buffer
err := hrd.URL.Execute(&urlBuffer, templateData)
if err != nil {
return nil, err
}
urlString := urlBuffer.String()
//TODO(jwetzell): support body
request, err := http.NewRequest(hrd.Method, urlString, bytes.NewBuffer([]byte{}))
if err != nil {
return nil, err
}
response, err := hrd.client.Do(request)
if err != nil {
return nil, err
}
body, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
}
//TODO(jwetzell): support headers, etc
return HTTPResponse{
Status: response.StatusCode,
Body: body,
}, nil
}
func (hrd *HTTPRequestDo) Type() string {
return hrd.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "http.request.do",
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
methodString, err := params.GetString("method")
if err != nil {
return nil, fmt.Errorf("http.request.do method error: %w", err)
}
urlString, err := params.GetString("url")
if err != nil {
return nil, fmt.Errorf("http.request.do url error: %w", err)
}
urlTemplate, err := template.New("url").Parse(urlString)
if err != nil {
return nil, err
}
client := &http.Client{
Timeout: 10 * time.Second,
}
return &HTTPRequestDo{config: config, URL: urlTemplate, Method: methodString, client: client}, nil
},
})
}

View File

@@ -0,0 +1,73 @@
package processor
import (
"context"
"errors"
"fmt"
"net/http"
"regexp"
"github.com/jwetzell/showbridge-go/internal/config"
)
type HTTPRequestFilter struct {
config config.ProcessorConfig
Path *regexp.Regexp
Method string
}
func (hrf *HTTPRequestFilter) Process(ctx context.Context, payload any) (any, error) {
payloadRequest, ok := GetAnyAs[*http.Request](payload)
if !ok {
return nil, errors.New("http.request.filter can only operate on http.Request payloads")
}
if hrf.Method != "" {
if payloadRequest.Method != hrf.Method {
return nil, nil
}
}
if !hrf.Path.MatchString(payloadRequest.URL.Path) {
return nil, nil
}
return payloadRequest, nil
}
func (hrf *HTTPRequestFilter) Type() string {
return hrf.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "http.request.filter",
New: func(moduleConfig config.ProcessorConfig) (Processor, error) {
params := moduleConfig.Params
pathString, err := params.GetString("path")
if err != nil {
return nil, fmt.Errorf("http.request.filter path error: %w", err)
}
pathRegexp, err := regexp.Compile(fmt.Sprintf("^%s$", pathString))
if err != nil {
return nil, err
}
methodString, err := params.GetString("method")
if err != nil {
if errors.Is(err, config.ErrParamNotFound) {
return &HTTPRequestFilter{config: moduleConfig, Path: pathRegexp}, nil
} else {
return nil, fmt.Errorf("http.request.filter method error: %w", err)
}
}
return &HTTPRequestFilter{config: moduleConfig, Path: pathRegexp, Method: methodString}, nil
},
})
}

View File

@@ -20,24 +20,24 @@ type HTTPResponse struct {
Body []byte Body []byte
} }
func (hrc *HTTPResponseCreate) Process(ctx context.Context, payload any) (any, error) { func (hre *HTTPResponseCreate) Process(ctx context.Context, payload any) (any, error) {
templateData := GetTemplateData(ctx, payload) templateData := GetTemplateData(ctx, payload)
var bodyBuffer bytes.Buffer var bodyBuffer bytes.Buffer
err := hrc.BodyTmpl.Execute(&bodyBuffer, templateData) err := hre.BodyTmpl.Execute(&bodyBuffer, templateData)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return HTTPResponse{ return HTTPResponse{
Status: hrc.Status, Status: hre.Status,
Body: bodyBuffer.Bytes(), Body: bodyBuffer.Bytes(),
}, nil }, nil
} }
func (hrc *HTTPResponseCreate) Type() string { func (hre *HTTPResponseCreate) Type() string {
return hrc.config.Type return hre.config.Type
} }
func init() { func init() {

View File

@@ -13,20 +13,15 @@ type JsonDecode struct {
} }
func (jd *JsonDecode) Process(ctx context.Context, payload any) (any, error) { func (jd *JsonDecode) Process(ctx context.Context, payload any) (any, error) {
payloadBytes, ok := GetAnyAsByteSlice(payload)
if !ok {
payloadString, ok := GetAnyAs[string](payload) payloadString, ok := GetAnyAs[string](payload)
if !ok { if !ok {
return nil, errors.New("json.decode can only process a string or []byte") return nil, errors.New("json.decode processor only accepts a string")
}
payloadBytes = []byte(payloadString)
} }
payloadJson := make(map[string]any) payloadJson := make(map[string]any)
err := json.Unmarshal(payloadBytes, &payloadJson) err := json.Unmarshal([]byte(payloadString), &payloadJson)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -0,0 +1,50 @@
//go:build cgo
package processor
import (
"context"
"errors"
"fmt"
"github.com/jwetzell/showbridge-go/internal/config"
"gitlab.com/gomidi/midi/v2"
)
type MIDIMessageFilter struct {
config config.ProcessorConfig
MIDIType string
}
func (mmf *MIDIMessageFilter) Process(ctx context.Context, payload any) (any, error) {
payloadMessage, ok := GetAnyAs[midi.Message](payload)
if !ok {
return nil, errors.New("midi.message.filter processor only accepts a midi.Message")
}
if payloadMessage.Type().String() != mmf.MIDIType {
return nil, nil
}
return payloadMessage, nil
}
func (mmf *MIDIMessageFilter) Type() string {
return mmf.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "midi.message.filter",
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
msgTypeString, err := params.GetString("type")
if err != nil {
return nil, fmt.Errorf("midi.message.filter type error: %w", err)
}
return &MIDIMessageFilter{config: config, MIDIType: msgTypeString}, nil
},
})
}

View File

@@ -0,0 +1,64 @@
package processor
import (
"context"
"errors"
"fmt"
"regexp"
"strings"
"github.com/jwetzell/osc-go"
"github.com/jwetzell/showbridge-go/internal/config"
)
type OSCMessageFilter struct {
config config.ProcessorConfig
Address *regexp.Regexp
}
func (omf *OSCMessageFilter) Process(ctx context.Context, payload any) (any, error) {
payloadMessage, ok := GetAnyAs[osc.OSCMessage](payload)
if !ok {
return nil, errors.New("osc.message.filter can only operate on OSCMessage payloads")
}
if !omf.Address.MatchString(payloadMessage.Address) {
return nil, nil
}
return payloadMessage, nil
}
func (omf *OSCMessageFilter) Type() string {
return omf.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "osc.message.filter",
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
addressString, err := params.GetString("address")
if err != nil {
return nil, fmt.Errorf("osc.message.filter address error: %w", err)
}
addressPattern := strings.ReplaceAll(addressString, "?", ".")
addressPattern = strings.ReplaceAll(addressPattern, "*", "[^/]*")
addressPattern = strings.ReplaceAll(addressPattern, "[!", "[^")
addressPattern = strings.ReplaceAll(addressPattern, "{", "(")
addressPattern = strings.ReplaceAll(addressPattern, "}", ")")
addressPattern = strings.ReplaceAll(addressPattern, ",", "|")
addressPatternRegexp, err := regexp.Compile(fmt.Sprintf("^%s$", addressPattern))
if err != nil {
return nil, err
}
return &OSCMessageFilter{config: config, Address: addressPatternRegexp}, nil
},
})
}

View File

@@ -3,8 +3,6 @@ package processor
import ( import (
"context" "context"
"fmt" "fmt"
"math"
"reflect"
"sync" "sync"
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
@@ -49,61 +47,9 @@ func GetAnyAs[T any](p any) (T, bool) {
return typed, ok return typed, ok
} }
func GetAnyAsByteSlice(p any) ([]byte, bool) {
v := reflect.ValueOf(p)
if v.Kind() != reflect.Slice {
return nil, false
}
result := make([]byte, v.Len())
for i := 0; i < v.Len(); i++ {
elem := v.Index(i).Interface()
byteValue, ok := elem.(byte)
if ok {
result[i] = byteValue
continue
}
uintValue, ok := elem.(uint)
if ok {
if uintValue > 255 {
return nil, false
}
result[i] = byte(uintValue)
continue
}
intValue, ok := elem.(int)
if ok {
if intValue < 0 || intValue > 255 {
return nil, false
}
result[i] = byte(intValue)
continue
}
floatValue, ok := elem.(float64)
if ok {
if floatValue != math.Floor(floatValue) {
return nil, false
}
if floatValue < 0 || floatValue > 255 {
return nil, false
}
result[i] = byte(floatValue)
continue
}
return nil, false
}
return result, true
}
type TemplateData struct { type TemplateData struct {
Payload any Payload any
Modules any Modules any
Sender any
}
type EnvData struct {
Payload any
Sender any
} }
func GetTemplateData(ctx context.Context, payload any) TemplateData { func GetTemplateData(ctx context.Context, payload any) TemplateData {
@@ -112,20 +58,5 @@ func GetTemplateData(ctx context.Context, payload any) TemplateData {
if modules != nil { if modules != nil {
templateData.Modules = modules templateData.Modules = modules
} }
sender := ctx.Value(common.SenderContextKey)
if sender != nil {
templateData.Sender = sender
}
return templateData return templateData
} }
func GetEnvData(ctx context.Context, payload any) EnvData {
envData := EnvData{Payload: payload}
sender := ctx.Value(common.SenderContextKey)
if sender != nil {
envData.Sender = sender
}
return envData
}

View File

@@ -1,55 +0,0 @@
package processor
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
type RouterOutput struct {
config config.ProcessorConfig
ModuleId string
logger *slog.Logger
}
func (ro *RouterOutput) Process(ctx context.Context, payload any) (any, error) {
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return nil, errors.New("router.output no router found")
}
err := router.HandleOutput(ctx, ro.ModuleId, payload)
if err != nil {
return nil, fmt.Errorf("router.output failed to send output: %w", err)
}
return payload, nil
}
func (ro *RouterOutput) Type() string {
return ro.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "router.output",
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
moduleId, err := params.GetString("module")
if err != nil {
return nil, fmt.Errorf("router.output module error: %w", err)
}
return &RouterOutput{config: config, ModuleId: moduleId, logger: slog.Default().With("component", "processor", "type", config.Type)}, nil
},
})
}

View File

@@ -17,7 +17,7 @@ type ScriptExpr struct {
func (se *ScriptExpr) Process(ctx context.Context, payload any) (any, error) { func (se *ScriptExpr) Process(ctx context.Context, payload any) (any, error) {
exprEnv := GetEnvData(ctx, payload) exprEnv := SafeExprEnv(payload)
output, err := expr.Run(se.Program, exprEnv) output, err := expr.Run(se.Program, exprEnv)
if err != nil { if err != nil {

View File

@@ -5,7 +5,6 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"modernc.org/quickjs" "modernc.org/quickjs"
) )
@@ -31,16 +30,6 @@ func (sj *ScriptJS) Process(ctx context.Context, payload any) (any, error) {
vm.SetProperty(vm.GlobalObject(), payloadAtom, payload) vm.SetProperty(vm.GlobalObject(), payloadAtom, payload)
sender := ctx.Value(common.SenderContextKey)
if sender != nil {
senderAtom, err := vm.NewAtom("sender")
if err != nil {
return nil, err
}
vm.SetProperty(vm.GlobalObject(), senderAtom, sender)
}
_, err = vm.Eval(sj.Program, quickjs.EvalGlobal) _, err = vm.Eval(sj.Program, quickjs.EvalGlobal)
if err != nil { if err != nil {

View File

@@ -15,21 +15,21 @@ type ScriptWASM struct {
Function string Function string
} }
func (sw *ScriptWASM) Process(ctx context.Context, payload any) (any, error) { func (se *ScriptWASM) Process(ctx context.Context, payload any) (any, error) {
payloadBytes, ok := GetAnyAs[[]byte](payload) payloadBytes, ok := GetAnyAs[[]byte](payload)
if !ok { if !ok {
return nil, fmt.Errorf("script.wasm can only process a byte array") return nil, fmt.Errorf("script.wasm can only operator on byte array")
} }
program, err := sw.Program.Instance(ctx, extism.PluginInstanceConfig{}) program, err := se.Program.Instance(ctx, extism.PluginInstanceConfig{})
if err != nil { if err != nil {
return nil, err return nil, err
} }
_, output, err := program.Call(sw.Function, payloadBytes) _, output, err := program.Call(se.Function, payloadBytes)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -38,8 +38,8 @@ func (sw *ScriptWASM) Process(ctx context.Context, payload any) (any, error) {
return output, nil return output, nil
} }
func (sw *ScriptWASM) Type() string { func (se *ScriptWASM) Type() string {
return sw.config.Type return se.config.Type
} }
func init() { func init() {

View File

@@ -22,12 +22,12 @@ type SipAudioFileResponse struct {
AudioFile string AudioFile string
} }
func (srac *SipResponseAudioCreate) Process(ctx context.Context, payload any) (any, error) { func (scc *SipResponseAudioCreate) Process(ctx context.Context, payload any) (any, error) {
templateData := GetTemplateData(ctx, payload) templateData := GetTemplateData(ctx, payload)
var audioFileBuffer bytes.Buffer var audioFileBuffer bytes.Buffer
err := srac.AudioFile.Execute(&audioFileBuffer, templateData) err := scc.AudioFile.Execute(&audioFileBuffer, templateData)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -36,14 +36,14 @@ func (srac *SipResponseAudioCreate) Process(ctx context.Context, payload any) (a
audioFileString := audioFileBuffer.String() audioFileString := audioFileBuffer.String()
return SipAudioFileResponse{ return SipAudioFileResponse{
PreWait: srac.PreWait, PreWait: scc.PreWait,
PostWait: srac.PostWait, PostWait: scc.PostWait,
AudioFile: audioFileString, AudioFile: audioFileString,
}, nil }, nil
} }
func (srac *SipResponseAudioCreate) Type() string { func (scc *SipResponseAudioCreate) Type() string {
return srac.config.Type return scc.config.Type
} }
func init() { func init() {

View File

@@ -25,12 +25,12 @@ type SipDTMFResponse struct {
Digits string Digits string
} }
func (srdc *SipResponseDTMFCreate) Process(ctx context.Context, payload any) (any, error) { func (scc *SipResponseDTMFCreate) Process(ctx context.Context, payload any) (any, error) {
templateData := GetTemplateData(ctx, payload) templateData := GetTemplateData(ctx, payload)
var digitsBuffer bytes.Buffer var digitsBuffer bytes.Buffer
err := srdc.Digits.Execute(&digitsBuffer, templateData) err := scc.Digits.Execute(&digitsBuffer, templateData)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -38,19 +38,19 @@ func (srdc *SipResponseDTMFCreate) Process(ctx context.Context, payload any) (an
digitsString := digitsBuffer.String() digitsString := digitsBuffer.String()
if !srdc.validDTMF.MatchString(digitsString) { if !scc.validDTMF.MatchString(digitsString) {
return nil, errors.New("sip.response.dtmf.create result of digits template contains invalid characters") return nil, errors.New("sip.response.dtmf.create result of digits template contains invalid characters")
} }
return SipDTMFResponse{ return SipDTMFResponse{
PreWait: srdc.PreWait, PreWait: scc.PreWait,
PostWait: srdc.PostWait, PostWait: scc.PostWait,
Digits: digitsString, Digits: digitsString,
}, nil }, nil
} }
func (srdc *SipResponseDTMFCreate) Type() string { func (scc *SipResponseDTMFCreate) Type() string {
return srdc.config.Type return scc.config.Type
} }
func init() { func init() {

View File

@@ -18,12 +18,8 @@ func (sf *StructFieldGet) Process(ctx context.Context, payload any) (any, error)
s := reflect.ValueOf(payload) s := reflect.ValueOf(payload)
if s.Kind() != reflect.Struct { if s.Kind() != reflect.Struct {
if s.Kind() == reflect.Pointer && s.Elem().Kind() == reflect.Struct {
s = s.Elem()
} else {
return nil, errors.New("struct.field.get processor only accepts a struct payload") return nil, errors.New("struct.field.get processor only accepts a struct payload")
} }
}
field := s.FieldByName(sf.Name) field := s.FieldByName(sf.Name)
if !field.IsValid() { if !field.IsValid() {

View File

@@ -18,12 +18,8 @@ func (sm *StructMethodGet) Process(ctx context.Context, payload any) (any, error
s := reflect.ValueOf(payload) s := reflect.ValueOf(payload)
if s.Kind() != reflect.Struct { if s.Kind() != reflect.Struct {
if s.Kind() == reflect.Pointer && s.Elem().Kind() == reflect.Struct {
s = s.Elem()
} else {
return nil, errors.New("struct.method.get processor only accepts a struct payload") return nil, errors.New("struct.method.get processor only accepts a struct payload")
} }
}
method := s.MethodByName(sm.Name) method := s.MethodByName(sm.Name)
if !method.IsValid() { if !method.IsValid() {

View File

@@ -0,0 +1,254 @@
package processor_test
import (
"reflect"
"testing"
"github.com/jwetzell/artnet-go"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestArtnetPacketFilterFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["artnet.packet.filter"]
if !ok {
t.Fatalf("artnet.packet.filter processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "artnet.packet.filter",
Params: map[string]any{
"opCode": float64(artnet.OpTimeCode),
},
})
if err != nil {
t.Fatalf("failed to create artnet.packet.filter processor: %s", err)
}
if processorInstance.Type() != "artnet.packet.filter" {
t.Fatalf("artnet.packet.filter processor has wrong type: %s", processorInstance.Type())
}
payload := &artnet.ArtTimeCode{
ID: []byte{'A', 'r', 't', '-', 'N', 'e', 't', 0x00},
OpCode: artnet.OpTimeCode,
ProtVerHi: 0,
ProtVerLo: 14,
Filler1: 0,
StreamId: 0,
Frames: 11,
Seconds: 17,
Minutes: 3,
Hours: 0,
Type: 0,
}
expected := &artnet.ArtTimeCode{
ID: []byte{'A', 'r', 't', '-', 'N', 'e', 't', 0x00},
OpCode: artnet.OpTimeCode,
ProtVerHi: 0,
ProtVerLo: 14,
Filler1: 0,
StreamId: 0,
Frames: 11,
Seconds: 17,
Minutes: 3,
Hours: 0,
Type: 0,
}
got, err := processorInstance.Process(t.Context(), payload)
if err != nil {
t.Fatalf("artnet.packet.filter processing failed: %s", err)
}
if !reflect.DeepEqual(got, expected) {
t.Fatalf("artnet.packet.filter got %+v, expected %+v", got, expected)
}
}
func TestGoodArtnetPacketFilter(t *testing.T) {
tests := []struct {
name string
params map[string]any
payload any
expected artnet.ArtNetPacket
}{
{
name: "tiemcode packet with matching opCode",
params: map[string]any{
"opCode": float64(artnet.OpTimeCode),
},
payload: &artnet.ArtTimeCode{
ID: []byte{'A', 'r', 't', '-', 'N', 'e', 't', 0x00},
OpCode: artnet.OpTimeCode,
ProtVerHi: 0,
ProtVerLo: 14,
Filler1: 0,
StreamId: 0,
Frames: 11,
Seconds: 17,
Minutes: 3,
Hours: 0,
Type: 0,
},
expected: &artnet.ArtTimeCode{
ID: []byte{'A', 'r', 't', '-', 'N', 'e', 't', 0x00},
OpCode: artnet.OpTimeCode,
ProtVerHi: 0,
ProtVerLo: 14,
Filler1: 0,
StreamId: 0,
Frames: 11,
Seconds: 17,
Minutes: 3,
Hours: 0,
Type: 0,
},
},
{
name: "timecode packet with mismatching opCode",
params: map[string]any{
"opCode": float64(artnet.OpDmx),
},
payload: &artnet.ArtTimeCode{
ID: []byte{'A', 'r', 't', '-', 'N', 'e', 't', 0x00},
OpCode: artnet.OpTimeCode,
ProtVerHi: 0,
ProtVerLo: 14,
Filler1: 0,
StreamId: 0,
Frames: 11,
Seconds: 17,
Minutes: 3,
Hours: 0,
Type: 0,
},
expected: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := processor.ProcessorRegistry["artnet.packet.filter"]
if !ok {
t.Fatalf("artnet.packet.filter processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "artnet.packet.filter",
Params: test.params,
})
if err != nil {
t.Fatalf("artnet.packet.filter failed to create processor: %s", err)
}
got, err := processorInstance.Process(t.Context(), test.payload)
if err != nil {
t.Fatalf("artnet.packet.filter processing failed: %s", err)
}
if test.expected == nil {
if got != nil {
t.Fatalf("artnet.packet.filter got %+v, expected nil", got)
}
return
}
gotPacket, ok := got.(artnet.ArtNetPacket)
if !ok {
t.Fatalf("artnet.packet.filter returned a %T payload: %s", got, got)
}
if !reflect.DeepEqual(gotPacket, test.expected) {
t.Fatalf("artnet.packet.filter got %+v, expected %+v", gotPacket, test.expected)
}
})
}
}
func TestBadArtnetPacketFilter(t *testing.T) {
tests := []struct {
name string
params map[string]any
payload any
errorString string
}{
{
name: "non-artnet input",
payload: []byte{0x01},
params: map[string]any{"opCode": float64(artnet.OpTimeCode)},
errorString: "artnet.packet.filter processor only accepts an ArtNetPacket",
},
{
name: "no opCode param",
payload: &artnet.ArtTimeCode{
ID: []byte{'A', 'r', 't', '-', 'N', 'e', 't', 0x00},
OpCode: artnet.OpTimeCode,
ProtVerHi: 0,
ProtVerLo: 14,
Filler1: 0,
StreamId: 0,
Frames: 11,
Seconds: 17,
Minutes: 3,
Hours: 0,
Type: 0,
},
params: map[string]any{},
errorString: "artnet.packet.filter opCode error: not found",
},
{
name: "opCode not a number",
payload: &artnet.ArtTimeCode{
ID: []byte{'A', 'r', 't', '-', 'N', 'e', 't', 0x00},
OpCode: artnet.OpTimeCode,
ProtVerHi: 0,
ProtVerLo: 14,
Filler1: 0,
StreamId: 0,
Frames: 11,
Seconds: 17,
Minutes: 3,
Hours: 0,
Type: 0,
},
params: map[string]any{"opCode": "100"},
errorString: "artnet.packet.filter opCode error: not a number",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := processor.ProcessorRegistry["artnet.packet.filter"]
if !ok {
t.Fatalf("artnet.packet.filter processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "artnet.packet.filter",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("artnet.packet.filter got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
got, err := processorInstance.Process(t.Context(), test.payload)
if err == nil {
t.Fatalf("artnet.packet.filter expected to fail but got payload: %s", got)
}
if err.Error() != test.errorString {
t.Fatalf("artnet.packet.filter got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -8,13 +8,13 @@ import (
) )
func TestHTTPRequestCreateFromRegistry(t *testing.T) { func TestHTTPRequestCreateFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["http.request.do"] registration, ok := processor.ProcessorRegistry["http.request.create"]
if !ok { if !ok {
t.Fatalf("http.request.do processor not registered") t.Fatalf("http.request.create processor not registered")
} }
processorInstance, err := registration.New(config.ProcessorConfig{ processorInstance, err := registration.New(config.ProcessorConfig{
Type: "http.request.do", Type: "http.request.create",
Params: map[string]any{ Params: map[string]any{
"method": "GET", "method": "GET",
"url": "http://example.com", "url": "http://example.com",
@@ -22,10 +22,10 @@ func TestHTTPRequestCreateFromRegistry(t *testing.T) {
}) })
if err != nil { if err != nil {
t.Fatalf("failed to create http.request.do processor: %s", err) t.Fatalf("failed to create http.request.create processor: %s", err)
} }
if processorInstance.Type() != "http.request.do" { if processorInstance.Type() != "http.request.create" {
t.Fatalf("http.request.do processor has wrong type: %s", processorInstance.Type()) t.Fatalf("http.request.create processor has wrong type: %s", processorInstance.Type())
} }
} }

View File

@@ -0,0 +1,31 @@
package processor_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestHTTPRequestFilterFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["http.request.filter"]
if !ok {
t.Fatalf("http.request.filter processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "http.request.filter",
Params: map[string]any{
"method": "GET",
"path": "/test",
},
})
if err != nil {
t.Fatalf("failed to create http.request.filter processor: %s", err)
}
if processorInstance.Type() != "http.request.filter" {
t.Fatalf("http.request.filter processor has wrong type: %s", processorInstance.Type())
}
}

View File

@@ -99,9 +99,9 @@ func TestBadJsonDecode(t *testing.T) {
errorString string errorString string
}{ }{
{ {
name: "non-string or byte input", name: "non-string input",
payload: 123, payload: []byte("hello"),
errorString: "json.decode can only process a string or []byte", errorString: "json.decode processor only accepts a string",
}, },
{ {
name: "invalid json", name: "invalid json",

View File

@@ -0,0 +1,157 @@
package processor_test
import (
"reflect"
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
"gitlab.com/gomidi/midi/v2"
)
func TestMIDIMessageFilterFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["midi.message.filter"]
if !ok {
t.Fatalf("midi.message.filter processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "midi.message.filter",
Params: map[string]any{
"type": "NoteOn",
},
})
if err != nil {
t.Fatalf("failed to create midi.message.filter processor: %s", err)
}
if processorInstance.Type() != "midi.message.filter" {
t.Fatalf("midi.message.filter processor has wrong type: %s", processorInstance.Type())
}
}
func TestGoodMIDIMessageFilter(t *testing.T) {
tests := []struct {
name string
params map[string]any
payload midi.Message
expected midi.Message
}{
{
name: "matches pattern",
payload: midi.NoteOn(1, 60, 127),
params: map[string]any{"type": "NoteOn"},
expected: midi.NoteOn(1, 60, 127),
},
{
name: "does not match pattern",
payload: midi.NoteOn(1, 60, 127),
params: map[string]any{"type": "NoteOff"},
expected: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := processor.ProcessorRegistry["midi.message.filter"]
if !ok {
t.Fatalf("midi.message.filter processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "midi.message.filter",
Params: test.params,
})
if err != nil {
t.Fatalf("midi.message.filter failed to create processor: %s", err)
}
got, err := processorInstance.Process(t.Context(), test.payload)
if err != nil {
t.Fatalf("midi.message.filter processing failed: %s", err)
}
if test.expected == nil {
if got != nil {
t.Fatalf("midi.message.filter got %+v, expected nil", got)
}
return
}
gotMIDIMessage, ok := got.(midi.Message)
if !ok {
t.Fatalf("midi.message.filter returned a %T payload: %s", got, got)
}
if !reflect.DeepEqual(gotMIDIMessage, test.expected) {
t.Fatalf("midi.message.filter got %+v, expected %+v", gotMIDIMessage, test.expected)
}
})
}
}
func TestBadMIDIMessageFilter(t *testing.T) {
tests := []struct {
name string
params map[string]any
payload any
errorString string
}{
{
name: "no type param",
params: map[string]any{},
payload: midi.NoteOn(1, 60, 127),
errorString: "midi.message.filter type error: not found",
},
{
name: "non-string type param",
params: map[string]any{
"type": 123,
},
payload: "hello",
errorString: "midi.message.filter type error: not a string",
},
{
name: "non-midi message input",
params: map[string]any{
"type": "NoteOn",
},
payload: []byte{0x68, 0x65, 0x6c, 0x6c, 0x6f},
errorString: "midi.message.filter processor only accepts a midi.Message",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := processor.ProcessorRegistry["midi.message.filter"]
if !ok {
t.Fatalf("midi.message.filter processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "midi.message.filter",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("midi.message.filter got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
got, err := processorInstance.Process(t.Context(), test.payload)
if err == nil {
t.Fatalf("midi.message.filter expected to fail but got payload: %s", got)
}
if err.Error() != test.errorString {
t.Fatalf("midi.message.filter got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -0,0 +1,169 @@
package processor_test
import (
"reflect"
"testing"
"github.com/jwetzell/osc-go"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestOSCMessageFilterFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["osc.message.filter"]
if !ok {
t.Fatalf("osc.message.filter processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "osc.message.filter",
Params: map[string]any{
"address": "/test*",
},
})
if err != nil {
t.Fatalf("failed to filter osc.message.filter processor: %s", err)
}
if processorInstance.Type() != "osc.message.filter" {
t.Fatalf("osc.message.filter processor has wrong type: %s", processorInstance.Type())
}
}
func TestGoodOSCMessageFilter(t *testing.T) {
tests := []struct {
name string
payload osc.OSCMessage
params map[string]any
expected any
}{
{
name: "basic address match",
params: map[string]any{
"address": "/test",
},
payload: osc.OSCMessage{Address: "/test"},
expected: osc.OSCMessage{Address: "/test"},
},
{
name: "basic address no match",
params: map[string]any{
"address": "/test",
},
payload: osc.OSCMessage{Address: "/testing"},
expected: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := processor.ProcessorRegistry["osc.message.filter"]
if !ok {
t.Fatalf("osc.message.filter processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "osc.message.filter",
Params: test.params,
})
if err != nil {
t.Fatalf("osc.message.filter failed to create processor: %s", err)
}
got, err := processorInstance.Process(t.Context(), test.payload)
if err != nil {
t.Fatalf("osc.message.filter processing failed: %s", err)
}
if test.expected == nil {
if got != nil {
t.Fatalf("osc.message.filter got %+v, expected nil", got)
}
return
}
gotMessage, ok := got.(osc.OSCMessage)
if !ok {
t.Fatalf("osc.message.filter returned a %T payload: %s", got, got)
}
if !reflect.DeepEqual(gotMessage, test.expected) {
t.Fatalf("osc.message.filter got %+v, expected %+v", gotMessage, test.expected)
}
})
}
}
func TestBadOSCMessageFilter(t *testing.T) {
tests := []struct {
name string
params map[string]any
payload any
errorString string
}{
{
name: "no address parameter",
params: map[string]any{},
payload: osc.OSCMessage{Address: "/test"},
errorString: "osc.message.filter address error: not found",
},
{
name: "non-string address parameter",
params: map[string]any{
"address": 123,
},
payload: osc.OSCMessage{Address: "/test"},
errorString: "osc.message.filter address error: not a string",
},
{
name: "bad address pattern",
params: map[string]any{
"address": "[",
},
payload: osc.OSCMessage{Address: "/test"},
errorString: "error parsing regexp: missing closing ]: `[$`",
},
{
name: "non-osc input",
params: map[string]any{
"address": "/test",
},
payload: []byte("hello"),
errorString: "osc.message.filter can only operate on OSCMessage payloads",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := processor.ProcessorRegistry["osc.message.filter"]
if !ok {
t.Fatalf("osc.message.filter processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "osc.message.filter",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("string.create got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
got, err := processorInstance.Process(t.Context(), test.payload)
if err == nil {
t.Fatalf("osc.message.filter expected to fail but succeeded, got: %v", got)
}
if err.Error() != test.errorString {
t.Fatalf("osc.message.filter got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -139,7 +139,7 @@ func TestBadScriptWASM(t *testing.T) {
"enableWasi": true, "enableWasi": true,
}, },
payload: "hello", payload: "hello",
errorString: "script.wasm can only process a byte array", errorString: "script.wasm can only operator on byte array",
}, },
{ {
name: "function not found in module", name: "function not found in module",

View File

@@ -72,12 +72,6 @@ func TestGoodStructFieldGet(t *testing.T) {
payload: TestStruct{Bool: true}, payload: TestStruct{Bool: true},
expected: true, expected: true,
}, },
{
name: "pointer to struct payload",
params: map[string]any{"name": "Data"},
payload: &TestStruct{Data: "hello"},
expected: "hello",
},
} }
for _, test := range tests { for _, test := range tests {

View File

@@ -89,14 +89,6 @@ func TestGoodStructMethodGet(t *testing.T) {
payload: TestStruct{}, payload: TestStruct{},
expected: nil, expected: nil,
}, },
{
name: "pointer to struct payload",
params: map[string]any{
"name": "GetData",
},
payload: &TestStruct{Data: "hello"},
expected: "hello",
},
} }
for _, test := range tests { for _, test := range tests {

View File

@@ -8,18 +8,18 @@ import (
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
type TimeSleep struct { type MetaDelay struct {
config config.ProcessorConfig config config.ProcessorConfig
Duration time.Duration Duration time.Duration
} }
func (ts *TimeSleep) Process(ctx context.Context, payload any) (any, error) { func (md *MetaDelay) Process(ctx context.Context, payload any) (any, error) {
time.Sleep(ts.Duration) time.Sleep(md.Duration)
return payload, nil return payload, nil
} }
func (ts *TimeSleep) Type() string { func (md *MetaDelay) Type() string {
return ts.config.Type return md.config.Type
} }
func init() { func init() {
@@ -33,7 +33,7 @@ func init() {
return nil, fmt.Errorf("time.sleep duration error: %w", err) return nil, fmt.Errorf("time.sleep duration error: %w", err)
} }
return &TimeSleep{config: config, Duration: time.Millisecond * time.Duration(durationNum)}, nil return &MetaDelay{config: config, Duration: time.Millisecond * time.Duration(durationNum)}, nil
}, },
}) })
} }

View File

@@ -17,9 +17,23 @@ type RouteError struct {
Config config.RouteConfig Config config.RouteConfig
Error error Error error
} }
type RouteIOError struct {
Index int
OutputError error
ProcessError error
InputError error
}
type RouteIO interface {
HandleInput(ctx context.Context, sourceId string, payload any) (bool, []RouteIOError)
HandleOutput(ctx context.Context, destinationId string, payload any) error
}
type Route struct { type Route struct {
input string input string
processors []processor.Processor processors []processor.Processor
output string
} }
func NewRoute(config config.RouteConfig) (*Route, error) { func NewRoute(config config.RouteConfig) (*Route, error) {
@@ -40,13 +54,17 @@ func NewRoute(config config.RouteConfig) (*Route, error) {
} }
} }
return &Route{input: config.Input, processors: processors}, nil return &Route{input: config.Input, processors: processors, output: config.Output}, nil
} }
func (r *Route) Input() string { func (r *Route) Input() string {
return r.input return r.input
} }
func (r *Route) Output() string {
return r.output
}
func (r *Route) ProcessPayload(ctx context.Context, payload any) (any, error) { func (r *Route) ProcessPayload(ctx context.Context, payload any) (any, error) {
tracer := otel.Tracer("route") tracer := otel.Tracer("route")
processCtx, processSpan := tracer.Start(ctx, "ProcessPayload") processCtx, processSpan := tracer.Start(ctx, "ProcessPayload")

View File

@@ -14,6 +14,7 @@ import (
func TestRouteCreate(t *testing.T) { func TestRouteCreate(t *testing.T) {
routeConfig := config.RouteConfig{ routeConfig := config.RouteConfig{
Input: "input", Input: "input",
Output: "output",
} }
testRoute, err := route.NewRoute(routeConfig) testRoute, err := route.NewRoute(routeConfig)
@@ -24,12 +25,15 @@ func TestRouteCreate(t *testing.T) {
if testRoute.Input() != routeConfig.Input { if testRoute.Input() != routeConfig.Input {
t.Fatalf("route input does not match expected input") t.Fatalf("route input does not match expected input")
} }
if testRoute.Output() != routeConfig.Output {
t.Fatalf("route output does not match expected output")
}
} }
type MockRouter struct{} type MockRouter struct{}
func (mr *MockRouter) HandleInput(ctx context.Context, sourceId string, payload any) (bool, []common.RouteIOError) { func (mr *MockRouter) HandleInput(ctx context.Context, sourceId string, payload any) (bool, []route.RouteIOError) {
return false, []common.RouteIOError{} return false, []route.RouteIOError{}
} }
func (mr *MockRouter) HandleOutput(ctx context.Context, destinationId string, payload any) error { func (mr *MockRouter) HandleOutput(ctx context.Context, destinationId string, payload any) error {
@@ -41,13 +45,8 @@ func TestGoodRouteHandleInput(t *testing.T) {
Input: "input", Input: "input",
Processors: []config.ProcessorConfig{ Processors: []config.ProcessorConfig{
{Type: "string.encode"}, {Type: "string.encode"},
{
Type: "router.output",
Params: config.Params{
"module": "output",
},
},
}, },
Output: "output",
} }
testRoute, err := route.NewRoute(routeConfig) testRoute, err := route.NewRoute(routeConfig)
@@ -76,13 +75,8 @@ func TestRouteHandleInputWithProcessorError(t *testing.T) {
Input: "input", Input: "input",
Processors: []config.ProcessorConfig{ Processors: []config.ProcessorConfig{
{Type: "string.create", Params: map[string]any{"template": "{{.invalid}}}"}}, {Type: "string.create", Params: map[string]any{"template": "{{.invalid}}}"}},
{
Type: "router.output",
Params: config.Params{
"module": "output",
},
},
}, },
Output: "output",
} }
testRoute, err := route.NewRoute(routeConfig) testRoute, err := route.NewRoute(routeConfig)
@@ -100,14 +94,8 @@ func TestRouteHandleInputWithProcessorError(t *testing.T) {
func TestRouteHandleNilPayload(t *testing.T) { func TestRouteHandleNilPayload(t *testing.T) {
routeConfig := config.RouteConfig{ routeConfig := config.RouteConfig{
Input: "input", Input: "input",
Processors: []config.ProcessorConfig{ Processors: []config.ProcessorConfig{},
{ Output: "output",
Type: "router.output",
Params: config.Params{
"module": "output",
},
},
},
} }
testRoute, err := route.NewRoute(routeConfig) testRoute, err := route.NewRoute(routeConfig)
@@ -130,13 +118,8 @@ func TestRouteHandleNilPayloadFromProcessor(t *testing.T) {
Input: "input", Input: "input",
Processors: []config.ProcessorConfig{ Processors: []config.ProcessorConfig{
{Type: "script.js", Params: map[string]any{"program": "payload = undefined"}}, {Type: "script.js", Params: map[string]any{"program": "payload = undefined"}},
{
Type: "router.output",
Params: config.Params{
"module": "output",
},
},
}, },
Output: "output",
} }
testRoute, err := route.NewRoute(routeConfig) testRoute, err := route.NewRoute(routeConfig)
@@ -160,6 +143,7 @@ func TestRouteUnknownProcessor(t *testing.T) {
Processors: []config.ProcessorConfig{ Processors: []config.ProcessorConfig{
{Type: "asdfasdflkjalkj"}, {Type: "asdfasdflkjalkj"},
}, },
Output: "output",
} }
_, err := route.NewRoute(routeConfig) _, err := route.NewRoute(routeConfig)
@@ -174,6 +158,7 @@ func TestRouteBadProcessorConfig(t *testing.T) {
Processors: []config.ProcessorConfig{ Processors: []config.ProcessorConfig{
{Type: "string.create", Params: map[string]any{}}, {Type: "string.create", Params: map[string]any{}},
}, },
Output: "output",
} }
_, err := route.NewRoute(routeConfig) _, err := route.NewRoute(routeConfig)

View File

@@ -172,10 +172,10 @@ func (r *Router) Stop() {
r.contextCancel() r.contextCancel()
} }
func (r *Router) HandleInput(ctx context.Context, sourceId string, payload any) (bool, []common.RouteIOError) { func (r *Router) HandleInput(ctx context.Context, sourceId string, payload any) (bool, []route.RouteIOError) {
spanCtx, span := otel.Tracer("router").Start(ctx, "input", trace.WithAttributes(attribute.String("source.id", sourceId)), trace.WithNewRoot()) spanCtx, span := otel.Tracer("router").Start(ctx, "input", trace.WithAttributes(attribute.String("source.id", sourceId)), trace.WithNewRoot())
defer span.End() defer span.End()
var routeIOErrors []common.RouteIOError var routeIOErrors []route.RouteIOError
routeFound := false routeFound := false
var routeWaitGroup sync.WaitGroup var routeWaitGroup sync.WaitGroup
@@ -190,22 +190,37 @@ func (r *Router) HandleInput(ctx context.Context, sourceId string, payload any)
routeFound = true routeFound = true
routeContext := context.WithValue(spanCtx, common.SourceContextKey, sourceId) routeContext := context.WithValue(spanCtx, common.SourceContextKey, sourceId)
routeContext = context.WithValue(routeContext, common.RouterContextKey, r)
routeContext = context.WithValue(routeContext, common.ModulesContextKey, r.ModuleInstances) routeContext = context.WithValue(routeContext, common.ModulesContextKey, r.ModuleInstances)
routeCtx, routeSpan := otel.Tracer("router").Start(routeContext, "route", trace.WithAttributes(attribute.Int("route.index", routeIndex), attribute.String("route.input", routeInstance.Input()))) routeCtx, routeSpan := otel.Tracer("router").Start(routeContext, "route", trace.WithAttributes(attribute.Int("route.index", routeIndex), attribute.String("route.input", routeInstance.Input()), attribute.String("route.output", routeInstance.Output())))
_, err := routeInstance.ProcessPayload(routeCtx, payload) payload, err := routeInstance.ProcessPayload(routeCtx, payload)
if err != nil { if err != nil {
if routeIOErrors == nil { if routeIOErrors == nil {
routeIOErrors = []common.RouteIOError{} routeIOErrors = []route.RouteIOError{}
} }
r.logger.Error("unable to process input", "route", routeIndex, "source", sourceId, "error", err) r.logger.Error("unable to process input", "route", routeIndex, "source", sourceId, "error", err)
routeIOErrors = append(routeIOErrors, common.RouteIOError{ routeIOErrors = append(routeIOErrors, route.RouteIOError{
Index: routeIndex, Index: routeIndex,
ProcessError: err, ProcessError: err,
}) })
return return
} }
if payload == nil {
r.logger.Debug("no payload after processing, route terminated", "route", routeIndex, "source", sourceId)
return
}
outputError := r.HandleOutput(routeCtx, routeInstance.Output(), payload)
if outputError != nil {
if routeIOErrors == nil {
routeIOErrors = []route.RouteIOError{}
}
routeIOErrors = append(routeIOErrors, route.RouteIOError{
Index: routeIndex,
OutputError: outputError,
})
}
routeSpan.End() routeSpan.End()
}) })
} }

View File

@@ -12,13 +12,14 @@ import (
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module" "github.com/jwetzell/showbridge-go/internal/module"
"github.com/jwetzell/showbridge-go/internal/route"
) )
type MockCounterModule struct { type MockCounterModule struct {
config config.ModuleConfig config config.ModuleConfig
ctx context.Context ctx context.Context
outputCount int outputCount int
router common.RouteIO router route.RouteIO
logger *slog.Logger logger *slog.Logger
cancel context.CancelFunc cancel context.CancelFunc
} }
@@ -33,7 +34,7 @@ func (mcm *MockCounterModule) Output(context.Context, any) error {
} }
func (mcm *MockCounterModule) Start(ctx context.Context) error { func (mcm *MockCounterModule) Start(ctx context.Context) error {
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO) router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok { if !ok {
return fmt.Errorf("mock.counter could not get router from context") return fmt.Errorf("mock.counter could not get router from context")
@@ -170,6 +171,7 @@ func TestNewRouterRouteWithUnknwonProcessor(t *testing.T) {
Type: "asdfasdf", Type: "asdfasdf",
}, },
}, },
Output: "mock",
}, },
}, },
} }
@@ -200,14 +202,7 @@ func TestRouterInputUnknownDestinationModule(t *testing.T) {
Routes: []config.RouteConfig{ Routes: []config.RouteConfig{
{ {
Input: "mock", Input: "mock",
Processors: []config.ProcessorConfig{ Output: "test",
{
Type: "router.output",
Params: config.Params{
"module": "test",
},
},
},
}, },
}, },
} }
@@ -243,8 +238,8 @@ func TestRouterInputUnknownDestinationModule(t *testing.T) {
t.Fatalf("router should have returned exactly 1 routing error, got: %d", len(routingErrors)) t.Fatalf("router should have returned exactly 1 routing error, got: %d", len(routingErrors))
} }
if routingErrors[0].ProcessError.Error() != "router.output failed to send output: no module found for destination id" { if routingErrors[0].OutputError.Error() != "no module found for destination id" {
t.Fatalf("routing output error did not match expected, got: %s", routingErrors[0].ProcessError.Error()) t.Fatalf("routing output error did not match expected, got: %s", routingErrors[0].OutputError.Error())
} }
} }
@@ -259,14 +254,7 @@ func TestRouterInputNoMatchingRoute(t *testing.T) {
Routes: []config.RouteConfig{ Routes: []config.RouteConfig{
{ {
Input: "test", Input: "test",
Processors: []config.ProcessorConfig{ Output: "mock",
{
Type: "router.output",
Params: config.Params{
"module": "mock",
},
},
},
}, },
}, },
} }
@@ -310,14 +298,7 @@ func TestRouterInputSingleRoute(t *testing.T) {
Routes: []config.RouteConfig{ Routes: []config.RouteConfig{
{ {
Input: "mock", Input: "mock",
Processors: []config.ProcessorConfig{ Output: "mock",
{
Type: "router.output",
Params: config.Params{
"module": "mock",
},
},
},
}, },
}, },
} }
@@ -381,36 +362,15 @@ func TestRouterInputMultipleRoutes(t *testing.T) {
Routes: []config.RouteConfig{ Routes: []config.RouteConfig{
{ {
Input: "mock", Input: "mock",
Processors: []config.ProcessorConfig{ Output: "mock",
{
Type: "router.output",
Params: config.Params{
"module": "mock",
},
},
},
}, },
{ {
Input: "mock", Input: "mock",
Processors: []config.ProcessorConfig{ Output: "mock",
{
Type: "router.output",
Params: config.Params{
"module": "mock",
},
},
},
}, },
{ {
Input: "mock", Input: "mock",
Processors: []config.ProcessorConfig{ Output: "mock",
{
Type: "router.output",
Params: config.Params{
"module": "mock",
},
},
},
}, },
}, },
} }
@@ -477,25 +437,11 @@ func TestRouterInputMultipleModules(t *testing.T) {
Routes: []config.RouteConfig{ Routes: []config.RouteConfig{
{ {
Input: "mock1", Input: "mock1",
Processors: []config.ProcessorConfig{ Output: "mock1",
{
Type: "router.output",
Params: config.Params{
"module": "mock1",
},
},
},
}, },
{ {
Input: "mock2", Input: "mock2",
Processors: []config.ProcessorConfig{ Output: "mock2",
{
Type: "router.output",
Params: config.Params{
"module": "mock2",
},
},
},
}, },
}, },
} }

View File

@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://showbridge.io/config.schema.json", "$id": "https://showbridge.io/config.schema.json",
"title": "Config", "title": "Config",
"description": "showbridge configuration", "description": "showbridge config file",
"type": "object", "type": "object",
"properties": { "properties": {
"modules": { "modules": {

View File

@@ -2,13 +2,28 @@
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://showbridge.io/modules.schema.json", "$id": "https://showbridge.io/modules.schema.json",
"title": "Modules", "title": "Modules",
"description": "module configurations", "description": "showbridge modules array",
"type": "array", "type": "array",
"items": { "items": {
"oneOf": [ "oneOf": [
{ {
"type": "object", "type": "object",
"title": "HTTP Server", "title": "HTTPClientModule",
"properties": {
"id": {
"type": "string",
"minLength": 1
},
"type": {
"const": "http.client"
}
},
"required": ["id", "type"],
"additionalProperties": false
},
{
"type": "object",
"title": "HTTPServerModule",
"properties": { "properties": {
"id": { "id": {
"type": "string", "type": "string",
@@ -21,7 +36,6 @@
"type": "object", "type": "object",
"properties": { "properties": {
"port": { "port": {
"title": "Port",
"type": "integer", "type": "integer",
"minimum": 1, "minimum": 1,
"maximum": 65535 "maximum": 65535
@@ -36,7 +50,7 @@
}, },
{ {
"type": "object", "type": "object",
"title": "Interval", "title": "TimeIntervalModule",
"properties": { "properties": {
"id": { "id": {
"type": "string", "type": "string",
@@ -49,9 +63,7 @@
"type": "object", "type": "object",
"properties": { "properties": {
"duration": { "duration": {
"title": "Duration", "type": "integer"
"type": "integer",
"description": "Interval duration in milliseconds"
} }
}, },
"required": ["duration"], "required": ["duration"],
@@ -63,7 +75,7 @@
}, },
{ {
"type": "object", "type": "object",
"title": "Timer", "title": "TimeTimerModule",
"properties": { "properties": {
"id": { "id": {
"type": "string", "type": "string",
@@ -76,9 +88,7 @@
"type": "object", "type": "object",
"properties": { "properties": {
"duration": { "duration": {
"title": "Duration", "type": "integer"
"type": "integer",
"description": "Timer duration in milliseconds"
} }
}, },
"required": ["duration"], "required": ["duration"],
@@ -90,7 +100,7 @@
}, },
{ {
"type": "object", "type": "object",
"title": "MIDI Input", "title": "MIDIInputModule",
"properties": { "properties": {
"id": { "id": {
"type": "string", "type": "string",
@@ -103,7 +113,6 @@
"type": "object", "type": "object",
"properties": { "properties": {
"port": { "port": {
"title": "Port",
"type": "string" "type": "string"
} }
}, },
@@ -116,7 +125,7 @@
}, },
{ {
"type": "object", "type": "object",
"title": "MIDI Output", "title": "MIDIOutputModule",
"properties": { "properties": {
"id": { "id": {
"type": "string", "type": "string",
@@ -129,7 +138,6 @@
"type": "object", "type": "object",
"properties": { "properties": {
"port": { "port": {
"title": "Port",
"type": "string" "type": "string"
} }
}, },
@@ -142,7 +150,7 @@
}, },
{ {
"type": "object", "type": "object",
"title": "MQTT Client", "title": "MQTTClientModule",
"properties": { "properties": {
"id": { "id": {
"type": "string", "type": "string",
@@ -155,15 +163,12 @@
"type": "object", "type": "object",
"properties": { "properties": {
"broker": { "broker": {
"title": "Broker URL",
"type": "string" "type": "string"
}, },
"topic": { "topic": {
"title": "Topic",
"type": "string" "type": "string"
}, },
"clientId": { "clientId": {
"title": "Client ID",
"type": "string" "type": "string"
} }
}, },
@@ -176,7 +181,7 @@
}, },
{ {
"type": "object", "type": "object",
"title": "NATS Client", "title": "NATSClientModule",
"properties": { "properties": {
"id": { "id": {
"type": "string", "type": "string",
@@ -189,11 +194,9 @@
"type": "object", "type": "object",
"properties": { "properties": {
"url": { "url": {
"title": "NATS Server URL",
"type": "string" "type": "string"
}, },
"subject": { "subject": {
"title": "Subject",
"type": "string" "type": "string"
} }
}, },
@@ -206,7 +209,7 @@
}, },
{ {
"type": "object", "type": "object",
"title": "NATS Server", "title": "NATSServerModule",
"properties": { "properties": {
"id": { "id": {
"type": "string", "type": "string",
@@ -219,12 +222,10 @@
"type": "object", "type": "object",
"properties": { "properties": {
"ip": { "ip": {
"title": "IP",
"type": "string", "type": "string",
"default": "0.0.0.0" "default": "0.0.0.0"
}, },
"port": { "port": {
"title": "Port",
"type": "integer", "type": "integer",
"minimum": 1, "minimum": 1,
"maximum": 65535, "maximum": 65535,
@@ -240,7 +241,7 @@
}, },
{ {
"type": "object", "type": "object",
"title": "PSN Client", "title": "PSNClientModule",
"properties": { "properties": {
"id": { "id": {
"type": "string", "type": "string",
@@ -255,7 +256,7 @@
}, },
{ {
"type": "object", "type": "object",
"title": "Serial Client", "title": "SerialClientModule",
"properties": { "properties": {
"id": { "id": {
"type": "string", "type": "string",
@@ -268,11 +269,9 @@
"type": "object", "type": "object",
"properties": { "properties": {
"port": { "port": {
"title": "Port",
"type": "string" "type": "string"
}, },
"baudRate": { "baudRate": {
"title": "Baud Rate",
"type": "integer" "type": "integer"
} }
}, },
@@ -285,7 +284,7 @@
}, },
{ {
"type": "object", "type": "object",
"title": "SIP Call Server", "title": "SIPCallServerModule",
"properties": { "properties": {
"id": { "id": {
"type": "string", "type": "string",
@@ -298,25 +297,21 @@
"type": "object", "type": "object",
"properties": { "properties": {
"ip": { "ip": {
"title": "IP",
"type": "string", "type": "string",
"default": "0.0.0.0" "default": "0.0.0.0"
}, },
"port": { "port": {
"title": "Port",
"type": "integer", "type": "integer",
"minimum": 1, "minimum": 1,
"maximum": 65535, "maximum": 65535,
"default": 5060 "default": 5060
}, },
"transport": { "transport": {
"title": "Transport",
"type": "string", "type": "string",
"enum": ["udp", "tcp", "ws", "udp4", "tcp4"], "enum": ["udp", "tcp", "ws", "udp4", "tcp4"],
"default": "udp" "default": "udp"
}, },
"userAgent": { "userAgent": {
"title": "User Agent",
"type": "string", "type": "string",
"default": "showbridge" "default": "showbridge"
} }
@@ -330,7 +325,7 @@
}, },
{ {
"type": "object", "type": "object",
"title": "SIP DTMF Server", "title": "SIPDTMFServerModule",
"properties": { "properties": {
"id": { "id": {
"type": "string", "type": "string",
@@ -343,30 +338,25 @@
"type": "object", "type": "object",
"properties": { "properties": {
"ip": { "ip": {
"title": "IP",
"type": "string", "type": "string",
"default": "0.0.0.0" "default": "0.0.0.0"
}, },
"port": { "port": {
"title": "Port",
"type": "integer", "type": "integer",
"minimum": 1, "minimum": 1,
"maximum": 65535, "maximum": 65535,
"default": 5060 "default": 5060
}, },
"transport": { "transport": {
"title": "Transport",
"type": "string", "type": "string",
"enum": ["udp", "tcp", "ws", "udp4", "tcp4"], "enum": ["udp", "tcp", "ws", "udp4", "tcp4"],
"default": "udp" "default": "udp"
}, },
"userAgent": { "userAgent": {
"title": "User Agent",
"type": "string", "type": "string",
"default": "showbridge" "default": "showbridge"
}, },
"separator": { "separator": {
"title": "DTMF Separator",
"type": "string", "type": "string",
"minLength": 1, "minLength": 1,
"maxLength": 1 "maxLength": 1
@@ -381,7 +371,7 @@
}, },
{ {
"type": "object", "type": "object",
"title": "TCP Client", "title": "TCPClientModule",
"properties": { "properties": {
"id": { "id": {
"type": "string", "type": "string",
@@ -394,17 +384,14 @@
"type": "object", "type": "object",
"properties": { "properties": {
"host": { "host": {
"title": "Host",
"type": "string" "type": "string"
}, },
"port": { "port": {
"title": "Port",
"type": "integer", "type": "integer",
"minimum": 1, "minimum": 1,
"maximum": 65535 "maximum": 65535
}, },
"framing": { "framing": {
"title": "Framing Method",
"type": "string", "type": "string",
"enum": ["LF", "CR", "CRLF", "SLIP", "RAW"] "enum": ["LF", "CR", "CRLF", "SLIP", "RAW"]
} }
@@ -418,7 +405,7 @@
}, },
{ {
"type": "object", "type": "object",
"title": "TCP Server", "title": "TCPServerModule",
"properties": { "properties": {
"id": { "id": {
"type": "string", "type": "string",
@@ -431,18 +418,15 @@
"type": "object", "type": "object",
"properties": { "properties": {
"ip": { "ip": {
"title": "IP",
"type": "string", "type": "string",
"default": "0.0.0.0" "default": "0.0.0.0"
}, },
"port": { "port": {
"title" : "Port",
"type": "integer", "type": "integer",
"minimum": 1, "minimum": 1,
"maximum": 65535 "maximum": 65535
}, },
"framing": { "framing": {
"title": "Framing Method",
"type": "string", "type": "string",
"enum": ["LF", "CR", "CRLF", "SLIP", "RAW"] "enum": ["LF", "CR", "CRLF", "SLIP", "RAW"]
} }
@@ -456,7 +440,7 @@
}, },
{ {
"type": "object", "type": "object",
"title": "UDP Client", "title": "UDPClientModule",
"properties": { "properties": {
"id": { "id": {
"type": "string", "type": "string",
@@ -469,11 +453,9 @@
"type": "object", "type": "object",
"properties": { "properties": {
"host": { "host": {
"title": "Host",
"type": "string" "type": "string"
}, },
"port": { "port": {
"title": "Port",
"type": "integer", "type": "integer",
"minimum": 1, "minimum": 1,
"maximum": 65535 "maximum": 65535
@@ -488,7 +470,7 @@
}, },
{ {
"type": "object", "type": "object",
"title": "UDP Multicast", "title": "UDPMulticastModule",
"properties": { "properties": {
"id": { "id": {
"type": "string", "type": "string",
@@ -501,11 +483,9 @@
"type": "object", "type": "object",
"properties": { "properties": {
"ip": { "ip": {
"title": "IP",
"type": "string" "type": "string"
}, },
"port": { "port": {
"title": "Port",
"type": "integer", "type": "integer",
"minimum": 1, "minimum": 1,
"maximum": 65535 "maximum": 65535
@@ -520,7 +500,7 @@
}, },
{ {
"type": "object", "type": "object",
"title": "UDP Server", "title": "UDPServerModule",
"properties": { "properties": {
"id": { "id": {
"type": "string", "type": "string",
@@ -533,12 +513,10 @@
"type": "object", "type": "object",
"properties": { "properties": {
"ip": { "ip": {
"title": "IP",
"type": "string", "type": "string",
"default": "0.0.0.0" "default": "0.0.0.0"
}, },
"port": { "port": {
"title": "Port",
"type": "integer", "type": "integer",
"minimum": 1, "minimum": 1,
"maximum": 65535 "maximum": 65535

View File

@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://showbridge.io/processors.schema.json", "$id": "https://showbridge.io/processors.schema.json",
"title": "Processors", "title": "Processors",
"description": "processor configurations", "description": "showbridge processors array",
"type": "array", "type": "array",
"items": { "items": {
"oneOf": [ "oneOf": [
@@ -30,6 +30,28 @@
"required": ["type"], "required": ["type"],
"additionalProperties": false "additionalProperties": false
}, },
{
"type": "object",
"title": "Filter ArtNet Packet",
"properties": {
"type": {
"type": "string",
"const": "artnet.packet.filter"
},
"params": {
"type": "object",
"properties": {
"opCode": {
"type": "integer"
}
},
"required": ["opCode"],
"additionalProperties": false
}
},
"required": ["type", "params"],
"additionalProperties": false
},
{ {
"type": "object", "type": "object",
"title": "Debug Log", "title": "Debug Log",
@@ -54,7 +76,6 @@
"type": "object", "type": "object",
"properties": { "properties": {
"expression": { "expression": {
"title": "Expression",
"type": "string" "type": "string"
} }
}, },
@@ -72,18 +93,6 @@
"type": { "type": {
"type": "string", "type": "string",
"const": "float.parse" "const": "float.parse"
},
"params": {
"type": "object",
"properties": {
"bitSize": {
"title": "Bit Size",
"type": "integer",
"enum": [32, 64],
"default": 64
}
},
"additionalProperties": false
} }
}, },
"required": ["type"], "required": ["type"],
@@ -101,39 +110,30 @@
"type": "object", "type": "object",
"properties": { "properties": {
"id": { "id": {
"title": "Camera ID",
"type": "string" "type": "string"
}, },
"pan": { "pan": {
"title": "Pan",
"type": "string" "type": "string"
}, },
"tilt": { "tilt": {
"title": "Tilt",
"type": "string" "type": "string"
}, },
"roll": { "roll": {
"title": "Roll",
"type": "string" "type": "string"
}, },
"posX": { "posX": {
"title": "Position X",
"type": "string" "type": "string"
}, },
"posY": { "posY": {
"title": "Position Y",
"type": "string" "type": "string"
}, },
"posZ": { "posZ": {
"title": "Position Z",
"type": "string" "type": "string"
}, },
"zoom": { "zoom": {
"title": "Zoom",
"type": "string" "type": "string"
}, },
"focus": { "focus": {
"title": "Focus",
"type": "string" "type": "string"
} }
}, },
@@ -180,22 +180,20 @@
}, },
{ {
"type": "object", "type": "object",
"title": "Do HTTP Request", "title": "Create HTTP Request",
"properties": { "properties": {
"type": { "type": {
"type": "string", "type": "string",
"const": "http.request.do" "const": "http.request.create"
}, },
"params": { "params": {
"type": "object", "type": "object",
"properties": { "properties": {
"method": { "method": {
"title": "HTTP Method",
"type": "string", "type": "string",
"enum": ["GET", "POST"] "enum": ["GET", "POST"]
}, },
"url": { "url": {
"title": "URL",
"type": "string" "type": "string"
} }
}, },
@@ -206,6 +204,32 @@
"required": ["type", "params"], "required": ["type", "params"],
"additionalProperties": false "additionalProperties": false
}, },
{
"type": "object",
"title": "Filter HTTP Request",
"properties": {
"type": {
"type": "string",
"const": "http.request.filter"
},
"params": {
"type": "object",
"properties": {
"path": {
"type": "string"
},
"method": {
"type": "string",
"enum": ["GET", "POST"]
}
},
"required": ["path"],
"additionalProperties": false
}
},
"required": ["type", "params"],
"additionalProperties": false
},
{ {
"type": "object", "type": "object",
"title": "Create HTTP Response", "title": "Create HTTP Response",
@@ -218,11 +242,9 @@
"type": "object", "type": "object",
"properties": { "properties": {
"status": { "status": {
"title": "Status Code",
"type": "integer" "type": "integer"
}, },
"body": { "body": {
"title": "Body",
"type": "string" "type": "string"
} }
}, },
@@ -240,24 +262,6 @@
"type": { "type": {
"type": "string", "type": "string",
"const": "int.parse" "const": "int.parse"
},
"params": {
"type": "object",
"properties": {
"base": {
"title": "Base",
"type": "integer",
"enum": [0,2, 8, 10, 16],
"default": 10
},
"bitSize": {
"title": "Bit Size",
"type": "integer",
"enum": [0, 8, 16, 32, 64],
"default": 64
}
},
"additionalProperties": false
} }
}, },
"required": ["type"], "required": ["type"],
@@ -275,11 +279,9 @@
"type": "object", "type": "object",
"properties": { "properties": {
"min": { "min": {
"title": "Minimum",
"type": "integer" "type": "integer"
}, },
"max": { "max": {
"title": "Maximum",
"type": "integer" "type": "integer"
} }
}, },
@@ -329,20 +331,16 @@
"type": "object", "type": "object",
"properties": { "properties": {
"type": { "type": {
"title": "MIDI Message Type",
"type": "string", "type": "string",
"enum": ["NoteOn", "noteon", "note_on"] "enum": ["NoteOn", "noteon", "note_on"]
}, },
"channel": { "channel": {
"title": "Channel",
"type": "string" "type": "string"
}, },
"note": { "note": {
"title": "Note",
"type": "string" "type": "string"
}, },
"velocity": { "velocity": {
"title": "Velocity",
"type": "string" "type": "string"
} }
}, },
@@ -353,20 +351,16 @@
"type": "object", "type": "object",
"properties": { "properties": {
"type": { "type": {
"title": "MIDI Message Type",
"type": "string", "type": "string",
"enum": ["NoteOff", "noteoff", "note_off"] "enum": ["NoteOff", "noteoff", "note_off"]
}, },
"channel": { "channel": {
"title": "Channel",
"type": "string" "type": "string"
}, },
"note": { "note": {
"title": "Note",
"type": "string" "type": "string"
}, },
"velocity": { "velocity": {
"title": "Velocity",
"type": "string" "type": "string"
} }
}, },
@@ -377,20 +371,16 @@
"type": "object", "type": "object",
"properties": { "properties": {
"type": { "type": {
"title": "MIDI Message Type",
"type": "string", "type": "string",
"enum": ["ControlChange", "controlchange", "control_change"] "enum": ["ControlChange", "controlchange", "control_change"]
}, },
"channel": { "channel": {
"title": "Channel",
"type": "string" "type": "string"
}, },
"control": { "control": {
"title": "Control",
"type": "string" "type": "string"
}, },
"value": { "value": {
"title": "Value",
"type": "string" "type": "string"
} }
}, },
@@ -401,16 +391,13 @@
"type": "object", "type": "object",
"properties": { "properties": {
"type": { "type": {
"title": "MIDI Message Type",
"type": "string", "type": "string",
"enum": ["ProgramChange", "programchange", "program_change"] "enum": ["ProgramChange", "programchange", "program_change"]
}, },
"channel": { "channel": {
"title": "Channel",
"type": "string" "type": "string"
}, },
"program": { "program": {
"title": "Program",
"type": "string" "type": "string"
} }
}, },
@@ -447,6 +434,29 @@
"required": ["type"], "required": ["type"],
"additionalProperties": false "additionalProperties": false
}, },
{
"type": "object",
"title": "Filter MIDI Message",
"properties": {
"type": {
"type": "string",
"const": "midi.message.filter"
},
"params": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["NoteOn", "NoteOff", "ControlChange", "ProgramChange"]
}
},
"required": ["type"],
"additionalProperties": false
}
},
"required": ["type", "params"],
"additionalProperties": false
},
{ {
"type": "object", "type": "object",
"title": "Unpack MIDI Message", "title": "Unpack MIDI Message",
@@ -471,19 +481,15 @@
"type": "object", "type": "object",
"properties": { "properties": {
"topic": { "topic": {
"title": "Topic",
"type": "string" "type": "string"
}, },
"qos": { "qos": {
"title": "QoS",
"type": "number" "type": "number"
}, },
"retained": { "retained": {
"title": "Retained",
"type": "boolean" "type": "boolean"
}, },
"payload": { "payload": {
"title": "Payload",
"type": "string" "type": "string"
} }
}, },
@@ -518,11 +524,9 @@
"type": "object", "type": "object",
"properties": { "properties": {
"subject": { "subject": {
"title": "Subject",
"type": "string" "type": "string"
}, },
"payload": { "payload": {
"title": "Payload",
"type": "string" "type": "string"
} }
}, },
@@ -545,18 +549,15 @@
"type": "object", "type": "object",
"properties": { "properties": {
"address": { "address": {
"title": "Address",
"type": "string" "type": "string"
}, },
"args": { "args": {
"title": "Arguments",
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "type": "string"
} }
}, },
"types": { "types": {
"title": "Argument Types",
"type": "string" "type": "string"
} }
}, },
@@ -593,22 +594,21 @@
}, },
{ {
"type": "object", "type": "object",
"title": "Router Output", "title": "Filter OSC Message",
"properties": { "properties": {
"type": { "type": {
"type": "string", "type": "string",
"const": "router.output" "const": "osc.message.filter"
}, },
"params": { "params": {
"type": "object", "type": "object",
"properties": { "properties": {
"module": { "address": {
"title": "Module ID", "type": "string"
"type": "string",
"description": "ID of module to send output to"
} }
}, },
"required": ["module"] "required": ["address"],
"additionalProperties": false
} }
}, },
"required": ["type", "params"], "required": ["type", "params"],
@@ -626,7 +626,6 @@
"type": "object", "type": "object",
"properties": { "properties": {
"expression": { "expression": {
"title": "Expression",
"type": "string" "type": "string"
} }
}, },
@@ -649,7 +648,6 @@
"type": "object", "type": "object",
"properties": { "properties": {
"program": { "program": {
"title": "Program",
"type": "string" "type": "string"
} }
}, },
@@ -672,16 +670,13 @@
"type": "object", "type": "object",
"properties": { "properties": {
"path": { "path": {
"title": "Path",
"type": "string" "type": "string"
}, },
"function": { "function": {
"title": "Function",
"type": "string", "type": "string",
"default": "process" "default": "process"
}, },
"enableWasi": { "enableWasi": {
"title": "Enable WASI",
"type": "boolean", "type": "boolean",
"default": false "default": false
} }
@@ -705,7 +700,6 @@
"type": "object", "type": "object",
"properties": { "properties": {
"template": { "template": {
"title": "Template",
"type": "string" "type": "string"
} }
}, },
@@ -751,7 +745,6 @@
"type": "object", "type": "object",
"properties": { "properties": {
"pattern": { "pattern": {
"title": "Pattern",
"type": "string" "type": "string"
} }
}, },
@@ -774,7 +767,6 @@
"type": "object", "type": "object",
"properties": { "properties": {
"separator": { "separator": {
"title": "Separator",
"type": "string" "type": "string"
} }
}, },
@@ -796,7 +788,6 @@
"type": "object", "type": "object",
"properties": { "properties": {
"name": { "name": {
"title": "Field Name",
"type": "string" "type": "string"
} }
}, },
@@ -818,7 +809,6 @@
"type": "object", "type": "object",
"properties": { "properties": {
"name": { "name": {
"title": "Method Name",
"type": "string" "type": "string"
} }
}, },
@@ -840,9 +830,7 @@
"type": "object", "type": "object",
"properties": { "properties": {
"duration": { "duration": {
"title": "Duration", "type": "integer"
"type": "integer",
"description": "Duration to sleep in milliseconds"
} }
}, },
"required": ["duration"] "required": ["duration"]

View File

@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://showbridge.io/routes.schema.json", "$id": "https://showbridge.io/routes.schema.json",
"title": "Routes", "title": "Routes",
"description": "route configurations", "description": "showbridge routes array",
"type": "array", "type": "array",
"items": { "items": {
"type": "object", "type": "object",
@@ -13,8 +13,12 @@
}, },
"processors": { "processors": {
"$ref": "https://showbridge.io/processors.schema.json" "$ref": "https://showbridge.io/processors.schema.json"
},
"output": {
"type": "string",
"minLength": 1
} }
}, },
"required": ["input"] "required": ["input", "output"]
} }
} }