From 77b0cbc925ee0679d8cdd381adf7ce826a0c6410 Mon Sep 17 00:00:00 2001 From: Sean Johnson Date: Thu, 23 Jan 2025 12:44:38 -0700 Subject: [PATCH] Initial commit. --- controller.go | 62 +++++++++++++++++++++++++++++++++++++ go.mod | 3 ++ handler.go | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++ sender.go | 85 +++++++++++++++++++++++++++++++++++++++++++++++++++ template.go | 48 +++++++++++++++++++++++++++++ 5 files changed, 282 insertions(+) create mode 100644 controller.go create mode 100644 go.mod create mode 100644 handler.go create mode 100644 sender.go create mode 100644 template.go diff --git a/controller.go b/controller.go new file mode 100644 index 0000000..34af6a0 --- /dev/null +++ b/controller.go @@ -0,0 +1,62 @@ +package sender + +import ( + "context" + "io" + "net/http" +) + +type ( + Options struct { + ErrorMap func(error) int + } + + JSONOptions struct { + Options + } + + Controller struct { + errorMap func(error) int + handlers map[string]func(w http.ResponseWriter, r *http.Request) + } +) + +func NewController( + errorMap func(error) int, +) *Controller { + return &Controller{ + errorMap: errorMap, + } +} + +func (c *Controller) Init(mux *http.ServeMux) { + for route, handler := range c.handlers { + mux.HandleFunc(route, handler) + } +} + +func (c *Controller) initHandlers() { + if c.handlers == nil { + c.handlers = make(map[string]func(w http.ResponseWriter, r *http.Request)) + } +} + +func (c *Controller) Handler(route string, handler func(context.Context, Sender)) { + c.initHandlers() + c.handlers[route] = Handler(handler) +} + +func (c *Controller) JsonHandler(route string, handler func(context.Context, Sender) (any, error)) { + c.initHandlers() + c.handlers[route] = JsonHandler(c.errorMap, handler) +} + +func (c *Controller) StreamHandler(route string, handler func(context.Context, Sender) (io.Reader, error)) { + c.initHandlers() + c.handlers[route] = StreamHandler(c.errorMap, handler) +} + +func (c *Controller) TemplateHandler(route string, template *Template, handler func(context.Context, Sender) (any, error)) { + c.initHandlers() + c.handlers[route] = TemplateHandler(c.errorMap, template, handler) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6d90fb6 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/cardboardrobots/sender + +go 1.23.1 diff --git a/handler.go b/handler.go new file mode 100644 index 0000000..52606d8 --- /dev/null +++ b/handler.go @@ -0,0 +1,84 @@ +package sender + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "runtime/debug" +) + +func Handler(handler func(context.Context, Sender)) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + s := NewSender(w, r) + defer func() { + if result := recover(); result != nil { + stack := debug.Stack() + s.SendStatus(http.StatusInternalServerError) + _ = s.SendJson(panicData{ + Panic: fmt.Sprint(result), + Stack: string(stack), + }) + } + }() + handler(r.Context(), s) + } +} + +func BasicHandler(errorMap func(error) int, handler func(context.Context, Sender) error) func(w http.ResponseWriter, r *http.Request) { + return Handler(func(ctx context.Context, s Sender) { + err := handler(ctx, s) + if err != nil { + s.SendError(errorMap(err), err) + } + }) +} + +func JsonHandler[T any](errorMap func(error) int, handler func(context.Context, Sender) (T, error)) func(w http.ResponseWriter, r *http.Request) { + return Handler(func(ctx context.Context, s Sender) { + result, err := handler(ctx, s) + if err == nil { + err1 := s.SendJson(result) + if err1 != nil { + err = errors.Join(err, err1) + } else { + return + } + } + + s.SendError(errorMap(err), err) + }) +} + +func StreamHandler(errorMap func(error) int, handler func(context.Context, Sender) (io.Reader, error)) func(w http.ResponseWriter, r *http.Request) { + return Handler(func(ctx context.Context, s Sender) { + result, err := handler(ctx, s) + if err == nil { + _, err1 := s.SendReader(result) + if err1 != nil { + err = errors.Join(err, err1) + } else { + return + } + } + + s.SendError(errorMap(err), err) + }) +} + +func TemplateHandler[T any](errorMap func(error) int, template *Template, handler func(context.Context, Sender) (T, error)) func(w http.ResponseWriter, r *http.Request) { + return Handler(func(ctx context.Context, s Sender) { + result, err := handler(ctx, s) + if err == nil { + err1 := template.ExecuteTemplate(s.response, result) + if err1 != nil { + err = errors.Join(err, err1) + } else { + return + } + } + + s.SendError(errorMap(err), err) + }) +} diff --git a/sender.go b/sender.go new file mode 100644 index 0000000..0c2a3cd --- /dev/null +++ b/sender.go @@ -0,0 +1,85 @@ +package sender + +import ( + "encoding/json" + "io" + "net/http" +) + +const ( + HeaderContentType = "Content-Type" + + ContentTypeApplicationJSON = "application/json" + ContentTypeTextHTML = "text/html; charset=utf-8" +) + +type ( + Sender struct { + response http.ResponseWriter + request *http.Request + } + + errorData struct { + Error string `json:"error"` + } + + panicData struct { + Panic string `json:"panic"` + Stack string `json:"stack"` + } +) + +func NewSender(w http.ResponseWriter, r *http.Request) Sender { + return Sender{ + response: w, + request: r, + } +} + +func (s Sender) Response() http.ResponseWriter { return s.response } +func (s Sender) Request() *http.Request { return s.request } + +func (s Sender) SendStatus(statusCode int) { + s.response.WriteHeader(statusCode) +} + +func (s Sender) SetContentTypeJSON() { + s.response.Header().Set(HeaderContentType, ContentTypeApplicationJSON) +} + +func (s Sender) SetContentTypeHTML() { + s.response.Header().Set(HeaderContentType, ContentTypeTextHTML) +} + +func (s Sender) SendReader(r io.Reader) (int64, error) { + return io.Copy(s.response, r) +} + +func (s Sender) SendBytes(data []byte) error { + _, err := s.response.Write(data) + if err != nil { + return s.SendError(http.StatusInternalServerError, err) + } + + return err +} + +func (s Sender) SendJson(value any) error { + s.SetContentTypeJSON() + + err := json.NewEncoder(s.response).Encode(value) + if err != nil { + return s.SendError(http.StatusInternalServerError, err) + } + + return err +} + +func (s Sender) SendError(statusCode int, err error) error { + s.SetContentTypeJSON() + + s.SendStatus(statusCode) + return json.NewEncoder(s.response).Encode(errorData{ + Error: err.Error(), + }) +} diff --git a/template.go b/template.go new file mode 100644 index 0000000..1e07087 --- /dev/null +++ b/template.go @@ -0,0 +1,48 @@ +package sender + +import ( + "html/template" + "io" + "io/fs" +) + +type Template struct { + debug bool + fs fs.FS + patterns []string + name string + template *template.Template +} + +func NewTemplate(fs fs.FS, patterns ...string) *Template { + return &Template{ + fs: fs, + patterns: patterns, + name: patterns[len(patterns)-1], + template: refreshTemplate(fs, patterns...), + } +} + +func (t *Template) EnableDebug() { + t.debug = true +} + +func (t *Template) DisableDebug() { + t.debug = false +} + +func (t *Template) Template() *template.Template { + if t.debug { + return refreshTemplate(t.fs, t.patterns...) + } + + return t.template +} + +func (t *Template) ExecuteTemplate(wr io.Writer, data any) error { + return t.Template().ExecuteTemplate(wr, t.name, data) +} + +func refreshTemplate(fs fs.FS, patterns ...string) *template.Template { + return template.Must(template.ParseFS(fs, patterns...)) +}