├── .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 | ![shortcut](https://raw.githubusercontent.com/jmjoy/http-api-tester/master/img/shortcut0.6.bmp) 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 |
56 |
57 |
58 |
59 | 60 | 61 | 72 |
73 | 74 |
75 | 76 | 77 |
78 |
79 |
80 |
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 |
Headers:
120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 |
http-equiv:content:
129 |
130 | 131 |
132 |
133 |
134 |
135 | 136 | 137 |
138 |
139 | 140 | 141 |
142 |
143 | 144 | 145 |
146 |
147 |
148 |
149 | 150 |
151 |
152 |
153 |
154 | 155 | 156 | 157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 | 166 |
167 | 168 |
169 | 170 | 171 | 190 | 191 | 192 | 209 | 210 | 211 | 227 | 228 | 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 | --------------------------------------------------------------------------------