Compare commits

..

74 Commits

Author SHA1 Message Date
Joel Wetzell
b6c1c5c600 fix schema for router.input 2026-03-14 09:05:22 -05:00
Joel Wetzell
b6ec615e40 comment 2026-03-13 21:37:40 -05:00
Joel Wetzell
216775c9c6 switch to read lock in getter method 2026-03-13 21:32:59 -05:00
Joel Wetzell
1bfee2186b add payload type to span attributes 2026-03-13 21:18:47 -05:00
Joel Wetzell
fa612231fb fix otel import path 2026-03-13 21:18:32 -05:00
Joel Wetzell
c1161c5aa9 add processor to push input into router (#104) 2026-03-13 21:17:54 -05:00
Joel Wetzell
b1076ce1e4 do some manual casting for script.js payload 2026-03-12 21:38:08 -05:00
Joel Wetzell
7f5204aca0 set minimum port above 1024 for server modules 2026-03-12 17:51:46 -05:00
Joel Wetzell
f3d87e92f8 Merge pull request #103 from jwetzell/feat/http-api
add an HTTP/WS API for interacting with webui
2026-03-12 17:35:53 -05:00
Joel Wetzell
e50e269697 pull from config changes channel to write out to config file 2026-03-12 17:34:12 -05:00
Joel Wetzell
ca6aebadef add channel for router to communicate config changes out 2026-03-12 17:33:41 -05:00
Joel Wetzell
818ddc18f4 go mod tidy 2026-03-12 17:13:12 -05:00
Joel Wetzell
3e7b245536 add config reloading via API endpoint and more WS work 2026-03-12 17:04:21 -05:00
Joel Wetzell
e996d84234 udpate module and route config error objects for JSON 2026-03-12 17:03:03 -05:00
Joel Wetzell
04c8ebbe06 add api config to example config 2026-03-11 21:07:30 -05:00
Joel Wetzell
0f57e123ce start work on http/ws api 2026-03-11 20:58:53 -05:00
Joel Wetzell
82ba1d5d10 log out errors running modules 2026-03-11 12:49:08 -05:00
Joel Wetzell
20c53e4996 Merge pull request #101 from jwetzell/dependabot/go_modules/github.com/nats-io/nats-server/v2-2.12.5
Bump github.com/nats-io/nats-server/v2 from 2.12.4 to 2.12.5
2026-03-11 10:03:20 -05:00
Joel Wetzell
8132faaba6 reuse quickjs objects in script.js processor 2026-03-10 18:14:50 -05:00
Joel Wetzell
5a0f21bd64 move any helper methods to common and reuse for params getter 2026-03-10 18:14:30 -05:00
dependabot[bot]
fd0f78733c Bump github.com/nats-io/nats-server/v2 from 2.12.4 to 2.12.5
Bumps [github.com/nats-io/nats-server/v2](https://github.com/nats-io/nats-server) from 2.12.4 to 2.12.5.
- [Release notes](https://github.com/nats-io/nats-server/releases)
- [Changelog](https://github.com/nats-io/nats-server/blob/main/RELEASES.md)
- [Commits](https://github.com/nats-io/nats-server/compare/v2.12.4...v2.12.5)

---
updated-dependencies:
- dependency-name: github.com/nats-io/nats-server/v2
  dependency-version: 2.12.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-10 17:26:30 +00:00
Joel Wetzell
65476d5ecc rename files in common package 2026-03-09 14:13:17 -05:00
Joel Wetzell
536bc2af4e Merge pull request #96 from jwetzell/dependabot/go_modules/go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp-1.42.0
Bump go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp from 1.41.0 to 1.42.0
2026-03-08 17:35:41 -05:00
dependabot[bot]
b89d907759 Bump go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
Bumps [go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp](https://github.com/open-telemetry/opentelemetry-go) from 1.41.0 to 1.42.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.41.0...v1.42.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
  dependency-version: 1.42.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-08 22:34:42 +00:00
Joel Wetzell
a3c48de2ad Merge pull request #97 from jwetzell/dependabot/go_modules/go.opentelemetry.io/otel/sdk-1.42.0
Bump go.opentelemetry.io/otel/sdk from 1.41.0 to 1.42.0
2026-03-08 17:33:34 -05:00
dependabot[bot]
8ca9d9940f Bump go.opentelemetry.io/otel/sdk from 1.41.0 to 1.42.0
Bumps [go.opentelemetry.io/otel/sdk](https://github.com/open-telemetry/opentelemetry-go) from 1.41.0 to 1.42.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.41.0...v1.42.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/sdk
  dependency-version: 1.42.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-08 22:27:05 +00:00
Joel Wetzell
53d0648774 Merge pull request #98 from jwetzell/dependabot/go_modules/go.opentelemetry.io/otel-1.42.0
Bump go.opentelemetry.io/otel from 1.41.0 to 1.42.0
2026-03-08 17:26:02 -05:00
Joel Wetzell
cf2881e5c3 Merge pull request #94 from jwetzell/dependabot/go_modules/go.opentelemetry.io/otel/trace-1.42.0
Bump go.opentelemetry.io/otel/trace from 1.41.0 to 1.42.0
2026-03-08 17:25:46 -05:00
Joel Wetzell
f437438dca Merge pull request #93 from jwetzell/dependabot/github_actions/docker/setup-qemu-action-4.0.0
Bump docker/setup-qemu-action from 3.7.0 to 4.0.0
2026-03-08 17:25:37 -05:00
Joel Wetzell
6b3c470534 Merge pull request #92 from jwetzell/dependabot/github_actions/docker/setup-buildx-action-4.0.0
Bump docker/setup-buildx-action from 3.12.0 to 4.0.0
2026-03-08 17:25:29 -05:00
Joel Wetzell
3bdff5309f Merge pull request #91 from jwetzell/dependabot/github_actions/docker/login-action-4.0.0
Bump docker/login-action from 3.7.0 to 4.0.0
2026-03-08 17:25:21 -05:00
Joel Wetzell
3792d40bb1 Merge pull request #90 from jwetzell/dependabot/github_actions/docker/metadata-action-6.0.0
Bump docker/metadata-action from 5.10.0 to 6.0.0
2026-03-08 17:25:10 -05:00
Joel Wetzell
0cca2e3be9 Merge pull request #95 from jwetzell/dependabot/github_actions/docker/build-push-action-7.0.0
Bump docker/build-push-action from 6.19.2 to 7.0.0
2026-03-08 17:24:41 -05:00
Joel Wetzell
5447781232 Merge pull request #99 from jwetzell/dependabot/go_modules/github.com/emiago/sipgo-1.2.1
Bump github.com/emiago/sipgo from 1.2.0 to 1.2.1
2026-03-08 17:24:25 -05:00
Joel Wetzell
c760b220c0 Merge pull request #100 from jwetzell/feat/filter-regex
rename string.filter to filter.regex
2026-03-08 17:22:17 -05:00
Joel Wetzell
cb71b5c5b8 rename string.filter to filter.regex 2026-03-08 17:19:49 -05:00
dependabot[bot]
84fd7c1028 Bump github.com/emiago/sipgo from 1.2.0 to 1.2.1
Bumps [github.com/emiago/sipgo](https://github.com/emiago/sipgo) from 1.2.0 to 1.2.1.
- [Release notes](https://github.com/emiago/sipgo/releases)
- [Commits](https://github.com/emiago/sipgo/compare/v1.2.0...v1.2.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-08 20:33:56 +00:00
dependabot[bot]
7bdf6e01b3 Bump go.opentelemetry.io/otel from 1.41.0 to 1.42.0
Bumps [go.opentelemetry.io/otel](https://github.com/open-telemetry/opentelemetry-go) from 1.41.0 to 1.42.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.41.0...v1.42.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel
  dependency-version: 1.42.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-08 20:33:51 +00:00
dependabot[bot]
1b607f64cd Bump go.opentelemetry.io/otel/trace from 1.41.0 to 1.42.0
Bumps [go.opentelemetry.io/otel/trace](https://github.com/open-telemetry/opentelemetry-go) from 1.41.0 to 1.42.0.
- [Release notes](https://github.com/open-telemetry/opentelemetry-go/releases)
- [Changelog](https://github.com/open-telemetry/opentelemetry-go/blob/main/CHANGELOG.md)
- [Commits](https://github.com/open-telemetry/opentelemetry-go/compare/v1.41.0...v1.42.0)

---
updated-dependencies:
- dependency-name: go.opentelemetry.io/otel/trace
  dependency-version: 1.42.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-08 20:33:36 +00:00
dependabot[bot]
28e9141cbc Bump docker/build-push-action from 6.19.2 to 7.0.0
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.19.2 to 7.0.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](10e90e3645...d08e5c354a)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-08 20:33:36 +00:00
dependabot[bot]
3b38110ffb Bump docker/setup-qemu-action from 3.7.0 to 4.0.0
Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 3.7.0 to 4.0.0.
- [Release notes](https://github.com/docker/setup-qemu-action/releases)
- [Commits](c7c5346462...ce360397dd)

---
updated-dependencies:
- dependency-name: docker/setup-qemu-action
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-08 20:33:32 +00:00
dependabot[bot]
25e8563cd1 Bump docker/setup-buildx-action from 3.12.0 to 4.0.0
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.12.0 to 4.0.0.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](8d2750c68a...4d04d5d948)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-08 20:33:29 +00:00
dependabot[bot]
80969ac1ff Bump docker/login-action from 3.7.0 to 4.0.0
Bumps [docker/login-action](https://github.com/docker/login-action) from 3.7.0 to 4.0.0.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](c94ce9fb46...b45d80f862)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-08 20:33:24 +00:00
dependabot[bot]
beb3664e3b Bump docker/metadata-action from 5.10.0 to 6.0.0
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5.10.0 to 6.0.0.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](c299e40c65...030e881283)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-08 20:33:21 +00:00
Joel Wetzell
6e53cbe8fd format README 2026-03-08 15:32:36 -05:00
Joel Wetzell
4ebaa48f93 yaml formatting 2026-03-08 15:32:27 -05:00
Joel Wetzell
79d535c283 json formatting 2026-03-08 15:31:04 -05:00
Joel Wetzell
85349f8540 Merge pull request #89 from jwetzell/feat/int-scale
add processor to scale int values
2026-03-08 15:29:27 -05:00
Joel Wetzell
e49900a3a7 add processor to scale int values 2026-03-08 15:28:05 -05:00
Joel Wetzell
1f706becdc fix go version in dockerfile 2026-03-08 14:20:28 -05:00
Joel Wetzell
13a494239e Merge pull request #88 from jwetzell/remove-specific-filter-processors
remove specific filter processors
2026-03-08 14:13:11 -05:00
Joel Wetzell
153de944a2 remove filter processors 2026-03-08 14:06:28 -05:00
Joel Wetzell
42c75074fe update Go version 2026-03-08 13:38:52 -05:00
Joel Wetzell
361b07ec00 fix error message 2026-03-08 13:36:06 -05:00
Joel Wetzell
8c4f0591e1 Merge pull request #87 from jwetzell/feat/script-processor-env-data
consistent script processor environments
2026-03-08 13:34:01 -05:00
Joel Wetzell
09ddc40f1f consistent script processor environments 2026-03-08 13:32:34 -05:00
Joel Wetzell
6382cf6944 Merge pull request #86 from jwetzell/feat/struct-get-pointer
allow struct based processors to also operate on pointers to structs
2026-03-08 13:16:08 -05:00
Joel Wetzell
0732113a02 allow struct based processors to also operate on pointers to structs 2026-03-08 13:14:50 -05:00
Joel Wetzell
e5db9a48a9 Merge pull request #85 from jwetzell/feat/json-decode-bytes
allow json.decode to take byte slice and string
2026-03-08 13:11:17 -05:00
Joel Wetzell
9502201261 allow json.decode to take byte slice and string 2026-03-08 13:09:22 -05:00
Joel Wetzell
9859aa8fd4 Merge pull request #84 from jwetzell/feat/http-client-processor
make http client into a processor instead of module
2026-03-08 13:08:26 -05:00
Joel Wetzell
b7b05cbb77 make http client into a processor instead of module 2026-03-08 13:03:54 -05:00
Joel Wetzell
7b1fe47039 align variable names 2026-03-08 09:36:59 -05:00
Joel Wetzell
0848d8ab6e add descriptions to time module params 2026-03-08 09:36:31 -05:00
Joel Wetzell
c1bb2ff4cb update schema descriptions 2026-03-08 09:22:57 -05:00
Joel Wetzell
50898b9130 add missing title for params property 2026-03-07 11:49:29 -06:00
Joel Wetzell
05e68e90af add title and descriptions for params schemas 2026-03-06 18:59:27 -06:00
Joel Wetzell
cd4c3f59f2 fix missing properties on parse processors 2026-03-06 18:37:04 -06:00
Joel Wetzell
50399fa811 update titles on module schemas 2026-03-06 18:36:49 -06:00
Joel Wetzell
2dd44a11fd Merge pull request #83 from jwetzell/feat/router-output-as-processor
switch router output to be a processor instead of specific output per route
2026-03-04 21:34:10 -06:00
Joel Wetzell
b7a8b04a72 switch router output to be a processor instead of specific output per route 2026-03-04 21:21:11 -06:00
Joel Wetzell
078e6ec68c more detailed error message 2026-03-04 19:42:48 -06:00
Joel Wetzell
b58187d6a9 Merge pull request #82 from jwetzell/feat/sender-template-data
add sender to template data for relevant modules
2026-03-04 19:39:54 -06:00
Joel Wetzell
38b8e44f04 add sender to template data for relevant modules 2026-03-04 19:38:30 -06:00
99 changed files with 2025 additions and 2021 deletions

View File

@@ -1,9 +1,9 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: 'github-actions' - package-ecosystem: "github-actions"
directory: '/' directory: "/"
schedule: schedule:
interval: 'weekly' interval: "weekly"
- package-ecosystem: gomod - package-ecosystem: gomod
directory: / directory: /
schedule: schedule:

14
.github/labeler.yml vendored
View File

@@ -1,27 +1,27 @@
config: config:
- changed-files: - changed-files:
- any-glob-to-any-file: 'internal/config/**' - any-glob-to-any-file: "internal/config/**"
framer: framer:
- changed-files: - changed-files:
- any-glob-to-any-file: 'internal/framer/**' - any-glob-to-any-file: "internal/framer/**"
module: module:
- changed-files: - changed-files:
- any-glob-to-any-file: 'internal/module/**' - any-glob-to-any-file: "internal/module/**"
processor: processor:
- changed-files: - changed-files:
- any-glob-to-any-file: 'internal/processor/**' - any-glob-to-any-file: "internal/processor/**"
router: router:
- changed-files: - changed-files:
- any-glob-to-any-file: 'router*' - any-glob-to-any-file: "router*"
route: route:
- changed-files: - changed-files:
- any-glob-to-any-file: 'internal/route/**' - any-glob-to-any-file: "internal/route/**"
cli: cli:
- changed-files: - changed-files:
- any-glob-to-any-file: 'cmd/showbridge/**' - any-glob-to-any-file: "cmd/showbridge/**"

2
.github/release.yml vendored
View File

@@ -26,4 +26,4 @@ changelog:
- cli - cli
- title: Other Changes - title: Other Changes
labels: labels:
- '*' - "*"

View File

@@ -19,7 +19,7 @@ jobs:
- name: setup go - name: setup go
uses: actions/setup-go@v6 uses: actions/setup-go@v6
with: with:
go-version-file: 'go.mod' go-version-file: "go.mod"
- name: release - name: release
uses: goreleaser/goreleaser-action@v7 uses: goreleaser/goreleaser-action@v7
with: with:
@@ -37,26 +37,26 @@ jobs:
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0 uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Login to Docker Hub - name: Login to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }} password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Setup Docker metadata - name: Setup Docker metadata
id: meta id: meta
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
with: with:
images: | images: |
jwetzell/showbridge jwetzell/showbridge
- name: Build and push - name: Build and push
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
with: with:
push: true push: true
context: ./ context: ./

View File

@@ -5,16 +5,16 @@ on:
branches: branches:
- main - main
paths: paths:
- '**/*.go' - "**/*.go"
- 'go.mod' - "go.mod"
- 'go.sum' - "go.sum"
push: push:
branches: branches:
- main - main
paths: paths:
- '**/*.go' - "**/*.go"
- 'go.mod' - "go.mod"
- 'go.sum' - "go.sum"
permissions: permissions:
contents: write contents: write
@@ -29,7 +29,7 @@ jobs:
- uses: actions/checkout@v6 - uses: actions/checkout@v6
- uses: actions/setup-go@v6 - uses: actions/setup-go@v6
with: with:
go-version-file: 'go.mod' go-version-file: "go.mod"
- name: mod tidy - name: mod tidy
run: go mod tidy run: go mod tidy

31
.vscode/launch.json vendored
View File

@@ -1,18 +1,17 @@
{ {
// Use IntelliSense to learn about possible attributes. // Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes. // Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{
{ "name": "Launch showbridge",
"name": "Launch showbridge", "type": "go",
"type": "go", "request": "launch",
"request": "launch", "mode": "auto",
"mode": "auto", "program": "cmd/showbridge",
"program": "cmd/showbridge", "args": ["--log-level", "debug"],
"args": ["--log-level", "debug"], "cwd": "./"
"cwd": "./" }
} ]
]
} }

View File

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

View File

@@ -3,7 +3,6 @@
# showbridge (go edition) # showbridge (go edition)
[![Coverage](https://github.com/jwetzell/showbridge-go/wiki/coverage.svg)](https://raw.githack.com/wiki/jwetzell/showbridge-go/coverage.html) [![Coverage](https://github.com/jwetzell/showbridge-go/wiki/coverage.svg)](https://raw.githack.com/wiki/jwetzell/showbridge-go/coverage.html)
Simple protocol router _/s_ Simple protocol router _/s_
</div> </div>
@@ -14,6 +13,7 @@ Simple protocol router _/s_
</p> </p>
### Supported Protocols ### Supported Protocols
- HTTP - HTTP
- UDP - UDP
- TCP - TCP

146
api.go Normal file
View File

@@ -0,0 +1,146 @@
package showbridge
import (
"context"
"embed"
_ "embed"
"encoding/json"
"fmt"
"net/http"
"time"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
"github.com/jwetzell/showbridge-go/internal/route"
)
func (r *Router) startAPIServer(config config.ApiConfig) {
r.logger.Debug("starting API server", "port", config.Port)
mux := http.NewServeMux()
mux.HandleFunc("/ws", r.handleWebsocket)
mux.HandleFunc("/health", r.handleHealthHTTP)
mux.HandleFunc("/schema/{schema}", r.handleSchemaHTTP)
mux.HandleFunc("/api/v1/config", r.handleConfigHTTP)
r.apiServerMu.Lock()
defer r.apiServerMu.Unlock()
r.apiServer = &http.Server{
Addr: fmt.Sprintf(":%d", config.Port),
Handler: mux,
}
go func() {
r.apiServer.ListenAndServe()
r.apiServerShutdown()
}()
}
func (r *Router) stopAPIServer() {
r.logger.Debug("stopping API server")
r.apiServerMu.Lock()
defer r.apiServerMu.Unlock()
if r.apiServer != nil {
apiShutdownCtx, apiShutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
r.apiServerShutdown = apiShutdownCancel
r.apiServer.Shutdown(apiShutdownCtx)
<-apiShutdownCtx.Done()
r.apiServer = nil
}
}
func (r *Router) handleHealthHTTP(w http.ResponseWriter, req *http.Request) {
switch req.Method {
case http.MethodGet:
w.Header().Set("Access-Control-Allow-Origin", "*")
w.WriteHeader(http.StatusOK)
case http.MethodOptions:
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
w.WriteHeader(http.StatusOK)
default:
w.Header().Set("Access-Control-Allow-Origin", "*")
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
func (r *Router) handleConfigHTTP(w http.ResponseWriter, req *http.Request) {
switch req.Method {
case http.MethodGet:
configJSON, err := json.Marshal(r.runningConfig)
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Type", "application/json")
w.Write(configJSON)
case http.MethodPut:
var newConfig config.Config
err := json.NewDecoder(req.Body).Decode(&newConfig)
if err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
moduleErrors, routeErrors := r.UpdateConfig(newConfig)
if len(moduleErrors) > 0 || len(routeErrors) > 0 {
errorResponse := struct {
ModuleErrors []module.ModuleError `json:"moduleErrors,omitempty"`
RouteErrors []route.RouteError `json:"routeErrors,omitempty"`
}{
ModuleErrors: moduleErrors,
RouteErrors: routeErrors,
}
errorResponseJSON, err := json.Marshal(errorResponse)
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusBadRequest)
w.Write(errorResponseJSON)
return
}
w.Header().Set("Access-Control-Allow-Origin", "*")
w.WriteHeader(http.StatusOK)
r.ConfigChange <- newConfig
case http.MethodOptions:
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, PUT, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
w.WriteHeader(http.StatusOK)
default:
w.Header().Set("Access-Control-Allow-Origin", "*")
w.WriteHeader(http.StatusMethodNotAllowed)
}
}
//go:embed schema
var schema embed.FS
func (r *Router) handleSchemaHTTP(w http.ResponseWriter, req *http.Request) {
switch req.Method {
case http.MethodGet:
schemaName := req.PathValue("schema")
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Type", "application/json")
configSchema, err := schema.ReadFile(fmt.Sprintf("schema/%s.schema.json", schemaName))
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
w.Write(configSchema)
case http.MethodOptions:
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
w.WriteHeader(http.StatusOK)
default:
w.Header().Set("Access-Control-Allow-Origin", "*")
w.WriteHeader(http.StatusMethodNotAllowed)
}
}

View File

@@ -15,7 +15,7 @@ builds:
- "amd64" - "amd64"
- "arm64" - "arm64"
ldflags: ldflags:
- '-s -w -X main.version={{.RawVersion}}-{{.ShortCommit}}' - "-s -w -X main.version={{.RawVersion}}-{{.ShortCommit}}"
archives: archives:
- formats: [tar.gz] - formats: [tar.gz]

View File

@@ -13,12 +13,14 @@ import (
"github.com/jwetzell/showbridge-go" "github.com/jwetzell/showbridge-go"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
"github.com/jwetzell/showbridge-go/internal/route"
"github.com/urfave/cli/v3" "github.com/urfave/cli/v3"
"go.opentelemetry.io/otel" "go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/sdk/resource" "go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace" sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.39.0" semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
) )
@@ -109,6 +111,20 @@ func readConfig(configPath string) (config.Config, error) {
return cfg, nil return cfg, nil
} }
func writeConfig(configPath string, newConfig config.Config) error {
configBytes, err := yaml.Marshal(newConfig)
if err != nil {
return err
}
err = os.WriteFile(configPath, configBytes, 0644)
if err != nil {
return err
}
return nil
}
func run(ctx context.Context, c *cli.Command) error { func run(ctx context.Context, c *cli.Command) error {
configPath := c.String("config") configPath := c.String("config")
if configPath == "" { if configPath == "" {
@@ -173,7 +189,19 @@ func run(ctx context.Context, c *cli.Command) error {
routerRunner: &sync.WaitGroup{}, routerRunner: &sync.WaitGroup{},
} }
router, err := showbridgeApp.getNewRouter() config, err := readConfig(showbridgeApp.configPath)
if err != nil {
return err
}
router, moduleErrors, routeErrors := showbridge.NewRouter(config)
showbridgeApp.logConfigErrors(moduleErrors, routeErrors)
if moduleErrors != nil || routeErrors != nil {
return fmt.Errorf("errors initializing modules or routes")
}
if err != nil { if err != nil {
return fmt.Errorf("failed to initialize router: %w", err) return fmt.Errorf("failed to initialize router: %w", err)
} }
@@ -185,7 +213,7 @@ func run(ctx context.Context, c *cli.Command) error {
}) })
showbridgeApp.routerMutex.Unlock() showbridgeApp.routerMutex.Unlock()
go showbridgeApp.handleHangup() go showbridgeApp.handleChannels()
<-showbridgeApp.ctx.Done() <-showbridgeApp.ctx.Done()
showbridgeApp.logger.Debug("shutting down router") showbridgeApp.logger.Debug("shutting down router")
@@ -195,40 +223,37 @@ func run(ctx context.Context, c *cli.Command) error {
return nil return nil
} }
func (app *showbridgeApp) handleHangup() { func (app *showbridgeApp) handleChannels() {
for { for {
select { select {
case <-sigHangup: case <-sigHangup:
app.logger.Info("received SIGHUP, reloading configuration") app.logger.Info("received SIGHUP, reloading configuration")
newRouter, err := app.getNewRouter() app.routerMutex.Lock()
config, err := readConfig(app.configPath)
if err != nil { if err != nil {
app.logger.Error("failed to reload configuration", "error", err) app.logger.Error("failed to read config file", "error", err)
app.routerMutex.Unlock()
continue continue
} }
app.routerMutex.Lock() moduleErrors, routeErrors := app.router.UpdateConfig(config)
app.router.Stop() app.logConfigErrors(moduleErrors, routeErrors)
app.routerRunner.Wait()
app.router = newRouter
app.routerRunner.Go(func() {
app.router.Start(context.Background())
})
app.logger.Info("configuration reloaded successfully") app.logger.Info("configuration reloaded successfully")
app.routerMutex.Unlock() app.routerMutex.Unlock()
case config := <-app.router.ConfigChange:
app.logger.Info("router config changed updating config file")
err := writeConfig(app.configPath, config)
if err != nil {
app.logger.Error("failed to write config file", "error", err)
continue
}
app.logger.Info("config file updated successfully")
case <-app.ctx.Done(): case <-app.ctx.Done():
return return
} }
} }
} }
func (app *showbridgeApp) getNewRouter() (*showbridge.Router, error) { func (app *showbridgeApp) logConfigErrors(moduleErrors []module.ModuleError, routeErrors []route.RouteError) {
// TODO(jwetzell): what should happen when the config file is unchanged?
config, err := readConfig(app.configPath)
if err != nil {
return nil, err
}
router, moduleErrors, routeErrors := showbridge.NewRouter(config)
for _, moduleError := range moduleErrors { for _, moduleError := range moduleErrors {
app.logger.Error("problem initializing module", "index", moduleError.Index, "error", moduleError.Error) app.logger.Error("problem initializing module", "index", moduleError.Index, "error", moduleError.Error)
} }
@@ -236,12 +261,6 @@ func (app *showbridgeApp) getNewRouter() (*showbridge.Router, error) {
for _, routeError := range routeErrors { for _, routeError := range routeErrors {
app.logger.Error("problem initializing route", "index", routeError.Index, "error", routeError.Error) app.logger.Error("problem initializing route", "index", routeError.Index, "error", routeError.Error)
} }
if moduleErrors != nil || routeErrors != nil {
return nil, fmt.Errorf("errors initializing modules or routes")
}
return router, nil
} }
func newTracerProvider(exp sdktrace.SpanExporter) *sdktrace.TracerProvider { func newTracerProvider(exp sdktrace.SpanExporter) *sdktrace.TracerProvider {

View File

@@ -1,3 +1,5 @@
api:
port: 8080
modules: modules:
- id: http - id: http
type: http.server type: http.server
@@ -13,6 +15,8 @@ routes:
processors: processors:
- type: osc.message.create - type: osc.message.create
params: params:
address: "{{.URL.Path}}" address: "{{.Payload.URL.Path}}"
- type: osc.message.encode - type: osc.message.encode
output: udp - type: router.output
params:
module: udp

64
events.go Normal file
View File

@@ -0,0 +1,64 @@
package showbridge
import (
"encoding/json"
"github.com/gorilla/websocket"
)
type Event struct {
Type string `json:"type"`
Data any `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
func (e Event) toJSON() ([]byte, error) {
return json.Marshal(e)
}
func (r *Router) handleEvent(event Event, sender *websocket.Conn) {
switch event.Type {
case "ping":
r.unicastEvent(Event{Type: "pong"}, sender)
default:
r.logger.Warn("unknown event type", "eventType", event.Type)
}
}
func (r *Router) unicastEvent(event Event, conn *websocket.Conn) {
eventJSON, err := event.toJSON()
if err != nil {
r.logger.Error("failed to marshal event to JSON", "error", err)
return
}
err = conn.WriteMessage(websocket.TextMessage, eventJSON)
if err != nil {
r.logger.Error("failed to write message to websocket connection", "error", err)
}
}
func (r *Router) broadcastEvent(event Event, excluded ...*websocket.Conn) {
eventJSON, err := event.toJSON()
if err != nil {
r.logger.Error("failed to marshal event to JSON", "error", err)
return
}
r.wsConnsMu.Lock()
defer r.wsConnsMu.Unlock()
for _, conn := range r.wsConns {
exclude := false
for _, excludedConn := range excluded {
if conn == excludedConn {
exclude = true
break
}
}
if exclude {
continue
}
err := conn.WriteMessage(websocket.TextMessage, eventJSON)
if err != nil {
r.logger.Error("failed to write message to websocket connection", "error", err)
}
}
}

34
go.mod
View File

@@ -1,32 +1,33 @@
module github.com/jwetzell/showbridge-go module github.com/jwetzell/showbridge-go
go 1.25.5 go 1.26.0
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.27.0 github.com/emiago/diago v0.27.0
github.com/emiago/sipgo v1.2.0 github.com/emiago/sipgo v1.2.1
github.com/expr-lang/expr v1.17.8 github.com/expr-lang/expr v1.17.8
github.com/extism/go-sdk v1.7.1 github.com/extism/go-sdk v1.7.1
github.com/gorilla/websocket v1.5.3
github.com/jwetzell/artnet-go v0.2.1 github.com/jwetzell/artnet-go v0.2.1
github.com/jwetzell/free-d-go v0.1.0 github.com/jwetzell/free-d-go v0.1.0
github.com/jwetzell/osc-go v0.2.0 github.com/jwetzell/osc-go v0.2.0
github.com/jwetzell/psn-go v0.3.0 github.com/jwetzell/psn-go v0.3.0
github.com/nats-io/nats-server/v2 v2.12.4 github.com/nats-io/nats-server/v2 v2.12.5
github.com/nats-io/nats.go v1.49.0 github.com/nats-io/nats.go v1.49.0
github.com/urfave/cli/v3 v3.7.0 github.com/urfave/cli/v3 v3.7.0
gitlab.com/gomidi/midi/v2 v2.3.23 gitlab.com/gomidi/midi/v2 v2.3.23
go.bug.st/serial v1.6.4 go.bug.st/serial v1.6.4
go.opentelemetry.io/otel v1.41.0 go.opentelemetry.io/otel v1.42.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0
go.opentelemetry.io/otel/sdk v1.41.0 go.opentelemetry.io/otel/sdk v1.42.0
go.opentelemetry.io/otel/trace v1.41.0 go.opentelemetry.io/otel/trace v1.42.0
modernc.org/quickjs v0.17.1 modernc.org/quickjs v0.17.1
sigs.k8s.io/yaml v1.6.0 sigs.k8s.io/yaml v1.6.0
) )
require ( require (
github.com/antithesishq/antithesis-sdk-go v0.5.0-default-no-op // indirect github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/creack/goselect v0.1.2 // indirect github.com/creack/goselect v0.1.2 // indirect
@@ -42,15 +43,14 @@ require (
github.com/gobwas/ws v1.4.0 // indirect github.com/gobwas/ws v1.4.0 // indirect
github.com/google/go-tpm v0.9.8 // indirect github.com/google/go-tpm v0.9.8 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca // indirect github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca // indirect
github.com/icholy/digest v1.1.0 // indirect github.com/icholy/digest v1.1.0 // indirect
github.com/klauspost/compress v1.18.3 // indirect github.com/klauspost/compress v1.18.4 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 // indirect github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 // indirect
github.com/nats-io/jwt/v2 v2.8.0 // indirect github.com/nats-io/jwt/v2 v2.8.0 // indirect
github.com/nats-io/nkeys v0.4.12 // indirect github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect github.com/nats-io/nuid v1.0.1 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/pion/logging v0.2.4 // indirect github.com/pion/logging v0.2.4 // indirect
@@ -65,20 +65,20 @@ require (
github.com/tetratelabs/wazero v1.9.0 // indirect github.com/tetratelabs/wazero v1.9.0 // indirect
github.com/zaf/g711 v1.4.0 // indirect github.com/zaf/g711 v1.4.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect
go.opentelemetry.io/otel/metric v1.41.0 // indirect go.opentelemetry.io/otel/metric v1.42.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/crypto v0.48.0 // indirect golang.org/x/crypto v0.48.0 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/net v0.50.0 // indirect golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.34.0 // indirect golang.org/x/text v0.34.0 // indirect
golang.org/x/time v0.14.0 // indirect golang.org/x/time v0.15.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/grpc v1.79.1 // indirect google.golang.org/grpc v1.79.2 // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302 // indirect gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302 // indirect
modernc.org/libc v1.67.1 // indirect modernc.org/libc v1.67.1 // indirect

64
go.sum
View File

@@ -1,5 +1,5 @@
github.com/antithesishq/antithesis-sdk-go v0.5.0-default-no-op h1:Ucf+QxEKMbPogRO5guBNe5cgd9uZgfoJLOYs8WWhtjM= github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op h1:kpBdlEPbRvff0mDD1gk7o9BhI16b9p5yYAXRlidpqJE=
github.com/antithesishq/antithesis-sdk-go v0.5.0-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E= github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
@@ -18,8 +18,8 @@ github.com/emiago/diago v0.27.0 h1:5SQBbcLR9ooxhSMlTkU9QSuDv/2nhMO9lxuWJTd/7rE=
github.com/emiago/diago v0.27.0/go.mod h1:8hUxCDPJY2p32hh+4ed7vHW/3yTMmEa3BjNctUPeGD0= github.com/emiago/diago v0.27.0/go.mod h1:8hUxCDPJY2p32hh+4ed7vHW/3yTMmEa3BjNctUPeGD0=
github.com/emiago/dtls/v3 v3.0.0-20260122183559-8b8d23e359c0 h1:o4LxpUnZ1zxiQ+Qjc9kLwXcjz31NGAHmnZ7xoJto3VM= github.com/emiago/dtls/v3 v3.0.0-20260122183559-8b8d23e359c0 h1:o4LxpUnZ1zxiQ+Qjc9kLwXcjz31NGAHmnZ7xoJto3VM=
github.com/emiago/dtls/v3 v3.0.0-20260122183559-8b8d23e359c0/go.mod h1:ydcZ977eS1I6uOWodzMuw30BwvNAzT9su/xcNYSJqjA= github.com/emiago/dtls/v3 v3.0.0-20260122183559-8b8d23e359c0/go.mod h1:ydcZ977eS1I6uOWodzMuw30BwvNAzT9su/xcNYSJqjA=
github.com/emiago/sipgo v1.2.0 h1:rmHFdCu9zu2Cabfd8+/eC9HQWyooqk8x+ti550z5lBw= github.com/emiago/sipgo v1.2.1 h1:5JTwogbe3yQFA3sjBVueN2Q4WTU350tGeBwPYT8HMv0=
github.com/emiago/sipgo v1.2.0/go.mod h1:DuwAxBZhKMqIzQFPGZb1MVAGU6Wuxj64oTOhd5dx/FY= github.com/emiago/sipgo v1.2.1/go.mod h1:DuwAxBZhKMqIzQFPGZb1MVAGU6Wuxj64oTOhd5dx/FY=
github.com/expr-lang/expr v1.17.8 h1:W1loDTT+0PQf5YteHSTpju2qfUfNoBt4yw9+wOEU9VM= github.com/expr-lang/expr v1.17.8 h1:W1loDTT+0PQf5YteHSTpju2qfUfNoBt4yw9+wOEU9VM=
github.com/expr-lang/expr v1.17.8/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4= github.com/expr-lang/expr v1.17.8/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/extism/go-sdk v1.7.1 h1:lWJos6uY+tRFdlIHR+SJjwFDApY7OypS/2nMhiVQ9Sw= github.com/extism/go-sdk v1.7.1 h1:lWJos6uY+tRFdlIHR+SJjwFDApY7OypS/2nMhiVQ9Sw=
@@ -65,8 +65,8 @@ github.com/jwetzell/osc-go v0.2.0 h1:4as+BYCeZhEddFczGveP5yZZxvY728Uavz+ZSLZfOII
github.com/jwetzell/osc-go v0.2.0/go.mod h1:D3ZIXYB12bt4S35lKFUqgCFbF1Y+9Ld0sOhHA9mGZZM= github.com/jwetzell/osc-go v0.2.0/go.mod h1:D3ZIXYB12bt4S35lKFUqgCFbF1Y+9Ld0sOhHA9mGZZM=
github.com/jwetzell/psn-go v0.3.0 h1:WVpCEmExYE8a+I5hQak5jNJJp2x35VdGX/VuMUKPmhY= github.com/jwetzell/psn-go v0.3.0 h1:WVpCEmExYE8a+I5hQak5jNJJp2x35VdGX/VuMUKPmhY=
github.com/jwetzell/psn-go v0.3.0/go.mod h1:bcEAeti4sQM375buujb3mIfmUstD4Aby18gq3ENb6+o= github.com/jwetzell/psn-go v0.3.0/go.mod h1:bcEAeti4sQM375buujb3mIfmUstD4Aby18gq3ENb6+o=
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -77,12 +77,12 @@ github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 h1:KGuD/pM2JpL
github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ= github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
github.com/nats-io/jwt/v2 v2.8.0 h1:K7uzyz50+yGZDO5o772eRE7atlcSEENpL7P+b74JV1g= github.com/nats-io/jwt/v2 v2.8.0 h1:K7uzyz50+yGZDO5o772eRE7atlcSEENpL7P+b74JV1g=
github.com/nats-io/jwt/v2 v2.8.0/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA= github.com/nats-io/jwt/v2 v2.8.0/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA=
github.com/nats-io/nats-server/v2 v2.12.4 h1:ZnT10v2LU2Xcoiy8ek9X6Se4YG8EuMfIfvAEuFVx1Ts= github.com/nats-io/nats-server/v2 v2.12.5 h1:EOHLbsLJgUHUwzkj9gBTOlubkX+dmSs0EYWMdBiHivU=
github.com/nats-io/nats-server/v2 v2.12.4/go.mod h1:5MCp/pqm5SEfsvVZ31ll1088ZTwEUdvRX1Hmh/mTTDg= github.com/nats-io/nats-server/v2 v2.12.5/go.mod h1:JQDAKcwdXs0NRhvYO31dzsXkzCyDkOBS7SKU3Nozu14=
github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE= github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE=
github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw= github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw=
github.com/nats-io/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc= github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg= github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w= github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
@@ -123,20 +123,20 @@ go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A=
go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI= go.bug.st/serial v1.6.4/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c= go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE= go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0 h1:ao6Oe+wSebTlQ1OEht7jlYTzQKE+pnx/iNywFvTbuuI= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0/go.mod h1:u3T6vz0gh/NVzgDgiwkgLxpsSF6PaPmo2il0apGJbls= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0/go.mod h1:J2pvYM5NGHofZ2/Ru6zw/TNWnEQp5crgyDeSrYpXkAw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0 h1:inYW9ZhgqiDqh6BioM7DVHHzEGVq76Db5897WLGZ5Go= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 h1:uLXP+3mghfMf7XmV4PkGfFhFKuNWoCvvx5wP/wOXo0o=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0/go.mod h1:Izur+Wt8gClgMJqO/cZ8wdeeMryJ/xxiOVgFSSfpDTY= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0/go.mod h1:v0Tj04armyT59mnURNUJf7RCKcKzq+lgJs6QSjHjaTc=
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ= go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps= go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8= go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90= go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8= go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y= go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0= go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis= go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -151,18 +151,18 @@ golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c= golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU= golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc= golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg= golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
@@ -171,8 +171,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -1,7 +1,179 @@
package common package common
type contextKey string import (
"math"
"reflect"
)
const RouterContextKey contextKey = contextKey("router") func GetAnyAs[T any](value any) (T, bool) {
const SourceContextKey contextKey = contextKey("source") typed, ok := value.(T)
const ModulesContextKey contextKey = contextKey("modules") return typed, ok
}
func GetAnyAsInt(value any) (int, bool) {
intValue, ok := value.(int)
if ok {
return intValue, true
}
uintValue, ok := value.(uint)
if ok {
return int(uintValue), true
}
byteValue, ok := value.(byte)
if ok {
return int(byteValue), true
}
floatValue, ok := value.(float64)
if ok {
if floatValue != math.Floor(floatValue) {
return 0, false
}
return int(floatValue), true
}
return 0, false
}
func GetAnyAsByteSlice(value any) ([]byte, bool) {
v := reflect.ValueOf(value)
if v.Kind() != reflect.Slice {
return nil, false
}
result := make([]byte, v.Len())
for i := 0; i < v.Len(); i++ {
elem := v.Index(i).Interface()
byteValue, ok := elem.(byte)
if ok {
result[i] = byteValue
continue
}
uintValue, ok := elem.(uint)
if ok {
if uintValue > 255 {
return nil, false
}
result[i] = byte(uintValue)
continue
}
intValue, ok := elem.(int)
if ok {
if intValue < 0 || intValue > 255 {
return nil, false
}
result[i] = byte(intValue)
continue
}
floatValue, ok := elem.(float64)
if ok {
if floatValue != math.Floor(floatValue) {
return nil, false
}
if floatValue < 0 || floatValue > 255 {
return nil, false
}
result[i] = byte(floatValue)
continue
}
return nil, false
}
return result, true
}
func GetAnyAsIntSlice(value any) ([]int, bool) {
v := reflect.ValueOf(value)
if v.Kind() != reflect.Slice {
return nil, false
}
result := make([]int, v.Len())
for i := 0; i < v.Len(); i++ {
elem := v.Index(i).Interface()
byteValue, ok := elem.(byte)
if ok {
result[i] = int(byteValue)
continue
}
uintValue, ok := elem.(uint)
if ok {
result[i] = int(uintValue)
continue
}
intValue, ok := elem.(int)
if ok {
result[i] = int(intValue)
continue
}
floatValue, ok := elem.(float64)
if ok {
if floatValue != math.Floor(floatValue) {
return nil, false
}
result[i] = int(floatValue)
continue
}
return nil, false
}
return result, true
}
func GetAnyAsFloat32(value any) (float32, bool) {
float32Value, ok := value.(float32)
if ok {
return float32Value, true
}
float64Value, ok := value.(float64)
if ok {
return float32(float64Value), true
}
intValue, ok := value.(int)
if ok {
return float32(intValue), true
}
uintValue, ok := value.(uint)
if ok {
return float32(uintValue), true
}
byteValue, ok := value.(byte)
if ok {
return float32(byteValue), true
}
return 0, false
}
func GetAnyAsFloat64(value any) (float64, bool) {
float64Value, ok := value.(float64)
if ok {
return float64Value, true
}
float32Value, ok := value.(float32)
if ok {
return float64(float32Value), true
}
intValue, ok := value.(int)
if ok {
return float64(intValue), true
}
uintValue, ok := value.(uint)
if ok {
return float64(uintValue), true
}
byteValue, ok := value.(byte)
if ok {
return float64(byteValue), true
}
return 0, false
}

View File

@@ -0,0 +1,8 @@
package common
type contextKey string
const RouterContextKey contextKey = contextKey("router")
const SourceContextKey contextKey = contextKey("source")
const ModulesContextKey contextKey = contextKey("modules")
const SenderContextKey contextKey = contextKey("sender")

View File

@@ -0,0 +1,15 @@
package common
import "context"
type RouteIO interface {
HandleInput(ctx context.Context, sourceId string, payload any) (bool, []RouteIOError)
HandleOutput(ctx context.Context, destinationId string, payload any) error
}
type RouteIOError struct {
Index int `json:"index"`
OutputError error `json:"outputError"`
ProcessError error `json:"processError"`
InputError error `json:"inputError"`
}

View File

@@ -1,282 +1,26 @@
package config package config
import (
"errors"
"fmt"
"math"
"reflect"
)
type Config struct { type Config struct {
Api ApiConfig `json:"api"`
Modules []ModuleConfig `json:"modules"` Modules []ModuleConfig `json:"modules"`
Routes []RouteConfig `json:"routes"` Routes []RouteConfig `json:"routes"`
} }
type Params map[string]any type ApiConfig struct {
Port int `json:"port"`
var (
ErrParamNotFound = errors.New("not found")
ErrParamNotString = errors.New("not a string")
ErrParamNotNumber = errors.New("not a number")
ErrParamNotInteger = errors.New("not an integer")
ErrParamNotBool = errors.New("not a boolean")
ErrParamNotSlice = errors.New("not a slice")
)
func (p Params) GetString(key string) (string, error) {
value, ok := p[key]
if !ok {
return "", ErrParamNotFound
}
stringValue, ok := value.(string)
if !ok {
return "", ErrParamNotString
}
return stringValue, nil
} }
func (p Params) GetInt(key string) (int, error) {
value, ok := p[key]
if !ok {
return 0, ErrParamNotFound
}
intValue, ok := value.(int)
if ok {
return intValue, nil
}
uintValue, ok := value.(uint)
if ok {
return int(uintValue), nil
}
byteValue, ok := value.(byte)
if ok {
return int(byteValue), nil
}
floatValue, ok := value.(float64)
if ok {
if floatValue != math.Floor(floatValue) {
return 0, ErrParamNotInteger
}
return int(floatValue), nil
}
return 0, ErrParamNotNumber
}
func (p Params) GetFloat32(key string) (float32, error) {
value, ok := p[key]
if !ok {
return 0, ErrParamNotFound
}
float32Value, ok := value.(float32)
if ok {
return float32Value, nil
}
float64Value, ok := value.(float64)
if ok {
return float32(float64Value), nil
}
intValue, ok := value.(int)
if ok {
return float32(intValue), nil
}
uintValue, ok := value.(uint)
if ok {
return float32(uintValue), nil
}
byteValue, ok := value.(byte)
if ok {
return float32(byteValue), nil
}
return 0, ErrParamNotNumber
}
func (p Params) GetFloat64(key string) (float64, error) {
value, ok := p[key]
if !ok {
return 0, ErrParamNotFound
}
float64Value, ok := value.(float64)
if ok {
return float64Value, nil
}
float32Value, ok := value.(float32)
if ok {
return float64(float32Value), nil
}
intValue, ok := value.(int)
if ok {
return float64(intValue), nil
}
uintValue, ok := value.(uint)
if ok {
return float64(uintValue), nil
}
byteValue, ok := value.(byte)
if ok {
return float64(byteValue), nil
}
return 0, ErrParamNotNumber
}
func (p Params) GetBool(key string) (bool, error) {
value, ok := p[key]
if !ok {
return false, ErrParamNotFound
}
boolValue, ok := value.(bool)
if !ok {
return false, ErrParamNotBool
}
return boolValue, nil
}
func (p Params) GetStringSlice(key string) ([]string, error) {
value, ok := p[key]
if !ok {
return nil, ErrParamNotFound
}
interfaceSlice, ok := value.([]any)
if !ok {
return nil, ErrParamNotSlice
}
stringSlice := make([]string, len(interfaceSlice))
for i, v := range interfaceSlice {
str, ok := v.(string)
if !ok {
return nil, fmt.Errorf("element at index %d is not a string", i)
}
stringSlice[i] = str
}
return stringSlice, nil
}
func (p Params) GetIntSlice(key string) ([]int, error) {
value, ok := p[key]
if !ok {
return nil, ErrParamNotFound
}
v := reflect.ValueOf(value)
if v.Kind() != reflect.Slice {
return nil, ErrParamNotSlice
}
result := make([]int, v.Len())
for i := 0; i < v.Len(); i++ {
elem := v.Index(i).Interface()
byteValue, ok := elem.(byte)
if ok {
result[i] = int(byteValue)
continue
}
uintValue, ok := elem.(uint)
if ok {
result[i] = int(uintValue)
continue
}
intValue, ok := elem.(int)
if ok {
result[i] = int(intValue)
continue
}
floatValue, ok := elem.(float64)
if ok {
if floatValue != math.Floor(floatValue) {
return nil, fmt.Errorf("element at index %d is not an integer", i)
}
result[i] = int(floatValue)
continue
}
return nil, fmt.Errorf("element at index %d is not a number", i)
}
return result, nil
}
func (p Params) GetByteSlice(key string) ([]byte, error) {
value, ok := p[key]
if !ok {
return nil, ErrParamNotFound
}
v := reflect.ValueOf(value)
if v.Kind() != reflect.Slice {
return nil, ErrParamNotSlice
}
result := make([]byte, v.Len())
for i := 0; i < v.Len(); i++ {
elem := v.Index(i).Interface()
byteValue, ok := elem.(byte)
if ok {
result[i] = byteValue
continue
}
uintValue, ok := elem.(uint)
if ok {
if uintValue > 255 {
return nil, fmt.Errorf("element at index %d is out of byte range", i)
}
result[i] = byte(uintValue)
continue
}
intValue, ok := elem.(int)
if ok {
if intValue < 0 || intValue > 255 {
return nil, fmt.Errorf("element at index %d is out of byte range", i)
}
result[i] = byte(intValue)
continue
}
floatValue, ok := elem.(float64)
if ok {
if floatValue != math.Floor(floatValue) {
return nil, fmt.Errorf("element at index %d is not an integer", i)
}
if floatValue < 0 || floatValue > 255 {
return nil, fmt.Errorf("element at index %d is out of byte range", i)
}
result[i] = byte(floatValue)
continue
}
return nil, fmt.Errorf("element at index %d is not a number", i)
}
return result, nil
}
type ModuleConfig struct { type ModuleConfig struct {
Id string `json:"id"` Id string `json:"id"`
Type string `json:"type"` Type string `json:"type"`
Params Params `json:"params"` Params Params `json:"params,omitempty"`
} }
type RouteConfig struct { type RouteConfig struct {
Input string `json:"input"` Input string `json:"input"`
Processors []ProcessorConfig `json:"processors"` Processors []ProcessorConfig `json:"processors"`
Output string `json:"output"`
} }
type ProcessorConfig struct { type ProcessorConfig struct {
Type string `json:"type"` Type string `json:"type"`
Params Params `json:"params"` Params Params `json:"params,omitempty"`
} }

140
internal/config/params.go Normal file
View File

@@ -0,0 +1,140 @@
package config
import (
"errors"
"fmt"
"github.com/jwetzell/showbridge-go/internal/common"
)
type Params map[string]any
var (
ErrParamNotFound = errors.New("not found")
ErrParamNotString = errors.New("not a string")
ErrParamNotNumber = errors.New("not a number")
ErrParamNotInteger = errors.New("not an integer")
ErrParamNotBool = errors.New("not a boolean")
ErrParamNotSlice = errors.New("not a slice")
ErrParamNotByteSlice = errors.New("not a byte slice")
ErrParamNotIntSlice = errors.New("not an int slice")
)
func (p Params) GetString(key string) (string, error) {
value, ok := p[key]
if !ok {
return "", ErrParamNotFound
}
stringValue, ok := value.(string)
if !ok {
return "", ErrParamNotString
}
return stringValue, nil
}
func (p Params) GetInt(key string) (int, error) {
value, ok := p[key]
if !ok {
return 0, ErrParamNotFound
}
intValue, ok := common.GetAnyAsInt(value)
if ok {
return intValue, nil
}
return 0, ErrParamNotNumber
}
func (p Params) GetFloat32(key string) (float32, error) {
value, ok := p[key]
if !ok {
return 0, ErrParamNotFound
}
floatValue, ok := common.GetAnyAsFloat32(value)
if ok {
return floatValue, nil
}
return 0, ErrParamNotNumber
}
func (p Params) GetFloat64(key string) (float64, error) {
value, ok := p[key]
if !ok {
return 0, ErrParamNotFound
}
floatValue, ok := common.GetAnyAsFloat64(value)
if ok {
return floatValue, nil
}
return 0, ErrParamNotNumber
}
func (p Params) GetBool(key string) (bool, error) {
value, ok := p[key]
if !ok {
return false, ErrParamNotFound
}
boolValue, ok := value.(bool)
if !ok {
return false, ErrParamNotBool
}
return boolValue, nil
}
func (p Params) GetStringSlice(key string) ([]string, error) {
value, ok := p[key]
if !ok {
return nil, ErrParamNotFound
}
interfaceSlice, ok := value.([]any)
if !ok {
return nil, ErrParamNotSlice
}
stringSlice := make([]string, len(interfaceSlice))
for i, v := range interfaceSlice {
str, ok := v.(string)
if !ok {
return nil, fmt.Errorf("element at index %d is not a string", i)
}
stringSlice[i] = str
}
return stringSlice, nil
}
func (p Params) GetIntSlice(key string) ([]int, error) {
value, ok := p[key]
if !ok {
return nil, ErrParamNotFound
}
intSlice, ok := common.GetAnyAsIntSlice(value)
if !ok {
return nil, ErrParamNotIntSlice
}
return intSlice, nil
}
func (p Params) GetByteSlice(key string) ([]byte, error) {
value, ok := p[key]
if !ok {
return nil, ErrParamNotFound
}
byteSlice, ok := common.GetAnyAsByteSlice(value)
if !ok {
return nil, ErrParamNotByteSlice
}
return byteSlice, nil
}

View File

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

View File

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

View File

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

View File

@@ -10,8 +10,6 @@ import (
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
"github.com/jwetzell/showbridge-go/internal/route"
"gitlab.com/gomidi/midi/v2" "gitlab.com/gomidi/midi/v2"
_ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv" _ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
) )
@@ -19,7 +17,7 @@ import (
type MIDIOutput struct { type MIDIOutput struct {
config config.ModuleConfig config config.ModuleConfig
ctx context.Context ctx context.Context
router route.RouteIO router common.RouteIO
Port string Port string
SendFunc func(midi.Message) error SendFunc func(midi.Message) error
logger *slog.Logger logger *slog.Logger
@@ -53,7 +51,7 @@ func (mo *MIDIOutput) Type() string {
func (mo *MIDIOutput) Start(ctx context.Context) error { func (mo *MIDIOutput) Start(ctx context.Context) error {
mo.logger.Debug("running") mo.logger.Debug("running")
defer midi.CloseDriver() defer midi.CloseDriver()
router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO) router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok { if !ok {
return errors.New("midi.output unable to get router from context") return errors.New("midi.output unable to get router from context")
@@ -86,7 +84,7 @@ func (mo *MIDIOutput) Output(ctx context.Context, payload any) error {
return errors.New("midi.output output is not setup") return errors.New("midi.output output is not setup")
} }
payloadMessage, ok := processor.GetAnyAs[midi.Message](payload) payloadMessage, ok := common.GetAnyAs[midi.Message](payload)
if !ok { if !ok {
return errors.New("midi.output can only ouptut midi.Message") return errors.New("midi.output can only ouptut midi.Message")

View File

@@ -10,9 +10,9 @@ import (
) )
type ModuleError struct { type ModuleError struct {
Index int Index int `json:"index"`
Config config.ModuleConfig Config config.ModuleConfig `json:"config"`
Error error Error string `json:"error"`
} }
type Module interface { type Module interface {

View File

@@ -9,14 +9,12 @@ import (
mqtt "github.com/eclipse/paho.mqtt.golang" mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
"github.com/jwetzell/showbridge-go/internal/route"
) )
type MQTTClient struct { type MQTTClient struct {
config config.ModuleConfig config config.ModuleConfig
ctx context.Context ctx context.Context
router route.RouteIO router common.RouteIO
Broker string Broker string
ClientID string ClientID string
Topic string Topic string
@@ -63,7 +61,7 @@ func (mc *MQTTClient) Type() string {
func (mc *MQTTClient) Start(ctx context.Context) error { func (mc *MQTTClient) Start(ctx context.Context) error {
mc.logger.Debug("running") mc.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO) router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok { if !ok {
return errors.New("mqtt.client unable to get router from context") return errors.New("mqtt.client unable to get router from context")
@@ -103,7 +101,7 @@ func (mc *MQTTClient) Start(ctx context.Context) error {
} }
func (mc *MQTTClient) Output(ctx context.Context, payload any) error { func (mc *MQTTClient) Output(ctx context.Context, payload any) error {
payloadMessage, ok := processor.GetAnyAs[mqtt.Message](payload) payloadMessage, ok := common.GetAnyAs[mqtt.Message](payload)
if !ok { if !ok {
return errors.New("mqtt.client is only able to output a MQTTMessage") return errors.New("mqtt.client is only able to output a MQTTMessage")

View File

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

View File

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

View File

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

View File

@@ -12,15 +12,13 @@ import (
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/framer" "github.com/jwetzell/showbridge-go/internal/framer"
"github.com/jwetzell/showbridge-go/internal/processor"
"github.com/jwetzell/showbridge-go/internal/route"
"go.bug.st/serial" "go.bug.st/serial"
) )
type SerialClient struct { type SerialClient struct {
config config.ModuleConfig config config.ModuleConfig
ctx context.Context ctx context.Context
router route.RouteIO router common.RouteIO
Port string Port string
Framer framer.Framer Framer framer.Framer
Mode *serial.Mode Mode *serial.Mode
@@ -86,7 +84,7 @@ func (sc *SerialClient) SetupPort() error {
func (sc *SerialClient) Start(ctx context.Context) error { func (sc *SerialClient) Start(ctx context.Context) error {
sc.logger.Debug("running") sc.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO) router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok { if !ok {
return errors.New("serial.client unable to get router from context") return errors.New("serial.client unable to get router from context")
@@ -158,7 +156,7 @@ func (sc *SerialClient) Start(ctx context.Context) error {
func (sc *SerialClient) Output(ctx context.Context, payload any) error { func (sc *SerialClient) Output(ctx context.Context, payload any) error {
payloadBytes, ok := processor.GetAnyAs[[]byte](payload) payloadBytes, ok := common.GetAnyAs[[]byte](payload)
if !ok { if !ok {
return errors.New("serial.client can only ouptut bytes") return errors.New("serial.client can only ouptut bytes")

View File

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

View File

@@ -18,13 +18,12 @@ import (
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor" "github.com/jwetzell/showbridge-go/internal/processor"
"github.com/jwetzell/showbridge-go/internal/route"
) )
type SIPDTMFServer struct { type SIPDTMFServer struct {
config config.ModuleConfig config config.ModuleConfig
ctx context.Context ctx context.Context
router route.RouteIO router common.RouteIO
IP string IP string
Port int Port int
Transport string Transport string
@@ -114,7 +113,7 @@ func (sds *SIPDTMFServer) Type() string {
func (sds *SIPDTMFServer) Start(ctx context.Context) error { func (sds *SIPDTMFServer) Start(ctx context.Context) error {
sds.logger.Debug("running") sds.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO) router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok { if !ok {
return errors.New("sip.dtmf.server unable to get router from context") return errors.New("sip.dtmf.server unable to get router from context")
@@ -200,7 +199,7 @@ func (sds *SIPDTMFServer) Output(ctx context.Context, payload any) error {
return errors.New("sip.dtmf.server inDialog already ended") return errors.New("sip.dtmf.server inDialog already ended")
} }
payloadDTMFResponse, ok := processor.GetAnyAs[processor.SipDTMFResponse](payload) payloadDTMFResponse, ok := common.GetAnyAs[processor.SipDTMFResponse](payload)
if ok { if ok {
dtmfWriter := call.inDialog.AudioWriterDTMF() dtmfWriter := call.inDialog.AudioWriterDTMF()
@@ -217,7 +216,7 @@ func (sds *SIPDTMFServer) Output(ctx context.Context, payload any) error {
return nil return nil
} }
payloadAudioFileResponse, ok := processor.GetAnyAs[processor.SipAudioFileResponse](payload) payloadAudioFileResponse, ok := common.GetAnyAs[processor.SipAudioFileResponse](payload)
if ok { if ok {
audioFile, err := os.Open(payloadAudioFileResponse.AudioFile) audioFile, err := os.Open(payloadAudioFileResponse.AudioFile)

View File

@@ -11,8 +11,6 @@ import (
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/framer" "github.com/jwetzell/showbridge-go/internal/framer"
"github.com/jwetzell/showbridge-go/internal/processor"
"github.com/jwetzell/showbridge-go/internal/route"
) )
type TCPClient struct { type TCPClient struct {
@@ -20,7 +18,7 @@ type TCPClient struct {
framer framer.Framer framer framer.Framer
conn *net.TCPConn conn *net.TCPConn
ctx context.Context ctx context.Context
router route.RouteIO router common.RouteIO
Addr *net.TCPAddr Addr *net.TCPAddr
logger *slog.Logger logger *slog.Logger
cancel context.CancelFunc cancel context.CancelFunc
@@ -71,7 +69,7 @@ func (tc *TCPClient) Type() string {
func (tc *TCPClient) Start(ctx context.Context) error { func (tc *TCPClient) Start(ctx context.Context) error {
tc.logger.Debug("running") tc.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO) router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok { if !ok {
return errors.New("net.tcp.client unable to get router from context") return errors.New("net.tcp.client unable to get router from context")
@@ -154,7 +152,7 @@ func (tc *TCPClient) Output(ctx context.Context, payload any) error {
return err return err
} }
} }
payloadBytes, ok := processor.GetAnyAs[[]byte](payload) payloadBytes, ok := common.GetAnyAs[[]byte](payload)
if !ok { if !ok {
return errors.New("net.tcp.client is only able to output bytes") return errors.New("net.tcp.client is only able to output bytes")
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,8 +9,6 @@ import (
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
"github.com/jwetzell/showbridge-go/internal/route"
) )
type UDPClient struct { type UDPClient struct {
@@ -19,7 +17,7 @@ type UDPClient struct {
Port uint16 Port uint16
conn *net.UDPConn conn *net.UDPConn
ctx context.Context ctx context.Context
router route.RouteIO router common.RouteIO
logger *slog.Logger logger *slog.Logger
cancel context.CancelFunc cancel context.CancelFunc
} }
@@ -64,7 +62,7 @@ func (uc *UDPClient) SetupConn() error {
func (uc *UDPClient) Start(ctx context.Context) error { func (uc *UDPClient) Start(ctx context.Context) error {
uc.logger.Debug("running") uc.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO) router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok { if !ok {
return errors.New("net.udp.client unable to get router from context") return errors.New("net.udp.client unable to get router from context")
@@ -89,7 +87,7 @@ func (uc *UDPClient) Start(ctx context.Context) error {
func (uc *UDPClient) Output(ctx context.Context, payload any) error { func (uc *UDPClient) Output(ctx context.Context, payload any) error {
payloadBytes, ok := processor.GetAnyAs[[]byte](payload) payloadBytes, ok := common.GetAnyAs[[]byte](payload)
if !ok { if !ok {
return errors.New("net.udp.client is only able to output bytes") return errors.New("net.udp.client is only able to output bytes")
} }

View File

@@ -10,15 +10,13 @@ import (
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
"github.com/jwetzell/showbridge-go/internal/route"
) )
type UDPMulticast struct { type UDPMulticast struct {
config config.ModuleConfig config config.ModuleConfig
conn *net.UDPConn conn *net.UDPConn
ctx context.Context ctx context.Context
router route.RouteIO router common.RouteIO
Addr *net.UDPAddr Addr *net.UDPAddr
logger *slog.Logger logger *slog.Logger
cancel context.CancelFunc cancel context.CancelFunc
@@ -58,7 +56,7 @@ func (um *UDPMulticast) Type() string {
func (um *UDPMulticast) Start(ctx context.Context) error { func (um *UDPMulticast) Start(ctx context.Context) error {
um.logger.Debug("running") um.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO) router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok { if !ok {
return errors.New("net.udp.multicast unable to get router from context") return errors.New("net.udp.multicast unable to get router from context")
@@ -110,7 +108,7 @@ func (um *UDPMulticast) Start(ctx context.Context) error {
func (um *UDPMulticast) Output(ctx context.Context, payload any) error { func (um *UDPMulticast) Output(ctx context.Context, payload any) error {
payloadBytes, ok := processor.GetAnyAs[[]byte](payload) payloadBytes, ok := common.GetAnyAs[[]byte](payload)
if !ok { if !ok {
return errors.New("net.udp.multicast can only output bytes") return errors.New("net.udp.multicast can only output bytes")
} }

View File

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

View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"github.com/jwetzell/artnet-go" "github.com/jwetzell/artnet-go"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
@@ -13,7 +14,7 @@ type ArtNetPacketDecode struct {
} }
func (apd *ArtNetPacketDecode) Process(ctx context.Context, payload any) (any, error) { func (apd *ArtNetPacketDecode) Process(ctx context.Context, payload any) (any, error) {
payloadBytes, ok := GetAnyAs[[]byte](payload) payloadBytes, ok := common.GetAnyAs[[]byte](payload)
if !ok { if !ok {
return nil, fmt.Errorf("artnet.packet.decode processor only accepts a []byte") return nil, fmt.Errorf("artnet.packet.decode processor only accepts a []byte")

View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"github.com/jwetzell/artnet-go" "github.com/jwetzell/artnet-go"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
@@ -13,7 +14,7 @@ type ArtNetPacketEncode struct {
} }
func (ape *ArtNetPacketEncode) Process(ctx context.Context, payload any) (any, error) { func (ape *ArtNetPacketEncode) Process(ctx context.Context, payload any) (any, error) {
payloadPacket, ok := GetAnyAs[artnet.ArtNetPacket](payload) payloadPacket, ok := common.GetAnyAs[artnet.ArtNetPacket](payload)
if !ok { if !ok {
return nil, fmt.Errorf("artnet.packet.encode processor only accepts an ArtNetPacket") return nil, fmt.Errorf("artnet.packet.encode processor only accepts an ArtNetPacket")

View File

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

View File

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

View File

@@ -6,41 +6,42 @@ import (
"fmt" "fmt"
"regexp" "regexp"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
type StringFilter struct { type FilterRegex struct {
config config.ProcessorConfig config config.ProcessorConfig
Pattern *regexp.Regexp Pattern *regexp.Regexp
} }
func (sf *StringFilter) Process(ctx context.Context, payload any) (any, error) { func (fr *FilterRegex) Process(ctx context.Context, payload any) (any, error) {
payloadString, ok := GetAnyAs[string](payload) payloadString, ok := common.GetAnyAs[string](payload)
if !ok { if !ok {
return nil, errors.New("string.filter processor only accepts a string") return nil, errors.New("filter.regex processor only accepts a string")
} }
if !sf.Pattern.MatchString(payloadString) { if !fr.Pattern.MatchString(payloadString) {
return nil, nil return nil, nil
} }
return payloadString, nil return payloadString, nil
} }
func (sf *StringFilter) Type() string { func (fr *FilterRegex) Type() string {
return sf.config.Type return fr.config.Type
} }
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "string.filter", Type: "filter.regex",
New: func(config config.ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params params := config.Params
patternString, err := params.GetString("pattern") patternString, err := params.GetString("pattern")
if err != nil { if err != nil {
return nil, fmt.Errorf("string.filter pattern error: %w", err) return nil, fmt.Errorf("filter.regex pattern error: %w", err)
} }
patternRegexp, err := regexp.Compile(patternString) patternRegexp, err := regexp.Compile(patternString)
@@ -49,7 +50,7 @@ func init() {
return nil, err return nil, err
} }
return &StringFilter{config: config, Pattern: patternRegexp}, nil return &FilterRegex{config: config, Pattern: patternRegexp}, nil
}, },
}) })
} }

View File

@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
@@ -15,7 +16,7 @@ type FloatParse struct {
} }
func (fp *FloatParse) Process(ctx context.Context, payload any) (any, error) { func (fp *FloatParse) Process(ctx context.Context, payload any) (any, error) {
payloadString, ok := GetAnyAs[string](payload) payloadString, ok := common.GetAnyAs[string](payload)
if !ok { if !ok {
return nil, errors.New("float.parse processor only accepts a string") return nil, errors.New("float.parse processor only accepts a string")

View File

@@ -5,6 +5,7 @@ import (
"errors" "errors"
freeD "github.com/jwetzell/free-d-go" freeD "github.com/jwetzell/free-d-go"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
@@ -12,8 +13,8 @@ type FreeDDecode struct {
config config.ProcessorConfig config config.ProcessorConfig
} }
func (fdd *FreeDDecode) Process(ctx context.Context, payload any) (any, error) { func (fd *FreeDDecode) Process(ctx context.Context, payload any) (any, error) {
payloadBytes, ok := GetAnyAs[[]byte](payload) payloadBytes, ok := common.GetAnyAs[[]byte](payload)
if !ok { if !ok {
return nil, errors.New("freed.decode processor only accepts a []byte") return nil, errors.New("freed.decode processor only accepts a []byte")
@@ -26,8 +27,8 @@ func (fdd *FreeDDecode) Process(ctx context.Context, payload any) (any, error) {
return payloadMessage, nil return payloadMessage, nil
} }
func (fdd *FreeDDecode) Type() string { func (fd *FreeDDecode) Type() string {
return fdd.config.Type return fd.config.Type
} }
func init() { func init() {

View File

@@ -5,6 +5,7 @@ import (
"errors" "errors"
freeD "github.com/jwetzell/free-d-go" freeD "github.com/jwetzell/free-d-go"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
@@ -12,8 +13,8 @@ type FreeDEncode struct {
config config.ProcessorConfig config config.ProcessorConfig
} }
func (fde *FreeDEncode) Process(ctx context.Context, payload any) (any, error) { func (fe *FreeDEncode) Process(ctx context.Context, payload any) (any, error) {
payloadPosition, ok := GetAnyAs[freeD.FreeDPosition](payload) payloadPosition, ok := common.GetAnyAs[freeD.FreeDPosition](payload)
if !ok { if !ok {
return nil, errors.New("freed.decode processor only accepts a FreeDEncode") return nil, errors.New("freed.decode processor only accepts a FreeDEncode")
@@ -23,8 +24,8 @@ func (fde *FreeDEncode) Process(ctx context.Context, payload any) (any, error) {
return payloadBytes, nil return payloadBytes, nil
} }
func (fde *FreeDEncode) Type() string { func (fe *FreeDEncode) Type() string {
return fde.config.Type return fe.config.Type
} }
func init() { func init() {

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
@@ -16,7 +17,7 @@ type IntParse struct {
} }
func (ip *IntParse) Process(ctx context.Context, payload any) (any, error) { func (ip *IntParse) Process(ctx context.Context, payload any) (any, error) {
payloadString, ok := GetAnyAs[string](payload) payloadString, ok := common.GetAnyAs[string](payload)
if !ok { if !ok {
return nil, errors.New("int.parse processor only accepts a string") return nil, errors.New("int.parse processor only accepts a string")

View File

@@ -0,0 +1,71 @@
package processor
import (
"context"
"errors"
"fmt"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
type IntScale struct {
OutMin int
OutMax int
InMin int
InMax int
config config.ProcessorConfig
}
func (ir *IntScale) Process(ctx context.Context, payload any) (any, error) {
payloadInt, ok := common.GetAnyAs[int](payload)
if !ok {
return nil, errors.New("int.scale can only process an int")
}
payloadInt = (payloadInt-ir.InMin)*(ir.OutMax-ir.OutMin)/(ir.InMax-ir.InMin) + ir.OutMin
return payloadInt, nil
}
func (ir *IntScale) Type() string {
return ir.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "int.scale",
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
inMinInt, err := params.GetInt("inMin")
if err != nil {
return nil, fmt.Errorf("int.scale inMin error: %w", err)
}
inMaxInt, err := params.GetInt("inMax")
if err != nil {
return nil, fmt.Errorf("int.scale inMax error: %w", err)
}
if inMaxInt < inMinInt {
return nil, errors.New("int.scale inMax must be greater than inMin")
}
outMinInt, err := params.GetInt("outMin")
if err != nil {
return nil, fmt.Errorf("int.scale outMin error: %w", err)
}
outMaxInt, err := params.GetInt("outMax")
if err != nil {
return nil, fmt.Errorf("int.scale outMax error: %w", err)
}
if outMaxInt < outMinInt {
return nil, errors.New("int.scale outMax must be greater than outMin")
}
return &IntScale{config: config, InMin: inMinInt, InMax: inMaxInt, OutMin: outMinInt, OutMax: outMaxInt}, nil
},
})
}

View File

@@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
@@ -13,15 +14,20 @@ type JsonDecode struct {
} }
func (jd *JsonDecode) Process(ctx context.Context, payload any) (any, error) { func (jd *JsonDecode) Process(ctx context.Context, payload any) (any, error) {
payloadString, ok := GetAnyAs[string](payload)
payloadBytes, ok := common.GetAnyAsByteSlice(payload)
if !ok { if !ok {
return nil, errors.New("json.decode processor only accepts a string") payloadString, ok := common.GetAnyAs[string](payload)
if !ok {
return nil, errors.New("json.decode can only process a string or []byte")
}
payloadBytes = []byte(payloadString)
} }
payloadJson := make(map[string]any) payloadJson := make(map[string]any)
err := json.Unmarshal([]byte(payloadString), &payloadJson) err := json.Unmarshal(payloadBytes, &payloadJson)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@@ -6,6 +6,7 @@ import (
"context" "context"
"errors" "errors"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"gitlab.com/gomidi/midi/v2" "gitlab.com/gomidi/midi/v2"
) )
@@ -15,7 +16,7 @@ type MIDIMessageDecode struct {
} }
func (mmd *MIDIMessageDecode) Process(ctx context.Context, payload any) (any, error) { func (mmd *MIDIMessageDecode) Process(ctx context.Context, payload any) (any, error) {
payloadBytes, ok := GetAnyAs[[]byte](payload) payloadBytes, ok := common.GetAnyAs[[]byte](payload)
if !ok { if !ok {
return nil, errors.New("midi.message.decode processor only accepts a []byte") return nil, errors.New("midi.message.decode processor only accepts a []byte")

View File

@@ -6,6 +6,7 @@ import (
"context" "context"
"errors" "errors"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"gitlab.com/gomidi/midi/v2" "gitlab.com/gomidi/midi/v2"
) )
@@ -15,7 +16,7 @@ type MIDIMessageEncode struct {
} }
func (mme *MIDIMessageEncode) Process(ctx context.Context, payload any) (any, error) { func (mme *MIDIMessageEncode) Process(ctx context.Context, payload any) (any, error) {
payloadMessage, ok := GetAnyAs[midi.Message](payload) payloadMessage, ok := common.GetAnyAs[midi.Message](payload)
if !ok { if !ok {
return nil, errors.New("midi.message.encode processor only accepts a midi.Message") return nil, errors.New("midi.message.encode processor only accepts a midi.Message")

View File

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

View File

@@ -7,6 +7,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"gitlab.com/gomidi/midi/v2" "gitlab.com/gomidi/midi/v2"
) )
@@ -45,7 +46,7 @@ type MIDIPitchBend struct {
} }
func (mmu *MIDIMessageUnpack) Process(ctx context.Context, payload any) (any, error) { func (mmu *MIDIMessageUnpack) Process(ctx context.Context, payload any) (any, error) {
payloadMidi, ok := GetAnyAs[midi.Message](payload) payloadMidi, ok := common.GetAnyAs[midi.Message](payload)
if !ok { if !ok {
return nil, errors.New("midi.message.unpack processor only accepts a midi.Message") return nil, errors.New("midi.message.unpack processor only accepts a midi.Message")

View File

@@ -5,6 +5,7 @@ import (
"errors" "errors"
mqtt "github.com/eclipse/paho.mqtt.golang" mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
@@ -13,7 +14,7 @@ type MQTTMessageEncode struct {
} }
func (mme *MQTTMessageEncode) Process(ctx context.Context, payload any) (any, error) { func (mme *MQTTMessageEncode) Process(ctx context.Context, payload any) (any, error) {
payloadMessage, ok := GetAnyAs[mqtt.Message](payload) payloadMessage, ok := common.GetAnyAs[mqtt.Message](payload)
if !ok { if !ok {
return nil, errors.New("mqtt.message.encode processor only accepts an mqtt.Message") return nil, errors.New("mqtt.message.encode processor only accepts an mqtt.Message")

View File

@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
osc "github.com/jwetzell/osc-go" osc "github.com/jwetzell/osc-go"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
@@ -14,7 +15,7 @@ type OSCMessageDecode struct {
} }
func (omd *OSCMessageDecode) Process(ctx context.Context, payload any) (any, error) { func (omd *OSCMessageDecode) Process(ctx context.Context, payload any) (any, error) {
payloadBytes, ok := GetAnyAs[[]byte](payload) payloadBytes, ok := common.GetAnyAs[[]byte](payload)
if !ok { if !ok {
return nil, errors.New("osc.message.decode processor only accepts a []byte payload") return nil, errors.New("osc.message.decode processor only accepts a []byte payload")

View File

@@ -5,6 +5,7 @@ import (
"errors" "errors"
osc "github.com/jwetzell/osc-go" osc "github.com/jwetzell/osc-go"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
@@ -13,7 +14,7 @@ type OSCMessageEncode struct {
} }
func (ome *OSCMessageEncode) Process(ctx context.Context, payload any) (any, error) { func (ome *OSCMessageEncode) Process(ctx context.Context, payload any) (any, error) {
payloadMessage, ok := GetAnyAs[*osc.OSCMessage](payload) payloadMessage, ok := common.GetAnyAs[*osc.OSCMessage](payload)
if !ok { if !ok {
return nil, errors.New("osc.message.encode processor only accepts an *OSCMessage") return nil, errors.New("osc.message.encode processor only accepts an *OSCMessage")

View File

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

View File

@@ -42,14 +42,15 @@ var (
ProcessorRegistry = make(map[string]ProcessorRegistration) ProcessorRegistry = make(map[string]ProcessorRegistration)
) )
func GetAnyAs[T any](p any) (T, bool) {
typed, ok := p.(T)
return typed, ok
}
type TemplateData struct { type TemplateData struct {
Payload any Payload any
Modules any Modules any
Sender any
}
type EnvData struct {
Payload any
Sender any
} }
func GetTemplateData(ctx context.Context, payload any) TemplateData { func GetTemplateData(ctx context.Context, payload any) TemplateData {
@@ -58,5 +59,20 @@ func GetTemplateData(ctx context.Context, payload any) TemplateData {
if modules != nil { if modules != nil {
templateData.Modules = modules templateData.Modules = modules
} }
sender := ctx.Value(common.SenderContextKey)
if sender != nil {
templateData.Sender = sender
}
return templateData return templateData
} }
func GetEnvData(ctx context.Context, payload any) EnvData {
envData := EnvData{Payload: payload}
sender := ctx.Value(common.SenderContextKey)
if sender != nil {
envData.Sender = sender
}
return envData
}

View File

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

View File

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

View File

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

View File

@@ -5,38 +5,45 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"modernc.org/quickjs" "modernc.org/quickjs"
) )
type ScriptJS struct { type ScriptJS struct {
config config.ProcessorConfig config config.ProcessorConfig
Program string vm *quickjs.VM
payloadAtom quickjs.Atom
senderAtom quickjs.Atom
Program string
} }
func (sj *ScriptJS) Process(ctx context.Context, payload any) (any, error) { func (sj *ScriptJS) Process(ctx context.Context, payload any) (any, error) {
vm, err := quickjs.NewVM() //NOTE(jwetzell): some weird conversion going on with these types
_, isUint8Slice := common.GetAnyAs[[]uint8](payload)
_, isbyteSlice := common.GetAnyAs[[]byte](payload)
if err != nil { if isUint8Slice || isbyteSlice {
return nil, err intSlice, ok := common.GetAnyAsIntSlice(payload)
if ok {
payload = intSlice
}
} }
defer vm.Close()
payloadAtom, err := vm.NewAtom("payload") sj.vm.SetProperty(sj.vm.GlobalObject(), sj.payloadAtom, payload)
sender := ctx.Value(common.SenderContextKey)
sj.vm.SetProperty(sj.vm.GlobalObject(), sj.senderAtom, sender)
_, err := sj.vm.Eval(sj.Program, quickjs.EvalGlobal)
if err != nil { if err != nil {
return nil, err return nil, err
} }
vm.SetProperty(vm.GlobalObject(), payloadAtom, payload) output, err := sj.vm.GetProperty(sj.vm.GlobalObject(), sj.payloadAtom)
_, err = vm.Eval(sj.Program, quickjs.EvalGlobal)
if err != nil {
return nil, err
}
output, err := vm.GetProperty(vm.GlobalObject(), payloadAtom)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -54,6 +61,7 @@ func (sj *ScriptJS) Process(ctx context.Context, payload any) (any, error) {
if ok { if ok {
var outputMap map[string]interface{} var outputMap map[string]interface{}
fmt.Println(outputObject.String())
err := json.Unmarshal([]byte(outputObject.String()), &outputMap) err := json.Unmarshal([]byte(outputObject.String()), &outputMap)
return outputMap, err return outputMap, err
} }
@@ -76,7 +84,23 @@ func init() {
return nil, fmt.Errorf("script.js program error: %w", err) return nil, fmt.Errorf("script.js program error: %w", err)
} }
return &ScriptJS{config: config, Program: programString}, nil vm, err := quickjs.NewVM()
if err != nil {
return nil, err
}
payloadAtom, err := vm.NewAtom("payload")
if err != nil {
return nil, err
}
senderAtom, err := vm.NewAtom("sender")
if err != nil {
return nil, err
}
return &ScriptJS{config: config, Program: programString, vm: vm, payloadAtom: payloadAtom, senderAtom: senderAtom}, nil
}, },
}) })
} }

View File

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

View File

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

View File

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

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
@@ -12,7 +13,7 @@ type StringDecode struct {
} }
func (sd *StringDecode) Process(ctx context.Context, payload any) (any, error) { func (sd *StringDecode) Process(ctx context.Context, payload any) (any, error) {
payloadBytes, ok := GetAnyAs[[]byte](payload) payloadBytes, ok := common.GetAnyAs[[]byte](payload)
if !ok { if !ok {
return nil, errors.New("string.decode processor only accepts a []byte") return nil, errors.New("string.decode processor only accepts a []byte")

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
@@ -12,7 +13,7 @@ type StringEncode struct {
} }
func (se *StringEncode) Process(ctx context.Context, payload any) (any, error) { func (se *StringEncode) Process(ctx context.Context, payload any) (any, error) {
payloadString, ok := GetAnyAs[string](payload) payloadString, ok := common.GetAnyAs[string](payload)
if !ok { if !ok {
return nil, errors.New("string.encode processor only accepts a string") return nil, errors.New("string.encode processor only accepts a string")

View File

@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
@@ -15,7 +16,7 @@ type StringSplit struct {
} }
func (ss *StringSplit) Process(ctx context.Context, payload any) (any, error) { func (ss *StringSplit) Process(ctx context.Context, payload any) (any, error) {
payloadString, ok := GetAnyAs[string](payload) payloadString, ok := common.GetAnyAs[string](payload)
if !ok { if !ok {
return nil, errors.New("string.split only accepts a string") return nil, errors.New("string.split only accepts a string")

View File

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

View File

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

View File

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

View File

@@ -9,23 +9,23 @@ import (
) )
func TestStringFilterFromRegistry(t *testing.T) { func TestStringFilterFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["string.filter"] registration, ok := processor.ProcessorRegistry["filter.regex"]
if !ok { if !ok {
t.Fatalf("string.filter processor not registered") t.Fatalf("filter.regex processor not registered")
} }
processorInstance, err := registration.New(config.ProcessorConfig{ processorInstance, err := registration.New(config.ProcessorConfig{
Type: "string.filter", Type: "filter.regex",
Params: map[string]any{ Params: map[string]any{
"pattern": "hello", "pattern": "hello",
}, },
}) })
if err != nil { if err != nil {
t.Fatalf("failed to create string.filter processor: %s", err) t.Fatalf("failed to create filter.regex processor: %s", err)
} }
if processorInstance.Type() != "string.filter" { if processorInstance.Type() != "filter.regex" {
t.Fatalf("string.filter processor has wrong type: %s", processorInstance.Type()) t.Fatalf("filter.regex processor has wrong type: %s", processorInstance.Type())
} }
payload := "hello" payload := "hello"
@@ -33,17 +33,17 @@ func TestStringFilterFromRegistry(t *testing.T) {
got, err := processorInstance.Process(t.Context(), payload) got, err := processorInstance.Process(t.Context(), payload)
if err != nil { if err != nil {
t.Fatalf("string.filter processing failed: %s", err) t.Fatalf("filter.regex processing failed: %s", err)
} }
gotString, ok := got.(string) gotString, ok := got.(string)
if !ok { if !ok {
t.Fatalf("string.filter should return byte slice") t.Fatalf("filter.regex should return byte slice")
} }
if gotString != expected { if gotString != expected {
t.Fatalf("string.filter got %+v, expected %+v", got, expected) t.Fatalf("filter.regex got %+v, expected %+v", got, expected)
} }
} }
@@ -76,40 +76,40 @@ func TestGoodStringFilter(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
registration, ok := processor.ProcessorRegistry["string.filter"] registration, ok := processor.ProcessorRegistry["filter.regex"]
if !ok { if !ok {
t.Fatalf("string.filter processor not registered") t.Fatalf("filter.regex processor not registered")
} }
processorInstance, err := registration.New(config.ProcessorConfig{ processorInstance, err := registration.New(config.ProcessorConfig{
Type: "string.filter", Type: "filter.regex",
Params: test.params, Params: test.params,
}) })
if err != nil { if err != nil {
t.Fatalf("string.filter failed to create processor: %s", err) t.Fatalf("filter.regex failed to create processor: %s", err)
} }
got, err := processorInstance.Process(t.Context(), test.payload) got, err := processorInstance.Process(t.Context(), test.payload)
if err != nil { if err != nil {
t.Fatalf("string.filter processing failed: %s", err) t.Fatalf("filter.regex processing failed: %s", err)
} }
if test.expected == nil { if test.expected == nil {
if got != nil { if got != nil {
t.Fatalf("string.filter got %+v, expected nil", got) t.Fatalf("filter.regex got %+v, expected nil", got)
} }
return return
} }
gotString, ok := got.(string) gotString, ok := got.(string)
if !ok { if !ok {
t.Fatalf("string.filter returned a %T payload: %s", got, got) t.Fatalf("filter.regex returned a %T payload: %s", got, got)
} }
if !reflect.DeepEqual(gotString, test.expected) { if !reflect.DeepEqual(gotString, test.expected) {
t.Fatalf("string.filter got %+v, expected %+v", gotString, test.expected) t.Fatalf("filter.regex got %+v, expected %+v", gotString, test.expected)
} }
}) })
} }
@@ -126,7 +126,7 @@ func TestBadStringFilter(t *testing.T) {
name: "no pattern param", name: "no pattern param",
payload: "hello", payload: "hello",
params: map[string]any{}, params: map[string]any{},
errorString: "string.filter pattern error: not found", errorString: "filter.regex pattern error: not found",
}, },
{ {
name: "non-string input", name: "non-string input",
@@ -134,7 +134,7 @@ func TestBadStringFilter(t *testing.T) {
params: map[string]any{ params: map[string]any{
"pattern": "hello", "pattern": "hello",
}, },
errorString: "string.filter processor only accepts a string", errorString: "filter.regex processor only accepts a string",
}, },
{ {
name: "non-string pattern param", name: "non-string pattern param",
@@ -142,7 +142,7 @@ func TestBadStringFilter(t *testing.T) {
params: map[string]any{ params: map[string]any{
"pattern": 123, "pattern": 123,
}, },
errorString: "string.filter pattern error: not a string", errorString: "filter.regex pattern error: not a string",
}, },
{ {
name: "invalid regex pattern", name: "invalid regex pattern",
@@ -156,19 +156,19 @@ func TestBadStringFilter(t *testing.T) {
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
registration, ok := processor.ProcessorRegistry["string.filter"] registration, ok := processor.ProcessorRegistry["filter.regex"]
if !ok { if !ok {
t.Fatalf("string.filter processor not registered") t.Fatalf("filter.regex processor not registered")
} }
processorInstance, err := registration.New(config.ProcessorConfig{ processorInstance, err := registration.New(config.ProcessorConfig{
Type: "string.filter", Type: "filter.regex",
Params: test.params, Params: test.params,
}) })
if err != nil { if err != nil {
if test.errorString != err.Error() { if test.errorString != err.Error() {
t.Fatalf("string.filter got error '%s', expected '%s'", err.Error(), test.errorString) t.Fatalf("filter.regex got error '%s', expected '%s'", err.Error(), test.errorString)
} }
return return
} }
@@ -176,11 +176,11 @@ func TestBadStringFilter(t *testing.T) {
got, err := processorInstance.Process(t.Context(), test.payload) got, err := processorInstance.Process(t.Context(), test.payload)
if err == nil { if err == nil {
t.Fatalf("string.filter expected to fail but got payload: %s", got) t.Fatalf("filter.regex expected to fail but got payload: %s", got)
} }
if err.Error() != test.errorString { if err.Error() != test.errorString {
t.Fatalf("string.filter got error '%s', expected '%s'", err.Error(), test.errorString) t.Fatalf("filter.regex got error '%s', expected '%s'", err.Error(), test.errorString)
} }
}) })
} }

View File

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

View File

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

View File

@@ -0,0 +1,170 @@
package processor_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
)
func TestIntScaleFromRegistry(t *testing.T) {
registration, ok := processor.ProcessorRegistry["int.scale"]
if !ok {
t.Fatalf("int.scale processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "int.scale",
Params: map[string]any{
"inMin": 0,
"inMax": 10,
"outMin": 0,
"outMax": 127,
},
})
if err != nil {
t.Fatalf("failed to create int.scale processor: %s", err)
}
if processorInstance.Type() != "int.scale" {
t.Fatalf("int.scale processor has wrong type: %s", processorInstance.Type())
}
}
func TestGoodIntScale(t *testing.T) {
tests := []struct {
name string
payload any
params map[string]any
expected int
}{
{
name: "0-10 -> 0-127",
params: map[string]any{
"inMin": 0,
"inMax": 10,
"outMin": 0,
"outMax": 127,
},
payload: 5,
expected: 63,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := processor.ProcessorRegistry["int.scale"]
if !ok {
t.Fatalf("int.scale processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "int.scale",
Params: test.params,
})
if err != nil {
t.Fatalf("int.scale failed to create processor: %s", err)
}
got, err := processorInstance.Process(t.Context(), test.payload)
if err != nil {
t.Fatalf("int.scale processing failed: %s", err)
}
gotInt, ok := got.(int)
if !ok {
t.Fatalf("int.scale returned a %T payload: %s", got, got)
}
if gotInt != test.expected {
t.Fatalf("int.scale got %d, expected %d", gotInt, test.expected)
}
})
}
}
func TestBadIntScale(t *testing.T) {
tests := []struct {
name string
params map[string]any
payload any
errorString string
}{
{
name: "no inMin param",
payload: "hello",
params: map[string]any{"inMax": 10, "outMin": 0, "outMax": 127},
errorString: "int.scale inMin error: not found",
},
{
name: "no inMax param",
payload: "hello",
params: map[string]any{"inMin": 0, "outMin": 0, "outMax": 127},
errorString: "int.scale inMax error: not found",
},
{
name: "no outMin param",
payload: "hello",
params: map[string]any{"inMin": 0, "inMax": 10, "outMax": 127},
errorString: "int.scale outMin error: not found",
},
{
name: "no outMax param",
payload: "hello",
params: map[string]any{"inMin": 0, "inMax": 10, "outMin": 0},
errorString: "int.scale outMax error: not found",
},
{
name: "inMin param not a number",
payload: "hello",
params: map[string]any{"inMin": "0", "max": 10, "outMin": 0, "outMax": 127},
errorString: "int.scale inMin error: not a number",
},
{
name: "inMax param not a number",
payload: "hello",
params: map[string]any{"inMin": 0, "inMax": "10", "outMin": 0, "outMax": 127},
errorString: "int.scale inMax error: not a number",
},
{
name: "inMax less than inMin",
payload: "hello",
params: map[string]any{"inMin": 10, "inMax": 0, "outMin": 0, "outMax": 127},
errorString: "int.scale inMax must be greater than inMin",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := processor.ProcessorRegistry["int.scale"]
if !ok {
t.Fatalf("int.scale processor not registered")
}
processorInstance, err := registration.New(config.ProcessorConfig{
Type: "int.scale",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("int.scale got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
got, err := processorInstance.Process(t.Context(), test.payload)
if err == nil {
t.Fatalf("int.scale expected to fail but got payload: %s", got)
}
if err.Error() != test.errorString {
t.Fatalf("int.scale got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

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

View File

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

View File

@@ -204,7 +204,7 @@ func TestBadMQTTMessageCreate(t *testing.T) {
"payload": 123, "payload": 123,
}, },
payload: 1, payload: 1,
errorString: "mqtt.message.create payload error: not a slice", errorString: "mqtt.message.create payload error: not a byte slice",
}, },
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,27 +13,13 @@ import (
) )
type RouteError struct { type RouteError struct {
Index int Index int `json:"index"`
Config config.RouteConfig Config config.RouteConfig `json:"config"`
Error error Error string `json:"error"`
} }
type RouteIOError struct {
Index int
OutputError error
ProcessError error
InputError error
}
type RouteIO interface {
HandleInput(ctx context.Context, sourceId string, payload any) (bool, []RouteIOError)
HandleOutput(ctx context.Context, destinationId string, payload any) error
}
type Route struct { type Route struct {
input string input string
processors []processor.Processor processors []processor.Processor
output string
} }
func NewRoute(config config.RouteConfig) (*Route, error) { func NewRoute(config config.RouteConfig) (*Route, error) {
@@ -54,20 +40,16 @@ func NewRoute(config config.RouteConfig) (*Route, error) {
} }
} }
return &Route{input: config.Input, processors: processors, output: config.Output}, nil return &Route{input: config.Input, processors: processors}, nil
} }
func (r *Route) Input() string { func (r *Route) Input() string {
return r.input return r.input
} }
func (r *Route) Output() string {
return r.output
}
func (r *Route) ProcessPayload(ctx context.Context, payload any) (any, error) { func (r *Route) ProcessPayload(ctx context.Context, payload any) (any, error) {
tracer := otel.Tracer("route") tracer := otel.Tracer("route")
processCtx, processSpan := tracer.Start(ctx, "ProcessPayload") processCtx, processSpan := tracer.Start(ctx, "ProcessPayload", trace.WithAttributes(attribute.String("payload.type", fmt.Sprintf("%T", payload))))
defer processSpan.End() defer processSpan.End()
for processorIndex, processor := range r.processors { for processorIndex, processor := range r.processors {
processorCtx, processorSpan := otel.Tracer("processor").Start(processCtx, "process", trace.WithAttributes(attribute.Int("processor.index", processorIndex), attribute.String("processor.type", processor.Type()))) processorCtx, processorSpan := otel.Tracer("processor").Start(processCtx, "process", trace.WithAttributes(attribute.Int("processor.index", processorIndex), attribute.String("processor.type", processor.Type())))

View File

@@ -7,14 +7,12 @@ import (
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/processor"
"github.com/jwetzell/showbridge-go/internal/route" "github.com/jwetzell/showbridge-go/internal/route"
) )
func TestRouteCreate(t *testing.T) { func TestRouteCreate(t *testing.T) {
routeConfig := config.RouteConfig{ routeConfig := config.RouteConfig{
Input: "input", Input: "input",
Output: "output",
} }
testRoute, err := route.NewRoute(routeConfig) testRoute, err := route.NewRoute(routeConfig)
@@ -25,15 +23,12 @@ func TestRouteCreate(t *testing.T) {
if testRoute.Input() != routeConfig.Input { if testRoute.Input() != routeConfig.Input {
t.Fatalf("route input does not match expected input") t.Fatalf("route input does not match expected input")
} }
if testRoute.Output() != routeConfig.Output {
t.Fatalf("route output does not match expected output")
}
} }
type MockRouter struct{} type MockRouter struct{}
func (mr *MockRouter) HandleInput(ctx context.Context, sourceId string, payload any) (bool, []route.RouteIOError) { func (mr *MockRouter) HandleInput(ctx context.Context, sourceId string, payload any) (bool, []common.RouteIOError) {
return false, []route.RouteIOError{} return false, []common.RouteIOError{}
} }
func (mr *MockRouter) HandleOutput(ctx context.Context, destinationId string, payload any) error { func (mr *MockRouter) HandleOutput(ctx context.Context, destinationId string, payload any) error {
@@ -45,8 +40,13 @@ func TestGoodRouteHandleInput(t *testing.T) {
Input: "input", Input: "input",
Processors: []config.ProcessorConfig{ Processors: []config.ProcessorConfig{
{Type: "string.encode"}, {Type: "string.encode"},
{
Type: "router.output",
Params: config.Params{
"module": "output",
},
},
}, },
Output: "output",
} }
testRoute, err := route.NewRoute(routeConfig) testRoute, err := route.NewRoute(routeConfig)
@@ -60,7 +60,7 @@ func TestGoodRouteHandleInput(t *testing.T) {
t.Fatalf("route ProcessPayload returned error: %v", err) t.Fatalf("route ProcessPayload returned error: %v", err)
} }
payloadBytes, ok := processor.GetAnyAs[[]byte](payload) payloadBytes, ok := common.GetAnyAs[[]byte](payload)
if !ok { if !ok {
t.Fatalf("payload should be []byte got %T", payload) t.Fatalf("payload should be []byte got %T", payload)
} }
@@ -75,8 +75,13 @@ func TestRouteHandleInputWithProcessorError(t *testing.T) {
Input: "input", Input: "input",
Processors: []config.ProcessorConfig{ Processors: []config.ProcessorConfig{
{Type: "string.create", Params: map[string]any{"template": "{{.invalid}}}"}}, {Type: "string.create", Params: map[string]any{"template": "{{.invalid}}}"}},
{
Type: "router.output",
Params: config.Params{
"module": "output",
},
},
}, },
Output: "output",
} }
testRoute, err := route.NewRoute(routeConfig) testRoute, err := route.NewRoute(routeConfig)
@@ -93,9 +98,15 @@ func TestRouteHandleInputWithProcessorError(t *testing.T) {
func TestRouteHandleNilPayload(t *testing.T) { func TestRouteHandleNilPayload(t *testing.T) {
routeConfig := config.RouteConfig{ routeConfig := config.RouteConfig{
Input: "input", Input: "input",
Processors: []config.ProcessorConfig{}, Processors: []config.ProcessorConfig{
Output: "output", {
Type: "router.output",
Params: config.Params{
"module": "output",
},
},
},
} }
testRoute, err := route.NewRoute(routeConfig) testRoute, err := route.NewRoute(routeConfig)
@@ -118,8 +129,13 @@ func TestRouteHandleNilPayloadFromProcessor(t *testing.T) {
Input: "input", Input: "input",
Processors: []config.ProcessorConfig{ Processors: []config.ProcessorConfig{
{Type: "script.js", Params: map[string]any{"program": "payload = undefined"}}, {Type: "script.js", Params: map[string]any{"program": "payload = undefined"}},
{
Type: "router.output",
Params: config.Params{
"module": "output",
},
},
}, },
Output: "output",
} }
testRoute, err := route.NewRoute(routeConfig) testRoute, err := route.NewRoute(routeConfig)
@@ -143,7 +159,6 @@ func TestRouteUnknownProcessor(t *testing.T) {
Processors: []config.ProcessorConfig{ Processors: []config.ProcessorConfig{
{Type: "asdfasdflkjalkj"}, {Type: "asdfasdflkjalkj"},
}, },
Output: "output",
} }
_, err := route.NewRoute(routeConfig) _, err := route.NewRoute(routeConfig)
@@ -158,7 +173,6 @@ func TestRouteBadProcessorConfig(t *testing.T) {
Processors: []config.ProcessorConfig{ Processors: []config.ProcessorConfig{
{Type: "string.create", Params: map[string]any{}}, {Type: "string.create", Params: map[string]any{}},
}, },
Output: "output",
} }
_, err := route.NewRoute(routeConfig) _, err := route.NewRoute(routeConfig)

191
router.go
View File

@@ -4,8 +4,11 @@ import (
"context" "context"
"errors" "errors"
"log/slog" "log/slog"
"net/http"
"reflect"
"sync" "sync"
"github.com/gorilla/websocket"
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module" "github.com/jwetzell/showbridge-go/internal/module"
@@ -17,15 +20,24 @@ import (
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
) )
// TODO(jwetzell): can/should this be split into different components?
type Router struct { type Router struct {
contextCancel context.CancelFunc contextCancel context.CancelFunc
Context context.Context Context context.Context
// TODO(jwetzell): do these need to be guarded against concurrency?
ModuleInstances map[string]module.Module ModuleInstances map[string]module.Module
// TODO(jwetzell): change to something easier to lookup // TODO(jwetzell): change to something easier to lookup
RouteInstances []*route.Route RouteInstances []*route.Route
moduleWait sync.WaitGroup ConfigChange chan config.Config
logger *slog.Logger moduleWait sync.WaitGroup
runningConfig config.Config logger *slog.Logger
runningConfig config.Config
runningConfigMu sync.RWMutex
wsConns []*websocket.Conn
wsConnsMu sync.Mutex
apiServer *http.Server
apiServerMu sync.Mutex
apiServerShutdown context.CancelFunc
} }
func (r *Router) addModule(moduleDecl config.ModuleConfig) error { func (r *Router) addModule(moduleDecl config.ModuleConfig) error {
@@ -102,19 +114,20 @@ func (r *Router) getModule(moduleId string) module.Module {
return moduleInstance return moduleInstance
} }
func NewRouter(config config.Config) (*Router, []module.ModuleError, []route.RouteError) { func NewRouter(routerConfig config.Config) (*Router, []module.ModuleError, []route.RouteError) {
router := Router{ router := Router{
ModuleInstances: make(map[string]module.Module), ModuleInstances: make(map[string]module.Module),
RouteInstances: []*route.Route{}, RouteInstances: []*route.Route{},
ConfigChange: make(chan config.Config, 1),
logger: slog.Default().With("component", "router"), logger: slog.Default().With("component", "router"),
runningConfig: config, runningConfig: routerConfig,
} }
router.logger.Debug("creating") router.logger.Debug("creating")
var moduleErrors []module.ModuleError var moduleErrors []module.ModuleError
for moduleIndex, moduleDecl := range config.Modules { for moduleIndex, moduleDecl := range routerConfig.Modules {
err := router.addModule(moduleDecl) err := router.addModule(moduleDecl)
if err != nil { if err != nil {
@@ -124,7 +137,7 @@ func NewRouter(config config.Config) (*Router, []module.ModuleError, []route.Rou
moduleErrors = append(moduleErrors, module.ModuleError{ moduleErrors = append(moduleErrors, module.ModuleError{
Index: moduleIndex, Index: moduleIndex,
Config: moduleDecl, Config: moduleDecl,
Error: err, Error: err.Error(),
}) })
continue continue
} }
@@ -132,7 +145,7 @@ func NewRouter(config config.Config) (*Router, []module.ModuleError, []route.Rou
} }
var routeErrors []route.RouteError var routeErrors []route.RouteError
for routeIndex, routeDecl := range config.Routes { for routeIndex, routeDecl := range routerConfig.Routes {
err := router.addRoute(routeDecl) err := router.addRoute(routeDecl)
if err != nil { if err != nil {
if routeErrors == nil { if routeErrors == nil {
@@ -141,7 +154,7 @@ func NewRouter(config config.Config) (*Router, []module.ModuleError, []route.Rou
routeErrors = append(routeErrors, route.RouteError{ routeErrors = append(routeErrors, route.RouteError{
Index: routeIndex, Index: routeIndex,
Config: routeDecl, Config: routeDecl,
Error: err, Error: err.Error(),
}) })
continue continue
} }
@@ -155,13 +168,11 @@ func (r *Router) Start(ctx context.Context) {
routerContext, cancel := context.WithCancel(ctx) routerContext, cancel := context.WithCancel(ctx)
r.Context = routerContext r.Context = routerContext
r.contextCancel = cancel r.contextCancel = cancel
contextWithRouter := context.WithValue(routerContext, common.RouterContextKey, r) r.startModules()
r.startAPIServer(r.runningConfig.Api)
for moduleId := range r.ModuleInstances {
// TODO(jwetzell): handle module run errors
r.startModule(contextWithRouter, moduleId)
}
<-r.Context.Done() <-r.Context.Done()
r.logger.Debug("shutting down api server")
r.stopAPIServer()
r.logger.Debug("waiting for modules to exit") r.logger.Debug("waiting for modules to exit")
r.moduleWait.Wait() r.moduleWait.Wait()
r.logger.Info("done") r.logger.Info("done")
@@ -172,12 +183,22 @@ func (r *Router) Stop() {
r.contextCancel() r.contextCancel()
} }
func (r *Router) HandleInput(ctx context.Context, sourceId string, payload any) (bool, []route.RouteIOError) { func (r *Router) HandleInput(ctx context.Context, sourceId string, payload any) (bool, []common.RouteIOError) {
spanCtx, span := otel.Tracer("router").Start(ctx, "input", trace.WithAttributes(attribute.String("source.id", sourceId)), trace.WithNewRoot()) r.runningConfigMu.RLock()
defer r.runningConfigMu.RUnlock()
spanCtx, span := otel.Tracer("router").Start(ctx, "input", trace.WithAttributes(attribute.String("source.id", sourceId)))
defer span.End() defer span.End()
var routeIOErrors []route.RouteIOError var routeIOErrors []common.RouteIOError
routeFound := false routeFound := false
r.broadcastEvent(Event{
Type: "input",
Data: map[string]any{
"source": sourceId,
},
})
var routeWaitGroup sync.WaitGroup var routeWaitGroup sync.WaitGroup
for routeIndex, routeInstance := range r.RouteInstances { for routeIndex, routeInstance := range r.RouteInstances {
@@ -190,37 +211,35 @@ func (r *Router) HandleInput(ctx context.Context, sourceId string, payload any)
routeFound = true routeFound = true
routeContext := context.WithValue(spanCtx, common.SourceContextKey, sourceId) routeContext := context.WithValue(spanCtx, common.SourceContextKey, sourceId)
routeContext = context.WithValue(routeContext, common.RouterContextKey, r)
routeContext = context.WithValue(routeContext, common.ModulesContextKey, r.ModuleInstances) routeContext = context.WithValue(routeContext, common.ModulesContextKey, r.ModuleInstances)
routeCtx, routeSpan := otel.Tracer("router").Start(routeContext, "route", trace.WithAttributes(attribute.Int("route.index", routeIndex), attribute.String("route.input", routeInstance.Input()), attribute.String("route.output", routeInstance.Output()))) routeCtx, routeSpan := otel.Tracer("router").Start(routeContext, "route", trace.WithAttributes(attribute.Int("route.index", routeIndex), attribute.String("route.input", routeInstance.Input())))
payload, err := routeInstance.ProcessPayload(routeCtx, payload) _, err := routeInstance.ProcessPayload(routeCtx, payload)
if err != nil { if err != nil {
if routeIOErrors == nil { if routeIOErrors == nil {
routeIOErrors = []route.RouteIOError{} routeIOErrors = []common.RouteIOError{}
} }
r.logger.Error("unable to process input", "route", routeIndex, "source", sourceId, "error", err) r.logger.Error("unable to process input", "route", routeIndex, "source", sourceId, "error", err)
routeIOErrors = append(routeIOErrors, route.RouteIOError{ routeIOErrors = append(routeIOErrors, common.RouteIOError{
Index: routeIndex, Index: routeIndex,
ProcessError: err, ProcessError: err,
}) })
return r.broadcastEvent(Event{
} Type: "route",
Data: map[string]any{
if payload == nil { "index": routeIndex,
r.logger.Debug("no payload after processing, route terminated", "route", routeIndex, "source", sourceId) },
return Error: err.Error(),
}
outputError := r.HandleOutput(routeCtx, routeInstance.Output(), payload)
if outputError != nil {
if routeIOErrors == nil {
routeIOErrors = []route.RouteIOError{}
}
routeIOErrors = append(routeIOErrors, route.RouteIOError{
Index: routeIndex,
OutputError: outputError,
}) })
return
} }
r.broadcastEvent(Event{
Type: "route",
Data: map[string]any{
"index": routeIndex,
},
})
routeSpan.End() routeSpan.End()
}) })
} }
@@ -232,7 +251,12 @@ func (r *Router) HandleInput(ctx context.Context, sourceId string, payload any)
func (r *Router) HandleOutput(ctx context.Context, destinationId string, payload any) error { func (r *Router) HandleOutput(ctx context.Context, destinationId string, payload any) error {
spanCtx, span := otel.Tracer("router").Start(ctx, "output", trace.WithAttributes(attribute.String("destination.id", destinationId))) spanCtx, span := otel.Tracer("router").Start(ctx, "output", trace.WithAttributes(attribute.String("destination.id", destinationId)))
defer span.End() defer span.End()
outputEvent := Event{
Type: "output",
Data: map[string]any{
"destination": destinationId,
},
}
destinationModule := r.getModule(destinationId) destinationModule := r.getModule(destinationId)
if destinationModule == nil { if destinationModule == nil {
@@ -240,6 +264,8 @@ func (r *Router) HandleOutput(ctx context.Context, destinationId string, payload
span.SetStatus(codes.Error, err.Error()) span.SetStatus(codes.Error, err.Error())
span.RecordError(err) span.RecordError(err)
r.logger.Error("no module found for destination id", "destinationId", destinationId) r.logger.Error("no module found for destination id", "destinationId", destinationId)
outputEvent.Error = err.Error()
r.broadcastEvent(outputEvent)
return err return err
} }
@@ -250,14 +276,93 @@ func (r *Router) HandleOutput(ctx context.Context, destinationId string, payload
moduleOutputSpan.SetStatus(codes.Error, err.Error()) moduleOutputSpan.SetStatus(codes.Error, err.Error())
moduleOutputSpan.RecordError(err) moduleOutputSpan.RecordError(err)
r.logger.ErrorContext(moduleOutputCtx, "module output encountered error", "module", destinationModule.Id(), "error", err) r.logger.ErrorContext(moduleOutputCtx, "module output encountered error", "module", destinationModule.Id(), "error", err)
outputEvent.Error = err.Error()
r.broadcastEvent(outputEvent)
return err return err
} else { } else {
moduleOutputSpan.SetStatus(codes.Ok, "module output successful") moduleOutputSpan.SetStatus(codes.Ok, "module output successful")
} }
r.broadcastEvent(outputEvent)
return nil return nil
} }
func (r *Router) startModules() {
contextWithRouter := context.WithValue(r.Context, common.RouterContextKey, r)
for moduleId := range r.ModuleInstances {
// TODO(jwetzell): handle module run errors
err := r.startModule(contextWithRouter, moduleId)
if err != nil {
r.logger.Error("error starting module", "moduleId", moduleId, "error", err)
}
}
}
func (r *Router) RunningConfig() config.Config { func (r *Router) RunningConfig() config.Config {
r.runningConfigMu.RLock()
defer r.runningConfigMu.RLock()
return r.runningConfig return r.runningConfig
} }
func (r *Router) UpdateConfig(newConfig config.Config) ([]module.ModuleError, []route.RouteError) {
r.runningConfigMu.Lock()
defer r.runningConfigMu.Unlock()
oldConfig := r.runningConfig
r.logger.Debug("received config update", "oldConfig", oldConfig, "newConfig", newConfig)
if !reflect.DeepEqual(oldConfig.Api, newConfig.Api) {
r.logger.Info("applying new API config")
r.stopAPIServer()
r.startAPIServer(newConfig.Api)
r.runningConfig.Api = newConfig.Api
}
// TODO(jwetzell): handle config update errors better
for _, moduleInstance := range r.ModuleInstances {
moduleInstance.Stop()
}
r.logger.Debug("waiting for modules to exit")
r.moduleWait.Wait()
r.ModuleInstances = make(map[string]module.Module)
r.RouteInstances = []*route.Route{}
var moduleErrors []module.ModuleError
for moduleIndex, moduleDecl := range newConfig.Modules {
err := r.addModule(moduleDecl)
if err != nil {
if moduleErrors == nil {
moduleErrors = []module.ModuleError{}
}
moduleErrors = append(moduleErrors, module.ModuleError{
Index: moduleIndex,
Config: moduleDecl,
Error: err.Error(),
})
continue
}
}
var routeErrors []route.RouteError
for routeIndex, routeDecl := range newConfig.Routes {
err := r.addRoute(routeDecl)
if err != nil {
if routeErrors == nil {
routeErrors = []route.RouteError{}
}
routeErrors = append(routeErrors, route.RouteError{
Index: routeIndex,
Config: routeDecl,
Error: err.Error(),
})
continue
}
}
r.runningConfig = newConfig
r.startModules()
return moduleErrors, routeErrors
}

View File

@@ -12,14 +12,13 @@ import (
"github.com/jwetzell/showbridge-go/internal/common" "github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module" "github.com/jwetzell/showbridge-go/internal/module"
"github.com/jwetzell/showbridge-go/internal/route"
) )
type MockCounterModule struct { type MockCounterModule struct {
config config.ModuleConfig config config.ModuleConfig
ctx context.Context ctx context.Context
outputCount int outputCount int
router route.RouteIO router common.RouteIO
logger *slog.Logger logger *slog.Logger
cancel context.CancelFunc cancel context.CancelFunc
} }
@@ -34,7 +33,7 @@ func (mcm *MockCounterModule) Output(context.Context, any) error {
} }
func (mcm *MockCounterModule) Start(ctx context.Context) error { func (mcm *MockCounterModule) Start(ctx context.Context) error {
router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO) router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok { if !ok {
return fmt.Errorf("mock.counter could not get router from context") return fmt.Errorf("mock.counter could not get router from context")
@@ -150,8 +149,8 @@ func TestNewRouterDuplicateModuleId(t *testing.T) {
t.Fatalf("router should have returned exactly 1 module error, got: %d", len(moduleErrors)) t.Fatalf("router should have returned exactly 1 module error, got: %d", len(moduleErrors))
} }
if moduleErrors[0].Error.Error() != "module id already exists" { if moduleErrors[0].Error != "module id already exists" {
t.Fatalf("module error did not match expected, got: %s", moduleErrors[0].Error.Error()) t.Fatalf("module error did not match expected, got: %s", moduleErrors[0].Error)
} }
} }
@@ -171,7 +170,6 @@ func TestNewRouterRouteWithUnknwonProcessor(t *testing.T) {
Type: "asdfasdf", Type: "asdfasdf",
}, },
}, },
Output: "mock",
}, },
}, },
} }
@@ -186,8 +184,8 @@ func TestNewRouterRouteWithUnknwonProcessor(t *testing.T) {
t.Fatalf("router should have returned exactly 1 route error, got: %d", len(routeErrors)) t.Fatalf("router should have returned exactly 1 route error, got: %d", len(routeErrors))
} }
if routeErrors[0].Error.Error() != "problem loading processor registration for processor type: asdfasdf" { if routeErrors[0].Error != "problem loading processor registration for processor type: asdfasdf" {
t.Fatalf("route error did not match expected, got: %s", routeErrors[0].Error.Error()) t.Fatalf("route error did not match expected, got: %s", routeErrors[0].Error)
} }
} }
@@ -201,8 +199,15 @@ func TestRouterInputUnknownDestinationModule(t *testing.T) {
}, },
Routes: []config.RouteConfig{ Routes: []config.RouteConfig{
{ {
Input: "mock", Input: "mock",
Output: "test", Processors: []config.ProcessorConfig{
{
Type: "router.output",
Params: config.Params{
"module": "test",
},
},
},
}, },
}, },
} }
@@ -238,8 +243,8 @@ func TestRouterInputUnknownDestinationModule(t *testing.T) {
t.Fatalf("router should have returned exactly 1 routing error, got: %d", len(routingErrors)) t.Fatalf("router should have returned exactly 1 routing error, got: %d", len(routingErrors))
} }
if routingErrors[0].OutputError.Error() != "no module found for destination id" { if routingErrors[0].ProcessError.Error() != "router.output failed to send output: no module found for destination id" {
t.Fatalf("routing output error did not match expected, got: %s", routingErrors[0].OutputError.Error()) t.Fatalf("routing output error did not match expected, got: %s", routingErrors[0].ProcessError.Error())
} }
} }
@@ -253,8 +258,15 @@ func TestRouterInputNoMatchingRoute(t *testing.T) {
}, },
Routes: []config.RouteConfig{ Routes: []config.RouteConfig{
{ {
Input: "test", Input: "test",
Output: "mock", Processors: []config.ProcessorConfig{
{
Type: "router.output",
Params: config.Params{
"module": "mock",
},
},
},
}, },
}, },
} }
@@ -297,8 +309,15 @@ func TestRouterInputSingleRoute(t *testing.T) {
}, },
Routes: []config.RouteConfig{ Routes: []config.RouteConfig{
{ {
Input: "mock", Input: "mock",
Output: "mock", Processors: []config.ProcessorConfig{
{
Type: "router.output",
Params: config.Params{
"module": "mock",
},
},
},
}, },
}, },
} }
@@ -361,16 +380,37 @@ func TestRouterInputMultipleRoutes(t *testing.T) {
}, },
Routes: []config.RouteConfig{ Routes: []config.RouteConfig{
{ {
Input: "mock", Input: "mock",
Output: "mock", Processors: []config.ProcessorConfig{
{
Type: "router.output",
Params: config.Params{
"module": "mock",
},
},
},
}, },
{ {
Input: "mock", Input: "mock",
Output: "mock", Processors: []config.ProcessorConfig{
{
Type: "router.output",
Params: config.Params{
"module": "mock",
},
},
},
}, },
{ {
Input: "mock", Input: "mock",
Output: "mock", Processors: []config.ProcessorConfig{
{
Type: "router.output",
Params: config.Params{
"module": "mock",
},
},
},
}, },
}, },
} }
@@ -436,12 +476,26 @@ func TestRouterInputMultipleModules(t *testing.T) {
}, },
Routes: []config.RouteConfig{ Routes: []config.RouteConfig{
{ {
Input: "mock1", Input: "mock1",
Output: "mock1", Processors: []config.ProcessorConfig{
{
Type: "router.output",
Params: config.Params{
"module": "mock1",
},
},
},
}, },
{ {
Input: "mock2", Input: "mock2",
Output: "mock2", Processors: []config.ProcessorConfig{
{
Type: "router.output",
Params: config.Params{
"module": "mock2",
},
},
},
}, },
}, },
} }

View File

@@ -2,9 +2,19 @@
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://showbridge.io/config.schema.json", "$id": "https://showbridge.io/config.schema.json",
"title": "Config", "title": "Config",
"description": "showbridge config file", "description": "showbridge configuration",
"type": "object", "type": "object",
"properties": { "properties": {
"api": {
"type": "object",
"properties": {
"port": {
"type": "integer",
"description": "Port for the API server to listen on"
}
},
"required": ["port"]
},
"modules": { "modules": {
"$ref": "https://showbridge.io/modules.schema.json" "$ref": "https://showbridge.io/modules.schema.json"
}, },

View File

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

View File

@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema", "$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://showbridge.io/processors.schema.json", "$id": "https://showbridge.io/processors.schema.json",
"title": "Processors", "title": "Processors",
"description": "showbridge processors array", "description": "processor configurations",
"type": "array", "type": "array",
"items": { "items": {
"oneOf": [ "oneOf": [
@@ -30,28 +30,6 @@
"required": ["type"], "required": ["type"],
"additionalProperties": false "additionalProperties": false
}, },
{
"type": "object",
"title": "Filter ArtNet Packet",
"properties": {
"type": {
"type": "string",
"const": "artnet.packet.filter"
},
"params": {
"type": "object",
"properties": {
"opCode": {
"type": "integer"
}
},
"required": ["opCode"],
"additionalProperties": false
}
},
"required": ["type", "params"],
"additionalProperties": false
},
{ {
"type": "object", "type": "object",
"title": "Debug Log", "title": "Debug Log",
@@ -76,6 +54,7 @@
"type": "object", "type": "object",
"properties": { "properties": {
"expression": { "expression": {
"title": "Expression",
"type": "string" "type": "string"
} }
}, },
@@ -86,6 +65,29 @@
"required": ["type", "params"], "required": ["type", "params"],
"additionalProperties": false "additionalProperties": false
}, },
{
"type": "object",
"title": "Filter by Regex",
"properties": {
"type": {
"type": "string",
"const": "filter.regex"
},
"params": {
"type": "object",
"properties": {
"pattern": {
"title": "Pattern",
"type": "string"
}
},
"required": ["pattern"],
"additionalProperties": false
}
},
"required": ["type", "params"],
"additionalProperties": false
},
{ {
"type": "object", "type": "object",
"title": "Parse Float", "title": "Parse Float",
@@ -93,6 +95,18 @@
"type": { "type": {
"type": "string", "type": "string",
"const": "float.parse" "const": "float.parse"
},
"params": {
"type": "object",
"properties": {
"bitSize": {
"title": "Bit Size",
"type": "integer",
"enum": [32, 64],
"default": 64
}
},
"additionalProperties": false
} }
}, },
"required": ["type"], "required": ["type"],
@@ -110,30 +124,39 @@
"type": "object", "type": "object",
"properties": { "properties": {
"id": { "id": {
"title": "Camera ID",
"type": "string" "type": "string"
}, },
"pan": { "pan": {
"title": "Pan",
"type": "string" "type": "string"
}, },
"tilt": { "tilt": {
"title": "Tilt",
"type": "string" "type": "string"
}, },
"roll": { "roll": {
"title": "Roll",
"type": "string" "type": "string"
}, },
"posX": { "posX": {
"title": "Position X",
"type": "string" "type": "string"
}, },
"posY": { "posY": {
"title": "Position Y",
"type": "string" "type": "string"
}, },
"posZ": { "posZ": {
"title": "Position Z",
"type": "string" "type": "string"
}, },
"zoom": { "zoom": {
"title": "Zoom",
"type": "string" "type": "string"
}, },
"focus": { "focus": {
"title": "Focus",
"type": "string" "type": "string"
} }
}, },
@@ -180,20 +203,22 @@
}, },
{ {
"type": "object", "type": "object",
"title": "Create HTTP Request", "title": "Do HTTP Request",
"properties": { "properties": {
"type": { "type": {
"type": "string", "type": "string",
"const": "http.request.create" "const": "http.request.do"
}, },
"params": { "params": {
"type": "object", "type": "object",
"properties": { "properties": {
"method": { "method": {
"title": "HTTP Method",
"type": "string", "type": "string",
"enum": ["GET", "POST"] "enum": ["GET", "POST"]
}, },
"url": { "url": {
"title": "URL",
"type": "string" "type": "string"
} }
}, },
@@ -204,32 +229,6 @@
"required": ["type", "params"], "required": ["type", "params"],
"additionalProperties": false "additionalProperties": false
}, },
{
"type": "object",
"title": "Filter HTTP Request",
"properties": {
"type": {
"type": "string",
"const": "http.request.filter"
},
"params": {
"type": "object",
"properties": {
"path": {
"type": "string"
},
"method": {
"type": "string",
"enum": ["GET", "POST"]
}
},
"required": ["path"],
"additionalProperties": false
}
},
"required": ["type", "params"],
"additionalProperties": false
},
{ {
"type": "object", "type": "object",
"title": "Create HTTP Response", "title": "Create HTTP Response",
@@ -242,9 +241,11 @@
"type": "object", "type": "object",
"properties": { "properties": {
"status": { "status": {
"title": "Status Code",
"type": "integer" "type": "integer"
}, },
"body": { "body": {
"title": "Body",
"type": "string" "type": "string"
} }
}, },
@@ -262,6 +263,24 @@
"type": { "type": {
"type": "string", "type": "string",
"const": "int.parse" "const": "int.parse"
},
"params": {
"type": "object",
"properties": {
"base": {
"title": "Base",
"type": "integer",
"enum": [0, 2, 8, 10, 16],
"default": 10
},
"bitSize": {
"title": "Bit Size",
"type": "integer",
"enum": [0, 8, 16, 32, 64],
"default": 64
}
},
"additionalProperties": false
} }
}, },
"required": ["type"], "required": ["type"],
@@ -279,9 +298,11 @@
"type": "object", "type": "object",
"properties": { "properties": {
"min": { "min": {
"title": "Minimum",
"type": "integer" "type": "integer"
}, },
"max": { "max": {
"title": "Maximum",
"type": "integer" "type": "integer"
} }
}, },
@@ -292,6 +313,41 @@
"required": ["type", "params"], "required": ["type", "params"],
"additionalProperties": false "additionalProperties": false
}, },
{
"type": "object",
"title": "Scale Int",
"properties": {
"type": {
"type": "string",
"const": "int.scale"
},
"params": {
"type": "object",
"properties": {
"inMin": {
"title": "Input Minimum",
"type": "integer"
},
"inMax": {
"title": "Input Maximum",
"type": "integer"
},
"outMin": {
"title": "Output Minimum",
"type": "integer"
},
"outMax": {
"title": "Output Maximum",
"type": "integer"
}
},
"required": ["inMin", "inMax", "outMin", "outMax"],
"additionalProperties": false
}
},
"required": ["type", "params"],
"additionalProperties": false
},
{ {
"type": "object", "type": "object",
"title": "Decode JSON", "title": "Decode JSON",
@@ -331,16 +387,20 @@
"type": "object", "type": "object",
"properties": { "properties": {
"type": { "type": {
"title": "MIDI Message Type",
"type": "string", "type": "string",
"enum": ["NoteOn", "noteon", "note_on"] "enum": ["NoteOn", "noteon", "note_on"]
}, },
"channel": { "channel": {
"title": "Channel",
"type": "string" "type": "string"
}, },
"note": { "note": {
"title": "Note",
"type": "string" "type": "string"
}, },
"velocity": { "velocity": {
"title": "Velocity",
"type": "string" "type": "string"
} }
}, },
@@ -351,16 +411,20 @@
"type": "object", "type": "object",
"properties": { "properties": {
"type": { "type": {
"title": "MIDI Message Type",
"type": "string", "type": "string",
"enum": ["NoteOff", "noteoff", "note_off"] "enum": ["NoteOff", "noteoff", "note_off"]
}, },
"channel": { "channel": {
"title": "Channel",
"type": "string" "type": "string"
}, },
"note": { "note": {
"title": "Note",
"type": "string" "type": "string"
}, },
"velocity": { "velocity": {
"title": "Velocity",
"type": "string" "type": "string"
} }
}, },
@@ -371,16 +435,20 @@
"type": "object", "type": "object",
"properties": { "properties": {
"type": { "type": {
"title": "MIDI Message Type",
"type": "string", "type": "string",
"enum": ["ControlChange", "controlchange", "control_change"] "enum": ["ControlChange", "controlchange", "control_change"]
}, },
"channel": { "channel": {
"title": "Channel",
"type": "string" "type": "string"
}, },
"control": { "control": {
"title": "Control",
"type": "string" "type": "string"
}, },
"value": { "value": {
"title": "Value",
"type": "string" "type": "string"
} }
}, },
@@ -391,13 +459,16 @@
"type": "object", "type": "object",
"properties": { "properties": {
"type": { "type": {
"title": "MIDI Message Type",
"type": "string", "type": "string",
"enum": ["ProgramChange", "programchange", "program_change"] "enum": ["ProgramChange", "programchange", "program_change"]
}, },
"channel": { "channel": {
"title": "Channel",
"type": "string" "type": "string"
}, },
"program": { "program": {
"title": "Program",
"type": "string" "type": "string"
} }
}, },
@@ -434,29 +505,6 @@
"required": ["type"], "required": ["type"],
"additionalProperties": false "additionalProperties": false
}, },
{
"type": "object",
"title": "Filter MIDI Message",
"properties": {
"type": {
"type": "string",
"const": "midi.message.filter"
},
"params": {
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": ["NoteOn", "NoteOff", "ControlChange", "ProgramChange"]
}
},
"required": ["type"],
"additionalProperties": false
}
},
"required": ["type", "params"],
"additionalProperties": false
},
{ {
"type": "object", "type": "object",
"title": "Unpack MIDI Message", "title": "Unpack MIDI Message",
@@ -481,15 +529,19 @@
"type": "object", "type": "object",
"properties": { "properties": {
"topic": { "topic": {
"title": "Topic",
"type": "string" "type": "string"
}, },
"qos": { "qos": {
"title": "QoS",
"type": "number" "type": "number"
}, },
"retained": { "retained": {
"title": "Retained",
"type": "boolean" "type": "boolean"
}, },
"payload": { "payload": {
"title": "Payload",
"type": "string" "type": "string"
} }
}, },
@@ -524,9 +576,11 @@
"type": "object", "type": "object",
"properties": { "properties": {
"subject": { "subject": {
"title": "Subject",
"type": "string" "type": "string"
}, },
"payload": { "payload": {
"title": "Payload",
"type": "string" "type": "string"
} }
}, },
@@ -549,15 +603,18 @@
"type": "object", "type": "object",
"properties": { "properties": {
"address": { "address": {
"title": "Address",
"type": "string" "type": "string"
}, },
"args": { "args": {
"title": "Arguments",
"type": "array", "type": "array",
"items": { "items": {
"type": "string" "type": "string"
} }
}, },
"types": { "types": {
"title": "Argument Types",
"type": "string" "type": "string"
} }
}, },
@@ -594,21 +651,45 @@
}, },
{ {
"type": "object", "type": "object",
"title": "Filter OSC Message", "title": "Router Input",
"properties": { "properties": {
"type": { "type": {
"type": "string", "type": "string",
"const": "osc.message.filter" "const": "router.input"
}, },
"params": { "params": {
"type": "object", "type": "object",
"properties": { "properties": {
"address": { "source": {
"type": "string" "title": "Source",
"type": "string",
"description": "source to report as to the router"
} }
}, },
"required": ["address"], "required": ["source"]
"additionalProperties": false }
},
"required": ["type", "params"],
"additionalProperties": false
},
{
"type": "object",
"title": "Router Output",
"properties": {
"type": {
"type": "string",
"const": "router.output"
},
"params": {
"type": "object",
"properties": {
"module": {
"title": "Module ID",
"type": "string",
"description": "ID of module to send output to"
}
},
"required": ["module"]
} }
}, },
"required": ["type", "params"], "required": ["type", "params"],
@@ -626,6 +707,7 @@
"type": "object", "type": "object",
"properties": { "properties": {
"expression": { "expression": {
"title": "Expression",
"type": "string" "type": "string"
} }
}, },
@@ -648,6 +730,7 @@
"type": "object", "type": "object",
"properties": { "properties": {
"program": { "program": {
"title": "Program",
"type": "string" "type": "string"
} }
}, },
@@ -670,13 +753,16 @@
"type": "object", "type": "object",
"properties": { "properties": {
"path": { "path": {
"title": "Path",
"type": "string" "type": "string"
}, },
"function": { "function": {
"title": "Function",
"type": "string", "type": "string",
"default": "process" "default": "process"
}, },
"enableWasi": { "enableWasi": {
"title": "Enable WASI",
"type": "boolean", "type": "boolean",
"default": false "default": false
} }
@@ -700,6 +786,7 @@
"type": "object", "type": "object",
"properties": { "properties": {
"template": { "template": {
"title": "Template",
"type": "string" "type": "string"
} }
}, },
@@ -733,28 +820,6 @@
"required": ["type"], "required": ["type"],
"additionalProperties": false "additionalProperties": false
}, },
{
"type": "object",
"title": "Filter String",
"properties": {
"type": {
"type": "string",
"const": "string.filter"
},
"params": {
"type": "object",
"properties": {
"pattern": {
"type": "string"
}
},
"required": ["pattern"],
"additionalProperties": false
}
},
"required": ["type", "params"],
"additionalProperties": false
},
{ {
"type": "object", "type": "object",
"title": "Split String", "title": "Split String",
@@ -767,6 +832,7 @@
"type": "object", "type": "object",
"properties": { "properties": {
"separator": { "separator": {
"title": "Separator",
"type": "string" "type": "string"
} }
}, },
@@ -788,6 +854,7 @@
"type": "object", "type": "object",
"properties": { "properties": {
"name": { "name": {
"title": "Field Name",
"type": "string" "type": "string"
} }
}, },
@@ -809,6 +876,7 @@
"type": "object", "type": "object",
"properties": { "properties": {
"name": { "name": {
"title": "Method Name",
"type": "string" "type": "string"
} }
}, },
@@ -830,7 +898,9 @@
"type": "object", "type": "object",
"properties": { "properties": {
"duration": { "duration": {
"type": "integer" "title": "Duration",
"type": "integer",
"description": "Duration to sleep in milliseconds"
} }
}, },
"required": ["duration"] "required": ["duration"]

View File

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

68
websocket.go Normal file
View File

@@ -0,0 +1,68 @@
package showbridge
import (
"encoding/json"
"net/http"
"github.com/gorilla/websocket"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true
},
}
func (r *Router) handleWebsocket(w http.ResponseWriter, req *http.Request) {
conn, err := upgrader.Upgrade(w, req, nil)
if err != nil {
r.logger.Error("websocket upgrade error", "error", err)
return
}
defer conn.Close()
r.wsConnsMu.Lock()
r.wsConns = append(r.wsConns, conn)
r.wsConnsMu.Unlock()
READ_LOOP:
for {
messageType, message, err := conn.ReadMessage()
if err != nil {
_, ok := err.(*websocket.CloseError)
if ok {
break READ_LOOP
}
}
switch messageType {
case websocket.TextMessage, websocket.BinaryMessage:
event := Event{}
err = json.Unmarshal(message, &event)
if err != nil {
r.logger.Error("websocket message unmarshal error", "error", err)
continue
}
r.handleEvent(event, conn)
case websocket.CloseMessage:
break READ_LOOP
case websocket.PingMessage:
err = conn.WriteMessage(websocket.PongMessage, nil)
if err != nil {
r.logger.Error("websocket pong error", "error", err)
}
default:
r.logger.Warn("unsupported websocket message type", "type", messageType)
continue
}
}
//NOTE(jwetzell): remove ws connection
r.wsConnsMu.Lock()
for i, c := range r.wsConns {
if c == conn {
r.wsConns = append(r.wsConns[:i], r.wsConns[i+1:]...)
break
}
}
r.wsConnsMu.Unlock()
}