-
Notifications
You must be signed in to change notification settings - Fork 31
/
Copy pathcontext.go
383 lines (307 loc) · 8.96 KB
/
context.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
package lion
import (
"context"
"encoding/json"
"encoding/xml"
"errors"
"fmt"
"net/http"
"net/url"
"time"
"github.com/celrenheit/lion/internal/matcher"
)
type ctxKeyType int
// Context key to store *ctx
const ctxKey ctxKeyType = 0
var (
// ErrInvalidRedirectStatusCode is used to notify when an invalid redirect status code is used on Context.Redirect()
ErrInvalidRedirectStatusCode = errors.New("Invalid redirect status code")
contentTypeJSON = "application/json; charset=utf-8"
contentTypeXML = "application/xml; charset=utf-8"
contentTypeTextPlain = "text/plain; charset=utf-8"
contentTypeTextHTML = "text/html; charset=utf-8"
)
// Check Context implements net.Context
var _ context.Context = (*ctx)(nil)
var _ Context = (*ctx)(nil)
var _ http.ResponseWriter = (*ctx)(nil)
// Context is used to store url params and is a convinient utility to read informations from the current *http.Request and render to the current http.ResponseWriter.
// It implements the http.ResponseWriter interface.
type Context interface {
context.Context
http.ResponseWriter
Param(key string) string
ParamOk(key string) (string, bool)
Clone() Context
Request() *http.Request
// Request
Cookie(name string) (*http.Cookie, error)
Query(name string) string
GetHeader(key string) string
// Response
WithStatus(code int) Context
WithHeader(key, value string) Context
WithCookie(cookie *http.Cookie) Context
// Rendering
JSON(data interface{}) error
XML(data interface{}) error
String(format string, a ...interface{}) error
Error(err error) error
File(path string) error
Attachment(path, filename string) error
Redirect(urlStr string) error
}
// Context implements context.Context and stores values of url parameters
type ctx struct {
http.ResponseWriter
parent context.Context
req *http.Request
params []parameter
code int
statusWritten bool
tags matcher.Tags
searchHistory []string
}
// newContext creates a new context instance
func newContext() *ctx {
return newContextWithParent(context.Background())
}
// newContextWithParent creates a new context with a parent context specified
func newContextWithParent(c context.Context) *ctx {
return newContextWithResReq(c, nil, nil)
}
func newContextWithResReq(c context.Context, w http.ResponseWriter, r *http.Request) *ctx {
return &ctx{
parent: c,
ResponseWriter: w,
req: r,
tags: make([]string, 1),
searchHistory: make([]string, 0, 20), // Preallocate enough capacity. TODO: Make it depend on max nodes
}
}
// Value returns the value for the passed key. If it is not found in the url params it returns parent's context Value
func (c *ctx) Value(key interface{}) interface{} {
if key == ctxKey {
return c
}
if k, ok := key.(string); ok {
if val, exist := c.ParamOk(k); exist {
return val
}
}
return c.parent.Value(key)
}
func (c *ctx) Deadline() (deadline time.Time, ok bool) {
return c.parent.Deadline()
}
func (c *ctx) Done() <-chan struct{} {
return c.parent.Done()
}
func (c *ctx) Err() error {
return c.parent.Err()
}
func (c *ctx) AddParam(key, val string) {
c.params = append(c.params, parameter{key, val})
}
// Param returns the value of a param.
// If it does not exist it returns an empty string
func (c *ctx) Param(key string) string {
val, _ := c.ParamOk(key)
return val
}
// ParamOk returns the value of a param and a boolean that indicates if the param exists.
func (c *ctx) ParamOk(key string) (string, bool) {
for _, p := range c.params {
if p.key == key {
return p.val, true
}
}
return "", false
}
func (c *ctx) Clone() Context {
nc := newContext()
nc.parent = c.parent
nc.params = make([]parameter, len(c.params), cap(c.params))
copy(nc.params, c.params)
// shallow copy of request
nr := &c.req
nc.req = *nr
return nc
}
func (c *ctx) SearchHistory() []string {
return c.searchHistory
}
///////////// REQUEST UTILS ////////////////
func (c *ctx) Request() *http.Request {
return c.req
}
func (c *ctx) Cookie(name string) (*http.Cookie, error) {
return c.Request().Cookie(name)
}
func (c *ctx) Query(name string) string {
return c.urlQueries().Get(name)
}
func (c *ctx) urlQueries() url.Values {
return c.Request().URL.Query()
}
func (c *ctx) GetHeader(key string) string {
return c.Request().Header.Get(key)
}
///////////// REQUEST UTILS ////////////////
///////////// RESPONSE MODIFIERS /////////////
// WithStatus sets the status code for the current request.
// If the status has already been written it will not change the current status code
func (c *ctx) WithStatus(code int) Context {
c.code = code
return c
}
func (c *ctx) writeHeader() {
if !c.isStatusWritten() {
c.WriteHeader(c.code)
c.statusWritten = true
}
}
func (c *ctx) isStatusWritten() bool {
return c.statusWritten
}
// WithHeader is a convenient alias for http.ResponseWriter.Header().Set()
func (c *ctx) WithHeader(key, value string) Context {
c.Header().Set(key, value)
return c
}
func (c *ctx) WithCookie(cookie *http.Cookie) Context {
http.SetCookie(c, cookie)
return c
}
///////////// RESPONSE MODIFIERS /////////////
///////////// RESPONSE RENDERING /////////////
func (c *ctx) JSON(data interface{}) error {
b, err := json.Marshal(data)
if err != nil {
return err
}
return c.raw(b, contentTypeJSON)
}
func (c *ctx) String(format string, a ...interface{}) error {
str := fmt.Sprintf(format, a...)
return c.raw([]byte(str), contentTypeTextPlain)
}
func (c *ctx) Error(err error) error {
if herr, ok := err.(HTTPError); ok {
return c.WithStatus(herr.Status()).
String(err.Error())
}
return c.String(err.Error())
}
func (c *ctx) XML(data interface{}) error {
b, err := xml.Marshal(data)
if err != nil {
return err
}
return c.raw(b, contentTypeXML)
}
func (c *ctx) File(path string) error {
http.ServeFile(c, c.Request(), path)
return nil
}
func (c *ctx) Attachment(path, filename string) error {
return c.WithHeader("Content-Disposition", "attachment; filename="+filename).
File(path)
}
func (c *ctx) setContentType(ctype string) {
c.Header().Set("Content-Type", ctype)
}
func (c *ctx) raw(b []byte, contentType string) error {
c.setContentType(contentType)
c.writeHeader()
_, err := c.Write(b)
return err
}
///////////// RESPONSE RENDERING /////////////
func (c *ctx) Redirect(urlStr string) error {
if c.code < 300 || c.code > 308 {
return ErrInvalidRedirectStatusCode
}
http.Redirect(c, c.Request(), urlStr, c.code)
return nil
}
func (c *ctx) Reset() {
c.params = c.params[:0]
c.parent = context.Background()
c.req = nil
c.ResponseWriter = nil
c.code = 0
c.statusWritten = false
c.searchHistory = c.searchHistory[:0]
}
func (c *ctx) Remove(key string) {
i := c.indexOf(key)
if i < 0 {
panicl("Cannot remove unknown key '%s' from context", key)
}
c.params = append(c.params[:i], c.params[i+1:]...)
}
func (c *ctx) indexOf(key string) int {
for i := len(c.params) - 1; i >= 0; i-- {
if c.params[i].key == key {
return i
}
}
return -1
}
// C returns a Context based on a context.Context passed. If it does not convert to Context, it creates a new one with the context passed as argument.
func C(req *http.Request) Context {
c := req.Context()
if val := c.Value(ctxKey); val != nil {
if ctx, ok := val.(*ctx); ok {
return ctx
}
}
return nil
}
// Param returns the value of a url param base on the passed context
func Param(req *http.Request, key string) string {
return C(req).Param(key)
}
func setParamContext(req *http.Request, c *ctx) *http.Request {
c.parent = req.Context()
return req.WithContext(context.WithValue(req.Context(), ctxKey, c))
}
type parameter struct {
key string
val string
}
// HTTPError allows to write an error to http.ResponseWriter.
// You can use it with Context. Like in the following example:
// func(c lion.Context) {
// c.Error(lion.ErrorUnauthorized)
// }
// This will return a response with status code 401 and a body of "Unauthorized".
// Check below for the available http error
type HTTPError interface {
error
Status() int
}
var (
// 4xx
// ErrorBadRequest returns a BadRequest response with the corresponding body
ErrorBadRequest HTTPError = httpError{http.StatusBadRequest}
// ErrorUnauthorized returns a Unauthorized response with the corresponding body
ErrorUnauthorized HTTPError = httpError{http.StatusUnauthorized}
// ErrorForbidden returns a Forbidden response with the corresponding body
ErrorForbidden HTTPError = httpError{http.StatusForbidden}
// ErrorNotFound returns a NotFound response with the corresponding body
ErrorNotFound HTTPError = httpError{http.StatusNotFound}
// ErrorMethodNotAllowed returns a MethodNotAllowed response with the corresponding body
ErrorMethodNotAllowed HTTPError = httpError{http.StatusMethodNotAllowed}
// 5xx
// ErrorInternalServer returns a InternalServerError response with the corresponding body
ErrorInternalServer HTTPError = httpError{http.StatusInternalServerError}
)
type httpError struct{ code int }
func (e httpError) Error() string {
return http.StatusText(e.code)
}
func (e httpError) Status() int {
return e.code
}