├── .gitignore ├── LICENSE ├── README.md ├── RELEASE ├── RunAllTests.sh ├── RunCodeCheck.sh ├── RunGoDoc.sh ├── go.mod ├── go.sum ├── spring-web-const.go ├── spring-web-container.go ├── spring-web-context.go ├── spring-web-filter.go ├── spring-web-mapper.go ├── spring-web-mapper_test.go ├── spring-web-mapping.go ├── spring-web-method.go ├── spring-web-method_test.go ├── spring-web-middleware.go ├── spring-web-redoc.go ├── spring-web-router.go ├── spring-web-rpc.go ├── spring-web-server.go ├── spring-web-swagger.go ├── spring-web-swagger_test.go ├── spring-web-url.go ├── spring-web-url_test.go └── spring-web-validator.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | .DS_Store 15 | 16 | .idea/ 17 | vendor/ 18 | 19 | covprofile 20 | coverage.html -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-spring-web -------------------------------------------------------------------------------- /RELEASE: -------------------------------------------------------------------------------- 1 | Release History: 2 | 3 | v1.0.4 2020-06-23 4 | 5 | Handler 提升为接口,打印更丰富的路由信息;FilterChain 提升为接口,完美适 6 | 配 echo 和 gin 的中间件机制;全面统一 echo、gin 以及 {} 路由风格;全面实 7 | 现 WebServer、WebContainer、Router、Mapper 四个级别的 Filter 机制; 8 | WebContainer 增加 ReadTimeout 和 WriteTimeout 配置;BIND 模式支持 9 | WebContext 参数,支持无参和无返回值,RPC 处理函数可定制;增加 gin、echo 10 | 与 WebContext 之间的互转函数;改造 WebServer 流式接口;优化 Swagger 11 | 使用;减小 WebContainer 方法集;引入参数校验框架;修复 gin 路由的 BUG; 12 | 增加 WebContext.SetRequest 函数,等等。 13 | 14 | v1.0.3 2020-04-14 15 | 16 | 初步实现 Swagger 框架,集成 Swagger-UI 和 ReDoc,增加 Swg Petstore 17 | 官方示例,统一 Spring-Web 的路由规则,和 echo 保持一致,对gin 进行适配, 18 | 支持 {} 路由风格,等等。 19 | 20 | v1.0.2 2020-03-24 21 | 22 | 重新设计 Request 接口,支持方法组合,支持 Container 级别的 Filter 配置, 23 | 适配 echo 和 gin 原生 handler,增加封装 Bind 操作的 Web RPC 适配函数。 24 | 25 | v1.0.0 2020-02-22 26 | 27 | 适配 echo、gin、http 标准框架函数,更新 gin 和 echo 版本,修复少量 BUG。 28 | 29 | v1.0.0-rc 2020-01-04 30 | 31 | 初步搭建 SpringWeb 框架,适配 echo 和 gin 两个主流 http web 框架,适配 32 | LoggerContext 接口,初步实现 Filter 机制,增加 RPC 适配函数,等等。 -------------------------------------------------------------------------------- /RunAllTests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 执行当前目录及子目录下的测试用例 4 | go test -count=1 ./... -------------------------------------------------------------------------------- /RunCodeCheck.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # https://github.com/golangci/golangci-lint 4 | golangci-lint run -------------------------------------------------------------------------------- /RunGoDoc.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | WORKSPACE=$(cd `dirname $0` && pwd -P) 4 | 5 | # NOTE: 只需要修改这里 6 | MODULE_NAME=go-spring-web 7 | 8 | PACKAGE_PATH=github.com/go-spring 9 | export GOPATH=/tmp/godoc-${MODULE_NAME} 10 | MODULE_PATH=${GOPATH}/src/${PACKAGE_PATH} 11 | 12 | rm -rf $MODULE_PATH/$MODULE_NAME &> /dev/null 13 | 14 | mkdir -p $GOPATH/bin 15 | mkdir -p $MODULE_PATH 16 | 17 | ln -sf $WORKSPACE ${MODULE_PATH}/${MODULE_NAME} 18 | 19 | cd $MODULE_PATH/$MODULE_NAME 20 | 21 | python -m webbrowser "http://localhost:6060/pkg/github.com/go-spring/"${MODULE_NAME} 22 | godoc -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-spring/go-spring-web 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/go-openapi/spec v0.19.7 7 | github.com/go-playground/validator/v10 v10.2.0 8 | github.com/go-spring/go-spring-error v0.0.0-20200808141421-a3d4acfeb21b 9 | github.com/go-spring/go-spring-logger v0.0.0-20200808141042-f30429918b41 10 | github.com/go-spring/go-spring-test v0.0.0-20200808140725-779b0a9e7a93 11 | github.com/go-spring/go-spring-utils v0.0.0-20200808140357-119aa665f28a 12 | github.com/magiconair/properties v1.8.1 13 | github.com/swaggo/http-swagger v0.0.0-20200308142732-58ac5e232fba 14 | ) 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= 2 | github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= 3 | github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 4 | github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI= 5 | github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= 6 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M= 7 | github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= 8 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 9 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 12 | github.com/gin-contrib/gzip v0.0.1/go.mod h1:fGBJBCdt6qCZuCAOwWuFhBB4OOq9EFqlo5dEaFhhu5w= 13 | github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= 14 | github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= 15 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 16 | github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= 17 | github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= 18 | github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= 19 | github.com/go-openapi/jsonpointer v0.17.0/go.mod h1:cOnomiV+CVVwFLk0A/MExoFMjwdsUdVpsRhURCKh+3M= 20 | github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= 21 | github.com/go-openapi/jsonpointer v0.19.3 h1:gihV7YNZK1iK6Tgwwsxo2rJbD1GTbdm72325Bq8FI3w= 22 | github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= 23 | github.com/go-openapi/jsonreference v0.17.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= 24 | github.com/go-openapi/jsonreference v0.19.0/go.mod h1:g4xxGn04lDIRh0GJb5QlpE3HfopLOL6uZrK/VgnsK9I= 25 | github.com/go-openapi/jsonreference v0.19.2 h1:o20suLFB4Ri0tuzpWtyHlh7E7HnkqTNLq6aR6WVNS1w= 26 | github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= 27 | github.com/go-openapi/spec v0.19.0/go.mod h1:XkF/MOi14NmjsfZ8VtAKf8pIlbZzyoTvZsdfssdxcBI= 28 | github.com/go-openapi/spec v0.19.7 h1:0xWSeMd35y5avQAThZR2PkEuqSosoS5t6gDH4L8n11M= 29 | github.com/go-openapi/spec v0.19.7/go.mod h1:Hm2Jr4jv8G1ciIAo+frC/Ft+rR2kQDh8JHKHb3gWUSk= 30 | github.com/go-openapi/swag v0.17.0/go.mod h1:AByQ+nYG6gQg71GINrmuDXCPWdL640yX49/kXLo40Tg= 31 | github.com/go-openapi/swag v0.19.2/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 32 | github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= 33 | github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= 34 | github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 35 | github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= 36 | github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= 37 | github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= 38 | github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= 39 | github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= 40 | github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= 41 | github.com/go-spring/go-spring-error v0.0.0-20200808141421-a3d4acfeb21b h1:dTTOxfYreLhVwG/ElOqbh9+Ar7bEgX1k7Y2yI0tcEOo= 42 | github.com/go-spring/go-spring-error v0.0.0-20200808141421-a3d4acfeb21b/go.mod h1:oboMN9paPF3H1aBmqE9M9+nIPUwOQpV2pPHyYLxHeXo= 43 | github.com/go-spring/go-spring-logger v0.0.0-20200808141042-f30429918b41 h1:3WcExndhdB23bhVCK93w9HzJjUdMTcNQ154O3YA7KJU= 44 | github.com/go-spring/go-spring-logger v0.0.0-20200808141042-f30429918b41/go.mod h1:KqmG4T8cdcm5QZkzYLqFiyx3eM4nXWNp00MhlIWMX2w= 45 | github.com/go-spring/go-spring-test v0.0.0-20200808140725-779b0a9e7a93 h1:g83LMBIxm9VuRRRa/BsOME6JgwwUZEzbu/480JiMRfU= 46 | github.com/go-spring/go-spring-test v0.0.0-20200808140725-779b0a9e7a93/go.mod h1:l5dMD3AuWs4vvcecUcAB4hba87cr6xeXUe/K1HNp39I= 47 | github.com/go-spring/go-spring-utils v0.0.0-20200808140357-119aa665f28a h1:+vBaoWmFA9tNxLFtaKwbWyDongOUxhNwUExG63wthr8= 48 | github.com/go-spring/go-spring-utils v0.0.0-20200808140357-119aa665f28a/go.mod h1:oPtJeTil+appmHgfKE9n4qQxvbwTwYpClLpkkSRgFQY= 49 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 50 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 51 | github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= 52 | github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 53 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 54 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 55 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 56 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 57 | github.com/labstack/gommon v0.3.0 h1:JEeO0bvc78PKdyHxloTKiF8BD5iGrH8T6MSeGvSgob0= 58 | github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k= 59 | github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= 60 | github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= 61 | github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= 62 | github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 63 | github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 64 | github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 65 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8= 66 | github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= 67 | github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= 68 | github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 69 | github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 70 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 71 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 72 | github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= 73 | github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= 74 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 75 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 76 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 77 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 78 | github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= 79 | github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 80 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 81 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 82 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 83 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 84 | github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14 h1:PyYN9JH5jY9j6av01SpfRMb+1DWg/i3MbGOKPxJ2wjM= 85 | github.com/swaggo/files v0.0.0-20190704085106-630677cd5c14/go.mod h1:gxQT6pBGRuIGunNf/+tSOB5OHvguWi8Tbt82WOkf35E= 86 | github.com/swaggo/gin-swagger v1.2.0/go.mod h1:qlH2+W7zXGZkczuL+r2nEBR2JTT+/lX05Nn6vPhc7OI= 87 | github.com/swaggo/http-swagger v0.0.0-20200308142732-58ac5e232fba h1:lUPlXKqgbqT2SVg2Y+eT9mu5wbqMnG+i/+Q9nK7C0Rs= 88 | github.com/swaggo/http-swagger v0.0.0-20200308142732-58ac5e232fba/go.mod h1:O1lAbCgAAX/KZ80LM/OXwtWFI/5TvZlwxSg8Cq08PV0= 89 | github.com/swaggo/swag v1.5.1/go.mod h1:1Bl9F/ZBpVWh22nY0zmYyASPO1lI/zIwRDrpZU+tv8Y= 90 | github.com/swaggo/swag v1.6.3 h1:N+uVPGP4H2hXoss2pt5dctoSUPKKRInr6qcTMOm0usI= 91 | github.com/swaggo/swag v1.6.3/go.mod h1:wcc83tB4Mb2aNiL/HP4MFeQdpHUrca+Rp/DRNgWAUio= 92 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 93 | github.com/ugorji/go v1.1.5-pre/go.mod h1:FwP/aQVg39TXzItUBMwnWp9T9gPQnXw4Poh4/oBQZ/0= 94 | github.com/ugorji/go/codec v0.0.0-20181022190402-e5e69e061d4f/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= 95 | github.com/ugorji/go/codec v1.1.5-pre/go.mod h1:tULtS6Gy1AE1yCENaw4Vb//HLH5njI2tfCQDUqRd8fI= 96 | github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= 97 | github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= 98 | github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= 99 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 100 | golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 101 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 102 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 103 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 104 | golang.org/x/net v0.0.0-20190611141213-3f473d35a33a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 105 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 106 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM= 107 | golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 108 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 109 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 110 | golang.org/x/sys v0.0.0-20181228144115-9a3f9b0469bb/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 111 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 112 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 113 | golang.org/x/sys v0.0.0-20190610200419-93c9922d18ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 114 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= 115 | golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 116 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 117 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 118 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 119 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 120 | golang.org/x/tools v0.0.0-20190606050223-4d9ae51c2468/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 121 | golang.org/x/tools v0.0.0-20190611222205-d73e1c7e250b h1:/mJ+GKieZA6hFDQGdWZrjj4AXPl5ylY+5HusG80roy0= 122 | golang.org/x/tools v0.0.0-20190611222205-d73e1c7e250b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 123 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 124 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 125 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 126 | gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= 127 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 128 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 129 | gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= 130 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 131 | -------------------------------------------------------------------------------- /spring-web-const.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package SpringWeb 18 | 19 | const ( 20 | HeaderContentDisposition = "Content-Disposition" 21 | HeaderContentType = "Content-Type" 22 | HeaderXForwardedProto = "X-Forwarded-Proto" 23 | HeaderXForwardedProtocol = "X-Forwarded-Protocol" 24 | HeaderXForwardedSsl = "X-Forwarded-Ssl" 25 | HeaderXUrlScheme = "X-Url-Scheme" 26 | 27 | CharsetUTF8 = "charset=UTF-8" 28 | 29 | MIMEApplicationJSON = "application/json" 30 | MIMEApplicationJSONCharsetUTF8 = MIMEApplicationJSON + "; " + CharsetUTF8 31 | MIMEApplicationJavaScript = "application/javascript" 32 | MIMEApplicationJavaScriptCharsetUTF8 = MIMEApplicationJavaScript + "; " + CharsetUTF8 33 | MIMEApplicationXML = "application/xml" 34 | MIMEApplicationXMLCharsetUTF8 = MIMEApplicationXML + "; " + CharsetUTF8 35 | MIMETextXML = "text/xml" 36 | MIMETextXMLCharsetUTF8 = MIMETextXML + "; " + CharsetUTF8 37 | MIMEApplicationForm = "application/x-www-form-urlencoded" 38 | MIMEApplicationProtobuf = "application/protobuf" 39 | MIMEApplicationMsgpack = "application/msgpack" 40 | MIMETextHTML = "text/html" 41 | MIMETextHTMLCharsetUTF8 = MIMETextHTML + "; " + CharsetUTF8 42 | MIMETextPlain = "text/plain" 43 | MIMETextPlainCharsetUTF8 = MIMETextPlain + "; " + CharsetUTF8 44 | MIMEMultipartForm = "multipart/form-data" 45 | MIMEOctetStream = "application/octet-stream" 46 | MIMEJsonAPI = "application/vnd.api+json" 47 | MIMEJsonStream = "application/x-json-stream" 48 | MIMEImagePng = "image/png" 49 | MIMEImageJpeg = "image/jpeg" 50 | MIMEImageGif = "image/gif" 51 | ) 52 | -------------------------------------------------------------------------------- /spring-web-container.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package SpringWeb 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "net/http" 23 | "time" 24 | 25 | "github.com/go-spring/go-spring-logger" 26 | "github.com/go-spring/go-spring-utils" 27 | "github.com/swaggo/http-swagger" 28 | ) 29 | 30 | // HandlerFunc 标准 Web 处理函数 31 | type HandlerFunc func(WebContext) 32 | 33 | // Handler Web 处理接口 34 | type Handler interface { 35 | // Invoke 响应函数 36 | Invoke(WebContext) 37 | 38 | // FileLine 获取用户函数的文件名、行号以及函数名称 39 | FileLine() (file string, line int, fnName string) 40 | } 41 | 42 | // ContainerConfig Web 容器配置 43 | type ContainerConfig struct { 44 | IP string // 监听 IP 45 | Port int // 监听端口 46 | EnableSSL bool // 使用 SSL 47 | KeyFile string // SSL 证书 48 | CertFile string // SSL 秘钥 49 | 50 | ReadTimeout time.Duration 51 | WriteTimeout time.Duration 52 | } 53 | 54 | // WebContainer Web 容器 55 | type WebContainer interface { 56 | // WebMapping 路由表 57 | WebMapping 58 | 59 | // Config 获取 Web 容器配置 60 | Config() ContainerConfig 61 | 62 | // GetFilters 返回过滤器列表 63 | GetFilters() []Filter 64 | 65 | // ResetFilters 重新设置过滤器列表 66 | ResetFilters(filters []Filter) 67 | 68 | // AddFilter 添加过滤器 69 | AddFilter(filter ...Filter) 70 | 71 | // GetLoggerFilter 获取 Logger Filter 72 | GetLoggerFilter() Filter 73 | 74 | // SetLoggerFilter 设置 Logger Filter 75 | SetLoggerFilter(filter Filter) 76 | 77 | // GetRecoveryFilter 获取 Recovery Filter 78 | GetRecoveryFilter() Filter 79 | 80 | // SetRecoveryFilter 设置 Recovery Filter 81 | SetRecoveryFilter(filter Filter) 82 | 83 | // AddRouter 添加新的路由信息 84 | AddRouter(router *Router) 85 | 86 | // EnableSwagger 是否启用 Swagger 功能 87 | EnableSwagger() bool 88 | 89 | // SetEnableSwagger 设置是否启用 Swagger 功能 90 | SetEnableSwagger(enable bool) 91 | 92 | // Swagger 返回和容器绑定的 Swagger 对象 93 | Swagger() *Swagger 94 | 95 | // Start 启动 Web 容器,非阻塞 96 | Start() 97 | 98 | // Stop 停止 Web 容器,阻塞 99 | Stop(ctx context.Context) 100 | } 101 | 102 | // BaseWebContainer WebContainer 的通用部分 103 | type BaseWebContainer struct { 104 | WebMapping 105 | 106 | config ContainerConfig 107 | 108 | enableSwag bool // 是否启用 Swagger 功能 109 | swagger *Swagger // 和容器绑定的 Swagger 对象 110 | 111 | filters []Filter // 其他过滤器 112 | loggerFilter Filter // 日志过滤器 113 | recoveryFilter Filter // 恢复过滤器 114 | } 115 | 116 | // NewBaseWebContainer BaseWebContainer 的构造函数 117 | func NewBaseWebContainer(config ContainerConfig) *BaseWebContainer { 118 | return &BaseWebContainer{ 119 | WebMapping: NewDefaultWebMapping(), 120 | config: config, 121 | enableSwag: true, 122 | loggerFilter: defaultLoggerFilter, 123 | recoveryFilter: defaultRecoveryFilter, 124 | } 125 | } 126 | 127 | // Address 返回监听地址 128 | func (c *BaseWebContainer) Address() string { 129 | return fmt.Sprintf("%s:%d", c.config.IP, c.config.Port) 130 | } 131 | 132 | // Config 获取 Web 容器配置 133 | func (c *BaseWebContainer) Config() ContainerConfig { 134 | return c.config 135 | } 136 | 137 | // GetFilters 返回过滤器列表 138 | func (c *BaseWebContainer) GetFilters() []Filter { 139 | return c.filters 140 | } 141 | 142 | // ResetFilters 重新设置过滤器列表 143 | func (c *BaseWebContainer) ResetFilters(filters []Filter) { 144 | c.filters = filters 145 | } 146 | 147 | // AddFilter 添加过滤器 148 | func (c *BaseWebContainer) AddFilter(filter ...Filter) { 149 | c.filters = append(c.filters, filter...) 150 | } 151 | 152 | // GetLoggerFilter 获取 Logger Filter 153 | func (c *BaseWebContainer) GetLoggerFilter() Filter { 154 | return c.loggerFilter 155 | } 156 | 157 | // SetLoggerFilter 设置 Logger Filter 158 | func (c *BaseWebContainer) SetLoggerFilter(filter Filter) { 159 | c.loggerFilter = filter 160 | } 161 | 162 | // GetRecoveryFilter 获取 Recovery Filter 163 | func (c *BaseWebContainer) GetRecoveryFilter() Filter { 164 | return c.recoveryFilter 165 | } 166 | 167 | // 设置 Recovery Filter 168 | func (c *BaseWebContainer) SetRecoveryFilter(filter Filter) { 169 | c.recoveryFilter = filter 170 | } 171 | 172 | // AddRouter 添加新的路由信息 173 | func (c *BaseWebContainer) AddRouter(router *Router) { 174 | for _, mapper := range router.mapping.Mappers() { 175 | c.AddMapper(mapper) 176 | } 177 | } 178 | 179 | // EnableSwagger 是否启用 Swagger 功能 180 | func (c *BaseWebContainer) EnableSwagger() bool { 181 | return c.enableSwag 182 | } 183 | 184 | // SetEnableSwagger 设置是否启用 Swagger 功能 185 | func (c *BaseWebContainer) SetEnableSwagger(enable bool) { 186 | c.enableSwag = enable 187 | } 188 | 189 | // Swagger 返回和容器绑定的 Swagger 对象 190 | func (c *BaseWebContainer) Swagger() *Swagger { 191 | if c.swagger == nil { 192 | c.swagger = NewSwagger() 193 | } 194 | return c.swagger 195 | } 196 | 197 | // PreStart 执行 Start 之前的准备工作 198 | func (c *BaseWebContainer) PreStart() { 199 | 200 | if c.enableSwag && c.swagger != nil { 201 | 202 | // 注册 path 的 Operation 203 | for _, mapper := range c.Mappers() { 204 | if op := mapper.swagger; op != nil { 205 | if err := op.parseBind(); err != nil { 206 | panic(err) 207 | } 208 | c.swagger.AddPath(mapper.Path(), mapper.Method(), op) 209 | } 210 | } 211 | 212 | doc := c.swagger.ReadDoc() 213 | hSwagger := httpSwagger.Handler(httpSwagger.URL("/swagger/doc.json")) 214 | 215 | // 注册 swagger-ui 和 doc.json 接口 216 | c.GetMapping("/swagger/*", func(webCtx WebContext) { 217 | if webCtx.PathParam("*") == "doc.json" { 218 | webCtx.Header(HeaderContentType, MIMEApplicationJSONCharsetUTF8) 219 | webCtx.String(http.StatusOK, doc) 220 | } else { 221 | hSwagger(webCtx.ResponseWriter(), webCtx.Request()) 222 | } 223 | }) 224 | 225 | // 注册 redoc 接口 226 | c.GetMapping("/redoc", ReDoc) 227 | } 228 | 229 | } 230 | 231 | // PrintMapper 打印路由注册信息 232 | func (c *BaseWebContainer) PrintMapper(m *Mapper) { 233 | file, line, fnName := m.handler.FileLine() 234 | SpringLogger.Infof("%v :%d %s -> %s:%d %s", GetMethod(m.method), c.config.Port, m.path, file, line, fnName) 235 | } 236 | 237 | /////////////////// Invoke Handler ////////////////////// 238 | 239 | // InvokeHandler 执行 Web 处理函数 240 | func InvokeHandler(ctx WebContext, fn Handler, filters []Filter) { 241 | if len(filters) > 0 { 242 | filters = append(filters, HandlerFilter(fn)) 243 | chain := NewDefaultFilterChain(filters) 244 | chain.Next(ctx) 245 | } else { 246 | fn.Invoke(ctx) 247 | } 248 | } 249 | 250 | /////////////////// Web Handlers ////////////////////// 251 | 252 | // fnHandler 封装 Web 处理函数 253 | type fnHandler HandlerFunc 254 | 255 | func (f fnHandler) Invoke(ctx WebContext) { 256 | f(ctx) 257 | } 258 | 259 | func (f fnHandler) FileLine() (file string, line int, fnName string) { 260 | return SpringUtils.FileLine(f) 261 | } 262 | 263 | // FUNC 标准 Web 处理函数的辅助函数 264 | func FUNC(fn HandlerFunc) Handler { 265 | return fnHandler(fn) 266 | } 267 | 268 | // httpHandler 标准 Http 处理函数 269 | type httpHandler http.HandlerFunc 270 | 271 | func (h httpHandler) Invoke(ctx WebContext) { 272 | h(ctx.ResponseWriter(), ctx.Request()) 273 | } 274 | 275 | func (h httpHandler) FileLine() (file string, line int, fnName string) { 276 | return SpringUtils.FileLine(h) 277 | } 278 | 279 | // HTTP 标准 Http 处理函数的辅助函数 280 | func HTTP(fn http.HandlerFunc) Handler { 281 | return httpHandler(fn) 282 | } 283 | 284 | // WrapF 标准 Http 处理函数的辅助函数,兼容 gin 写法 285 | func WrapF(fn http.HandlerFunc) Handler { 286 | return httpHandler(fn) 287 | } 288 | 289 | // WrapH 标准 Http 处理函数的辅助函数,兼容 gin 写法 290 | func WrapH(h http.Handler) Handler { 291 | return httpHandler(h.ServeHTTP) 292 | } 293 | 294 | /////////////////// Web Filters ////////////////////// 295 | 296 | var defaultRecoveryFilter = &recoveryFilter{} 297 | 298 | // recoveryFilter 恢复过滤器 299 | type recoveryFilter struct{} 300 | 301 | func (f *recoveryFilter) Invoke(ctx WebContext, chain FilterChain) { 302 | 303 | defer func() { 304 | if err := recover(); err != nil { 305 | ctx.LogError("[PANIC RECOVER] ", err) 306 | ctx.Status(http.StatusInternalServerError) 307 | } 308 | }() 309 | 310 | chain.Next(ctx) 311 | } 312 | 313 | var defaultLoggerFilter = &loggerFilter{} 314 | 315 | // loggerFilter 日志过滤器 316 | type loggerFilter struct{} 317 | 318 | func (f *loggerFilter) Invoke(ctx WebContext, chain FilterChain) { 319 | start := time.Now() 320 | chain.Next(ctx) 321 | ctx.LogInfo("cost: ", time.Since(start)) 322 | } 323 | -------------------------------------------------------------------------------- /spring-web-context.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package SpringWeb 18 | 19 | import ( 20 | "io" 21 | "mime/multipart" 22 | "net/http" 23 | "net/url" 24 | 25 | "github.com/go-spring/go-spring-logger" 26 | ) 27 | 28 | // WebContextKey WebContext 和 NativeContext 相互转换的 Key 29 | const WebContextKey = "@WebCtx" 30 | 31 | // WebContext 上下文接口,设计理念:为社区中优秀的 Web 服务器提供一个抽象层, 32 | // 使得底层可以灵活切换,因此在功能上取这些 Web 服务器功能的交集,同时提供获取 33 | // 底层对象的接口,以便在不能满足用户要求的时候使用底层实现的能力,当然要慎用。 34 | type WebContext interface { 35 | ///////////////////////////////////////// 36 | // 通用能力部分 37 | 38 | // LoggerContext 日志接口上下文 39 | SpringLogger.LoggerContext 40 | 41 | // NativeContext 返回封装的底层上下文对象 42 | NativeContext() interface{} 43 | 44 | // Get retrieves data from the context. 45 | Get(key string) interface{} 46 | 47 | // Set saves data in the context. 48 | Set(key string, val interface{}) 49 | 50 | ///////////////////////////////////////// 51 | // Request Part 52 | 53 | // Request returns `*http.Request`. 54 | Request() *http.Request 55 | 56 | // SetRequest sets `*http.Request`. 57 | SetRequest(r *http.Request) 58 | 59 | // IsTLS returns true if HTTP connection is TLS otherwise false. 60 | IsTLS() bool 61 | 62 | // IsWebSocket returns true if HTTP connection is WebSocket otherwise false. 63 | IsWebSocket() bool 64 | 65 | // Scheme returns the HTTP protocol scheme, `http` or `https`. 66 | Scheme() string 67 | 68 | // ClientIP implements a best effort algorithm to return the real client IP, 69 | // it parses X-Real-IP and X-Forwarded-For in order to work properly with 70 | // reverse-proxies such us: nginx or haproxy. Use X-Forwarded-For before 71 | // X-Real-Ip as nginx uses X-Real-Ip with the proxy's IP. 72 | ClientIP() string 73 | 74 | // Path returns the registered path for the handler. 75 | Path() string 76 | 77 | // Handler returns the matched handler by router. 78 | Handler() Handler 79 | 80 | // ContentType returns the Content-Type header of the request. 81 | ContentType() string 82 | 83 | // GetHeader returns value from request headers. 84 | GetHeader(key string) string 85 | 86 | // GetRawData return stream data. 87 | GetRawData() ([]byte, error) 88 | 89 | // PathParam returns path parameter by name. 90 | PathParam(name string) string 91 | 92 | // PathParamNames returns path parameter names. 93 | PathParamNames() []string 94 | 95 | // PathParamValues returns path parameter values. 96 | PathParamValues() []string 97 | 98 | // QueryParam returns the query param for the provided name. 99 | QueryParam(name string) string 100 | 101 | // QueryParams returns the query parameters as `url.Values`. 102 | QueryParams() url.Values 103 | 104 | // QueryString returns the URL query string. 105 | QueryString() string 106 | 107 | // FormValue returns the form field value for the provided name. 108 | FormValue(name string) string 109 | 110 | // FormParams returns the form parameters as `url.Values`. 111 | FormParams() (url.Values, error) 112 | 113 | // FormFile returns the multipart form file for the provided name. 114 | FormFile(name string) (*multipart.FileHeader, error) 115 | 116 | // SaveUploadedFile uploads the form file to specific dst. 117 | SaveUploadedFile(file *multipart.FileHeader, dst string) error 118 | 119 | // MultipartForm returns the multipart form. 120 | MultipartForm() (*multipart.Form, error) 121 | 122 | // Cookie returns the named cookie provided in the request. 123 | Cookie(name string) (*http.Cookie, error) 124 | 125 | // Cookies returns the HTTP cookies sent with the request. 126 | Cookies() []*http.Cookie 127 | 128 | // Bind binds the request body into provided type `i`. The default binder 129 | // does it based on Content-Type header. 130 | Bind(i interface{}) error 131 | 132 | ///////////////////////////////////////// 133 | // Response Part 134 | 135 | // ResponseWriter returns `http.ResponseWriter`. 136 | ResponseWriter() http.ResponseWriter 137 | 138 | // Status sets the HTTP response code. 139 | Status(code int) 140 | 141 | // Header is a intelligent shortcut for c.Writer.Header().Set(key, value). 142 | // It writes a header in the response. 143 | // If value == "", this method removes the header `c.Writer.Header().Del(key)` 144 | Header(key, value string) 145 | 146 | // SetCookie adds a `Set-Cookie` header in HTTP response. 147 | SetCookie(cookie *http.Cookie) 148 | 149 | // NoContent sends a response with no body and a status code. 150 | NoContent(code int) 151 | 152 | // String writes the given string into the response body. 153 | String(code int, format string, values ...interface{}) 154 | 155 | // HTML sends an HTTP response with status code. 156 | HTML(code int, html string) 157 | 158 | // HTMLBlob sends an HTTP blob response with status code. 159 | HTMLBlob(code int, b []byte) 160 | 161 | // JSON sends a JSON response with status code. 162 | JSON(code int, i interface{}) 163 | 164 | // JSONPretty sends a pretty-print JSON with status code. 165 | JSONPretty(code int, i interface{}, indent string) 166 | 167 | // JSONBlob sends a JSON blob response with status code. 168 | JSONBlob(code int, b []byte) 169 | 170 | // JSONP sends a JSONP response with status code. It uses `callback` 171 | // to construct the JSONP payload. 172 | JSONP(code int, callback string, i interface{}) 173 | 174 | // JSONPBlob sends a JSONP blob response with status code. It uses 175 | // `callback` to construct the JSONP payload. 176 | JSONPBlob(code int, callback string, b []byte) 177 | 178 | // XML sends an XML response with status code. 179 | XML(code int, i interface{}) 180 | 181 | // XMLPretty sends a pretty-print XML with status code. 182 | XMLPretty(code int, i interface{}, indent string) 183 | 184 | // XMLBlob sends an XML blob response with status code. 185 | XMLBlob(code int, b []byte) 186 | 187 | // Blob sends a blob response with status code and content type. 188 | Blob(code int, contentType string, b []byte) 189 | 190 | // Stream sends a streaming response with status code and content type. 191 | Stream(code int, contentType string, r io.Reader) 192 | 193 | // File sends a response with the content of the file. 194 | File(file string) 195 | 196 | // Attachment sends a response as attachment, prompting client to save the 197 | // file. 198 | Attachment(file string, name string) 199 | 200 | // Inline sends a response as inline, opening the file in the browser. 201 | Inline(file string, name string) 202 | 203 | // Redirect redirects the request to a provided URL with status code. 204 | Redirect(code int, url string) 205 | 206 | // SSEvent writes a Server-Sent Event into the body stream. 207 | SSEvent(name string, message interface{}) 208 | } 209 | -------------------------------------------------------------------------------- /spring-web-filter.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package SpringWeb 18 | 19 | // Filter 过滤器接口 20 | type Filter interface { 21 | // Invoke 通过 chain.Next() 驱动链条向后执行 22 | Invoke(ctx WebContext, chain FilterChain) 23 | } 24 | 25 | // handlerFilter 包装 Web 处理接口的过滤器 26 | type handlerFilter struct { 27 | fn Handler 28 | } 29 | 30 | // HandlerFilter 把 Web 处理接口转换成过滤器 31 | func HandlerFilter(fn Handler) Filter { 32 | return &handlerFilter{fn: fn} 33 | } 34 | 35 | func (h *handlerFilter) Invoke(ctx WebContext, _ FilterChain) { 36 | h.fn.Invoke(ctx) 37 | } 38 | 39 | // FilterChain 过滤器链条接口 40 | type FilterChain interface { 41 | Next(ctx WebContext) 42 | } 43 | 44 | // DefaultFilterChain 默认的过滤器链条 45 | type DefaultFilterChain struct { 46 | filters []Filter // 过滤器列表 47 | next int // 下一个等待执行的过滤器的序号 48 | } 49 | 50 | // NewDefaultFilterChain DefaultFilterChain 的构造函数 51 | func NewDefaultFilterChain(filters []Filter) *DefaultFilterChain { 52 | return &DefaultFilterChain{filters: filters} 53 | } 54 | 55 | func (chain *DefaultFilterChain) Next(ctx WebContext) { 56 | 57 | // 链条执行到此结束 58 | if chain.next >= len(chain.filters) { 59 | return 60 | } 61 | 62 | // 执行下一个过滤器 63 | f := chain.filters[chain.next] 64 | chain.next++ 65 | f.Invoke(ctx, chain) 66 | } 67 | -------------------------------------------------------------------------------- /spring-web-mapper.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package SpringWeb 18 | 19 | import ( 20 | "fmt" 21 | ) 22 | 23 | // Mapper 路由映射器 24 | type Mapper struct { 25 | method uint32 // 方法 26 | path string // 路径 27 | handler Handler // 处理函数 28 | filters []Filter // 过滤器列表 29 | swagger *Operation 30 | } 31 | 32 | // NewMapper Mapper 的构造函数 33 | func NewMapper(method uint32, path string, fn Handler, filters []Filter) *Mapper { 34 | return &Mapper{ 35 | method: method, 36 | path: path, 37 | handler: fn, 38 | filters: filters, 39 | } 40 | } 41 | 42 | // Key 返回 Mapper 的标识符 43 | func (m *Mapper) Key() string { 44 | return fmt.Sprintf("0x%.4x@%s", m.method, m.path) 45 | } 46 | 47 | // Method 返回 Mapper 的方法 48 | func (m *Mapper) Method() uint32 { 49 | return m.method 50 | } 51 | 52 | // Path 返回 Mapper 的路径 53 | func (m *Mapper) Path() string { 54 | return m.path 55 | } 56 | 57 | // Handler 返回 Mapper 的处理函数 58 | func (m *Mapper) Handler() Handler { 59 | return m.handler 60 | } 61 | 62 | // Filters 返回 Mapper 的过滤器列表 63 | func (m *Mapper) Filters() []Filter { 64 | return m.filters 65 | } 66 | 67 | // Swagger 生成并返回 Operation 对象 68 | func (m *Mapper) Swagger(id string) *Operation { 69 | m.swagger = NewOperation(id) 70 | return m.swagger 71 | } 72 | 73 | // GetSwagger 返回 Swagger 文档 74 | func (m *Mapper) GetSwagger() *Operation { 75 | return m.swagger 76 | } 77 | 78 | // WithSwagger 设置 Swagger 文档 79 | func (m *Mapper) WithSwagger(swagger *Operation) *Mapper { 80 | m.swagger = swagger 81 | return m 82 | } 83 | -------------------------------------------------------------------------------- /spring-web-mapper_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package SpringWeb_test 18 | 19 | import ( 20 | "fmt" 21 | "testing" 22 | 23 | "github.com/go-spring/go-spring-web" 24 | ) 25 | 26 | func TestMapper_Key(t *testing.T) { 27 | fmt.Println(SpringWeb.NewMapper(SpringWeb.MethodAny, "/", nil, nil).Key()) 28 | fmt.Println(SpringWeb.NewMapper(SpringWeb.MethodGet, "/", nil, nil).Key()) 29 | fmt.Println(SpringWeb.NewMapper(SpringWeb.MethodGetPost, "/", nil, nil).Key()) 30 | } 31 | -------------------------------------------------------------------------------- /spring-web-mapping.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package SpringWeb 18 | 19 | // UrlRegister 路由注册接口 20 | type UrlRegister interface { 21 | 22 | // Request 注册任意 HTTP 方法处理函数 23 | Request(method uint32, path string, fn Handler, filters ...Filter) *Mapper 24 | 25 | // HandleGet 注册 GET 方法处理函数 26 | HandleGet(path string, fn Handler, filters ...Filter) *Mapper 27 | 28 | // GetMapping 注册 GET 方法处理函数 29 | GetMapping(path string, fn HandlerFunc, filters ...Filter) *Mapper 30 | 31 | // GetBinding 注册 GET 方法处理函数 32 | GetBinding(path string, fn interface{}, filters ...Filter) *Mapper 33 | 34 | // HandlePost 注册 POST 方法处理函数 35 | HandlePost(path string, fn Handler, filters ...Filter) *Mapper 36 | 37 | // PostMapping 注册 POST 方法处理函数 38 | PostMapping(path string, fn HandlerFunc, filters ...Filter) *Mapper 39 | 40 | // PostBinding 注册 POST 方法处理函数 41 | PostBinding(path string, fn interface{}, filters ...Filter) *Mapper 42 | 43 | // HandlePut 注册 PUT 方法处理函数 44 | HandlePut(path string, fn Handler, filters ...Filter) *Mapper 45 | 46 | // PutMapping 注册 PUT 方法处理函数 47 | PutMapping(path string, fn HandlerFunc, filters ...Filter) *Mapper 48 | 49 | // PutBinding 注册 PUT 方法处理函数 50 | PutBinding(path string, fn interface{}, filters ...Filter) *Mapper 51 | 52 | // HandleDelete 注册 DELETE 方法处理函数 53 | HandleDelete(path string, fn Handler, filters ...Filter) *Mapper 54 | 55 | // DeleteMapping 注册 DELETE 方法处理函数 56 | DeleteMapping(path string, fn HandlerFunc, filters ...Filter) *Mapper 57 | 58 | // DeleteBinding 注册 DELETE 方法处理函数 59 | DeleteBinding(path string, fn interface{}, filters ...Filter) *Mapper 60 | } 61 | 62 | // defaultUrlRegister 路由注册接口的默认实现 63 | type defaultUrlRegister struct { 64 | request func(method uint32, path string, fn Handler, filters []Filter) *Mapper 65 | } 66 | 67 | // Request 注册任意 HTTP 方法处理函数 68 | func (r *defaultUrlRegister) Request(method uint32, path string, fn Handler, filters ...Filter) *Mapper { 69 | return r.request(method, path, fn, filters) 70 | } 71 | 72 | // HandleGet 注册 GET 方法处理函数 73 | func (r *defaultUrlRegister) HandleGet(path string, fn Handler, filters ...Filter) *Mapper { 74 | return r.request(MethodGet, path, fn, filters) 75 | } 76 | 77 | // GetMapping 注册 GET 方法处理函数 78 | func (r *defaultUrlRegister) GetMapping(path string, fn HandlerFunc, filters ...Filter) *Mapper { 79 | return r.request(MethodGet, path, FUNC(fn), filters) 80 | } 81 | 82 | // GetBinding 注册 GET 方法处理函数 83 | func (r *defaultUrlRegister) GetBinding(path string, fn interface{}, filters ...Filter) *Mapper { 84 | return r.request(MethodGet, path, BIND(fn), filters) 85 | } 86 | 87 | // HandlePost 注册 POST 方法处理函数 88 | func (r *defaultUrlRegister) HandlePost(path string, fn Handler, filters ...Filter) *Mapper { 89 | return r.request(MethodPost, path, fn, filters) 90 | } 91 | 92 | // PostMapping 注册 POST 方法处理函数 93 | func (r *defaultUrlRegister) PostMapping(path string, fn HandlerFunc, filters ...Filter) *Mapper { 94 | return r.request(MethodPost, path, FUNC(fn), filters) 95 | } 96 | 97 | // PostBinding 注册 POST 方法处理函数 98 | func (r *defaultUrlRegister) PostBinding(path string, fn interface{}, filters ...Filter) *Mapper { 99 | return r.request(MethodPost, path, BIND(fn), filters) 100 | } 101 | 102 | // HandlePut 注册 PUT 方法处理函数 103 | func (r *defaultUrlRegister) HandlePut(path string, fn Handler, filters ...Filter) *Mapper { 104 | return r.request(MethodPut, path, fn, filters) 105 | } 106 | 107 | // PutMapping 注册 PUT 方法处理函数 108 | func (r *defaultUrlRegister) PutMapping(path string, fn HandlerFunc, filters ...Filter) *Mapper { 109 | return r.request(MethodPut, path, FUNC(fn), filters) 110 | } 111 | 112 | // PutBinding 注册 PUT 方法处理函数 113 | func (r *defaultUrlRegister) PutBinding(path string, fn interface{}, filters ...Filter) *Mapper { 114 | return r.request(MethodPut, path, BIND(fn), filters) 115 | } 116 | 117 | // HandleDelete 注册 DELETE 方法处理函数 118 | func (r *defaultUrlRegister) HandleDelete(path string, fn Handler, filters ...Filter) *Mapper { 119 | return r.request(MethodDelete, path, fn, filters) 120 | } 121 | 122 | // DeleteMapping 注册 DELETE 方法处理函数 123 | func (r *defaultUrlRegister) DeleteMapping(path string, fn HandlerFunc, filters ...Filter) *Mapper { 124 | return r.request(MethodDelete, path, FUNC(fn), filters) 125 | } 126 | 127 | // DeleteBinding 注册 DELETE 方法处理函数 128 | func (r *defaultUrlRegister) DeleteBinding(path string, fn interface{}, filters ...Filter) *Mapper { 129 | return r.request(MethodDelete, path, BIND(fn), filters) 130 | } 131 | 132 | // WebMapping 路由表,Spring-Web 使用的路由规则和 echo 完全相同,并对 gin 做了适配。 133 | type WebMapping interface { 134 | UrlRegister 135 | 136 | // Mappers 返回映射器列表 137 | Mappers() map[string]*Mapper 138 | 139 | // AddMapper 添加一个 Mapper 140 | AddMapper(m *Mapper) *Mapper 141 | 142 | // Route 返回和 Mapping 绑定的路由分组 143 | Route(basePath string, filters ...Filter) *Router 144 | } 145 | 146 | // defaultWebMapping 路由表的默认实现 147 | type defaultWebMapping struct { 148 | UrlRegister 149 | 150 | mappers map[string]*Mapper 151 | } 152 | 153 | // NewDefaultWebMapping defaultWebMapping 的构造函数 154 | func NewDefaultWebMapping() *defaultWebMapping { 155 | m := &defaultWebMapping{} 156 | m.mappers = make(map[string]*Mapper) 157 | m.UrlRegister = &defaultUrlRegister{request: m.request} 158 | return m 159 | } 160 | 161 | // Mappers 返回映射器列表 162 | func (w *defaultWebMapping) Mappers() map[string]*Mapper { 163 | return w.mappers 164 | } 165 | 166 | // AddMapper 添加一个 Mapper 167 | func (w *defaultWebMapping) AddMapper(m *Mapper) *Mapper { 168 | w.mappers[m.Key()] = m 169 | return m 170 | } 171 | 172 | // Route 返回和 Mapping 绑定的路由分组 173 | func (w *defaultWebMapping) Route(basePath string, filters ...Filter) *Router { 174 | return routerWithMapping(w, basePath, filters) 175 | } 176 | 177 | func (w *defaultWebMapping) request(method uint32, path string, fn Handler, filters []Filter) *Mapper { 178 | m := NewMapper(method, path, fn, filters) 179 | w.mappers[m.Key()] = m 180 | return m 181 | } 182 | -------------------------------------------------------------------------------- /spring-web-method.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package SpringWeb 18 | 19 | import ( 20 | "net/http" 21 | ) 22 | 23 | const ( 24 | MethodGet = 0x0001 // "GET" 25 | MethodHead = 0x0002 // "HEAD" 26 | MethodPost = 0x0004 // "POST" 27 | MethodPut = 0x0008 // "PUT" 28 | MethodPatch = 0x0010 // "PATCH" 29 | MethodDelete = 0x0020 // "DELETE" 30 | MethodConnect = 0x0040 // "CONNECT" 31 | MethodOptions = 0x0080 // "OPTIONS" 32 | MethodTrace = 0x0100 // "TRACE" 33 | MethodAny = 0xffff 34 | MethodGetPost = MethodGet | MethodPost 35 | ) 36 | 37 | var methods = map[uint32]string{ 38 | MethodGet: http.MethodGet, 39 | MethodHead: http.MethodHead, 40 | MethodPost: http.MethodPost, 41 | MethodPut: http.MethodPut, 42 | MethodPatch: http.MethodPatch, 43 | MethodDelete: http.MethodDelete, 44 | MethodConnect: http.MethodConnect, 45 | MethodOptions: http.MethodOptions, 46 | MethodTrace: http.MethodTrace, 47 | } 48 | 49 | // GetMethod 返回 method 对应的 HTTP 方法 50 | func GetMethod(method uint32) []string { 51 | var r []string 52 | for k, v := range methods { 53 | if method&k == k { 54 | r = append(r, v) 55 | } 56 | } 57 | return r 58 | } 59 | -------------------------------------------------------------------------------- /spring-web-method_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package SpringWeb_test 18 | 19 | import ( 20 | "net/http" 21 | "testing" 22 | 23 | "github.com/go-spring/go-spring-web" 24 | ) 25 | 26 | // cacheMethods 27 | var cacheMethods = map[uint32][]string{ 28 | SpringWeb.MethodGet: {http.MethodGet}, 29 | SpringWeb.MethodHead: {http.MethodHead}, 30 | SpringWeb.MethodPost: {http.MethodPost}, 31 | SpringWeb.MethodPut: {http.MethodPut}, 32 | SpringWeb.MethodPatch: {http.MethodPatch}, 33 | SpringWeb.MethodDelete: {http.MethodDelete}, 34 | SpringWeb.MethodConnect: {http.MethodConnect}, 35 | SpringWeb.MethodOptions: {http.MethodOptions}, 36 | SpringWeb.MethodTrace: {http.MethodTrace}, 37 | SpringWeb.MethodGetPost: {http.MethodGet, http.MethodPost}, 38 | SpringWeb.MethodAny: {http.MethodGet, http.MethodHead, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete, http.MethodConnect, http.MethodOptions, http.MethodTrace}, 39 | } 40 | 41 | func GetMethodViaCache(method uint32) []string { 42 | if r, ok := cacheMethods[method]; ok { 43 | return r 44 | } 45 | return SpringWeb.GetMethod(method) 46 | } 47 | 48 | func BenchmarkGetMethod(b *testing.B) { 49 | // 测试结论:使用缓存不一定能提高效率 50 | 51 | b.Run("1", func(b *testing.B) { 52 | SpringWeb.GetMethod(SpringWeb.MethodGet) 53 | }) 54 | 55 | b.Run("cache-1", func(b *testing.B) { 56 | GetMethodViaCache(SpringWeb.MethodGet) 57 | }) 58 | 59 | b.Run("2", func(b *testing.B) { 60 | SpringWeb.GetMethod(SpringWeb.MethodGet | SpringWeb.MethodHead) 61 | }) 62 | 63 | b.Run("cache-2", func(b *testing.B) { 64 | GetMethodViaCache(SpringWeb.MethodGet | SpringWeb.MethodHead) 65 | }) 66 | 67 | b.Run("3", func(b *testing.B) { 68 | SpringWeb.GetMethod(SpringWeb.MethodGet | SpringWeb.MethodHead | SpringWeb.MethodPost) 69 | }) 70 | 71 | b.Run("cache-3", func(b *testing.B) { 72 | GetMethodViaCache(SpringWeb.MethodGet | SpringWeb.MethodHead | SpringWeb.MethodPost) 73 | }) 74 | 75 | b.Run("4", func(b *testing.B) { 76 | SpringWeb.GetMethod(SpringWeb.MethodGet | SpringWeb.MethodHead | SpringWeb.MethodPost | SpringWeb.MethodPut) 77 | }) 78 | 79 | b.Run("cache-4", func(b *testing.B) { 80 | GetMethodViaCache(SpringWeb.MethodGet | SpringWeb.MethodHead | SpringWeb.MethodPost | SpringWeb.MethodPut) 81 | }) 82 | 83 | b.Run("5", func(b *testing.B) { 84 | SpringWeb.GetMethod(SpringWeb.MethodGet | SpringWeb.MethodHead | SpringWeb.MethodPost | SpringWeb.MethodPut | SpringWeb.MethodPatch) 85 | }) 86 | 87 | b.Run("cache-5", func(b *testing.B) { 88 | GetMethodViaCache(SpringWeb.MethodGet | SpringWeb.MethodHead | SpringWeb.MethodPost | SpringWeb.MethodPut | SpringWeb.MethodPatch) 89 | }) 90 | 91 | b.Run("6", func(b *testing.B) { 92 | SpringWeb.GetMethod(SpringWeb.MethodGet | SpringWeb.MethodHead | SpringWeb.MethodPost | SpringWeb.MethodPut | SpringWeb.MethodPatch | SpringWeb.MethodDelete) 93 | }) 94 | 95 | b.Run("cache-6", func(b *testing.B) { 96 | GetMethodViaCache(SpringWeb.MethodGet | SpringWeb.MethodHead | SpringWeb.MethodPost | SpringWeb.MethodPut | SpringWeb.MethodPatch | SpringWeb.MethodDelete) 97 | }) 98 | 99 | b.Run("7", func(b *testing.B) { 100 | SpringWeb.GetMethod(SpringWeb.MethodGet | SpringWeb.MethodHead | SpringWeb.MethodPost | SpringWeb.MethodPut | SpringWeb.MethodPatch | SpringWeb.MethodDelete | SpringWeb.MethodConnect) 101 | }) 102 | 103 | b.Run("cache-7", func(b *testing.B) { 104 | GetMethodViaCache(SpringWeb.MethodGet | SpringWeb.MethodHead | SpringWeb.MethodPost | SpringWeb.MethodPut | SpringWeb.MethodPatch | SpringWeb.MethodDelete | SpringWeb.MethodConnect) 105 | }) 106 | 107 | b.Run("8", func(b *testing.B) { 108 | SpringWeb.GetMethod(SpringWeb.MethodGet | SpringWeb.MethodHead | SpringWeb.MethodPost | SpringWeb.MethodPut | SpringWeb.MethodPatch | SpringWeb.MethodDelete | SpringWeb.MethodConnect | SpringWeb.MethodOptions) 109 | }) 110 | 111 | b.Run("cache-8", func(b *testing.B) { 112 | GetMethodViaCache(SpringWeb.MethodGet | SpringWeb.MethodHead | SpringWeb.MethodPost | SpringWeb.MethodPut | SpringWeb.MethodPatch | SpringWeb.MethodDelete | SpringWeb.MethodConnect | SpringWeb.MethodOptions) 113 | }) 114 | 115 | b.Run("9", func(b *testing.B) { 116 | SpringWeb.GetMethod(SpringWeb.MethodGet | SpringWeb.MethodHead | SpringWeb.MethodPost | SpringWeb.MethodPut | SpringWeb.MethodPatch | SpringWeb.MethodDelete | SpringWeb.MethodConnect | SpringWeb.MethodOptions | SpringWeb.MethodTrace) 117 | }) 118 | 119 | b.Run("cache-9", func(b *testing.B) { 120 | GetMethodViaCache(SpringWeb.MethodGet | SpringWeb.MethodHead | SpringWeb.MethodPost | SpringWeb.MethodPut | SpringWeb.MethodPatch | SpringWeb.MethodDelete | SpringWeb.MethodConnect | SpringWeb.MethodOptions | SpringWeb.MethodTrace) 121 | }) 122 | } 123 | -------------------------------------------------------------------------------- /spring-web-middleware.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package SpringWeb 18 | 19 | // logger 20 | // echo: https://github.com/labstack/echo/blob/master/middleware/logger.go 21 | // gin: https://github.com/gin-contrib/logger/blob/master/logger.go 22 | // https://github.com/gin-contrib/zap/blob/master/zap.go 23 | 24 | // recover 25 | // echo: https://github.com/labstack/echo/blob/master/middleware/recover.go 26 | // gin: https://github.com/gin-contrib/rollbar/blob/master/recovey.go 27 | 28 | // method_override 29 | // echo: https://github.com/labstack/echo/blob/master/middleware/method_override.go 30 | // gin: 31 | 32 | // redirect 33 | // echo: https://github.com/labstack/echo/blob/master/middleware/redirect.go 34 | // gin: 35 | 36 | // request_id 37 | // echo: https://github.com/labstack/echo/blob/master/middleware/request_id.go 38 | // gin: https://github.com/gin-contrib/requestid/blob/master/requestid.go 39 | 40 | // rewrite 41 | // echo: https://github.com/labstack/echo/blob/master/middleware/rewrite.go 42 | // gin: 43 | 44 | // basic_auth 45 | // echo: https://github.com/labstack/echo/blob/master/middleware/basic_auth.go 46 | // gin: 47 | 48 | // key_auth 49 | // echo: https://github.com/labstack/echo/blob/master/middleware/key_auth.go 50 | // gin: 51 | 52 | // body_dump 53 | // echo: https://github.com/labstack/echo/blob/master/middleware/body_dump.go 54 | // gin: 55 | 56 | // body_limit (413 - Request Entity Too Large) 57 | // echo: https://github.com/labstack/echo/blob/master/middleware/body_limit.go 58 | // gin: https://github.com/gin-contrib/size/blob/master/size.go 59 | 60 | // compress (gzip) 61 | // echo: https://github.com/labstack/echo/blob/master/middleware/compress.go 62 | // gin: https://github.com/gin-contrib/gzip/blob/master/gzip.go 63 | 64 | // cors (Cross-Origin Resource Sharing) 65 | // echo: https://github.com/labstack/echo/blob/master/middleware/cors.go 66 | // gin: https://github.com/gin-contrib/cors/blob/master/cors.go 67 | 68 | // csrf (Cross-Site Request Forgery) 69 | // echo: https://github.com/labstack/echo/blob/master/middleware/csrf.go 70 | // gin: 71 | 72 | // secure (XSS) 73 | // echo: https://github.com/labstack/echo/blob/master/middleware/secure.go 74 | // gin: 75 | 76 | // jwt (JSON Web Token) 77 | // echo: https://github.com/labstack/echo/blob/master/middleware/jwt.go 78 | // gin: https://github.com/appleboy/gin-jwt/blob/master/auth_jwt.go 79 | 80 | // proxy 81 | // echo: https://github.com/labstack/echo/blob/master/middleware/proxy.go 82 | // gin: 83 | 84 | // slash 85 | // echo: https://github.com/labstack/echo/blob/master/middleware/slash.go 86 | // gin: 87 | 88 | // static 89 | // echo: https://github.com/labstack/echo/blob/master/middleware/static.go 90 | // gin: https://github.com/gin-contrib/static/blob/master/static.go 91 | 92 | // casbin 93 | // echo: https://github.com/labstack/echo-contrib/blob/master/casbin/casbin.go 94 | // gin: https://github.com/gin-contrib/authz/blob/master/authz.go 95 | 96 | // tracing 97 | // echo: https://github.com/labstack/echo-contrib/blob/master/jaegertracing/jaegertracing.go 98 | // gin: https://github.com/gin-contrib/opengintracing/blob/master/tracing.go 99 | 100 | // prometheus 101 | // echo: https://github.com/labstack/echo-contrib/blob/master/prometheus/prometheus.go 102 | // gin: https://github.com/zsais/go-gin-prometheus/blob/master/middleware.go 103 | 104 | // session 105 | // echo: https://github.com/labstack/echo-contrib/blob/master/session/session.go 106 | // gin: https://github.com/gin-contrib/sessions/blob/master/sessions.go 107 | 108 | // pprof 109 | // echo: 110 | // gin: https://github.com/gin-contrib/pprof/blob/master/pprof.go 111 | 112 | // cache 113 | // echo: 114 | // gin: https://github.com/gin-contrib/cache/blob/master/cache.go 115 | 116 | // location 117 | // echo: 118 | // gin: https://github.com/gin-contrib/location/blob/master/location.go 119 | 120 | // httpsign 121 | // echo: 122 | // gin: https://github.com/gin-contrib/httpsign/blob/master/signatureheader.go 123 | // https://github.com/gin-contrib/httpsign/blob/master/authenticator.go 124 | 125 | // limiter 126 | // echo: 127 | // gin: https://github.com/ulule/limiter/blob/master/drivers/middleware/gin/middleware.go 128 | 129 | // swagger 130 | // echo: https://github.com/swaggo/echo-swagger/blob/master/swagger.go 131 | // gin: https://github.com/swaggo/gin-swagger/blob/master/swagger.go 132 | 133 | // oauth2 134 | // echo: 135 | // gin: https://github.com/zalando/gin-oauth2/blob/master/ginoauth2.go 136 | -------------------------------------------------------------------------------- /spring-web-redoc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package SpringWeb 18 | 19 | import ( 20 | "html/template" 21 | ) 22 | 23 | // ReDoc redoc 响应函数 24 | func ReDoc(ctx WebContext) { 25 | 26 | index, err := template.New("redoc.html").Parse(redocTempl) 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | // 不确定 Execute 是否线程安全,官方文档表示也许是线程安全的,谁知道呢 32 | _ = index.Execute(ctx.ResponseWriter(), map[string]interface{}{ 33 | "URL": "/swagger/doc.json", 34 | }) 35 | } 36 | 37 | const redocTempl = ` 38 | 39 | 40 | 41 | ReDoc 42 | 43 | 44 | 45 | 46 | 47 | 50 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | ` 63 | -------------------------------------------------------------------------------- /spring-web-router.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package SpringWeb 18 | 19 | // Router 路由分组 20 | type Router struct { 21 | UrlRegister 22 | 23 | mapping WebMapping 24 | basePath string 25 | filters []Filter 26 | } 27 | 28 | // NewRouter Router 的构造函数,不依赖具体的 WebMapping 对象 29 | func NewRouter(basePath string, filters ...Filter) *Router { 30 | return routerWithMapping(NewDefaultWebMapping(), basePath, filters) 31 | } 32 | 33 | // routerWithMapping Router 的构造函数,依赖具体的 WebMapping 对象 34 | func routerWithMapping(mapping WebMapping, basePath string, filters []Filter) *Router { 35 | r := &Router{} 36 | r.filters = filters 37 | r.mapping = mapping 38 | r.basePath = basePath 39 | r.UrlRegister = &defaultUrlRegister{request: r.request} 40 | return r 41 | } 42 | 43 | func (r *Router) request(method uint32, path string, fn Handler, filters []Filter) *Mapper { 44 | filters = append(r.filters, filters...) 45 | return r.mapping.Request(method, r.basePath+path, fn, filters...) 46 | } 47 | -------------------------------------------------------------------------------- /spring-web-rpc.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package SpringWeb 18 | 19 | import ( 20 | "context" 21 | "errors" 22 | "fmt" 23 | "net/http" 24 | "reflect" 25 | 26 | "github.com/go-spring/go-spring-error" 27 | "github.com/go-spring/go-spring-utils" 28 | ) 29 | 30 | // contextType context.Context 的反射类型 31 | var contextType = reflect.TypeOf((*context.Context)(nil)).Elem() 32 | 33 | // bindHandler BIND 形式的 Web 处理接口 34 | type bindHandler struct { 35 | fn interface{} 36 | fnType reflect.Type 37 | fnValue reflect.Value 38 | bindType reflect.Type 39 | } 40 | 41 | func (b *bindHandler) Invoke(ctx WebContext) { 42 | RpcInvoke(ctx, b.call) 43 | } 44 | 45 | func (b *bindHandler) call(ctx WebContext) interface{} { 46 | 47 | // 反射创建需要绑定请求参数 48 | bindVal := reflect.New(b.bindType.Elem()) 49 | err := ctx.Bind(bindVal.Interface()) 50 | SpringError.ERROR.Panic(err).When(err != nil) 51 | 52 | // 执行处理函数,并返回结果 53 | in := []reflect.Value{reflect.ValueOf(ctx.Context()), bindVal} 54 | return b.fnValue.Call(in)[0].Interface() 55 | } 56 | 57 | func (b *bindHandler) FileLine() (file string, line int, fnName string) { 58 | return SpringUtils.FileLine(b.fn) 59 | } 60 | 61 | func validBindFn(fnType reflect.Type) bool { 62 | 63 | // 必须是函数,必须有两个入参,必须有一个返回值 64 | if fnType.Kind() != reflect.Func || fnType.NumIn() != 2 || fnType.NumOut() != 1 { 65 | return false 66 | } 67 | 68 | // 第一个入参必须是 context.Context 类型 69 | if fnType.In(0) != contextType { 70 | return false 71 | } 72 | 73 | req := fnType.In(1) // 第二个入参必须是结构体指针 74 | return req.Kind() == reflect.Ptr && req.Elem().Kind() == reflect.Struct 75 | } 76 | 77 | // BIND 转换成 BIND 形式的 Web 处理接口 78 | func BIND(fn interface{}) Handler { 79 | if fnType := reflect.TypeOf(fn); validBindFn(fnType) { 80 | return &bindHandler{ 81 | fn: fn, 82 | fnType: fnType, 83 | fnValue: reflect.ValueOf(fn), 84 | bindType: fnType.In(1), 85 | } 86 | } 87 | panic(errors.New("fn should be func(context.Context, *struct})anything")) 88 | } 89 | 90 | // RpcInvoke 可自定义的 rpc 执行函数 91 | var RpcInvoke = defaultRpcInvoke 92 | 93 | // defaultRpcInvoke 默认的 rpc 执行函数 94 | func defaultRpcInvoke(webCtx WebContext, fn func(WebContext) interface{}) { 95 | 96 | // 目前 HTTP RPC 只能返回 json 格式的数据 97 | webCtx.Header("Content-Type", "application/json") 98 | 99 | defer func() { 100 | if r := recover(); r != nil { 101 | result, ok := r.(*SpringError.RpcResult) 102 | if !ok { 103 | var err error 104 | if err, ok = r.(error); !ok { 105 | err = errors.New(fmt.Sprint(r)) 106 | } 107 | result = SpringError.ERROR.Error(err) 108 | } 109 | webCtx.JSON(http.StatusOK, result) 110 | } 111 | }() 112 | 113 | result := SpringError.SUCCESS.Data(fn(webCtx)) 114 | webCtx.JSON(http.StatusOK, result) 115 | } 116 | -------------------------------------------------------------------------------- /spring-web-server.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package SpringWeb 18 | 19 | import ( 20 | "context" 21 | 22 | "github.com/go-spring/go-spring-utils" 23 | ) 24 | 25 | // WebServer 一个 WebServer 包含多个 WebContainer 26 | type WebServer struct { 27 | containers []WebContainer // Web 容器列表 28 | filters []Filter // 共用的普通过滤器 29 | loggerFilter Filter // 共用的日志过滤器 30 | recoveryFilter Filter // 共用的恢复过滤器 31 | } 32 | 33 | // NewWebServer WebServer 的构造函数 34 | func NewWebServer() *WebServer { 35 | return &WebServer{} 36 | } 37 | 38 | // Filters 获取过滤器列表 39 | func (s *WebServer) Filters() []Filter { 40 | return s.filters 41 | } 42 | 43 | // AddFilter 添加共用的普通过滤器 44 | func (s *WebServer) AddFilter(filter ...Filter) *WebServer { 45 | s.filters = append(s.filters, filter...) 46 | return s 47 | } 48 | 49 | // ResetFilters 重新设置过滤器列表 50 | func (s *WebServer) ResetFilters(filters []Filter) { 51 | s.filters = filters 52 | } 53 | 54 | // GetLoggerFilter 获取 Logger Filter 55 | func (s *WebServer) GetLoggerFilter() Filter { 56 | return s.loggerFilter 57 | } 58 | 59 | // SetLoggerFilter 设置共用的日志过滤器 60 | func (s *WebServer) SetLoggerFilter(filter Filter) *WebServer { 61 | s.loggerFilter = filter 62 | return s 63 | } 64 | 65 | // GetRecoveryFilter 获取 Recovery Filter 66 | func (s *WebServer) GetRecoveryFilter() Filter { 67 | return s.recoveryFilter 68 | } 69 | 70 | // SetRecoveryFilter 设置共用的恢复过滤器 71 | func (s *WebServer) SetRecoveryFilter(filter Filter) *WebServer { 72 | s.recoveryFilter = filter 73 | return s 74 | } 75 | 76 | // Containers 返回 WebContainer 实例列表 77 | func (s *WebServer) Containers() []WebContainer { 78 | return s.containers 79 | } 80 | 81 | // AddContainer 添加 WebContainer 实例 82 | func (s *WebServer) AddContainer(container ...WebContainer) *WebServer { 83 | s.containers = append(s.containers, container...) 84 | return s 85 | } 86 | 87 | // Start 启动 Web 容器,非阻塞调用 88 | func (s *WebServer) Start() { 89 | for _, c := range s.containers { 90 | 91 | // 如果 Container 使用的是默认值的话,Container 使用 Server 的日志过滤器 92 | if s.loggerFilter != nil && c.GetLoggerFilter() == defaultLoggerFilter { 93 | c.SetLoggerFilter(s.loggerFilter) 94 | } 95 | 96 | // 如果 Container 使用的是默认值的话,Container 使用 Server 的恢复过滤器 97 | if s.recoveryFilter != nil && c.GetRecoveryFilter() == defaultRecoveryFilter { 98 | c.SetRecoveryFilter(s.recoveryFilter) 99 | } 100 | 101 | // 添加 Server 的普通过滤器给 Container 102 | filters := append(s.filters, c.GetFilters()...) 103 | c.ResetFilters(filters) 104 | 105 | c.Start() 106 | } 107 | } 108 | 109 | // Stop 停止 Web 容器,阻塞调用 110 | func (s *WebServer) Stop(ctx context.Context) { 111 | var wg SpringUtils.WaitGroup 112 | for _, container := range s.containers { 113 | c := container // 避免延迟绑定 114 | wg.Add(func() { c.Stop(ctx) }) 115 | } 116 | wg.Wait() 117 | } 118 | -------------------------------------------------------------------------------- /spring-web-swagger.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package SpringWeb 18 | 19 | import ( 20 | "encoding/xml" 21 | "fmt" 22 | "net/http" 23 | "reflect" 24 | "strings" 25 | "time" 26 | 27 | "github.com/go-openapi/spec" 28 | ) 29 | 30 | // Swagger 封装 spec.Swagger 对象,提供流式调用 31 | type Swagger struct { 32 | spec.Swagger 33 | } 34 | 35 | // NewSwagger swagger 的构造函数 36 | func NewSwagger() *Swagger { 37 | return &Swagger{ 38 | Swagger: spec.Swagger{ 39 | SwaggerProps: spec.SwaggerProps{ 40 | Swagger: "2.0", 41 | Info: &spec.Info{ 42 | InfoProps: spec.InfoProps{ 43 | Contact: &spec.ContactInfo{}, 44 | License: &spec.License{}, 45 | }, 46 | }, 47 | Paths: &spec.Paths{ 48 | Paths: make(map[string]spec.PathItem), 49 | }, 50 | Definitions: make(map[string]spec.Schema), 51 | SecurityDefinitions: map[string]*spec.SecurityScheme{}, 52 | }, 53 | }, 54 | } 55 | } 56 | 57 | // ReadDoc 获取应用的 Swagger 描述内容 58 | func (s *Swagger) ReadDoc() string { 59 | if b, err := s.MarshalJSON(); err == nil { 60 | return string(b) 61 | } else { 62 | panic(err) 63 | } 64 | } 65 | 66 | // WithID 设置应用 ID 67 | func (s *Swagger) WithID(id string) *Swagger { 68 | s.ID = id 69 | return s 70 | } 71 | 72 | // WithConsumes 设置消费协议 73 | func (s *Swagger) WithConsumes(consumes ...string) *Swagger { 74 | s.Consumes = consumes 75 | return s 76 | } 77 | 78 | // WithProduces 设置生产协议 79 | func (s *Swagger) WithProduces(produces ...string) *Swagger { 80 | s.Produces = produces 81 | return s 82 | } 83 | 84 | // WithSchemes 设置服务协议 85 | func (s *Swagger) WithSchemes(schemes ...string) *Swagger { 86 | s.Schemes = schemes 87 | return s 88 | } 89 | 90 | // WithDescription 设置服务描述 91 | func (s *Swagger) WithDescription(description string) *Swagger { 92 | s.Info.Description = description 93 | return s 94 | } 95 | 96 | // WithTitle 设置服务名称 97 | func (s *Swagger) WithTitle(title string) *Swagger { 98 | s.Info.Title = title 99 | return s 100 | } 101 | 102 | // WithTermsOfService 设置服务条款地址 103 | func (s *Swagger) WithTermsOfService(termsOfService string) *Swagger { 104 | s.Info.TermsOfService = termsOfService 105 | return s 106 | } 107 | 108 | // WithContact 设置作者的名字、主页地址、邮箱 109 | func (s *Swagger) WithContact(name string, url string, email string) *Swagger { 110 | c := new(spec.ContactInfo) 111 | c.Name = name 112 | c.URL = url 113 | c.Email = email 114 | s.Info.Contact = c 115 | return s 116 | } 117 | 118 | // WithLicense 设置开源协议的名称、地址 119 | func (s *Swagger) WithLicense(name string, url string) *Swagger { 120 | l := new(spec.License) 121 | l.Name = name 122 | l.URL = url 123 | s.Info.License = l 124 | return s 125 | } 126 | 127 | // WithVersion 设置 API 版本号 128 | func (s *Swagger) WithVersion(version string) *Swagger { 129 | s.Info.Version = version 130 | return s 131 | } 132 | 133 | // WithHost 设置可用服务器地址 134 | func (s *Swagger) WithHost(host string) *Swagger { 135 | s.Host = host 136 | return s 137 | } 138 | 139 | // WithBasePath 设置 API 路径的前缀 140 | func (s *Swagger) WithBasePath(basePath string) *Swagger { 141 | s.BasePath = basePath 142 | return s 143 | } 144 | 145 | // WithTags 添加标签 146 | func (s *Swagger) WithTags(tags ...spec.Tag) *Swagger { 147 | s.Swagger.Tags = tags 148 | return s 149 | } 150 | 151 | // AddPath 添加一个路由 152 | func (s *Swagger) AddPath(path string, method uint32, op *Operation, 153 | parameters ...spec.Parameter) *Swagger { 154 | 155 | path = strings.TrimPrefix(path, s.BasePath) 156 | path = strings.TrimRight(path, "/") 157 | pathItem, ok := s.Paths.Paths[path] 158 | 159 | if !ok { 160 | pathItem = spec.PathItem{ 161 | PathItemProps: spec.PathItemProps{ 162 | Parameters: parameters, 163 | }, 164 | } 165 | } 166 | 167 | for _, m := range GetMethod(method) { 168 | switch m { 169 | case http.MethodGet: 170 | pathItem.Get = op.Operation 171 | case http.MethodPost: 172 | pathItem.Post = op.Operation 173 | case http.MethodPut: 174 | pathItem.Put = op.Operation 175 | case http.MethodDelete: 176 | pathItem.Delete = op.Operation 177 | case http.MethodOptions: 178 | pathItem.Options = op.Operation 179 | case http.MethodHead: 180 | pathItem.Head = op.Operation 181 | case http.MethodPatch: 182 | pathItem.Patch = op.Operation 183 | } 184 | } 185 | 186 | s.Paths.Paths[path] = pathItem 187 | return s 188 | } 189 | 190 | // AddDefinition 添加一个定义 191 | func (s *Swagger) AddDefinition(name string, schema *spec.Schema) *Swagger { 192 | s.Definitions[name] = *schema 193 | return s 194 | } 195 | 196 | type DefinitionField struct { 197 | Description string 198 | Example interface{} 199 | Enums []interface{} 200 | } 201 | 202 | // BindDefinitions 绑定一个定义 203 | func (s *Swagger) BindDefinitions(i ...interface{}) *Swagger { 204 | m := map[string]DefinitionField{} 205 | for _, v := range i { 206 | s.BindDefinitionWithTags(v, m) 207 | } 208 | return s 209 | } 210 | 211 | // BindDefinitionWithTags 绑定一个定义 212 | func (s *Swagger) BindDefinitionWithTags(i interface{}, attachFields map[string]DefinitionField) *Swagger { 213 | 214 | it := reflect.TypeOf(i) 215 | if it.Kind() == reflect.Ptr { 216 | it = it.Elem() 217 | } 218 | 219 | objSchema := new(spec.Schema).Typed("object", "") 220 | for i := 0; i < it.NumField(); i++ { // TODO json 和 xml 分开解析 221 | f := it.Field(i) 222 | 223 | // 处理 XML 标签 224 | var xmlTag []string 225 | if tag, ok := f.Tag.Lookup("xml"); ok { 226 | xmlTag = strings.Split(tag, ",") 227 | if f.Type == reflect.TypeOf(xml.Name{}) { 228 | objSchema.WithXMLName(xmlTag[0]) 229 | continue 230 | } 231 | } 232 | 233 | propName := f.Name 234 | 235 | // 处理 JSON 标签 236 | var jsonTag []string 237 | if tag, ok := f.Tag.Lookup("json"); ok { 238 | jsonTag = strings.Split(tag, ",") 239 | propName = jsonTag[0] 240 | } 241 | 242 | var propSchema *spec.Schema 243 | switch k := f.Type.Kind(); k { 244 | case reflect.Bool: 245 | propSchema = spec.BoolProperty() 246 | case reflect.Int8: 247 | propSchema = spec.Int8Property() 248 | case reflect.Int16: 249 | propSchema = spec.Int16Property() 250 | case reflect.Int32: 251 | propSchema = spec.Int32Property() 252 | case reflect.Int64: 253 | propSchema = spec.Int64Property() 254 | case reflect.String: 255 | propSchema = spec.StringProperty() 256 | case reflect.Struct: 257 | if f.Type == reflect.TypeOf(time.Time{}) { 258 | propSchema = spec.DateTimeProperty() 259 | } else { 260 | panic(fmt.Errorf("unsupported swagger type %s", f.Type)) 261 | } 262 | case reflect.Ptr: 263 | if et := f.Type.Elem(); et.Kind() == reflect.Struct { 264 | propSchema = spec.RefSchema("#/definitions/" + et.Name()) 265 | } else { 266 | panic(fmt.Errorf("unsupported swagger type %s", f.Type)) 267 | } 268 | case reflect.Slice: 269 | { 270 | et := f.Type.Elem() 271 | 272 | var items *spec.Schema 273 | switch k := et.Kind(); k { 274 | case reflect.Bool: 275 | items = spec.BoolProperty() 276 | case reflect.Int8: 277 | items = spec.Int8Property() 278 | case reflect.Int16: 279 | items = spec.Int16Property() 280 | case reflect.Int32: 281 | items = spec.Int32Property() 282 | case reflect.Int64: 283 | items = spec.Int64Property() 284 | case reflect.String: 285 | items = spec.StringProperty() 286 | case reflect.Struct: 287 | items = spec.RefSchema("#/definitions/" + et.Name()) 288 | default: 289 | panic(fmt.Errorf("unsupported swagger type %s", f.Type)) 290 | } 291 | 292 | if len(xmlTag) > 0 { 293 | items.WithXMLName(xmlTag[0]) 294 | } 295 | 296 | propSchema = spec.ArrayProperty(items) 297 | } 298 | default: 299 | panic(fmt.Errorf("unsupported swagger type %s", f.Type)) 300 | } 301 | 302 | if len(xmlTag) > 1 { 303 | for _, v := range xmlTag { 304 | if v == "wrapped" { 305 | propSchema.AsWrappedXML() 306 | break 307 | } 308 | } 309 | } 310 | 311 | required := true 312 | 313 | for _, v := range jsonTag { 314 | if v == "omitempty" { 315 | required = false 316 | break 317 | } 318 | } 319 | 320 | if required { 321 | objSchema.AddRequired(propName) 322 | } 323 | 324 | if attachField, ok := attachFields[propName]; ok { 325 | if len(attachField.Enums) > 0 { 326 | propSchema.WithEnum(attachField.Enums...) 327 | } 328 | if attachField.Description != "" { 329 | propSchema.WithDescription(attachField.Description) 330 | } 331 | if attachField.Example != "" { 332 | propSchema.WithExample(attachField.Example) 333 | } 334 | } 335 | 336 | objSchema.SetProperty(propName, *propSchema) 337 | } 338 | 339 | s.Definitions[it.Name()] = *objSchema 340 | return s 341 | } 342 | 343 | // AddBasicSecurityDefinition 添加 Basic 方式认证 344 | func (s *Swagger) AddBasicSecurityDefinition() *Swagger { 345 | s.Swagger.SecurityDefinitions["BasicAuth"] = spec.BasicAuth() 346 | return s 347 | } 348 | 349 | // AddApiKeySecurityDefinition 添加 ApiKey 方式认证 350 | func (s *Swagger) AddApiKeySecurityDefinition(name string, in string) *Swagger { 351 | if name == "" { 352 | name = "ApiKeyAuth" 353 | } 354 | s.Swagger.SecurityDefinitions[name] = spec.APIKeyAuth(name, in) 355 | return s 356 | } 357 | 358 | // AddOauth2ApplicationSecurityDefinition 添加 OAuth2 Application 方式认证 359 | func (s *Swagger) AddOauth2ApplicationSecurityDefinition(name string, tokenUrl string, scopes map[string]string) *Swagger { 360 | if name == "" { 361 | name = "OAuth2Application" 362 | } 363 | securityScheme := spec.OAuth2Application(tokenUrl) 364 | return s.securitySchemeWithScopes(name, securityScheme, scopes) 365 | } 366 | 367 | // AddOauth2ImplicitSecurityDefinition 添加 OAuth2 Implicit 方式认证 368 | func (s *Swagger) AddOauth2ImplicitSecurityDefinition(name string, authorizationUrl string, scopes map[string]string) *Swagger { 369 | if name == "" { 370 | name = "OAuth2Implicit" 371 | } 372 | securityScheme := spec.OAuth2Implicit(authorizationUrl) 373 | return s.securitySchemeWithScopes(name, securityScheme, scopes) 374 | } 375 | 376 | // AddOauth2PasswordSecurityDefinition 添加 OAuth2 Password 方式认证 377 | func (s *Swagger) AddOauth2PasswordSecurityDefinition(name string, tokenUrl string, scopes map[string]string) *Swagger { 378 | if name == "" { 379 | name = "OAuth2Password" 380 | } 381 | securityScheme := spec.OAuth2Password(tokenUrl) 382 | return s.securitySchemeWithScopes(name, securityScheme, scopes) 383 | } 384 | 385 | // AddOauth2AccessCodeSecurityDefinition 添加 OAuth2 AccessCode 方式认证 386 | func (s *Swagger) AddOauth2AccessCodeSecurityDefinition(name string, authorizationUrl string, tokenUrl string, scopes map[string]string) *Swagger { 387 | if name == "" { 388 | name = "OAuth2AccessCode" 389 | } 390 | securityScheme := spec.OAuth2AccessToken(authorizationUrl, tokenUrl) 391 | return s.securitySchemeWithScopes(name, securityScheme, scopes) 392 | } 393 | 394 | func (s *Swagger) securitySchemeWithScopes(name string, scheme *spec.SecurityScheme, scopes map[string]string) *Swagger { 395 | securityScheme := scheme 396 | for scope, description := range scopes { 397 | securityScheme.AddScope(scope, description) 398 | } 399 | s.Swagger.SecurityDefinitions[name] = securityScheme 400 | return s 401 | } 402 | 403 | // WithExternalDocs 404 | func (s *Swagger) WithExternalDocs(externalDocs *spec.ExternalDocumentation) *Swagger { 405 | s.Swagger.ExternalDocs = externalDocs 406 | return s 407 | } 408 | 409 | type bindParam struct { 410 | param interface{} 411 | description string 412 | } 413 | 414 | // Operation 封装 *spec.Operation 对象,提供更多功能 415 | type Operation struct { 416 | *spec.Operation 417 | bindParam *bindParam 418 | } 419 | 420 | // NewOperation creates a new operation instance. 421 | func NewOperation(id string) *Operation { 422 | return &Operation{Operation: spec.NewOperation(id)} 423 | } 424 | 425 | // WithID sets the ID property on this operation, allows for chaining. 426 | func (o *Operation) WithID(id string) *Operation { 427 | o.Operation.WithID(id) 428 | return o 429 | } 430 | 431 | // WithDescription sets the description on this operation, allows for chaining 432 | func (o *Operation) WithDescription(description string) *Operation { 433 | o.Operation.WithDescription(description) 434 | return o 435 | } 436 | 437 | // WithSummary sets the summary on this operation, allows for chaining 438 | func (o *Operation) WithSummary(summary string) *Operation { 439 | o.Operation.WithSummary(summary) 440 | return o 441 | } 442 | 443 | // WithExternalDocs sets/removes the external docs for/from this operation. 444 | func (o *Operation) WithExternalDocs(description, url string) *Operation { 445 | o.Operation.WithExternalDocs(description, url) 446 | return o 447 | } 448 | 449 | // Deprecate marks the operation as deprecated 450 | func (o *Operation) Deprecate() *Operation { 451 | o.Operation.Deprecate() 452 | return o 453 | } 454 | 455 | // Undeprecate marks the operation as not deprecated 456 | func (o *Operation) Undeprecate() *Operation { 457 | o.Operation.Undeprecate() 458 | return o 459 | } 460 | 461 | // WithConsumes adds media types for incoming body values 462 | func (o *Operation) WithConsumes(mediaTypes ...string) *Operation { 463 | o.Operation.WithConsumes(mediaTypes...) 464 | return o 465 | } 466 | 467 | // WithProduces adds media types for outgoing body values 468 | func (o *Operation) WithProduces(mediaTypes ...string) *Operation { 469 | o.Operation.WithProduces(mediaTypes...) 470 | return o 471 | } 472 | 473 | // WithTags adds tags for this operation 474 | func (o *Operation) WithTags(tags ...string) *Operation { 475 | o.Operation.WithTags(tags...) 476 | return o 477 | } 478 | 479 | // SetSchemes 设置服务协议 480 | func (o *Operation) WithSchemes(schemes ...string) *Operation { 481 | o.Operation.Schemes = schemes 482 | return o 483 | } 484 | 485 | // AddParam adds a parameter to this operation 486 | func (o *Operation) AddParam(param *spec.Parameter) *Operation { 487 | o.Operation.AddParam(param) 488 | return o 489 | } 490 | 491 | // RemoveParam removes a parameter from the operation 492 | func (o *Operation) RemoveParam(name, in string) *Operation { 493 | o.Operation.RemoveParam(name, in) 494 | return o 495 | } 496 | 497 | // SecuredWith adds a security scope to this operation. 498 | func (o *Operation) SecuredWith(name string, scopes ...string) *Operation { 499 | o.Operation.SecuredWith(name, scopes...) 500 | return o 501 | } 502 | 503 | // WithDefaultResponse adds a default response to the operation. 504 | func (o *Operation) WithDefaultResponse(response *spec.Response) *Operation { 505 | o.Operation.WithDefaultResponse(response) 506 | return o 507 | } 508 | 509 | // RespondsWith adds a status code response to the operation. 510 | func (o *Operation) RespondsWith(code int, response *spec.Response) *Operation { 511 | o.Operation.RespondsWith(code, response) 512 | return o 513 | } 514 | 515 | // Bind 绑定请求参数 516 | func (o *Operation) BindParam(i interface{}, description string) *Operation { 517 | o.bindParam = &bindParam{param: i, description: description} 518 | return o 519 | } 520 | 521 | // parseBind 解析绑定的请求参数 522 | func (o *Operation) parseBind() error { 523 | if o.bindParam != nil && o.bindParam.param != nil { 524 | t := reflect.TypeOf(o.bindParam.param) 525 | if t.Kind() == reflect.Ptr { 526 | t = t.Elem() 527 | } 528 | if t.Kind() == reflect.Struct { 529 | schema := spec.RefSchema("#/definitions/" + t.Name()) 530 | param := BodyParam("body", schema). 531 | WithDescription(o.bindParam.description). 532 | AsRequired() 533 | o.AddParam(param) 534 | } 535 | } 536 | return nil 537 | } 538 | 539 | // HeaderParam creates a header parameter, this is always required by default 540 | func HeaderParam(name string, typ, format string) *spec.Parameter { 541 | param := spec.HeaderParam(name) 542 | param.Typed(typ, format) 543 | return param 544 | } 545 | 546 | // PathParam creates a path parameter, this is always required 547 | func PathParam(name string, typ, format string) *spec.Parameter { 548 | param := spec.PathParam(name) 549 | param.Typed(typ, format) 550 | return param 551 | } 552 | 553 | // BodyParam creates a body parameter 554 | func BodyParam(name string, schema *spec.Schema) *spec.Parameter { 555 | return &spec.Parameter{ParamProps: spec.ParamProps{Name: name, In: "body", Schema: schema}} 556 | } 557 | 558 | // NewResponse creates a new response instance 559 | func NewResponse(description string) *spec.Response { 560 | resp := new(spec.Response) 561 | resp.Description = description 562 | return resp 563 | } 564 | 565 | // NewBindResponse creates a new response instance 566 | func NewBindResponse(i interface{}, description string) *spec.Response { 567 | resp := new(spec.Response) 568 | resp.Description = description 569 | 570 | t := reflect.TypeOf(i) 571 | 572 | if t.Kind() == reflect.Ptr { 573 | t = t.Elem() 574 | } 575 | 576 | slice := false 577 | if t.Kind() == reflect.Slice { 578 | slice = true 579 | t = t.Elem() 580 | } 581 | 582 | if t.Kind() == reflect.Struct { 583 | schema := spec.RefSchema("#/definitions/" + t.Name()) 584 | if slice { 585 | resp.WithSchema(spec.ArrayProperty(schema)) 586 | } else { 587 | resp.WithSchema(schema) 588 | } 589 | } 590 | 591 | return resp 592 | } 593 | -------------------------------------------------------------------------------- /spring-web-swagger_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package SpringWeb_test 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "testing" 23 | 24 | "github.com/go-spring/go-spring-test" 25 | "github.com/go-spring/go-spring-utils" 26 | "github.com/go-spring/go-spring-web" 27 | ) 28 | 29 | func TestSwagger(t *testing.T) { 30 | 31 | t.Run("petstore.swagger.io", func(t *testing.T) { 32 | doc := `{ 33 | "swagger": "2.0", 34 | "info": { 35 | "description": "This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key ` + "`special-key`" + ` to test the authorization filters.", 36 | "version": "1.0.5", 37 | "title": "Swagger Petstore", 38 | "termsOfService": "http://swagger.io/terms/", 39 | "contact": { 40 | "email": "apiteam@swagger.io" 41 | }, 42 | "license": { 43 | "name": "Apache 2.0", 44 | "url": "http://www.apache.org/licenses/LICENSE-2.0.html" 45 | } 46 | }, 47 | "host": "petstore.swagger.io", 48 | "basePath": "/v2", 49 | "tags": [{ 50 | "name": "pet", 51 | "description": "Everything about your Pets", 52 | "externalDocs": { 53 | "description": "Find out more", 54 | "url": "http://swagger.io" 55 | } 56 | }, { 57 | "name": "store", 58 | "description": "Access to Petstore orders" 59 | }, { 60 | "name": "user", 61 | "description": "Operations about user", 62 | "externalDocs": { 63 | "description": "Find out more about our store", 64 | "url": "http://swagger.io" 65 | } 66 | }], 67 | "schemes": ["https", "http"], 68 | "paths": { 69 | "/pet/{petId}/uploadImage": { 70 | "post": { 71 | "tags": ["pet"], 72 | "summary": "uploads an image", 73 | "description": "", 74 | "operationId": "uploadFile", 75 | "consumes": ["multipart/form-data"], 76 | "produces": ["application/json"], 77 | "parameters": [{ 78 | "name": "petId", 79 | "in": "path", 80 | "description": "ID of pet to update", 81 | "required": true, 82 | "type": "integer", 83 | "format": "int64" 84 | }, { 85 | "name": "additionalMetadata", 86 | "in": "formData", 87 | "description": "Additional data to pass to server", 88 | "required": false, 89 | "type": "string" 90 | }, { 91 | "name": "file", 92 | "in": "formData", 93 | "description": "file to upload", 94 | "required": false, 95 | "type": "file" 96 | }], 97 | "responses": { 98 | "200": { 99 | "description": "successful operation", 100 | "schema": { 101 | "$ref": "#/definitions/ApiResponse" 102 | } 103 | } 104 | }, 105 | "security": [{ 106 | "petstore_auth": ["write:pets", "read:pets"] 107 | }] 108 | } 109 | }, 110 | "/pet": { 111 | "post": { 112 | "tags": ["pet"], 113 | "summary": "Add a new pet to the store", 114 | "description": "", 115 | "operationId": "addPet", 116 | "consumes": ["application/json", "application/xml"], 117 | "produces": ["application/json", "application/xml"], 118 | "parameters": [{ 119 | "in": "body", 120 | "name": "body", 121 | "description": "Pet object that needs to be added to the store", 122 | "required": true, 123 | "schema": { 124 | "$ref": "#/definitions/Pet" 125 | } 126 | }], 127 | "responses": { 128 | "405": { 129 | "description": "Invalid input" 130 | } 131 | }, 132 | "security": [{ 133 | "petstore_auth": ["write:pets", "read:pets"] 134 | }] 135 | }, 136 | "put": { 137 | "tags": ["pet"], 138 | "summary": "Update an existing pet", 139 | "description": "", 140 | "operationId": "updatePet", 141 | "consumes": ["application/json", "application/xml"], 142 | "produces": ["application/json", "application/xml"], 143 | "parameters": [{ 144 | "in": "body", 145 | "name": "body", 146 | "description": "Pet object that needs to be added to the store", 147 | "required": true, 148 | "schema": { 149 | "$ref": "#/definitions/Pet" 150 | } 151 | }], 152 | "responses": { 153 | "400": { 154 | "description": "Invalid ID supplied" 155 | }, 156 | "404": { 157 | "description": "Pet not found" 158 | }, 159 | "405": { 160 | "description": "Validation exception" 161 | } 162 | }, 163 | "security": [{ 164 | "petstore_auth": ["write:pets", "read:pets"] 165 | }] 166 | } 167 | }, 168 | "/pet/findByStatus": { 169 | "get": { 170 | "tags": ["pet"], 171 | "summary": "Finds Pets by status", 172 | "description": "Multiple status values can be provided with comma separated strings", 173 | "operationId": "findPetsByStatus", 174 | "produces": ["application/json", "application/xml"], 175 | "parameters": [{ 176 | "name": "status", 177 | "in": "query", 178 | "description": "Status values that need to be considered for filter", 179 | "required": true, 180 | "type": "array", 181 | "items": { 182 | "type": "string", 183 | "enum": ["available", "pending", "sold"], 184 | "default": "available" 185 | }, 186 | "collectionFormat": "multi" 187 | }], 188 | "responses": { 189 | "200": { 190 | "description": "successful operation", 191 | "schema": { 192 | "type": "array", 193 | "items": { 194 | "$ref": "#/definitions/Pet" 195 | } 196 | } 197 | }, 198 | "400": { 199 | "description": "Invalid status value" 200 | } 201 | }, 202 | "security": [{ 203 | "petstore_auth": ["write:pets", "read:pets"] 204 | }] 205 | } 206 | }, 207 | "/pet/findByTags": { 208 | "get": { 209 | "tags": ["pet"], 210 | "summary": "Finds Pets by tags", 211 | "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", 212 | "operationId": "findPetsByTags", 213 | "produces": ["application/json", "application/xml"], 214 | "parameters": [{ 215 | "name": "tags", 216 | "in": "query", 217 | "description": "Tags to filter by", 218 | "required": true, 219 | "type": "array", 220 | "items": { 221 | "type": "string" 222 | }, 223 | "collectionFormat": "multi" 224 | }], 225 | "responses": { 226 | "200": { 227 | "description": "successful operation", 228 | "schema": { 229 | "type": "array", 230 | "items": { 231 | "$ref": "#/definitions/Pet" 232 | } 233 | } 234 | }, 235 | "400": { 236 | "description": "Invalid tag value" 237 | } 238 | }, 239 | "security": [{ 240 | "petstore_auth": ["write:pets", "read:pets"] 241 | }], 242 | "deprecated": true 243 | } 244 | }, 245 | "/pet/{petId}": { 246 | "get": { 247 | "tags": ["pet"], 248 | "summary": "Find pet by ID", 249 | "description": "Returns a single pet", 250 | "operationId": "getPetById", 251 | "produces": ["application/json", "application/xml"], 252 | "parameters": [{ 253 | "name": "petId", 254 | "in": "path", 255 | "description": "ID of pet to return", 256 | "required": true, 257 | "type": "integer", 258 | "format": "int64" 259 | }], 260 | "responses": { 261 | "200": { 262 | "description": "successful operation", 263 | "schema": { 264 | "$ref": "#/definitions/Pet" 265 | } 266 | }, 267 | "400": { 268 | "description": "Invalid ID supplied" 269 | }, 270 | "404": { 271 | "description": "Pet not found" 272 | } 273 | }, 274 | "security": [{ 275 | "api_key": [] 276 | }] 277 | }, 278 | "post": { 279 | "tags": ["pet"], 280 | "summary": "Updates a pet in the store with form data", 281 | "description": "", 282 | "operationId": "updatePetWithForm", 283 | "consumes": ["application/x-www-form-urlencoded"], 284 | "produces": ["application/json", "application/xml"], 285 | "parameters": [{ 286 | "name": "petId", 287 | "in": "path", 288 | "description": "ID of pet that needs to be updated", 289 | "required": true, 290 | "type": "integer", 291 | "format": "int64" 292 | }, { 293 | "name": "name", 294 | "in": "formData", 295 | "description": "Updated name of the pet", 296 | "required": false, 297 | "type": "string" 298 | }, { 299 | "name": "status", 300 | "in": "formData", 301 | "description": "Updated status of the pet", 302 | "required": false, 303 | "type": "string" 304 | }], 305 | "responses": { 306 | "405": { 307 | "description": "Invalid input" 308 | } 309 | }, 310 | "security": [{ 311 | "petstore_auth": ["write:pets", "read:pets"] 312 | }] 313 | }, 314 | "delete": { 315 | "tags": ["pet"], 316 | "summary": "Deletes a pet", 317 | "description": "", 318 | "operationId": "deletePet", 319 | "produces": ["application/json", "application/xml"], 320 | "parameters": [{ 321 | "name": "api_key", 322 | "in": "header", 323 | "required": false, 324 | "type": "string" 325 | }, { 326 | "name": "petId", 327 | "in": "path", 328 | "description": "Pet id to delete", 329 | "required": true, 330 | "type": "integer", 331 | "format": "int64" 332 | }], 333 | "responses": { 334 | "400": { 335 | "description": "Invalid ID supplied" 336 | }, 337 | "404": { 338 | "description": "Pet not found" 339 | } 340 | }, 341 | "security": [{ 342 | "petstore_auth": ["write:pets", "read:pets"] 343 | }] 344 | } 345 | }, 346 | "/store/order": { 347 | "post": { 348 | "tags": ["store"], 349 | "summary": "Place an order for a pet", 350 | "description": "", 351 | "operationId": "placeOrder", 352 | "consumes": ["application/json"], 353 | "produces": ["application/json", "application/xml"], 354 | "parameters": [{ 355 | "in": "body", 356 | "name": "body", 357 | "description": "order placed for purchasing the pet", 358 | "required": true, 359 | "schema": { 360 | "$ref": "#/definitions/Order" 361 | } 362 | }], 363 | "responses": { 364 | "200": { 365 | "description": "successful operation", 366 | "schema": { 367 | "$ref": "#/definitions/Order" 368 | } 369 | }, 370 | "400": { 371 | "description": "Invalid Order" 372 | } 373 | } 374 | } 375 | }, 376 | "/store/order/{orderId}": { 377 | "get": { 378 | "tags": ["store"], 379 | "summary": "Find purchase order by ID", 380 | "description": "For valid response try integer IDs with value >= 1 and <= 10. Other values will generated exceptions", 381 | "operationId": "getOrderById", 382 | "produces": ["application/json", "application/xml"], 383 | "parameters": [{ 384 | "name": "orderId", 385 | "in": "path", 386 | "description": "ID of pet that needs to be fetched", 387 | "required": true, 388 | "type": "integer", 389 | "maximum": 10, 390 | "minimum": 1, 391 | "format": "int64" 392 | }], 393 | "responses": { 394 | "200": { 395 | "description": "successful operation", 396 | "schema": { 397 | "$ref": "#/definitions/Order" 398 | } 399 | }, 400 | "400": { 401 | "description": "Invalid ID supplied" 402 | }, 403 | "404": { 404 | "description": "Order not found" 405 | } 406 | } 407 | }, 408 | "delete": { 409 | "tags": ["store"], 410 | "summary": "Delete purchase order by ID", 411 | "description": "For valid response try integer IDs with positive integer value. Negative or non-integer values will generate API errors", 412 | "operationId": "deleteOrder", 413 | "produces": ["application/json", "application/xml"], 414 | "parameters": [{ 415 | "name": "orderId", 416 | "in": "path", 417 | "description": "ID of the order that needs to be deleted", 418 | "required": true, 419 | "type": "integer", 420 | "minimum": 1, 421 | "format": "int64" 422 | }], 423 | "responses": { 424 | "400": { 425 | "description": "Invalid ID supplied" 426 | }, 427 | "404": { 428 | "description": "Order not found" 429 | } 430 | } 431 | } 432 | }, 433 | "/store/inventory": { 434 | "get": { 435 | "tags": ["store"], 436 | "summary": "Returns pet inventories by status", 437 | "description": "Returns a map of status codes to quantities", 438 | "operationId": "getInventory", 439 | "produces": ["application/json"], 440 | "parameters": [], 441 | "responses": { 442 | "200": { 443 | "description": "successful operation", 444 | "schema": { 445 | "type": "object", 446 | "additionalProperties": { 447 | "type": "integer", 448 | "format": "int32" 449 | } 450 | } 451 | } 452 | }, 453 | "security": [{ 454 | "api_key": [] 455 | }] 456 | } 457 | }, 458 | "/user/createWithArray": { 459 | "post": { 460 | "tags": ["user"], 461 | "summary": "Creates list of users with given input array", 462 | "description": "", 463 | "operationId": "createUsersWithArrayInput", 464 | "consumes": ["application/json"], 465 | "produces": ["application/json", "application/xml"], 466 | "parameters": [{ 467 | "in": "body", 468 | "name": "body", 469 | "description": "List of user object", 470 | "required": true, 471 | "schema": { 472 | "type": "array", 473 | "items": { 474 | "$ref": "#/definitions/User" 475 | } 476 | } 477 | }], 478 | "responses": { 479 | "default": { 480 | "description": "successful operation" 481 | } 482 | } 483 | } 484 | }, 485 | "/user/createWithList": { 486 | "post": { 487 | "tags": ["user"], 488 | "summary": "Creates list of users with given input array", 489 | "description": "", 490 | "operationId": "createUsersWithListInput", 491 | "consumes": ["application/json"], 492 | "produces": ["application/json", "application/xml"], 493 | "parameters": [{ 494 | "in": "body", 495 | "name": "body", 496 | "description": "List of user object", 497 | "required": true, 498 | "schema": { 499 | "type": "array", 500 | "items": { 501 | "$ref": "#/definitions/User" 502 | } 503 | } 504 | }], 505 | "responses": { 506 | "default": { 507 | "description": "successful operation" 508 | } 509 | } 510 | } 511 | }, 512 | "/user/{username}": { 513 | "get": { 514 | "tags": ["user"], 515 | "summary": "Get user by user name", 516 | "description": "", 517 | "operationId": "getUserByName", 518 | "produces": ["application/json", "application/xml"], 519 | "parameters": [{ 520 | "name": "username", 521 | "in": "path", 522 | "description": "The name that needs to be fetched. Use user1 for testing. ", 523 | "required": true, 524 | "type": "string" 525 | }], 526 | "responses": { 527 | "200": { 528 | "description": "successful operation", 529 | "schema": { 530 | "$ref": "#/definitions/User" 531 | } 532 | }, 533 | "400": { 534 | "description": "Invalid username supplied" 535 | }, 536 | "404": { 537 | "description": "User not found" 538 | } 539 | } 540 | }, 541 | "put": { 542 | "tags": ["user"], 543 | "summary": "Updated user", 544 | "description": "This can only be done by the logged in user.", 545 | "operationId": "updateUser", 546 | "consumes": ["application/json"], 547 | "produces": ["application/json", "application/xml"], 548 | "parameters": [{ 549 | "name": "username", 550 | "in": "path", 551 | "description": "name that need to be updated", 552 | "required": true, 553 | "type": "string" 554 | }, { 555 | "in": "body", 556 | "name": "body", 557 | "description": "Updated user object", 558 | "required": true, 559 | "schema": { 560 | "$ref": "#/definitions/User" 561 | } 562 | }], 563 | "responses": { 564 | "400": { 565 | "description": "Invalid user supplied" 566 | }, 567 | "404": { 568 | "description": "User not found" 569 | } 570 | } 571 | }, 572 | "delete": { 573 | "tags": ["user"], 574 | "summary": "Delete user", 575 | "description": "This can only be done by the logged in user.", 576 | "operationId": "deleteUser", 577 | "produces": ["application/json", "application/xml"], 578 | "parameters": [{ 579 | "name": "username", 580 | "in": "path", 581 | "description": "The name that needs to be deleted", 582 | "required": true, 583 | "type": "string" 584 | }], 585 | "responses": { 586 | "400": { 587 | "description": "Invalid username supplied" 588 | }, 589 | "404": { 590 | "description": "User not found" 591 | } 592 | } 593 | } 594 | }, 595 | "/user/login": { 596 | "get": { 597 | "tags": ["user"], 598 | "summary": "Logs user into the system", 599 | "description": "", 600 | "operationId": "loginUser", 601 | "produces": ["application/json", "application/xml"], 602 | "parameters": [{ 603 | "name": "username", 604 | "in": "query", 605 | "description": "The user name for login", 606 | "required": true, 607 | "type": "string" 608 | }, { 609 | "name": "password", 610 | "in": "query", 611 | "description": "The password for login in clear text", 612 | "required": true, 613 | "type": "string" 614 | }], 615 | "responses": { 616 | "200": { 617 | "description": "successful operation", 618 | "headers": { 619 | "X-Expires-After": { 620 | "type": "string", 621 | "format": "date-time", 622 | "description": "date in UTC when token expires" 623 | }, 624 | "X-Rate-Limit": { 625 | "type": "integer", 626 | "format": "int32", 627 | "description": "calls per hour allowed by the user" 628 | } 629 | }, 630 | "schema": { 631 | "type": "string" 632 | } 633 | }, 634 | "400": { 635 | "description": "Invalid username/password supplied" 636 | } 637 | } 638 | } 639 | }, 640 | "/user/logout": { 641 | "get": { 642 | "tags": ["user"], 643 | "summary": "Logs out current logged in user session", 644 | "description": "", 645 | "operationId": "logoutUser", 646 | "produces": ["application/json", "application/xml"], 647 | "parameters": [], 648 | "responses": { 649 | "default": { 650 | "description": "successful operation" 651 | } 652 | } 653 | } 654 | }, 655 | "/user": { 656 | "post": { 657 | "tags": ["user"], 658 | "summary": "Create user", 659 | "description": "This can only be done by the logged in user.", 660 | "operationId": "createUser", 661 | "consumes": ["application/json"], 662 | "produces": ["application/json", "application/xml"], 663 | "parameters": [{ 664 | "in": "body", 665 | "name": "body", 666 | "description": "Created user object", 667 | "required": true, 668 | "schema": { 669 | "$ref": "#/definitions/User" 670 | } 671 | }], 672 | "responses": { 673 | "default": { 674 | "description": "successful operation" 675 | } 676 | } 677 | } 678 | } 679 | }, 680 | "securityDefinitions": { 681 | "api_key": { 682 | "type": "apiKey", 683 | "name": "api_key", 684 | "in": "header" 685 | }, 686 | "petstore_auth": { 687 | "type": "oauth2", 688 | "authorizationUrl": "https://petstore.swagger.io/oauth/authorize", 689 | "flow": "implicit", 690 | "scopes": { 691 | "read:pets": "read your pets", 692 | "write:pets": "modify pets in your account" 693 | } 694 | } 695 | }, 696 | "definitions": { 697 | "ApiResponse": { 698 | "type": "object", 699 | "properties": { 700 | "code": { 701 | "type": "integer", 702 | "format": "int32" 703 | }, 704 | "type": { 705 | "type": "string" 706 | }, 707 | "message": { 708 | "type": "string" 709 | } 710 | } 711 | }, 712 | "Category": { 713 | "type": "object", 714 | "properties": { 715 | "id": { 716 | "type": "integer", 717 | "format": "int64" 718 | }, 719 | "name": { 720 | "type": "string" 721 | } 722 | }, 723 | "xml": { 724 | "name": "Category" 725 | } 726 | }, 727 | "Pet": { 728 | "type": "object", 729 | "required": ["name", "photoUrls"], 730 | "properties": { 731 | "id": { 732 | "type": "integer", 733 | "format": "int64" 734 | }, 735 | "category": { 736 | "$ref": "#/definitions/Category" 737 | }, 738 | "name": { 739 | "type": "string", 740 | "example": "doggie" 741 | }, 742 | "photoUrls": { 743 | "type": "array", 744 | "xml": { 745 | "wrapped": true 746 | }, 747 | "items": { 748 | "type": "string", 749 | "xml": { 750 | "name": "photoUrl" 751 | } 752 | } 753 | }, 754 | "tags": { 755 | "type": "array", 756 | "xml": { 757 | "wrapped": true 758 | }, 759 | "items": { 760 | "xml": { 761 | "name": "tag" 762 | }, 763 | "$ref": "#/definitions/Tag" 764 | } 765 | }, 766 | "status": { 767 | "type": "string", 768 | "description": "pet status in the store", 769 | "enum": ["available", "pending", "sold"] 770 | } 771 | }, 772 | "xml": { 773 | "name": "Pet" 774 | } 775 | }, 776 | "Tag": { 777 | "type": "object", 778 | "properties": { 779 | "id": { 780 | "type": "integer", 781 | "format": "int64" 782 | }, 783 | "name": { 784 | "type": "string" 785 | } 786 | }, 787 | "xml": { 788 | "name": "Tag" 789 | } 790 | }, 791 | "Order": { 792 | "type": "object", 793 | "properties": { 794 | "id": { 795 | "type": "integer", 796 | "format": "int64" 797 | }, 798 | "petId": { 799 | "type": "integer", 800 | "format": "int64" 801 | }, 802 | "quantity": { 803 | "type": "integer", 804 | "format": "int32" 805 | }, 806 | "shipDate": { 807 | "type": "string", 808 | "format": "date-time" 809 | }, 810 | "status": { 811 | "type": "string", 812 | "description": "Order Status", 813 | "enum": ["placed", "approved", "delivered"] 814 | }, 815 | "complete": { 816 | "type": "boolean" 817 | } 818 | }, 819 | "xml": { 820 | "name": "Order" 821 | } 822 | }, 823 | "User": { 824 | "type": "object", 825 | "properties": { 826 | "id": { 827 | "type": "integer", 828 | "format": "int64" 829 | }, 830 | "username": { 831 | "type": "string" 832 | }, 833 | "firstName": { 834 | "type": "string" 835 | }, 836 | "lastName": { 837 | "type": "string" 838 | }, 839 | "email": { 840 | "type": "string" 841 | }, 842 | "password": { 843 | "type": "string" 844 | }, 845 | "phone": { 846 | "type": "string" 847 | }, 848 | "userStatus": { 849 | "type": "integer", 850 | "format": "int32", 851 | "description": "User Status" 852 | } 853 | }, 854 | "xml": { 855 | "name": "User" 856 | } 857 | } 858 | }, 859 | "externalDocs": { 860 | "description": "Find out more about Swagger", 861 | "url": "http://swagger.io" 862 | } 863 | }` 864 | 865 | sw := SpringWeb.NewSwagger() 866 | _ = json.Unmarshal([]byte(doc), &sw) 867 | 868 | str := SpringUtils.ToJson(sw) 869 | fmt.Println(str) 870 | 871 | var m1 map[string]interface{} 872 | _ = json.Unmarshal([]byte(str), &m1) 873 | 874 | var m2 map[string]interface{} 875 | _ = json.Unmarshal([]byte(doc), &m2) 876 | 877 | SpringTest.DiffMap(t, m1, m2) 878 | }) 879 | } 880 | -------------------------------------------------------------------------------- /spring-web-url.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package SpringWeb 18 | 19 | import ( 20 | "errors" 21 | "strings" 22 | ) 23 | 24 | // 路由风格有 echo、gin 和 {} 三种, 25 | // /a/:b/c/:d/* 这种是 echo 风格; 26 | // /a/:b/c/:d/*e 这种是 gin 风格; 27 | // /a/{b}/c/{e:*} 这种是 {} 风格; 28 | // /a/{b}/c/{*:e} 这也是 {} 风格; 29 | // /a/{b}/c/{*} 这种也是 {} 风格。 30 | 31 | type PathStyleEnum int 32 | 33 | const ( 34 | EchoPathStyle = PathStyleEnum(0) 35 | GinPathStyle = PathStyleEnum(1) 36 | JavaPathStyle = PathStyleEnum(2) 37 | ) 38 | 39 | // DefaultWildCardName 默认统配符的名称 40 | const DefaultWildCardName = "@_@" 41 | 42 | // pathStyle URL 地址风格 43 | type pathStyle interface { 44 | addKnownPath(path string) 45 | addNamedPath(path string) 46 | addWildCard(name string) 47 | wildCardName() string 48 | String() string 49 | } 50 | 51 | type basePathStyle struct { 52 | s strings.Builder 53 | w string // 通配符的名称 54 | } 55 | 56 | func (p *basePathStyle) wildCardName() string { 57 | return p.w 58 | } 59 | 60 | func (p *basePathStyle) String() string { 61 | return p.s.String() 62 | } 63 | 64 | // echoPathStyle Echo 地址风格 65 | type echoPathStyle struct { 66 | basePathStyle 67 | } 68 | 69 | func (p *echoPathStyle) addKnownPath(path string) { 70 | p.s.WriteString("/" + path) 71 | } 72 | 73 | func (p *echoPathStyle) addNamedPath(path string) { 74 | p.s.WriteString("/:" + path) 75 | } 76 | 77 | func (p *echoPathStyle) addWildCard(name string) { 78 | p.s.WriteString("/*") 79 | p.w = name 80 | } 81 | 82 | // ginPathStyle Gin 地址风格 83 | type ginPathStyle struct { 84 | basePathStyle 85 | } 86 | 87 | func (p *ginPathStyle) addKnownPath(path string) { 88 | p.s.WriteString("/" + path) 89 | } 90 | 91 | func (p *ginPathStyle) addNamedPath(path string) { 92 | p.s.WriteString("/:" + path) 93 | } 94 | 95 | func (p *ginPathStyle) addWildCard(name string) { 96 | if name == "" { // gin 的路由需要指定一个名称 97 | name = DefaultWildCardName 98 | } 99 | p.s.WriteString("/*" + name) 100 | p.w = name 101 | } 102 | 103 | // javaPathStyle {} 地址风格 104 | type javaPathStyle struct { 105 | basePathStyle 106 | } 107 | 108 | func (p *javaPathStyle) addKnownPath(path string) { 109 | p.s.WriteString("/" + path) 110 | } 111 | 112 | func (p *javaPathStyle) addNamedPath(path string) { 113 | p.s.WriteString("/{" + path + "}") 114 | } 115 | 116 | func (p *javaPathStyle) addWildCard(name string) { 117 | if name != "" { 118 | p.s.WriteString("/{*:" + name + "}") 119 | } else { 120 | p.s.WriteString("/{*}") 121 | } 122 | p.w = name 123 | } 124 | 125 | // ToPathStyle 将 URL 转换为指定风格的表示形式 126 | func ToPathStyle(path string, style PathStyleEnum) (string, string) { 127 | 128 | var p pathStyle 129 | switch style { 130 | case EchoPathStyle: 131 | p = &echoPathStyle{} 132 | case GinPathStyle: 133 | p = &ginPathStyle{} 134 | case JavaPathStyle: 135 | p = &javaPathStyle{} 136 | default: 137 | panic(errors.New("error path style")) 138 | } 139 | 140 | // 去掉开始的 / 字符,后面好计算 141 | if path[0] == '/' { 142 | path = path[1:] 143 | } 144 | 145 | for _, s := range strings.Split(path, "/") { 146 | 147 | // 尾部的 '/' 特殊处理 148 | if len(s) == 0 { 149 | p.addKnownPath(s) 150 | continue 151 | } 152 | 153 | switch s[0] { 154 | case '{': 155 | if s[len(s)-1] != '}' { 156 | panic(errors.New("error url path")) 157 | } 158 | if ss := strings.Split(s[1:len(s)-1], ":"); len(ss) > 1 { 159 | if ss[0] == "*" { 160 | p.addWildCard(ss[1]) 161 | } else if ss[1] == "*" { 162 | p.addWildCard(ss[0]) 163 | } else { 164 | panic(errors.New("error url path")) 165 | } 166 | } else if s[1] == '*' { 167 | p.addWildCard(s[2 : len(s)-1]) 168 | } else { 169 | p.addNamedPath(s[1 : len(s)-1]) 170 | } 171 | case '*': 172 | if s == "*" { 173 | p.addWildCard("") 174 | } else { 175 | p.addWildCard(s[1:]) 176 | } 177 | case ':': 178 | p.addNamedPath(s[1:]) 179 | default: 180 | p.addKnownPath(s) 181 | } 182 | } 183 | return p.String(), p.wildCardName() 184 | } 185 | -------------------------------------------------------------------------------- /spring-web-url_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package SpringWeb_test 18 | 19 | import ( 20 | "testing" 21 | 22 | "github.com/go-spring/go-spring-web" 23 | "github.com/magiconair/properties/assert" 24 | ) 25 | 26 | func TestToPathStyle(t *testing.T) { 27 | 28 | t.Run("/:a", func(t *testing.T) { 29 | newPath, wildCardName := SpringWeb.ToPathStyle("/:a", SpringWeb.EchoPathStyle) 30 | assert.Equal(t, "/:a", newPath) 31 | assert.Equal(t, "", wildCardName) 32 | newPath, wildCardName = SpringWeb.ToPathStyle("/:a", SpringWeb.GinPathStyle) 33 | assert.Equal(t, "/:a", newPath) 34 | assert.Equal(t, "", wildCardName) 35 | newPath, wildCardName = SpringWeb.ToPathStyle("/:a", SpringWeb.JavaPathStyle) 36 | assert.Equal(t, "/{a}", newPath) 37 | assert.Equal(t, "", wildCardName) 38 | }) 39 | 40 | t.Run("/{a}", func(t *testing.T) { 41 | newPath, wildCardName := SpringWeb.ToPathStyle("/{a}", SpringWeb.EchoPathStyle) 42 | assert.Equal(t, "/:a", newPath) 43 | assert.Equal(t, "", wildCardName) 44 | newPath, wildCardName = SpringWeb.ToPathStyle("/{a}", SpringWeb.GinPathStyle) 45 | assert.Equal(t, "/:a", newPath) 46 | assert.Equal(t, "", wildCardName) 47 | newPath, wildCardName = SpringWeb.ToPathStyle("/{a}", SpringWeb.JavaPathStyle) 48 | assert.Equal(t, "/{a}", newPath) 49 | assert.Equal(t, "", wildCardName) 50 | }) 51 | 52 | t.Run("/:a/b/:c", func(t *testing.T) { 53 | newPath, wildCardName := SpringWeb.ToPathStyle("/:a/b/:c", SpringWeb.EchoPathStyle) 54 | assert.Equal(t, "/:a/b/:c", newPath) 55 | assert.Equal(t, "", wildCardName) 56 | newPath, wildCardName = SpringWeb.ToPathStyle("/:a/b/:c", SpringWeb.GinPathStyle) 57 | assert.Equal(t, "/:a/b/:c", newPath) 58 | assert.Equal(t, "", wildCardName) 59 | newPath, wildCardName = SpringWeb.ToPathStyle("/:a/b/:c", SpringWeb.JavaPathStyle) 60 | assert.Equal(t, "/{a}/b/{c}", newPath) 61 | assert.Equal(t, "", wildCardName) 62 | }) 63 | 64 | t.Run("/{a}/b/{c}", func(t *testing.T) { 65 | newPath, wildCardName := SpringWeb.ToPathStyle("/{a}/b/{c}", SpringWeb.EchoPathStyle) 66 | assert.Equal(t, "/:a/b/:c", newPath) 67 | assert.Equal(t, "", wildCardName) 68 | newPath, wildCardName = SpringWeb.ToPathStyle("/{a}/b/{c}", SpringWeb.GinPathStyle) 69 | assert.Equal(t, "/:a/b/:c", newPath) 70 | assert.Equal(t, "", wildCardName) 71 | newPath, wildCardName = SpringWeb.ToPathStyle("/{a}/b/{c}", SpringWeb.JavaPathStyle) 72 | assert.Equal(t, "/{a}/b/{c}", newPath) 73 | assert.Equal(t, "", wildCardName) 74 | }) 75 | 76 | t.Run("/:a/b/:c/*", func(t *testing.T) { 77 | newPath, wildCardName := SpringWeb.ToPathStyle("/:a/b/:c/*", SpringWeb.EchoPathStyle) 78 | assert.Equal(t, "/:a/b/:c/*", newPath) 79 | assert.Equal(t, "", wildCardName) 80 | newPath, wildCardName = SpringWeb.ToPathStyle("/:a/b/:c/*", SpringWeb.GinPathStyle) 81 | assert.Equal(t, "/:a/b/:c/*@_@", newPath) 82 | assert.Equal(t, "@_@", wildCardName) 83 | newPath, wildCardName = SpringWeb.ToPathStyle("/:a/b/:c/*", SpringWeb.JavaPathStyle) 84 | assert.Equal(t, "/{a}/b/{c}/{*}", newPath) 85 | assert.Equal(t, "", wildCardName) 86 | }) 87 | 88 | t.Run("/{a}/b/{c}/{*}", func(t *testing.T) { 89 | newPath, wildCardName := SpringWeb.ToPathStyle("/{a}/b/{c}/{*}", SpringWeb.EchoPathStyle) 90 | assert.Equal(t, "/:a/b/:c/*", newPath) 91 | assert.Equal(t, "", wildCardName) 92 | newPath, wildCardName = SpringWeb.ToPathStyle("/{a}/b/{c}/{*}", SpringWeb.GinPathStyle) 93 | assert.Equal(t, "/:a/b/:c/*@_@", newPath) 94 | assert.Equal(t, "@_@", wildCardName) 95 | newPath, wildCardName = SpringWeb.ToPathStyle("/{a}/b/{c}/{*}", SpringWeb.JavaPathStyle) 96 | assert.Equal(t, "/{a}/b/{c}/{*}", newPath) 97 | assert.Equal(t, "", wildCardName) 98 | }) 99 | 100 | t.Run("/:a/b/:c/*e", func(t *testing.T) { 101 | newPath, wildCardName := SpringWeb.ToPathStyle("/:a/b/:c/*e", SpringWeb.EchoPathStyle) 102 | assert.Equal(t, "/:a/b/:c/*", newPath) 103 | assert.Equal(t, "e", wildCardName) 104 | newPath, wildCardName = SpringWeb.ToPathStyle("/:a/b/:c/*e", SpringWeb.GinPathStyle) 105 | assert.Equal(t, "/:a/b/:c/*e", newPath) 106 | assert.Equal(t, "e", wildCardName) 107 | newPath, wildCardName = SpringWeb.ToPathStyle("/:a/b/:c/*e", SpringWeb.JavaPathStyle) 108 | assert.Equal(t, "/{a}/b/{c}/{*:e}", newPath) 109 | assert.Equal(t, "e", wildCardName) 110 | }) 111 | 112 | t.Run("/{a}/b/{c}/{*:e}", func(t *testing.T) { 113 | newPath, wildCardName := SpringWeb.ToPathStyle("/{a}/b/{c}/{*:e}", SpringWeb.EchoPathStyle) 114 | assert.Equal(t, "/:a/b/:c/*", newPath) 115 | assert.Equal(t, "e", wildCardName) 116 | newPath, wildCardName = SpringWeb.ToPathStyle("/{a}/b/{c}/{*:e}", SpringWeb.GinPathStyle) 117 | assert.Equal(t, "/:a/b/:c/*e", newPath) 118 | assert.Equal(t, "e", wildCardName) 119 | newPath, wildCardName = SpringWeb.ToPathStyle("/{a}/b/{c}/{*:e}", SpringWeb.JavaPathStyle) 120 | assert.Equal(t, "/{a}/b/{c}/{*:e}", newPath) 121 | assert.Equal(t, "e", wildCardName) 122 | }) 123 | 124 | t.Run("/{a}/b/{c}/{e:*}", func(t *testing.T) { 125 | newPath, wildCardName := SpringWeb.ToPathStyle("/{a}/b/{c}/{e:*}", SpringWeb.EchoPathStyle) 126 | assert.Equal(t, "/:a/b/:c/*", newPath) 127 | assert.Equal(t, "e", wildCardName) 128 | newPath, wildCardName = SpringWeb.ToPathStyle("/{a}/b/{c}/{e:*}", SpringWeb.GinPathStyle) 129 | assert.Equal(t, "/:a/b/:c/*e", newPath) 130 | assert.Equal(t, "e", wildCardName) 131 | newPath, wildCardName = SpringWeb.ToPathStyle("/{a}/b/{c}/{e:*}", SpringWeb.JavaPathStyle) 132 | assert.Equal(t, "/{a}/b/{c}/{*:e}", newPath) 133 | assert.Equal(t, "e", wildCardName) 134 | }) 135 | } 136 | -------------------------------------------------------------------------------- /spring-web-validator.go: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2019 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package SpringWeb 18 | 19 | import ( 20 | "reflect" 21 | 22 | "github.com/go-playground/validator/v10" 23 | "github.com/go-spring/go-spring-utils" 24 | ) 25 | 26 | // WebValidator 适配 gin 和 echo 的校验器接口 27 | type WebValidator interface { 28 | Engine() interface{} 29 | Validate(i interface{}) error 30 | ValidateStruct(i interface{}) error 31 | } 32 | 33 | // Validator 全局参数校验器,没有啥好办法不做成全局变量 34 | var Validator WebValidator = NewBuiltInValidator() 35 | 36 | // BuiltInValidator 内置的参数校验器 37 | type BuiltInValidator struct { 38 | validator *validator.Validate 39 | } 40 | 41 | // NewBuiltInValidator BuiltInValidator 的构造函数 42 | func NewBuiltInValidator() *BuiltInValidator { 43 | return &BuiltInValidator{validator: validator.New()} 44 | } 45 | 46 | func (v *BuiltInValidator) Engine() interface{} { 47 | return v.validator 48 | } 49 | 50 | // Validate echo 的参数校验接口 51 | func (v *BuiltInValidator) Validate(i interface{}) error { 52 | return v.validateStruct(i) 53 | } 54 | 55 | // ValidateStruct gin 的参数校验接口 56 | func (v *BuiltInValidator) ValidateStruct(i interface{}) error { 57 | return v.validateStruct(i) 58 | } 59 | 60 | // validateStruct receives any kind of type, but only performed struct or pointer to struct type. 61 | func (v *BuiltInValidator) validateStruct(i interface{}) error { 62 | if SpringUtils.Indirect(reflect.TypeOf(i)).Kind() == reflect.Struct { 63 | if err := v.validator.Struct(i); err != nil { 64 | return err 65 | } 66 | } 67 | return nil 68 | } 69 | --------------------------------------------------------------------------------