From b15e282d59f0bd460e4c3433d8832a84455a7d61 Mon Sep 17 00:00:00 2001 From: Joel Wetzell Date: Sun, 28 Dec 2025 12:25:25 -0600 Subject: [PATCH] support basic http server response control with body string template for now --- internal/module/http-server.go | 130 ++++++++++++++------- internal/processor/http-response-create.go | 81 +++++++++++++ 2 files changed, 167 insertions(+), 44 deletions(-) create mode 100644 internal/processor/http-response-create.go diff --git a/internal/module/http-server.go b/internal/module/http-server.go index 1930a3a..3fe8a6d 100644 --- a/internal/module/http-server.go +++ b/internal/module/http-server.go @@ -9,6 +9,7 @@ import ( "net/http" "github.com/jwetzell/showbridge-go/internal/config" + "github.com/jwetzell/showbridge-go/internal/processor" "github.com/jwetzell/showbridge-go/internal/route" ) @@ -27,12 +28,29 @@ type ResponseIOError struct { InputError *string `json:"inputError"` } -type ResponseData struct { +type IOResponseData struct { IOErrors []ResponseIOError `json:"ioErrors"` Message string `json:"message"` Status string `json:"status"` } +type httpServerContextKey string + +type HTTPServerResponseWriter struct { + http.ResponseWriter + done bool +} + +func (hsrw *HTTPServerResponseWriter) WriteHeader(status int) { + hsrw.done = true + hsrw.ResponseWriter.WriteHeader(status) +} + +func (hsrw *HTTPServerResponseWriter) Write(data []byte) (int, error) { + hsrw.done = true + return hsrw.ResponseWriter.Write(data) +} + func init() { RegisterModule(ModuleRegistration{ Type: "http.server", @@ -69,66 +87,74 @@ func (hs *HTTPServer) Type() string { } func (hs *HTTPServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") + responseWriter := HTTPServerResponseWriter{ResponseWriter: w} - response := ResponseData{ + response := IOResponseData{ Message: "routing successful", Status: "ok", } - if hs.router != nil { - aRouteFound, routingErrors := hs.router.HandleInput(hs.ctx, hs.Id(), r) - if aRouteFound { - if routingErrors != nil { - w.WriteHeader(http.StatusInternalServerError) - response.Status = "error" - response.Message = "routing failed" + inputContext := context.WithValue(hs.ctx, httpServerContextKey("responseWriter"), &responseWriter) + aRouteFound, routingErrors := hs.router.HandleInput(inputContext, hs.Id(), r) + if !responseWriter.done { + if aRouteFound { + if routingErrors != nil { + w.WriteHeader(http.StatusInternalServerError) + response.Status = "error" + response.Message = "routing failed" - response.IOErrors = []ResponseIOError{} - for _, responseIOError := range routingErrors { - errorToAdd := ResponseIOError{ - Index: responseIOError.Index, - } - - if responseIOError.InputError != nil { - errorMsg := responseIOError.InputError.Error() - errorToAdd.InputError = &errorMsg - } - - if responseIOError.ProcessError != nil { - errorMsg := responseIOError.ProcessError.Error() - errorToAdd.ProcessError = &errorMsg - } - - if responseIOError.OutputErrors != nil { - outputErrorMsgs := []string{} - - for _, outputError := range responseIOError.OutputErrors { - outputErrorMsgs = append(outputErrorMsgs, outputError.Error()) + response.IOErrors = []ResponseIOError{} + for _, responseIOError := range routingErrors { + errorToAdd := ResponseIOError{ + Index: responseIOError.Index, } - errorToAdd.OutputErrors = outputErrorMsgs + if responseIOError.InputError != nil { + errorMsg := responseIOError.InputError.Error() + errorToAdd.InputError = &errorMsg + } + + if responseIOError.ProcessError != nil { + errorMsg := responseIOError.ProcessError.Error() + errorToAdd.ProcessError = &errorMsg + } + + if responseIOError.OutputErrors != nil { + outputErrorMsgs := []string{} + + for _, outputError := range responseIOError.OutputErrors { + outputErrorMsgs = append(outputErrorMsgs, outputError.Error()) + } + + errorToAdd.OutputErrors = outputErrorMsgs + } + + response.IOErrors = append(response.IOErrors, errorToAdd) + } - - response.IOErrors = append(response.IOErrors, errorToAdd) - + json.NewEncoder(w).Encode(response) + return + } else { + w.WriteHeader(http.StatusOK) + response.Message = "routing successful" + json.NewEncoder(w).Encode(response) + return } } else { - w.WriteHeader(http.StatusOK) - response.Message = "routing successful" + w.WriteHeader(http.StatusNotFound) + response.Status = "error" + response.Message = "no matching routes found" + json.NewEncoder(w).Encode(response) + return } - } else { - w.WriteHeader(http.StatusNotFound) - response.Status = "error" - response.Message = "no matching routes found" } } else { w.WriteHeader(http.StatusInternalServerError) response.Message = "no router registered" response.Status = "error" + json.NewEncoder(w).Encode(response) + return } - - json.NewEncoder(w).Encode(response) } func (hs *HTTPServer) Run() error { @@ -156,5 +182,21 @@ func (hs *HTTPServer) Run() error { } func (hs *HTTPServer) Output(ctx context.Context, payload any) error { - return errors.New("http.server output is not implemented") + responseWriter, ok := ctx.Value(httpServerContextKey("responseWriter")).(*HTTPServerResponseWriter) + + if !ok { + return errors.New("http.server output must originate from an http.server input") + } + + payloadResponse, ok := payload.(processor.HTTPResponse) + + if !ok { + return errors.New("http.server is only able to output HTTPResponse") + } + + responseWriter.WriteHeader(payloadResponse.Status) + + responseWriter.Write(payloadResponse.Body) + + return nil } diff --git a/internal/processor/http-response-create.go b/internal/processor/http-response-create.go new file mode 100644 index 0000000..1be3cd7 --- /dev/null +++ b/internal/processor/http-response-create.go @@ -0,0 +1,81 @@ +package processor + +import ( + "bytes" + "context" + "errors" + "text/template" + + "github.com/jwetzell/showbridge-go/internal/config" +) + +type HTTPResponseCreate struct { + Status int + Body *template.Template + config config.ProcessorConfig +} + +type HTTPResponse struct { + Status int + Body []byte +} + +func (hre *HTTPResponseCreate) Process(ctx context.Context, payload any) (any, error) { + var bodyBuffer bytes.Buffer + err := hre.Body.Execute(&bodyBuffer, payload) + + 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", + New: func(config config.ProcessorConfig) (Processor, error) { + params := config.Params + + status, ok := params["status"] + + if !ok { + return nil, errors.New("http.response.create requires a status parameter") + } + + statusNum, ok := status.(float64) + + if !ok { + return nil, errors.New("http.resposne.create status must be a number") + } + + body, ok := params["body"] + + if !ok { + return nil, errors.New("osc.message.create requires an body parameter") + } + + bodyString, ok := body.(string) + + if !ok { + return nil, errors.New("osc.message.create body must be a string") + } + + bodyTemplate, err := template.New("body").Parse(bodyString) + + if err != nil { + return nil, err + } + + // TODO(jwetzell): support other body kind (direct bytes from input, from file?) + return &HTTPResponseCreate{config: config, Status: int(statusNum), Body: bodyTemplate}, nil + }, + }) +}