├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── api.go ├── api_test.go ├── apidoc-test.html.json ├── apidoc.go ├── apidoc_test.go ├── default.tpl.html ├── example ├── basic │ ├── Makefile │ ├── main.go │ └── main_api_test.go └── gin │ ├── Makefile │ ├── README.md │ ├── custom-apidoc.html │ ├── custom-apidoc.html.json │ ├── custom.tpl.html │ ├── main.go │ ├── main_api_test.go │ └── view.v1.png ├── json.go ├── json_test.go ├── project.go └── project_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | !.gitkeep 2 | .DS_Store 3 | 4 | # Created by https://www.gitignore.io/api/go 5 | 6 | ### Go ### 7 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 8 | *.o 9 | *.a 10 | *.so 11 | 12 | # Folders 13 | _obj 14 | _test 15 | 16 | # Architecture specific extensions/prefixes 17 | *.[568vq] 18 | [568vq].out 19 | 20 | *.cgo1.go 21 | *.cgo2.c 22 | _cgo_defun.c 23 | _cgo_gotypes.go 24 | _cgo_export.* 25 | 26 | _testmain.go 27 | 28 | *.exe 29 | *.test 30 | *.prof -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 gotokatsuya 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | go test ./ -v 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # apidoc 2 | 3 | Automatic API Document Generator. 4 | 5 | ## Supporting web framework 6 | 7 | - gin 8 | 9 | ## Usage 10 | 11 | ### gin 12 | 13 | ```go 14 | type ginBodyWriter struct { 15 | gin.ResponseWriter 16 | body *bytes.Buffer 17 | } 18 | 19 | func (w ginBodyWriter) Body() []byte { 20 | return w.body.Bytes() 21 | } 22 | 23 | func (w ginBodyWriter) Write(b []byte) (int, error) { 24 | w.body.Write(b) 25 | return w.ResponseWriter.Write(b) 26 | } 27 | 28 | func (w ginBodyWriter) WriteString(s string) (int, error) { 29 | w.body.WriteString(s) 30 | return w.ResponseWriter.WriteString(s) 31 | } 32 | 33 | func newGinBodyWriter(w gin.ResponseWriter) *ginBodyWriter { 34 | return &ginBodyWriter{body: bytes.NewBufferString(""), ResponseWriter: w} 35 | } 36 | 37 | func apidocMiddleware(c *gin.Context) { 38 | if apidoc.IsDisabled() { 39 | return 40 | } 41 | 42 | api := apidoc.NewAPI() 43 | // Ignore header names 44 | api.SuppressedRequestHeaders("Cache-Control", "Content-Length", "X-Request-Id", "ETag", "Set-Cookie") 45 | api.ReadRequest(c.Request, false) 46 | 47 | // Need to implement own Writer intercepting Write(), WriteString() calls to get response body. 48 | gbw := newGinBodyWriter(c.Writer) 49 | c.Writer = gbw 50 | 51 | // before processing request 52 | 53 | c.Next() 54 | 55 | // after processing request 56 | 57 | // Ignore header names 58 | api.SuppressedResponseHeaders("Cache-Control", "Content-Length", "X-Request-Id", "X-Runtime", "X-XSS-Protection", "ETag") 59 | api.ReadResponseHeader(c.Writer.Header()) 60 | api.WrapResponseBody(gbw.Body()) 61 | api.ResponseStatusCode = c.Writer.Status() 62 | 63 | apidoc.Gen(api) 64 | } 65 | 66 | func getEngine() *gin.Engine { 67 | r := gin.Default() 68 | r.Use(apidocMiddleware) 69 | 70 | ... 71 | 72 | return r 73 | } 74 | 75 | func init() { 76 | d := flag.Bool("d", false, "disable api doc") 77 | flag.Parse() 78 | if *d { 79 | apidoc.Disable() 80 | } 81 | if apidoc.IsDisabled() { 82 | return 83 | } 84 | apidoc.Init(apidoc.Project{ 85 | DocumentTitle: "readme", 86 | DocumentPath: "readme-apidoc.html", 87 | TemplatePath: "readme.tpl.html", 88 | }) 89 | } 90 | 91 | func main() { 92 | // Listen and Server in 0.0.0.0:8080 93 | getEngine().Run(":8080") 94 | } 95 | ``` 96 | 97 | ## View 98 | 99 | ![view.png](https://github.com/gotokatsuya/apidoc/blob/master/example/gin/view.v1.png) 100 | 101 | https://gotokatsuya.github.io/apidoc/example/gin/custom-apidoc.html 102 | -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | package apidoc 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "io/ioutil" 7 | "log" 8 | "net/http" 9 | "net/http/httputil" 10 | "net/url" 11 | "strings" 12 | ) 13 | 14 | // API has request and response info 15 | type API struct { 16 | // Request 17 | RequestMethod string `json:"request_method"` 18 | RequestPath string `json:"request_path"` 19 | RequestHeaders map[string]string `json:"request_headers"` 20 | RequestSuppressedHeaders map[string]bool `json:"request_suppressed_headers"` 21 | RequestURLParams map[string]string `json:"request_url_params"` 22 | RequestPostForms map[string]string `json:"request_post_forms"` 23 | RequestBody string `json:"request_body"` 24 | 25 | // Response 26 | ResponseHeaders map[string]string `json:"response_headers"` 27 | ResponseSuppressedHeaders map[string]bool `json:"response_suppressed_headers"` 28 | ResponseStatusCode int `json:"response_status_code"` 29 | ResponseBody string `json:"response_body"` 30 | } 31 | 32 | // NewAPI new api instance 33 | func NewAPI() API { 34 | return API{ 35 | RequestHeaders: map[string]string{}, 36 | RequestURLParams: map[string]string{}, 37 | RequestPostForms: map[string]string{}, 38 | 39 | ResponseHeaders: map[string]string{}, 40 | } 41 | } 42 | 43 | func (a API) equal(a2 API) bool { 44 | return a.RequestMethod == a2.RequestMethod && a.RequestPath == a2.RequestPath && a.ResponseStatusCode == a2.ResponseStatusCode 45 | } 46 | 47 | // SuppressedRequestHeaders ignore request headers 48 | func (a *API) SuppressedRequestHeaders(headers ...string) { 49 | a.RequestSuppressedHeaders = make(map[string]bool, len(headers)) 50 | for _, header := range headers { 51 | a.RequestSuppressedHeaders[header] = true 52 | } 53 | } 54 | 55 | // ReadRequestHeader read request http.Header 56 | func (a *API) ReadRequestHeader(httpHeader http.Header) error { 57 | b := bytes.NewBuffer([]byte("")) 58 | if err := httpHeader.WriteSubset(b, a.RequestSuppressedHeaders); err != nil { 59 | return err 60 | } 61 | for _, header := range strings.Split(b.String(), "\n") { 62 | values := strings.Split(header, ":") 63 | if len(values) < 2 { 64 | continue 65 | } 66 | key := values[0] 67 | if key == "" { 68 | continue 69 | } 70 | a.RequestHeaders[key] = values[1] 71 | } 72 | return nil 73 | } 74 | 75 | // ReadRequestURLParams read request uri 76 | func (a *API) ReadRequestURLParams(uri string) error { 77 | u, err := url.Parse(uri) 78 | if err != nil { 79 | return err 80 | } 81 | for _, param := range strings.Split(u.Query().Encode(), "&") { 82 | values := strings.Split(param, "=") 83 | if len(values) < 2 { 84 | continue 85 | } 86 | key := values[0] 87 | if key == "" { 88 | continue 89 | } 90 | a.RequestURLParams[key] = values[1] 91 | } 92 | return nil 93 | } 94 | 95 | // Reference https://golang.org/src/net/http/httputil/dump.go 96 | func drainBody(b io.ReadCloser) (r1, r2 io.ReadCloser, err error) { 97 | var buf bytes.Buffer 98 | if _, err = buf.ReadFrom(b); err != nil { 99 | return nil, nil, err 100 | } 101 | if err = b.Close(); err != nil { 102 | return nil, nil, err 103 | } 104 | return ioutil.NopCloser(&buf), ioutil.NopCloser(bytes.NewReader(buf.Bytes())), nil 105 | } 106 | 107 | // ReadRequestBody read request body 108 | // Reference https://golang.org/src/net/http/httputil/dump.go 109 | func (a *API) ReadRequestBody(req *http.Request) error { 110 | var err error 111 | save := req.Body 112 | if req.Body == nil { 113 | req.Body = nil 114 | } else { 115 | save, req.Body, err = drainBody(req.Body) 116 | if err != nil { 117 | return err 118 | } 119 | } 120 | 121 | var b bytes.Buffer 122 | 123 | if req.Body != nil { 124 | chunked := len(req.TransferEncoding) > 0 && req.TransferEncoding[0] == "chunked" 125 | var dest io.Writer = &b 126 | if chunked { 127 | dest = httputil.NewChunkedWriter(dest) 128 | } 129 | _, err = io.Copy(dest, req.Body) 130 | if chunked { 131 | err = dest.(io.Closer).Close() 132 | } 133 | } 134 | 135 | req.Body = save 136 | if err != nil { 137 | return err 138 | } 139 | 140 | contentType, ok := a.RequestHeaders["Content-Type"] 141 | if !ok { 142 | return nil 143 | } 144 | ct := strings.TrimSpace(contentType) 145 | switch { 146 | case strings.Contains(ct, "application/x-www-form-urlencoded"): 147 | for _, param := range strings.Split(b.String(), "&") { 148 | values := strings.Split(param, "=") 149 | if len(values) < 2 { 150 | continue 151 | } 152 | key := values[0] 153 | if key == "" { 154 | continue 155 | } 156 | a.RequestPostForms[key] = values[1] 157 | } 158 | case strings.Contains(ct, "application/json"): 159 | out, err := PrettyPrint(b.Bytes()) 160 | if err != nil { 161 | return err 162 | } 163 | a.RequestBody = string(out) 164 | case strings.Contains(ct, "multipart/form-data"): 165 | // TODO handling multipart/form-data 166 | } 167 | return nil 168 | } 169 | 170 | func (a *API) getRequestURI(req *http.Request) string { 171 | reqURI := req.RequestURI 172 | if reqURI == "" { 173 | reqURI = req.URL.RequestURI() 174 | } 175 | return reqURI 176 | } 177 | 178 | // ReadRequest read values from http.Request 179 | func (a *API) ReadRequest(req *http.Request, throwErr bool) error { 180 | a.RequestMethod = req.Method 181 | a.RequestPath = strings.Split(a.getRequestURI(req), "?")[0] 182 | if err := a.ReadRequestHeader(req.Header); err != nil { 183 | if throwErr { 184 | return err 185 | } 186 | log.Println(err) 187 | } 188 | if err := a.ReadRequestURLParams(a.getRequestURI(req)); err != nil { 189 | if throwErr { 190 | return err 191 | } 192 | log.Println(err) 193 | } 194 | if err := a.ReadRequestBody(req); err != nil { 195 | if throwErr { 196 | return err 197 | } 198 | log.Println(err) 199 | } 200 | return nil 201 | } 202 | 203 | // SuppressedResponseHeaders ignore response headers 204 | func (a *API) SuppressedResponseHeaders(headers ...string) { 205 | a.ResponseSuppressedHeaders = make(map[string]bool, len(headers)) 206 | for _, header := range headers { 207 | a.ResponseSuppressedHeaders[header] = true 208 | } 209 | } 210 | 211 | // ReadResponseHeader read http.Header 212 | func (a *API) ReadResponseHeader(httpHeader http.Header) error { 213 | b := bytes.NewBuffer([]byte("")) 214 | if err := httpHeader.WriteSubset(b, a.ResponseSuppressedHeaders); err != nil { 215 | return err 216 | } 217 | for _, header := range strings.Split(b.String(), "\n") { 218 | values := strings.Split(header, ":") 219 | if len(values) < 2 { 220 | continue 221 | } 222 | key := values[0] 223 | if key == "" { 224 | continue 225 | } 226 | a.ResponseHeaders[key] = values[1] 227 | } 228 | return nil 229 | } 230 | 231 | // WrapResponseBody wrap body prettyprint if json 232 | func (a *API) WrapResponseBody(body []byte) error { 233 | contentType, ok := a.ResponseHeaders["Content-Type"] 234 | if ok && strings.Contains(strings.TrimSpace(contentType), "application/json") { 235 | prettyBody, err := PrettyPrint(body) 236 | if err != nil { 237 | return err 238 | } 239 | a.ResponseBody = string(prettyBody) 240 | return nil 241 | } 242 | 243 | a.ResponseBody = string(body) 244 | 245 | return nil 246 | } 247 | -------------------------------------------------------------------------------- /api_test.go: -------------------------------------------------------------------------------- 1 | package apidoc 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func TestSuppressedRequestHeaders(t *testing.T) { 11 | api := NewAPI() 12 | api.SuppressedRequestHeaders("Cache-Control") 13 | if !api.RequestSuppressedHeaders["Cache-Control"] { 14 | t.Fatal("Cache-Control is not set") 15 | } 16 | } 17 | 18 | func TestReadRequestHeader(t *testing.T) { 19 | api := NewAPI() 20 | var header http.Header = make(map[string][]string) 21 | header.Set("X-Name", "gotokatsuya") 22 | if err := api.ReadRequestHeader(header); err != nil { 23 | t.Fatal(err) 24 | } 25 | if strings.TrimSpace(api.RequestHeaders["X-Name"]) != "gotokatsuya" { 26 | t.Fatal("X-Name is not equal") 27 | } 28 | } 29 | 30 | func TestReadRequestURLParams(t *testing.T) { 31 | api := NewAPI() 32 | uri := "http://localhost:8080/hello?key=world" 33 | if err := api.ReadRequestURLParams(uri); err != nil { 34 | t.Fatal(err) 35 | } 36 | if api.RequestURLParams["key"] != "world" { 37 | t.Fatal("key is not equal") 38 | } 39 | } 40 | 41 | func TestSuppressedResponseHeaders(t *testing.T) { 42 | api := NewAPI() 43 | api.SuppressedResponseHeaders("Cache-Control") 44 | if !api.ResponseSuppressedHeaders["Cache-Control"] { 45 | t.Fatal("Cache-Control is not set") 46 | } 47 | } 48 | 49 | func TestReadResponseHeader(t *testing.T) { 50 | api := NewAPI() 51 | var header http.Header = make(map[string][]string) 52 | header.Set("X-Name", "gotokatsuya") 53 | if err := api.ReadResponseHeader(header); err != nil { 54 | t.Fatal(err) 55 | } 56 | if strings.TrimSpace(api.ResponseHeaders["X-Name"]) != "gotokatsuya" { 57 | t.Fatal("X-Name is not equal") 58 | } 59 | } 60 | 61 | func TestWrapResponseBody(t *testing.T) { 62 | api := NewAPI() 63 | 64 | type res struct { 65 | Name string `json:"name"` 66 | } 67 | r := res{ 68 | Name: "gotokatsuya", 69 | } 70 | in, err := json.Marshal(r) 71 | if err != nil { 72 | t.Fatal(err) 73 | } 74 | api.ResponseHeaders["Content-Type"] = "application/json" 75 | if err := api.WrapResponseBody(in); err != nil { 76 | t.Fatal(err) 77 | } 78 | t.Log(api.ResponseBody) 79 | } 80 | -------------------------------------------------------------------------------- /apidoc-test.html.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request_method": "GET", 4 | "request_path": "/users", 5 | "request_headers": { 6 | "Accept-Encoding": " gzip\r", 7 | "User-Agent": " Go-http-client/1.1\r" 8 | }, 9 | "request_suppressed_headers": { 10 | "Cache-Control": true, 11 | "Content-Length": true, 12 | "ETag": true, 13 | "Set-Cookie": true, 14 | "X-Request-Id": true 15 | }, 16 | "request_url_params": { 17 | "limit": "30" 18 | }, 19 | "request_post_forms": {}, 20 | "request_body": "", 21 | "response_headers": { 22 | "Content-Type": " application/json; charset=utf-8\r" 23 | }, 24 | "response_suppressed_headers": { 25 | "Cache-Control": true, 26 | "Content-Length": true, 27 | "ETag": true, 28 | "X-Request-Id": true, 29 | "X-Runtime": true, 30 | "X-XSS-Protection": true 31 | }, 32 | "response_status_code": 200, 33 | "response_body": "{\n \"limit\": \"30\",\n \"users\": [\n {\n \"id\": 1,\n \"name\": \"test1\"\n },\n {\n \"id\": 2,\n \"name\": \"test2\"\n }\n ]\n}\n" 34 | }, 35 | { 36 | "request_method": "GET", 37 | "request_path": "/users/1", 38 | "request_headers": { 39 | "Accept-Encoding": " gzip\r", 40 | "User-Agent": " Go-http-client/1.1\r" 41 | }, 42 | "request_suppressed_headers": { 43 | "Cache-Control": true, 44 | "Content-Length": true, 45 | "ETag": true, 46 | "Set-Cookie": true, 47 | "X-Request-Id": true 48 | }, 49 | "request_url_params": {}, 50 | "request_post_forms": {}, 51 | "request_body": "", 52 | "response_headers": { 53 | "Content-Type": " application/json; charset=utf-8\r" 54 | }, 55 | "response_suppressed_headers": { 56 | "Cache-Control": true, 57 | "Content-Length": true, 58 | "ETag": true, 59 | "X-Request-Id": true, 60 | "X-Runtime": true, 61 | "X-XSS-Protection": true 62 | }, 63 | "response_status_code": 200, 64 | "response_body": "{\n \"user\": {\n \"id\": 1,\n \"name\": \"test\"\n }\n}\n" 65 | }, 66 | { 67 | "request_method": "POST", 68 | "request_path": "/users", 69 | "request_headers": { 70 | "Accept-Encoding": " gzip\r", 71 | "Content-Type": " application/x-www-form-urlencoded\r", 72 | "User-Agent": " Go-http-client/1.1\r" 73 | }, 74 | "request_suppressed_headers": { 75 | "Cache-Control": true, 76 | "Content-Length": true, 77 | "ETag": true, 78 | "Set-Cookie": true, 79 | "X-Request-Id": true 80 | }, 81 | "request_url_params": {}, 82 | "request_post_forms": { 83 | "name": "gotokatsuya" 84 | }, 85 | "request_body": "", 86 | "response_headers": { 87 | "Content-Type": " application/json; charset=utf-8\r" 88 | }, 89 | "response_suppressed_headers": { 90 | "Cache-Control": true, 91 | "Content-Length": true, 92 | "ETag": true, 93 | "X-Request-Id": true, 94 | "X-Runtime": true, 95 | "X-XSS-Protection": true 96 | }, 97 | "response_status_code": 200, 98 | "response_body": "{\n \"user\": {\n \"id\": 1,\n \"name\": \"gotokatsuya\"\n }\n}\n" 99 | }, 100 | { 101 | "request_method": "PUT", 102 | "request_path": "/users", 103 | "request_headers": { 104 | "Accept-Encoding": " gzip\r", 105 | "Content-Type": " application/json; charset=utf-8\r", 106 | "User-Agent": " Go-http-client/1.1\r" 107 | }, 108 | "request_suppressed_headers": { 109 | "Cache-Control": true, 110 | "Content-Length": true, 111 | "ETag": true, 112 | "Set-Cookie": true, 113 | "X-Request-Id": true 114 | }, 115 | "request_url_params": {}, 116 | "request_post_forms": {}, 117 | "request_body": "{\n \"name\": \"gotokatsuya\"\n}\n", 118 | "response_headers": { 119 | "Content-Type": " application/json; charset=utf-8\r" 120 | }, 121 | "response_suppressed_headers": { 122 | "Cache-Control": true, 123 | "Content-Length": true, 124 | "ETag": true, 125 | "X-Request-Id": true, 126 | "X-Runtime": true, 127 | "X-XSS-Protection": true 128 | }, 129 | "response_status_code": 200, 130 | "response_body": "{\n \"user\": {\n \"id\": 1,\n \"name\": \"gotokatsuya\"\n }\n}\n" 131 | } 132 | ] -------------------------------------------------------------------------------- /apidoc.go: -------------------------------------------------------------------------------- 1 | package apidoc 2 | 3 | var ( 4 | // disable not gen docs if true 5 | disable bool 6 | 7 | // global project 8 | p Project 9 | ) 10 | 11 | // Init initialize project setting 12 | func Init(newProject Project) error { 13 | p = newProject 14 | p.APIs = []API{} 15 | if err := p.loadDocumentJSONFile(); err != nil { 16 | return err 17 | } 18 | if err := p.writeDocumentFile(); err != nil { 19 | return err 20 | } 21 | return nil 22 | } 23 | 24 | // Enable enable generator 25 | func Enable() { 26 | disable = false 27 | } 28 | 29 | // Disable disable generator 30 | func Disable() { 31 | disable = true 32 | } 33 | 34 | // IsDisabled ref disable 35 | func IsDisabled() bool { 36 | return disable 37 | } 38 | 39 | // Clear delete all files 40 | func Clear() error { 41 | if err := p.deleteDocumentJSONFile(); err != nil { 42 | return err 43 | } 44 | if err := p.deleteDocumentFile(); err != nil { 45 | return err 46 | } 47 | p.APIs = []API{} 48 | return nil 49 | } 50 | 51 | // Gen generate api document 52 | func Gen(api API) error { 53 | p.appendAPI(api) 54 | if err := p.writeDocumentJSONFile(); err != nil { 55 | return err 56 | } 57 | if err := p.writeDocumentFile(); err != nil { 58 | return err 59 | } 60 | return nil 61 | } 62 | -------------------------------------------------------------------------------- /apidoc_test.go: -------------------------------------------------------------------------------- 1 | package apidoc 2 | 3 | import "testing" 4 | 5 | func TestInit(t *testing.T) { 6 | if err := Init(Project{ 7 | DocumentTitle: "apidoc-test", 8 | DocumentPath: "apidoc-test.html", 9 | }); err != nil { 10 | t.Fatal(err) 11 | } 12 | if p.DocumentTitle != "apidoc-test" { 13 | t.Fatal("DocumentTitle is not equal") 14 | } 15 | if p.DocumentPath != "apidoc-test.html" { 16 | t.Fatal("DocumentPath is not equal") 17 | } 18 | if err := p.deleteDocumentFile(); err != nil { 19 | t.Fatal(err) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /default.tpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | API Doc 5 | 6 | 7 | 8 | 9 | 10 | 26 | 27 | 28 | 42 |
43 |
44 |
45 | 50 |
51 |
52 | {{ range $key, $value := .apis}} 53 |
54 | 55 | {{ if $value.RequestHeaders }} 56 |

Request Headers

57 | 58 | 59 | 60 | 61 | 62 | {{ range $key, $value := $value.RequestHeaders }} 63 | 64 | 65 | 66 | 67 | {{ end }} 68 |
KeyValue
{{ $key }} {{ $value }}
69 | {{ end }} 70 | 71 | {{ if $value.RequestPostForms }} 72 |

Post Form

73 | 74 | 75 | 76 | 77 | 78 | {{ range $key, $value := $value.RequestPostForms }} 79 | 80 | 81 | 82 | 83 | {{ end }} 84 |
KeyValue
{{ $key }} {{ $value }}
85 | {{ end }} 86 | 87 | {{ if $value.RequestURLParams }} 88 |

URL Params

89 | 90 | 91 | 92 | 93 | 94 | {{ range $key, $value := $value.RequestURLParams }} 95 | 96 | 97 | 98 | 99 | {{ end }} 100 |
KeyValue
{{ $key }} {{ $value }}
101 | {{ end }} 102 | 103 | {{ if $value.RequestBody }} 104 |

Request Body

105 |
{{ $value.RequestBody }}
106 | {{ end }} 107 | 108 | {{ if $value.ResponseStatusCode }} 109 |

Response Code

110 | {{ $value.ResponseStatusCode }} 111 | {{ end }} 112 | 113 | {{ if $value.ResponseHeaders }} 114 |

Response Headers

115 | 116 | 117 | 118 | 119 | 120 | {{ range $key, $value := $value.ResponseHeaders }} 121 | 122 | 123 | 124 | 125 | {{ end }} 126 |
KeyValue
{{ $key }} {{ $value }}
127 | {{ end }} 128 | 129 | {{ if $value.ResponseBody }} 130 |

Response Body

131 |
{{ $value.ResponseBody }}
132 | {{ end }} 133 |
134 |
135 | {{ end }} 136 |
137 |
138 |
139 |
140 | 141 | -------------------------------------------------------------------------------- /example/basic/Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | go test main_api_test.go main.go -v 3 | 4 | run: 5 | go run main.go -d 6 | -------------------------------------------------------------------------------- /example/basic/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "net/http/httptest" 9 | 10 | "github.com/gotokatsuya/apidoc" 11 | ) 12 | 13 | func apidocMiddleware(handler http.Handler) http.Handler { 14 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 15 | if apidoc.IsDisabled() { 16 | handler.ServeHTTP(w, r) 17 | return 18 | } 19 | 20 | api := apidoc.NewAPI() 21 | api.SuppressedRequestHeaders("Cache-Control", "Content-Length", "X-Request-Id", "ETag", "Set-Cookie") 22 | api.ReadRequest(r, false) 23 | 24 | recorder := httptest.NewRecorder() 25 | 26 | handler.ServeHTTP(recorder, r) 27 | 28 | api.SuppressedResponseHeaders("Cache-Control", "Content-Length", "X-Request-Id", "X-Runtime", "X-XSS-Protection", "ETag") 29 | api.ReadResponseHeader(recorder.Header()) 30 | api.WrapResponseBody(recorder.Body.Bytes()) 31 | api.ResponseStatusCode = recorder.Code 32 | 33 | apidoc.Gen(api) 34 | 35 | for key, values := range recorder.Header() { 36 | for _, value := range values { 37 | w.Header().Set(key, value) 38 | } 39 | } 40 | w.WriteHeader(recorder.Code) 41 | w.Write(recorder.Body.Bytes()) 42 | }) 43 | } 44 | 45 | func init() { 46 | d := flag.Bool("d", false, "disable api doc") 47 | flag.Parse() 48 | if *d { 49 | apidoc.Disable() 50 | } 51 | if apidoc.IsDisabled() { 52 | return 53 | } 54 | apidoc.Init(apidoc.Project{ 55 | DocumentTitle: "example-basic", 56 | DocumentPath: "basic-apidoc.html", 57 | }) 58 | } 59 | 60 | func getHandler() http.Handler { 61 | mux := http.DefaultServeMux 62 | mux.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) { 63 | w.WriteHeader(http.StatusOK) 64 | fmt.Fprintf(w, "Hello") 65 | }) 66 | return apidocMiddleware(mux) 67 | } 68 | 69 | func main() { 70 | log.Fatal(http.ListenAndServe(":8079", getHandler())) 71 | } 72 | -------------------------------------------------------------------------------- /example/basic/main_api_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io/ioutil" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | ) 9 | 10 | func TestIndexUsers(t *testing.T) { 11 | ts := httptest.NewServer(getHandler()) 12 | defer ts.Close() 13 | resp, err := http.Get(ts.URL + "/users") 14 | if err != nil { 15 | t.Fatal(err) 16 | } 17 | if resp.StatusCode != 200 { 18 | t.Fatal(resp.StatusCode) 19 | } 20 | defer resp.Body.Close() 21 | b, err := ioutil.ReadAll(resp.Body) 22 | if err != nil { 23 | t.Fatal(err) 24 | } 25 | t.Log(string(b)) 26 | } 27 | -------------------------------------------------------------------------------- /example/gin/Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | go test main_api_test.go main.go -v 3 | 4 | run: 5 | go run main.go -d 6 | -------------------------------------------------------------------------------- /example/gin/README.md: -------------------------------------------------------------------------------- 1 | ## Run test and Generate api doc 2 | 3 | ```bash 4 | $ make test 5 | ``` 6 | 7 | -------------------------------------------------------------------------------- /example/gin/custom-apidoc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | API Doc 5 | 6 | 7 | 8 | 9 | 10 | 26 | 27 | 28 | 42 |
43 |
44 |
45 | 56 |
57 |
58 | 59 |
60 | 61 | 62 |

Request Headers

63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 |
KeyValue
Accept-Encoding gzip
User-Agent Go-http-client/1.1
80 | 81 | 82 | 83 | 84 | 85 |

URL Params

86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 |
KeyValue
limit 30
98 | 99 | 100 | 101 | 102 | 103 |

Response Code

104 | 200 105 | 106 | 107 | 108 |

Response Headers

109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 |
KeyValue
Content-Type application/json; charset=utf-8
121 | 122 | 123 | 124 |

Response Body

125 |
{
126 |   "limit": "30",
127 |   "users": [
128 |     {
129 |       "id": 1,
130 |       "name": "test1"
131 |     },
132 |     {
133 |       "id": 2,
134 |       "name": "test2"
135 |     }
136 |   ]
137 | }
138 | 
139 | 140 |
141 |
142 | 143 |
144 | 145 | 146 |

Request Headers

147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 |
KeyValue
Accept-Encoding gzip
User-Agent Go-http-client/1.1
164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 |

Response Code

174 | 200 175 | 176 | 177 | 178 |

Response Headers

179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 |
KeyValue
Content-Type application/json; charset=utf-8
191 | 192 | 193 | 194 |

Response Body

195 |
{
196 |   "user": {
197 |     "id": 1,
198 |     "name": "test"
199 |   }
200 | }
201 | 
202 | 203 |
204 |
205 | 206 |
207 | 208 | 209 |

Request Headers

210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 |
KeyValue
Accept-Encoding gzip
Content-Type application/x-www-form-urlencoded
User-Agent Go-http-client/1.1
232 | 233 | 234 | 235 |

Post Form

236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 |
KeyValue
name gotokatsuya
248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 |

Response Code

256 | 200 257 | 258 | 259 | 260 |

Response Headers

261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 |
KeyValue
Content-Type application/json; charset=utf-8
273 | 274 | 275 | 276 |

Response Body

277 |
{
278 |   "user": {
279 |     "id": 1,
280 |     "name": "gotokatsuya"
281 |   }
282 | }
283 | 
284 | 285 |
286 |
287 | 288 |
289 | 290 | 291 |

Request Headers

292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 |
KeyValue
Accept-Encoding gzip
Content-Type application/json; charset=utf-8
User-Agent Go-http-client/1.1
314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 |

Request Body

322 |
{
323 |   "name": "gotokatsuya"
324 | }
325 | 
326 | 327 | 328 | 329 |

Response Code

330 | 200 331 | 332 | 333 | 334 |

Response Headers

335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | 345 | 346 |
KeyValue
Content-Type application/json; charset=utf-8
347 | 348 | 349 | 350 |

Response Body

351 |
{
352 |   "user": {
353 |     "id": 1,
354 |     "name": "gotokatsuya"
355 |   }
356 | }
357 | 
358 | 359 |
360 |
361 | 362 |
363 |
364 |
365 |
366 | 367 | -------------------------------------------------------------------------------- /example/gin/custom-apidoc.html.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "request_method": "GET", 4 | "request_path": "/users", 5 | "request_headers": { 6 | "Accept-Encoding": " gzip\r", 7 | "User-Agent": " Go-http-client/1.1\r" 8 | }, 9 | "request_suppressed_headers": { 10 | "Cache-Control": true, 11 | "Content-Length": true, 12 | "ETag": true, 13 | "Set-Cookie": true, 14 | "X-Request-Id": true 15 | }, 16 | "request_url_params": { 17 | "limit": "30" 18 | }, 19 | "request_post_forms": {}, 20 | "request_body": "", 21 | "response_headers": { 22 | "Content-Type": " application/json; charset=utf-8\r" 23 | }, 24 | "response_suppressed_headers": { 25 | "Cache-Control": true, 26 | "Content-Length": true, 27 | "ETag": true, 28 | "X-Request-Id": true, 29 | "X-Runtime": true, 30 | "X-XSS-Protection": true 31 | }, 32 | "response_status_code": 200, 33 | "response_body": "{\n \"limit\": \"30\",\n \"users\": [\n {\n \"id\": 1,\n \"name\": \"test1\"\n },\n {\n \"id\": 2,\n \"name\": \"test2\"\n }\n ]\n}\n" 34 | }, 35 | { 36 | "request_method": "GET", 37 | "request_path": "/users/1", 38 | "request_headers": { 39 | "Accept-Encoding": " gzip\r", 40 | "User-Agent": " Go-http-client/1.1\r" 41 | }, 42 | "request_suppressed_headers": { 43 | "Cache-Control": true, 44 | "Content-Length": true, 45 | "ETag": true, 46 | "Set-Cookie": true, 47 | "X-Request-Id": true 48 | }, 49 | "request_url_params": {}, 50 | "request_post_forms": {}, 51 | "request_body": "", 52 | "response_headers": { 53 | "Content-Type": " application/json; charset=utf-8\r" 54 | }, 55 | "response_suppressed_headers": { 56 | "Cache-Control": true, 57 | "Content-Length": true, 58 | "ETag": true, 59 | "X-Request-Id": true, 60 | "X-Runtime": true, 61 | "X-XSS-Protection": true 62 | }, 63 | "response_status_code": 200, 64 | "response_body": "{\n \"user\": {\n \"id\": 1,\n \"name\": \"test\"\n }\n}\n" 65 | }, 66 | { 67 | "request_method": "POST", 68 | "request_path": "/users", 69 | "request_headers": { 70 | "Accept-Encoding": " gzip\r", 71 | "Content-Type": " application/x-www-form-urlencoded\r", 72 | "User-Agent": " Go-http-client/1.1\r" 73 | }, 74 | "request_suppressed_headers": { 75 | "Cache-Control": true, 76 | "Content-Length": true, 77 | "ETag": true, 78 | "Set-Cookie": true, 79 | "X-Request-Id": true 80 | }, 81 | "request_url_params": {}, 82 | "request_post_forms": { 83 | "name": "gotokatsuya" 84 | }, 85 | "request_body": "", 86 | "response_headers": { 87 | "Content-Type": " application/json; charset=utf-8\r" 88 | }, 89 | "response_suppressed_headers": { 90 | "Cache-Control": true, 91 | "Content-Length": true, 92 | "ETag": true, 93 | "X-Request-Id": true, 94 | "X-Runtime": true, 95 | "X-XSS-Protection": true 96 | }, 97 | "response_status_code": 200, 98 | "response_body": "{\n \"user\": {\n \"id\": 1,\n \"name\": \"gotokatsuya\"\n }\n}\n" 99 | }, 100 | { 101 | "request_method": "PUT", 102 | "request_path": "/users", 103 | "request_headers": { 104 | "Accept-Encoding": " gzip\r", 105 | "Content-Type": " application/json; charset=utf-8\r", 106 | "User-Agent": " Go-http-client/1.1\r" 107 | }, 108 | "request_suppressed_headers": { 109 | "Cache-Control": true, 110 | "Content-Length": true, 111 | "ETag": true, 112 | "Set-Cookie": true, 113 | "X-Request-Id": true 114 | }, 115 | "request_url_params": {}, 116 | "request_post_forms": {}, 117 | "request_body": "{\n \"name\": \"gotokatsuya\"\n}\n", 118 | "response_headers": { 119 | "Content-Type": " application/json; charset=utf-8\r" 120 | }, 121 | "response_suppressed_headers": { 122 | "Cache-Control": true, 123 | "Content-Length": true, 124 | "ETag": true, 125 | "X-Request-Id": true, 126 | "X-Runtime": true, 127 | "X-XSS-Protection": true 128 | }, 129 | "response_status_code": 200, 130 | "response_body": "{\n \"user\": {\n \"id\": 1,\n \"name\": \"gotokatsuya\"\n }\n}\n" 131 | } 132 | ] -------------------------------------------------------------------------------- /example/gin/custom.tpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | API Doc 5 | 6 | 7 | 8 | 9 | 10 | 26 | 27 | 28 | 42 |
43 |
44 |
45 | 50 |
51 |
52 | {{ range $key, $value := .apis}} 53 |
54 | 55 | {{ if $value.RequestHeaders }} 56 |

Request Headers

57 | 58 | 59 | 60 | 61 | 62 | {{ range $key, $value := $value.RequestHeaders }} 63 | 64 | 65 | 66 | 67 | {{ end }} 68 |
KeyValue
{{ $key }} {{ $value }}
69 | {{ end }} 70 | 71 | {{ if $value.RequestPostForms }} 72 |

Post Form

73 | 74 | 75 | 76 | 77 | 78 | {{ range $key, $value := $value.RequestPostForms }} 79 | 80 | 81 | 82 | 83 | {{ end }} 84 |
KeyValue
{{ $key }} {{ $value }}
85 | {{ end }} 86 | 87 | {{ if $value.RequestURLParams }} 88 |

URL Params

89 | 90 | 91 | 92 | 93 | 94 | {{ range $key, $value := $value.RequestURLParams }} 95 | 96 | 97 | 98 | 99 | {{ end }} 100 |
KeyValue
{{ $key }} {{ $value }}
101 | {{ end }} 102 | 103 | {{ if $value.RequestBody }} 104 |

Request Body

105 |
{{ $value.RequestBody }}
106 | {{ end }} 107 | 108 | {{ if $value.ResponseStatusCode }} 109 |

Response Code

110 | {{ $value.ResponseStatusCode }} 111 | {{ end }} 112 | 113 | {{ if $value.ResponseHeaders }} 114 |

Response Headers

115 | 116 | 117 | 118 | 119 | 120 | {{ range $key, $value := $value.ResponseHeaders }} 121 | 122 | 123 | 124 | 125 | {{ end }} 126 |
KeyValue
{{ $key }} {{ $value }}
127 | {{ end }} 128 | 129 | {{ if $value.ResponseBody }} 130 |

Response Body

131 |
{{ $value.ResponseBody }}
132 | {{ end }} 133 |
134 |
135 | {{ end }} 136 |
137 |
138 |
139 |
140 | 141 | -------------------------------------------------------------------------------- /example/gin/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "log" 7 | "strconv" 8 | 9 | "github.com/gin-gonic/gin" 10 | 11 | "github.com/gotokatsuya/apidoc" 12 | ) 13 | 14 | type ginBodyWriter struct { 15 | gin.ResponseWriter 16 | body *bytes.Buffer 17 | } 18 | 19 | func (w ginBodyWriter) Body() []byte { 20 | return w.body.Bytes() 21 | } 22 | 23 | func (w ginBodyWriter) Write(b []byte) (int, error) { 24 | w.body.Write(b) 25 | return w.ResponseWriter.Write(b) 26 | } 27 | 28 | func (w ginBodyWriter) WriteString(s string) (int, error) { 29 | w.body.WriteString(s) 30 | return w.ResponseWriter.WriteString(s) 31 | } 32 | 33 | func newGinBodyWriter(w gin.ResponseWriter) *ginBodyWriter { 34 | return &ginBodyWriter{body: bytes.NewBufferString(""), ResponseWriter: w} 35 | } 36 | 37 | func apidocMiddleware(c *gin.Context) { 38 | if apidoc.IsDisabled() { 39 | return 40 | } 41 | 42 | api := apidoc.NewAPI() 43 | api.SuppressedRequestHeaders("Cache-Control", "Content-Length", "X-Request-Id", "ETag", "Set-Cookie") 44 | api.ReadRequest(c.Request, false) 45 | 46 | gbw := newGinBodyWriter(c.Writer) 47 | c.Writer = gbw 48 | 49 | // before processing request 50 | 51 | c.Next() 52 | 53 | // after processing request 54 | 55 | api.SuppressedResponseHeaders("Cache-Control", "Content-Length", "X-Request-Id", "X-Runtime", "X-XSS-Protection", "ETag") 56 | api.ReadResponseHeader(c.Writer.Header()) 57 | api.WrapResponseBody(gbw.Body()) 58 | api.ResponseStatusCode = c.Writer.Status() 59 | 60 | apidoc.Gen(api) 61 | } 62 | 63 | func getEngine() *gin.Engine { 64 | r := gin.Default() 65 | r.Use(apidocMiddleware) 66 | 67 | type user struct { 68 | ID int `json:"id"` 69 | Name string `json:"name"` 70 | } 71 | r.GET("/users", func(c *gin.Context) { 72 | limit := c.Query("limit") 73 | c.JSON(200, gin.H{ 74 | "limit": limit, 75 | "users": []user{ 76 | user{ID: 1, Name: "test1"}, 77 | user{ID: 2, Name: "test2"}}, 78 | }) 79 | }) 80 | r.GET("/users/:id", func(c *gin.Context) { 81 | id, err := strconv.Atoi(c.Param("id")) 82 | if err != nil { 83 | log.Println(err) 84 | return 85 | } 86 | c.JSON(200, gin.H{"user": user{ID: id, Name: "test"}}) 87 | }) 88 | r.POST("/users", func(c *gin.Context) { 89 | name := c.PostForm("name") 90 | c.JSON(200, gin.H{"user": user{ID: 1, Name: name}}) 91 | }) 92 | r.PUT("/users", func(c *gin.Context) { 93 | type req struct { 94 | Name string `form:"name" json:"name" binding:"required"` 95 | } 96 | var r req 97 | if err := c.BindJSON(&r); err != nil { 98 | log.Println(err) 99 | return 100 | } 101 | c.JSON(200, gin.H{"user": user{ID: 1, Name: r.Name}}) 102 | }) 103 | return r 104 | } 105 | 106 | func init() { 107 | d := flag.Bool("d", false, "disable api doc") 108 | flag.Parse() 109 | if *d { 110 | apidoc.Disable() 111 | } 112 | if apidoc.IsDisabled() { 113 | return 114 | } 115 | apidoc.Init(apidoc.Project{ 116 | DocumentTitle: "example-gin", 117 | DocumentPath: "custom-apidoc.html", 118 | TemplatePath: "custom.tpl.html", 119 | }) 120 | } 121 | 122 | func main() { 123 | // Listen and Server in 0.0.0.0:8080 124 | getEngine().Run(":8080") 125 | } 126 | -------------------------------------------------------------------------------- /example/gin/main_api_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/httptest" 9 | "net/url" 10 | "testing" 11 | ) 12 | 13 | func TestIndexUsers(t *testing.T) { 14 | ts := httptest.NewServer(getEngine()) 15 | defer ts.Close() 16 | resp, err := http.Get(ts.URL + "/users?limit=30") 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | if resp.StatusCode != 200 { 21 | t.Fatal(resp.StatusCode) 22 | } 23 | defer resp.Body.Close() 24 | b, err := ioutil.ReadAll(resp.Body) 25 | if err != nil { 26 | t.Fatal(err) 27 | } 28 | t.Log(string(b)) 29 | } 30 | 31 | func TestShowUsers(t *testing.T) { 32 | ts := httptest.NewServer(getEngine()) 33 | defer ts.Close() 34 | resp, err := http.Get(ts.URL + "/users/1") 35 | if err != nil { 36 | t.Fatal(err) 37 | } 38 | if resp.StatusCode != 200 { 39 | t.Fatal(resp.StatusCode) 40 | } 41 | defer resp.Body.Close() 42 | b, err := ioutil.ReadAll(resp.Body) 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | t.Log(string(b)) 47 | } 48 | 49 | func TestCreateUsers(t *testing.T) { 50 | ts := httptest.NewServer(getEngine()) 51 | defer ts.Close() 52 | values := url.Values{} 53 | values.Add("name", "gotokatsuya") 54 | resp, err := http.PostForm(ts.URL+"/users", values) 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | if resp.StatusCode != 200 { 59 | t.Fatal(resp.StatusCode) 60 | } 61 | defer resp.Body.Close() 62 | b, err := ioutil.ReadAll(resp.Body) 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | t.Log(string(b)) 67 | } 68 | 69 | func TestUpdateUsers(t *testing.T) { 70 | ts := httptest.NewServer(getEngine()) 71 | defer ts.Close() 72 | type req struct { 73 | Name string `json:"name"` 74 | } 75 | r := req{Name: "gotokatsuya"} 76 | buf := new(bytes.Buffer) 77 | if err := json.NewEncoder(buf).Encode(r); err != nil { 78 | t.Fatal(err) 79 | } 80 | newReq, err := http.NewRequest("PUT", ts.URL+"/users", buf) 81 | if err != nil { 82 | t.Fatal(err) 83 | } 84 | newReq.Header.Set("Content-Type", "application/json; charset=utf-8") 85 | client := &http.Client{} 86 | resp, err := client.Do(newReq) 87 | if err != nil { 88 | t.Fatal(err) 89 | } 90 | if resp.StatusCode != 200 { 91 | t.Fatal(resp.StatusCode) 92 | } 93 | defer resp.Body.Close() 94 | b, err := ioutil.ReadAll(resp.Body) 95 | if err != nil { 96 | t.Fatal(err) 97 | } 98 | t.Log(string(b)) 99 | } 100 | -------------------------------------------------------------------------------- /example/gin/view.v1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gotokatsuya/apidoc/53e1bd9e6ed1c142ea8ae7cb02b2ca9ade66b6e5/example/gin/view.v1.png -------------------------------------------------------------------------------- /json.go: -------------------------------------------------------------------------------- 1 | package apidoc 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | ) 7 | 8 | // PrettyPrint print rich json 9 | func PrettyPrint(in []byte) ([]byte, error) { 10 | var out bytes.Buffer 11 | if err := json.Indent(&out, in, "", " "); err != nil { 12 | return nil, err 13 | } 14 | return out.Bytes(), nil 15 | } 16 | -------------------------------------------------------------------------------- /json_test.go: -------------------------------------------------------------------------------- 1 | package apidoc 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | ) 7 | 8 | func TestPrettyPrint(t *testing.T) { 9 | type data struct { 10 | Name string `json:"name"` 11 | } 12 | 13 | d := data{ 14 | Name: "gotokatsuya", 15 | } 16 | in, err := json.Marshal(d) 17 | if err != nil { 18 | t.Fatal(err) 19 | } 20 | out, err := PrettyPrint(in) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | t.Log(string(out)) 25 | } 26 | -------------------------------------------------------------------------------- /project.go: -------------------------------------------------------------------------------- 1 | package apidoc 2 | 3 | import ( 4 | "encoding/json" 5 | "go/build" 6 | "html/template" 7 | "io" 8 | "os" 9 | "path" 10 | "path/filepath" 11 | ) 12 | 13 | // Project has project setting 14 | type Project struct { 15 | DocumentTitle string 16 | DocumentPath string 17 | TemplatePath string 18 | 19 | APIs []API 20 | } 21 | 22 | func (p *Project) hasDocumentPath() bool { 23 | return p.DocumentPath != "" 24 | } 25 | 26 | func (p *Project) getDocumentPath() string { 27 | if p.hasDocumentPath() { 28 | return p.DocumentPath 29 | } 30 | return "apidoc.html" 31 | } 32 | 33 | func (p *Project) getDocumentJSONPath() string { 34 | return p.getDocumentPath() + ".json" 35 | } 36 | 37 | func (p *Project) hasTemplatePath() bool { 38 | return p.TemplatePath != "" 39 | } 40 | 41 | func findAppPath() string { 42 | const appName = "github.com/gotokatsuya/apidoc" 43 | appPkg, err := build.Import(appName, "", build.FindOnly) 44 | if err != nil { 45 | return "" 46 | } 47 | return path.Join(appPkg.SrcRoot, appName) 48 | } 49 | 50 | func (p *Project) getTemplatePath() string { 51 | if p.hasTemplatePath() { 52 | return p.TemplatePath 53 | } 54 | return path.Join(findAppPath(), "default.tpl.html") 55 | } 56 | 57 | func (p *Project) openDocumentJSONFile() (*os.File, error) { 58 | filePath, err := filepath.Abs(p.getDocumentJSONPath()) 59 | if err != nil { 60 | return nil, err 61 | } 62 | file, err := os.Open(filePath) 63 | if err != nil { 64 | return nil, err 65 | } 66 | return file, nil 67 | } 68 | 69 | func (p *Project) loadDocumentJSONFile() error { 70 | file, err := p.openDocumentJSONFile() 71 | defer file.Close() 72 | if err != nil { 73 | return err 74 | } 75 | if err := json.NewDecoder(io.Reader(file)).Decode(&p.APIs); err != nil { 76 | return err 77 | } 78 | return nil 79 | } 80 | 81 | func (p *Project) createDocumentJSONFile() (*os.File, error) { 82 | filePath, err := filepath.Abs(p.getDocumentJSONPath()) 83 | if err != nil { 84 | return nil, err 85 | } 86 | file, err := os.Create(filePath) 87 | if err != nil { 88 | return nil, err 89 | } 90 | return file, nil 91 | } 92 | 93 | func (p *Project) deleteDocumentJSONFile() error { 94 | filePath, err := filepath.Abs(p.getDocumentJSONPath()) 95 | if err != nil { 96 | return err 97 | } 98 | if err := os.Remove(filePath); err != nil { 99 | return err 100 | } 101 | return nil 102 | } 103 | 104 | func (p *Project) writeDocumentJSONFile() error { 105 | file, err := p.createDocumentJSONFile() 106 | defer file.Close() 107 | if err != nil { 108 | return err 109 | } 110 | b, err := json.Marshal(p.APIs) 111 | if err != nil { 112 | return err 113 | } 114 | out, err := PrettyPrint(b) 115 | if err != nil { 116 | return err 117 | } 118 | if _, err := file.Write(out); err != nil { 119 | return err 120 | } 121 | return nil 122 | } 123 | 124 | func (p *Project) createDocumentFile() (*os.File, error) { 125 | filePath, err := filepath.Abs(p.getDocumentPath()) 126 | if err != nil { 127 | return nil, err 128 | } 129 | file, err := os.Create(filePath) 130 | if err != nil { 131 | return nil, err 132 | } 133 | return file, nil 134 | } 135 | 136 | func (p *Project) deleteDocumentFile() error { 137 | filePath, err := filepath.Abs(p.getDocumentPath()) 138 | if err != nil { 139 | return err 140 | } 141 | if err := os.Remove(filePath); err != nil { 142 | return err 143 | } 144 | return nil 145 | } 146 | 147 | func (p *Project) writeDocumentFile() error { 148 | t := template.Must(template.ParseFiles(p.getTemplatePath())) 149 | file, err := p.createDocumentFile() 150 | defer file.Close() 151 | if err != nil { 152 | return err 153 | } 154 | return t.Execute(io.Writer(file), map[string]interface{}{ 155 | "title": p.DocumentTitle, 156 | "apis": p.APIs, 157 | }) 158 | } 159 | 160 | func (p *Project) appendAPI(newAPI API) { 161 | for i, api := range p.APIs { 162 | if newAPI.equal(api) { 163 | // replace 164 | p.APIs[i] = newAPI 165 | return 166 | } 167 | } 168 | p.APIs = append(p.APIs, newAPI) 169 | } 170 | -------------------------------------------------------------------------------- /project_test.go: -------------------------------------------------------------------------------- 1 | package apidoc 2 | 3 | import "testing" 4 | 5 | func TestAppendAPI(t *testing.T) { 6 | p := Project{ 7 | APIs: make([]API, 0), 8 | } 9 | a1 := NewAPI() 10 | a1.RequestMethod = "GET" 11 | a1.RequestPath = "/users" 12 | a1.RequestBody = "body1" 13 | p.appendAPI(a1) 14 | if len(p.APIs) != 1 { 15 | t.Fatal("API len is not 1") 16 | } 17 | 18 | a2 := NewAPI() 19 | a2.RequestMethod = "GET" 20 | a2.RequestPath = "/users" 21 | a2.RequestBody = "body2" 22 | p.appendAPI(a2) 23 | if len(p.APIs) != 1 { 24 | t.Fatal("API len is not 1") 25 | } 26 | 27 | a3 := NewAPI() 28 | a3.RequestMethod = "POST" 29 | a3.RequestPath = "/users" 30 | a3.RequestBody = "body3" 31 | p.appendAPI(a3) 32 | if len(p.APIs) != 2 { 33 | t.Fatal("API len is not 2") 34 | } 35 | } 36 | --------------------------------------------------------------------------------