diff options
author | Mike Crute <mike@crute.us> | 2023-08-01 17:19:17 -0700 |
---|---|---|
committer | Mike Crute <mike@crute.us> | 2023-08-01 17:19:17 -0700 |
commit | 058e4faba9c1531661234a5adaa7a6c3791f92d4 (patch) | |
tree | bf7cc7879e1ad41f8dc1cbbaae74dad632556af8 /echo/controller | |
parent | 14a8d74efed00c13bd59e2ff9adc0b743803535d (diff) | |
download | golib-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.go | 196 | ||||
-rw-r--r-- | echo/controller/content_type_negotiator_v2_test.go | 72 |
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 @@ | |||
1 | package controller | ||
2 | |||
3 | import ( | ||
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 | |||
13 | const ( | ||
14 | ContextMediaTypeParameters = "__golib_echo_negotiator_media_type_params" | ||
15 | DefaultContentType = "*/*" | ||
16 | ) | ||
17 | |||
18 | type ContentTypeHandlerMap map[string]ErrorHandler | ||
19 | |||
20 | type ErrorHandler interface { | ||
21 | HandleError(echo.Context, int, error) error | ||
22 | } | ||
23 | |||
24 | type GetableHandler interface{ HandleGet(echo.Context) error } | ||
25 | type HeadableHandler interface{ HandleHead(echo.Context) error } | ||
26 | type PostableHandler interface{ HandlePost(echo.Context) error } | ||
27 | type PutableHandler interface{ HandlePut(echo.Context) error } | ||
28 | type PatchableHandler interface{ HandlePatch(echo.Context) error } | ||
29 | type DeleteableHandler interface{ HandleDelete(echo.Context) error } | ||
30 | type ConnectableHandler interface{ HandleConnect(echo.Context) error } | ||
31 | type OptionableHandler interface{ HandleOptions(echo.Context) error } | ||
32 | type TraceableHandler interface{ HandleTrace(echo.Context) error } | ||
33 | |||
34 | type 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 | |||
40 | func 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 | |||
48 | func 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. | ||
92 | func (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 | |||
106 | func (h *ContentTypeNegotiatingHandlerV2) Default(hnd ErrorHandler) *ContentTypeNegotiatingHandlerV2 { | ||
107 | return h.Handles(DefaultContentType, hnd) | ||
108 | } | ||
109 | |||
110 | // Handles is a fluent builder that panics on error | ||
111 | func (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 | |||
118 | func (h *ContentTypeNegotiatingHandlerV2) HandlesAll(hnds ContentTypeHandlerMap) *ContentTypeNegotiatingHandlerV2 { | ||
119 | for ct, hnd := range hnds { | ||
120 | h.Handles(ct, hnd) | ||
121 | } | ||
122 | return h | ||
123 | } | ||
124 | |||
125 | func (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 | |||
138 | func (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 | |||
164 | func (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 @@ | |||
1 | package controller | ||
2 | |||
3 | import ( | ||
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 | |||
26 | type GetOnlyHandler struct{} | ||
27 | |||
28 | func (h *GetOnlyHandler) HandleError(c echo.Context, code int, err error) error { | ||
29 | return c.String(code, fmt.Sprintf("%s -- default", err)) | ||
30 | } | ||
31 | |||
32 | func (h *GetOnlyHandler) HandleGet(c echo.Context) error { | ||
33 | return c.String(http.StatusOK, "GET OK -- default") | ||
34 | } | ||
35 | |||
36 | type GetPostHandler struct{ Name string } | ||
37 | |||
38 | func (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 | |||
42 | func (h *GetPostHandler) HandleGet(c echo.Context) error { | ||
43 | return c.String(http.StatusOK, fmt.Sprintf("%s GET OK", h.Name)) | ||
44 | } | ||
45 | |||
46 | func (h *GetPostHandler) HandlePost(c echo.Context) error { | ||
47 | return c.String(http.StatusOK, fmt.Sprintf("%s POST OK", h.Name)) | ||
48 | } | ||
49 | |||
50 | func 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 | } | ||