package raml_test import ( "context" "errors" "fmt" "math/rand" "net/http" "testing" "github.com/go-chi/chi" "github.com/go-chi/chi/middleware" "github.com/go-chi/docgen" "github.com/go-chi/docgen/raml" "github.com/go-chi/render" yaml "gopkg.in/yaml.v2" ) func TestWalkerRAML(t *testing.T) { r := Router() ramlDocs := &raml.RAML{ Title: "Big Mux", BaseUri: "https://bigmux.example.com", Version: "v1.0", MediaType: "application/json", } if err := chi.Walk(r, func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error { handlerInfo := docgen.GetFuncInfo(handler) resource := &raml.Resource{ Description: handlerInfo.Comment, } return ramlDocs.Add(method, route, resource) }); err != nil { t.Error(err) } _, err := yaml.Marshal(ramlDocs) if err != nil { t.Error(err) } } // Copy-pasted from _examples/raml. We can't simply import it, since it's main pkg. func Router() chi.Router { r := chi.NewRouter() r.Use(middleware.RequestID) r.Use(middleware.Logger) r.Use(middleware.Recoverer) r.Get("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("root.")) }) r.Get("/ping", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("pong")) }) r.Get("/panic", func(w http.ResponseWriter, r *http.Request) { panic("test") }) // RESTy routes for "articles" resource r.Route("/articles", func(r chi.Router) { r.With(paginate).Get("/", ListArticles) r.Post("/", CreateArticle) // POST /articles r.Get("/search", SearchArticles) // GET /articles/search r.Route("/:articleID", func(r chi.Router) { r.Use(ArticleCtx) // Load the *Article on the request context r.Get("/", GetArticle) // GET /articles/123 r.Put("/", UpdateArticle) // PUT /articles/123 r.Delete("/", DeleteArticle) // DELETE /articles/123 }) }) // Mount the admin sub-router, the same as a call to // Route("/admin", func(r chi.Router) { with routes here }) r.Mount("/admin", adminRouter()) return r } type Article struct { ID string `json:"id"` Title string `json:"title"` } // Article fixture data var articles = []*Article{ {ID: "1", Title: "Hi"}, {ID: "2", Title: "sup"}, } // ArticleCtx middleware is used to load an Article object from // the URL parameters passed through as the request. In case // the Article could not be found, we stop here and return a 404. func ArticleCtx(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { articleID := chi.URLParam(r, "articleID") article, err := dbGetArticle(articleID) if err != nil { render.Status(r, http.StatusNotFound) render.JSON(w, r, http.StatusText(http.StatusNotFound)) return } ctx := context.WithValue(r.Context(), "article", article) next.ServeHTTP(w, r.WithContext(ctx)) }) } // Search Articles. // Searches the Articles data for a matching article. // It's just a stub, but you get the idea. func SearchArticles(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, articles) } // List Articles. // Returns an array of Articles. func ListArticles(w http.ResponseWriter, r *http.Request) { render.JSON(w, r, articles) } // Create new Article. // Ppersists the posted Article and returns it // back to the client as an acknowledgement. func CreateArticle(w http.ResponseWriter, r *http.Request) { article := &Article{} render.JSON(w, r, article) } // Get a specific Article. func GetArticle(w http.ResponseWriter, r *http.Request) { article := r.Context().Value("article").(*Article) render.JSON(w, r, article) } // Update a specific Article. // Updates an existing Article in our persistent store. func UpdateArticle(w http.ResponseWriter, r *http.Request) { article := r.Context().Value("article").(*Article) render.JSON(w, r, article) } // Delete a specific Article. // Removes an existing Article from our persistent store. func DeleteArticle(w http.ResponseWriter, r *http.Request) { article := r.Context().Value("article").(*Article) render.JSON(w, r, article) } // A completely separate router for administrator routes func adminRouter() chi.Router { r := chi.NewRouter() r.Use(AdminOnly) r.Get("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("admin: index")) }) r.Get("/accounts", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("admin: list accounts..")) }) r.Get("/users/:userId", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(fmt.Sprintf("admin: view user id %v", chi.URLParam(r, "userId")))) }) return r } // AdminOnly middleware restricts access to just administrators. func AdminOnly(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { isAdmin, ok := r.Context().Value("acl.admin").(bool) if !ok || !isAdmin { http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden) return } next.ServeHTTP(w, r) }) } // paginate is a stub, but very possible to implement middleware logic // to handle the request params for handling a paginated request. func paginate(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // just a stub.. some ideas are to look at URL query params for something like // the page number, or the limit, and send a query cursor down the chain next.ServeHTTP(w, r) }) } //-- // Below are a bunch of helper functions that mock some kind of storage func dbNewArticle(article *Article) (string, error) { article.ID = fmt.Sprintf("%d", rand.Intn(100)+10) articles = append(articles, article) return article.ID, nil } func dbGetArticle(id string) (*Article, error) { for _, a := range articles { if a.ID == id { return a, nil } } return nil, errors.New("article not found.") } func dbRemoveArticle(id string) (*Article, error) { for i, a := range articles { if a.ID == id { articles = append((articles)[:i], (articles)[i+1:]...) return a, nil } } return nil, errors.New("article not found.") }