Compare commits

..

1 Commits

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

View File

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

27
.github/labeler.yml vendored Normal file
View File

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

2
.github/release.yml vendored
View File

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

18
.github/workflows/label-pr.yaml vendored Normal file
View File

@@ -0,0 +1,18 @@
# Taken from https://github.com/go-gitea/gitea
name: Add labels to PR
on:
pull_request_target:
types: [opened, synchronize, reopened]
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true
jobs:
labeler:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
with:
sync-labels: true

View File

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

View File

@@ -5,16 +5,16 @@ on:
branches:
- main
paths:
- "**/*.go"
- "go.mod"
- "go.sum"
- '**/*.go'
- 'go.mod'
- 'go.sum'
push:
branches:
- main
paths:
- "**/*.go"
- "go.mod"
- "go.sum"
- '**/*.go'
- 'go.mod'
- 'go.sum'
permissions:
contents: write
@@ -29,7 +29,7 @@ jobs:
- uses: actions/checkout@v6
- uses: actions/setup-go@v6
with:
go-version-file: "go.mod"
go-version-file: 'go.mod'
- name: mod tidy
run: go mod tidy
@@ -37,7 +37,7 @@ jobs:
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
run: cat coverage.out | grep -v "github.com/jwetzell/showbridge-go/cmd" > coverage.nocmd.out
- name: Update coverage report
uses: ncruces/go-coverage-report@v0.3.2
with:
@@ -45,5 +45,5 @@ jobs:
report: true
chart: true
amend: true
if: github.event_name == 'push'
if: github.event_name == 'push'
continue-on-error: true

33
.vscode/launch.json vendored
View File

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

View File

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

View File

@@ -3,6 +3,7 @@
# showbridge (go edition)
[![Coverage](https://github.com/jwetzell/showbridge-go/wiki/coverage.svg)](https://raw.githack.com/wiki/jwetzell/showbridge-go/coverage.html)
Simple protocol router _/s_
</div>
@@ -13,7 +14,6 @@ Simple protocol router _/s_
</p>
### Supported Protocols
- HTTP
- UDP
- TCP
@@ -25,8 +25,7 @@ Simple protocol router _/s_
- [OSC](https://opensoundcontrol.stanford.edu/spec-1_0.html)
- [FreeD](https://ptzoptics.com/freed/)
- [SIP](https://en.wikipedia.org/wiki/Session_Initiation_Protocol)
- [Redis](https://redis.io/)
### CLI Usage
```
@@ -37,10 +36,9 @@ USAGE:
showbridge [global options]
GLOBAL OPTIONS:
--config string path to config file (default: "./config.yaml") [$SHOWBRIDGE_CONFIG]
--log-level string set log level (default: "info") [$SHOWBRIDGE_LOG_LEVEL]
--log-format string log format to use (default: "text") [$SHOWBRIDGE_LOG_FORMAT]
--trace enable OpenTelemetry tracing [$SHOWBRIDGE_TRACE]
--config string path to config file (default: "./config.yaml")
--log-level string set log level (default: "info")
--log-format string log format to use (default: "text")
--help, -h show help
--version, -v print the version
```

255
api.go
View File

@@ -1,255 +0,0 @@
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

@@ -1,5 +0,0 @@
package main
import "syscall/js"
var document = js.Global().Get("document")

View File

@@ -1,71 +0,0 @@
<html>
<head>
<meta charset="utf-8" />
<script src="wasm_exec.js"></script>
<style>
body {
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
height: 100vh;
margin: 0;
padding: 0;
}
#output-container {
flex-grow: 1;
}
#log-container {
background-color: #1e1e1e;
width: 100%;
height: 200px;
overflow-y: auto;
overflow-x: scroll;
box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.5);
}
#logs {
margin: 0;
padding-left: 2px;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
font-size: 13px;
line-height: 1.4;
color: #a8cc8c;
}
</style>
<script>
const container = document.getElementById("log-container");
function isScrolledToBottom() {
const scrollThreshold = 1;
return (
container.scrollHeight - container.clientHeight <=
container.scrollTop + scrollThreshold
);
}
function scrollToBottomManual() {
const container = document.getElementById("log-container");
// Set scrollTop to the maximum value possible (scrollHeight - clientHeight)
container.scrollTop = container.scrollHeight - container.clientHeight;
}
</script>
</head>
<body>
<div id="output-container">
<div id="output"></div>
</div>
<div id="log-container">
<pre id="logs"></pre>
</div>
<script>
const go = new Go();
WebAssembly.instantiateStreaming(
fetch("main.wasm"),
go.importObject,
).then((result) => {
go.run(result.instance);
});
</script>
</body>
</html>

View File

@@ -1,42 +0,0 @@
package main
import "syscall/js"
type LogWriter struct {
Element js.Value
Container js.Value
}
func (pw *LogWriter) ScrollToBottom() {
scrollHeight := pw.Container.Get("scrollHeight").Int()
clientHeight := pw.Container.Get("clientHeight").Int()
pw.Container.Set("scrollTop", scrollHeight-clientHeight)
}
func (pw *LogWriter) IsScrolledToBottom() bool {
scrollHeight := pw.Container.Get("scrollHeight").Int()
clientHeight := pw.Container.Get("clientHeight").Int()
scrollTop := pw.Container.Get("scrollTop").Int()
return scrollHeight-clientHeight <= scrollTop+25
}
func (pw *LogWriter) Write(p []byte) (n int, err error) {
if !pw.Element.IsUndefined() {
currentText := pw.Element.Get("textContent").String()
newText := currentText + string(p)
pw.Element.Set("textContent", newText)
if pw.IsScrolledToBottom() {
pw.ScrollToBottom()
}
}
return len(p), nil
}
func NewLogWriter(id string) *LogWriter {
element := document.Call("getElementById", id)
container := element.Get("parentElement")
return &LogWriter{
Element: element,
Container: container,
}
}

View File

@@ -1,71 +0,0 @@
package main
import (
"context"
"fmt"
"log/slog"
"github.com/jwetzell/showbridge-go"
"github.com/jwetzell/showbridge-go/internal/config"
)
func main() {
slog.SetLogLoggerLevel(slog.LevelDebug)
slog.SetDefault(slog.New(slog.NewTextHandler(NewLogWriter("logs"), &slog.HandlerOptions{
Level: slog.LevelDebug,
})))
router, moduleConfigErrors, routeConfigErrors := showbridge.NewRouter(config.Config{
Api: config.ApiConfig{
Enabled: false,
Port: 0,
},
Modules: []config.ModuleConfig{
{
Id: "midi",
Type: "midi.input",
Params: map[string]any{
"port": "Launchpad S",
},
},
},
Routes: []config.RouteConfig{
{
Input: "midi",
Processors: []config.ProcessorConfig{
{
Type: "debug.log",
},
{
Type: "web.set",
Params: map[string]any{
"id": "output",
"property": "textContent",
"value": "{{.Payload}}",
},
},
},
},
},
})
if len(moduleConfigErrors) > 0 {
for _, err := range moduleConfigErrors {
println("Module config error:", err.Error)
}
}
if len(routeConfigErrors) > 0 {
for _, err := range routeConfigErrors {
println("Route config error:", err.Error)
}
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
router.Start(ctx)
fmt.Println("router stopped")
}()
<-ctx.Done()
}

View File

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

View File

@@ -2,7 +2,6 @@ package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
@@ -14,15 +13,12 @@ import (
"github.com/jwetzell/showbridge-go"
"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"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.40.0"
semconv "go.opentelemetry.io/otel/semconv/v1.39.0"
"sigs.k8s.io/yaml"
)
@@ -38,10 +34,9 @@ func main() {
Version: version,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "config",
Value: "./config.yaml",
Usage: "path to config file",
Sources: cli.EnvVars("SHOWBRIDGE_CONFIG"),
Name: "config",
Value: "./config.yaml",
Usage: "path to config file",
},
&cli.StringFlag{
Name: "log-level",
@@ -54,7 +49,6 @@ func main() {
}
return nil
},
Sources: cli.EnvVars("SHOWBRIDGE_LOG_LEVEL"),
},
&cli.StringFlag{
Name: "log-format",
@@ -67,13 +61,11 @@ func main() {
}
return nil
},
Sources: cli.EnvVars("SHOWBRIDGE_LOG_FORMAT"),
},
&cli.BoolFlag{
Name: "trace",
Value: false,
Usage: "enable OpenTelemetry tracing",
Sources: cli.EnvVars("SHOWBRIDGE_TRACE"),
Name: "trace",
Value: false,
Usage: "enable OpenTelemetry tracing",
},
},
Action: run,
@@ -109,28 +101,7 @@ func readConfig(configPath string) (config.Config, error) {
return config.Config{}, err
}
//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)
err = yaml.Unmarshal(configBytes, &cfg)
if err != nil {
return config.Config{}, err
}
@@ -138,20 +109,6 @@ func readConfig(configPath string) (config.Config, error) {
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 {
configPath := c.String("config")
if configPath == "" {
@@ -216,19 +173,7 @@ func run(ctx context.Context, c *cli.Command) error {
routerRunner: &sync.WaitGroup{},
}
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")
}
router, err := showbridgeApp.getNewRouter()
if err != nil {
return fmt.Errorf("failed to initialize router: %w", err)
}
@@ -240,7 +185,7 @@ func run(ctx context.Context, c *cli.Command) error {
})
showbridgeApp.routerMutex.Unlock()
go showbridgeApp.handleChannels()
go showbridgeApp.handleHangup()
<-showbridgeApp.ctx.Done()
showbridgeApp.logger.Debug("shutting down router")
@@ -250,37 +195,40 @@ func run(ctx context.Context, c *cli.Command) error {
return nil
}
func (app *showbridgeApp) handleChannels() {
func (app *showbridgeApp) handleHangup() {
for {
select {
case <-sigHangup:
app.logger.Info("received SIGHUP, reloading configuration")
app.routerMutex.Lock()
config, err := readConfig(app.configPath)
newRouter, err := app.getNewRouter()
if err != nil {
app.logger.Error("failed to read config file", "error", err)
app.routerMutex.Unlock()
app.logger.Error("failed to reload configuration", "error", err)
continue
}
moduleErrors, routeErrors := app.router.UpdateConfig(config)
app.logConfigErrors(moduleErrors, routeErrors)
app.routerMutex.Lock()
app.router.Stop()
app.routerRunner.Wait()
app.router = newRouter
app.routerRunner.Go(func() {
app.router.Start(context.Background())
})
app.logger.Info("configuration reloaded successfully")
app.routerMutex.Unlock()
case 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():
return
}
}
}
func (app *showbridgeApp) logConfigErrors(moduleErrors []module.ModuleError, routeErrors []route.RouteError) {
func (app *showbridgeApp) getNewRouter() (*showbridge.Router, error) {
// TODO(jwetzell): what should happen when the config file is unchanged?
config, err := readConfig(app.configPath)
if err != nil {
return nil, err
}
router, moduleErrors, routeErrors := showbridge.NewRouter(config)
for _, moduleError := range moduleErrors {
app.logger.Error("problem initializing module", "index", moduleError.Index, "error", moduleError.Error)
}
@@ -288,6 +236,12 @@ func (app *showbridgeApp) logConfigErrors(moduleErrors []module.ModuleError, rou
for _, routeError := range routeErrors {
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 {

View File

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

View File

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

52
go.mod
View File

@@ -1,40 +1,35 @@
module github.com/jwetzell/showbridge-go
go 1.26.0
go 1.25.5
require (
github.com/eclipse/paho.mqtt.golang v1.5.1
github.com/emiago/diago v0.28.0
github.com/emiago/sipgo v1.2.1
github.com/emiago/diago v0.27.0
github.com/emiago/sipgo v1.2.0
github.com/expr-lang/expr v1.17.8
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/free-d-go v0.1.0
github.com/jwetzell/osc-go v0.2.0
github.com/jwetzell/psn-go v0.3.0
github.com/nats-io/nats-server/v2 v2.12.6
github.com/nats-io/nats-server/v2 v2.12.4
github.com/nats-io/nats.go v1.49.0
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.opentelemetry.io/otel v1.42.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0
go.opentelemetry.io/otel/sdk v1.42.0
go.opentelemetry.io/otel/trace v1.42.0
go.opentelemetry.io/otel v1.41.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0
go.opentelemetry.io/otel/sdk v1.41.0
go.opentelemetry.io/otel/trace v1.41.0
modernc.org/quickjs v0.17.1
modernc.org/sqlite v1.47.0
sigs.k8s.io/yaml v1.6.0
)
require (
github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op // indirect
github.com/antithesishq/antithesis-sdk-go v0.5.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/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/dylibso/observe-sdk/go v0.0.0-20240819160327-2d926c5d788a // indirect
github.com/emiago/dtls/v3 v3.0.0-20260122183559-8b8d23e359c0 // indirect
@@ -47,14 +42,15 @@ require (
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/gorilla/websocket v1.5.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
github.com/ianlancetaylor/demangle v0.0.0-20240805132620-81f5be970eca // indirect
github.com/icholy/digest v1.1.0 // indirect
github.com/klauspost/compress v1.18.4 // indirect
github.com/klauspost/compress v1.18.3 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 // indirect
github.com/nats-io/jwt/v2 v2.8.1 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/jwt/v2 v2.8.0 // indirect
github.com/nats-io/nkeys v0.4.12 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/pion/logging v0.2.4 // indirect
@@ -69,23 +65,23 @@ require (
github.com/tetratelabs/wazero v1.9.0 // indirect
github.com/zaf/g711 v1.4.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect
go.opentelemetry.io/otel/metric v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0 // indirect
go.opentelemetry.io/otel/metric v1.41.0 // 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
golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/time v0.15.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.50.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/time v0.14.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect
google.golang.org/grpc v1.79.2 // indirect
google.golang.org/grpc v1.79.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/hraban/opus.v2 v2.0.0-20230925203106-0188a62cb302 // indirect
modernc.org/libc v1.70.0 // indirect
modernc.org/libc v1.67.1 // indirect
modernc.org/libquickjs v0.12.3 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect

130
go.sum
View File

@@ -1,9 +1,5 @@
github.com/antithesishq/antithesis-sdk-go v0.6.0-default-no-op h1:kpBdlEPbRvff0mDD1gk7o9BhI16b9p5yYAXRlidpqJE=
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/antithesishq/antithesis-sdk-go v0.5.0-default-no-op h1:Ucf+QxEKMbPogRO5guBNe5cgd9uZgfoJLOYs8WWhtjM=
github.com/antithesishq/antithesis-sdk-go v0.5.0-default-no-op/go.mod h1:IUpT2DPAKh6i/YhSbt6Gl3v2yvUZjmKncl7U91fup7E=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
@@ -12,20 +8,18 @@ github.com/creack/goselect v0.1.2 h1:2DNy14+JPjRBgPzAd1thbQp4BSIihxcBf0IXhQXDRa0
github.com/creack/goselect v0.1.2/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
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/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/go.mod h1:C8DzXehI4zAbrdlbtOByKX6pfivJTBiV9Jjqv56Yd9Q=
github.com/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=
github.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=
github.com/emiago/diago v0.28.0 h1:VCiimhFYLoBTsxH6WrufLSt6tKWG8Fv7LfCkZHqct2E=
github.com/emiago/diago v0.28.0/go.mod h1:eQT6j9co9PMQ/25aUiM2jpvwxxWFXLWi2w5R3lZNmKg=
github.com/emiago/diago v0.27.0 h1:5SQBbcLR9ooxhSMlTkU9QSuDv/2nhMO9lxuWJTd/7rE=
github.com/emiago/diago v0.27.0/go.mod h1:8hUxCDPJY2p32hh+4ed7vHW/3yTMmEa3BjNctUPeGD0=
github.com/emiago/dtls/v3 v3.0.0-20260122183559-8b8d23e359c0 h1:o4LxpUnZ1zxiQ+Qjc9kLwXcjz31NGAHmnZ7xoJto3VM=
github.com/emiago/dtls/v3 v3.0.0-20260122183559-8b8d23e359c0/go.mod h1:ydcZ977eS1I6uOWodzMuw30BwvNAzT9su/xcNYSJqjA=
github.com/emiago/sipgo v1.2.1 h1:5JTwogbe3yQFA3sjBVueN2Q4WTU350tGeBwPYT8HMv0=
github.com/emiago/sipgo v1.2.1/go.mod h1:DuwAxBZhKMqIzQFPGZb1MVAGU6Wuxj64oTOhd5dx/FY=
github.com/emiago/sipgo v1.2.0 h1:rmHFdCu9zu2Cabfd8+/eC9HQWyooqk8x+ti550z5lBw=
github.com/emiago/sipgo v1.2.0/go.mod h1:DuwAxBZhKMqIzQFPGZb1MVAGU6Wuxj64oTOhd5dx/FY=
github.com/expr-lang/expr v1.17.8 h1:W1loDTT+0PQf5YteHSTpju2qfUfNoBt4yw9+wOEU9VM=
github.com/expr-lang/expr v1.17.8/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/extism/go-sdk v1.7.1 h1:lWJos6uY+tRFdlIHR+SJjwFDApY7OypS/2nMhiVQ9Sw=
@@ -51,10 +45,6 @@ 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-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/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
@@ -75,10 +65,8 @@ github.com/jwetzell/osc-go v0.2.0 h1:4as+BYCeZhEddFczGveP5yZZxvY728Uavz+ZSLZfOII
github.com/jwetzell/osc-go v0.2.0/go.mod h1:D3ZIXYB12bt4S35lKFUqgCFbF1Y+9Ld0sOhHA9mGZZM=
github.com/jwetzell/psn-go v0.3.0 h1:WVpCEmExYE8a+I5hQak5jNJJp2x35VdGX/VuMUKPmhY=
github.com/jwetzell/psn-go v0.3.0/go.mod h1:bcEAeti4sQM375buujb3mIfmUstD4Aby18gq3ENb6+o=
github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
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/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -87,14 +75,14 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76 h1:KGuD/pM2JpL9FAYvBrnBBeENKZNh6eNtjqytV6TYjnk=
github.com/minio/highwayhash v1.0.4-0.20251030100505-070ab1a87a76/go.mod h1:GGYsuwP/fPD6Y9hMiXuapVvlIUEhFhMTh0rxU3ik1LQ=
github.com/nats-io/jwt/v2 v2.8.1 h1:V0xpGuD/N8Mi+fQNDynXohVvp7ZztevW5io8CUWlPmU=
github.com/nats-io/jwt/v2 v2.8.1/go.mod h1:nWnOEEiVMiKHQpnAy4eXlizVEtSfzacZ1Q43LIRavZg=
github.com/nats-io/nats-server/v2 v2.12.6 h1:Egbx9Vl7Ch8wTtpXPGqbehkZ+IncKqShUxvrt1+Enc8=
github.com/nats-io/nats-server/v2 v2.12.6/go.mod h1:4HPlrvtmSO3yd7KcElDNMx9kv5EBJBnJJzQPptXlheo=
github.com/nats-io/jwt/v2 v2.8.0 h1:K7uzyz50+yGZDO5o772eRE7atlcSEENpL7P+b74JV1g=
github.com/nats-io/jwt/v2 v2.8.0/go.mod h1:me11pOkwObtcBNR8AiMrUbtVOUGkqYjMQZ6jnSdVUIA=
github.com/nats-io/nats-server/v2 v2.12.4 h1:ZnT10v2LU2Xcoiy8ek9X6Se4YG8EuMfIfvAEuFVx1Ts=
github.com/nats-io/nats-server/v2 v2.12.4/go.mod h1:5MCp/pqm5SEfsvVZ31ll1088ZTwEUdvRX1Hmh/mTTDg=
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/nkeys v0.4.12 h1:nssm7JKOG9/x4J8II47VWCL1Ds29avyiQDRn0ckMvDc=
github.com/nats-io/nkeys v0.4.12/go.mod h1:MT59A1HYcjIcyQDJStTfaOY6vhy9XTUjOFo+SVsvpBg=
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/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
@@ -115,8 +103,6 @@ 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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
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/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
@@ -131,64 +117,62 @@ github.com/urfave/cli/v3 v3.7.0 h1:AGSnbUyjtLiM+WJUb4dzXKldl/gL+F8OwmRDtVr6g2U=
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/go.mod h1:eCDXt3dSp/kYYAoooba7ukD/Q75jvAaS4WOMr0l1Roo=
github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0=
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/go.mod h1:nofMJxTeNVny/m6+KaafC6vJGj3miwQZ6vW4BZUGJPI=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 h1:THuZiwpQZuHPul65w4WcwEnkX2QIuMT+UFoOrygtoJw=
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.42.0 h1:uLXP+3mghfMf7XmV4PkGfFhFKuNWoCvvx5wP/wOXo0o=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0/go.mod h1:v0Tj04armyT59mnURNUJf7RCKcKzq+lgJs6QSjHjaTc=
go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo=
go.opentelemetry.io/otel/sdk v1.42.0/go.mod h1:rGHCAxd9DAph0joO4W6OPwxjNTYWghRWmkHuGbayMts=
go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9RKCAZ3YGuA=
go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc=
go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0 h1:ao6Oe+wSebTlQ1OEht7jlYTzQKE+pnx/iNywFvTbuuI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.41.0/go.mod h1:u3T6vz0gh/NVzgDgiwkgLxpsSF6PaPmo2il0apGJbls=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0 h1:inYW9ZhgqiDqh6BioM7DVHHzEGVq76Db5897WLGZ5Go=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.41.0/go.mod h1:Izur+Wt8gClgMJqO/cZ8wdeeMryJ/xxiOVgFSSfpDTY=
go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
go.opentelemetry.io/otel/sdk v1.41.0 h1:YPIEXKmiAwkGl3Gu1huk1aYWwtpRLeskpV+wPisxBp8=
go.opentelemetry.io/otel/sdk v1.41.0/go.mod h1:ahFdU0G5y8IxglBf0QBJXgSe7agzjE4GiTJ6HT9ud90=
go.opentelemetry.io/otel/sdk/metric v1.41.0 h1:siZQIYBAUd1rlIWQT2uCxWJxcCO7q3TriaMlf08rXw8=
go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y=
go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.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/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
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/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1:JLQynH/LBHfCTSbDWl+py8C+Rg/k1OVH3xfcaiANuF0=
google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/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/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/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=
@@ -202,18 +186,18 @@ gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
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/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.32.0 h1:hjG66bI/kqIPX1b2yT6fr/jt+QedtP2fqojG2VrFuVw=
modernc.org/ccgo/v4 v4.32.0/go.mod h1:6F08EBCx5uQc38kMGl+0Nm0oWczoo1c7cgpzEry7Uc0=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/ccgo/v4 v4.30.1 h1:4r4U1J6Fhj98NKfSjnPUN7Ze2c6MnAdL0hWw6+LrJpc=
modernc.org/ccgo/v4 v4.30.1/go.mod h1:bIOeI1JL54Utlxn+LwrFyjCx2n2RDiYEaJVSrgdrRfM=
modernc.org/fileutil v1.3.40 h1:ZGMswMNc9JOCrcrakF1HrvmergNLAmxOPjizirpfqBA=
modernc.org/fileutil v1.3.40/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/gc/v3 v3.1.1 h1:k8T3gkXWY9sEiytKhcgyiZ2L0DTyCQ/nvX+LoCljoRE=
modernc.org/gc/v3 v3.1.1/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.70.0 h1:U58NawXqXbgpZ/dcdS9kMshu08aiA6b7gusEusqzNkw=
modernc.org/libc v1.70.0/go.mod h1:OVmxFGP1CI/Z4L3E0Q3Mf1PDE0BucwMkcXjjLntvHJo=
modernc.org/libc v1.67.1 h1:bFaqOaa5/zbWYJo8aW0tXPX21hXsngG2M7mckCnFSVk=
modernc.org/libc v1.67.1/go.mod h1:QvvnnJ5P7aitu0ReNpVIEyesuhmDLQ8kaEoyMjIFZJA=
modernc.org/libquickjs v0.12.3 h1:2IU9B6njBmce2PuYttJDkXeoLRV9WnvgP+eU5HAC8YI=
modernc.org/libquickjs v0.12.3/go.mod h1:iCsgVxnHTX3i0YPxxHBmJk0GLA5sVUHXWI/090UXgeE=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
@@ -226,8 +210,6 @@ modernc.org/quickjs v0.17.1 h1:CbYnbTf7ksZk9YZ1rRM2Ab1Zfi+X6s50kXiOhpd2NIg=
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/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/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

View File

@@ -1,175 +1,7 @@
package common
import (
"math"
"reflect"
)
type contextKey string
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
}
const RouterContextKey contextKey = contextKey("router")
const SourceContextKey contextKey = contextKey("source")
const ModulesContextKey contextKey = contextKey("modules")

View File

@@ -1,424 +0,0 @@
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

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

View File

@@ -1,26 +0,0 @@
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

@@ -1,40 +0,0 @@
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

@@ -1,82 +0,0 @@
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

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

View File

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

View File

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

View File

@@ -0,0 +1,297 @@
package config_test
import (
"encoding/json"
"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 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 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 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 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 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 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 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)
}
})
}
}

View File

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

View File

@@ -1,140 +0,0 @@
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

@@ -1,628 +0,0 @@
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

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

View File

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

View File

@@ -1,91 +0,0 @@
//go:build !js
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,7 +0,0 @@
//go:build cgo
package module
import (
_ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
)

View File

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

View File

@@ -1,5 +1,3 @@
//go:build !js
package module
import (
@@ -8,20 +6,19 @@ import (
"errors"
"fmt"
"log/slog"
"net"
"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/processor"
"github.com/jwetzell/showbridge-go/internal/route"
)
type HTTPServer struct {
config config.ModuleConfig
Port uint16
ctx context.Context
router common.RouteIO
router route.RouteIO
logger *slog.Logger
cancel context.CancelFunc
}
@@ -58,22 +55,8 @@ func (hsrw *HTTPServerResponseWriter) Write(data []byte) (int, error) {
func init() {
RegisterModule(ModuleRegistration{
Type: "http.server",
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) {
Type: "http.server",
New: func(config config.ModuleConfig) (Module, error) {
params := config.Params
portNum, err := params.GetInt("port")
if err != nil {
@@ -101,10 +84,6 @@ func (hs *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
if hs.router != nil {
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)
if !responseWriter.done {
if aRouteFound {
@@ -164,7 +143,7 @@ func (hs *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func (hs *HTTPServer) Start(ctx context.Context) error {
hs.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok {
return errors.New("http.server unable to get router from context")
@@ -204,7 +183,7 @@ func (hs *HTTPServer) Output(ctx context.Context, payload any) error {
return errors.New("http.server output must originate from an http.server input")
}
payloadResponse, ok := common.GetAnyAs[processor.HTTPResponse](payload)
payloadResponse, ok := processor.GetAnyAs[processor.HTTPResponse](payload)
if !ok {
return errors.New("http.server is only able to output HTTPResponse")
@@ -222,3 +201,7 @@ func (hs *HTTPServer) Output(ctx context.Context, payload any) error {
func (hs *HTTPServer) Stop() {
hs.cancel()
}
func (hs *HTTPServer) Get(key string) (any, error) {
return nil, errors.New("http.server does not support Get")
}

View File

@@ -1,4 +1,4 @@
//go:build cgo || js
//go:build cgo
package module
@@ -8,16 +8,17 @@ import (
"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/jwetzell/showbridge-go/internal/route"
"gitlab.com/gomidi/midi/v2"
_ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
)
type MIDIInput struct {
config config.ModuleConfig
ctx context.Context
router common.RouteIO
router route.RouteIO
Port string
SendFunc func(midi.Message) error
logger *slog.Logger
@@ -26,20 +27,8 @@ type MIDIInput struct {
func init() {
RegisterModule(ModuleRegistration{
Type: "midi.input",
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) {
Type: "midi.input",
New: func(config config.ModuleConfig) (Module, error) {
params := config.Params
portString, err := params.GetString("port")
if err != nil {
@@ -62,7 +51,7 @@ func (mi *MIDIInput) Type() string {
func (mi *MIDIInput) Start(ctx context.Context) error {
mi.logger.Debug("running")
defer midi.CloseDriver()
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok {
return errors.New("midi.input unable to get router from context")
@@ -94,6 +83,19 @@ func (mi *MIDIInput) Start(ctx context.Context) error {
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() {
mi.cancel()
}
func (mi *MIDIInput) Get(key string) (any, error) {
switch key {
case "port":
return mi.Port, nil
default:
return nil, errors.New("midi.input key not found")
}
}

View File

@@ -1,4 +1,4 @@
//go:build cgo || js
//go:build cgo
package module
@@ -8,16 +8,18 @@ import (
"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/jwetzell/showbridge-go/internal/processor"
"github.com/jwetzell/showbridge-go/internal/route"
"gitlab.com/gomidi/midi/v2"
_ "gitlab.com/gomidi/midi/v2/drivers/rtmididrv"
)
type MIDIOutput struct {
config config.ModuleConfig
ctx context.Context
router common.RouteIO
router route.RouteIO
Port string
SendFunc func(midi.Message) error
logger *slog.Logger
@@ -26,20 +28,8 @@ type MIDIOutput struct {
func init() {
RegisterModule(ModuleRegistration{
Type: "midi.output",
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) {
Type: "midi.output",
New: func(config config.ModuleConfig) (Module, error) {
params := config.Params
portString, err := params.GetString("port")
@@ -63,7 +53,7 @@ func (mo *MIDIOutput) Type() string {
func (mo *MIDIOutput) Start(ctx context.Context) error {
mo.logger.Debug("running")
defer midi.CloseDriver()
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok {
return errors.New("midi.output unable to get router from context")
@@ -96,10 +86,10 @@ func (mo *MIDIOutput) Output(ctx context.Context, payload any) error {
return errors.New("midi.output output is not setup")
}
payloadMessage, ok := common.GetAnyAs[midi.Message](payload)
payloadMessage, ok := processor.GetAnyAs[midi.Message](payload)
if !ok {
return errors.New("midi.output can only output midi.Message")
return errors.New("midi.output can only ouptut midi.Message")
}
return mo.SendFunc(payloadMessage)
@@ -108,3 +98,12 @@ func (mo *MIDIOutput) Output(ctx context.Context, payload any) error {
func (mo *MIDIOutput) Stop() {
mo.cancel()
}
func (mo *MIDIOutput) Get(key string) (any, error) {
switch key {
case "port":
return mo.Port, nil
default:
return nil, errors.New("midi.output key not found")
}
}

View File

@@ -1,27 +1,32 @@
package module
import (
"context"
"fmt"
"log/slog"
"sync"
"github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
type ModuleError struct {
Index int `json:"index"`
Config config.ModuleConfig `json:"config"`
Error string `json:"error"`
Index int
Config config.ModuleConfig
Error error
}
type Module interface {
Id() string
Type() string
Start(context.Context) error
Stop()
Output(context.Context, any) error
Get(key string) (any, error)
}
type ModuleRegistration struct {
Type string `json:"type"`
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
ParamsSchema *jsonschema.Schema `json:"paramsSchema,omitempty"`
New func(config.ModuleConfig) (common.Module, error)
Type string `json:"type"`
New func(config.ModuleConfig) (Module, error)
}
func RegisterModule(mod ModuleRegistration) {

View File

@@ -1,5 +1,3 @@
//go:build !js
package module
import (
@@ -9,15 +7,16 @@ import (
"log/slog"
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/processor"
"github.com/jwetzell/showbridge-go/internal/route"
)
type MQTTClient struct {
config config.ModuleConfig
ctx context.Context
router common.RouteIO
router route.RouteIO
Broker string
ClientID string
Topic string
@@ -28,28 +27,8 @@ type MQTTClient struct {
func init() {
RegisterModule(ModuleRegistration{
Type: "mqtt.client",
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) {
Type: "mqtt.client",
New: func(config config.ModuleConfig) (Module, error) {
params := config.Params
brokerString, err := params.GetString("broker")
@@ -84,7 +63,7 @@ func (mc *MQTTClient) Type() string {
func (mc *MQTTClient) Start(ctx context.Context) error {
mc.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok {
return errors.New("mqtt.client unable to get router from context")
@@ -124,7 +103,7 @@ func (mc *MQTTClient) Start(ctx context.Context) error {
}
func (mc *MQTTClient) Output(ctx context.Context, payload any) error {
payloadMessage, ok := common.GetAnyAs[mqtt.Message](payload)
payloadMessage, ok := processor.GetAnyAs[mqtt.Message](payload)
if !ok {
return errors.New("mqtt.client is only able to output a MQTTMessage")
@@ -148,3 +127,7 @@ func (mc *MQTTClient) Output(ctx context.Context, payload any) error {
func (mc *MQTTClient) Stop() {
mc.cancel()
}
func (mc *MQTTClient) Get(key string) (any, error) {
return nil, errors.New("mqtt.client does not support Get")
}

View File

@@ -1,5 +1,3 @@
//go:build !js
package module
import (
@@ -7,17 +5,17 @@ import (
"errors"
"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/processor"
"github.com/jwetzell/showbridge-go/internal/route"
"github.com/nats-io/nats.go"
)
type NATSClient struct {
config config.ModuleConfig
ctx context.Context
router common.RouteIO
router route.RouteIO
URL string
Subject string
client *nats.Conn
@@ -27,24 +25,8 @@ type NATSClient struct {
func init() {
RegisterModule(ModuleRegistration{
Type: "nats.client",
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) {
Type: "nats.client",
New: func(config config.ModuleConfig) (Module, error) {
params := config.Params
urlString, err := params.GetString("url")
if err != nil {
@@ -72,7 +54,7 @@ func (nc *NATSClient) Type() string {
func (nc *NATSClient) Start(ctx context.Context) error {
nc.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok {
return errors.New("nats.client unable to get router from context")
@@ -113,7 +95,7 @@ func (nc *NATSClient) Start(ctx context.Context) error {
func (nc *NATSClient) Output(ctx context.Context, payload any) error {
payloadMessage, ok := common.GetAnyAs[processor.NATSMessage](payload)
payloadMessage, ok := processor.GetAnyAs[processor.NATSMessage](payload)
if !ok {
return errors.New("nats.client is only able to output NATSMessage")
@@ -135,3 +117,7 @@ func (nc *NATSClient) Output(ctx context.Context, payload any) error {
func (nc *NATSClient) Stop() {
nc.cancel()
}
func (nc *NATSClient) Get(key string) (any, error) {
return nil, errors.New("nats.client does not support Get")
}

View File

@@ -1,19 +1,16 @@
//go:build !js
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/jwetzell/showbridge-go/internal/route"
"github.com/nats-io/nats-server/v2/server"
)
@@ -22,7 +19,7 @@ type NATSServer struct {
ctx context.Context
Ip string
Port int
router common.RouteIO
router route.RouteIO
server *server.Server
logger *slog.Logger
cancel context.CancelFunc
@@ -30,29 +27,8 @@ type NATSServer struct {
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) {
Type: "nats.server",
New: func(moduleConfig config.ModuleConfig) (Module, error) {
params := moduleConfig.Params
portNum, err := params.GetInt("port")
if err != nil {
@@ -91,7 +67,7 @@ func (ns *NATSServer) Type() string {
func (ns *NATSServer) Start(ctx context.Context) error {
ns.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok {
return errors.New("nats.server unable to get router from context")
@@ -127,9 +103,17 @@ func (ns *NATSServer) Start(ctx context.Context) error {
return nil
}
func (ns *NATSServer) Output(ctx context.Context, payload any) error {
return errors.ErrUnsupported
}
func (ns *NATSServer) Stop() {
ns.cancel()
if ns.server != nil {
ns.server.Shutdown()
}
}
func (ns *NATSServer) Get(key string) (any, error) {
return nil, errors.New("nats.server does not support Get")
}

View File

@@ -1,10 +1,9 @@
//go:build !js
package module
import (
"context"
"errors"
"fmt"
"log/slog"
"net"
"time"
@@ -12,13 +11,14 @@ import (
"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/route"
)
type PSNClient struct {
config config.ModuleConfig
conn *net.UDPConn
ctx context.Context
router common.RouteIO
router route.RouteIO
decoder *psn.Decoder
logger *slog.Logger
cancel context.CancelFunc
@@ -26,9 +26,8 @@ type PSNClient struct {
func init() {
RegisterModule(ModuleRegistration{
Type: "psn.client",
Title: "PosiStageNet Client",
New: func(config config.ModuleConfig) (common.Module, error) {
Type: "psn.client",
New: func(config config.ModuleConfig) (Module, error) {
return &PSNClient{config: config, decoder: psn.NewDecoder(), logger: CreateLogger(config)}, nil
},
@@ -45,7 +44,7 @@ func (pc *PSNClient) Type() string {
func (pc *PSNClient) Start(ctx context.Context) error {
pc.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok {
return errors.New("psn.client unable to get router from context")
@@ -107,6 +106,21 @@ 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() {
pc.cancel()
}
func (pc *PSNClient) Get(key string) (any, error) {
switch key {
case "trackers":
return pc.decoder.Trackers, nil
case "name":
return pc.decoder.SystemName, nil
default:
return nil, errors.New("psn.client key not found")
}
}

View File

@@ -1,128 +0,0 @@
//go:build !js
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,17 +9,18 @@ import (
"log/slog"
"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/framer"
"github.com/jwetzell/showbridge-go/internal/processor"
"github.com/jwetzell/showbridge-go/internal/route"
"go.bug.st/serial"
)
type SerialClient struct {
config config.ModuleConfig
ctx context.Context
router common.RouteIO
router route.RouteIO
Port string
Framer framer.Framer
Mode *serial.Mode
@@ -30,24 +31,8 @@ type SerialClient struct {
func init() {
RegisterModule(ModuleRegistration{
Type: "serial.client",
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) {
Type: "serial.client",
New: func(config config.ModuleConfig) (Module, error) {
params := config.Params
portString, err := params.GetString("port")
if err != nil {
@@ -101,7 +86,7 @@ func (sc *SerialClient) SetupPort() error {
func (sc *SerialClient) Start(ctx context.Context) error {
sc.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok {
return errors.New("serial.client unable to get router from context")
@@ -173,10 +158,10 @@ func (sc *SerialClient) Start(ctx context.Context) error {
func (sc *SerialClient) Output(ctx context.Context, payload any) error {
payloadBytes, ok := common.GetAnyAsByteSlice(payload)
payloadBytes, ok := processor.GetAnyAs[[]byte](payload)
if !ok {
return errors.New("serial.client can only output bytes")
return errors.New("serial.client can only ouptut bytes")
}
_, err := sc.port.Write(sc.Framer.Encode(payloadBytes))
@@ -186,3 +171,12 @@ func (sc *SerialClient) Output(ctx context.Context, payload any) error {
func (sc *SerialClient) Stop() {
sc.cancel()
}
func (sc *SerialClient) Get(key string) (any, error) {
switch key {
case "port":
return sc.Port, nil
default:
return nil, errors.New("serial.client key not found")
}
}

View File

@@ -1,15 +1,13 @@
//go:build !js
package module
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log/slog"
"os"
"path"
"sync"
"time"
@@ -17,23 +15,24 @@ import (
"github.com/emiago/diago/media"
"github.com/emiago/sipgo"
"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/processor"
"github.com/jwetzell/showbridge-go/internal/route"
)
type SIPCallServer struct {
config config.ModuleConfig
ctx context.Context
router common.RouteIO
IP string
Port int
Transport string
UserAgent string
dg *diago.Diago
logger *slog.Logger
cancel context.CancelFunc
config config.ModuleConfig
ctx context.Context
router route.RouteIO
IP string
Port int
Transport string
UserAgent string
RecordingPath string
dg *diago.Diago
logger *slog.Logger
cancel context.CancelFunc
}
type SIPCallMessage struct {
@@ -49,39 +48,8 @@ type sipCallContextKey string
func init() {
RegisterModule(ModuleRegistration{
Type: "sip.call.server",
Title: "SIP Call 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(`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) {
Type: "sip.call.server",
New: func(moduleConfig config.ModuleConfig) (Module, error) {
params := moduleConfig.Params
portNum, err := params.GetInt("port")
if err != nil {
@@ -120,7 +88,19 @@ func init() {
}
}
return &SIPCallServer{config: moduleConfig, IP: ipString, Port: int(portNum), Transport: transportString, UserAgent: userAgentString, logger: CreateLogger(moduleConfig)}, nil
recordingPathString := ""
recordingPath, ok := params["recordingPath"]
if ok {
specificRecordingPath, ok := recordingPath.(string)
if !ok {
return nil, errors.New("sip.call.server recordingPath must be a string")
}
recordingPathString = specificRecordingPath
}
return &SIPCallServer{config: moduleConfig, IP: ipString, Port: int(portNum), Transport: transportString, UserAgent: userAgentString, RecordingPath: recordingPathString, logger: CreateLogger(moduleConfig)}, nil
},
})
}
@@ -135,7 +115,7 @@ func (scs *SIPCallServer) Type() string {
func (scs *SIPCallServer) Start(ctx context.Context) error {
scs.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok {
return errors.New("sip.call.server unable to get router from context")
@@ -185,6 +165,41 @@ func (scs *SIPCallServer) HandleCall(inDialog *diago.DialogServerSession) {
dialogContext := context.WithValue(scs.ctx, sipCallContextKey("call"), &SIPCall{
inDialog: inDialog,
})
if scs.RecordingPath != "" {
filename := path.Join(scs.RecordingPath, fmt.Sprintf("%s-%d.wav", inDialog.ToUser(), time.Now().Unix()))
wavFile, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0755)
if err != nil {
scs.logger.Error("error creating recording file", "error", err, "filename", filename)
} else {
recorder, err := inDialog.AudioStereoRecordingCreate(wavFile)
if err != nil {
scs.logger.Error("error creating recording", "error", err)
wavFile.Close()
} else {
go func() {
<-inDialog.Context().Done()
err := recorder.Close()
if err != nil {
scs.logger.Error("error closing recording", "error", err)
}
wavFile.Close()
scs.logger.Debug("finished recording", "filename", filename)
}()
go func() {
bytes, err := media.Copy(recorder.AudioReader(), recorder.AudioWriter())
fmt.Println("recorded bytes", bytes)
if err != nil {
if !errors.Is(err, io.EOF) {
scs.logger.Error("error while recording", "error", err)
}
}
}()
}
}
}
scs.router.HandleInput(dialogContext, scs.Id(), SIPCallMessage{
To: inDialog.ToUser(),
})
@@ -208,7 +223,7 @@ func (scs *SIPCallServer) Output(ctx context.Context, payload any) error {
return errors.New("sip.call.server inDialog already ended")
}
payloadDTMFResponse, ok := common.GetAnyAs[processor.SipDTMFResponse](payload)
payloadDTMFResponse, ok := processor.GetAnyAs[processor.SipDTMFResponse](payload)
if ok {
dtmfWriter := call.inDialog.AudioWriterDTMF()
@@ -224,7 +239,7 @@ func (scs *SIPCallServer) Output(ctx context.Context, payload any) error {
return nil
}
payloadAudioFileResponse, ok := common.GetAnyAs[processor.SipAudioFileResponse](payload)
payloadAudioFileResponse, ok := processor.GetAnyAs[processor.SipAudioFileResponse](payload)
if ok {
audioFile, err := os.Open(payloadAudioFileResponse.AudioFile)
@@ -256,3 +271,7 @@ func (scs *SIPCallServer) Output(ctx context.Context, payload any) error {
func (scs *SIPCallServer) Stop() {
scs.cancel()
}
func (scs *SIPCallServer) Get(key string) (any, error) {
return nil, errors.New("sip.call.server does not support Get")
}

View File

@@ -1,10 +1,7 @@
//go:build !js
package module
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
@@ -18,16 +15,16 @@ import (
"github.com/emiago/diago/media"
"github.com/emiago/sipgo"
"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/processor"
"github.com/jwetzell/showbridge-go/internal/route"
)
type SIPDTMFServer struct {
config config.ModuleConfig
ctx context.Context
router common.RouteIO
router route.RouteIO
IP string
Port int
Transport string
@@ -49,45 +46,8 @@ type SIPDTMFCall struct {
func init() {
RegisterModule(ModuleRegistration{
Type: "sip.dtmf.server",
Title: "SIP DTMF 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(`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) {
Type: "sip.dtmf.server",
New: func(moduleConfig config.ModuleConfig) (Module, error) {
params := moduleConfig.Params
portNum, err := params.GetInt("port")
@@ -154,7 +114,7 @@ func (sds *SIPDTMFServer) Type() string {
func (sds *SIPDTMFServer) Start(ctx context.Context) error {
sds.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok {
return errors.New("sip.dtmf.server unable to get router from context")
@@ -240,7 +200,7 @@ func (sds *SIPDTMFServer) Output(ctx context.Context, payload any) error {
return errors.New("sip.dtmf.server inDialog already ended")
}
payloadDTMFResponse, ok := common.GetAnyAs[processor.SipDTMFResponse](payload)
payloadDTMFResponse, ok := processor.GetAnyAs[processor.SipDTMFResponse](payload)
if ok {
dtmfWriter := call.inDialog.AudioWriterDTMF()
@@ -257,7 +217,7 @@ func (sds *SIPDTMFServer) Output(ctx context.Context, payload any) error {
return nil
}
payloadAudioFileResponse, ok := common.GetAnyAs[processor.SipAudioFileResponse](payload)
payloadAudioFileResponse, ok := processor.GetAnyAs[processor.SipAudioFileResponse](payload)
if ok {
audioFile, err := os.Open(payloadAudioFileResponse.AudioFile)
@@ -290,3 +250,7 @@ func (sds *SIPDTMFServer) Output(ctx context.Context, payload any) error {
func (sds *SIPDTMFServer) Stop() {
sds.cancel()
}
func (sds *SIPDTMFServer) Get(key string) (any, error) {
return nil, errors.New("sip.dtmf.server does not support Get")
}

View File

@@ -1,5 +1,3 @@
//go:build !js
package module
import (
@@ -10,10 +8,11 @@ import (
"net"
"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/framer"
"github.com/jwetzell/showbridge-go/internal/processor"
"github.com/jwetzell/showbridge-go/internal/route"
)
type TCPClient struct {
@@ -21,7 +20,7 @@ type TCPClient struct {
framer framer.Framer
conn *net.TCPConn
ctx context.Context
router common.RouteIO
router route.RouteIO
Addr *net.TCPAddr
logger *slog.Logger
cancel context.CancelFunc
@@ -29,31 +28,8 @@ type TCPClient struct {
func init() {
RegisterModule(ModuleRegistration{
Type: "net.tcp.client",
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) {
Type: "net.tcp.client",
New: func(config config.ModuleConfig) (Module, error) {
params := config.Params
hostString, err := params.GetString("host")
if err != nil {
@@ -95,7 +71,7 @@ func (tc *TCPClient) Type() string {
func (tc *TCPClient) Start(ctx context.Context) error {
tc.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok {
return errors.New("net.tcp.client unable to get router from context")
@@ -178,7 +154,7 @@ func (tc *TCPClient) Output(ctx context.Context, payload any) error {
return err
}
}
payloadBytes, ok := common.GetAnyAsByteSlice(payload)
payloadBytes, ok := processor.GetAnyAs[[]byte](payload)
if !ok {
return errors.New("net.tcp.client is only able to output bytes")
}
@@ -189,3 +165,20 @@ func (tc *TCPClient) Output(ctx context.Context, payload any) error {
func (tc *TCPClient) Stop() {
tc.cancel()
}
func (tc *TCPClient) Get(key string) (any, error) {
switch key {
case "host":
host, err := tc.config.Params.GetString("host")
if err != nil {
return nil, fmt.Errorf("net.tcp.client host error: %w", err)
}
return host, nil
case "ip":
return tc.Addr.IP.String(), nil
case "port":
return tc.Addr.Port, nil
default:
return nil, errors.New("net.tcp.client key not found")
}
}

View File

@@ -1,10 +1,7 @@
//go:build !js
package module
import (
"context"
"encoding/json"
"errors"
"fmt"
"log/slog"
@@ -14,10 +11,11 @@ import (
"syscall"
"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/framer"
"github.com/jwetzell/showbridge-go/internal/processor"
"github.com/jwetzell/showbridge-go/internal/route"
)
type TCPServer struct {
@@ -25,7 +23,7 @@ type TCPServer struct {
Addr *net.TCPAddr
Framer framer.Framer
ctx context.Context
router common.RouteIO
router route.RouteIO
quit chan interface{}
wg sync.WaitGroup
connections []*net.TCPConn
@@ -36,32 +34,8 @@ type TCPServer struct {
func init() {
RegisterModule(ModuleRegistration{
Type: "net.tcp.server",
Title: "TCP 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),
},
"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) {
Type: "net.tcp.server",
New: func(moduleConfig config.ModuleConfig) (Module, error) {
params := moduleConfig.Params
portNum, err := params.GetInt("port")
if err != nil {
@@ -168,13 +142,7 @@ ClientRead:
messages := ts.Framer.Decode(buffer[0:byteCount])
for _, message := range messages {
if ts.router != nil {
senderAddr, ok := client.RemoteAddr().(*net.TCPAddr)
if ok {
senderCtx := context.WithValue(ts.ctx, common.SenderContextKey, senderAddr)
ts.router.HandleInput(senderCtx, ts.Id(), message)
} else {
ts.router.HandleInput(ts.ctx, ts.Id(), message)
}
ts.router.HandleInput(ts.ctx, ts.Id(), message)
} else {
ts.logger.Error("input received but no router is configured")
}
@@ -187,7 +155,7 @@ ClientRead:
func (ts *TCPServer) Start(ctx context.Context) error {
ts.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok {
return errors.New("net.tcp.server unable to get router from context")
@@ -234,7 +202,7 @@ AcceptLoop:
}
func (ts *TCPServer) Output(ctx context.Context, payload any) error {
payloadBytes, ok := common.GetAnyAsByteSlice(payload)
payloadBytes, ok := processor.GetAnyAs[[]byte](payload)
if !ok {
return errors.New("net.tcp.server is only able to output bytes")
@@ -260,3 +228,14 @@ func (ts *TCPServer) Stop() {
ts.cancel()
ts.wg.Wait()
}
func (ts *TCPServer) Get(key string) (any, error) {
switch key {
case "ip":
return ts.Addr.IP.String(), nil
case "port":
return ts.Addr.Port, nil
default:
return nil, errors.New("net.tcp.server key not found")
}
}

View File

@@ -1,87 +0,0 @@
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

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

View File

@@ -1,14 +1,39 @@
package module_test
import (
"context"
"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"
)
type TestModule struct {
}
func (m *TestModule) Output(ctx context.Context, payload any) error {
return nil
}
func (m *TestModule) Start(ctx context.Context) error {
<-ctx.Done()
return nil
}
func (m *TestModule) Stop() {}
func (m *TestModule) Type() string {
return "module.test"
}
func (m *TestModule) Id() string {
return "test"
}
func (m *TestModule) Get(key string) (any, error) {
return nil, nil
}
func TestModuleBadRegistrationNoType(t *testing.T) {
defer func() {
if r := recover(); r == nil {
@@ -18,8 +43,8 @@ func TestModuleBadRegistrationNoType(t *testing.T) {
module.RegisterModule(module.ModuleRegistration{
Type: "",
New: func(config config.ModuleConfig) (common.Module, error) {
return &test.TestModule{}, nil
New: func(config config.ModuleConfig) (module.Module, error) {
return &TestModule{}, nil
},
})
}
@@ -46,15 +71,15 @@ func TestModuleBadRegistrationExistingType(t *testing.T) {
module.RegisterModule(module.ModuleRegistration{
Type: "module.test",
New: func(config config.ModuleConfig) (common.Module, error) {
return &test.TestModule{}, nil
New: func(config config.ModuleConfig) (module.Module, error) {
return &TestModule{}, nil
},
})
module.RegisterModule(module.ModuleRegistration{
Type: "module.test",
New: func(config config.ModuleConfig) (common.Module, error) {
return &test.TestModule{}, nil
New: func(config config.ModuleConfig) (module.Module, error) {
return &TestModule{}, nil
},
})
}

View File

@@ -1,108 +0,0 @@
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

@@ -7,16 +7,16 @@ import (
"log/slog"
"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/route"
)
type TimeInterval struct {
config config.ModuleConfig
Duration uint32
ctx context.Context
router common.RouteIO
router route.RouteIO
ticker *time.Ticker
logger *slog.Logger
cancel context.CancelFunc
@@ -24,21 +24,8 @@ type TimeInterval struct {
func init() {
RegisterModule(ModuleRegistration{
Type: "time.interval",
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) {
Type: "time.interval",
New: func(config config.ModuleConfig) (Module, error) {
params := config.Params
durationInt, err := params.GetInt("duration")
@@ -60,7 +47,7 @@ func (i *TimeInterval) Type() string {
func (i *TimeInterval) Start(ctx context.Context) error {
i.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok {
return errors.New("time.interval unable to get router from context")
@@ -88,6 +75,20 @@ 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() {
i.cancel()
}
func (i *TimeInterval) Get(key string) (any, error) {
switch key {
case "duration":
return i.Duration, nil
default:
return nil, errors.New("time.interval key not found")
}
}

View File

@@ -7,16 +7,16 @@ import (
"log/slog"
"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/route"
)
type TimeTimer struct {
config config.ModuleConfig
Duration uint32
ctx context.Context
router common.RouteIO
router route.RouteIO
timer *time.Timer
logger *slog.Logger
cancel context.CancelFunc
@@ -24,21 +24,8 @@ type TimeTimer struct {
func init() {
RegisterModule(ModuleRegistration{
Type: "time.timer",
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) {
Type: "time.timer",
New: func(config config.ModuleConfig) (Module, error) {
params := config.Params
durationNum, err := params.GetInt("duration")
@@ -61,10 +48,10 @@ func (t *TimeTimer) Type() string {
func (t *TimeTimer) Start(ctx context.Context) error {
t.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok {
return errors.New("time.timer unable to get router from context")
return errors.New("net.tcp.client unable to get router from context")
}
t.router = router
moduleContext, cancel := context.WithCancel(ctx)
@@ -87,6 +74,20 @@ 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() {
t.cancel()
}
func (t *TimeTimer) Get(key string) (any, error) {
switch key {
case "duration":
return t.Duration, nil
default:
return nil, errors.New("time.timer key not found")
}
}

View File

@@ -1,5 +1,3 @@
//go:build !js
package module
import (
@@ -9,9 +7,10 @@ import (
"log/slog"
"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/processor"
"github.com/jwetzell/showbridge-go/internal/route"
)
type UDPClient struct {
@@ -20,33 +19,15 @@ type UDPClient struct {
Port uint16
conn *net.UDPConn
ctx context.Context
router common.RouteIO
router route.RouteIO
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
RegisterModule(ModuleRegistration{
Type: "net.udp.client",
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) {
Type: "net.udp.client",
New: func(config config.ModuleConfig) (Module, error) {
params := config.Params
hostString, err := params.GetString("host")
if err != nil {
@@ -83,7 +64,7 @@ func (uc *UDPClient) SetupConn() error {
func (uc *UDPClient) Start(ctx context.Context) error {
uc.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok {
return errors.New("net.udp.client unable to get router from context")
@@ -108,7 +89,7 @@ func (uc *UDPClient) Start(ctx context.Context) error {
func (uc *UDPClient) Output(ctx context.Context, payload any) error {
payloadBytes, ok := common.GetAnyAsByteSlice(payload)
payloadBytes, ok := processor.GetAnyAs[[]byte](payload)
if !ok {
return errors.New("net.udp.client is only able to output bytes")
}
@@ -127,3 +108,20 @@ func (uc *UDPClient) Output(ctx context.Context, payload any) error {
func (uc *UDPClient) Stop() {
uc.cancel()
}
func (uc *UDPClient) Get(key string) (any, error) {
switch key {
case "host":
host, err := uc.config.Params.GetString("host")
if err != nil {
return nil, fmt.Errorf("net.udp.client host error: %w", err)
}
return host, nil
case "ip":
return uc.Addr.IP.String(), nil
case "port":
return uc.Addr.Port, nil
default:
return nil, errors.New("net.udp.client key not found")
}
}

View File

@@ -1,5 +1,3 @@
//go:build !js
package module
import (
@@ -10,16 +8,17 @@ import (
"net"
"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/processor"
"github.com/jwetzell/showbridge-go/internal/route"
)
type UDPMulticast struct {
config config.ModuleConfig
conn *net.UDPConn
ctx context.Context
router common.RouteIO
router route.RouteIO
Addr *net.UDPAddr
logger *slog.Logger
cancel context.CancelFunc
@@ -27,26 +26,8 @@ type UDPMulticast struct {
func init() {
RegisterModule(ModuleRegistration{
Type: "net.udp.multicast",
Title: "UDP Multicast",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"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) {
Type: "net.udp.multicast",
New: func(moduleConfig config.ModuleConfig) (Module, error) {
params := moduleConfig.Params
ipString, err := params.GetString("ip")
if err != nil {
@@ -77,7 +58,7 @@ func (um *UDPMulticast) Type() string {
func (um *UDPMulticast) Start(ctx context.Context) error {
um.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok {
return errors.New("net.udp.multicast unable to get router from context")
@@ -129,7 +110,7 @@ func (um *UDPMulticast) Start(ctx context.Context) error {
func (um *UDPMulticast) Output(ctx context.Context, payload any) error {
payloadBytes, ok := common.GetAnyAsByteSlice(payload)
payloadBytes, ok := processor.GetAnyAs[[]byte](payload)
if !ok {
return errors.New("net.udp.multicast can only output bytes")
}
@@ -145,3 +126,14 @@ func (um *UDPMulticast) Output(ctx context.Context, payload any) error {
func (um *UDPMulticast) Stop() {
um.cancel()
}
func (um *UDPMulticast) Get(key string) (any, error) {
switch key {
case "ip":
return um.Addr.IP.String(), nil
case "port":
return um.Addr.Port, nil
default:
return nil, errors.New("net.udp.multicast key not found")
}
}

View File

@@ -1,19 +1,16 @@
//go:build !js
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/jwetzell/showbridge-go/internal/route"
)
type UDPServer struct {
@@ -21,41 +18,15 @@ type UDPServer struct {
BufferSize int
config config.ModuleConfig
ctx context.Context
router common.RouteIO
router route.RouteIO
logger *slog.Logger
cancel context.CancelFunc
}
func init() {
RegisterModule(ModuleRegistration{
Type: "net.udp.server",
Title: "UDP 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),
},
"bufferSize": {
Title: "Buffer Size",
Type: "integer",
Minimum: jsonschema.Ptr[float64](1),
Maximum: jsonschema.Ptr[float64](65535),
Default: json.RawMessage("2048"),
},
},
Required: []string{"port"},
AdditionalProperties: nil,
},
New: func(moduleConfig config.ModuleConfig) (common.Module, error) {
Type: "net.udp.server",
New: func(moduleConfig config.ModuleConfig) (Module, error) {
params := moduleConfig.Params
portNum, err := params.GetInt("port")
if err != nil {
@@ -99,7 +70,7 @@ func (us *UDPServer) Type() string {
func (us *UDPServer) Start(ctx context.Context) error {
us.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
router, ok := ctx.Value(common.RouterContextKey).(route.RouteIO)
if !ok {
return errors.New("net.udp.server unable to get router from context")
@@ -126,7 +97,7 @@ func (us *UDPServer) Start(ctx context.Context) error {
default:
listener.SetDeadline(time.Now().Add(time.Millisecond * 200))
numBytes, senderAddr, err := listener.ReadFromUDP(buffer)
numBytes, _, err := listener.ReadFromUDP(buffer)
if err != nil {
//NOTE(jwetzell) we hit deadline
if opErr, ok := err.(*net.OpError); ok && opErr.Timeout() {
@@ -136,8 +107,7 @@ func (us *UDPServer) Start(ctx context.Context) error {
}
message := buffer[:numBytes]
if us.router != nil {
senderCtx := context.WithValue(us.ctx, common.SenderContextKey, senderAddr)
us.router.HandleInput(senderCtx, us.Id(), message)
us.router.HandleInput(us.ctx, us.Id(), message)
} else {
us.logger.Error("input received but no router is configured")
}
@@ -153,3 +123,14 @@ func (us *UDPServer) Output(ctx context.Context, payload any) error {
func (us *UDPServer) Stop() {
us.cancel()
}
func (us *UDPServer) Get(key string) (any, error) {
switch key {
case "ip":
return us.Addr.IP.String(), nil
case "port":
return us.Addr.Port, nil
default:
return nil, errors.New("net.udp.server key not found")
}
}

View File

@@ -1,91 +0,0 @@
//go:build js
package module
import (
"context"
"errors"
"fmt"
"log/slog"
"syscall/js"
"time"
"github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
type WebOnClick struct {
config config.ModuleConfig
ctx context.Context
router common.RouteIO
logger *slog.Logger
ElementId string
}
func init() {
RegisterModule(ModuleRegistration{
Type: "web.onclick",
Title: "On Click",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"id": {
Title: "Element ID",
Type: "string",
Description: "ID of the HTML element to attach the click listener to",
},
},
Required: []string{"duration"},
AdditionalProperties: nil,
},
New: func(config config.ModuleConfig) (common.Module, error) {
params := config.Params
idString, err := params.GetString("id")
if err != nil {
return nil, fmt.Errorf("web.onclick id error: %w", err)
}
return &WebOnClick{ElementId: idString, config: config, logger: CreateLogger(config)}, nil
},
})
}
func (woc *WebOnClick) Id() string {
return woc.config.Id
}
func (woc *WebOnClick) Type() string {
return woc.config.Type
}
func (woc *WebOnClick) Start(ctx context.Context) error {
woc.logger.Debug("running")
router, ok := ctx.Value(common.RouterContextKey).(common.RouteIO)
if !ok {
return errors.New("net.tcp.client unable to get router from context")
}
woc.router = router
woc.ctx = ctx
element := js.Global().Get("document").Call("getElementById", woc.ElementId)
if element.IsNull() || element.IsUndefined() {
return fmt.Errorf("web.onclick unable to find element with id: %s", woc.ElementId)
}
element.Set("onclick", js.FuncOf(func(js.Value, []js.Value) interface{} {
if woc.router != nil {
woc.router.HandleInput(woc.ctx, woc.Id(), time.Now())
}
return nil
}))
<-ctx.Done()
return nil
}
func (woc *WebOnClick) Stop() {
}

View File

@@ -1,7 +0,0 @@
//go:build js
package module
import (
_ "gitlab.com/gomidi/midi/v2/drivers/webmididrv"
)

View File

@@ -1,5 +1,3 @@
//go:build !js
package processor
import (
@@ -7,7 +5,6 @@ import (
"fmt"
"github.com/jwetzell/artnet-go"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
@@ -15,25 +12,20 @@ type ArtNetPacketDecode struct {
config config.ProcessorConfig
}
func (apd *ArtNetPacketDecode) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
payload := wrappedPayload.Payload
payloadBytes, ok := common.GetAnyAsByteSlice(payload)
func (apd *ArtNetPacketDecode) Process(ctx context.Context, payload any) (any, error) {
payloadBytes, ok := GetAnyAs[[]byte](payload)
if !ok {
wrappedPayload.End = true
return wrappedPayload, fmt.Errorf("artnet.packet.decode processor only accepts a []byte")
return nil, fmt.Errorf("artnet.packet.decode processor only accepts a []byte")
}
payloadMessage, err := artnet.Decode(payloadBytes)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
wrappedPayload.Payload = payloadMessage
return wrappedPayload, nil
return payloadMessage, nil
}
func (apd *ArtNetPacketDecode) Type() string {
@@ -42,8 +34,7 @@ func (apd *ArtNetPacketDecode) Type() string {
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "artnet.packet.decode",
Title: "Decode ArtNet Packet",
Type: "artnet.packet.decode",
New: func(config config.ProcessorConfig) (Processor, error) {
return &ArtNetPacketDecode{config: config}, nil
},

View File

@@ -1,5 +1,3 @@
//go:build !js
package processor
import (
@@ -7,7 +5,6 @@ import (
"fmt"
"github.com/jwetzell/artnet-go"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
@@ -15,24 +12,19 @@ type ArtNetPacketEncode struct {
config config.ProcessorConfig
}
func (ape *ArtNetPacketEncode) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
payload := wrappedPayload.Payload
payloadPacket, ok := common.GetAnyAs[artnet.ArtNetPacket](payload)
func (ape *ArtNetPacketEncode) Process(ctx context.Context, payload any) (any, error) {
payloadPacket, ok := GetAnyAs[artnet.ArtNetPacket](payload)
if !ok {
wrappedPayload.End = true
return wrappedPayload, fmt.Errorf("artnet.packet.encode processor only accepts an ArtNetPacket")
return nil, fmt.Errorf("artnet.packet.encode processor only accepts an ArtNetPacket")
}
payloadBytes, err := payloadPacket.MarshalBinary()
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
wrappedPayload.Payload = payloadBytes
return wrappedPayload, nil
return payloadBytes, nil
}
func (ape *ArtNetPacketEncode) Type() string {
@@ -41,8 +33,7 @@ func (ape *ArtNetPacketEncode) Type() string {
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "artnet.packet.encode",
Title: "Encode ArtNet Packet",
Type: "artnet.packet.encode",
New: func(config config.ProcessorConfig) (Processor, error) {
return &ArtNetPacketEncode{config: config}, nil
},

View File

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

View File

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

View File

@@ -1,40 +0,0 @@
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

@@ -6,8 +6,6 @@ import (
"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"
)
@@ -17,50 +15,45 @@ type FilterExpr struct {
Program *vm.Program
}
func (fe *FilterExpr) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
func SafeExprEnv(payload any) any {
exprEnv := ExprEnv{
Payload: payload,
}
exprEnv := wrappedPayload
return exprEnv
}
output, err := expr.Run(fe.Program, exprEnv)
type ExprEnv struct {
Payload any
}
func (se *FilterExpr) Process(ctx context.Context, payload any) (any, error) {
exprEnv := SafeExprEnv(payload)
output, err := expr.Run(se.Program, exprEnv)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
outputBool, ok := output.(bool)
if !ok {
wrappedPayload.End = true
return wrappedPayload, fmt.Errorf("filter.expr expression did not return a boolean")
return nil, fmt.Errorf("filter.expr expression did not return a boolean")
}
if !outputBool {
wrappedPayload.End = true
return wrappedPayload, nil
return nil, nil
}
wrappedPayload.Payload = exprEnv.Payload
return wrappedPayload, nil
return payload, nil
}
func (fe *FilterExpr) Type() string {
return fe.config.Type
func (se *FilterExpr) Type() string {
return se.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,
},
Type: "filter.expr",
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params

View File

@@ -1,73 +0,0 @@
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,13 +2,10 @@ package processor
import (
"context"
"encoding/json"
"errors"
"fmt"
"strconv"
"github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
@@ -17,22 +14,18 @@ type FloatParse struct {
config config.ProcessorConfig
}
func (fp *FloatParse) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
payload := wrappedPayload.Payload
payloadString, ok := common.GetAnyAs[string](payload)
func (fp *FloatParse) Process(ctx context.Context, payload any) (any, error) {
payloadString, ok := GetAnyAs[string](payload)
if !ok {
wrappedPayload.End = true
return wrappedPayload, errors.New("float.parse processor only accepts a string")
return nil, errors.New("float.parse processor only accepts a string")
}
payloadFloat, err := strconv.ParseFloat(payloadString, fp.BitSize)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
wrappedPayload.Payload = payloadFloat
return wrappedPayload, nil
return payloadFloat, nil
}
func (fp *FloatParse) Type() string {
@@ -41,20 +34,7 @@ func (fp *FloatParse) Type() string {
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "float.parse",
Title: "Parse Float",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"bitSize": {
Title: "Bit Size",
Type: "integer",
Enum: []any{32, 64},
Default: json.RawMessage("64"),
},
},
AdditionalProperties: nil,
},
Type: "float.parse",
New: func(moduleConfig config.ProcessorConfig) (Processor, error) {
params := moduleConfig.Params

View File

@@ -2,13 +2,10 @@ 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"
)
@@ -19,19 +16,16 @@ type FloatRandom struct {
config config.ProcessorConfig
}
func (fr *FloatRandom) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
func (fr *FloatRandom) Process(ctx context.Context, payload any) (any, error) {
if fr.BitSize == 32 {
payloadFloat := rand.Float32()*(float32(fr.Max)-float32(fr.Min)) + float32(fr.Min)
wrappedPayload.Payload = payloadFloat
return wrappedPayload, nil
return payloadFloat, nil
}
if fr.BitSize == 64 {
payloadFloat := rand.Float64()*(fr.Max-fr.Min) + fr.Min
wrappedPayload.Payload = payloadFloat
return wrappedPayload, nil
return payloadFloat, nil
}
wrappedPayload.End = true
return wrappedPayload, errors.New("float.random bitSize error: must be 32 or 64")
return nil, errors.New("float.random bitSize error: must be 32 or 64")
}
func (fr *FloatRandom) Type() string {
@@ -40,29 +34,7 @@ func (fr *FloatRandom) Type() string {
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,
},
Type: "float.random",
New: func(processorConfig config.ProcessorConfig) (Processor, error) {
params := processorConfig.Params

View File

@@ -7,9 +7,7 @@ import (
"strconv"
"text/template"
"github.com/google/jsonschema-go/jsonschema"
freeD "github.com/jwetzell/free-d-go"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
@@ -26,16 +24,15 @@ type FreeDCreate struct {
Focus *template.Template
}
func (fc *FreeDCreate) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
func (fc *FreeDCreate) Process(ctx context.Context, payload any) (any, error) {
templateData := wrappedPayload
templateData := GetTemplateData(ctx, payload)
var idBuffer bytes.Buffer
err := fc.Id.Execute(&idBuffer, templateData)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
idString := idBuffer.String()
@@ -43,16 +40,14 @@ func (fc *FreeDCreate) Process(ctx context.Context, wrappedPayload common.Wrappe
idNum, err := strconv.ParseUint(idString, 10, 8)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
var panBuffer bytes.Buffer
err = fc.Pan.Execute(&panBuffer, templateData)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
panString := panBuffer.String()
@@ -60,16 +55,14 @@ func (fc *FreeDCreate) Process(ctx context.Context, wrappedPayload common.Wrappe
panNum, err := strconv.ParseFloat(panString, 32)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
var tiltBuffer bytes.Buffer
err = fc.Tilt.Execute(&tiltBuffer, templateData)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
tiltString := tiltBuffer.String()
@@ -77,16 +70,14 @@ func (fc *FreeDCreate) Process(ctx context.Context, wrappedPayload common.Wrappe
tiltNum, err := strconv.ParseFloat(tiltString, 32)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
var rollBuffer bytes.Buffer
err = fc.Roll.Execute(&rollBuffer, templateData)
err = fc.Tilt.Execute(&rollBuffer, templateData)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
rollString := rollBuffer.String()
@@ -94,16 +85,14 @@ func (fc *FreeDCreate) Process(ctx context.Context, wrappedPayload common.Wrappe
rollNum, err := strconv.ParseFloat(rollString, 32)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
var posXBuffer bytes.Buffer
err = fc.PosX.Execute(&posXBuffer, templateData)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
posXString := posXBuffer.String()
@@ -111,16 +100,14 @@ func (fc *FreeDCreate) Process(ctx context.Context, wrappedPayload common.Wrappe
posXNum, err := strconv.ParseFloat(posXString, 32)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
var posYBuffer bytes.Buffer
err = fc.PosY.Execute(&posYBuffer, templateData)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
posYString := posYBuffer.String()
@@ -128,16 +115,14 @@ func (fc *FreeDCreate) Process(ctx context.Context, wrappedPayload common.Wrappe
posYNum, err := strconv.ParseFloat(posYString, 32)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
var posZBuffer bytes.Buffer
err = fc.PosZ.Execute(&posZBuffer, templateData)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
posZString := posZBuffer.String()
@@ -145,16 +130,14 @@ func (fc *FreeDCreate) Process(ctx context.Context, wrappedPayload common.Wrappe
posZNum, err := strconv.ParseFloat(posZString, 32)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
var zoomBuffer bytes.Buffer
err = fc.Zoom.Execute(&zoomBuffer, templateData)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
zoomString := zoomBuffer.String()
@@ -162,16 +145,14 @@ func (fc *FreeDCreate) Process(ctx context.Context, wrappedPayload common.Wrappe
zoomNum, err := strconv.ParseInt(zoomString, 10, 32)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
var focusBuffer bytes.Buffer
err = fc.Focus.Execute(&focusBuffer, templateData)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
focusString := focusBuffer.String()
@@ -179,8 +160,7 @@ func (fc *FreeDCreate) Process(ctx context.Context, wrappedPayload common.Wrappe
focusNum, err := strconv.ParseInt(focusString, 10, 32)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
payloadMessage := freeD.FreeDPosition{
@@ -195,9 +175,7 @@ func (fc *FreeDCreate) Process(ctx context.Context, wrappedPayload common.Wrappe
Focus: int32(focusNum),
}
wrappedPayload.Payload = payloadMessage
return wrappedPayload, nil
return payloadMessage, nil
}
func (fc *FreeDCreate) Type() string {
@@ -206,61 +184,7 @@ func (fc *FreeDCreate) Type() string {
func init() {
RegisterProcessor(ProcessorRegistration{
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,
},
Type: "freed.create",
New: func(config config.ProcessorConfig) (Processor, error) {
// TODO(jwetzell): make some params optional
@@ -283,10 +207,6 @@ func init() {
panTemplate, err := template.New("pan").Parse(panString)
if err != nil {
return nil, err
}
tiltString, err := params.GetString("tilt")
if err != nil {
return nil, fmt.Errorf("freed.create tilt error: %w", err)
@@ -294,10 +214,6 @@ func init() {
tiltTemplate, err := template.New("tilt").Parse(tiltString)
if err != nil {
return nil, err
}
rollString, err := params.GetString("roll")
if err != nil {
return nil, fmt.Errorf("freed.create roll error: %w", err)
@@ -349,19 +265,12 @@ func init() {
zoomTemplate, err := template.New("zoom").Parse(zoomString)
if err != nil {
return nil, err
}
focusString, err := params.GetString("focus")
if err != nil {
return nil, fmt.Errorf("freed.create focus error: %w", err)
}
focusTemplate, err := template.New("focus").Parse(focusString)
if err != nil {
return nil, err
}
return &FreeDCreate{
config: config,

View File

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

View File

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

View File

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

View File

@@ -1,122 +0,0 @@
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
}
type HTTPResponse struct {
Status int
Body []byte
}
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", "PUT", "PATCH", "DELETE"},
},
"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

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

View File

@@ -1,5 +1,3 @@
//go:build !js
package processor
import (
@@ -8,8 +6,6 @@ import (
"fmt"
"text/template"
"github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
@@ -19,46 +15,34 @@ type HTTPResponseCreate struct {
config config.ProcessorConfig
}
func (hrc *HTTPResponseCreate) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
templateData := wrappedPayload
var bodyBuffer bytes.Buffer
err := hrc.BodyTmpl.Execute(&bodyBuffer, templateData)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
}
wrappedPayload.Payload = HTTPResponse{
Status: hrc.Status,
Body: bodyBuffer.Bytes(),
}
return wrappedPayload, nil
type HTTPResponse struct {
Status int
Body []byte
}
func (hrc *HTTPResponseCreate) Type() string {
return hrc.config.Type
func (hre *HTTPResponseCreate) Process(ctx context.Context, payload any) (any, error) {
templateData := GetTemplateData(ctx, payload)
var bodyBuffer bytes.Buffer
err := hre.BodyTmpl.Execute(&bodyBuffer, templateData)
if err != nil {
return nil, err
}
return HTTPResponse{
Status: hre.Status,
Body: bodyBuffer.Bytes(),
}, nil
}
func (hre *HTTPResponseCreate) Type() string {
return hre.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
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,
},
Type: "http.response.create",
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params

View File

@@ -2,13 +2,10 @@ package processor
import (
"context"
"encoding/json"
"errors"
"fmt"
"strconv"
"github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
@@ -18,22 +15,18 @@ type IntParse struct {
config config.ProcessorConfig
}
func (ip *IntParse) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
payload := wrappedPayload.Payload
payloadString, ok := common.GetAnyAs[string](payload)
func (ip *IntParse) Process(ctx context.Context, payload any) (any, error) {
payloadString, ok := GetAnyAs[string](payload)
if !ok {
wrappedPayload.End = true
return wrappedPayload, errors.New("int.parse processor only accepts a string")
return nil, errors.New("int.parse processor only accepts a string")
}
payloadInt, err := strconv.ParseInt(payloadString, ip.Base, ip.BitSize)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
wrappedPayload.Payload = payloadInt
return wrappedPayload, nil
return payloadInt, nil
}
func (ip *IntParse) Type() string {
@@ -42,26 +35,7 @@ func (ip *IntParse) Type() string {
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "int.parse",
Title: "Parse Int",
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,
},
Type: "int.parse",
New: func(moduleConfig config.ProcessorConfig) (Processor, error) {
params := moduleConfig.Params

View File

@@ -6,8 +6,6 @@ import (
"fmt"
"math/rand/v2"
"github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
@@ -17,10 +15,9 @@ type IntRandom struct {
config config.ProcessorConfig
}
func (ir *IntRandom) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
func (ir *IntRandom) Process(ctx context.Context, payload any) (any, error) {
payloadInt := rand.IntN(ir.Max-ir.Min+1) + ir.Min
wrappedPayload.Payload = payloadInt
return wrappedPayload, nil
return payloadInt, nil
}
func (ir *IntRandom) Type() string {
@@ -29,23 +26,7 @@ func (ir *IntRandom) Type() string {
func init() {
RegisterProcessor(ProcessorRegistration{
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,
},
Type: "int.random",
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params

View File

@@ -1,99 +0,0 @@
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,7 +5,6 @@ import (
"encoding/json"
"errors"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
@@ -13,30 +12,21 @@ type JsonDecode struct {
config config.ProcessorConfig
}
func (jd *JsonDecode) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
payload := wrappedPayload.Payload
payloadBytes, ok := common.GetAnyAsByteSlice(payload)
func (jd *JsonDecode) Process(ctx context.Context, payload any) (any, error) {
payloadString, ok := GetAnyAs[string](payload)
if !ok {
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)
return nil, errors.New("json.decode processor only accepts a string")
}
payloadJson := make(map[string]any)
err := json.Unmarshal(payloadBytes, &payloadJson)
err := json.Unmarshal([]byte(payloadString), &payloadJson)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
wrappedPayload.Payload = payloadJson
return wrappedPayload, nil
return payloadJson, nil
}
@@ -46,8 +36,7 @@ func (jd *JsonDecode) Type() string {
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "json.decode",
Title: "Decode JSON",
Type: "json.decode",
New: func(config config.ProcessorConfig) (Processor, error) {
return &JsonDecode{config: config}, nil
},

View File

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

View File

@@ -1,88 +0,0 @@
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

@@ -1,114 +0,0 @@
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

@@ -1,4 +1,4 @@
//go:build cgo || js
//go:build cgo
package processor
@@ -9,8 +9,6 @@ import (
"strconv"
"text/template"
"github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
"gitlab.com/gomidi/midi/v2"
)
@@ -18,11 +16,11 @@ import (
// TODO(jwetzell): support using numbers in config file treated as hardcoded values
type MIDIMessageCreate struct {
config config.ProcessorConfig
ProcessFunc func(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error)
ProcessFunc func(ctx context.Context, payload any) (any, error)
}
func (mmc *MIDIMessageCreate) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
return mmc.ProcessFunc(ctx, wrappedPayload)
func (mmc *MIDIMessageCreate) Process(ctx context.Context, payload any) (any, error) {
return mmc.ProcessFunc(ctx, payload)
}
func (mmc *MIDIMessageCreate) Type() string {
@@ -66,15 +64,14 @@ func newMidiNoteOnCreate(config config.ProcessorConfig) (Processor, error) {
return nil, err
}
return &MIDIMessageCreate{config: config, ProcessFunc: func(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
templateData := wrappedPayload
return &MIDIMessageCreate{config: config, ProcessFunc: func(ctx context.Context, payload any) (any, error) {
templateData := GetTemplateData(ctx, payload)
var channelBuffer bytes.Buffer
err := channelTemplate.Execute(&channelBuffer, templateData)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
channelValue, err := strconv.ParseUint(channelBuffer.String(), 10, 8)
@@ -83,8 +80,7 @@ func newMidiNoteOnCreate(config config.ProcessorConfig) (Processor, error) {
err = noteTemplate.Execute(&noteBuffer, templateData)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
noteValue, err := strconv.ParseUint(noteBuffer.String(), 10, 8)
@@ -93,14 +89,12 @@ func newMidiNoteOnCreate(config config.ProcessorConfig) (Processor, error) {
err = velocityTemplate.Execute(&velocityBuffer, templateData)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
velocityValue, err := strconv.ParseUint(velocityBuffer.String(), 10, 8)
payloadMessage := midi.NoteOn(uint8(channelValue), uint8(noteValue), uint8(velocityValue))
wrappedPayload.Payload = payloadMessage
return wrappedPayload, nil
return payloadMessage, nil
}}, nil
}
@@ -141,16 +135,15 @@ func newMidiNoteOffCreate(config config.ProcessorConfig) (Processor, error) {
return nil, err
}
return &MIDIMessageCreate{config: config, ProcessFunc: func(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
return &MIDIMessageCreate{config: config, ProcessFunc: func(ctx context.Context, payload any) (any, error) {
templateData := wrappedPayload
templateData := GetTemplateData(ctx, payload)
var channelBuffer bytes.Buffer
err := channelTemplate.Execute(&channelBuffer, templateData)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
channelValue, err := strconv.ParseUint(channelBuffer.String(), 10, 8)
@@ -159,8 +152,7 @@ func newMidiNoteOffCreate(config config.ProcessorConfig) (Processor, error) {
err = noteTemplate.Execute(&noteBuffer, templateData)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
noteValue, err := strconv.ParseUint(noteBuffer.String(), 10, 8)
@@ -169,15 +161,13 @@ func newMidiNoteOffCreate(config config.ProcessorConfig) (Processor, error) {
err = velocityTemplate.Execute(&velocityBuffer, templateData)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
velocityValue, err := strconv.ParseUint(velocityBuffer.String(), 10, 8)
payloadMessage := midi.NoteOffVelocity(uint8(channelValue), uint8(noteValue), uint8(velocityValue))
wrappedPayload.Payload = payloadMessage
return wrappedPayload, nil
return payloadMessage, nil
}}, nil
}
@@ -218,16 +208,15 @@ func newMidiControlChangeCreate(config config.ProcessorConfig) (Processor, error
return nil, err
}
return &MIDIMessageCreate{config: config, ProcessFunc: func(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
return &MIDIMessageCreate{config: config, ProcessFunc: func(ctx context.Context, payload any) (any, error) {
templateData := wrappedPayload
templateData := GetTemplateData(ctx, payload)
var channelBuffer bytes.Buffer
err := channelTemplate.Execute(&channelBuffer, templateData)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
channelValue, err := strconv.ParseUint(channelBuffer.String(), 10, 8)
@@ -236,8 +225,7 @@ func newMidiControlChangeCreate(config config.ProcessorConfig) (Processor, error
err = controlTemplate.Execute(&controlBuffer, templateData)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
controlValue, err := strconv.ParseUint(controlBuffer.String(), 10, 8)
@@ -246,15 +234,13 @@ func newMidiControlChangeCreate(config config.ProcessorConfig) (Processor, error
err = valueTemplate.Execute(&valueBuffer, templateData)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
valueValue, err := strconv.ParseUint(valueBuffer.String(), 10, 8)
payloadMessage := midi.ControlChange(uint8(channelValue), uint8(controlValue), uint8(valueValue))
wrappedPayload.Payload = payloadMessage
return wrappedPayload, nil
return payloadMessage, nil
}}, nil
}
@@ -284,15 +270,14 @@ func newMidiProgramChangeCreate(config config.ProcessorConfig) (Processor, error
return nil, err
}
return &MIDIMessageCreate{config: config, ProcessFunc: func(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
templateData := wrappedPayload
return &MIDIMessageCreate{config: config, ProcessFunc: func(ctx context.Context, payload any) (any, error) {
templateData := GetTemplateData(ctx, payload)
var channelBuffer bytes.Buffer
err := channelTemplate.Execute(&channelBuffer, templateData)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
channelValue, err := strconv.ParseUint(channelBuffer.String(), 10, 8)
@@ -301,119 +286,19 @@ func newMidiProgramChangeCreate(config config.ProcessorConfig) (Processor, error
err = programTemplate.Execute(&programBuffer, templateData)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
programValue, err := strconv.ParseUint(programBuffer.String(), 10, 8)
payloadMessage := midi.ProgramChange(uint8(channelValue), uint8(programValue))
wrappedPayload.Payload = payloadMessage
return wrappedPayload, nil
return payloadMessage, nil
}}, nil
}
func init() {
RegisterProcessor(ProcessorRegistration{
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,
},
},
},
Type: "midi.message.create",
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params

View File

@@ -1,4 +1,4 @@
//go:build cgo || js
//go:build cgo
package processor
@@ -6,7 +6,6 @@ import (
"context"
"errors"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
"gitlab.com/gomidi/midi/v2"
)
@@ -15,19 +14,16 @@ type MIDIMessageDecode struct {
config config.ProcessorConfig
}
func (mmd *MIDIMessageDecode) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
payload := wrappedPayload.Payload
payloadBytes, ok := common.GetAnyAsByteSlice(payload)
func (mmd *MIDIMessageDecode) Process(ctx context.Context, payload any) (any, error) {
payloadBytes, ok := GetAnyAs[[]byte](payload)
if !ok {
wrappedPayload.End = true
return wrappedPayload, errors.New("midi.message.decode processor only accepts a []byte")
return nil, errors.New("midi.message.decode processor only accepts a []byte")
}
payloadMessage := midi.Message(payloadBytes)
wrappedPayload.Payload = payloadMessage
return wrappedPayload, nil
return payloadMessage, nil
}
func (mmd *MIDIMessageDecode) Type() string {
@@ -36,8 +32,7 @@ func (mmd *MIDIMessageDecode) Type() string {
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "midi.message.decode",
Title: "Decode MIDI Message",
Type: "midi.message.decode",
New: func(config config.ProcessorConfig) (Processor, error) {
return &MIDIMessageDecode{config: config}, nil
},

View File

@@ -1,4 +1,4 @@
//go:build cgo || js
//go:build cgo
package processor
@@ -6,7 +6,6 @@ import (
"context"
"errors"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
"gitlab.com/gomidi/midi/v2"
)
@@ -15,17 +14,14 @@ type MIDIMessageEncode struct {
config config.ProcessorConfig
}
func (mme *MIDIMessageEncode) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
payload := wrappedPayload.Payload
payloadMessage, ok := common.GetAnyAs[midi.Message](payload)
func (mme *MIDIMessageEncode) Process(ctx context.Context, payload any) (any, error) {
payloadMessage, ok := GetAnyAs[midi.Message](payload)
if !ok {
wrappedPayload.End = true
return wrappedPayload, errors.New("midi.message.encode processor only accepts a midi.Message")
return nil, errors.New("midi.message.encode processor only accepts a midi.Message")
}
wrappedPayload.Payload = payloadMessage.Bytes()
return wrappedPayload, nil
return payloadMessage.Bytes(), nil
}
func (mme *MIDIMessageEncode) Type() string {
@@ -34,8 +30,7 @@ func (mme *MIDIMessageEncode) Type() string {
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "midi.message.encode",
Title: "Encode MIDI Message",
Type: "midi.message.encode",
New: func(config config.ProcessorConfig) (Processor, error) {
return &MIDIMessageEncode{config: config}, nil
},

View File

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

View File

@@ -1,4 +1,4 @@
//go:build cgo || js
//go:build cgo
package processor
@@ -7,7 +7,6 @@ import (
"errors"
"fmt"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
"gitlab.com/gomidi/midi/v2"
)
@@ -45,44 +44,36 @@ type MIDIPitchBend struct {
Absolute uint16
}
func (mmu *MIDIMessageUnpack) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
payload := wrappedPayload.Payload
payloadMidi, ok := common.GetAnyAs[midi.Message](payload)
func (mmu *MIDIMessageUnpack) Process(ctx context.Context, payload any) (any, error) {
payloadMidi, ok := GetAnyAs[midi.Message](payload)
if !ok {
wrappedPayload.End = true
return wrappedPayload, errors.New("midi.message.unpack processor only accepts a midi.Message")
return nil, errors.New("midi.message.unpack processor only accepts a midi.Message")
}
switch payloadMidi.Type() {
case midi.NoteOnMsg:
noteOnMsg := MIDINoteOn{}
payloadMidi.GetNoteOn(&noteOnMsg.Channel, &noteOnMsg.Note, &noteOnMsg.Velocity)
wrappedPayload.Payload = noteOnMsg
return wrappedPayload, nil
return noteOnMsg, nil
case midi.NoteOffMsg:
noteOffMsg := MIDINoteOff{}
payloadMidi.GetNoteOff(&noteOffMsg.Channel, &noteOffMsg.Note, &noteOffMsg.Velocity)
wrappedPayload.Payload = noteOffMsg
return wrappedPayload, nil
return noteOffMsg, nil
case midi.ControlChangeMsg:
controlChangeMsg := MIDIControlChange{}
payloadMidi.GetControlChange(&controlChangeMsg.Channel, &controlChangeMsg.Control, &controlChangeMsg.Value)
wrappedPayload.Payload = controlChangeMsg
return wrappedPayload, nil
return controlChangeMsg, nil
case midi.ProgramChangeMsg:
programChangeMsg := MIDIProgramChange{}
payloadMidi.GetProgramChange(&programChangeMsg.Channel, &programChangeMsg.Program)
wrappedPayload.Payload = programChangeMsg
return wrappedPayload, nil
return programChangeMsg, nil
case midi.PitchBendMsg:
pitchBendMsg := MIDIPitchBend{}
payloadMidi.GetPitchBend(&pitchBendMsg.Channel, &pitchBendMsg.Relative, &pitchBendMsg.Absolute)
wrappedPayload.Payload = pitchBendMsg
return wrappedPayload, nil
return pitchBendMsg, nil
default:
wrappedPayload.End = true
return wrappedPayload, fmt.Errorf("midi.message.unpack message type not supported %v", payloadMidi.Type())
return nil, fmt.Errorf("midi.message.unpack message type not supported %v", payloadMidi.Type())
}
}
@@ -92,8 +83,7 @@ func (mmu *MIDIMessageUnpack) Type() string {
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "midi.message.unpack",
Title: "Unpack MIDI Message",
Type: "midi.message.unpack",
New: func(config config.ProcessorConfig) (Processor, error) {
return &MIDIMessageUnpack{config: config}, nil
},

View File

@@ -1,5 +1,3 @@
//go:build !js
package processor
import (
@@ -7,8 +5,6 @@ import (
"errors"
"fmt"
"github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
@@ -64,17 +60,17 @@ func (mm MQTTMessage) Payload() []byte {
func (mm MQTTMessage) Ack() {}
func (mmc *MQTTMessageCreate) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
func (mmc *MQTTMessageCreate) Process(ctx context.Context, payload any) (any, error) {
// TODO(jwetzell): support templating
wrappedPayload.Payload = MQTTMessage{
message := MQTTMessage{
topic: mmc.Topic,
qos: mmc.QoS,
retained: mmc.Retained,
payload: mmc.Payload,
}
return wrappedPayload, nil
return message, nil
}
func (mmc *MQTTMessageCreate) Type() string {
@@ -83,31 +79,7 @@ func (mmc *MQTTMessageCreate) Type() string {
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "mqtt.message.create",
Title: "Create MQTT Message",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"topic": {
Title: "Topic",
Type: "string",
},
"qos": {
Title: "QoS",
Type: "number",
},
"retained": {
Title: "Retained",
Type: "boolean",
},
"payload": {
Title: "Payload",
Type: "string",
},
},
Required: []string{"topic", "qos", "retained", "payload"},
AdditionalProperties: nil,
},
Type: "mqtt.message.create",
New: func(processorConfig config.ProcessorConfig) (Processor, error) {
params := processorConfig.Params
topicString, err := params.GetString("topic")

View File

@@ -0,0 +1,36 @@
package processor
import (
"context"
"errors"
mqtt "github.com/eclipse/paho.mqtt.golang"
"github.com/jwetzell/showbridge-go/internal/config"
)
type MQTTMessageEncode struct {
config config.ProcessorConfig
}
func (mme *MQTTMessageEncode) Process(ctx context.Context, payload any) (any, error) {
payloadMessage, ok := GetAnyAs[mqtt.Message](payload)
if !ok {
return nil, errors.New("mqtt.message.encode processor only accepts an mqtt.Message")
}
return payloadMessage.Payload(), nil
}
func (mme *MQTTMessageEncode) Type() string {
return mme.config.Type
}
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "mqtt.message.encode",
New: func(config config.ProcessorConfig) (Processor, error) {
return &MQTTMessageEncode{config: config}, nil
},
})
}

View File

@@ -1,5 +1,3 @@
//go:build !js
package processor
import (
@@ -8,8 +6,6 @@ import (
"fmt"
"text/template"
"github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
@@ -24,16 +20,14 @@ type NATSMessageCreate struct {
Payload *template.Template
}
func (nmc *NATSMessageCreate) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
templateData := wrappedPayload
func (nmc *NATSMessageCreate) Process(ctx context.Context, payload any) (any, error) {
templateData := GetTemplateData(ctx, payload)
var payloadBuffer bytes.Buffer
err := nmc.Payload.Execute(&payloadBuffer, templateData)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
payloadString := payloadBuffer.String()
@@ -42,18 +36,17 @@ func (nmc *NATSMessageCreate) Process(ctx context.Context, wrappedPayload common
err = nmc.Subject.Execute(&subjectBuffer, templateData)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
subjectString := subjectBuffer.String()
wrappedPayload.Payload = NATSMessage{
message := NATSMessage{
Subject: subjectString,
Payload: []byte(payloadString),
}
return wrappedPayload, nil
return message, nil
}
func (nmc *NATSMessageCreate) Type() string {
@@ -62,23 +55,7 @@ func (nmc *NATSMessageCreate) Type() string {
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "nats.message.create",
Title: "Create NATS Message",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"subject": {
Title: "Subject",
Type: "string",
},
"payload": {
Title: "Payload",
Type: "string",
},
},
Required: []string{"subject", "payload"},
AdditionalProperties: nil,
},
Type: "nats.message.create",
New: func(config config.ProcessorConfig) (Processor, error) {
params := config.Params
subjectString, err := params.GetString("subject")

View File

@@ -9,9 +9,7 @@ import (
"strconv"
"text/template"
"github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/osc-go"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
@@ -22,28 +20,25 @@ type OSCMessageCreate struct {
Types string
}
func (omc *OSCMessageCreate) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
func (omc *OSCMessageCreate) Process(ctx context.Context, payload any) (any, error) {
templateData := wrappedPayload
templateData := GetTemplateData(ctx, payload)
var addressBuffer bytes.Buffer
err := omc.Address.Execute(&addressBuffer, templateData)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
addressString := addressBuffer.String()
if len(addressString) == 0 {
wrappedPayload.End = true
return wrappedPayload, errors.New("osc.message.create address must not be empty")
return nil, errors.New("osc.message.create address must not be empty")
}
if addressString[0] != '/' {
wrappedPayload.End = true
return wrappedPayload, errors.New("osc.message.create address must start with '/'")
return nil, errors.New("osc.message.create address must start with '/'")
}
payloadMessage := &osc.OSCMessage{
@@ -57,8 +52,7 @@ func (omc *OSCMessageCreate) Process(ctx context.Context, wrappedPayload common.
err := argTemplate.Execute(&argBuffer, templateData)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
argString := argBuffer.String()
@@ -66,8 +60,7 @@ func (omc *OSCMessageCreate) Process(ctx context.Context, wrappedPayload common.
typedArg, err := argToTypedArg(argString, omc.Types[argIndex])
if err != nil {
wrappedPayload.End = true
return wrappedPayload, err
return nil, err
}
args = append(args, typedArg)
@@ -77,8 +70,7 @@ func (omc *OSCMessageCreate) Process(ctx context.Context, wrappedPayload common.
payloadMessage.Args = args
}
wrappedPayload.Payload = payloadMessage
return wrappedPayload, nil
return payloadMessage, nil
}
func (omc *OSCMessageCreate) Type() string {
@@ -87,30 +79,7 @@ func (omc *OSCMessageCreate) Type() string {
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "osc.message.create",
Title: "Create OSC Message",
ParamsSchema: &jsonschema.Schema{
Type: "object",
Properties: map[string]*jsonschema.Schema{
"address": {
Title: "Address",
Type: "string",
},
"args": {
Title: "Arguments",
Type: "array",
Items: &jsonschema.Schema{
Type: "string",
},
},
"types": {
Title: "Argument Types",
Type: "string",
},
},
Required: []string{"address"},
AdditionalProperties: nil,
},
Type: "osc.message.create",
New: func(processorConfig config.ProcessorConfig) (Processor, error) {
params := processorConfig.Params
addressString, err := params.GetString("address")

View File

@@ -6,7 +6,6 @@ import (
"fmt"
osc "github.com/jwetzell/osc-go"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
@@ -14,32 +13,26 @@ type OSCMessageDecode struct {
config config.ProcessorConfig
}
func (omd *OSCMessageDecode) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
payload := wrappedPayload.Payload
payloadBytes, ok := common.GetAnyAsByteSlice(payload)
func (omd *OSCMessageDecode) Process(ctx context.Context, payload any) (any, error) {
payloadBytes, ok := GetAnyAs[[]byte](payload)
if !ok {
wrappedPayload.End = true
return wrappedPayload, errors.New("osc.message.decode processor only accepts a []byte payload")
return nil, errors.New("osc.message.decode processor only accepts a []byte payload")
}
if len(payloadBytes) == 0 {
wrappedPayload.End = true
return wrappedPayload, errors.New("osc.message.decode processor can't work on empty []byte")
return nil, errors.New("osc.message.decode processor can't work on empty []byte")
}
if payloadBytes[0] != '/' {
wrappedPayload.End = true
return wrappedPayload, errors.New("osc.message.decode processor needs an OSC looking []byte")
return nil, errors.New("osc.message.decode processor needs an OSC looking []byte")
}
message, err := osc.MessageFromBytes(payloadBytes)
if err != nil {
wrappedPayload.End = true
return wrappedPayload, fmt.Errorf("osc.message.decode processor failed to decode OSC message: %w", err)
return nil, fmt.Errorf("osc.message.decode processor failed to decode OSC message: %w", err)
}
wrappedPayload.Payload = message
return wrappedPayload, nil
return message, nil
}
func (omd *OSCMessageDecode) Type() string {
@@ -48,8 +41,7 @@ func (omd *OSCMessageDecode) Type() string {
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "osc.message.decode",
Title: "Decode OSC Message",
Type: "osc.message.decode",
New: func(config config.ProcessorConfig) (Processor, error) {
return &OSCMessageDecode{config: config}, nil
},

View File

@@ -5,7 +5,6 @@ import (
"errors"
osc "github.com/jwetzell/osc-go"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
@@ -13,17 +12,15 @@ type OSCMessageEncode struct {
config config.ProcessorConfig
}
func (ome *OSCMessageEncode) Process(ctx context.Context, wrappedPayload common.WrappedPayload) (common.WrappedPayload, error) {
payload := wrappedPayload.Payload
payloadMessage, ok := common.GetAnyAs[*osc.OSCMessage](payload)
func (ome *OSCMessageEncode) Process(ctx context.Context, payload any) (any, error) {
payloadMessage, ok := GetAnyAs[*osc.OSCMessage](payload)
if !ok {
wrappedPayload.End = true
return wrappedPayload, errors.New("osc.message.encode processor only accepts an *OSCMessage")
return nil, errors.New("osc.message.encode processor only accepts an *OSCMessage")
}
wrappedPayload.Payload = payloadMessage.ToBytes()
return wrappedPayload, nil
bytes := payloadMessage.ToBytes()
return bytes, nil
}
func (ome *OSCMessageEncode) Type() string {
@@ -32,8 +29,7 @@ func (ome *OSCMessageEncode) Type() string {
func init() {
RegisterProcessor(ProcessorRegistration{
Type: "osc.message.encode",
Title: "Encode OSC Message",
Type: "osc.message.encode",
New: func(config config.ProcessorConfig) (Processor, error) {
return &OSCMessageEncode{config: config}, nil
},

View File

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

View File

@@ -5,22 +5,18 @@ import (
"fmt"
"sync"
"github.com/google/jsonschema-go/jsonschema"
"github.com/jwetzell/showbridge-go/internal/common"
"github.com/jwetzell/showbridge-go/internal/config"
)
type Processor interface {
Type() string
Process(context.Context, common.WrappedPayload) (common.WrappedPayload, error)
Process(context.Context, any) (any, error)
}
type ProcessorRegistration struct {
Type string `json:"type"`
Title string `json:"title,omitempty"`
Description string `json:"description,omitempty"`
ParamsSchema *jsonschema.Schema `json:"paramsSchema,omitempty"`
New func(config.ProcessorConfig) (Processor, error)
Type string `json:"type"`
New func(config.ProcessorConfig) (Processor, error)
}
func RegisterProcessor(processor ProcessorRegistration) {
@@ -45,3 +41,22 @@ var (
processorRegistryMu sync.RWMutex
ProcessorRegistry = make(map[string]ProcessorRegistration)
)
func GetAnyAs[T any](p any) (T, bool) {
typed, ok := p.(T)
return typed, ok
}
type TemplateData struct {
Payload any
Modules any
}
func GetTemplateData(ctx context.Context, payload any) TemplateData {
templateData := TemplateData{Payload: payload}
modules := ctx.Value(common.ModulesContextKey)
if modules != nil {
templateData.Modules = modules
}
return templateData
}

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