├── .gitignore ├── LICENSE ├── README.md ├── api.go ├── error.go ├── example ├── README.md ├── go.mod ├── go.sum └── main.go ├── go.mod ├── go.sum ├── init.go ├── resource.go └── utils.go /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/go,linux,jetbrains+all 3 | # Edit at https://www.gitignore.io/?templates=go,linux,jetbrains+all 4 | 5 | ### Go ### 6 | # Binaries for programs and plugins 7 | *.exe 8 | *.exe~ 9 | *.dll 10 | *.so 11 | *.dylib 12 | 13 | # Test binary, built with `go test -c` 14 | *.test 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | 19 | ### Go Patch ### 20 | /vendor/ 21 | /Godeps/ 22 | 23 | ### JetBrains+all ### 24 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 25 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 26 | 27 | # User-specific stuff 28 | .idea/**/workspace.xml 29 | .idea/**/tasks.xml 30 | .idea/**/usage.statistics.xml 31 | .idea/**/dictionaries 32 | .idea/**/shelf 33 | 34 | # Generated files 35 | .idea/**/contentModel.xml 36 | 37 | # Sensitive or high-churn files 38 | .idea/**/dataSources/ 39 | .idea/**/dataSources.ids 40 | .idea/**/dataSources.local.xml 41 | .idea/**/sqlDataSources.xml 42 | .idea/**/dynamic.xml 43 | .idea/**/uiDesigner.xml 44 | .idea/**/dbnavigator.xml 45 | 46 | # Gradle 47 | .idea/**/gradle.xml 48 | .idea/**/libraries 49 | 50 | # Gradle and Maven with auto-import 51 | # When using Gradle or Maven with auto-import, you should exclude module files, 52 | # since they will be recreated, and may cause churn. Uncomment if using 53 | # auto-import. 54 | # .idea/modules.xml 55 | # .idea/*.iml 56 | # .idea/modules 57 | 58 | # CMake 59 | cmake-build-*/ 60 | 61 | # Mongo Explorer plugin 62 | .idea/**/mongoSettings.xml 63 | 64 | # File-based project format 65 | *.iws 66 | 67 | # IntelliJ 68 | out/ 69 | 70 | # mpeltonen/sbt-idea plugin 71 | .idea_modules/ 72 | 73 | # JIRA plugin 74 | atlassian-ide-plugin.xml 75 | 76 | # Cursive Clojure plugin 77 | .idea/replstate.xml 78 | 79 | # Crashlytics plugin (for Android Studio and IntelliJ) 80 | com_crashlytics_export_strings.xml 81 | crashlytics.properties 82 | crashlytics-build.properties 83 | fabric.properties 84 | 85 | # Editor-based Rest Client 86 | .idea/httpRequests 87 | 88 | # Android studio 3.1+ serialized cache file 89 | .idea/caches/build_file_checksums.ser 90 | 91 | ### JetBrains+all Patch ### 92 | # Ignores the whole .idea folder and all .iml files 93 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 94 | 95 | .idea/ 96 | 97 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 98 | 99 | *.iml 100 | modules.xml 101 | .idea/misc.xml 102 | *.ipr 103 | 104 | # Sonarlint plugin 105 | .idea/sonarlint 106 | 107 | ### Linux ### 108 | *~ 109 | 110 | # temporary files which can be created if a process still has a handle open of a deleted file 111 | .fuse_hidden* 112 | 113 | # KDE directory preferences 114 | .directory 115 | 116 | # Linux trash folder which might appear on any partition or disk 117 | .Trash-* 118 | 119 | # .nfs files are created when an open file is removed but is still being accessed 120 | .nfs* 121 | 122 | # End of https://www.gitignore.io/api/go,linux,jetbrains+all 123 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 mocha 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gin-restful 2 | gin-restful은 gin으로 restful api를 편하게 개발하기 위해 만든 라이브러리입니다. 3 | Api에 Resource를 등록하는 형태로 restful api 를 개발할 수 있습니다. 4 | 문서나 주석은 영어를 잘 하지 못해서 한글로 작성했습니다. 5 | 6 | ## 개요 7 | gin을 이용한 더 편한 restful api 개발을 위해 8 | Api에 Resource를 등록시키는 형태로 개발할 수 있게 만들었습니다. 9 | Resource는 어떤 구조체도 될 수 있으며 url이 호출되었을 때 10 | http method와 이름이 같은 메서드가 호출 됩니다. 11 | Resource를 등록만 하면 자동으로 Handler Method의 인자를 12 | 분석하여 url를 만들어 gin에 등록합니다. 13 | 14 | ## 예시 15 | ```go 16 | package main 17 | 18 | import ( 19 | "github.com/gin-gonic/gin" 20 | "github.com/hwangseonu/gin-restful" 21 | "net/http" 22 | ) 23 | 24 | //restful api 를 구현하기 위한 SampleResource 구조체입니다. 25 | //gin-restful 의 Resource 구조체 포인터를 임베딩합니다. 26 | //각 Handler 마다 다른 Middleware 들을 적용할 수 있습니다. 27 | type SampleResource struct { 28 | *gin_restful.Resource 29 | } 30 | 31 | //Json Body 를 테스트하기 위한 구조체입니다. 32 | type Data struct { 33 | Name string `json:"name" validate:"required,notblank"` 34 | } 35 | 36 | //SampleResource 의 Url 로 GET 요청이 들어왔을 때 실행되는 Handler 입니다. 37 | //gin.H(json) 와 status code 를 반환합니다. 38 | //path variable 인 name 을 json 에 담아 반환합니다. 39 | func (r SampleResource) Get(name string) (gin.H, int) { 40 | return gin.H{ 41 | "name": name, 42 | }, http.StatusOK 43 | } 44 | 45 | //SampleResource 의 Url 로 POST 요청이 들어왔을 때 실행되는 Handler 입니다. 46 | //json body 를 Data 구조체로 받습니다. 47 | //gin.H 과 status code 를 반환합니다. 48 | //요청으로 받은 payload 를 그대로 반환합니다. 49 | func (r SampleResource) Post(c *gin.Context, json Data) (Data, int) { 50 | return json, 200 51 | } 52 | 53 | //Middleware 테스트용 Sample Middleware 입니다. 54 | //콘솔에 "Hello, World" 를 출력합니다. 55 | func SampleMiddleware(c *gin.Context) { 56 | println("Hello, World") 57 | } 58 | 59 | //Api 인스턴스를 "/" 주소로 생성합니다. 60 | //SampleResource 의 인스턴스를 생성하여 Api "/samples" 주소로 등록합니다. 61 | //SampleResource GET handler 에 SampleMiddleware 를 등록합니다. 62 | //gin 서버를 5000 포트에서 실행합니다. 63 | func main() { 64 | r := gin.Default() 65 | v1 := gin_restful.NewApi(r, "/") 66 | res := SampleResource{gin_restful.InitResource()} 67 | res.AddMiddleware(SampleMiddleware, http.MethodGet) 68 | v1.AddResource(res, "/samples") 69 | _ = r.Run(":5000") 70 | } 71 | 72 | ``` -------------------------------------------------------------------------------- /api.go: -------------------------------------------------------------------------------- 1 | //gin_restful 은 gin 을 이용한 restful api 를 간편하게 만들기 위한 extension 입니다. 2 | //go 언어로 restful api 를 더 편하게 만들고 싶어 개발하였습니다. 3 | package gin_restful 4 | 5 | import ( 6 | "github.com/gin-gonic/gin" 7 | "reflect" 8 | "strings" 9 | ) 10 | 11 | //Api 구조체는 Resource 인스턴스들을 관리하고 gin 서버에 등록하기 위한 구조체입니다. 12 | //NewApi 함수로 인스턴스를 생성하여 사용합니다. 13 | //AddResource 함수로 Api 인스턴스에 Resource 를 등록할 수 있습니다. 14 | type Api struct { 15 | App *gin.RouterGroup 16 | Prefix string 17 | Resources map[string]interface{} 18 | } 19 | 20 | //Api 구조체의 인스턴스를 인스턴스를 생성하여 포인터로 반환하는 함수입니다. 21 | //첫번째 인자 app(type: *gin.Engine)은 Resource 를 등록 할 gin 서버의 인스턴스입니다. 22 | //두번째 인자 prefix(type string)은 api url 의 제일 앞 부분에 붙습니다. 23 | func NewApi(app *gin.Engine, prefix string) *Api { 24 | return &Api{ 25 | App: app.Group(prefix), 26 | Prefix: prefix, 27 | Resources: make(map[string]interface{}), 28 | } 29 | } 30 | 31 | //Api 인스턴스에 새로운 Resource 를 등록하는 메서드입니다. 32 | //Api 의 필드 App 이 nil 이 아니라면 gin 서버에 즉시 등록하고 nil 이라면 Api 구조체에 잠시 저장합니다. 33 | //서버에 등록할 때는 Resource 의 메서드 중 http method 인 것을 찾아 인자를 파싱하여 url 을 생성합니다. 34 | //요청을 받았을때 메서드가 실행되며 각각의 인자는 자동으로 채워집니다. 35 | //string, int, float, bool 타입의 인자는 url 에서 파싱하여 전달합니다. 36 | //*gin.Context 타입의 인자는 해당 요청의 context 로 채워집니다. 37 | //구조체 타입의 인자는 하나만 존재할 수 있으며 요청의 body 를 파싱하여 채워집니다. 38 | func (a *Api) AddResource(resource interface{}, url string) { 39 | if a.App != nil { 40 | a.registerResource(resource, url) 41 | } else { 42 | a.Resources[url] = resource 43 | } 44 | } 45 | 46 | //Api 인스턴스에 등록된 Resource 의 Handler 들을 gin.HandlerChain 타입으로 반환하는 메서드입니다. 47 | func (a *Api) GetHandlersChain() gin.HandlersChain { 48 | result := make([]gin.HandlerFunc, 0) 49 | for _, v := range a.Resources { 50 | for i := 0; i < reflect.TypeOf(v).NumMethod(); i++ { 51 | value := reflect.ValueOf(v) 52 | method := reflect.TypeOf(v).Method(i) 53 | if !isHttpMethod(method.Name) { 54 | continue 55 | } 56 | args := parseArgs(method) 57 | result = append(result, createHandlerFunc(value, method, args)) 58 | } 59 | } 60 | return result 61 | } 62 | 63 | //새로운 gin 서버를 Api 에 등록하고 Api 에 저장되어있던 Resource 들을 gin 서버에 등록시키는 메서드입니다. 64 | func (a *Api) InitApp(e *gin.Engine) { 65 | a.App = e.Group(a.Prefix) 66 | for k, v := range a.Resources { 67 | a.registerResource(v, k) 68 | } 69 | } 70 | 71 | //Api 인스턴스에 등록된 Resource 를 gin 서버에 등록시키는 메서드입니다. 72 | func (a *Api) registerResource(resource interface{}, url string) { 73 | for i := 0; i < reflect.TypeOf(resource).NumMethod(); i++ { 74 | value := reflect.ValueOf(resource) 75 | method := reflect.TypeOf(resource).Method(i) 76 | if !isHttpMethod(method.Name) { 77 | continue 78 | } 79 | args := parseArgs(method) 80 | url := createUrl(url, args) 81 | g := a.App.Group(url, parseMiddlewares(resource, method.Name)...) 82 | g.Handle(strings.ToUpper(method.Name), "", createHandlerFunc(value, method, args)) 83 | } 84 | } -------------------------------------------------------------------------------- /error.go: -------------------------------------------------------------------------------- 1 | package gin_restful 2 | 3 | type ApplicationError struct { 4 | Message string 5 | Status int 6 | } 7 | 8 | func (e ApplicationError) Error() string { 9 | return e.Message 10 | } 11 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # examples 2 | 3 | you can find examples for gin-restful at [here](https://github.com/hwangseonu/gin-restful-example) 4 | -------------------------------------------------------------------------------- /example/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hwangseonu/gin-restful/example 2 | 3 | go 1.12 4 | 5 | require github.com/hwangseonu/gin-restful v0.0.0-20190327104301-ecf4330bc9ea // indirect 6 | -------------------------------------------------------------------------------- /example/go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g= 3 | github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= 4 | github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs= 5 | github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= 6 | github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc= 7 | github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= 8 | github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM= 9 | github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= 10 | github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk= 11 | github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= 12 | github.com/hwangseonu/gin-restful v0.0.0-20190327104301-ecf4330bc9ea h1:GNEn7iODpv4SZIrpSQ0mqxq1WobVrzYwO+xrFqMzyxg= 13 | github.com/hwangseonu/gin-restful v0.0.0-20190327104301-ecf4330bc9ea/go.mod h1:1mYZ8WFvtXWStgxzMllNx1ETC4Fa0nxtRgGEFYdCdOE= 14 | github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 15 | github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= 16 | github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= 17 | github.com/mattn/go-isatty v0.0.6 h1:SrwhHcpV4nWrMGdNcC2kXpMfcBVYGDuTArqyhocJgvA= 18 | github.com/mattn/go-isatty v0.0.6/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 19 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 20 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 21 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 22 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 23 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 24 | github.com/ugorji/go v1.1.2 h1:JON3E2/GPW2iDNGoSAusl1KDf5TRQ8k8q7Tp097pZGs= 25 | github.com/ugorji/go v1.1.2/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ= 26 | github.com/ugorji/go/codec v0.0.0-20190309163734-c4a1c341dc93 h1:JnDJ9gMf6CfErtoOXnghtY5hhMuDtW4tUBaWSBrqvKs= 27 | github.com/ugorji/go/codec v0.0.0-20190309163734-c4a1c341dc93/go.mod h1:iT03XoTwV7xq/+UGwKO3UbC1nNNlopQiY61beSdrtOA= 28 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 29 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 30 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 31 | golang.org/x/sys v0.0.0-20190310054646-10058d7d4faa h1:lqti/xP+yD/6zH5TqEwx2MilNIJY5Vbc6Qr8J3qyPIQ= 32 | golang.org/x/sys v0.0.0-20190310054646-10058d7d4faa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 33 | google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 34 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 35 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 36 | gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= 37 | gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= 38 | gopkg.in/go-playground/validator.v9 v9.27.0 h1:wCg/0hk9RzcB0CYw8pYV6FiBYug1on0cpco9YZF8jqA= 39 | gopkg.in/go-playground/validator.v9 v9.27.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= 40 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 41 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 42 | -------------------------------------------------------------------------------- /example/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/hwangseonu/gin-restful" 6 | "net/http" 7 | ) 8 | 9 | //restful api 를 구현하기 위한 SampleResource 구조체입니다. 10 | //gin-restful 의 Resource 구조체 포인터를 임베딩합니다. 11 | //각 Handler 마다 다른 Middleware 들을 적용할 수 있습니다. 12 | type SampleResource struct { 13 | *gin_restful.Resource 14 | } 15 | 16 | //Json Body 를 테스트하기 위한 구조체입니다. 17 | type Data struct { 18 | Name string `json:"name" validate:"required,notblank"` 19 | } 20 | 21 | //SampleResource 의 Url 로 GET 요청이 들어왔을 때 실행되는 Handler 입니다. 22 | //gin.H(json) 와 status code 를 반환합니다. 23 | //path variable 인 name 을 json 에 담아 반환합니다. 24 | func (r SampleResource) Get(name string) (gin.H, int) { 25 | return gin.H{ 26 | "name": name, 27 | }, http.StatusOK 28 | } 29 | 30 | //SampleResource 의 Url 로 POST 요청이 들어왔을 때 실행되는 Handler 입니다. 31 | //json body 를 Data 구조체로 받습니다. 32 | //gin.H 과 status code 를 반환합니다. 33 | //요청으로 받은 payload 를 그대로 반환합니다. 34 | func (r SampleResource) Post(c *gin.Context, json Data) (Data, int) { 35 | return json, 200 36 | } 37 | 38 | //Middleware 테스트용 Sample Middleware 입니다. 39 | //콘솔에 "Hello, World" 를 출력합니다. 40 | func SampleMiddleware(c *gin.Context) { 41 | println("Hello, World") 42 | } 43 | 44 | //Api 인스턴스를 "/" 주소로 생성합니다. 45 | //SampleResource 의 인스턴스를 생성하여 Api "/samples" 주소로 등록합니다. 46 | //SampleResource GET handler 에 SampleMiddleware 를 등록합니다. 47 | //gin 서버를 5000 포트에서 실행합니다. 48 | func main() { 49 | r := gin.Default() 50 | v1 := gin_restful.NewApi(r, "/") 51 | res := SampleResource{gin_restful.InitResource()} 52 | res.AddMiddleware(SampleMiddleware, http.MethodGet) 53 | v1.AddResource(res, "/samples") 54 | _ = r.Run(":5000") 55 | } 56 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hwangseonu/gin-restful 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/gin-gonic/gin v1.4.0 7 | github.com/go-playground/locales v0.12.1 // indirect 8 | github.com/go-playground/universal-translator v0.16.0 // indirect 9 | github.com/leodido/go-urn v1.1.0 // indirect 10 | gopkg.in/go-playground/validator.v9 v9.29.0 11 | ) 12 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g= 4 | github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= 5 | github.com/gin-gonic/gin v1.4.0 h1:3tMoCCfM7ppqsR0ptz/wi1impNpT7/9wQtMZ8lr1mCQ= 6 | github.com/gin-gonic/gin v1.4.0/go.mod h1:OW2EZn3DO8Ln9oIKOvM++LBO+5UPHJJDH72/q/3rZdM= 7 | github.com/go-playground/locales v0.12.1 h1:2FITxuFt/xuCNP1Acdhv62OzaCiviiE4kotfhkmOqEc= 8 | github.com/go-playground/locales v0.12.1/go.mod h1:IUMDtCfWo/w/mtMfIE/IG2K+Ey3ygWanZIBtBW0W2TM= 9 | github.com/go-playground/universal-translator v0.16.0 h1:X++omBR/4cE2MNg91AoC3rmGrCjJ8eAeUP/K/EKx4DM= 10 | github.com/go-playground/universal-translator v0.16.0/go.mod h1:1AnU7NaIRDWWzGEKwgtJRd2xk99HeFyHw3yid4rvQIY= 11 | github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= 12 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 13 | github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs= 14 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 15 | github.com/leodido/go-urn v1.1.0 h1:Sm1gr51B1kKyfD2BlRcLSiEkffoG96g6TPv6eRoEiB8= 16 | github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw= 17 | github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc= 18 | github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 19 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 20 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 21 | github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= 22 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 23 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 24 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 25 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 26 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 27 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 28 | github.com/ugorji/go v1.1.4 h1:j4s+tAvLfL3bZyefP2SEWmhBzmuIlH/eqNuPdFPgngw= 29 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 30 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 31 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw= 32 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 33 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 34 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= 35 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 36 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 37 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 38 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 39 | gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= 40 | gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= 41 | gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= 42 | gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= 43 | gopkg.in/go-playground/validator.v9 v9.29.0 h1:5ofssLNYgAA/inWn6rTZ4juWpRJUwEnXc1LG2IeXwgQ= 44 | gopkg.in/go-playground/validator.v9 v9.29.0/go.mod h1:+c9/zcJMFNgbLvly1L1V+PpxWdVbfP1avr/N00E2vyQ= 45 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 46 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 47 | -------------------------------------------------------------------------------- /init.go: -------------------------------------------------------------------------------- 1 | package gin_restful 2 | 3 | import ( 4 | "errors" 5 | "github.com/gin-gonic/gin/binding" 6 | "gopkg.in/go-playground/validator.v9" 7 | "reflect" 8 | "regexp" 9 | "sync" 10 | ) 11 | 12 | var validate = new(defaultValidator) 13 | 14 | func init() { 15 | validate.lazyinit() 16 | err := validate.validate.RegisterValidation("notblank", NotBlankValidate) 17 | if err != nil { 18 | panic(err) 19 | } 20 | binding.Validator = validate 21 | } 22 | 23 | type defaultValidator struct { 24 | once sync.Once 25 | validate *validator.Validate 26 | } 27 | 28 | var _ binding.StructValidator = &defaultValidator{} 29 | 30 | func (v *defaultValidator) ValidateStruct(obj interface{}) error { 31 | if kindOfData(obj) == reflect.Struct { 32 | v.lazyinit() 33 | if err := v.validate.Struct(obj); err != nil { 34 | return error(err) 35 | } 36 | } 37 | return nil 38 | } 39 | 40 | func (v *defaultValidator) Engine() interface{} { 41 | v.lazyinit() 42 | return v.validate 43 | } 44 | 45 | func (v *defaultValidator) lazyinit() { 46 | v.once.Do(func() { 47 | v.validate = validator.New() 48 | v.validate.SetTagName("binding") 49 | 50 | // add any custom validations etc. here 51 | }) 52 | } 53 | 54 | func kindOfData(data interface{}) reflect.Kind { 55 | value := reflect.ValueOf(data) 56 | valueType := value.Kind() 57 | 58 | if valueType == reflect.Ptr { 59 | valueType = value.Elem().Kind() 60 | } 61 | return valueType 62 | } 63 | 64 | func mustString(i interface{}) (string, error) { 65 | if s, ok := i.(string); ok { 66 | return s, nil 67 | } else { 68 | return "", errors.New(" is must int") 69 | } 70 | } 71 | 72 | //구조체 필드에 `binding:"notblank"` 과 같이 사용합니다. 73 | func NotBlankValidate(fl validator.FieldLevel) bool { 74 | if str, err := mustString(fl.Field().Interface()); err != nil { 75 | return false 76 | } else if regexp.MustCompile(`^\s*$`).MatchString(str) { 77 | return false 78 | } else { 79 | return true 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /resource.go: -------------------------------------------------------------------------------- 1 | package gin_restful 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "strings" 6 | ) 7 | 8 | //Resource 는 기본적으로 Api 인스턴스에 등록 가능한 Resource 의 형태입니다. 9 | //각 Handler 에 적용될 Middleware 들을 포함합니다. 10 | //Resource 포인터를 임베딩하여 사용할 수 있지만 Middleware 가 필요없다면 임베딩 하지 않아도 무방합니다. 11 | //Resource 포인터를 임베딩할 때 InitResource 함수를 사용합니다. 12 | //Resource 에 Handler 를 등록할 때는 사용할 http 메서드와 같은 메서드를 정의하면 됩니다. 13 | //Handler 의 이름으로는 Get, Post, Put, Patch, Delete 등을 사용할 수 있습니다. 14 | //Handler 이름의 첫글자는 무조건 대문자여야합니다. 15 | //Handler 의 에는 *gin.Context, string, int, float, bool, struct 타입의 인자를 사용할 수 있습니다. 16 | //Resource 를 서버에 등록할 때는 Resource 의 메서드 중 http method 인 것을 찾아 인자를 파싱하여 url 을 생성합니다. 17 | //요청을 받았을때 메서드가 실행되며 각각의 인자는 자동으로 채워집니다. 18 | //*gin.Context 와 struct 를 제외한 인자는 path variable 이 됩니다. 19 | //*gin.Context 타입의 인자는 해당 요청의 context 로 채워집니다. 20 | //구조체 타입의 인자는 하나만 존재할 수 있으며 요청의 body 를 파싱하여 채워집니다. 21 | type Resource struct { 22 | Middlewares map[string][]gin.HandlerFunc 23 | } 24 | 25 | //Resource 구조체를 초기화하여 포인터로 반환해주는 함수입니다. 26 | //새로운 Resource 구조체를 정의하여 사용할 때 Resource 를 임베딩 하기 위해 사용합니다. 27 | func InitResource() *Resource { 28 | return &Resource{ 29 | Middlewares: make(map[string][]gin.HandlerFunc, 0), 30 | } 31 | } 32 | 33 | //Resource 인스턴스에 각 http method 에 사용할 Middleware 를 등록하는 메서드입니다. 34 | //methods 는 사용가능한 http method 의 이름과 같아야 합니다. 35 | func (r *Resource) AddMiddleware(middleware gin.HandlerFunc, methods ...string) { 36 | for _, m := range methods { 37 | m = strings.ToUpper(m) 38 | middlewares := r.Middlewares[m] 39 | middlewares = append(middlewares, middleware) 40 | r.Middlewares[m] = middlewares 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /utils.go: -------------------------------------------------------------------------------- 1 | package gin_restful 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "github.com/gin-gonic/gin" 7 | "net/http" 8 | "reflect" 9 | "strconv" 10 | "strings" 11 | ) 12 | 13 | var httpmethods = []string{ 14 | http.MethodGet, 15 | http.MethodPost, 16 | http.MethodPatch, 17 | http.MethodDelete, 18 | http.MethodPut, 19 | http.MethodHead, 20 | http.MethodConnect, 21 | http.MethodOptions, 22 | http.MethodTrace, 23 | } 24 | 25 | func getJsonData(c *gin.Context, json interface{}) (interface{}, error) { 26 | mustType := reflect.TypeOf(json) 27 | value := reflect.New(mustType) 28 | if err := c.ShouldBindJSON(value.Interface()); err != nil { 29 | return nil, err 30 | } 31 | return value.Elem().Interface(), nil 32 | } 33 | 34 | func isHttpMethod(name string) bool { 35 | for _, k := range httpmethods { 36 | if strings.ToUpper(name) == k { 37 | return true 38 | } 39 | } 40 | return false 41 | } 42 | 43 | func createUrl(url string, args []reflect.Type) string { 44 | for i, a := range args { 45 | if a.Kind() == reflect.Struct { 46 | continue 47 | } 48 | if a.String() == "*gin.Context" { 49 | continue 50 | } 51 | url += "/:" + a.String() + strconv.Itoa(i) 52 | } 53 | return url 54 | } 55 | 56 | func parseArgs(method reflect.Method) []reflect.Type { 57 | args := make([]reflect.Type, 0) 58 | can := []string{"string", "int", "float64", "bool", "*gin.Context"} 59 | 60 | addedStruct := false 61 | for i := 1; i < method.Type.NumIn(); i++ { 62 | arg := method.Type.In(i) 63 | if arg.Kind() == reflect.Struct { 64 | if addedStruct { 65 | panic(errors.New("method argument can string, int, float64, bool, *gin.Context, one struct")) 66 | } else { 67 | addedStruct = true 68 | args = append(args, arg) 69 | continue 70 | } 71 | } 72 | if !contains(can, arg.String()) { 73 | panic(errors.New("method argument can string, int, float64, bool, *gin.Context, one struct")) 74 | } 75 | args = append(args, arg) 76 | } 77 | return args 78 | } 79 | 80 | func createValues(c *gin.Context, resource reflect.Value, args []reflect.Type) ([]reflect.Value, error) { 81 | values := []reflect.Value{resource} 82 | 83 | for i, arg := range args { 84 | p := c.Param(arg.String() + strconv.Itoa(i)) 85 | switch arg.Kind() { 86 | case reflect.String: 87 | values = append(values, reflect.ValueOf(p)) 88 | break 89 | case reflect.Int: 90 | if num, err := strconv.Atoi(p); err != nil { 91 | return []reflect.Value{}, ApplicationError{ 92 | Message: "argument " + arg.String() + strconv.Itoa(i) + " is must int", 93 | Status: http.StatusBadRequest, 94 | } 95 | } else { 96 | values = append(values, reflect.ValueOf(num)) 97 | } 98 | break 99 | case reflect.Float64: 100 | if num, err := strconv.ParseFloat(p, 64); err != nil { 101 | return []reflect.Value{}, ApplicationError{ 102 | Message: "argument " + arg.String() + strconv.Itoa(i) + "is must float64", 103 | Status: http.StatusBadRequest, 104 | } 105 | } else { 106 | values = append(values, reflect.ValueOf(num)) 107 | } 108 | break 109 | case reflect.Bool: 110 | if p == "" || p == "false" || p == "0" || p == "null" || p == "nil" || p == "off" { 111 | values = append(values, reflect.ValueOf(false)) 112 | } else { 113 | values = append(values, reflect.ValueOf(true)) 114 | } 115 | break 116 | case reflect.Ptr: 117 | if arg.String() == "*gin.Context" { 118 | values = append(values, reflect.ValueOf(c)) 119 | } 120 | break 121 | case reflect.Struct: 122 | body := reflect.New(arg).Elem().Interface() 123 | if v, err := getJsonData(c, body); err != nil { 124 | return []reflect.Value{}, ApplicationError{ 125 | Message: err.Error(), 126 | Status: 400, 127 | } 128 | } else { 129 | values = append(values, reflect.ValueOf(v)) 130 | } 131 | break 132 | default: 133 | panic(errors.New("method argument can string, int, float64, bool, *gin.Context, one struct")) 134 | } 135 | } 136 | return values, nil 137 | } 138 | 139 | func contains(s []string, e string) bool { 140 | for _, a := range s { 141 | if a == e { 142 | return true 143 | } 144 | } 145 | return false 146 | } 147 | 148 | func createHandlerFunc(resource reflect.Value, method reflect.Method, args []reflect.Type) gin.HandlerFunc { 149 | return func(c *gin.Context) { 150 | values, err := createValues(c, resource, args) 151 | if err != nil { 152 | ae, ok := err.(ApplicationError) 153 | status := http.StatusInternalServerError 154 | if ok { 155 | status = ae.Status 156 | } 157 | c.JSON(status, gin.H{"message": err.Error()}) 158 | return 159 | } 160 | returns := method.Func.Call(values) 161 | status := http.StatusOK 162 | switch len(returns) { 163 | case 0: 164 | c.Status(http.StatusOK) 165 | return 166 | case 2: 167 | status = int(returns[1].Int()) 168 | } 169 | if _, err := json.MarshalIndent(returns[0].Interface(), "", " "); err != nil { 170 | c.String(status, returns[0].String()) 171 | } else { 172 | c.JSON(status, returns[0].Interface()) 173 | } 174 | } 175 | } 176 | 177 | func parseMiddlewares(resource interface{}, method string) []gin.HandlerFunc { 178 | r := reflect.ValueOf(resource) 179 | method = strings.ToUpper(method) 180 | f := r.FieldByName("Middlewares") 181 | if !f.IsValid() { 182 | return make([]gin.HandlerFunc, 0) 183 | } 184 | middlewares := r.FieldByName("Middlewares").Interface().(map[string][]gin.HandlerFunc) 185 | return middlewares[method] 186 | } 187 | --------------------------------------------------------------------------------