From fb668425dea6c6779078b2e89c12a1164445d5b7 Mon Sep 17 00:00:00 2001 From: dhax Date: Thu, 28 Sep 2017 21:58:25 +0200 Subject: [PATCH] adds api routes to documentation --- Gopkg.lock | 8 +- README.md | 7 +- cmd/gendoc.go | 72 ++++++ routes.md | 220 +++++++++++++++++ vendor/github.com/go-chi/docgen/.travis.yml | 15 ++ vendor/github.com/go-chi/docgen/LICENSE | 20 ++ vendor/github.com/go-chi/docgen/builder.go | 127 ++++++++++ vendor/github.com/go-chi/docgen/docgen.go | 64 +++++ .../github.com/go-chi/docgen/docgen_test.go | 181 ++++++++++++++ vendor/github.com/go-chi/docgen/funcinfo.go | 113 +++++++++ vendor/github.com/go-chi/docgen/markdown.go | 211 ++++++++++++++++ vendor/github.com/go-chi/docgen/raml/raml.go | 159 +++++++++++++ .../go-chi/docgen/raml/raml_test.go | 225 ++++++++++++++++++ vendor/github.com/go-chi/docgen/util.go | 37 +++ 14 files changed, 1457 insertions(+), 2 deletions(-) create mode 100644 cmd/gendoc.go create mode 100644 routes.md create mode 100644 vendor/github.com/go-chi/docgen/.travis.yml create mode 100644 vendor/github.com/go-chi/docgen/LICENSE create mode 100644 vendor/github.com/go-chi/docgen/builder.go create mode 100644 vendor/github.com/go-chi/docgen/docgen.go create mode 100644 vendor/github.com/go-chi/docgen/docgen_test.go create mode 100644 vendor/github.com/go-chi/docgen/funcinfo.go create mode 100644 vendor/github.com/go-chi/docgen/markdown.go create mode 100644 vendor/github.com/go-chi/docgen/raml/raml.go create mode 100644 vendor/github.com/go-chi/docgen/raml/raml_test.go create mode 100644 vendor/github.com/go-chi/docgen/util.go diff --git a/Gopkg.lock b/Gopkg.lock index 0e9ed0d..ebfbc8d 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -43,6 +43,12 @@ revision = "dba6525398619dead495962a916728e7ee2ca322" version = "v1.0.0" +[[projects]] + name = "github.com/go-chi/docgen" + packages = ["."] + revision = "ac43d9a63f3b58b1e82922411d2de365d896ee72" + version = "v1.0.2" + [[projects]] name = "github.com/go-chi/jwtauth" packages = ["."] @@ -261,6 +267,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "7eee4771041702973c1452dd07edcb18368c463ba616225a8479676443a53faf" + inputs-digest = "4e5e0d4a6f658ef6e0fce069907b987ec1e2459bbf2b1cfe8118caf22888a05c" solver-name = "gps-cdcl" solver-version = 1 diff --git a/README.md b/README.md index 59b88aa..e25ad53 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,12 @@ Easily extendible RESTful API boilerplate aiming to follow idiomatic go and best - Run all migrations found in ./database/migrate with: ```go-base migrate``` - Run the application: ```go-base serve``` +Run ```go-base``` only to see the cobra generated help message. + +### API Routes + +Check [routes.md](routes.md) file for an overview of the provided API routes. + #### Demo client application For demonstration of the login and account management features this API also serves a Single Page Application (SPA) as a Progressive Web App (PWA) done with [Quasar Framework](http://quasar-framework.org) which itself is powered by [Vue.js](https://vuejs.org). The client's source code can be found [here](https://github.com/dhax/go-base-client). @@ -51,4 +57,3 @@ EMAIL_SMTP_USER | string || email smtp username EMAIL_SMTP_PASSWORD | string || email smtp password EMAIL_FROM_ADDRESS | string || from address used in sending emails EMAIL_FROM_NAME | string || from name used in sending emails - diff --git a/cmd/gendoc.go b/cmd/gendoc.go new file mode 100644 index 0000000..15df37a --- /dev/null +++ b/cmd/gendoc.go @@ -0,0 +1,72 @@ +// Copyright © 2017 NAME HERE +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cmd + +import ( + "fmt" + "io/ioutil" + + "github.com/dhax/go-base/api" + "github.com/go-chi/docgen" + "github.com/spf13/cobra" +) + +var ( + routes bool +) + +// gendocCmd represents the gendoc command +var gendocCmd = &cobra.Command{ + Use: "gendoc", + Short: "Generate project documentation", + Long: `A longer description that spans multiple lines and likely contains examples +and usage of using your command. For example: + +Cobra is a CLI library for Go that empowers applications. +This application is a tool to generate the needed files +to quickly create a Cobra application.`, + Run: func(cmd *cobra.Command, args []string) { + if routes { + genRoutesDoc() + } + }, +} + +func init() { + RootCmd.AddCommand(gendocCmd) + + // Here you will define your flags and configuration settings. + + // Cobra supports Persistent Flags which will work for this command + // and all subcommands, e.g.: + // gendocCmd.PersistentFlags().String("foo", "", "A help for foo") + + // Cobra supports local flags which will only run when this command + // is called directly, e.g.: + // gendocCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle") + gendocCmd.Flags().BoolVarP(&routes, "routes", "r", false, "create api routes markdown file") +} + +func genRoutesDoc() { + fmt.Println("generating routes markdown file") + api, _ := api.NewAPI() + md := docgen.MarkdownRoutesDoc(api, docgen.MarkdownOpts{ + ProjectPath: "github.com/dhax/go-base", + Intro: "GoBase REST API.", + }) + if err := ioutil.WriteFile("routes.md", []byte(md), 0644); err != nil { + fmt.Println(err) + } +} diff --git a/routes.md b/routes.md new file mode 100644 index 0000000..6cdf317 --- /dev/null +++ b/routes.md @@ -0,0 +1,220 @@ +# github.com/dhax/go-base + +GoBase REST API. + +## Routes + +
+`/*` + +- [Recoverer](/vendor/github.com/go-chi/chi/middleware/recoverer.go#L18) +- [RequestID](/vendor/github.com/go-chi/chi/middleware/request_id.go#L63) +- [DefaultCompress](/vendor/github.com/go-chi/chi/middleware/compress.go#L38) +- [Timeout.func1](/vendor/github.com/go-chi/chi/middleware/timeout.go#L33) +- [RequestLogger.func1](/vendor/github.com/go-chi/chi/middleware/logger.go#L36) +- [SetContentType.func1](/vendor/github.com/go-chi/render/content_type.go#L49) +- **/*** + - _GET_ + - [SPAHandler.func1](/api/api.go#L101) + +
+
+`/admin/*` + +- [Recoverer](/vendor/github.com/go-chi/chi/middleware/recoverer.go#L18) +- [RequestID](/vendor/github.com/go-chi/chi/middleware/request_id.go#L63) +- [DefaultCompress](/vendor/github.com/go-chi/chi/middleware/compress.go#L38) +- [Timeout.func1](/vendor/github.com/go-chi/chi/middleware/timeout.go#L33) +- [RequestLogger.func1](/vendor/github.com/go-chi/chi/middleware/logger.go#L36) +- [SetContentType.func1](/vendor/github.com/go-chi/render/content_type.go#L49) +- **/admin/*** + - [RequiresRole.func1](/auth/authorizer.go#L11) + - **/** + - _GET_ + - [(*API).Router.func1](/api/admin/api.go#L42) + +
+
+`/admin/*/accounts/*` + +- [Recoverer](/vendor/github.com/go-chi/chi/middleware/recoverer.go#L18) +- [RequestID](/vendor/github.com/go-chi/chi/middleware/request_id.go#L63) +- [DefaultCompress](/vendor/github.com/go-chi/chi/middleware/compress.go#L38) +- [Timeout.func1](/vendor/github.com/go-chi/chi/middleware/timeout.go#L33) +- [RequestLogger.func1](/vendor/github.com/go-chi/chi/middleware/logger.go#L36) +- [SetContentType.func1](/vendor/github.com/go-chi/render/content_type.go#L49) +- **/admin/*** + - [RequiresRole.func1](/auth/authorizer.go#L11) + - **/accounts/*** + - **/** + - _GET_ + - [(*AccountResource).(github.com/dhax/go-base/api/admin.list)-fm](/api/admin/accounts.go#L50) + - _POST_ + - [(*AccountResource).(github.com/dhax/go-base/api/admin.create)-fm](/api/admin/accounts.go#L51) + +
+
+`/admin/*/accounts/*/{accountID}/*` + +- [Recoverer](/vendor/github.com/go-chi/chi/middleware/recoverer.go#L18) +- [RequestID](/vendor/github.com/go-chi/chi/middleware/request_id.go#L63) +- [DefaultCompress](/vendor/github.com/go-chi/chi/middleware/compress.go#L38) +- [Timeout.func1](/vendor/github.com/go-chi/chi/middleware/timeout.go#L33) +- [RequestLogger.func1](/vendor/github.com/go-chi/chi/middleware/logger.go#L36) +- [SetContentType.func1](/vendor/github.com/go-chi/render/content_type.go#L49) +- **/admin/*** + - [RequiresRole.func1](/auth/authorizer.go#L11) + - **/accounts/*** + - **/{accountID}/*** + - [(*AccountResource).(github.com/dhax/go-base/api/admin.accountCtx)-fm](/api/admin/accounts.go#L53) + - **/** + - _PUT_ + - [(*AccountResource).(github.com/dhax/go-base/api/admin.update)-fm](/api/admin/accounts.go#L55) + - _DELETE_ + - [(*AccountResource).(github.com/dhax/go-base/api/admin.delete)-fm](/api/admin/accounts.go#L56) + - _GET_ + - [(*AccountResource).(github.com/dhax/go-base/api/admin.get)-fm](/api/admin/accounts.go#L54) + +
+
+`/api/*/account/*` + +- [Recoverer](/vendor/github.com/go-chi/chi/middleware/recoverer.go#L18) +- [RequestID](/vendor/github.com/go-chi/chi/middleware/request_id.go#L63) +- [DefaultCompress](/vendor/github.com/go-chi/chi/middleware/compress.go#L38) +- [Timeout.func1](/vendor/github.com/go-chi/chi/middleware/timeout.go#L33) +- [RequestLogger.func1](/vendor/github.com/go-chi/chi/middleware/logger.go#L36) +- [SetContentType.func1](/vendor/github.com/go-chi/render/content_type.go#L49) +- **/api/*** + - **/account/*** + - [(*AccountResource).(github.com/dhax/go-base/api/app.accountCtx)-fm](/api/app/account.go#L48) + - **/** + - _PUT_ + - [(*AccountResource).(github.com/dhax/go-base/api/app.update)-fm](/api/app/account.go#L50) + - _DELETE_ + - [(*AccountResource).(github.com/dhax/go-base/api/app.delete)-fm](/api/app/account.go#L51) + - _GET_ + - [(*AccountResource).(github.com/dhax/go-base/api/app.get)-fm](/api/app/account.go#L49) + +
+
+`/api/*/account/*/profile` + +- [Recoverer](/vendor/github.com/go-chi/chi/middleware/recoverer.go#L18) +- [RequestID](/vendor/github.com/go-chi/chi/middleware/request_id.go#L63) +- [DefaultCompress](/vendor/github.com/go-chi/chi/middleware/compress.go#L38) +- [Timeout.func1](/vendor/github.com/go-chi/chi/middleware/timeout.go#L33) +- [RequestLogger.func1](/vendor/github.com/go-chi/chi/middleware/logger.go#L36) +- [SetContentType.func1](/vendor/github.com/go-chi/render/content_type.go#L49) +- **/api/*** + - **/account/*** + - [(*AccountResource).(github.com/dhax/go-base/api/app.accountCtx)-fm](/api/app/account.go#L48) + - **/profile** + - _PUT_ + - [(*AccountResource).(github.com/dhax/go-base/api/app.updateProfile)-fm](/api/app/account.go#L56) + +
+
+`/api/*/account/*/token/{tokenID}/*` + +- [Recoverer](/vendor/github.com/go-chi/chi/middleware/recoverer.go#L18) +- [RequestID](/vendor/github.com/go-chi/chi/middleware/request_id.go#L63) +- [DefaultCompress](/vendor/github.com/go-chi/chi/middleware/compress.go#L38) +- [Timeout.func1](/vendor/github.com/go-chi/chi/middleware/timeout.go#L33) +- [RequestLogger.func1](/vendor/github.com/go-chi/chi/middleware/logger.go#L36) +- [SetContentType.func1](/vendor/github.com/go-chi/render/content_type.go#L49) +- **/api/*** + - **/account/*** + - [(*AccountResource).(github.com/dhax/go-base/api/app.accountCtx)-fm](/api/app/account.go#L48) + - **/token/{tokenID}/*** + - **/** + - _PUT_ + - [(*AccountResource).(github.com/dhax/go-base/api/app.updateToken)-fm](/api/app/account.go#L53) + - _DELETE_ + - [(*AccountResource).(github.com/dhax/go-base/api/app.deleteToken)-fm](/api/app/account.go#L54) + +
+
+`/auth/*/login` + +- [Recoverer](/vendor/github.com/go-chi/chi/middleware/recoverer.go#L18) +- [RequestID](/vendor/github.com/go-chi/chi/middleware/request_id.go#L63) +- [DefaultCompress](/vendor/github.com/go-chi/chi/middleware/compress.go#L38) +- [Timeout.func1](/vendor/github.com/go-chi/chi/middleware/timeout.go#L33) +- [RequestLogger.func1](/vendor/github.com/go-chi/chi/middleware/logger.go#L36) +- [SetContentType.func1](/vendor/github.com/go-chi/render/content_type.go#L49) +- **/auth/*** + - [SetContentType.func1](/vendor/github.com/go-chi/render/content_type.go#L49) + - **/login** + - _POST_ + - [(*Resource).(github.com/dhax/go-base/auth.login)-fm](/auth/api.go#L67) + +
+
+`/auth/*/logout` + +- [Recoverer](/vendor/github.com/go-chi/chi/middleware/recoverer.go#L18) +- [RequestID](/vendor/github.com/go-chi/chi/middleware/request_id.go#L63) +- [DefaultCompress](/vendor/github.com/go-chi/chi/middleware/compress.go#L38) +- [Timeout.func1](/vendor/github.com/go-chi/chi/middleware/timeout.go#L33) +- [RequestLogger.func1](/vendor/github.com/go-chi/chi/middleware/logger.go#L36) +- [SetContentType.func1](/vendor/github.com/go-chi/render/content_type.go#L49) +- **/auth/*** + - [SetContentType.func1](/vendor/github.com/go-chi/render/content_type.go#L49) + - **/logout** + - _POST_ + - [Verifier.func1](/vendor/github.com/go-chi/jwtauth/jwtauth.go#L70) + - [AuthenticateRefreshJWT](/auth/authenticator.go#L66) + - [(*Resource).(github.com/dhax/go-base/auth.logout)-fm](/auth/api.go#L73) + +
+
+`/auth/*/refresh` + +- [Recoverer](/vendor/github.com/go-chi/chi/middleware/recoverer.go#L18) +- [RequestID](/vendor/github.com/go-chi/chi/middleware/request_id.go#L63) +- [DefaultCompress](/vendor/github.com/go-chi/chi/middleware/compress.go#L38) +- [Timeout.func1](/vendor/github.com/go-chi/chi/middleware/timeout.go#L33) +- [RequestLogger.func1](/vendor/github.com/go-chi/chi/middleware/logger.go#L36) +- [SetContentType.func1](/vendor/github.com/go-chi/render/content_type.go#L49) +- **/auth/*** + - [SetContentType.func1](/vendor/github.com/go-chi/render/content_type.go#L49) + - **/refresh** + - _POST_ + - [Verifier.func1](/vendor/github.com/go-chi/jwtauth/jwtauth.go#L70) + - [AuthenticateRefreshJWT](/auth/authenticator.go#L66) + - [(*Resource).(github.com/dhax/go-base/auth.refresh)-fm](/auth/api.go#L72) + +
+
+`/auth/*/token` + +- [Recoverer](/vendor/github.com/go-chi/chi/middleware/recoverer.go#L18) +- [RequestID](/vendor/github.com/go-chi/chi/middleware/request_id.go#L63) +- [DefaultCompress](/vendor/github.com/go-chi/chi/middleware/compress.go#L38) +- [Timeout.func1](/vendor/github.com/go-chi/chi/middleware/timeout.go#L33) +- [RequestLogger.func1](/vendor/github.com/go-chi/chi/middleware/logger.go#L36) +- [SetContentType.func1](/vendor/github.com/go-chi/render/content_type.go#L49) +- **/auth/*** + - [SetContentType.func1](/vendor/github.com/go-chi/render/content_type.go#L49) + - **/token** + - _POST_ + - [(*Resource).(github.com/dhax/go-base/auth.token)-fm](/auth/api.go#L68) + +
+
+`/ping` + +- [Recoverer](/vendor/github.com/go-chi/chi/middleware/recoverer.go#L18) +- [RequestID](/vendor/github.com/go-chi/chi/middleware/request_id.go#L63) +- [DefaultCompress](/vendor/github.com/go-chi/chi/middleware/compress.go#L38) +- [Timeout.func1](/vendor/github.com/go-chi/chi/middleware/timeout.go#L33) +- [RequestLogger.func1](/vendor/github.com/go-chi/chi/middleware/logger.go#L36) +- [SetContentType.func1](/vendor/github.com/go-chi/render/content_type.go#L49) +- **/ping** + - _GET_ + - [NewAPI.func2](/api/api.go#L73) + +
+ +Total # of routes: 12 diff --git a/vendor/github.com/go-chi/docgen/.travis.yml b/vendor/github.com/go-chi/docgen/.travis.yml new file mode 100644 index 0000000..a026204 --- /dev/null +++ b/vendor/github.com/go-chi/docgen/.travis.yml @@ -0,0 +1,15 @@ +language: go + +go: + - 1.7.x + - 1.8.x + - tip + +install: + - go get -u golang.org/x/tools/cmd/goimports + +script: + - go get -d -t ./... + - go test ./... + - > + goimports -d -e ./ | grep '.*' && { echo; echo "Aborting due to non-empty goimports output."; exit 1; } || : diff --git a/vendor/github.com/go-chi/docgen/LICENSE b/vendor/github.com/go-chi/docgen/LICENSE new file mode 100644 index 0000000..4344db7 --- /dev/null +++ b/vendor/github.com/go-chi/docgen/LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2016-Present https://github.com/go-chi authors + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/go-chi/docgen/builder.go b/vendor/github.com/go-chi/docgen/builder.go new file mode 100644 index 0000000..7b396a3 --- /dev/null +++ b/vendor/github.com/go-chi/docgen/builder.go @@ -0,0 +1,127 @@ +package docgen + +import ( + "errors" + "fmt" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/go-chi/chi" +) + +func BuildDoc(r chi.Routes) (Doc, error) { + d := Doc{} + + goPath := os.Getenv("GOPATH") + if goPath == "" { + return d, errors.New("docgen: unable to determine your $GOPATH") + } + + // Walk and generate the router docs + d.Router = buildDocRouter(r) + return d, nil +} + +func buildDocRouter(r chi.Routes) DocRouter { + rts := r + dr := DocRouter{Middlewares: []DocMiddleware{}} + drts := DocRoutes{} + dr.Routes = drts + + for _, mw := range rts.Middlewares() { + dmw := DocMiddleware{ + FuncInfo: buildFuncInfo(mw), + } + dr.Middlewares = append(dr.Middlewares, dmw) + } + + for _, rt := range rts.Routes() { + drt := DocRoute{Pattern: rt.Pattern, Handlers: DocHandlers{}} + + if rt.SubRoutes != nil { + subRoutes := rt.SubRoutes + subDrts := buildDocRouter(subRoutes) + drt.Router = &subDrts + + } else { + hall := rt.Handlers["*"] + for method, h := range rt.Handlers { + if method != "*" && hall != nil && fmt.Sprintf("%v", hall) == fmt.Sprintf("%v", h) { + continue + } + + dh := DocHandler{Method: method, Middlewares: []DocMiddleware{}} + + var endpoint http.Handler + chain, _ := h.(*chi.ChainHandler) + + if chain != nil { + for _, mw := range chain.Middlewares { + dh.Middlewares = append(dh.Middlewares, DocMiddleware{ + FuncInfo: buildFuncInfo(mw), + }) + } + endpoint = chain.Endpoint + } else { + endpoint = h + } + + dh.FuncInfo = buildFuncInfo(endpoint) + + drt.Handlers[method] = dh + } + } + + drts[rt.Pattern] = drt + } + + return dr +} + +func buildFuncInfo(i interface{}) FuncInfo { + fi := FuncInfo{} + frame := getCallerFrame(i) + goPathSrc := filepath.Join(os.Getenv("GOPATH"), "src") + + if frame == nil { + fi.Unresolvable = true + return fi + } + + pkgName := getPkgName(frame.File) + if pkgName == "chi" { + fi.Unresolvable = true + } + funcPath := frame.Func.Name() + + idx := strings.Index(funcPath, "/"+pkgName) + if idx > 0 { + fi.Pkg = funcPath[:idx+1+len(pkgName)] + fi.Func = funcPath[idx+2+len(pkgName):] + } else { + fi.Func = funcPath + } + + if strings.Index(fi.Func, ".func") > 0 { + fi.Anonymous = true + } + + fi.File = frame.File + fi.Line = frame.Line + if filepath.HasPrefix(fi.File, goPathSrc) { + fi.File = fi.File[len(goPathSrc)+1:] + } + + // Check if file info is unresolvable + if !strings.Contains(funcPath, pkgName) { + fi.Unresolvable = true + } + + if !fi.Unresolvable { + fi.Comment = getFuncComment(frame.File, frame.Line) + } + + return fi +} diff --git a/vendor/github.com/go-chi/docgen/docgen.go b/vendor/github.com/go-chi/docgen/docgen.go new file mode 100644 index 0000000..66d5a36 --- /dev/null +++ b/vendor/github.com/go-chi/docgen/docgen.go @@ -0,0 +1,64 @@ +package docgen + +import ( + "encoding/json" + "fmt" + + "github.com/go-chi/chi" +) + +type Doc struct { + Router DocRouter `json:"router"` +} + +type DocRouter struct { + Middlewares []DocMiddleware `json:"middlewares"` + Routes DocRoutes `json:"routes"` +} + +type DocMiddleware struct { + FuncInfo +} + +type DocRoute struct { + Pattern string `json:"-"` + Handlers DocHandlers `json:"handlers,omitempty"` + Router *DocRouter `json:"router,omitempty"` +} + +type DocRoutes map[string]DocRoute // Pattern : DocRoute + +type DocHandler struct { + Middlewares []DocMiddleware `json:"middlewares"` + Method string `json:"method"` + FuncInfo +} + +type DocHandlers map[string]DocHandler // Method : DocHandler + +func PrintRoutes(r chi.Routes) { + var printRoutes func(parentPattern string, r chi.Routes) + printRoutes = func(parentPattern string, r chi.Routes) { + rts := r.Routes() + for _, rt := range rts { + if rt.SubRoutes == nil { + fmt.Println(parentPattern + rt.Pattern) + } else { + pat := rt.Pattern + + subRoutes := rt.SubRoutes + printRoutes(parentPattern+pat, subRoutes) + } + } + } + printRoutes("", r) +} + +func JSONRoutesDoc(r chi.Routes) string { + doc, _ := BuildDoc(r) + v, err := json.MarshalIndent(doc, "", " ") + if err != nil { + panic(err) + } + return string(v) +} diff --git a/vendor/github.com/go-chi/docgen/docgen_test.go b/vendor/github.com/go-chi/docgen/docgen_test.go new file mode 100644 index 0000000..a7d2802 --- /dev/null +++ b/vendor/github.com/go-chi/docgen/docgen_test.go @@ -0,0 +1,181 @@ +package docgen_test + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/go-chi/chi" + "github.com/go-chi/docgen" +) + +// RequestID comment goes here. +func RequestID(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), "requestID", "1") + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} + +func hubIndexHandler(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + s := fmt.Sprintf("/hubs/%s reqid:%s session:%s", + chi.URLParam(r, "hubID"), ctx.Value("requestID"), ctx.Value("session.user")) + w.Write([]byte(s)) +} + +// Generate docs for the MuxBig from chi/mux_test.go +func TestMuxBig(t *testing.T) { + var r, sr1, sr2, sr3, sr4, sr5, sr6 *chi.Mux + r = chi.NewRouter() + r.Use(RequestID) + + // Some inline middleware, 1 + // We just love Go's ast tools + r.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + next.ServeHTTP(w, r) + }) + }) + r.Group(func(r chi.Router) { + r.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), "session.user", "anonymous") + next.ServeHTTP(w, r.WithContext(ctx)) + }) + }) + r.Get("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("fav")) + }) + r.Get("/hubs/{hubID}/view", func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + s := fmt.Sprintf("/hubs/%s/view reqid:%s session:%s", chi.URLParam(r, "hubID"), + ctx.Value("requestID"), ctx.Value("session.user")) + w.Write([]byte(s)) + }) + r.Get("/hubs/{hubID}/view/*", func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + s := fmt.Sprintf("/hubs/%s/view/%s reqid:%s session:%s", chi.URLParamFromCtx(ctx, "hubID"), + chi.URLParam(r, "*"), ctx.Value("requestID"), ctx.Value("session.user")) + w.Write([]byte(s)) + }) + }) + r.Group(func(r chi.Router) { + r.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), "session.user", "elvis") + next.ServeHTTP(w, r.WithContext(ctx)) + }) + }) + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + s := fmt.Sprintf("/ reqid:%s session:%s", ctx.Value("requestID"), ctx.Value("session.user")) + w.Write([]byte(s)) + }) + r.Get("/suggestions", func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + s := fmt.Sprintf("/suggestions reqid:%s session:%s", ctx.Value("requestID"), ctx.Value("session.user")) + w.Write([]byte(s)) + }) + + r.Get("/woot/{wootID}/*", func(w http.ResponseWriter, r *http.Request) { + s := fmt.Sprintf("/woot/%s/%s", chi.URLParam(r, "wootID"), chi.URLParam(r, "*")) + w.Write([]byte(s)) + }) + + r.Route("/hubs", func(r chi.Router) { + sr1 = r.(*chi.Mux) + r.Use(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + next.ServeHTTP(w, r) + }) + }) + r.Route("/{hubID}", func(r chi.Router) { + sr2 = r.(*chi.Mux) + r.Get("/", hubIndexHandler) + r.Get("/touch", func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + s := fmt.Sprintf("/hubs/%s/touch reqid:%s session:%s", chi.URLParam(r, "hubID"), + ctx.Value("requestID"), ctx.Value("session.user")) + w.Write([]byte(s)) + }) + + sr3 = chi.NewRouter() + sr3.Get("/", func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + s := fmt.Sprintf("/hubs/%s/webhooks reqid:%s session:%s", chi.URLParam(r, "hubID"), + ctx.Value("requestID"), ctx.Value("session.user")) + w.Write([]byte(s)) + }) + sr3.Route("/{webhookID}", func(r chi.Router) { + sr4 = r.(*chi.Mux) + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + s := fmt.Sprintf("/hubs/%s/webhooks/%s reqid:%s session:%s", chi.URLParam(r, "hubID"), + chi.URLParam(r, "webhookID"), ctx.Value("requestID"), ctx.Value("session.user")) + w.Write([]byte(s)) + }) + }) + + // TODO: /webooks is not coming up as a subrouter here... + // we kind of want to wrap a Router... ? + // perhaps add .Router() to the middleware inline thing.. + // and use that always.. or, can detect in that method.. + r.Mount("/webhooks", chi.Chain(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), "hook", true))) + }) + }).Handler(sr3)) + + // HMMMM.. only let Mount() for just a Router..? + // r.Mount("/webhooks", Use(...).Router(sr3)) + // ... could this work even....? + + // HMMMMMMMMMMMMMMMMMMMMMMMM... + // even if Mount() were to record all subhandlers mounted, we still couldn't get at the + // routes + + r.Route("/posts", func(r chi.Router) { + sr5 = r.(*chi.Mux) + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + s := fmt.Sprintf("/hubs/%s/posts reqid:%s session:%s", chi.URLParam(r, "hubID"), + ctx.Value("requestID"), ctx.Value("session.user")) + w.Write([]byte(s)) + }) + }) + }) + }) + + r.Route("/folders/", func(r chi.Router) { + sr6 = r.(*chi.Mux) + r.Get("/", func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + s := fmt.Sprintf("/folders/ reqid:%s session:%s", + ctx.Value("requestID"), ctx.Value("session.user")) + w.Write([]byte(s)) + }) + r.Get("/public", func(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + s := fmt.Sprintf("/folders/public reqid:%s session:%s", + ctx.Value("requestID"), ctx.Value("session.user")) + w.Write([]byte(s)) + }) + r.Get("/in", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}).ServeHTTP) + + r.With(func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), "search", true))) + }) + }).Get("/search", func(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("searching..")) + }) + }) + }) + + fmt.Println(docgen.JSONRoutesDoc(r)) + + // docgen.PrintRoutes(r) + +} diff --git a/vendor/github.com/go-chi/docgen/funcinfo.go b/vendor/github.com/go-chi/docgen/funcinfo.go new file mode 100644 index 0000000..66612da --- /dev/null +++ b/vendor/github.com/go-chi/docgen/funcinfo.go @@ -0,0 +1,113 @@ +package docgen + +import ( + "go/parser" + "go/token" + "os" + "path/filepath" + "reflect" + "runtime" + "strings" +) + +type FuncInfo struct { + Pkg string `json:"pkg"` + Func string `json:"func"` + Comment string `json:"comment"` + File string `json:"file,omitempty"` + Line int `json:"line,omitempty"` + Anonymous bool `json:"anonymous,omitempty"` + Unresolvable bool `json:"unresolvable,omitempty"` +} + +func GetFuncInfo(i interface{}) FuncInfo { + fi := FuncInfo{} + frame := getCallerFrame(i) + goPathSrc := filepath.Join(os.Getenv("GOPATH"), "src") + + if frame == nil { + fi.Unresolvable = true + return fi + } + + pkgName := getPkgName(frame.File) + if pkgName == "chi" { + fi.Unresolvable = true + } + funcPath := frame.Func.Name() + + idx := strings.Index(funcPath, "/"+pkgName) + if idx > 0 { + fi.Pkg = funcPath[:idx+1+len(pkgName)] + fi.Func = funcPath[idx+2+len(pkgName):] + } else { + fi.Func = funcPath + } + + if strings.Index(fi.Func, ".func") > 0 { + fi.Anonymous = true + } + + fi.File = frame.File + fi.Line = frame.Line + if filepath.HasPrefix(fi.File, goPathSrc) { + fi.File = fi.File[len(goPathSrc)+1:] + } + + // Check if file info is unresolvable + if strings.Index(funcPath, pkgName) < 0 { + fi.Unresolvable = true + } + + if !fi.Unresolvable { + fi.Comment = getFuncComment(frame.File, frame.Line) + } + + return fi +} + +func getCallerFrame(i interface{}) *runtime.Frame { + pc := reflect.ValueOf(i).Pointer() + frames := runtime.CallersFrames([]uintptr{pc}) + if frames == nil { + return nil + } + frame, _ := frames.Next() + if frame.Entry == 0 { + return nil + } + return &frame +} + +func getPkgName(file string) string { + fset := token.NewFileSet() + astFile, err := parser.ParseFile(fset, file, nil, parser.PackageClauseOnly) + if err != nil { + return "" + } + if astFile.Name == nil { + return "" + } + return astFile.Name.Name +} + +func getFuncComment(file string, line int) string { + fset := token.NewFileSet() + + astFile, err := parser.ParseFile(fset, file, nil, parser.ParseComments) + if err != nil { + return "" + } + + if len(astFile.Comments) == 0 { + return "" + } + + for _, cmt := range astFile.Comments { + if fset.Position(cmt.End()).Line+1 == line { + return cmt.Text() + } + } + + return "" +} diff --git a/vendor/github.com/go-chi/docgen/markdown.go b/vendor/github.com/go-chi/docgen/markdown.go new file mode 100644 index 0000000..ec4371e --- /dev/null +++ b/vendor/github.com/go-chi/docgen/markdown.go @@ -0,0 +1,211 @@ +package docgen + +import ( + "bytes" + "errors" + "fmt" + "sort" + "strings" + + "github.com/go-chi/chi" +) + +type MarkdownDoc struct { + Opts MarkdownOpts + Router chi.Router + Doc Doc + Routes map[string]DocRouter // Pattern : DocRouter + + buf *bytes.Buffer +} + +type MarkdownOpts struct { + // ProjectPath is the base Go import path of the project + ProjectPath string + + // Intro text included at the top of the generated markdown file. + Intro string + + // ForceRelativeLinks to be relative even if they're not on github + ForceRelativeLinks bool + + // URLMap allows specifying a map of package import paths to their link sources + // Used for mapping vendored dependencies to their upstream sources + // For example: + // map[string]string{"github.com/my/package/vendor/go-chi/chi/": "https://github.com/go-chi/chi/blob/master/"} + URLMap map[string]string +} + +func MarkdownRoutesDoc(r chi.Router, opts MarkdownOpts) string { + md := &MarkdownDoc{Router: r, Opts: opts} + if err := md.Generate(); err != nil { + return fmt.Sprintf("ERROR: %s\n", err.Error()) + } + return md.String() +} + +func (md *MarkdownDoc) String() string { + return md.buf.String() +} + +func (md *MarkdownDoc) Generate() error { + if md.Router == nil { + return errors.New("docgen: router is nil") + } + + doc, err := BuildDoc(md.Router) + if err != nil { + return err + } + + md.Doc = doc + md.buf = &bytes.Buffer{} + md.Routes = make(map[string]DocRouter) + + md.WriteIntro() + md.WriteRoutes() + + return nil +} + +func (md *MarkdownDoc) WriteIntro() { + pkgName := md.Opts.ProjectPath + md.buf.WriteString(fmt.Sprintf("# %s\n\n", pkgName)) + + intro := md.Opts.Intro + md.buf.WriteString(fmt.Sprintf("%s\n\n", intro)) +} + +func (md *MarkdownDoc) WriteRoutes() { + md.buf.WriteString(fmt.Sprintf("## Routes\n\n")) + + var buildRoutesMap func(parentPattern string, ar, nr, dr *DocRouter) + buildRoutesMap = func(parentPattern string, ar, nr, dr *DocRouter) { + + nr.Middlewares = append(nr.Middlewares, dr.Middlewares...) + + for pat, rt := range dr.Routes { + pattern := parentPattern + pat + + nr.Routes = DocRoutes{} + + if rt.Router != nil { + nnr := &DocRouter{} + nr.Routes[pat] = DocRoute{ + Pattern: pat, + Handlers: rt.Handlers, + Router: nnr, + } + buildRoutesMap(pattern, ar, nnr, rt.Router) + + } else if len(rt.Handlers) > 0 { + nr.Routes[pat] = DocRoute{ + Pattern: pat, + Handlers: rt.Handlers, + Router: nil, + } + + // Remove the trailing slash if the handler is a subroute for "/" + routeKey := pattern + if pat == "/" && len(routeKey) > 1 { + routeKey = routeKey[:len(routeKey)-1] + } + md.Routes[routeKey] = copyDocRouter(*ar) + + } else { + panic("not possible") + } + } + + } + + // Build a route tree that consists of the full route pattern + // and the part of the tree for just that specific route, stored + // in routes map on the markdown struct. This is the structure we + // are going to render to markdown. + dr := md.Doc.Router + ar := DocRouter{} + buildRoutesMap("", &ar, &ar, &dr) + + // Generate the markdown to render the above structure + var printRouter func(depth int, dr DocRouter) + printRouter = func(depth int, dr DocRouter) { + + tabs := "" + for i := 0; i < depth; i++ { + tabs += "\t" + } + + // Middlewares + for _, mw := range dr.Middlewares { + md.buf.WriteString(fmt.Sprintf("%s- [%s](%s)\n", tabs, mw.Func, md.githubSourceURL(mw.File, mw.Line))) + } + + // Routes + for _, rt := range dr.Routes { + md.buf.WriteString(fmt.Sprintf("%s- **%s**\n", tabs, rt.Pattern)) + + if rt.Router != nil { + printRouter(depth+1, *rt.Router) + } else { + for meth, dh := range rt.Handlers { + md.buf.WriteString(fmt.Sprintf("%s\t- _%s_\n", tabs, meth)) + + // Handler middlewares + for _, mw := range dh.Middlewares { + md.buf.WriteString(fmt.Sprintf("%s\t\t- [%s](%s)\n", tabs, mw.Func, md.githubSourceURL(mw.File, mw.Line))) + } + + // Handler endpoint + md.buf.WriteString(fmt.Sprintf("%s\t\t- [%s](%s)\n", tabs, dh.Func, md.githubSourceURL(dh.File, dh.Line))) + } + } + } + } + + routePaths := []string{} + for pat := range md.Routes { + routePaths = append(routePaths, pat) + } + sort.Strings(routePaths) + + for _, pat := range routePaths { + dr := md.Routes[pat] + md.buf.WriteString(fmt.Sprintf("
\n")) + md.buf.WriteString(fmt.Sprintf("`%s`\n", pat)) + md.buf.WriteString(fmt.Sprintf("\n")) + printRouter(0, dr) + md.buf.WriteString(fmt.Sprintf("\n")) + md.buf.WriteString(fmt.Sprintf("
\n")) + } + + md.buf.WriteString(fmt.Sprintf("\n")) + md.buf.WriteString(fmt.Sprintf("Total # of routes: %d\n", len(md.Routes))) + + // TODO: total number of handlers.. +} + +func (md *MarkdownDoc) githubSourceURL(file string, line int) string { + // Currently, we only automatically link to source for github projects + if strings.Index(file, "github.com/") != 0 && !md.Opts.ForceRelativeLinks { + return "" + } + if md.Opts.ProjectPath == "" { + return "" + } + for pkg, url := range md.Opts.URLMap { + if idx := strings.Index(file, pkg); idx >= 0 { + pos := idx + len(pkg) + url = strings.TrimRight(url, "/") + filepath := strings.TrimLeft(file[pos:], "/") + return fmt.Sprintf("%s/%s#L%d", url, filepath, line) + } + } + if idx := strings.Index(file, md.Opts.ProjectPath); idx >= 0 { + // relative + pos := idx + len(md.Opts.ProjectPath) + return fmt.Sprintf("%s#L%d", file[pos:], line) + } + // absolute + return fmt.Sprintf("https://%s#L%d", file, line) +} diff --git a/vendor/github.com/go-chi/docgen/raml/raml.go b/vendor/github.com/go-chi/docgen/raml/raml.go new file mode 100644 index 0000000..14dcaa8 --- /dev/null +++ b/vendor/github.com/go-chi/docgen/raml/raml.go @@ -0,0 +1,159 @@ +package raml + +import ( + "errors" + "fmt" + "strings" + + yaml "gopkg.in/yaml.v2" +) + +var header = `#%RAML 1.0 +--- +` + +type RAML struct { + Title string `yaml:"title,omitempty"` + BaseUri string `yaml:"baseUri,omitempty"` + Protocols []string `yaml:"protocols,omitempty"` + MediaType string `yaml:"mediaType,omitempty"` + Version string `yaml:"version,omitempty"` + Documentation []Documentation `yaml:"documentation,omitempty"` + + Resources `yaml:",inline"` +} + +func (r *RAML) String() string { + bytes, _ := yaml.Marshal(r) + return fmt.Sprintf("%s%s", header, bytes) +} + +type Documentation struct { + Title string `yaml:"title"` + Content string `yaml:"content"` +} + +type Resources map[string]*Resource + +type Resource struct { + DisplayName string `yaml:"displayName,omitempty"` + Description string `yaml:"description,omitempty"` + Responses Responses `yaml:"responses,omitempty"` + Body Body `yaml:"body,omitempty"` + Is []string `yaml:"is,omitempty"` + Type string `yaml:"type,omitempty"` + SecuredBy []string `yaml:"securedBy,omitempty"` + UriParameters []string `yaml:"uirParameters,omitempty"` + QueryParameters []string `yaml:"queryParameters,omitempty"` + + Resources `yaml:",inline"` +} + +type Responses map[int]Response + +type Response struct { + Body `yaml:"body,omitempty"` +} + +type Body map[string]Example // Content-Type to Example + +type Example struct { + Example string `yaml:"example,omitempty"` +} + +func (r *RAML) Add(method string, route string, resource *Resource) error { + if resource == nil { + return errors.New("raml.Add(): resource can't be nil") + } + if r.Resources == nil { + r.Resources = Resources{} + } + + return r.Resources.upsert(method, route, resource) +} + +func (r *RAML) AddUnder(parentRoute string, method string, route string, resource *Resource) error { + if resource == nil { + return errors.New("raml.Add(): resource can't be nil") + } + if r.Resources == nil { + r.Resources = Resources{} + } + + if parentRoute == "" || parentRoute == "/" { + return errors.New("raml.AddUnderParent(): parentRoute can't be empty or '/'") + } + + if !strings.HasPrefix(route, parentRoute) { + return errors.New("raml.AddUnderParent(): parentRoute must be present in the route string") + } + + route = strings.TrimPrefix(route, parentRoute) + if route == "" { + route = "/" + } + + parentNode, found := r.Resources[parentRoute] + if !found { + parentNode = &Resource{ + Resources: Resources{}, + Responses: Responses{}, + } + r.Resources[parentRoute] = parentNode + } + + return parentNode.Resources.upsert(method, route, resource) +} + +// Find or create node tree from a given route and inject the resource. +func (r Resources) upsert(method string, route string, resource *Resource) error { + currentNode := r + + parts := strings.Split(route, "/") + if len(parts) > 0 { + last := len(parts) - 1 + + // Upsert route of the resource. + for _, part := range parts[:last] { + if part == "" { + continue + } + part = "/" + part + + node, found := currentNode[part] + if !found { + node = &Resource{ + Resources: Resources{}, + Responses: Responses{}, + } + + currentNode[part] = node + } + currentNode = node.Resources + } + + if parts[last] != "" { + // Upsert resource into the very bottom of the node tree. + part := "/" + parts[last] + node, found := currentNode[part] + if !found { + node = &Resource{ + Resources: Resources{}, + Responses: Responses{}, + } + } + currentNode[part] = node + currentNode = node.Resources + } + } + + method = strings.ToLower(method) + if _, found := currentNode[method]; found { + return nil + // return fmt.Errorf("duplicated method route: %v %v", method, route) + } + + currentNode[method] = resource + + return nil +} diff --git a/vendor/github.com/go-chi/docgen/raml/raml_test.go b/vendor/github.com/go-chi/docgen/raml/raml_test.go new file mode 100644 index 0000000..fd221e6 --- /dev/null +++ b/vendor/github.com/go-chi/docgen/raml/raml_test.go @@ -0,0 +1,225 @@ +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.") +} diff --git a/vendor/github.com/go-chi/docgen/util.go b/vendor/github.com/go-chi/docgen/util.go new file mode 100644 index 0000000..03a2758 --- /dev/null +++ b/vendor/github.com/go-chi/docgen/util.go @@ -0,0 +1,37 @@ +package docgen + +func copyDocRouter(dr DocRouter) DocRouter { + var cloneRouter func(dr DocRouter) DocRouter + var cloneRoutes func(drt DocRoutes) DocRoutes + + cloneRoutes = func(drts DocRoutes) DocRoutes { + rts := DocRoutes{} + + for pat, drt := range drts { + rt := DocRoute{Pattern: drt.Pattern} + if len(drt.Handlers) > 0 { + rt.Handlers = DocHandlers{} + for meth, dh := range drt.Handlers { + rt.Handlers[meth] = dh + } + } + if drt.Router != nil { + rr := cloneRouter(*drt.Router) + rt.Router = &rr + } + rts[pat] = rt + } + + return rts + } + + cloneRouter = func(dr DocRouter) DocRouter { + cr := DocRouter{} + cr.Middlewares = make([]DocMiddleware, len(dr.Middlewares)) + copy(cr.Middlewares, dr.Middlewares) + cr.Routes = cloneRoutes(dr.Routes) + return cr + } + + return cloneRouter(dr) +}