83 Commits

Author SHA1 Message Date
Joel Wetzell
b639a5c786 Merge pull request #29 from jwetzell/feat/int-random
add processors to create random int/uint
2025-12-28 09:06:12 -06:00
Joel Wetzell
d170958d9b add processors to create random int/uint 2025-12-28 09:05:01 -06:00
Joel Wetzell
ed4c14e82b rework route to just process payload 2025-12-28 08:24:12 -06:00
Joel Wetzell
fcb1378784 fix route test 2025-12-27 23:02:37 -06:00
Joel Wetzell
97f4cbeace fix filename 2025-12-27 22:59:50 -06:00
Joel Wetzell
8ffc7d02a5 stuff values into context 2025-12-27 22:59:30 -06:00
Joel Wetzell
3458b52206 Merge pull request #28 from jwetzell/feat/artnet-module
add artnet module and processors
2025-12-26 11:59:59 -06:00
Joel Wetzell
e12b6e098e add artnet packet filter 2025-12-26 11:51:22 -06:00
Joel Wetzell
58cb7766fe change namespace 2025-12-26 11:51:22 -06:00
Joel Wetzell
407f1f3618 add basic artnet decode/encode 2025-12-26 11:51:22 -06:00
Joel Wetzell
3c0f555a6f Merge pull request #27 from jwetzell/chore/time-namespace
move timer and interval to time namespace
2025-12-26 11:18:44 -06:00
Joel Wetzell
6c6e50e0eb move timer and interval to time namespace 2025-12-26 11:17:29 -06:00
Joel Wetzell
0ed4d6669f don't use default mux 2025-12-26 11:01:30 -06:00
Joel Wetzell
cc37559bde use router.Stop 2025-12-26 10:43:04 -06:00
Joel Wetzell
18b307bca1 control flow of router running with better context handling and logging in cmd 2025-12-26 10:39:09 -06:00
Joel Wetzell
17106b5096 add more debug logging to router 2025-12-26 10:38:33 -06:00
Joel Wetzell
df9ffdda9e add debug log about waiting for modules to close 2025-12-26 10:24:31 -06:00
Joel Wetzell
479cf41ea0 use WaitGroup Go function 2025-12-26 10:24:15 -06:00
Joel Wetzell
af833cb212 cleanup debug.log 2025-12-26 10:23:39 -06:00
Joel Wetzell
a6a8e275ee remove unecessary logger 2025-12-26 10:23:33 -06:00
Joel Wetzell
6968539c59 cleanup logging in router 2025-12-26 10:19:29 -06:00
Joel Wetzell
e96913230b logging cleanup 2025-12-26 10:08:24 -06:00
Joel Wetzell
f1dff33704 centralize module logger creation 2025-12-26 09:51:55 -06:00
Joel Wetzell
ff426994e4 add tests for registration and error cases to script.expr 2025-12-24 20:53:46 -06:00
Joel Wetzell
a1275b3b69 add tests for error scenarios to script.js 2025-12-24 20:53:25 -06:00
Joel Wetzell
227d042feb add bitsize and registration tests for float.parse 2025-12-24 20:32:42 -06:00
Joel Wetzell
e53515267e tests for debug.log 2025-12-24 20:32:31 -06:00
Joel Wetzell
502586dec6 add base and bitsize to int.parse tests 2025-12-24 20:25:59 -06:00
Joel Wetzell
cad3714664 add base and bitsize to bad tests 2025-12-24 20:25:46 -06:00
Joel Wetzell
cf6519b594 more tests for script.js 2025-12-24 20:16:02 -06:00
Joel Wetzell
7dcac9470a tests for general processor stuff 2025-12-24 20:15:46 -06:00
Joel Wetzell
e5268f42f8 add more tests for int/uint parsers 2025-12-24 20:15:29 -06:00
Joel Wetzell
ccff105e37 test error cleanup 2025-12-24 19:45:24 -06:00
Joel Wetzell
6611821155 add tests for route 2025-12-24 19:16:52 -06:00
Joel Wetzell
7cde1c244a cleanup route interface 2025-12-24 19:16:46 -06:00
Joel Wetzell
70878e6df6 Merge pull request #26 from jwetzell/fix/slip-framer
fix incorrect slip frame encoding
2025-12-24 19:15:17 -06:00
Joel Wetzell
bd005da358 fix incorrect slip frame encoding 2025-12-24 19:13:33 -06:00
Joel Wetzell
25f2ec30c2 rework framer tests 2025-12-24 19:13:04 -06:00
Joel Wetzell
e679bf2b46 add test for string.create 2025-12-24 18:18:08 -06:00
Joel Wetzell
460d2f051d switch separator test to use GetFramer 2025-12-24 18:17:57 -06:00
Joel Wetzell
efdcbae5c4 no need for framer to return two values 2025-12-24 18:17:41 -06:00
Joel Wetzell
076a13f48a Merge pull request #25 from jwetzell/feat/sleep-processor
add a processor that sleeps
2025-12-24 16:33:41 -06:00
Joel Wetzell
971eea6e41 add a processor that sleeps 2025-12-24 16:32:44 -06:00
Joel Wetzell
1172159455 align variable names with struct name 2025-12-24 16:06:01 -06:00
Joel Wetzell
ecb415f321 add basic test for raw framer 2025-12-24 16:02:23 -06:00
Joel Wetzell
b552672011 update README 2025-12-24 15:18:32 -06:00
Joel Wetzell
168265f0a9 move docker building to main release workflow 2025-12-24 14:05:40 -06:00
Joel Wetzell
caee7b269c fix docker job name 2025-12-24 13:26:28 -06:00
Joel Wetzell
b0cc47236f add debug flag and working directory to launch.json 2025-12-24 13:26:09 -06:00
Joel Wetzell
f6c2b1d9ac Merge pull request #24 from jwetzell/feat/docker
add docker image
2025-12-24 13:24:29 -06:00
Joel Wetzell
f977b845be add github action to build docker image 2025-12-24 13:22:52 -06:00
Joel Wetzell
0166383978 set workdir 2025-12-24 13:22:45 -06:00
Joel Wetzell
66cfaa3091 update Go version in dockerfile 2025-12-24 13:22:34 -06:00
Joel Wetzell
0904d2fcb8 add more to dockerignore 2025-12-24 13:21:53 -06:00
Joel Wetzell
6a13c38e77 add dockerfile 2025-12-24 12:44:25 -06:00
Joel Wetzell
1a8ccfc64a align variable name 2025-12-24 10:54:30 -06:00
Joel Wetzell
2e3bb408c3 Merge pull request #23 from jwetzell/feat/configurable-number-parsing
make base and bitsize configurable for number parsers
2025-12-24 10:35:06 -06:00
Joel Wetzell
3d75165a61 cleanout TODO 2025-12-24 10:34:20 -06:00
Joel Wetzell
50f755f914 make base and bitsize configurable for number parsers 2025-12-24 10:34:06 -06:00
Joel Wetzell
b10e296d0a Merge pull request #22 from jwetzell/feat/udp-server-buffersize
allow configuring buffer size for udp server
2025-12-24 10:27:14 -06:00
Joel Wetzell
91c44420cb allow configuring buffer size for udp server 2025-12-24 10:24:24 -06:00
Joel Wetzell
c1b2fa714e update midi module section 2025-12-23 15:06:23 -06:00
Joel Wetzell
ff1949ce69 use errors.New when not formatting 2025-12-23 15:00:30 -06:00
Joel Wetzell
76583b1fb8 Merge pull request #20 from jwetzell/dependabot/go_modules/github.com/expr-lang/expr-1.17.7
Bump github.com/expr-lang/expr from 1.17.6 to 1.17.7
2025-12-23 14:29:05 -06:00
dependabot[bot]
e6433878b5 Bump github.com/expr-lang/expr from 1.17.6 to 1.17.7
Bumps [github.com/expr-lang/expr](https://github.com/expr-lang/expr) from 1.17.6 to 1.17.7.
- [Release notes](https://github.com/expr-lang/expr/releases)
- [Commits](https://github.com/expr-lang/expr/compare/v1.17.6...v1.17.7)

---
updated-dependencies:
- dependency-name: github.com/expr-lang/expr
  dependency-version: 1.17.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-23 20:27:34 +00:00
Joel Wetzell
3ad067cbb8 Merge pull request #19 from jwetzell/dependabot/go_modules/github.com/emiago/sipgo-1.0.1
Bump github.com/emiago/sipgo from 1.0.1-alpha.0.20251212165843-9c9bcdf9126f to 1.0.1
2025-12-23 14:26:28 -06:00
Joel Wetzell
30b406d601 Merge pull request #21 from jwetzell/dependabot/go_modules/github.com/nats-io/nats.go-1.48.0
Bump github.com/nats-io/nats.go from 1.47.0 to 1.48.0
2025-12-23 14:25:25 -06:00
dependabot[bot]
7315d40015 Bump github.com/nats-io/nats.go from 1.47.0 to 1.48.0
Bumps [github.com/nats-io/nats.go](https://github.com/nats-io/nats.go) from 1.47.0 to 1.48.0.
- [Release notes](https://github.com/nats-io/nats.go/releases)
- [Commits](https://github.com/nats-io/nats.go/compare/v1.47.0...v1.48.0)

---
updated-dependencies:
- dependency-name: github.com/nats-io/nats.go
  dependency-version: 1.48.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-22 17:33:17 +00:00
dependabot[bot]
e7105b8b39 Bump github.com/emiago/sipgo
Bumps [github.com/emiago/sipgo](https://github.com/emiago/sipgo) from 1.0.1-alpha.0.20251212165843-9c9bcdf9126f to 1.0.1.
- [Release notes](https://github.com/emiago/sipgo/releases)
- [Commits](https://github.com/emiago/sipgo/commits/v1.0.1)

---
updated-dependencies:
- dependency-name: github.com/emiago/sipgo
  dependency-version: 1.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-22 17:33:08 +00:00
Joel Wetzell
ec40194ecb add TODO 2025-12-21 15:13:06 -06:00
Joel Wetzell
85d8dc5787 cleanup error messages 2025-12-20 14:18:56 -06:00
Joel Wetzell
b27bfb1a6d don't wrap string.filter pattern 2025-12-20 10:59:33 -06:00
Joel Wetzell
fb9c0bc657 cleanup error messages 2025-12-20 07:50:22 -06:00
Joel Wetzell
d32d23041b add usage to cmd 2025-12-19 22:30:18 -06:00
Joel Wetzell
b4149df00a setup slog inside cmd with debug and json flag 2025-12-19 22:27:05 -06:00
Joel Wetzell
61bd4b64f5 setup loggers inside modules with attributes 2025-12-19 22:26:25 -06:00
Joel Wetzell
38d73881c9 return error don't just log it 2025-12-19 22:25:26 -06:00
Joel Wetzell
3138bdfcdb remove TODO's 2025-12-19 12:32:13 -06:00
Joel Wetzell
ba862300b2 fix config example 2025-12-19 09:02:05 -06:00
Joel Wetzell
e414bf336e Merge pull request #18 from jwetzell/test/script-processors
add basic tests for script processors
2025-12-18 20:06:21 -06:00
Joel Wetzell
da95fb5c75 add basic test for script.js 2025-12-18 20:04:52 -06:00
Joel Wetzell
1fdc30aa9e add basic test for script.expr 2025-12-18 20:04:41 -06:00
Joel Wetzell
128920dff9 fix typo in string.split test 2025-12-18 20:03:50 -06:00
86 changed files with 2580 additions and 686 deletions

5
.dockerignore Normal file
View File

@@ -0,0 +1,5 @@
Dockerfile
.github
.vscode
README.md
LICENSE

View File

@@ -3,7 +3,7 @@ name: showbridge release
on: on:
push: push:
tags: tags:
- "*" - "v*"
permissions: permissions:
contents: write contents: write
@@ -29,3 +29,40 @@ jobs:
args: release --clean args: release --clean
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
docker:
runs-on: ubuntu-latest
needs: goreleaser
steps:
- name: Check out repository
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Login to Docker Hub
uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Setup Docker metadata
id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: |
jwetzell/showbridge
- name: Build and push
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
push: true
context: ./
file: ./Dockerfile
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64,linux/arm/v7

4
.vscode/launch.json vendored
View File

@@ -10,7 +10,9 @@
"type": "go", "type": "go",
"request": "launch", "request": "launch",
"mode": "auto", "mode": "auto",
"program": "cmd/showbridge" "program": "cmd/showbridge",
"args": ["--debug"],
"cwd": "./"
} }
] ]
} }

15
Dockerfile Normal file
View File

@@ -0,0 +1,15 @@
ARG GO_VERSION=1.25.5
FROM golang:${GO_VERSION}-alpine AS build
RUN apk --no-cache add ca-certificates tzdata
WORKDIR /build
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build ./cmd/showbridge
FROM scratch
WORKDIR /app
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
COPY --from=build /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=build /build/showbridge /app/showbridge
ENTRYPOINT [ "/app/showbridge" ]

View File

@@ -6,28 +6,37 @@ Simple protocol router _/s_
</div> </div>
<p align="center">
<a href="https://github.com/jwetzell/showbridge-go/releases">Releases</a> ·
<a href="https://docs.showbridge.io">Documentation</a>
</p>
### Supported Protocols ### Supported Protocols
- HTTP - HTTP
- client
- server
- UDP - UDP
- client
- server
- TCP - TCP
- client
- server
- [MQTT](https://mqtt.org/) - [MQTT](https://mqtt.org/)
- client
- [NATS](https://nats.io/) - [NATS](https://nats.io/)
- client
- [PosiStageNet](https://posistage.net/) - [PosiStageNet](https://posistage.net/)
- client - MIDI (not included in pre-built binaries yet)
- MIDI - Serial (not included in pre-built binaries yet)
- client (not included in pre-built binaries yet) - [OSC](https://opensoundcontrol.stanford.edu/spec-1_0.html)
- [FreeD](https://ptzoptics.com/freed/)
- [SIP](https://en.wikipedia.org/wiki/Session_Initiation_Protocol) - [SIP](https://en.wikipedia.org/wiki/Session_Initiation_Protocol)
- call server
- [DTMF](https://en.wikipedia.org/wiki/Dual-tone_multi-frequency_signaling) server
### CLI Usage
```
NAME:
showbridge - Simple protocol router /s
USAGE:
showbridge [global options]
GLOBAL OPTIONS:
--config string path to config file (default: "./config.yaml")
--debug set log level to DEBUG
--json log using JSON
--help, -h show help
--version, -v print the version
```

View File

@@ -2,10 +2,11 @@ package main
import ( import (
"context" "context"
"fmt" "errors"
"log/slog" "log/slog"
"os" "os"
"os/signal" "os/signal"
"sync"
"github.com/jwetzell/showbridge-go" "github.com/jwetzell/showbridge-go"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
@@ -20,37 +21,29 @@ var (
func main() { func main() {
cmd := &cli.Command{ cmd := &cli.Command{
Name: "showbridge", Name: "showbridge",
Usage: "Simple protocol router /s",
Version: version, Version: version,
Flags: []cli.Flag{ Flags: []cli.Flag{
&cli.StringFlag{ &cli.StringFlag{
Name: "config", Name: "config",
Value: "./config.yaml", Value: "./config.yaml",
Usage: "path to config file",
},
&cli.BoolFlag{
Name: "debug",
Value: false,
Usage: "set log level to DEBUG",
},
&cli.BoolFlag{
Name: "json",
Value: false,
Usage: "log using JSON",
}, },
}, },
Action: func(ctx context.Context, c *cli.Command) error { Action: run,
configPath := c.String("config")
if configPath == "" {
return fmt.Errorf("config value cannot be empty")
} }
config, err := readConfig(configPath) ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
if err != nil {
return err
}
router, moduleErrors, routeErrors := showbridge.NewRouter(ctx, config)
for _, moduleError := range moduleErrors {
slog.Error("problem initializing module", "index", moduleError.Index, "error", moduleError.Error)
}
for _, routeError := range routeErrors {
slog.Error("problem initializing route", "index", routeError.Index, "error", routeError.Error)
}
router.Run()
return nil
},
}
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Interrupt)
defer cancel() defer cancel()
err := cmd.Run(ctx, os.Args) err := cmd.Run(ctx, os.Args)
@@ -76,3 +69,60 @@ func readConfig(configPath string) (config.Config, error) {
return cfg, nil return cfg, nil
} }
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
}
logLevel := slog.LevelInfo
if c.Bool("debug") {
logLevel = slog.LevelDebug
}
logHandlerOptions := &slog.HandlerOptions{
Level: logLevel,
}
logOutput := os.Stderr
var logHandler slog.Handler = slog.NewTextHandler(logOutput, logHandlerOptions)
if c.Bool("json") {
logHandler = slog.NewJSONHandler(logOutput, logHandlerOptions)
}
slog.SetDefault(slog.New(logHandler))
commandLogger := slog.Default().With("component", "cmd")
router, moduleErrors, routeErrors := showbridge.NewRouter(context.Background(), config)
for _, moduleError := range moduleErrors {
commandLogger.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)
}
routerRunner := sync.WaitGroup{}
routerRunner.Go(func() {
router.Run()
})
<-ctx.Done()
commandLogger.Debug("shutting down router")
router.Stop()
commandLogger.Debug("waiting for router to exit")
routerRunner.Wait()
return nil
}

View File

@@ -1,6 +1,6 @@
modules: modules:
- id: http - id: http
type: net.http.server type: http.server
params: params:
port: 3000 port: 3000
- id: udp - id: udp

9
go.mod
View File

@@ -1,16 +1,17 @@
module github.com/jwetzell/showbridge-go module github.com/jwetzell/showbridge-go
go 1.25.3 go 1.25.5
require ( require (
github.com/eclipse/paho.mqtt.golang v1.5.1 github.com/eclipse/paho.mqtt.golang v1.5.1
github.com/emiago/diago v0.23.1-0.20251211215055-e1d875617111 github.com/emiago/diago v0.23.1-0.20251211215055-e1d875617111
github.com/emiago/sipgo v1.0.1-alpha.0.20251212165843-9c9bcdf9126f github.com/emiago/sipgo v1.0.1
github.com/expr-lang/expr v1.17.6 github.com/expr-lang/expr v1.17.7
github.com/jwetzell/artnet-go v0.0.0-20251223201031-5097901aa9db
github.com/jwetzell/free-d-go v0.1.0 github.com/jwetzell/free-d-go v0.1.0
github.com/jwetzell/osc-go v0.1.0 github.com/jwetzell/osc-go v0.1.0
github.com/jwetzell/psn-go v0.3.0 github.com/jwetzell/psn-go v0.3.0
github.com/nats-io/nats.go v1.47.0 github.com/nats-io/nats.go v1.48.0
github.com/urfave/cli/v3 v3.6.1 github.com/urfave/cli/v3 v3.6.1
gitlab.com/gomidi/midi/v2 v2.3.18 gitlab.com/gomidi/midi/v2 v2.3.18
go.bug.st/serial v1.6.4 go.bug.st/serial v1.6.4

14
go.sum
View File

@@ -8,10 +8,10 @@ github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2I
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU= github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
github.com/emiago/diago v0.23.1-0.20251211215055-e1d875617111 h1:jqhOZbH40pf2jiUhGaBNk334wOtNYvAaXg/mHOXhy/Y= github.com/emiago/diago v0.23.1-0.20251211215055-e1d875617111 h1:jqhOZbH40pf2jiUhGaBNk334wOtNYvAaXg/mHOXhy/Y=
github.com/emiago/diago v0.23.1-0.20251211215055-e1d875617111/go.mod h1:3vLCCq8/G/Ei5I64IHtrmBTag+nPLcgXcKeN1KkLtuc= github.com/emiago/diago v0.23.1-0.20251211215055-e1d875617111/go.mod h1:3vLCCq8/G/Ei5I64IHtrmBTag+nPLcgXcKeN1KkLtuc=
github.com/emiago/sipgo v1.0.1-alpha.0.20251212165843-9c9bcdf9126f h1:n3wpx7ZyJBr0popXyATnTUZp/AdLIHjKbnKzr4ruF7g= github.com/emiago/sipgo v1.0.1 h1:8eCZ6L/VX3isyByyv1RrBoQ5GyBoRXBHkNMYjwacRfk=
github.com/emiago/sipgo v1.0.1-alpha.0.20251212165843-9c9bcdf9126f/go.mod h1:DuwAxBZhKMqIzQFPGZb1MVAGU6Wuxj64oTOhd5dx/FY= github.com/emiago/sipgo v1.0.1/go.mod h1:DuwAxBZhKMqIzQFPGZb1MVAGU6Wuxj64oTOhd5dx/FY=
github.com/expr-lang/expr v1.17.6 h1:1h6i8ONk9cexhDmowO/A64VPxHScu7qfSl2k8OlINec= github.com/expr-lang/expr v1.17.7 h1:Q0xY/e/2aCIp8g9s/LGvMDCC5PxYlvHgDZRQ4y16JX8=
github.com/expr-lang/expr v1.17.6/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/expr-lang/expr v1.17.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/go-audio/riff v1.0.0 h1:d8iCGbDvox9BfLagY94fBynxSPHO80LmZCaOsmKxokA= 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-audio/riff v1.0.0/go.mod h1:l3cQwc85y79NQFCRB7TiPoNiaijp6q8Z0Uv38rVG498=
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
@@ -30,6 +30,8 @@ github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4= github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y= github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
github.com/jwetzell/artnet-go v0.0.0-20251223201031-5097901aa9db h1:PR9sdDEWBufi8/3yiQHRXncLNKUHBMKiEBDtdNWsK/E=
github.com/jwetzell/artnet-go v0.0.0-20251223201031-5097901aa9db/go.mod h1:gli97Z32a0kMkZ6taoTiK7/lqHcF/dhiGjGJdx/PxqA=
github.com/jwetzell/free-d-go v0.1.0 h1:xHt6dvyit98X+OC3jVzV0aLidxbyzi3vI9QiYkteEtA= github.com/jwetzell/free-d-go v0.1.0 h1:xHt6dvyit98X+OC3jVzV0aLidxbyzi3vI9QiYkteEtA=
github.com/jwetzell/free-d-go v0.1.0/go.mod h1:KmrkooRARRaxJTBSPvwt/6IMAIaHH1R8bSA8cwbbELw= github.com/jwetzell/free-d-go v0.1.0/go.mod h1:KmrkooRARRaxJTBSPvwt/6IMAIaHH1R8bSA8cwbbELw=
github.com/jwetzell/osc-go v0.1.0 h1:EXxup5VWBErHot2Ri4MFToPf6KCzLDTbCt2x6GLfw8I= github.com/jwetzell/osc-go v0.1.0 h1:EXxup5VWBErHot2Ri4MFToPf6KCzLDTbCt2x6GLfw8I=
@@ -44,8 +46,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM= github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U=
github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g=
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE= github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=

View File

@@ -1,9 +1,5 @@
package framer package framer
import (
"fmt"
)
type Framer interface { type Framer interface {
Decode([]byte) [][]byte Decode([]byte) [][]byte
Encode([]byte) []byte Encode([]byte) []byte
@@ -11,19 +7,19 @@ type Framer interface {
Buffer() []byte Buffer() []byte
} }
func GetFramer(framingType string) (Framer, error) { func GetFramer(framingType string) Framer {
switch framingType { switch framingType {
case "CR": case "CR":
return NewByteSeparatorFramer([]byte{'\r'}), nil return NewByteSeparatorFramer([]byte{'\r'})
case "LF": case "LF":
return NewByteSeparatorFramer([]byte{'\n'}), nil return NewByteSeparatorFramer([]byte{'\n'})
case "CRLF": case "CRLF":
return NewByteSeparatorFramer([]byte{'\r', '\n'}), nil return NewByteSeparatorFramer([]byte{'\r', '\n'})
case "SLIP": case "SLIP":
return NewSlipFramer(), nil return NewSlipFramer()
case "RAW": case "RAW":
return NewRawFramer(), nil return NewRawFramer()
default: default:
return nil, fmt.Errorf("unknown framing method: %s", framingType) return nil
} }
} }

View File

@@ -0,0 +1,15 @@
package framer_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/framer"
)
func TestNilGetFramer(t *testing.T) {
nilFramer := framer.GetFramer("asldfiudchuehrkbjbkjrbb")
if nilFramer != nil {
t.Fatalf("Expected nil framer, got %v", nilFramer)
}
}

View File

@@ -0,0 +1,78 @@
package framer_test
import (
"slices"
"testing"
"github.com/jwetzell/showbridge-go/internal/framer"
)
func TestGoodRawFramerDecode(t *testing.T) {
tests := []struct {
name string
framer framer.Framer
input []byte
expected [][]byte
}{
{
name: "basic raw framer",
framer: framer.GetFramer("RAW"),
input: []byte("Hello\nWorld\nThis is a test\n"),
expected: [][]byte{
[]byte("Hello\nWorld\nThis is a test\n"),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
frames := test.framer.Decode(test.input)
if len(frames) != len(test.expected) {
t.Fatalf("raw framer got %d frames, expected %d", len(frames), len(test.expected))
}
for i, frame := range frames {
if !slices.Equal(frame, test.expected[i]) {
t.Errorf("raw framer frame %d got %s, expected %s", i, frame, test.expected[i])
}
}
})
}
}
func TestGoodRawFramerEncode(t *testing.T) {
tests := []struct {
name string
framer framer.Framer
expected []byte
input []byte
}{
{
name: "basic raw framer",
framer: framer.GetFramer("RAW"),
expected: []byte("Hello\nWorld\nThis is a test\n"),
input: []byte("Hello\nWorld\nThis is a test\n"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
frame := test.framer.Encode(test.input)
if !slices.Equal(frame, test.expected) {
t.Fatalf("raw frame encode got %s, expected %s", frame, test.expected)
}
})
}
}
func TestRawFramerBuffer(t *testing.T) {
framer := framer.GetFramer("RAW")
framer.Decode([]byte("Hello, World!"))
if !slices.Equal(framer.Buffer(), []byte{}) {
t.Fatalf("raw framer buffer got %s, expected empty", framer.Buffer())
}
framer.Clear()
if !slices.Equal(framer.Buffer(), []byte{}) {
t.Fatalf("raw framer buffer got %s, expected empty after clear", framer.Buffer())
}
}

View File

@@ -7,7 +7,7 @@ import (
"github.com/jwetzell/showbridge-go/internal/framer" "github.com/jwetzell/showbridge-go/internal/framer"
) )
func TestGoodSeparatorFramer(t *testing.T) { func TestGoodSeparatorFramerDecode(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
framer framer.Framer framer framer.Framer
@@ -17,7 +17,7 @@ func TestGoodSeparatorFramer(t *testing.T) {
}{ }{
{ {
name: "new line separator", name: "new line separator",
framer: framer.NewByteSeparatorFramer([]byte{0x0a}), framer: framer.GetFramer("LF"),
input: []byte("Hello\nWorld\nThis is a test\n"), input: []byte("Hello\nWorld\nThis is a test\n"),
expected: [][]byte{ expected: [][]byte{
[]byte("Hello"), []byte("Hello"),
@@ -26,9 +26,20 @@ func TestGoodSeparatorFramer(t *testing.T) {
}, },
buffer: []byte{}, buffer: []byte{},
}, },
{
name: "CR separator",
framer: framer.GetFramer("CR"),
input: []byte("Hello\rWorld\rThis is a test\r"),
expected: [][]byte{
[]byte("Hello"),
[]byte("World"),
[]byte("This is a test"),
},
buffer: []byte{},
},
{ {
name: "CRLF separator", name: "CRLF separator",
framer: framer.NewByteSeparatorFramer([]byte{0x0d, 0x0a}), framer: framer.GetFramer("CRLF"),
input: []byte("Hello\r\nWorld\r\nThis is a test\r\n"), input: []byte("Hello\r\nWorld\r\nThis is a test\r\n"),
expected: [][]byte{ expected: [][]byte{
[]byte("Hello"), []byte("Hello"),
@@ -39,7 +50,7 @@ func TestGoodSeparatorFramer(t *testing.T) {
}, },
{ {
name: "extra data after separator", name: "extra data after separator",
framer: framer.NewByteSeparatorFramer([]byte{0x0d, 0x0a}), framer: framer.GetFramer("CRLF"),
input: []byte("Hello\r\nWorld\r\nThis is a test\r\nextra"), input: []byte("Hello\r\nWorld\r\nThis is a test\r\nextra"),
expected: [][]byte{ expected: [][]byte{
[]byte("Hello"), []byte("Hello"),
@@ -54,7 +65,7 @@ func TestGoodSeparatorFramer(t *testing.T) {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
frames := test.framer.Decode(test.input) frames := test.framer.Decode(test.input)
if len(frames) != len(test.expected) { if len(frames) != len(test.expected) {
t.Errorf("separator framer got %d frames, expected %d", len(frames), len(test.expected)) t.Fatalf("separator framer got %d frames, expected %d", len(frames), len(test.expected))
} }
for i, frame := range frames { for i, frame := range frames {
if !slices.Equal(frame, test.expected[i]) { if !slices.Equal(frame, test.expected[i]) {
@@ -62,8 +73,58 @@ func TestGoodSeparatorFramer(t *testing.T) {
} }
} }
if !slices.Equal(test.framer.Buffer(), test.buffer) { if !slices.Equal(test.framer.Buffer(), test.buffer) {
t.Errorf("separator framer buffer got %s, expected %s", test.framer.Buffer(), test.buffer) t.Fatalf("separator framer buffer got %s, expected %s", test.framer.Buffer(), test.buffer)
} }
}) })
} }
} }
func TestGoodSeparatorFramerEncode(t *testing.T) {
tests := []struct {
name string
framer framer.Framer
input []byte
expected []byte
}{
{
name: "new line separator",
framer: framer.GetFramer("LF"),
input: []byte("Hello"),
expected: []byte("Hello\n"),
},
{
name: "CR separator",
framer: framer.GetFramer("CR"),
input: []byte("Hello"),
expected: []byte("Hello\r"),
},
{
name: "CRLF separator",
framer: framer.GetFramer("CRLF"),
input: []byte("Hello"),
expected: []byte("Hello\r\n"),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
frame := test.framer.Encode(test.input)
if !slices.Equal(frame, test.expected) {
t.Fatalf("separator framer got %s, expected %s", frame, test.expected)
}
})
}
}
func TestSeparatorFrameBuffer(t *testing.T) {
framer := framer.GetFramer("LF")
framer.Decode([]byte("Hello\nWorld\nThis is a test\nextra"))
if !slices.Equal(framer.Buffer(), []byte("extra")) {
t.Fatalf("separator framer buffer got %s, expected %s", framer.Buffer(), []byte("extra"))
}
framer.Clear()
if !slices.Equal(framer.Buffer(), []byte{}) {
t.Fatalf("separator framer buffer got %s, expected empty slice", framer.Buffer())
}
}

View File

@@ -59,9 +59,9 @@ func (sf *SlipFramer) Encode(data []byte) []byte {
for _, byteToEncode := range data { for _, byteToEncode := range data {
switch byteToEncode { switch byteToEncode {
case END: case END:
encodedBytes = append(encodedBytes, ESC_END) encodedBytes = append(encodedBytes, ESC, ESC_END)
case ESC: case ESC:
encodedBytes = append(encodedBytes, ESC_ESC) encodedBytes = append(encodedBytes, ESC, ESC_ESC)
default: default:
encodedBytes = append(encodedBytes, byteToEncode) encodedBytes = append(encodedBytes, byteToEncode)
} }

View File

@@ -7,7 +7,7 @@ import (
"github.com/jwetzell/showbridge-go/internal/framer" "github.com/jwetzell/showbridge-go/internal/framer"
) )
func TestGoodSLIPFramer(t *testing.T) { func TestGoodSLIPFramerDecode(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
framer framer.Framer framer framer.Framer
@@ -17,20 +17,34 @@ func TestGoodSLIPFramer(t *testing.T) {
}{ }{
{ {
name: "OSC SLIP messages", name: "OSC SLIP messages",
framer: framer.NewSlipFramer(), framer: framer.GetFramer("SLIP"),
input: []byte{0xc0, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0xc0}, input: []byte{0xc0, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0xc0},
expected: [][]byte{ expected: [][]byte{
{0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00}, {0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00},
}, },
buffer: []byte{}, buffer: []byte{},
}, },
{
name: "SLIP decode escaped end",
framer: framer.GetFramer("SLIP"),
expected: [][]byte{{0xc0}},
input: []byte{0xc0, 0xdb, 0xdc, 0xc0},
buffer: []byte{},
},
{
name: "SLIP decode escaped escape",
framer: framer.GetFramer("SLIP"),
expected: [][]byte{{0xdb}},
input: []byte{0xc0, 0xdb, 0xdd, 0xc0},
buffer: []byte{},
},
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
frames := test.framer.Decode(test.input) frames := test.framer.Decode(test.input)
if len(frames) != len(test.expected) { if len(frames) != len(test.expected) {
t.Errorf("SLIP framer got %d frames, expected %d", len(frames), len(test.expected)) t.Fatalf("SLIP framer got %d frames, expected %d", len(frames), len(test.expected))
} }
for i, frame := range frames { for i, frame := range frames {
if !slices.Equal(frame, test.expected[i]) { if !slices.Equal(frame, test.expected[i]) {
@@ -38,8 +52,59 @@ func TestGoodSLIPFramer(t *testing.T) {
} }
} }
if !slices.Equal(test.framer.Buffer(), test.buffer) { if !slices.Equal(test.framer.Buffer(), test.buffer) {
t.Errorf("SLIP framer buffer got %s, expected %s", test.framer.Buffer(), test.buffer) t.Fatalf("SLIP framer buffer got %s, expected %s", test.framer.Buffer(), test.buffer)
} }
}) })
} }
} }
func TestGoodSLIPFramerEncode(t *testing.T) {
tests := []struct {
name string
framer framer.Framer
input []byte
expected []byte
}{
{
name: "OSC SLIP messages",
framer: framer.GetFramer("SLIP"),
input: []byte{
0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00,
},
expected: []byte{0xc0, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0xc0},
},
{
name: "SLIP encode end",
framer: framer.GetFramer("SLIP"),
input: []byte{0xc0},
expected: []byte{0xc0, 0xdb, 0xdc, 0xc0},
},
{
name: "SLIP encode esc",
framer: framer.GetFramer("SLIP"),
input: []byte{0xdb},
expected: []byte{0xc0, 0xdb, 0xdd, 0xc0},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
frame := test.framer.Encode(test.input)
if !slices.Equal(frame, test.expected) {
t.Fatalf("SLIP framer frame got %s, expected %s", frame, test.expected)
}
})
}
}
func TestSlipFramerBuffer(t *testing.T) {
framer := framer.GetFramer("SLIP")
framer.Decode([]byte{0xc0, 0x2f, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x00, 0x00, 0x2c, 0x00, 0x00, 0x00, 0xc0, 0xc0, 0x45})
if !slices.Equal(framer.Buffer(), []byte{0x45}) {
t.Fatalf("SLIP framer buffer got %s, expected %s", framer.Buffer(), []byte{0x45})
}
framer.Clear()
if !slices.Equal(framer.Buffer(), []byte{}) {
t.Fatalf("SLIP framer buffer got %s, expected empty slice", framer.Buffer())
}
}

View File

@@ -2,7 +2,7 @@ package module
import ( import (
"context" "context"
"fmt" "errors"
"log/slog" "log/slog"
"net/http" "net/http"
"time" "time"
@@ -16,14 +16,21 @@ type HTTPClient struct {
ctx context.Context ctx context.Context
client *http.Client client *http.Client
router route.RouteIO router route.RouteIO
logger *slog.Logger
} }
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "http.client", Type: "http.client",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) { New: func(ctx context.Context, config config.ModuleConfig) (Module, error) {
return &HTTPClient{config: config, ctx: ctx, router: router}, nil router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
if !ok {
return nil, errors.New("http.client unable to get router from context")
}
return &HTTPClient{config: config, ctx: ctx, router: router, logger: CreateLogger(config)}, nil
}, },
}) })
} }
@@ -43,20 +50,20 @@ func (hc *HTTPClient) Run() error {
} }
<-hc.ctx.Done() <-hc.ctx.Done()
slog.Debug("router context done in module", "id", hc.Id()) hc.logger.Debug("done")
return nil return nil
} }
func (hc *HTTPClient) Output(payload any) error { func (hc *HTTPClient) Output(ctx context.Context, payload any) error {
payloadRequest, ok := payload.(*http.Request) payloadRequest, ok := payload.(*http.Request)
if !ok { if !ok {
return fmt.Errorf("http.client is only able to output an http.Request") return errors.New("http.client is only able to output an http.Request")
} }
if hc.client == nil { if hc.client == nil {
return fmt.Errorf("http.client client is nil") return errors.New("http.client client is nil")
} }
response, err := hc.client.Do(payloadRequest) response, err := hc.client.Do(payloadRequest)

View File

@@ -3,6 +3,7 @@ package module
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"log/slog" "log/slog"
"net/http" "net/http"
@@ -16,6 +17,7 @@ type HTTPServer struct {
Port uint16 Port uint16
ctx context.Context ctx context.Context
router route.RouteIO router route.RouteIO
logger *slog.Logger
} }
type ResponseData struct { type ResponseData struct {
@@ -26,20 +28,26 @@ type ResponseData struct {
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "http.server", Type: "http.server",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) { New: func(ctx context.Context, config config.ModuleConfig) (Module, error) {
params := config.Params params := config.Params
port, ok := params["port"] port, ok := params["port"]
if !ok { if !ok {
return nil, fmt.Errorf("http.server requires a port parameter") return nil, errors.New("http.server requires a port parameter")
} }
portNum, ok := port.(float64) portNum, ok := port.(float64)
if !ok { if !ok {
return nil, fmt.Errorf("http.server port must be uint16") return nil, errors.New("http.server port must be uint16")
} }
return &HTTPServer{Port: uint16(portNum), config: config, ctx: ctx, router: router}, nil router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
if !ok {
return nil, errors.New("http.server unable to get router from context")
}
return &HTTPServer{Port: uint16(portNum), config: config, ctx: ctx, router: router, logger: CreateLogger(config)}, nil
}, },
}) })
} }
@@ -52,7 +60,7 @@ func (hs *HTTPServer) Type() string {
return hs.config.Type return hs.config.Type
} }
func (hs *HTTPServer) HandleDefault(w http.ResponseWriter, r *http.Request) { func (hs *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
response := ResponseData{ response := ResponseData{
@@ -80,30 +88,29 @@ func (hs *HTTPServer) HandleDefault(w http.ResponseWriter, r *http.Request) {
} }
func (hs *HTTPServer) Run() error { func (hs *HTTPServer) Run() error {
http.HandleFunc("/", hs.HandleDefault)
httpServer := &http.Server{ httpServer := &http.Server{
Addr: fmt.Sprintf(":%d", hs.Port), Addr: fmt.Sprintf(":%d", hs.Port),
Handler: http.DefaultServeMux, Handler: hs,
} }
go func() { go func() {
<-hs.ctx.Done() <-hs.ctx.Done()
slog.Debug("router context done in module", "id", hs.Id())
httpServer.Close() httpServer.Close()
}() }()
err := httpServer.ListenAndServe() err := httpServer.ListenAndServe()
slog.Debug("http.server closed", "id", hs.Id())
// TODO(jwetzell): handle server closed error differently // TODO(jwetzell): handle server closed error differently
if err != nil { if err != nil {
if err.Error() != "http: Server closed" {
return err return err
} }
}
<-hs.ctx.Done() <-hs.ctx.Done()
hs.logger.Debug("done")
return nil return nil
} }
func (hs *HTTPServer) Output(payload any) error { func (hs *HTTPServer) Output(ctx context.Context, payload any) error {
return fmt.Errorf("http.server output is not implemented") return errors.New("http.server output is not implemented")
} }

View File

@@ -4,6 +4,7 @@ package module
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"log/slog" "log/slog"
@@ -19,49 +20,56 @@ type MIDIInput struct {
router route.RouteIO router route.RouteIO
Port string Port string
SendFunc func(midi.Message) error SendFunc func(midi.Message) error
logger *slog.Logger
} }
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "midi.input", Type: "midi.input",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) { New: func(ctx context.Context, config config.ModuleConfig) (Module, error) {
params := config.Params params := config.Params
port, ok := params["port"] port, ok := params["port"]
if !ok { if !ok {
return nil, fmt.Errorf("midi.input requires a port parameter") return nil, errors.New("midi.input requires a port parameter")
} }
portString, ok := port.(string) portString, ok := port.(string)
if !ok { if !ok {
return nil, fmt.Errorf("midi.input port must be a string") return nil, errors.New("midi.input port must be a string")
} }
return &MIDIInput{config: config, Port: portString, ctx: ctx, router: router}, nil router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
if !ok {
return nil, errors.New("midi.input unable to get router from context")
}
return &MIDIInput{config: config, Port: portString, ctx: ctx, router: router, logger: CreateLogger(config)}, nil
}, },
}) })
} }
func (mc *MIDIInput) Id() string { func (mi *MIDIInput) Id() string {
return mc.config.Id return mi.config.Id
} }
func (mc *MIDIInput) Type() string { func (mi *MIDIInput) Type() string {
return mc.config.Type return mi.config.Type
} }
func (mc *MIDIInput) Run() error { func (mi *MIDIInput) Run() error {
defer midi.CloseDriver() defer midi.CloseDriver()
in, err := midi.FindInPort(mc.Port) in, err := midi.FindInPort(mi.Port)
if err != nil { if err != nil {
return fmt.Errorf("midi.input can't find input port: %s", mc.Port) return fmt.Errorf("midi.input can't find input port: %s", mi.Port)
} }
stop, err := midi.ListenTo(in, func(msg midi.Message, timestampms int32) { stop, err := midi.ListenTo(in, func(msg midi.Message, timestampms int32) {
if mc.router != nil { if mi.router != nil {
mc.router.HandleInput(mc.Id(), msg) mi.router.HandleInput(mi.Id(), msg)
} }
}, midi.UseSysEx()) }, midi.UseSysEx())
@@ -71,11 +79,11 @@ func (mc *MIDIInput) Run() error {
defer stop() defer stop()
<-mc.ctx.Done() <-mi.ctx.Done()
slog.Debug("router context done in module", "id", mc.Id()) mi.logger.Debug("done")
return nil return nil
} }
func (mc *MIDIInput) Output(payload any) error { func (mi *MIDIInput) Output(ctx context.Context, payload any) error {
return fmt.Errorf("midi.input output is not implemented") return errors.New("midi.input output is not implemented")
} }

View File

@@ -1,89 +0,0 @@
//go:build cgo
package module
import (
"context"
"fmt"
"log/slog"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/route"
"gitlab.com/gomidi/midi/v2"
_ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
)
type MIDIOutput struct {
config config.ModuleConfig
ctx context.Context
router route.RouteIO
Port string
SendFunc func(midi.Message) error
}
func init() {
RegisterModule(ModuleRegistration{
//TODO(jwetzell): find a better namespace than "misc"
Type: "midi.output",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) {
params := config.Params
port, ok := params["port"]
if !ok {
return nil, fmt.Errorf("midi.output requires a port parameter")
}
portString, ok := port.(string)
if !ok {
return nil, fmt.Errorf("midi.output port must be a string")
}
return &MIDIOutput{config: config, Port: portString, ctx: ctx, router: router}, nil
},
})
}
func (mc *MIDIOutput) Id() string {
return mc.config.Id
}
func (mc *MIDIOutput) Type() string {
return mc.config.Type
}
func (mc *MIDIOutput) Run() error {
defer midi.CloseDriver()
out, err := midi.FindOutPort(mc.Port)
if err != nil {
return fmt.Errorf("midi.output can't find output port: %s", mc.Port)
}
send, err := midi.SendTo(out)
if err != nil {
return err
}
mc.SendFunc = send
<-mc.ctx.Done()
slog.Debug("router context done in module", "id", mc.Id())
return nil
}
func (mc *MIDIOutput) Output(payload any) error {
if mc.SendFunc == nil {
return fmt.Errorf("midi.output output is not setup")
}
payloadMessage, ok := payload.(midi.Message)
if !ok {
return fmt.Errorf("midi.output can only ouptut midi.Message")
}
return mc.SendFunc(payloadMessage)
}

View File

@@ -0,0 +1,96 @@
//go:build cgo
package module
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/route"
"gitlab.com/gomidi/midi/v2"
_ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
)
type MIDIOutput struct {
config config.ModuleConfig
ctx context.Context
router route.RouteIO
Port string
SendFunc func(midi.Message) error
logger *slog.Logger
}
func init() {
RegisterModule(ModuleRegistration{
Type: "midi.output",
New: func(ctx context.Context, config config.ModuleConfig) (Module, error) {
params := config.Params
port, ok := params["port"]
if !ok {
return nil, errors.New("midi.output requires a port parameter")
}
portString, ok := port.(string)
if !ok {
return nil, errors.New("midi.output port must be a string")
}
router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
if !ok {
return nil, errors.New("midi.output unable to get router from context")
}
return &MIDIOutput{config: config, Port: portString, ctx: ctx, router: router, logger: CreateLogger(config)}, nil
},
})
}
func (mo *MIDIOutput) Id() string {
return mo.config.Id
}
func (mo *MIDIOutput) Type() string {
return mo.config.Type
}
func (mo *MIDIOutput) Run() error {
defer midi.CloseDriver()
out, err := midi.FindOutPort(mo.Port)
if err != nil {
return fmt.Errorf("midi.output can't find output port: %s", mo.Port)
}
send, err := midi.SendTo(out)
if err != nil {
return err
}
mo.SendFunc = send
<-mo.ctx.Done()
mo.logger.Debug("done")
return nil
}
func (mo *MIDIOutput) Output(ctx context.Context, payload any) error {
if mo.SendFunc == nil {
return errors.New("midi.output output is not setup")
}
payloadMessage, ok := payload.(midi.Message)
if !ok {
return errors.New("midi.output can only ouptut midi.Message")
}
return mo.SendFunc(payloadMessage)
}

View File

@@ -3,10 +3,10 @@ package module
import ( import (
"context" "context"
"fmt" "fmt"
"log/slog"
"sync" "sync"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/route"
) )
type ModuleError struct { type ModuleError struct {
@@ -19,12 +19,12 @@ type Module interface {
Id() string Id() string
Type() string Type() string
Run() error Run() error
Output(any) error Output(context.Context, any) error
} }
type ModuleRegistration struct { type ModuleRegistration struct {
Type string `json:"type"` Type string `json:"type"`
New func(context.Context, config.ModuleConfig, route.RouteIO) (Module, error) New func(context.Context, config.ModuleConfig) (Module, error)
} }
func RegisterModule(mod ModuleRegistration) { func RegisterModule(mod ModuleRegistration) {
@@ -49,3 +49,7 @@ var (
moduleRegistryMu sync.RWMutex moduleRegistryMu sync.RWMutex
ModuleRegistry = make(map[string]ModuleRegistration) ModuleRegistry = make(map[string]ModuleRegistration)
) )
func CreateLogger(config config.ModuleConfig) *slog.Logger {
return slog.Default().With("component", "module", "id", config.Id, "type", config.Type)
}

View File

@@ -2,7 +2,7 @@ package module
import ( import (
"context" "context"
"fmt" "errors"
"log/slog" "log/slog"
mqtt "github.com/eclipse/paho.mqtt.golang" mqtt "github.com/eclipse/paho.mqtt.golang"
@@ -18,50 +18,57 @@ type MQTTClient struct {
ClientID string ClientID string
Topic string Topic string
client mqtt.Client client mqtt.Client
logger *slog.Logger
} }
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "mqtt.client", Type: "mqtt.client",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) { New: func(ctx context.Context, config config.ModuleConfig) (Module, error) {
params := config.Params params := config.Params
broker, ok := params["broker"] broker, ok := params["broker"]
if !ok { if !ok {
return nil, fmt.Errorf("mqtt.client requires a broker parameter") return nil, errors.New("mqtt.client requires a broker parameter")
} }
brokerString, ok := broker.(string) brokerString, ok := broker.(string)
if !ok { if !ok {
return nil, fmt.Errorf("mqtt.client broker must be string") return nil, errors.New("mqtt.client broker must be string")
} }
topic, ok := params["topic"] topic, ok := params["topic"]
if !ok { if !ok {
return nil, fmt.Errorf("mqtt.client requires a topic parameter") return nil, errors.New("mqtt.client requires a topic parameter")
} }
topicString, ok := topic.(string) topicString, ok := topic.(string)
if !ok { if !ok {
return nil, fmt.Errorf("mqtt.client topic must be string") return nil, errors.New("mqtt.client topic must be string")
} }
clientId, ok := params["clientId"] clientId, ok := params["clientId"]
if !ok { if !ok {
return nil, fmt.Errorf("mqtt.client requires a clientId parameter") return nil, errors.New("mqtt.client requires a clientId parameter")
} }
clientIdString, ok := clientId.(string) clientIdString, ok := clientId.(string)
if !ok { if !ok {
return nil, fmt.Errorf("mqtt.client clientId must be string") return nil, errors.New("mqtt.client clientId must be string")
} }
return &MQTTClient{config: config, Broker: brokerString, Topic: topicString, ClientID: clientIdString, ctx: ctx, router: router}, nil router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
if !ok {
return nil, errors.New("mqtt.client unable to get router from context")
}
return &MQTTClient{config: config, Broker: brokerString, Topic: topicString, ClientID: clientIdString, ctx: ctx, router: router, logger: CreateLogger(config)}, nil
}, },
}) })
} }
@@ -99,25 +106,23 @@ func (mc *MQTTClient) Run() error {
} }
<-mc.ctx.Done() <-mc.ctx.Done()
slog.Debug("router context done in module", "id", mc.Id()) mc.logger.Debug("done")
return nil return nil
} }
func (mc *MQTTClient) Output(payload any) error { func (mc *MQTTClient) Output(ctx context.Context, payload any) error {
payloadMessage, ok := payload.(mqtt.Message) payloadMessage, ok := payload.(mqtt.Message)
fmt.Printf("payload type: %T\n", payload)
if !ok { if !ok {
return fmt.Errorf("mqtt.client is only able to output a MQTTMessage") return errors.New("mqtt.client is only able to output a MQTTMessage")
} }
if mc.client == nil { if mc.client == nil {
return fmt.Errorf("mqtt.client client is not setup") return errors.New("mqtt.client client is not setup")
} }
if !mc.client.IsConnected() { if !mc.client.IsConnected() {
return fmt.Errorf("mqtt.client is not connected") return errors.New("mqtt.client is not connected")
} }
token := mc.client.Publish(payloadMessage.Topic(), payloadMessage.Qos(), payloadMessage.Retained(), payloadMessage.Payload()) token := mc.client.Publish(payloadMessage.Topic(), payloadMessage.Qos(), payloadMessage.Retained(), payloadMessage.Payload())

View File

@@ -2,7 +2,7 @@ package module
import ( import (
"context" "context"
"fmt" "errors"
"log/slog" "log/slog"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
@@ -18,38 +18,45 @@ type NATSClient struct {
URL string URL string
Subject string Subject string
client *nats.Conn client *nats.Conn
logger *slog.Logger
} }
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "nats.client", Type: "nats.client",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) { New: func(ctx context.Context, config config.ModuleConfig) (Module, error) {
params := config.Params params := config.Params
url, ok := params["url"] url, ok := params["url"]
if !ok { if !ok {
return nil, fmt.Errorf("nats.client requires a url parameter") return nil, errors.New("nats.client requires a url parameter")
} }
urlString, ok := url.(string) urlString, ok := url.(string)
if !ok { if !ok {
return nil, fmt.Errorf("nats.client url must be string") return nil, errors.New("nats.client url must be string")
} }
subject, ok := params["subject"] subject, ok := params["subject"]
if !ok { if !ok {
return nil, fmt.Errorf("nats.client requires a subject parameter") return nil, errors.New("nats.client requires a subject parameter")
} }
subjectString, ok := subject.(string) subjectString, ok := subject.(string)
if !ok { if !ok {
return nil, fmt.Errorf("nats.client subject must be string") return nil, errors.New("nats.client subject must be string")
} }
return &NATSClient{config: config, URL: urlString, Subject: subjectString, ctx: ctx, router: router}, nil router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
if !ok {
return nil, errors.New("nats.client unable to get router from context")
}
return &NATSClient{config: config, URL: urlString, Subject: subjectString, ctx: ctx, router: router, logger: CreateLogger(config)}, nil
}, },
}) })
} }
@@ -87,24 +94,24 @@ func (nc *NATSClient) Run() error {
defer sub.Unsubscribe() defer sub.Unsubscribe()
<-nc.ctx.Done() <-nc.ctx.Done()
slog.Debug("router context done in module", "id", nc.Id()) nc.logger.Debug("done")
return nil return nil
} }
func (nc *NATSClient) Output(payload any) error { func (nc *NATSClient) Output(ctx context.Context, payload any) error {
payloadMessage, ok := payload.(processor.NATSMessage) payloadMessage, ok := payload.(processor.NATSMessage)
if !ok { if !ok {
return fmt.Errorf("nats.client is only able to output NATSMessage") return errors.New("nats.client is only able to output NATSMessage")
} }
if nc.client == nil { if nc.client == nil {
return fmt.Errorf("nats.client client is not setup") return errors.New("nats.client client is not setup")
} }
if !nc.client.IsConnected() { if !nc.client.IsConnected() {
return fmt.Errorf("nats.client is not connected") return errors.New("nats.client is not connected")
} }
err := nc.client.Publish(payloadMessage.Subject, payloadMessage.Payload) err := nc.client.Publish(payloadMessage.Subject, payloadMessage.Payload)

View File

@@ -2,6 +2,7 @@ package module
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"log/slog" "log/slog"
"net" "net"
@@ -18,14 +19,19 @@ type PSNClient struct {
ctx context.Context ctx context.Context
router route.RouteIO router route.RouteIO
decoder *psn.Decoder decoder *psn.Decoder
logger *slog.Logger
} }
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "psn.client", Type: "psn.client",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) { New: func(ctx context.Context, config config.ModuleConfig) (Module, error) {
router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
return &PSNClient{config: config, decoder: psn.NewDecoder(), ctx: ctx, router: router}, nil if !ok {
return nil, errors.New("psn.client unable to get router from context")
}
return &PSNClient{config: config, decoder: psn.NewDecoder(), ctx: ctx, router: router, logger: CreateLogger(config)}, nil
}, },
}) })
} }
@@ -58,7 +64,7 @@ func (pc *PSNClient) Run() error {
select { select {
case <-pc.ctx.Done(): case <-pc.ctx.Done():
// TODO(jwetzell): cleanup? // TODO(jwetzell): cleanup?
slog.Debug("router context done in module", "id", pc.Id()) pc.logger.Debug("done")
return nil return nil
default: default:
pc.conn.SetDeadline(time.Now().Add(time.Millisecond * 200)) pc.conn.SetDeadline(time.Now().Add(time.Millisecond * 200))
@@ -76,7 +82,7 @@ func (pc *PSNClient) Run() error {
message := buffer[:numBytes] message := buffer[:numBytes]
err := pc.decoder.Decode(message) err := pc.decoder.Decode(message)
if err != nil { if err != nil {
slog.Error("psn.client problem decoding psn traffic", "id", pc.Id(), "error", err) pc.logger.Error("problem decoding psn traffic", "error", err)
} }
if pc.router != nil { if pc.router != nil {
@@ -84,13 +90,13 @@ func (pc *PSNClient) Run() error {
pc.router.HandleInput(pc.Id(), tracker) pc.router.HandleInput(pc.Id(), tracker)
} }
} else { } else {
slog.Error("psn.client has no router", "id", pc.Id()) pc.logger.Error("has no router")
} }
} }
} }
} }
} }
func (pc *PSNClient) Output(payload any) error { func (pc *PSNClient) Output(ctx context.Context, payload any) error {
return fmt.Errorf("psn.client output is not implemented") return fmt.Errorf("psn.client output is not implemented")
} }

View File

@@ -4,6 +4,7 @@ package module
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"log/slog" "log/slog"
"time" "time"
@@ -22,24 +23,24 @@ type SerialClient struct {
Framer framer.Framer Framer framer.Framer
Mode *serial.Mode Mode *serial.Mode
port serial.Port port serial.Port
logger *slog.Logger
} }
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
//TODO(jwetzell): find a better namespace than "misc"
Type: "serial.client", Type: "serial.client",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) { New: func(ctx context.Context, config config.ModuleConfig) (Module, error) {
params := config.Params params := config.Params
port, ok := params["port"] port, ok := params["port"]
if !ok { if !ok {
return nil, fmt.Errorf("serial.client requires a port parameter") return nil, errors.New("serial.client requires a port parameter")
} }
portString, ok := port.(string) portString, ok := port.(string)
if !ok { if !ok {
return nil, fmt.Errorf("serial.client port must be a string") return nil, errors.New("serial.client port must be a string")
} }
framingMethod := "RAW" framingMethod := "RAW"
@@ -50,107 +51,113 @@ func init() {
framingMethodString, ok := framingMethodRaw.(string) framingMethodString, ok := framingMethodRaw.(string)
if !ok { if !ok {
return nil, fmt.Errorf("serial.client framing method must be a string") return nil, errors.New("serial.client framing method must be a string")
} }
framingMethod = framingMethodString framingMethod = framingMethodString
} }
framer, err := framer.GetFramer(framingMethod) framer := framer.GetFramer(framingMethod)
if err != nil { if framer == nil {
return nil, err return nil, fmt.Errorf("serial.client unknown framing method: %s", framingMethod)
} }
buadRate, ok := params["baudRate"] buadRate, ok := params["baudRate"]
if !ok { if !ok {
return nil, fmt.Errorf("serial.client requires a baudRate parameter") return nil, errors.New("serial.client requires a baudRate parameter")
} }
baudRateNum, ok := buadRate.(float64) baudRateNum, ok := buadRate.(float64)
if !ok { if !ok {
return nil, fmt.Errorf("serial.client baudRate must be a number") return nil, errors.New("serial.client baudRate must be a number")
} }
mode := serial.Mode{ mode := serial.Mode{
BaudRate: int(baudRateNum), BaudRate: int(baudRateNum),
} }
return &SerialClient{config: config, Port: portString, Framer: framer, Mode: &mode, ctx: ctx, router: router}, nil router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
if !ok {
return nil, errors.New("serial.client unable to get router from context")
}
return &SerialClient{config: config, Port: portString, Framer: framer, Mode: &mode, ctx: ctx, router: router, logger: CreateLogger(config)}, nil
}, },
}) })
} }
func (mc *SerialClient) Id() string { func (sc *SerialClient) Id() string {
return mc.config.Id return sc.config.Id
} }
func (mc *SerialClient) Type() string { func (sc *SerialClient) Type() string {
return mc.config.Type return sc.config.Type
} }
func (mc *SerialClient) SetupPort() error { func (sc *SerialClient) SetupPort() error {
port, err := serial.Open(mc.Port, mc.Mode) port, err := serial.Open(sc.Port, sc.Mode)
if err != nil { if err != nil {
return fmt.Errorf("serial.client can't open input port: %s", mc.Port) return fmt.Errorf("serial.client can't open input port: %s", sc.Port)
} }
mc.port = port sc.port = port
return nil return nil
} }
func (mc *SerialClient) Run() error { func (sc *SerialClient) Run() error {
// TODO(jwetzell): shutdown with router.Context properly // TODO(jwetzell): shutdown with router.Context properly
go func() { go func() {
<-mc.ctx.Done() <-sc.ctx.Done()
slog.Debug("router context done in module", "id", mc.Id()) sc.logger.Debug("done")
if mc.port != nil { if sc.port != nil {
mc.port.Close() sc.port.Close()
} }
}() }()
for { for {
err := mc.SetupPort() err := sc.SetupPort()
if err != nil { if err != nil {
if mc.ctx.Err() != nil { if sc.ctx.Err() != nil {
slog.Debug("router context done in module", "id", mc.Id()) sc.logger.Debug("done")
return nil return nil
} }
slog.Error("serial.client", "id", mc.Id(), "error", err.Error()) sc.logger.Error("port setup error", "error", err.Error())
time.Sleep(time.Second * 2) time.Sleep(time.Second * 2)
continue continue
} }
buffer := make([]byte, 1024) buffer := make([]byte, 1024)
select { select {
case <-mc.ctx.Done(): case <-sc.ctx.Done():
slog.Debug("router context done in module", "id", mc.Id()) sc.logger.Debug("done")
return nil return nil
default: default:
READ: READ:
for { for {
select { select {
case <-mc.ctx.Done(): case <-sc.ctx.Done():
slog.Debug("router context done in module", "id", mc.Id()) sc.logger.Debug("done")
return nil return nil
default: default:
byteCount, err := mc.port.Read(buffer) byteCount, err := sc.port.Read(buffer)
if err != nil { if err != nil {
mc.Framer.Clear() sc.Framer.Clear()
break READ break READ
} }
if mc.Framer != nil { if sc.Framer != nil {
if byteCount > 0 { if byteCount > 0 {
messages := mc.Framer.Decode(buffer[0:byteCount]) messages := sc.Framer.Decode(buffer[0:byteCount])
for _, message := range messages { for _, message := range messages {
if mc.router != nil { if sc.router != nil {
mc.router.HandleInput(mc.Id(), message) sc.router.HandleInput(sc.Id(), message)
} else { } else {
slog.Error("serial.client has no router", "id", mc.Id()) sc.logger.Error("input received but no router is configured")
} }
} }
} }
@@ -161,14 +168,14 @@ func (mc *SerialClient) Run() error {
} }
} }
func (mc *SerialClient) Output(payload any) error { func (sc *SerialClient) Output(ctx context.Context, payload any) error {
payloadBytes, ok := payload.([]byte) payloadBytes, ok := payload.([]byte)
if !ok { if !ok {
return fmt.Errorf("serial.client can only ouptut bytes") return errors.New("serial.client can only ouptut bytes")
} }
_, err := mc.port.Write(mc.Framer.Encode(payloadBytes)) _, err := sc.port.Write(sc.Framer.Encode(payloadBytes))
return err return err
} }

View File

@@ -2,6 +2,7 @@ package module
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"io" "io"
"log/slog" "log/slog"
@@ -24,6 +25,7 @@ type SIPCallServer struct {
Transport string Transport string
UserAgent string UserAgent string
dg *diago.Diago dg *diago.Diago
logger *slog.Logger
} }
type SIPCallMessage struct { type SIPCallMessage struct {
@@ -33,7 +35,7 @@ type SIPCallMessage struct {
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "sip.call.server", Type: "sip.call.server",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) { New: func(ctx context.Context, config config.ModuleConfig) (Module, error) {
params := config.Params params := config.Params
portNum := 5060 portNum := 5060
@@ -42,7 +44,7 @@ func init() {
specificPortNum, ok := port.(float64) specificPortNum, ok := port.(float64)
if !ok { if !ok {
return nil, fmt.Errorf("sip.call.server port must be a number") return nil, errors.New("sip.call.server port must be a number")
} }
portNum = int(specificPortNum) portNum = int(specificPortNum)
} }
@@ -55,7 +57,7 @@ func init() {
specificIpString, ok := ip.(string) specificIpString, ok := ip.(string)
if !ok { if !ok {
return nil, fmt.Errorf("sip.call.server ip must be a string") return nil, errors.New("sip.call.server ip must be a string")
} }
ipString = specificIpString ipString = specificIpString
} }
@@ -68,7 +70,7 @@ func init() {
specificTransportString, ok := transport.(string) specificTransportString, ok := transport.(string)
if !ok { if !ok {
return nil, fmt.Errorf("sip.call.server transport must be a string") return nil, errors.New("sip.call.server transport must be a string")
} }
transportString = specificTransportString transportString = specificTransportString
} }
@@ -81,28 +83,34 @@ func init() {
specificTransportString, ok := userAgent.(string) specificTransportString, ok := userAgent.(string)
if !ok { if !ok {
return nil, fmt.Errorf("sip.call.server userAgent must be a string") return nil, errors.New("sip.call.server userAgent must be a string")
} }
userAgentString = specificTransportString userAgentString = specificTransportString
} }
return &SIPCallServer{config: config, ctx: ctx, router: router, IP: ipString, Port: int(portNum), Transport: transportString, UserAgent: userAgentString}, nil
router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
if !ok {
return nil, errors.New("sip.call.server unable to get router from context")
}
return &SIPCallServer{config: config, ctx: ctx, router: router, IP: ipString, Port: int(portNum), Transport: transportString, UserAgent: userAgentString, logger: CreateLogger(config)}, nil
}, },
}) })
} }
func (sds *SIPCallServer) Id() string { func (scs *SIPCallServer) Id() string {
return sds.config.Id return scs.config.Id
} }
func (sds *SIPCallServer) Type() string { func (scs *SIPCallServer) Type() string {
return sds.config.Type return scs.config.Type
} }
func (sds *SIPCallServer) Run() error { func (scs *SIPCallServer) Run() error {
diagoLogger := slog.New(slog.NewJSONHandler(io.Discard, nil)) diagoLogger := slog.New(slog.NewJSONHandler(io.Discard, nil))
ua, _ := sipgo.NewUA( ua, _ := sipgo.NewUA(
sipgo.WithUserAgent(sds.UserAgent), sipgo.WithUserAgent(scs.UserAgent),
sipgo.WithUserAgentTransportLayerOptions(sip.WithTransportLayerLogger(diagoLogger)), sipgo.WithUserAgentTransportLayerOptions(sip.WithTransportLayerLogger(diagoLogger)),
sipgo.WithUserAgentTransactionLayerOptions(sip.WithTransactionLayerLogger(diagoLogger)), sipgo.WithUserAgentTransactionLayerOptions(sip.WithTransactionLayerLogger(diagoLogger)),
) )
@@ -112,74 +120,74 @@ func (sds *SIPCallServer) Run() error {
media.SetDefaultLogger(diagoLogger) media.SetDefaultLogger(diagoLogger)
dg := diago.NewDiago(ua, diago.WithLogger(diagoLogger), diago.WithTransport( dg := diago.NewDiago(ua, diago.WithLogger(diagoLogger), diago.WithTransport(
diago.Transport{ diago.Transport{
Transport: sds.Transport, Transport: scs.Transport,
BindHost: sds.IP, BindHost: scs.IP,
BindPort: sds.Port, BindPort: scs.Port,
}, },
)) ))
go func() { go func() {
dg.Serve(sds.ctx, func(inDialog *diago.DialogServerSession) { dg.Serve(scs.ctx, func(inDialog *diago.DialogServerSession) {
sds.HandleCall(inDialog) scs.HandleCall(inDialog)
}) })
}() }()
sds.dg = dg scs.dg = dg
<-sds.ctx.Done() <-scs.ctx.Done()
slog.Debug("router context done in module", "id", sds.Id()) scs.logger.Debug("done")
return nil return nil
} }
func (sds *SIPCallServer) HandleCall(inDialog *diago.DialogServerSession) { func (scs *SIPCallServer) HandleCall(inDialog *diago.DialogServerSession) {
inDialog.Trying() inDialog.Trying()
inDialog.Ringing() inDialog.Ringing()
inDialog.Answer() inDialog.Answer()
sds.router.HandleInput(sds.Id(), SIPCallMessage{ scs.router.HandleInput(scs.Id(), SIPCallMessage{
To: inDialog.ToUser(), To: inDialog.ToUser(),
}) })
<-inDialog.Context().Done() <-inDialog.Context().Done()
} }
func (sds *SIPCallServer) Output(payload any) error { func (scs *SIPCallServer) Output(ctx context.Context, payload any) error {
payloadMsg, ok := payload.(string) payloadMsg, ok := payload.(string)
if !ok { if !ok {
return fmt.Errorf("sip.call.server output payload must be of type string") return errors.New("sip.call.server output payload must be of type string")
} }
if sds.dg == nil { if scs.dg == nil {
return fmt.Errorf("sip.call.server diago is not initialized") return errors.New("sip.call.server diago is not initialized")
} }
var uri sip.Uri var uri sip.Uri
err := sip.ParseUri(payloadMsg, &uri) err := sip.ParseUri(payloadMsg, &uri)
if err != nil { if err != nil {
return fmt.Errorf("sip.call.server output payload is not a valid SIP URI: %v", err) return fmt.Errorf("sip.call.server output payload is not a valid SIP URI: %s", err)
} }
outDialog, err := sds.dg.NewDialog(uri, diago.NewDialogOptions{ outDialog, err := scs.dg.NewDialog(uri, diago.NewDialogOptions{
Transport: sds.Transport, Transport: scs.Transport,
}) })
if err != nil { if err != nil {
return fmt.Errorf("sip.call.server failed to create new dialog: %v", err) return fmt.Errorf("sip.call.server failed to create new dialog: %s", err)
} }
err = outDialog.Invite(sds.ctx, diago.InviteClientOptions{}) err = outDialog.Invite(scs.ctx, diago.InviteClientOptions{})
if err != nil { if err != nil {
return fmt.Errorf("sip.call.server failed to send invite: %v", err) return fmt.Errorf("sip.call.server failed to send invite: %s", err)
} }
err = outDialog.Ack(sds.ctx) err = outDialog.Ack(scs.ctx)
if err != nil { if err != nil {
return fmt.Errorf("sip.call.server failed to send ack: %v", err) return fmt.Errorf("sip.call.server failed to send ack: %s", err)
} }
// TODO(jwetzell): make this configurable // TODO(jwetzell): make this configurable
// NOTE(jwetzell): wait 5 seconds before hanging up the call // NOTE(jwetzell): wait 5 seconds before hanging up the call
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
err = outDialog.Hangup(sds.ctx) err = outDialog.Hangup(scs.ctx)
if err != nil { if err != nil {
return fmt.Errorf("sip.call.server failed to hangup call: %v", err) return fmt.Errorf("sip.call.server failed to hangup call: %s", err)
} }
return nil return nil
} }

View File

@@ -2,7 +2,7 @@ package module
import ( import (
"context" "context"
"fmt" "errors"
"io" "io"
"log/slog" "log/slog"
"strings" "strings"
@@ -24,6 +24,7 @@ type SIPDTMFServer struct {
Port int Port int
Transport string Transport string
Separator string Separator string
logger *slog.Logger
} }
type SIPDTMFMessage struct { type SIPDTMFMessage struct {
@@ -34,7 +35,7 @@ type SIPDTMFMessage struct {
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "sip.dtmf.server", Type: "sip.dtmf.server",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) { New: func(ctx context.Context, config config.ModuleConfig) (Module, error) {
params := config.Params params := config.Params
portNum := 5060 portNum := 5060
@@ -43,7 +44,7 @@ func init() {
specificPortNum, ok := port.(float64) specificPortNum, ok := port.(float64)
if !ok { if !ok {
return nil, fmt.Errorf("sip.dtmf.server port must be a number") return nil, errors.New("sip.dtmf.server port must be a number")
} }
portNum = int(specificPortNum) portNum = int(specificPortNum)
} }
@@ -56,7 +57,7 @@ func init() {
specificIpString, ok := ip.(string) specificIpString, ok := ip.(string)
if !ok { if !ok {
return nil, fmt.Errorf("sip.dtmf.server ip must be a string") return nil, errors.New("sip.dtmf.server ip must be a string")
} }
ipString = specificIpString ipString = specificIpString
} }
@@ -69,28 +70,33 @@ func init() {
specificTransportString, ok := transport.(string) specificTransportString, ok := transport.(string)
if !ok { if !ok {
return nil, fmt.Errorf("sip.dtmf.server transport must be a string") return nil, errors.New("sip.dtmf.server transport must be a string")
} }
transportString = specificTransportString transportString = specificTransportString
} }
separator, ok := params["separator"] separator, ok := params["separator"]
if !ok { if !ok {
return nil, fmt.Errorf("sip.dtmf.server requires a separator parameter") return nil, errors.New("sip.dtmf.server requires a separator parameter")
} }
separatorString, ok := separator.(string) separatorString, ok := separator.(string)
if !ok { if !ok {
return nil, fmt.Errorf("sip.dtmf.server separator must be a string") return nil, errors.New("sip.dtmf.server separator must be a string")
} }
if len(separatorString) != 1 { if len(separatorString) != 1 {
return nil, fmt.Errorf("sip.dtmf.server separator must be a single character") return nil, errors.New("sip.dtmf.server separator must be a single character")
} }
if !strings.ContainsRune("0123456789*#ABCD", rune(separatorString[0])) { if !strings.ContainsRune("0123456789*#ABCD", rune(separatorString[0])) {
return nil, fmt.Errorf("sip.dtmf.server separator must be a valid DTMF character") return nil, errors.New("sip.dtmf.server separator must be a valid DTMF character")
} }
return &SIPDTMFServer{config: config, ctx: ctx, router: router, IP: ipString, Port: int(portNum), Transport: transportString, Separator: separatorString}, nil router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
if !ok {
return nil, errors.New("sip.dtmf.server unable to get router from context")
}
return &SIPDTMFServer{config: config, ctx: ctx, router: router, IP: ipString, Port: int(portNum), Transport: transportString, Separator: separatorString, logger: CreateLogger(config)}, nil
}, },
}) })
} }
@@ -131,7 +137,7 @@ func (sds *SIPDTMFServer) Run() error {
} }
<-sds.ctx.Done() <-sds.ctx.Done()
slog.Debug("router context done in module", "id", sds.Id()) sds.logger.Debug("done")
return nil return nil
} }
@@ -158,6 +164,6 @@ func (sds *SIPDTMFServer) HandleCall(inDialog *diago.DialogServerSession) error
}, 5*time.Second) }, 5*time.Second)
} }
func (sds *SIPDTMFServer) Output(payload any) error { func (sds *SIPDTMFServer) Output(ctx context.Context, payload any) error {
return fmt.Errorf("sip.dtmf.server output is not implemented") return errors.New("sip.dtmf.server output is not implemented")
} }

View File

@@ -2,6 +2,7 @@ package module
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"log/slog" "log/slog"
"net" "net"
@@ -19,34 +20,35 @@ type TCPClient struct {
ctx context.Context ctx context.Context
router route.RouteIO router route.RouteIO
Addr *net.TCPAddr Addr *net.TCPAddr
logger *slog.Logger
} }
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "net.tcp.client", Type: "net.tcp.client",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) { New: func(ctx context.Context, config config.ModuleConfig) (Module, error) {
params := config.Params params := config.Params
host, ok := params["host"] host, ok := params["host"]
if !ok { if !ok {
return nil, fmt.Errorf("net.tcp.client requires a host parameter") return nil, errors.New("net.tcp.client requires a host parameter")
} }
hostString, ok := host.(string) hostString, ok := host.(string)
if !ok { if !ok {
return nil, fmt.Errorf("net.tcp.client host must be string") return nil, errors.New("net.tcp.client host must be string")
} }
port, ok := params["port"] port, ok := params["port"]
if !ok { if !ok {
return nil, fmt.Errorf("net.tcp.client requires a port parameter") return nil, errors.New("net.tcp.client requires a port parameter")
} }
portNum, ok := port.(float64) portNum, ok := port.(float64)
if !ok { if !ok {
return nil, fmt.Errorf("net.tcp.client port must be a number") return nil, errors.New("net.tcp.client port must be a number")
} }
addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", hostString, uint16(portNum))) addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", hostString, uint16(portNum)))
@@ -62,18 +64,24 @@ func init() {
framingMethodString, ok := framingMethodRaw.(string) framingMethodString, ok := framingMethodRaw.(string)
if !ok { if !ok {
return nil, fmt.Errorf("misc.serial.client framing method must be a string") return nil, errors.New("net.tcp.client framing method must be a string")
} }
framingMethod = framingMethodString framingMethod = framingMethodString
} }
framer, err := framer.GetFramer(framingMethod) framer := framer.GetFramer(framingMethod)
if err != nil { if framer == nil {
return nil, err return nil, fmt.Errorf("net.tcp.client unknown framing method: %s", framingMethod)
} }
return &TCPClient{framer: framer, Addr: addr, config: config, ctx: ctx, router: router}, nil router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
if !ok {
return nil, errors.New("net.tcp.client unable to get router from context")
}
return &TCPClient{framer: framer, Addr: addr, config: config, ctx: ctx, router: router, logger: CreateLogger(config)}, nil
}, },
}) })
} }
@@ -91,7 +99,7 @@ func (tc *TCPClient) Run() error {
// TODO(jwetzell): shutdown with router.Context properly // TODO(jwetzell): shutdown with router.Context properly
go func() { go func() {
<-tc.ctx.Done() <-tc.ctx.Done()
slog.Debug("router context done in module", "id", tc.Id()) tc.logger.Debug("done")
if tc.conn != nil { if tc.conn != nil {
tc.conn.Close() tc.conn.Close()
} }
@@ -101,10 +109,10 @@ func (tc *TCPClient) Run() error {
err := tc.SetupConn() err := tc.SetupConn()
if err != nil { if err != nil {
if tc.ctx.Err() != nil { if tc.ctx.Err() != nil {
slog.Debug("router context done in module", "id", tc.Id()) tc.logger.Debug("done")
return nil return nil
} }
slog.Error("net.tcp.client", "id", tc.Id(), "error", err.Error()) tc.logger.Error("connection error", "error", err.Error())
time.Sleep(time.Second * 2) time.Sleep(time.Second * 2)
continue continue
} }
@@ -112,14 +120,14 @@ func (tc *TCPClient) Run() error {
buffer := make([]byte, 1024) buffer := make([]byte, 1024)
select { select {
case <-tc.ctx.Done(): case <-tc.ctx.Done():
slog.Debug("router context done in module", "id", tc.Id()) tc.logger.Debug("done")
return nil return nil
default: default:
READ: READ:
for { for {
select { select {
case <-tc.ctx.Done(): case <-tc.ctx.Done():
slog.Debug("router context done in module", "id", tc.Id()) tc.logger.Debug("done")
return nil return nil
default: default:
byteCount, err := tc.conn.Read(buffer) byteCount, err := tc.conn.Read(buffer)
@@ -136,7 +144,7 @@ func (tc *TCPClient) Run() error {
if tc.router != nil { if tc.router != nil {
tc.router.HandleInput(tc.Id(), message) tc.router.HandleInput(tc.Id(), message)
} else { } else {
slog.Error("net.tcp.client has no router", "id", tc.Id()) tc.logger.Error("input received but no router is configured")
} }
} }
} }
@@ -153,7 +161,7 @@ func (tc *TCPClient) SetupConn() error {
return err return err
} }
func (tc *TCPClient) Output(payload any) error { func (tc *TCPClient) Output(ctx context.Context, payload any) error {
// NOTE(jwetzell): not sure how this would occur but // NOTE(jwetzell): not sure how this would occur but
if tc.conn == nil { if tc.conn == nil {
err := tc.SetupConn() err := tc.SetupConn()
@@ -163,7 +171,7 @@ func (tc *TCPClient) Output(payload any) error {
} }
payloadBytes, ok := payload.([]byte) payloadBytes, ok := payload.([]byte)
if !ok { if !ok {
return fmt.Errorf("net.tcp.client is only able to output bytes") return errors.New("net.tcp.client is only able to output bytes")
} }
_, err := tc.conn.Write(tc.framer.Encode(payloadBytes)) _, err := tc.conn.Write(tc.framer.Encode(payloadBytes))
return err return err

View File

@@ -26,22 +26,23 @@ type TCPServer struct {
wg sync.WaitGroup wg sync.WaitGroup
connections []*net.TCPConn connections []*net.TCPConn
connectionsMu sync.RWMutex connectionsMu sync.RWMutex
logger *slog.Logger
} }
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "net.tcp.server", Type: "net.tcp.server",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) { New: func(ctx context.Context, config config.ModuleConfig) (Module, error) {
params := config.Params params := config.Params
port, ok := params["port"] port, ok := params["port"]
if !ok { if !ok {
return nil, fmt.Errorf("net.tcp.server requires a port parameter") return nil, errors.New("net.tcp.server requires a port parameter")
} }
portNum, ok := port.(float64) portNum, ok := port.(float64)
if !ok { if !ok {
return nil, fmt.Errorf("net.tcp.server port must be a number") return nil, errors.New("net.tcp.server port must be a number")
} }
framingMethod := "RAW" framingMethod := "RAW"
@@ -52,15 +53,15 @@ func init() {
framingMethodString, ok := framingMethodRaw.(string) framingMethodString, ok := framingMethodRaw.(string)
if !ok { if !ok {
return nil, fmt.Errorf("misc.serial.client framing method must be a string") return nil, errors.New("net.tcp.server framing method must be a string")
} }
framingMethod = framingMethodString framingMethod = framingMethodString
} }
framer, err := framer.GetFramer(framingMethod) framer := framer.GetFramer(framingMethod)
if err != nil { if framer == nil {
return nil, err return nil, fmt.Errorf("net.tcp.server unknown framing method: %s", framingMethod)
} }
ipString := "0.0.0.0" ipString := "0.0.0.0"
@@ -71,7 +72,7 @@ func init() {
specificIpString, ok := ip.(string) specificIpString, ok := ip.(string)
if !ok { if !ok {
return nil, fmt.Errorf("net.tcp.server ip must be a string") return nil, errors.New("net.tcp.server ip must be a string")
} }
ipString = specificIpString ipString = specificIpString
} }
@@ -81,7 +82,13 @@ func init() {
return nil, err return nil, err
} }
return &TCPServer{Framer: framer, Addr: addr, config: config, quit: make(chan interface{}), ctx: ctx, router: router}, nil router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
if !ok {
return nil, errors.New("net.tcp.server unable to get router from context")
}
return &TCPServer{Framer: framer, Addr: addr, config: config, quit: make(chan interface{}), ctx: ctx, router: router, logger: CreateLogger(config)}, nil
}, },
}) })
} }
@@ -98,7 +105,7 @@ func (ts *TCPServer) handleClient(client *net.TCPConn) {
ts.connectionsMu.Lock() ts.connectionsMu.Lock()
ts.connections = append(ts.connections, client) ts.connections = append(ts.connections, client)
ts.connectionsMu.Unlock() ts.connectionsMu.Unlock()
slog.Debug("net.tcp.server connection accepted", "id", ts.Id(), "remoteAddr", client.RemoteAddr().String()) ts.logger.Debug("net.tcp.server connection accepted", "remoteAddr", client.RemoteAddr().String())
defer client.Close() defer client.Close()
buffer := make([]byte, 1024) buffer := make([]byte, 1024)
@@ -125,7 +132,7 @@ ClientRead:
break break
} }
} }
slog.Debug("net.tcp.server connection reset", "id", ts.Id(), "remoteAddr", client.RemoteAddr().String()) ts.logger.Debug("connection reset", "remoteAddr", client.RemoteAddr().String())
ts.connectionsMu.Unlock() ts.connectionsMu.Unlock()
} }
} }
@@ -138,7 +145,7 @@ ClientRead:
break break
} }
} }
slog.Debug("net.tcp.server stream ended", "id", ts.Id(), "remoteAddr", client.RemoteAddr().String()) ts.logger.Debug("stream ended", "remoteAddr", client.RemoteAddr().String())
ts.connectionsMu.Unlock() ts.connectionsMu.Unlock()
} }
return return
@@ -150,7 +157,7 @@ ClientRead:
if ts.router != nil { if ts.router != nil {
ts.router.HandleInput(ts.Id(), message) ts.router.HandleInput(ts.Id(), message)
} else { } else {
slog.Error("net.tcp.server has no router", "id", ts.Id()) ts.logger.Error("input received but no router is configured")
} }
} }
} }
@@ -170,7 +177,7 @@ func (ts *TCPServer) Run() error {
<-ts.ctx.Done() <-ts.ctx.Done()
close(ts.quit) close(ts.quit)
listener.Close() listener.Close()
slog.Debug("router context done in module", "id", ts.Id()) ts.logger.Debug("done")
}() }()
AcceptLoop: AcceptLoop:
@@ -181,7 +188,7 @@ AcceptLoop:
case <-ts.quit: case <-ts.quit:
break AcceptLoop break AcceptLoop
default: default:
slog.Debug("net.tcp.server problem with listener", "error", err) ts.logger.Debug("problem with listener", "error", err)
} }
} else { } else {
ts.wg.Add(1) ts.wg.Add(1)
@@ -196,11 +203,11 @@ AcceptLoop:
return nil return nil
} }
func (ts *TCPServer) Output(payload any) error { func (ts *TCPServer) Output(ctx context.Context, payload any) error {
payloadBytes, ok := payload.([]byte) payloadBytes, ok := payload.([]byte)
if !ok { if !ok {
return fmt.Errorf("net.tcp.server is only able to output bytes") return errors.New("net.tcp.server is only able to output bytes")
} }
ts.connectionsMu.Lock() ts.connectionsMu.Lock()
errorString := "" errorString := ""
@@ -216,5 +223,5 @@ func (ts *TCPServer) Output(payload any) error {
if errorString == "" { if errorString == "" {
return nil return nil
} }
return fmt.Errorf("%s", errorString) return fmt.Errorf("net.tcp.server error during output: %s", errorString)
} }

View File

@@ -2,7 +2,7 @@ package module
import ( import (
"context" "context"
"fmt" "errors"
"log/slog" "log/slog"
"time" "time"
@@ -10,45 +10,52 @@ import (
"github.com/jwetzell/showbridge-go/internal/route" "github.com/jwetzell/showbridge-go/internal/route"
) )
type Interval struct { type TimeInterval struct {
config config.ModuleConfig config config.ModuleConfig
Duration uint32 Duration uint32
ctx context.Context ctx context.Context
router route.RouteIO router route.RouteIO
ticker *time.Ticker ticker *time.Ticker
logger *slog.Logger
} }
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "gen.interval", Type: "time.interval",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) { New: func(ctx context.Context, config config.ModuleConfig) (Module, error) {
params := config.Params params := config.Params
duration, ok := params["duration"] duration, ok := params["duration"]
if !ok { if !ok {
return nil, fmt.Errorf("gen.interval requires a duration parameter") return nil, errors.New("time.interval requires a duration parameter")
} }
durationNum, ok := duration.(float64) durationNum, ok := duration.(float64)
if !ok { if !ok {
return nil, fmt.Errorf("gen.interval duration must be number") return nil, errors.New("time.interval duration must be number")
} }
return &Interval{Duration: uint32(durationNum), config: config, ctx: ctx, router: router}, nil router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
if !ok {
return nil, errors.New("time.interval unable to get router from context")
}
return &TimeInterval{Duration: uint32(durationNum), config: config, ctx: ctx, router: router, logger: CreateLogger(config)}, nil
}, },
}) })
} }
func (i *Interval) Id() string { func (i *TimeInterval) Id() string {
return i.config.Id return i.config.Id
} }
func (i *Interval) Type() string { func (i *TimeInterval) Type() string {
return i.config.Type return i.config.Type
} }
func (i *Interval) Run() error { func (i *TimeInterval) Run() error {
ticker := time.NewTicker(time.Millisecond * time.Duration(i.Duration)) ticker := time.NewTicker(time.Millisecond * time.Duration(i.Duration))
i.ticker = ticker i.ticker = ticker
defer ticker.Stop() defer ticker.Stop()
@@ -56,7 +63,7 @@ func (i *Interval) Run() error {
for { for {
select { select {
case <-i.ctx.Done(): case <-i.ctx.Done():
slog.Debug("router context done in module", "id", i.Id()) i.logger.Debug("done")
return nil return nil
case <-ticker.C: case <-ticker.C:
if i.router != nil { if i.router != nil {
@@ -67,7 +74,7 @@ func (i *Interval) Run() error {
} }
func (i *Interval) Output(payload any) error { func (i *TimeInterval) Output(ctx context.Context, payload any) error {
i.ticker.Reset(time.Millisecond * time.Duration(i.Duration)) i.ticker.Reset(time.Millisecond * time.Duration(i.Duration))
return nil return nil
} }

View File

@@ -2,7 +2,7 @@ package module
import ( import (
"context" "context"
"fmt" "errors"
"log/slog" "log/slog"
"time" "time"
@@ -10,52 +10,59 @@ import (
"github.com/jwetzell/showbridge-go/internal/route" "github.com/jwetzell/showbridge-go/internal/route"
) )
type Timer struct { type TimeTimer struct {
config config.ModuleConfig config config.ModuleConfig
Duration uint32 Duration uint32
ctx context.Context ctx context.Context
router route.RouteIO router route.RouteIO
timer *time.Timer timer *time.Timer
logger *slog.Logger
} }
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "gen.timer", Type: "time.timer",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) { New: func(ctx context.Context, config config.ModuleConfig) (Module, error) {
params := config.Params params := config.Params
duration, ok := params["duration"] duration, ok := params["duration"]
if !ok { if !ok {
return nil, fmt.Errorf("gen.timer requires a duration parameter") return nil, errors.New("time.timer requires a duration parameter")
} }
durationNum, ok := duration.(float64) durationNum, ok := duration.(float64)
if !ok { if !ok {
return nil, fmt.Errorf("gen.timer duration must be a number") return nil, errors.New("time.timer duration must be a number")
} }
return &Timer{Duration: uint32(durationNum), config: config, ctx: ctx, router: router}, nil router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
if !ok {
return nil, errors.New("net.tcp.client unable to get router from context")
}
return &TimeTimer{Duration: uint32(durationNum), config: config, ctx: ctx, router: router, logger: CreateLogger(config)}, nil
}, },
}) })
} }
func (t *Timer) Id() string { func (t *TimeTimer) Id() string {
return t.config.Id return t.config.Id
} }
func (t *Timer) Type() string { func (t *TimeTimer) Type() string {
return t.config.Type return t.config.Type
} }
func (t *Timer) Run() error { func (t *TimeTimer) Run() error {
t.timer = time.NewTimer(time.Millisecond * time.Duration(t.Duration)) t.timer = time.NewTimer(time.Millisecond * time.Duration(t.Duration))
defer t.timer.Stop() defer t.timer.Stop()
for { for {
select { select {
case <-t.ctx.Done(): case <-t.ctx.Done():
t.timer.Stop() t.timer.Stop()
slog.Debug("router context done in module", "id", t.Id()) t.logger.Debug("done")
return nil return nil
case time := <-t.timer.C: case time := <-t.timer.C:
if t.router != nil { if t.router != nil {
@@ -65,7 +72,7 @@ func (t *Timer) Run() error {
} }
} }
func (t *Timer) Output(payload any) error { func (t *TimeTimer) Output(ctx context.Context, payload any) error {
t.timer.Reset(time.Millisecond * time.Duration(t.Duration)) t.timer.Reset(time.Millisecond * time.Duration(t.Duration))
return nil return nil
} }

View File

@@ -2,6 +2,7 @@ package module
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"log/slog" "log/slog"
"net" "net"
@@ -17,34 +18,35 @@ type UDPClient struct {
conn *net.UDPConn conn *net.UDPConn
ctx context.Context ctx context.Context
router route.RouteIO router route.RouteIO
logger *slog.Logger
} }
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "net.udp.client", Type: "net.udp.client",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) { New: func(ctx context.Context, config config.ModuleConfig) (Module, error) {
params := config.Params params := config.Params
host, ok := params["host"] host, ok := params["host"]
if !ok { if !ok {
return nil, fmt.Errorf("net.udp.client requires a host parameter") return nil, errors.New("net.udp.client requires a host parameter")
} }
hostString, ok := host.(string) hostString, ok := host.(string)
if !ok { if !ok {
return nil, fmt.Errorf("net.udp.client host must be a string") return nil, errors.New("net.udp.client host must be a string")
} }
port, ok := params["port"] port, ok := params["port"]
if !ok { if !ok {
return nil, fmt.Errorf("net.udp.client requires a port parameter") return nil, errors.New("net.udp.client requires a port parameter")
} }
portNum, ok := port.(float64) portNum, ok := port.(float64)
if !ok { if !ok {
return nil, fmt.Errorf("net.udp.client port must be a number") return nil, errors.New("net.udp.client port must be a number")
} }
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", hostString, uint16(portNum))) addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", hostString, uint16(portNum)))
@@ -52,7 +54,13 @@ func init() {
return nil, err return nil, err
} }
return &UDPClient{Addr: addr, config: config, ctx: ctx, router: router}, nil router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
if !ok {
return nil, errors.New("net.udp.client unable to get router from context")
}
return &UDPClient{Addr: addr, config: config, ctx: ctx, router: router, logger: CreateLogger(config)}, nil
}, },
}) })
} }
@@ -79,18 +87,18 @@ func (uc *UDPClient) Run() error {
} }
<-uc.ctx.Done() <-uc.ctx.Done()
slog.Debug("router context done in module", "id", uc.Id()) uc.logger.Debug("done")
if uc.conn != nil { if uc.conn != nil {
uc.conn.Close() uc.conn.Close()
} }
return nil return nil
} }
func (uc *UDPClient) Output(payload any) error { func (uc *UDPClient) Output(ctx context.Context, payload any) error {
payloadBytes, ok := payload.([]byte) payloadBytes, ok := payload.([]byte)
if !ok { if !ok {
return fmt.Errorf("net.udp.client is only able to output bytes") return errors.New("net.udp.client is only able to output bytes")
} }
if uc.conn != nil { if uc.conn != nil {
_, err := uc.conn.Write(payloadBytes) _, err := uc.conn.Write(payloadBytes)
@@ -99,7 +107,7 @@ func (uc *UDPClient) Output(payload any) error {
return err return err
} }
} else { } else {
return fmt.Errorf("net.udp.client client is not setup") return errors.New("net.udp.client client is not setup")
} }
return nil return nil
} }

View File

@@ -2,6 +2,7 @@ package module
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"log/slog" "log/slog"
"net" "net"
@@ -17,41 +18,48 @@ type UDPMulticast struct {
ctx context.Context ctx context.Context
router route.RouteIO router route.RouteIO
Addr *net.UDPAddr Addr *net.UDPAddr
logger *slog.Logger
} }
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "net.udp.multicast", Type: "net.udp.multicast",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) { New: func(ctx context.Context, config config.ModuleConfig) (Module, error) {
params := config.Params params := config.Params
ip, ok := params["ip"] ip, ok := params["ip"]
if !ok { if !ok {
return nil, fmt.Errorf("net.udp.client requires am ip parameter") return nil, errors.New("net.udp.multicast requires an ip parameter")
} }
ipString, ok := ip.(string) ipString, ok := ip.(string)
if !ok { if !ok {
return nil, fmt.Errorf("net.udp.client ip must be a string") return nil, errors.New("net.udp.multicast ip must be a string")
} }
port, ok := params["port"] port, ok := params["port"]
if !ok { if !ok {
return nil, fmt.Errorf("net.udp.client requires a port parameter") return nil, errors.New("net.udp.multicast requires a port parameter")
} }
portNum, ok := port.(float64) portNum, ok := port.(float64)
if !ok { if !ok {
return nil, fmt.Errorf("net.udp.client port must be a number") return nil, errors.New("net.udp.multicast port must be a number")
} }
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", ipString, uint16(portNum))) addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", ipString, uint16(portNum)))
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &UDPMulticast{config: config, Addr: addr, ctx: ctx, router: router}, nil
router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
if !ok {
return nil, errors.New("net.udp.multicast unable to get router from context")
}
return &UDPMulticast{config: config, Addr: addr, ctx: ctx, router: router, logger: CreateLogger(config)}, nil
}, },
}) })
} }
@@ -79,7 +87,7 @@ func (um *UDPMulticast) Run() error {
select { select {
case <-um.ctx.Done(): case <-um.ctx.Done():
// TODO(jwetzell): cleanup? // TODO(jwetzell): cleanup?
slog.Debug("router context done in module", "id", um.Id()) um.logger.Debug("done")
return nil return nil
default: default:
um.conn.SetDeadline(time.Now().Add(time.Millisecond * 200)) um.conn.SetDeadline(time.Now().Add(time.Millisecond * 200))
@@ -99,22 +107,22 @@ func (um *UDPMulticast) Run() error {
if um.router != nil { if um.router != nil {
um.router.HandleInput(um.Id(), message) um.router.HandleInput(um.Id(), message)
} else { } else {
slog.Error("net.udp.multicast has no router", "id", um.Id()) um.logger.Error("input received but no router is configured")
} }
} }
} }
} }
} }
func (um *UDPMulticast) Output(payload any) error { func (um *UDPMulticast) Output(ctx context.Context, payload any) error {
payloadBytes, ok := payload.([]byte) payloadBytes, ok := payload.([]byte)
if !ok { if !ok {
return fmt.Errorf("net.udp.multicast can only output bytes") return errors.New("net.udp.multicast can only output bytes")
} }
if um.conn == nil { if um.conn == nil {
return fmt.Errorf("net.udp.multicast connection is not setup") return errors.New("net.udp.multicast connection is not setup")
} }
_, err := um.conn.Write(payloadBytes) _, err := um.conn.Write(payloadBytes)

View File

@@ -2,6 +2,7 @@ package module
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"log" "log"
"log/slog" "log/slog"
@@ -14,25 +15,27 @@ import (
type UDPServer struct { type UDPServer struct {
Addr *net.UDPAddr Addr *net.UDPAddr
BufferSize int
config config.ModuleConfig config config.ModuleConfig
ctx context.Context ctx context.Context
router route.RouteIO router route.RouteIO
logger *slog.Logger
} }
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "net.udp.server", Type: "net.udp.server",
New: func(ctx context.Context, config config.ModuleConfig, router route.RouteIO) (Module, error) { New: func(ctx context.Context, config config.ModuleConfig) (Module, error) {
params := config.Params params := config.Params
port, ok := params["port"] port, ok := params["port"]
if !ok { if !ok {
return nil, fmt.Errorf("net.udp.server requires a port parameter") return nil, errors.New("net.udp.server requires a port parameter")
} }
portNum, ok := port.(float64) portNum, ok := port.(float64)
if !ok { if !ok {
return nil, fmt.Errorf("net.udp.server port must be a number") return nil, errors.New("net.udp.server port must be a number")
} }
ipString := "0.0.0.0" ipString := "0.0.0.0"
@@ -43,7 +46,7 @@ func init() {
specificIpString, ok := ip.(string) specificIpString, ok := ip.(string)
if !ok { if !ok {
return nil, fmt.Errorf("net.udp.server ip must be a string") return nil, errors.New("net.udp.server ip must be a string")
} }
ipString = specificIpString ipString = specificIpString
} }
@@ -53,7 +56,24 @@ func init() {
log.Fatalf("error resolving UDP address: %v", err) log.Fatalf("error resolving UDP address: %v", err)
} }
return &UDPServer{Addr: addr, config: config, ctx: ctx, router: router}, nil bufferSizeNum := 2048
bufferSize, ok := params["bufferSize"]
if ok {
bufferSizeFloat, ok := bufferSize.(float64)
if !ok {
return nil, errors.New("net.udp.server bufferSize must be a number")
}
bufferSizeNum = int(bufferSizeFloat)
}
router, ok := ctx.Value(route.RouterContextKey).(route.RouteIO)
if !ok {
return nil, errors.New("net.udp.server unable to get router from context")
}
return &UDPServer{Addr: addr, BufferSize: bufferSizeNum, config: config, ctx: ctx, router: router, logger: CreateLogger(config)}, nil
}, },
}) })
} }
@@ -75,13 +95,12 @@ func (us *UDPServer) Run() error {
defer listener.Close() defer listener.Close()
// TODO(jwetzell): make buffer size configurable buffer := make([]byte, us.BufferSize)
buffer := make([]byte, 65535)
for { for {
select { select {
case <-us.ctx.Done(): case <-us.ctx.Done():
// TODO(jwetzell): cleanup? // TODO(jwetzell): cleanup?
slog.Debug("router context done in module", "id", us.Id()) us.logger.Debug("done")
return nil return nil
default: default:
listener.SetDeadline(time.Now().Add(time.Millisecond * 200)) listener.SetDeadline(time.Now().Add(time.Millisecond * 200))
@@ -98,13 +117,13 @@ func (us *UDPServer) Run() error {
if us.router != nil { if us.router != nil {
us.router.HandleInput(us.Id(), message) us.router.HandleInput(us.Id(), message)
} else { } else {
slog.Error("net.udp.server has no router", "id", us.Id()) us.logger.Error("input received but no router is configured")
} }
} }
} }
} }
func (us *UDPServer) Output(payload any) error { func (us *UDPServer) Output(ctx context.Context, payload any) error {
return fmt.Errorf("net.udp.server output is not implemented") return errors.New("net.udp.server output is not implemented")
} }

View File

@@ -0,0 +1,42 @@
package processor
import (
"context"
"fmt"
"github.com/jwetzell/artnet-go"
"github.com/jwetzell/showbridge-go/internal/config"
)
type ArtNetPacketDecode struct {
config config.ProcessorConfig
}
func (apd *ArtNetPacketDecode) Process(ctx context.Context, payload any) (any, error) {
payloadBytes, ok := payload.([]byte)
if !ok {
return nil, fmt.Errorf("artnet.packet.decode processor only accepts a []byte")
}
payloadMessage, err := artnet.Decode(payloadBytes)
if err != nil {
return nil, err
}
return payloadMessage, nil
}
func (apd *ArtNetPacketDecode) Type() string {
return apd.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "artnet.packet.decode",
New: func(config config.ProcessorConfig) (Processor, error) {
return &ArtNetPacketDecode{config: config}, nil
},
})
}

View File

@@ -0,0 +1,41 @@
package processor
import (
"context"
"fmt"
"github.com/jwetzell/artnet-go"
"github.com/jwetzell/showbridge-go/internal/config"
)
type ArtNetPacketEncode struct {
config config.ProcessorConfig
}
func (ape *ArtNetPacketEncode) Process(ctx context.Context, payload any) (any, error) {
payloadPacket, ok := payload.(artnet.ArtNetPacket)
if !ok {
return nil, fmt.Errorf("artnet.packet.encode processor only accepts an ArtNetPacket")
}
payloadBytes, err := payloadPacket.MarshalBinary()
if err != nil {
return nil, err
}
return payloadBytes, nil
}
func (ape *ArtNetPacketEncode) Type() string {
return ape.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "artnet.packet.encode",
New: func(config config.ProcessorConfig) (Processor, error) {
return &ArtNetPacketEncode{config: config}, nil
},
})
}

View File

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

View File

@@ -10,10 +10,11 @@ import (
type DebugLog struct { type DebugLog struct {
config config.ProcessorConfig config config.ProcessorConfig
logger *slog.Logger
} }
func (dl *DebugLog) Process(ctx context.Context, payload any) (any, error) { func (dl *DebugLog) Process(ctx context.Context, payload any) (any, error) {
slog.Debug("debug.log", "payload", payload, "payloadType", fmt.Sprintf("%T", payload)) dl.logger.Debug("", "payload", payload, "payloadType", fmt.Sprintf("%T", payload))
return payload, nil return payload, nil
} }
@@ -25,7 +26,7 @@ func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "debug.log", Type: "debug.log",
New: func(config config.ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
return &DebugLog{config: config}, nil return &DebugLog{config: config, logger: slog.Default().With("component", "processor", "type", config.Type)}, nil
}, },
}) })
} }

View File

@@ -0,0 +1,39 @@
package processor_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestDebugLogFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["debug.log"]
if !ok {
t.Fatalf("debug.log processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "debug.log",
})
if err != nil {
t.Fatalf("failed to create debug.log processor: %s", err)
}
if processorInstance.Type() != "debug.log" {
t.Fatalf("debug.log processor has wrong type: %s", processorInstance.Type())
}
payload := "test"
expected := "test"
got, err := processorInstance.Process(t.Context(), payload)
if err != nil {
t.Fatalf("debug.log processing failed: %s", err)
}
if got != expected {
t.Fatalf("debug.log got %+v, expected %+v", got, expected)
}
}

View File

@@ -2,13 +2,14 @@ package processor
import ( import (
"context" "context"
"fmt" "errors"
"strconv" "strconv"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
type FloatParse struct { type FloatParse struct {
BitSize int
config config.ProcessorConfig config config.ProcessorConfig
} }
@@ -16,11 +17,10 @@ func (fp *FloatParse) Process(ctx context.Context, payload any) (any, error) {
payloadString, ok := payload.(string) payloadString, ok := payload.(string)
if !ok { if !ok {
return nil, fmt.Errorf("float.parse processor only accepts a string") return nil, errors.New("float.parse processor only accepts a string")
} }
// TODO(jwetzell): make bitSize configurable payloadFloat, err := strconv.ParseFloat(payloadString, fp.BitSize)
payloadFloat, err := strconv.ParseFloat(payloadString, 64)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -35,7 +35,19 @@ func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "float.parse", Type: "float.parse",
New: func(config config.ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
return &FloatParse{config: config}, nil params := config.Params
bitSizeNum := 64
bitSize, ok := params["bitSize"]
if ok {
bitSizeFloat, ok := bitSize.(float64)
if !ok {
return nil, errors.New("float.parse bitSize must be a number")
}
bitSizeNum = int(bitSizeFloat)
}
return &FloatParse{config: config, BitSize: bitSizeNum}, nil
}, },
}) })
} }

View File

@@ -3,30 +3,107 @@ package processor_test
import ( import (
"testing" "testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor" "github.com/jwetzell/showbridge-go/internal/processor"
) )
func TestFloatParseFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["float.parse"]
if !ok {
t.Fatalf("float.parse processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "float.parse",
})
if err != nil {
t.Fatalf("failed to create float.parse processor: %s", err)
}
if processorInstance.Type() != "float.parse" {
t.Fatalf("float.parse processor has wrong type: %s", processorInstance.Type())
}
}
func TestFloatParseBadConfigBitSizeString(t *testing.T) {
registration, ok := processor.ProcessorRegistry["float.parse"]
if !ok {
t.Fatalf("float.parse processor not registered")
}
_, err := registration.New(config.ProcessorConfig{
Type: "float.parse",
Params: map[string]any{
"bitSize": "64",
},
})
if err == nil {
t.Fatalf("float.parse should have returned an error for bad bitSize config")
}
}
func TestFloatParseGoodConfig(t *testing.T) {
registration, ok := processor.ProcessorRegistry["float.parse"]
if !ok {
t.Fatalf("float.parse processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "float.parse",
Params: map[string]any{
"bitSize": 64.0,
},
})
if err != nil {
t.Fatalf("float.parse should have created processor but got error: %s", err)
}
payload := "12345.0"
expected := float64(12345.0)
got, err := processorInstance.Process(t.Context(), payload)
if err != nil {
t.Fatalf("float.parse processing failed: %s", err)
}
gotFloat, ok := got.(float64)
if !ok {
t.Fatalf("float.parse returned a %T payload: %s", got, got)
}
if gotFloat != expected {
t.Fatalf("float.parse got %f, expected %f", gotFloat, expected)
}
}
func TestGoodFloatParse(t *testing.T) { func TestGoodFloatParse(t *testing.T) {
floatParser := processor.FloatParse{} floatParser := processor.FloatParse{}
tests := []struct { tests := []struct {
processor processor.Processor processor processor.Processor
name string name string
payload any payload any
bitSize int
expected float64 expected float64
}{ }{
{ {
name: "positive number", name: "positive number",
payload: "12345.67", payload: "12345.67",
bitSize: 64,
expected: 12345.67, expected: 12345.67,
}, },
{ {
name: "negative number", name: "negative number",
payload: "-12345.67", payload: "-12345.67",
bitSize: 64,
expected: -12345.67, expected: -12345.67,
}, },
{ {
name: "zero", name: "zero",
payload: "0", payload: "0",
bitSize: 64,
expected: 0, expected: 0,
}, },
} }
@@ -37,48 +114,59 @@ func TestGoodFloatParse(t *testing.T) {
gotFloat, ok := got.(float64) gotFloat, ok := got.(float64)
if !ok { if !ok {
t.Errorf("float.parse returned a %T payload: %s", got, got) t.Fatalf("float.parse returned a %T payload: %s", got, got)
} }
if err != nil { if err != nil {
t.Errorf("float.parse failed: %s", err) t.Fatalf("float.parse failed: %s", err)
} }
if gotFloat != test.expected { if gotFloat != test.expected {
t.Errorf("float.parse got %f, expected %f", gotFloat, test.expected) t.Fatalf("float.parse got %f, expected %f", gotFloat, test.expected)
} }
}) })
} }
} }
func TestBadFloatParse(t *testing.T) { func TestBadFloatParse(t *testing.T) {
floatParser := processor.FloatParse{}
tests := []struct { tests := []struct {
processor processor.Processor processor processor.Processor
name string name string
payload any payload any
bitSize int
errorString string errorString string
}{ }{
{ {
name: "non-string input", name: "non-string input",
payload: []byte{0x01}, payload: []byte{0x01},
bitSize: 64,
errorString: "float.parse processor only accepts a string", errorString: "float.parse processor only accepts a string",
}, },
{ {
name: "not float string", name: "not float string",
payload: "abcd", payload: "abcd",
bitSize: 64,
errorString: "strconv.ParseFloat: parsing \"abcd\": invalid syntax", errorString: "strconv.ParseFloat: parsing \"abcd\": invalid syntax",
}, },
{
name: "bit size overflow",
payload: "1.79e+64",
bitSize: 32,
errorString: "strconv.ParseFloat: parsing \"1.79e+64\": value out of range",
},
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
floatParser := processor.FloatParse{
BitSize: test.bitSize,
}
got, err := floatParser.Process(t.Context(), test.payload) got, err := floatParser.Process(t.Context(), test.payload)
if err == nil { if err == nil {
t.Errorf("float.parse expected to fail but succeeded, got: %v", got) t.Fatalf("float.parse expected to fail but succeeded, got: %v", got)
} }
if err.Error() != test.errorString { if err.Error() != test.errorString {
t.Errorf("float.parse got error '%s', expected '%s'", err.Error(), test.errorString) t.Fatalf("float.parse got error '%s', expected '%s'", err.Error(), test.errorString)
} }
}) })
} }

View File

@@ -3,7 +3,7 @@ package processor
import ( import (
"bytes" "bytes"
"context" "context"
"fmt" "errors"
"strconv" "strconv"
"text/template" "text/template"
@@ -190,13 +190,13 @@ func init() {
id, ok := params["id"] id, ok := params["id"]
if !ok { if !ok {
return nil, fmt.Errorf("freed.create requires an id parameter") return nil, errors.New("freed.create requires an id parameter")
} }
idString, ok := id.(string) idString, ok := id.(string)
if !ok { if !ok {
return nil, fmt.Errorf("freed.create id must be a string") return nil, errors.New("freed.create id must be a string")
} }
idTemplate, err := template.New("id").Parse(idString) idTemplate, err := template.New("id").Parse(idString)
@@ -208,13 +208,13 @@ func init() {
pan, ok := params["pan"] pan, ok := params["pan"]
if !ok { if !ok {
return nil, fmt.Errorf("freed.create requires an pan parameter") return nil, errors.New("freed.create requires a pan parameter")
} }
panString, ok := pan.(string) panString, ok := pan.(string)
if !ok { if !ok {
return nil, fmt.Errorf("freed.create pan must be a string") return nil, errors.New("freed.create pan must be a string")
} }
panTemplate, err := template.New("pan").Parse(panString) panTemplate, err := template.New("pan").Parse(panString)
@@ -222,13 +222,13 @@ func init() {
tilt, ok := params["tilt"] tilt, ok := params["tilt"]
if !ok { if !ok {
return nil, fmt.Errorf("freed.create requires an tilt parameter") return nil, errors.New("freed.create requires a tilt parameter")
} }
tiltString, ok := tilt.(string) tiltString, ok := tilt.(string)
if !ok { if !ok {
return nil, fmt.Errorf("freed.create tilt must be a string") return nil, errors.New("freed.create tilt must be a string")
} }
tiltTemplate, err := template.New("tilt").Parse(tiltString) tiltTemplate, err := template.New("tilt").Parse(tiltString)
@@ -236,13 +236,13 @@ func init() {
roll, ok := params["roll"] roll, ok := params["roll"]
if !ok { if !ok {
return nil, fmt.Errorf("freed.create requires an roll parameter") return nil, errors.New("freed.create requires a roll parameter")
} }
rollString, ok := roll.(string) rollString, ok := roll.(string)
if !ok { if !ok {
return nil, fmt.Errorf("freed.create roll must be a string") return nil, errors.New("freed.create roll must be a string")
} }
rollTemplate, err := template.New("roll").Parse(rollString) rollTemplate, err := template.New("roll").Parse(rollString)
@@ -254,13 +254,13 @@ func init() {
posX, ok := params["posX"] posX, ok := params["posX"]
if !ok { if !ok {
return nil, fmt.Errorf("freed.create requires a posX parameter") return nil, errors.New("freed.create requires a posX parameter")
} }
posXString, ok := posX.(string) posXString, ok := posX.(string)
if !ok { if !ok {
return nil, fmt.Errorf("freed.create posX must be a string") return nil, errors.New("freed.create posX must be a string")
} }
posXTemplate, err := template.New("posX").Parse(posXString) posXTemplate, err := template.New("posX").Parse(posXString)
@@ -272,13 +272,13 @@ func init() {
posY, ok := params["posY"] posY, ok := params["posY"]
if !ok { if !ok {
return nil, fmt.Errorf("freed.create requires a posY parameter") return nil, errors.New("freed.create requires a posY parameter")
} }
posYString, ok := posY.(string) posYString, ok := posY.(string)
if !ok { if !ok {
return nil, fmt.Errorf("freed.create posY must be a string") return nil, errors.New("freed.create posY must be a string")
} }
posYTemplate, err := template.New("posY").Parse(posYString) posYTemplate, err := template.New("posY").Parse(posYString)
@@ -290,13 +290,13 @@ func init() {
posZ, ok := params["posZ"] posZ, ok := params["posZ"]
if !ok { if !ok {
return nil, fmt.Errorf("freed.create requires a posZ parameter") return nil, errors.New("freed.create requires a posZ parameter")
} }
posZString, ok := posZ.(string) posZString, ok := posZ.(string)
if !ok { if !ok {
return nil, fmt.Errorf("freed.create posZ must be a string") return nil, errors.New("freed.create posZ must be a string")
} }
posZTemplate, err := template.New("posZ").Parse(posZString) posZTemplate, err := template.New("posZ").Parse(posZString)
@@ -308,13 +308,13 @@ func init() {
zoom, ok := params["zoom"] zoom, ok := params["zoom"]
if !ok { if !ok {
return nil, fmt.Errorf("freed.create requires an zoom parameter") return nil, errors.New("freed.create requires a zoom parameter")
} }
zoomString, ok := zoom.(string) zoomString, ok := zoom.(string)
if !ok { if !ok {
return nil, fmt.Errorf("freed.create zoom must be a string") return nil, errors.New("freed.create zoom must be a string")
} }
zoomTemplate, err := template.New("zoom").Parse(zoomString) zoomTemplate, err := template.New("zoom").Parse(zoomString)
@@ -322,13 +322,13 @@ func init() {
focus, ok := params["focus"] focus, ok := params["focus"]
if !ok { if !ok {
return nil, fmt.Errorf("freed.create requires an focus parameter") return nil, errors.New("freed.create requires a focus parameter")
} }
focusString, ok := focus.(string) focusString, ok := focus.(string)
if !ok { if !ok {
return nil, fmt.Errorf("freed.create focus must be a string") return nil, errors.New("freed.create focus must be a string")
} }
focusTemplate, err := template.New("focus").Parse(focusString) focusTemplate, err := template.New("focus").Parse(focusString)

View File

@@ -2,8 +2,7 @@ package processor
import ( import (
"context" "context"
"fmt" "errors"
"log/slog"
freeD "github.com/jwetzell/free-d-go" freeD "github.com/jwetzell/free-d-go"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
@@ -17,12 +16,12 @@ func (fdd *FreeDDecode) Process(ctx context.Context, payload any) (any, error) {
payloadBytes, ok := payload.([]byte) payloadBytes, ok := payload.([]byte)
if !ok { if !ok {
return nil, fmt.Errorf("freed.decode processor only accepts a []byte") return nil, errors.New("freed.decode processor only accepts a []byte")
} }
payloadMessage, err := freeD.Decode(payloadBytes) payloadMessage, err := freeD.Decode(payloadBytes)
if err != nil { if err != nil {
slog.Error("error decoding", "err", err) return nil, err
} }
return payloadMessage, nil return payloadMessage, nil
} }

View File

@@ -2,7 +2,7 @@ package processor
import ( import (
"context" "context"
"fmt" "errors"
freeD "github.com/jwetzell/free-d-go" freeD "github.com/jwetzell/free-d-go"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
@@ -16,7 +16,7 @@ func (fde *FreeDEncode) Process(ctx context.Context, payload any) (any, error) {
payloadPosition, ok := payload.(freeD.FreeDPosition) payloadPosition, ok := payload.(freeD.FreeDPosition)
if !ok { if !ok {
return nil, fmt.Errorf("freed.decode processor only accepts a FreeDEncode") return nil, errors.New("freed.decode processor only accepts a FreeDEncode")
} }
payloadBytes := freeD.Encode(payloadPosition) payloadBytes := freeD.Encode(payloadPosition)

View File

@@ -3,7 +3,7 @@ package processor
import ( import (
"bytes" "bytes"
"context" "context"
"fmt" "errors"
"net/http" "net/http"
"text/template" "text/template"
@@ -16,10 +16,10 @@ type HTTPRequestCreate struct {
URL *template.Template URL *template.Template
} }
func (hre *HTTPRequestCreate) Process(ctx context.Context, payload any) (any, error) { func (hrc *HTTPRequestCreate) Process(ctx context.Context, payload any) (any, error) {
var urlBuffer bytes.Buffer var urlBuffer bytes.Buffer
err := hre.URL.Execute(&urlBuffer, payload) err := hrc.URL.Execute(&urlBuffer, payload)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -28,7 +28,7 @@ func (hre *HTTPRequestCreate) Process(ctx context.Context, payload any) (any, er
urlString := urlBuffer.String() urlString := urlBuffer.String()
//TODO(jwetzell): support body //TODO(jwetzell): support body
request, err := http.NewRequest(hre.Method, urlString, bytes.NewBuffer([]byte{})) request, err := http.NewRequest(hrc.Method, urlString, bytes.NewBuffer([]byte{}))
if err != nil { if err != nil {
return nil, err return nil, err
@@ -37,8 +37,8 @@ func (hre *HTTPRequestCreate) Process(ctx context.Context, payload any) (any, er
return request, nil return request, nil
} }
func (hre *HTTPRequestCreate) Type() string { func (hrc *HTTPRequestCreate) Type() string {
return hre.config.Type return hrc.config.Type
} }
func init() { func init() {
@@ -50,25 +50,25 @@ func init() {
method, ok := params["method"] method, ok := params["method"]
if !ok { if !ok {
return nil, fmt.Errorf("http.request.create requires an method parameter") return nil, errors.New("http.request.create requires a method parameter")
} }
methodString, ok := method.(string) methodString, ok := method.(string)
if !ok { if !ok {
return nil, fmt.Errorf("http.request.create url must be a string") return nil, errors.New("http.request.create url must be a string")
} }
url, ok := params["url"] url, ok := params["url"]
if !ok { if !ok {
return nil, fmt.Errorf("http.request.create requires an url parameter") return nil, errors.New("http.request.create requires a url parameter")
} }
urlString, ok := url.(string) urlString, ok := url.(string)
if !ok { if !ok {
return nil, fmt.Errorf("http.request.create url must be a string") return nil, errors.New("http.request.create url must be a string")
} }
urlTemplate, err := template.New("url").Parse(urlString) urlTemplate, err := template.New("url").Parse(urlString)

View File

@@ -2,7 +2,7 @@ package processor
import ( import (
"context" "context"
"fmt" "errors"
"io" "io"
"net/http" "net/http"
@@ -17,7 +17,7 @@ func (hre *HTTPRequestEncode) Process(ctx context.Context, payload any) (any, er
payloadRequest, ok := payload.(*http.Request) payloadRequest, ok := payload.(*http.Request)
if !ok { if !ok {
return nil, fmt.Errorf("http.request.encode processor only accepts an http.Request") return nil, errors.New("http.request.encode processor only accepts an http.Request")
} }
bytes, err := io.ReadAll(payloadRequest.Body) bytes, err := io.ReadAll(payloadRequest.Body)

View File

@@ -2,6 +2,7 @@ package processor
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"regexp" "regexp"
@@ -20,7 +21,7 @@ func (hrf *HTTPRequestFilter) Process(ctx context.Context, payload any) (any, er
payloadRequest, ok := payload.(*http.Request) payloadRequest, ok := payload.(*http.Request)
if !ok { if !ok {
return nil, fmt.Errorf("http.request.filter can only operate on http.Request payloads") return nil, errors.New("http.request.filter can only operate on http.Request payloads")
} }
if hrf.Method != "" { if hrf.Method != "" {
@@ -48,13 +49,13 @@ func init() {
path, ok := params["path"] path, ok := params["path"]
if !ok { if !ok {
return nil, fmt.Errorf("http.request.filter requires an path parameter") return nil, errors.New("http.request.filter requires a path parameter")
} }
pathString, ok := path.(string) pathString, ok := path.(string)
if !ok { if !ok {
return nil, fmt.Errorf("http.request.filter path must be a string") return nil, errors.New("http.request.filter path must be a string")
} }
pathRegexp, err := regexp.Compile(fmt.Sprintf("^%s$", pathString)) pathRegexp, err := regexp.Compile(fmt.Sprintf("^%s$", pathString))
@@ -69,7 +70,7 @@ func init() {
methodString, ok := method.(string) methodString, ok := method.(string)
if !ok { if !ok {
return nil, fmt.Errorf("http.request.filter method must be a string") return nil, errors.New("http.request.filter method must be a string")
} }
return &HTTPRequestFilter{config: config, Path: pathRegexp, Method: methodString}, nil return &HTTPRequestFilter{config: config, Path: pathRegexp, Method: methodString}, nil
} }

View File

@@ -2,7 +2,7 @@ package processor
import ( import (
"context" "context"
"fmt" "errors"
"io" "io"
"net/http" "net/http"
@@ -17,7 +17,7 @@ func (hre *HTTPResponseEncode) Process(ctx context.Context, payload any) (any, e
payloadResponse, ok := payload.(*http.Response) payloadResponse, ok := payload.(*http.Response)
if !ok { if !ok {
return nil, fmt.Errorf("http.response.encode processor only accepts an http.Response") return nil, errors.New("http.response.encode processor only accepts an http.Response")
} }
defer payloadResponse.Body.Close() defer payloadResponse.Body.Close()

View File

@@ -2,13 +2,15 @@ package processor
import ( import (
"context" "context"
"fmt" "errors"
"strconv" "strconv"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
type IntParse struct { type IntParse struct {
Base int
BitSize int
config config.ProcessorConfig config config.ProcessorConfig
} }
@@ -16,11 +18,10 @@ func (ip *IntParse) Process(ctx context.Context, payload any) (any, error) {
payloadString, ok := payload.(string) payloadString, ok := payload.(string)
if !ok { if !ok {
return nil, fmt.Errorf("int.parse processor only accepts a string") return nil, errors.New("int.parse processor only accepts a string")
} }
// TODO(jwetzell): make base and bitSize configurable payloadInt, err := strconv.ParseInt(payloadString, ip.Base, ip.BitSize)
payloadInt, err := strconv.ParseInt(payloadString, 10, 64)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -35,7 +36,32 @@ func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "int.parse", Type: "int.parse",
New: func(config config.ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
return &IntParse{config: config}, nil params := config.Params
baseNum := 10
base, ok := params["base"]
if ok {
baseFloat, ok := base.(float64)
if !ok {
return nil, errors.New("int.parse base must be a number")
}
baseNum = int(baseFloat)
}
bitSizeNum := 64
bitSize, ok := params["bitSize"]
if ok {
bitSizeFloat, ok := bitSize.(float64)
if !ok {
return nil, errors.New("int.parse bitSize must be a number")
}
bitSizeNum = int(bitSizeFloat)
}
return &IntParse{config: config, Base: baseNum, BitSize: bitSizeNum}, nil
}, },
}) })
} }

View File

@@ -3,82 +3,216 @@ package processor_test
import ( import (
"testing" "testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor" "github.com/jwetzell/showbridge-go/internal/processor"
) )
func TestIntParseFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["int.parse"]
if !ok {
t.Fatalf("int.parse processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "int.parse",
})
if err != nil {
t.Fatalf("failed to create int.parse processor: %s", err)
}
if processorInstance.Type() != "int.parse" {
t.Fatalf("int.parse processor has wrong type: %s", processorInstance.Type())
}
}
func TestIntParseBadConfigBaseString(t *testing.T) {
registration, ok := processor.ProcessorRegistry["int.parse"]
if !ok {
t.Fatalf("int.parse processor not registered")
}
_, err := registration.New(config.ProcessorConfig{
Type: "int.parse",
Params: map[string]any{
"base": "10",
},
})
if err == nil {
t.Fatalf("int.parse should have returned an error for bad base config")
}
}
func TestIntParseBadConfigBitSizeString(t *testing.T) {
registration, ok := processor.ProcessorRegistry["int.parse"]
if !ok {
t.Fatalf("int.parse processor not registered")
}
_, err := registration.New(config.ProcessorConfig{
Type: "int.parse",
Params: map[string]any{
"bitSize": "64",
},
})
if err == nil {
t.Fatalf("int.parse should have returned an error for bad bitSize config")
}
}
func TestIntParseGoodConfig(t *testing.T) {
registration, ok := processor.ProcessorRegistry["int.parse"]
if !ok {
t.Fatalf("int.parse processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "int.parse",
Params: map[string]any{
"base": 10.0,
"bitSize": 64.0,
},
})
if err != nil {
t.Fatalf("int.parse should have created processor but got error: %s", err)
}
payload := "12345"
expected := int64(12345)
got, err := processorInstance.Process(t.Context(), payload)
if err != nil {
t.Fatalf("int.parse processing failed: %s", err)
}
gotInt, ok := got.(int64)
if !ok {
t.Fatalf("int.parse returned a %T payload: %s", got, got)
}
if gotInt != expected {
t.Fatalf("int.parse got %d, expected %d", gotInt, expected)
}
}
func TestGoodIntParse(t *testing.T) { func TestGoodIntParse(t *testing.T) {
intParser := processor.IntParse{}
tests := []struct { tests := []struct {
processor processor.Processor processor processor.Processor
name string name string
payload any payload any
expected int64 expected int64
base int
bitSize int
}{ }{
{ {
name: "positive number", name: "positive number",
payload: "12345", payload: "12345",
expected: 12345, expected: 12345,
base: 10,
bitSize: 64,
}, },
{ {
name: "negative number", name: "negative number",
payload: "-12345", payload: "-12345",
expected: -12345, expected: -12345,
base: 10,
bitSize: 64,
}, },
{ {
name: "zero", name: "zero",
payload: "0", payload: "0",
expected: 0, expected: 0,
base: 10,
bitSize: 64,
},
{
name: "binary",
payload: "1010101",
expected: 85,
base: 2,
bitSize: 64,
},
{
name: "hex",
payload: "15F",
expected: 351,
base: 16,
bitSize: 64,
}, },
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
intParser := processor.IntParse{
Base: test.base,
BitSize: test.bitSize,
}
got, err := intParser.Process(t.Context(), test.payload) got, err := intParser.Process(t.Context(), test.payload)
gotInt, ok := got.(int64) gotInt, ok := got.(int64)
if !ok { if !ok {
t.Errorf("int.parse returned a %T payload: %s", got, got) t.Fatalf("int.parse returned a %T payload: %s", got, got)
} }
if err != nil { if err != nil {
t.Errorf("int.parse failed: %s", err) t.Fatalf("int.parse failed: %s", err)
} }
if gotInt != test.expected { if gotInt != test.expected {
t.Errorf("int.parse got %d, expected %d", gotInt, test.expected) t.Fatalf("int.parse got %d, expected %d", gotInt, test.expected)
} }
}) })
} }
} }
func TestBadIntParse(t *testing.T) { func TestBadIntParse(t *testing.T) {
intParser := processor.IntParse{}
tests := []struct { tests := []struct {
processor processor.Processor processor processor.Processor
name string name string
payload any payload any
base int
bitSize int
errorString string errorString string
}{ }{
{ {
name: "non-string input", name: "non-string input",
payload: []byte{0x01}, payload: []byte{0x01},
base: 10,
bitSize: 64,
errorString: "int.parse processor only accepts a string", errorString: "int.parse processor only accepts a string",
}, },
{ {
name: "not int string", name: "not int string",
payload: "123.46", payload: "123.46",
base: 10,
bitSize: 64,
errorString: "strconv.ParseInt: parsing \"123.46\": invalid syntax", errorString: "strconv.ParseInt: parsing \"123.46\": invalid syntax",
}, },
{
name: "bit overflow",
payload: "12345678901234567890",
base: 10,
bitSize: 32,
errorString: "strconv.ParseInt: parsing \"12345678901234567890\": value out of range",
},
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
intParser := processor.IntParse{
Base: test.base,
BitSize: test.bitSize,
}
got, err := intParser.Process(t.Context(), test.payload) got, err := intParser.Process(t.Context(), test.payload)
if err == nil { if err == nil {
t.Errorf("int.parse expected to fail but succeeded, got: %v", got) t.Fatalf("int.parse expected to fail but succeeded, got: %v", got)
} }
if err.Error() != test.errorString { if err.Error() != test.errorString {
t.Errorf("int.parse got error '%s', expected '%s'", err.Error(), test.errorString) t.Fatalf("int.parse got error '%s', expected '%s'", err.Error(), test.errorString)
} }
}) })
} }

View File

@@ -0,0 +1,61 @@
package processor
import (
"context"
"errors"
"math/rand/v2"
"github.com/jwetzell/showbridge-go/internal/config"
)
type IntRandom struct {
Min int
Max int
config config.ProcessorConfig
}
func (up *IntRandom) Process(ctx context.Context, payload any) (any, error) {
payloadInt := rand.IntN(up.Max-up.Min+1) + up.Min
return payloadInt, nil
}
func (up *IntRandom) Type() string {
return up.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "int.random",
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
min, ok := params["min"]
if !ok {
return nil, errors.New("int.random requires a min parameter")
}
minFloat, ok := min.(float64)
if !ok {
return nil, errors.New("int.random min must be a number")
}
max, ok := params["max"]
if !ok {
return nil, errors.New("int.random requires a max parameter")
}
maxFloat, ok := max.(float64)
if !ok {
return nil, errors.New("int.random max must be a number")
}
if maxFloat < minFloat {
return nil, errors.New("int.random max must be greater than min")
}
return &IntRandom{config: config, Min: int(minFloat), Max: int(maxFloat)}, nil
},
})
}

View File

@@ -5,6 +5,7 @@ package processor
import ( import (
"bytes" "bytes"
"context" "context"
"errors"
"fmt" "fmt"
"strconv" "strconv"
"text/template" "text/template"
@@ -13,17 +14,18 @@ import (
"gitlab.com/gomidi/midi/v2" "gitlab.com/gomidi/midi/v2"
) )
// TODO(jwetzell): support using numbers in config file treated as hardcoded values
type MIDIMessageCreate struct { type MIDIMessageCreate struct {
config config.ProcessorConfig config config.ProcessorConfig
ProcessFunc func(ctx context.Context, payload any) (any, error) ProcessFunc func(ctx context.Context, payload any) (any, error)
} }
func (mmd *MIDIMessageCreate) Process(ctx context.Context, payload any) (any, error) { func (mmc *MIDIMessageCreate) Process(ctx context.Context, payload any) (any, error) {
return mmd.ProcessFunc(ctx, payload) return mmc.ProcessFunc(ctx, payload)
} }
func (mmd *MIDIMessageCreate) Type() string { func (mmc *MIDIMessageCreate) Type() string {
return mmd.config.Type return mmc.config.Type
} }
func newMidiNoteOnCreate(config config.ProcessorConfig) (Processor, error) { func newMidiNoteOnCreate(config config.ProcessorConfig) (Processor, error) {
@@ -33,13 +35,13 @@ func newMidiNoteOnCreate(config config.ProcessorConfig) (Processor, error) {
channel, ok := params["channel"] channel, ok := params["channel"]
if !ok { if !ok {
return nil, fmt.Errorf("midi.message.create NoteOn requires a channel parameter") return nil, errors.New("midi.message.create NoteOn requires a channel parameter")
} }
channelString, ok := channel.(string) channelString, ok := channel.(string)
if !ok { if !ok {
return nil, fmt.Errorf("midi.message.create NoteOn channel must be a string") return nil, errors.New("midi.message.create NoteOn channel must be a string")
} }
channelTemplate, err := template.New("channel").Parse(channelString) channelTemplate, err := template.New("channel").Parse(channelString)
@@ -51,13 +53,13 @@ func newMidiNoteOnCreate(config config.ProcessorConfig) (Processor, error) {
note, ok := params["note"] note, ok := params["note"]
if !ok { if !ok {
return nil, fmt.Errorf("midi.message.create NoteOn requires a note parameter") return nil, errors.New("midi.message.create NoteOn requires a note parameter")
} }
noteString, ok := note.(string) noteString, ok := note.(string)
if !ok { if !ok {
return nil, fmt.Errorf("midi.message.create NoteOn note must be a string") return nil, errors.New("midi.message.create NoteOn note must be a string")
} }
noteTemplate, err := template.New("note").Parse(noteString) noteTemplate, err := template.New("note").Parse(noteString)
@@ -69,13 +71,13 @@ func newMidiNoteOnCreate(config config.ProcessorConfig) (Processor, error) {
velocity, ok := params["velocity"] velocity, ok := params["velocity"]
if !ok { if !ok {
return nil, fmt.Errorf("midi.message.create NoteOn requires a velocity parameter") return nil, errors.New("midi.message.create NoteOn requires a velocity parameter")
} }
velocityString, ok := velocity.(string) velocityString, ok := velocity.(string)
if !ok { if !ok {
return nil, fmt.Errorf("midi.message.create NoteOn velocity must be a string") return nil, errors.New("midi.message.create NoteOn velocity must be a string")
} }
velocityTemplate, err := template.New("velocity").Parse(velocityString) velocityTemplate, err := template.New("velocity").Parse(velocityString)
@@ -124,13 +126,13 @@ func newMidiNoteOffCreate(config config.ProcessorConfig) (Processor, error) {
channel, ok := params["channel"] channel, ok := params["channel"]
if !ok { if !ok {
return nil, fmt.Errorf("midi.message.create NoteOn requires a channel parameter") return nil, errors.New("midi.message.create NoteOn requires a channel parameter")
} }
channelString, ok := channel.(string) channelString, ok := channel.(string)
if !ok { if !ok {
return nil, fmt.Errorf("midi.message.create NoteOn channel must be a string") return nil, errors.New("midi.message.create NoteOn channel must be a string")
} }
channelTemplate, err := template.New("channel").Parse(channelString) channelTemplate, err := template.New("channel").Parse(channelString)
@@ -142,13 +144,13 @@ func newMidiNoteOffCreate(config config.ProcessorConfig) (Processor, error) {
note, ok := params["note"] note, ok := params["note"]
if !ok { if !ok {
return nil, fmt.Errorf("midi.message.create NoteOn requires a note parameter") return nil, errors.New("midi.message.create NoteOn requires a note parameter")
} }
noteString, ok := note.(string) noteString, ok := note.(string)
if !ok { if !ok {
return nil, fmt.Errorf("midi.message.create NoteOn note must be a string") return nil, errors.New("midi.message.create NoteOn note must be a string")
} }
noteTemplate, err := template.New("note").Parse(noteString) noteTemplate, err := template.New("note").Parse(noteString)
@@ -189,13 +191,13 @@ func newMidiControlChangeCreate(config config.ProcessorConfig) (Processor, error
channel, ok := params["channel"] channel, ok := params["channel"]
if !ok { if !ok {
return nil, fmt.Errorf("midi.message.create ControlChange requires a channel parameter") return nil, errors.New("midi.message.create ControlChange requires a channel parameter")
} }
channelString, ok := channel.(string) channelString, ok := channel.(string)
if !ok { if !ok {
return nil, fmt.Errorf("midi.message.create ControlChange channel must be a string") return nil, errors.New("midi.message.create ControlChange channel must be a string")
} }
channelTemplate, err := template.New("channel").Parse(channelString) channelTemplate, err := template.New("channel").Parse(channelString)
@@ -207,13 +209,13 @@ func newMidiControlChangeCreate(config config.ProcessorConfig) (Processor, error
controller, ok := params["controller"] controller, ok := params["controller"]
if !ok { if !ok {
return nil, fmt.Errorf("midi.message.create ControlChange requires a controller parameter") return nil, errors.New("midi.message.create ControlChange requires a controller parameter")
} }
controllerString, ok := controller.(string) controllerString, ok := controller.(string)
if !ok { if !ok {
return nil, fmt.Errorf("midi.message.create ControlChange controller must be a string") return nil, errors.New("midi.message.create ControlChange controller must be a string")
} }
controllerTemplate, err := template.New("controller").Parse(controllerString) controllerTemplate, err := template.New("controller").Parse(controllerString)
@@ -225,13 +227,13 @@ func newMidiControlChangeCreate(config config.ProcessorConfig) (Processor, error
value, ok := params["value"] value, ok := params["value"]
if !ok { if !ok {
return nil, fmt.Errorf("midi.message.create ControlChange requires a value parameter") return nil, errors.New("midi.message.create ControlChange requires a value parameter")
} }
valueString, ok := value.(string) valueString, ok := value.(string)
if !ok { if !ok {
return nil, fmt.Errorf("midi.message.create ControlChange value must be a string") return nil, errors.New("midi.message.create ControlChange value must be a string")
} }
valueTemplate, err := template.New("value").Parse(valueString) valueTemplate, err := template.New("value").Parse(valueString)
@@ -281,13 +283,13 @@ func newMidiProgramChangeCreate(config config.ProcessorConfig) (Processor, error
channel, ok := params["channel"] channel, ok := params["channel"]
if !ok { if !ok {
return nil, fmt.Errorf("midi.message.create ProgramChange requires a channel parameter") return nil, errors.New("midi.message.create ProgramChange requires a channel parameter")
} }
channelString, ok := channel.(string) channelString, ok := channel.(string)
if !ok { if !ok {
return nil, fmt.Errorf("midi.message.create ProgramChange channel must be a string") return nil, errors.New("midi.message.create ProgramChange channel must be a string")
} }
channelTemplate, err := template.New("channel").Parse(channelString) channelTemplate, err := template.New("channel").Parse(channelString)
@@ -299,13 +301,13 @@ func newMidiProgramChangeCreate(config config.ProcessorConfig) (Processor, error
program, ok := params["program"] program, ok := params["program"]
if !ok { if !ok {
return nil, fmt.Errorf("midi.message.create ProgramChange requires a program parameter") return nil, errors.New("midi.message.create ProgramChange requires a program parameter")
} }
programString, ok := program.(string) programString, ok := program.(string)
if !ok { if !ok {
return nil, fmt.Errorf("midi.message.create ProgramChange program must be a string") return nil, errors.New("midi.message.create ProgramChange program must be a string")
} }
programTemplate, err := template.New("program").Parse(programString) programTemplate, err := template.New("program").Parse(programString)
@@ -348,13 +350,13 @@ func init() {
msgType, ok := params["type"] msgType, ok := params["type"]
if !ok { if !ok {
return nil, fmt.Errorf("midi.message.create requires a type parameter") return nil, errors.New("midi.message.create requires a type parameter")
} }
msgTypeString, ok := msgType.(string) msgTypeString, ok := msgType.(string)
if !ok { if !ok {
return nil, fmt.Errorf("midi.message.create type parameter must be a string") return nil, errors.New("midi.message.create type parameter must be a string")
} }
switch msgTypeString { switch msgTypeString {

View File

@@ -4,7 +4,7 @@ package processor
import ( import (
"context" "context"
"fmt" "errors"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"gitlab.com/gomidi/midi/v2" "gitlab.com/gomidi/midi/v2"
@@ -18,7 +18,7 @@ func (mmd *MIDIMessageDecode) Process(ctx context.Context, payload any) (any, er
payloadBytes, ok := payload.([]byte) payloadBytes, ok := payload.([]byte)
if !ok { if !ok {
return nil, fmt.Errorf("midi.message.decode processor only accepts a []byte") return nil, errors.New("midi.message.decode processor only accepts a []byte")
} }
payloadMessage := midi.Message(payloadBytes) payloadMessage := midi.Message(payloadBytes)

View File

@@ -4,7 +4,7 @@ package processor
import ( import (
"context" "context"
"fmt" "errors"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"gitlab.com/gomidi/midi/v2" "gitlab.com/gomidi/midi/v2"
@@ -18,7 +18,7 @@ func (mme *MIDIMessageEncode) Process(ctx context.Context, payload any) (any, er
payloadMessage, ok := payload.(midi.Message) payloadMessage, ok := payload.(midi.Message)
if !ok { if !ok {
return nil, fmt.Errorf("midi.message.encode processor only accepts an midi.Message") return nil, errors.New("midi.message.encode processor only accepts an midi.Message")
} }
return payloadMessage.Bytes(), nil return payloadMessage.Bytes(), nil

View File

@@ -4,7 +4,7 @@ package processor
import ( import (
"context" "context"
"fmt" "errors"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"gitlab.com/gomidi/midi/v2" "gitlab.com/gomidi/midi/v2"
@@ -19,7 +19,7 @@ func (mmf *MIDIMessageFilter) Process(ctx context.Context, payload any) (any, er
payloadMessage, ok := payload.(midi.Message) payloadMessage, ok := payload.(midi.Message)
if !ok { if !ok {
return nil, fmt.Errorf("midi.message.filter processor only accepts an midi.Message") return nil, errors.New("midi.message.filter processor only accepts an midi.Message")
} }
if payloadMessage.Type().String() != mmf.MIDIType { if payloadMessage.Type().String() != mmf.MIDIType {
@@ -41,12 +41,12 @@ func init() {
midiType, ok := params["type"] midiType, ok := params["type"]
if !ok { if !ok {
return nil, fmt.Errorf("midi.message.filter requires a type parameter") return nil, errors.New("midi.message.filter requires a type parameter")
} }
midiTypeString, ok := midiType.(string) midiTypeString, ok := midiType.(string)
if !ok { if !ok {
return nil, fmt.Errorf("midi.message.filter type must be a string") return nil, errors.New("midi.message.filter type must be a string")
} }
return &MIDIMessageFilter{config: config, MIDIType: midiTypeString}, nil return &MIDIMessageFilter{config: config, MIDIType: midiTypeString}, nil

View File

@@ -4,6 +4,7 @@ package processor
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
@@ -47,7 +48,7 @@ func (mmu *MIDIMessageUnpack) Process(ctx context.Context, payload any) (any, er
payloadMidi, ok := payload.(midi.Message) payloadMidi, ok := payload.(midi.Message)
if !ok { if !ok {
return nil, fmt.Errorf("midi.message.unpack processor only accepts a midi.Message") return nil, errors.New("midi.message.unpack processor only accepts a midi.Message")
} }
switch payloadMidi.Type() { switch payloadMidi.Type() {

View File

@@ -2,7 +2,7 @@ package processor
import ( import (
"context" "context"
"fmt" "errors"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
@@ -74,44 +74,44 @@ func init() {
topic, ok := params["topic"] topic, ok := params["topic"]
if !ok { if !ok {
return nil, fmt.Errorf("mqtt.message.create requires an topic parameter") return nil, errors.New("mqtt.message.create requires a topic parameter")
} }
topicString, ok := topic.(string) topicString, ok := topic.(string)
if !ok { if !ok {
return nil, fmt.Errorf("mqtt.message.create topic must be a string") return nil, errors.New("mqtt.message.create topic must be a string")
} }
qos, ok := params["qos"] qos, ok := params["qos"]
if !ok { if !ok {
return nil, fmt.Errorf("mqtt.message.create requires an qos parameter") return nil, errors.New("mqtt.message.create requires a qos parameter")
} }
qosByte, ok := qos.(float64) qosByte, ok := qos.(float64)
if !ok { if !ok {
return nil, fmt.Errorf("mqtt.message.create qos must be a number") return nil, errors.New("mqtt.message.create qos must be a number")
} }
retained, ok := params["retained"] retained, ok := params["retained"]
if !ok { if !ok {
return nil, fmt.Errorf("mqtt.message.create requires an retained parameter") return nil, errors.New("mqtt.message.create requires a retained parameter")
} }
retainedBool, ok := retained.(bool) retainedBool, ok := retained.(bool)
if !ok { if !ok {
return nil, fmt.Errorf("mqtt.message.create retained must be a boolean") return nil, errors.New("mqtt.message.create retained must be a boolean")
} }
//TODO(jwetzell): convert payload into []byte or string for sending //TODO(jwetzell): convert payload into []byte or string for sending
payload, ok := params["payload"] payload, ok := params["payload"]
if !ok { if !ok {
return nil, fmt.Errorf("mqtt.message.create requires an payload parameter") return nil, errors.New("mqtt.message.create requires a payload parameter")
} }
if payloadBytes, ok := payload.([]byte); ok { if payloadBytes, ok := payload.([]byte); ok {
@@ -121,7 +121,7 @@ func init() {
payloadString, ok := payload.(string) payloadString, ok := payload.(string)
if !ok { if !ok {
return nil, fmt.Errorf("mqtt.message.create payload must be a string or byte array") return nil, errors.New("mqtt.message.create payload must be a string or byte array")
} }
payloadBytes := []byte(payloadString) payloadBytes := []byte(payloadString)

View File

@@ -2,7 +2,7 @@ package processor
import ( import (
"context" "context"
"fmt" "errors"
mqtt "github.com/eclipse/paho.mqtt.golang" mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
@@ -16,7 +16,7 @@ func (mme *MQTTMessageEncode) Process(ctx context.Context, payload any) (any, er
payloadMessage, ok := payload.(mqtt.Message) payloadMessage, ok := payload.(mqtt.Message)
if !ok { if !ok {
return nil, fmt.Errorf("mqtt.message.encode processor only accepts an mqtt.Message") return nil, errors.New("mqtt.message.encode processor only accepts an mqtt.Message")
} }
return payloadMessage.Payload(), nil return payloadMessage.Payload(), nil

View File

@@ -3,7 +3,7 @@ package processor
import ( import (
"bytes" "bytes"
"context" "context"
"fmt" "errors"
"text/template" "text/template"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
@@ -52,25 +52,25 @@ func init() {
subject, ok := params["subject"] subject, ok := params["subject"]
if !ok { if !ok {
return nil, fmt.Errorf("nats.message.create requires a subject parameter") return nil, errors.New("nats.message.create requires a subject parameter")
} }
subjectString, ok := subject.(string) subjectString, ok := subject.(string)
if !ok { if !ok {
return nil, fmt.Errorf("nats.message.create subject must be a string") return nil, errors.New("nats.message.create subject must be a string")
} }
payload, ok := params["payload"] payload, ok := params["payload"]
if !ok { if !ok {
return nil, fmt.Errorf("osc.message.create requires a payload parameter") return nil, errors.New("nats.message.create requires a payload parameter")
} }
payloadString, ok := payload.(string) payloadString, ok := payload.(string)
if !ok { if !ok {
return nil, fmt.Errorf("osc.message.create payload must be a string") return nil, errors.New("nats.message.create payload must be a string")
} }
payloadTemplate, err := template.New("payload").Parse(payloadString) payloadTemplate, err := template.New("payload").Parse(payloadString)

View File

@@ -2,7 +2,7 @@ package processor
import ( import (
"context" "context"
"fmt" "errors"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/nats-io/nats.go" "github.com/nats-io/nats.go"
@@ -16,7 +16,7 @@ func (nme *NATSMessageEncode) Process(ctx context.Context, payload any) (any, er
payloadMessage, ok := payload.(*nats.Msg) payloadMessage, ok := payload.(*nats.Msg)
if !ok { if !ok {
return nil, fmt.Errorf("nats.message.encode processor only accepts an nats.Msg") return nil, errors.New("nats.message.encode processor only accepts an nats.Msg")
} }
return payloadMessage.Data, nil return payloadMessage.Data, nil

View File

@@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"context" "context"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"strconv" "strconv"
"text/template" "text/template"
@@ -19,10 +20,10 @@ type OSCMessageCreate struct {
Types string Types string
} }
func (o *OSCMessageCreate) Process(ctx context.Context, payload any) (any, error) { func (omc *OSCMessageCreate) Process(ctx context.Context, payload any) (any, error) {
var addressBuffer bytes.Buffer var addressBuffer bytes.Buffer
err := o.Address.Execute(&addressBuffer, payload) err := omc.Address.Execute(&addressBuffer, payload)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -31,11 +32,11 @@ func (o *OSCMessageCreate) Process(ctx context.Context, payload any) (any, error
addressString := addressBuffer.String() addressString := addressBuffer.String()
if len(addressString) == 0 { if len(addressString) == 0 {
return nil, fmt.Errorf("osc.message.create address must not be empty") return nil, errors.New("osc.message.create address must not be empty")
} }
if addressString[0] != '/' { if addressString[0] != '/' {
return nil, fmt.Errorf("osc.message.create address must start with '/'") return nil, errors.New("osc.message.create address must start with '/'")
} }
payloadMessage := osc.OSCMessage{ payloadMessage := osc.OSCMessage{
@@ -44,7 +45,7 @@ func (o *OSCMessageCreate) Process(ctx context.Context, payload any) (any, error
args := []osc.OSCArg{} args := []osc.OSCArg{}
for argIndex, argTemplate := range o.Args { for argIndex, argTemplate := range omc.Args {
var argBuffer bytes.Buffer var argBuffer bytes.Buffer
err := argTemplate.Execute(&argBuffer, payload) err := argTemplate.Execute(&argBuffer, payload)
@@ -54,7 +55,7 @@ func (o *OSCMessageCreate) Process(ctx context.Context, payload any) (any, error
argString := argBuffer.String() argString := argBuffer.String()
typedArg, err := argToTypedArg(argString, o.Types[argIndex]) typedArg, err := argToTypedArg(argString, omc.Types[argIndex])
if err != nil { if err != nil {
return nil, err return nil, err
@@ -70,8 +71,8 @@ func (o *OSCMessageCreate) Process(ctx context.Context, payload any) (any, error
return payloadMessage, nil return payloadMessage, nil
} }
func (o *OSCMessageCreate) Type() string { func (omc *OSCMessageCreate) Type() string {
return o.config.Type return omc.config.Type
} }
func init() { func init() {
@@ -82,13 +83,13 @@ func init() {
address, ok := params["address"] address, ok := params["address"]
if !ok { if !ok {
return nil, fmt.Errorf("osc.message.create requires an address parameter") return nil, errors.New("osc.message.create requires an address parameter")
} }
addressString, ok := address.(string) addressString, ok := address.(string)
if !ok { if !ok {
return nil, fmt.Errorf("osc.message.create address must be a string") return nil, errors.New("osc.message.create address must be a string")
} }
addressTemplate, err := template.New("address").Parse(addressString) addressTemplate, err := template.New("address").Parse(addressString)
@@ -109,17 +110,17 @@ func init() {
types, ok := params["types"] types, ok := params["types"]
if !ok { if !ok {
return nil, fmt.Errorf("osc.message.create requires a types parameter with args") return nil, errors.New("osc.message.create requires a types parameter with args")
} }
typesString, ok := types.(string) typesString, ok := types.(string)
if !ok { if !ok {
return nil, fmt.Errorf("osc.message.create types must be a string") return nil, errors.New("osc.message.create types must be a string")
} }
if len(rawArgs) != len(typesString) { if len(rawArgs) != len(typesString) {
return nil, fmt.Errorf("osc.message.create args and types must be the same length") return nil, errors.New("osc.message.create args and types must be the same length")
} }
argTemplates := []*template.Template{} argTemplates := []*template.Template{}
@@ -128,7 +129,7 @@ func init() {
argString, ok := rawArg.(string) argString, ok := rawArg.(string)
if !ok { if !ok {
return nil, fmt.Errorf("osc.message.create arg must be a string") return nil, errors.New("osc.message.create arg must be a string")
} }
argTemplate, err := template.New("arg").Parse(argString) argTemplate, err := template.New("arg").Parse(argString)

View File

@@ -2,7 +2,7 @@ package processor
import ( import (
"context" "context"
"fmt" "errors"
osc "github.com/jwetzell/osc-go" osc "github.com/jwetzell/osc-go"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
@@ -12,19 +12,19 @@ type OSCMessageDecode struct {
config config.ProcessorConfig config config.ProcessorConfig
} }
func (o *OSCMessageDecode) Process(ctx context.Context, payload any) (any, error) { func (omd *OSCMessageDecode) Process(ctx context.Context, payload any) (any, error) {
payloadBytes, ok := payload.([]byte) payloadBytes, ok := payload.([]byte)
if !ok { if !ok {
return nil, fmt.Errorf("osc.message.decode processor only accepts a []byte payload") return nil, errors.New("osc.message.decode processor only accepts a []byte payload")
} }
if len(payloadBytes) == 0 { if len(payloadBytes) == 0 {
return nil, fmt.Errorf("osc.message.decode processor can't work on empty []byte") return nil, errors.New("osc.message.decode processor can't work on empty []byte")
} }
if payloadBytes[0] != '/' { if payloadBytes[0] != '/' {
return nil, fmt.Errorf("osc.message.decode processor needs an OSC looking []byte") return nil, errors.New("osc.message.decode processor needs an OSC looking []byte")
} }
message, err := osc.MessageFromBytes(payloadBytes) message, err := osc.MessageFromBytes(payloadBytes)
@@ -34,8 +34,8 @@ func (o *OSCMessageDecode) Process(ctx context.Context, payload any) (any, error
return message, nil return message, nil
} }
func (o *OSCMessageDecode) Type() string { func (omd *OSCMessageDecode) Type() string {
return o.config.Type return omd.config.Type
} }
func init() { func init() {

View File

@@ -2,7 +2,7 @@ package processor
import ( import (
"context" "context"
"fmt" "errors"
osc "github.com/jwetzell/osc-go" osc "github.com/jwetzell/osc-go"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
@@ -12,19 +12,19 @@ type OSCMessageEncode struct {
config config.ProcessorConfig config config.ProcessorConfig
} }
func (o *OSCMessageEncode) Process(ctx context.Context, payload any) (any, error) { func (ome *OSCMessageEncode) Process(ctx context.Context, payload any) (any, error) {
payloadMessage, ok := payload.(osc.OSCMessage) payloadMessage, ok := payload.(osc.OSCMessage)
if !ok { if !ok {
return nil, fmt.Errorf("osc.message.encode processor only accepts an OSCMessage") return nil, errors.New("osc.message.encode processor only accepts an OSCMessage")
} }
bytes := payloadMessage.ToBytes() bytes := payloadMessage.ToBytes()
return bytes, nil return bytes, nil
} }
func (o *OSCMessageEncode) Type() string { func (ome *OSCMessageEncode) Type() string {
return o.config.Type return ome.config.Type
} }
func init() { func init() {

View File

@@ -2,6 +2,7 @@ package processor
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"regexp" "regexp"
"strings" "strings"
@@ -15,23 +16,23 @@ type OSCMessageFilter struct {
Address *regexp.Regexp Address *regexp.Regexp
} }
func (o *OSCMessageFilter) Process(ctx context.Context, payload any) (any, error) { func (omf *OSCMessageFilter) Process(ctx context.Context, payload any) (any, error) {
payloadMessage, ok := payload.(osc.OSCMessage) payloadMessage, ok := payload.(osc.OSCMessage)
if !ok { if !ok {
return nil, fmt.Errorf("osc.message.filter can only operate on OSCMessage payloads") return nil, errors.New("osc.message.filter can only operate on OSCMessage payloads")
} }
if !o.Address.MatchString(payloadMessage.Address) { if !omf.Address.MatchString(payloadMessage.Address) {
return nil, nil return nil, nil
} }
return payloadMessage, nil return payloadMessage, nil
} }
func (o *OSCMessageFilter) Type() string { func (omf *OSCMessageFilter) Type() string {
return o.config.Type return omf.config.Type
} }
func init() { func init() {
@@ -42,13 +43,13 @@ func init() {
address, ok := params["address"] address, ok := params["address"]
if !ok { if !ok {
return nil, fmt.Errorf("osc.message.filter requires an address parameter") return nil, errors.New("osc.message.filter requires an address parameter")
} }
addressString, ok := address.(string) addressString, ok := address.(string)
if !ok { if !ok {
return nil, fmt.Errorf("osc.message.filter address must be a string") return nil, errors.New("osc.message.filter address must be a string")
} }
addressPattern := strings.ReplaceAll(addressString, "?", ".") addressPattern := strings.ReplaceAll(addressString, "?", ".")

View File

@@ -3,7 +3,7 @@ package processor
import ( import (
"bytes" "bytes"
"context" "context"
"fmt" "errors"
"text/template" "text/template"
osc "github.com/jwetzell/osc-go" osc "github.com/jwetzell/osc-go"
@@ -15,16 +15,15 @@ type OSCMessageTransform struct {
Address *template.Template Address *template.Template
} }
func (o *OSCMessageTransform) Process(ctx context.Context, payload any) (any, error) { func (omt *OSCMessageTransform) Process(ctx context.Context, payload any) (any, error) {
payloadMessage, ok := payload.(osc.OSCMessage) payloadMessage, ok := payload.(osc.OSCMessage)
if !ok { if !ok {
return nil, fmt.Errorf("osc.message.transform processor only accepts an OSCMessage") return nil, errors.New("osc.message.transform processor only accepts an OSCMessage")
} }
var addressBuffer bytes.Buffer var addressBuffer bytes.Buffer
//TODO(jwetzell): actually inject data into template err := omt.Address.Execute(&addressBuffer, payloadMessage)
err := o.Address.Execute(&addressBuffer, payloadMessage)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -33,11 +32,11 @@ func (o *OSCMessageTransform) Process(ctx context.Context, payload any) (any, er
addressString := addressBuffer.String() addressString := addressBuffer.String()
if len(addressString) == 0 { if len(addressString) == 0 {
return nil, fmt.Errorf("osc.message.transform address must not be empty") return nil, errors.New("osc.message.transform address must not be empty")
} }
if addressString[0] != '/' { if addressString[0] != '/' {
return nil, fmt.Errorf("osc.message.transform address must start with '/'") return nil, errors.New("osc.message.transform address must start with '/'")
} }
payloadMessage.Address = addressString payloadMessage.Address = addressString
@@ -45,8 +44,8 @@ func (o *OSCMessageTransform) Process(ctx context.Context, payload any) (any, er
return payloadMessage, nil return payloadMessage, nil
} }
func (o *OSCMessageTransform) Type() string { func (omt *OSCMessageTransform) Type() string {
return o.config.Type return omt.config.Type
} }
func init() { func init() {
@@ -57,13 +56,13 @@ func init() {
address, ok := params["address"] address, ok := params["address"]
if !ok { if !ok {
return nil, fmt.Errorf("osc.message.transform requires an address parameter") return nil, errors.New("osc.message.transform requires an address parameter")
} }
addressString, ok := address.(string) addressString, ok := address.(string)
if !ok { if !ok {
return nil, fmt.Errorf("osc.message.transform address must be a string") return nil, errors.New("osc.message.transform address must be a string")
} }
addressTemplate, err := template.New("address").Parse(addressString) addressTemplate, err := template.New("address").Parse(addressString)

View File

@@ -0,0 +1,62 @@
package processor_test
import (
"context"
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
type TestProcessor struct {
}
func (p *TestProcessor) Type() string {
return "test"
}
func (p *TestProcessor) Process(ctx context.Context, input any) (any, error) {
return input, nil
}
func TestProcessorBadRegistrationNoType(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Fatalf("processor registration should have panicked but did not")
}
}()
processor.RegisterProcessor(processor.ProcessorRegistration{
Type: "",
New: func(config config.ProcessorConfig) (processor.Processor, error) {
return &TestProcessor{}, nil
},
})
}
func TestProcessorBadRegistrationNoNew(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Fatalf("processor registration should have panicked but did not")
}
}()
processor.RegisterProcessor(processor.ProcessorRegistration{
Type: "test",
New: nil,
})
}
func TestProcessorBadRegistrationExistingType(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Fatalf("processor registration should have panicked but did not")
}
}()
processor.RegisterProcessor(processor.ProcessorRegistration{
Type: "string.create",
New: func(config config.ProcessorConfig) (processor.Processor, error) {
return &TestProcessor{}, nil
},
})
}

View File

@@ -2,7 +2,7 @@ package processor
import ( import (
"context" "context"
"fmt" "errors"
"github.com/expr-lang/expr" "github.com/expr-lang/expr"
"github.com/expr-lang/expr/vm" "github.com/expr-lang/expr/vm"
@@ -38,13 +38,13 @@ func init() {
expression, ok := params["expression"] expression, ok := params["expression"]
if !ok { if !ok {
return nil, fmt.Errorf("script.expr requires an expression parameter") return nil, errors.New("script.expr requires an expression parameter")
} }
expressionString, ok := expression.(string) expressionString, ok := expression.(string)
if !ok { if !ok {
return nil, fmt.Errorf("script.expr expression must be a string") return nil, errors.New("script.expr expression must be a string")
} }
program, err := expr.Compile(expressionString) program, err := expr.Compile(expressionString)

View File

@@ -0,0 +1,171 @@
package processor_test
import (
"testing"
"github.com/expr-lang/expr"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestScriptExprFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["script.expr"]
if !ok {
t.Fatalf("script.expr processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "script.expr",
Params: map[string]any{
"expression": "foo + bar",
},
})
if err != nil {
t.Fatalf("failed to create script.expr processor: %s", err)
}
if processorInstance.Type() != "script.expr" {
t.Fatalf("script.expr processor has wrong type: %s", processorInstance.Type())
}
}
func TestScriptExprNoProgram(t *testing.T) {
registration, ok := processor.ProcessorRegistry["script.expr"]
if !ok {
t.Fatalf("script.expr processor not registered")
}
_, err := registration.New(config.ProcessorConfig{
Type: "script.expr",
Params: map[string]any{},
})
if err == nil {
t.Fatalf("script.expr processor should have thrown an error when creating")
}
}
func TestScriptExprBadConfigWrongExpressionType(t *testing.T) {
registration, ok := processor.ProcessorRegistry["script.expr"]
if !ok {
t.Fatalf("script.expr processor not registered")
}
_, err := registration.New(config.ProcessorConfig{
Type: "script.expr",
Params: map[string]any{
"expression": 12345,
},
})
if err == nil {
t.Fatalf("script.expr processor should have thrown an error when creating with non-string expression")
}
}
func TestScriptExprBadConfigNonCompilingExpression(t *testing.T) {
registration, ok := processor.ProcessorRegistry["script.expr"]
if !ok {
t.Fatalf("script.expr processor not registered")
}
_, err := registration.New(config.ProcessorConfig{
Type: "script.expr",
Params: map[string]any{
"expression": "foo + ",
},
})
if err == nil {
t.Fatalf("script.expr processor should have thrown an error when creating with non-compiling expression")
}
}
func TestGoodScriptExpr(t *testing.T) {
tests := []struct {
program string
name string
payload map[string]any
expected any
}{
{
program: "foo + bar",
name: "number",
payload: map[string]any{
"foo": 1,
"bar": 1,
},
expected: 2,
},
{
program: "foo + bar",
name: "string",
payload: map[string]any{
"foo": "1",
"bar": "1",
},
expected: "11",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
program, err := expr.Compile(test.program)
if err != nil {
t.Fatalf("script.expr failed to compile program: %s", err)
}
exprProcessor := &processor.ScriptExpr{Program: program}
got, err := exprProcessor.Process(t.Context(), test.payload)
if err != nil {
t.Fatalf("script.expr failed: %s", err)
}
//TODO(jwetzell): work out better way to compare the any/any
if got != test.expected {
t.Fatalf("script.expr got %+v (%T), expected %+v (%T)", got, got, test.expected, test.expected)
}
})
}
}
func TestBadScriptExpr(t *testing.T) {
tests := []struct {
program string
name string
payload map[string]any
errorString string
}{
{
name: "accessing missing field",
program: "foo + bar",
payload: map[string]any{
"foo": 1,
},
errorString: "invalid operation: int + <nil> (1:5)\n | foo + bar\n | ....^",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
program, err := expr.Compile(test.program)
if err != nil {
t.Fatalf("script.expr failed to compile program: %s", err)
}
exprProcessor := &processor.ScriptExpr{Program: program}
got, err := exprProcessor.Process(t.Context(), test.payload)
if err == nil {
t.Fatalf("script.expr expected to fail but succeeded, got: %v", got)
}
if err.Error() != test.errorString {
t.Fatalf("script.expr got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -3,7 +3,7 @@ package processor
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "errors"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"modernc.org/quickjs" "modernc.org/quickjs"
@@ -74,13 +74,13 @@ func init() {
program, ok := params["program"] program, ok := params["program"]
if !ok { if !ok {
return nil, fmt.Errorf("script.js requires a program parameter") return nil, errors.New("script.js requires a program parameter")
} }
programString, ok := program.(string) programString, ok := program.(string)
if !ok { if !ok {
return nil, fmt.Errorf("script.js program must be a string") return nil, errors.New("script.js program must be a string")
} }
return &ScriptJS{config: config, Program: programString}, nil return &ScriptJS{config: config, Program: programString}, nil

View File

@@ -0,0 +1,179 @@
package processor_test
import (
"maps"
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestScriptJSFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["script.js"]
if !ok {
t.Fatalf("script.js processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "script.js",
Params: map[string]any{
"program": `
payload = payload + 1
`,
},
})
if err != nil {
t.Fatalf("failed to create script.js processor: %s", err)
}
if processorInstance.Type() != "script.js" {
t.Fatalf("script.js processor has wrong type: %s", processorInstance.Type())
}
payload := 1
expected := 2
got, err := processorInstance.Process(t.Context(), payload)
if err != nil {
t.Fatalf("script.js processing failed: %s", err)
}
if got != expected {
t.Fatalf("script.js got %+v, expected %+v", got, expected)
}
}
func TestScriptJSNoProgram(t *testing.T) {
registration, ok := processor.ProcessorRegistry["script.js"]
if !ok {
t.Fatalf("script.js processor not registered")
}
_, err := registration.New(config.ProcessorConfig{
Type: "script.js",
Params: map[string]any{},
})
if err == nil {
t.Fatalf("script.js processor should have thrown an error when creating")
}
}
func TestScriptJSBadConfigWrongProgramType(t *testing.T) {
registration, ok := processor.ProcessorRegistry["script.js"]
if !ok {
t.Fatalf("script.js processor not registered")
}
_, err := registration.New(config.ProcessorConfig{
Type: "script.js",
Params: map[string]any{
"program": 12345,
},
})
if err == nil {
t.Fatalf("script.js processor should have thrown an error when creating with non-string program")
}
}
func TestGoodScriptJS(t *testing.T) {
tests := []struct {
processor processor.Processor
name string
payload any
expected any
}{
{
name: "number",
processor: &processor.ScriptJS{Program: `
payload = payload + 1
`},
payload: 1,
expected: 2,
},
{
name: "string",
processor: &processor.ScriptJS{Program: `
payload = payload + "1"
`},
payload: "1",
expected: "11",
},
{
name: "object",
processor: &processor.ScriptJS{Program: `
payload = { key: payload }
`},
payload: "1",
expected: map[string]any{"key": "1"},
},
{
name: "nil",
processor: &processor.ScriptJS{Program: `
payload = undefined
`},
payload: "1",
expected: nil,
},
}
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("script.js process failed: %s", err)
}
//TODO(jwetzell): work out better way to compare the any/any
gotMap, ok := got.(map[string]interface{})
if ok {
// got a map
expectedMap, ok := test.expected.(map[string]interface{})
if ok {
if !maps.Equal(gotMap, expectedMap) {
t.Fatalf("script.js got %+v, expected %+v", got, test.expected)
}
} else {
t.Fatalf("script.js got %+v, expected %+v", got, test.expected)
}
} else {
if got != test.expected {
t.Fatalf("script.js got %+v, expected %+v", got, test.expected)
}
}
})
}
}
func TestBadScriptJS(t *testing.T) {
tests := []struct {
name string
processor processor.Processor
payload any
errorString string
}{
{
name: "accessing not defined variable",
processor: &processor.ScriptJS{Program: `paylod = foo`},
payload: 0,
errorString: "ReferenceError: 'foo' is not defined",
},
}
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("script.js expected to fail but succeeded, got: %v", got)
}
if err.Error() != test.errorString {
t.Fatalf("script.js got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -3,7 +3,7 @@ package processor
import ( import (
"bytes" "bytes"
"context" "context"
"fmt" "errors"
"text/template" "text/template"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
@@ -39,13 +39,13 @@ func init() {
tmpl, ok := params["template"] tmpl, ok := params["template"]
if !ok { if !ok {
return nil, fmt.Errorf("string.create requires a template parameter") return nil, errors.New("string.create requires a template parameter")
} }
templateString, ok := tmpl.(string) templateString, ok := tmpl.(string)
if !ok { if !ok {
return nil, fmt.Errorf("string.create template must be a string") return nil, errors.New("string.create template must be a string")
} }
templateTemplate, err := template.New("template").Parse(templateString) templateTemplate, err := template.New("template").Parse(templateString)

View File

@@ -0,0 +1,81 @@
package processor_test
import (
"testing"
"text/template"
"github.com/jwetzell/showbridge-go/internal/processor"
)
type TestStruct struct {
Data string
}
func (t TestStruct) GetData() string {
return t.Data
}
func TestGoodStringCreate(t *testing.T) {
tests := []struct {
name string
template string
payload any
expected string
}{
{
name: "string payload",
template: "{{.}}",
payload: "hello",
expected: "hello",
},
{
name: "number payload",
template: "{{.}}",
payload: 4,
expected: "4",
},
{
name: "boolean payload",
template: "{{.}}",
payload: true,
expected: "true",
},
{
name: "struct payload - field",
template: "{{.Data}}",
payload: TestStruct{Data: "test"},
expected: "test",
},
{
name: "struct payload - method",
template: "{{.GetData}}",
payload: TestStruct{Data: "test"},
expected: "test",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
template, err := template.New("template").Parse(test.template)
if err != nil {
t.Fatalf("string.create template parsing failed: %s", err)
}
processor := &processor.StringCreate{Template: template}
got, err := processor.Process(t.Context(), test.payload)
gotStrings, ok := got.(string)
if !ok {
t.Fatalf("string.create returned a %T payload: %s", got, got)
}
if err != nil {
t.Fatalf("string.create failed: %s", err)
}
if gotStrings != test.expected {
t.Fatalf("string.create got %s, expected %s", got, test.expected)
}
})
}
}

View File

@@ -2,7 +2,7 @@ package processor
import ( import (
"context" "context"
"fmt" "errors"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
@@ -15,7 +15,7 @@ func (sd *StringDecode) Process(ctx context.Context, payload any) (any, error) {
payloadBytes, ok := payload.([]byte) payloadBytes, ok := payload.([]byte)
if !ok { if !ok {
return nil, fmt.Errorf("string.decode processor only accepts a []byte") return nil, errors.New("string.decode processor only accepts a []byte")
} }
payloadMessage := string(payloadBytes) payloadMessage := string(payloadBytes)

View File

@@ -28,13 +28,13 @@ func TestGoodStringDecode(t *testing.T) {
gotString, ok := got.(string) gotString, ok := got.(string)
if !ok { if !ok {
t.Errorf("string.decode returned a %T payload: %s", got, got) t.Fatalf("string.decode returned a %T payload: %s", got, got)
} }
if err != nil { if err != nil {
t.Errorf("string.decode failed: %s", err) t.Fatalf("string.decode failed: %s", err)
} }
if gotString != test.expected { if gotString != test.expected {
t.Errorf("string.decode got %s, expected %s", got, test.expected) t.Fatalf("string.decode got %s, expected %s", got, test.expected)
} }
}) })
} }
@@ -61,10 +61,10 @@ func TestBadStringDecode(t *testing.T) {
got, err := test.processor.Process(t.Context(), test.payload) got, err := test.processor.Process(t.Context(), test.payload)
if err == nil { if err == nil {
t.Errorf("string.decode expected to fail but got payload: %s", got) t.Fatalf("string.decode expected to fail but got payload: %s", got)
} }
if err.Error() != test.errorString { if err.Error() != test.errorString {
t.Errorf("string.decode got error '%s', expected '%s'", err.Error(), test.errorString) t.Fatalf("string.decode got error '%s', expected '%s'", err.Error(), test.errorString)
} }
}) })
} }

View File

@@ -2,7 +2,7 @@ package processor
import ( import (
"context" "context"
"fmt" "errors"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
@@ -15,7 +15,7 @@ func (se *StringEncode) Process(ctx context.Context, payload any) (any, error) {
payloadString, ok := payload.(string) payloadString, ok := payload.(string)
if !ok { if !ok {
return nil, fmt.Errorf("string.encode processor only accepts a string") return nil, errors.New("string.encode processor only accepts a string")
} }
payloadBytes := []byte(payloadString) payloadBytes := []byte(payloadString)

View File

@@ -29,13 +29,13 @@ func TestGoodStringEncode(t *testing.T) {
gotBytes, ok := got.([]byte) gotBytes, ok := got.([]byte)
if !ok { if !ok {
t.Errorf("string.encode returned a %T payload: %s", got, got) t.Fatalf("string.encode returned a %T payload: %s", got, got)
} }
if err != nil { if err != nil {
t.Errorf("string.encode failed: %s", err) t.Fatalf("string.encode failed: %s", err)
} }
if !slices.Equal(gotBytes, test.expected) { if !slices.Equal(gotBytes, test.expected) {
t.Errorf("string.encode got %s, expected %s", got, test.expected) t.Fatalf("string.encode got %s, expected %s", got, test.expected)
} }
}) })
} }
@@ -62,10 +62,10 @@ func TestBadStringEncode(t *testing.T) {
got, err := test.processor.Process(t.Context(), test.payload) got, err := test.processor.Process(t.Context(), test.payload)
if err == nil { if err == nil {
t.Errorf("string.encode expected to fail but got payload: %s", got) t.Fatalf("string.encode expected to fail but got payload: %s", got)
} }
if err.Error() != test.errorString { if err.Error() != test.errorString {
t.Errorf("string.encode got error '%s', expected '%s'", err.Error(), test.errorString) t.Fatalf("string.encode got error '%s', expected '%s'", err.Error(), test.errorString)
} }
}) })
} }

View File

@@ -2,7 +2,7 @@ package processor
import ( import (
"context" "context"
"fmt" "errors"
"regexp" "regexp"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
@@ -13,22 +13,22 @@ type StringFilter struct {
Pattern *regexp.Regexp Pattern *regexp.Regexp
} }
func (se *StringFilter) Process(ctx context.Context, payload any) (any, error) { func (sf *StringFilter) Process(ctx context.Context, payload any) (any, error) {
payloadString, ok := payload.(string) payloadString, ok := payload.(string)
if !ok { if !ok {
return nil, fmt.Errorf("string.filter processor only accepts a string") return nil, errors.New("string.filter processor only accepts a string")
} }
if !se.Pattern.MatchString(payloadString) { if !sf.Pattern.MatchString(payloadString) {
return nil, nil return nil, nil
} }
return payloadString, nil return payloadString, nil
} }
func (se *StringFilter) Type() string { func (sf *StringFilter) Type() string {
return se.config.Type return sf.config.Type
} }
func init() { func init() {
@@ -40,16 +40,16 @@ func init() {
pattern, ok := params["pattern"] pattern, ok := params["pattern"]
if !ok { if !ok {
return nil, fmt.Errorf("http.request.filter requires an pattern parameter") return nil, errors.New("string.filter requires a pattern parameter")
} }
patternString, ok := pattern.(string) patternString, ok := pattern.(string)
if !ok { if !ok {
return nil, fmt.Errorf("http.request.filter pattern must be a string") return nil, errors.New("string.filter pattern must be a string")
} }
patternRegexp, err := regexp.Compile(fmt.Sprintf("^%s$", patternString)) patternRegexp, err := regexp.Compile(patternString)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@@ -2,7 +2,7 @@ package processor
import ( import (
"context" "context"
"fmt" "errors"
"strings" "strings"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
@@ -13,20 +13,20 @@ type StringSplit struct {
Separator string Separator string
} }
func (se *StringSplit) Process(ctx context.Context, payload any) (any, error) { func (ss *StringSplit) Process(ctx context.Context, payload any) (any, error) {
payloadString, ok := payload.(string) payloadString, ok := payload.(string)
if !ok { if !ok {
return nil, fmt.Errorf("string.split only accepts a string") return nil, errors.New("string.split only accepts a string")
} }
payloadParts := strings.Split(payloadString, se.Separator) payloadParts := strings.Split(payloadString, ss.Separator)
return payloadParts, nil return payloadParts, nil
} }
func (se *StringSplit) Type() string { func (ss *StringSplit) Type() string {
return se.config.Type return ss.config.Type
} }
func init() { func init() {
@@ -38,13 +38,13 @@ func init() {
separator, ok := params["separator"] separator, ok := params["separator"]
if !ok { if !ok {
return nil, fmt.Errorf("string.split requires a separator") return nil, errors.New("string.split requires a separator")
} }
separatorString, ok := separator.(string) separatorString, ok := separator.(string)
if !ok { if !ok {
return nil, fmt.Errorf("string.split separator must be a string") return nil, errors.New("string.split separator must be a string")
} }
return &StringSplit{config: config, Separator: separatorString}, nil return &StringSplit{config: config, Separator: separatorString}, nil

View File

@@ -28,19 +28,19 @@ func TestGoodStringSplit(t *testing.T) {
gotStrings, ok := got.([]string) gotStrings, ok := got.([]string)
if !ok { if !ok {
t.Errorf("string.split returned a %T payload: %s", got, got) t.Fatalf("string.split returned a %T payload: %s", got, got)
} }
if err != nil { if err != nil {
t.Errorf("string.split failed: %s", err) t.Fatalf("string.split failed: %s", err)
} }
if !slices.Equal(gotStrings, test.expected) { if !slices.Equal(gotStrings, test.expected) {
t.Errorf("string.split got %s, expected %s", got, test.expected) t.Fatalf("string.split got %s, expected %s", got, test.expected)
} }
}) })
} }
} }
func TestBasStringSplit(t *testing.T) { func TestBadStringSplit(t *testing.T) {
tests := []struct { tests := []struct {
processor processor.Processor processor processor.Processor
name string name string
@@ -60,10 +60,10 @@ func TestBasStringSplit(t *testing.T) {
got, err := test.processor.Process(t.Context(), test.payload) got, err := test.processor.Process(t.Context(), test.payload)
if err == nil { if err == nil {
t.Errorf("string.split expected error but got none, payload: %s", got) t.Fatalf("string.split expected error but got none, payload: %s", got)
} }
if err.Error() != test.errorString { if err.Error() != test.errorString {
t.Errorf("string.split got error '%s', expected '%s'", err.Error(), test.errorString) t.Fatalf("string.split got error '%s', expected '%s'", err.Error(), test.errorString)
} }
}) })
} }

View File

@@ -0,0 +1,45 @@
package processor
import (
"context"
"errors"
"time"
"github.com/jwetzell/showbridge-go/internal/config"
)
type MetaDelay struct {
config config.ProcessorConfig
Duration time.Duration
}
func (md *MetaDelay) Process(ctx context.Context, payload any) (any, error) {
time.Sleep(md.Duration)
return payload, nil
}
func (md *MetaDelay) Type() string {
return md.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "time.sleep",
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
duration, ok := params["duration"]
if !ok {
return nil, errors.New("time.sleep requires a duration parameter")
}
durationNum, ok := duration.(float64)
if !ok {
return nil, errors.New("time.sleep duration must be number")
}
return &MetaDelay{config: config, Duration: time.Millisecond * time.Duration(durationNum)}, nil
},
})
}

View File

@@ -2,13 +2,15 @@ package processor
import ( import (
"context" "context"
"fmt" "errors"
"strconv" "strconv"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
type UintParse struct { type UintParse struct {
Base int
BitSize int
config config.ProcessorConfig config config.ProcessorConfig
} }
@@ -16,11 +18,10 @@ func (up *UintParse) Process(ctx context.Context, payload any) (any, error) {
payloadString, ok := payload.(string) payloadString, ok := payload.(string)
if !ok { if !ok {
return nil, fmt.Errorf("uint.parse processor only accepts a string") return nil, errors.New("uint.parse processor only accepts a string")
} }
// TODO(jwetzell): make base and bitSize configurable payloadUint, err := strconv.ParseUint(payloadString, up.Base, up.BitSize)
payloadUint, err := strconv.ParseUint(payloadString, 10, 64)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -35,7 +36,31 @@ func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "uint.parse", Type: "uint.parse",
New: func(config config.ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
return &UintParse{config: config}, nil params := config.Params
baseNum := 10
base, ok := params["base"]
if ok {
baseFloat, ok := base.(float64)
if !ok {
return nil, errors.New("uint.parse base must be a number")
}
baseNum = int(baseFloat)
}
bitSizeNum := 64
bitSize, ok := params["bitSize"]
if ok {
bitSizeFloat, ok := bitSize.(float64)
if !ok {
return nil, errors.New("uint.parse bitSize must be a number")
}
bitSizeNum = int(bitSizeFloat)
}
return &UintParse{config: config, Base: baseNum, BitSize: bitSizeNum}, nil
}, },
}) })
} }

View File

@@ -3,42 +3,158 @@ package processor_test
import ( import (
"testing" "testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor" "github.com/jwetzell/showbridge-go/internal/processor"
) )
func TestUintParseFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["uint.parse"]
if !ok {
t.Fatalf("uint.parse processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "uint.parse",
})
if err != nil {
t.Fatalf("failed to create uint.parse processor: %s", err)
}
if processorInstance.Type() != "uint.parse" {
t.Fatalf("uint.parse processor has wrong type: %s", processorInstance.Type())
}
}
func TestUintParseBadConfigBaseString(t *testing.T) {
registration, ok := processor.ProcessorRegistry["uint.parse"]
if !ok {
t.Fatalf("uint.parse processor not registered")
}
_, err := registration.New(config.ProcessorConfig{
Type: "uint.parse",
Params: map[string]any{
"base": "10",
},
})
if err == nil {
t.Fatalf("uint.parse should have returned an error for bad base config")
}
}
func TestUintParseBadConfigBitSizeString(t *testing.T) {
registration, ok := processor.ProcessorRegistry["uint.parse"]
if !ok {
t.Fatalf("uint.parse processor not registered")
}
_, err := registration.New(config.ProcessorConfig{
Type: "uint.parse",
Params: map[string]any{
"bitSize": "64",
},
})
if err == nil {
t.Fatalf("uint.parse should have returned an error for bad bitSize config")
}
}
func TestUintParseGoodConfig(t *testing.T) {
registration, ok := processor.ProcessorRegistry["uint.parse"]
if !ok {
t.Fatalf("uint.parse processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "uint.parse",
Params: map[string]any{
"base": 10.0,
"bitSize": 64.0,
},
})
if err != nil {
t.Fatalf("uint.parse should have created processor but got error: %s", err)
}
payload := "12345"
expected := uint64(12345)
got, err := processorInstance.Process(t.Context(), payload)
if err != nil {
t.Fatalf("uint.parse processing failed: %s", err)
}
gotUint, ok := got.(uint64)
if !ok {
t.Fatalf("uint.parse returned a %T payload: %s", got, got)
}
if gotUint != expected {
t.Fatalf("uint.parse got %d, expected %d", gotUint, expected)
}
}
func TestGoodUintParse(t *testing.T) { func TestGoodUintParse(t *testing.T) {
uintParser := processor.UintParse{}
tests := []struct { tests := []struct {
processor processor.Processor processor processor.Processor
name string name string
payload any payload any
expected uint64 expected uint64
base int
bitSize int
}{ }{
{ {
name: "positive number", name: "positive number",
payload: "12345", payload: "12345",
expected: 12345, expected: 12345,
base: 10,
bitSize: 64,
}, },
{ {
name: "zero", name: "zero",
payload: "0", payload: "0",
expected: 0, expected: 0,
base: 10,
bitSize: 64,
},
{
name: "binary",
payload: "1010101",
expected: 85,
base: 2,
bitSize: 64,
},
{
name: "hex",
payload: "15F",
expected: 351,
base: 16,
bitSize: 64,
}, },
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
uintParser := processor.UintParse{
Base: test.base,
BitSize: test.bitSize,
}
got, err := uintParser.Process(t.Context(), test.payload) got, err := uintParser.Process(t.Context(), test.payload)
gotUint, ok := got.(uint64) gotUint, ok := got.(uint64)
if !ok { if !ok {
t.Errorf("uint.parse returned a %T payload: %s", got, got) t.Fatalf("uint.parse returned a %T payload: %s", got, got)
} }
if err != nil { if err != nil {
t.Errorf("uint.parse failed: %s", err) t.Fatalf("uint.parse failed: %s", err)
} }
if gotUint != test.expected { if gotUint != test.expected {
t.Errorf("uint.parse got %d, expected %d", gotUint, test.expected) t.Fatalf("uint.parse got %d, expected %d", gotUint, test.expected)
} }
}) })
} }
@@ -50,18 +166,31 @@ func TestBadUintParse(t *testing.T) {
processor processor.Processor processor processor.Processor
name string name string
payload any payload any
base int
bitSize int
errorString string errorString string
}{ }{
{ {
name: "non-string input", name: "non-string input",
payload: []byte{0x01}, payload: []byte{0x01},
base: 10,
bitSize: 64,
errorString: "uint.parse processor only accepts a string", errorString: "uint.parse processor only accepts a string",
}, },
{ {
name: "not uint string", name: "not uint string",
payload: "-1234", payload: "-1234",
base: 10,
bitSize: 64,
errorString: "strconv.ParseUint: parsing \"-1234\": invalid syntax", errorString: "strconv.ParseUint: parsing \"-1234\": invalid syntax",
}, },
{
name: "bit overflow",
payload: "123456789012345678901234567",
base: 10,
bitSize: 32,
errorString: "strconv.ParseUint: parsing \"123456789012345678901234567\": value out of range",
},
} }
for _, test := range tests { for _, test := range tests {
@@ -69,11 +198,11 @@ func TestBadUintParse(t *testing.T) {
got, err := uintParser.Process(t.Context(), test.payload) got, err := uintParser.Process(t.Context(), test.payload)
if err == nil { if err == nil {
t.Errorf("uint.parse expected to fail but succeeded, got: %v", got) t.Fatalf("uint.parse expected to fail but succeeded, got: %v", got)
} }
if err.Error() != test.errorString { if err.Error() != test.errorString {
t.Errorf("uint.parse got error '%s', expected '%s'", err.Error(), test.errorString) t.Fatalf("uint.parse got error '%s', expected '%s'", err.Error(), test.errorString)
} }
}) })
} }

View File

@@ -0,0 +1,61 @@
package processor
import (
"context"
"errors"
"math/rand/v2"
"github.com/jwetzell/showbridge-go/internal/config"
)
type UintRandom struct {
Min uint
Max uint
config config.ProcessorConfig
}
func (ur *UintRandom) Process(ctx context.Context, payload any) (any, error) {
payloadInt := rand.UintN(ur.Max-ur.Min+1) + ur.Min
return payloadInt, nil
}
func (ur *UintRandom) Type() string {
return ur.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "uint.random",
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
min, ok := params["min"]
if !ok {
return nil, errors.New("uint.random requires a min parameter")
}
minFloat, ok := min.(float64)
if !ok {
return nil, errors.New("uint.random min must be a number")
}
max, ok := params["max"]
if !ok {
return nil, errors.New("uint.random requires a max parameter")
}
maxFloat, ok := max.(float64)
if !ok {
return nil, errors.New("uint.random max must be a number")
}
if maxFloat < minFloat {
return nil, errors.New("uint.random max must be greater than min")
}
return &UintRandom{config: config, Min: uint(minFloat), Max: uint(maxFloat)}, nil
},
})
}

View File

@@ -8,6 +8,11 @@ import (
"github.com/jwetzell/showbridge-go/internal/processor" "github.com/jwetzell/showbridge-go/internal/processor"
) )
type routeContextKey string
var RouterContextKey routeContextKey = routeContextKey("router")
var SourceContextKey routeContextKey = routeContextKey("source")
type RouteError struct { type RouteError struct {
Index int Index int
Config config.RouteConfig Config config.RouteConfig
@@ -21,14 +26,13 @@ type RouteIOError struct {
type RouteIO interface { type RouteIO interface {
HandleInput(sourceId string, payload any) []RouteIOError HandleInput(sourceId string, payload any) []RouteIOError
HandleOutput(sourceId string, destinationId string, payload any) error HandleOutput(ctx context.Context, destinationId string, payload any) error
} }
type Route interface { type Route interface {
Input() string Input() string
Output() string Output() string
HandleInput(ctx context.Context, sourceId string, payload any, router RouteIO) error ProcessPayload(ctx context.Context, payload any) (any, error)
HandleOutput(ctx context.Context, sourceId string, payload any, router RouteIO) error
} }
type ProcessorRoute struct { type ProcessorRoute struct {
@@ -66,21 +70,18 @@ func (r *ProcessorRoute) Output() string {
return r.output return r.output
} }
func (r *ProcessorRoute) HandleInput(ctx context.Context, sourceId string, payload any, router RouteIO) error { func (r *ProcessorRoute) ProcessPayload(ctx context.Context, payload any) (any, error) {
var err error
for _, processor := range r.processors { for _, processor := range r.processors {
payload, err = processor.Process(ctx, payload) processedPayload, err := processor.Process(ctx, payload)
if err != nil { if err != nil {
return err return nil, err
} }
//NOTE(jwetzell) nil payload will result in the route being "terminated" //NOTE(jwetzell) nil payload will result in the route being "terminated"
if payload == nil { if processedPayload == nil {
return nil return nil, nil
} }
} payload = processedPayload
return r.HandleOutput(ctx, sourceId, payload, router)
} }
func (r *ProcessorRoute) HandleOutput(ctx context.Context, sourceId string, payload any, router RouteIO) error { return payload, nil
return router.HandleOutput(sourceId, r.output, payload)
} }

View File

@@ -0,0 +1,166 @@
package route_test
import (
"context"
"slices"
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/route"
)
func TestRouteCreate(t *testing.T) {
routeConfig := config.RouteConfig{
Input: "input",
Output: "output",
}
testRoute, err := route.NewRoute(routeConfig)
if err != nil {
t.Fatalf("route failed to create: %v", err)
}
if testRoute.Input() != routeConfig.Input {
t.Fatalf("route input does not match expected input")
}
if testRoute.Output() != routeConfig.Output {
t.Fatalf("route output does not match expected output")
}
}
type MockRouter struct{}
func (mr *MockRouter) HandleInput(sourceId string, payload any) []route.RouteIOError {
return nil
}
func (mr *MockRouter) HandleOutput(ctx context.Context, destinationId string, payload any) error {
return nil
}
func TestGoodRouteHandleInput(t *testing.T) {
routeConfig := config.RouteConfig{
Input: "input",
Processors: []config.ProcessorConfig{
{Type: "string.encode"},
},
Output: "output",
}
testRoute, err := route.NewRoute(routeConfig)
if err != nil {
t.Fatalf("route failed to create: %v", err)
}
inputData := "test input data"
payload, err := testRoute.ProcessPayload(context.WithValue(t.Context(), route.RouterContextKey, &MockRouter{}), inputData)
if err != nil {
t.Fatalf("route ProcessPayload returned error: %v", err)
}
payloadBytes, ok := payload.([]byte)
if !ok {
t.Fatalf("payload should be []byte got %T", payload)
}
if !slices.Equal([]byte(inputData), payloadBytes) {
t.Fatalf("route returned the wrong payload. expected: %+v got %+v", inputData, payloadBytes)
}
}
func TestRouteHandleInputWithProcessorError(t *testing.T) {
routeConfig := config.RouteConfig{
Input: "input",
Processors: []config.ProcessorConfig{
{Type: "string.create", Params: map[string]any{"template": "{{.invalid}}}"}},
},
Output: "output",
}
testRoute, err := route.NewRoute(routeConfig)
if err != nil {
t.Fatalf("route failed to create: %v", err)
}
inputData := "test input data"
_, err = testRoute.ProcessPayload(context.WithValue(t.Context(), route.RouterContextKey, &MockRouter{}), inputData)
if err == nil {
t.Fatalf("route HandleOutput did not return error for bad processor")
}
}
func TestRouteHandleNilPayload(t *testing.T) {
routeConfig := config.RouteConfig{
Input: "input",
Processors: []config.ProcessorConfig{},
Output: "output",
}
testRoute, err := route.NewRoute(routeConfig)
if err != nil {
t.Fatalf("route failed to create: %v", err)
return
}
payload, err := testRoute.ProcessPayload(context.WithValue(t.Context(), route.RouterContextKey, &MockRouter{}), nil)
if err != nil {
t.Fatalf("route ProcessPayload returned error: %v", err)
}
if payload != nil {
t.Fatalf("route returned the wrong payload")
}
}
func TestRouteHandleNilPayloadFromProcessor(t *testing.T) {
routeConfig := config.RouteConfig{
Input: "input",
Processors: []config.ProcessorConfig{
{Type: "script.js", Params: map[string]any{"program": "payload = undefined"}},
},
Output: "output",
}
testRoute, err := route.NewRoute(routeConfig)
if err != nil {
t.Fatalf("route failed to create: %v", err)
}
payload, err := testRoute.ProcessPayload(context.WithValue(t.Context(), route.RouterContextKey, &MockRouter{}), "test")
if err != nil {
t.Fatalf("route HandleOutput returned error for nil payload: %v", err)
}
if payload != nil {
t.Fatalf("route returned the wrong payload")
}
}
func TestRouteUnknownProcessor(t *testing.T) {
routeConfig := config.RouteConfig{
Input: "input",
Processors: []config.ProcessorConfig{
{Type: "asdfasdflkjalkj"},
},
Output: "output",
}
_, err := route.NewRoute(routeConfig)
if err == nil {
t.Fatalf("route error expected when creating route with an unknown processor, got nil")
}
}
func TestRouteBadProcessorConfig(t *testing.T) {
routeConfig := config.RouteConfig{
Input: "input",
Processors: []config.ProcessorConfig{
{Type: "string.create", Params: map[string]any{}},
},
Output: "output",
}
_, err := route.NewRoute(routeConfig)
if err == nil {
t.Fatalf("route error expected creating route with bad processor, got nil")
}
}

View File

@@ -2,9 +2,9 @@ package showbridge
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"log/slog" "log/slog"
"os"
"sync" "sync"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
@@ -18,25 +18,24 @@ type Router struct {
ModuleInstances []module.Module ModuleInstances []module.Module
RouteInstances []route.Route RouteInstances []route.Route
moduleWait sync.WaitGroup moduleWait sync.WaitGroup
logger *slog.Logger
} }
func NewRouter(ctx context.Context, config config.Config) (*Router, []module.ModuleError, []route.RouteError) { func NewRouter(ctx context.Context, config config.Config) (*Router, []module.ModuleError, []route.RouteError) {
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
}))
slog.SetDefault(logger)
slog.Debug("creating router")
routerContext, cancel := context.WithCancel(ctx) routerContext, cancel := context.WithCancel(ctx)
router := Router{ router := Router{
Context: routerContext,
contextCancel: cancel, contextCancel: cancel,
ModuleInstances: []module.Module{}, ModuleInstances: []module.Module{},
RouteInstances: []route.Route{}, RouteInstances: []route.Route{},
logger: slog.Default().With("component", "router"),
} }
router.Context = context.WithValue(routerContext, route.RouterContextKey, &router)
router.logger.Debug("creating")
var moduleErrors []module.ModuleError var moduleErrors []module.ModuleError
for moduleIndex, moduleDecl := range config.Modules { for moduleIndex, moduleDecl := range config.Modules {
@@ -49,7 +48,7 @@ func NewRouter(ctx context.Context, config config.Config) (*Router, []module.Mod
moduleErrors = append(moduleErrors, module.ModuleError{ moduleErrors = append(moduleErrors, module.ModuleError{
Index: moduleIndex, Index: moduleIndex,
Config: moduleDecl, Config: moduleDecl,
Error: fmt.Errorf("module type not defined"), Error: errors.New("module type not defined"),
}) })
continue continue
} }
@@ -64,14 +63,14 @@ func NewRouter(ctx context.Context, config config.Config) (*Router, []module.Mod
moduleErrors = append(moduleErrors, module.ModuleError{ moduleErrors = append(moduleErrors, module.ModuleError{
Index: moduleIndex, Index: moduleIndex,
Config: moduleDecl, Config: moduleDecl,
Error: fmt.Errorf("duplicate module id"), Error: errors.New("duplicate module id"),
}) })
break break
} }
} }
if !moduleInstanceExists { if !moduleInstanceExists {
moduleInstance, err := moduleInfo.New(router.Context, moduleDecl, &router) moduleInstance, err := moduleInfo.New(router.Context, moduleDecl)
if err != nil { if err != nil {
if moduleErrors == nil { if moduleErrors == nil {
moduleErrors = []module.ModuleError{} moduleErrors = []module.ModuleError{}
@@ -110,23 +109,23 @@ func NewRouter(ctx context.Context, config config.Config) (*Router, []module.Mod
} }
func (r *Router) Run() { func (r *Router) Run() {
slog.Info("running router") r.logger.Info("running")
for _, moduleInstance := range r.ModuleInstances { for _, moduleInstance := range r.ModuleInstances {
r.moduleWait.Add(1) r.moduleWait.Go(func() {
go func() {
err := moduleInstance.Run() err := moduleInstance.Run()
if err != nil { if err != nil {
slog.Error("error encountered running module", "id", moduleInstance.Id(), "error", err) r.logger.Error("error encountered running module", "error", err)
} }
r.moduleWait.Done() })
}()
} }
<-r.Context.Done() <-r.Context.Done()
r.logger.Debug("waiting for modules to exit")
r.moduleWait.Wait() r.moduleWait.Wait()
slog.Info("router done") r.logger.Info("done")
} }
func (r *Router) Stop() { func (r *Router) Stop() {
r.logger.Debug("stopping")
r.contextCancel() r.contextCancel()
} }
@@ -134,7 +133,9 @@ func (r *Router) HandleInput(sourceId string, payload any) []route.RouteIOError
var routingErrors []route.RouteIOError var routingErrors []route.RouteIOError
for routeIndex, routeInstance := range r.RouteInstances { for routeIndex, routeInstance := range r.RouteInstances {
if routeInstance.Input() == sourceId { if routeInstance.Input() == sourceId {
err := routeInstance.HandleInput(r.Context, sourceId, payload, r) routeContext := context.WithValue(r.Context, route.SourceContextKey, sourceId)
payload, err := routeInstance.ProcessPayload(routeContext, payload)
if err != nil { if err != nil {
if routingErrors == nil { if routingErrors == nil {
routingErrors = []route.RouteIOError{} routingErrors = []route.RouteIOError{}
@@ -143,17 +144,18 @@ func (r *Router) HandleInput(sourceId string, payload any) []route.RouteIOError
Index: routeIndex, Index: routeIndex,
Error: err, Error: err,
}) })
slog.Error("router unable to route input", "route", routeIndex, "source", sourceId, "error", err) r.logger.Error("unable to route input", "route", routeIndex, "source", sourceId, "error", err)
} }
r.HandleOutput(routeContext, routeInstance.Output(), payload)
} }
} }
return routingErrors return routingErrors
} }
func (r *Router) HandleOutput(sourceId string, destinationId string, payload any) error { func (r *Router) HandleOutput(ctx context.Context, destinationId string, payload any) error {
for _, moduleInstance := range r.ModuleInstances { for _, moduleInstance := range r.ModuleInstances {
if moduleInstance.Id() == destinationId { if moduleInstance.Id() == destinationId {
return moduleInstance.Output(payload) return moduleInstance.Output(ctx, payload)
} }
} }
return fmt.Errorf("router could not find module instance for destination %s", destinationId) return fmt.Errorf("router could not find module instance for destination %s", destinationId)