Compare commits

..

251 Commits

Author SHA1 Message Date
Joel Wetzell
1361e16b28 Merge pull request #118 from jwetzell/fix/format-redis-client-logging
cleanup redis logging
2026-03-23 20:17:52 -05:00
Joel Wetzell
6c9d1b317d cleanup redis logging 2026-03-23 20:16:09 -05:00
Joel Wetzell
53e5e7db9e Merge pull request #117 from jwetzell/feat/validate-config-updates-with-schema
validate config updates via cmd and api with schema
2026-03-23 20:14:56 -05:00
Joel Wetzell
13f7b9e927 validate config updates via cmd and api with schema 2026-03-23 20:12:26 -05:00
Joel Wetzell
9a50ca8cfe move schema to an internal package 2026-03-23 12:51:26 -05:00
Joel Wetzell
256eeac6a8 Merge pull request #116 from jwetzell/feat/dynamic-json-schema
generate json-schema dynamically in Go
2026-03-23 12:15:45 -05:00
Joel Wetzell
842495f010 generate json-schema dynamically in Go 2026-03-23 12:11:10 -05:00
Joel Wetzell
0922ece656 rename filter.unique test as well 2026-03-23 10:25:48 -05:00
Joel Wetzell
4c7dd1b4d8 rename filter.unique to filter.change 2026-03-23 10:16:17 -05:00
Joel Wetzell
2fe2250a57 fix filter regex test names 2026-03-23 00:36:08 -05:00
Joel Wetzell
c91f14185a Merge pull request #114 from jwetzell/feat/filter-unique-processor
add processor to filter out unique values
2026-03-23 00:35:39 -05:00
Joel Wetzell
e32776b02c add processor to filter out unique values 2026-03-23 00:29:42 -05:00
Joel Wetzell
5bb3f08006 add more tests for bad GetAnyAs scenarios 2026-03-22 22:47:43 -05:00
Joel Wetzell
a0f3ee3b05 add tests for wrapped payload 2026-03-22 22:39:41 -05:00
Joel Wetzell
9843c116b2 move test implementations to a shared internal package 2026-03-22 22:39:29 -05:00
Joel Wetzell
71b6a6d4a8 add test layout for router input/output processors 2026-03-22 21:58:17 -05:00
Joel Wetzell
bb3d939bcd test script slice input and output 2026-03-22 21:44:55 -05:00
Joel Wetzell
97742d7e59 add test for no rows to db.query 2026-03-22 21:30:09 -05:00
Joel Wetzell
9646c3f2d3 add tests for correct module type from processors context 2026-03-22 21:27:20 -05:00
Joel Wetzell
26dc976565 add test for extended OSC types 2026-03-22 21:26:37 -05:00
Joel Wetzell
70f64e83c7 add tests to check getting modules from context 2026-03-22 21:00:11 -05:00
Joel Wetzell
13e5d4d85a add tests for db and kv processors 2026-03-22 20:41:39 -05:00
Joel Wetzell
bfd0d3a90a add test module with kv and db capabilities to processor tests 2026-03-22 20:41:21 -05:00
Joel Wetzell
60e9253b51 add registry and config tests for db.sqlite and redis.client 2026-03-22 20:40:37 -05:00
Joel Wetzell
5f056496ce remove Output function from non-output modules 2026-03-22 20:03:50 -05:00
Joel Wetzell
279952f1ea add tests for comman GetAnyAs functions 2026-03-22 19:57:53 -05:00
Joel Wetzell
94bc22928b add tests for bad params Get scenarios 2026-03-22 19:39:24 -05:00
Joel Wetzell
e5e1ea9f63 use http fileserver to serve schema directory 2026-03-21 11:51:49 -05:00
Joel Wetzell
1cdfe929b4 block config updates if one is already in progress 2026-03-21 11:32:11 -05:00
Joel Wetzell
012c416432 use GetAnyAsByteSlice whenever possible 2026-03-18 21:57:21 -05:00
Joel Wetzell
0763b4d720 Merge pull request #113 from jwetzell/feat/script-js-array-return
try processing return value of script.js as an array and a map
2026-03-18 21:54:47 -05:00
Joel Wetzell
14f3ae74a7 try processing return value of script.js as an array and a map 2026-03-18 21:51:25 -05:00
Joel Wetzell
c2b73ac710 convert query to template 2026-03-18 20:54:51 -05:00
Joel Wetzell
57262c3377 actually use value template in Set 2026-03-18 20:54:42 -05:00
Joel Wetzell
1fb59cc947 type Modules property of WrappedPayload 2026-03-18 16:24:49 -05:00
Joel Wetzell
2fbca6209e make an OutputModule interface and remove output from modules that don't implement it 2026-03-18 16:24:19 -05:00
Joel Wetzell
3fedb7ac92 Merge pull request #112 from jwetzell/feat/sqlite-module
add module and processor for interacting with SQLite DB
2026-03-18 14:35:48 -05:00
Joel Wetzell
3f4271b5ef add module and processor for interacting with SQLite DB 2026-03-18 14:21:36 -05:00
Joel Wetzell
1e3b2571fb Merge pull request #111 from jwetzell/feat/kv-processors
more generic key value module things
2026-03-18 14:20:12 -05:00
Joel Wetzell
4559c6efbd add processors for key value operations 2026-03-18 14:18:07 -05:00
Joel Wetzell
7e75fdb758 shift Get method to only key/value modules add Set method to redis.client 2026-03-18 12:53:50 -05:00
Joel Wetzell
2127c6fd5b move module interface to common package 2026-03-18 12:47:28 -05:00
Joel Wetzell
2a40fa561c add test for multiple return values 2026-03-17 19:33:41 -05:00
Joel Wetzell
1467d9de16 add test for slice to struct get processors 2026-03-17 19:17:10 -05:00
Joel Wetzell
5fc7b7399f Merge pull request #110 from jwetzell/feat/api-server-enabled-option
add option to enable api server
2026-03-17 19:12:40 -05:00
Joel Wetzell
3d664fff67 add option to enable api server 2026-03-17 19:03:43 -05:00
Joel Wetzell
9c03ad2c14 test getting running config from router 2026-03-17 18:58:18 -05:00
Joel Wetzell
35262fd505 Merge pull request #109 from jwetzell/dependabot/go_modules/github.com/emiago/diago-0.28.0
Bump github.com/emiago/diago from 0.27.0 to 0.28.0
2026-03-17 13:28:19 -05:00
dependabot[bot]
feccab91d5 Bump github.com/emiago/diago from 0.27.0 to 0.28.0
Bumps [github.com/emiago/diago](https://github.com/emiago/diago) from 0.27.0 to 0.28.0.
- [Release notes](https://github.com/emiago/diago/releases)
- [Commits](https://github.com/emiago/diago/compare/v0.27.0...v0.28.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-17 17:26:19 +00:00
Joel Wetzell
b17e0489a5 switch test wasm plugin to version that does not require WASI 2026-03-17 12:06:12 -05:00
Joel Wetzell
97c1975c7f Merge pull request #108 from jwetzell/chore/remove-mqtt-message-encode
remove badly shaped mqtt.message.encode processor
2026-03-17 12:05:00 -05:00
Joel Wetzell
6336800641 remove badly shaped mqtt.message.encode processor 2026-03-17 12:04:01 -05:00
Joel Wetzell
d9ac6d1a85 add good tests for sip processors 2026-03-16 22:00:25 -05:00
Joel Wetzell
be3b9e3d5a add tests for sip processors 2026-03-16 21:58:10 -05:00
Joel Wetzell
44cc2e322c add missing test boilerplate for processors 2026-03-16 21:49:13 -05:00
Joel Wetzell
5f9547fddb add test for http.response.create 2026-03-16 21:32:30 -05:00
Joel Wetzell
8d8c5d4073 add test for http.requrest.do 2026-03-16 20:43:35 -05:00
Joel Wetzell
6d382ecd20 add test for midi.message.unpack 2026-03-16 20:38:43 -05:00
Joel Wetzell
f0aa5a9eb0 add test for nats.message.create 2026-03-16 20:28:21 -05:00
Joel Wetzell
e84bd6ead4 fix test error messages 2026-03-16 20:21:57 -05:00
Joel Wetzell
3d54ac7a0d add more bad test cases for freed.create 2026-03-16 20:13:19 -05:00
Joel Wetzell
2eae266903 fix template error handling in freed.create 2026-03-16 20:12:59 -05:00
Joel Wetzell
c7d407d507 add bad test for freed.create 2026-03-16 19:59:02 -05:00
Joel Wetzell
2d74e14387 add tests for freed processors 2026-03-16 19:36:32 -05:00
Joel Wetzell
e2a0f4ef90 Merge pull request #107 from jwetzell/fix/freed-processor-roll
fix roll value in freed.create
2026-03-16 19:32:04 -05:00
Joel Wetzell
54307bc817 fix roll value in freed.create 2026-03-16 19:30:55 -05:00
Joel Wetzell
e95fb44064 add Source to wrapped payload 2026-03-16 18:54:54 -05:00
Joel Wetzell
36c0e9b23e explicitly set WrappedPayload.End to false 2026-03-16 18:54:44 -05:00
Joel Wetzell
b57faaccb9 Merge pull request #106 from jwetzell/feat/redis-client-module
add redis client module
2026-03-16 17:21:27 -05:00
Joel Wetzell
814bb9c1dc add redis client module 2026-03-16 17:17:49 -05:00
Joel Wetzell
6fa2c1a746 Merge pull request #105 from jwetzell/chore/wrap-payload-in-processor
wrap payload for all processors
2026-03-16 17:09:02 -05:00
Joel Wetzell
f273aedbc6 wrap payload for all processors 2026-03-16 17:05:49 -05:00
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
Joel Wetzell
dff33f722d Merge pull request #81 from jwetzell/feat/richer-templating-data
use a struct to pass multiple pieces of data into templating context
2026-03-04 12:40:37 -06:00
Joel Wetzell
572a54d3b2 use a struct to pass multiple pieces of data into templating context 2026-03-04 12:38:51 -06:00
Joel Wetzell
6a21cc2639 make method to get data out of a module 2026-03-04 12:35:15 -06:00
Joel Wetzell
0d4ec24a9b move context keys to common package 2026-03-04 12:33:35 -06:00
Joel Wetzell
10c6e84682 fix template usage in freed.create 2026-03-04 09:14:09 -06:00
Joel Wetzell
7ac0ec7582 remove uint parse and random 2026-03-03 21:42:08 -06:00
Joel Wetzell
c25a36f09c Merge pull request #80 from jwetzell/feat/random-float
add processor to generator random float
2026-03-03 21:26:28 -06:00
Joel Wetzell
8d2c022baf fix variable name 2026-03-03 21:21:47 -06:00
Joel Wetzell
76318b4e10 add processor to generator random float 2026-03-03 21:20:36 -06:00
Joel Wetzell
633eaba0ec fix osc.message.create tests 2026-03-03 07:00:05 -06:00
Joel Wetzell
00b6f3660d fix osc.message.create 2026-03-03 06:58:46 -06:00
Joel Wetzell
a275cd2b78 upgrade osc library and switch to pointers 2026-03-02 21:17:49 -06:00
Joel Wetzell
5cb2f845a1 add more error case tests to osc.message.decode 2026-03-02 21:04:18 -06:00
Joel Wetzell
8f769f0a4a add OSC encode/decode test with arg 2026-03-02 20:59:29 -06:00
Joel Wetzell
fdd4b341cb fix osc message decode test 2026-03-02 20:56:16 -06:00
Joel Wetzell
9a9bf10078 add tests for osc message encode/decode 2026-03-02 20:54:03 -06:00
Joel Wetzell
ea46885f70 rework GetInt and GetIntSlice and more testing for params config 2026-03-02 20:53:53 -06:00
Joel Wetzell
fa8f0b7cfc add test for getting array and void from struct 2026-03-02 20:53:20 -06:00
Joel Wetzell
d316147411 align test err checking and logs 2026-03-02 20:53:00 -06:00
Joel Wetzell
30564523d7 Merge pull request #78 from jwetzell/dependabot/go_modules/github.com/urfave/cli/v3-3.7.0
Bump github.com/urfave/cli/v3 from 3.6.2 to 3.7.0
2026-03-02 14:07:05 -06:00
dependabot[bot]
f7231e54c2 Bump github.com/urfave/cli/v3 from 3.6.2 to 3.7.0
Bumps [github.com/urfave/cli/v3](https://github.com/urfave/cli) from 3.6.2 to 3.7.0.
- [Release notes](https://github.com/urfave/cli/releases)
- [Changelog](https://github.com/urfave/cli/blob/main/docs/CHANGELOG.md)
- [Commits](https://github.com/urfave/cli/compare/v3.6.2...v3.7.0)

---
updated-dependencies:
- dependency-name: github.com/urfave/cli/v3
  dependency-version: 3.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 20:05:27 +00:00
Joel Wetzell
14de829e7a Merge pull request #75 from jwetzell/dependabot/go_modules/go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp-1.41.0
Bump go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp from 1.40.0 to 1.41.0
2026-03-02 14:04:27 -06:00
Joel Wetzell
33a54eb24c Merge pull request #76 from jwetzell/dependabot/go_modules/gitlab.com/gomidi/midi/v2-2.3.23
Bump gitlab.com/gomidi/midi/v2 from 2.3.22 to 2.3.23
2026-03-02 14:04:12 -06:00
Joel Wetzell
3c6c50c914 add tests for midi and mqtt message create processors 2026-03-02 14:01:47 -06:00
Joel Wetzell
da516f5102 fix test wording 2026-03-02 14:01:32 -06:00
Joel Wetzell
1f88819428 use params methods iin mqtt.message.create 2026-03-02 14:01:20 -06:00
Joel Wetzell
df4f0f745d fix GetByteSlice for params 2026-03-02 14:00:47 -06:00
dependabot[bot]
d86bff30cd Bump gitlab.com/gomidi/midi/v2 from 2.3.22 to 2.3.23
Bumps [gitlab.com/gomidi/midi/v2](https://gitlab.com/gomidi/midi) from 2.3.22 to 2.3.23.
- [Commits](https://gitlab.com/gomidi/midi/compare/v2.3.22...v2.3.23)

---
updated-dependencies:
- dependency-name: gitlab.com/gomidi/midi/v2
  dependency-version: 2.3.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 19:21:14 +00:00
dependabot[bot]
22d8bbf88d 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.40.0 to 1.41.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.40.0...v1.41.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-02 19:21:05 +00:00
Joel Wetzell
925873a124 add tests for midi.message.decode 2026-03-02 12:55:33 -06:00
Joel Wetzell
08308c1891 add count flag to hopefully avoid cache issues 2026-03-02 12:47:01 -06:00
Joel Wetzell
f68312ee84 standardize layout of bad tests 2026-03-02 12:34:46 -06:00
Joel Wetzell
cd1b5e1437 add tests for artnet packet encode/decode processors 2026-03-02 12:34:26 -06:00
Joel Wetzell
34af7d7aec cleanup unnecessary use of floats 2026-03-02 12:34:06 -06:00
Joel Wetzell
df14024012 add convenience method for casting payloads 2026-03-02 12:06:42 -06:00
Joel Wetzell
af39c16f30 add more convenience functions for getting slices from params and add tests 2026-03-02 12:03:35 -06:00
Joel Wetzell
7d2a3225a7 make more error const for params config 2026-03-02 12:03:05 -06:00
Joel Wetzell
b5ffee022a use GetStringSlice in osc.message.create 2026-03-02 12:02:27 -06:00
Joel Wetzell
cd264d9ed4 add function to get string slice from params 2026-03-02 09:07:31 -06:00
Joel Wetzell
9d14e5929f wrap all payload types for expr env 2026-03-01 23:49:53 -06:00
Joel Wetzell
8fe1463198 Merge pull request #74 from jwetzell/feat/expr-payload-wrapping
wrap literal payloads into a struct for expr processors
2026-03-01 23:36:28 -06:00
Joel Wetzell
28a5d1ea90 wrap literal payloads into a struct for expr processors 2026-03-01 23:35:08 -06:00
Joel Wetzell
e9d5931e9d Merge pull request #73 from jwetzell/feat/expr-filter
add processor to filter using expr expression
2026-03-01 23:15:02 -06:00
Joel Wetzell
d90f103d00 add processor to filter using expr expression 2026-03-01 23:04:04 -06:00
Joel Wetzell
05b0de1dfd remote http request/response encoding 2026-03-01 23:02:56 -06:00
Joel Wetzell
0d8dbab743 add title for all processor schemas 2026-03-01 22:12:45 -06:00
Joel Wetzell
fb599f1057 Merge pull request #72 from jwetzell/feat/struct-processors
add processors to pull data out of struct
2026-03-01 21:57:08 -06:00
Joel Wetzell
a2efed0ee2 add processor to pull method out of struct 2026-03-01 21:47:37 -06:00
Joel Wetzell
533fe150c8 add processor to pull field out of struct 2026-03-01 21:47:31 -06:00
Joel Wetzell
b653179e6d add functions to test struct 2026-03-01 21:44:41 -06:00
Joel Wetzell
695cabf15e move test struct to common place 2026-03-01 21:36:12 -06:00
Joel Wetzell
ccac116f8d fix error message 2026-03-01 15:00:09 -06:00
Joel Wetzell
c298f63ffc add convenience method to pull params from config 2026-03-01 14:57:19 -06:00
Joel Wetzell
183182e6cd use control instead of controller in midi.message.create 2026-03-01 14:21:36 -06:00
Joel Wetzell
01f172dbee Merge pull request #71 from jwetzell/feat/nats-server-module
add nats.server module
2026-03-01 13:18:28 -06:00
Joel Wetzell
20fd4170ed handle param values that aren't from loading in JSON 2026-03-01 13:16:13 -06:00
Joel Wetzell
bcf9299505 remove nats message encode 2026-03-01 13:03:26 -06:00
Joel Wetzell
4f7820af5e add basic nats server module 2026-03-01 13:02:55 -06:00
Joel Wetzell
87fcc63068 Merge pull request #70 from jwetzell/dependabot/go_modules/github.com/nats-io/nats.go-1.49.0
Bump github.com/nats-io/nats.go from 1.48.0 to 1.49.0
2026-02-27 08:31:56 -06:00
dependabot[bot]
1c49776662 Bump github.com/nats-io/nats.go from 1.48.0 to 1.49.0
Bumps [github.com/nats-io/nats.go](https://github.com/nats-io/nats.go) from 1.48.0 to 1.49.0.
- [Release notes](https://github.com/nats-io/nats.go/releases)
- [Commits](https://github.com/nats-io/nats.go/compare/v1.48.0...v1.49.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-27 14:21:09 +00:00
Joel Wetzell
1a7207ae4c Merge pull request #66 from jwetzell/dependabot/github_actions/docker/build-push-action-6.19.2
Bump docker/build-push-action from 6.18.0 to 6.19.2
2026-02-27 08:20:11 -06:00
Joel Wetzell
f86a34cc92 Merge pull request #67 from jwetzell/dependabot/go_modules/github.com/expr-lang/expr-1.17.8
Bump github.com/expr-lang/expr from 1.17.7 to 1.17.8
2026-02-27 08:19:46 -06:00
Joel Wetzell
d9d64bd248 Merge pull request #68 from jwetzell/dependabot/go_modules/gitlab.com/gomidi/midi/v2-2.3.22
Bump gitlab.com/gomidi/midi/v2 from 2.3.20 to 2.3.22
2026-02-27 08:19:24 -06:00
Joel Wetzell
e828dc1b0b Merge pull request #69 from jwetzell/dependabot/github_actions/goreleaser/goreleaser-action-7
Bump goreleaser/goreleaser-action from 6 to 7
2026-02-27 08:19:08 -06:00
dependabot[bot]
cc4d50bc17 Bump goreleaser/goreleaser-action from 6 to 7
Bumps [goreleaser/goreleaser-action](https://github.com/goreleaser/goreleaser-action) from 6 to 7.
- [Release notes](https://github.com/goreleaser/goreleaser-action/releases)
- [Commits](https://github.com/goreleaser/goreleaser-action/compare/v6...v7)

---
updated-dependencies:
- dependency-name: goreleaser/goreleaser-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-23 19:22:05 +00:00
Joel Wetzell
abd37439c5 rework Route struct 2026-02-16 19:13:04 -06:00
Joel Wetzell
8c3f93b601 update tracer name for processor 2026-02-16 18:57:44 -06:00
Joel Wetzell
2f7f6967e1 update tracer name for module 2026-02-16 18:25:30 -06:00
Joel Wetzell
09b030efa6 rework otel tracer providing 2026-02-16 17:11:43 -06:00
dependabot[bot]
bb49dba22e Bump gitlab.com/gomidi/midi/v2 from 2.3.20 to 2.3.22
Bumps [gitlab.com/gomidi/midi/v2](https://gitlab.com/gomidi/midi) from 2.3.20 to 2.3.22.
- [Commits](https://gitlab.com/gomidi/midi/compare/v2.3.20...v2.3.22)

---
updated-dependencies:
- dependency-name: gitlab.com/gomidi/midi/v2
  dependency-version: 2.3.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 18:21:08 +00:00
dependabot[bot]
4ed6866731 Bump github.com/expr-lang/expr from 1.17.7 to 1.17.8
Bumps [github.com/expr-lang/expr](https://github.com/expr-lang/expr) from 1.17.7 to 1.17.8.
- [Release notes](https://github.com/expr-lang/expr/releases)
- [Commits](https://github.com/expr-lang/expr/compare/v1.17.7...v1.17.8)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 18:21:00 +00:00
dependabot[bot]
2d392873ca Bump docker/build-push-action from 6.18.0 to 6.19.2
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.18.0 to 6.19.2.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](263435318d...10e90e3645)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-16 18:20:37 +00:00
Joel Wetzell
11e25ab8f7 require id properties to be at least a one character 2026-02-13 21:46:46 -06:00
Joel Wetzell
87e6b09156 fix random processors schemas 2026-02-13 21:45:19 -06:00
Joel Wetzell
8cb2a0e9f0 add more tests for osc.message.create 2026-02-10 21:23:11 -06:00
Joel Wetzell
263943f4d6 add tests for osc.message.create and filter 2026-02-10 21:16:02 -06:00
Joel Wetzell
f9865765c6 fix test names for script.wasm 2026-02-10 21:15:52 -06:00
Joel Wetzell
e6aaffbb95 add error tests for script.wasm 2026-02-10 20:49:40 -06:00
Joel Wetzell
90a14b4d1b add more tests for router 2026-02-10 20:42:52 -06:00
Joel Wetzell
8bdd338a27 Merge pull request #59 from jwetzell/dependabot/go_modules/go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp-1.40.0
Bump go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp from 1.28.0 to 1.40.0
2026-02-09 23:23:18 -06:00
Joel Wetzell
b20c16be4f Merge pull request #62 from jwetzell/dependabot/go_modules/github.com/emiago/diago-0.27.0
Bump github.com/emiago/diago from 0.26.2 to 0.27.0
2026-02-09 23:22:55 -06:00
Joel Wetzell
de060057e6 Merge pull request #61 from jwetzell/dependabot/go_modules/gitlab.com/gomidi/midi/v2-2.3.20
Bump gitlab.com/gomidi/midi/v2 from 2.3.18 to 2.3.20
2026-02-09 23:22:44 -06:00
Joel Wetzell
4526e805af add basic tests for mqtt.message.encode 2026-02-09 23:18:19 -06:00
Joel Wetzell
1d8baa8c17 add function to create new MQTTMessage 2026-02-09 23:17:53 -06:00
Joel Wetzell
060c12512c generate coverage file manually to filter out cmd package 2026-02-09 23:09:57 -06:00
Joel Wetzell
444093d375 add tests for error json decode/encode error scenarios 2026-02-09 22:28:55 -06:00
Joel Wetzell
a217b729cd add tests for error script.wasm error scenarios 2026-02-09 22:28:34 -06:00
Joel Wetzell
ad76480008 add good/bad tests for time.sleep 2026-02-09 22:16:54 -06:00
Joel Wetzell
a408e281a3 standardize processor instance creation in tests 2026-02-09 22:09:48 -06:00
Joel Wetzell
f028634401 cleanup grammar on error messages 2026-02-09 21:29:12 -06:00
Joel Wetzell
961316d28b add tests for invalid addr to net servers 2026-02-09 21:23:09 -06:00
Joel Wetzell
e22a941e4a fix non returning error in udp server 2026-02-09 21:19:29 -06:00
Joel Wetzell
b0cffe819d add more bad test for net servers 2026-02-09 21:14:02 -06:00
Joel Wetzell
0cdd1f6e6a add bad creation tests for sip servers 2026-02-09 21:06:02 -06:00
Joel Wetzell
a9245ab88c fix error message 2026-02-09 21:02:44 -06:00
Joel Wetzell
d69d3a370d Merge pull request #64 from jwetzell/feat/sip-dtmf-server-useragent
add optional userAgent param to sip.dtmf.server
2026-02-09 20:58:48 -06:00
Joel Wetzell
019624f152 add optional userAgent param to sip.dtmf.server 2026-02-09 20:51:03 -06:00
Joel Wetzell
90a3119fb8 fix variable name 2026-02-09 20:48:07 -06:00
Joel Wetzell
54c14cbbae make nil payload a debug log 2026-02-09 19:57:17 -06:00
Joel Wetzell
969ac6e04b add test for module registraion 2026-02-09 19:45:04 -06:00
Joel Wetzell
bfa63499c3 add basic tests for bad module creation 2026-02-09 19:44:54 -06:00
Joel Wetzell
6b178d1ae4 cleanup error messages 2026-02-09 19:15:34 -06:00
dependabot[bot]
42bd5c46b5 Bump github.com/emiago/diago from 0.26.2 to 0.27.0
Bumps [github.com/emiago/diago](https://github.com/emiago/diago) from 0.26.2 to 0.27.0.
- [Release notes](https://github.com/emiago/diago/releases)
- [Commits](https://github.com/emiago/diago/compare/v0.26.2...v0.27.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-09 20:50:55 +00:00
dependabot[bot]
515a8d14fd Bump gitlab.com/gomidi/midi/v2 from 2.3.18 to 2.3.20
Bumps [gitlab.com/gomidi/midi/v2](https://gitlab.com/gomidi/midi) from 2.3.18 to 2.3.20.
- [Commits](https://gitlab.com/gomidi/midi/compare/v2.3.18...v2.3.20)

---
updated-dependencies:
- dependency-name: gitlab.com/gomidi/midi/v2
  dependency-version: 2.3.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-09 20:50:51 +00:00
dependabot[bot]
1f3a2b9076 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.28.0 to 1.40.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.28.0...v1.40.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-09 20:50:36 +00:00
207 changed files with 14368 additions and 5368 deletions

View File

@@ -1,10 +1,10 @@
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:
interval: "weekly" interval: "weekly"

18
.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

@@ -15,4 +15,4 @@ jobs:
steps: steps:
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1 - uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
with: with:
sync-labels: true sync-labels: true

View File

@@ -19,9 +19,9 @@ 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@v6 uses: goreleaser/goreleaser-action@v7
with: with:
workdir: cmd/showbridge workdir: cmd/showbridge
distribution: goreleaser distribution: goreleaser
@@ -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@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0 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,18 +29,21 @@ 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
- name: run tests - name: run tests
run: go test ./... run: go test ./... -count=1 -coverpkg "github.com/jwetzell/showbridge-go/..." -coverprofile coverage.out
- name: clean up coverage report
run: cat coverage.out | grep -v "github.com/jwetzell/showbridge-go/cmd" > coverage.nocmd.out
- name: Update coverage report - name: Update coverage report
uses: ncruces/go-coverage-report@v0.3.2 uses: ncruces/go-coverage-report@v0.3.2
with: with:
coverage-file: coverage.nocmd.out
report: true report: true
chart: true chart: true
amend: true amend: true
if: github.event_name == 'push' if: github.event_name == 'push'
continue-on-error: true continue-on-error: true

33
.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
@@ -25,7 +25,7 @@ Simple protocol router _/s_
- [OSC](https://opensoundcontrol.stanford.edu/spec-1_0.html) - [OSC](https://opensoundcontrol.stanford.edu/spec-1_0.html)
- [FreeD](https://ptzoptics.com/freed/) - [FreeD](https://ptzoptics.com/freed/)
- [SIP](https://en.wikipedia.org/wiki/Session_Initiation_Protocol) - [SIP](https://en.wikipedia.org/wiki/Session_Initiation_Protocol)
### CLI Usage ### CLI Usage
``` ```

255
api.go Normal file
View File

@@ -0,0 +1,255 @@
package showbridge
import (
"context"
_ "embed"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
"github.com/jwetzell/showbridge-go/internal/route"
"github.com/jwetzell/showbridge-go/internal/schema"
)
func (r *Router) startAPIServer(config config.ApiConfig) {
if !config.Enabled {
r.logger.Warn("API not enabled")
return
}
r.logger.Debug("starting API server", "port", config.Port)
mux := http.NewServeMux()
mux.HandleFunc("/ws", r.handleWebsocket)
mux.HandleFunc("/health", r.handleHealthHTTP)
mux.HandleFunc("/api/v1/config", r.handleConfigHTTP)
mux.HandleFunc("/schema/config.schema.json", handleConfigSchema)
mux.HandleFunc("/schema/routes.schema.json", handleRoutesSchema)
mux.HandleFunc("/schema/modules.schema.json", handleModulesSchema)
mux.HandleFunc("/schema/processors.schema.json", handleProcessorsSchema)
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() {
if r.apiServer == nil {
return
}
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:
if r.updatingConfig {
http.Error(w, "Config update in progress.", http.StatusConflict)
return
}
//TODO(jwetzell): again way too much marshaling
cfgBytes, err := io.ReadAll(req.Body)
if err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
cfgMap := make(map[string]any)
err = json.Unmarshal(cfgBytes, &cfgMap)
if err != nil {
http.Error(w, "Bad request", http.StatusBadRequest)
return
}
err = schema.ApplyDefaults(&cfgMap)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
err = schema.ValidateConfig(cfgMap)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
validCfgBytes, err := json.Marshal(cfgMap)
if err != nil {
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
var newConfig config.Config
err = json.Unmarshal(validCfgBytes, &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)
}
}
func handleConfigSchema(w http.ResponseWriter, req *http.Request) {
switch req.Method {
case http.MethodGet:
schemaJSON, err := json.Marshal(schema.ConfigSchema)
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(schemaJSON)
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 handleRoutesSchema(w http.ResponseWriter, req *http.Request) {
switch req.Method {
case http.MethodGet:
schemaJSON, err := json.Marshal(schema.RoutesConfigSchema)
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(schemaJSON)
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 handleModulesSchema(w http.ResponseWriter, req *http.Request) {
switch req.Method {
case http.MethodGet:
schemaJSON, err := json.Marshal(schema.GetModulesSchema())
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(schemaJSON)
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 handleProcessorsSchema(w http.ResponseWriter, req *http.Request) {
switch req.Method {
case http.MethodGet:
schemaJSON, err := json.Marshal(schema.GetProcessorsSchema())
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(schemaJSON)
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

@@ -2,6 +2,7 @@ package main
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
@@ -13,13 +14,15 @@ 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/jwetzell/showbridge-go/internal/schema"
"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"
"go.opentelemetry.io/otel/trace"
"sigs.k8s.io/yaml" "sigs.k8s.io/yaml"
) )
@@ -90,7 +93,6 @@ type showbridgeApp struct {
logger *slog.Logger logger *slog.Logger
router *showbridge.Router router *showbridge.Router
routerRunner *sync.WaitGroup routerRunner *sync.WaitGroup
tracer trace.Tracer
routerMutex sync.Mutex routerMutex sync.Mutex
} }
@@ -103,7 +105,28 @@ func readConfig(configPath string) (config.Config, error) {
return config.Config{}, err return config.Config{}, err
} }
err = yaml.Unmarshal(configBytes, &cfg) //TODO(jwetzell): this is an annoying amount of marshaling
yamlMap := make(map[string]any)
err = yaml.Unmarshal(configBytes, &yamlMap)
if err != nil {
return config.Config{}, err
}
err = schema.ApplyDefaults(&yamlMap)
if err != nil {
return config.Config{}, err
}
err = schema.ValidateConfig(yamlMap)
if err != nil {
return config.Config{}, err
}
validatedConfigBytes, err := json.Marshal(yamlMap)
err = json.Unmarshal(validatedConfigBytes, &cfg)
if err != nil { if err != nil {
return config.Config{}, err return config.Config{}, err
} }
@@ -111,6 +134,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 == "" {
@@ -155,7 +192,6 @@ func run(ctx context.Context, c *cli.Command) error {
slog.SetDefault(slog.New(logHandler)) slog.SetDefault(slog.New(logHandler))
var tracer trace.Tracer
if c.Bool("trace") { if c.Bool("trace") {
exporter, err := otlptracehttp.New(ctx) exporter, err := otlptracehttp.New(ctx)
if err != nil { if err != nil {
@@ -166,9 +202,7 @@ func run(ctx context.Context, c *cli.Command) error {
otel.SetTracerProvider(tracerProvider) otel.SetTracerProvider(tracerProvider)
defer tracerProvider.Shutdown(ctx) defer tracerProvider.Shutdown(ctx)
tracer = tracerProvider.Tracer("showbridge") otel.SetTracerProvider(tracerProvider)
} else {
tracer = otel.Tracer("showbridge")
} }
showbridgeApp := &showbridgeApp{ showbridgeApp := &showbridgeApp{
@@ -176,10 +210,21 @@ func run(ctx context.Context, c *cli.Command) error {
configPath: configPath, configPath: configPath,
logger: slog.Default().With("component", "cmd"), logger: slog.Default().With("component", "cmd"),
routerRunner: &sync.WaitGroup{}, routerRunner: &sync.WaitGroup{},
tracer: tracer,
} }
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)
} }
@@ -191,7 +236,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")
@@ -201,40 +246,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, app.tracer)
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)
} }
@@ -242,12 +284,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,6 @@
api:
enabled: true
port: 8080
modules: modules:
- id: http - id: http
type: http.server type: http.server
@@ -13,6 +16,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)
}
}
}

70
go.mod
View File

@@ -1,33 +1,40 @@
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.26.2 github.com/emiago/diago v0.28.0
github.com/emiago/sipgo v1.1.2 github.com/emiago/sipgo v1.2.1
github.com/expr-lang/expr v1.17.7 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/google/jsonschema-go v0.4.2
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.1.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.go v1.48.0 github.com/nats-io/nats-server/v2 v2.12.5
github.com/urfave/cli/v3 v3.6.2 github.com/nats-io/nats.go v1.49.0
gitlab.com/gomidi/midi/v2 v2.3.18 github.com/redis/go-redis/v9 v9.18.0
github.com/urfave/cli/v3 v3.7.0
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.40.0 go.opentelemetry.io/otel v1.42.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0
go.opentelemetry.io/otel/sdk v1.40.0 go.opentelemetry.io/otel/sdk v1.42.0
go.opentelemetry.io/otel/trace v1.40.0 go.opentelemetry.io/otel/trace v1.42.0
modernc.org/quickjs v0.17.1 modernc.org/quickjs v0.17.1
modernc.org/sqlite v1.47.0
sigs.k8s.io/yaml v1.6.0 sigs.k8s.io/yaml v1.6.0
) )
require ( require (
github.com/cenkalti/backoff/v4 v4.3.0 // 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/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
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect
github.com/emiago/dtls/v3 v3.0.0-20260122183559-8b8d23e359c0 // indirect github.com/emiago/dtls/v3 v3.0.0-20260122183559-8b8d23e359c0 // indirect
@@ -38,14 +45,16 @@ require (
github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect
github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/pool v0.2.1 // indirect
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/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.21.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.0 // 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/nats-io/nkeys v0.4.11 // 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/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
@@ -60,22 +69,23 @@ 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.28.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect go.opentelemetry.io/otel/metric v1.42.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.uber.org/atomic v1.11.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.42.0 // indirect golang.org/x/crypto v0.48.0 // indirect
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect golang.org/x/net v0.51.0 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.29.0 // indirect golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect golang.org/x/time v0.15.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/grpc v1.65.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/protobuf v1.34.2 // indirect google.golang.org/grpc v1.79.2 // 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.70.0 // indirect
modernc.org/libquickjs v0.12.3 // indirect modernc.org/libquickjs v0.12.3 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect modernc.org/memory v1.11.0 // indirect

175
go.sum
View File

@@ -1,25 +1,33 @@
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op h1:kpBdlEPbRvff0mDD1gk7o9BhI16b9p5yYAXRlidpqJE=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0= github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0=
github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a h1:UwSIFv5g5lIvbGgtf3tVwC7Ky9rmMFBp0RMs+6f6YqE= github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a h1:UwSIFv5g5lIvbGgtf3tVwC7Ky9rmMFBp0RMs+6f6YqE=
github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q= github.com/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q=
github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE= github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU= github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
github.com/emiago/diago v0.26.2 h1:3QL03V0drX96eIBFBpfueNcywydRgYqffKihluGL0gA= github.com/emiago/diago v0.28.0 h1:VCiimhFYLoBTsxH6WrufLSt6tKWG8Fv7LfCkZHqct2E=
github.com/emiago/diago v0.26.2/go.mod h1:jZ+7EnKcmgqKnLjCHPqfbP4Y/9Q/JLSLxMflDrp2J1M= github.com/emiago/diago v0.28.0/go.mod h1:eQT6j9co9PMQ/25aUiM2jpvwxxWFXLWi2w5R3lZNmKg=
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.1.2 h1:JvLqEvqNSQm2mBX40qZ7O0WC3Ee67Z0UrfmBI7y6Beo= github.com/emiago/sipgo v1.2.1 h1:5JTwogbe3yQFA3sjBVueN2Q4WTU350tGeBwPYT8HMv0=
github.com/emiago/sipgo v1.1.2/go.mod h1:DuwAxBZhKMqIzQFPGZb1MVAGU6Wuxj64oTOhd5dx/FY= github.com/emiago/sipgo v1.2.1/go.mod h1:DuwAxBZhKMqIzQFPGZb1MVAGU6Wuxj64oTOhd5dx/FY=
github.com/expr-lang/expr v1.17.7 h1:Q0xY/e/2aCIp8g9s/LGvMDCC5PxYlvHgDZRQ4y16JX8= github.com/expr-lang/expr v1.17.8 h1:W1loDTT+0PQf5YteHSTpju2qfUfNoBt4yw9+wOEU9VM=
github.com/expr-lang/expr v1.17.7/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=
github.com/extism/go-sdk v1.7.1/go.mod h1:IT+Xdg5AZM9hVtpFUA+uZCJMge/hbvshl8bwzLtFyKA= github.com/extism/go-sdk v1.7.1/go.mod h1:IT+Xdg5AZM9hVtpFUA+uZCJMge/hbvshl8bwzLtFyKA=
github.com/go-audio/riff v1.0.0 h1:d8iCGbDvox9BfLagY94fBynxSPHO80LmZCaOsmKxokA= github.com/go-audio/riff v1.0.0 h1:d8iCGbDvox9BfLagY94fBynxSPHO80LmZCaOsmKxokA=
@@ -37,14 +45,22 @@ github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs=
github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/go-tpm v0.9.8 h1:slArAR9Ft+1ybZu0lBwpSmpwhRXaa85hWtMinMyRAWo=
github.com/google/go-tpm v0.9.8/go.mod h1:h9jEsEECg7gtLis0upRBQU+GhYVH6jMjrFxI8u6bVUY=
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.21.0 h1:CWyXh/jylQWp2dtiV33mY4iSSp6yf4lmn+c7/tN+ObI= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.21.0/go.mod h1:nCLIt0w3Ept2NwF8ThLmrppXsfT07oC8k0XNDxd8sVU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca h1:T54Ema1DU8ngI+aef9ZhAhNGQhcRTrWxVeG07F+c/Rw= github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca h1:T54Ema1DU8ngI+aef9ZhAhNGQhcRTrWxVeG07F+c/Rw=
@@ -55,22 +71,30 @@ github.com/jwetzell/artnet-go v0.2.1 h1:iYTKWcwYrF5kBkYfkw2UbWvoueeA23iKEn7fR27m
github.com/jwetzell/artnet-go v0.2.1/go.mod h1:gli97Z32a0kMkZ6taoTiK7/lqHcF/dhiGjGJdx/PxqA= github.com/jwetzell/artnet-go v0.2.1/go.mod h1:gli97Z32a0kMkZ6taoTiK7/lqHcF/dhiGjGJdx/PxqA=
github.com/jwetzell/free-d-go v0.1.0 h1:xHt6dvyit98X+OC3jVzV0aLidxbyzi3vI9QiYkteEtA= github.com/jwetzell/free-d-go v0.1.0 h1:xHt6dvyit98X+OC3jVzV0aLidxbyzi3vI9QiYkteEtA=
github.com/jwetzell/free-d-go v0.1.0/go.mod h1:KmrkooRARRaxJTBSPvwt/6IMAIaHH1R8bSA8cwbbELw= github.com/jwetzell/free-d-go v0.1.0/go.mod h1:KmrkooRARRaxJTBSPvwt/6IMAIaHH1R8bSA8cwbbELw=
github.com/jwetzell/osc-go v0.1.0 h1:EXxup5VWBErHot2Ri4MFToPf6KCzLDTbCt2x6GLfw8I= github.com/jwetzell/osc-go v0.2.0 h1:4as+BYCeZhEddFczGveP5yZZxvY728Uavz+ZSLZfOII=
github.com/jwetzell/osc-go v0.1.0/go.mod h1:xLz0jTwebSxtx1TkKN1YVdeRqvpFNweDhTut5TE393A= 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.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
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=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U= github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 h1:KGuD/pM2JpL9FAYvBrnBBeENKZNh6eNtjqytV6TYjnk=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= github.com/nats-io/jwt/v2 v2.8.0 h1:K7uzyz50+yGZDO5o772eRE7atlcSEENpL7P+b74JV1g=
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE= github.com/nats-io/jwt/v2 v2.8.0/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA=
github.com/nats-io/nats-server/v2 v2.12.5 h1:EOHLbsLJgUHUwzkj9gBTOlubkX+dmSs0EYWMdBiHivU=
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/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw=
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
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=
@@ -91,6 +115,8 @@ github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM= github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/redis/go-redis/v9 v9.18.0 h1:pMkxYPkEbMPwRdenAzUNyFNrDgHx9U+DrBabWNfSRQs=
github.com/redis/go-redis/v9 v9.18.0/go.mod h1:k3ufPphLU5YXwNTUcCRXGxUoF1fqxnhFQmscfkCoDA0=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
@@ -101,63 +127,70 @@ github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZ
github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk= github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk=
github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I=
github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM=
github.com/urfave/cli/v3 v3.6.2 h1:lQuqiPrZ1cIz8hz+HcrG0TNZFxU70dPZ3Yl+pSrH9A8= github.com/urfave/cli/v3 v3.7.0 h1:AGSnbUyjtLiM+WJUb4dzXKldl/gL+F8OwmRDtVr6g2U=
github.com/urfave/cli/v3 v3.6.2/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= github.com/urfave/cli/v3 v3.7.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso=
github.com/zaf/g711 v1.4.0 h1:XZYkjjiAg9QTBnHqEg37m2I9q3IIDv5JRYXs2N8ma7c= github.com/zaf/g711 v1.4.0 h1:XZYkjjiAg9QTBnHqEg37m2I9q3IIDv5JRYXs2N8ma7c=
github.com/zaf/g711 v1.4.0/go.mod h1:eCDXt3dSp/kYYAoooba7ukD/Q75jvAaS4WOMr0l1Roo= github.com/zaf/g711 v1.4.0/go.mod h1:eCDXt3dSp/kYYAoooba7ukD/Q75jvAaS4WOMr0l1Roo=
gitlab.com/gomidi/midi/v2 v2.3.18 h1:sj2fOhtvOe+zI8YJe8qTxLw5zv0ntULLUDwcFOaZQbI= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
gitlab.com/gomidi/midi/v2 v2.3.18/go.mod h1:jDpP4O4skYi+7iVwt6Zyp18bd2M4hkjtMuw2cmgKgfw= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA=
gitlab.com/gomidi/midi/v2 v2.3.23 h1:P8NxV4EzV9c+BjpwTeB+G/qa+Xdq/UTazS2fKxY0O0g=
gitlab.com/gomidi/midi/v2 v2.3.23/go.mod h1:jDpP4O4skYi+7iVwt6Zyp18bd2M4hkjtMuw2cmgKgfw=
go.bug.st/serial v1.6.4 h1:7FmqNPgVp3pu2Jz5PoPtbZ9jJO5gnEnZIvnI1lzve8A= 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.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= 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.28.0 h1:j9+03ymgYhPKmeXGk5Zu+cIZOlVzd9Zv7QIiyItjFBU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0 h1:uLXP+3mghfMf7XmV4PkGfFhFKuNWoCvvx5wP/wOXo0o=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0/go.mod h1:Y5+XiUG4Emn1hTfciPzGPJaSI+RpDts6BnCIir0SLqk= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0/go.mod h1:v0Tj04armyT59mnURNUJf7RCKcKzq+lgJs6QSjHjaTc=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0 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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= 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/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
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/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=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@@ -169,18 +202,18 @@ gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis= modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc= modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM= modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA= modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc= modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE= modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY= modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks= modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI= modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.67.1 h1:bFaqOaa5/zbWYJo8aW0tXPX21hXsngG2M7mckCnFSVk= modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
modernc.org/libc v1.67.1/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA= modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
modernc.org/libquickjs v0.12.3 h1:2IU9B6njBmce2PuYttJDkXeoLRV9WnvgP+eU5HAC8YI= modernc.org/libquickjs v0.12.3 h1:2IU9B6njBmce2PuYttJDkXeoLRV9WnvgP+eU5HAC8YI=
modernc.org/libquickjs v0.12.3/go.mod h1:iCsgVxnHTX3i0YPxxHBmJk0GLA5sVUHXWI/090UXgeE= modernc.org/libquickjs v0.12.3/go.mod h1:iCsgVxnHTX3i0YPxxHBmJk0GLA5sVUHXWI/090UXgeE=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
@@ -193,6 +226,8 @@ modernc.org/quickjs v0.17.1 h1:CbYnbTf7ksZk9YZ1rRM2Ab1Zfi+X6s50kXiOhpd2NIg=
modernc.org/quickjs v0.17.1/go.mod h1:hATT7DIJc33I5Q/Fjffhm0tpUHNSqdKHma/ossibTA0= modernc.org/quickjs v0.17.1/go.mod h1:hATT7DIJc33I5Q/Fjffhm0tpUHNSqdKHma/ossibTA0=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.47.0 h1:R1XyaNpoW4Et9yly+I2EeX7pBza/w+pmYee/0HJDyKk=
modernc.org/sqlite v1.47.0/go.mod h1:hWjRO6Tj/5Ik8ieqxQybiEOUXy0NJFNp2tpvVpKlvig=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

175
internal/common/common.go Normal file
View File

@@ -0,0 +1,175 @@
package common
import (
"math"
"reflect"
)
func GetAnyAs[T any](value any) (T, bool) {
typed, ok := value.(T)
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
}
float32Value, ok := value.(float32)
if ok {
if float64(float32Value) != math.Floor(float64(float32Value)) {
return 0, false
}
return int(float32Value), true
}
float64Value, ok := value.(float64)
if ok {
if float64Value != math.Floor(float64Value) {
return 0, false
}
return int(float64Value), true
}
return 0, false
}
func GetAnyAsByte(value any) (byte, bool) {
byteValue, ok := value.(byte)
if ok {
return byte(byteValue), true
}
intValue, ok := value.(int)
if ok {
return byte(intValue), true
}
uintValue, ok := value.(uint)
if ok {
return byte(uintValue), true
}
float32Value, ok := value.(float32)
if ok {
if float64(float32Value) != math.Floor(float64(float32Value)) {
return 0, false
}
return byte(float32Value), true
}
float64Value, ok := value.(float64)
if ok {
if float64Value != math.Floor(float64Value) {
return 0, false
}
return byte(float64Value), 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()
elemValue, ok := GetAnyAsByte(elem)
if !ok {
return nil, false
}
result[i] = elemValue
}
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()
elemInt, ok := GetAnyAsInt(elem)
if !ok {
return nil, false
}
result[i] = elemInt
}
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,424 @@
package common_test
import (
"slices"
"testing"
"github.com/jwetzell/showbridge-go/internal/common"
)
func TestGoodGetAnyAsInt(t *testing.T) {
testCases := []struct {
name string
value any
typedValue int
}{
{
name: "int",
value: int(42),
typedValue: 42,
},
{
name: "uint",
value: uint(42),
typedValue: 42,
},
{
name: "float32 without decimal",
value: float32(42.0),
typedValue: 42,
},
{
name: "float64 without decimal",
value: float64(42.0),
typedValue: 42,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
value, ok := common.GetAnyAsInt(testCase.value)
if !ok {
t.Fatalf("GetAnyAsInt expected to succeed but failed")
}
if value != testCase.typedValue {
t.Fatalf("GetAnyAsInt expected got %d, expected %d", value, testCase.typedValue)
}
})
}
}
func TestBadGetAnyAsInt(t *testing.T) {
testCases := []struct {
name string
value any
}{
{
name: "string",
value: "value",
},
{
name: "float32 with decimal",
value: float32(1.5),
},
{
name: "float64 with decimal",
value: float64(1.5),
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
value, ok := common.GetAnyAsInt(testCase.value)
if ok {
t.Fatalf("GetAnyAsInt expected to fail but succeeded, got: %v", value)
}
})
}
}
func TestGoodGetAnyAsByte(t *testing.T) {
testCases := []struct {
name string
value any
typedValue byte
}{
{
name: "int",
value: int(42),
typedValue: 42,
},
{
name: "uint",
value: uint(42),
typedValue: 42,
},
{
name: "float32 without decimal",
value: float32(42.0),
typedValue: 42,
},
{
name: "float64 without decimal",
value: float64(42.0),
typedValue: 42,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
value, ok := common.GetAnyAsByte(testCase.value)
if !ok {
t.Fatalf("GetAnyAsByte expected to succeed but failed")
}
if value != testCase.typedValue {
t.Fatalf("GetAnyAsByte expected got %d, expected %d", value, testCase.typedValue)
}
})
}
}
func TestBadGetAnyAsByte(t *testing.T) {
testCases := []struct {
name string
value any
}{
{
name: "string",
value: "value",
},
{
name: "float32 with decimal",
value: float32(1.5),
},
{
name: "float64 with decimal",
value: float64(1.5),
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
value, ok := common.GetAnyAsByte(testCase.value)
if ok {
t.Fatalf("GetAnyAsByte expected to fail but succeeded, got: %v", value)
}
})
}
}
func TestGoodGetAnyAsByteSlice(t *testing.T) {
testCases := []struct {
name string
value any
typedValue []byte
}{
{
name: "byte slice",
value: []byte{1, 2, 3},
typedValue: []byte{1, 2, 3},
},
{
name: "int slice",
value: []int{1, 2, 3},
typedValue: []byte{1, 2, 3},
},
{
name: "uint slice",
value: []uint{1, 2, 3},
typedValue: []byte{1, 2, 3},
},
{
name: "float32 without decimal slice",
value: []float32{1, 2, 3},
typedValue: []byte{1, 2, 3},
},
{
name: "float64 without decimal slice",
value: []float64{1, 2, 3},
typedValue: []byte{1, 2, 3},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
value, ok := common.GetAnyAsByteSlice(testCase.value)
if !ok {
t.Fatalf("GetAnyAsByteSlice expected to succeed but failed")
}
if !slices.Equal(value, testCase.typedValue) {
t.Fatalf("GetAnyAsByteSlice expected got %d, expected %d", value, testCase.typedValue)
}
})
}
}
func TestBadGetAnyAsByteSlice(t *testing.T) {
testCases := []struct {
name string
value any
}{
{
name: "not a slice",
value: "value",
},
{
name: "not a int slice",
value: []any{"value1", 2},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
value, ok := common.GetAnyAsByteSlice(testCase.value)
if ok {
t.Fatalf("GetAnyAsByteSlice expected to fail but succeeded, got: %v", value)
}
})
}
}
func TestGoodGetAnyAsIntSlice(t *testing.T) {
testCases := []struct {
name string
value any
typedValue []int
}{
{
name: "int slice",
value: []int{1, 2, 3},
typedValue: []int{1, 2, 3},
},
{
name: "byte slice",
value: []byte{1, 2, 3},
typedValue: []int{1, 2, 3},
},
{
name: "uint slice",
value: []uint{1, 2, 3},
typedValue: []int{1, 2, 3},
},
{
name: "float32 without decimal slice",
value: []float32{1, 2, 3},
typedValue: []int{1, 2, 3},
},
{
name: "float64 without decimal slice",
value: []float64{1, 2, 3},
typedValue: []int{1, 2, 3},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
value, ok := common.GetAnyAsIntSlice(testCase.value)
if !ok {
t.Fatalf("GetAnyAsIntSlice expected to succeed but failed")
}
if !slices.Equal(value, testCase.typedValue) {
t.Fatalf("GetAnyAsIntSlice expected got %d, expected %d", value, testCase.typedValue)
}
})
}
}
func TestBadGetAnyAsIntSlice(t *testing.T) {
testCases := []struct {
name string
value any
}{
{
name: "not a slice",
value: "value",
},
{
name: "not a int slice",
value: []any{"value1", 2},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
value, ok := common.GetAnyAsIntSlice(testCase.value)
if ok {
t.Fatalf("GetAnyAsIntSlice expected to fail but succeeded, got: %v", value)
}
})
}
}
func TestGoodGetAnyAsFloat32(t *testing.T) {
testCases := []struct {
name string
value any
typedValue float32
}{
{
name: "int",
value: int(42),
typedValue: 42,
},
{
name: "uint",
value: uint(42),
typedValue: 42,
},
{
name: "byte",
value: byte(42),
typedValue: 42,
},
{
name: "float32",
value: float32(42.3),
typedValue: 42.3,
},
{
name: "float64",
value: float64(42.3),
typedValue: 42.3,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
value, ok := common.GetAnyAsFloat32(testCase.value)
if !ok {
t.Fatalf("GetAnyAsFloat32 expected to succeed but failed")
}
if value != testCase.typedValue {
t.Fatalf("GetAnyAsFloat32 expected got %f, expected %f", value, testCase.typedValue)
}
})
}
}
func TestBadGetAnyAsFloat32(t *testing.T) {
testCases := []struct {
name string
value any
}{
{
name: "string",
value: "value",
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
value, ok := common.GetAnyAsFloat32(testCase.value)
if ok {
t.Fatalf("GetAnyAsFloat32 expected to fail but succeeded, got: %v", value)
}
})
}
}
func TestGoodGetAnyAsFloat64(t *testing.T) {
testCases := []struct {
name string
value any
typedValue float64
}{
{
name: "int",
value: int(42),
typedValue: 42,
},
{
name: "uint",
value: uint(42),
typedValue: 42,
},
{
name: "byte",
value: byte(42),
typedValue: 42,
},
{
name: "float32",
value: float32(42.5),
typedValue: 42.5,
},
{
name: "float64",
value: float64(42.5),
typedValue: 42.5,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
value, ok := common.GetAnyAsFloat64(testCase.value)
if !ok {
t.Fatalf("GetAnyAsFloat64 expected to succeed but failed")
}
if value != testCase.typedValue {
t.Fatalf("GetAnyAsFloat64 expected got %f, expected %f", value, testCase.typedValue)
}
})
}
}
func TestBadGetAnyAsFloat64(t *testing.T) {
testCases := []struct {
name string
value any
}{
{
name: "string",
value: "value",
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
value, ok := common.GetAnyAsFloat64(testCase.value)
if ok {
t.Fatalf("GetAnyAsFloat64 expected to fail but succeeded, got: %v", value)
}
})
}
}

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")

26
internal/common/module.go Normal file
View File

@@ -0,0 +1,26 @@
package common
import (
"context"
"database/sql"
)
type Module interface {
Id() string
Type() string
Start(context.Context) error
Stop()
}
type OutputModule interface {
Output(context.Context, any) error
}
type KeyValueModule interface {
Get(key string) (any, error)
Set(key string, value any) error
}
type DatabaseModule interface {
Database() *sql.DB
}

View File

@@ -0,0 +1,40 @@
package common
import (
"context"
)
type WrappedPayload struct {
Payload any
Modules map[string]Module
Sender any
Source string
End bool
}
func GetWrappedPayload(ctx context.Context, payload any) WrappedPayload {
wrappedPayload := WrappedPayload{
Payload: payload,
End: false,
}
modules := ctx.Value(ModulesContextKey)
if modules != nil {
moduleMap, ok := modules.(map[string]Module)
if ok {
wrappedPayload.Modules = moduleMap
} else {
wrappedPayload.Modules = make(map[string]Module)
}
}
sender := ctx.Value(SenderContextKey)
if sender != nil {
wrappedPayload.Sender = sender
}
source := ctx.Value(SourceContextKey)
if source != nil {
wrappedPayload.Source = source.(string)
}
return wrappedPayload
}

View File

@@ -0,0 +1,82 @@
package common_test
import (
"context"
"reflect"
"testing"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/test"
)
func TestGoodGetWrappedPayload(t *testing.T) {
testCases := []struct {
name string
ctx context.Context
payload any
expected common.WrappedPayload
}{
{
name: "basic",
ctx: t.Context(),
payload: "test",
expected: common.WrappedPayload{
Payload: "test",
},
},
{
name: "with modules in context",
ctx: test.GetContextWithModules(t.Context(), map[string]common.Module{}),
payload: "test",
expected: common.WrappedPayload{
Payload: "test",
Modules: map[string]common.Module{},
},
},
{
name: "with sender in context",
ctx: test.GetContextWithSender(t.Context(), "sender"),
payload: "test",
expected: common.WrappedPayload{
Payload: "test",
Sender: "sender",
},
},
{
name: "with source in context",
ctx: test.GetContextWithSource(t.Context(), "source"),
payload: "test",
expected: common.WrappedPayload{
Payload: "test",
Source: "source",
},
},
{
name: "with all fields in context",
ctx: test.GetContextWithSource(
test.GetContextWithSender(
test.GetContextWithModules(t.Context(), map[string]common.Module{}),
"sender",
),
"source",
),
payload: "test",
expected: common.WrappedPayload{
Payload: "test",
Modules: map[string]common.Module{},
Sender: "sender",
Source: "source",
},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
wrappedPayload := common.GetWrappedPayload(testCase.ctx, testCase.payload)
if !reflect.DeepEqual(wrappedPayload, testCase.expected) {
t.Fatalf("GetWrappedPayload expected got %+v, expected %+v", wrappedPayload, testCase.expected)
}
})
}
}

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"`
}

6
internal/config/api.go Normal file
View File

@@ -0,0 +1,6 @@
package config
type ApiConfig struct {
Enabled bool `json:"enabled"`
Port int `json:"port"`
}

View File

@@ -1,23 +1,7 @@
package config package config
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 ModuleConfig struct {
Id string `json:"id"`
Type string `json:"type"`
Params map[string]any `json:"params"`
}
type RouteConfig struct {
Input string `json:"input"`
Processors []ProcessorConfig `json:"processors"`
Output string `json:"output"`
}
type ProcessorConfig struct {
Type string `json:"type"`
Params map[string]any `json:"params"`
}

View File

@@ -0,0 +1,7 @@
package config
type ModuleConfig struct {
Id string `json:"id"`
Type string `json:"type"`
Params Params `json:"params,omitempty"`
}

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

@@ -0,0 +1,140 @@
package config
import (
"errors"
"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")
ErrParamNotStringSlice = errors.New("not a string 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, ErrParamNotStringSlice
}
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

@@ -0,0 +1,628 @@
package config_test
import (
"encoding/json"
"errors"
"slices"
"testing"
"github.com/jwetzell/showbridge-go/internal/config"
)
func TestGoodStringParamsJSON(t *testing.T) {
testCases := []struct {
name string
paramsJSON string
key string
expected string
}{
{
name: "string param",
paramsJSON: `{"key": "value"}`,
key: "key",
expected: "value",
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
params := config.Params{}
err := json.Unmarshal([]byte(testCase.paramsJSON), &params)
if err != nil {
t.Fatalf("Failed to unmarshal params JSON: %v", err)
}
value, err := params.GetString(testCase.key)
if err != nil {
t.Fatalf("GetString returned error: %v", err)
}
if value != testCase.expected {
t.Fatalf("GetString got %s, expected %s", value, testCase.expected)
}
})
}
}
func TestBadStringParamsJSON(t *testing.T) {
testCases := []struct {
name string
paramsJSON string
key string
returnError error
}{
{
name: "key not found",
paramsJSON: `{"key": "value"}`,
key: "test",
returnError: config.ErrParamNotFound,
},
{
name: "not a string",
paramsJSON: `{"key": 1}`,
key: "key",
returnError: config.ErrParamNotString,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
params := config.Params{}
err := json.Unmarshal([]byte(testCase.paramsJSON), &params)
if err != nil {
t.Fatalf("Failed to unmarshal params JSON: %v", err)
}
value, err := params.GetString(testCase.key)
if err == nil {
t.Fatalf("GetString expected to fail but succeeded, got: %v", value)
}
if !errors.Is(err, testCase.returnError) {
t.Fatalf("GetString got error '%s', expected '%s'", err, testCase.returnError)
}
})
}
}
func TestGoodIntParamsJSON(t *testing.T) {
testCases := []struct {
name string
paramsJSON string
key string
expected int
}{
{
name: "int param",
paramsJSON: `{"key": 1}`,
key: "key",
expected: 1,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
params := config.Params{}
err := json.Unmarshal([]byte(testCase.paramsJSON), &params)
if err != nil {
t.Fatalf("Failed to unmarshal params JSON: %v", err)
}
value, err := params.GetInt(testCase.key)
if err != nil {
t.Fatalf("GetInt returned error: %v", err)
}
if value != testCase.expected {
t.Fatalf("GetInt got %d, expected %d", value, testCase.expected)
}
})
}
}
func TestBadIntParamsJSON(t *testing.T) {
testCases := []struct {
name string
paramsJSON string
key string
returnError error
}{
{
name: "key not found",
paramsJSON: `{"key": 1}`,
key: "test",
returnError: config.ErrParamNotFound,
},
{
name: "not a number",
paramsJSON: `{"key": "1"}`,
key: "key",
returnError: config.ErrParamNotNumber,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
params := config.Params{}
err := json.Unmarshal([]byte(testCase.paramsJSON), &params)
if err != nil {
t.Fatalf("Failed to unmarshal params JSON: %v", err)
}
value, err := params.GetInt(testCase.key)
if err == nil {
t.Fatalf("GetInt expected to fail but succeeded, got: %v", value)
}
if !errors.Is(err, testCase.returnError) {
t.Fatalf("GetInt got error '%s', expected '%s'", err, testCase.returnError)
}
})
}
}
func TestGoodFloat32ParamsJSON(t *testing.T) {
testCases := []struct {
name string
paramsJSON string
key string
expected float32
}{
{
name: "no decimal param",
paramsJSON: `{"key": 1}`,
key: "key",
expected: 1,
},
{
name: "float param",
paramsJSON: `{"key": 1.23}`,
key: "key",
expected: 1.23,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
params := config.Params{}
err := json.Unmarshal([]byte(testCase.paramsJSON), &params)
if err != nil {
t.Fatalf("Failed to unmarshal params JSON: %v", err)
}
value, err := params.GetFloat32(testCase.key)
if err != nil {
t.Fatalf("GetFloat32 returned error: %v", err)
}
if value != testCase.expected {
t.Fatalf("GetFloat32 got %f, expected %f", value, testCase.expected)
}
})
}
}
func TestBadFloat32ParamsJSON(t *testing.T) {
testCases := []struct {
name string
paramsJSON string
key string
returnError error
}{
{
name: "key not found",
paramsJSON: `{"key": 1}`,
key: "test",
returnError: config.ErrParamNotFound,
},
{
name: "not a number",
paramsJSON: `{"key": "1"}`,
key: "key",
returnError: config.ErrParamNotNumber,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
params := config.Params{}
err := json.Unmarshal([]byte(testCase.paramsJSON), &params)
if err != nil {
t.Fatalf("Failed to unmarshal params JSON: %v", err)
}
value, err := params.GetFloat32(testCase.key)
if err == nil {
t.Fatalf("GetFloat32 expected to fail but succeeded, got: %v", value)
}
if !errors.Is(err, testCase.returnError) {
t.Fatalf("GetFloat32 got error '%s', expected '%s'", err, testCase.returnError)
}
})
}
}
func TestGoodFloat64ParamsJSON(t *testing.T) {
testCases := []struct {
name string
paramsJSON string
key string
expected float64
}{
{
name: "no decimal param",
paramsJSON: `{"key": 1}`,
key: "key",
expected: 1,
},
{
name: "float param",
paramsJSON: `{"key": 1.23}`,
key: "key",
expected: 1.23,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
params := config.Params{}
err := json.Unmarshal([]byte(testCase.paramsJSON), &params)
if err != nil {
t.Fatalf("Failed to unmarshal params JSON: %v", err)
}
value, err := params.GetFloat64(testCase.key)
if err != nil {
t.Fatalf("GetFloat64 returned error: %v", err)
}
if value != testCase.expected {
t.Fatalf("GetFloat64 got %f, expected %f", value, testCase.expected)
}
})
}
}
func TestBadFloat64ParamsJSON(t *testing.T) {
testCases := []struct {
name string
paramsJSON string
key string
returnError error
}{
{
name: "key not found",
paramsJSON: `{"key": 1}`,
key: "test",
returnError: config.ErrParamNotFound,
},
{
name: "not a number",
paramsJSON: `{"key": "1"}`,
key: "key",
returnError: config.ErrParamNotNumber,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
params := config.Params{}
err := json.Unmarshal([]byte(testCase.paramsJSON), &params)
if err != nil {
t.Fatalf("Failed to unmarshal params JSON: %v", err)
}
value, err := params.GetFloat64(testCase.key)
if err == nil {
t.Fatalf("GetFloat64 expected to fail but succeeded, got: %v", value)
}
if !errors.Is(err, testCase.returnError) {
t.Fatalf("GetFloat64 got error '%s', expected '%s'", err, testCase.returnError)
}
})
}
}
func TestGoodBoolParamsJSON(t *testing.T) {
testCases := []struct {
name string
paramsJSON string
key string
expected bool
}{
{
name: "bool param",
paramsJSON: `{"key": true}`,
key: "key",
expected: true,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
params := config.Params{}
err := json.Unmarshal([]byte(testCase.paramsJSON), &params)
if err != nil {
t.Fatalf("Failed to unmarshal params JSON: %v", err)
}
value, err := params.GetBool(testCase.key)
if err != nil {
t.Fatalf("GetBool returned error: %v", err)
}
if value != testCase.expected {
t.Fatalf("GetBool got %t, expected %t", value, testCase.expected)
}
})
}
}
func TestBadBoolParamsJSON(t *testing.T) {
testCases := []struct {
name string
paramsJSON string
key string
returnError error
}{
{
name: "key not found",
paramsJSON: `{"key": 1}`,
key: "test",
returnError: config.ErrParamNotFound,
},
{
name: "not a bool",
paramsJSON: `{"key": "1"}`,
key: "key",
returnError: config.ErrParamNotBool,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
params := config.Params{}
err := json.Unmarshal([]byte(testCase.paramsJSON), &params)
if err != nil {
t.Fatalf("Failed to unmarshal params JSON: %v", err)
}
value, err := params.GetBool(testCase.key)
if err == nil {
t.Fatalf("GetBool expected to fail but succeeded, got: %v", value)
}
if !errors.Is(err, testCase.returnError) {
t.Fatalf("GetBool got error '%s', expected '%s'", err, testCase.returnError)
}
})
}
}
func TestGoodStringSliceParamsJSON(t *testing.T) {
testCases := []struct {
name string
paramsJSON string
key string
expected []string
}{
{
name: "string array",
paramsJSON: `{"key": ["value1", "value2"]}`,
key: "key",
expected: []string{"value1", "value2"},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
params := config.Params{}
err := json.Unmarshal([]byte(testCase.paramsJSON), &params)
if err != nil {
t.Fatalf("Failed to unmarshal params JSON: %v", err)
}
value, err := params.GetStringSlice(testCase.key)
if err != nil {
t.Fatalf("GetStringSlice returned error: %v", err)
}
if !slices.Equal(value, testCase.expected) {
t.Fatalf("GetStringSlice got %v, expected %v", value, testCase.expected)
}
})
}
}
func TestBadStringSliceParamsJSON(t *testing.T) {
testCases := []struct {
name string
paramsJSON string
key string
returnError error
}{
{
name: "key not found",
paramsJSON: `{"key": ["value1", "value2"]}`,
key: "test",
returnError: config.ErrParamNotFound,
},
{
name: "not a slice",
paramsJSON: `{"key": "value"}`,
key: "key",
returnError: config.ErrParamNotSlice,
},
{
name: "not a string slice",
paramsJSON: `{"key": ["value1", 2]}`,
key: "key",
returnError: config.ErrParamNotStringSlice,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
params := config.Params{}
err := json.Unmarshal([]byte(testCase.paramsJSON), &params)
if err != nil {
t.Fatalf("Failed to unmarshal params JSON: %v", err)
}
value, err := params.GetStringSlice(testCase.key)
if err == nil {
t.Fatalf("GetStringSlice expected to fail but succeeded, got: %v", value)
}
if !errors.Is(err, testCase.returnError) {
t.Fatalf("GetStringSlice got error '%s', expected '%s'", err, testCase.returnError)
}
})
}
}
func TestGoodIntSliceParamsJSON(t *testing.T) {
testCases := []struct {
name string
paramsJSON string
key string
expected []int
}{
{
name: "int array",
paramsJSON: `{"key": [1, 2, 3]}`,
key: "key",
expected: []int{1, 2, 3},
},
{
name: "int array with floats",
paramsJSON: `{"key": [1.0, 2.0, 3.0]}`,
key: "key",
expected: []int{1, 2, 3},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
params := config.Params{}
err := json.Unmarshal([]byte(testCase.paramsJSON), &params)
if err != nil {
t.Fatalf("Failed to unmarshal params JSON: %v", err)
}
value, err := params.GetIntSlice(testCase.key)
if err != nil {
t.Fatalf("GetIntSlice returned error: %v", err)
}
if !slices.Equal(value, testCase.expected) {
t.Fatalf("GetIntSlice got %v, expected %v", value, testCase.expected)
}
})
}
}
func TestBadIntSliceParamsJSON(t *testing.T) {
testCases := []struct {
name string
paramsJSON string
key string
returnError error
}{
{
name: "key not found",
paramsJSON: `{"key": ["value1", "value2"]}`,
key: "test",
returnError: config.ErrParamNotFound,
},
{
name: "not a slice",
paramsJSON: `{"key": "value"}`,
key: "key",
returnError: config.ErrParamNotIntSlice,
},
{
name: "not a int slice",
paramsJSON: `{"key": ["value1", 2]}`,
key: "key",
returnError: config.ErrParamNotIntSlice,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
params := config.Params{}
err := json.Unmarshal([]byte(testCase.paramsJSON), &params)
if err != nil {
t.Fatalf("Failed to unmarshal params JSON: %v", err)
}
value, err := params.GetIntSlice(testCase.key)
if err == nil {
t.Fatalf("GetIntSlice expected to fail but succeeded, got: %v", value)
}
if !errors.Is(err, testCase.returnError) {
t.Fatalf("GetIntSlice got error '%s', expected '%s'", err, testCase.returnError)
}
})
}
}
func TestGoodByteSliceParamsJSON(t *testing.T) {
testCases := []struct {
name string
paramsJSON string
key string
expected []byte
}{
{
name: "byte array",
paramsJSON: `{"key": [1,2,3,4]}`,
key: "key",
expected: []byte{1, 2, 3, 4},
},
{
name: "byte array with floats",
paramsJSON: `{"key": [1.0,2.0,3.0,4.0]}`,
key: "key",
expected: []byte{1, 2, 3, 4},
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
params := config.Params{}
err := json.Unmarshal([]byte(testCase.paramsJSON), &params)
if err != nil {
t.Fatalf("Failed to unmarshal params JSON: %v", err)
}
value, err := params.GetByteSlice(testCase.key)
if err != nil {
t.Fatalf("GetByteSlice returned error: %v", err)
}
if !slices.Equal(value, testCase.expected) {
t.Fatalf("GetByteSlice got %v, expected %v", value, testCase.expected)
}
})
}
}
func TestBadByteSliceParamsJSON(t *testing.T) {
testCases := []struct {
name string
paramsJSON string
key string
returnError error
}{
{
name: "key not found",
paramsJSON: `{"key": ["value1", "value2"]}`,
key: "test",
returnError: config.ErrParamNotFound,
},
{
name: "not a slice",
paramsJSON: `{"key": "value"}`,
key: "key",
returnError: config.ErrParamNotByteSlice,
},
{
name: "not a int slice",
paramsJSON: `{"key": ["value1", 2]}`,
key: "key",
returnError: config.ErrParamNotByteSlice,
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
params := config.Params{}
err := json.Unmarshal([]byte(testCase.paramsJSON), &params)
if err != nil {
t.Fatalf("Failed to unmarshal params JSON: %v", err)
}
value, err := params.GetByteSlice(testCase.key)
if err == nil {
t.Fatalf("GetByteSlice expected to fail but succeeded, got: %v", value)
}
if !errors.Is(err, testCase.returnError) {
t.Fatalf("GetByteSlice got error '%s', expected '%s'", err, testCase.returnError)
}
})
}
}

View File

@@ -0,0 +1,6 @@
package config
type ProcessorConfig struct {
Type string `json:"type"`
Params Params `json:"params,omitempty"`
}

6
internal/config/route.go Normal file
View File

@@ -0,0 +1,6 @@
package config
type RouteConfig struct {
Input string `json:"input"`
Processors []ProcessorConfig `json:"processors"`
}

View File

@@ -0,0 +1,89 @@
package module
import (
"context"
"database/sql"
"errors"
"fmt"
"log/slog"
"github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
_ "modernc.org/sqlite"
)
type DbSqlite struct {
config config.ModuleConfig
Dsn string
ctx context.Context
router common.RouteIO
db *sql.DB
logger *slog.Logger
}
func init() {
RegisterModule(ModuleRegistration{
Type: "db.sqlite",
Title: "SQLite Database",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"dsn": {
Type: "string",
MinLength: jsonschema.Ptr(1),
},
},
Required: []string{"dsn"},
},
New: func(config config.ModuleConfig) (common.Module, error) {
params := config.Params
dsnString, err := params.GetString("dsn")
if err != nil {
return nil, fmt.Errorf("db.sqlite dsn error: %w", err)
}
return &DbSqlite{Dsn: dsnString, config: config, logger: CreateLogger(config)}, nil
},
})
}
func (t *DbSqlite) Id() string {
return t.config.Id
}
func (t *DbSqlite) Type() string {
return t.config.Type
}
func (t *DbSqlite) Start(ctx context.Context) error {
t.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("db.sqlite unable to get router from context")
}
t.router = router
t.ctx = ctx
db, err := sql.Open("sqlite", t.Dsn)
if err != nil {
return fmt.Errorf("db.sqlite error opening database: %w", err)
}
t.db = db
defer t.db.Close()
<-ctx.Done()
return nil
}
func (t *DbSqlite) Stop() {
if t.db != nil {
t.db.Close()
}
}
func (t *DbSqlite) Database() *sql.DB {
return t.db
}

View File

@@ -1,89 +0,0 @@
package module
import (
"context"
"errors"
"log/slog"
"net/http"
"time"
"github.com/jwetzell/showbridge-go/internal/config"
"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(route.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 := payload.(*http.Request)
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()
}

View File

@@ -6,18 +6,20 @@ import (
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
"net"
"net/http" "net/http"
"github.com/google/jsonschema-go/jsonschema"
"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
} }
@@ -54,20 +56,27 @@ func (hsrw *HTTPServerResponseWriter) Write(data []byte) (int, error) {
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "http.server", Type: "http.server",
New: func(config config.ModuleConfig) (Module, error) { Title: "HTTP Server",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"port": {
Title: "Port",
Type: "integer",
Minimum: jsonschema.Ptr[float64](1024),
Maximum: jsonschema.Ptr[float64](65535),
},
},
Required: []string{"port"},
AdditionalProperties: nil,
},
New: func(config config.ModuleConfig) (common.Module, error) {
params := config.Params params := config.Params
port, ok := params["port"] portNum, err := params.GetInt("port")
if !ok { if err != nil {
return nil, errors.New("http.server requires a port parameter") return nil, fmt.Errorf("http.server port error: %w", err)
} }
portNum, ok := port.(float64)
if !ok {
return nil, errors.New("http.server port must be uint16")
}
return &HTTPServer{Port: uint16(portNum), config: config, logger: CreateLogger(config)}, nil return &HTTPServer{Port: uint16(portNum), config: config, logger: CreateLogger(config)}, nil
}, },
}) })
@@ -90,6 +99,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 {
@@ -149,7 +162,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(route.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")
@@ -189,7 +202,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 := payload.(processor.HTTPResponse) 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

@@ -8,8 +8,9 @@ import (
"fmt" "fmt"
"log/slog" "log/slog"
"github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/route"
"gitlab.com/gomidi/midi/v2" "gitlab.com/gomidi/midi/v2"
_ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv" _ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
) )
@@ -17,7 +18,7 @@ import (
type MIDIInput struct { type MIDIInput struct {
config config.ModuleConfig config config.ModuleConfig
ctx context.Context ctx context.Context
router 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
@@ -26,19 +27,24 @@ type MIDIInput struct {
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "midi.input", Type: "midi.input",
New: func(config config.ModuleConfig) (Module, error) { Title: "MIDI Input",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"port": {
Title: "Port",
Type: "string",
},
},
Required: []string{"port"},
AdditionalProperties: nil,
},
New: func(config config.ModuleConfig) (common.Module, error) {
params := config.Params params := config.Params
port, ok := params["port"] portString, err := params.GetString("port")
if err != nil {
if !ok { return nil, fmt.Errorf("midi.input port error: %w", err)
return nil, errors.New("midi.input requires a port parameter")
}
portString, ok := port.(string)
if !ok {
return nil, errors.New("midi.input port must be a string")
} }
return &MIDIInput{config: config, Port: portString, logger: CreateLogger(config)}, nil return &MIDIInput{config: config, Port: portString, logger: CreateLogger(config)}, nil
@@ -57,7 +63,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(route.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")
@@ -89,10 +95,6 @@ func (mi *MIDIInput) Start(ctx context.Context) error {
return nil return nil
} }
func (mi *MIDIInput) Output(ctx context.Context, payload any) error {
return errors.New("midi.input output is not implemented")
}
func (mi *MIDIInput) Stop() { func (mi *MIDIInput) Stop() {
mi.cancel() mi.cancel()
} }

View File

@@ -8,8 +8,9 @@ import (
"fmt" "fmt"
"log/slog" "log/slog"
"github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/route"
"gitlab.com/gomidi/midi/v2" "gitlab.com/gomidi/midi/v2"
_ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv" _ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
) )
@@ -17,7 +18,7 @@ import (
type 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
@@ -26,20 +27,25 @@ type MIDIOutput struct {
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "midi.output", Type: "midi.output",
New: func(config config.ModuleConfig) (Module, error) { Title: "MIDI Output",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"port": {
Title: "Port",
Type: "string",
},
},
Required: []string{"port"},
AdditionalProperties: nil,
},
New: func(config config.ModuleConfig) (common.Module, error) {
params := config.Params params := config.Params
port, ok := params["port"] portString, err := params.GetString("port")
if err != nil {
if !ok { return nil, fmt.Errorf("midi.output port error: %w", err)
return nil, errors.New("midi.output requires a port parameter")
}
portString, ok := port.(string)
if !ok {
return nil, errors.New("midi.output port must be a string")
} }
return &MIDIOutput{config: config, Port: portString, logger: CreateLogger(config)}, nil return &MIDIOutput{config: config, Port: portString, logger: CreateLogger(config)}, nil
@@ -58,7 +64,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(route.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")
@@ -91,10 +97,10 @@ 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 := payload.(midi.Message) 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 output midi.Message")
} }
return mo.SendFunc(payloadMessage) return mo.SendFunc(payloadMessage)

View File

@@ -1,31 +1,27 @@
package module package module
import ( import (
"context"
"fmt" "fmt"
"log/slog" "log/slog"
"sync" "sync"
"github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
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 {
Id() string
Type() string
Start(context.Context) error
Stop()
Output(context.Context, any) error
} }
type ModuleRegistration struct { type ModuleRegistration struct {
Type string `json:"type"` Type string `json:"type"`
New func(config.ModuleConfig) (Module, error) Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
ParamsSchema *jsonschema.Schema `json:"paramsSchema,omitempty"`
New func(config.ModuleConfig) (common.Module, error)
} }
func RegisterModule(mod ModuleRegistration) { func RegisterModule(mod ModuleRegistration) {

View File

@@ -3,17 +3,19 @@ package module
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"log/slog" "log/slog"
mqtt "github.com/eclipse/paho.mqtt.golang" mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/google/jsonschema-go/jsonschema"
"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 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
@@ -24,43 +26,45 @@ type MQTTClient struct {
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "mqtt.client", Type: "mqtt.client",
New: func(config config.ModuleConfig) (Module, error) { Title: "MQTT Client",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"broker": {
Title: "Broker URL",
Type: "string",
},
"topic": {
Title: "Topic",
Type: "string",
},
"clientId": {
Title: "Client ID",
Type: "string",
},
},
Required: []string{"broker", "topic", "clientId"},
AdditionalProperties: nil,
},
New: func(config config.ModuleConfig) (common.Module, error) {
params := config.Params params := config.Params
broker, ok := params["broker"] brokerString, err := params.GetString("broker")
if !ok { if err != nil {
return nil, errors.New("mqtt.client requires a broker parameter") return nil, fmt.Errorf("mqtt.client broker error: %w", err)
} }
brokerString, ok := broker.(string) topicString, err := params.GetString("topic")
if !ok { if err != nil {
return nil, errors.New("mqtt.client broker must be string") return nil, fmt.Errorf("mqtt.client topic error: %w", err)
} }
topic, ok := params["topic"] clientIdString, err := params.GetString("clientId")
if !ok { if err != nil {
return nil, errors.New("mqtt.client requires a topic parameter") return nil, fmt.Errorf("mqtt.client clientId error: %w", err)
}
topicString, ok := topic.(string)
if !ok {
return nil, errors.New("mqtt.client topic must be string")
}
clientId, ok := params["clientId"]
if !ok {
return nil, errors.New("mqtt.client requires a clientId parameter")
}
clientIdString, ok := clientId.(string)
if !ok {
return nil, errors.New("mqtt.client clientId must be string")
} }
return &MQTTClient{config: config, Broker: brokerString, Topic: topicString, ClientID: clientIdString, logger: CreateLogger(config)}, nil return &MQTTClient{config: config, Broker: brokerString, Topic: topicString, ClientID: clientIdString, logger: CreateLogger(config)}, nil
@@ -78,7 +82,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(route.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")
@@ -118,7 +122,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 := payload.(mqtt.Message) 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

@@ -5,16 +5,17 @@ import (
"errors" "errors"
"log/slog" "log/slog"
"github.com/google/jsonschema-go/jsonschema"
"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
@@ -24,31 +25,34 @@ type NATSClient struct {
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "nats.client", Type: "nats.client",
New: func(config config.ModuleConfig) (Module, error) { Title: "NATS Client",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"url": {
Title: "NATS Server URL",
Type: "string",
},
"subject": {
Title: "Subject",
Type: "string",
},
},
Required: []string{"url", "subject"},
AdditionalProperties: nil,
},
New: func(config config.ModuleConfig) (common.Module, error) {
params := config.Params params := config.Params
url, ok := params["url"] urlString, err := params.GetString("url")
if err != nil {
if !ok { return nil, errors.New("nats.client url error: " + err.Error())
return nil, errors.New("nats.client requires a url parameter")
} }
urlString, ok := url.(string) subjectString, err := params.GetString("subject")
if !ok { if err != nil {
return nil, errors.New("nats.client url must be string") return nil, errors.New("nats.client subject error: " + err.Error())
}
subject, ok := params["subject"]
if !ok {
return nil, errors.New("nats.client requires a subject parameter")
}
subjectString, ok := subject.(string)
if !ok {
return nil, errors.New("nats.client subject must be string")
} }
return &NATSClient{config: config, URL: urlString, Subject: subjectString, logger: CreateLogger(config)}, nil return &NATSClient{config: config, URL: urlString, Subject: subjectString, logger: CreateLogger(config)}, nil
@@ -66,7 +70,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(route.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")
@@ -107,7 +111,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 := payload.(processor.NATSMessage) 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

@@ -0,0 +1,133 @@
package module
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
"net"
"time"
"github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/nats-io/nats-server/v2/server"
)
type NATSServer struct {
config config.ModuleConfig
ctx context.Context
Ip string
Port int
router common.RouteIO
server *server.Server
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
RegisterModule(ModuleRegistration{
Type: "nats.server",
Title: "NATS Server",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"ip": {
Title: "IP",
Type: "string",
Default: json.RawMessage(`"0.0.0.0"`),
},
"port": {
Title: "Port",
Type: "integer",
Minimum: jsonschema.Ptr[float64](1024),
Maximum: jsonschema.Ptr[float64](65535),
Default: json.RawMessage(`4222`),
},
},
Required: []string{},
AdditionalProperties: nil,
},
New: func(moduleConfig config.ModuleConfig) (common.Module, error) {
params := moduleConfig.Params
portNum, err := params.GetInt("port")
if err != nil {
if errors.Is(err, config.ErrParamNotFound) {
portNum = 4222
} else {
return nil, fmt.Errorf("nats.server port error: %w", err)
}
}
ipString, err := params.GetString("ip")
if err != nil {
if errors.Is(err, config.ErrParamNotFound) {
ipString = "0.0.0.0"
} else {
return nil, fmt.Errorf("nats.server ip error: %w", err)
}
}
_, err = net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", ipString, uint16(portNum)))
if err != nil {
return nil, err
}
return &NATSServer{config: moduleConfig, logger: CreateLogger(moduleConfig), Ip: ipString, Port: portNum}, nil
},
})
}
func (ns *NATSServer) Id() string {
return ns.config.Id
}
func (ns *NATSServer) Type() string {
return ns.config.Type
}
func (ns *NATSServer) Start(ctx context.Context) error {
ns.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("nats.server unable to get router from context")
}
ns.router = router
moduleContext, cancel := context.WithCancel(ctx)
ns.ctx = moduleContext
ns.cancel = cancel
natsServer, err := server.NewServer(&server.Options{
Host: ns.Ip,
Port: ns.Port,
NoLog: true,
})
if err != nil {
return err
}
ns.server = natsServer
natsServer.Start()
defer natsServer.Shutdown()
if !natsServer.ReadyForConnections(5 * time.Second) {
return errors.New("nats.server failed to start")
}
ns.logger.Info("NATS server started", "client_url", natsServer.ClientURL())
<-ns.ctx.Done()
ns.logger.Debug("done")
return nil
}
func (ns *NATSServer) Stop() {
ns.cancel()
if ns.server != nil {
ns.server.Shutdown()
}
}

View File

@@ -3,21 +3,20 @@ package module
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"log/slog" "log/slog"
"net" "net"
"time" "time"
"github.com/jwetzell/psn-go" "github.com/jwetzell/psn-go"
"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
@@ -25,8 +24,9 @@ type PSNClient struct {
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "psn.client", Type: "psn.client",
New: func(config config.ModuleConfig) (Module, error) { Title: "PosiStageNet Client",
New: func(config config.ModuleConfig) (common.Module, error) {
return &PSNClient{config: config, decoder: psn.NewDecoder(), logger: CreateLogger(config)}, nil return &PSNClient{config: config, decoder: psn.NewDecoder(), logger: CreateLogger(config)}, nil
}, },
@@ -43,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(route.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")
@@ -105,10 +105,6 @@ func (pc *PSNClient) Start(ctx context.Context) error {
} }
} }
func (pc *PSNClient) Output(ctx context.Context, payload any) error {
return fmt.Errorf("psn.client output is not implemented")
}
func (pc *PSNClient) Stop() { func (pc *PSNClient) Stop() {
pc.cancel() pc.cancel()
} }

View File

@@ -0,0 +1,126 @@
package module
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/redis/go-redis/v9"
)
type RedisClient struct {
config config.ModuleConfig
ctx context.Context
router common.RouteIO
Host string
Port uint16
client *redis.Client
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
RegisterModule(ModuleRegistration{
Type: "redis.client",
Title: "Redis Client",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"host": {
Type: "string",
},
"port": {
Type: "integer",
Minimum: jsonschema.Ptr[float64](1),
Maximum: jsonschema.Ptr[float64](65535),
},
},
Required: []string{"host", "port"},
AdditionalProperties: nil,
},
New: func(config config.ModuleConfig) (common.Module, error) {
params := config.Params
hostString, err := params.GetString("host")
if err != nil {
return nil, errors.New("redis.client host error: " + err.Error())
}
portInt, err := params.GetInt("port")
if err != nil {
return nil, errors.New("redis.client port error: " + err.Error())
}
return &RedisClient{config: config, Host: hostString, Port: uint16(portInt), logger: CreateLogger(config)}, nil
},
})
}
func (rc *RedisClient) Id() string {
return rc.config.Id
}
func (rc *RedisClient) Type() string {
return rc.config.Type
}
func (rc *RedisClient) Printf(ctx context.Context, format string, v ...interface{}) {
msg := fmt.Sprintf(format, v...)
rc.logger.Debug(msg)
}
func (rc *RedisClient) Start(ctx context.Context) error {
redis.SetLogger(rc)
rc.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("redis.client unable to get router from context")
}
rc.router = router
moduleContext, cancel := context.WithCancel(ctx)
rc.ctx = moduleContext
rc.cancel = cancel
client := redis.NewClient(&redis.Options{
Addr: fmt.Sprintf("%s:%d", rc.Host, rc.Port),
Password: "",
DB: 0,
})
rc.client = client
defer client.Close()
<-rc.ctx.Done()
rc.logger.Debug("done")
return nil
}
func (rc *RedisClient) Stop() {
rc.cancel()
}
func (rc *RedisClient) Get(key string) (any, error) {
if rc.client != nil {
val, err := rc.client.Get(rc.ctx, key).Result()
if err != nil {
return nil, err
}
return val, nil
}
return nil, errors.New("redis.client not setup")
}
func (rc *RedisClient) Set(key string, value any) error {
if rc.client != nil {
status := rc.client.Set(rc.ctx, key, value, 0)
return status.Err()
}
return errors.New("redis.client not setup")
}

View File

@@ -9,16 +9,17 @@ import (
"log/slog" "log/slog"
"time" "time"
"github.com/google/jsonschema-go/jsonschema"
"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/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
@@ -29,51 +30,48 @@ type SerialClient struct {
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "serial.client", Type: "serial.client",
New: func(config config.ModuleConfig) (Module, error) { Title: "Serial Client",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"port": {
Title: "Port",
Type: "string",
},
"baudRate": {
Title: "Baud Rate",
Type: "integer",
},
},
Required: []string{"port", "baudRate"},
AdditionalProperties: nil,
},
New: func(config config.ModuleConfig) (common.Module, error) {
params := config.Params params := config.Params
port, ok := params["port"] portString, err := params.GetString("port")
if err != nil {
if !ok { return nil, fmt.Errorf("serial.client port error: %w", err)
return nil, errors.New("serial.client requires a port parameter")
} }
portString, ok := port.(string) framingMethodString, err := params.GetString("framing")
if err != nil {
if !ok { return nil, fmt.Errorf("serial.client framing error: %w", err)
return nil, errors.New("serial.client port must be a string")
}
framingMethod, ok := params["framing"]
if !ok {
return nil, errors.New("serial.client requires a framing parameter")
}
framingMethodString, ok := framingMethod.(string)
if !ok {
return nil, errors.New("serial.client framing method must be a string")
} }
framer := framer.GetFramer(framingMethodString) framer := framer.GetFramer(framingMethodString)
if framer == nil { if framer == nil {
return nil, fmt.Errorf("serial.client unknown framing method: %s", framingMethod) return nil, fmt.Errorf("serial.client unknown framing method: %s", framingMethodString)
} }
buadRate, ok := params["baudRate"] baudRateInt, err := params.GetInt("baudRate")
if !ok { if err != nil {
return nil, errors.New("serial.client requires a baudRate parameter") return nil, fmt.Errorf("serial.client baudRate error: %w", err)
}
baudRateNum, ok := buadRate.(float64)
if !ok {
return nil, errors.New("serial.client baudRate must be a number")
} }
mode := serial.Mode{ mode := serial.Mode{
BaudRate: int(baudRateNum), BaudRate: baudRateInt,
} }
return &SerialClient{config: config, Port: portString, Framer: framer, Mode: &mode, logger: CreateLogger(config)}, nil return &SerialClient{config: config, Port: portString, Framer: framer, Mode: &mode, logger: CreateLogger(config)}, nil
@@ -103,7 +101,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(route.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")
@@ -175,10 +173,10 @@ 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 := payload.([]byte) payloadBytes, ok := common.GetAnyAsByteSlice(payload)
if !ok { if !ok {
return errors.New("serial.client can only ouptut bytes") return errors.New("serial.client can only output bytes")
} }
_, err := sc.port.Write(sc.Framer.Encode(payloadBytes)) _, err := sc.port.Write(sc.Framer.Encode(payloadBytes))

View File

@@ -2,6 +2,7 @@ package module
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@@ -14,15 +15,16 @@ import (
"github.com/emiago/diago/media" "github.com/emiago/diago/media"
"github.com/emiago/sipgo" "github.com/emiago/sipgo"
"github.com/emiago/sipgo/sip" "github.com/emiago/sipgo/sip"
"github.com/google/jsonschema-go/jsonschema"
"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
@@ -45,61 +47,78 @@ type sipCallContextKey string
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "sip.call.server", Type: "sip.call.server",
New: func(config config.ModuleConfig) (Module, error) { Title: "SIP Call Server",
params := config.Params ParamsSchema: &jsonschema.Schema{
portNum := 5060 Type: "object",
Properties: map[string]*jsonschema.Schema{
"ip": {
Title: "IP",
Type: "string",
Default: json.RawMessage(`"0.0.0.0"`),
},
"port": {
Title: "Port",
Type: "integer",
Minimum: jsonschema.Ptr[float64](1024),
Maximum: jsonschema.Ptr[float64](65535),
Default: json.RawMessage(`5060`),
},
"transport": {
Title: "Transport",
Type: "string",
Enum: []any{"udp", "tcp", "ws", "udp4", "tcp4"},
Default: json.RawMessage(`"udp"`),
},
"userAgent": {
Title: "User Agent",
Type: "string",
Default: json.RawMessage(`"showbridge"`),
},
},
Required: []string{},
AdditionalProperties: nil,
},
New: func(moduleConfig config.ModuleConfig) (common.Module, error) {
params := moduleConfig.Params
portNum, err := params.GetInt("port")
if err != nil {
port, ok := params["port"] if errors.Is(err, config.ErrParamNotFound) {
if ok { portNum = 5060
specificPortNum, ok := port.(float64) } else {
return nil, fmt.Errorf("sip.call.server port error: %w", err)
if !ok {
return nil, errors.New("sip.call.server port must be a number")
} }
portNum = int(specificPortNum)
} }
ipString := "0.0.0.0" ipString, err := params.GetString("ip")
if err != nil {
ip, ok := params["ip"] if errors.Is(err, config.ErrParamNotFound) {
if ok { ipString = "0.0.0.0"
} else {
specificIpString, ok := ip.(string) return nil, fmt.Errorf("sip.call.server ip error: %w", err)
if !ok {
return nil, errors.New("sip.call.server ip must be a string")
} }
ipString = specificIpString
} }
transportString := "udp" transportString, err := params.GetString("transport")
if err != nil {
transport, ok := params["transport"] if errors.Is(err, config.ErrParamNotFound) {
if ok { transportString = "udp"
} else {
specificTransportString, ok := transport.(string) return nil, fmt.Errorf("sip.call.server transport error: %w", err)
if !ok {
return nil, errors.New("sip.call.server transport must be a string")
} }
transportString = specificTransportString
} }
userAgentString := "showbridge" userAgentString, err := params.GetString("userAgent")
if err != nil {
userAgent, ok := params["userAgent"] if errors.Is(err, config.ErrParamNotFound) {
if ok { userAgentString = "showbridge"
} else {
specificTransportString, ok := userAgent.(string) return nil, fmt.Errorf("sip.call.server userAgent error: %w", err)
if !ok {
return nil, errors.New("sip.call.server userAgent must be a string")
} }
userAgentString = specificTransportString
} }
return &SIPCallServer{config: config, IP: ipString, Port: int(portNum), Transport: transportString, UserAgent: userAgentString, logger: CreateLogger(config)}, nil return &SIPCallServer{config: moduleConfig, IP: ipString, Port: int(portNum), Transport: transportString, UserAgent: userAgentString, logger: CreateLogger(moduleConfig)}, nil
}, },
}) })
} }
@@ -114,7 +133,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(route.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")
@@ -187,7 +206,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 := payload.(processor.SipDTMFResponse) payloadDTMFResponse, ok := common.GetAnyAs[processor.SipDTMFResponse](payload)
if ok { if ok {
dtmfWriter := call.inDialog.AudioWriterDTMF() dtmfWriter := call.inDialog.AudioWriterDTMF()
@@ -203,7 +222,7 @@ func (scs *SIPCallServer) Output(ctx context.Context, payload any) error {
return nil return nil
} }
payloadAudioFileResponse, ok := payload.(processor.SipAudioFileResponse) 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

@@ -2,6 +2,7 @@ package module
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@@ -15,18 +16,20 @@ import (
"github.com/emiago/diago/media" "github.com/emiago/diago/media"
"github.com/emiago/sipgo" "github.com/emiago/sipgo"
"github.com/emiago/sipgo/sip" "github.com/emiago/sipgo/sip"
"github.com/google/jsonschema-go/jsonschema"
"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
UserAgent string
Separator string Separator string
logger *slog.Logger logger *slog.Logger
cancel context.CancelFunc cancel context.CancelFunc
@@ -44,54 +47,87 @@ type SIPDTMFCall struct {
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "sip.dtmf.server", Type: "sip.dtmf.server",
New: func(config config.ModuleConfig) (Module, error) { Title: "SIP DTMF Server",
params := config.Params ParamsSchema: &jsonschema.Schema{
portNum := 5060 Type: "object",
Properties: map[string]*jsonschema.Schema{
"ip": {
Title: "IP",
Type: "string",
Default: json.RawMessage(`"0.0.0.0"`),
},
"port": {
Title: "Port",
Type: "integer",
Minimum: jsonschema.Ptr[float64](1024),
Maximum: jsonschema.Ptr[float64](65535),
Default: json.RawMessage(`5060`),
},
"transport": {
Title: "Transport",
Type: "string",
Enum: []any{"udp", "tcp", "ws", "udp4", "tcp4"},
Default: json.RawMessage(`"udp"`),
},
"userAgent": {
Title: "User Agent",
Type: "string",
Default: json.RawMessage(`"showbridge"`),
},
"separator": {
Title: "DTMF Separator",
Type: "string",
MinLength: jsonschema.Ptr(1),
MaxLength: jsonschema.Ptr(1),
},
},
Required: []string{"separator"},
AdditionalProperties: nil,
},
New: func(moduleConfig config.ModuleConfig) (common.Module, error) {
params := moduleConfig.Params
port, ok := params["port"] portNum, err := params.GetInt("port")
if ok { if err != nil {
specificPortNum, ok := port.(float64)
if !ok { if errors.Is(err, config.ErrParamNotFound) {
return nil, errors.New("sip.dtmf.server port must be a number") portNum = 5060
} else {
return nil, fmt.Errorf("sip.dtmf.server port error: %w", err)
} }
portNum = int(specificPortNum)
} }
ipString := "0.0.0.0" ipString, err := params.GetString("ip")
if err != nil {
ip, ok := params["ip"] if errors.Is(err, config.ErrParamNotFound) {
if ok { ipString = "0.0.0.0"
} else {
specificIpString, ok := ip.(string) return nil, fmt.Errorf("sip.dtmf.server ip error: %w", err)
if !ok {
return nil, errors.New("sip.dtmf.server ip must be a string")
} }
ipString = specificIpString
} }
transportString := "udp" transportString, err := params.GetString("transport")
if err != nil {
transport, ok := params["transport"] if errors.Is(err, config.ErrParamNotFound) {
if ok { transportString = "udp"
} else {
specificTransportString, ok := transport.(string) return nil, fmt.Errorf("sip.dtmf.server transport error: %w", err)
if !ok {
return nil, errors.New("sip.dtmf.server transport must be a string")
} }
transportString = specificTransportString
} }
separator, ok := params["separator"] userAgentString, err := params.GetString("userAgent")
if !ok { if err != nil {
return nil, errors.New("sip.dtmf.server requires a separator parameter") if errors.Is(err, config.ErrParamNotFound) {
userAgentString = "showbridge"
} else {
return nil, fmt.Errorf("sip.dtmf.server userAgent error: %w", err)
}
} }
separatorString, ok := separator.(string)
if !ok { separatorString, err := params.GetString("separator")
return nil, errors.New("sip.dtmf.server separator must be a string") if err != nil {
return nil, fmt.Errorf("sip.dtmf.server separator error: %w", err)
} }
if len(separatorString) != 1 { if len(separatorString) != 1 {
@@ -101,7 +137,7 @@ func init() {
if !strings.ContainsRune("0123456789*#ABCD", rune(separatorString[0])) { if !strings.ContainsRune("0123456789*#ABCD", rune(separatorString[0])) {
return nil, errors.New("sip.dtmf.server separator must be a valid DTMF character") return nil, errors.New("sip.dtmf.server separator must be a valid DTMF character")
} }
return &SIPDTMFServer{config: config, IP: ipString, Port: int(portNum), Transport: transportString, Separator: separatorString, logger: CreateLogger(config)}, nil return &SIPDTMFServer{config: moduleConfig, IP: ipString, Port: int(portNum), Transport: transportString, UserAgent: userAgentString, Separator: separatorString, logger: CreateLogger(moduleConfig)}, nil
}, },
}) })
} }
@@ -116,7 +152,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(route.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")
@@ -129,6 +165,7 @@ func (sds *SIPDTMFServer) Start(ctx context.Context) error {
diagoLogger := slog.New(slog.NewJSONHandler(io.Discard, nil)) diagoLogger := slog.New(slog.NewJSONHandler(io.Discard, nil))
ua, _ := sipgo.NewUA( ua, _ := sipgo.NewUA(
sipgo.WithUserAgent(sds.UserAgent),
sipgo.WithUserAgentTransportLayerOptions(sip.WithTransportLayerLogger(diagoLogger)), sipgo.WithUserAgentTransportLayerOptions(sip.WithTransportLayerLogger(diagoLogger)),
sipgo.WithUserAgentTransactionLayerOptions(sip.WithTransactionLayerLogger(diagoLogger)), sipgo.WithUserAgentTransactionLayerOptions(sip.WithTransactionLayerLogger(diagoLogger)),
) )
@@ -201,7 +238,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 := payload.(processor.SipDTMFResponse) payloadDTMFResponse, ok := common.GetAnyAs[processor.SipDTMFResponse](payload)
if ok { if ok {
dtmfWriter := call.inDialog.AudioWriterDTMF() dtmfWriter := call.inDialog.AudioWriterDTMF()
@@ -218,7 +255,7 @@ func (sds *SIPDTMFServer) Output(ctx context.Context, payload any) error {
return nil return nil
} }
payloadAudioFileResponse, ok := payload.(processor.SipAudioFileResponse) 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

@@ -8,9 +8,10 @@ import (
"net" "net"
"time" "time"
"github.com/google/jsonschema-go/jsonschema"
"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/route"
) )
type TCPClient struct { type TCPClient struct {
@@ -18,7 +19,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
@@ -26,30 +27,40 @@ type TCPClient struct {
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "net.tcp.client", Type: "net.tcp.client",
New: func(config config.ModuleConfig) (Module, error) { Title: "TCP Client",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"host": {
Title: "Host",
Type: "string",
},
"port": {
Title: "Port",
Type: "integer",
Minimum: jsonschema.Ptr[float64](1),
Maximum: jsonschema.Ptr[float64](65535),
},
"framing": {
Title: "Framing Method",
Type: "string",
Enum: []any{"LF", "CR", "CRLF", "SLIP", "RAW"},
},
},
Required: []string{"host", "port", "framing"},
AdditionalProperties: nil,
},
New: func(config config.ModuleConfig) (common.Module, error) {
params := config.Params params := config.Params
host, ok := params["host"] hostString, err := params.GetString("host")
if err != nil {
if !ok { return nil, fmt.Errorf("net.tcp.client host error: %w", err)
return nil, errors.New("net.tcp.client requires a host parameter")
} }
hostString, ok := host.(string) portNum, err := params.GetInt("port")
if err != nil {
if !ok { return nil, fmt.Errorf("net.tcp.client port error: %w", err)
return nil, errors.New("net.tcp.client host must be string")
}
port, ok := params["port"]
if !ok {
return nil, errors.New("net.tcp.client requires a port parameter")
}
portNum, ok := port.(float64)
if !ok {
return nil, errors.New("net.tcp.client port must be a number")
} }
addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", hostString, uint16(portNum))) addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", hostString, uint16(portNum)))
@@ -57,22 +68,15 @@ func init() {
return nil, err return nil, err
} }
framingMethod, ok := params["framing"] framingMethodString, err := params.GetString("framing")
if err != nil {
if !ok { return nil, fmt.Errorf("net.tcp.client framing error: %w", err)
return nil, errors.New("net.tcp.client requires a framing parameter")
}
framingMethodString, ok := framingMethod.(string)
if !ok {
return nil, errors.New("net.tcp.client framing method must be a string")
} }
framer := framer.GetFramer(framingMethodString) framer := framer.GetFramer(framingMethodString)
if framer == nil { if framer == nil {
return nil, fmt.Errorf("net.tcp.client unknown framing method: %s", framingMethod) return nil, fmt.Errorf("net.tcp.client unknown framing method: %s", framingMethodString)
} }
return &TCPClient{framer: framer, Addr: addr, config: config, logger: CreateLogger(config)}, nil return &TCPClient{framer: framer, Addr: addr, config: config, logger: CreateLogger(config)}, nil
}, },
@@ -89,7 +93,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(route.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")
@@ -172,7 +176,7 @@ func (tc *TCPClient) Output(ctx context.Context, payload any) error {
return err return err
} }
} }
payloadBytes, ok := payload.([]byte) payloadBytes, ok := common.GetAnyAsByteSlice(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

@@ -2,6 +2,7 @@ package module
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"log/slog" "log/slog"
@@ -11,9 +12,10 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/google/jsonschema-go/jsonschema"
"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/route"
) )
type TCPServer struct { type TCPServer struct {
@@ -21,7 +23,7 @@ type TCPServer struct {
Addr *net.TCPAddr Addr *net.TCPAddr
Framer framer.Framer Framer framer.Framer
ctx context.Context ctx context.Context
router route.RouteIO router common.RouteIO
quit chan interface{} quit chan interface{}
wg sync.WaitGroup wg sync.WaitGroup
connections []*net.TCPConn connections []*net.TCPConn
@@ -32,56 +34,63 @@ type TCPServer struct {
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "net.tcp.server", Type: "net.tcp.server",
New: func(config config.ModuleConfig) (Module, error) { Title: "TCP Server",
params := config.Params ParamsSchema: &jsonschema.Schema{
port, ok := params["port"] Type: "object",
if !ok { Properties: map[string]*jsonschema.Schema{
return nil, errors.New("net.tcp.server requires a port parameter") "ip": {
Title: "IP",
Type: "string",
Default: json.RawMessage(`"0.0.0.0"`),
},
"port": {
Title: "Port",
Type: "integer",
Minimum: jsonschema.Ptr[float64](1024),
Maximum: jsonschema.Ptr[float64](65535),
},
"framing": {
Title: "Framing Method",
Type: "string",
Enum: []any{"LF", "CR", "CRLF", "SLIP", "RAW"},
},
},
Required: []string{"port", "framing"},
AdditionalProperties: nil,
},
New: func(moduleConfig config.ModuleConfig) (common.Module, error) {
params := moduleConfig.Params
portNum, err := params.GetInt("port")
if err != nil {
return nil, fmt.Errorf("net.tcp.server port error: %w", err)
} }
portNum, ok := port.(float64) framingMethodString, err := params.GetString("framing")
if err != nil {
if !ok { return nil, fmt.Errorf("net.tcp.server framing error: %w", err)
return nil, errors.New("net.tcp.server port must be a number")
}
framingMethod, ok := params["framing"]
if !ok {
return nil, errors.New("net.tcp.server requires a framing parameter")
}
framingMethodString, ok := framingMethod.(string)
if !ok {
return nil, errors.New("net.tcp.server framing method must be a string")
} }
framer := framer.GetFramer(framingMethodString) framer := framer.GetFramer(framingMethodString)
if framer == nil { if framer == nil {
return nil, fmt.Errorf("net.tcp.server unknown framing method: %s", framingMethod) return nil, fmt.Errorf("net.tcp.server unknown framing method: %s", framingMethodString)
} }
ipString := "0.0.0.0" ipString, err := params.GetString("ip")
if err != nil {
ip, ok := params["ip"] if errors.Is(err, config.ErrParamNotFound) {
if ok { ipString = "0.0.0.0"
} else {
specificIpString, ok := ip.(string) return nil, fmt.Errorf("net.tcp.server ip error: %w", err)
if !ok {
return nil, errors.New("net.tcp.server ip must be a string")
} }
ipString = specificIpString
} }
addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", ipString, uint16(portNum))) addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf("%s:%d", ipString, uint16(portNum)))
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &TCPServer{Framer: framer, Addr: addr, config: config, quit: make(chan interface{}), logger: CreateLogger(config)}, nil return &TCPServer{Framer: framer, Addr: addr, config: moduleConfig, quit: make(chan interface{}), logger: CreateLogger(moduleConfig)}, nil
}, },
}) })
} }
@@ -157,7 +166,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")
} }
@@ -170,7 +185,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(route.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")
@@ -217,7 +232,7 @@ AcceptLoop:
} }
func (ts *TCPServer) Output(ctx context.Context, payload any) error { func (ts *TCPServer) Output(ctx context.Context, payload any) error {
payloadBytes, ok := payload.([]byte) payloadBytes, ok := common.GetAnyAsByteSlice(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

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

View File

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

View File

@@ -17,7 +17,7 @@ func TestHTTPServerFromRegistry(t *testing.T) {
Id: "test", Id: "test",
Type: "http.server", Type: "http.server",
Params: map[string]any{ Params: map[string]any{
"port": 3000.0, "port": 3000,
}, },
}) })
@@ -33,3 +33,55 @@ func TestHTTPServerFromRegistry(t *testing.T) {
t.Fatalf("http.server module has wrong type: %s", moduleInstance.Type()) t.Fatalf("http.server module has wrong type: %s", moduleInstance.Type())
} }
} }
func TestBadHTTPServer(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{
{
name: "no port param",
params: map[string]any{},
errorString: "http.server port error: not found",
},
{
name: "non-numeric port",
params: map[string]any{"port": "3000"},
errorString: "http.server port error: not a number",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["http.server"]
if !ok {
t.Fatalf("http.server module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "http.server",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("http.server got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("http.server expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("http.server got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -33,3 +33,55 @@ func TestMIDIInputFromRegistry(t *testing.T) {
t.Fatalf("midi.input module has wrong type: %s", moduleInstance.Type()) t.Fatalf("midi.input module has wrong type: %s", moduleInstance.Type())
} }
} }
func TestBadMIDIInput(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{
{
name: "no port param",
params: map[string]any{},
errorString: "midi.input port error: not found",
},
{
name: "non-string port",
params: map[string]any{"port": 123},
errorString: "midi.input port error: not a string",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["midi.input"]
if !ok {
t.Fatalf("midi.input module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "midi.input",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("midi.input got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("midi.input expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("midi.input got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -33,3 +33,55 @@ func TestMIDIOutputFromRegistry(t *testing.T) {
t.Fatalf("midi.output module has wrong type: %s", moduleInstance.Type()) t.Fatalf("midi.output module has wrong type: %s", moduleInstance.Type())
} }
} }
func TestBadMIDIOutput(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{
{
name: "no port param",
params: map[string]any{},
errorString: "midi.output port error: not found",
},
{
name: "non-string port",
params: map[string]any{"port": 123},
errorString: "midi.output port error: not a string",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["midi.output"]
if !ok {
t.Fatalf("midi.output module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "midi.output",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("midi.output got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("midi.output expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("midi.output got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -0,0 +1,60 @@
package module_test
import (
"testing"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
"github.com/jwetzell/showbridge-go/internal/module"
"github.com/jwetzell/showbridge-go/internal/test"
)
func TestModuleBadRegistrationNoType(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Fatalf("module registration should have panicked but did not")
}
}()
module.RegisterModule(module.ModuleRegistration{
Type: "",
New: func(config config.ModuleConfig) (common.Module, error) {
return &test.TestModule{}, nil
},
})
}
func TestModuleBadRegistrationNoNew(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Fatalf("processor registration should have panicked but did not")
}
}()
module.RegisterModule(module.ModuleRegistration{
Type: "module.test",
New: nil,
})
}
func TestModuleBadRegistrationExistingType(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Fatalf("processor registration should have panicked but did not")
}
}()
module.RegisterModule(module.ModuleRegistration{
Type: "module.test",
New: func(config config.ModuleConfig) (common.Module, error) {
return &test.TestModule{}, nil
},
})
module.RegisterModule(module.ModuleRegistration{
Type: "module.test",
New: func(config config.ModuleConfig) (common.Module, error) {
return &test.TestModule{}, nil
},
})
}

View File

@@ -35,3 +35,96 @@ func TestMQTTClientFromRegistry(t *testing.T) {
t.Fatalf("mqtt.client module has wrong type: %s", moduleInstance.Type()) t.Fatalf("mqtt.client module has wrong type: %s", moduleInstance.Type())
} }
} }
func TestBadMQTTClient(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{
{
name: "no broker param",
params: map[string]any{
"topic": "test/topic",
"clientId": "test",
},
errorString: "mqtt.client broker error: not found",
},
{
name: "non-string broker",
params: map[string]any{
"broker": 123,
"topic": "test/topic",
"clientId": "test",
},
errorString: "mqtt.client broker error: not a string",
},
{
name: "no topic param",
params: map[string]any{
"broker": "mqtt://localhost:1883",
"clientId": "test",
},
errorString: "mqtt.client topic error: not found",
},
{
name: "non-string topic",
params: map[string]any{
"broker": "mqtt://localhost:1883",
"topic": 123,
"clientId": "test",
},
errorString: "mqtt.client topic error: not a string",
},
{
name: "no clientId param",
params: map[string]any{
"broker": "mqtt://localhost:1883",
"topic": "test/topic",
},
errorString: "mqtt.client clientId error: not found",
},
{
name: "non-string clientId",
params: map[string]any{
"broker": "mqtt://localhost:1883",
"topic": "test/topic",
"clientId": 123,
},
errorString: "mqtt.client clientId error: not a string",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["mqtt.client"]
if !ok {
t.Fatalf("mqtt.client module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "mqtt.client",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("mqtt.client got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("mqtt.client expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("mqtt.client got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -34,3 +34,75 @@ func TestNATSClientFromRegistry(t *testing.T) {
t.Fatalf("nats.client module has wrong type: %s", moduleInstance.Type()) t.Fatalf("nats.client module has wrong type: %s", moduleInstance.Type())
} }
} }
func TestBadNATSClient(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{
{
name: "no url param",
params: map[string]any{
"subject": "test/subject",
},
errorString: "nats.client url error: not found",
},
{
name: "non-string url",
params: map[string]any{
"url": 123,
"subject": "test/subject",
},
errorString: "nats.client url error: not a string",
},
{
name: "no subject param",
params: map[string]any{
"url": "nats://127.0.0.1:4222",
},
errorString: "nats.client subject error: not found",
},
{
name: "non-string subject",
params: map[string]any{
"url": "nats://127.0.0.1:4222",
"subject": 123,
},
errorString: "nats.client subject error: not a string",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["nats.client"]
if !ok {
t.Fatalf("nats.client module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "nats.client",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("nats.client got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("nats.client expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("nats.client got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

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

View File

@@ -30,3 +30,44 @@ func TestPSNClientFromRegistry(t *testing.T) {
t.Fatalf("psn.client module has wrong type: %s", moduleInstance.Type()) t.Fatalf("psn.client module has wrong type: %s", moduleInstance.Type())
} }
} }
func TestBadPSNClient(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["psn.client"]
if !ok {
t.Fatalf("psn.client module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "psn.client",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("psn.client got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("psn.client expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("psn.client got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

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

View File

@@ -19,7 +19,7 @@ func TestSerialClientFromRegistry(t *testing.T) {
Params: map[string]any{ Params: map[string]any{
"port": "/dev/ttyUSB0", "port": "/dev/ttyUSB0",
"framing": "LF", "framing": "LF",
"baudRate": 9600.0, "baudRate": 9600,
}, },
}) })
@@ -35,3 +35,83 @@ func TestSerialClientFromRegistry(t *testing.T) {
t.Fatalf("serial.client module has wrong type: %s", moduleInstance.Type()) t.Fatalf("serial.client module has wrong type: %s", moduleInstance.Type())
} }
} }
func TestBadSerialClient(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{
{
name: "no port param",
params: map[string]any{
"framing": "LF",
},
errorString: "serial.client port error: not found",
},
{
name: "non-string port param",
params: map[string]any{
"port": 8000,
"framing": "LF",
},
errorString: "serial.client port error: not a string",
},
{
name: "no framing param",
params: map[string]any{
"port": "/dev/ttyUSB0",
},
errorString: "serial.client framing error: not found",
},
{
name: "non-string framing param",
params: map[string]any{
"port": "/dev/ttyUSB0",
"framing": 1,
},
errorString: "serial.client framing error: not a string",
},
{
name: "unkown framing method",
params: map[string]any{
"port": "/dev/ttyUSB0",
"framing": "asdfasdfasdfasdflkj",
},
errorString: "serial.client unknown framing method: asdfasdfasdfasdflkj",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["serial.client"]
if !ok {
t.Fatalf("serial.client module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "serial.client",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("serial.client got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("serial.client expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("serial.client got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -30,3 +30,73 @@ func TestSIPCallServerFromRegistry(t *testing.T) {
t.Fatalf("sip.call.server module has wrong type: %s", moduleInstance.Type()) t.Fatalf("sip.call.server module has wrong type: %s", moduleInstance.Type())
} }
} }
func TestBadSIPCallServer(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{
{
name: "non-number port param",
params: map[string]any{
"port": "8000",
},
errorString: "sip.call.server port error: not a number",
},
{
name: "non-string ip param",
params: map[string]any{
"ip": 123,
},
errorString: "sip.call.server ip error: not a string",
},
{
name: "non-string transport param",
params: map[string]any{
"transport": 123,
},
errorString: "sip.call.server transport error: not a string",
},
{
name: "non-string userAgent param",
params: map[string]any{
"userAgent": 123,
},
errorString: "sip.call.server userAgent error: not a string",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["sip.call.server"]
if !ok {
t.Fatalf("sip.call.server module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "sip.call.server",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("sip.call.server got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("sip.call.server expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("sip.call.server got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -33,3 +33,89 @@ func TestSIPDTMFServerFromRegistry(t *testing.T) {
t.Fatalf("sip.dtmf.server module has wrong type: %s", moduleInstance.Type()) t.Fatalf("sip.dtmf.server module has wrong type: %s", moduleInstance.Type())
} }
} }
func TestBadSIPDTMFServer(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{
{
name: "no separator param",
params: map[string]any{},
errorString: "sip.dtmf.server separator error: not found",
},
{
name: "non-string separator param",
params: map[string]any{
"separator": 123,
},
errorString: "sip.dtmf.server separator error: not a string",
},
{
name: "non-number port param",
params: map[string]any{
"separator": "#",
"port": "8000",
},
errorString: "sip.dtmf.server port error: not a number",
},
{
name: "non-string ip param",
params: map[string]any{
"separator": "#",
"ip": 123,
},
errorString: "sip.dtmf.server ip error: not a string",
},
{
name: "non-string transport param",
params: map[string]any{
"separator": "#",
"transport": 123,
},
errorString: "sip.dtmf.server transport error: not a string",
},
{
name: "non-string userAgent param",
params: map[string]any{
"separator": "#",
"userAgent": 123,
},
errorString: "sip.dtmf.server userAgent error: not a string",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["sip.dtmf.server"]
if !ok {
t.Fatalf("sip.dtmf.server module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "sip.dtmf.server",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("sip.dtmf.server got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("sip.dtmf.server expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("sip.dtmf.server got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -18,7 +18,7 @@ func TestTCPClientFromRegistry(t *testing.T) {
Type: "net.tcp.client", Type: "net.tcp.client",
Params: map[string]any{ Params: map[string]any{
"host": "localhost", "host": "localhost",
"port": 8000.0, "port": 8000,
"framing": "LF", "framing": "LF",
}, },
}) })
@@ -35,3 +35,75 @@ func TestTCPClientFromRegistry(t *testing.T) {
t.Fatalf("net.tcp.client module has wrong type: %s", moduleInstance.Type()) t.Fatalf("net.tcp.client module has wrong type: %s", moduleInstance.Type())
} }
} }
func TestBadTCPClient(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{
{
name: "no port param",
params: map[string]any{
"host": "localhost",
},
errorString: "net.tcp.client port error: not found",
},
{
name: "non-number port param",
params: map[string]any{
"host": "localhost",
"port": "8000",
},
errorString: "net.tcp.client port error: not a number",
},
{
name: "no host param",
params: map[string]any{
"port": 8000,
},
errorString: "net.tcp.client host error: not found",
},
{
name: "non-string host param",
params: map[string]any{
"host": 123,
"port": 8000,
},
errorString: "net.tcp.client host error: not a string",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["net.tcp.client"]
if !ok {
t.Fatalf("net.tcp.client module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "net.tcp.client",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("net.tcp.client got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("net.tcp.client expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("net.tcp.client got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -17,7 +17,7 @@ func TestTCPServerFromRegistry(t *testing.T) {
Id: "test", Id: "test",
Type: "net.tcp.server", Type: "net.tcp.server",
Params: map[string]any{ Params: map[string]any{
"port": 8000.0, "port": 8000,
"framing": "LF", "framing": "LF",
}, },
}) })
@@ -34,3 +34,101 @@ func TestTCPServerFromRegistry(t *testing.T) {
t.Fatalf("net.tcp.server module has wrong type: %s", moduleInstance.Type()) t.Fatalf("net.tcp.server module has wrong type: %s", moduleInstance.Type())
} }
} }
func TestBadTCPServer(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{
{
name: "no port param",
params: map[string]any{
"framing": "LF",
},
errorString: "net.tcp.server port error: not found",
},
{
name: "non-number port param",
params: map[string]any{
"port": "8000",
"framing": "LF",
},
errorString: "net.tcp.server port error: not a number",
},
{
name: "no framing param",
params: map[string]any{
"port": 8000,
},
errorString: "net.tcp.server framing error: not found",
},
{
name: "non-string framing param",
params: map[string]any{
"port": 8000,
"framing": 1,
},
errorString: "net.tcp.server framing error: not a string",
},
{
name: "unkown framing method",
params: map[string]any{
"port": 8000,
"framing": "asdfasdfasdfasdflkj",
},
errorString: "net.tcp.server unknown framing method: asdfasdfasdfasdflkj",
},
{
name: "non-string ip param",
params: map[string]any{
"port": 8000,
"framing": "LF",
"ip": 123,
},
errorString: "net.tcp.server ip error: not a string",
},
{
name: "invalid addr",
params: map[string]any{
"ip": "127.0.0.",
"port": 8000,
"framing": "LF",
},
errorString: "lookup 127.0.0.: no such host",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["net.tcp.server"]
if !ok {
t.Fatalf("net.tcp.server module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "net.tcp.server",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("net.tcp.server got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("net.tcp.server expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("net.tcp.server got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -17,7 +17,7 @@ func TestTimeIntervalFromRegistry(t *testing.T) {
Id: "test", Id: "test",
Type: "time.interval", Type: "time.interval",
Params: map[string]any{ Params: map[string]any{
"duration": 1000.0, "duration": 1000,
}, },
}) })
@@ -33,3 +33,57 @@ func TestTimeIntervalFromRegistry(t *testing.T) {
t.Fatalf("time.interval module has wrong type: %s", moduleInstance.Type()) t.Fatalf("time.interval module has wrong type: %s", moduleInstance.Type())
} }
} }
func TestBadTimeInterval(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{
{
name: "no duration param",
params: map[string]any{},
errorString: "time.interval duration error: not found",
},
{
name: "non-number duration param",
params: map[string]any{
"duration": "8000",
},
errorString: "time.interval duration error: not a number",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["time.interval"]
if !ok {
t.Fatalf("time.interval module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "time.interval",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("time.interval got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("time.interval expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("time.interval got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -17,7 +17,7 @@ func TestTimeTimerFromRegistry(t *testing.T) {
Id: "test", Id: "test",
Type: "time.timer", Type: "time.timer",
Params: map[string]any{ Params: map[string]any{
"duration": 1000.0, "duration": 1000,
}, },
}) })
@@ -33,3 +33,57 @@ func TestTimeTimerFromRegistry(t *testing.T) {
t.Fatalf("time.timer module has wrong type: %s", moduleInstance.Type()) t.Fatalf("time.timer module has wrong type: %s", moduleInstance.Type())
} }
} }
func TestBadTimeTimer(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{
{
name: "no duration param",
params: map[string]any{},
errorString: "time.timer duration error: not found",
},
{
name: "non-number duration param",
params: map[string]any{
"duration": "8000",
},
errorString: "time.timer duration error: not a number",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["time.timer"]
if !ok {
t.Fatalf("time.timer module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "time.timer",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("time.timer got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("time.timer expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("time.timer got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -18,7 +18,7 @@ func TestUDPClientFromRegistry(t *testing.T) {
Type: "net.udp.client", Type: "net.udp.client",
Params: map[string]any{ Params: map[string]any{
"host": "localhost", "host": "localhost",
"port": 8000.0, "port": 8000,
"framing": "LF", "framing": "LF",
}, },
}) })
@@ -35,3 +35,75 @@ func TestUDPClientFromRegistry(t *testing.T) {
t.Fatalf("net.udp.client module has wrong type: %s", moduleInstance.Type()) t.Fatalf("net.udp.client module has wrong type: %s", moduleInstance.Type())
} }
} }
func TestBadUDPClient(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{
{
name: "no port param",
params: map[string]any{
"host": "localhost",
},
errorString: "net.udp.client port error: not found",
},
{
name: "non-number port param",
params: map[string]any{
"host": "localhost",
"port": "8000",
},
errorString: "net.udp.client port error: not a number",
},
{
name: "no host param",
params: map[string]any{
"port": 8000,
},
errorString: "net.udp.client host error: not found",
},
{
name: "non-string host param",
params: map[string]any{
"host": 123,
"port": 8000,
},
errorString: "net.udp.client host error: not a string",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["net.udp.client"]
if !ok {
t.Fatalf("net.udp.client module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "net.udp.client",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("net.udp.client got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("net.udp.client expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("net.udp.client got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -18,7 +18,7 @@ func TestUDPMulticastFromRegistry(t *testing.T) {
Type: "net.udp.multicast", Type: "net.udp.multicast",
Params: map[string]any{ Params: map[string]any{
"ip": "236.10.10.10", "ip": "236.10.10.10",
"port": 56565.0, "port": 56565,
}, },
}) })
@@ -34,3 +34,83 @@ func TestUDPMulticastFromRegistry(t *testing.T) {
t.Fatalf("net.udp.multicast module has wrong type: %s", moduleInstance.Type()) t.Fatalf("net.udp.multicast module has wrong type: %s", moduleInstance.Type())
} }
} }
func TestBadUDPMulticast(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{
{
name: "no port param",
params: map[string]any{
"ip": "localhost",
},
errorString: "net.udp.multicast port error: not found",
},
{
name: "non-number port param",
params: map[string]any{
"ip": "localhost",
"port": "8000",
},
errorString: "net.udp.multicast port error: not a number",
},
{
name: "no ip param",
params: map[string]any{
"port": 8000,
},
errorString: "net.udp.multicast ip error: not found",
},
{
name: "non-string ip param",
params: map[string]any{
"ip": 123,
"port": 8000,
},
errorString: "net.udp.multicast ip error: not a string",
},
{
name: "invalid addr",
params: map[string]any{
"ip": "127.0.0.",
"port": 8000,
},
errorString: "lookup 127.0.0.: no such host",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["net.udp.multicast"]
if !ok {
t.Fatalf("net.udp.multicast module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "net.udp.multicast",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("net.udp.multicast got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("net.udp.multicast expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("net.udp.multicast got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -17,7 +17,7 @@ func TestUDPServerFromRegistry(t *testing.T) {
Id: "test", Id: "test",
Type: "net.udp.server", Type: "net.udp.server",
Params: map[string]any{ Params: map[string]any{
"port": 8000.0, "port": 8000,
}, },
}) })
@@ -33,3 +33,81 @@ func TestUDPServerFromRegistry(t *testing.T) {
t.Fatalf("net.udp.server module has wrong type: %s", moduleInstance.Type()) t.Fatalf("net.udp.server module has wrong type: %s", moduleInstance.Type())
} }
} }
func TestBadUDPServer(t *testing.T) {
tests := []struct {
name string
params map[string]any
errorString string
}{
{
name: "no port param",
params: map[string]any{},
errorString: "net.udp.server port error: not found",
},
{
name: "non-number port param",
params: map[string]any{
"port": "8000",
},
errorString: "net.udp.server port error: not a number",
},
{
name: "non-string ip param",
params: map[string]any{
"port": 8000,
"ip": 123,
},
errorString: "net.udp.server ip error: not a string",
},
{
name: "non-number bufferSize param",
params: map[string]any{
"port": 8000,
"bufferSize": "1024",
},
errorString: "net.udp.server bufferSize error: not a number",
},
{
name: "invalid addr",
params: map[string]any{
"ip": "127.0.0.",
"port": 8000,
},
errorString: "lookup 127.0.0.: no such host",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
registration, ok := module.ModuleRegistry["net.udp.server"]
if !ok {
t.Fatalf("net.udp.server module not registered")
}
moduleInstance, err := registration.New(config.ModuleConfig{
Id: "test",
Type: "net.udp.server",
Params: test.params,
})
if err != nil {
if test.errorString != err.Error() {
t.Fatalf("net.udp.server got error '%s', expected '%s'", err.Error(), test.errorString)
}
return
}
err = moduleInstance.Start(t.Context())
if err == nil {
t.Fatalf("net.udp.server expected to fail")
}
if err.Error() != test.errorString {
t.Fatalf("net.udp.server got error '%s', expected '%s'", err.Error(), test.errorString)
}
})
}
}

View File

@@ -3,18 +3,20 @@ package module
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"log/slog" "log/slog"
"time" "time"
"github.com/google/jsonschema-go/jsonschema"
"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
@@ -22,21 +24,28 @@ type TimeInterval struct {
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "time.interval", Type: "time.interval",
New: func(config config.ModuleConfig) (Module, error) { Title: "Interval",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"duration": {
Title: "Duration",
Type: "integer",
Description: "Interval duration in milliseconds",
},
},
Required: []string{"duration"},
AdditionalProperties: nil,
},
New: func(config config.ModuleConfig) (common.Module, error) {
params := config.Params params := config.Params
duration, ok := params["duration"] durationInt, err := params.GetInt("duration")
if !ok { if err != nil {
return nil, errors.New("time.interval requires a duration parameter") return nil, fmt.Errorf("time.interval duration error: %w", err)
} }
return &TimeInterval{Duration: uint32(durationInt), config: config, logger: CreateLogger(config)}, nil
durationNum, ok := duration.(float64)
if !ok {
return nil, errors.New("time.interval duration must be number")
}
return &TimeInterval{Duration: uint32(durationNum), config: config, logger: CreateLogger(config)}, nil
}, },
}) })
} }
@@ -51,7 +60,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(route.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")
@@ -79,11 +88,6 @@ func (i *TimeInterval) Start(ctx context.Context) error {
} }
func (i *TimeInterval) Output(ctx context.Context, payload any) error {
i.ticker.Reset(time.Millisecond * time.Duration(i.Duration))
return nil
}
func (i *TimeInterval) Stop() { func (i *TimeInterval) Stop() {
i.cancel() i.cancel()
} }

View File

@@ -3,18 +3,20 @@ package module
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"log/slog" "log/slog"
"time" "time"
"github.com/google/jsonschema-go/jsonschema"
"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
@@ -22,19 +24,26 @@ type TimeTimer struct {
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "time.timer", Type: "time.timer",
New: func(config config.ModuleConfig) (Module, error) { Title: "Timer",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"duration": {
Title: "Duration",
Type: "integer",
Description: "Interval duration in milliseconds",
},
},
Required: []string{"duration"},
AdditionalProperties: nil,
},
New: func(config config.ModuleConfig) (common.Module, error) {
params := config.Params params := config.Params
duration, ok := params["duration"] durationNum, err := params.GetInt("duration")
if !ok { if err != nil {
return nil, errors.New("time.timer requires a duration parameter") return nil, fmt.Errorf("time.timer duration error: %w", err)
}
durationNum, ok := duration.(float64)
if !ok {
return nil, errors.New("time.timer duration must be a number")
} }
return &TimeTimer{Duration: uint32(durationNum), config: config, logger: CreateLogger(config)}, nil return &TimeTimer{Duration: uint32(durationNum), config: config, logger: CreateLogger(config)}, nil
@@ -52,7 +61,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(route.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")
@@ -78,11 +87,6 @@ func (t *TimeTimer) Start(ctx context.Context) error {
} }
} }
func (t *TimeTimer) Output(ctx context.Context, payload any) error {
t.timer.Reset(time.Millisecond * time.Duration(t.Duration))
return nil
}
func (t *TimeTimer) Stop() { func (t *TimeTimer) Stop() {
t.cancel() t.cancel()
} }

View File

@@ -7,8 +7,9 @@ import (
"log/slog" "log/slog"
"net" "net"
"github.com/google/jsonschema-go/jsonschema"
"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 UDPClient struct { type UDPClient struct {
@@ -17,37 +18,42 @@ 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
} }
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "net.udp.client", Type: "net.udp.client",
New: func(config config.ModuleConfig) (Module, error) { Title: "UDP Client",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"host": {
Title: "Host",
Type: "string",
},
"port": {
Title: "Port",
Type: "integer",
Minimum: jsonschema.Ptr[float64](1),
Maximum: jsonschema.Ptr[float64](65535),
},
},
Required: []string{"host", "port"},
AdditionalProperties: nil,
},
New: func(config config.ModuleConfig) (common.Module, error) {
params := config.Params params := config.Params
host, ok := params["host"] hostString, err := params.GetString("host")
if err != nil {
if !ok { return nil, fmt.Errorf("net.udp.client host error: %w", err)
return nil, errors.New("net.udp.client requires a host parameter")
} }
hostString, ok := host.(string) portNum, err := params.GetInt("port")
if err != nil {
if !ok { return nil, fmt.Errorf("net.udp.client port error: %w", err)
return nil, errors.New("net.udp.client host must be a string")
}
port, ok := params["port"]
if !ok {
return nil, errors.New("net.udp.client requires a port parameter")
}
portNum, ok := port.(float64)
if !ok {
return nil, errors.New("net.udp.client port must be a number")
} }
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", hostString, uint16(portNum))) addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", hostString, uint16(portNum)))
@@ -75,7 +81,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(route.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")
@@ -100,7 +106,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 := payload.([]byte) payloadBytes, ok := common.GetAnyAsByteSlice(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

@@ -8,15 +8,16 @@ import (
"net" "net"
"time" "time"
"github.com/google/jsonschema-go/jsonschema"
"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 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
@@ -24,37 +25,42 @@ type UDPMulticast struct {
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "net.udp.multicast", Type: "net.udp.multicast",
New: func(config config.ModuleConfig) (Module, error) { Title: "UDP Multicast",
params := config.Params ParamsSchema: &jsonschema.Schema{
ip, ok := params["ip"] Type: "object",
Properties: map[string]*jsonschema.Schema{
if !ok { "ip": {
return nil, errors.New("net.udp.multicast requires an ip parameter") Title: "IP",
Type: "string",
},
"port": {
Title: "Port",
Type: "integer",
Minimum: jsonschema.Ptr[float64](1024),
Maximum: jsonschema.Ptr[float64](65535),
},
},
Required: []string{"ip", "port"},
AdditionalProperties: nil,
},
New: func(moduleConfig config.ModuleConfig) (common.Module, error) {
params := moduleConfig.Params
ipString, err := params.GetString("ip")
if err != nil {
return nil, fmt.Errorf("net.udp.multicast ip error: %w", err)
} }
ipString, ok := ip.(string) portNum, err := params.GetInt("port")
if err != nil {
if !ok { return nil, fmt.Errorf("net.udp.multicast port error: %w", err)
return nil, errors.New("net.udp.multicast ip must be a string")
}
port, ok := params["port"]
if !ok {
return nil, errors.New("net.udp.multicast requires a port parameter")
}
portNum, ok := port.(float64)
if !ok {
return nil, errors.New("net.udp.multicast port must be a number")
} }
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", ipString, uint16(portNum))) addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", ipString, uint16(portNum)))
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &UDPMulticast{config: config, Addr: addr, logger: CreateLogger(config)}, nil return &UDPMulticast{config: moduleConfig, Addr: addr, logger: CreateLogger(moduleConfig)}, nil
}, },
}) })
} }
@@ -69,7 +75,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(route.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")
@@ -121,7 +127,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 := payload.([]byte) payloadBytes, ok := common.GetAnyAsByteSlice(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

@@ -4,13 +4,13 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"log"
"log/slog" "log/slog"
"net" "net"
"time" "time"
"github.com/google/jsonschema-go/jsonschema"
"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,57 +18,62 @@ 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
} }
func init() { func init() {
RegisterModule(ModuleRegistration{ RegisterModule(ModuleRegistration{
Type: "net.udp.server", Type: "net.udp.server",
New: func(config config.ModuleConfig) (Module, error) { Title: "UDP Server",
params := config.Params ParamsSchema: &jsonschema.Schema{
port, ok := params["port"] Type: "object",
if !ok { Properties: map[string]*jsonschema.Schema{
return nil, errors.New("net.udp.server requires a port parameter") "ip": {
Title: "IP",
Type: "string",
},
"port": {
Title: "Port",
Type: "integer",
Minimum: jsonschema.Ptr[float64](1024),
Maximum: jsonschema.Ptr[float64](65535),
},
},
Required: []string{"ip", "port"},
AdditionalProperties: nil,
},
New: func(moduleConfig config.ModuleConfig) (common.Module, error) {
params := moduleConfig.Params
portNum, err := params.GetInt("port")
if err != nil {
return nil, fmt.Errorf("net.udp.server port error: %w", err)
} }
portNum, ok := port.(float64) ipString, err := params.GetString("ip")
if err != nil {
if !ok { if errors.Is(err, config.ErrParamNotFound) {
return nil, errors.New("net.udp.server port must be a number") ipString = "0.0.0.0"
} } else {
return nil, fmt.Errorf("net.udp.server ip error: %w", err)
ipString := "0.0.0.0"
ip, ok := params["ip"]
if ok {
specificIpString, ok := ip.(string)
if !ok {
return nil, errors.New("net.udp.server ip must be a string")
} }
ipString = specificIpString
} }
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", ipString, uint16(portNum))) addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf("%s:%d", ipString, uint16(portNum)))
if err != nil { if err != nil {
log.Fatalf("error resolving UDP address: %v", err) return nil, err
} }
bufferSizeNum := 2048 bufferSizeNum, err := params.GetInt("bufferSize")
bufferSize, ok := params["bufferSize"] if err != nil {
if errors.Is(err, config.ErrParamNotFound) {
if ok { bufferSizeNum = 2048
bufferSizeFloat, ok := bufferSize.(float64) } else {
return nil, fmt.Errorf("net.udp.server bufferSize error: %w", err)
if !ok {
return nil, errors.New("net.udp.server bufferSize must be a number")
} }
bufferSizeNum = int(bufferSizeFloat)
} }
return &UDPServer{Addr: addr, BufferSize: bufferSizeNum, config: config, logger: CreateLogger(config)}, nil return &UDPServer{Addr: addr, BufferSize: bufferSizeNum, config: moduleConfig, logger: CreateLogger(moduleConfig)}, nil
}, },
}) })
} }
@@ -83,7 +88,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(route.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")
@@ -110,7 +115,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() {
@@ -120,7 +125,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"
) )
@@ -12,20 +13,25 @@ type ArtNetPacketDecode struct {
config config.ProcessorConfig config config.ProcessorConfig
} }
func (apd *ArtNetPacketDecode) Process(ctx context.Context, payload any) (any, error) { func (apd *ArtNetPacketDecode) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
payloadBytes, ok := payload.([]byte) payload := wrappedPayload.Payload
payloadBytes, ok := common.GetAnyAsByteSlice(payload)
if !ok { if !ok {
return nil, fmt.Errorf("artnet.packet.decode processor only accepts a []byte") wrappedPayload.End = true
return wrappedPayload, fmt.Errorf("artnet.packet.decode processor only accepts a []byte")
} }
payloadMessage, err := artnet.Decode(payloadBytes) payloadMessage, err := artnet.Decode(payloadBytes)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
return payloadMessage, nil wrappedPayload.Payload = payloadMessage
return wrappedPayload, nil
} }
func (apd *ArtNetPacketDecode) Type() string { func (apd *ArtNetPacketDecode) Type() string {
@@ -34,7 +40,8 @@ func (apd *ArtNetPacketDecode) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "artnet.packet.decode", Type: "artnet.packet.decode",
Title: "Decode ArtNet Packet",
New: func(config config.ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
return &ArtNetPacketDecode{config: config}, nil return &ArtNetPacketDecode{config: config}, nil
}, },

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"
) )
@@ -12,19 +13,24 @@ type ArtNetPacketEncode struct {
config config.ProcessorConfig config config.ProcessorConfig
} }
func (ape *ArtNetPacketEncode) Process(ctx context.Context, payload any) (any, error) { func (ape *ArtNetPacketEncode) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
payloadPacket, ok := payload.(artnet.ArtNetPacket) payload := wrappedPayload.Payload
payloadPacket, ok := common.GetAnyAs[artnet.ArtNetPacket](payload)
if !ok { if !ok {
return nil, fmt.Errorf("artnet.packet.encode processor only accepts an ArtNetPacket") wrappedPayload.End = true
return wrappedPayload, fmt.Errorf("artnet.packet.encode processor only accepts an ArtNetPacket")
} }
payloadBytes, err := payloadPacket.MarshalBinary() payloadBytes, err := payloadPacket.MarshalBinary()
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
return payloadBytes, nil wrappedPayload.Payload = payloadBytes
return wrappedPayload, nil
} }
func (ape *ArtNetPacketEncode) Type() string { func (ape *ArtNetPacketEncode) Type() string {
@@ -33,7 +39,8 @@ func (ape *ArtNetPacketEncode) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "artnet.packet.encode", Type: "artnet.packet.encode",
Title: "Encode ArtNet Packet",
New: func(config config.ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
return &ArtNetPacketEncode{config: config}, nil return &ArtNetPacketEncode{config: config}, nil
}, },

View File

@@ -1,52 +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 := payload.(artnet.ArtNetPacket)
if !ok {
return nil, fmt.Errorf("artnet.packet.filter processor only accepts an ArtNetPacket")
}
if payloadPacket.GetOpCode() != apf.OpCode {
return nil, nil
}
return payloadPacket, nil
}
func (apf *ArtNetPacketFilter) Type() string {
return apf.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "artnet.packet.filter",
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
opCode, ok := params["opCode"]
if !ok {
return nil, fmt.Errorf("artnet.packet.filter requires an opCode parameter")
}
opCodeNum, ok := opCode.(float64)
if !ok {
return nil, fmt.Errorf("artnet.packet.filter opCode must be a number")
}
return &ArtNetPacketFilter{config: config, OpCode: uint16(opCodeNum)}, nil
},
})
}

View File

@@ -0,0 +1,131 @@
package processor
import (
"bytes"
"context"
"errors"
"fmt"
"log/slog"
"text/template"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
type DbQuery struct {
config config.ProcessorConfig
ModuleId string
Query *template.Template
logger *slog.Logger
}
func (dq *DbQuery) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
if wrappedPayload.Modules == nil {
wrappedPayload.End = true
return wrappedPayload, errors.New("db.query wrapped payload has no modules")
}
module, ok := wrappedPayload.Modules[dq.ModuleId]
if !ok {
wrappedPayload.End = true
return wrappedPayload, fmt.Errorf("db.query unable to find module with id: %s", dq.ModuleId)
}
dbModule, ok := module.(common.DatabaseModule)
if !ok {
wrappedPayload.End = true
return wrappedPayload, fmt.Errorf("db.query module with id %s is not a DatabaseModule", dq.ModuleId)
}
db := dbModule.Database()
if db == nil {
wrappedPayload.End = true
return wrappedPayload, fmt.Errorf("db.query module with id %s returned nil database", dq.ModuleId)
}
var queryBuffer bytes.Buffer
err := dq.Query.Execute(&queryBuffer, wrappedPayload)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
}
// support proper parameterized queries
rows, err := db.QueryContext(ctx, queryBuffer.String())
if err != nil {
wrappedPayload.End = true
return wrappedPayload, fmt.Errorf("db.query error executing query: %w", err)
}
defer rows.Close()
columns, err := rows.Columns()
if err != nil {
wrappedPayload.End = true
return wrappedPayload, fmt.Errorf("db.query error getting columns: %w", err)
}
results := make([]map[string]any, 0)
for rows.Next() {
columnValues := make([]interface{}, len(columns))
for i := range columnValues {
columnValues[i] = new(interface{})
}
if err := rows.Scan(columnValues...); err != nil {
wrappedPayload.End = true
return wrappedPayload, fmt.Errorf("db.query error scanning row: %w", err)
}
rowMap := make(map[string]any)
for i, colName := range columns {
value := *columnValues[i].(*interface{})
rowMap[colName] = value
}
results = append(results, rowMap)
}
if len(results) == 0 {
wrappedPayload.Payload = nil
return wrappedPayload, nil
} else if len(results) == 1 {
wrappedPayload.Payload = results[0]
return wrappedPayload, nil
}
wrappedPayload.Payload = results
return wrappedPayload, nil
}
func (dq *DbQuery) Type() string {
return dq.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "db.query",
Title: "Query Database",
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
moduleIdString, err := params.GetString("module")
if err != nil {
return nil, fmt.Errorf("db.query module error: %w", err)
}
queryString, err := params.GetString("query")
if err != nil {
return nil, fmt.Errorf("db.query query error: %w", err)
}
queryTemplate, err := template.New("query").Parse(queryString)
if err != nil {
return nil, err
}
return &DbQuery{config: config, ModuleId: moduleIdString, Query: queryTemplate, logger: slog.Default().With("component", "processor", "type", config.Type)}, nil
},
})
}

View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"log/slog" "log/slog"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
@@ -13,11 +14,12 @@ type DebugLog struct {
logger *slog.Logger logger *slog.Logger
} }
func (dl *DebugLog) Process(ctx context.Context, payload any) (any, error) { func (dl *DebugLog) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
payload := wrappedPayload.Payload
payloadString := fmt.Sprintf("%+v", payload) payloadString := fmt.Sprintf("%+v", payload)
payloadType := fmt.Sprintf("%T", payload) payloadType := fmt.Sprintf("%T", payload)
dl.logger.Debug("", "payload", payloadString, "payloadType", payloadType) dl.logger.Debug("", "payload", payloadString, "payloadType", payloadType)
return payload, nil return wrappedPayload, nil
} }
func (dl *DebugLog) Type() string { func (dl *DebugLog) Type() string {
@@ -26,7 +28,8 @@ func (dl *DebugLog) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "debug.log", Type: "debug.log",
Title: "Debug Log",
New: func(config config.ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
return &DebugLog{config: config, logger: slog.Default().With("component", "processor", "type", config.Type)}, nil return &DebugLog{config: config, logger: slog.Default().With("component", "processor", "type", config.Type)}, nil
}, },

View File

@@ -0,0 +1,40 @@
package processor
import (
"context"
"reflect"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
type FilterChange struct {
config config.ProcessorConfig
previous any
}
func (fc *FilterChange) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
payload := wrappedPayload.Payload
if reflect.DeepEqual(payload, fc.previous) {
wrappedPayload.End = true
return wrappedPayload, nil
}
fc.previous = payload
return wrappedPayload, nil
}
func (fc *FilterChange) Type() string {
return fc.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "filter.change",
Title: "Filter On Change",
New: func(config config.ProcessorConfig) (Processor, error) {
return &FilterChange{config: config}, nil
},
})
}

View File

@@ -0,0 +1,80 @@
package processor
import (
"context"
"fmt"
"github.com/expr-lang/expr"
"github.com/expr-lang/expr/vm"
"github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
// NOTE(jwetzell): see language definition https://expr-lang.org/docs/language-definition
type FilterExpr struct {
config config.ProcessorConfig
Program *vm.Program
}
func (fe *FilterExpr) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
exprEnv := wrappedPayload
output, err := expr.Run(fe.Program, exprEnv)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
}
outputBool, ok := output.(bool)
if !ok {
wrappedPayload.End = true
return wrappedPayload, fmt.Errorf("filter.expr expression did not return a boolean")
}
if !outputBool {
wrappedPayload.End = true
return wrappedPayload, nil
}
wrappedPayload.Payload = exprEnv.Payload
return wrappedPayload, nil
}
func (fe *FilterExpr) Type() string {
return fe.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "filter.expr",
Title: "Filter by Expr expression",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"expression": {
Title: "Expression",
Type: "string",
},
},
Required: []string{"expression"},
AdditionalProperties: nil,
},
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
expressionString, err := params.GetString("expression")
if err != nil {
return nil, fmt.Errorf("filter.expr expression error: %w", err)
}
program, err := expr.Compile(expressionString)
if err != nil {
return nil, err
}
return &FilterExpr{config: config, Program: program}, nil
},
})
}

View File

@@ -0,0 +1,73 @@
package processor
import (
"context"
"errors"
"fmt"
"regexp"
"github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
type FilterRegex struct {
config config.ProcessorConfig
Pattern *regexp.Regexp
}
func (fr *FilterRegex) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
payload := wrappedPayload.Payload
payloadString, ok := common.GetAnyAs[string](payload)
if !ok {
wrappedPayload.End = true
return wrappedPayload, errors.New("filter.regex processor only accepts a string")
}
if !fr.Pattern.MatchString(payloadString) {
wrappedPayload.End = true
return wrappedPayload, nil
}
wrappedPayload.Payload = payloadString
return wrappedPayload, nil
}
func (fr *FilterRegex) Type() string {
return fr.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "filter.regex",
Title: "Filter by Regex",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"pattern": {
Title: "Pattern",
Type: "string",
},
},
Required: []string{"pattern"},
AdditionalProperties: nil,
},
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
patternString, err := params.GetString("pattern")
if err != nil {
return nil, fmt.Errorf("filter.regex pattern error: %w", err)
}
patternRegexp, err := regexp.Compile(patternString)
if err != nil {
return nil, err
}
return &FilterRegex{config: config, Pattern: patternRegexp}, nil
},
})
}

View File

@@ -2,9 +2,13 @@ package processor
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt"
"strconv" "strconv"
"github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
@@ -13,18 +17,22 @@ type FloatParse struct {
config config.ProcessorConfig config config.ProcessorConfig
} }
func (fp *FloatParse) Process(ctx context.Context, payload any) (any, error) { func (fp *FloatParse) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
payloadString, ok := payload.(string) payload := wrappedPayload.Payload
payloadString, ok := common.GetAnyAs[string](payload)
if !ok { if !ok {
return nil, errors.New("float.parse processor only accepts a string") wrappedPayload.End = true
return wrappedPayload, errors.New("float.parse processor only accepts a string")
} }
payloadFloat, err := strconv.ParseFloat(payloadString, fp.BitSize) payloadFloat, err := strconv.ParseFloat(payloadString, fp.BitSize)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
return payloadFloat, nil wrappedPayload.Payload = payloadFloat
return wrappedPayload, nil
} }
func (fp *FloatParse) Type() string { func (fp *FloatParse) Type() string {
@@ -33,21 +41,32 @@ func (fp *FloatParse) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "float.parse", Type: "float.parse",
New: func(config config.ProcessorConfig) (Processor, error) { Title: "Parse Float",
params := config.Params ParamsSchema: &jsonschema.Schema{
bitSizeNum := 64 Type: "object",
bitSize, ok := params["bitSize"] Properties: map[string]*jsonschema.Schema{
if ok { "bitSize": {
bitSizeFloat, ok := bitSize.(float64) Title: "Bit Size",
Type: "integer",
Enum: []any{32, 64},
Default: json.RawMessage("64"),
},
},
AdditionalProperties: nil,
},
New: func(moduleConfig config.ProcessorConfig) (Processor, error) {
params := moduleConfig.Params
if !ok { bitSizeNum, err := params.GetInt("bitSize")
return nil, errors.New("float.parse bitSize must be a number") if err != nil {
if errors.Is(err, config.ErrParamNotFound) {
bitSizeNum = 64
} else {
return nil, fmt.Errorf("float.parse bitSize error: %w", err)
} }
bitSizeNum = int(bitSizeFloat)
} }
return &FloatParse{config: config, BitSize: bitSizeNum}, nil return &FloatParse{config: moduleConfig, BitSize: bitSizeNum}, nil
}, },
}) })
} }

View File

@@ -0,0 +1,99 @@
package processor
import (
"context"
"encoding/json"
"errors"
"fmt"
"math/rand/v2"
"github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
type FloatRandom struct {
BitSize int
Min float64
Max float64
config config.ProcessorConfig
}
func (fr *FloatRandom) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
if fr.BitSize == 32 {
payloadFloat := rand.Float32()*(float32(fr.Max)-float32(fr.Min)) + float32(fr.Min)
wrappedPayload.Payload = payloadFloat
return wrappedPayload, nil
}
if fr.BitSize == 64 {
payloadFloat := rand.Float64()*(fr.Max-fr.Min) + fr.Min
wrappedPayload.Payload = payloadFloat
return wrappedPayload, nil
}
wrappedPayload.End = true
return wrappedPayload, errors.New("float.random bitSize error: must be 32 or 64")
}
func (fr *FloatRandom) Type() string {
return fr.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "float.random",
Title: "Random Float",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"bitSize": {
Title: "Bit Size",
Type: "integer",
Enum: []any{32, 64},
Default: json.RawMessage("32"),
},
"min": {
Title: "Minimum",
Type: "number",
},
"max": {
Title: "Maximum",
Type: "number",
},
},
Required: []string{"min", "max"},
AdditionalProperties: nil,
},
New: func(processorConfig config.ProcessorConfig) (Processor, error) {
params := processorConfig.Params
bitSizeInt, err := params.GetInt("bitSize")
if err != nil {
if errors.Is(err, config.ErrParamNotFound) {
bitSizeInt = 32
} else {
return nil, fmt.Errorf("float.random bitSize error: %w", err)
}
}
if bitSizeInt != 32 && bitSizeInt != 64 {
return nil, errors.New("float.random bitSize error: must be 32 or 64")
}
minFloat, err := params.GetFloat64("min")
if err != nil {
return nil, fmt.Errorf("float.random min error: %w", err)
}
maxFloat, err := params.GetFloat64("max")
if err != nil {
return nil, fmt.Errorf("float.random max error: %w", err)
}
if maxFloat < minFloat {
return nil, errors.New("float.random max must be greater than min")
}
return &FloatRandom{config: processorConfig, Min: minFloat, Max: maxFloat, BitSize: bitSizeInt}, nil
},
})
}

View File

@@ -3,11 +3,13 @@ package processor
import ( import (
"bytes" "bytes"
"context" "context"
"errors" "fmt"
"strconv" "strconv"
"text/template" "text/template"
"github.com/google/jsonschema-go/jsonschema"
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"
) )
@@ -24,13 +26,16 @@ type FreeDCreate struct {
Focus *template.Template Focus *template.Template
} }
func (fc *FreeDCreate) Process(ctx context.Context, payload any) (any, error) { func (fc *FreeDCreate) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
templateData := wrappedPayload
var idBuffer bytes.Buffer var idBuffer bytes.Buffer
err := fc.Id.Execute(&idBuffer, payload) err := fc.Id.Execute(&idBuffer, templateData)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
idString := idBuffer.String() idString := idBuffer.String()
@@ -38,14 +43,16 @@ func (fc *FreeDCreate) Process(ctx context.Context, payload any) (any, error) {
idNum, err := strconv.ParseUint(idString, 10, 8) idNum, err := strconv.ParseUint(idString, 10, 8)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
var panBuffer bytes.Buffer var panBuffer bytes.Buffer
err = fc.Pan.Execute(&panBuffer, payload) err = fc.Pan.Execute(&panBuffer, templateData)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
panString := panBuffer.String() panString := panBuffer.String()
@@ -53,14 +60,16 @@ func (fc *FreeDCreate) Process(ctx context.Context, payload any) (any, error) {
panNum, err := strconv.ParseFloat(panString, 32) panNum, err := strconv.ParseFloat(panString, 32)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
var tiltBuffer bytes.Buffer var tiltBuffer bytes.Buffer
err = fc.Tilt.Execute(&tiltBuffer, payload) err = fc.Tilt.Execute(&tiltBuffer, templateData)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
tiltString := tiltBuffer.String() tiltString := tiltBuffer.String()
@@ -68,14 +77,16 @@ func (fc *FreeDCreate) Process(ctx context.Context, payload any) (any, error) {
tiltNum, err := strconv.ParseFloat(tiltString, 32) tiltNum, err := strconv.ParseFloat(tiltString, 32)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
var rollBuffer bytes.Buffer var rollBuffer bytes.Buffer
err = fc.Tilt.Execute(&rollBuffer, payload) err = fc.Roll.Execute(&rollBuffer, templateData)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
rollString := rollBuffer.String() rollString := rollBuffer.String()
@@ -83,14 +94,16 @@ func (fc *FreeDCreate) Process(ctx context.Context, payload any) (any, error) {
rollNum, err := strconv.ParseFloat(rollString, 32) rollNum, err := strconv.ParseFloat(rollString, 32)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
var posXBuffer bytes.Buffer var posXBuffer bytes.Buffer
err = fc.PosX.Execute(&posXBuffer, payload) err = fc.PosX.Execute(&posXBuffer, templateData)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
posXString := posXBuffer.String() posXString := posXBuffer.String()
@@ -98,14 +111,16 @@ func (fc *FreeDCreate) Process(ctx context.Context, payload any) (any, error) {
posXNum, err := strconv.ParseFloat(posXString, 32) posXNum, err := strconv.ParseFloat(posXString, 32)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
var posYBuffer bytes.Buffer var posYBuffer bytes.Buffer
err = fc.PosY.Execute(&posYBuffer, payload) err = fc.PosY.Execute(&posYBuffer, templateData)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
posYString := posYBuffer.String() posYString := posYBuffer.String()
@@ -113,14 +128,16 @@ func (fc *FreeDCreate) Process(ctx context.Context, payload any) (any, error) {
posYNum, err := strconv.ParseFloat(posYString, 32) posYNum, err := strconv.ParseFloat(posYString, 32)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
var posZBuffer bytes.Buffer var posZBuffer bytes.Buffer
err = fc.PosZ.Execute(&posZBuffer, payload) err = fc.PosZ.Execute(&posZBuffer, templateData)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
posZString := posZBuffer.String() posZString := posZBuffer.String()
@@ -128,14 +145,16 @@ func (fc *FreeDCreate) Process(ctx context.Context, payload any) (any, error) {
posZNum, err := strconv.ParseFloat(posZString, 32) posZNum, err := strconv.ParseFloat(posZString, 32)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
var zoomBuffer bytes.Buffer var zoomBuffer bytes.Buffer
err = fc.Zoom.Execute(&zoomBuffer, payload) err = fc.Zoom.Execute(&zoomBuffer, templateData)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
zoomString := zoomBuffer.String() zoomString := zoomBuffer.String()
@@ -143,14 +162,16 @@ func (fc *FreeDCreate) Process(ctx context.Context, payload any) (any, error) {
zoomNum, err := strconv.ParseInt(zoomString, 10, 32) zoomNum, err := strconv.ParseInt(zoomString, 10, 32)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
var focusBuffer bytes.Buffer var focusBuffer bytes.Buffer
err = fc.Zoom.Execute(&focusBuffer, payload) err = fc.Focus.Execute(&focusBuffer, templateData)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
focusString := focusBuffer.String() focusString := focusBuffer.String()
@@ -158,7 +179,8 @@ func (fc *FreeDCreate) Process(ctx context.Context, payload any) (any, error) {
focusNum, err := strconv.ParseInt(focusString, 10, 32) focusNum, err := strconv.ParseInt(focusString, 10, 32)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
payloadMessage := freeD.FreeDPosition{ payloadMessage := freeD.FreeDPosition{
@@ -173,7 +195,9 @@ func (fc *FreeDCreate) Process(ctx context.Context, payload any) (any, error) {
Focus: int32(focusNum), Focus: int32(focusNum),
} }
return payloadMessage, nil wrappedPayload.Payload = payloadMessage
return wrappedPayload, nil
} }
func (fc *FreeDCreate) Type() string { func (fc *FreeDCreate) Type() string {
@@ -182,21 +206,68 @@ func (fc *FreeDCreate) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "freed.create", Type: "freed.create",
Title: "Create FreeD",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"id": {
Title: "Camera ID",
Type: "string",
},
"pan": {
Title: "Pan",
Type: "string",
},
"tilt": {
Title: "Tilt",
Type: "string",
},
"roll": {
Title: "Roll",
Type: "string",
},
"posX": {
Title: "Position X",
Type: "string",
},
"posY": {
Title: "Position Y",
Type: "string",
},
"posZ": {
Title: "Position Z",
Type: "string",
},
"zoom": {
Title: "Zoom",
Type: "string",
},
"focus": {
Title: "Focus",
Type: "string",
},
},
Required: []string{
"id",
"pan",
"tilt",
"roll",
"posX",
"posY",
"posZ",
"zoom",
"focus",
},
AdditionalProperties: nil,
},
New: func(config config.ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
// TODO(jwetzell): make some params optional // TODO(jwetzell): make some params optional
params := config.Params params := config.Params
id, ok := params["id"] idString, err := params.GetString("id")
if err != nil {
if !ok { return nil, fmt.Errorf("freed.create id error: %w", err)
return nil, errors.New("freed.create requires an id parameter")
}
idString, ok := id.(string)
if !ok {
return nil, errors.New("freed.create id must be a string")
} }
idTemplate, err := template.New("id").Parse(idString) idTemplate, err := template.New("id").Parse(idString)
@@ -205,44 +276,31 @@ func init() {
return nil, err return nil, err
} }
pan, ok := params["pan"] panString, err := params.GetString("pan")
if err != nil {
if !ok { return nil, fmt.Errorf("freed.create pan error: %w", err)
return nil, errors.New("freed.create requires a pan parameter")
}
panString, ok := pan.(string)
if !ok {
return nil, errors.New("freed.create pan must be a string")
} }
panTemplate, err := template.New("pan").Parse(panString) panTemplate, err := template.New("pan").Parse(panString)
tilt, ok := params["tilt"] if err != nil {
return nil, err
if !ok {
return nil, errors.New("freed.create requires a tilt parameter")
} }
tiltString, ok := tilt.(string) tiltString, err := params.GetString("tilt")
if err != nil {
if !ok { return nil, fmt.Errorf("freed.create tilt error: %w", err)
return nil, errors.New("freed.create tilt must be a string")
} }
tiltTemplate, err := template.New("tilt").Parse(tiltString) tiltTemplate, err := template.New("tilt").Parse(tiltString)
roll, ok := params["roll"] if err != nil {
return nil, err
if !ok {
return nil, errors.New("freed.create requires a roll parameter")
} }
rollString, ok := roll.(string) rollString, err := params.GetString("roll")
if err != nil {
if !ok { return nil, fmt.Errorf("freed.create roll error: %w", err)
return nil, errors.New("freed.create roll must be a string")
} }
rollTemplate, err := template.New("roll").Parse(rollString) rollTemplate, err := template.New("roll").Parse(rollString)
@@ -251,16 +309,9 @@ func init() {
return nil, err return nil, err
} }
posX, ok := params["posX"] posXString, err := params.GetString("posX")
if err != nil {
if !ok { return nil, fmt.Errorf("freed.create posX error: %w", err)
return nil, errors.New("freed.create requires a posX parameter")
}
posXString, ok := posX.(string)
if !ok {
return nil, errors.New("freed.create posX must be a string")
} }
posXTemplate, err := template.New("posX").Parse(posXString) posXTemplate, err := template.New("posX").Parse(posXString)
@@ -269,16 +320,9 @@ func init() {
return nil, err return nil, err
} }
posY, ok := params["posY"] posYString, err := params.GetString("posY")
if err != nil {
if !ok { return nil, fmt.Errorf("freed.create posY error: %w", err)
return nil, errors.New("freed.create requires a posY parameter")
}
posYString, ok := posY.(string)
if !ok {
return nil, errors.New("freed.create posY must be a string")
} }
posYTemplate, err := template.New("posY").Parse(posYString) posYTemplate, err := template.New("posY").Parse(posYString)
@@ -287,16 +331,9 @@ func init() {
return nil, err return nil, err
} }
posZ, ok := params["posZ"] posZString, err := params.GetString("posZ")
if err != nil {
if !ok { return nil, fmt.Errorf("freed.create posZ error: %w", err)
return nil, errors.New("freed.create requires a posZ parameter")
}
posZString, ok := posZ.(string)
if !ok {
return nil, errors.New("freed.create posZ must be a string")
} }
posZTemplate, err := template.New("posZ").Parse(posZString) posZTemplate, err := template.New("posZ").Parse(posZString)
@@ -305,33 +342,26 @@ func init() {
return nil, err return nil, err
} }
zoom, ok := params["zoom"] zoomString, err := params.GetString("zoom")
if err != nil {
if !ok { return nil, fmt.Errorf("freed.create zoom error: %w", err)
return nil, errors.New("freed.create requires a zoom parameter")
}
zoomString, ok := zoom.(string)
if !ok {
return nil, errors.New("freed.create zoom must be a string")
} }
zoomTemplate, err := template.New("zoom").Parse(zoomString) zoomTemplate, err := template.New("zoom").Parse(zoomString)
focus, ok := params["focus"] if err != nil {
return nil, err
if !ok {
return nil, errors.New("freed.create requires a focus parameter")
} }
focusString, ok := focus.(string) focusString, err := params.GetString("focus")
if err != nil {
if !ok { return nil, fmt.Errorf("freed.create focus error: %w", err)
return nil, errors.New("freed.create focus must be a string")
} }
focusTemplate, err := template.New("focus").Parse(focusString) focusTemplate, err := template.New("focus").Parse(focusString)
if err != nil {
return nil, err
}
return &FreeDCreate{ return &FreeDCreate{
config: config, config: config,

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,27 +13,32 @@ 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, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
payloadBytes, ok := payload.([]byte) payload := wrappedPayload.Payload
payloadBytes, ok := common.GetAnyAsByteSlice(payload)
if !ok { if !ok {
return nil, errors.New("freed.decode processor only accepts a []byte") wrappedPayload.End = true
return wrappedPayload, errors.New("freed.decode processor only accepts a []byte")
} }
payloadMessage, err := freeD.Decode(payloadBytes) payloadMessage, err := freeD.Decode(payloadBytes)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
return payloadMessage, nil wrappedPayload.Payload = payloadMessage
return wrappedPayload, nil
} }
func (fdd *FreeDDecode) Type() string { func (fd *FreeDDecode) Type() string {
return fdd.config.Type return fd.config.Type
} }
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "freed.decode", Type: "freed.decode",
Title: "Decode FreeD",
New: func(config config.ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
return &FreeDDecode{config: config}, nil return &FreeDDecode{config: config}, nil
}, },

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,24 +13,29 @@ 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, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
payloadPosition, ok := payload.(freeD.FreeDPosition) payload := wrappedPayload.Payload
payloadPosition, ok := common.GetAnyAs[freeD.FreeDPosition](payload)
if !ok { if !ok {
return nil, errors.New("freed.decode processor only accepts a FreeDEncode") wrappedPayload.End = true
return wrappedPayload, errors.New("freed.encode processor only accepts a FreeDPosition")
} }
payloadBytes := freeD.Encode(payloadPosition) payloadBytes := freeD.Encode(payloadPosition)
return payloadBytes, nil
wrappedPayload.Payload = payloadBytes
return wrappedPayload, nil
} }
func (fde *FreeDEncode) Type() string { func (fe *FreeDEncode) Type() string {
return fde.config.Type return fe.config.Type
} }
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "freed.encode", Type: "freed.encode",
Title: "Encode FreeD",
New: func(config config.ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
return &FreeDEncode{config: config}, nil return &FreeDEncode{config: config}, nil
}, },

View File

@@ -1,82 +0,0 @@
package processor
import (
"bytes"
"context"
"errors"
"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) {
var urlBuffer bytes.Buffer
err := hrc.URL.Execute(&urlBuffer, payload)
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
method, ok := params["method"]
if !ok {
return nil, errors.New("http.request.create requires a method parameter")
}
methodString, ok := method.(string)
if !ok {
return nil, errors.New("http.request.create url must be a string")
}
url, ok := params["url"]
if !ok {
return nil, errors.New("http.request.create requires a url parameter")
}
urlString, ok := url.(string)
if !ok {
return nil, errors.New("http.request.create url must be a string")
}
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,117 @@
package processor
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"text/template"
"time"
"github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common"
"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, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
templateData := wrappedPayload
var urlBuffer bytes.Buffer
err := hrd.URL.Execute(&urlBuffer, templateData)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
}
urlString := urlBuffer.String()
//TODO(jwetzell): support body
request, err := http.NewRequest(hrd.Method, urlString, bytes.NewBuffer([]byte{}))
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
}
response, err := hrd.client.Do(request)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
}
body, err := io.ReadAll(response.Body)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
}
//TODO(jwetzell): support headers, etc
wrappedPayload.Payload = HTTPResponse{
Status: response.StatusCode,
Body: body,
}
return wrappedPayload, nil
}
func (hrd *HTTPRequestDo) Type() string {
return hrd.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "http.request.do",
Title: "Do HTTP Request",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"method": {
Title: "HTTP Method",
Type: "string",
Enum: []any{"GET", "POST"},
},
"url": {
Title: "URL",
Type: "string",
},
},
Required: []string{"method", "url"},
AdditionalProperties: nil,
},
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,42 +0,0 @@
package processor
import (
"context"
"errors"
"io"
"net/http"
"github.com/jwetzell/showbridge-go/internal/config"
)
type HTTPRequestEncode struct {
config config.ProcessorConfig
}
func (hre *HTTPRequestEncode) Process(ctx context.Context, payload any) (any, error) {
payloadRequest, ok := payload.(*http.Request)
if !ok {
return nil, errors.New("http.request.encode processor only accepts an http.Request")
}
bytes, err := io.ReadAll(payloadRequest.Body)
if err != nil {
return nil, err
}
return bytes, nil
}
func (hre *HTTPRequestEncode) Type() string {
return hre.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "http.request.encode",
New: func(config config.ProcessorConfig) (Processor, error) {
return &HTTPRequestEncode{config: config}, nil
},
})
}

View File

@@ -1,81 +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 := payload.(*http.Request)
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(config config.ProcessorConfig) (Processor, error) {
params := config.Params
path, ok := params["path"]
if !ok {
return nil, errors.New("http.request.filter requires a path parameter")
}
pathString, ok := path.(string)
if !ok {
return nil, errors.New("http.request.filter path must be a string")
}
pathRegexp, err := regexp.Compile(fmt.Sprintf("^%s$", pathString))
if err != nil {
return nil, err
}
method, ok := params["method"]
if ok {
methodString, ok := method.(string)
if !ok {
return nil, errors.New("http.request.filter method must be a string")
}
return &HTTPRequestFilter{config: config, Path: pathRegexp, Method: methodString}, nil
}
return &HTTPRequestFilter{config: config, Path: pathRegexp}, nil
},
})
}

View File

@@ -3,9 +3,11 @@ package processor
import ( import (
"bytes" "bytes"
"context" "context"
"errors" "fmt"
"text/template" "text/template"
"github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
@@ -20,52 +22,57 @@ 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, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
templateData := wrappedPayload
var bodyBuffer bytes.Buffer var bodyBuffer bytes.Buffer
err := hre.BodyTmpl.Execute(&bodyBuffer, payload) err := hrc.BodyTmpl.Execute(&bodyBuffer, templateData)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
wrappedPayload.Payload = HTTPResponse{
return HTTPResponse{ Status: hrc.Status,
Status: hre.Status,
Body: bodyBuffer.Bytes(), Body: bodyBuffer.Bytes(),
}, nil }
return wrappedPayload, nil
} }
func (hre *HTTPResponseCreate) Type() string { func (hrc *HTTPResponseCreate) Type() string {
return hre.config.Type return hrc.config.Type
} }
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "http.response.create", Type: "http.response.create",
Title: "Create HTTP Response",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"status": {
Title: "Status Code",
Type: "integer",
},
"body": {
Title: "Body",
Type: "string",
},
},
Required: []string{"status", "body"},
AdditionalProperties: nil,
},
New: func(config config.ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params params := config.Params
status, ok := params["status"] statusNum, err := params.GetInt("status")
if err != nil {
if !ok { return nil, fmt.Errorf("http.response.create status error: %w", err)
return nil, errors.New("http.response.create requires a status parameter")
} }
statusNum, ok := status.(float64) bodyTemplateString, err := params.GetString("bodyTemplate")
if err != nil {
if !ok { return nil, fmt.Errorf("http.response.create bodyTemplate error: %w", err)
return nil, errors.New("http.response.create status must be a number")
}
bodyTmpl, ok := params["bodyTemplate"]
if !ok {
return nil, errors.New("http.response.create requires a bodyTemplate parameter")
}
bodyTemplateString, ok := bodyTmpl.(string)
if !ok {
return nil, errors.New("http.response.create bodyTemplate must be a string")
} }
bodyTemplate, err := template.New("body").Parse(bodyTemplateString) bodyTemplate, err := template.New("body").Parse(bodyTemplateString)

View File

@@ -1,43 +0,0 @@
package processor
import (
"context"
"errors"
"io"
"net/http"
"github.com/jwetzell/showbridge-go/internal/config"
)
type HTTPResponseEncode struct {
config config.ProcessorConfig
}
func (hre *HTTPResponseEncode) Process(ctx context.Context, payload any) (any, error) {
payloadResponse, ok := payload.(*http.Response)
if !ok {
return nil, errors.New("http.response.encode processor only accepts an http.Response")
}
defer payloadResponse.Body.Close()
bytes, err := io.ReadAll(payloadResponse.Body)
if err != nil {
return nil, err
}
return bytes, nil
}
func (hre *HTTPResponseEncode) Type() string {
return hre.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "http.response.encode",
New: func(config config.ProcessorConfig) (Processor, error) {
return &HTTPResponseEncode{config: config}, nil
},
})
}

View File

@@ -2,9 +2,13 @@ package processor
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt"
"strconv" "strconv"
"github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
@@ -14,18 +18,22 @@ type IntParse struct {
config config.ProcessorConfig config config.ProcessorConfig
} }
func (ip *IntParse) Process(ctx context.Context, payload any) (any, error) { func (ip *IntParse) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
payloadString, ok := payload.(string) payload := wrappedPayload.Payload
payloadString, ok := common.GetAnyAs[string](payload)
if !ok { if !ok {
return nil, errors.New("int.parse processor only accepts a string") wrappedPayload.End = true
return wrappedPayload, errors.New("int.parse processor only accepts a string")
} }
payloadInt, err := strconv.ParseInt(payloadString, ip.Base, ip.BitSize) payloadInt, err := strconv.ParseInt(payloadString, ip.Base, ip.BitSize)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
return payloadInt, nil wrappedPayload.Payload = payloadInt
return wrappedPayload, nil
} }
func (ip *IntParse) Type() string { func (ip *IntParse) Type() string {
@@ -34,34 +42,47 @@ func (ip *IntParse) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "int.parse", Type: "int.parse",
New: func(config config.ProcessorConfig) (Processor, error) { Title: "Parse Int",
params := config.Params ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"base": {
Title: "Base",
Type: "integer",
Enum: []any{0, 2, 8, 10, 16},
Default: json.RawMessage("10"),
},
"bitSize": {
Title: "Bit Size",
Type: "integer",
Enum: []any{0, 8, 16, 32, 64},
Default: json.RawMessage("64"),
},
},
AdditionalProperties: nil,
},
New: func(moduleConfig config.ProcessorConfig) (Processor, error) {
params := moduleConfig.Params
baseNum := 10 baseNum, err := params.GetInt("base")
base, ok := params["base"] if err != nil {
if ok { if errors.Is(err, config.ErrParamNotFound) {
baseFloat, ok := base.(float64) baseNum = 10
} else {
if !ok { return nil, fmt.Errorf("int.parse base error: %w", err)
return nil, errors.New("int.parse base must be a number")
} }
baseNum = int(baseFloat)
} }
bitSizeNum := 64 bitSizeNum, err := params.GetInt("bitSize")
bitSize, ok := params["bitSize"] if err != nil {
if ok { if errors.Is(err, config.ErrParamNotFound) {
bitSizeFloat, ok := bitSize.(float64) bitSizeNum = 64
} else {
if !ok { return nil, fmt.Errorf("int.parse bitSize error: %w", err)
return nil, errors.New("int.parse bitSize must be a number")
} }
bitSizeNum = int(bitSizeFloat)
} }
return &IntParse{config: config, Base: baseNum, BitSize: bitSizeNum}, nil return &IntParse{config: moduleConfig, Base: baseNum, BitSize: bitSizeNum}, nil
}, },
}) })
} }

View File

@@ -3,8 +3,11 @@ package processor
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"math/rand/v2" "math/rand/v2"
"github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
@@ -14,48 +17,53 @@ type IntRandom struct {
config config.ProcessorConfig config config.ProcessorConfig
} }
func (up *IntRandom) Process(ctx context.Context, payload any) (any, error) { func (ir *IntRandom) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
payloadInt := rand.IntN(up.Max-up.Min+1) + up.Min payloadInt := rand.IntN(ir.Max-ir.Min+1) + ir.Min
return payloadInt, nil wrappedPayload.Payload = payloadInt
return wrappedPayload, nil
} }
func (up *IntRandom) Type() string { func (ir *IntRandom) Type() string {
return up.config.Type return ir.config.Type
} }
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "int.random", Type: "int.random",
Title: "Random Int",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"min": {
Title: "Minimum",
Type: "integer",
},
"max": {
Title: "Maximum",
Type: "integer",
},
},
Required: []string{"min", "max"},
AdditionalProperties: nil,
},
New: func(config config.ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params params := config.Params
min, ok := params["min"] minInt, err := params.GetInt("min")
if !ok { if err != nil {
return nil, errors.New("int.random requires a min parameter") return nil, fmt.Errorf("int.random min error: %w", err)
} }
minFloat, ok := min.(float64) maxInt, err := params.GetInt("max")
if err != nil {
if !ok { return nil, fmt.Errorf("int.random max error: %w", err)
return nil, errors.New("int.random min must be a number")
} }
max, ok := params["max"] if maxInt < minInt {
if !ok {
return nil, errors.New("int.random requires a max parameter")
}
maxFloat, ok := max.(float64)
if !ok {
return nil, errors.New("int.random max must be a number")
}
if maxFloat < minFloat {
return nil, errors.New("int.random max must be greater than min") return nil, errors.New("int.random max must be greater than min")
} }
return &IntRandom{config: config, Min: int(minFloat), Max: int(maxFloat)}, nil return &IntRandom{config: config, Min: int(minInt), Max: int(maxInt)}, nil
}, },
}) })
} }

View File

@@ -0,0 +1,99 @@
package processor
import (
"context"
"errors"
"fmt"
"github.com/google/jsonschema-go/jsonschema"
"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, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
payload := wrappedPayload.Payload
payloadInt, ok := common.GetAnyAs[int](payload)
if !ok {
wrappedPayload.End = true
return wrappedPayload, errors.New("int.scale can only process an int")
}
payloadInt = (payloadInt-ir.InMin)*(ir.OutMax-ir.OutMin)/(ir.InMax-ir.InMin) + ir.OutMin
wrappedPayload.Payload = payloadInt
return wrappedPayload, nil
}
func (ir *IntScale) Type() string {
return ir.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "int.scale",
Title: "Scale Int",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"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: []string{"inMin", "inMax", "outMin", "outMax"},
AdditionalProperties: nil,
},
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"
) )
@@ -12,21 +13,30 @@ type JsonDecode struct {
config config.ProcessorConfig config config.ProcessorConfig
} }
func (jd *JsonDecode) Process(ctx context.Context, payload any) (any, error) { func (jd *JsonDecode) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
payloadString, ok := payload.(string) payload := wrappedPayload.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 {
wrappedPayload.End = true
return wrappedPayload, 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 wrappedPayload.End = true
return wrappedPayload, err
} }
return payloadJson, nil wrappedPayload.Payload = payloadJson
return wrappedPayload, nil
} }
@@ -36,7 +46,8 @@ func (jd *JsonDecode) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "json.decode", Type: "json.decode",
Title: "Decode JSON",
New: func(config config.ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
return &JsonDecode{config: config}, nil return &JsonDecode{config: config}, nil
}, },

View File

@@ -5,6 +5,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config" "github.com/jwetzell/showbridge-go/internal/config"
) )
@@ -12,20 +13,23 @@ type JsonEncode struct {
config config.ProcessorConfig config config.ProcessorConfig
} }
func (je *JsonEncode) Process(ctx context.Context, payload any) (any, error) { func (je *JsonEncode) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
payload := wrappedPayload.Payload
var payloadBuffer bytes.Buffer var payloadBuffer bytes.Buffer
err := json.NewEncoder(&payloadBuffer).Encode(payload) err := json.NewEncoder(&payloadBuffer).Encode(payload)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
payloadBytes := payloadBuffer.Bytes() payloadBytes := payloadBuffer.Bytes()
payloadBytes = payloadBytes[0 : len(payloadBytes)-1] payloadBytes = payloadBytes[0 : len(payloadBytes)-1]
return payloadBytes, nil wrappedPayload.Payload = payloadBytes
return wrappedPayload, nil
} }
func (je *JsonEncode) Type() string { func (je *JsonEncode) Type() string {
@@ -34,7 +38,8 @@ func (je *JsonEncode) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "json.encode", Type: "json.encode",
Title: "Encode JSON",
New: func(config config.ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
return &JsonEncode{config: config}, nil return &JsonEncode{config: config}, nil
}, },

View File

@@ -0,0 +1,88 @@
package processor
import (
"context"
"errors"
"fmt"
"log/slog"
"github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
type KVGet struct {
config config.ProcessorConfig
ModuleId string
Key string
logger *slog.Logger
}
func (kvg *KVGet) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
if wrappedPayload.Modules == nil {
wrappedPayload.End = true
return wrappedPayload, errors.New("kv.get wrapped payload has no modules")
}
module, ok := wrappedPayload.Modules[kvg.ModuleId]
if !ok {
wrappedPayload.End = true
return wrappedPayload, fmt.Errorf("kv.get unable to find module with id: %s", kvg.ModuleId)
}
kvModule, ok := module.(common.KeyValueModule)
if !ok {
wrappedPayload.End = true
return wrappedPayload, fmt.Errorf("kv.get module with id %s is not a KeyValueModule", kvg.ModuleId)
}
value, err := kvModule.Get(kvg.Key)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, fmt.Errorf("kv.get error getting key: %w", err)
}
wrappedPayload.Payload = value
return wrappedPayload, nil
}
func (kvg *KVGet) Type() string {
return kvg.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "kv.get",
Title: "Get Key",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"module": {
Title: "Module ID",
Type: "string",
},
"key": {
Title: "Key",
Type: "string",
},
},
Required: []string{"module", "key"},
AdditionalProperties: nil,
},
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
moduleIdString, err := params.GetString("module")
if err != nil {
return nil, fmt.Errorf("kv.get module error: %w", err)
}
keyString, err := params.GetString("key")
if err != nil {
return nil, fmt.Errorf("kv.get key error: %w", err)
}
return &KVGet{config: config, ModuleId: moduleIdString, Key: keyString, logger: slog.Default().With("component", "processor", "type", config.Type)}, nil
},
})
}

View File

@@ -0,0 +1,114 @@
package processor
import (
"bytes"
"context"
"errors"
"fmt"
"html/template"
"log/slog"
"github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
type KVSet struct {
config config.ProcessorConfig
ModuleId string
Key string
Value *template.Template
logger *slog.Logger
}
func (kvs *KVSet) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
if wrappedPayload.Modules == nil {
wrappedPayload.End = true
return wrappedPayload, errors.New("kv.set wrapped payload has no modules")
}
module, ok := wrappedPayload.Modules[kvs.ModuleId]
if !ok {
wrappedPayload.End = true
return wrappedPayload, fmt.Errorf("kv.set unable to find module with id: %s", kvs.ModuleId)
}
kvModule, ok := module.(common.KeyValueModule)
if !ok {
wrappedPayload.End = true
return wrappedPayload, fmt.Errorf("kv.set module with id %s is not a KeyValueModule", kvs.ModuleId)
}
var valueBuffer bytes.Buffer
err := kvs.Value.Execute(&valueBuffer, wrappedPayload)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
}
err = kvModule.Set(kvs.Key, valueBuffer.String())
if err != nil {
wrappedPayload.End = true
return wrappedPayload, fmt.Errorf("kv.set error setting key: %w", err)
}
return wrappedPayload, nil
}
func (kvs *KVSet) Type() string {
return kvs.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "kv.set",
Title: "Set Key",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"module": {
Title: "Module ID",
Type: "string",
},
"key": {
Title: "Key",
Type: "string",
},
"value": {
Title: "Value",
Type: "string",
},
},
Required: []string{"module", "key", "value"},
AdditionalProperties: nil,
},
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
moduleIdString, err := params.GetString("module")
if err != nil {
return nil, fmt.Errorf("kv.set module error: %w", err)
}
keyString, err := params.GetString("key")
if err != nil {
return nil, fmt.Errorf("kv.set key error: %w", err)
}
valueString, err := params.GetString("value")
if err != nil {
return nil, fmt.Errorf("kv.set value error: %w", err)
}
valueTemplate, err := template.New("template").Parse(valueString)
if err != nil {
return nil, err
}
return &KVSet{config: config, ModuleId: moduleIdString, Key: keyString, Value: valueTemplate, logger: slog.Default().With("component", "processor", "type", config.Type)}, nil
},
})
}

View File

@@ -5,11 +5,12 @@ package processor
import ( import (
"bytes" "bytes"
"context" "context"
"errors"
"fmt" "fmt"
"strconv" "strconv"
"text/template" "text/template"
"github.com/google/jsonschema-go/jsonschema"
"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"
) )
@@ -17,11 +18,11 @@ import (
// TODO(jwetzell): support using numbers in config file treated as hardcoded values // TODO(jwetzell): support using numbers in config file treated as hardcoded values
type MIDIMessageCreate struct { type MIDIMessageCreate struct {
config config.ProcessorConfig config config.ProcessorConfig
ProcessFunc func(ctx context.Context, payload any) (any, error) ProcessFunc func(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error)
} }
func (mmc *MIDIMessageCreate) Process(ctx context.Context, payload any) (any, error) { func (mmc *MIDIMessageCreate) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
return mmc.ProcessFunc(ctx, payload) return mmc.ProcessFunc(ctx, wrappedPayload)
} }
func (mmc *MIDIMessageCreate) Type() string { func (mmc *MIDIMessageCreate) Type() string {
@@ -32,16 +33,9 @@ func newMidiNoteOnCreate(config config.ProcessorConfig) (Processor, error) {
params := config.Params params := config.Params
channel, ok := params["channel"] channelString, err := params.GetString("channel")
if err != nil {
if !ok { return nil, fmt.Errorf("midi.message.create channel error: %w", err)
return nil, errors.New("midi.message.create NoteOn requires a channel parameter")
}
channelString, ok := channel.(string)
if !ok {
return nil, errors.New("midi.message.create NoteOn channel must be a string")
} }
channelTemplate, err := template.New("channel").Parse(channelString) channelTemplate, err := template.New("channel").Parse(channelString)
@@ -50,16 +44,9 @@ func newMidiNoteOnCreate(config config.ProcessorConfig) (Processor, error) {
return nil, err return nil, err
} }
note, ok := params["note"] noteString, err := params.GetString("note")
if err != nil {
if !ok { return nil, fmt.Errorf("midi.message.create note error: %w", err)
return nil, errors.New("midi.message.create NoteOn requires a note parameter")
}
noteString, ok := note.(string)
if !ok {
return nil, errors.New("midi.message.create NoteOn note must be a string")
} }
noteTemplate, err := template.New("note").Parse(noteString) noteTemplate, err := template.New("note").Parse(noteString)
@@ -68,16 +55,9 @@ func newMidiNoteOnCreate(config config.ProcessorConfig) (Processor, error) {
return nil, err return nil, err
} }
velocity, ok := params["velocity"] velocityString, err := params.GetString("velocity")
if err != nil {
if !ok { return nil, fmt.Errorf("midi.message.create velocity error: %w", err)
return nil, errors.New("midi.message.create NoteOn requires a velocity parameter")
}
velocityString, ok := velocity.(string)
if !ok {
return nil, errors.New("midi.message.create NoteOn velocity must be a string")
} }
velocityTemplate, err := template.New("velocity").Parse(velocityString) velocityTemplate, err := template.New("velocity").Parse(velocityString)
@@ -86,36 +66,41 @@ func newMidiNoteOnCreate(config config.ProcessorConfig) (Processor, error) {
return nil, err return nil, err
} }
return &MIDIMessageCreate{config: config, ProcessFunc: func(ctx context.Context, payload any) (any, error) { return &MIDIMessageCreate{config: config, ProcessFunc: func(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
templateData := wrappedPayload
var channelBuffer bytes.Buffer var channelBuffer bytes.Buffer
err := channelTemplate.Execute(&channelBuffer, payload) err := channelTemplate.Execute(&channelBuffer, templateData)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
channelValue, err := strconv.ParseUint(channelBuffer.String(), 10, 8) channelValue, err := strconv.ParseUint(channelBuffer.String(), 10, 8)
var noteBuffer bytes.Buffer var noteBuffer bytes.Buffer
err = noteTemplate.Execute(&noteBuffer, payload) err = noteTemplate.Execute(&noteBuffer, templateData)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
noteValue, err := strconv.ParseUint(noteBuffer.String(), 10, 8) noteValue, err := strconv.ParseUint(noteBuffer.String(), 10, 8)
var velocityBuffer bytes.Buffer var velocityBuffer bytes.Buffer
err = velocityTemplate.Execute(&velocityBuffer, payload) err = velocityTemplate.Execute(&velocityBuffer, templateData)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
velocityValue, err := strconv.ParseUint(velocityBuffer.String(), 10, 8) velocityValue, err := strconv.ParseUint(velocityBuffer.String(), 10, 8)
payloadMessage := midi.NoteOn(uint8(channelValue), uint8(noteValue), uint8(velocityValue)) payloadMessage := midi.NoteOn(uint8(channelValue), uint8(noteValue), uint8(velocityValue))
return payloadMessage, nil wrappedPayload.Payload = payloadMessage
return wrappedPayload, nil
}}, nil }}, nil
} }
@@ -123,16 +108,9 @@ func newMidiNoteOffCreate(config config.ProcessorConfig) (Processor, error) {
params := config.Params params := config.Params
channel, ok := params["channel"] channelString, err := params.GetString("channel")
if err != nil {
if !ok { return nil, fmt.Errorf("midi.message.create channel error: %w", err)
return nil, errors.New("midi.message.create NoteOn requires a channel parameter")
}
channelString, ok := channel.(string)
if !ok {
return nil, errors.New("midi.message.create NoteOn channel must be a string")
} }
channelTemplate, err := template.New("channel").Parse(channelString) channelTemplate, err := template.New("channel").Parse(channelString)
@@ -141,16 +119,9 @@ func newMidiNoteOffCreate(config config.ProcessorConfig) (Processor, error) {
return nil, err return nil, err
} }
note, ok := params["note"] noteString, err := params.GetString("note")
if err != nil {
if !ok { return nil, fmt.Errorf("midi.message.create note error: %w", err)
return nil, errors.New("midi.message.create NoteOn requires a note parameter")
}
noteString, ok := note.(string)
if !ok {
return nil, errors.New("midi.message.create NoteOn note must be a string")
} }
noteTemplate, err := template.New("note").Parse(noteString) noteTemplate, err := template.New("note").Parse(noteString)
@@ -159,28 +130,54 @@ func newMidiNoteOffCreate(config config.ProcessorConfig) (Processor, error) {
return nil, err return nil, err
} }
return &MIDIMessageCreate{config: config, ProcessFunc: func(ctx context.Context, payload any) (any, error) { velocityString, err := params.GetString("velocity")
if err != nil {
return nil, fmt.Errorf("midi.message.create velocity error: %w", err)
}
velocityTemplate, err := template.New("velocity").Parse(velocityString)
if err != nil {
return nil, err
}
return &MIDIMessageCreate{config: config, ProcessFunc: func(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
templateData := wrappedPayload
var channelBuffer bytes.Buffer var channelBuffer bytes.Buffer
err := channelTemplate.Execute(&channelBuffer, payload) err := channelTemplate.Execute(&channelBuffer, templateData)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
channelValue, err := strconv.ParseUint(channelBuffer.String(), 10, 8) channelValue, err := strconv.ParseUint(channelBuffer.String(), 10, 8)
var noteBuffer bytes.Buffer var noteBuffer bytes.Buffer
err = noteTemplate.Execute(&noteBuffer, payload) err = noteTemplate.Execute(&noteBuffer, templateData)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
noteValue, err := strconv.ParseUint(noteBuffer.String(), 10, 8) noteValue, err := strconv.ParseUint(noteBuffer.String(), 10, 8)
payloadMessage := midi.NoteOff(uint8(channelValue), uint8(noteValue)) var velocityBuffer bytes.Buffer
return payloadMessage, nil err = velocityTemplate.Execute(&velocityBuffer, templateData)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
}
velocityValue, err := strconv.ParseUint(velocityBuffer.String(), 10, 8)
payloadMessage := midi.NoteOffVelocity(uint8(channelValue), uint8(noteValue), uint8(velocityValue))
wrappedPayload.Payload = payloadMessage
return wrappedPayload, nil
}}, nil }}, nil
} }
@@ -188,16 +185,9 @@ func newMidiControlChangeCreate(config config.ProcessorConfig) (Processor, error
params := config.Params params := config.Params
channel, ok := params["channel"] channelString, err := params.GetString("channel")
if err != nil {
if !ok { return nil, fmt.Errorf("midi.message.create channel error: %w", err)
return nil, errors.New("midi.message.create ControlChange requires a channel parameter")
}
channelString, ok := channel.(string)
if !ok {
return nil, errors.New("midi.message.create ControlChange channel must be a string")
} }
channelTemplate, err := template.New("channel").Parse(channelString) channelTemplate, err := template.New("channel").Parse(channelString)
@@ -206,34 +196,20 @@ func newMidiControlChangeCreate(config config.ProcessorConfig) (Processor, error
return nil, err return nil, err
} }
controller, ok := params["controller"] controlString, err := params.GetString("control")
if err != nil {
if !ok { return nil, fmt.Errorf("midi.message.create control error: %w", err)
return nil, errors.New("midi.message.create ControlChange requires a controller parameter")
} }
controllerString, ok := controller.(string) controlTemplate, err := template.New("control").Parse(controlString)
if !ok {
return nil, errors.New("midi.message.create ControlChange controller must be a string")
}
controllerTemplate, err := template.New("controller").Parse(controllerString)
if err != nil { if err != nil {
return nil, err return nil, err
} }
value, ok := params["value"] valueString, err := params.GetString("value")
if err != nil {
if !ok { return nil, fmt.Errorf("midi.message.create value error: %w", err)
return nil, errors.New("midi.message.create ControlChange requires a value parameter")
}
valueString, ok := value.(string)
if !ok {
return nil, errors.New("midi.message.create ControlChange value must be a string")
} }
valueTemplate, err := template.New("value").Parse(valueString) valueTemplate, err := template.New("value").Parse(valueString)
@@ -242,37 +218,43 @@ func newMidiControlChangeCreate(config config.ProcessorConfig) (Processor, error
return nil, err return nil, err
} }
return &MIDIMessageCreate{config: config, ProcessFunc: func(ctx context.Context, payload any) (any, error) { return &MIDIMessageCreate{config: config, ProcessFunc: func(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
templateData := wrappedPayload
var channelBuffer bytes.Buffer var channelBuffer bytes.Buffer
err := channelTemplate.Execute(&channelBuffer, payload) err := channelTemplate.Execute(&channelBuffer, templateData)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
channelValue, err := strconv.ParseUint(channelBuffer.String(), 10, 8) channelValue, err := strconv.ParseUint(channelBuffer.String(), 10, 8)
var controllerBuffer bytes.Buffer var controlBuffer bytes.Buffer
err = controllerTemplate.Execute(&controllerBuffer, payload) err = controlTemplate.Execute(&controlBuffer, templateData)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
controllerValue, err := strconv.ParseUint(controllerBuffer.String(), 10, 8) controlValue, err := strconv.ParseUint(controlBuffer.String(), 10, 8)
var valueBuffer bytes.Buffer var valueBuffer bytes.Buffer
err = valueTemplate.Execute(&valueBuffer, payload) err = valueTemplate.Execute(&valueBuffer, templateData)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
valueValue, err := strconv.ParseUint(valueBuffer.String(), 10, 8) valueValue, err := strconv.ParseUint(valueBuffer.String(), 10, 8)
payloadMessage := midi.ControlChange(uint8(channelValue), uint8(controllerValue), uint8(valueValue)) payloadMessage := midi.ControlChange(uint8(channelValue), uint8(controlValue), uint8(valueValue))
return payloadMessage, nil wrappedPayload.Payload = payloadMessage
return wrappedPayload, nil
}}, nil }}, nil
} }
@@ -280,16 +262,9 @@ func newMidiProgramChangeCreate(config config.ProcessorConfig) (Processor, error
params := config.Params params := config.Params
channel, ok := params["channel"] channelString, err := params.GetString("channel")
if err != nil {
if !ok { return nil, fmt.Errorf("midi.message.create channel error: %w", err)
return nil, errors.New("midi.message.create ProgramChange requires a channel parameter")
}
channelString, ok := channel.(string)
if !ok {
return nil, errors.New("midi.message.create ProgramChange channel must be a string")
} }
channelTemplate, err := template.New("channel").Parse(channelString) channelTemplate, err := template.New("channel").Parse(channelString)
@@ -298,16 +273,9 @@ func newMidiProgramChangeCreate(config config.ProcessorConfig) (Processor, error
return nil, err return nil, err
} }
program, ok := params["program"] programString, err := params.GetString("program")
if err != nil {
if !ok { return nil, fmt.Errorf("midi.message.create program error: %w", err)
return nil, errors.New("midi.message.create ProgramChange requires a program parameter")
}
programString, ok := program.(string)
if !ok {
return nil, errors.New("midi.message.create ProgramChange program must be a string")
} }
programTemplate, err := template.New("program").Parse(programString) programTemplate, err := template.New("program").Parse(programString)
@@ -316,47 +284,142 @@ func newMidiProgramChangeCreate(config config.ProcessorConfig) (Processor, error
return nil, err return nil, err
} }
return &MIDIMessageCreate{config: config, ProcessFunc: func(ctx context.Context, payload any) (any, error) { return &MIDIMessageCreate{config: config, ProcessFunc: func(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
templateData := wrappedPayload
var channelBuffer bytes.Buffer var channelBuffer bytes.Buffer
err := channelTemplate.Execute(&channelBuffer, payload) err := channelTemplate.Execute(&channelBuffer, templateData)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
channelValue, err := strconv.ParseUint(channelBuffer.String(), 10, 8) channelValue, err := strconv.ParseUint(channelBuffer.String(), 10, 8)
var programBuffer bytes.Buffer var programBuffer bytes.Buffer
err = programTemplate.Execute(&programBuffer, payload) err = programTemplate.Execute(&programBuffer, templateData)
if err != nil { if err != nil {
return nil, err wrappedPayload.End = true
return wrappedPayload, err
} }
programValue, err := strconv.ParseUint(programBuffer.String(), 10, 8) programValue, err := strconv.ParseUint(programBuffer.String(), 10, 8)
payloadMessage := midi.ProgramChange(uint8(channelValue), uint8(programValue)) payloadMessage := midi.ProgramChange(uint8(channelValue), uint8(programValue))
return payloadMessage, nil wrappedPayload.Payload = payloadMessage
return wrappedPayload, nil
}}, nil }}, nil
} }
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "midi.message.create", Type: "midi.message.create",
Title: "Create MIDI Message",
ParamsSchema: &jsonschema.Schema{
Type: "object",
OneOf: []*jsonschema.Schema{
{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"type": {
Title: "MIDI Message Type",
Type: "string",
Enum: []any{"NoteOn", "noteon", "note_on"},
},
"channel": {
Title: "Channel",
Type: "string",
},
"note": {
Title: "Note",
Type: "string",
},
"velocity": {
Title: "Velocity",
Type: "string",
},
},
Required: []string{"type", "channel", "note", "velocity"},
AdditionalProperties: nil,
},
{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"type": {
Title: "MIDI Message Type",
Type: "string",
Enum: []any{"NoteOff", "noteoff", "note_off"},
},
"channel": {
Title: "Channel",
Type: "string",
},
"note": {
Title: "Note",
Type: "string",
},
"velocity": {
Title: "Velocity",
Type: "string",
},
},
Required: []string{"type", "channel", "note", "velocity"},
AdditionalProperties: nil,
},
{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"type": {
Title: "MIDI Message Type",
Type: "string",
Enum: []any{"ControlChange", "controlchange", "control_change"},
},
"channel": {
Title: "Channel",
Type: "string",
},
"control": {
Title: "Control",
Type: "string",
},
"value": {
Title: "Value",
Type: "string",
},
},
Required: []string{"type", "channel", "control", "value"},
AdditionalProperties: nil,
},
{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"type": {
Title: "MIDI Message Type",
Type: "string",
Enum: []any{"ProgramChange", "programchange", "program_change"},
},
"channel": {
Title: "Channel",
Type: "string",
},
"program": {
Title: "Program",
Type: "string",
},
},
Required: []string{"type", "channel", "program"},
AdditionalProperties: nil,
},
},
},
New: func(config config.ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params params := config.Params
msgType, ok := params["type"] msgTypeString, err := params.GetString("type")
if err != nil {
if !ok { return nil, fmt.Errorf("midi.message.create type error: %w", err)
return nil, errors.New("midi.message.create requires a type parameter")
}
msgTypeString, ok := msgType.(string)
if !ok {
return nil, errors.New("midi.message.create type parameter must be a string")
} }
switch msgTypeString { switch msgTypeString {

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"
) )
@@ -14,16 +15,19 @@ type MIDIMessageDecode struct {
config config.ProcessorConfig config config.ProcessorConfig
} }
func (mmd *MIDIMessageDecode) Process(ctx context.Context, payload any) (any, error) { func (mmd *MIDIMessageDecode) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
payloadBytes, ok := payload.([]byte) payload := wrappedPayload.Payload
payloadBytes, ok := common.GetAnyAsByteSlice(payload)
if !ok { if !ok {
return nil, errors.New("midi.message.decode processor only accepts a []byte") wrappedPayload.End = true
return wrappedPayload, errors.New("midi.message.decode processor only accepts a []byte")
} }
payloadMessage := midi.Message(payloadBytes) payloadMessage := midi.Message(payloadBytes)
return payloadMessage, nil wrappedPayload.Payload = payloadMessage
return wrappedPayload, nil
} }
func (mmd *MIDIMessageDecode) Type() string { func (mmd *MIDIMessageDecode) Type() string {
@@ -32,7 +36,8 @@ func (mmd *MIDIMessageDecode) Type() string {
func init() { func init() {
RegisterProcessor(ProcessorRegistration{ RegisterProcessor(ProcessorRegistration{
Type: "midi.message.decode", Type: "midi.message.decode",
Title: "Decode MIDI Message",
New: func(config config.ProcessorConfig) (Processor, error) { New: func(config config.ProcessorConfig) (Processor, error) {
return &MIDIMessageDecode{config: config}, nil return &MIDIMessageDecode{config: config}, nil
}, },

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