├── .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 | 
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 |
51 |
52 | {{ range $key, $value := .apis}}
53 |
54 |
55 | {{ if $value.RequestHeaders }}
56 |
Request Headers
57 |
58 |
59 | Key |
60 | Value |
61 |
62 | {{ range $key, $value := $value.RequestHeaders }}
63 |
64 | {{ $key }} |
65 | {{ $value }} |
66 |
67 | {{ end }}
68 |
69 | {{ end }}
70 |
71 | {{ if $value.RequestPostForms }}
72 |
Post Form
73 |
74 |
75 | Key |
76 | Value |
77 |
78 | {{ range $key, $value := $value.RequestPostForms }}
79 |
80 | {{ $key }} |
81 | {{ $value }} |
82 |
83 | {{ end }}
84 |
85 | {{ end }}
86 |
87 | {{ if $value.RequestURLParams }}
88 |
URL Params
89 |
90 |
91 | Key |
92 | Value |
93 |
94 | {{ range $key, $value := $value.RequestURLParams }}
95 |
96 | {{ $key }} |
97 | {{ $value }} |
98 |
99 | {{ end }}
100 |
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 | Key |
118 | Value |
119 |
120 | {{ range $key, $value := $value.ResponseHeaders }}
121 |
122 | {{ $key }} |
123 | {{ $value }} |
124 |
125 | {{ end }}
126 |
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 |
57 |
58 |
59 |
60 |
61 |
62 |
Request Headers
63 |
64 |
65 | Key |
66 | Value |
67 |
68 |
69 |
70 | Accept-Encoding |
71 | gzip
|
72 |
73 |
74 |
75 | User-Agent |
76 | Go-http-client/1.1
|
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
URL Params
86 |
87 |
88 | Key |
89 | Value |
90 |
91 |
92 |
93 | limit |
94 | 30 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
Response Code
104 |
200
105 |
106 |
107 |
108 |
Response Headers
109 |
110 |
111 | Key |
112 | Value |
113 |
114 |
115 |
116 | Content-Type |
117 | application/json; charset=utf-8
|
118 |
119 |
120 |
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 | Key |
150 | Value |
151 |
152 |
153 |
154 | Accept-Encoding |
155 | gzip
|
156 |
157 |
158 |
159 | User-Agent |
160 | Go-http-client/1.1
|
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
Response Code
174 |
200
175 |
176 |
177 |
178 |
Response Headers
179 |
180 |
181 | Key |
182 | Value |
183 |
184 |
185 |
186 | Content-Type |
187 | application/json; charset=utf-8
|
188 |
189 |
190 |
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 | Key |
213 | Value |
214 |
215 |
216 |
217 | Accept-Encoding |
218 | gzip
|
219 |
220 |
221 |
222 | Content-Type |
223 | application/x-www-form-urlencoded
|
224 |
225 |
226 |
227 | User-Agent |
228 | Go-http-client/1.1
|
229 |
230 |
231 |
232 |
233 |
234 |
235 |
Post Form
236 |
237 |
238 | Key |
239 | Value |
240 |
241 |
242 |
243 | name |
244 | gotokatsuya |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 |
254 |
255 |
Response Code
256 |
200
257 |
258 |
259 |
260 |
Response Headers
261 |
262 |
263 | Key |
264 | Value |
265 |
266 |
267 |
268 | Content-Type |
269 | application/json; charset=utf-8
|
270 |
271 |
272 |
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 | Key |
295 | Value |
296 |
297 |
298 |
299 | Accept-Encoding |
300 | gzip
|
301 |
302 |
303 |
304 | Content-Type |
305 | application/json; charset=utf-8
|
306 |
307 |
308 |
309 | User-Agent |
310 | Go-http-client/1.1
|
311 |
312 |
313 |
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 | Key |
338 | Value |
339 |
340 |
341 |
342 | Content-Type |
343 | application/json; charset=utf-8
|
344 |
345 |
346 |
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 |
51 |
52 | {{ range $key, $value := .apis}}
53 |
54 |
55 | {{ if $value.RequestHeaders }}
56 |
Request Headers
57 |
58 |
59 | Key |
60 | Value |
61 |
62 | {{ range $key, $value := $value.RequestHeaders }}
63 |
64 | {{ $key }} |
65 | {{ $value }} |
66 |
67 | {{ end }}
68 |
69 | {{ end }}
70 |
71 | {{ if $value.RequestPostForms }}
72 |
Post Form
73 |
74 |
75 | Key |
76 | Value |
77 |
78 | {{ range $key, $value := $value.RequestPostForms }}
79 |
80 | {{ $key }} |
81 | {{ $value }} |
82 |
83 | {{ end }}
84 |
85 | {{ end }}
86 |
87 | {{ if $value.RequestURLParams }}
88 |
URL Params
89 |
90 |
91 | Key |
92 | Value |
93 |
94 | {{ range $key, $value := $value.RequestURLParams }}
95 |
96 | {{ $key }} |
97 | {{ $value }} |
98 |
99 | {{ end }}
100 |
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 | Key |
118 | Value |
119 |
120 | {{ range $key, $value := $value.ResponseHeaders }}
121 |
122 | {{ $key }} |
123 | {{ $value }} |
124 |
125 | {{ end }}
126 |
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 |
--------------------------------------------------------------------------------