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 package main
40
41 import (
42 "context"
43 "errors"
44 "flag"
45 "fmt"
46 "math/rand"
47 "net/http"
48 "strings"
49
50 "github.com/go-chi/chi"
51 "github.com/go-chi/chi/middleware"
52 "github.com/go-chi/docgen"
53 "github.com/go-chi/render"
54 )
55
56 var routes = flag.Bool("routes", false, "Generate router documentation")
57
58 func main() {
59 flag.Parse()
60
61 r := chi.NewRouter()
62
63 r.Use(middleware.RequestID)
64 r.Use(middleware.Logger)
65 r.Use(middleware.Recoverer)
66 r.Use(middleware.URLFormat)
67 r.Use(render.SetContentType(render.ContentTypeJSON))
68
69 r.Get("/", func(w http.ResponseWriter, r *http.Request) {
70 w.Write([]byte("root."))
71 })
72
73 r.Get("/ping", func(w http.ResponseWriter, r *http.Request) {
74 w.Write([]byte("pong"))
75 })
76
77 r.Get("/panic", func(w http.ResponseWriter, r *http.Request) {
78 panic("test")
79 })
80
81
82 r.Route("/articles", func(r chi.Router) {
83 r.With(paginate).Get("/", ListArticles)
84 r.Post("/", CreateArticle)
85 r.Get("/search", SearchArticles)
86
87 r.Route("/{articleID}", func(r chi.Router) {
88 r.Use(ArticleCtx)
89 r.Get("/", GetArticle)
90 r.Put("/", UpdateArticle)
91 r.Delete("/", DeleteArticle)
92 })
93
94
95 r.With(ArticleCtx).Get("/{articleSlug:[a-z-]+}", GetArticle)
96 })
97
98
99
100 r.Mount("/admin", adminRouter())
101
102
103
104
105 if *routes {
106
107 fmt.Println(docgen.MarkdownRoutesDoc(r, docgen.MarkdownOpts{
108 ProjectPath: "github.com/go-chi/chi",
109 Intro: "Welcome to the chi/_examples/rest generated docs.",
110 }))
111 return
112 }
113
114 http.ListenAndServe(":3333", r)
115 }
116
117 func ListArticles(w http.ResponseWriter, r *http.Request) {
118 if err := render.RenderList(w, r, NewArticleListResponse(articles)); err != nil {
119 render.Render(w, r, ErrRender(err))
120 return
121 }
122 }
123
124
125
126
127 func ArticleCtx(next http.Handler) http.Handler {
128 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
129 var article *Article
130 var err error
131
132 if articleID := chi.URLParam(r, "articleID"); articleID != "" {
133 article, err = dbGetArticle(articleID)
134 } else if articleSlug := chi.URLParam(r, "articleSlug"); articleSlug != "" {
135 article, err = dbGetArticleBySlug(articleSlug)
136 } else {
137 render.Render(w, r, ErrNotFound)
138 return
139 }
140 if err != nil {
141 render.Render(w, r, ErrNotFound)
142 return
143 }
144
145 ctx := context.WithValue(r.Context(), "article", article)
146 next.ServeHTTP(w, r.WithContext(ctx))
147 })
148 }
149
150
151
152 func SearchArticles(w http.ResponseWriter, r *http.Request) {
153 render.RenderList(w, r, NewArticleListResponse(articles))
154 }
155
156
157
158 func CreateArticle(w http.ResponseWriter, r *http.Request) {
159 data := &ArticleRequest{}
160 if err := render.Bind(r, data); err != nil {
161 render.Render(w, r, ErrInvalidRequest(err))
162 return
163 }
164
165 article := data.Article
166 dbNewArticle(article)
167
168 render.Status(r, http.StatusCreated)
169 render.Render(w, r, NewArticleResponse(article))
170 }
171
172
173
174
175
176 func GetArticle(w http.ResponseWriter, r *http.Request) {
177
178
179
180 article := r.Context().Value("article").(*Article)
181
182 if err := render.Render(w, r, NewArticleResponse(article)); err != nil {
183 render.Render(w, r, ErrRender(err))
184 return
185 }
186 }
187
188
189 func UpdateArticle(w http.ResponseWriter, r *http.Request) {
190 article := r.Context().Value("article").(*Article)
191
192 data := &ArticleRequest{Article: article}
193 if err := render.Bind(r, data); err != nil {
194 render.Render(w, r, ErrInvalidRequest(err))
195 return
196 }
197 article = data.Article
198 dbUpdateArticle(article.ID, article)
199
200 render.Render(w, r, NewArticleResponse(article))
201 }
202
203
204 func DeleteArticle(w http.ResponseWriter, r *http.Request) {
205 var err error
206
207
208
209
210 article := r.Context().Value("article").(*Article)
211
212 article, err = dbRemoveArticle(article.ID)
213 if err != nil {
214 render.Render(w, r, ErrInvalidRequest(err))
215 return
216 }
217
218 render.Render(w, r, NewArticleResponse(article))
219 }
220
221
222 func adminRouter() chi.Router {
223 r := chi.NewRouter()
224 r.Use(AdminOnly)
225 r.Get("/", func(w http.ResponseWriter, r *http.Request) {
226 w.Write([]byte("admin: index"))
227 })
228 r.Get("/accounts", func(w http.ResponseWriter, r *http.Request) {
229 w.Write([]byte("admin: list accounts.."))
230 })
231 r.Get("/users/{userId}", func(w http.ResponseWriter, r *http.Request) {
232 w.Write([]byte(fmt.Sprintf("admin: view user id %v", chi.URLParam(r, "userId"))))
233 })
234 return r
235 }
236
237
238 func AdminOnly(next http.Handler) http.Handler {
239 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
240 isAdmin, ok := r.Context().Value("acl.admin").(bool)
241 if !ok || !isAdmin {
242 http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
243 return
244 }
245 next.ServeHTTP(w, r)
246 })
247 }
248
249
250
251 func paginate(next http.Handler) http.Handler {
252 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
253
254
255 next.ServeHTTP(w, r)
256 })
257 }
258
259
260
261 func init() {
262 render.Respond = func(w http.ResponseWriter, r *http.Request, v interface{}) {
263 if err, ok := v.(error); ok {
264
265
266 if _, ok := r.Context().Value(render.StatusCtxKey).(int); !ok {
267 w.WriteHeader(400)
268 }
269
270
271 fmt.Printf("Logging err: %s\n", err.Error())
272
273
274
275
276 render.DefaultResponder(w, r, render.M{"status": "error"})
277 return
278 }
279
280 render.DefaultResponder(w, r, v)
281 }
282 }
283
284
285
286
287
288
289
290
291
292
293 type UserPayload struct {
294 *User
295 Role string `json:"role"`
296 }
297
298 func NewUserPayloadResponse(user *User) *UserPayload {
299 return &UserPayload{User: user}
300 }
301
302
303
304 func (u *UserPayload) Bind(r *http.Request) error {
305 return nil
306 }
307
308 func (u *UserPayload) Render(w http.ResponseWriter, r *http.Request) error {
309 u.Role = "collaborator"
310 return nil
311 }
312
313
314
315
316
317
318
319
320
321
322 type ArticleRequest struct {
323 *Article
324
325 User *UserPayload `json:"user,omitempty"`
326
327 ProtectedID string `json:"id"`
328 }
329
330 func (a *ArticleRequest) Bind(r *http.Request) error {
331
332
333 if a.Article == nil {
334 return errors.New("missing required Article fields.")
335 }
336
337
338
339
340
341
342 a.ProtectedID = ""
343 a.Article.Title = strings.ToLower(a.Article.Title)
344 return nil
345 }
346
347
348
349
350
351
352
353 type ArticleResponse struct {
354 *Article
355
356 User *UserPayload `json:"user,omitempty"`
357
358
359
360 Elapsed int64 `json:"elapsed"`
361 }
362
363 func NewArticleResponse(article *Article) *ArticleResponse {
364 resp := &ArticleResponse{Article: article}
365
366 if resp.User == nil {
367 if user, _ := dbGetUser(resp.UserID); user != nil {
368 resp.User = NewUserPayloadResponse(user)
369 }
370 }
371
372 return resp
373 }
374
375 func (rd *ArticleResponse) Render(w http.ResponseWriter, r *http.Request) error {
376
377 rd.Elapsed = 10
378 return nil
379 }
380
381 func NewArticleListResponse(articles []*Article) []render.Renderer {
382 list := []render.Renderer{}
383 for _, article := range articles {
384 list = append(list, NewArticleResponse(article))
385 }
386 return list
387 }
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404 type ErrResponse struct {
405 Err error `json:"-"`
406 HTTPStatusCode int `json:"-"`
407
408 StatusText string `json:"status"`
409 AppCode int64 `json:"code,omitempty"`
410 ErrorText string `json:"error,omitempty"`
411 }
412
413 func (e *ErrResponse) Render(w http.ResponseWriter, r *http.Request) error {
414 render.Status(r, e.HTTPStatusCode)
415 return nil
416 }
417
418 func ErrInvalidRequest(err error) render.Renderer {
419 return &ErrResponse{
420 Err: err,
421 HTTPStatusCode: 400,
422 StatusText: "Invalid request.",
423 ErrorText: err.Error(),
424 }
425 }
426
427 func ErrRender(err error) render.Renderer {
428 return &ErrResponse{
429 Err: err,
430 HTTPStatusCode: 422,
431 StatusText: "Error rendering response.",
432 ErrorText: err.Error(),
433 }
434 }
435
436 var ErrNotFound = &ErrResponse{HTTPStatusCode: 404, StatusText: "Resource not found."}
437
438
439
440
441
442
443 type User struct {
444 ID int64 `json:"id"`
445 Name string `json:"name"`
446 }
447
448
449
450 type Article struct {
451 ID string `json:"id"`
452 UserID int64 `json:"user_id"`
453 Title string `json:"title"`
454 Slug string `json:"slug"`
455 }
456
457
458 var articles = []*Article{
459 {ID: "1", UserID: 100, Title: "Hi", Slug: "hi"},
460 {ID: "2", UserID: 200, Title: "sup", Slug: "sup"},
461 {ID: "3", UserID: 300, Title: "alo", Slug: "alo"},
462 {ID: "4", UserID: 400, Title: "bonjour", Slug: "bonjour"},
463 {ID: "5", UserID: 500, Title: "whats up", Slug: "whats-up"},
464 }
465
466
467 var users = []*User{
468 {ID: 100, Name: "Peter"},
469 {ID: 200, Name: "Julia"},
470 }
471
472 func dbNewArticle(article *Article) (string, error) {
473 article.ID = fmt.Sprintf("%d", rand.Intn(100)+10)
474 articles = append(articles, article)
475 return article.ID, nil
476 }
477
478 func dbGetArticle(id string) (*Article, error) {
479 for _, a := range articles {
480 if a.ID == id {
481 return a, nil
482 }
483 }
484 return nil, errors.New("article not found.")
485 }
486
487 func dbGetArticleBySlug(slug string) (*Article, error) {
488 for _, a := range articles {
489 if a.Slug == slug {
490 return a, nil
491 }
492 }
493 return nil, errors.New("article not found.")
494 }
495
496 func dbUpdateArticle(id string, article *Article) (*Article, error) {
497 for i, a := range articles {
498 if a.ID == id {
499 articles[i] = article
500 return article, nil
501 }
502 }
503 return nil, errors.New("article not found.")
504 }
505
506 func dbRemoveArticle(id string) (*Article, error) {
507 for i, a := range articles {
508 if a.ID == id {
509 articles = append((articles)[:i], (articles)[i+1:]...)
510 return a, nil
511 }
512 }
513 return nil, errors.New("article not found.")
514 }
515
516 func dbGetUser(id int64) (*User, error) {
517 for _, u := range users {
518 if u.ID == id {
519 return u, nil
520 }
521 }
522 return nil, errors.New("user not found.")
523 }
524
View as plain text