Skip to content

Commit

Permalink
✨ Add JSON POST body parsing middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
wesen committed Feb 7, 2025
1 parent c161f33 commit a5ac19f
Show file tree
Hide file tree
Showing 2 changed files with 234 additions and 11 deletions.
58 changes: 47 additions & 11 deletions pkg/glazed/handlers/json/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,32 @@ package json
import (
"bytes"
"encoding/json"
"net/http"

"github.com/go-go-golems/glazed/pkg/cmds"
"github.com/go-go-golems/glazed/pkg/cmds/layers"
"github.com/go-go-golems/glazed/pkg/cmds/middlewares"
"github.com/go-go-golems/glazed/pkg/cmds/parameters"
json2 "github.com/go-go-golems/glazed/pkg/formatters/json"
"github.com/go-go-golems/glazed/pkg/middlewares/row"
"github.com/go-go-golems/parka/pkg/glazed/handlers"
middlewares2 "github.com/go-go-golems/parka/pkg/glazed/middlewares"
parka_middlewares "github.com/go-go-golems/parka/pkg/glazed/middlewares"
"github.com/labstack/echo/v4"
"net/http"
)

type QueryHandler struct {
cmd cmds.Command
middlewares []middlewares.Middleware
// useJSONBody determines whether to use JSON body parsing (POST) or query parameters (GET)
useJSONBody bool
}

type QueryHandlerOption func(*QueryHandler)

func NewQueryHandler(cmd cmds.Command, options ...QueryHandlerOption) *QueryHandler {
h := &QueryHandler{
cmd: cmd,
cmd: cmd,
useJSONBody: false, // default to query parameters
}

for _, option := range options {
Expand All @@ -40,18 +44,39 @@ func WithMiddlewares(middlewares ...middlewares.Middleware) QueryHandlerOption {
}
}

// WithJSONBody configures the handler to use JSON body parsing instead of query parameters
func WithJSONBody() QueryHandlerOption {
return func(handler *QueryHandler) {
handler.useJSONBody = true
}
}

var _ handlers.Handler = (*QueryHandler)(nil)

func (h *QueryHandler) Handle(c echo.Context) error {
description := h.cmd.Description()
parsedLayers := layers.NewParsedLayers()

middlewares_ := append(
[]middlewares.Middleware{
middlewares2.UpdateFromQueryParameters(c, parameters.WithParseStepSource("query")),
},
h.middlewares...,
)
var jsonMiddleware *parka_middlewares.JSONBodyMiddleware
if h.useJSONBody {
jsonMiddleware = parka_middlewares.NewJSONBodyMiddleware(c, parameters.WithParseStepSource("json"))
defer func() {
if err := jsonMiddleware.Close(); err != nil {
// We can only log this error since we're in a defer
c.Logger().Errorf("failed to cleanup JSON middleware: %v", err)
}
}()
}

// Build the middleware chain
middlewares_ := make([]middlewares.Middleware, 0)
if h.useJSONBody {
middlewares_ = append(middlewares_, jsonMiddleware.Middleware())
} else {
middlewares_ = append(middlewares_, parka_middlewares.UpdateFromQueryParameters(c, parameters.WithParseStepSource("query")))
}

middlewares_ = append(middlewares_, h.middlewares...)
middlewares_ = append(middlewares_, middlewares.SetFromDefaults())

err := middlewares.ExecuteMiddlewares(description.Layers, parsedLayers, middlewares_...)
Expand All @@ -71,14 +96,14 @@ func (h *QueryHandler) Handle(c echo.Context) error {
return err
}

foo := struct {
response := struct {
Data string `json:"data"`
}{
Data: buf.String(),
}
encoder := json.NewEncoder(c.Response())
encoder.SetIndent("", " ")
err = encoder.Encode(foo)
err = encoder.Encode(response)
if err != nil {
return err
}
Expand Down Expand Up @@ -120,10 +145,21 @@ func (h *QueryHandler) Handle(c echo.Context) error {
return nil
}

// CreateJSONQueryHandler creates a new JSON handler that uses query parameters
func CreateJSONQueryHandler(
cmd cmds.Command,
options ...QueryHandlerOption,
) echo.HandlerFunc {
handler := NewQueryHandler(cmd, options...)
return handler.Handle
}

// CreateJSONBodyHandler creates a new JSON handler that uses POST body parsing
func CreateJSONBodyHandler(
cmd cmds.Command,
options ...QueryHandlerOption,
) echo.HandlerFunc {
options = append(options, WithJSONBody())
handler := NewQueryHandler(cmd, options...)
return handler.Handle
}
187 changes: 187 additions & 0 deletions pkg/glazed/middlewares/json.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package middlewares

import (
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"sync"

"github.com/go-go-golems/glazed/pkg/cmds/layers"
"github.com/go-go-golems/glazed/pkg/cmds/middlewares"
"github.com/go-go-golems/glazed/pkg/cmds/parameters"
"github.com/labstack/echo/v4"
"github.com/pkg/errors"
)

// JSONBodyMiddleware is a struct-based middleware that handles JSON POST requests
// and manages temporary files created during processing.
type JSONBodyMiddleware struct {
c echo.Context
options []parameters.ParseStepOption
files []string
mu sync.Mutex
}

// NewJSONBodyMiddleware creates a new JSONBodyMiddleware instance
func NewJSONBodyMiddleware(c echo.Context, options ...parameters.ParseStepOption) *JSONBodyMiddleware {
return &JSONBodyMiddleware{
c: c,
options: options,
files: make([]string, 0),
}
}

// Close cleans up any temporary files created during processing
func (m *JSONBodyMiddleware) Close() error {
m.mu.Lock()
defer m.mu.Unlock()

var errs []error
for _, f := range m.files {
if err := os.Remove(f); err != nil {
errs = append(errs, errors.Wrapf(err, "failed to remove temporary file %s", f))
}
}
m.files = m.files[:0] // Clear the files list

if len(errs) > 0 {
return errors.Errorf("failed to clean up some temporary files: %v", errs)
}
return nil
}

// addFile adds a temporary file to be cleaned up later
func (m *JSONBodyMiddleware) addFile(path string) {
m.mu.Lock()
defer m.mu.Unlock()
m.files = append(m.files, path)
}

// createTempFileFromString creates a temporary file with the given content
func (m *JSONBodyMiddleware) createTempFileFromString(content string) (string, error) {
tmpFile, err := os.CreateTemp("", "parka-json-*")
if err != nil {
return "", errors.Wrap(err, "could not create temporary file")
}
defer tmpFile.Close()

_, err = tmpFile.WriteString(content)
if err != nil {
return "", errors.Wrap(err, "could not write to temporary file")
}

return tmpFile.Name(), nil
}

// Middleware returns the actual middleware function
func (m *JSONBodyMiddleware) Middleware() middlewares.Middleware {
return func(next middlewares.HandlerFunc) middlewares.HandlerFunc {
return func(layers_ *layers.ParameterLayers, parsedLayers *layers.ParsedLayers) error {
// Read the request body
body, err := io.ReadAll(m.c.Request().Body)
if err != nil {
return errors.Wrap(err, "could not read request body")
}

// Parse JSON
var jsonData map[string]interface{}
if err := json.Unmarshal(body, &jsonData); err != nil {
return errors.Wrap(err, "could not parse JSON body")
}

err = layers_.ForEachE(func(_ string, l layers.ParameterLayer) error {
parsedLayer := parsedLayers.GetOrCreate(l)

pds := l.GetParameterDefinitions()
err := pds.ForEachE(func(p *parameters.ParameterDefinition) error {
value, exists := jsonData[p.Name]
if !exists {
if p.Required {
return errors.Errorf("required parameter '%s' is missing", p.Name)
}
return nil
}

// Handle file-like parameters
if p.Type.NeedsFileContent("") {
switch v := value.(type) {
case string:
// Create a temporary file with the content
tmpPath, err := m.createTempFileFromString(v)
if err != nil {
return err
}
m.addFile(tmpPath)

// Parse the file content
f, err := os.Open(tmpPath)
if err != nil {
return errors.Wrapf(err, "could not open temporary file for parameter '%s'", p.Name)
}
defer f.Close()

parsed, err := p.ParseFromReader(f, filepath.Base(tmpPath))
if err != nil {
return errors.Wrapf(err, "invalid value for parameter '%s'", p.Name)
}

parsedLayer.Parameters.UpdateValue(p.Name, p, parsed.Value, m.options...)
return nil
default:
return errors.Errorf("invalid type for file parameter '%s': expected string", p.Name)
}
}

// Handle regular parameters
var stringValue string
switch v := value.(type) {
case string:
stringValue = v
case float64:
stringValue = fmt.Sprintf("%v", v)
case bool:
stringValue = fmt.Sprintf("%v", v)
case []interface{}:
// Handle array parameters
if p.Type.IsList() {
strValues := make([]string, len(v))
for i, item := range v {
strValues[i] = fmt.Sprintf("%v", item)
}
parsedParam, err := p.ParseParameter(strValues, m.options...)
if err != nil {
return errors.Wrapf(err, "invalid value for parameter '%s'", p.Name)
}
parsedLayer.Parameters.Update(p.Name, parsedParam)
return nil
}
return errors.Errorf("received array for non-array parameter '%s'", p.Name)
default:
return errors.Errorf("unsupported type for parameter '%s'", p.Name)
}

parsedParam, err := p.ParseParameter([]string{stringValue}, m.options...)
if err != nil {
return errors.Wrapf(err, "invalid value for parameter '%s': %s", p.Name, stringValue)
}
parsedLayer.Parameters.Update(p.Name, parsedParam)

return nil
})

if err != nil {
return err
}
return nil
})

if err != nil {
return err
}

return next(layers_, parsedLayers)
}
}
}

0 comments on commit a5ac19f

Please sign in to comment.