Skip to content

Commit

Permalink
feat(mvc): treat errors in an idiomatic way
Browse files Browse the repository at this point in the history
  • Loading branch information
alexfalkowski committed Feb 1, 2025
1 parent 45a9baf commit 1723288
Show file tree
Hide file tree
Showing 11 changed files with 169 additions and 158 deletions.
13 changes: 2 additions & 11 deletions cmd/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
package cmd_test

import (
"context"
"os"
"testing"
"time"
Expand All @@ -29,7 +28,6 @@ import (
"github.com/alexfalkowski/go-service/hooks"
"github.com/alexfalkowski/go-service/id"
"github.com/alexfalkowski/go-service/module"
"github.com/alexfalkowski/go-service/net/http/mvc"
"github.com/alexfalkowski/go-service/telemetry"
"github.com/alexfalkowski/go-service/test"
st "github.com/alexfalkowski/go-service/time"
Expand Down Expand Up @@ -227,12 +225,6 @@ func crypt(a argon2.Signer, _ ed25519.Signer, _ rsa.Cipher, _ aes.Cipher, _ hmac
return nil
}

func controller(router *mvc.Router) {
router.Route("GET /test", func(_ context.Context) (mvc.View, mvc.Model) {
return mvc.View("test.tmpl"), nil
})
}

func tokens(_ token.KID, _ *token.JWT, _ *token.Paseto, _ *token.Token) {}

func shutdown(s fx.Shutdowner) {
Expand All @@ -251,8 +243,7 @@ func opts() []fx.Option {
fx.Provide(registrations), fx.Provide(healthObserver), fx.Provide(livenessObserver),
fx.Provide(readinessObserver), fx.Provide(grpcObserver), fx.Invoke(shutdown),
fx.Invoke(featureClient), fx.Invoke(webHooks), fx.Invoke(configs),
fx.Provide(ver), fx.Invoke(meter),
fx.Invoke(netTime), fx.Invoke(crypt), fx.Invoke(environment),
fx.Invoke(controller), fx.Invoke(tokens),
fx.Provide(ver), fx.Invoke(meter), fx.Invoke(netTime),
fx.Invoke(crypt), fx.Invoke(environment), fx.Invoke(tokens),
}
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ require (
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/tools v0.28.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f // indirect
)
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -318,8 +318,8 @@ golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f h1:gap6+3Gk41EItBuyi4XX/bp4oqJ3UwuIMl25yGinuAA=
google.golang.org/genproto/googleapis/api v0.0.0-20250115164207-1a7da9e5054f/go.mod h1:Ic02D47M+zbarjYYUlK57y316f2MoN0gjAwI3f2S95o=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250115164207-1a7da9e5054f h1:OxYkA3wjPsZyBylwymxSHa7ViiW1Sml4ToBrncvFehI=
Expand Down
8 changes: 8 additions & 0 deletions net/http/mvc/controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package mvc

import (
"context"
)

// Controller for mvc.
type Controller[Model any] func(ctx context.Context) (View, *Model, error)
128 changes: 6 additions & 122 deletions net/http/mvc/mvc.go
Original file line number Diff line number Diff line change
@@ -1,131 +1,15 @@
package mvc

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

"github.com/alexfalkowski/go-service/meta"
nh "github.com/alexfalkowski/go-service/net/http"
hc "github.com/alexfalkowski/go-service/net/http/context"
"github.com/alexfalkowski/go-service/net/http/status"
"github.com/go-sprout/sprout/sprigin"
"go.uber.org/fx"
)

type (
// ViewsParams for mvc.
ViewsParams struct {
fx.In

FS fs.FS `optional:"true"`
Patterns Patterns `optional:"true"`
}

// Patterns to render views.
Patterns []string
)

// IsValid verifies the params are present.
func (p ViewsParams) IsValid() bool {
return p.FS != nil && len(p.Patterns) != 0
}

// NewView from fs with patterns.
func NewViews(params ViewsParams) *Views {
var tpl *template.Template

if params.IsValid() {
tpl = template.Must(template.New("").Funcs(sprigin.FuncMap()).ParseFS(params.FS, params.Patterns...))
}

return &Views{template: tpl, fs: params.FS}
}

// View for mvc.
type Views struct {
template *template.Template
fs fs.FS
}

// IsValid verifies that ut has an fs and template.
func (v *Views) IsValid() bool {
return v.template != nil && v.fs != nil
}

// NewRouter for mvc.
func NewRouter(mux *http.ServeMux, views *Views) *Router {
return &Router{mux: mux, views: views}
}

type (
// Router for mvc.
Router struct {
mux *http.ServeMux
views *Views
}

// View to render.
View string

// Model for mvc.
Model any

// Controller for mvc.
Controller func(ctx context.Context) (View, Model)
var (
mux *http.ServeMux
views *Views
)

// Route the path with controller for mvc.
func (r *Router) Route(path string, controller Controller) bool {
if !r.views.IsValid() {
return false
}

handler := func(res http.ResponseWriter, req *http.Request) {
res.Header().Set("Content-Type", "text/html; charset=utf-8")

ctx := req.Context()
ctx = hc.WithRequest(ctx, req)
ctx = hc.WithResponse(ctx, res)

view, model := controller(ctx)

if err, ok := model.(error); ok {
meta.WithAttribute(ctx, "mvcModelError", meta.Error(err))
res.WriteHeader(status.Code(err))
}

if err := r.views.template.ExecuteTemplate(res, string(view), model); err != nil {
meta.WithAttribute(ctx, "mvcViewError", meta.Error(err))
res.WriteHeader(status.Code(err))
}
}

r.mux.HandleFunc(path, handler)

return true
}

// Static file name to be served via path.
func (r *Router) Static(path, name string) bool {
if !r.views.IsValid() {
return false
}

handler := func(res http.ResponseWriter, req *http.Request) {
ctx := req.Context()

bytes, err := fs.ReadFile(r.views.fs, name)
if err != nil {
meta.WithAttribute(ctx, "mvcStaticError", meta.Error(err))
res.WriteHeader(status.Code(err))
}

nh.WriteResponse(ctx, res, bytes)
}

r.mux.HandleFunc(path, handler)

return true
// Register for mvc.
func Register(mu *http.ServeMux, vi *Views) {
mux, views = mu, vi
}
63 changes: 63 additions & 0 deletions net/http/mvc/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package mvc

import (
"io/fs"
"net/http"

"github.com/alexfalkowski/go-service/meta"
nh "github.com/alexfalkowski/go-service/net/http"
hc "github.com/alexfalkowski/go-service/net/http/context"
"github.com/alexfalkowski/go-service/net/http/status"
)

// Route the path with controller for mvc.
func Route[Model any](path string, controller Controller[Model]) bool {
if !views.IsValid() {
return false
}

handler := func(res http.ResponseWriter, req *http.Request) {
res.Header().Set("Content-Type", "text/html; charset=utf-8")

ctx := req.Context()
ctx = hc.WithRequest(ctx, req)
ctx = hc.WithResponse(ctx, res)

view, model, err := controller(ctx)
if err != nil {
meta.WithAttribute(ctx, "mvcModelError", meta.Error(err))
res.WriteHeader(status.Code(err))

view.Render(ctx, res, err)
} else {
view.Render(ctx, res, model)
}
}

mux.HandleFunc(path, handler)

return true
}

// Static file name to be served via path.
func Static(path, name string) bool {
if !views.IsValid() {
return false
}

handler := func(res http.ResponseWriter, req *http.Request) {
ctx := req.Context()

bytes, err := fs.ReadFile(views.fs, name)
if err != nil {
meta.WithAttribute(ctx, "mvcStaticError", meta.Error(err))
res.WriteHeader(status.Code(err))
}

nh.WriteResponse(ctx, res, bytes)
}

mux.HandleFunc(path, handler)

return true
}
64 changes: 64 additions & 0 deletions net/http/mvc/view.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package mvc

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

"github.com/alexfalkowski/go-service/meta"
"github.com/alexfalkowski/go-service/net/http/status"
"github.com/go-sprout/sprout/sprigin"
"go.uber.org/fx"
)

type (
// ViewsParams for mvc.
ViewsParams struct {
fx.In

FS fs.FS `optional:"true"`
Patterns Patterns `optional:"true"`
}

// Patterns to render views.
Patterns []string
)

// IsValid verifies the params are present.
func (p ViewsParams) IsValid() bool {
return p.FS != nil && len(p.Patterns) != 0
}

// NewView from fs with patterns.
func NewViews(params ViewsParams) *Views {
var tpl *template.Template

if params.IsValid() {
tpl = template.Must(template.New("").Funcs(sprigin.FuncMap()).ParseFS(params.FS, params.Patterns...))
}

return &Views{template: tpl, fs: params.FS}
}

// View for mvc.
type Views struct {
template *template.Template
fs fs.FS
}

// IsValid verifies that ut has an fs and template.
func (v *Views) IsValid() bool {
return v.template != nil && v.fs != nil
}

// View to render.
type View string

// Render the view.
func (v View) Render(ctx context.Context, res http.ResponseWriter, model any) {
if err := views.template.ExecuteTemplate(res, string(v), model); err != nil {
meta.WithAttribute(ctx, "mvcViewError", meta.Error(err))
res.WriteHeader(status.Code(err))
}
}
5 changes: 2 additions & 3 deletions test/world.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,6 @@ type World struct {
PG *pg.Config
*Server
*Client
*mvc.Router
*events.Event
*eh.Receiver
Sender client.Client
Expand Down Expand Up @@ -184,7 +183,7 @@ func NewWorld(t *testing.T, opts ...WorldOption) *World {
}

views := mvc.NewViews(mvc.ViewsParams{FS: &Views, Patterns: mvc.Patterns{"views/*.tmpl"}})
router := mvc.NewRouter(mux, views)
mvc.Register(mux, views)

restClient := restClient(client, os)

Expand All @@ -201,7 +200,7 @@ func NewWorld(t *testing.T, opts ...WorldOption) *World {
Logger: logger, Tracer: tracer,
Lifecycle: lc, ServeMux: mux,
Server: server, Client: client,
Router: router, Rest: restClient,
Rest: restClient,
Receiver: receiver, Sender: sender,
PG: pgConfig,
}
Expand Down
6 changes: 3 additions & 3 deletions transport/http/benchmark_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,10 @@ func BenchmarkRoute(b *testing.B) {
cl := &test.Client{Lifecycle: lc, Logger: logger, Tracer: tc, Transport: cfg, Meter: m}

v := mvc.NewViews(mvc.ViewsParams{FS: &test.Views, Patterns: mvc.Patterns{"views/*.tmpl"}})
r := mvc.NewRouter(mux, v)
mvc.Register(mux, v)

r.Route("GET /hello", func(_ context.Context) (mvc.View, mvc.Model) {
return mvc.View("hello.tmpl"), &test.Model
mvc.Route("GET /hello", func(_ context.Context) (mvc.View, *test.PageData, error) {
return mvc.View("hello.tmpl"), &test.Model, nil
})

client := cl.NewHTTP()
Expand Down
2 changes: 1 addition & 1 deletion transport/http/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ var Module = fx.Options(
fx.Provide(http.NewServeMux),
fx.Provide(content.NewContent),
fx.Provide(mvc.NewViews),
fx.Provide(mvc.NewRouter),
fx.Invoke(mvc.Register),
fx.Invoke(rpc.Register),
fx.Invoke(rest.Register),
fx.Provide(NewServer),
Expand Down
Loading

0 comments on commit 1723288

Please sign in to comment.