From 5aa3ba2460ceb1a8cd757d6525f2f4dfeec9de97 Mon Sep 17 00:00:00 2001 From: Sven Walter Date: Fri, 5 Jul 2024 11:40:44 +0200 Subject: [PATCH] add jet template engine --- examples/full/go.mod | 2 + examples/full/go.sum | 4 ++ go.mod | 2 + go.sum | 4 ++ migrations.md | 61 ++++++++++++++++++ pkg/webutil/jetview.go | 130 +++++++++++++++++++++++++++++++++++++++ pkg/webutil/view.go | 5 ++ pkg/webutil/viewfuncs.go | 80 ++++++++++++++++++++++++ 8 files changed, 288 insertions(+) create mode 100644 pkg/webutil/jetview.go create mode 100644 pkg/webutil/viewfuncs.go diff --git a/examples/full/go.mod b/examples/full/go.mod index fd205a3..c930a48 100644 --- a/examples/full/go.mod +++ b/examples/full/go.mod @@ -19,6 +19,8 @@ require ( dario.cat/mergo v1.0.0 // indirect github.com/AlekSi/pointer v1.2.0 // indirect github.com/BurntSushi/toml v1.2.1 // indirect + github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect + github.com/CloudyKit/jet/v6 v6.2.0 // indirect github.com/DataDog/appsec-internal-go v1.6.0 // indirect github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0 // indirect github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.48.1 // indirect diff --git a/examples/full/go.sum b/examples/full/go.sum index 2caa0fb..06d44b4 100644 --- a/examples/full/go.sum +++ b/examples/full/go.sum @@ -4,6 +4,10 @@ github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= +github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= +github.com/CloudyKit/jet/v6 v6.2.0 h1:EpcZ6SR9n28BUGtNJSvlBqf90IpjeFr36Tizxhn/oME= +github.com/CloudyKit/jet/v6 v6.2.0/go.mod h1:d3ypHeIRNo2+XyqnGA8s+aphtcVpjP5hPwP/Lzo7Ro4= github.com/DataDog/appsec-internal-go v1.6.0 h1:QHvPOv/O0s2fSI/BraZJNpRDAtdlrRm5APJFZNBxjAw= github.com/DataDog/appsec-internal-go v1.6.0/go.mod h1:pEp8gjfNLtEOmz+iZqC8bXhu0h4k7NUsW/qiQb34k1U= github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0 h1:bUMSNsw1iofWiju9yc1f+kBd33E3hMJtq9GuU602Iy8= diff --git a/go.mod b/go.mod index 270c944..77d64fc 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/rebuy-de/rebuy-go-sdk/v8 go 1.22.0 require ( + github.com/CloudyKit/jet/v6 v6.2.0 github.com/afiskon/promtail-client v0.0.0-20190305142237-506f3f921e9c github.com/alicebob/miniredis/v2 v2.31.1 github.com/aws/aws-sdk-go-v2 v1.25.0 @@ -48,6 +49,7 @@ require ( dario.cat/mergo v1.0.0 // indirect github.com/AlekSi/pointer v1.2.0 // indirect github.com/BurntSushi/toml v1.2.1 // indirect + github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 // indirect github.com/DataDog/appsec-internal-go v1.6.0 // indirect github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0 // indirect github.com/DataDog/datadog-agent/pkg/remoteconfig/state v0.48.1 // indirect diff --git a/go.sum b/go.sum index a0c4a6b..094066a 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,10 @@ github.com/AlekSi/pointer v1.2.0 h1:glcy/gc4h8HnG2Z3ZECSzZ1IX1x2JxRVuDzaJwQE0+w= github.com/AlekSi/pointer v1.2.0/go.mod h1:gZGfd3dpW4vEc/UlyfKKi1roIqcCgwOIvb0tSNSBle0= github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53 h1:sR+/8Yb4slttB4vD+b9btVEnWgL3Q00OBTzVT8B9C0c= +github.com/CloudyKit/fastprinter v0.0.0-20200109182630-33d98a066a53/go.mod h1:+3IMCy2vIlbG1XG/0ggNQv0SvxCAIpPM5b1nCz56Xno= +github.com/CloudyKit/jet/v6 v6.2.0 h1:EpcZ6SR9n28BUGtNJSvlBqf90IpjeFr36Tizxhn/oME= +github.com/CloudyKit/jet/v6 v6.2.0/go.mod h1:d3ypHeIRNo2+XyqnGA8s+aphtcVpjP5hPwP/Lzo7Ro4= github.com/DataDog/appsec-internal-go v1.6.0 h1:QHvPOv/O0s2fSI/BraZJNpRDAtdlrRm5APJFZNBxjAw= github.com/DataDog/appsec-internal-go v1.6.0/go.mod h1:pEp8gjfNLtEOmz+iZqC8bXhu0h4k7NUsW/qiQb34k1U= github.com/DataDog/datadog-agent/pkg/obfuscate v0.48.0 h1:bUMSNsw1iofWiju9yc1f+kBd33E3hMJtq9GuU602Iy8= diff --git a/migrations.md b/migrations.md index 07032c2..d2d2e43 100644 --- a/migrations.md +++ b/migrations.md @@ -13,3 +13,64 @@ strongly recommended to align projects using this SDK. * Use the pattern `return fmt.Errorf("something happenend with %#v: %w", someID, err)` * The stack trace feature gets lost. Therefore it is suggested to properly add error messages each time handling errors. + + +## 2024-07-05 Switch to Jet Templates + +### Reasoning + +The builtin template engine has some shortcommings like being able to pass multiple and optional parameters to a template fragment. For example this block definition is not possible to builtin template engine: + +``` +{{ block entityLink(label, id, tab, extraClasses) }} + {{ e := findEntity(.) }} + + + + + {{ if label }} + {{ label }} + {{ else if id }} + {{ id }} + {{ else }} + {{ e.Plural }} + {{ end }} + +{{ end }} +``` + +### Hints + +* The handler function signatures changed from `func(*webutil.View, *http.Request) webutil.Response` to `func(*http.Request) webutil.Response`. +* Wrap function changes from `webutil.ViewHandler.Wrap` to `webutil.Wrap`. +* Since the `View` interface is not part of the function signature, it needs to be added to the struct where the handler function is attached to. +* Non-HTML responses are now created with functions directly from the `webutil` package. For example `return webutilext.ViewError(http.StatusInternalServerError, err)` instead of `return v.Error(http.StatusBadRequest, fmt.Errorf(`unknown value for "until"`))`. where `v` was passed to the handler. + +### Examples + +Jet Set for Development: + +```go +jet.NewSet( + jet.NewOSFileSystemLoader("./pkg/app/web/templates"), + jet.InDevelopmentMode(), +) +``` + +Jet Set for Production: + +```go +jet.NewSet( + webutil.JetFSLoader{FS: templateFS}, +) +``` + +Jet Viewer based on Jet Set: + +```go +webutilext.NewJetViewer( + js, + webutil.JetVarOption("clusterName", cn), + webutil.JetVarOption("assetPath", "/assets/"+prefix), +) +``` diff --git a/pkg/webutil/jetview.go b/pkg/webutil/jetview.go new file mode 100644 index 0000000..b787ad2 --- /dev/null +++ b/pkg/webutil/jetview.go @@ -0,0 +1,130 @@ +package webutil + +import ( + "fmt" + "io" + "io/fs" + "net/http" + "reflect" + "strings" + + "github.com/CloudyKit/jet/v6" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" +) + +type JetViewer struct { + views *jet.Set +} + +type JetOption func(*jet.Set) + +func JetFunctionOption(name string, fn any) JetOption { + return func(set *jet.Set) { + set.AddGlobal(name, fn) + } +} + +func JetFunctionMapOption(funcs map[string]any) JetOption { + return func(set *jet.Set) { + for name, fn := range funcs { + set.AddGlobal(name, fn) + } + } +} + +func JetVarOption(key string, value any) JetOption { + return func(set *jet.Set) { + set.AddGlobal(key, value) + } +} + +func NewJetViewer(js *jet.Set, options ...JetOption) *JetViewer { + jv := &JetViewer{ + views: js, + } + + jv.views.AddGlobal("contains", strings.Contains) + + jv.views.AddGlobalFunc("deref", func(a jet.Arguments) reflect.Value { + a.RequireNumOfArguments("pointer", 1, 1) + v := a.Get(0) + if v.Kind() == reflect.Ptr { + return v.Elem() + } + + return v + }) + + jv.apply(options...) + + return jv +} + +func (j *JetViewer) apply(options ...JetOption) { + for _, option := range options { + option(j.views) + } +} + +type JetViewerHTMLOption func(*jet.VarMap) + +func WithVar(name string, value any) JetViewerHTMLOption { + return func(vars *jet.VarMap) { + vars.Set(name, value) + } +} + +func WithVarf(name string, s string, a ...any) JetViewerHTMLOption { + return WithVar(name, fmt.Sprintf(s, a...)) +} + +func (j *JetViewer) HTML(status int, filename string, data any, opts ...JetViewerHTMLOption) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + span, ctx := tracer.StartSpanFromContext( + r.Context(), "render", + tracer.Tag(ext.ResourceName, filename), + tracer.Tag(ext.SpanKind, ext.SpanKindInternal), + ) + r = r.WithContext(ctx) + defer span.Finish() + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + view, err := j.views.GetTemplate(filename) + if err != nil { + ViewError(http.StatusInternalServerError, err)(w, r) + return + } + + vars := make(jet.VarMap) + vars.Set("currentURLPath", r.URL.Path) + + for _, o := range opts { + o(&vars) + } + + err = view.Execute(w, vars, data) + if err != nil { + ViewError(http.StatusInternalServerError, err)(w, r) + return + } + } +} + +type JetFSLoader struct { + fs.FS +} + +func (l JetFSLoader) Exists(path string) bool { + f, err := l.Open(path) + if err != nil { + return false + } + f.Close() + return true +} + +func (l JetFSLoader) Open(path string) (io.ReadCloser, error) { + path = strings.TrimLeft(path, "/") + return l.FS.Open(path) +} diff --git a/pkg/webutil/view.go b/pkg/webutil/view.go index 3865f40..b1598d3 100644 --- a/pkg/webutil/view.go +++ b/pkg/webutil/view.go @@ -12,11 +12,13 @@ import ( "github.com/sirupsen/logrus" ) +// Deprecated: Use webutil.JetViewer type ViewHandler struct { FS fs.FS FuncMaps []TemplateFuncMap } +// Deprecated: Use webutil.JetViewer func NewViewHandler(fs fs.FS, fms ...TemplateFuncMap) *ViewHandler { v := &ViewHandler{ FS: fs, @@ -26,6 +28,7 @@ func NewViewHandler(fs fs.FS, fms ...TemplateFuncMap) *ViewHandler { return v } +// Deprecated: Use webutil.JetViewer type ResponseHandlerFunc func(*View, *http.Request) Response func (h *ViewHandler) Wrap(fn ResponseHandlerFunc) http.HandlerFunc { @@ -52,6 +55,7 @@ func (h *ViewHandler) Render(filename string, r *http.Request, d interface{}) (* return buf, errors.Wrap(err, "executing template failed") } +// Deprecated type TemplateFuncMap func(*http.Request) template.FuncMap func SimpleTemplateFuncMap(name string, fn interface{}) TemplateFuncMap { @@ -62,6 +66,7 @@ func SimpleTemplateFuncMap(name string, fn interface{}) TemplateFuncMap { } } +// Deprecated func SimpleTemplateFuncMaps(fm template.FuncMap) TemplateFuncMap { return func(_ *http.Request) template.FuncMap { return fm 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...) + } +}