aboutsummaryrefslogtreecommitdiff
path: root/echo/controller
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2023-08-01 17:19:17 -0700
committerMike Crute <mike@crute.us>2023-08-01 17:19:17 -0700
commit058e4faba9c1531661234a5adaa7a6c3791f92d4 (patch)
treebf7cc7879e1ad41f8dc1cbbaae74dad632556af8 /echo/controller
parent14a8d74efed00c13bd59e2ff9adc0b743803535d (diff)
downloadgolib-058e4faba9c1531661234a5adaa7a6c3791f92d4.tar.bz2
golib-058e4faba9c1531661234a5adaa7a6c3791f92d4.tar.xz
golib-058e4faba9c1531661234a5adaa7a6c3791f92d4.zip
echo: add new content type negotiatorecho/v0.10.0
Diffstat (limited to 'echo/controller')
-rw-r--r--echo/controller/content_type_negotiator_v2.go196
-rw-r--r--echo/controller/content_type_negotiator_v2_test.go72
2 files changed, 268 insertions, 0 deletions
diff --git a/echo/controller/content_type_negotiator_v2.go b/echo/controller/content_type_negotiator_v2.go
new file mode 100644
index 0000000..63d7fd3
--- /dev/null
+++ b/echo/controller/content_type_negotiator_v2.go
@@ -0,0 +1,196 @@
1package controller
2
3import (
4 "errors"
5 "net/http"
6
7 "github.com/labstack/echo/v4"
8
9 glecho "code.crute.us/mcrute/golib/echo"
10 glhttp "code.crute.us/mcrute/golib/net/http"
11)
12
13const (
14 ContextMediaTypeParameters = "__golib_echo_negotiator_media_type_params"
15 DefaultContentType = "*/*"
16)
17
18type ContentTypeHandlerMap map[string]ErrorHandler
19
20type ErrorHandler interface {
21 HandleError(echo.Context, int, error) error
22}
23
24type GetableHandler interface{ HandleGet(echo.Context) error }
25type HeadableHandler interface{ HandleHead(echo.Context) error }
26type PostableHandler interface{ HandlePost(echo.Context) error }
27type PutableHandler interface{ HandlePut(echo.Context) error }
28type PatchableHandler interface{ HandlePatch(echo.Context) error }
29type DeleteableHandler interface{ HandleDelete(echo.Context) error }
30type ConnectableHandler interface{ HandleConnect(echo.Context) error }
31type OptionableHandler interface{ HandleOptions(echo.Context) error }
32type TraceableHandler interface{ HandleTrace(echo.Context) error }
33
34type ContentTypeNegotiatingHandlerV2 struct {
35 handlers map[string]map[string]echo.HandlerFunc // Content-Type -> HTTP Method -> Handler
36 errorHandlers map[string]ErrorHandler
37 mediaTypes glhttp.AcceptableTypes
38}
39
40func NewContentTypeNegotiatingHandlerV2() *ContentTypeNegotiatingHandlerV2 {
41 return &ContentTypeNegotiatingHandlerV2{
42 handlers: map[string]map[string]echo.HandlerFunc{},
43 mediaTypes: glhttp.AcceptableTypes{},
44 errorHandlers: map[string]ErrorHandler{},
45 }
46}
47
48func buildMethodMap(hnd ErrorHandler) map[string]echo.HandlerFunc {
49 methods := map[string]echo.HandlerFunc{}
50
51 if h, ok := hnd.(GetableHandler); ok {
52 methods[http.MethodGet] = h.HandleGet
53 }
54
55 if h, ok := hnd.(HeadableHandler); ok {
56 methods[http.MethodHead] = h.HandleHead
57 }
58
59 if h, ok := hnd.(PostableHandler); ok {
60 methods[http.MethodPost] = h.HandlePost
61 }
62
63 if h, ok := hnd.(PutableHandler); ok {
64 methods[http.MethodPut] = h.HandlePut
65 }
66
67 if h, ok := hnd.(PatchableHandler); ok {
68 methods[http.MethodPatch] = h.HandlePatch
69 }
70
71 if h, ok := hnd.(DeleteableHandler); ok {
72 methods[http.MethodDelete] = h.HandleDelete
73 }
74
75 if h, ok := hnd.(ConnectableHandler); ok {
76 methods[http.MethodConnect] = h.HandleConnect
77 }
78
79 if h, ok := hnd.(OptionableHandler); ok {
80 methods[http.MethodOptions] = h.HandleOptions
81 }
82
83 if h, ok := hnd.(TraceableHandler); ok {
84 methods[http.MethodTrace] = h.HandleTrace
85 }
86
87 return methods
88}
89
90// RegisterHandler registers a handler for a content type and method. It
91// will return an error if registration fails.
92func (h *ContentTypeNegotiatingHandlerV2) RegisterHandler(ct string, hnd ErrorHandler) error {
93 mt, err := glhttp.ParseMediaType(ct)
94 if err != nil {
95 return err
96 }
97
98 h.mediaTypes = append(h.mediaTypes, mt)
99 h.mediaTypes.Sorted()
100
101 h.handlers[mt.String()] = buildMethodMap(hnd)
102 h.errorHandlers[mt.String()] = hnd
103 return nil
104}
105
106func (h *ContentTypeNegotiatingHandlerV2) Default(hnd ErrorHandler) *ContentTypeNegotiatingHandlerV2 {
107 return h.Handles(DefaultContentType, hnd)
108}
109
110// Handles is a fluent builder that panics on error
111func (h *ContentTypeNegotiatingHandlerV2) Handles(ct string, hnd ErrorHandler) *ContentTypeNegotiatingHandlerV2 {
112 if err := h.RegisterHandler(ct, hnd); err != nil {
113 panic(err)
114 }
115 return h
116}
117
118func (h *ContentTypeNegotiatingHandlerV2) HandlesAll(hnds ContentTypeHandlerMap) *ContentTypeNegotiatingHandlerV2 {
119 for ct, hnd := range hnds {
120 h.Handles(ct, hnd)
121 }
122 return h
123}
124
125func (h *ContentTypeNegotiatingHandlerV2) handleDefaultError(c echo.Context, code int, status string) error {
126 if hnd, ok := h.errorHandlers[DefaultContentType]; ok {
127 return hnd.HandleError(c, code, errors.New(status))
128 }
129
130 return c.JSON(code, map[string]any{
131 "error": map[string]any{
132 "code": code,
133 "message": status,
134 },
135 })
136}
137
138func (h *ContentTypeNegotiatingHandlerV2) Handle(c echo.Context) error {
139 accept, err := glhttp.ParseAccept(c.Request().Header.Values("Accept"))
140 if err != nil {
141 return err
142 }
143
144 match, matcher := h.mediaTypes.FindMatch(accept)
145 if match == nil {
146 code := http.StatusNotAcceptable
147 return h.handleDefaultError(c, code, http.StatusText(code))
148 }
149
150 c.Set(ContextMediaTypeParameters, matcher.Parameters)
151
152 ctHandlers := h.handlers[match.String()]
153 handler, ok := ctHandlers[c.Request().Method]
154 if !ok {
155 code := http.StatusMethodNotAllowed
156 return h.errorHandlers[match.String()].HandleError(c, code, errors.New(http.StatusText(code)))
157 }
158
159 c.Response().Header().Set("Content-Type", match.String())
160
161 return handler(c)
162}
163
164func (h *ContentTypeNegotiatingHandlerV2) Connect(path string, r glecho.URLRouter, mw ...echo.MiddlewareFunc) *ContentTypeNegotiatingHandlerV2 {
165 allMethods := map[string]*int{}
166 for _, v := range h.handlers {
167 for k, _ := range v {
168 allMethods[k] = nil
169 }
170 }
171
172 for k, _ := range allMethods {
173 switch k {
174 case http.MethodGet:
175 r.GET(path, h.Handle, mw...)
176 case http.MethodHead:
177 r.HEAD(path, h.Handle, mw...)
178 case http.MethodPost:
179 r.POST(path, h.Handle, mw...)
180 case http.MethodPut:
181 r.PUT(path, h.Handle, mw...)
182 case http.MethodPatch:
183 r.PATCH(path, h.Handle, mw...)
184 case http.MethodDelete:
185 r.DELETE(path, h.Handle, mw...)
186 case http.MethodConnect:
187 r.Add(http.MethodConnect, path, h.Handle, mw...)
188 case http.MethodOptions:
189 r.OPTIONS(path, h.Handle, mw...)
190 case http.MethodTrace:
191 r.Add(http.MethodTrace, path, h.Handle, mw...)
192 }
193 }
194
195 return h
196}
diff --git a/echo/controller/content_type_negotiator_v2_test.go b/echo/controller/content_type_negotiator_v2_test.go
new file mode 100644
index 0000000..689003a
--- /dev/null
+++ b/echo/controller/content_type_negotiator_v2_test.go
@@ -0,0 +1,72 @@
1package controller
2
3import (
4 "fmt"
5 "io"
6 "net/http"
7 "net/http/httptest"
8 "testing"
9
10 "github.com/labstack/echo/v4"
11 "github.com/stretchr/testify/assert"
12)
13
14// TODO
15// Not Acceptable Error
16// Bad Request Error
17// Other error
18// Error with default handler
19// Error without default handler
20
21// Check context params
22// No handler for method
23// Check content-type return header
24// Handler runs
25
26type GetOnlyHandler struct{}
27
28func (h *GetOnlyHandler) HandleError(c echo.Context, code int, err error) error {
29 return c.String(code, fmt.Sprintf("%s -- default", err))
30}
31
32func (h *GetOnlyHandler) HandleGet(c echo.Context) error {
33 return c.String(http.StatusOK, "GET OK -- default")
34}
35
36type GetPostHandler struct{ Name string }
37
38func (h *GetPostHandler) HandleError(c echo.Context, code int, err error) error {
39 return c.String(code, fmt.Sprintf("%s %s", h.Name, err))
40}
41
42func (h *GetPostHandler) HandleGet(c echo.Context) error {
43 return c.String(http.StatusOK, fmt.Sprintf("%s GET OK", h.Name))
44}
45
46func (h *GetPostHandler) HandlePost(c echo.Context) error {
47 return c.String(http.StatusOK, fmt.Sprintf("%s POST OK", h.Name))
48}
49
50func TestHandlerSuccess(t *testing.T) {
51 hnd := NewContentTypeNegotiatingHandlerV2().
52 HandlesAll(ContentTypeHandlerMap{
53 "*/*": &GetOnlyHandler{},
54 "text/xml": &GetPostHandler{"xml"},
55 "text/*": &GetPostHandler{"html"},
56 "text/plain": &GetPostHandler{"plain"},
57 })
58
59 e := echo.New()
60 req := httptest.NewRequest(http.MethodPost, "/", nil)
61 req.Header.Set("Accept", "text/*,*/*;q=0.8")
62 //"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8")
63 rec := httptest.NewRecorder()
64 c := e.NewContext(req, rec)
65
66 assert.NoError(t, hnd.Handle(c))
67
68 res := rec.Result()
69 o, _ := io.ReadAll(res.Body)
70 fmt.Printf("%s\n", o)
71 assert.False(t, true)
72}