diff --git a/migrations.md b/migrations.md index 9abea2f..da7bd52 100644 --- a/migrations.md +++ b/migrations.md @@ -3,7 +3,24 @@ This file contains a list of tasks that are either required or at least strongly recommended to align projects using this SDK. -## 2024-07-19 Replace cdnmirror with yarn +## M0003 2024-08-16 Change viewer interfaces of webutil + +### Reasoning + +The previous interface is a bit awkward to use with dependency injection together with splitting the HTTP handlers into +multiple structs. Additionally there were cases where we wanted to use the `webutil.Response` type for convenience, but +did not actually need any HTML rendering. + +Therefore the interfaces are changed this way: +* The new `webuitil.WrapView` function replaces the old `webuitl.ViewHandler.Wrap` function and does not require any + template definitions. +* All `webutil.Response` functions, that do not need templates, are pure functions now (ie not attached to a type). +* The handler interface gets reduced to `func(*http.Request) Response`, so it does not contain the view parameter + anymore. When using HTML, it is required to attach the `webutil.GoTemplateViewer` to the struct that implements the + handler. + + +## M0002 2024-07-19 Replace cdnmirror with yarn ### Reasoning @@ -231,7 +248,7 @@ The URL follows this pattern: `https://unpkg.com/{package}@{version}/{import}`, There is not guarantee that this work, tho. -## 2024-06-14 Remove all uses of `github.com/pkg/errors` +## M0001 2024-06-14 Remove all uses of `github.com/pkg/errors` ### Reasoning diff --git a/pkg/webutil/view.go b/pkg/webutil/view.go index 3865f40..b20e464 100644 --- a/pkg/webutil/view.go +++ b/pkg/webutil/view.go @@ -12,11 +12,13 @@ import ( "github.com/sirupsen/logrus" ) +// Deprecated: M0003 Use GoTemplateViewer and View* functions instead. type ViewHandler struct { FS fs.FS FuncMaps []TemplateFuncMap } +// Deprecated: M0003 Use GoTemplateViewer and View* functions instead. func NewViewHandler(fs fs.FS, fms ...TemplateFuncMap) *ViewHandler { v := &ViewHandler{ FS: fs, @@ -28,12 +30,14 @@ func NewViewHandler(fs fs.FS, fms ...TemplateFuncMap) *ViewHandler { type ResponseHandlerFunc func(*View, *http.Request) Response +// Deprecated: M0003 Use GoTemplateViewer and View* functions instead. func (h *ViewHandler) Wrap(fn ResponseHandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { fn(&View{handler: h}, r)(w, r) } } +// Deprecated: M0003 Use GoTemplateViewer and View* functions instead. func (h *ViewHandler) Render(filename string, r *http.Request, d interface{}) (*bytes.Buffer, error) { t := template.New(filename) @@ -70,10 +74,12 @@ func SimpleTemplateFuncMaps(fm template.FuncMap) TemplateFuncMap { type Response = http.HandlerFunc +// Deprecated: M0003 Use GoTemplateViewer and View* functions instead. type View struct { handler *ViewHandler } +// Deprecated: M0003 Use GoTemplateViewer and View* functions instead. func (v *View) Error(status int, err error) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { l := logrus. @@ -91,10 +97,12 @@ func (v *View) Error(status int, err error) http.HandlerFunc { } } +// Deprecated: M0003 Use GoTemplateViewer and View* functions instead. func (v *View) Errorf(status int, text string, a ...interface{}) http.HandlerFunc { return v.Error(status, fmt.Errorf(text, a...)) } +// Deprecated: M0003 Use GoTemplateViewer and View* functions instead. func (v *View) Redirect(status int, location string, args ...interface{}) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { url := fmt.Sprintf(location, args...) @@ -102,6 +110,7 @@ func (v *View) Redirect(status int, location string, args ...interface{}) http.H } } +// Deprecated: M0003 Use GoTemplateViewer and View* functions instead. func (v *View) JSON(status int, data any) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { buf := new(bytes.Buffer) @@ -120,6 +129,7 @@ func (v *View) JSON(status int, data any) http.HandlerFunc { } } +// Deprecated: M0003 Use GoTemplateViewer and View* functions instead. func (v *View) HTML(status int, filename string, data any) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { buf, err := v.handler.Render(filename, r, data) diff --git a/pkg/webutil/viewfuncs.go b/pkg/webutil/viewfuncs.go new file mode 100644 index 0000000..d632330 --- /dev/null +++ b/pkg/webutil/viewfuncs.go @@ -0,0 +1,80 @@ +package webutil + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/http" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" +) + +func WrapView(fn func(*http.Request) Response) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + fn(r)(w, r) + } +} + +func ViewError(status int, err error) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + l := logrus. + WithField("stacktrace", fmt.Sprintf("%+v", err)). + WithError(errors.WithStack(err)) + + if errors.Is(err, context.Canceled) { + l.Debugf("request cancelled: %s", err) + + // The code is copied from nginx, where it means that the client + // closed the connection. It is necessary to alter the status code, + // because DataDog will report errors, if the code is >=500, + // regardless of the connection state. + status = 499 + } else if status >= 500 { + l.Errorf("request failed: %s", err) + } else { + l.Warnf("request failed: %s", err) + } + + w.WriteHeader(status) + fmt.Fprint(w, err.Error()) + } +} + +func ViewErrorf(status int, text string, a ...interface{}) http.HandlerFunc { + return ViewError(status, fmt.Errorf(text, a...)) +} + +func ViewRedirect(status int, location string, args ...interface{}) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + url := fmt.Sprintf(location, args...) + http.Redirect(w, r, url, status) + } +} + +func ViewJSON(status int, data any) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + buf := new(bytes.Buffer) + enc := json.NewEncoder(buf) + enc.SetIndent("", " ") + + err := enc.Encode(data) + if err != nil { + ViewError(http.StatusInternalServerError, err)(w, r) + return + } + + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.WriteHeader(status) + buf.WriteTo(w) + } +} + +func ViewInlineHTML(status int, data string, a ...any) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.WriteHeader(status) + fmt.Fprintf(w, data, a...) + } +} diff --git a/pkg/webutil/viewgo.go b/pkg/webutil/viewgo.go new file mode 100644 index 0000000..7d4e993 --- /dev/null +++ b/pkg/webutil/viewgo.go @@ -0,0 +1,70 @@ +package webutil + +import ( + "bytes" + "html/template" + "io/fs" + "net/http" + + "github.com/sirupsen/logrus" +) + +type GoTemplateViewer struct { + fs fs.FS + funcMaps []TemplateFuncMap +} + +func NewGoTemplateViewer(fs fs.FS, fms ...TemplateFuncMap) *GoTemplateViewer { + return &GoTemplateViewer{ + fs: fs, + funcMaps: fms, + } +} + +func (v *GoTemplateViewer) prepare(filename string, r *http.Request) (*template.Template, error) { + t := template.New(filename) + + for _, fm := range v.funcMaps { + t = t.Funcs(fm(r)) + } + + return t.ParseFS(v.fs, "*") +} + +func (v *GoTemplateViewer) HTML(status int, filename string, data any) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + + t, err := v.prepare(filename, r) + if err != nil { + ViewError(http.StatusInternalServerError, err)(w, r) + return + } + + w.WriteHeader(status) + + err = t.Execute(w, data) + if err != nil { + // It is possible that we already sent the header, but we try again anyways. + w.WriteHeader(http.StatusInternalServerError) + + // We do not send the actual error to the client, since we don't know what we already sent. + logrus.Errorf("failed to render: %v", err.Error()) + } + } +} + +func (v *GoTemplateViewer) Render(filename string, r *http.Request, data any) (*bytes.Buffer, error) { + t, err := v.prepare(filename, r) + if err != nil { + return nil, err + } + + buf := new(bytes.Buffer) + err = t.Execute(buf, data) + if err != nil { + return nil, err + } + + return buf, nil +}