69 Commits

Author SHA1 Message Date
Joel Wetzell
988437fccf add test for loading from registry for time modules 2026-02-09 18:19:17 -06:00
Joel Wetzell
dc19d18b81 add tests to ensure module id returns correctly 2026-02-09 18:17:42 -06:00
Joel Wetzell
86f1082159 add test for loading from registry for all modules 2026-02-09 18:11:08 -06:00
Joel Wetzell
a46054c427 fix net.udp.server type return value 2026-02-09 18:10:28 -06:00
Joel Wetzell
9dc4706fd8 add test for loading from registry for rest of processors 2026-02-09 17:42:16 -06:00
Joel Wetzell
f91cb9dbbf add basic tests for script.wasm 2026-02-09 15:07:18 -06:00
Joel Wetzell
10b5cbae6c add coverage badge 2026-02-09 15:06:04 -06:00
Joel Wetzell
a9f9f1ba2c Merge pull request #63 from jwetzell/feat/script-wasm-wasi
add option to enable WASI for script.wasm processor
2026-02-09 14:55:18 -06:00
Joel Wetzell
8e27dc81a2 add option to enable WASI for script.wasm processor 2026-02-09 14:52:41 -06:00
Joel Wetzell
5dc897c4b2 allow contents write permission 2026-02-09 14:49:44 -06:00
Joel Wetzell
7015a9d7a2 move tests to sub folders 2026-02-09 14:38:13 -06:00
Joel Wetzell
3c6d98638e add coverage report step to test workflow 2026-02-09 14:33:20 -06:00
Joel Wetzell
8ec8ad91a6 Merge pull request #58 from jwetzell/fix/sip-server-postwait
fix postwait on sip server for dtmf response
2026-02-09 12:05:44 -06:00
Joel Wetzell
f919017ca6 fix postwait on sip server for dtmf response 2026-02-09 12:03:54 -06:00
Joel Wetzell
66f170047a remove arm images for now 2026-02-08 22:02:33 -06:00
Joel Wetzell
7ff20e0136 add more tests for string.split 2026-02-08 21:50:31 -06:00
Joel Wetzell
b53494fddc add more error tests for string.filter 2026-02-08 21:49:30 -06:00
Joel Wetzell
3688510ded fix test names 2026-02-08 21:49:12 -06:00
Joel Wetzell
d0bdc44c30 add test for string.filter 2026-02-08 21:40:53 -06:00
Joel Wetzell
1c49548a4c test cleanup 2026-02-08 21:38:44 -06:00
Joel Wetzell
4ea6c8f11f test cleanup 2026-02-08 21:31:08 -06:00
Joel Wetzell
0ef512eb28 add registry test for json.decode 2026-02-08 21:23:21 -06:00
Joel Wetzell
ebbce3261c add registry test for json.encode 2026-02-08 21:21:28 -06:00
Joel Wetzell
79fa925823 clean up json.encode test 2026-02-08 21:21:14 -06:00
Joel Wetzell
659c95102b Merge pull request #57 from jwetzell/feat/json-decode-processor
add json.decode processor
2026-02-08 21:15:55 -06:00
Joel Wetzell
e06c4195c7 add json.decode processor 2026-02-08 21:13:34 -06:00
Joel Wetzell
28e3c99ad8 add test loading from registry for time.sleep 2026-02-08 15:56:33 -06:00
Joel Wetzell
7312bfec20 add error tests for random processors 2026-02-08 15:56:14 -06:00
Joel Wetzell
4bbf69e644 add test for artnet.packet.filter 2026-02-08 15:55:48 -06:00
Joel Wetzell
1ba51446ad fix error messages in test 2026-02-08 15:55:32 -06:00
Joel Wetzell
2266d6a9b8 Merge pull request #56 from jwetzell/feat/json-schema
add json schema definitions for config
2026-02-08 15:04:25 -06:00
Joel Wetzell
f1f340f963 add schema for rest of processors 2026-02-08 13:34:40 -06:00
Joel Wetzell
4ca989280c switch interval and timer to new namespace 2026-02-08 09:45:22 -06:00
Joel Wetzell
ef83e84e84 add schema for time.sleep processor 2026-02-08 09:45:22 -06:00
Joel Wetzell
b8ad912855 add JSON schema for config file 2026-02-08 09:45:22 -06:00
Joel Wetzell
f766d0e2a4 Merge pull request #55 from jwetzell/feat/router-reload
reload router config on SIGHUP
2026-02-08 09:44:49 -06:00
Joel Wetzell
725e8ad670 add mutex for router operations 2026-02-07 17:07:18 -06:00
Joel Wetzell
7fa3b6d33d TODO 2026-02-07 16:29:49 -06:00
Joel Wetzell
5e957d7d03 expose running config from router 2026-02-07 16:28:06 -06:00
Joel Wetzell
bd89df3da2 create app struct 2026-02-07 14:15:24 -06:00
Joel Wetzell
914f6aa833 mess around with being able to reload router 2026-02-07 13:42:55 -06:00
Joel Wetzell
2104d9f1a9 change to start/stop not run/stop 2026-02-07 12:51:01 -06:00
Joel Wetzell
457ff83c23 add more convenience methods for start/stopping modules on router 2026-02-07 12:48:27 -06:00
Joel Wetzell
37c7a23063 add debug log at beginning of running 2026-02-07 12:40:30 -06:00
Joel Wetzell
9ba3a88e8c add removeModule function to router 2026-02-07 09:53:48 -06:00
Joel Wetzell
33ecc94097 add Stop function to module 2026-02-07 09:53:38 -06:00
Joel Wetzell
8f5091cf9b load router from context in mock module 2026-02-05 20:20:00 -06:00
Joel Wetzell
b095419b6e switch module instances to map on id 2026-02-05 20:02:03 -06:00
Joel Wetzell
1e872ba0b9 fix RouteIO interface 2026-02-05 19:51:09 -06:00
Joel Wetzell
b959b88527 split out module and route creation in router and router output only does one module 2026-02-05 19:31:31 -06:00
Joel Wetzell
0dfbf30fa7 Merge pull request #54 from jwetzell/feat/opentelemetry
add option to export open telemetry tracing
2026-02-04 19:22:15 -06:00
Joel Wetzell
abff674a75 fix router test 2026-02-04 19:17:57 -06:00
Joel Wetzell
a920f34926 rework route and processing spans 2026-02-04 19:01:18 -06:00
Joel Wetzell
4dac1faae0 add basic opentelemetry 2026-02-04 19:01:18 -06:00
Joel Wetzell
ab5b40ee72 remove osc.message.transform 2026-02-04 18:39:16 -06:00
Joel Wetzell
5a29139e63 Merge pull request #53 from jwetzell/chore/testing
add more testing
2026-02-04 18:38:18 -06:00
Joel Wetzell
1587f84981 add test for uint.random processor 2026-02-04 18:30:51 -06:00
Joel Wetzell
0cb13a30f9 add test for int.random processor 2026-02-04 18:30:41 -06:00
Joel Wetzell
8212d15a70 add test for module with no id 2026-02-04 18:30:25 -06:00
Joel Wetzell
eee356225f Merge pull request #52 from jwetzell/fix/debug-log-payload-string
more compatible printing of payload in debug.log
2026-02-03 22:52:20 -06:00
Joel Wetzell
5cd02f9e3a more compatible printing of payload in debug.log 2026-02-03 22:50:30 -06:00
Joel Wetzell
100edab869 Merge pull request #51 from jwetzell/fix/no-default-framing-method
don't default framing method
2026-02-03 22:28:47 -06:00
Joel Wetzell
fd4c2cb59b make framing parameter required in net.tcp.client 2026-02-03 22:27:58 -06:00
Joel Wetzell
b6ee3c68f1 don't default framing method 2026-02-03 22:26:16 -06:00
Joel Wetzell
7e0c2b2560 Merge pull request #48 from jwetzell/dependabot/github_actions/docker/login-action-3.7.0
Bump docker/login-action from 3.6.0 to 3.7.0
2026-02-03 11:35:37 -06:00
Joel Wetzell
c9a3eb80bc Merge pull request #50 from jwetzell/dependabot/go_modules/github.com/emiago/diago-0.26.2
Bump github.com/emiago/diago from 0.25.0 to 0.26.2
2026-02-03 11:35:13 -06:00
dependabot[bot]
2a5feec07a Bump github.com/emiago/diago from 0.25.0 to 0.26.2
Bumps [github.com/emiago/diago](https://github.com/emiago/diago) from 0.25.0 to 0.26.2.
- [Release notes](https://github.com/emiago/diago/releases)
- [Commits](https://github.com/emiago/diago/compare/v0.25.0...v0.26.2)

---
updated-dependencies:
- dependency-name: github.com/emiago/diago
  dependency-version: 0.26.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-02 23:54:12 +00:00
dependabot[bot]
ced4bc839e Bump docker/login-action from 3.6.0 to 3.7.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 3.6.0 to 3.7.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](5e57cd1181...c94ce9fb46)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 3.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-02 23:53:47 +00:00
Joel Wetzell
ce6a9ff4a8 update labeler 2026-02-02 13:14:11 -06:00
104 changed files with 4656 additions and 471 deletions

9
.github/labeler.yml vendored
View File

@@ -14,11 +14,14 @@ processor:
- changed-files:
- any-glob-to-any-file: 'internal/processor/**'
routing:
router:
- changed-files:
- any-glob-to-any-file: 'router*'
route:
- changed-files:
- any-glob-to-any-file: 'internal/route/**'
- any-glob-to-any-file: 'router*'
cli:
- changed-files:
- any-glob-to-any-file: 'cmd/showbridge/**'

View File

@@ -43,7 +43,7 @@ jobs:
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Login to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -65,4 +65,4 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64,linux/arm/v7
platforms: linux/amd64

View File

@@ -17,7 +17,7 @@ on:
- 'go.sum'
permissions:
contents: read
contents: write
jobs:
test:
@@ -34,4 +34,13 @@ jobs:
run: go mod tidy
- name: run tests
run: go test ./...
run: go test ./...
- name: Update coverage report
uses: ncruces/go-coverage-report@v0.3.2
with:
report: true
chart: true
amend: true
if: github.event_name == 'push'
continue-on-error: true

View File

@@ -1,7 +1,9 @@
<div align="center">
# showbridge (go edition)
# showbridge (go edition)
[![Coverage](https://github.com/jwetzell/showbridge-go/wiki/coverage.svg)](https://raw.githack.com/wiki/jwetzell/showbridge-go/coverage.html)
Simple protocol router _/s_
</div>

View File

@@ -9,15 +9,23 @@ import (
"os/signal"
"slices"
"sync"
"syscall"
"github.com/jwetzell/showbridge-go"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/urfave/cli/v3"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.39.0"
"go.opentelemetry.io/otel/trace"
"sigs.k8s.io/yaml"
)
var (
version = "dev"
version = "dev"
sigHangup = make(chan os.Signal, 1)
)
func main() {
@@ -55,11 +63,18 @@ func main() {
return nil
},
},
&cli.BoolFlag{
Name: "trace",
Value: false,
Usage: "enable OpenTelemetry tracing",
},
},
Action: run,
}
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
signal.Notify(sigHangup, syscall.SIGHUP)
defer cancel()
err := cmd.Run(ctx, os.Args)
@@ -69,6 +84,16 @@ func main() {
}
type showbridgeApp struct {
ctx context.Context
configPath string
logger *slog.Logger
router *showbridge.Router
routerRunner *sync.WaitGroup
tracer trace.Tracer
routerMutex sync.Mutex
}
func readConfig(configPath string) (config.Config, error) {
cfg := config.Config{}
@@ -89,12 +114,7 @@ func readConfig(configPath string) (config.Config, error) {
func run(ctx context.Context, c *cli.Command) error {
configPath := c.String("config")
if configPath == "" {
return errors.New("config value cannot be empty")
}
config, err := readConfig(configPath)
if err != nil {
return err
return errors.New("config path cannot be empty")
}
logLevel := slog.LevelInfo
@@ -135,28 +155,117 @@ func run(ctx context.Context, c *cli.Command) error {
slog.SetDefault(slog.New(logHandler))
commandLogger := slog.Default().With("component", "cmd")
var tracer trace.Tracer
if c.Bool("trace") {
exporter, err := otlptracehttp.New(ctx)
if err != nil {
return fmt.Errorf("failed to create trace exporter: %w", err)
}
router, moduleErrors, routeErrors := showbridge.NewRouter(config)
tracerProvider := newTracerProvider(exporter)
otel.SetTracerProvider(tracerProvider)
defer tracerProvider.Shutdown(ctx)
tracer = tracerProvider.Tracer("showbridge")
} else {
tracer = otel.Tracer("showbridge")
}
showbridgeApp := &showbridgeApp{
ctx: ctx,
configPath: configPath,
logger: slog.Default().With("component", "cmd"),
routerRunner: &sync.WaitGroup{},
tracer: tracer,
}
router, err := showbridgeApp.getNewRouter()
if err != nil {
return fmt.Errorf("failed to initialize router: %w", err)
}
showbridgeApp.routerMutex.Lock()
showbridgeApp.router = router
showbridgeApp.routerRunner.Go(func() {
router.Start(context.Background())
})
showbridgeApp.routerMutex.Unlock()
go showbridgeApp.handleHangup()
<-showbridgeApp.ctx.Done()
showbridgeApp.logger.Debug("shutting down router")
showbridgeApp.router.Stop()
showbridgeApp.logger.Debug("waiting for router to exit")
showbridgeApp.routerRunner.Wait()
return nil
}
func (app *showbridgeApp) handleHangup() {
for {
select {
case <-sigHangup:
app.logger.Info("received SIGHUP, reloading configuration")
newRouter, err := app.getNewRouter()
if err != nil {
app.logger.Error("failed to reload configuration", "error", err)
continue
}
app.routerMutex.Lock()
app.router.Stop()
app.routerRunner.Wait()
app.router = newRouter
app.routerRunner.Go(func() {
app.router.Start(context.Background())
})
app.logger.Info("configuration reloaded successfully")
app.routerMutex.Unlock()
case <-app.ctx.Done():
return
}
}
}
func (app *showbridgeApp) getNewRouter() (*showbridge.Router, error) {
// TODO(jwetzell): what should happen when the config file is unchanged?
config, err := readConfig(app.configPath)
if err != nil {
return nil, err
}
router, moduleErrors, routeErrors := showbridge.NewRouter(config, app.tracer)
for _, moduleError := range moduleErrors {
commandLogger.Error("problem initializing module", "index", moduleError.Index, "error", moduleError.Error)
app.logger.Error("problem initializing module", "index", moduleError.Index, "error", moduleError.Error)
}
for _, routeError := range routeErrors {
commandLogger.Error("problem initializing route", "index", routeError.Index, "error", routeError.Error)
app.logger.Error("problem initializing route", "index", routeError.Index, "error", routeError.Error)
}
routerRunner := sync.WaitGroup{}
if moduleErrors != nil || routeErrors != nil {
return nil, fmt.Errorf("errors initializing modules or routes")
}
routerRunner.Go(func() {
router.Run(context.Background())
})
<-ctx.Done()
commandLogger.Debug("shutting down router")
router.Stop()
commandLogger.Debug("waiting for router to exit")
routerRunner.Wait()
return nil
return router, nil
}
func newTracerProvider(exp sdktrace.SpanExporter) *sdktrace.TracerProvider {
r, err := resource.Merge(
resource.Default(),
resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceName("showbridge"),
semconv.ServiceVersion(version),
),
)
if err != nil {
panic(err)
}
return sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exp),
sdktrace.WithResource(r),
)
}

24
go.mod
View File

@@ -4,8 +4,8 @@ go 1.25.5
require (
github.com/eclipse/paho.mqtt.golang v1.5.1
github.com/emiago/diago v0.25.0
github.com/emiago/sipgo v1.1.1
github.com/emiago/diago v0.26.2
github.com/emiago/sipgo v1.1.2
github.com/expr-lang/expr v1.17.7
github.com/extism/go-sdk v1.7.1
github.com/jwetzell/artnet-go v0.2.1
@@ -16,21 +16,31 @@ require (
github.com/urfave/cli/v3 v3.6.2
gitlab.com/gomidi/midi/v2 v2.3.18
go.bug.st/serial v1.6.4
go.opentelemetry.io/otel v1.40.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0
go.opentelemetry.io/otel/sdk v1.40.0
go.opentelemetry.io/otel/trace v1.40.0
modernc.org/quickjs v0.17.1
sigs.k8s.io/yaml v1.6.0
)
require (
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/creack/goselect v0.1.2 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect
github.com/emiago/dtls/v3 v3.0.0-20260122183559-8b8d23e359c0 // indirect
github.com/go-audio/riff v1.0.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect
github.com/gobwas/ws v1.4.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.21.0 // indirect
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca // indirect
github.com/icholy/digest v1.1.0 // indirect
github.com/klauspost/compress v1.18.0 // indirect
@@ -44,17 +54,25 @@ require (
github.com/pion/rtp v1.8.26 // indirect
github.com/pion/srtp/v3 v3.0.9 // indirect
github.com/pion/transport/v3 v3.1.1 // indirect
github.com/pion/transport/v4 v4.0.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 // indirect
github.com/tetratelabs/wazero v1.9.0 // indirect
github.com/zaf/g711 v1.4.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/crypto v0.42.0 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.29.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
google.golang.org/grpc v1.65.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302 // indirect
modernc.org/libc v1.67.1 // indirect

57
go.sum
View File

@@ -1,3 +1,7 @@
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -8,16 +12,23 @@ github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a h1:UwSIFv5g
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q=
github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
github.com/emiago/diago v0.25.0 h1:YkjHahAMyIhvk5TKTMYoFJvsbNaJhyMBAiGggc97HDc=
github.com/emiago/diago v0.25.0/go.mod h1:HQNLzmwucATviInW5OqpnpqFjtz2jAtxKKWx8Nh98wo=
github.com/emiago/sipgo v1.1.1 h1:egwB7o9b3QpeTbqRFT9ECOYcWT/rw2UVTWA1qYG1HBs=
github.com/emiago/sipgo v1.1.1/go.mod h1:DuwAxBZhKMqIzQFPGZb1MVAGU6Wuxj64oTOhd5dx/FY=
github.com/emiago/diago v0.26.2 h1:3QL03V0drX96eIBFBpfueNcywydRgYqffKihluGL0gA=
github.com/emiago/diago v0.26.2/go.mod h1:jZ+7EnKcmgqKnLjCHPqfbP4Y/9Q/JLSLxMflDrp2J1M=
github.com/emiago/dtls/v3 v3.0.0-20260122183559-8b8d23e359c0 h1:o4LxpUnZ1zxiQ+Qjc9kLwXcjz31NGAHmnZ7xoJto3VM=
github.com/emiago/dtls/v3 v3.0.0-20260122183559-8b8d23e359c0/go.mod h1:ydcZ977eS1I6uOWodzMuw30BwvNAzT9su/xcNYSJqjA=
github.com/emiago/sipgo v1.1.2 h1:JvLqEvqNSQm2mBX40qZ7O0WC3Ee67Z0UrfmBI7y6Beo=
github.com/emiago/sipgo v1.1.2/go.mod h1:DuwAxBZhKMqIzQFPGZb1MVAGU6Wuxj64oTOhd5dx/FY=
github.com/expr-lang/expr v1.17.7 h1:Q0xY/e/2aCIp8g9s/LGvMDCC5PxYlvHgDZRQ4y16JX8=
github.com/expr-lang/expr v1.17.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/extism/go-sdk v1.7.1 h1:lWJos6uY+tRFdlIHR+SJjwFDApY7OypS/2nMhiVQ9Sw=
github.com/extism/go-sdk v1.7.1/go.mod h1:IT+Xdg5AZM9hVtpFUA+uZCJMge/hbvshl8bwzLtFyKA=
github.com/go-audio/riff v1.0.0 h1:d8iCGbDvox9BfLagY94fBynxSPHO80LmZCaOsmKxokA=
github.com/go-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
@@ -32,6 +43,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.21.0 h1:CWyXh/jylQWp2dtiV33mY4iSSp6yf4lmn+c7/tN+ObI=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.21.0/go.mod h1:nCLIt0w3Ept2NwF8ThLmrppXsfT07oC8k0XNDxd8sVU=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca h1:T54Ema1DU8ngI+aef9ZhAhNGQhcRTrWxVeG07F+c/Rw=
@@ -74,12 +87,14 @@ github.com/pion/srtp/v3 v3.0.9 h1:lRGF4G61xxj+m/YluB3ZnBpiALSri2lTzba0kGZMrQY=
github.com/pion/srtp/v3 v3.0.9/go.mod h1:E+AuWd7Ug2Fp5u38MKnhduvpVkveXJX6J4Lq4rxUYt8=
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q=
@@ -94,8 +109,26 @@ gitlab.com/gomidi/midi/v2 v2.3.18 h1:sj2fOhtvOe+zI8YJe8qTxLw5zv0ntULLUDwcFOaZQbI
gitlab.com/gomidi/midi/v2 v2.3.18/go.mod h1:jDpP4O4skYi+7iVwt6Zyp18bd2M4hkjtMuw2cmgKgfw=
go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A=
go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE=
@@ -111,10 +144,18 @@ golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8=
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -17,6 +17,7 @@ type HTTPClient struct {
client *http.Client
router route.RouteIO
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
@@ -37,14 +38,17 @@ func (hc *HTTPClient) Type() string {
return hc.config.Type
}
func (hc *HTTPClient) Run(ctx context.Context) error {
func (hc *HTTPClient) Start(ctx context.Context) error {
hc.logger.Debug("running")
router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
if !ok {
return errors.New("http.client unable to get router from context")
}
hc.router = router
hc.ctx = ctx
moduleContext, cancel := context.WithCancel(ctx)
hc.ctx = moduleContext
hc.cancel = cancel
hc.client = &http.Client{
Timeout: 10 * time.Second,
@@ -79,3 +83,7 @@ func (hc *HTTPClient) Output(ctx context.Context, payload any) error {
return nil
}
func (hc *HTTPClient) Stop() {
hc.cancel()
}

View File

@@ -19,13 +19,14 @@ type HTTPServer struct {
ctx context.Context
router route.RouteIO
logger *slog.Logger
cancel context.CancelFunc
}
type ResponseIOError struct {
Index int `json:"index"`
OutputErrors []string `json:"outputErrors"`
ProcessError *string `json:"processError"`
InputError *string `json:"inputError"`
Index int `json:"index"`
OutputError *string `json:"outputError"`
ProcessError *string `json:"processError"`
InputError *string `json:"inputError"`
}
type IOResponseData struct {
@@ -113,14 +114,9 @@ func (hs *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
errorToAdd.ProcessError = &errorMsg
}
if responseIOError.OutputErrors != nil {
outputErrorMsgs := []string{}
for _, outputError := range responseIOError.OutputErrors {
outputErrorMsgs = append(outputErrorMsgs, outputError.Error())
}
errorToAdd.OutputErrors = outputErrorMsgs
if responseIOError.OutputError != nil {
errorMsg := responseIOError.OutputError.Error()
errorToAdd.OutputError = &errorMsg
}
response.IOErrors = append(response.IOErrors, errorToAdd)
@@ -151,14 +147,17 @@ func (hs *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
func (hs *HTTPServer) Run(ctx context.Context) error {
func (hs *HTTPServer) Start(ctx context.Context) error {
hs.logger.Debug("running")
router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
if !ok {
return errors.New("http.server unable to get router from context")
}
hs.router = router
hs.ctx = ctx
moduleContext, cancel := context.WithCancel(ctx)
hs.ctx = moduleContext
hs.cancel = cancel
httpServer := &http.Server{
Addr: fmt.Sprintf(":%d", hs.Port),
@@ -204,3 +203,7 @@ func (hs *HTTPServer) Output(ctx context.Context, payload any) error {
responseWriter.Write(payloadResponse.Body)
return nil
}
func (hs *HTTPServer) Stop() {
hs.cancel()
}

View File

@@ -21,6 +21,7 @@ type MIDIInput struct {
Port string
SendFunc func(midi.Message) error
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
@@ -53,7 +54,8 @@ func (mi *MIDIInput) Type() string {
return mi.config.Type
}
func (mi *MIDIInput) Run(ctx context.Context) error {
func (mi *MIDIInput) Start(ctx context.Context) error {
mi.logger.Debug("running")
defer midi.CloseDriver()
router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
@@ -61,7 +63,9 @@ func (mi *MIDIInput) Run(ctx context.Context) error {
return errors.New("midi.input unable to get router from context")
}
mi.router = router
mi.ctx = ctx
moduleContext, cancel := context.WithCancel(ctx)
mi.ctx = moduleContext
mi.cancel = cancel
in, err := midi.FindInPort(mi.Port)
if err != nil {
@@ -88,3 +92,7 @@ func (mi *MIDIInput) Run(ctx context.Context) error {
func (mi *MIDIInput) Output(ctx context.Context, payload any) error {
return errors.New("midi.input output is not implemented")
}
func (mi *MIDIInput) Stop() {
mi.cancel()
}

View File

@@ -21,6 +21,7 @@ type MIDIOutput struct {
Port string
SendFunc func(midi.Message) error
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
@@ -54,7 +55,8 @@ func (mo *MIDIOutput) Type() string {
return mo.config.Type
}
func (mo *MIDIOutput) Run(ctx context.Context) error {
func (mo *MIDIOutput) Start(ctx context.Context) error {
mo.logger.Debug("running")
defer midi.CloseDriver()
router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
@@ -62,7 +64,9 @@ func (mo *MIDIOutput) Run(ctx context.Context) error {
return errors.New("midi.output unable to get router from context")
}
mo.router = router
mo.ctx = ctx
moduleContext, cancel := context.WithCancel(ctx)
mo.ctx = moduleContext
mo.cancel = cancel
out, err := midi.FindOutPort(mo.Port)
@@ -95,3 +99,7 @@ func (mo *MIDIOutput) Output(ctx context.Context, payload any) error {
return mo.SendFunc(payloadMessage)
}
func (mo *MIDIOutput) Stop() {
mo.cancel()
}

View File

@@ -18,7 +18,8 @@ type ModuleError struct {
type Module interface {
Id() string
Type() string
Run(context.Context) error
Start(context.Context) error
Stop()
Output(context.Context, any) error
}

View File

@@ -19,6 +19,7 @@ type MQTTClient struct {
Topic string
client mqtt.Client
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
@@ -75,14 +76,17 @@ func (mc *MQTTClient) Type() string {
return mc.config.Type
}
func (mc *MQTTClient) Run(ctx context.Context) error {
func (mc *MQTTClient) Start(ctx context.Context) error {
mc.logger.Debug("running")
router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
if !ok {
return errors.New("mqtt.client unable to get router from context")
}
mc.router = router
mc.ctx = ctx
moduleContext, cancel := context.WithCancel(ctx)
mc.ctx = moduleContext
mc.cancel = cancel
opts := mqtt.NewClientOptions()
opts.AddBroker(mc.Broker)
@@ -98,6 +102,7 @@ func (mc *MQTTClient) Run(ctx context.Context) error {
}
mc.client = mqtt.NewClient(opts)
defer mc.client.Disconnect(250)
token := mc.client.Connect()
@@ -133,3 +138,7 @@ func (mc *MQTTClient) Output(ctx context.Context, payload any) error {
return token.Error()
}
func (mc *MQTTClient) Stop() {
mc.cancel()
}

View File

@@ -19,6 +19,7 @@ type NATSClient struct {
Subject string
client *nats.Conn
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
@@ -63,7 +64,8 @@ func (nc *NATSClient) Type() string {
return nc.config.Type
}
func (nc *NATSClient) Run(ctx context.Context) error {
func (nc *NATSClient) Start(ctx context.Context) error {
nc.logger.Debug("running")
router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
if !ok {
@@ -71,7 +73,9 @@ func (nc *NATSClient) Run(ctx context.Context) error {
}
nc.router = router
nc.ctx = ctx
moduleContext, cancel := context.WithCancel(ctx)
nc.ctx = moduleContext
nc.cancel = cancel
client, err := nats.Connect(nc.URL, nats.RetryOnFailedConnect(true))
@@ -121,3 +125,7 @@ func (nc *NATSClient) Output(ctx context.Context, payload any) error {
return err
}
func (nc *NATSClient) Stop() {
nc.cancel()
}

View File

@@ -20,6 +20,7 @@ type PSNClient struct {
router route.RouteIO
decoder *psn.Decoder
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
@@ -40,14 +41,17 @@ func (pc *PSNClient) Type() string {
return pc.config.Type
}
func (pc *PSNClient) Run(ctx context.Context) error {
func (pc *PSNClient) Start(ctx context.Context) error {
pc.logger.Debug("running")
router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
if !ok {
return errors.New("psn.client unable to get router from context")
}
pc.router = router
pc.ctx = ctx
moduleContext, cancel := context.WithCancel(ctx)
pc.ctx = moduleContext
pc.cancel = cancel
addr, err := net.ResolveUDPAddr("udp", "236.10.10.10:56565")
if err != nil {
@@ -104,3 +108,7 @@ func (pc *PSNClient) Run(ctx context.Context) error {
func (pc *PSNClient) Output(ctx context.Context, payload any) error {
return fmt.Errorf("psn.client output is not implemented")
}
func (pc *PSNClient) Stop() {
pc.cancel()
}

View File

@@ -24,6 +24,7 @@ type SerialClient struct {
Mode *serial.Mode
port serial.Port
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
@@ -43,20 +44,19 @@ func init() {
return nil, errors.New("serial.client port must be a string")
}
framingMethod := "RAW"
framingMethod, ok := params["framing"]
framingMethodRaw, ok := params["framing"]
if ok {
framingMethodString, ok := framingMethodRaw.(string)
if !ok {
return nil, errors.New("serial.client framing method must be a string")
}
framingMethod = framingMethodString
if !ok {
return nil, errors.New("serial.client requires a framing parameter")
}
framer := framer.GetFramer(framingMethod)
framingMethodString, ok := framingMethod.(string)
if !ok {
return nil, errors.New("serial.client framing method must be a string")
}
framer := framer.GetFramer(framingMethodString)
if framer == nil {
return nil, fmt.Errorf("serial.client unknown framing method: %s", framingMethod)
@@ -101,7 +101,8 @@ func (sc *SerialClient) SetupPort() error {
return nil
}
func (sc *SerialClient) Run(ctx context.Context) error {
func (sc *SerialClient) Start(ctx context.Context) error {
sc.logger.Debug("running")
router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
if !ok {
@@ -109,7 +110,9 @@ func (sc *SerialClient) Run(ctx context.Context) error {
}
sc.router = router
sc.ctx = ctx
moduleContext, cancel := context.WithCancel(ctx)
sc.ctx = moduleContext
sc.cancel = cancel
// TODO(jwetzell): shutdown with router.Context properly
go func() {
@@ -181,3 +184,7 @@ func (sc *SerialClient) Output(ctx context.Context, payload any) error {
_, err := sc.port.Write(sc.Framer.Encode(payloadBytes))
return err
}
func (sc *SerialClient) Stop() {
sc.cancel()
}

View File

@@ -29,6 +29,7 @@ type SIPCallServer struct {
UserAgent string
dg *diago.Diago
logger *slog.Logger
cancel context.CancelFunc
}
type SIPCallMessage struct {
@@ -111,14 +112,17 @@ func (scs *SIPCallServer) Type() string {
return scs.config.Type
}
func (scs *SIPCallServer) Run(ctx context.Context) error {
func (scs *SIPCallServer) Start(ctx context.Context) error {
scs.logger.Debug("running")
router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
if !ok {
return errors.New("sip.call.server unable to get router from context")
}
scs.router = router
scs.ctx = ctx
moduleContext, cancel := context.WithCancel(ctx)
scs.ctx = moduleContext
scs.cancel = cancel
diagoLogger := slog.New(slog.NewJSONHandler(io.Discard, nil))
@@ -187,7 +191,6 @@ func (scs *SIPCallServer) Output(ctx context.Context, payload any) error {
if ok {
dtmfWriter := call.inDialog.AudioWriterDTMF()
time.Sleep(time.Millisecond * time.Duration(payloadDTMFResponse.PreWait))
for i, dtmfRune := range payloadDTMFResponse.Digits {
err := dtmfWriter.WriteDTMF(dtmfRune)
@@ -196,7 +199,7 @@ func (scs *SIPCallServer) Output(ctx context.Context, payload any) error {
return fmt.Errorf("sip.dtmf.server error output dtmf digit at index %d", i)
}
}
time.Sleep(time.Millisecond * time.Duration(payloadDTMFResponse.PreWait))
time.Sleep(time.Millisecond * time.Duration(payloadDTMFResponse.PostWait))
return nil
}
@@ -228,3 +231,7 @@ func (scs *SIPCallServer) Output(ctx context.Context, payload any) error {
}
return errors.New("sip.dtmf.server can only output SipDTMFResponse or SipAudioFileResponse")
}
func (scs *SIPCallServer) Stop() {
scs.cancel()
}

View File

@@ -29,6 +29,7 @@ type SIPDTMFServer struct {
Transport string
Separator string
logger *slog.Logger
cancel context.CancelFunc
}
type SIPDTMFMessage struct {
@@ -113,14 +114,17 @@ func (sds *SIPDTMFServer) Type() string {
return sds.config.Type
}
func (sds *SIPDTMFServer) Run(ctx context.Context) error {
func (sds *SIPDTMFServer) Start(ctx context.Context) error {
sds.logger.Debug("running")
router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
if !ok {
return errors.New("sip.dtmf.server unable to get router from context")
}
sds.router = router
sds.ctx = ctx
moduleContext, cancel := context.WithCancel(ctx)
sds.ctx = moduleContext
sds.cancel = cancel
diagoLogger := slog.New(slog.NewJSONHandler(io.Discard, nil))
@@ -210,7 +214,7 @@ func (sds *SIPDTMFServer) Output(ctx context.Context, payload any) error {
return fmt.Errorf("sip.dtmf.server error output dtmf digit at index %d", i)
}
}
time.Sleep(time.Millisecond * time.Duration(payloadDTMFResponse.PreWait))
time.Sleep(time.Millisecond * time.Duration(payloadDTMFResponse.PostWait))
return nil
}
@@ -243,3 +247,7 @@ func (sds *SIPDTMFServer) Output(ctx context.Context, payload any) error {
return errors.New("sip.dtmf.server can only output SipDTMFResponse or SipAudioFileResponse")
}
func (sds *SIPDTMFServer) Stop() {
sds.cancel()
}

View File

@@ -21,6 +21,7 @@ type TCPClient struct {
router route.RouteIO
Addr *net.TCPAddr
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
@@ -56,20 +57,19 @@ func init() {
return nil, err
}
framingMethod := "RAW"
framingMethod, ok := params["framing"]
framingMethodRaw, ok := params["framing"]
if ok {
framingMethodString, ok := framingMethodRaw.(string)
if !ok {
return nil, errors.New("net.tcp.client framing method must be a string")
}
framingMethod = framingMethodString
if !ok {
return nil, errors.New("net.tcp.client requires a framing parameter")
}
framer := framer.GetFramer(framingMethod)
framingMethodString, ok := framingMethod.(string)
if !ok {
return nil, errors.New("net.tcp.client framing method must be a string")
}
framer := framer.GetFramer(framingMethodString)
if framer == nil {
return nil, fmt.Errorf("net.tcp.client unknown framing method: %s", framingMethod)
@@ -87,15 +87,17 @@ func (tc *TCPClient) Type() string {
return tc.config.Type
}
func (tc *TCPClient) Run(ctx context.Context) error {
func (tc *TCPClient) Start(ctx context.Context) error {
tc.logger.Debug("running")
router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
if !ok {
return errors.New("net.tcp.client unable to get router from context")
}
tc.router = router
tc.ctx = ctx
moduleContext, cancel := context.WithCancel(ctx)
tc.ctx = moduleContext
tc.cancel = cancel
// TODO(jwetzell): shutdown with router.Context properly
go func() {
@@ -177,3 +179,7 @@ func (tc *TCPClient) Output(ctx context.Context, payload any) error {
_, err := tc.conn.Write(tc.framer.Encode(payloadBytes))
return err
}
func (tc *TCPClient) Stop() {
tc.cancel()
}

View File

@@ -27,6 +27,7 @@ type TCPServer struct {
connections []*net.TCPConn
connectionsMu sync.RWMutex
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
@@ -45,20 +46,19 @@ func init() {
return nil, errors.New("net.tcp.server port must be a number")
}
framingMethod := "RAW"
framingMethod, ok := params["framing"]
framingMethodRaw, ok := params["framing"]
if ok {
framingMethodString, ok := framingMethodRaw.(string)
if !ok {
return nil, errors.New("net.tcp.server framing method must be a string")
}
framingMethod = framingMethodString
if !ok {
return nil, errors.New("net.tcp.server requires a framing parameter")
}
framer := framer.GetFramer(framingMethod)
framingMethodString, ok := framingMethod.(string)
if !ok {
return nil, errors.New("net.tcp.server framing method must be a string")
}
framer := framer.GetFramer(framingMethodString)
if framer == nil {
return nil, fmt.Errorf("net.tcp.server unknown framing method: %s", framingMethod)
@@ -106,6 +106,15 @@ ClientRead:
for {
select {
case <-ts.quit:
client.Close()
ts.connectionsMu.Lock()
for i := 0; i < len(ts.connections); i++ {
if ts.connections[i] == client {
ts.connections = slices.Delete(ts.connections, i, i+1)
break
}
}
ts.connectionsMu.Unlock()
return
default:
client.SetDeadline(time.Now().Add(time.Millisecond * 200))
@@ -159,15 +168,17 @@ ClientRead:
}
}
func (ts *TCPServer) Run(ctx context.Context) error {
func (ts *TCPServer) Start(ctx context.Context) error {
ts.logger.Debug("running")
router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
if !ok {
return errors.New("net.tcp.server unable to get router from context")
}
ts.router = router
ts.ctx = ctx
moduleContext, cancel := context.WithCancel(ctx)
ts.ctx = moduleContext
ts.cancel = cancel
listener, err := net.ListenTCP("tcp", ts.Addr)
if err != nil {
@@ -227,3 +238,8 @@ func (ts *TCPServer) Output(ctx context.Context, payload any) error {
}
return fmt.Errorf("net.tcp.server error during output: %s", errorString)
}
func (ts *TCPServer) Stop() {
ts.cancel()
ts.wg.Wait()
}

View File

@@ -0,0 +1,32 @@
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())
}
}

View File

@@ -0,0 +1,35 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestHTTPServerFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["http.server"]
if !ok {
t.Fatalf("http.server module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "http.server",
Params: map[string]any{
"port": 3000.0,
},
})
if err != nil {
t.Fatalf("failed to create http.server module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("http.server module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "http.server" {
t.Fatalf("http.server module has wrong type: %s", moduleInstance.Type())
}
}

View File

@@ -0,0 +1,35 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestMIDIInputFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["midi.input"]
if !ok {
t.Fatalf("midi.input module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "midi.input",
Params: map[string]any{
"port": "test",
},
})
if err != nil {
t.Fatalf("failed to create midi.input module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("midi.input module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "midi.input" {
t.Fatalf("midi.input module has wrong type: %s", moduleInstance.Type())
}
}

View File

@@ -0,0 +1,35 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestMIDIOutputFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["midi.output"]
if !ok {
t.Fatalf("midi.output module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "midi.output",
Params: map[string]any{
"port": "test",
},
})
if err != nil {
t.Fatalf("failed to create midi.output module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("midi.output module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "midi.output" {
t.Fatalf("midi.output module has wrong type: %s", moduleInstance.Type())
}
}

View File

@@ -0,0 +1,37 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestMQTTClientFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["mqtt.client"]
if !ok {
t.Fatalf("mqtt.client module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "mqtt.client",
Params: map[string]any{
"broker": "mqtt://localhost:1883",
"topic": "test/topic",
"clientId": "test",
},
})
if err != nil {
t.Fatalf("failed to create mqtt.client module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("mqtt.client module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "mqtt.client" {
t.Fatalf("mqtt.client module has wrong type: %s", moduleInstance.Type())
}
}

View File

@@ -0,0 +1,36 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestNATSClientFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["nats.client"]
if !ok {
t.Fatalf("nats.client module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "nats.client",
Params: map[string]any{
"url": "nats://127.0.0.1:4222",
"subject": "test",
},
})
if err != nil {
t.Fatalf("failed to create nats.client module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("nats.client module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "nats.client" {
t.Fatalf("nats.client module has wrong type: %s", moduleInstance.Type())
}
}

View File

@@ -0,0 +1,32 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestPSNClientFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["psn.client"]
if !ok {
t.Fatalf("psn.client module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "psn.client",
})
if err != nil {
t.Fatalf("failed to create psn.client module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("psn.client module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "psn.client" {
t.Fatalf("psn.client module has wrong type: %s", moduleInstance.Type())
}
}

View File

@@ -0,0 +1,37 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestSerialClientFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["serial.client"]
if !ok {
t.Fatalf("serial.client module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "serial.client",
Params: map[string]any{
"port": "/dev/ttyUSB0",
"framing": "LF",
"baudRate": 9600.0,
},
})
if err != nil {
t.Fatalf("failed to create serial.client module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("serial.client module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "serial.client" {
t.Fatalf("serial.client module has wrong type: %s", moduleInstance.Type())
}
}

View File

@@ -0,0 +1,32 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestSIPCallServerFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["sip.call.server"]
if !ok {
t.Fatalf("sip.call.server module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "sip.call.server",
})
if err != nil {
t.Fatalf("failed to create sip.call.server module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("sip.call.server module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "sip.call.server" {
t.Fatalf("sip.call.server module has wrong type: %s", moduleInstance.Type())
}
}

View File

@@ -0,0 +1,35 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestSIPDTMFServerFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["sip.dtmf.server"]
if !ok {
t.Fatalf("sip.dtmf.server module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "sip.dtmf.server",
Params: map[string]any{
"separator": "#",
},
})
if err != nil {
t.Fatalf("failed to create sip.dtmf.server module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("sip.dtmf.server module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "sip.dtmf.server" {
t.Fatalf("sip.dtmf.server module has wrong type: %s", moduleInstance.Type())
}
}

View File

@@ -0,0 +1,37 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestTCPClientFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["net.tcp.client"]
if !ok {
t.Fatalf("net.tcp.client module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "net.tcp.client",
Params: map[string]any{
"host": "localhost",
"port": 8000.0,
"framing": "LF",
},
})
if err != nil {
t.Fatalf("failed to create net.tcp.client module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("net.tcp.client module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "net.tcp.client" {
t.Fatalf("net.tcp.client module has wrong type: %s", moduleInstance.Type())
}
}

View File

@@ -0,0 +1,36 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestTCPServerFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["net.tcp.server"]
if !ok {
t.Fatalf("net.tcp.server module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "net.tcp.server",
Params: map[string]any{
"port": 8000.0,
"framing": "LF",
},
})
if err != nil {
t.Fatalf("failed to create net.tcp.server module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("net.tcp.server module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "net.tcp.server" {
t.Fatalf("net.tcp.server module has wrong type: %s", moduleInstance.Type())
}
}

View File

@@ -0,0 +1,35 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestTimeIntervalFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["time.interval"]
if !ok {
t.Fatalf("time.interval module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "time.interval",
Params: map[string]any{
"duration": 1000.0,
},
})
if err != nil {
t.Fatalf("failed to create time.interval module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("time.interval module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "time.interval" {
t.Fatalf("time.interval module has wrong type: %s", moduleInstance.Type())
}
}

View File

@@ -0,0 +1,35 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestTimeTimerFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["time.timer"]
if !ok {
t.Fatalf("time.timer module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "time.timer",
Params: map[string]any{
"duration": 1000.0,
},
})
if err != nil {
t.Fatalf("failed to create time.timer module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("time.timer module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "time.timer" {
t.Fatalf("time.timer module has wrong type: %s", moduleInstance.Type())
}
}

View File

@@ -0,0 +1,37 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestUDPClientFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["net.udp.client"]
if !ok {
t.Fatalf("udp.client module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "net.udp.client",
Params: map[string]any{
"host": "localhost",
"port": 8000.0,
"framing": "LF",
},
})
if err != nil {
t.Fatalf("failed to create net.udp.client module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("net.udp.client module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "net.udp.client" {
t.Fatalf("net.udp.client module has wrong type: %s", moduleInstance.Type())
}
}

View File

@@ -0,0 +1,36 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestUDPMulticastFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["net.udp.multicast"]
if !ok {
t.Fatalf("udp.multicast module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "net.udp.multicast",
Params: map[string]any{
"ip": "236.10.10.10",
"port": 56565.0,
},
})
if err != nil {
t.Fatalf("failed to create net.udp.multicast module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("net.udp.multicast module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "net.udp.multicast" {
t.Fatalf("net.udp.multicast module has wrong type: %s", moduleInstance.Type())
}
}

View File

@@ -0,0 +1,35 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
)
func TestUDPServerFromRegistry(t *testing.T) {
registration, ok := module.ModuleRegistry["net.udp.server"]
if !ok {
t.Fatalf("net.udp.server module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "net.udp.server",
Params: map[string]any{
"port": 8000.0,
},
})
if err != nil {
t.Fatalf("failed to create udp.server module: %s", err)
}
if moduleInstance.Id() != "test" {
t.Fatalf("udp.server module has wrong id: %s", moduleInstance.Id())
}
if moduleInstance.Type() != "net.udp.server" {
t.Fatalf("net.udp.server module has wrong type: %s", moduleInstance.Type())
}
}

View File

@@ -17,6 +17,7 @@ type TimeInterval struct {
router route.RouteIO
ticker *time.Ticker
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
@@ -48,15 +49,17 @@ func (i *TimeInterval) Type() string {
return i.config.Type
}
func (i *TimeInterval) Run(ctx context.Context) error {
func (i *TimeInterval) Start(ctx context.Context) error {
i.logger.Debug("running")
router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
if !ok {
return errors.New("time.interval unable to get router from context")
}
i.router = router
i.ctx = ctx
moduleContext, cancel := context.WithCancel(ctx)
i.ctx = moduleContext
i.cancel = cancel
ticker := time.NewTicker(time.Millisecond * time.Duration(i.Duration))
i.ticker = ticker
@@ -80,3 +83,7 @@ func (i *TimeInterval) Output(ctx context.Context, payload any) error {
i.ticker.Reset(time.Millisecond * time.Duration(i.Duration))
return nil
}
func (i *TimeInterval) Stop() {
i.cancel()
}

View File

@@ -17,6 +17,7 @@ type TimeTimer struct {
router route.RouteIO
timer *time.Timer
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
@@ -49,15 +50,17 @@ func (t *TimeTimer) Type() string {
return t.config.Type
}
func (t *TimeTimer) Run(ctx context.Context) error {
func (t *TimeTimer) Start(ctx context.Context) error {
t.logger.Debug("running")
router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
if !ok {
return errors.New("net.tcp.client unable to get router from context")
}
t.router = router
t.ctx = ctx
moduleContext, cancel := context.WithCancel(ctx)
t.ctx = moduleContext
t.cancel = cancel
t.timer = time.NewTimer(time.Millisecond * time.Duration(t.Duration))
defer t.timer.Stop()
@@ -79,3 +82,7 @@ func (t *TimeTimer) Output(ctx context.Context, payload any) error {
t.timer.Reset(time.Millisecond * time.Duration(t.Duration))
return nil
}
func (t *TimeTimer) Stop() {
t.cancel()
}

View File

@@ -19,6 +19,7 @@ type UDPClient struct {
ctx context.Context
router route.RouteIO
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
@@ -72,15 +73,17 @@ func (uc *UDPClient) SetupConn() error {
return err
}
func (uc *UDPClient) Run(ctx context.Context) error {
func (uc *UDPClient) Start(ctx context.Context) error {
uc.logger.Debug("running")
router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
if !ok {
return errors.New("net.udp.client unable to get router from context")
}
uc.router = router
uc.ctx = ctx
moduleContext, cancel := context.WithCancel(ctx)
uc.ctx = moduleContext
uc.cancel = cancel
err := uc.SetupConn()
if err != nil {
@@ -112,3 +115,7 @@ func (uc *UDPClient) Output(ctx context.Context, payload any) error {
}
return nil
}
func (uc *UDPClient) Stop() {
uc.cancel()
}

View File

@@ -19,6 +19,7 @@ type UDPMulticast struct {
router route.RouteIO
Addr *net.UDPAddr
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
@@ -66,15 +67,17 @@ func (um *UDPMulticast) Type() string {
return um.config.Type
}
func (um *UDPMulticast) Run(ctx context.Context) error {
func (um *UDPMulticast) Start(ctx context.Context) error {
um.logger.Debug("running")
router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
if !ok {
return errors.New("net.udp.multicast unable to get router from context")
}
um.router = router
um.ctx = ctx
moduleContext, cancel := context.WithCancel(ctx)
um.ctx = moduleContext
um.cancel = cancel
client, err := net.ListenMulticastUDP("udp", nil, um.Addr)
if err != nil {
@@ -130,3 +133,7 @@ func (um *UDPMulticast) Output(ctx context.Context, payload any) error {
_, err := um.conn.Write(payloadBytes)
return err
}
func (um *UDPMulticast) Stop() {
um.cancel()
}

View File

@@ -20,6 +20,7 @@ type UDPServer struct {
ctx context.Context
router route.RouteIO
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
@@ -77,18 +78,20 @@ func (us *UDPServer) Id() string {
}
func (us *UDPServer) Type() string {
return us.config.Id
return us.config.Type
}
func (us *UDPServer) Run(ctx context.Context) error {
func (us *UDPServer) Start(ctx context.Context) error {
us.logger.Debug("running")
router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
if !ok {
return errors.New("net.udp.server unable to get router from context")
}
us.router = router
us.ctx = ctx
moduleContext, cancel := context.WithCancel(ctx)
us.ctx = moduleContext
us.cancel = cancel
listener, err := net.ListenUDP("udp", us.Addr)
if err != nil {
@@ -129,3 +132,7 @@ func (us *UDPServer) Run(ctx context.Context) error {
func (us *UDPServer) Output(ctx context.Context, payload any) error {
return errors.New("net.udp.server output is not implemented")
}
func (us *UDPServer) Stop() {
us.cancel()
}

View File

@@ -14,7 +14,9 @@ type DebugLog struct {
}
func (dl *DebugLog) Process(ctx context.Context, payload any) (any, error) {
dl.logger.Debug("", "payload", payload, "payloadType", fmt.Sprintf("%T", payload))
payloadString := fmt.Sprintf("%+v", payload)
payloadType := fmt.Sprintf("%T", payload)
dl.logger.Debug("", "payload", payloadString, "payloadType", payloadType)
return payload, nil
}

View File

@@ -0,0 +1,44 @@
package processor
import (
"context"
"encoding/json"
"errors"
"github.com/jwetzell/showbridge-go/internal/config"
)
type JsonDecode struct {
config config.ProcessorConfig
}
func (jd *JsonDecode) Process(ctx context.Context, payload any) (any, error) {
payloadString, ok := payload.(string)
if !ok {
return nil, errors.New("json.decode processor only accepts a string")
}
payloadJson := make(map[string]any)
err := json.Unmarshal([]byte(payloadString), &payloadJson)
if err != nil {
return nil, err
}
return payloadJson, nil
}
func (jd *JsonDecode) Type() string {
return jd.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "json.decode",
New: func(config config.ProcessorConfig) (Processor, error) {
return &JsonDecode{config: config}, nil
},
})
}

View File

@@ -1,45 +0,0 @@
package processor_test
import (
"slices"
"testing"
"github.com/jwetzell/osc-go"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestGoodJsonEncode(t *testing.T) {
stringEncoder := processor.JsonEncode{}
tests := []struct {
processor processor.Processor
name string
payload any
expected []byte
}{
{
processor: &stringEncoder,
name: "hello",
payload: osc.OSCMessage{
Address: "/hello",
},
expected: []byte("{\"address\":\"/hello\",\"args\":null}"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := test.processor.Process(t.Context(), test.payload)
gotBytes, ok := got.([]byte)
if !ok {
t.Fatalf("json.encode returned a %T payload: %s", got, got)
}
if err != nil {
t.Fatalf("json.encode failed: %s", err)
}
if !slices.Equal(gotBytes, test.expected) {
t.Fatalf("json.encode got %x, expected %s", got, test.expected)
}
})
}
}

View File

@@ -1,77 +0,0 @@
package processor
import (
"bytes"
"context"
"errors"
"text/template"
osc "github.com/jwetzell/osc-go"
"github.com/jwetzell/showbridge-go/internal/config"
)
type OSCMessageTransform struct {
config config.ProcessorConfig
Address *template.Template
}
func (omt *OSCMessageTransform) Process(ctx context.Context, payload any) (any, error) {
payloadMessage, ok := payload.(osc.OSCMessage)
if !ok {
return nil, errors.New("osc.message.transform processor only accepts an OSCMessage")
}
var addressBuffer bytes.Buffer
err := omt.Address.Execute(&addressBuffer, payloadMessage)
if err != nil {
return nil, err
}
addressString := addressBuffer.String()
if len(addressString) == 0 {
return nil, errors.New("osc.message.transform address must not be empty")
}
if addressString[0] != '/' {
return nil, errors.New("osc.message.transform address must start with '/'")
}
payloadMessage.Address = addressString
return payloadMessage, nil
}
func (omt *OSCMessageTransform) Type() string {
return omt.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "osc.message.transform",
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
address, ok := params["address"]
if !ok {
return nil, errors.New("osc.message.transform requires an address parameter")
}
addressString, ok := address.(string)
if !ok {
return nil, errors.New("osc.message.transform address must be a string")
}
addressTemplate, err := template.New("address").Parse(addressString)
if err != nil {
return nil, err
}
return &OSCMessageTransform{config: config, Address: addressTemplate}, nil
},
})
}

View File

@@ -72,6 +72,18 @@ func init() {
functionString = specificFunctionString
}
enableWasiBool := false
enableWasi, ok := params["enableWasi"]
if ok {
specificEnableWasi, ok := enableWasi.(bool)
if !ok {
return nil, fmt.Errorf("script.wasm enableWasi must be a boolean")
}
enableWasiBool = specificEnableWasi
}
manifest := extism.Manifest{
Wasm: []extism.Wasm{
extism.WasmFile{
@@ -80,7 +92,9 @@ func init() {
},
}
program, err := extism.NewCompiledPlugin(context.Background(), manifest, extism.PluginConfig{}, []extism.HostFunction{})
program, err := extism.NewCompiledPlugin(context.Background(), manifest, extism.PluginConfig{
EnableWasi: enableWasiBool,
}, []extism.HostFunction{})
if err != nil {
return nil, err

View File

@@ -1,70 +0,0 @@
package processor_test
import (
"slices"
"testing"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestGoodStringSplit(t *testing.T) {
tests := []struct {
processor processor.Processor
name string
payload any
expected []string
}{
{
processor: &processor.StringSplit{Separator: ","},
name: "comma separated",
payload: "part1,part2,part3",
expected: []string{"part1", "part2", "part3"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := test.processor.Process(t.Context(), test.payload)
gotStrings, ok := got.([]string)
if !ok {
t.Fatalf("string.split returned a %T payload: %s", got, got)
}
if err != nil {
t.Fatalf("string.split failed: %s", err)
}
if !slices.Equal(gotStrings, test.expected) {
t.Fatalf("string.split got %s, expected %s", got, test.expected)
}
})
}
}
func TestBadStringSplit(t *testing.T) {
tests := []struct {
processor processor.Processor
name string
payload any
errorString string
}{
{
processor: &processor.StringSplit{Separator: ","},
name: "hello",
payload: []byte{0x68, 0x65, 0x6c, 0x6c, 0x6f},
errorString: "string.split only accepts a string",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := test.processor.Process(t.Context(), test.payload)
if err == nil {
t.Fatalf("string.split expected error but got none, payload: %s", got)
}
if err.Error() != test.errorString {
t.Fatalf("string.split got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -0,0 +1,27 @@
package processor_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestArtnetPacketCreateFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["artnet.packet.decode"]
if !ok {
t.Fatalf("artnet.packet.decode processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "artnet.packet.decode",
})
if err != nil {
t.Fatalf("failed to create artnet.packet.decode processor: %s", err)
}
if processorInstance.Type() != "artnet.packet.decode" {
t.Fatalf("artnet.packet.decode processor has wrong type: %s", processorInstance.Type())
}
}

View File

@@ -0,0 +1,27 @@
package processor_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestArtnetPacketEncodeFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["artnet.packet.encode"]
if !ok {
t.Fatalf("artnet.packet.encode processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "artnet.packet.encode",
})
if err != nil {
t.Fatalf("failed to create artnet.packet.encode processor: %s", err)
}
if processorInstance.Type() != "artnet.packet.encode" {
t.Fatalf("artnet.packet.encode processor has wrong type: %s", processorInstance.Type())
}
}

View File

@@ -0,0 +1,239 @@
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
payload any
opCode uint16
expected artnet.ArtNetPacket
}{
{
name: "tiemcode packet with matching opCode",
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,
},
opCode: artnet.OpTimeCode,
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: "tiemcode packet with mismatching opCode",
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,
},
opCode: artnet.OpDmx,
expected: nil,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
artnetPacketFilter := processor.ArtNetPacketFilter{
OpCode: test.opCode,
}
got, err := artnetPacketFilter.Process(t.Context(), test.payload)
if err != nil {
t.Fatalf("artnet.packet.filter 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
payload any
params map[string]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 requires an opCode parameter",
},
{
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 must be 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

@@ -0,0 +1,38 @@
package processor_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestFreeDCreateFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["freed.create"]
if !ok {
t.Fatalf("freed.create processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "freed.create",
Params: map[string]any{
"id": "0",
"pan": "0",
"tilt": "0",
"roll": "0",
"posX": "0",
"posY": "0",
"posZ": "0",
"zoom": "0",
"focus": "0",
},
})
if err != nil {
t.Fatalf("failed to create freed.create processor: %s", err)
}
if processorInstance.Type() != "freed.create" {
t.Fatalf("freed.create processor has wrong type: %s", processorInstance.Type())
}
}

View File

@@ -0,0 +1,27 @@
package processor_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestFreeDDecodeFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["freed.decode"]
if !ok {
t.Fatalf("freed.decode processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "freed.decode",
})
if err != nil {
t.Fatalf("failed to create freed.decode processor: %s", err)
}
if processorInstance.Type() != "freed.decode" {
t.Fatalf("freed.decode processor has wrong type: %s", processorInstance.Type())
}
}

View File

@@ -0,0 +1,27 @@
package processor_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestFreeDEncodeFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["freed.encode"]
if !ok {
t.Fatalf("freed.encode processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "freed.encode",
})
if err != nil {
t.Fatalf("failed to create freed.encode processor: %s", err)
}
if processorInstance.Type() != "freed.encode" {
t.Fatalf("freed.encode processor has wrong type: %s", processorInstance.Type())
}
}

BIN
internal/processor/test/good.wasm Executable file

Binary file not shown.

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 TestHTTPRequestCreateFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["http.request.create"]
if !ok {
t.Fatalf("http.request.create processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "http.request.create",
Params: map[string]any{
"method": "GET",
"url": "http://example.com",
},
})
if err != nil {
t.Fatalf("failed to create http.request.create processor: %s", err)
}
if processorInstance.Type() != "http.request.create" {
t.Fatalf("http.request.create processor has wrong type: %s", processorInstance.Type())
}
}

View File

@@ -0,0 +1,27 @@
package processor_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestHTTPRequestEncodeFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["http.request.encode"]
if !ok {
t.Fatalf("http.request.encode processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "http.request.encode",
})
if err != nil {
t.Fatalf("failed to create http.request.encode processor: %s", err)
}
if processorInstance.Type() != "http.request.encode" {
t.Fatalf("http.request.encode 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

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

View File

@@ -0,0 +1,27 @@
package processor_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestHTTPResponseEncodeFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["http.response.encode"]
if !ok {
t.Fatalf("http.response.encode processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "http.response.encode",
})
if err != nil {
t.Fatalf("failed to create http.response.encode processor: %s", err)
}
if processorInstance.Type() != "http.response.encode" {
t.Fatalf("http.response.encode processor has wrong type: %s", processorInstance.Type())
}
}

View File

@@ -0,0 +1,175 @@
package processor_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestIntRandomFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["int.random"]
if !ok {
t.Fatalf("int.random processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "int.random",
Params: map[string]any{
"min": 1.0,
"max": 10.0,
},
})
if err != nil {
t.Fatalf("failed to create int.random processor: %s", err)
}
if processorInstance.Type() != "int.random" {
t.Fatalf("int.random processor has wrong type: %s", processorInstance.Type())
}
}
func TestIntRandomGoodConfig(t *testing.T) {
registration, ok := processor.ProcessorRegistry["int.random"]
if !ok {
t.Fatalf("int.random processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "int.random",
Params: map[string]any{
"min": 1.0,
"max": 10.0,
},
})
if err != nil {
t.Fatalf("int.random should have created processor but got error: %s", err)
}
payload := "12345"
got, err := processorInstance.Process(t.Context(), payload)
if err != nil {
t.Fatalf("int.random processing failed: %s", err)
}
gotInt, ok := got.(int)
if !ok {
t.Fatalf("int.random returned a %T payload: %s", got, got)
}
if gotInt < 1 || gotInt > 10 {
t.Fatalf("int.random got %d, expected between %d and %d", gotInt, 1, 10)
}
}
func TestGoodIntRandom(t *testing.T) {
tests := []struct {
processor processor.Processor
name string
payload any
min int
max int
}{
{
name: "1-10",
payload: "12345",
min: 1,
max: 10,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
intRandom := processor.IntRandom{
Min: test.min,
Max: test.max,
}
got, err := intRandom.Process(t.Context(), test.payload)
gotInt, ok := got.(int)
if !ok {
t.Fatalf("int.random returned a %T payload: %s", got, got)
}
if err != nil {
t.Fatalf("int.random failed: %s", err)
}
if gotInt < test.min || gotInt > test.max {
t.Fatalf("int.random got %d, expected between %d and %d", gotInt, test.min, test.max)
}
})
}
}
func TestBadIntRandom(t *testing.T) {
tests := []struct {
name string
params map[string]any
payload any
errorString string
}{
{
name: "no min param",
payload: "hello",
params: map[string]any{"max": 10.0},
errorString: "int.random requires a min parameter",
},
{
name: "no max param",
payload: "hello",
params: map[string]any{"min": 1.0},
errorString: "int.random requires a max parameter",
},
{
name: "min param not a number",
payload: "hello",
params: map[string]any{"min": "1", "max": 10.0},
errorString: "int.random min must be a number",
},
{
name: "max param not a number",
payload: "hello",
params: map[string]any{"min": 1.0, "max": "10"},
errorString: "int.random max must be a number",
},
{
name: "max less than min",
payload: "hello",
params: map[string]any{"min": 1.0, "max": 0.0},
errorString: "int.random max must be greater than min",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := processor.ProcessorRegistry["int.random"]
if !ok {
t.Fatalf("int.random processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "int.random",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("int.random got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
got, err := processorInstance.Process(t.Context(), test.payload)
if err == nil {
t.Fatalf("int.random expected to fail but got payload: %s", got)
}
if err.Error() != test.errorString {
t.Fatalf("int.random got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -0,0 +1,91 @@
package processor_test
import (
"reflect"
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestJsonDecodeFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["json.decode"]
if !ok {
t.Fatalf("json.decode processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "json.decode",
})
if err != nil {
t.Fatalf("failed to create json.decode processor: %s", err)
}
if processorInstance.Type() != "json.decode" {
t.Fatalf("json.decode processor has wrong type: %s", processorInstance.Type())
}
payload := "{\"property\":\"hello\"}"
expected := map[string]any{
"property": "hello",
}
got, err := processorInstance.Process(t.Context(), payload)
if err != nil {
t.Fatalf("json.decode processing failed: %s", err)
}
gotMap, ok := got.(map[string]any)
if !ok {
t.Fatalf("json.decode should return byte slice")
}
if !reflect.DeepEqual(gotMap, expected) {
t.Fatalf("json.decode got %+v, expected %+v", got, expected)
}
}
func TestGoodJsonDecode(t *testing.T) {
jsonDecoder := processor.JsonDecode{}
tests := []struct {
name string
payload string
expected map[string]any
}{
{
name: "basic json",
payload: "{\"address\":\"/hello\",\"args\":null}",
expected: map[string]any{
"address": "/hello",
"args": nil,
},
},
{
name: "array",
payload: "{\"address\":\"/hello\",\"args\":[1,2,3]}",
expected: map[string]any{
"address": "/hello",
"args": []any{1.0, 2.0, 3.0},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := jsonDecoder.Process(t.Context(), test.payload)
gotMap, ok := got.(map[string]any)
if !ok {
t.Fatalf("json.decode returned a %T payload: %s", got, got)
}
if err != nil {
t.Fatalf("json.decode failed: %s", err)
}
if !reflect.DeepEqual(gotMap, test.expected) {
t.Fatalf("json.decode got %x, expected %s", got, test.expected)
}
})
}
}

View File

@@ -0,0 +1,85 @@
package processor_test
import (
"slices"
"testing"
"github.com/jwetzell/osc-go"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestJsonEncodeFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["json.encode"]
if !ok {
t.Fatalf("json.encode processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "json.encode",
})
if err != nil {
t.Fatalf("failed to create json.encode processor: %s", err)
}
if processorInstance.Type() != "json.encode" {
t.Fatalf("json.encode processor has wrong type: %s", processorInstance.Type())
}
payload := struct {
Property string `json:"property"`
}{
Property: "hello",
}
expected := []byte("{\"property\":\"hello\"}")
got, err := processorInstance.Process(t.Context(), payload)
if err != nil {
t.Fatalf("json.encode processing failed: %s", err)
}
gotBytes, ok := got.([]byte)
if !ok {
t.Fatalf("json.encode should return byte slice")
}
if !slices.Equal(gotBytes, expected) {
t.Fatalf("json.encode got %+v, expected %+v", got, expected)
}
}
func TestGoodJsonEncode(t *testing.T) {
jsonEncoder := processor.JsonEncode{}
tests := []struct {
name string
payload any
expected []byte
}{
{
name: "basic struct",
payload: osc.OSCMessage{
Address: "/hello",
},
expected: []byte("{\"address\":\"/hello\",\"args\":null}"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := jsonEncoder.Process(t.Context(), test.payload)
gotBytes, ok := got.([]byte)
if !ok {
t.Fatalf("json.encode returned a %T payload: %s", got, got)
}
if err != nil {
t.Fatalf("json.encode failed: %s", err)
}
if !slices.Equal(gotBytes, test.expected) {
t.Fatalf("json.encode got %x, expected %s", got, test.expected)
}
})
}
}

View File

@@ -0,0 +1,33 @@
package processor_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestMIDIMessageCreateFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["midi.message.create"]
if !ok {
t.Fatalf("midi.message.create processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "midi.message.create",
Params: map[string]any{
"type": "note_on",
"channel": "1",
"note": "60",
"velocity": "100",
},
})
if err != nil {
t.Fatalf("failed to create midi.message.create processor: %s", err)
}
if processorInstance.Type() != "midi.message.create" {
t.Fatalf("midi.message.create processor has wrong type: %s", processorInstance.Type())
}
}

View File

@@ -0,0 +1,27 @@
package processor_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestMIDIMessageDecodeFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["midi.message.decode"]
if !ok {
t.Fatalf("midi.message.decode processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "midi.message.decode",
})
if err != nil {
t.Fatalf("failed to create midi.message.decode processor: %s", err)
}
if processorInstance.Type() != "midi.message.decode" {
t.Fatalf("midi.message.decode processor has wrong type: %s", processorInstance.Type())
}
}

View File

@@ -0,0 +1,27 @@
package processor_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestMIDIMessageEncodeFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["midi.message.encode"]
if !ok {
t.Fatalf("midi.message.encode processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "midi.message.encode",
})
if err != nil {
t.Fatalf("failed to create midi.message.encode processor: %s", err)
}
if processorInstance.Type() != "midi.message.encode" {
t.Fatalf("midi.message.encode processor has wrong type: %s", processorInstance.Type())
}
}

View File

@@ -0,0 +1,30 @@
package processor_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
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())
}
}

View File

@@ -0,0 +1,27 @@
package processor_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestMIDIMessageUnpackFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["midi.message.unpack"]
if !ok {
t.Fatalf("midi.message.unpack processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "midi.message.unpack",
})
if err != nil {
t.Fatalf("failed to create midi.message.unpack processor: %s", err)
}
if processorInstance.Type() != "midi.message.unpack" {
t.Fatalf("midi.message.unpack processor has wrong type: %s", processorInstance.Type())
}
}

View File

@@ -0,0 +1,33 @@
package processor_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestMQTTMessageCreateFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["mqtt.message.create"]
if !ok {
t.Fatalf("mqtt.message.create processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "mqtt.message.create",
Params: map[string]any{
"topic": "test/topic",
"payload": "Hello, World!",
"qos": 1.0,
"retained": true,
},
})
if err != nil {
t.Fatalf("failed to create mqtt.message.create processor: %s", err)
}
if processorInstance.Type() != "mqtt.message.create" {
t.Fatalf("mqtt.message.create processor has wrong type: %s", processorInstance.Type())
}
}

View File

@@ -0,0 +1,27 @@
package processor_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestMQTTMessageEncodeFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["mqtt.message.encode"]
if !ok {
t.Fatalf("mqtt.message.encode processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "mqtt.message.encode",
})
if err != nil {
t.Fatalf("failed to create mqtt.message.encode processor: %s", err)
}
if processorInstance.Type() != "mqtt.message.encode" {
t.Fatalf("mqtt.message.encode 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 TestNATSMessageCreateFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["nats.message.create"]
if !ok {
t.Fatalf("nats.message.create processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "nats.message.create",
Params: map[string]any{
"subject": "test",
"payload": "Hello, World!",
},
})
if err != nil {
t.Fatalf("failed to create nats.message.create processor: %s", err)
}
if processorInstance.Type() != "nats.message.create" {
t.Fatalf("nats.message.create processor has wrong type: %s", processorInstance.Type())
}
}

View File

@@ -0,0 +1,27 @@
package processor_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestNATSMessageEncodeFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["nats.message.encode"]
if !ok {
t.Fatalf("nats.message.encode processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "nats.message.encode",
})
if err != nil {
t.Fatalf("failed to create nats.message.encode processor: %s", err)
}
if processorInstance.Type() != "nats.message.encode" {
t.Fatalf("nats.message.encode processor has wrong type: %s", processorInstance.Type())
}
}

View File

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

View File

@@ -0,0 +1,27 @@
package processor_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestOSCMessageDecodeFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["osc.message.decode"]
if !ok {
t.Fatalf("osc.message.decode processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "osc.message.decode",
})
if err != nil {
t.Fatalf("failed to create osc.message.decode processor: %s", err)
}
if processorInstance.Type() != "osc.message.decode" {
t.Fatalf("osc.message.decode processor has wrong type: %s", processorInstance.Type())
}
}

View File

@@ -0,0 +1,27 @@
package processor_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestOSCMessageEncodeFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["osc.message.encode"]
if !ok {
t.Fatalf("osc.message.encode processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "osc.message.encode",
})
if err != nil {
t.Fatalf("failed to create osc.message.encode processor: %s", err)
}
if processorInstance.Type() != "osc.message.encode" {
t.Fatalf("osc.message.encode processor has wrong type: %s", processorInstance.Type())
}
}

View File

@@ -0,0 +1,30 @@
package processor_test
import (
"testing"
"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())
}
}

View File

@@ -0,0 +1,127 @@
package processor_test
import (
"slices"
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestScriptWASMFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["script.wasm"]
if !ok {
t.Fatalf("script.wasm processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "script.wasm",
Params: map[string]any{
"path": "good.wasm",
},
})
if err != nil {
t.Fatalf("failed to create script.wasm processor: %s", err)
}
if processorInstance.Type() != "script.wasm" {
t.Fatalf("script.wasm processor has wrong type: %s", processorInstance.Type())
}
}
func TestScriptWASMNoPath(t *testing.T) {
registration, ok := processor.ProcessorRegistry["script.wasm"]
if !ok {
t.Fatalf("script.wasm processor not registered")
}
_, err := registration.New(config.ProcessorConfig{
Type: "script.wasm",
Params: map[string]any{},
})
if err == nil {
t.Fatalf("script.wasm processor should have thrown an error when creating")
}
}
func TestScriptWASMBadConfigWrongPathType(t *testing.T) {
registration, ok := processor.ProcessorRegistry["script.wasm"]
if !ok {
t.Fatalf("script.wasm processor not registered")
}
_, err := registration.New(config.ProcessorConfig{
Type: "script.wasm",
Params: map[string]any{
"path": 12345,
},
})
if err == nil {
t.Fatalf("script.wasm processor should have thrown an error when creating with non-string path")
}
}
func TestGoodScriptWASM(t *testing.T) {
tests := []struct {
name string
payload []byte
params map[string]any
expected []byte
}{
{
name: "number",
params: map[string]any{
"path": "good.wasm",
"enableWasi": true,
},
payload: []byte("hello"),
expected: []byte("Processed: hello"),
},
{
name: "number",
params: map[string]any{
"path": "good.wasm",
"enableWasi": true,
"function": "greet",
},
payload: []byte("world"),
expected: []byte("Hello, world"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := processor.ProcessorRegistry["script.wasm"]
if !ok {
t.Fatalf("script.wasm processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "script.wasm",
Params: test.params,
})
if err != nil {
t.Fatalf("script.wasm failed to create processor: %s", err)
}
got, err := processorInstance.Process(t.Context(), test.payload)
if err != nil {
t.Fatalf("script.wasm process failed: %s", err)
}
gotBytes, ok := got.([]byte)
if !ok {
t.Fatalf("script.wasm returned a %T payload: %s", got, got)
}
if !slices.Equal(gotBytes, test.expected) {
t.Fatalf("script.wasm got %+v, expected %+v", gotBytes, test.expected)
}
})
}
}

View File

@@ -0,0 +1,32 @@
package processor_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestSipResponseAudioCreateFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["sip.response.audio.create"]
if !ok {
t.Fatalf("sip.response.audio.create processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "sip.response.audio.create",
Params: map[string]any{
"preWait": 0.0,
"audioFile": "good.wav",
"postWait": 0.0,
},
})
if err != nil {
t.Fatalf("failed to filter sip.response.audio.create processor: %s", err)
}
if processorInstance.Type() != "sip.response.audio.create" {
t.Fatalf("sip.response.audio.create processor has wrong type: %s", processorInstance.Type())
}
}

View File

@@ -0,0 +1,32 @@
package processor_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestSipResponseDTMFCreateFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["sip.response.dtmf.create"]
if !ok {
t.Fatalf("sip.response.dtmf.create processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "sip.response.dtmf.create",
Params: map[string]any{
"preWait": 0.0,
"digits": "good.wav",
"postWait": 0.0,
},
})
if err != nil {
t.Fatalf("failed to filter sip.response.dtmf.create processor: %s", err)
}
if processorInstance.Type() != "sip.response.dtmf.create" {
t.Fatalf("sip.response.dtmf.create processor has wrong type: %s", processorInstance.Type())
}
}

View File

@@ -168,7 +168,7 @@ func TestBadStringCreate(t *testing.T) {
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("string.encode got error '%s', expected '%s'", err.Error(), test.errorString)
t.Fatalf("string.create got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
@@ -176,11 +176,11 @@ func TestBadStringCreate(t *testing.T) {
got, err := processorInstance.Process(t.Context(), test.payload)
if err == nil {
t.Fatalf("string.encode expected to fail but got payload: %s", got)
t.Fatalf("string.create expected to fail but got payload: %s", got)
}
if err.Error() != test.errorString {
t.Fatalf("string.encode got error '%s', expected '%s'", err.Error(), test.errorString)
t.Fatalf("string.create got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}

View File

@@ -40,22 +40,20 @@ func TestStringDecodeFromRegistry(t *testing.T) {
func TestGoodStringDecode(t *testing.T) {
stringDecoder := processor.StringDecode{}
tests := []struct {
processor processor.Processor
name string
payload any
expected string
name string
payload any
expected string
}{
{
processor: &stringDecoder,
name: "hello",
payload: []byte{0x68, 0x65, 0x6c, 0x6c, 0x6f},
expected: "hello",
name: "basic string",
payload: []byte{0x68, 0x65, 0x6c, 0x6c, 0x6f},
expected: "hello",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := test.processor.Process(t.Context(), test.payload)
got, err := stringDecoder.Process(t.Context(), test.payload)
gotString, ok := got.(string)
if !ok {
@@ -74,13 +72,11 @@ func TestGoodStringDecode(t *testing.T) {
func TestBadStringDecode(t *testing.T) {
stringDecoder := processor.StringDecode{}
tests := []struct {
processor processor.Processor
name string
payload any
errorString string
}{
{
processor: &stringDecoder,
name: "non-[]byte input",
payload: "hello",
errorString: "string.decode processor only accepts a []byte",
@@ -89,7 +85,7 @@ func TestBadStringDecode(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := test.processor.Process(t.Context(), test.payload)
got, err := stringDecoder.Process(t.Context(), test.payload)
if err == nil {
t.Fatalf("string.decode expected to fail but got payload: %s", got)

View File

@@ -47,22 +47,20 @@ func TestStringEncodeFromRegistry(t *testing.T) {
func TestGoodStringEncode(t *testing.T) {
stringEncoder := processor.StringEncode{}
tests := []struct {
processor processor.Processor
name string
payload any
expected []byte
name string
payload any
expected []byte
}{
{
processor: &stringEncoder,
name: "hello",
payload: "hello",
expected: []byte{0x68, 0x65, 0x6c, 0x6c, 0x6f},
name: "basic string",
payload: "hello",
expected: []byte{0x68, 0x65, 0x6c, 0x6c, 0x6f},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := test.processor.Process(t.Context(), test.payload)
got, err := stringEncoder.Process(t.Context(), test.payload)
gotBytes, ok := got.([]byte)
if !ok {
@@ -81,13 +79,11 @@ func TestGoodStringEncode(t *testing.T) {
func TestBadStringEncode(t *testing.T) {
stringEncoder := processor.StringEncode{}
tests := []struct {
processor processor.Processor
name string
payload any
errorString string
}{
{
processor: &stringEncoder,
name: "non-string input",
payload: []byte{0x68, 0x65, 0x6c, 0x6c, 0x6f},
errorString: "string.encode processor only accepts a string",
@@ -96,7 +92,7 @@ func TestBadStringEncode(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := test.processor.Process(t.Context(), test.payload)
got, err := stringEncoder.Process(t.Context(), test.payload)
if err == nil {
t.Fatalf("string.encode expected to fail but got payload: %s", got)

View File

@@ -0,0 +1,187 @@
package processor_test
import (
"reflect"
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestStringFilterFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["string.filter"]
if !ok {
t.Fatalf("string.filter processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "string.filter",
Params: map[string]any{
"pattern": "hello",
},
})
if err != nil {
t.Fatalf("failed to create string.filter processor: %s", err)
}
if processorInstance.Type() != "string.filter" {
t.Fatalf("string.filter processor has wrong type: %s", processorInstance.Type())
}
payload := "hello"
expected := "hello"
got, err := processorInstance.Process(t.Context(), payload)
if err != nil {
t.Fatalf("string.filter processing failed: %s", err)
}
gotString, ok := got.(string)
if !ok {
t.Fatalf("string.filter should return byte slice")
}
if gotString != expected {
t.Fatalf("string.filter got %+v, expected %+v", got, expected)
}
}
func TestGoodStringFilter(t *testing.T) {
tests := []struct {
name string
params map[string]any
payload string
expected any
}{
{
name: "matches pattern",
payload: "hello",
params: map[string]any{"pattern": "hello"},
expected: "hello",
},
{
name: "does not match pattern",
payload: "hello",
params: map[string]any{"pattern": "world"},
expected: nil,
},
{
name: "basic regex",
payload: "hello world",
params: map[string]any{"pattern": ".* world"},
expected: "hello world",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := processor.ProcessorRegistry["string.filter"]
if !ok {
t.Fatalf("string.filter processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "string.filter",
Params: test.params,
})
if err != nil {
t.Fatalf("string.filter failed to create processor: %s", err)
}
got, err := processorInstance.Process(t.Context(), test.payload)
if err != nil {
t.Fatalf("string.filter failed: %s", err)
}
if test.expected == nil {
if got != nil {
t.Fatalf("string.filter got %+v, expected nil", got)
}
return
}
gotString, ok := got.(string)
if !ok {
t.Fatalf("string.filter returned a %T payload: %s", got, got)
}
if !reflect.DeepEqual(gotString, test.expected) {
t.Fatalf("string.filter got %+v, expected %+v", gotString, test.expected)
}
})
}
}
func TestBadStringFilter(t *testing.T) {
tests := []struct {
name string
payload any
params map[string]any
errorString string
}{
{
name: "no pattern param",
payload: "hello",
params: map[string]any{},
errorString: "string.filter requires a pattern parameter",
},
{
name: "non-string input",
payload: []byte{0x68, 0x65, 0x6c, 0x6c, 0x6f},
params: map[string]any{
"pattern": "hello",
},
errorString: "string.filter processor only accepts a string",
},
{
name: "non-string pattern param",
payload: "hello",
params: map[string]any{
"pattern": 123,
},
errorString: "string.filter pattern must be a string",
},
{
name: "invalid regex pattern",
payload: "hello",
params: map[string]any{
"pattern": "*invalid",
},
errorString: "error parsing regexp: missing argument to repetition operator: `*`",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := processor.ProcessorRegistry["string.filter"]
if !ok {
t.Fatalf("string.filter processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "string.filter",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("string.filter got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
got, err := processorInstance.Process(t.Context(), test.payload)
if err == nil {
t.Fatalf("string.filter expected to fail but got payload: %s", got)
}
if err.Error() != test.errorString {
t.Fatalf("string.filter got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -0,0 +1,139 @@
package processor_test
import (
"slices"
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestStringSplitFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["string.split"]
if !ok {
t.Fatalf("string.split processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "string.split",
Params: map[string]any{
"separator": ",",
},
})
if err != nil {
t.Fatalf("failed to create string.split processor: %s", err)
}
if processorInstance.Type() != "string.split" {
t.Fatalf("string.split processor has wrong type: %s", processorInstance.Type())
}
payload := "part1,part2,part3"
expected := []string{"part1", "part2", "part3"}
got, err := processorInstance.Process(t.Context(), payload)
if err != nil {
t.Fatalf("string.split processing failed: %s", err)
}
gotStrings, ok := got.([]string)
if !ok {
t.Fatalf("string.split should return a slice of strings")
}
if !slices.Equal(gotStrings, expected) {
t.Fatalf("string.split got %+v, expected %+v", got, expected)
}
}
func TestGoodStringSplit(t *testing.T) {
tests := []struct {
processor processor.Processor
name string
payload any
expected []string
}{
{
processor: &processor.StringSplit{Separator: ","},
name: "comma separated",
payload: "part1,part2,part3",
expected: []string{"part1", "part2", "part3"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
got, err := test.processor.Process(t.Context(), test.payload)
gotStrings, ok := got.([]string)
if !ok {
t.Fatalf("string.split returned a %T payload: %s", got, got)
}
if err != nil {
t.Fatalf("string.split failed: %s", err)
}
if !slices.Equal(gotStrings, test.expected) {
t.Fatalf("string.split got %s, expected %s", got, test.expected)
}
})
}
}
func TestBadStringSplit(t *testing.T) {
tests := []struct {
name string
payload any
params map[string]any
errorString string
}{
{
name: "non-string input",
payload: []byte{0x68, 0x65, 0x6c, 0x6c, 0x6f},
params: map[string]any{"separator": ","},
errorString: "string.split only accepts a string",
},
{
name: "missing separator param",
payload: "part1,part2,part3",
params: map[string]any{},
errorString: "string.split requires a separator",
},
{
name: "non-string separator param",
payload: "part1,part2,part3",
params: map[string]any{"separator": 123},
errorString: "string.split separator must be a string",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := processor.ProcessorRegistry["string.split"]
if !ok {
t.Fatalf("string.split processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "string.split",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("string.split got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
got, err := processorInstance.Process(t.Context(), test.payload)
if err == nil {
t.Fatalf("string.split expected error but got none, payload: %s", got)
}
if err.Error() != test.errorString {
t.Fatalf("string.split got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -0,0 +1,30 @@
package processor_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestTimeSleepFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["time.sleep"]
if !ok {
t.Fatalf("time.sleep processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "time.sleep",
Params: map[string]any{
"duration": 1000.0,
},
})
if err != nil {
t.Fatalf("failed to create time.sleep processor: %s", err)
}
if processorInstance.Type() != "time.sleep" {
t.Fatalf("time.sleep processor has wrong type: %s", processorInstance.Type())
}
}

View File

@@ -0,0 +1,177 @@
package processor_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestUintRandomFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["uint.random"]
if !ok {
t.Fatalf("uint.random processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "uint.random",
Params: map[string]any{
"min": 1.0,
"max": 10.0,
},
})
if err != nil {
t.Fatalf("failed to create uint.random processor: %s", err)
}
if processorInstance.Type() != "uint.random" {
t.Fatalf("uint.random processor has wrong type: %s", processorInstance.Type())
}
}
func TestUintRandomGoodConfig(t *testing.T) {
registration, ok := processor.ProcessorRegistry["uint.random"]
if !ok {
t.Fatalf("uint.random processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "uint.random",
Params: map[string]any{
"min": 1.0,
"max": 10.0,
},
})
if err != nil {
t.Fatalf("uint.random should have created processor but got error: %s", err)
}
payload := "12345"
got, err := processorInstance.Process(t.Context(), payload)
if err != nil {
t.Fatalf("uint.random processing failed: %s", err)
}
gotUint, ok := got.(uint)
if !ok {
t.Fatalf("uint.random returned a %T payload: %s", got, got)
}
if gotUint < 1 || gotUint > 10 {
t.Fatalf("uint.random got %d, expected between %d and %d", gotUint, 1, 10)
}
}
func TestGoodUintRandom(t *testing.T) {
tests := []struct {
processor processor.Processor
name string
payload any
min uint
max uint
}{
{
name: "1-10",
payload: "12345",
min: 1,
max: 10,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
uintRandom := processor.UintRandom{
Min: test.min,
Max: test.max,
}
got, err := uintRandom.Process(t.Context(), test.payload)
gotUint, ok := got.(uint)
if !ok {
t.Fatalf("uint.random returned a %T payload: %s", got, got)
}
if err != nil {
t.Fatalf("uint.random failed: %s", err)
}
if gotUint < test.min || gotUint > test.max {
t.Fatalf("uint.random got %d, expected between %d and %d", gotUint, test.min, test.max)
}
})
}
}
func TestBadUintRandom(t *testing.T) {
tests := []struct {
name string
params map[string]any
payload any
errorString string
}{
{
name: "no min param",
payload: "hello",
params: map[string]any{"max": 10.0},
errorString: "uint.random requires a min parameter",
},
{
name: "no max param",
payload: "hello",
params: map[string]any{"min": 1.0},
errorString: "uint.random requires a max parameter",
},
{
name: "min param not a number",
payload: "hello",
params: map[string]any{"min": "1", "max": 10.0},
errorString: "uint.random min must be a number",
},
{
name: "max param not a number",
payload: "hello",
params: map[string]any{"min": 1.0, "max": "10"},
errorString: "uint.random max must be a number",
},
{
name: "max less than min",
payload: "hello",
params: map[string]any{"min": 1.0, "max": 0.0},
errorString: "uint.random max must be greater than min",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := processor.ProcessorRegistry["uint.random"]
if !ok {
t.Fatalf("uint.random processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "uint.random",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("uint.random got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
got, err := processorInstance.Process(t.Context(), test.payload)
if err == nil {
t.Fatalf("uint.random expected to fail but got payload: %s", got)
}
if err.Error() != test.errorString {
t.Fatalf("uint.random got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -6,6 +6,9 @@ import (
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)
type routeContextKey string
@@ -21,14 +24,14 @@ type RouteError struct {
type RouteIOError struct {
Index int
OutputErrors []error
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
HandleOutput(ctx context.Context, destinationId string, payload any) error
}
type Route interface {
@@ -73,17 +76,32 @@ func (r *ProcessorRoute) Output() string {
}
func (r *ProcessorRoute) ProcessPayload(ctx context.Context, payload any) (any, error) {
for _, processor := range r.processors {
processedPayload, err := processor.Process(ctx, payload)
parentSpan := trace.SpanFromContext(ctx)
tracer := parentSpan.TracerProvider().Tracer("route.ProcessPayload")
processCtx, processSpan := tracer.Start(ctx, "route.process")
defer processSpan.End()
for processorIndex, processor := range r.processors {
processorCtx, processorSpan := tracer.Start(processCtx, "route.processor", trace.WithAttributes(attribute.Int("processor.index", processorIndex), attribute.String("processor.type", processor.Type())))
processedPayload, err := processor.Process(processorCtx, payload)
if err != nil {
processorSpan.SetStatus(codes.Error, "route processor error")
processorSpan.RecordError(err)
processorSpan.End()
processSpan.SetStatus(codes.Error, "route processing error")
processSpan.RecordError(err)
return nil, err
}
processorSpan.SetStatus(codes.Ok, "processor successful")
//NOTE(jwetzell) nil payload will result in the route being "terminated"
if processedPayload == nil {
processSpan.SetStatus(codes.Ok, "route processing terminated early due to nil payload")
processorSpan.End()
return nil, nil
}
payload = processedPayload
processorSpan.End()
}
processSpan.SetStatus(codes.Ok, "route processing successful")
return payload, nil
}

228
router.go
View File

@@ -9,94 +9,131 @@ import (
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
"github.com/jwetzell/showbridge-go/internal/route"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
)
type Router struct {
contextCancel context.CancelFunc
Context context.Context
ModuleInstances []module.Module
RouteInstances []route.Route
moduleWait sync.WaitGroup
logger *slog.Logger
ModuleInstances map[string]module.Module
// TODO(jwetzell): change to something easier to lookup
RouteInstances []route.Route
moduleWait sync.WaitGroup
logger *slog.Logger
tracer trace.Tracer
runningConfig config.Config
}
func NewRouter(config config.Config) (*Router, []module.ModuleError, []route.RouteError) {
router := Router{
ModuleInstances: []module.Module{},
RouteInstances: []route.Route{},
logger: slog.Default().With("component", "router"),
func (r *Router) addModule(moduleDecl config.ModuleConfig) error {
if moduleDecl.Id == "" {
return errors.New("module id cannot be empty")
}
moduleInfo, ok := module.ModuleRegistry[moduleDecl.Type]
if !ok {
return errors.New("module type not defined")
}
_, ok = r.ModuleInstances[moduleDecl.Id]
if ok {
return errors.New("module id already exists")
}
moduleInstance, err := moduleInfo.New(moduleDecl)
if err != nil {
return err
}
r.ModuleInstances[moduleDecl.Id] = moduleInstance
return nil
}
func (r *Router) removeModule(moduleId string) error {
err := r.stopModule(moduleId)
if err != nil {
return err
}
delete(r.ModuleInstances, moduleId)
return nil
}
func (r *Router) startModule(ctx context.Context, moduleId string) error {
moduleInstance := r.getModule(moduleId)
if moduleInstance == nil {
return errors.New("module id not found")
}
r.moduleWait.Go(func() {
err := moduleInstance.Start(ctx)
if err != nil {
// TODO(jwetzell): propagate module run errors better
r.logger.Error("error encountered running module", "moduleId", moduleId, "error", err)
}
})
return nil
}
func (r *Router) stopModule(moduleId string) error {
moduleInstance := r.getModule(moduleId)
if moduleInstance == nil {
return errors.New("module id not found")
}
moduleInstance.Stop()
return nil
}
// TODO(jwetzell): support removing route
func (r *Router) addRoute(routeDecl config.RouteConfig) error {
routeInstance, err := route.NewRoute(routeDecl)
if err != nil {
return err
}
r.RouteInstances = append(r.RouteInstances, routeInstance)
return nil
}
func (r *Router) getModule(moduleId string) module.Module {
moduleInstance, ok := r.ModuleInstances[moduleId]
if !ok {
return nil
}
return moduleInstance
}
func NewRouter(config config.Config, tracer trace.Tracer) (*Router, []module.ModuleError, []route.RouteError) {
router := Router{
ModuleInstances: make(map[string]module.Module),
RouteInstances: []route.Route{},
logger: slog.Default().With("component", "router"),
tracer: tracer,
runningConfig: config,
}
router.logger.Debug("creating")
var moduleErrors []module.ModuleError
for moduleIndex, moduleDecl := range config.Modules {
if moduleDecl.Id == "" {
err := router.addModule(moduleDecl)
if err != nil {
if moduleErrors == nil {
moduleErrors = []module.ModuleError{}
}
moduleErrors = append(moduleErrors, module.ModuleError{
Index: moduleIndex,
Config: moduleDecl,
Error: errors.New("module id cannot be empty"),
Error: err,
})
continue
}
moduleInfo, ok := module.ModuleRegistry[moduleDecl.Type]
if !ok {
if moduleErrors == nil {
moduleErrors = []module.ModuleError{}
}
moduleErrors = append(moduleErrors, module.ModuleError{
Index: moduleIndex,
Config: moduleDecl,
Error: errors.New("module type not defined"),
})
continue
}
moduleInstanceExists := false
for _, moduleInstance := range router.ModuleInstances {
if moduleInstance.Id() == moduleDecl.Id {
moduleInstanceExists = true
if moduleErrors == nil {
moduleErrors = []module.ModuleError{}
}
moduleErrors = append(moduleErrors, module.ModuleError{
Index: moduleIndex,
Config: moduleDecl,
Error: errors.New("duplicate module id"),
})
break
}
}
if !moduleInstanceExists {
moduleInstance, err := moduleInfo.New(moduleDecl)
if err != nil {
if moduleErrors == nil {
moduleErrors = []module.ModuleError{}
}
moduleErrors = append(moduleErrors, module.ModuleError{
Index: moduleIndex,
Config: moduleDecl,
Error: err,
})
continue
}
router.ModuleInstances = append(router.ModuleInstances, moduleInstance)
}
}
var routeErrors []route.RouteError
for routeIndex, routeDecl := range config.Routes {
routeInstance, err := route.NewRoute(routeDecl)
err := router.addRoute(routeDecl)
if err != nil {
if routeErrors == nil {
routeErrors = []route.RouteError{}
@@ -108,26 +145,21 @@ func NewRouter(config config.Config) (*Router, []module.ModuleError, []route.Rou
})
continue
}
router.RouteInstances = append(router.RouteInstances, routeInstance)
}
return &router, moduleErrors, routeErrors
}
func (r *Router) Run(ctx context.Context) {
func (r *Router) Start(ctx context.Context) {
r.logger.Info("running")
routerContext, cancel := context.WithCancel(ctx)
r.Context = routerContext
r.contextCancel = cancel
contextWithRouter := context.WithValue(routerContext, route.RouterContextKey, r)
for _, moduleInstance := range r.ModuleInstances {
r.moduleWait.Go(func() {
err := moduleInstance.Run(contextWithRouter)
if err != nil {
r.logger.Error("error encountered running module", "error", err)
}
})
for moduleId := range r.ModuleInstances {
// TODO(jwetzell): handle module run errors
r.startModule(contextWithRouter, moduleId)
}
<-r.Context.Done()
r.logger.Debug("waiting for modules to exit")
@@ -141,6 +173,8 @@ func (r *Router) Stop() {
}
func (r *Router) HandleInput(ctx context.Context, sourceId string, payload any) (bool, []route.RouteIOError) {
spanCtx, span := r.tracer.Start(ctx, "router.input", trace.WithAttributes(attribute.String("source.id", sourceId)), trace.WithNewRoot())
defer span.End()
var routeIOErrors []route.RouteIOError
routeFound := false
@@ -151,9 +185,10 @@ func (r *Router) HandleInput(ctx context.Context, sourceId string, payload any)
routeWaitGroup.Go(func() {
routeFound = true
routeContext := context.WithValue(ctx, route.SourceContextKey, sourceId)
routeContext := context.WithValue(spanCtx, route.SourceContextKey, sourceId)
payload, err := routeInstance.ProcessPayload(routeContext, payload)
routeCtx, routeSpan := r.tracer.Start(routeContext, "route", trace.WithAttributes(attribute.Int("route.index", routeIndex), attribute.String("route.input", routeInstance.Input()), attribute.String("route.output", routeInstance.Output())))
payload, err := routeInstance.ProcessPayload(routeCtx, payload)
if err != nil {
if routeIOErrors == nil {
routeIOErrors = []route.RouteIOError{}
@@ -171,38 +206,53 @@ func (r *Router) HandleInput(ctx context.Context, sourceId string, payload any)
return
}
outputErrors := r.HandleOutput(routeContext, routeInstance.Output(), payload)
if outputErrors != nil {
outputError := r.HandleOutput(routeCtx, routeInstance.Output(), payload)
if outputError != nil {
if routeIOErrors == nil {
routeIOErrors = []route.RouteIOError{}
}
routeIOErrors = append(routeIOErrors, route.RouteIOError{
Index: routeIndex,
OutputErrors: outputErrors,
Index: routeIndex,
OutputError: outputError,
})
}
routeSpan.End()
})
}
}
routeWaitGroup.Wait()
return routeFound, routeIOErrors
}
func (r *Router) HandleOutput(ctx context.Context, destinationId string, payload any) []error {
func (r *Router) HandleOutput(ctx context.Context, destinationId string, payload any) error {
spanCtx, span := r.tracer.Start(ctx, "router.output", trace.WithAttributes(attribute.String("destination.id", destinationId)))
defer span.End()
var outputErrors []error
for _, moduleInstance := range r.ModuleInstances {
if moduleInstance.Id() == destinationId {
err := moduleInstance.Output(ctx, payload)
if err != nil {
if outputErrors == nil {
outputErrors = []error{}
}
outputErrors = append(outputErrors, err)
r.logger.Error("unable to route output", "module", moduleInstance.Id(), "error", err)
}
}
destinationModule := r.getModule(destinationId)
if destinationModule == nil {
err := errors.New("no module found for destination id")
span.SetStatus(codes.Error, err.Error())
span.RecordError(err)
r.logger.Error("no module found for destination id", "destinationId", destinationId)
return err
}
return outputErrors
moduleOutputCtx, moduleOutputSpan := r.tracer.Start(spanCtx, "module.output", trace.WithAttributes(attribute.String("module.id", destinationModule.Id()), attribute.String("module.type", destinationModule.Type())))
defer moduleOutputSpan.End()
err := destinationModule.Output(moduleOutputCtx, payload)
if err != nil {
moduleOutputSpan.SetStatus(codes.Error, err.Error())
moduleOutputSpan.RecordError(err)
r.logger.Error("module output encountered error", "module", destinationModule.Id(), "error", err)
return err
} else {
moduleOutputSpan.SetStatus(codes.Ok, "module output successful")
}
return nil
}
func (r *Router) RunningConfig() config.Config {
return r.runningConfig
}

View File

@@ -12,41 +12,58 @@ import (
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
"github.com/jwetzell/showbridge-go/internal/route"
"go.opentelemetry.io/otel"
)
type MockModule struct {
var (
tracer = otel.Tracer("showbridge.test")
)
type MockCounterModule struct {
config config.ModuleConfig
ctx context.Context
outputCount int
router route.RouteIO
logger *slog.Logger
cancel context.CancelFunc
}
func (m *MockModule) Id() string {
return m.config.Id
func (mcm *MockCounterModule) Id() string {
return mcm.config.Id
}
func (m *MockModule) Output(context.Context, any) error {
m.outputCount += 1
func (mcm *MockCounterModule) Output(context.Context, any) error {
mcm.outputCount += 1
return nil
}
func (m *MockModule) Run(ctx context.Context) error {
m.ctx = ctx
<-m.ctx.Done()
func (mcm *MockCounterModule) Start(ctx context.Context) error {
router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
if !ok {
return fmt.Errorf("mock.counter could not get router from context")
}
mcm.router = router
moduleContext, cancel := context.WithCancel(ctx)
mcm.ctx = moduleContext
mcm.cancel = cancel
<-mcm.ctx.Done()
return nil
}
func (m *MockModule) Type() string {
return m.config.Type
func (mcm *MockCounterModule) Type() string {
return mcm.config.Type
}
func (mcm *MockCounterModule) Stop() {
mcm.cancel()
}
func init() {
module.RegisterModule(module.ModuleRegistration{
Type: "mock.counter",
New: func(config config.ModuleConfig) (module.Module, error) {
return &MockModule{config: config, logger: slog.Default()}, nil
return &MockCounterModule{config: config, logger: slog.Default()}, nil
},
})
}
@@ -61,7 +78,7 @@ func TestNewRouter(t *testing.T) {
},
}
_, moduleErrors, routeErrors := showbridge.NewRouter(routerConfig)
_, moduleErrors, routeErrors := showbridge.NewRouter(routerConfig, tracer)
if moduleErrors != nil {
t.Fatalf("router should not have returned any module errors: %v", moduleErrors)
@@ -72,6 +89,23 @@ func TestNewRouter(t *testing.T) {
}
}
func TestNewRouterNoModuleId(t *testing.T) {
routerConfig := config.Config{
Modules: []config.ModuleConfig{
{
Id: "",
Type: "mock.counter",
},
},
}
_, moduleErrors, _ := showbridge.NewRouter(routerConfig, tracer)
if moduleErrors == nil {
t.Fatalf("router should have returned 'unknown module' module errors")
}
}
func TestNewRouterUnknownModuleType(t *testing.T) {
routerConfig := config.Config{
Modules: []config.ModuleConfig{
@@ -82,7 +116,7 @@ func TestNewRouterUnknownModuleType(t *testing.T) {
},
}
_, moduleErrors, _ := showbridge.NewRouter(routerConfig)
_, moduleErrors, _ := showbridge.NewRouter(routerConfig, tracer)
if moduleErrors == nil {
t.Fatalf("router should have returned 'unknown module' module errors")
@@ -103,7 +137,7 @@ func TestNewRouterDuplicateModuleId(t *testing.T) {
},
}
_, moduleErrors, _ := showbridge.NewRouter(routerConfig)
_, moduleErrors, _ := showbridge.NewRouter(routerConfig, tracer)
if moduleErrors == nil {
t.Fatalf("router should have returned 'duplicate id' module error")
@@ -126,7 +160,7 @@ func TestRouterInputSingleRoute(t *testing.T) {
},
}
router, moduleErrors, routeErrors := showbridge.NewRouter(routerConfig)
router, moduleErrors, routeErrors := showbridge.NewRouter(routerConfig, tracer)
if moduleErrors != nil {
t.Fatalf("router should not have returned any module errors: %v", moduleErrors)
@@ -139,7 +173,8 @@ func TestRouterInputSingleRoute(t *testing.T) {
routerRunner := sync.WaitGroup{}
routerRunner.Go(func() {
router.Run(t.Context())
router.Start(t.Context())
fmt.Println("router stopped")
})
time.Sleep(time.Second * 1)
@@ -161,7 +196,7 @@ func TestRouterInputSingleRoute(t *testing.T) {
for _, moduleInstance := range router.ModuleInstances {
if moduleInstance.Id() == "mock" {
mockModuleInstance, ok := moduleInstance.(*MockModule)
mockModuleInstance, ok := moduleInstance.(*MockCounterModule)
if !ok {
t.Fatalf("couldn't get mock module")
}
@@ -197,7 +232,7 @@ func TestRouterInputMultipleRoutes(t *testing.T) {
},
}
router, moduleErrors, routeErrors := showbridge.NewRouter(routerConfig)
router, moduleErrors, routeErrors := showbridge.NewRouter(routerConfig, tracer)
if moduleErrors != nil {
t.Fatalf("router should not have returned any module errors: %v", moduleErrors)
@@ -210,7 +245,7 @@ func TestRouterInputMultipleRoutes(t *testing.T) {
routerRunner := sync.WaitGroup{}
routerRunner.Go(func() {
router.Run(t.Context())
router.Start(t.Context())
})
time.Sleep(time.Second * 1)
@@ -231,7 +266,7 @@ func TestRouterInputMultipleRoutes(t *testing.T) {
for _, moduleInstance := range router.ModuleInstances {
if moduleInstance.Id() == "mock" {
mockModuleInstance, ok := moduleInstance.(*MockModule)
mockModuleInstance, ok := moduleInstance.(*MockCounterModule)
if !ok {
t.Fatalf("couldn't get mock module")
}
@@ -268,7 +303,7 @@ func TestRouterInputMultipleModules(t *testing.T) {
},
}
router, moduleErrors, routeErrors := showbridge.NewRouter(routerConfig)
router, moduleErrors, routeErrors := showbridge.NewRouter(routerConfig, tracer)
if moduleErrors != nil {
t.Fatalf("router should not have returned any module errors: %v", moduleErrors)
@@ -281,7 +316,7 @@ func TestRouterInputMultipleModules(t *testing.T) {
routerRunner := sync.WaitGroup{}
routerRunner.Go(func() {
router.Run(t.Context())
router.Start(t.Context())
})
time.Sleep(time.Second * 1)
@@ -316,7 +351,7 @@ func TestRouterInputMultipleModules(t *testing.T) {
for _, moduleInstance := range router.ModuleInstances {
if moduleInstance.Id() == "mock1" {
mockModuleInstance, ok := moduleInstance.(*MockModule)
mockModuleInstance, ok := moduleInstance.(*MockCounterModule)
if !ok {
t.Fatalf("couldn't get mock module")
}
@@ -327,7 +362,7 @@ func TestRouterInputMultipleModules(t *testing.T) {
break
}
if moduleInstance.Id() == "mock2" {
mockModuleInstance, ok := moduleInstance.(*MockModule)
mockModuleInstance, ok := moduleInstance.(*MockCounterModule)
if !ok {
t.Fatalf("couldn't get mock module")
}

Some files were not shown because too many files have changed in this diff Show More