├── .bowerrc
├── .gitattributes
├── .gitignore
├── LICENSE
├── README.md
├── app
├── app.go
├── controller.go
├── controller_test.go
├── db.go
├── error.go
├── log.go
├── model.go
└── model_test.go
├── bower.json
├── controller
├── bookmark.go
├── bookmarks.go
├── favicon.go
├── history.go
├── index.go
└── static.go
├── create_view.sh
├── errors
└── errors.go
├── favicon.ico
├── img
├── shortcut0.5.jpg
└── shortcut0.6.bmp
├── main.go
├── model
├── bookmark.go
├── bookmark_test.go
├── bookmarks.go
├── bookmarks_test.go
├── history.go
├── model.go
├── model_test.go
├── struct.go
├── struct_test.go
├── submit.go
└── submit_test.go
├── plugin
└── md5signature.go
├── router
└── router.go
├── static
├── bower_components
│ └── .gitignore
├── index.css
├── index.js
└── json-formater.js
├── text
├── .gitignore
└── provider.go
└── view
└── index.html
/.bowerrc:
--------------------------------------------------------------------------------
1 | {
2 | "directory": "static/bower_components/"
3 | }
4 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.gitattributes text eol=lf
2 | *.gitignore text eol=lf
3 | *.go text eol=lf
4 | *.html text eol=lf
5 | *.css text eol=lf
6 | *.js text eol=lf
7 | *.json text eol=lf
8 | *.md text eol=lf
9 | *.log text eol=lf
10 |
11 | LICENSE text eol=lf
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled Object files, Static and Dynamic libs (Shared Objects)
2 | *.o
3 | *.a
4 | *.so
5 |
6 | # Folders
7 | _obj
8 | _test
9 |
10 | # Architecture specific extensions/prefixes
11 | *.[568vq]
12 | [568vq].out
13 |
14 | *.cgo1.go
15 | *.cgo2.c
16 | _cgo_defun.c
17 | _cgo_gotypes.go
18 | _cgo_export.*
19 |
20 | _testmain.go
21 |
22 | *.exe
23 | *.test
24 | *.prof
25 | *.zip
26 |
27 | tags
28 |
29 | # release directory
30 | /release/
31 |
32 | # binary
33 | /http-api-tester
34 |
35 | # db
36 | /http-api-tester.db
37 |
38 | *.tern-port
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright {yyyy} {name of copyright owner}
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
203 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # http-api-tester
2 |
3 | HTTP接口测试工具
4 |
5 | ## Shortcut
6 |
7 | 
8 |
9 | ## Download
10 |
11 | Just one file ~! (Since v0.5)
12 |
13 | [releases](https://github.com/jmjoy/http-api-tester/releases)
14 |
15 | ## Compile
16 |
17 | ### Requirements
18 |
19 | go get "github.com/boltdb/bolt" \
20 | "github.com/fatih/color" \
21 | "github.com/jmjoy/boomer" \
22 | "github.com/jmjoy/file2string"
23 |
24 | ### Install
25 |
26 | bower install
27 | sh create-view.sh
28 | go build
29 |
30 | ## Usage
31 |
32 | → ./http-api-tester --help
33 | Usage of ./http-api-tester:
34 | -db string
35 | 数据库路径 (default "http-api-tester.db")
36 | -p string
37 | 服务器运行端口 (default "8080")
38 |
39 | ## TODO
40 |
41 | - [x] Support all http request method
42 | - [x] Support more request body format
43 | - [x] Adjust config file
44 | - [x] Support history list
45 | - [x] Support move header
46 |
47 | ## License
48 |
49 | [MIT](https://github.com/jmjoy/http-interface-tester/blob/master/LICENSE)
50 |
--------------------------------------------------------------------------------
/app/app.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "net/http"
7 | "reflect"
8 | "strings"
9 | "sync"
10 | )
11 |
12 | // Config is a struct named Config
13 | type Config struct {
14 | // User defined
15 | Port string
16 | DbPath string
17 |
18 | // App need
19 | Routers map[string]func() IController
20 | }
21 |
22 | // Run is a function named Run
23 | func Run(cfg Config) {
24 | port := cfg.Port
25 | if !strings.ContainsRune(port, ':') {
26 | port = "localhost:" + port
27 | }
28 |
29 | // init db config
30 | if err := InitDb(cfg.DbPath); err != nil {
31 | Log(LOG_LV_FAIL, err)
32 | return
33 | }
34 |
35 | // register restful router
36 | for pattren, fn := range cfg.Routers {
37 | HandleRestful(pattren, fn)
38 | }
39 |
40 | Log(LOG_LV_INFO, "测试接口服务器在跑了,请访问 http://"+port)
41 | Log(LOG_LV_FAIL, http.ListenAndServe(port, nil))
42 | }
43 |
44 | var controllerPoolMap = make(map[string]*sync.Pool)
45 |
46 | func HandleRestful(pattern string, fn func() IController) {
47 | controllerPoolMap[pattern] = &sync.Pool{
48 | // New: func() interface{} {
49 | // TODO Find why can't new a substruct
50 | // return reflect.New(reflect.TypeOf(c)).Elem().Interface()
51 | // },
52 | New: func() interface{} {
53 | return fn()
54 | },
55 | }
56 |
57 | http.HandleFunc(pattern, func(w http.ResponseWriter, r *http.Request) {
58 | c := controllerPoolMap[pattern].Get().(IController)
59 | defer controllerPoolMap[pattern].Put(c)
60 |
61 | // ResetController(c, w, r)
62 | // TODO Here alway new a Controller
63 | reflect.ValueOf(c).Elem().FieldByName("Controller").
64 | Set(reflect.ValueOf(&Controller{R: r, W: w}))
65 | // c.Reset(w, r)
66 |
67 | var err error
68 | switch r.Method {
69 | case "GET":
70 | err = c.Get()
71 |
72 | case "POST":
73 | err = c.Post()
74 |
75 | case "PUT":
76 | err = c.Put()
77 |
78 | case "DELETE":
79 | err = c.Delete()
80 |
81 | default:
82 | err = ErrMethodNotAllowed
83 | }
84 |
85 | // handle error
86 | if err != nil {
87 | switch err.(type) {
88 | case *ApiStatusError: // api error
89 | apiStatusErr := err.(*ApiStatusError)
90 | status := apiStatusErr.status
91 | message := apiStatusErr.message
92 | if err := jsonRender(w, status, message, nil); err != nil {
93 | panic(err)
94 | }
95 |
96 | case *StatusError: // web or server error
97 | statusErr := err.(*StatusError)
98 | status := statusErr.status
99 | message := statusErr.message
100 | Log(LOG_LV_FAIL, fmt.Sprintf("<%d> %s (%s)", status, message, r.URL))
101 | http.Error(w, message, status)
102 |
103 | default: // system error
104 | status := http.StatusNotAcceptable
105 | message := err.Error()
106 | Log(LOG_LV_FAIL, fmt.Sprintf("<%d> %s (%s)", status, message, r.URL))
107 | http.Error(w, message, status)
108 | }
109 | }
110 |
111 | })
112 | }
113 |
114 | func jsonRender(w http.ResponseWriter, status int, message string, data interface{}) error {
115 | out := map[string]interface{}{
116 | "Status": status,
117 | "Message": message,
118 | "Data": data,
119 | }
120 | buf, err := json.Marshal(out)
121 | if err != nil {
122 | return err
123 | }
124 | _, err = w.Write(buf)
125 | if err != nil {
126 | return err
127 | }
128 | return nil
129 | }
130 |
--------------------------------------------------------------------------------
/app/controller.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "io/ioutil"
5 | "net/http"
6 | "net/url"
7 | "reflect"
8 |
9 | "encoding/json"
10 | )
11 |
12 | // IController is a interface named IController
13 | type IController interface {
14 | // reset rw of one context
15 | Reset(http.ResponseWriter, *http.Request)
16 |
17 | // RESTful
18 | Get() error
19 | Post() error
20 | Put() error
21 | Delete() error
22 | }
23 |
24 | func ResetController(c IController, w http.ResponseWriter, r *http.Request) {
25 | field := reflect.ValueOf(c).FieldByName("Controller")
26 |
27 | panic(field.CanSet())
28 | }
29 |
30 | var _ IController = new(Controller)
31 |
32 | // Controller is a struct named Controller
33 | type Controller struct {
34 | W http.ResponseWriter
35 | R *http.Request
36 |
37 | query url.Values // get params
38 | }
39 |
40 | // Reset reset controller for handle one request
41 | func (this *Controller) Reset(w http.ResponseWriter, r *http.Request) {
42 | if this == nil {
43 | this = new(Controller)
44 | }
45 |
46 | this.W = w
47 | this.R = r
48 | }
49 |
50 | func (this *Controller) MethodNotAllowed() error {
51 | return ErrMethodNotAllowed
52 | }
53 |
54 | func (this *Controller) Get() error {
55 | return this.MethodNotAllowed()
56 | }
57 |
58 | func (this *Controller) Post() error {
59 | return this.MethodNotAllowed()
60 | }
61 |
62 | func (this *Controller) Put() error {
63 | return this.MethodNotAllowed()
64 | }
65 |
66 | func (this *Controller) Delete() error {
67 | return this.MethodNotAllowed()
68 | }
69 |
70 | func (this *Controller) JsonRender(status int, message string, data interface{}) error {
71 | return jsonRender(this.W, status, message, data)
72 | }
73 |
74 | func (this *Controller) JsonSuccess(data interface{}) error {
75 | return this.JsonRender(200, "", data)
76 | }
77 |
78 | func (this *Controller) QueryGet(key string) string {
79 | if this.query == nil {
80 | this.query = this.R.URL.Query()
81 | }
82 | return this.query.Get(key)
83 | }
84 |
85 | func (this *Controller) ParseJsonBody(data interface{}) (err error) {
86 | // Get Body
87 | buf, err := ioutil.ReadAll(this.R.Body)
88 | if err != nil {
89 | return
90 | }
91 |
92 | if err = json.Unmarshal(buf, data); err != nil {
93 | return ErrBadRequest.NewMessage(err)
94 | }
95 |
96 | return
97 | }
98 |
--------------------------------------------------------------------------------
/app/controller_test.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "net/http"
5 | "reflect"
6 | "testing"
7 | )
8 |
9 | type MyController struct {
10 | *Controller
11 | }
12 |
13 | func TestController(t *testing.T) {
14 | req, _ := http.NewRequest("GET", "http://www.baidu.com", nil)
15 |
16 | c0 := MyController{
17 | Controller: &Controller{R: req},
18 | }
19 | t.Logf("%#v", c0)
20 |
21 | c := new(MyController)
22 | reflect.ValueOf(c).Elem().FieldByName("Controller").Set(reflect.ValueOf(&Controller{R: req}))
23 | // c.Controller.Reset(nil, req)
24 | t.Logf("%#v", c)
25 |
26 | if !reflect.DeepEqual(c0, c) {
27 | // t.Fatal("not equal!")
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/app/db.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "github.com/boltdb/bolt"
5 | )
6 |
7 | // Db is a single instance of dbHelper
8 | var Db *dbHelper
9 |
10 | type dbHelper struct {
11 | dbPath string
12 | }
13 |
14 | func InitDb(dbPath string) error {
15 | // check can open?
16 | db, err := bolt.Open(dbPath, 0600, nil)
17 | if err != nil {
18 | return err
19 | }
20 | defer db.Close()
21 |
22 | Db = &dbHelper{dbPath: dbPath}
23 | return nil
24 | }
25 |
26 | func (this *dbHelper) Get(bucket string, key string) ([]byte, error) {
27 | db, err := bolt.Open(this.dbPath, 0600, nil)
28 | if err != nil {
29 | return nil, err
30 | }
31 | defer db.Close()
32 |
33 | var value []byte
34 | err = db.View(func(tx *bolt.Tx) error {
35 | bk := tx.Bucket([]byte(bucket))
36 | if bk == nil {
37 | return ErrBucketNotFound
38 | }
39 | buf := bk.Get([]byte(key))
40 | if buf == nil {
41 | return nil
42 | }
43 | value = make([]byte, len(buf))
44 | copy(value, buf)
45 |
46 | return nil
47 | })
48 |
49 | if err != nil {
50 | return nil, err
51 | }
52 |
53 | return value, nil
54 | }
55 |
56 | func (this *dbHelper) Each(bucket string, fn func([]byte, []byte) error) error {
57 | db, err := bolt.Open(this.dbPath, 0600, nil)
58 | if err != nil {
59 | return err
60 | }
61 | defer db.Close()
62 |
63 | return db.View(func(tx *bolt.Tx) error {
64 | bk := tx.Bucket([]byte(bucket))
65 | if bk == nil {
66 | return ErrBucketNotFound
67 | }
68 |
69 | return bk.ForEach(fn)
70 | })
71 | }
72 |
73 | func (this *dbHelper) Put(bucket string, key string, value []byte) error {
74 | db, err := bolt.Open(this.dbPath, 0600, nil)
75 | if err != nil {
76 | return err
77 | }
78 | defer db.Close()
79 |
80 | err = db.Update(func(tx *bolt.Tx) error {
81 | bk, err := tx.CreateBucketIfNotExists([]byte(bucket))
82 | if err != nil {
83 | return err
84 | }
85 | err = bk.Put([]byte(key), value)
86 | if err != nil {
87 | return err
88 | }
89 | return nil
90 | })
91 |
92 | if err != nil {
93 | return err
94 | }
95 |
96 | return nil
97 | }
98 |
99 | func (this *dbHelper) Delete(bucket string, key string) error {
100 | db, err := bolt.Open(this.dbPath, 0600, nil)
101 | if err != nil {
102 | return err
103 | }
104 | defer db.Close()
105 |
106 | err = db.Update(func(tx *bolt.Tx) error {
107 | bk := tx.Bucket([]byte(bucket))
108 | if bk == nil {
109 | return ErrBucketNotFound
110 | }
111 | err = bk.Delete([]byte(key))
112 | if err != nil {
113 | return err
114 | }
115 | return nil
116 | })
117 |
118 | if err != nil {
119 | return err
120 | }
121 |
122 | return nil
123 | }
124 |
--------------------------------------------------------------------------------
/app/error.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "net/http"
7 | "reflect"
8 | )
9 |
10 | type StatusError struct {
11 | status int
12 | message string
13 | }
14 |
15 | func NewStatusError(status int, message string) *StatusError {
16 | return &StatusError{
17 | status: status,
18 | message: message,
19 | }
20 | }
21 |
22 | func (this *StatusError) Error() string {
23 | return this.message
24 | }
25 |
26 | func (this *StatusError) NewMessage(message interface{}) *StatusError {
27 | return NewStatusError(this.status, fmt.Sprint(message))
28 | }
29 |
30 | func (this *StatusError) NewMessageSpf(args ...interface{}) *StatusError {
31 | return NewStatusError(this.status, fmt.Sprintf(this.message, args...))
32 | }
33 |
34 | func (this *StatusError) isNameEqual(err error) bool {
35 | return reflect.TypeOf(this).Name() == reflect.TypeOf(err).Name()
36 | }
37 |
38 | type ApiStatusError struct {
39 | *StatusError
40 | }
41 |
42 | func NewApiStatusError(status int, message string) *ApiStatusError {
43 | return &ApiStatusError{
44 | StatusError: NewStatusError(status, message),
45 | }
46 | }
47 |
48 | // definded error
49 | var (
50 | ErrBadRequest = NewStatusError(http.StatusBadRequest, "bad request")
51 | ErrNotFound = NewStatusError(http.StatusNotFound, "not found")
52 | ErrMethodNotAllowed = NewStatusError(http.StatusMethodNotAllowed, "method not allowed")
53 | ErrInternalServerError = NewStatusError(http.StatusInternalServerError, "internal server error")
54 |
55 | ErrBucketNotFound = errors.New("Bucket not found")
56 | )
57 |
--------------------------------------------------------------------------------
/app/log.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "github.com/fatih/color"
5 | )
6 |
7 | type LogLevel int
8 |
9 | const (
10 | LOG_LV_SUCC LogLevel = iota
11 | LOG_LV_INFO
12 | LOG_LV_FAIL
13 | )
14 |
15 | func Log(lv LogLevel, e interface{}) {
16 | var attr color.Attribute
17 | var tip string
18 |
19 | switch lv {
20 | case LOG_LV_SUCC:
21 | attr = color.FgGreen
22 | tip = "SUCC"
23 |
24 | case LOG_LV_INFO:
25 | attr = color.FgCyan
26 | tip = "INFO"
27 |
28 | case LOG_LV_FAIL:
29 | attr = color.FgRed
30 | tip = "FAIL"
31 |
32 | default:
33 | panic("No this log level")
34 | }
35 |
36 | color.New(attr).Printf("[%s] %s\n", tip, e)
37 | }
38 |
--------------------------------------------------------------------------------
/app/model.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "encoding/json"
5 | )
6 |
7 | type Model struct {
8 | bucket string
9 | }
10 |
11 | func NewModel(bucket string) *Model {
12 | return &Model{
13 | bucket: bucket,
14 | }
15 | }
16 |
17 | func (this *Model) Get(key string, data interface{}) (ok bool, err error) {
18 | buf, err := Db.Get(this.bucket, key)
19 | if err != nil {
20 | if err == ErrBucketNotFound {
21 | err = nil
22 | }
23 | return
24 | }
25 |
26 | if buf == nil {
27 | return
28 | }
29 |
30 | if err = json.Unmarshal(buf, data); err != nil {
31 | return
32 | }
33 |
34 | return true, nil
35 | }
36 |
37 | func (this *Model) Keys() (keys []string, err error) {
38 | keys = make([]string, 0, 2)
39 | err = Db.Each(this.bucket, func(k, v []byte) error {
40 | keys = append(keys, string(k))
41 | return nil
42 | })
43 | if err == ErrBucketNotFound {
44 | err = nil
45 | }
46 | return
47 | }
48 |
49 | func (this *Model) Put(key string, data interface{}) (err error) {
50 | buf, err := json.Marshal(data)
51 | if err != nil {
52 | return
53 | }
54 | return Db.Put(this.bucket, key, buf)
55 | }
56 |
57 | func (this *Model) Delete(key string) (err error) {
58 | return Db.Delete(this.bucket, key)
59 | }
60 |
--------------------------------------------------------------------------------
/app/model_test.go:
--------------------------------------------------------------------------------
1 | package app
2 |
3 | import (
4 | "os"
5 | "reflect"
6 | "testing"
7 | )
8 |
9 | type Person struct {
10 | Name string
11 | Age int
12 | }
13 |
14 | func TestMain(m *testing.M) {
15 | dbPath := "./model_test.db"
16 | defer os.Remove(dbPath)
17 |
18 | if err := InitDb(dbPath); err != nil {
19 | panic(err)
20 | }
21 | m.Run()
22 | }
23 |
24 | func TestCRUD(t *testing.T) {
25 | bucket := "test"
26 | testkey := "testkey"
27 |
28 | model := NewModel(bucket)
29 |
30 | var data map[string]string
31 | has, err := model.Get(testkey, &data)
32 | if err != nil {
33 | panic(err)
34 | }
35 | if has {
36 | t.Fatal("Has data?")
37 | }
38 |
39 | person := Person{
40 | Name: "jmjoy",
41 | Age: 23,
42 | }
43 | err = model.Put(testkey, person)
44 | if err != nil {
45 | panic(err)
46 | }
47 |
48 | var person0 Person
49 | has, err = model.Get(testkey, &person0)
50 | if err != nil {
51 | panic(err)
52 | }
53 | if !has {
54 | t.Fatal("No data?")
55 | }
56 |
57 | if person != person0 {
58 | t.Fatal("Not euqal!")
59 | }
60 |
61 | err = model.Delete(testkey)
62 | if err != nil {
63 | panic(err)
64 | }
65 |
66 | has, err = model.Get(testkey, &person0)
67 | if err != nil {
68 | panic(err)
69 | }
70 | if has {
71 | t.Fatal("Has data?")
72 | }
73 | }
74 |
75 | func TestKeys(t *testing.T) {
76 | bucket := "test0"
77 | model := NewModel(bucket)
78 |
79 | keys, err := model.Keys()
80 | if err != nil {
81 | panic(err)
82 | }
83 | if len(keys) != 0 {
84 | t.Fatal("not empty?")
85 | }
86 |
87 | testKeys := []string{
88 | "1", "2", "3",
89 | }
90 | for _, k := range testKeys {
91 | model.Put(k, "data")
92 | }
93 |
94 | keys, err = model.Keys()
95 | if err != nil {
96 | panic(err)
97 | }
98 | t.Log(keys)
99 | if !reflect.DeepEqual(testKeys, keys) {
100 | t.Fatal("not equal?")
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "http-api-tester",
3 | "version": "0.0.0",
4 | "homepage": "https://github.com/jmjoy/http-api-tester",
5 | "authors": [
6 | "jmjoy <918734043@qq.com>"
7 | ],
8 | "license": "MIT",
9 | "ignore": [
10 | "**/.*",
11 | "node_modules",
12 | "bower_components",
13 | "test",
14 | "tests"
15 | ],
16 | "dependencies": {
17 | "bootstrap": "~3.3.5",
18 | "bootstrap-select": "~1.7.5",
19 | "bootstrap-switch": "~3.3.2",
20 | "handlebars": "~4.0.5",
21 | "jquery.hotkeys": "jQuery.Hotkeys#^0.2.0"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/controller/bookmark.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "github.com/jmjoy/http-api-tester/app"
5 | "github.com/jmjoy/http-api-tester/model"
6 | )
7 |
8 | type BookmarkController struct {
9 | *app.Controller
10 | }
11 |
12 | // Get: Get current bookmark
13 | func (this *BookmarkController) Get() (err error) {
14 | data, err := model.BookmarkModel.GetCurrent()
15 | if err != nil {
16 | return
17 | }
18 |
19 | return this.JsonSuccess(data)
20 | }
21 |
22 | // Post: Set current bookmark
23 | func (this *BookmarkController) Post() (err error) {
24 | var inputs map[string]string
25 | if err = this.ParseJsonBody(&inputs); err != nil {
26 | return
27 | }
28 |
29 | data, err := model.BookmarkModel.SetCurrent(inputs["Name"])
30 | if err != nil {
31 | return
32 | }
33 |
34 | return this.JsonSuccess(data)
35 | }
36 |
--------------------------------------------------------------------------------
/controller/bookmarks.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "github.com/jmjoy/http-api-tester/app"
5 | "github.com/jmjoy/http-api-tester/model"
6 | )
7 |
8 | type BookmarksController struct {
9 | *app.Controller
10 | }
11 |
12 | // Get: get bookmark config by name or current
13 | func (this *BookmarksController) Get() error {
14 | name := this.QueryGet("name")
15 |
16 | data, err := model.BookmarksModel.Get(name)
17 | if err != nil {
18 | return err
19 | }
20 |
21 | return this.JsonSuccess(data)
22 | }
23 |
24 | // Post: add bookmark config
25 | func (this *BookmarksController) Post() error {
26 | return this.Upsert(model.UPSERT_ADD)
27 | }
28 |
29 | // Put: update bookmark config
30 | func (this *BookmarksController) Put() error {
31 | return this.Upsert(model.UPSERT_UPDATE)
32 | }
33 |
34 | func (this *BookmarksController) Upsert(typ model.UpsertType) (err error) {
35 | var bookmark model.Bookmark
36 |
37 | if err = this.ParseJsonBody(&bookmark); err != nil {
38 | return
39 | }
40 |
41 | if err = model.BookmarksModel.Upsert(bookmark, typ); err != nil {
42 | return
43 | }
44 |
45 | return this.JsonSuccess(nil)
46 | }
47 |
48 | // Delete: delete bookmark
49 | func (this *BookmarksController) Delete() (err error) {
50 | name := this.QueryGet("Name")
51 | if err = model.BookmarksModel.Delete(name); err != nil {
52 | return
53 | }
54 | return this.JsonSuccess(nil)
55 | }
56 |
--------------------------------------------------------------------------------
/controller/favicon.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "github.com/jmjoy/http-api-tester/app"
5 | "github.com/jmjoy/http-api-tester/text"
6 | )
7 |
8 | type FaviconController struct {
9 | *app.Controller
10 | }
11 |
12 | // Get: favicon.ico
13 | func (this *FaviconController) Get() (err error) {
14 | favicon := text.ProvideBytes("favicon.ico")
15 | _, err = this.W.Write(favicon)
16 | return
17 | }
18 |
--------------------------------------------------------------------------------
/controller/history.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "github.com/jmjoy/http-api-tester/app"
5 | "github.com/jmjoy/http-api-tester/model"
6 | )
7 |
8 | type HistoryController struct {
9 | *app.Controller
10 | }
11 |
12 | // Get: get all history
13 | func (this *HistoryController) Get() error {
14 | datas, err := model.HistoryModel.GetAll()
15 | if err != nil {
16 | return err
17 | }
18 |
19 | return this.JsonSuccess(datas)
20 | }
21 |
--------------------------------------------------------------------------------
/controller/index.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "io"
5 |
6 | "github.com/jmjoy/http-api-tester/app"
7 | "github.com/jmjoy/http-api-tester/model"
8 | "github.com/jmjoy/http-api-tester/text"
9 | )
10 |
11 | type IndexController struct {
12 | *app.Controller
13 | }
14 |
15 | // Get:
16 | func (this *IndexController) Get() error {
17 | switch this.QueryGet("act") {
18 | case "initData": // api for get init data
19 | return this.initData()
20 |
21 | default: // index page
22 | return this.indexPage()
23 | }
24 | }
25 |
26 | func (this *IndexController) indexPage() (err error) {
27 | _, err = io.WriteString(this.W, text.ProvideString("view/index.html"))
28 | return
29 | }
30 |
31 | func (this *IndexController) initData() (err error) {
32 | bookmark, err := model.BookmarkModel.GetCurrent()
33 | if err != nil {
34 | return
35 | }
36 |
37 | bookmarkNames, err := model.BookmarksModel.GetAllNames()
38 | if err != nil {
39 | return
40 | }
41 |
42 | return this.JsonSuccess(map[string]interface{}{
43 | "Bookmarks": bookmarkNames,
44 | "Bookmark": bookmark,
45 | "Plugins": model.PluginPool(),
46 | })
47 | }
48 |
49 | // Post: Submit
50 | func (this *IndexController) Post() (err error) {
51 | var data model.Data
52 | if err = this.ParseJsonBody(&data); err != nil {
53 | return
54 | }
55 |
56 | resp, err := model.SubmitModel.Submit(data)
57 | if err != nil {
58 | return
59 | }
60 | return this.JsonSuccess(resp)
61 | }
62 |
--------------------------------------------------------------------------------
/controller/static.go:
--------------------------------------------------------------------------------
1 | package controller
2 |
3 | import (
4 | "path"
5 |
6 | "github.com/jmjoy/http-api-tester/app"
7 | "github.com/jmjoy/http-api-tester/text"
8 | )
9 |
10 | var extMimeMap = map[string]string{
11 | ".css": "text/css;charset=utf-8",
12 | ".js": "application/x-javascript",
13 | ".map": "text/map;charset=utf-8",
14 | }
15 |
16 | type StaticController struct {
17 | *app.Controller
18 | }
19 |
20 | func (this *StaticController) Get() error {
21 | uS := this.R.URL.String()[1:] // remove fst "/"
22 | ext := path.Ext(uS)
23 |
24 | var buf []byte
25 | var cType string
26 |
27 | if mimeType, has := extMimeMap[ext]; has {
28 | cType = mimeType
29 | content := text.ProvideString(uS)
30 | if content == "" {
31 | return app.ErrNotFound
32 | }
33 | buf = []byte(content)
34 |
35 | } else { // is not textual file
36 | buf = text.ProvideBytes(uS)
37 | if buf == nil {
38 | return app.ErrNotFound
39 | }
40 | }
41 |
42 | this.W.Header().Set("Content-Type", cType)
43 | this.W.Write(buf)
44 | return nil
45 | }
46 |
--------------------------------------------------------------------------------
/create_view.sh:
--------------------------------------------------------------------------------
1 | file2string -pkg text -o text/text.go -var Text \
2 | static/bower_components/bootstrap/dist/**/*.min.* \
3 | static/bower_components/bootstrap/dist/fonts/* \
4 | static/bower_components/bootstrap-select/dist/**/*.min.* \
5 | static/bower_components/bootstrap-select/dist/**/*.map \
6 | static/bower_components/bootstrap-switch/dist/**/*.min.* \
7 | static/bower_components/bootstrap-switch/dist/css/bootstrap3/*.min.* \
8 | static/bower_components/handlebars/handlebars.min.js \
9 | static/bower_components/jquery/dist/jquery.min.js \
10 | static/bower_components/jquery/dist/jquery.min.map \
11 | static/bower_components/jquery.hotkeys/jquery.hotkeys.js \
12 | static/*.* \
13 | view/* \
14 | favicon.ico
15 |
--------------------------------------------------------------------------------
/errors/errors.go:
--------------------------------------------------------------------------------
1 | // user errros
2 | package errors
3 |
4 | import (
5 | "github.com/jmjoy/http-api-tester/app"
6 | )
7 |
8 | // ApiStatusError
9 | var (
10 | ErrBookmarkNameEmpty = app.NewApiStatusError(1000, "书签名字不能为空")
11 | ErrBookmarkNameInvalid = app.NewApiStatusError(1001, "书签名字不合格")
12 | ErrBookmarkNotFound = app.NewApiStatusError(1002, "书签不存在")
13 | ErrBookmarkExisted = app.NewApiStatusError(1003, "书签已存在")
14 | ErrBookmarkEditDefault = app.NewApiStatusError(1003, "不能新增或修改默认书签")
15 |
16 | ErrUrlEmpty = app.NewApiStatusError(2000, "URL不能为空")
17 | ErrUrlUnknowScheme = app.NewApiStatusError(2001, "URL未知协议:%s")
18 | ErrUrlEmptyHost = app.NewApiStatusError(2002, "URL的Host不能为空")
19 | ErrUrlUnknowArgMethod = app.NewApiStatusError(2003, "URL的参数中包含未知请求方式:%s")
20 |
21 | ErrJsonCompact = app.NewApiStatusError(3000, "JSON格式出错:%s")
22 | )
23 |
--------------------------------------------------------------------------------
/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jmjoy/http-api-tester/aaac7d5a88bbcf2fd23049bcc08e8a55ab2b7b9e/favicon.ico
--------------------------------------------------------------------------------
/img/shortcut0.5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jmjoy/http-api-tester/aaac7d5a88bbcf2fd23049bcc08e8a55ab2b7b9e/img/shortcut0.5.jpg
--------------------------------------------------------------------------------
/img/shortcut0.6.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jmjoy/http-api-tester/aaac7d5a88bbcf2fd23049bcc08e8a55ab2b7b9e/img/shortcut0.6.bmp
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 |
6 | "github.com/jmjoy/http-api-tester/app"
7 | _ "github.com/jmjoy/http-api-tester/plugin"
8 | "github.com/jmjoy/http-api-tester/router"
9 | "github.com/jmjoy/http-api-tester/text"
10 | )
11 |
12 | const (
13 | NAME = "http-api-tester"
14 | VERSION = "0.5"
15 |
16 | IS_DEBUG = false
17 | )
18 |
19 | var (
20 | gPort string
21 | gDbPath string
22 | )
23 |
24 | func init() {
25 | flag.StringVar(&gPort, "p", "8080", "服务器运行端口")
26 | flag.StringVar(&gDbPath, "db", NAME+".db", "数据库路径")
27 | }
28 |
29 | func main() {
30 | flag.Parse()
31 |
32 | text.IsDebug = IS_DEBUG
33 | text.BasePath = "."
34 |
35 | app.Run(app.Config{
36 | Port: gPort,
37 | DbPath: gDbPath,
38 | Routers: router.Routers,
39 | })
40 | }
41 |
--------------------------------------------------------------------------------
/model/bookmark.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "github.com/jmjoy/http-api-tester/app"
5 | )
6 |
7 | var BookmarkModel = &bookmarkModel{
8 | Model: app.NewModel("bookmark"),
9 | selected: "selected",
10 | }
11 |
12 | type bookmarkModel struct {
13 | *app.Model
14 | selected string
15 | }
16 |
17 | func (this *bookmarkModel) GetCurrentKey() (name string, has bool, err error) {
18 | has, err = this.Get(this.selected, &name)
19 | return
20 | }
21 |
22 | func (this *bookmarkModel) GetCurrent() (bookmark Bookmark, err error) {
23 | name, has, err := this.GetCurrentKey()
24 | if err != nil {
25 | return
26 | }
27 | if !has {
28 | return Bookmark{
29 | Name: BOOKMARK_DEFAULT_NAME,
30 | Data: DataDefault(),
31 | }, nil
32 | }
33 |
34 | data, err := BookmarksModel.Get(name)
35 | bookmark = Bookmark{
36 | Name: name,
37 | Data: data,
38 | }
39 | return
40 | }
41 |
42 | func (this *bookmarkModel) SetCurrent(name string) (data Data, err error) {
43 | if data, err = BookmarksModel.Get(name); err != nil {
44 | return
45 | }
46 |
47 | if err = this.Put(this.selected, name); err != nil {
48 | return
49 | }
50 |
51 | return
52 | }
53 |
54 | func (this *bookmarkModel) DeleteCurrent() (err error) {
55 | return this.Delete(this.selected)
56 | }
57 |
--------------------------------------------------------------------------------
/model/bookmark_test.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 |
7 | "github.com/jmjoy/http-api-tester/errors"
8 | )
9 |
10 | func TestBookmarkCRUD(t *testing.T) {
11 | testkey := "testBookmark"
12 |
13 | name, has, err := BookmarkModel.GetCurrentKey()
14 | t.Log("name:", name)
15 | if err != nil {
16 | panic(err)
17 | }
18 | if has {
19 | t.Fatal("Has set current bookmark?")
20 | }
21 |
22 | bookmark, err := BookmarkModel.GetCurrent()
23 | if err != nil {
24 | panic(err)
25 | }
26 | if !reflect.DeepEqual(bookmark.Data, DataDefault()) ||
27 | bookmark.Name != BOOKMARK_DEFAULT_NAME {
28 | t.Fatal("not euqal?")
29 | }
30 |
31 | var defaultBookmark = Bookmark{
32 | Name: testkey,
33 | Data: DataDefault(),
34 | }
35 | err = BookmarksModel.Upsert(defaultBookmark, UPSERT_ADD)
36 | if err != nil {
37 | panic(err)
38 | }
39 |
40 | _, err = BookmarkModel.SetCurrent("not-existed")
41 | if err != errors.ErrBookmarkNotFound {
42 | t.Fatal("found?")
43 | }
44 | data, err := BookmarkModel.SetCurrent(testkey)
45 | if err != nil {
46 | panic(err)
47 | }
48 | if !reflect.DeepEqual(data, defaultBookmark.Data) {
49 | t.Fatal("not equal?")
50 | }
51 |
52 | bookmark, err = BookmarkModel.GetCurrent()
53 | if err != nil {
54 | panic(err)
55 | }
56 |
57 | if !reflect.DeepEqual(bookmark.Data, DataDefault()) || bookmark.Name != testkey {
58 | t.Fatal("not equal?")
59 | }
60 |
61 | if err = BookmarksModel.Delete(testkey); err != nil {
62 | panic(err)
63 | }
64 |
65 | if err = BookmarkModel.DeleteCurrent(); err != nil {
66 | panic(err)
67 | }
68 |
69 | bookmark, err = BookmarkModel.GetCurrent()
70 | if err != nil {
71 | panic(err)
72 | }
73 |
74 | if !reflect.DeepEqual(bookmark.Data, DataDefault()) {
75 | t.Fatal("not equal?")
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/model/bookmarks.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "github.com/jmjoy/http-api-tester/app"
5 | "github.com/jmjoy/http-api-tester/errors"
6 | )
7 |
8 | var BookmarksModel = &bookmarksModel{
9 | Model: app.NewModel("bookmarks"),
10 | }
11 |
12 | type bookmarksModel struct {
13 | *app.Model
14 | }
15 |
16 | func (this *bookmarksModel) Get(name string) (data Data, err error) {
17 | if err = this.validateBookmarkName(name); err != nil {
18 | // get default bookmark
19 | if err == errors.ErrBookmarkEditDefault {
20 | data = DataDefault()
21 | err = nil
22 | }
23 | return
24 | }
25 |
26 | has, err := this.Model.Get(name, &data)
27 | if err != nil {
28 | return
29 | }
30 |
31 | if !has {
32 | err = errors.ErrBookmarkNotFound
33 | return
34 | }
35 |
36 | return
37 | }
38 |
39 | func (this *bookmarksModel) GetAllNames() (names []string, err error) {
40 | otherNames, err := this.Model.Keys()
41 | if err != nil {
42 | return
43 | }
44 | names = []string{BOOKMARK_DEFAULT_NAME}
45 | if len(otherNames) != 0 {
46 | names = append(names, otherNames...)
47 | }
48 | return
49 | }
50 |
51 | func (this *bookmarksModel) Upsert(bookmark Bookmark, typ UpsertType) (err error) {
52 | if err = this.validateBookmarkName(bookmark.Name); err != nil {
53 | return
54 | }
55 |
56 | var data Data
57 | has, err := this.Model.Get(bookmark.Name, &data)
58 | if err != nil {
59 | return
60 | }
61 |
62 | // check is exists or not
63 | if typ == UPSERT_ADD {
64 | if has {
65 | return errors.ErrBookmarkExisted
66 | }
67 | } else {
68 | if !has {
69 | return errors.ErrBookmarkNotFound
70 | }
71 | }
72 |
73 | if err = this.Put(bookmark.Name, bookmark.Data); err != nil {
74 | return
75 | }
76 |
77 | // set upserted bookmark to current
78 | _, err = BookmarkModel.SetCurrent(bookmark.Name)
79 | return
80 | }
81 |
82 | func (this *bookmarksModel) Delete(name string) (err error) {
83 | if err = this.validateBookmarkName(name); err != nil {
84 | return
85 | }
86 |
87 | if err = this.Model.Delete(name); err != nil {
88 | return
89 | }
90 |
91 | return BookmarkModel.DeleteCurrent()
92 | }
93 |
94 | func (this *bookmarksModel) validateBookmarkName(name string) error {
95 | if name == "" {
96 | return errors.ErrBookmarkNameEmpty
97 | }
98 |
99 | if name == BOOKMARK_DEFAULT_NAME {
100 | return errors.ErrBookmarkEditDefault
101 | }
102 |
103 | // TODO 暂时允许所有名字
104 | return nil
105 | }
106 |
--------------------------------------------------------------------------------
/model/bookmarks_test.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "testing"
5 |
6 | "reflect"
7 |
8 | "github.com/jmjoy/http-api-tester/errors"
9 | )
10 |
11 | var testData = Data{}
12 |
13 | func TestBookmarksCRUD(t *testing.T) {
14 | testkey := "test"
15 |
16 | _, err := BookmarksModel.Get(testkey)
17 | if err != errors.ErrBookmarkNotFound {
18 | t.Fatal("bookmark existd?")
19 | }
20 |
21 | var defaultBookmark = Bookmark{
22 | Name: testkey,
23 | Data: DataDefault(),
24 | }
25 |
26 | err = BookmarksModel.Upsert(defaultBookmark, UPSERT_ADD)
27 | if err != nil {
28 | panic(err)
29 | }
30 |
31 | var inDefaultBookmark = Bookmark{
32 | Name: BOOKMARK_DEFAULT_NAME,
33 | Data: DataDefault(),
34 | }
35 | err = BookmarksModel.Upsert(inDefaultBookmark, UPSERT_ADD)
36 | if err != errors.ErrBookmarkEditDefault {
37 | t.Fatal("Can edit default?")
38 | }
39 |
40 | err = BookmarksModel.Upsert(defaultBookmark, UPSERT_ADD)
41 | if err != errors.ErrBookmarkExisted {
42 | t.Fatal("Bookmark not existed?")
43 | }
44 |
45 | data, err := BookmarksModel.Get(testkey)
46 | if err != nil {
47 | panic(err)
48 | }
49 | if !reflect.DeepEqual(data, defaultBookmark.Data) {
50 | t.Fatal("Data not equal!")
51 | }
52 |
53 | // new bookmark
54 | newBookmark := defaultBookmark
55 | defaultBookmark.Data.Method = "POST"
56 | defaultBookmark.Data.Url = "http://www.baidu.com"
57 |
58 | err = BookmarksModel.Upsert(newBookmark, UPSERT_UPDATE)
59 | if err != nil {
60 | panic(err)
61 | }
62 |
63 | data, err = BookmarksModel.Get(testkey)
64 | if err != nil {
65 | panic(err)
66 | }
67 | if !reflect.DeepEqual(data, newBookmark.Data) {
68 | t.Fatal("Data not equal!")
69 | }
70 |
71 | err = BookmarksModel.Delete(testkey)
72 | if err != nil {
73 | panic(err)
74 | }
75 |
76 | _, err = BookmarksModel.Get(testkey)
77 | if err != errors.ErrBookmarkNotFound {
78 | t.Fatal("bookmark existd?")
79 | }
80 |
81 | _, has, err := BookmarkModel.GetCurrentKey()
82 | if has {
83 | t.Fatal("has selected bookmark?")
84 | }
85 |
86 | err = BookmarksModel.Delete(BOOKMARK_DEFAULT_NAME)
87 | if err != errors.ErrBookmarkEditDefault {
88 | t.Fatal("Can delete default?")
89 | }
90 | }
91 |
92 | func TestGetAllNames(t *testing.T) {
93 | testKey := "test0"
94 | defaultNames := []string{BOOKMARK_DEFAULT_NAME}
95 | testNames := []string{BOOKMARK_DEFAULT_NAME, testKey}
96 |
97 | names, err := BookmarksModel.GetAllNames()
98 | if err != nil {
99 | panic(err)
100 | }
101 | t.Log(defaultNames, names)
102 | if !reflect.DeepEqual(defaultNames, names) {
103 | t.Fatal("not equal?")
104 | }
105 |
106 | if err = BookmarksModel.Put(testKey, "hello world"); err != nil {
107 | panic(err)
108 | }
109 |
110 | names, err = BookmarksModel.GetAllNames()
111 | if err != nil {
112 | panic(err)
113 | }
114 | if !reflect.DeepEqual(testNames, names) {
115 | t.Fatal("not equal?")
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/model/history.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import "github.com/jmjoy/http-api-tester/app"
4 |
5 | var HistoryModel = &historyModel{
6 | Model: app.NewModel("history"),
7 | key: "history",
8 | }
9 |
10 | type historyModel struct {
11 | *app.Model
12 | key string
13 | }
14 |
15 | func (this *historyModel) GetAll() (datas []Data, err error) {
16 | has, err := this.Model.Get(this.key, &datas)
17 | if err != nil {
18 | return
19 | }
20 |
21 | if !has {
22 | datas = []Data{}
23 | return
24 | }
25 |
26 | return
27 | }
28 |
29 | func (this *historyModel) Insert(data Data) (err error) {
30 | var datas []Data
31 | datas, err = this.GetAll()
32 | if err != nil {
33 | return
34 | }
35 |
36 | datas = append(datas, data)
37 | if len(datas) > 50 { // 最多保存50条历史记录
38 | datas = datas[1:]
39 | }
40 |
41 | err = this.Model.Put(this.key, datas)
42 | return
43 | }
44 |
--------------------------------------------------------------------------------
/model/model.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | type UpsertType string
4 |
5 | const (
6 | UPSERT_ADD UpsertType = "ADD"
7 | UPSERT_UPDATE UpsertType = "UPDATE"
8 | )
9 |
10 | const (
11 | BOOKMARK_DEFAULT_NAME = "Default"
12 | PLUGIN_DEFAULT_NAME = "default"
13 | )
14 |
--------------------------------------------------------------------------------
/model/model_test.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "os"
5 | "testing"
6 |
7 | "net/http"
8 | "net/http/httptest"
9 |
10 | "github.com/jmjoy/http-api-tester/app"
11 | )
12 |
13 | var testSrv *httptest.Server
14 | var testData0 Data
15 |
16 | func TestMain(m *testing.M) {
17 | dbPath := "./model_test.db"
18 | if _, err := os.Stat(dbPath); err == nil {
19 | os.Remove(dbPath)
20 | }
21 | defer os.Remove(dbPath)
22 |
23 | // init db
24 | if err := app.InitDb(dbPath); err != nil {
25 | panic(err)
26 | }
27 |
28 | // test server
29 | testSrv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
30 | if err := r.ParseForm(); err != nil {
31 | panic(err)
32 | }
33 | content := r.URL.Query().Encode() + " " + r.PostForm.Encode()
34 | w.Write([]byte(content))
35 | }))
36 | defer testSrv.Close()
37 |
38 | initData()
39 |
40 | m.Run()
41 | }
42 |
43 | func initData() {
44 | testData0 = Data{
45 | Method: "POST",
46 | Url: testSrv.URL,
47 | Args: []Arg{
48 | Arg{"k1", "v1", "GET"},
49 | Arg{"k2", "v2", "GET"},
50 | Arg{"k3", "v3", "POST"},
51 | Arg{"k4", "v4", "POST"},
52 | },
53 | }
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/model/struct.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | goerrors "errors"
5 | "net/http"
6 | "net/url"
7 |
8 | "strings"
9 |
10 | "bytes"
11 | "encoding/json"
12 |
13 | "github.com/jmjoy/http-api-tester/errors"
14 | )
15 |
16 | // bookmarks
17 | type BookmarkMap map[string]Data
18 |
19 | // named bookmark
20 | type Bookmark struct {
21 | Name string
22 | Data Data
23 | }
24 |
25 | // Submit Arg
26 | type Arg struct {
27 | Key string
28 | Value string
29 | Method string
30 | }
31 |
32 | // Header
33 | type Header struct {
34 | Key string
35 | Value string
36 | }
37 |
38 | // Benchmark data
39 | type Bm struct {
40 | Switch bool
41 | N uint
42 | C uint
43 | }
44 |
45 | type Plugin struct {
46 | Key string
47 | Data map[string]string
48 | }
49 |
50 | // Submit Data
51 | type Data struct {
52 | Method string
53 | Url string
54 | Args []Arg
55 | Headers []Header
56 | Bm Bm
57 | Plugin Plugin
58 | Enctype string
59 | JsonContent string
60 | PlainContent string
61 | }
62 |
63 | func DataDefault() Data {
64 | return Data{
65 | Method: "GET",
66 | Args: []Arg{},
67 | Headers: []Header{},
68 | Bm: Bm{
69 | N: 100,
70 | C: 10,
71 | },
72 | Plugin: Plugin{
73 | Key: PLUGIN_DEFAULT_NAME,
74 | Data: map[string]string{},
75 | },
76 | }
77 | }
78 |
79 | func (this Data) Validate() error {
80 | if this.Url == "" {
81 | return errors.ErrUrlEmpty
82 | }
83 |
84 | u, err := url.Parse(this.Url)
85 | if err != nil {
86 | return err
87 | }
88 |
89 | if u.Scheme != "http" && u.Scheme != "https" {
90 | return errors.ErrUrlUnknowScheme.NewMessageSpf(u.Scheme)
91 | }
92 |
93 | if u.Host == "" {
94 | return errors.ErrUrlEmptyHost
95 | }
96 |
97 | for _, arg := range this.Args {
98 | switch arg.Method {
99 | case "GET", "POST":
100 | default:
101 | return errors.ErrUrlUnknowArgMethod.NewMessageSpf(arg.Method)
102 | }
103 | }
104 |
105 | return nil
106 | }
107 |
108 | type Response struct {
109 | ReqUrl string
110 | ReqBody string
111 | Status string
112 | Test string
113 | Bm string
114 | }
115 |
116 | type RequestMaker struct {
117 | Method string
118 | Url *url.URL
119 | ContentType string
120 | Body string
121 | Headers []Header
122 | }
123 |
124 | func NewRequestMaker(data Data) (reqMaker *RequestMaker, err error) {
125 | u, err := url.Parse(data.Url)
126 | if err != nil {
127 | return
128 | }
129 |
130 | var contentType string
131 | var body string
132 |
133 | switch data.Enctype {
134 | case "x_www":
135 | contentType = "application/x-www-form-urlencoded"
136 |
137 | querys := u.Query()
138 | forms := make(url.Values)
139 |
140 | for _, arg := range data.Args {
141 | switch arg.Method {
142 | case "GET":
143 | querys.Add(arg.Key, arg.Value)
144 |
145 | case "POST":
146 | forms.Add(arg.Key, arg.Value)
147 | }
148 | }
149 |
150 | u.RawQuery = querys.Encode()
151 |
152 | body = forms.Encode()
153 |
154 | case "json":
155 | contentType = "text/json"
156 |
157 | content := strings.TrimSpace(data.JsonContent)
158 | if len(content) == 0 {
159 | break
160 | }
161 |
162 | buffer := new(bytes.Buffer)
163 | err = json.Compact(buffer, []byte(content))
164 | if err != nil {
165 | err = errors.ErrJsonCompact.NewMessageSpf(err)
166 | return
167 | }
168 | body = buffer.String()
169 |
170 | case "plain":
171 | contentType = "text/plain"
172 | body = data.PlainContent
173 | }
174 |
175 | reqMaker = &RequestMaker{
176 | Method: data.Method,
177 | Url: u,
178 | ContentType: contentType,
179 | Body: body,
180 | Headers: data.Headers,
181 | }
182 | return
183 | }
184 |
185 | func (this *RequestMaker) NewRequest() (request *http.Request, err error) {
186 | request, err = http.NewRequest(
187 | this.Method,
188 | this.Url.String(),
189 | strings.NewReader(this.Body),
190 | )
191 | if err != nil {
192 | return
193 | }
194 |
195 | request.Header.Set("Content-Type", this.ContentType)
196 |
197 | for _, header := range this.Headers {
198 | request.Header.Set(http.CanonicalHeaderKey(header.Key), header.Value)
199 | }
200 |
201 | return
202 | }
203 |
204 | var pluginPool = make(map[string]PluginInfo)
205 |
206 | func PluginPool() map[string]PluginInfo {
207 | return pluginPool
208 | }
209 |
210 | type pluginHandler func(Data) (Data, error)
211 |
212 | type PluginInfo struct {
213 | DisplayName string
214 | FieldNames map[string]string
215 | Handler pluginHandler `json:"-"`
216 | }
217 |
218 | func (this PluginInfo) IsNull() bool {
219 | return this.DisplayName == "" || this.FieldNames == nil || this.Handler == nil
220 | }
221 |
222 | func RegisterPluginHandler(name string, info PluginInfo) error {
223 | if _, has := pluginPool[name]; has {
224 | return goerrors.New("plugin has existed, CAN'T register again")
225 | }
226 | if info.IsNull() {
227 | return goerrors.New("handler CAN'T be NULL")
228 | }
229 | pluginPool[name] = info
230 | return nil
231 | }
232 |
233 | func HookPlugin(data Data) (Data, error) {
234 | plugin, has := pluginPool[data.Plugin.Key]
235 | if !has {
236 | // if not exists, return default handler
237 | return data, nil
238 | }
239 | return plugin.Handler(data)
240 | }
241 |
242 | func init() {
243 | // default plugin: not use!
244 | RegisterPluginHandler(PLUGIN_DEFAULT_NAME, PluginInfo{
245 | DisplayName: "不使用插件",
246 | FieldNames: map[string]string{},
247 | Handler: func(data Data) (Data, error) {
248 | return data, nil
249 | },
250 | })
251 | }
252 |
--------------------------------------------------------------------------------
/model/struct_test.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "io/ioutil"
5 | "net/http"
6 | "testing"
7 | )
8 |
9 | func TestRequestMaker(t *testing.T) {
10 | reqMaker, err := NewRequestMaker(testData0)
11 | if err != nil {
12 | panic(err)
13 | }
14 | req, err := reqMaker.NewRequest()
15 | if err != nil {
16 | panic(err)
17 | }
18 | t.Log("request content-type:", req.Header.Get("Content-Type"))
19 |
20 | response, err := http.DefaultClient.Do(req)
21 | if err != nil {
22 | panic(err)
23 | }
24 | defer response.Body.Close()
25 | buf, err := ioutil.ReadAll(response.Body)
26 | if err != nil {
27 | panic(err)
28 | }
29 | t.Log(string(buf))
30 |
31 | if "k1=v1&k2=v2 k3=v3&k4=v4" != string(buf) {
32 | t.Fatal("response content not correct!")
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/model/submit.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "net/http"
7 | "strings"
8 |
9 | "bytes"
10 |
11 | "github.com/jmjoy/boomer"
12 | )
13 |
14 | var SubmitModel = &submitModel{}
15 |
16 | type submitModel struct{}
17 |
18 | func (this *submitModel) Submit(data Data) (resp Response, err error) {
19 | if err = data.Validate(); err != nil {
20 | return
21 | }
22 |
23 | data, err = HookPlugin(data)
24 | if err != nil {
25 | return
26 | }
27 |
28 | reqMaker, err := NewRequestMaker(data)
29 | if err != nil {
30 | return
31 | }
32 |
33 | var response Response
34 |
35 | if err = this.submitTest(data, reqMaker, &response); err != nil {
36 | return
37 | }
38 |
39 | if err = this.submitBenckmark(data, data.Bm, reqMaker, &response); err != nil {
40 | return
41 | }
42 |
43 | // save to history
44 | if err = HistoryModel.Insert(data); err != nil {
45 | return
46 | }
47 |
48 | return response, nil
49 | }
50 |
51 | func (this *submitModel) submitTest(data Data, reqMaker *RequestMaker, response *Response) error {
52 | req, err := reqMaker.NewRequest()
53 | if err != nil {
54 | return err
55 | }
56 |
57 | resp, err := http.DefaultClient.Do(req)
58 | if err != nil {
59 | return err
60 | }
61 | defer resp.Body.Close()
62 |
63 | respBodyBuf, err := ioutil.ReadAll(resp.Body)
64 | if err != nil {
65 | return err
66 | }
67 |
68 | response.ReqUrl = req.URL.String()
69 | response.ReqBody = reqMaker.Body
70 | response.Status = resp.Status
71 | response.Test = string(respBodyBuf)
72 |
73 | return nil
74 | }
75 |
76 | func (this *submitModel) submitBenckmark(data Data, bm Bm, reqMaker *RequestMaker, response *Response) error {
77 | if !bm.Switch {
78 | return nil
79 | }
80 |
81 | // limit N, C reduce server pressure.
82 | var n, c = bm.N, bm.C
83 | if bm.N >= 1000 {
84 | n = 1000
85 | }
86 | if bm.C >= 500 {
87 | c = 500
88 | }
89 |
90 | req, err := reqMaker.NewRequest()
91 | if err != nil {
92 | return err
93 | }
94 |
95 | result := (&boomer.Boomer{
96 | Request: req,
97 | RequestBody: reqMaker.Body,
98 | N: int(n),
99 | C: int(c),
100 | Timeout: 35,
101 | }).Run()
102 |
103 | response.Bm = formatReportResult(result)
104 |
105 | return nil
106 | }
107 |
108 | func formatReportResult(result *boomer.ReportResult) string {
109 | buffer := new(bytes.Buffer)
110 |
111 | buffer.WriteString("\nSummary:\n")
112 | buffer.WriteString(fmt.Sprintf(" Total:\t%4.4f secs.\n", result.Summary.TotalSecond))
113 | buffer.WriteString(fmt.Sprintf(" Slowest:\t%4.4f secs.\n", result.Summary.SlowestSecond))
114 | buffer.WriteString(fmt.Sprintf(" Fastest:\t%4.4f secs.\n", result.Summary.FastestSecond))
115 | buffer.WriteString(fmt.Sprintf(" Average:\t%4.4f secs.\n", result.Summary.AverageSecond))
116 | buffer.WriteString(fmt.Sprintf(" Requests/sec:\t%4.4f\n", result.Summary.RequestsPerSec))
117 | if result.Summary.TotalSize > 0 {
118 | buffer.WriteString(fmt.Sprintf(" Total Data Received:\t%d bytes.\n", result.Summary.TotalSize))
119 | buffer.WriteString(fmt.Sprintf(" Response Size per Request:\t%d bytes.\n", result.Summary.SizePerRequest))
120 | }
121 |
122 | buffer.WriteString("\nStatus code distribution:\n")
123 | for code, num := range result.StatusCodeDist {
124 | buffer.WriteString(fmt.Sprintf(" [%d]\t%d responses\n", code, num))
125 | }
126 |
127 | buffer.WriteString("\nResponse time histogram:\n")
128 | for _, v := range result.ResponseTimes {
129 | buffer.WriteString(fmt.Sprintf(" %4.3f [%v]\t|%v\n", v.Second, v.Count, strings.Repeat("*", v.BarLen)))
130 | }
131 |
132 | buffer.WriteString("\nLatency distribution:\n")
133 | for k, v := range result.LatencyDist {
134 | buffer.WriteString(fmt.Sprintf(" %v%% in %4.4f secs.\n", k, v))
135 | }
136 |
137 | if len(result.ErrorDist) > 0 {
138 | buffer.WriteString("\nError distribution:\n")
139 | for err, num := range result.ErrorDist {
140 | buffer.WriteString(fmt.Sprintf(" [%d]\t%s\n", num, err))
141 | }
142 | }
143 |
144 | return buffer.String()
145 | }
146 |
--------------------------------------------------------------------------------
/model/submit_test.go:
--------------------------------------------------------------------------------
1 | package model
2 |
3 | import (
4 | "reflect"
5 | "testing"
6 | )
7 |
8 | func TestSubmitTest(t *testing.T) {
9 | testResp := Response{
10 | ReqUrl: testSrv.URL + "?k1=v1&k2=v2",
11 | Status: "200 OK",
12 | Test: "k1=v1&k2=v2 k3=v3&k4=v4",
13 | ReqBody: "k3=v3&k4=v4",
14 | }
15 |
16 | resp, err := SubmitModel.Submit(testData0)
17 | if err != nil {
18 | panic(err)
19 | }
20 |
21 | t.Logf("%#v", resp)
22 | if !reflect.DeepEqual(testResp, resp) {
23 | t.Fatal("response not equal!")
24 | }
25 | }
26 |
27 | func TestSubmitBenckmark(t *testing.T) {
28 | testData1 := testData0
29 | testData1.Bm = Bm{
30 | Switch: true,
31 | N: 10,
32 | C: 1,
33 | }
34 |
35 | resp, err := SubmitModel.Submit(testData1)
36 | if err != nil {
37 | panic(err)
38 | }
39 |
40 | t.Log(resp.Bm)
41 | if resp.Bm == "" {
42 | t.Fatal("no benchmark data?")
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/plugin/md5signature.go:
--------------------------------------------------------------------------------
1 | package plugin
2 |
3 | import (
4 | "crypto/md5"
5 | "errors"
6 | "fmt"
7 | "sort"
8 | "strings"
9 |
10 | "github.com/jmjoy/http-api-tester/model"
11 | )
12 |
13 | func init() {
14 | handler := func(data model.Data) (model.Data, error) {
15 | keyName, has := data.Plugin.Data["keyName"]
16 | if !has {
17 | return model.Data{}, errors.New("md5 signature key name DOESN't exist!")
18 | }
19 |
20 | password, has := data.Plugin.Data["password"]
21 | if !has {
22 | return model.Data{}, errors.New("md5 signature password DOESN'T exist!")
23 | }
24 |
25 | argMap := make(map[string]string, len(data.Args))
26 | argKeys := make([]string, 0, len(data.Args))
27 | for _, arg := range data.Args {
28 | // if method == "GET", only when args method is "GET", arg will be signature
29 | if data.Method != "GET" || arg.Method == "GET" {
30 | argKeys = append(argKeys, arg.Key)
31 | }
32 |
33 | argMap[arg.Key] = arg.Value
34 | }
35 | sort.Strings(argKeys)
36 |
37 | values := make([]string, 0, len(argKeys))
38 | for _, argKey := range argKeys {
39 | values = append(values, argMap[argKey])
40 | }
41 |
42 | values = append(values, password)
43 | text := strings.Join(values, "")
44 | md5Text := fmt.Sprintf("%x", md5.Sum([]byte(text)))
45 | data.Args = append(data.Args, model.Arg{
46 | Key: keyName,
47 | Value: md5Text,
48 | Method: "GET",
49 | })
50 |
51 | return data, nil
52 | }
53 |
54 | model.RegisterPluginHandler("md5signature", model.PluginInfo{
55 | DisplayName: "MD5签名认证",
56 | FieldNames: map[string]string{
57 | "keyName": "密钥名称",
58 | "password": "密码",
59 | },
60 | Handler: handler,
61 | })
62 | }
63 |
--------------------------------------------------------------------------------
/router/router.go:
--------------------------------------------------------------------------------
1 | package router
2 |
3 | import (
4 | "github.com/jmjoy/http-api-tester/app"
5 | "github.com/jmjoy/http-api-tester/controller"
6 | )
7 |
8 | // TODO I want to use map[string]IController before but...
9 | var Routers = map[string]func() app.IController{
10 | "/": func() app.IController { return new(controller.IndexController) },
11 | "/favicon.ico": func() app.IController { return new(controller.FaviconController) },
12 | "/static/": func() app.IController { return new(controller.StaticController) },
13 | "/bookmark": func() app.IController { return new(controller.BookmarkController) },
14 | "/bookmarks": func() app.IController { return new(controller.BookmarksController) },
15 | "/history": func() app.IController { return new(controller.HistoryController) },
16 | }
17 |
--------------------------------------------------------------------------------
/static/bower_components/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/static/index.css:
--------------------------------------------------------------------------------
1 | @charset "utf-8";
2 |
3 | .inline {
4 | display: inline;
5 | }
6 |
7 | /* CSS Document */
8 | .jf-ObjectBrace {
9 | color: #00AA00;
10 | font-weight: bold;
11 | }
12 | .jf-ArrayBrace {
13 | color: #0033FF;
14 | font-weight: bold;
15 | }
16 | .jf-PropertyName {
17 | color: #CC0000;
18 | font-weight: bold;
19 | }
20 | .jf-String {
21 | color: #007777;
22 | }
23 | .jf-Number {
24 | color: #AA00AA;
25 | }
26 | .jf-Boolean {
27 | color: #0000FF;
28 | }
29 | .jf-Null {
30 | color: #0000FF;
31 | }
32 | .jf-Comma {
33 | color: #000000;
34 | font-weight: bold;
35 | }
36 | pre.jf-CodeContainer {
37 | margin-top: 0;
38 | margin-bottom: 0;
39 | text-align: left;
40 | }
41 |
42 | ul.navbar-right li {
43 | margin: auto 1px;
44 | }
45 |
46 | div.content {
47 | padding-top: 65px;
48 | }
49 |
50 | div.content-left {
51 |
52 | }
53 |
54 | div.content-right {
55 | padding-top: 5px;
56 | min-height: 500px;
57 | background: rgb(255, 251, 232);
58 | }
59 |
60 | div.panel {
61 | margin: 3px auto;
62 | }
63 |
64 | td {
65 | text-align: center;
66 | vertical-align: middle;
67 | }
68 |
69 | .form-inline .form-group {
70 | margin-right: 5px;
71 | }
72 |
73 | #bm_n, #bm_c {
74 | width: 8em;
75 | }
76 |
77 | #result_test_panel, #result_test_panel, #result_header_panel {
78 | margin: 5px auto;
79 | width: 100%;
80 | }
81 |
82 | .margin-right-sm {
83 | margin-right: 4px;
84 | }
85 |
86 | #enctype_left {
87 | height: 41px;
88 | line-height: 41px;
89 | }
90 |
91 | .title-headers {
92 | padding-top: 4px;
93 | padding-bottom: 4px;
94 | }
--------------------------------------------------------------------------------
/static/index.js:
--------------------------------------------------------------------------------
1 | var configs = {
2 | "indexUrl": "/",
3 | "initDataUrl": "/?act=initData",
4 | "bookmarkUrl": "/bookmark",
5 | "bookmarksUrl": "/bookmarks",
6 | "historyUrl": "/history"
7 | };
8 |
9 | // TODO Move all global variables to this global object
10 | var global = {
11 | "environ": "prod",
12 | "plugins": {},
13 | "currentEnctype": "x_www",
14 | "currentHistory": []
15 | };
16 |
17 | var utils = {
18 | "tplCompile": function(id) {
19 | var html = $("#" + id).html();
20 | return Handlebars.compile(html);
21 | },
22 |
23 | "ajax": function(url, method, requestData, successCallback, errorCallback) {
24 | $.ajax({
25 | "url": url,
26 | "type": method,
27 | "data": JSON.stringify(requestData),
28 | "contentType": "application/json",
29 | "cache": false,
30 | "dataType": "json",
31 | "success": function(data){
32 | successCallback(data);
33 | },
34 | "error": function(XMLHttpRequest, textStatus, errorThrown) {
35 | console.log(XMLHttpRequest, textStatus, errorThrown);
36 | var status = XMLHttpRequest.status;
37 | var statusText = XMLHttpRequest.statusText;
38 | var responseText = XMLHttpRequest.responseText;
39 | errorCallback("[" +status + "] [" + statusText + "] " + responseText);
40 | }
41 | });
42 | }
43 | };
44 |
45 | var templates = {
46 | "argsOptions": utils.tplCompile("args_tpl"),
47 | "pluginOptions": utils.tplCompile("plugin_option_tpl"),
48 | "pluginPanel": utils.tplCompile("plugin_panel_tpl"),
49 | "bookmarkOptions": utils.tplCompile("bookmark_option_tpl"),
50 | "result": utils.tplCompile("result_tpl"),
51 | "headersOptions": utils.tplCompile("headers_tpl"),
52 | "history": utils.tplCompile("history_tpl")
53 |
54 | };
55 |
56 | var page = {
57 | "initComponents": function() {
58 | // enctype
59 | $('#enctype a').click(function(e) {
60 | e.preventDefault();
61 | global.currentEnctype = $(e.target).attr('href').substr(9);
62 | $(this).tab('show');
63 | });
64 | $('#enctype a:first').tab('show');
65 |
66 | // hotkeys
67 | $(document).bind('keydown', 'esc', function(event) {
68 | event.preventDefault();
69 | console.log('press hotkey');
70 | $("#submit_btn").click();
71 | });
72 |
73 | // history
74 | $('#history_modal').on('show.bs.modal', function (e) {
75 | $("#history_panel").html(""); // 先清空
76 | utils.ajax(configs.historyUrl, "GET", {}, function(respData) {
77 | console.log("history data:", respData);
78 | if (respData.Status != 200) {
79 | return page.message(respData.Message);
80 | }
81 |
82 | // 成功
83 | respData.Data.reverse();
84 | global.currentHistory = respData.Data;
85 | page.renderHistory(respData.Data);
86 |
87 | }, function(textStatus) {
88 | return page.message(textStatus);
89 | });
90 | });
91 | },
92 |
93 | "renderData": function(data) {
94 | // url
95 | // $('#method').bootstrapSwitch('state', data.Method=="GET");
96 | $("#method").selectpicker();
97 | $("#method").selectpicker('val', data.Method);
98 |
99 | $('#url').val(data.Url);
100 |
101 | // args
102 | this.renderArgs(data.Args, true);
103 |
104 | // headers
105 | this.renderHeaders(data.Headers, true);
106 |
107 | // bm
108 | $('#bm_switch').bootstrapSwitch('state', data.Bm.Switch);
109 | $('#bm_n').val(data.Bm.N);
110 | $('#bm_c').val(data.Bm.C);
111 |
112 | // plugin
113 | this.renderPlugin(data.Plugin);
114 |
115 | // enctype
116 | $("#enctype_json_content").val(data.JsonContent);
117 | $("#enctype_plain_content").val(data.PlainContent);
118 | $("a[href='#enctype_"+data.Enctype+"']").click();
119 | },
120 |
121 | "renderBookmarks": function(bookmarks, bookmarkName) {
122 | var html = templates.bookmarkOptions({"Bookmarks": bookmarks});
123 | $("#bookmark").html(html);
124 |
125 | $("#bookmark").selectpicker();
126 | $("#bookmark").selectpicker('val', bookmarkName);
127 | $('#bookmark').selectpicker("refresh");
128 | },
129 |
130 | "renderPlugin": function(plugin) {
131 | var html = templates.pluginPanel(global.plugins[plugin.Key]);
132 | $("#plugin_panel").html(html);
133 | $("#plugin").selectpicker();
134 | $("#plugin").selectpicker('val', plugin.Key);
135 |
136 | // init plugins value
137 | var pluginData = plugin.Data;
138 | for (var i in pluginData) {
139 | $("#plugin_" + i).val(pluginData[i]);
140 | }
141 | },
142 |
143 | "renderPlugins": function(plugins, pluginKey) {
144 | console.log("Plugins:", plugins);
145 | console.log("Plugin key:", pluginKey);
146 |
147 | var html = templates.pluginOptions({"Plugins": plugins});
148 | $("#plugin").html(html);
149 | },
150 |
151 | "renderArgs": function(args, isReset) {
152 | var html = templates.argsOptions({"Args": args});
153 | if (isReset) {
154 | return $("#args_body").html(html);
155 | }
156 | return $("#args_body").append(html);
157 | },
158 |
159 | "renderHeaders": function(headers, isReset) {
160 | var html = templates.headersOptions({"Headers": headers});
161 | if (isReset) {
162 | return $("#headers_body").html(html);
163 | }
164 | return $("#headers_body").append(html);
165 | },
166 |
167 | "renderHistory": function(history) {
168 | var html = templates.history({"History": history});
169 | return $("#history_panel").html(html);
170 | },
171 |
172 | "renderResult": function(result) {
173 | var html = templates.result(result);
174 | $("#result_panel").html(html);
175 |
176 | try {
177 | var div = $('
');
178 | $("#result_test_panel").append(div);
179 | var options = {"dom" : div};
180 | var jf = new JsonFormater(options); //创建对象
181 | jf.doFormat(result.Test); //格式化json
182 |
183 | } catch(e) {
184 | var iFrame = $('');
185 | $("#result_test_panel").append(iFrame);
186 | var iFrameDoc = iFrame[0].contentDocument || iFrame[0].contentWindow.document;
187 | iFrameDoc.write(result.Test);
188 | iFrameDoc.close();
189 | }
190 |
191 | $("#result_new_tab_btn").click(function() {
192 | var newWindowObi=window.open("在新标签中浏览");
193 | newWindowObi.document.write(result.Test);
194 | });
195 | },
196 |
197 | "refresh": function() {
198 | $('.switch[type="checkbox"]').bootstrapSwitch();
199 | $('.selectpicker').selectpicker("refresh");
200 | $('#plugin').selectpicker("refresh");
201 | },
202 |
203 | "message": function (msg) {
204 | $("#alerter_content").html(msg);
205 | $("#alerter").modal('show');
206 | },
207 |
208 | "inputDialoyMessage": function(msg) {
209 | $("#bookmark_add_err").html(msg);
210 | $("#bookmark_add_err").removeClass("hidden");
211 | return false;
212 | },
213 |
214 | "inputDialoyMessageHide": function() {
215 | $("#bookmark_add_err").addClass("hidden");
216 | }
217 | };
218 |
219 | var args = {
220 | "add": function() {
221 | page.renderArgs([{
222 | "Key": "",
223 | "Value": "",
224 | "Method": "GET"
225 | }], false);
226 |
227 | page.refresh();
228 | },
229 |
230 | "remove": function (btn) {
231 | $(btn).parent().parent().remove();
232 | }
233 | };
234 |
235 | var headers = {
236 | "add": function() {
237 | page.renderHeaders([{
238 | "Key": "",
239 | "Value": ""
240 | }], false);
241 |
242 | page.refresh();
243 | },
244 |
245 | "remove": function (btn) {
246 | $(btn).parent().parent().remove();
247 | }
248 | };
249 |
250 | var dataProvider = {
251 | "get": function() {
252 | var data = {};
253 |
254 | // 获取数据
255 | // data.Method = $("#method").bootstrapSwitch("state") ? "GET" : "POST";
256 | data.Method = $("#method").selectpicker('val');
257 | data.Url = $.trim($("#url").val());
258 | data.Bm = {};
259 | data.Bm.Switch = $("#bm_switch").bootstrapSwitch("state");
260 | data.Bm.N = parseInt($.trim($("#bm_n").val()));
261 | data.Bm.C = parseInt($.trim($("#bm_c").val()));
262 |
263 | // args
264 | data.Args = [];
265 | $("#args_body tr").each(function() {
266 | var key = $.trim($(this).find(".arg-key").val());
267 | if (key == "") {
268 | return false;
269 | }
270 | data.Args.push({
271 | "Key": key,
272 | "Value": $.trim($(this).find(".arg-value").val()),
273 | "Method": $(this).find(".arg-method").bootstrapSwitch("state") ? "GET" : "POST"
274 | });
275 | return true;
276 | });
277 |
278 | // headers
279 | data.Headers = [];
280 | $("#headers_body tr").each(function() {
281 | var key = $.trim($(this).find(".header-key").val());
282 | if (key == "") {
283 | return false;
284 | }
285 | data.Headers.push({
286 | "Key": key,
287 | "Value": $.trim($(this).find(".header-value").val())
288 | });
289 | return true;
290 | });
291 |
292 | // plugin
293 | data.Plugin = {};
294 | data.Plugin.Data = {};
295 | data.Plugin.Key = $("#plugin").val();
296 | for (var i in global.plugins[data.Plugin.Key].FieldNames) {
297 | data.Plugin.Data[i] = $.trim($("#plugin_"+i).val());
298 | }
299 |
300 | // 校验
301 | if (isNaN(data.Bm.N) || isNaN(data.Bm.C)) {
302 | throw "压测数据必须是数字";
303 | }
304 | if (data.Bm.N <= 0 || data.Bm.C <= 0) {
305 | throw "压测数据必须是正整数";
306 | }
307 |
308 | // enctype
309 | data.Enctype = global.currentEnctype;
310 |
311 | // json
312 | data.JsonContent = $("#enctype_json_content").val();
313 |
314 | // plain
315 | data.PlainContent = $("#enctype_plain_content").val();
316 |
317 | return data;
318 | },
319 |
320 | "submit": function() {
321 | var data = null;
322 | try {
323 | data = dataProvider.get();
324 | } catch(e) {
325 | return page.message(e);
326 | }
327 | console.log("request data:", data);
328 |
329 | var btn = this;
330 | $(btn).button('loading');
331 |
332 | utils.ajax(configs.indexUrl, "POST", data, function(respData) {
333 | $(btn).button('reset');
334 |
335 | console.log("response data:", respData);
336 | if (respData.Status != 200) {
337 | return page.message(respData.Message);
338 | }
339 |
340 | // 成功
341 | page.renderResult(respData.Data);
342 |
343 | }, function(textStatus) {
344 | $(btn).button('reset');
345 | return page.message(textStatus);
346 | });
347 | }
348 |
349 | };
350 |
351 | var plugins = {
352 | "use": function() {
353 | var key = $("#plugin").selectpicker('val');
354 | console.log("select plugin:", key);
355 | var plugin = {"Key": key, "Data": {}};
356 | page.renderPlugin(plugin);
357 | }
358 | };
359 |
360 | var bookmarks = {
361 | "handleUse": function() { // TODO
362 | var btn = this;
363 | $(btn).button('loading');
364 | var name = $("#bookmark").selectpicker('val');
365 |
366 | utils.ajax(configs.bookmarkUrl, "POST", {"Name":name}, function(data) {
367 | $(btn).button('reset');
368 |
369 | console.log(data);
370 |
371 | if (data.Status != 200) {
372 | return page.inputDialoyMessage(data.Message);
373 | }
374 |
375 | page.renderData(data.Data);
376 | return page.refresh();
377 |
378 | }, function(textStatus) {
379 | $(btn).button('reset'); // 恢复按钮状态
380 | return page.inputDialoyMessage(textStatus);
381 | });
382 | },
383 |
384 | "add": function() {
385 | // validate data
386 | try {
387 | dataProvider.get();
388 | } catch (e) {
389 | return page.message(e);
390 | }
391 |
392 | $("#bookmark_add_err").html("");
393 | $("#bookmark_add_err").addClass("hidden");
394 | $("#bookmark_add_input").val("");
395 | $("#add_dialog").modal("show");
396 |
397 | return true;
398 | },
399 |
400 | "handleAdd": function() {
401 | var name = $.trim($("#bookmark_add_input").val());
402 |
403 | var bookmark = {};
404 | try {
405 | bookmark.Data = dataProvider.get();
406 | bookmark.Name = name;
407 | } catch(e) {
408 | return page.inputDialoyMessage(e);
409 | }
410 |
411 | // 安全禁用
412 | var btn = this;
413 | $(btn).button('loading');
414 |
415 | console.log("insert bookmark", bookmark);
416 |
417 | utils.ajax(configs.bookmarksUrl, "POST", bookmark, function(data) {
418 | $(btn).button('reset'); // 恢复按钮状态
419 |
420 | if (data.Status != 200) {
421 | return page.inputDialoyMessage(data.Message);
422 | }
423 |
424 | // 成功[
425 | var renderData = {"Bookmarks":[bookmark.Name]};
426 | var html = templates.bookmarkOptions(renderData);
427 | $('#bookmark').append(html);
428 | $('#add_dialog').modal('hide');
429 | $('#bookmark').selectpicker();
430 | $("#bookmark").selectpicker('val', bookmark.Name);
431 | $('#bookmark').selectpicker("refresh");
432 |
433 | }, function(textStatus) {
434 | $(btn).button('reset'); // 恢复按钮状态
435 | return page.inputDialoyMessage(textStatus);
436 | });
437 | },
438 |
439 | "edit": function() {
440 | $("#confirm_dialog_title").html("编辑书签");
441 | $("#confirm_dialog_text").html("您确定要将当前内容替换到选定书签吗?");
442 | $("#confirm_dialog_submit_btn").unbind();
443 | $("#confirm_dialog_submit_btn").click(bookmarks.handleEdit);
444 | $("#confirm_dialog").modal("show");
445 | },
446 |
447 | "handleEdit": function() {
448 | var bookmark = {};
449 | try {
450 | bookmark.Data = dataProvider.get();
451 | bookmark.Name = $("#bookmark").selectpicker('val');
452 | } catch(e) {
453 | $("#confirm_dialog").modal('hide');
454 | return page.message(e);
455 | }
456 |
457 | console.log("updateData", bookmark);
458 |
459 | // 安全禁用
460 | var btn = this;
461 | $(btn).button('loading');
462 |
463 | utils.ajax(configs.bookmarksUrl, "PUT", bookmark, function(data) {
464 | // 恢复按钮状态
465 | $(btn).button('reset');
466 | $("#confirm_dialog").modal('hide');
467 |
468 | if (data.Status != 200) {
469 | return page.message(data.Message);
470 | }
471 | // 成功,无动作
472 |
473 | }, function(textStatus) {
474 | // 恢复按钮状态
475 | $(btn).button('reset');
476 | $("#confirm_dialog").modal('hide');
477 | return page.message(textStatus);
478 | });
479 | },
480 |
481 | "delete": function() {
482 | $("#confirm_dialog_title").html("删除书签");
483 | $("#confirm_dialog_text").html("您确定删除选定书签吗?");
484 | $("#confirm_dialog_submit_btn").unbind();
485 | $("#confirm_dialog_submit_btn").click(bookmarks.handleDelete);
486 | $("#confirm_dialog").modal("show");
487 | },
488 |
489 | "handleDelete": function() {
490 | var name = $("#bookmark").selectpicker("val");
491 |
492 | // 安全禁用
493 | var btn = this;
494 | $(btn).button('loading');
495 |
496 | var url = configs.bookmarksUrl + "?Name=" + name;
497 | console.log(url);
498 | utils.ajax(url, "DELETE", {}, function(data) {
499 | // 恢复按钮状态
500 | $(btn).button('reset');
501 | $("#confirm_dialog").modal('hide');
502 |
503 | if (data.Status != 200) {
504 | return page.message(data.Message);
505 | }
506 |
507 | // 成功
508 | $("#bookmark option[value="+name+"]").remove();
509 | $("#bookmark").selectpicker('refresh');
510 |
511 | }, function(textStatus) {
512 | // 恢复按钮状态
513 | $(btn).button('reset');
514 | $("#confirm_dialog").modal('hide');
515 | return page.message(textStatus);
516 | });
517 | }
518 |
519 | };
520 |
521 | var historyModel = {
522 | "use": function(index) {
523 | console.log(index);
524 | page.renderData(global.currentHistory[index]);
525 | $('#history_modal').modal('hide');
526 | page.refresh();
527 | }
528 | };
529 |
530 | // window.onload
531 | $(function() {
532 | // init libaray
533 | if (global.environ != "dev") {
534 | console.log = function() {};
535 | }
536 |
537 | Handlebars.registerHelper('eq', function(v1, v2, options) {
538 | if(v1 == v2) {
539 | return options.fn(this);
540 | }
541 | return options.inverse(this);
542 | });
543 |
544 | Handlebars.registerHelper('neq', function(v1, v2, options) {
545 | if(v1 != v2) {
546 | return options.fn(this);
547 | }
548 | return options.inverse(this);
549 | });
550 |
551 | Handlebars.registerHelper("math", function(lvalue, operator, rvalue, options) {
552 | lvalue = parseFloat(lvalue);
553 | rvalue = parseFloat(rvalue);
554 |
555 | return {
556 | "+": lvalue + rvalue,
557 | "-": lvalue - rvalue,
558 | "*": lvalue * rvalue,
559 | "/": lvalue / rvalue,
560 | "%": lvalue % rvalue
561 | }[operator];
562 | });
563 |
564 | // init components
565 | page.initComponents();
566 |
567 | // 【回调地狱】获取初始化数据 initData
568 | return utils.ajax(configs.initDataUrl, "GET", {}, function(respData) {
569 | if (respData.Status != 200) {
570 | page.message(respData.Message);
571 | throw "init error";
572 | }
573 |
574 | console.log("initData:", respData);
575 |
576 | var bookmarkData = respData.Data;
577 | global.plugins = bookmarkData.Plugins;
578 |
579 | page.renderBookmarks(bookmarkData.Bookmarks, bookmarkData.Bookmark.Name);
580 | page.renderPlugins(bookmarkData.Plugins, bookmarkData.Bookmark.Data.Plugin.Key);
581 | page.renderData(bookmarkData.Bookmark.Data);
582 | page.refresh();
583 |
584 | // event binding
585 | $("#bookmark_use_btn").click(bookmarks.handleUse);
586 | $("#bookmark_add_btn").click(bookmarks.add);
587 | $("#bookmark_add_input").focus(page.inputDialoyMessageHide);
588 | $("#bookamrkAddBtn").click(bookmarks.handleAdd);
589 | $("#bookmark_edit_btn").click(bookmarks.edit);
590 | $("#bookmark_drop_btn").click(bookmarks.delete);
591 | $("#plugin_use_btn").click(plugins.use);
592 | $("#submit_btn").click(dataProvider.submit);
593 |
594 | }, function(textStatus) {
595 | page.message("Request error: " + textStatus);
596 | throw "init error";
597 | });
598 | });
599 |
--------------------------------------------------------------------------------
/static/json-formater.js:
--------------------------------------------------------------------------------
1 | function JsonFormater(opt) {
2 | this.options = $.extend({
3 | dom: '',
4 | tabSize: 2,
5 | singleTab: " ",
6 | quoteKeys: true,
7 | imgCollapsed: "data:image/gif;base64,R0lGODlhHAALAMQAAP////7++/z8/Pb29fb18PHx7e/w6/Hw6e3s5unp4+jm2ODg3t3a0dnVy9bQxtLMv8zJurDC1L+9sMK4p32buDMzMwAAAP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEHABcALAAAAAAcAAsAAAVU4CWOZGmeV0StLBWhsEgBdA1QMUwJvMUTuNyJMihaBodFUFiiECxQKGMpqlSq14uVRCkUEJbEokHVZrdmrqLRsDgekDLzQoFIJni8nKlqrV5zgYIhADs=",
8 | imgExpanded: "data:image/gif;base64,R0lGODlhHAALAMQAAP////7++/z8/Pb29fb18PHx7e/w6/Hw6e3s5unp4+Dg3t3a0djY0dnVy9fTxNbQxtLMv8zJurDC1L+9sMK4p32buAAAAP///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEHABcALAAAAAAcAAsAAAVL4CWOZGmel1StbCWhsFgBdA1UMVwJQd8TuNypMigWD4qgsFQhWJ7PhXI5qhQKCERC0ZhSLxUFo+FwQCJeagUyobjd6aWqtXp979QQADs=",
9 | isCollapsible: true
10 | }, opt || {});
11 | this.isFormated = false;
12 | this.obj = {
13 | _dateObj: new Date(),
14 | _regexpObj: new RegExp()
15 | };
16 | this.init();
17 | }
18 | JsonFormater.prototype = {
19 | init: function () {
20 | this.tab = this.multiplyString(this.options.tabSize, this.options.singleTab);
21 | this.bindEvent();
22 | },
23 | doFormat: function (json) {
24 | var html;
25 | var obj;
26 | try {
27 | if(typeof json == 'object'){
28 | obj = [json];
29 | }else{
30 | if (json == ""){
31 | json = "\"\"";
32 | }
33 | obj = eval("[" + json + "]");
34 | }
35 | html = this.ProcessObject(obj[0], 0, false, false, false);
36 | $(this.options.dom).html("" + html + "
");
37 | this.isFormated = true;
38 | } catch (e) {
39 | $(this.options.dom).html("");
40 | this.isFormated = false;
41 | throw e;
42 | }
43 | },
44 | bindEvent: function () {
45 | var that = this;
46 | $(this.options.dom).off('click','.imgToggle');
47 | $(this.options.dom).on('click', '.imgToggle', function () {
48 | if (that.isFormated == false) {
49 | return;
50 | }
51 | that.makeContentVisible($(this).parent().next(), !$(this).data('status'));
52 | });
53 | },
54 | expandAll: function () {
55 | if (this.isFormated == false) {
56 | return;
57 | }
58 | var that = this;
59 | this.traverseChildren($(this.options.dom), function(element){
60 | if(element.hasClass('jf-collapsible')){
61 | that.makeContentVisible(element, true);
62 | }
63 | }, 0);
64 | },
65 | collapseAll: function () {
66 | if (this.isFormated == false) {
67 | return;
68 | }
69 | var that = this;
70 | this.traverseChildren($(this.options.dom), function(element){
71 | if(element.hasClass('jf-collapsible')){
72 | that.makeContentVisible(element, false);
73 | }
74 | }, 0);
75 | },
76 | collapseLevel: function(level){
77 | if (this.isFormated == false) {
78 | return;
79 | }
80 | var that = this;
81 | this.traverseChildren($(this.options.dom), function(element, depth){
82 | if(element.hasClass('jf-collapsible')){
83 | if(depth >= level){
84 | that.makeContentVisible(element, false);
85 | }else{
86 | that.makeContentVisible(element, true);
87 | }
88 | }
89 | }, 0);
90 |
91 | },
92 | isArray: function (obj) {
93 | return obj &&
94 | typeof obj === 'object' &&
95 | typeof obj.length === 'number' && !(obj.propertyIsEnumerable('length'));
96 | },
97 | getRow: function (indent, data, isPropertyContent) {
98 | var tabs = "";
99 | if (!isPropertyContent) {
100 | tabs = this.multiplyString(indent, this.tab);
101 | }
102 | if (data != null && data.length > 0 && data.charAt(data.length - 1) != "\n") {
103 | data = data + "\n";
104 | }
105 | return tabs + data;
106 | },
107 | formatLiteral: function (literal, quote, comma, indent, isArray, style) {
108 | if (typeof literal == 'string') {
109 | literal = literal.split("<").join("<").split(">").join(">");
110 | }
111 | var str = "" + quote + literal + quote + comma + "";
112 | if (isArray) str = this.getRow(indent, str);
113 | return str;
114 | },
115 | formatFunction: function (indent, obj) {
116 | var tabs;
117 | var i;
118 | var funcStrArray = obj.toString().split("\n");
119 | var str = "";
120 | tabs = this.multiplyString(indent, this.tab);
121 | for (i = 0; i < funcStrArray.length; i++) {
122 | str += ((i == 0) ? "" : tabs) + funcStrArray[i] + "\n";
123 | }
124 | return str;
125 | },
126 | multiplyString: function (num, str) {
127 | var result = '';
128 | for (var i = 0; i < num; i++) {
129 | result += str;
130 | }
131 | return result;
132 | },
133 | traverseChildren: function (element, func, depth) {
134 | var length = element.children().length;
135 | for (var i = 0; i < length; i++) {
136 | this.traverseChildren(element.children().eq(i), func, depth + 1);
137 | }
138 | func(element, depth);
139 | },
140 | makeContentVisible : function(element, visible){
141 | var img = element.prev().find('img');
142 | if(visible){
143 | element.show();
144 | img.attr('src', this.options.imgExpanded);
145 | img.data('status', 1);
146 | }else{
147 | element.hide();
148 | img.attr('src', this.options.imgCollapsed);
149 | img.data('status', 0);
150 | }
151 | },
152 | ProcessObject: function (obj, indent, addComma, isArray, isPropertyContent) {
153 | var html = "";
154 | var comma = (addComma) ? ", " : "";
155 | var type = typeof obj;
156 | var clpsHtml = "";
157 | var prop;
158 | if (this.isArray(obj)) {
159 | if (obj.length == 0) {
160 | html += this.getRow(indent, "[ ]" + comma, isPropertyContent);
161 | } else {
162 | clpsHtml = this.options.isCollapsible ? "
" : "";
163 | html += this.getRow(indent, "[" + clpsHtml, isPropertyContent);
164 | for (var i = 0; i < obj.length; i++) {
165 | html += this.ProcessObject(obj[i], indent + 1, i < (obj.length - 1), true, false);
166 | }
167 | clpsHtml = this.options.isCollapsible ? "" : "";
168 | html += this.getRow(indent, clpsHtml + "]" + comma);
169 | }
170 | } else if (type == 'object') {
171 | if (obj == null) {
172 | html += this.formatLiteral("null", "", comma, indent, isArray, "Null");
173 | } else {
174 | var numProps = 0;
175 | for (prop in obj) numProps++;
176 | if (numProps == 0) {
177 | html += this.getRow(indent, "{ }" + comma, isPropertyContent);
178 | } else {
179 | clpsHtml = this.options.isCollapsible ? "
" : "";
180 | html += this.getRow(indent, "{" + clpsHtml, isPropertyContent);
181 | var j = 0;
182 | for (prop in obj) {
183 | var quote = this.options.quoteKeys ? "\"" : "";
184 | html += this.getRow(indent + 1, "" + quote + prop + quote + ": " + this.ProcessObject(obj[prop], indent + 1, ++j < numProps, false, true));
185 | }
186 | clpsHtml = this.options.isCollapsible ? "" : "";
187 | html += this.getRow(indent, clpsHtml + "}" + comma);
188 | }
189 | }
190 | } else if (type == 'number') {
191 | html += this.formatLiteral(obj, "", comma, indent, isArray, "Number");
192 | } else if (type == 'boolean') {
193 | html += this.formatLiteral(obj, "", comma, indent, isArray, "Boolean");
194 | }else if (type == 'undefined') {
195 | html += this.formatLiteral("undefined", "", comma, indent, isArray, "Null");
196 | } else {
197 | html += this.formatLiteral(obj.toString().split("\\").join("\\\\").split('"').join('\\"'), "\"", comma, indent, isArray, "String");
198 | }
199 | return html;
200 | }
201 | };
202 |
--------------------------------------------------------------------------------
/text/.gitignore:
--------------------------------------------------------------------------------
1 | # auto-create file
2 | text.go
3 |
--------------------------------------------------------------------------------
/text/provider.go:
--------------------------------------------------------------------------------
1 | package text
2 |
3 | import (
4 | "encoding/base64"
5 | "io/ioutil"
6 | "os"
7 | "path/filepath"
8 | )
9 |
10 | var (
11 | BasePath string
12 | IsDebug bool
13 | )
14 |
15 | func ProvideString(path string) string {
16 | if IsDebug {
17 | if !isFileExists(path) {
18 | return ""
19 | }
20 | buf, err := ioutil.ReadFile(filepath.Join(BasePath, path))
21 | if err != nil {
22 | panic(err)
23 | }
24 | return string(buf)
25 | }
26 |
27 | return Text[path]
28 | }
29 |
30 | func ProvideBytes(path string) []byte {
31 | if IsDebug {
32 | if !isFileExists(path) {
33 | return nil
34 | }
35 | buf, err := ioutil.ReadFile(filepath.Join(BasePath, path))
36 | if err != nil {
37 | panic(err)
38 | }
39 | return buf
40 | }
41 |
42 | content, has := Text[path]
43 | if !has {
44 | return nil
45 | }
46 | buf, err := base64.StdEncoding.DecodeString(content)
47 | if err != nil {
48 | panic(err)
49 | }
50 | return buf
51 | }
52 |
53 | func isFileExists(path string) bool {
54 | _, err := os.Stat(path)
55 | return !os.IsNotExist(err)
56 | }
57 |
--------------------------------------------------------------------------------
/view/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | 接口测试工具
9 |
10 |
11 |
12 |
13 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
51 |
52 |
53 |
54 |
55 |
81 |
82 |
83 |
84 |
85 | Enctype:
86 |
87 |
94 |
95 |
96 |
97 |
98 |
99 | 键: |
100 | 值: |
101 | 类型: |
102 | |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 | http-equiv: |
123 | content: |
124 | |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
149 |
150 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
178 |
179 |
180 |
181 |
182 |
183 |
187 |
188 |
189 |
190 |
191 |
192 |
209 |
210 |
211 |
227 |
228 |
229 |
230 |
231 |
235 |
236 |
237 |
240 |
241 |
242 |
243 |
244 |
245 |
263 |
264 |
279 |
280 |
285 |
286 |
291 |
292 |
300 |
301 |
328 |
329 |
340 |
341 |
342 |
343 |
344 |
345 |
346 |
347 |
348 |
349 |
350 |
351 |
352 |
353 |
--------------------------------------------------------------------------------