From bd7874eee4c6b64b117a3c8fa63dbfd02e7ed4d0 Mon Sep 17 00:00:00 2001 From: Manuel Odendahl Date: Wed, 8 May 2024 14:26:26 -0400 Subject: [PATCH] :umbrella: Fix tests --- cmd/parka/cmds/serve.go | 2 +- go.mod | 12 +- go.sum | 18 +++ pkg/NOTES.md | 4 +- pkg/glazed/handlers/datatables/datatables.go | 20 +-- pkg/glazed/handlers/glazed/glazed.go | 13 +- pkg/glazed/handlers/handler.go | 9 +- pkg/glazed/handlers/json/json.go | 27 ++-- .../handlers/output-file/output-file.go | 120 ++++++---------- pkg/glazed/handlers/sse/sse.go | 37 ++--- .../handlers/text/text-glazed-cmd_test.go | 12 +- .../handlers/text/text-writer-cmd_test.go | 26 ++-- pkg/glazed/handlers/text/text.go | 27 ++-- pkg/glazed/middlewares/form.go | 18 ++- pkg/glazed/middlewares/form_test.go | 15 +- pkg/glazed/middlewares/query.go | 10 +- pkg/glazed/middlewares/query_test.go | 12 +- pkg/handlers/command-dir/command-dir.go | 134 ++++++++++-------- pkg/handlers/command/command.go | 22 +-- pkg/handlers/static-dir/static.go | 3 +- pkg/handlers/static-file/static-file.go | 8 +- pkg/handlers/template/template.go | 2 +- pkg/render/renderer.go | 101 +++++++------ pkg/server/server.go | 39 +++-- pkg/utils/fs/fs.go | 15 +- pkg/utils/http.go | 3 + pkg/utils/test-helpers.go | 21 +-- 27 files changed, 375 insertions(+), 355 deletions(-) create mode 100644 pkg/utils/http.go diff --git a/cmd/parka/cmds/serve.go b/cmd/parka/cmds/serve.go index 0d768eb..8b929db 100644 --- a/cmd/parka/cmds/serve.go +++ b/cmd/parka/cmds/serve.go @@ -45,7 +45,7 @@ var ServeCmd = &cobra.Command{ Str("templateDir", "pkg/web/src/templates"). Msg("Using assets from disk") serverOptions = append(serverOptions, - server.WithStaticPaths(fs.NewStaticPath(http.FS(os.DirFS("pkg/web/dist")), "/dist")), + server.WithStaticPaths(fs.NewStaticPath(os.DirFS("pkg/web/dist"), "/dist")), ) defaultLookups = append(defaultLookups, render.NewLookupTemplateFromDirectory("pkg/web/src/templates")) } else { diff --git a/go.mod b/go.mod index 5fbaf1f..86199bb 100644 --- a/go.mod +++ b/go.mod @@ -74,11 +74,13 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect github.com/kopoli/go-terminal-size v0.0.0-20170219200355-5c97524c8b54 // indirect + github.com/labstack/echo/v4 v4.12.0 // indirect + github.com/labstack/gommon v0.4.2 // indirect github.com/leodido/go-urn v1.2.4 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/microcosm-cc/bluemonday v1.0.25 // indirect @@ -105,6 +107,8 @@ require ( github.com/tj/go-naturaldate v1.3.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.2.11 // indirect + github.com/valyala/bytebufferpool v1.0.0 // indirect + github.com/valyala/fasttemplate v1.2.2 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/xuri/efp v0.0.0-20220603152613-6918739fd470 // indirect github.com/xuri/excelize/v2 v2.7.1 // indirect @@ -112,10 +116,10 @@ require ( github.com/yuin/goldmark-emoji v1.0.2 // indirect go.mongodb.org/mongo-driver v1.11.3 // indirect golang.org/x/arch v0.3.0 // indirect - golang.org/x/crypto v0.16.0 // indirect + golang.org/x/crypto v0.22.0 // indirect golang.org/x/image v0.14.0 // indirect - golang.org/x/net v0.19.0 // indirect - golang.org/x/sys v0.18.0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index 754d267..93fc9ff 100644 --- a/go.sum +++ b/go.sum @@ -266,6 +266,10 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= +github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= +github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= +github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= @@ -276,7 +280,10 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= @@ -373,6 +380,10 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo= +github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= @@ -422,6 +433,8 @@ golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -497,6 +510,8 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -561,12 +576,15 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= diff --git a/pkg/NOTES.md b/pkg/NOTES.md index 544d9e7..8c86c0a 100644 --- a/pkg/NOTES.md +++ b/pkg/NOTES.md @@ -40,7 +40,7 @@ and call Run on the underlying glaze.Command. #### CommandHandlerFunc - parsing incoming requests into a command's parameters and layers This is done through the `CommandHandlerFunc` type, which is a function that -takes a `*gin.Context` and a `*CommandContext` and is allowed to modify both as +takes a `echo.Context` and a `*CommandContext` and is allowed to modify both as it does its thing. A `CommandContext` is a struct that has a reference to the `GlazeCommand` and keeps @@ -53,7 +53,7 @@ necessary to run the glaze command. #### CreateProcessorFunc - creating the output formatter We also have something called a `CreateProcessorFunc`, which is a function that -takes a `*gin.Context` and a `*CommandContext` and returns a `glaze.Processor`. +takes a `echo.Context` and a `*CommandContext` and returns a `glaze.Processor`. This allows us to override what output formatter is created, depending on the request. The default handler will process the parsed parameters for the `glazed` layer, just as it would on the command line. If nothing is set, it would create a JSON output formatter. diff --git a/pkg/glazed/handlers/datatables/datatables.go b/pkg/glazed/handlers/datatables/datatables.go index da4aec3..623402e 100644 --- a/pkg/glazed/handlers/datatables/datatables.go +++ b/pkg/glazed/handlers/datatables/datatables.go @@ -4,7 +4,6 @@ import ( "context" "embed" "fmt" - "github.com/gin-gonic/gin" "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" @@ -19,6 +18,7 @@ import ( parka_middlewares "github.com/go-go-golems/parka/pkg/glazed/middlewares" "github.com/go-go-golems/parka/pkg/render" "github.com/go-go-golems/parka/pkg/render/layout" + "github.com/labstack/echo/v4" "github.com/pkg/errors" "github.com/rs/zerolog/log" "golang.org/x/sync/errgroup" @@ -167,7 +167,7 @@ func WithStreamRows(streamRows bool) QueryHandlerOption { var _ handlers.Handler = &QueryHandler{} -func (qh *QueryHandler) Handle(c *gin.Context, w io.Writer) error { +func (qh *QueryHandler) Handle(c echo.Context) error { description := qh.cmd.Description() parsedLayers := layers.NewParsedLayers() @@ -181,7 +181,7 @@ func (qh *QueryHandler) Handle(c *gin.Context, w io.Writer) error { dt_ := qh.dt.Clone() if cm_, ok := qh.cmd.(cmds.CommandWithMetadata); ok { - dt_.CommandMetadata, err = cm_.Metadata(c, parsedLayers) + dt_.CommandMetadata, err = cm_.Metadata(c.Request().Context(), parsedLayers) } var of formatters.RowOutputFormatter @@ -217,7 +217,7 @@ func (qh *QueryHandler) Handle(c *gin.Context, w io.Writer) error { columnsC <- []types.FieldName{} close(columnsC) close(rowC) - err_ := qh.renderTemplate(parsedLayers, w, dt_, columnsC) + err_ := qh.renderTemplate(parsedLayers, c.Response(), dt_, columnsC) if err_ != nil { return err_ } @@ -239,7 +239,7 @@ func (qh *QueryHandler) Handle(c *gin.Context, w io.Writer) error { gp.AddTableMiddleware(table.NewOutputChannelMiddleware(of, rowC)) } - ctx := c.Request.Context() + ctx := c.Request().Context() ctx2, cancel := context.WithCancel(ctx) eg, ctx3 := errgroup.WithContext(ctx2) @@ -298,7 +298,7 @@ func (qh *QueryHandler) Handle(c *gin.Context, w io.Writer) error { eg.Go(func() error { // if qh.Cmd implements cmds.CommandWithMetadata, get Metadata - err := qh.renderTemplate(parsedLayers, w, dt_, columnsC) + err := qh.renderTemplate(parsedLayers, c.Response(), dt_, columnsC) if err != nil { return err } @@ -364,8 +364,8 @@ func CreateDataTablesHandler( path string, commandPath string, options ...QueryHandlerOption, -) gin.HandlerFunc { - return func(c *gin.Context) { +) echo.HandlerFunc { + return func(c echo.Context) error { name := cmd.Description().Name dateTime := time.Now().Format("2006-01-02--15-04-05") links := []layout.Link{ @@ -415,9 +415,11 @@ func CreateDataTablesHandler( handler := NewQueryHandler(cmd, options_...) - err := handler.Handle(c, c.Writer) + err := handler.Handle(c) if err != nil && !errors.Is(err, context.Canceled) { log.Error().Err(err).Msg("error handling query") } + + return nil } } diff --git a/pkg/glazed/handlers/glazed/glazed.go b/pkg/glazed/handlers/glazed/glazed.go index c9bd16d..5040cf5 100644 --- a/pkg/glazed/handlers/glazed/glazed.go +++ b/pkg/glazed/handlers/glazed/glazed.go @@ -1,7 +1,6 @@ package glazed import ( - "github.com/gin-gonic/gin" "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" @@ -9,8 +8,9 @@ import ( "github.com/go-go-golems/glazed/pkg/settings" "github.com/go-go-golems/parka/pkg/glazed/handlers" middlewares2 "github.com/go-go-golems/parka/pkg/glazed/middlewares" + "github.com/labstack/echo/v4" "github.com/pkg/errors" - "io" + "net/http" ) type QueryHandler struct { @@ -40,7 +40,7 @@ func NewQueryHandler(cmd cmds.GlazeCommand, options ...QueryHandlerOption) *Quer var _ handlers.Handler = (*QueryHandler)(nil) -func (h *QueryHandler) Handle(c *gin.Context, writer io.Writer) error { +func (h *QueryHandler) Handle(c echo.Context) error { description := h.cmd.Description() parsedLayers := layers.NewParsedLayers() @@ -63,14 +63,15 @@ func (h *QueryHandler) Handle(c *gin.Context, writer io.Writer) error { return err } - of, err := settings.SetupProcessorOutput(gp, glazedLayer, writer) + of, err := settings.SetupProcessorOutput(gp, glazedLayer, c.Response()) if err != nil { return err } - c.Header("Content-Type", of.ContentType()) + c.Response().Header().Set("Content-Type", of.ContentType()) + c.Response().WriteHeader(http.StatusOK) - ctx := c.Request.Context() + ctx := c.Request().Context() err = h.cmd.RunIntoGlazeProcessor(ctx, parsedLayers, gp) if err != nil { return err diff --git a/pkg/glazed/handlers/handler.go b/pkg/glazed/handlers/handler.go index 9229b66..89d684b 100644 --- a/pkg/glazed/handlers/handler.go +++ b/pkg/glazed/handlers/handler.go @@ -1,13 +1,16 @@ package handlers import ( - "github.com/gin-gonic/gin" "github.com/go-go-golems/glazed/pkg/cmds" - "io" + "github.com/labstack/echo/v4" ) +// Handler wraps the normal handle method to allow rendering into a different writer, +// so that we can provide file downloads. +// +// TODO(manuel, 2024-05-07) I don't think we actually need this type Handler interface { - Handle(c *gin.Context, w io.Writer) error + Handle(c echo.Context) error } type UnsupportedCommandError struct { diff --git a/pkg/glazed/handlers/json/json.go b/pkg/glazed/handlers/json/json.go index d83755a..d12282c 100644 --- a/pkg/glazed/handlers/json/json.go +++ b/pkg/glazed/handlers/json/json.go @@ -3,7 +3,6 @@ package json import ( "bytes" "encoding/json" - "github.com/gin-gonic/gin" "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" @@ -12,8 +11,7 @@ import ( "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" - "github.com/rs/zerolog/log" - "io" + "github.com/labstack/echo/v4" "net/http" ) @@ -44,7 +42,7 @@ func WithMiddlewares(middlewares ...middlewares.Middleware) QueryHandlerOption { var _ handlers.Handler = (*QueryHandler)(nil) -func (h *QueryHandler) Handle(c *gin.Context, writer io.Writer) error { +func (h *QueryHandler) Handle(c echo.Context) error { description := h.cmd.Description() parsedLayers := layers.NewParsedLayers() @@ -58,9 +56,10 @@ func (h *QueryHandler) Handle(c *gin.Context, writer io.Writer) error { return err } - c.Header("Content-Type", "application/json") + c.Response().Header().Set("Content-Type", "application/json") + c.Response().WriteHeader(http.StatusOK) - ctx := c.Request.Context() + ctx := c.Request().Context() switch cmd := h.cmd.(type) { case cmds.WriterCommand: buf := bytes.Buffer{} @@ -74,7 +73,7 @@ func (h *QueryHandler) Handle(c *gin.Context, writer io.Writer) error { }{ Data: buf.String(), } - encoder := json.NewEncoder(writer) + encoder := json.NewEncoder(c.Response()) encoder.SetIndent("", " ") err = encoder.Encode(foo) if err != nil { @@ -89,7 +88,7 @@ func (h *QueryHandler) Handle(c *gin.Context, writer io.Writer) error { // remove table middlewares because we are a streaming handler gp.ReplaceTableMiddleware() - gp.AddRowMiddleware(row.NewOutputMiddleware(json2.NewOutputFormatter(), writer)) + gp.AddRowMiddleware(row.NewOutputMiddleware(json2.NewOutputFormatter(), c.Response())) if err != nil { return err @@ -121,15 +120,7 @@ func (h *QueryHandler) Handle(c *gin.Context, writer io.Writer) error { func CreateJSONQueryHandler( cmd cmds.Command, options ...QueryHandlerOption, -) gin.HandlerFunc { +) echo.HandlerFunc { handler := NewQueryHandler(cmd, options...) - return func(c *gin.Context) { - err := handler.Handle(c, c.Writer) - if err != nil { - log.Error().Err(err).Msg("failed to handle query") - c.JSON(http.StatusInternalServerError, gin.H{ - "error": err.Error(), - }) - } - } + return handler.Handle } diff --git a/pkg/glazed/handlers/output-file/output-file.go b/pkg/glazed/handlers/output-file/output-file.go index 7922574..e655451 100644 --- a/pkg/glazed/handlers/output-file/output-file.go +++ b/pkg/glazed/handlers/output-file/output-file.go @@ -1,64 +1,24 @@ package output_file import ( - "bytes" "fmt" - "github.com/gin-gonic/gin" "github.com/go-go-golems/glazed/pkg/cmds" "github.com/go-go-golems/glazed/pkg/cmds/middlewares" "github.com/go-go-golems/glazed/pkg/cmds/parameters" "github.com/go-go-golems/glazed/pkg/helpers/list" "github.com/go-go-golems/glazed/pkg/settings" - "github.com/go-go-golems/parka/pkg/glazed/handlers" "github.com/go-go-golems/parka/pkg/glazed/handlers/glazed" parka_middlewares "github.com/go-go-golems/parka/pkg/glazed/middlewares" + "github.com/labstack/echo/v4" + "github.com/pkg/errors" "io" + "net/http" + "net/http/httptest" "os" "path/filepath" "strings" ) -type OutputFileHandler struct { - handler handlers.Handler - outputFileName string -} - -func NewOutputFileHandler(handler handlers.Handler, outputFileName string) *OutputFileHandler { - h := &OutputFileHandler{ - handler: handler, - outputFileName: outputFileName, - } - - return h -} - -var _ handlers.Handler = (*OutputFileHandler)(nil) - -func (h *OutputFileHandler) Handle(c *gin.Context, w io.Writer) error { - buf := &bytes.Buffer{} - err := h.handler.Handle(c, buf) - if err != nil { - return err - } - - c.Status(200) - - f, err := os.Open(h.outputFileName) - if err != nil { - return err - } - defer func(f *os.File) { - _ = f.Close() - }(f) - - _, err = io.Copy(c.Writer, f) - if err != nil { - return err - } - - return nil -} - // CreateGlazedFileHandler creates a handler that will run a glazed command and write the output // with a Content-Disposition header to the response writer. // @@ -68,8 +28,8 @@ func CreateGlazedFileHandler( cmd cmds.GlazeCommand, fileName string, middlewares_ ...middlewares.Middleware, -) gin.HandlerFunc { - return func(c *gin.Context) { +) echo.HandlerFunc { + return func(c echo.Context) error { glazedOverrides := map[string]interface{}{} needsRealFileOutput := false @@ -97,28 +57,12 @@ func CreateGlazedFileHandler( glazedOverrides["output"] = "table" glazedOverrides["table-format"] = "ascii" } else { - c.JSON(500, gin.H{"error": "could not determine output format"}) - return + return errors.New("unsupported file format") } var tmpFile *os.File var err error - // excel output needs a real output file, otherwise we can go stream to the HTTP response - if needsRealFileOutput { - tmpFile, err = os.CreateTemp("/tmp", fmt.Sprintf("glazed-output-*.%s", fileName)) - if err != nil { - c.JSON(500, gin.H{"error": "could not create temporary file"}) - return - } - defer func(name string) { - _ = os.Remove(name) - }(tmpFile.Name()) - - // now check file suffix for content-type - glazedOverrides["output-file"] = tmpFile.Name() - } - glazedOverride := middlewares.UpdateFromMap( map[string]map[string]interface{}{ settings.GlazedSlug: glazedOverrides, @@ -134,25 +78,55 @@ func CreateGlazedFileHandler( )) baseName := filepath.Base(fileName) - c.Writer.Header().Set("Content-Disposition", "attachment; filename="+baseName) + c.Response().Header().Set("Content-Disposition", "attachment; filename="+baseName) + // excel output needs a real output file, otherwise we can go stream to the HTTP response if needsRealFileOutput { - outputFileHandler := NewOutputFileHandler(handler, tmpFile.Name()) + tmpFile, err = os.CreateTemp("/tmp", fmt.Sprintf("glazed-output-*.%s", fileName)) + if err != nil { + return errors.Wrap(err, "could not create temporary file") + } + defer func(name string) { + _ = os.Remove(name) + }(tmpFile.Name()) - err = outputFileHandler.Handle(c, c.Writer) + // now check file suffix for content-type + glazedOverrides["output-file"] = tmpFile.Name() + + // here we have the output of the handler go to a request that we discard, and + // we instead copy the temporary file to the response writer + res := httptest.NewRecorder() + req := c.Request() + newCtx := c.Echo().NewContext(req, res) + + err = handler.Handle(newCtx) + if err != nil { + return err + } + + // copy tmpFile to output + f, err := os.Open(tmpFile.Name()) + if err != nil { + return errors.Wrap(err, "could not open temporary file") + } + defer func(f *os.File) { + _ = f.Close() + }(f) + + c.Response().Header().Set("Content-Type", "application/octet-stream") + c.Response().WriteHeader(http.StatusOK) + + _, err = io.Copy(c.Response().Writer, f) if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) - return + return err } } else { - err = handler.Handle(c, c.Writer) + err = handler.Handle(c) if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) - return + return err } } - // override parameter layers at the end - + return nil } } diff --git a/pkg/glazed/handlers/sse/sse.go b/pkg/glazed/handlers/sse/sse.go index a7082b2..3bb58ea 100644 --- a/pkg/glazed/handlers/sse/sse.go +++ b/pkg/glazed/handlers/sse/sse.go @@ -4,7 +4,6 @@ package sse import ( "fmt" - "github.com/gin-gonic/gin" "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" @@ -13,7 +12,7 @@ import ( "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" - "github.com/rs/zerolog/log" + "github.com/labstack/echo/v4" "golang.org/x/sync/errgroup" "net/http" ) @@ -43,7 +42,7 @@ func WithMiddlewares(middlewares ...middlewares.Middleware) QueryHandlerOption { } } -func (h *QueryHandler) Handle(c *gin.Context, writer gin.ResponseWriter) error { +func (h *QueryHandler) Handle(c echo.Context) error { description := h.cmd.Description() parsedLayers := layers.NewParsedLayers() @@ -54,9 +53,12 @@ func (h *QueryHandler) Handle(c *gin.Context, writer gin.ResponseWriter) error { if err != nil { return err } - c.Header("Content-Type", "text/event-stream") + c.Response().Header().Set("Content-Type", "text/event-stream") + c.Response().Header().Set("Cache-Control", "no-cache") + c.Response().Header().Set("Connection", "keep-alive") + c.Response().WriteHeader(http.StatusOK) - ctx := c.Request.Context() + ctx := c.Request().Context() switch cmd := h.cmd.(type) { case cmds.WriterCommand: // Create a writer that on every read amount of bytes sends an sse message @@ -83,12 +85,11 @@ func (h *QueryHandler) Handle(c *gin.Context, writer gin.ResponseWriter) error { return nil } // write SSE event to writer - s := fmt.Sprintf("data: %s\n\n", msg) - _, err := writer.Write([]byte(s)) + _, err := fmt.Fprintf(c.Response(), "data: %s\n\n", msg) if err != nil { return err } - writer.Flush() + c.Response().Flush() } } }) @@ -102,8 +103,8 @@ func (h *QueryHandler) Handle(c *gin.Context, writer gin.ResponseWriter) error { } gp.ReplaceTableMiddleware() - c := make(chan string, 100) - r := row.NewOutputChannelMiddleware(json2.NewOutputFormatter(), c) + eventChan := make(chan string, 100) + r := row.NewOutputChannelMiddleware(json2.NewOutputFormatter(), eventChan) gp.AddRowMiddleware(r) eg := errgroup.Group{} @@ -124,8 +125,8 @@ func (h *QueryHandler) Handle(c *gin.Context, writer gin.ResponseWriter) error { select { case <-ctx.Done(): return ctx.Err() - case row := <-c: - _, err := writer.Write([]byte(row)) + case row := <-eventChan: + _, err := c.Response().Write([]byte(row)) if err != nil { return err } @@ -153,17 +154,9 @@ func (h *QueryHandler) Handle(c *gin.Context, writer gin.ResponseWriter) error { func CreateQueryHandler( cmd cmds.Command, middlewares_ ...middlewares.Middleware, -) gin.HandlerFunc { +) echo.HandlerFunc { handler := NewQueryHandler(cmd, WithMiddlewares(middlewares_...), ) - return func(c *gin.Context) { - err := handler.Handle(c, c.Writer) - if err != nil { - log.Error().Err(err).Msg("failed to handle query") - c.JSON(http.StatusInternalServerError, gin.H{ - "error": err.Error(), - }) - } - } + return handler.Handle } diff --git a/pkg/glazed/handlers/text/text-glazed-cmd_test.go b/pkg/glazed/handlers/text/text-glazed-cmd_test.go index 4449b34..96ba147 100644 --- a/pkg/glazed/handlers/text/text-glazed-cmd_test.go +++ b/pkg/glazed/handlers/text/text-glazed-cmd_test.go @@ -3,11 +3,11 @@ package text import ( _ "embed" "encoding/json" - "github.com/gin-gonic/gin" "github.com/go-go-golems/glazed/pkg/cmds" "github.com/go-go-golems/glazed/pkg/cmds/helpers" "github.com/go-go-golems/glazed/pkg/helpers/yaml" "github.com/go-go-golems/parka/pkg/utils" + "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "net/http" @@ -37,19 +37,17 @@ func TestTextHandlerGlazeCommand(t *testing.T) { for _, tt := range tests { t.Run(tt.Name, func(t *testing.T) { - gin.SetMode(gin.TestMode) - c, _ := utils.MockGinContextWithQueryParameters(tt.QueryParameters) + req := utils.NewRequestWithQueryParameters(tt.QueryParameters) // Create ParameterLayers and ParsedLayers from test definitions layers_ := helpers.NewTestParameterLayers(tt.ParameterLayers) cmd, err := utils.NewTestGlazedCommand(cmds.WithLayers(layers_)) require.NoError(t, err) - router := gin.Default() - router.GET("/", CreateQueryHandler(cmd)) - resp := httptest.NewRecorder() - router.ServeHTTP(resp, c.Request) + e := echo.New() + e.GET("/", CreateQueryHandler(cmd)) + e.ServeHTTP(resp, req) // Check for expected error if tt.ExpectedError { diff --git a/pkg/glazed/handlers/text/text-writer-cmd_test.go b/pkg/glazed/handlers/text/text-writer-cmd_test.go index 3c2d2f3..c00f8e6 100644 --- a/pkg/glazed/handlers/text/text-writer-cmd_test.go +++ b/pkg/glazed/handlers/text/text-writer-cmd_test.go @@ -3,11 +3,12 @@ package text import ( _ "embed" "encoding/json" - "github.com/gin-gonic/gin" "github.com/go-go-golems/glazed/pkg/cmds" "github.com/go-go-golems/glazed/pkg/cmds/helpers" "github.com/go-go-golems/glazed/pkg/helpers/yaml" + "github.com/go-go-golems/parka/pkg/server" "github.com/go-go-golems/parka/pkg/utils" + "github.com/labstack/echo/v4" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "net/http" @@ -38,8 +39,7 @@ func TestTextHandler(t *testing.T) { for _, tt := range tests { t.Run(tt.Name, func(t *testing.T) { - gin.SetMode(gin.TestMode) - c, _ := utils.MockGinContextWithQueryParameters(tt.QueryParameters) + req := utils.NewRequestWithQueryParameters(tt.QueryParameters) // Create ParameterLayers and ParsedLayers from test definitions layers_ := helpers.NewTestParameterLayers(tt.ParameterLayers) @@ -47,25 +47,25 @@ func TestTextHandler(t *testing.T) { // TODO(manuel, 2024-01-02) We also need to test with glazed commands cmd := cmds.NewTemplateCommand(tt.Name, tt.Template, cmds.WithLayers(layers_)) - router := gin.Default() - router.GET("/", CreateQueryHandler(cmd)) - - resp := httptest.NewRecorder() - router.ServeHTTP(resp, c.Request) + rec := httptest.NewRecorder() + e := echo.New() + e.HTTPErrorHandler = server.CustomHTTPErrorHandler + e.GET("/", CreateQueryHandler(cmd)) + e.ServeHTTP(rec, req) // Check for expected error if tt.ExpectedError { - assert.Equal(t, http.StatusInternalServerError, resp.Code) + assert.Equal(t, http.StatusInternalServerError, rec.Code) var json_ map[string]interface{} - err := json.Unmarshal(resp.Body.Bytes(), &json_) + err := json.Unmarshal(rec.Body.Bytes(), &json_) require.NoError(t, err) if tt.ErrorString != "" { assert.Equal(t, tt.ErrorString, json_["error"]) } } else { - assert.Equal(t, http.StatusOK, resp.Code) - assert.Equal(t, tt.ExpectedOutput, resp.Body.String()) - assert.Equal(t, "text/plain; charset=utf-8", resp.Header().Get("Content-Type")) + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, tt.ExpectedOutput, rec.Body.String()) + assert.Equal(t, "text/plain; charset=utf-8", rec.Header().Get("Content-Type")) } }) } diff --git a/pkg/glazed/handlers/text/text.go b/pkg/glazed/handlers/text/text.go index 62f3449..2478063 100644 --- a/pkg/glazed/handlers/text/text.go +++ b/pkg/glazed/handlers/text/text.go @@ -1,7 +1,6 @@ package text import ( - "github.com/gin-gonic/gin" "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" @@ -10,10 +9,8 @@ import ( "github.com/go-go-golems/glazed/pkg/settings" "github.com/go-go-golems/parka/pkg/glazed/handlers" parka_middlewares "github.com/go-go-golems/parka/pkg/glazed/middlewares" + "github.com/labstack/echo/v4" "github.com/pkg/errors" - "github.com/rs/zerolog/log" - "io" - "net/http" ) type QueryHandler struct { @@ -43,7 +40,7 @@ func WithMiddlewares(middlewares ...middlewares.Middleware) QueryHandlerOption { var _ handlers.Handler = (*QueryHandler)(nil) -func (h *QueryHandler) Handle(c *gin.Context, writer io.Writer) error { +func (h *QueryHandler) Handle(c echo.Context) error { description := h.cmd.Description() parsedLayers := layers.NewParsedLayers() @@ -57,12 +54,12 @@ func (h *QueryHandler) Handle(c *gin.Context, writer io.Writer) error { if err != nil { return err } - c.Header("Content-Type", "text/plain; charset=utf-8") + c.Response().Header().Set("Content-Type", "text/plain; charset=utf-8") - ctx := c.Request.Context() + ctx := c.Request().Context() switch cmd := h.cmd.(type) { case cmds.WriterCommand: - err := cmd.RunIntoWriter(ctx, parsedLayers, writer) + err := cmd.RunIntoWriter(ctx, parsedLayers, c.Response()) if err != nil { return err } @@ -87,7 +84,7 @@ func (h *QueryHandler) Handle(c *gin.Context, writer io.Writer) error { return err } - gp.AddTableMiddleware(table.NewOutputMiddleware(of, writer)) + gp.AddTableMiddleware(table.NewOutputMiddleware(of, c.Response())) err = cmd.RunIntoGlazeProcessor(ctx, parsedLayers, gp) if err != nil { @@ -114,17 +111,9 @@ func (h *QueryHandler) Handle(c *gin.Context, writer io.Writer) error { func CreateQueryHandler( cmd cmds.Command, middlewares_ ...middlewares.Middleware, -) gin.HandlerFunc { +) echo.HandlerFunc { handler := NewQueryHandler(cmd, WithMiddlewares(middlewares_...), ) - return func(c *gin.Context) { - err := handler.Handle(c, c.Writer) - if err != nil { - log.Error().Err(err).Msg("failed to handle query") - c.JSON(http.StatusInternalServerError, gin.H{ - "error": err.Error(), - }) - } - } + return handler.Handle } diff --git a/pkg/glazed/middlewares/form.go b/pkg/glazed/middlewares/form.go index 161ab8e..2197ab2 100644 --- a/pkg/glazed/middlewares/form.go +++ b/pkg/glazed/middlewares/form.go @@ -2,18 +2,22 @@ package middlewares import ( "fmt" - "github.com/gin-gonic/gin" "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/go-go-golems/glazed/pkg/helpers/cast" + "github.com/labstack/echo/v4" "github.com/pkg/errors" ) -func getListParameterFromForm(c *gin.Context, p *parameters.ParameterDefinition, options ...parameters.ParseStepOption) (*parameters.ParsedParameter, error) { +func getListParameterFromForm(c echo.Context, p *parameters.ParameterDefinition, options ...parameters.ParseStepOption) (*parameters.ParsedParameter, error) { if p.Type.IsList() { // check p.Name[] parameter - values, ok := c.GetPostFormArray(fmt.Sprintf("%s[]", p.Name)) + values_, err := c.FormParams() + if err != nil { + return nil, err + } + values, ok := values_[fmt.Sprintf("%s[]", p.Name)] if ok { pValue, err := p.ParseParameter(values, options...) if err != nil { @@ -28,7 +32,7 @@ func getListParameterFromForm(c *gin.Context, p *parameters.ParameterDefinition, } } -func getFileParameterFromForm(c *gin.Context, p *parameters.ParameterDefinition) (interface{}, error) { +func getFileParameterFromForm(c echo.Context, p *parameters.ParameterDefinition) (interface{}, error) { form, err := c.MultipartForm() if err != nil { return nil, err @@ -105,7 +109,7 @@ func getFileParameterFromForm(c *gin.Context, p *parameters.ParameterDefinition) return v, nil } -func UpdateFromFormQuery(c *gin.Context, options ...parameters.ParseStepOption) middlewares.Middleware { +func UpdateFromFormQuery(c echo.Context, options ...parameters.ParseStepOption) middlewares.Middleware { return func(next middlewares.HandlerFunc) middlewares.HandlerFunc { return func(layers_ *layers.ParameterLayers, parsedLayers *layers.ParsedLayers) error { err := layers_.ForEachE(func(_ string, l layers.ParameterLayer) error { @@ -141,9 +145,9 @@ func UpdateFromFormQuery(c *gin.Context, options ...parameters.ParseStepOption) return nil } - value, ok := c.GetPostForm(p.Name) + value := c.FormValue(p.Name) // TODO(manuel, 2023-02-28) is this enough to check if a file is missing? - if !ok { + if value == "" { if p.Required { return fmt.Errorf("required parameter '%s' is missing", p.Name) } diff --git a/pkg/glazed/middlewares/form_test.go b/pkg/glazed/middlewares/form_test.go index 02e541f..d02cbfc 100644 --- a/pkg/glazed/middlewares/form_test.go +++ b/pkg/glazed/middlewares/form_test.go @@ -3,13 +3,14 @@ package middlewares import ( _ "embed" "github.com/go-go-golems/parka/pkg/utils" + "github.com/labstack/echo/v4" + "net/http/httptest" "testing" "github.com/go-go-golems/glazed/pkg/cmds/helpers" "github.com/go-go-golems/glazed/pkg/cmds/layers" "github.com/go-go-golems/glazed/pkg/helpers/yaml" - "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -35,17 +36,21 @@ func TestUpdateFromFormQuery(t *testing.T) { for _, tt := range tests { t.Run(tt.Name, func(t *testing.T) { - // Create a mock gin.Context with the multipart form data - gin.SetMode(gin.TestMode) - c, _ := utils.MockGinContextWithMultipartForm(tt.Form) + req, err := utils.NewRequestWithMultipartForm(tt.Form) + require.NoError(t, err) // Create ParameterLayers and ParsedLayers from test definitions layers_ := helpers.NewTestParameterLayers(tt.ParameterLayers) parsedLayers := helpers.NewTestParsedLayers(layers_, tt.ParsedLayers...) + resp := httptest.NewRecorder() + e := echo.New() + + c := e.NewContext(req, resp) + // Create the middleware and execute it middleware := UpdateFromFormQuery(c) - err := middleware(func(layers_ *layers.ParameterLayers, parsedLayers *layers.ParsedLayers) error { + err = middleware(func(layers_ *layers.ParameterLayers, parsedLayers *layers.ParsedLayers) error { return nil })(layers_, parsedLayers) diff --git a/pkg/glazed/middlewares/query.go b/pkg/glazed/middlewares/query.go index ba47ebb..b2c1622 100644 --- a/pkg/glazed/middlewares/query.go +++ b/pkg/glazed/middlewares/query.go @@ -2,14 +2,14 @@ package middlewares import ( "fmt" - "github.com/gin-gonic/gin" "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" "strings" ) -func UpdateFromQueryParameters(c *gin.Context, options ...parameters.ParseStepOption) middlewares.Middleware { +func UpdateFromQueryParameters(c echo.Context, options ...parameters.ParseStepOption) middlewares.Middleware { return func(next middlewares.HandlerFunc) middlewares.HandlerFunc { return func(layers_ *layers.ParameterLayers, parsedLayers *layers.ParsedLayers) error { err := next(layers_, parsedLayers) @@ -28,7 +28,7 @@ func UpdateFromQueryParameters(c *gin.Context, options ...parameters.ParseStepOp if p.Type.IsList() { // check p.Name[] parameter - values, ok := c.GetQueryArray(fmt.Sprintf("%s[]", p.Name)) + values, ok := c.QueryParams()[fmt.Sprintf("%s[]", p.Name)] if ok { // TODO(manuel, 2023-12-25) Need to pass in options to ParseParameter pp, err := p.ParseParameter(values, options...) @@ -39,8 +39,8 @@ func UpdateFromQueryParameters(c *gin.Context, options ...parameters.ParseStepOp return nil } } - value, ok := c.GetQuery(p.Name) - if !ok || value == "" { + value := c.QueryParam(p.Name) + if value == "" { if p.Required { return fmt.Errorf("required parameter '%s' is missing", p.Name) } diff --git a/pkg/glazed/middlewares/query_test.go b/pkg/glazed/middlewares/query_test.go index 9b6ba28..4fbedd8 100644 --- a/pkg/glazed/middlewares/query_test.go +++ b/pkg/glazed/middlewares/query_test.go @@ -6,9 +6,10 @@ import ( "github.com/go-go-golems/glazed/pkg/cmds/layers" "github.com/go-go-golems/glazed/pkg/helpers/yaml" "github.com/go-go-golems/parka/pkg/utils" + "github.com/labstack/echo/v4" + "net/http/httptest" "testing" - "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -34,14 +35,17 @@ func TestUpdateFromQueryParameters(t *testing.T) { for _, tt := range tests { t.Run(tt.Name, func(t *testing.T) { - // Create a mock gin.Context with the multipart form data - gin.SetMode(gin.TestMode) - c, _ := utils.MockGinContextWithQueryParameters(tt.QueryParameters) + req := utils.NewRequestWithQueryParameters(tt.QueryParameters) // Create ParameterLayers and ParsedLayers from test definitions layers_ := helpers.NewTestParameterLayers(tt.ParameterLayers) parsedLayers := helpers.NewTestParsedLayers(layers_, tt.ParsedLayers...) + resp := httptest.NewRecorder() + e := echo.New() + e.ServeHTTP(resp, req) + c := e.NewContext(req, resp) + // Create the middleware and execute it middleware := UpdateFromQueryParameters(c) err := middleware(func(layers_ *layers.ParameterLayers, parsedLayers *layers.ParsedLayers) error { diff --git a/pkg/handlers/command-dir/command-dir.go b/pkg/handlers/command-dir/command-dir.go index db77a6a..3f3f375 100644 --- a/pkg/handlers/command-dir/command-dir.go +++ b/pkg/handlers/command-dir/command-dir.go @@ -34,7 +34,6 @@ package command_dir import ( "fmt" - "github.com/gin-gonic/gin" "github.com/go-go-golems/clay/pkg/repositories" "github.com/go-go-golems/glazed/pkg/cmds" "github.com/go-go-golems/parka/pkg/glazed/handlers/datatables" @@ -45,7 +44,10 @@ import ( "github.com/go-go-golems/parka/pkg/handlers/config" "github.com/go-go-golems/parka/pkg/render" parka "github.com/go-go-golems/parka/pkg/server" + "github.com/go-go-golems/parka/pkg/utils" + "github.com/labstack/echo/v4" "github.com/pkg/errors" + "net/http" "os" "path/filepath" "strings" @@ -224,58 +226,55 @@ func (cd *CommandDirHandler) Serve(server *parka.Server, path string) error { path = strings.TrimSuffix(path, "/") middlewares_ := cd.ParameterFilter.ComputeMiddlewares(cd.Stream) - server.Router.GET(path+"/data/*path", func(c *gin.Context) { + server.Router.GET(path+"/data/*path", func(c echo.Context) error { commandPath := c.Param("path") commandPath = strings.TrimPrefix(commandPath, "/") - command, ok := getRepositoryCommand(c, cd.Repository, commandPath) - if !ok { - c.JSON(404, gin.H{"error": fmt.Sprintf("command %s not found", commandPath)}) - return + command, err := getRepositoryCommand(c, cd.Repository, commandPath) + if err != nil { + return err } switch v := command.(type) { case cmds.GlazeCommand: - json.CreateJSONQueryHandler(v, json.WithMiddlewares(middlewares_...))(c) + return json.CreateJSONQueryHandler(v, json.WithMiddlewares(middlewares_...))(c) default: - text.CreateQueryHandler(v)(c) + return text.CreateQueryHandler(v)(c) } }) - server.Router.GET(path+"/text/*path", func(c *gin.Context) { + server.Router.GET(path+"/text/*path", func(c echo.Context) error { commandPath := c.Param("path") commandPath = strings.TrimPrefix(commandPath, "/") - command, ok := getRepositoryCommand(c, cd.Repository, commandPath) - if !ok { - c.JSON(404, gin.H{"error": fmt.Sprintf("command %s not found", commandPath)}) - return + command, err := getRepositoryCommand(c, cd.Repository, commandPath) + if err != nil { + return err } - text.CreateQueryHandler(command, middlewares_...)(c) + return text.CreateQueryHandler(command, middlewares_...)(c) }) - server.Router.GET(path+"/streaming/*path", func(c *gin.Context) { + server.Router.GET(path+"/streaming/*path", func(c echo.Context) error { commandPath := c.Param("path") commandPath = strings.TrimPrefix(commandPath, "/") - command, ok := getRepositoryCommand(c, cd.Repository, commandPath) - if !ok { - c.JSON(404, gin.H{"error": fmt.Sprintf("command %s not found", commandPath)}) - return + command, err := getRepositoryCommand(c, cd.Repository, commandPath) + if err != nil { + return err } - sse.CreateQueryHandler(command, middlewares_...)(c) + return sse.CreateQueryHandler(command, middlewares_...)(c) }) server.Router.GET(path+"/datatables/*path", - func(c *gin.Context) { + func(c echo.Context) error { commandPath := c.Param("path") commandPath = strings.TrimPrefix(commandPath, "/") // Get repository command - command, ok := getRepositoryCommand(c, cd.Repository, commandPath) - if !ok { - c.JSON(404, gin.H{"error": fmt.Sprintf("command %s not found", commandPath)}) - return + command, err := getRepositoryCommand(c, cd.Repository, commandPath) + if err != nil { + return err } + switch v := command.(type) { case cmds.GlazeCommand: options := []datatables.QueryHandlerOption{ @@ -286,36 +285,33 @@ func (cd *CommandDirHandler) Serve(server *parka.Server, path string) error { datatables.WithStreamRows(cd.Stream), } - datatables.CreateDataTablesHandler(v, path, commandPath, options...)(c) + return datatables.CreateDataTablesHandler(v, path, commandPath, options...)(c) default: - c.JSON(500, gin.H{"error": fmt.Sprintf("command %s is not a glazed command", commandPath)}) + return c.JSON(http.StatusInternalServerError, utils.H{"error": fmt.Sprintf("command %s is not a glazed command", commandPath)}) } }) - server.Router.GET(path+"/download/*path", func(c *gin.Context) { + server.Router.GET(path+"/download/*path", func(c echo.Context) error { path_ := c.Param("path") path_ = strings.TrimPrefix(path_, "/") index := strings.LastIndex(path_, "/") if index == -1 { - c.JSON(500, gin.H{"error": "could not find file name"}) - return + return c.JSON(http.StatusInternalServerError, utils.H{"error": "could not find file name"}) } if index >= len(path_)-1 { - c.JSON(500, gin.H{"error": "could not find file name"}) - return + return c.JSON(http.StatusInternalServerError, utils.H{"error": "could not find file name"}) } fileName := path_[index+1:] commandPath := strings.TrimPrefix(path_[:index], "/") - command, ok := getRepositoryCommand(c, cd.Repository, commandPath) - if !ok { - // JSON output and error code already handled by getRepositoryCommand - return + command, err := getRepositoryCommand(c, cd.Repository, commandPath) + if err != nil { + return err } switch v := command.(type) { case cmds.GlazeCommand: - output_file.CreateGlazedFileHandler( + return output_file.CreateGlazedFileHandler( v, fileName, middlewares_..., @@ -325,20 +321,22 @@ func (cd *CommandDirHandler) Serve(server *parka.Server, path string) error { handler := text.NewQueryHandler(command) baseName := filepath.Base(fileName) - c.Writer.Header().Set("Content-Disposition", "attachment; filename="+baseName) + c.Response().Header().Set("Content-Disposition", "attachment; filename="+baseName) - err := handler.Handle(c, c.Writer) + err := handler.Handle(c) if err != nil { - c.JSON(500, gin.H{"error": err.Error()}) - return + return err } + return nil + default: - c.JSON(500, gin.H{"error": fmt.Sprintf("command %s is not a glazed/writer command", commandPath)}) + return c.JSON(http.StatusInternalServerError, utils.H{"error": fmt.Sprintf("command %s is not a glazed/writer command", commandPath)}) } + }) - server.Router.GET(path+"/commands/*path", func(c *gin.Context) { + server.Router.GET(path+"/commands/*path", func(c echo.Context) error { path_ := c.Param("path") path_ = strings.TrimPrefix(path_, "/") path_ = strings.TrimSuffix(path_, "/") @@ -348,8 +346,7 @@ func (cd *CommandDirHandler) Serve(server *parka.Server, path string) error { } renderNode, ok := cd.Repository.GetRenderNode(splitPath) if !ok { - c.JSON(404, gin.H{"error": fmt.Sprintf("command %s not found", path_)}) - return + return errors.Errorf("command %s not found", path_) } templateName := cd.IndexTemplateName if cd.IndexTemplateName == "" { @@ -357,8 +354,7 @@ func (cd *CommandDirHandler) Serve(server *parka.Server, path string) error { } templ, err := cd.TemplateLookup.Lookup(templateName) if err != nil { - c.JSON(500, gin.H{"error": errors.Wrapf(err, "could not load index template").Error()}) - return + return err } var nodes []*repositories.RenderNode @@ -368,39 +364,63 @@ func (cd *CommandDirHandler) Serve(server *parka.Server, path string) error { } else { nodes = append(nodes, renderNode.Children...) } - err = templ.Execute(c.Writer, gin.H{ + err = templ.Execute(c.Response(), utils.H{ "nodes": nodes, "path": path, }) if err != nil { - c.JSON(500, gin.H{"error": errors.Wrapf(err, "could not execute index template").Error()}) - return + return err } + + return nil }) return nil } +type CommandNotFound struct { + CommandPath string +} + +func (e CommandNotFound) Error() string { + return fmt.Sprintf("command %s not found", e.CommandPath) +} + +type AmbiguousCommand struct { + CommandPath string + PotentialCommands []string +} + +func (e AmbiguousCommand) Error() string { + return fmt.Sprintf("command %s is ambiguous, could be one of: %s", e.CommandPath, strings.Join(e.PotentialCommands, ", ")) + +} + // getRepositoryCommand lookups a command in the given repository and return success as bool and the given command, // or sends an error code over HTTP using the gin.Context. -func getRepositoryCommand(c *gin.Context, r *repositories.Repository, commandPath string) ( +func getRepositoryCommand(c echo.Context, r *repositories.Repository, commandPath string) ( cmds.Command, - bool, + error, ) { path := strings.Split(commandPath, "/") commands := r.CollectCommands(path, false) if len(commands) == 0 { - c.JSON(404, gin.H{"error": fmt.Sprintf("command %s not found", commandPath)}) - return nil, false + return nil, CommandNotFound{CommandPath: commandPath} } if len(commands) > 1 { - c.JSON(404, gin.H{"error": fmt.Sprintf("ambiguous command %s", commandPath)}) - return nil, false + err := &AmbiguousCommand{ + CommandPath: commandPath, + } + for _, command := range commands { + description := command.Description() + err.PotentialCommands = append(err.PotentialCommands, strings.Join(description.Parents, " ")+" "+description.Name) + } + return nil, err } // NOTE(manuel, 2023-05-15) Check if this is actually an alias, and populate the defaults from the alias flags // This could potentially be moved to the repository code itself - return commands[0], true + return commands[0], nil } diff --git a/pkg/handlers/command/command.go b/pkg/handlers/command/command.go index f4fd0d8..9dfafbd 100644 --- a/pkg/handlers/command/command.go +++ b/pkg/handlers/command/command.go @@ -1,7 +1,6 @@ package command import ( - "github.com/gin-gonic/gin" "github.com/go-go-golems/glazed/pkg/cmds" "github.com/go-go-golems/glazed/pkg/cmds/alias" "github.com/go-go-golems/glazed/pkg/cmds/loaders" @@ -11,7 +10,10 @@ import ( "github.com/go-go-golems/parka/pkg/handlers/config" "github.com/go-go-golems/parka/pkg/render" parka "github.com/go-go-golems/parka/pkg/server" + "github.com/go-go-golems/parka/pkg/utils" + "github.com/labstack/echo/v4" "github.com/pkg/errors" + "net/http" "os" "strings" ) @@ -228,14 +230,14 @@ func (ch *CommandHandler) Serve(server *parka.Server, path string) error { middlewares_ := ch.ParameterFilter.ComputeMiddlewares(ch.Stream) - server.Router.GET(path+"/data", func(c *gin.Context) { + server.Router.GET(path+"/data", func(c echo.Context) error { options := []json.QueryHandlerOption{ json.WithMiddlewares(middlewares_...), } - json.CreateJSONQueryHandler(ch.Command, options...)(c) + return json.CreateJSONQueryHandler(ch.Command, options...)(c) }) // TODO(manuel, 2024-01-17) This doesn't seem to match what is in command-dir - server.Router.GET(path+"/glazed", func(c *gin.Context) { + server.Router.GET(path+"/glazed", func(c echo.Context) error { options := []datatables.QueryHandlerOption{ datatables.WithMiddlewares(middlewares_...), datatables.WithTemplateLookup(ch.TemplateLookup), @@ -244,23 +246,21 @@ func (ch *CommandHandler) Serve(server *parka.Server, path string) error { datatables.WithStreamRows(ch.Stream), } - datatables.CreateDataTablesHandler(ch.Command, path, "", options...)(c) + return datatables.CreateDataTablesHandler(ch.Command, path, "", options...)(c) }) - server.Router.GET(path+"/download/*path", func(c *gin.Context) { + server.Router.GET(path+"/download/*path", func(c echo.Context) error { path_ := c.Param("path") path_ = strings.TrimPrefix(path_, "/") index := strings.LastIndex(path_, "/") if index == -1 { - c.JSON(500, gin.H{"error": "could not find file name"}) - return + return c.JSON(http.StatusInternalServerError, utils.H{"error": "could not find file name"}) } if index >= len(path_)-1 { - c.JSON(500, gin.H{"error": "could not find file name"}) - return + return c.JSON(http.StatusInternalServerError, utils.H{"error": "could not find file name"}) } fileName := path_[index+1:] - output_file.CreateGlazedFileHandler( + return output_file.CreateGlazedFileHandler( ch.Command, fileName, middlewares_..., diff --git a/pkg/handlers/static-dir/static.go b/pkg/handlers/static-dir/static.go index de9b2d5..75be66d 100644 --- a/pkg/handlers/static-dir/static.go +++ b/pkg/handlers/static-dir/static.go @@ -5,7 +5,6 @@ import ( "github.com/go-go-golems/parka/pkg/server" fs2 "github.com/go-go-golems/parka/pkg/utils/fs" "io/fs" - "net/http" "os" "strings" ) @@ -63,6 +62,6 @@ func (s *StaticDirHandler) Serve(server *server.Server, path string) error { if s.localPath != "" { fs_ = fs2.NewAddPrefixPathFS(s.fs, s.localPath) } - server.Router.StaticFS(path, http.FS(fs_)) + server.Router.StaticFS(path, fs_) return nil } diff --git a/pkg/handlers/static-file/static-file.go b/pkg/handlers/static-file/static-file.go index d5c0c24..7d65e8f 100644 --- a/pkg/handlers/static-file/static-file.go +++ b/pkg/handlers/static-file/static-file.go @@ -3,8 +3,8 @@ package static_file import ( "github.com/go-go-golems/parka/pkg/handlers/config" "github.com/go-go-golems/parka/pkg/server" + fs2 "github.com/go-go-golems/parka/pkg/utils/fs" "io/fs" - "net/http" "os" "strings" ) @@ -56,10 +56,6 @@ func NewStaticFileHandlerFromConfig(shf *config.StaticFile, options ...StaticFil } func (s *StaticFileHandler) Serve(server *server.Server, path string) error { - server.Router.StaticFileFS( - path, - s.localPath, - http.FS(s.fs), - ) + server.Router.StaticFS(path, fs2.NewEmbedFileSystem(s.fs, s.localPath)) return nil } diff --git a/pkg/handlers/template/template.go b/pkg/handlers/template/template.go index ad66d40..84633ff 100644 --- a/pkg/handlers/template/template.go +++ b/pkg/handlers/template/template.go @@ -86,7 +86,7 @@ func NewTemplateHandlerFromConfig( } func (t *TemplateHandler) Serve(server_ *server.Server, path string) error { - server_.Router.Handle("GET", path, t.renderer.HandleWithTrimPrefix("", nil)) + server_.Router.Pre(t.renderer.HandleWithTrimPrefix(path, nil)) return nil } diff --git a/pkg/render/renderer.go b/pkg/render/renderer.go index 5061658..6dd81c0 100644 --- a/pkg/render/renderer.go +++ b/pkg/render/renderer.go @@ -1,7 +1,7 @@ package render import ( - "github.com/gin-gonic/gin" + "github.com/labstack/echo/v4" "github.com/pkg/errors" "github.com/rs/zerolog/log" "html/template" @@ -167,7 +167,7 @@ func (r *Renderer) LookupTemplate(name ...string) (*template.Template, error) { // This could lead to partial writes with an error code of 200 if there is an error rendering the template, // not sure if that's exactly what we want. func (r *Renderer) Render( - c *gin.Context, + c echo.Context, page string, data map[string]interface{}, ) error { @@ -195,10 +195,10 @@ func (r *Renderer) Render( if baseTemplate == nil { // no base template to render the markdown to HTML, so just return the markdown - c.Header("Content-Type", "text/plain") - c.Status(http.StatusOK) + c.Response().Header().Set("Content-Type", "text/plain") + c.Response().WriteHeader(http.StatusOK) - err := t.Execute(c.Writer, data) + err := t.Execute(c.Response(), data) if err != nil { return errors.Wrap(err, "error executing template") } @@ -212,9 +212,9 @@ func (r *Renderer) Render( return errors.Wrap(err, "error rendering markdown") } - c.Status(http.StatusOK) + c.Response().WriteHeader(http.StatusOK) err = baseTemplate.Execute( - c.Writer, + c.Response(), map[string]interface{}{ "markdown": template.HTML(markdown), }) @@ -230,9 +230,9 @@ func (r *Renderer) Render( return &NoPageFoundError{Page: page} } - c.Status(http.StatusOK) + c.Response().WriteHeader(http.StatusOK) - err := t.Execute(c.Writer, data) + err := t.Execute(c.Response(), data) if err != nil { return errors.Wrap(err, "error executing template") } @@ -241,56 +241,63 @@ func (r *Renderer) Render( } func (r *Renderer) HandleWithTemplate( + path string, templateName string, data map[string]interface{}, -) gin.HandlerFunc { - return func(c *gin.Context) { - if c.Writer.Written() { - c.Next() - return - } - err := r.Render(c, templateName, data) - if err != nil { - if _, ok := err.(*NoPageFoundError); ok { - c.Next() - return +) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if c.Response().Committed { + return next(c) } - _ = c.AbortWithError(http.StatusInternalServerError, err) - return + + if c.Request().URL.Path == path { + err := r.Render(c, templateName, data) + if err != nil { + if _, ok := err.(*NoPageFoundError); ok { + return next(c) + } + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + return nil + } + + return next(c) } - c.Status(http.StatusOK) } } -func (r *Renderer) Handle(data map[string]interface{}) gin.HandlerFunc { - return r.HandleWithTrimPrefix("", data) -} - -func (r *Renderer) HandleWithTrimPrefix(prefix string, data map[string]interface{}) gin.HandlerFunc { +func (r *Renderer) HandleWithTrimPrefix(prefix string, data map[string]interface{}) echo.MiddlewareFunc { prefix = strings.TrimPrefix(prefix, "/") - return func(c *gin.Context) { - if c.Writer.Written() { - c.Next() - return - } - // check if context is already finished - rawPath := c.Request.URL.Path - if len(rawPath) > 0 && rawPath[0] == '/' { - trimmedPath := rawPath[1:] - trimmedPath = strings.TrimPrefix(trimmedPath, prefix) - if trimmedPath == "" || strings.HasSuffix(trimmedPath, "/") { - trimmedPath += "index" + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + if c.Response().Committed { + return next(c) } - err := r.Render(c, trimmedPath, data) - if err != nil { - if _, ok := err.(*NoPageFoundError); ok { - c.Next() - return + rawPath := c.Request().URL.Path + + if len(rawPath) > 0 && rawPath[0] == '/' { + trimmedPath := rawPath[1:] + trimmedPath = strings.TrimPrefix(trimmedPath, prefix) + if trimmedPath == "" || strings.HasSuffix(trimmedPath, "/") { + trimmedPath += "index" } - _ = c.AbortWithError(http.StatusInternalServerError, err) - return + + err := r.Render(c, trimmedPath, data) + if err != nil { + if _, ok := err.(*NoPageFoundError); ok { + return next(c) + } + return echo.NewHTTPError(http.StatusInternalServerError, err.Error()) + } + + return nil } + + // TODO(manuel, 2024-05-07) I'm not entirely sure this is the correct way of doing things + // this is if the rawPath is empty? I'm not sure I understand the logic here + return c.NoContent(http.StatusOK) } } } diff --git a/pkg/server/server.go b/pkg/server/server.go index ca48918..f27af01 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -4,10 +4,10 @@ import ( "context" "embed" "fmt" - "github.com/gin-gonic/contrib/gzip" - "github.com/gin-gonic/gin" "github.com/go-go-golems/parka/pkg/render" utils_fs "github.com/go-go-golems/parka/pkg/utils/fs" + "github.com/labstack/echo/v4" + "github.com/labstack/echo/v4/middleware" "golang.org/x/sync/errgroup" "io/fs" "net/http" @@ -29,7 +29,7 @@ var distFS embed.FS // Router is the gin.Engine that is used to serve the content, and it is exposed so that you // can use it as just a gin.Engine if you want to. type Server struct { - Router *gin.Engine + Router *echo.Echo // TODO(manuel, 2023-06-05) This should become a standard Static handler to be added to the Routes StaticPaths []utils_fs.StaticPath @@ -143,7 +143,7 @@ func WithDefaultParkaRenderer(options ...render.RendererOption) ServerOption { return WithDefaultRenderer(renderer) } -func GetParkaStaticHttpFS() http.FileSystem { +func GetParkaStaticHttpFS() fs.FS { return utils_fs.NewEmbedFileSystem(distFS, "web/dist") } @@ -159,7 +159,7 @@ func WithDefaultParkaStaticPaths() ServerOption { func WithGzip() ServerOption { return func(s *Server) error { - s.Router.Use(gzip.Gzip(gzip.DefaultCompression)) + s.Router.Use(middleware.Gzip()) return nil } } @@ -169,7 +169,7 @@ func WithGzip() ServerOption { // These files provide tailwind support for Markdown rendering and a standard index and base page template. // NOTE(manuel, 2023-04-16) This is definitely ripe to be removed. func NewServer(options ...ServerOption) (*Server, error) { - router := gin.Default() + router := echo.New() s := &Server{ Router: router, @@ -201,9 +201,7 @@ func (s *Server) RegisterDebugRoutes() { for route, handler := range handlers_ { route_ := route handler_ := handler - s.Router.GET(route_, func(c *gin.Context) { - handler_(c.Writer, c.Request) - }) + s.Router.GET(route_, echo.WrapHandler(handler_)) } } @@ -215,8 +213,8 @@ func (s *Server) Run(ctx context.Context) error { // match all remaining paths to the templates if s.DefaultRenderer != nil { - s.Router.GET("/", s.DefaultRenderer.HandleWithTemplate("index", nil)) - s.Router.Use(s.DefaultRenderer.Handle(nil)) + s.Router.Pre(s.DefaultRenderer.HandleWithTemplate("/", "index", nil)) + s.Router.Pre(s.DefaultRenderer.HandleWithTrimPrefix("", nil)) } addr := fmt.Sprintf("%s:%d", s.Address, s.Port) @@ -239,3 +237,22 @@ func (s *Server) Run(ctx context.Context) error { return eg.Wait() } + +func CustomHTTPErrorHandler(err error, c echo.Context) { + code := http.StatusInternalServerError + if he, ok := err.(*echo.HTTPError); ok { + code = he.Code + } + + // Create a custom error response + errorResponse := map[string]interface{}{ + "error": err.Error(), + } + + // Send the custom error response + if !c.Response().Committed { + _ = c.JSON(code, errorResponse) + } + + c.Logger().Error(err) +} diff --git a/pkg/utils/fs/fs.go b/pkg/utils/fs/fs.go index 9c48ea7..2a72b32 100644 --- a/pkg/utils/fs/fs.go +++ b/pkg/utils/fs/fs.go @@ -2,7 +2,6 @@ package fs import ( "io/fs" - "net/http" "path/filepath" "strings" ) @@ -10,10 +9,12 @@ import ( // EmbedFileSystem is a helper to make an embed FS work as a http.FS, // which allows us to serve embed.FS using gin's `Static` middleware. type EmbedFileSystem struct { - f http.FileSystem + f fs.FS stripPrefix string } +var _ fs.FS = &EmbedFileSystem{} + // NewEmbedFileSystem will create a new EmbedFileSystem that will serve the given embed.FS // under the given URL path. stripPrefix will be added to the beginning of all paths when // looking up files in the embed.FS. @@ -22,14 +23,14 @@ func NewEmbedFileSystem(f fs.FS, stripPrefix string) *EmbedFileSystem { stripPrefix += "/" } return &EmbedFileSystem{ - f: http.FS(f), + f: f, stripPrefix: stripPrefix, } } // Open will open the file with the given name from the embed.FS. The name will be prefixed // with the stripPrefix that was given when creating the EmbedFileSystem. -func (e *EmbedFileSystem) Open(name string) (http.File, error) { +func (e *EmbedFileSystem) Open(name string) (fs.File, error) { name = strings.TrimPrefix(name, "/") return e.f.Open(e.stripPrefix + name) } @@ -49,7 +50,7 @@ func (e *EmbedFileSystem) Exists(prefix string, path string) bool { if err != nil { return false } - defer func(f http.File) { + defer func(f fs.File) { _ = f.Close() }(f) return true @@ -57,12 +58,12 @@ func (e *EmbedFileSystem) Exists(prefix string, path string) bool { // StaticPath allows you to serve static files from a http.FileSystem under a given URL path UrlPath. type StaticPath struct { - FS http.FileSystem + FS fs.FS UrlPath string } // NewStaticPath creates a new StaticPath that will serve files from the given http.FileSystem. -func NewStaticPath(fs http.FileSystem, urlPath string) StaticPath { +func NewStaticPath(fs fs.FS, urlPath string) StaticPath { return StaticPath{ FS: fs, UrlPath: urlPath, diff --git a/pkg/utils/http.go b/pkg/utils/http.go new file mode 100644 index 0000000..cb054de --- /dev/null +++ b/pkg/utils/http.go @@ -0,0 +1,3 @@ +package utils + +type H map[string]interface{} diff --git a/pkg/utils/test-helpers.go b/pkg/utils/test-helpers.go index d4e01b7..3c3ff14 100644 --- a/pkg/utils/test-helpers.go +++ b/pkg/utils/test-helpers.go @@ -6,8 +6,6 @@ import ( "net/http" "net/http/httptest" "net/url" - - "github.com/gin-gonic/gin" ) // QueryParameter holds a key-value pair for a query parameter. @@ -34,11 +32,11 @@ type File struct { Content string `yaml:"content"` } -// MockGinContextWithQueryParameters creates a mock *gin.Context with the +// NewRequestWithQueryParameters creates a mock echo.Context with the // provided query parameters set. It is intended for use in tests. // // It returns a GET request for the path "/" -func MockGinContextWithQueryParameters(parameters []QueryParameter) (*gin.Context, error) { +func NewRequestWithQueryParameters(parameters []QueryParameter) *http.Request { // Create a new HTTP request req := httptest.NewRequest(http.MethodGet, "/", nil) @@ -53,20 +51,16 @@ func MockGinContextWithQueryParameters(parameters []QueryParameter) (*gin.Contex // Set the RawQuery field of the request URL req.URL.RawQuery = values.Encode() - // Create a new gin context with the request - c, _ := gin.CreateTestContext(httptest.NewRecorder()) - c.Request = req - - return c, nil + return req } -// MockGinContextWithMultipartForm creates a mock gin.Context populated +// NewRequestWithMultipartForm creates a mock gin.Context populated // with the provided multipart form data. It constructs the multipart // form, attaches it to a mock request, and creates a gin.Context using // that request. Returns the created context and any error. // // The request is a POST request to "/" with the provided form data. -func MockGinContextWithMultipartForm(form MultipartForm) (*gin.Context, error) { +func NewRequestWithMultipartForm(form MultipartForm) (*http.Request, error) { body := &bytes.Buffer{} writer := multipart.NewWriter(body) @@ -99,8 +93,5 @@ func MockGinContextWithMultipartForm(form MultipartForm) (*gin.Context, error) { req := httptest.NewRequest("POST", "/", body) req.Header.Set("Content-Type", writer.FormDataContentType()) - - c, _ := gin.CreateTestContext(httptest.NewRecorder()) - c.Request = req - return c, nil + return req, nil }