Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add jet template engine #207

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions examples/full/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions examples/full/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -49,6 +50,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
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
61 changes: 61 additions & 0 deletions migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,3 +241,64 @@ There is not guarantee that this work, tho.

* 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(.) }}
<a class="ry-nowrap ry-underline {{ extraClasses }}" href="/{{.}}{{if id}}/{{id}}{{end}}{{ if tab }}/{{tab}}{{end}}">
<span class="icon">
<i class="fa-solid {{ e.IconClasses }}"></i>
</span>
{{ if label }}
<span>{{ label }}</span>
{{ else if id }}
<span>{{ id }}</span>
{{ else }}
<span>{{ e.Plural }}</span>
{{ end }}
</a>
{{ 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.WrapView`.
* 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),
)
```
13 changes: 13 additions & 0 deletions pkg/webutil/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,8 @@ func DevAuthMiddleware(roles ...string) func(http.Handler) http.Handler {
// {{ else }}
// <a class="nav-link" href="/auth/login">Login</span></a>
// {{ end }}
//
// Deprecated: use AuthJetOptions
func AuthTemplateFunctions(r *http.Request) template.FuncMap {
authenticated := true
info := AuthInfoFromRequest(r)
Expand All @@ -329,6 +331,17 @@ func AuthTemplateFunctions(r *http.Request) template.FuncMap {
}
}

func AuthJetOptions() JetOption {
return JetOptions(
WithRequestVar("AuthIsAuthenticated", func(r *http.Request) bool {
return AuthInfoFromRequest(r) != nil
}),
WithRequestVar("AuthInfo", func(r *http.Request) *AuthInfo {
return AuthInfoFromRequest(r)
}),
)
}

// AuthInfoFromContext extracts the AuthInfo from the given context. The
// AuthInfo is injected into the request via the AuthMiddleware. Therefore it
// is required to use this middleware to be able to get the AuthInfo.
Expand Down
4 changes: 4 additions & 0 deletions pkg/webutil/view.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ import (
"github.com/sirupsen/logrus"
)

// Deprecated: Use webutil.JetViewer or webutil.GoTemplateViewer
type ViewHandler struct {
FS fs.FS
FuncMaps []TemplateFuncMap
}

// Deprecated: Use webutil.JetViewer or webutil.GoTemplateViewer
func NewViewHandler(fs fs.FS, fms ...TemplateFuncMap) *ViewHandler {
v := &ViewHandler{
FS: fs,
Expand All @@ -26,6 +28,7 @@ func NewViewHandler(fs fs.FS, fms ...TemplateFuncMap) *ViewHandler {
return v
}

// Deprecated: Use webutil.JetViewer or webutil.GoTemplateViewer
type ResponseHandlerFunc func(*View, *http.Request) Response

func (h *ViewHandler) Wrap(fn ResponseHandlerFunc) http.HandlerFunc {
Expand Down Expand Up @@ -62,6 +65,7 @@ func SimpleTemplateFuncMap(name string, fn interface{}) TemplateFuncMap {
}
}

// Deprecated
func SimpleTemplateFuncMaps(fm template.FuncMap) TemplateFuncMap {
return func(_ *http.Request) template.FuncMap {
return fm
Expand Down
80 changes: 80 additions & 0 deletions pkg/webutil/viewfuncs.go
Original file line number Diff line number Diff line change
@@ -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...)
}
}
43 changes: 43 additions & 0 deletions pkg/webutil/viewgo.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package webutil

import (
"bytes"
"html/template"
"io/fs"
"net/http"

"github.com/pkg/errors"
)

type GoTemplateViewer struct {
fs fs.FS
funcMaps []TemplateFuncMap
}

func NewGoTemplateViewer(fs fs.FS, fms ...TemplateFuncMap) *GoTemplateViewer {
return &GoTemplateViewer{
FS: fs,

Check failure on line 19 in pkg/webutil/viewgo.go

View workflow job for this annotation

GitHub Actions / CI Build

unknown field FS in struct literal of type GoTemplateViewer
FuncMaps: fms,

Check failure on line 20 in pkg/webutil/viewgo.go

View workflow job for this annotation

GitHub Actions / CI Build

unknown field FuncMaps in struct literal of type GoTemplateViewer
}
}

func (v *GoTemplateViewer) HTML(status int, filename string, data any) http.HandlerFunc {
}

Check failure on line 25 in pkg/webutil/viewgo.go

View workflow job for this annotation

GitHub Actions / CI Build

missing return

func (v *GoTemplateViewer) Render(filename string, r *http.Request, data any) (*bytes.Buffer, error) {
t := template.New(filename)

for _, fm := range h.FuncMaps {

Check failure on line 30 in pkg/webutil/viewgo.go

View workflow job for this annotation

GitHub Actions / CI Build

undefined: h
t = t.Funcs(fm(r))
}

t, err := t.ParseFS(h.FS, "*")

Check failure on line 34 in pkg/webutil/viewgo.go

View workflow job for this annotation

GitHub Actions / CI Build

undefined: h
if err != nil {
return nil, errors.Wrap(err, "parsing template failed")
}

buf := new(bytes.Buffer)
err = t.Execute(buf, data)

return buf, errors.Wrap(err, "executing template failed")
}
93 changes: 93 additions & 0 deletions pkg/webutil/viewjet.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package webutil

import (
"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
options []JetOption
}

func NewJetViewer(js *jet.Set, options ...JetOption) *JetViewer {
jv := &JetViewer{
views: js,
options: options,
}

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
})

return jv
}

func (j *JetViewer) HTML(status int, filename string, data any, opts ...JetOption) 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 j.options {
o(r, &vars)
}

for _, o := range opts {
o(r, &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)
}
Loading
Loading