├── .github └── workflows │ └── build.yaml ├── .gitignore ├── LICENSE ├── README ├── amock ├── README ├── amock.go ├── amock_test.go └── foo │ └── foo.go ├── go.mod ├── go.sum ├── iffy ├── README ├── iffy.go ├── iffy_test.go └── package.go ├── tonic ├── README ├── handler.go ├── listen.go ├── route.go ├── route_test.go ├── tonic.go ├── tonic_test.go └── utils │ ├── jujerr │ └── jujerr.go │ └── listenproxyproto │ └── listener.go └── zesty ├── README ├── utils └── rekordo │ ├── database.go │ └── register.go ├── zesty.go └── zesty_test.go /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | go-version: ['1.20', '1.21', '1.22'] 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Setup Go ${{ matrix.go-version }} 16 | uses: actions/setup-go@v4 17 | with: 18 | go-version: ${{ matrix.go-version }} 19 | - name: Display Go version 20 | run: go version 21 | - name: Build 22 | run: go build ./... 23 | 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *TODO* 2 | .history/ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Thomas Schaffer 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 | 23 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | # Gadgeto! 2 | 3 | Author: Thomas Schaffer 4 | Language: Golang 5 | License: MIT 6 | 7 | Gadgeto! is a collection of tools that aim to facilitate the development of 8 | REST APIs in Go. 9 | These tools are based on and enrich popular open-source solutions, to provide 10 | higher-level functionalities. 11 | 12 | Components: 13 | - tonic: Based on the REST framework Gin (https://github.com/gin-gonic/gin), 14 | tonic lets you write simpler handler functions, and handles 15 | repetitive tasks for you (parameter binding, error handling). 16 | 17 | - zesty: Based on Gorp (https://github.com/go-gorp/gorp), zesty abstracts 18 | DB specifics for easy (nested) transaction management. 19 | 20 | - iffy: An HTTP testing library for functional/unit tests, 21 | iffy does all the hard work (http, marshaling, templating...) and 22 | lets you describe http calls as one-liners, chaining them in complex scenarios. 23 | 24 | - amock: An HTTP mocking library. amock lets you easily write mock objects to inject 25 | into http clients. 26 | It does not require any go-generate tricks, and lets you inject mocks 27 | into existing libraries that may not be properly abstracted through interfaces. 28 | -------------------------------------------------------------------------------- /amock/README: -------------------------------------------------------------------------------- 1 | amock lets you easily mock any HTTP dependency you may have. 2 | It respects the http.RoundTripper interface to replace an http client's transport. 3 | 4 | Responses are stacked, and indexed by code path: you specify responses for a certain Go function, 5 | and when it's invoked the mock object will go up the stack until it reaches max depth, 6 | or finds a function for which you specified a response. 7 | 8 | The response will be pop'ed, so the next identical call will get the next expected response. 9 | 10 | You can specify conditional filters on responses: 11 | 12 | - OnFunc(foo.GetFoo): 13 | Filter on calls that went through a given go function 14 | 15 | 16 | - OnIdentifier("foo"): 17 | Shortcut to filter on requests following a path pattern of /.../foo(/...) 18 | It is a reasonable assumption that REST implementations follow that pattern, 19 | which makes writing conditions for these simple cases very easy. 20 | 21 | - On(func(c *amock.Context) bool { return c.Request.Method == "GET" } ): 22 | More verbose but possible to express anything. 23 | 24 | For a working example, see amock_test.go 25 | -------------------------------------------------------------------------------- /amock/amock.go: -------------------------------------------------------------------------------- 1 | package amock 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | "reflect" 10 | "regexp" 11 | "runtime" 12 | "runtime/debug" 13 | "sync" 14 | "testing" 15 | ) 16 | 17 | // MockRoundTripper implements http.RoundTripper for mocking/testing purposes 18 | type MockRoundTripper struct { 19 | sync.Mutex 20 | Responses []*Response 21 | potentialCallers map[string]struct{} 22 | } 23 | 24 | // ResponsePayload is an interface that the Body object you pass in your expected responses can respect. 25 | // It lets you customize the way your body is handled. If you pass an object that does NOT respect ResponsePayload, 26 | // JSON is the default. 27 | type ResponsePayload interface { 28 | Payload() ([]byte, error) 29 | } 30 | 31 | // Raw respects the ResponsePayload interface. It lets you define a Body object with raw bytes. 32 | type Raw []byte 33 | 34 | func (r Raw) Payload() ([]byte, error) { 35 | return []byte(r), nil 36 | } 37 | 38 | // JSON respects the ResponsePayload interface. It encloses another object and marshals it into json. 39 | // This is used if your body object does not respect ResponsePayload. 40 | type JSON struct { 41 | Obj interface{} 42 | } 43 | 44 | func (j JSON) Payload() ([]byte, error) { 45 | return json.Marshal(j.Obj) 46 | } 47 | 48 | // An expected mocked response. Defining traits are status and body. 49 | // Optionally includes conditional filter function defined by one or several On(...) or OnIdentifier(...) calls. 50 | type Response struct { 51 | Status int 52 | headers http.Header 53 | Body ResponsePayload 54 | Cond func(*Context) bool 55 | sticky bool 56 | Mock *MockRoundTripper 57 | } 58 | 59 | // Context describes the context of the current call to conditional filter functions 60 | type Context struct { 61 | Request *http.Request 62 | callers map[string]struct{} 63 | mock *MockRoundTripper 64 | } 65 | 66 | // Callers returns the functions in the current stack that may be of interest to the conditional filter funcs 67 | func (c *Context) Callers() map[string]struct{} { 68 | if c.callers == nil { 69 | c.callers = c.mock.callers() 70 | } 71 | return c.callers 72 | } 73 | 74 | // NewMock creates a MockRoundTripper object 75 | func NewMock() *MockRoundTripper { 76 | return &MockRoundTripper{ 77 | potentialCallers: map[string]struct{}{}, 78 | } 79 | } 80 | 81 | // Sticky marks the response as reusable. It will not get consumed whenever it is returned. 82 | func (r *Response) Sticky() *Response { 83 | r.Mock.Lock() 84 | defer r.Mock.Unlock() 85 | r.sticky = true 86 | return r 87 | } 88 | 89 | // Headers adds http headers to the response 90 | func (r *Response) Headers(h http.Header) *Response { 91 | r.Mock.Lock() 92 | defer r.Mock.Unlock() 93 | r.headers = h 94 | return r 95 | } 96 | 97 | // merges two conditional filter functions into a composite one (logical AND) 98 | func condAND(fs ...func(*Context) bool) func(*Context) bool { 99 | return func(c *Context) bool { 100 | for _, f := range fs { 101 | if !f(c) { 102 | return false 103 | } 104 | } 105 | return true 106 | } 107 | } 108 | 109 | // addCond merges a conditional filter with the existing ones on a Response. 110 | func (r *Response) addCond(cond func(*Context) bool) { 111 | if r.Cond != nil { 112 | r.Cond = condAND(r.Cond, cond) 113 | } else { 114 | r.Cond = cond 115 | } 116 | } 117 | 118 | // OnFunc matches calls that went throug a given go function. 119 | // It accepts a reference to a function as input, and panics otherwise. 120 | func (r *Response) OnFunc(callerFunc interface{}) *Response { 121 | r.Mock.Lock() 122 | defer r.Mock.Unlock() 123 | caller := getFunctionName(callerFunc) 124 | r.Mock.potentialCaller(caller) 125 | cond := func(c *Context) bool { 126 | callers := c.Callers() 127 | _, ok := callers[caller] 128 | return ok 129 | } 130 | r.addCond(cond) 131 | return r 132 | } 133 | 134 | // OnIdentifier adds a conditional filter to the response. 135 | // The response will be selected only if the HTTP path of the request contains 136 | // "/.../IDENT(/...)": the identifier enclosed in a distinct path segment 137 | func (r *Response) OnIdentifier(ident string) *Response { 138 | r.Mock.Lock() 139 | defer r.Mock.Unlock() 140 | ident = regexp.QuoteMeta(ident) 141 | matcher := regexp.MustCompile(`/[^/]+/` + ident + `(?:/.*|$)`) 142 | cond := func(c *Context) bool { 143 | return matcher.MatchString(c.Request.URL.Path) 144 | } 145 | r.addCond(cond) 146 | return r 147 | } 148 | 149 | // On adds a conditional filter to the response. 150 | func (r *Response) On(f func(*Context) bool) *Response { 151 | r.Mock.Lock() 152 | defer r.Mock.Unlock() 153 | r.addCond(f) 154 | return r 155 | } 156 | 157 | // potentialCaller marks a function as worthy of consideration when going through the stack. 158 | // It is called by the OnFunc() filter. 159 | func (mc *MockRoundTripper) potentialCaller(caller string) { 160 | mc.potentialCallers[caller] = struct{}{} 161 | } 162 | 163 | // callers scans the stack for functions defined in "potentialCallers". 164 | // potentialCallers are the aggregated values passed to OnFunc() filters of the responses 165 | // attached to this mock. 166 | func (mc *MockRoundTripper) callers() map[string]struct{} { 167 | ret := map[string]struct{}{} 168 | callers := make([]uintptr, 50) 169 | runtime.Callers(3, callers) 170 | frames := runtime.CallersFrames(callers) 171 | for { 172 | frame, more := frames.Next() 173 | _, ok := mc.potentialCallers[frame.Function] 174 | if ok { 175 | ret[frame.Function] = struct{}{} 176 | } 177 | if !more { 178 | break 179 | } 180 | } 181 | return ret 182 | } 183 | 184 | // Expect adds a new expected response, specifying status and body. The other components (headers, conditional filters) 185 | // can be further specified by chaining setter calls on the response object. 186 | func (mc *MockRoundTripper) Expect(status int, body interface{}) *Response { 187 | mc.Lock() 188 | defer mc.Unlock() 189 | 190 | bodyPL, ok := body.(ResponsePayload) 191 | if !ok { 192 | bodyPL = JSON{body} 193 | } 194 | resp := &Response{Status: status, Body: bodyPL, Mock: mc} 195 | mc.Responses = append(mc.Responses, resp) 196 | return resp 197 | } 198 | 199 | // Hack to fix method vs function references 200 | // 201 | // var f foo.Foo 202 | // f.UpdateFoo and (*foo.Foo).UpdateFoo have a different uintptr 203 | // Their stringified name is suffixed with -... (illegal in identifier names) 204 | var regexTrimMethodSuffix = regexp.MustCompile(`-[^/\.]+$`) 205 | 206 | func getFunctionName(i interface{}) string { 207 | name := runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() 208 | name = regexTrimMethodSuffix.ReplaceAllString(name, "") 209 | return name 210 | } 211 | 212 | // RoundTrip respects http.RoundTripper. It finds the code path taken to get to here, and returns the first matching expected response. 213 | func (mc *MockRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { 214 | 215 | mc.Lock() 216 | defer mc.Unlock() 217 | 218 | if len(mc.Responses) == 0 { 219 | return nil, ErrUnexpectedCall("no more expected responses") 220 | } 221 | 222 | ctx := &Context{Request: r, mock: mc} 223 | 224 | var resp *Response 225 | 226 | for i, rsp := range mc.Responses { 227 | if rsp.Cond == nil || rsp.Cond(ctx) { 228 | // Delete elem in place 229 | if !rsp.sticky { 230 | mc.Responses = append(mc.Responses[:i], mc.Responses[i+1:]...) 231 | } 232 | resp = rsp 233 | break 234 | } 235 | } 236 | 237 | if resp == nil { 238 | return nil, ErrUnexpectedCall("remaining responses have unmet conditions") 239 | } 240 | 241 | var respBody []byte 242 | var err error 243 | 244 | if resp.Body != nil { 245 | respBody, err = resp.Body.Payload() 246 | if err != nil { 247 | return nil, err 248 | } 249 | } 250 | 251 | return &http.Response{ 252 | Proto: "HTTP/1.1", 253 | ProtoMajor: 1, 254 | ProtoMinor: 1, 255 | Status: http.StatusText(resp.Status), 256 | StatusCode: resp.Status, 257 | Header: resp.headers, 258 | Body: ioutil.NopCloser(bytes.NewReader(respBody)), 259 | Request: r, 260 | ContentLength: int64(len(respBody)), 261 | }, nil 262 | } 263 | 264 | // AssertEmpty ensures all expected responses have been consumed. 265 | // It will call t.Error() detailing the remaining unconsumed responses. 266 | func (mc *MockRoundTripper) AssertEmpty(t *testing.T) { 267 | mc.Lock() 268 | defer mc.Unlock() 269 | 270 | i := 0 271 | for _, r := range mc.Responses { 272 | // ignore sticky responses 273 | if !r.sticky { 274 | i++ 275 | } 276 | } 277 | 278 | if i > 0 { 279 | t.Error(fmt.Sprintf("%d expected responses remaining", i)) 280 | } 281 | } 282 | 283 | // ErrUnexpectedCall crafts an error including a stack trace, to pinpoint a call that did not match 284 | // any of the configured responses 285 | func ErrUnexpectedCall(reason string) error { 286 | return fmt.Errorf("unexpected call: %s\n%s", reason, string(debug.Stack())) 287 | } 288 | -------------------------------------------------------------------------------- /amock/amock_test.go: -------------------------------------------------------------------------------- 1 | package amock 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/loopfz/gadgeto/amock/foo" 8 | ) 9 | 10 | func TestMock(t *testing.T) { 11 | 12 | mock := NewMock() 13 | mock.Expect(200, foo.Foo{Identifier: "f1234", BarCount: 42}).OnIdentifier("f1234").OnFunc(foo.GetFoo) 14 | foo.Client.Transport = mock 15 | 16 | fmt.Println("get a foo with an identifier not matching the expected one") 17 | 18 | f, err := foo.GetFoo("f1") 19 | if err == nil { 20 | t.Error("Should not have returned foo object with non-matching ident") 21 | } 22 | 23 | fmt.Println("returned:", err) 24 | 25 | fmt.Println("----------------------------------------------------------------------") 26 | 27 | fmt.Println("get a foo with the correct identifier but going through an unexpected code path") 28 | 29 | f, err = foo.GetFoo2("f1234") 30 | if err == nil { 31 | t.Error("Should not have returned foo object with non-matching func") 32 | } 33 | 34 | fmt.Println("returned:", err) 35 | 36 | fmt.Println("----------------------------------------------------------------------") 37 | 38 | fmt.Println("get a foo with the correct identifier") 39 | 40 | f, err = foo.GetFoo("f1234") 41 | if err != nil { 42 | t.Error(err) 43 | } 44 | 45 | fmt.Println("returned:", f) 46 | 47 | fmt.Println("----------------------------------------------------------------------") 48 | 49 | fmt.Println("update foo object and call an update _method_") 50 | 51 | f.BarCount = 43 52 | 53 | mock.Expect(200, f).OnIdentifier(f.Identifier).OnFunc(f.UpdateFoo) 54 | 55 | f, err = f.UpdateFoo() 56 | if err != nil { 57 | t.Error(err) 58 | } 59 | 60 | fmt.Println("returned:", f) 61 | 62 | fmt.Println("----------------------------------------------------------------------") 63 | 64 | fmt.Println("make the mock simulate a 503, get a foo expecting an error") 65 | 66 | mock.Expect(503, Raw([]byte(`

503 Service Unavailable

67 | No server is available to handle this request. 68 | `))).Sticky() 69 | 70 | f, err = foo.GetFoo("f2") 71 | if err == nil { 72 | t.Error("expected a 503 error") 73 | } 74 | 75 | fmt.Println("returned:", err) 76 | 77 | fmt.Println("----------------------------------------------------------------------") 78 | 79 | fmt.Println("previous response is sticky, retry and expect same error") 80 | 81 | f, err2 := foo.GetFoo("f2") 82 | if err2 == nil { 83 | t.Error("expected a 503 error") 84 | } 85 | if err.Error() != err2.Error() { 86 | t.Errorf("Errors mismatched: '%s' // '%s'", err.Error(), err2.Error()) 87 | } 88 | 89 | fmt.Println("returned:", err2) 90 | 91 | mock.AssertEmpty(t) 92 | } 93 | -------------------------------------------------------------------------------- /amock/foo/foo.go: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io/ioutil" 8 | "net/http" 9 | ) 10 | 11 | var Client = &http.Client{} 12 | 13 | type Foo struct { 14 | Identifier string `json:"identifier"` 15 | BarCount int `json:"bar_count"` 16 | } 17 | 18 | func GetFoo(ident string) (*Foo, error) { 19 | return doGetFoo(ident) 20 | } 21 | 22 | func GetFoo2(ident string) (*Foo, error) { 23 | return doGetFoo(ident) 24 | } 25 | 26 | func doGetFoo(ident string) (*Foo, error) { 27 | resp, err := Client.Get("http://www.foo.com/foo/" + ident) 28 | if err != nil { 29 | return nil, err 30 | } 31 | if resp.StatusCode < 200 || resp.StatusCode >= 400 { 32 | return nil, fmt.Errorf("got http error %d", resp.StatusCode) 33 | } 34 | body, err := ioutil.ReadAll(resp.Body) 35 | if err != nil { 36 | return nil, err 37 | } 38 | ret := &Foo{} 39 | err = json.Unmarshal(body, ret) 40 | if err != nil { 41 | return nil, err 42 | } 43 | return ret, nil 44 | } 45 | 46 | func (f *Foo) UpdateFoo() (*Foo, error) { 47 | 48 | body, err := json.Marshal(f) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | req, err := http.NewRequest("PUT", "http://www.foo.com/foo/"+f.Identifier, bytes.NewReader(body)) 54 | resp, err := Client.Do(req) 55 | if err != nil { 56 | return nil, err 57 | } 58 | if resp.StatusCode < 200 || resp.StatusCode >= 400 { 59 | return nil, fmt.Errorf("got http error %d", resp.StatusCode) 60 | } 61 | body, err = ioutil.ReadAll(resp.Body) 62 | if err != nil { 63 | return nil, err 64 | } 65 | ret := &Foo{} 66 | err = json.Unmarshal(body, ret) 67 | if err != nil { 68 | return nil, err 69 | } 70 | 71 | return ret, nil 72 | } 73 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/loopfz/gadgeto 2 | 3 | require ( 4 | github.com/gin-gonic/gin v1.9.1 5 | github.com/go-gorp/gorp v2.2.0+incompatible 6 | github.com/go-playground/validator/v10 v10.19.0 7 | github.com/google/uuid v1.6.0 8 | github.com/juju/errors v0.0.0-20200330140219-3fe23663418f 9 | github.com/mattn/go-sqlite3 v1.14.22 10 | github.com/pires/go-proxyproto v0.7.0 11 | sigs.k8s.io/yaml v1.4.0 12 | ) 13 | 14 | require ( 15 | github.com/bytedance/sonic v1.9.1 // indirect 16 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 17 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 18 | github.com/gin-contrib/sse v0.1.0 // indirect 19 | github.com/go-playground/locales v0.14.1 // indirect 20 | github.com/go-playground/universal-translator v0.18.1 // indirect 21 | github.com/go-sql-driver/mysql v1.5.0 // indirect 22 | github.com/goccy/go-json v0.10.2 // indirect 23 | github.com/json-iterator/go v1.1.12 // indirect 24 | github.com/juju/testing v0.0.0-20210302031854-2c7ee8570c07 // indirect 25 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 26 | github.com/leodido/go-urn v1.4.0 // indirect 27 | github.com/lib/pq v1.9.0 // indirect 28 | github.com/mattn/go-isatty v0.0.19 // indirect 29 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 30 | github.com/modern-go/reflect2 v1.0.2 // indirect 31 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 32 | github.com/poy/onpar v1.1.2 // indirect 33 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 34 | github.com/ugorji/go/codec v1.2.12 // indirect 35 | github.com/ziutek/mymysql v1.5.4 // indirect 36 | golang.org/x/arch v0.3.0 // indirect 37 | golang.org/x/crypto v0.19.0 // indirect 38 | golang.org/x/net v0.21.0 // indirect 39 | golang.org/x/sys v0.17.0 // indirect 40 | golang.org/x/text v0.14.0 // indirect 41 | google.golang.org/protobuf v1.30.0 // indirect 42 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect 43 | gopkg.in/yaml.v3 v3.0.1 // indirect 44 | ) 45 | 46 | go 1.20 47 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= 2 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 3 | github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= 4 | github.com/a8m/expect v1.0.0/go.mod h1:4IwSCMumY49ScypDnjNbYEjgVeqy1/U2cEs3Lat96eA= 5 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 6 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 7 | github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8= 8 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 9 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 10 | github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= 11 | github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= 12 | github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= 13 | github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= 14 | github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= 15 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= 16 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= 17 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 18 | github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= 19 | github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= 20 | github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= 21 | github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= 22 | github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= 23 | github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 24 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 25 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 26 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 27 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 28 | github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= 29 | github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= 30 | github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= 31 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 32 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= 33 | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 34 | github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= 35 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 36 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 37 | github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= 38 | github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= 39 | github.com/go-gorp/gorp v2.2.0+incompatible h1:xAUh4QgEeqPPhK3vxZN+bzrim1z5Av6q837gtjUlshc= 40 | github.com/go-gorp/gorp v2.2.0+incompatible/go.mod h1:7IfkAQnO7jfT/9IQ3R9wL1dFhukN6aQxzKTHnkxzA/E= 41 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 42 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 43 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 44 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 45 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 46 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 47 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 48 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 49 | github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4= 50 | github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 51 | github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= 52 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 53 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 54 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 55 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 56 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 57 | github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= 58 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 59 | github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 60 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 61 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 62 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 63 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 64 | github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= 65 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 66 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 67 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 68 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 69 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 70 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 71 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 72 | github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= 73 | github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= 74 | github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= 75 | github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= 76 | github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= 77 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 78 | github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= 79 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 80 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 81 | github.com/juju/ansiterm v0.0.0-20160907234532-b99631de12cf/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= 82 | github.com/juju/clock v0.0.0-20190205081909-9c5c9712527c/go.mod h1:nD0vlnrUjcjJhqN5WuCWZyzfd5AHZAC9/ajvbSx69xA= 83 | github.com/juju/cmd v0.0.0-20171107070456-e74f39857ca0/go.mod h1:yWJQHl73rdSX4DHVKGqkAip+huBslxRwS8m9CrOLq18= 84 | github.com/juju/collections v0.0.0-20200605021417-0d0ec82b7271/go.mod h1:5XgO71dV1JClcOJE+4dzdn4HrI5LiyKd7PlVG6eZYhY= 85 | github.com/juju/errors v0.0.0-20150916125642-1b5e39b83d18/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= 86 | github.com/juju/errors v0.0.0-20200330140219-3fe23663418f h1:MCOvExGLpaSIzLYB4iQXEHP4jYVU6vmzLNQPdMVrxnM= 87 | github.com/juju/errors v0.0.0-20200330140219-3fe23663418f/go.mod h1:W54LbzXuIE0boCoNJfwqpmkKJ1O4TCTZMetAt6jGk7Q= 88 | github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE= 89 | github.com/juju/httpprof v0.0.0-20141217160036-14bf14c30767/go.mod h1:+MaLYz4PumRkkyHYeXJ2G5g5cIW0sli2bOfpmbaMV/g= 90 | github.com/juju/loggo v0.0.0-20170605014607-8232ab8918d9/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= 91 | github.com/juju/loggo v0.0.0-20200526014432-9ce3a2e09b5e h1:FdDd7bdI6cjq5vaoYlK1mfQYfF9sF2VZw8VEZMsl5t8= 92 | github.com/juju/loggo v0.0.0-20200526014432-9ce3a2e09b5e/go.mod h1:vgyd7OREkbtVEN/8IXZe5Ooef3LQePvuBm9UWj6ZL8U= 93 | github.com/juju/mgo/v2 v2.0.0-20210302023703-70d5d206e208 h1:/WiCm+Vpj87e4QWuWwPD/bNE9kDrWCLvPBHOQNcG2+A= 94 | github.com/juju/mgo/v2 v2.0.0-20210302023703-70d5d206e208/go.mod h1:0OChplkvPTZ174D2FYZXg4IB9hbEwyHkD+zT+/eK+Fg= 95 | github.com/juju/mutex v0.0.0-20171110020013-1fe2a4bf0a3a/go.mod h1:Y3oOzHH8CQ0Ppt0oCKJ2JFO81/EsWenH5AEqigLH+yY= 96 | github.com/juju/retry v0.0.0-20151029024821-62c620325291/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4= 97 | github.com/juju/retry v0.0.0-20180821225755-9058e192b216/go.mod h1:OohPQGsr4pnxwD5YljhQ+TZnuVRYpa5irjugL1Yuif4= 98 | github.com/juju/testing v0.0.0-20180402130637-44801989f0f7/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= 99 | github.com/juju/testing v0.0.0-20190723135506-ce30eb24acd2/go.mod h1:63prj8cnj0tU0S9OHjGJn+b1h0ZghCndfnbQolrYTwA= 100 | github.com/juju/testing v0.0.0-20210302031854-2c7ee8570c07 h1:6QA3rIUc3TBPbv8zWa2KQ2TWn6gsn1EU0UhwRi6kOhA= 101 | github.com/juju/testing v0.0.0-20210302031854-2c7ee8570c07/go.mod h1:7lxZW0B50+xdGFkvhAb8bwAGt6IU87JB1H9w4t8MNVM= 102 | github.com/juju/utils v0.0.0-20180424094159-2000ea4ff043/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk= 103 | github.com/juju/utils v0.0.0-20200116185830-d40c2fe10647/go.mod h1:6/KLg8Wz/y2KVGWEpkK9vMNGkOnu4k/cqs8Z1fKjTOk= 104 | github.com/juju/utils/v2 v2.0.0-20200923005554-4646bfea2ef1/go.mod h1:fdlDtQlzundleLLz/ggoYinEt/LmnrpNKcNTABQATNI= 105 | github.com/juju/version v0.0.0-20161031051906-1f41e27e54f2/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U= 106 | github.com/juju/version v0.0.0-20180108022336-b64dbd566305/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U= 107 | github.com/juju/version v0.0.0-20191219164919-81c1be00b9a6/go.mod h1:kE8gK5X0CImdr7qpSKl3xB2PmpySSmfj7zVbkZFs81U= 108 | github.com/julienschmidt/httprouter v1.1.1-0.20151013225520-77a895ad01eb/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 109 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 110 | github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= 111 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 112 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 113 | github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= 114 | github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= 115 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 116 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 117 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 118 | github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= 119 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 120 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 121 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 122 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 123 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 124 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 125 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 126 | github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8= 127 | github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 128 | github.com/lunixbochs/vtclean v0.0.0-20160125035106-4fbf7632a2c6/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= 129 | github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= 130 | github.com/masterzen/azure-sdk-for-go v3.2.0-beta.0.20161014135628-ee4f0065d00c+incompatible/go.mod h1:mf8fjOu33zCqxUjuiU3I8S1lJMyEAlH+0F2+M5xl3hE= 131 | github.com/masterzen/simplexml v0.0.0-20160608183007-4572e39b1ab9/go.mod h1:kCEbxUJlNDEBNbdQMkPSp6yaKcRXVI6f4ddk8Riv4bc= 132 | github.com/masterzen/winrm v0.0.0-20161014151040-7a535cd943fc/go.mod h1:CfZSN7zwz5gJiFhZJz49Uzk7mEBHIceWmbFmYx7Hf7E= 133 | github.com/masterzen/xmlpath v0.0.0-20140218185901-13f4951698ad/go.mod h1:A0zPC53iKKKcXYxr4ROjpQRQ5FgJXtelNdSmHHuq/tY= 134 | github.com/mattn/go-colorable v0.0.6/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= 135 | github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= 136 | github.com/mattn/go-isatty v0.0.0-20160806122752-66b8e73f3f5c/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= 137 | github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= 138 | github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= 139 | github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= 140 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 141 | github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= 142 | github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 143 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 144 | github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= 145 | github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 146 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 147 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 148 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 149 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 150 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 151 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 152 | github.com/nelsam/hel/v2 v2.3.2/go.mod h1:1ZTGfU2PFTOd5mx22i5O0Lc2GY933lQ2wb/ggy+rL3w= 153 | github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U= 154 | github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= 155 | github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 156 | github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= 157 | github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= 158 | github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs= 159 | github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4= 160 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 161 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 162 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 163 | github.com/poy/onpar v0.0.0-20200406201722-06f95a1c68e8/go.mod h1:nSbFQvMj97ZyhFRSJYtut+msi4sOY6zJDGCdSc+/rZU= 164 | github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= 165 | github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= 166 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 167 | github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= 168 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 169 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 170 | github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= 171 | github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 172 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 173 | github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 174 | github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= 175 | github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 176 | github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 177 | github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 178 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 179 | github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= 180 | github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 181 | github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 182 | github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= 183 | github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= 184 | github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 185 | github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 186 | github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= 187 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 188 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 189 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 190 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 191 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 192 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 193 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 194 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 195 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 196 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 197 | github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 198 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 199 | github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= 200 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 201 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 202 | github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= 203 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 204 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 205 | github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= 206 | github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= 207 | github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 208 | github.com/ziutek/mymysql v1.5.4 h1:GB0qdRGsTwQSBVYuVShFBKaXSnSnYYC2d9knnE1LHFs= 209 | github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0= 210 | go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= 211 | go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= 212 | go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= 213 | go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= 214 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 215 | golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= 216 | golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 217 | golang.org/x/crypto v0.0.0-20180214000028-650f4a345ab4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 218 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 219 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 220 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 221 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 222 | golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 223 | golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= 224 | golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= 225 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 226 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 227 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 228 | golang.org/x/net v0.0.0-20180406214816-61147c48b25b/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 229 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 230 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 231 | golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 232 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 233 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 234 | golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 235 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 236 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 237 | golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 238 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 239 | golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= 240 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 241 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 242 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 243 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 244 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 245 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 246 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 247 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 248 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 249 | golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 250 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 251 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 252 | golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 253 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 254 | golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 255 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 256 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 257 | golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 258 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 259 | golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= 260 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 261 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 262 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 263 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 264 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 265 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 266 | golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 267 | golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 268 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 269 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 270 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 271 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 272 | golang.org/x/tools v0.0.0-20200313205530-4303120df7d8/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= 273 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 274 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 275 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 276 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 277 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 278 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 279 | google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= 280 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 281 | google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= 282 | google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= 283 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 284 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 285 | gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 286 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 287 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 288 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 289 | gopkg.in/errgo.v1 v1.0.0-20161222125816-442357a80af5/go.mod h1:u0ALmqvLRxLI95fkdCEWrE6mhWYZW1aMOJHp5YXLHTg= 290 | gopkg.in/httprequest.v1 v1.1.1/go.mod h1:/CkavNL+g3qLOrpFHVrEx4NKepeqR4XTZWNj4sGGjz0= 291 | gopkg.in/mgo.v2 v2.0.0-20160818015218-f2b6f6c918c4/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= 292 | gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22/go.mod h1:yeKp02qBN3iKW1OzL3MGk2IdtZzaj7SFntXj72NppTA= 293 | gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= 294 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 295 | gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637/go.mod h1:BHsqpu/nsuzkT5BpiH1EMZPLyqSMM8JbIavyFACoFNk= 296 | gopkg.in/yaml.v2 v2.0.0-20170712054546-1be3d31502d6/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 297 | gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= 298 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 299 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 300 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 301 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 302 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 303 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 304 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 305 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 306 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 307 | launchpad.net/gocheck v0.0.0-20140225173054-000000000087/go.mod h1:hj7XX3B/0A+80Vse0e+BUHsHMTEhd0O4cpUHr/e/BUM= 308 | launchpad.net/xmlpath v0.0.0-20130614043138-000000000004/go.mod h1:vqyExLOM3qBx7mvYRkoxjSCF945s0mbe7YynlKYXtsA= 309 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 310 | sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= 311 | sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 312 | -------------------------------------------------------------------------------- /iffy/README: -------------------------------------------------------------------------------- 1 | iffy helps you test http handlers. 2 | 3 | We assume JSON for any marshaling/unmarshaling. If you use something else, that's fine, but the advanced features (responseObject + templating) will not work. 4 | 5 | Example: 6 | 7 | func TestFoo(t *testing.T) { 8 | 9 | // Instantiate & configure anything that implements http.Handler 10 | r := gin.Default() 11 | r.GET("/hello", tonic.Handler(helloHandler, 200)) 12 | r.POST("/foo", tonic.Handler(newFoo, 201)) 13 | r.DELETE("/foo/:fooid", tonic.Handler(delFoo, 204)) 14 | 15 | tester := iffy.NewTester(t, r) 16 | 17 | // Variadic list of checker functions = func(r *http.Response, body string, responseObject interface{}) error 18 | // 19 | // Checkers can use closures to trap checker-specific configs -> ExpectStatus(200) 20 | // Some are provided in the iffy package, but you can use your own Checker functions 21 | tester.AddCall("helloworld", "GET", "/hello?who=world", "").Checkers(iffy.ExpectStatus(200), iffy.ExpectJSONFields("msg", "bla")) 22 | tester.AddCall("badhello", "GET", "/hello", "").Checkers(iffy.ExpectStatus(400)) 23 | 24 | // Optionally, pass an instantiated response object ( &Foo{} ) 25 | // The response body will be unmarshaled into it, then it will be presented to the Checker functions (parameter 'responseObject') 26 | // That way your custom checkers can directly use your business objects (ExpectValidFoo) 27 | tester.AddCall("createfoo", "POST", "/foo", `{"bar": "baz"}`).ResponseObject(&Foo{}).Checkers(iffy.ExpectStatus(201), ExpectValidFoo) 28 | 29 | // You can template query string and/or body using partial results from previous calls 30 | // e.g.: delete the object that was created in a previous step 31 | tester.AddCall("deletefoo", "DELETE", "/foo/{{.createfoo.id}}", "").Checkers(iffy.ExpectStatus(204)) 32 | 33 | tester.Run() 34 | } 35 | 36 | For a real-life example, see https://github.com/loopfz/gadgeto/blob/master/tonic/tonic_test.go 37 | -------------------------------------------------------------------------------- /iffy/iffy.go: -------------------------------------------------------------------------------- 1 | package iffy 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "net/http" 10 | "net/http/httptest" 11 | "testing" 12 | "text/template" 13 | ) 14 | 15 | type Tester struct { 16 | t *testing.T 17 | r http.Handler 18 | Calls []*Call 19 | values Values 20 | Fatal bool 21 | } 22 | 23 | type Headers map[string]string 24 | 25 | type Call struct { 26 | Name string 27 | Method string 28 | QueryStr string 29 | Body string 30 | headers Headers 31 | host string 32 | respObject interface{} 33 | checkers []Checker 34 | } 35 | 36 | func (c *Call) ResponseObject(respObject interface{}) *Call { 37 | c.respObject = respObject 38 | return c 39 | } 40 | 41 | func (c *Call) Headers(h Headers) *Call { 42 | c.headers = h 43 | return c 44 | } 45 | 46 | func (c *Call) Host(h string) *Call { 47 | c.host = h 48 | return c 49 | } 50 | 51 | func (c *Call) Checkers(ch ...Checker) *Call { 52 | c.checkers = ch 53 | return c 54 | } 55 | 56 | type Checker func(r *http.Response, body string, respObject interface{}) error 57 | 58 | // Tester 59 | 60 | func NewTester(t *testing.T, r http.Handler, calls ...*Call) *Tester { 61 | return &Tester{ 62 | t: t, 63 | r: r, 64 | values: make(Values), 65 | } 66 | } 67 | 68 | func (t *Tester) Reset() { 69 | t.Calls = []*Call{} 70 | } 71 | 72 | func (t *Tester) AddCall(name, method, querystr, body string) *Call { 73 | c := &Call{ 74 | Name: name, 75 | Method: method, 76 | QueryStr: querystr, 77 | Body: body, 78 | } 79 | t.Calls = append(t.Calls, c) 80 | return c 81 | } 82 | 83 | func (it *Tester) Run() { 84 | for _, c := range it.Calls { 85 | it.t.Run(c.Name, func(t *testing.T) { 86 | body := bytes.NewBufferString(it.applyTemplate(c.Body)) 87 | requestURI := it.applyTemplate(c.QueryStr) 88 | 89 | req, err := http.NewRequest(c.Method, requestURI, body) 90 | if err != nil { 91 | t.Error(err) 92 | return 93 | } 94 | 95 | // Save unparsed url for http routers whi use it 96 | req.RequestURI = requestURI 97 | 98 | if c.Body != "" { 99 | req.Header.Set("content-type", "application/json") 100 | } 101 | if c.headers != nil { 102 | for k, v := range c.headers { 103 | req.Header.Set(it.applyTemplate(k), it.applyTemplate(v)) 104 | } 105 | } 106 | if c.host != "" { 107 | req.Host = c.host 108 | } 109 | w := httptest.NewRecorder() 110 | it.r.ServeHTTP(w, req) 111 | resp := w.Result() 112 | var respBody string 113 | if resp.Body != nil { 114 | rb, err := ioutil.ReadAll(resp.Body) 115 | if err != nil { 116 | t.Error(err) 117 | } 118 | respBody = string(rb) 119 | resp.Body.Close() 120 | if c.respObject != nil { 121 | err = json.Unmarshal(rb, c.respObject) 122 | if err != nil { 123 | t.Error(err) 124 | } 125 | } 126 | 127 | dec := json.NewDecoder(bytes.NewBuffer(rb)) 128 | dec.UseNumber() 129 | 130 | var retJson interface{} 131 | err = dec.Decode(&retJson) 132 | if err == nil { 133 | it.values[c.Name] = retJson 134 | } 135 | } 136 | failed := false 137 | for _, checker := range c.checkers { 138 | err = checker(resp, respBody, c.respObject) 139 | if err != nil { 140 | t.Errorf("%s: %s", c.Name, err) 141 | failed = true 142 | } 143 | } 144 | if failed && it.Fatal { 145 | t.FailNow() 146 | } 147 | }) 148 | } 149 | } 150 | 151 | func (t *Tester) applyTemplate(s string) string { 152 | b, err := t.values.Apply(s) 153 | if err != nil { 154 | t.t.Error(err) 155 | return "" 156 | } 157 | return string(b) 158 | } 159 | 160 | type Values map[string]interface{} 161 | 162 | func (v Values) Apply(templateStr string) ([]byte, error) { 163 | 164 | var funcMap = template.FuncMap{ 165 | "field": v.fieldTmpl, 166 | "json": v.jsonFieldTmpl, 167 | } 168 | 169 | tmpl, err := template.New("tmpl").Funcs(funcMap).Parse(templateStr) 170 | if err != nil { 171 | return nil, err 172 | } 173 | 174 | b := new(bytes.Buffer) 175 | 176 | err = tmpl.Execute(b, v) 177 | if err != nil { 178 | return nil, err 179 | } 180 | 181 | return b.Bytes(), nil 182 | } 183 | 184 | // templating funcs 185 | 186 | func (v Values) fieldTmpl(key ...string) (interface{}, error) { 187 | var i interface{} 188 | 189 | i = map[string]interface{}(v) 190 | var ok bool 191 | 192 | for _, k := range key { 193 | switch i.(type) { 194 | case map[string]interface{}: 195 | i, ok = i.(map[string]interface{})[k] 196 | if !ok { 197 | i = "" 198 | } 199 | case map[string]string: 200 | i, ok = i.(map[string]string)[k] 201 | if !ok { 202 | i = "" 203 | } 204 | default: 205 | return nil, fmt.Errorf("cannot dereference %T", i) 206 | } 207 | } 208 | return i, nil 209 | } 210 | 211 | func (v Values) jsonFieldTmpl(key ...string) (interface{}, error) { 212 | i, err := v.fieldTmpl(key...) 213 | if err != nil { 214 | return nil, err 215 | } 216 | marshalled, err := json.Marshal(i) 217 | if err != nil { 218 | return nil, err 219 | } 220 | return string(marshalled), nil 221 | } 222 | 223 | // BUILT IN CHECKERS 224 | 225 | func ExpectStatus(st int) Checker { 226 | return func(r *http.Response, body string, respObject interface{}) error { 227 | if r.StatusCode != st { 228 | return fmt.Errorf("Bad status code: expected %d, got %d", st, r.StatusCode) 229 | } 230 | return nil 231 | } 232 | } 233 | 234 | func DumpResponse(t *testing.T) Checker { 235 | return func(r *http.Response, body string, respObject interface{}) error { 236 | t.Log(body) 237 | return nil 238 | } 239 | } 240 | 241 | func UnmarshalResponse(i interface{}) Checker { 242 | return func(r *http.Response, body string, respObject interface{}) error { 243 | return json.Unmarshal([]byte(body), i) 244 | } 245 | } 246 | 247 | func ExpectJSONFields(fields ...string) Checker { 248 | return func(r *http.Response, body string, respObject interface{}) error { 249 | m := map[string]interface{}{} 250 | err := json.Unmarshal([]byte(body), &m) 251 | if err != nil { 252 | return err 253 | } 254 | for _, f := range fields { 255 | if _, ok := m[f]; !ok { 256 | return fmt.Errorf("Missing expected field '%s'", f) 257 | } 258 | } 259 | return nil 260 | } 261 | } 262 | 263 | func ExpectListLength(length int) Checker { 264 | return func(r *http.Response, body string, respObject interface{}) error { 265 | l := []interface{}{} 266 | err := json.Unmarshal([]byte(body), &l) 267 | if err != nil { 268 | return err 269 | } 270 | if len(l) != length { 271 | return fmt.Errorf("Expected a list of length %d, got %d", length, len(l)) 272 | } 273 | return nil 274 | } 275 | } 276 | 277 | func ExpectListNonEmpty(r *http.Response, body string, respObject interface{}) error { 278 | l := []interface{}{} 279 | err := json.Unmarshal([]byte(body), &l) 280 | if err != nil { 281 | return err 282 | } 283 | if len(l) == 0 { 284 | return errors.New("Expected a non empty list") 285 | } 286 | return nil 287 | } 288 | 289 | func ExpectJSONBranch(nodes ...string) Checker { 290 | return func(r *http.Response, body string, respObject interface{}) error { 291 | m := map[string]interface{}{} 292 | err := json.Unmarshal([]byte(body), &m) 293 | if err != nil { 294 | return err 295 | } 296 | for i, n := range nodes { 297 | v, ok := m[n] 298 | if !ok { 299 | return fmt.Errorf("Missing node '%s'", n) 300 | } 301 | if child, ok := v.(map[string]interface{}); ok { 302 | m = child 303 | } else if i == len(nodes)-2 { 304 | // last child is not an object anymore 305 | // and there's only one more node to check 306 | // test last child against last provided node 307 | lastNode := nodes[i+1] 308 | if fmt.Sprintf("%v", v) != lastNode { 309 | return fmt.Errorf("Wrong value: expected '%v', got '%v'", lastNode, v) 310 | } 311 | return nil 312 | } 313 | } 314 | return nil 315 | } 316 | } 317 | -------------------------------------------------------------------------------- /iffy/iffy_test.go: -------------------------------------------------------------------------------- 1 | package iffy_test 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/loopfz/gadgeto/iffy" 10 | "github.com/loopfz/gadgeto/tonic" 11 | ) 12 | 13 | func helloHandler(c *gin.Context) (interface{}, error) { 14 | who, has := c.GetQuery("who") 15 | if !has { 16 | return nil, fmt.Errorf("wrong request") 17 | } 18 | return &struct { 19 | Msg string `json:"msg"` 20 | }{Msg: who}, nil 21 | } 22 | func newFoo(c *gin.Context) error { return nil } 23 | func delFoo(c *gin.Context) error { return nil } 24 | 25 | type Foo struct{} 26 | 27 | func ExpectValidFoo(r *http.Response, body string, respObject interface{}) error { return nil } 28 | 29 | func Test_Tester_Run(t *testing.T) { 30 | // Instantiate & configure anything that implements http.Handler 31 | gin.SetMode(gin.ReleaseMode) 32 | r := gin.Default() 33 | 34 | r.GET("/hello", tonic.Handler(helloHandler, 200)) 35 | r.POST("/foo", tonic.Handler(newFoo, 201)) 36 | r.DELETE("/foo/:fooid", tonic.Handler(delFoo, 204)) 37 | 38 | tester := iffy.NewTester(t, r) 39 | 40 | // Variadic list of checker functions = func(r *http.Response, body string, responseObject interface{}) error 41 | // 42 | // Checkers can use closures to trap checker-specific configs -> ExpectStatus(200) 43 | // Some are provided in the iffy package, but you can use your own Checker functions 44 | tester.AddCall("helloworld", "GET", "/hello?who=world", "").Checkers(iffy.ExpectStatus(200), iffy.ExpectJSONFields("msg")) 45 | tester.AddCall("badhello", "GET", "/hello", "").Checkers(iffy.ExpectStatus(400)) 46 | 47 | tester.Run() 48 | } 49 | -------------------------------------------------------------------------------- /iffy/package.go: -------------------------------------------------------------------------------- 1 | // Package iffy helps you test http handlers. 2 | // 3 | // We assume JSON for any marshaling/unmarshaling. If you use something else, that's fine, but the advanced features (responseObject + templating) will not work. 4 | // For a real-life example, see https://github.com/loopfz/gadgeto/blob/master/tonic/tonic_test.go 5 | package iffy 6 | -------------------------------------------------------------------------------- /tonic/README: -------------------------------------------------------------------------------- 1 | tonic lets you write simpler gin handlers. 2 | The way it works is that it generates wrapping gin-compatible handlers, 3 | that do all the repetitive work and wrap the call to your simple tonic handler. 4 | 5 | Package tonic handles path/query/body parameter binding in a single consolidated input object 6 | which allows you to remove all the boilerplate code that retrieves and tests the presence 7 | of various parameters. 8 | 9 | Here is an example input object. 10 | 11 | type MyInput struct { 12 | Foo int `path:"foo"` 13 | Bar string `query:"bar" default:"foobar"` 14 | Baz string `json:"baz"` 15 | } 16 | 17 | Output objects can be of any type, and will be marshaled to JSON. 18 | 19 | Input validation is performed after binding into the object using the validator library 20 | (https://github.com/go-playground/validator). You can use the tag 'validate' on your object 21 | definition to perform validation inside the tonic handler phase. 22 | 23 | type MyInput struct { 24 | Foo int `path:"foo" validate:"required,gt=10"` 25 | Bar string `query:"bar" default:"foobar" validate:"nefield=Baz"` 26 | Baz string `json:"baz" validate:"required,email"` 27 | } 28 | 29 | enum input validation is also implemented natively by tonic, and can check that the provided input 30 | value corresponds to one of the expected enum values. 31 | 32 | type MyInput struct { 33 | Bar string `query:"bar" enum:"foo,buz,biz"` 34 | } 35 | 36 | 37 | The handler can return an error, which will be returned to the caller. 38 | 39 | Here is a basic application that greets a user on http://localhost:8080/hello/me 40 | 41 | import ( 42 | "errors" 43 | "fmt" 44 | 45 | "github.com/gin-gonic/gin" 46 | "github.com/loopfz/gadgeto/tonic" 47 | ) 48 | 49 | type GreetUserInput struct { 50 | Name string `path:"name" validate:"required,gt=3" description:"User name"` 51 | } 52 | 53 | type GreetUserOutput struct { 54 | Message string `json:"message"` 55 | } 56 | 57 | func GreetUser(c *gin.Context, in *GreetUserInput) (*GreetUserOutput, error) { 58 | if in.Name == "satan" { 59 | return nil, errors.New("go to hell") 60 | } 61 | return &GreetUserOutput{Message: fmt.Sprintf("Hello %s!", in.Name)}, nil 62 | } 63 | 64 | func main() { 65 | r := gin.Default() 66 | r.GET("/hello/:name", tonic.Handler(GreetUser, 200)) 67 | r.Run(":8080") 68 | } 69 | 70 | 71 | If needed, you can also override different parts of the logic via certain available hooks in tonic: 72 | - binding 73 | - error handling 74 | - render 75 | 76 | We provide defaults for these (bind from JSON, render into JSON, error = http status 400). 77 | You will probably want to customize the error hook, to produce finer grained error status codes. 78 | 79 | The role of this error hook is to inspect the returned error object and deduce the http specifics from it. 80 | We provide a ready-to-use error hook that depends on the juju/errors package (richer errors): 81 | https://github.com/loopfz/gadgeto/tree/master/tonic/utils/jujerr 82 | 83 | Example of the same application as before, using juju errors: 84 | 85 | import ( 86 | "fmt" 87 | 88 | "github.com/gin-gonic/gin" 89 | "github.com/juju/errors" 90 | "github.com/loopfz/gadgeto/tonic" 91 | "github.com/loopfz/gadgeto/tonic/utils/jujerr" 92 | ) 93 | 94 | type GreetUserInput struct { 95 | Name string `path:"name" description:"User name" validate:"required"` 96 | } 97 | 98 | type GreetUserOutput struct { 99 | Message string `json:"message"` 100 | } 101 | 102 | func GreetUser(c *gin.Context, in *GreetUserInput) (*GreetUserOutput, error) { 103 | if in.Name == "satan" { 104 | return nil, errors.NewForbidden(nil, "go to hell") 105 | } 106 | return &GreetUserOutput{Message: fmt.Sprintf("Hello %s!", in.Name)}, nil 107 | } 108 | 109 | func main() { 110 | tonic.SetErrorHook(jujerr.ErrHook) 111 | r := gin.Default() 112 | r.GET("/hello/:name", tonic.Handler(GreetUser, 200)) 113 | r.Run(":8080") 114 | } 115 | 116 | 117 | You can also easily serve auto-generated swagger documentation (using tonic data) with https://github.com/wi2l/fizz 118 | -------------------------------------------------------------------------------- /tonic/handler.go: -------------------------------------------------------------------------------- 1 | package tonic 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "runtime" 7 | "strconv" 8 | "strings" 9 | "sync" 10 | 11 | "github.com/gin-gonic/gin" 12 | validator "github.com/go-playground/validator/v10" 13 | "github.com/google/uuid" 14 | ) 15 | 16 | var ( 17 | validatorObj *validator.Validate 18 | validatorOnce sync.Once 19 | ) 20 | 21 | // Handler returns a Gin HandlerFunc that wraps the handler passed 22 | // in parameters. 23 | // The handler may use the following signature: 24 | // 25 | // func(*gin.Context, [input object ptr]) ([output object], error) 26 | // 27 | // Input and output objects are both optional. 28 | // As such, the minimal accepted signature is: 29 | // 30 | // func(*gin.Context) error 31 | // 32 | // The wrapping gin-handler will bind the parameters from the query-string, 33 | // path, body and headers, and handle the errors. 34 | // 35 | // Handler will panic if the tonic handler or its input/output values 36 | // are of incompatible type. 37 | func Handler(h interface{}, status int, options ...func(*Route)) gin.HandlerFunc { 38 | hv := reflect.ValueOf(h) 39 | 40 | if hv.Kind() != reflect.Func { 41 | panic(fmt.Sprintf("handler parameters must be a function, got %T", h)) 42 | } 43 | ht := hv.Type() 44 | fname := fmt.Sprintf("%s_%s", runtime.FuncForPC(hv.Pointer()).Name(), uuid.Must(uuid.NewRandom()).String()) 45 | 46 | in := input(ht, fname) 47 | out := output(ht, fname) 48 | 49 | // Wrap Gin handler. 50 | f := func(c *gin.Context) { 51 | _, ok := c.Get(tonicWantRouteInfos) 52 | if ok { 53 | r := &Route{} 54 | r.defaultStatusCode = status 55 | r.handler = hv 56 | r.handlerType = ht 57 | r.inputType = in 58 | r.outputType = out 59 | for _, opt := range options { 60 | opt(r) 61 | } 62 | c.Set(tonicRoutesInfos, r) 63 | c.Abort() 64 | return 65 | } 66 | // funcIn contains the input parameters of the 67 | // tonic handler call. 68 | args := []reflect.Value{reflect.ValueOf(c)} 69 | 70 | // Tonic handler has custom input, handle 71 | // binding. 72 | if in != nil { 73 | input := reflect.New(in) 74 | // Bind the body with the hook. 75 | if err := bindHook(c, input.Interface()); err != nil { 76 | handleError(c, BindError{message: err.Error(), typ: in}) 77 | return 78 | } 79 | // Bind query-parameters. 80 | if err := bind(c, input, QueryTag, extractQuery); err != nil { 81 | handleError(c, err) 82 | return 83 | } 84 | // Bind path arguments. 85 | if err := bind(c, input, PathTag, extractPath); err != nil { 86 | handleError(c, err) 87 | return 88 | } 89 | // Bind headers. 90 | if err := bind(c, input, HeaderTag, extractHeader); err != nil { 91 | handleError(c, err) 92 | return 93 | } 94 | // validating query and path inputs if they have a validate tag 95 | initValidator() 96 | args = append(args, input) 97 | if err := validatorObj.Struct(input.Interface()); err != nil { 98 | handleError(c, BindError{message: err.Error(), validationErr: err}) 99 | return 100 | } 101 | } 102 | // Call tonic handler with the arguments 103 | // and extract the returned values. 104 | var err, val interface{} 105 | 106 | ret := hv.Call(args) 107 | if out != nil { 108 | val = ret[0].Interface() 109 | err = ret[1].Interface() 110 | } else { 111 | err = ret[0].Interface() 112 | } 113 | // Handle the error returned by the 114 | // handler invocation, if any. 115 | if err != nil { 116 | handleError(c, err.(error)) 117 | return 118 | } 119 | renderHook(c, status, val) 120 | } 121 | // Register route in tonic-enabled routes map 122 | route := &Route{ 123 | defaultStatusCode: status, 124 | handler: hv, 125 | handlerType: ht, 126 | inputType: in, 127 | outputType: out, 128 | } 129 | for _, opt := range options { 130 | opt(route) 131 | } 132 | routesMu.Lock() 133 | routes[fname] = route 134 | routesMu.Unlock() 135 | 136 | ret := func(c *gin.Context) { execHook(c, f, fname) } 137 | 138 | funcsMu.Lock() 139 | defer funcsMu.Unlock() 140 | funcs[runtime.FuncForPC(reflect.ValueOf(ret).Pointer()).Name()] = struct{}{} 141 | 142 | return ret 143 | } 144 | 145 | // RegisterValidation registers a custom validation on the validator.Validate instance of the package 146 | // NOTE: calling this function may instantiate the validator itself. 147 | // NOTE: this function is not thread safe, since the validator validation registration isn't 148 | func RegisterValidation(tagName string, validationFunc validator.Func) error { 149 | initValidator() 150 | return validatorObj.RegisterValidation(tagName, validationFunc) 151 | } 152 | 153 | // RegisterTagNameFunc registers a function to get alternate names for StructFields. 154 | // 155 | // eg. to use the names which have been specified for JSON representations of structs, rather than normal Go field names: 156 | // 157 | // tonic.RegisterTagNameFunc(func(fld reflect.StructField) string { 158 | // name := strings.SplitN(fld.Tag.Get("json"), ",", 2)[0] 159 | // if name == "-" { 160 | // return "" 161 | // } 162 | // return name 163 | // }) 164 | func RegisterTagNameFunc(registerTagFunc validator.TagNameFunc) { 165 | initValidator() 166 | validatorObj.RegisterTagNameFunc(registerTagFunc) 167 | } 168 | 169 | func initValidator() { 170 | validatorOnce.Do(func() { 171 | validatorObj = validator.New() 172 | validatorObj.SetTagName(ValidationTag) 173 | }) 174 | } 175 | 176 | // bind binds the fields the fields of the input object in with 177 | // the values of the parameters extracted from the Gin context. 178 | // It reads tag to know what to extract using the extractor func. 179 | func bind(c *gin.Context, v reflect.Value, tag string, extract extractor) error { 180 | t := v.Type() 181 | 182 | if t.Kind() == reflect.Ptr { 183 | t = t.Elem() 184 | v = v.Elem() 185 | } 186 | for i := 0; i < t.NumField(); i++ { 187 | ft := t.Field(i) 188 | field := v.Field(i) 189 | 190 | // Handle embedded fields with a recursive call. 191 | // If the field is a pointer, but is nil, we 192 | // create a new value of the same type, or we 193 | // take the existing memory address. 194 | if ft.Anonymous { 195 | if field.Kind() == reflect.Ptr { 196 | if field.IsNil() { 197 | field.Set(reflect.New(field.Type().Elem())) 198 | } 199 | } else { 200 | if field.CanAddr() { 201 | field = field.Addr() 202 | } 203 | } 204 | err := bind(c, field, tag, extract) 205 | if err != nil { 206 | return err 207 | } 208 | continue 209 | } 210 | tagValue := ft.Tag.Get(tag) 211 | if tagValue == "" { 212 | continue 213 | } 214 | // Set-up context for extractors. 215 | // Query. 216 | c.Set(ExplodeTag, true) // default 217 | if explodeVal, ok := ft.Tag.Lookup(ExplodeTag); ok { 218 | if explode, err := strconv.ParseBool(explodeVal); err == nil && !explode { 219 | c.Set(ExplodeTag, false) 220 | } 221 | } 222 | _, fieldValues, err := extract(c, tagValue) 223 | if err != nil { 224 | return BindError{field: ft.Name, typ: t, message: err.Error()} 225 | } 226 | // Extract default value and use it in place 227 | // if no values were returned. 228 | def, ok := ft.Tag.Lookup(DefaultTag) 229 | if ok && len(fieldValues) == 0 { 230 | if c.GetBool(ExplodeTag) { 231 | fieldValues = append(fieldValues, strings.Split(def, ",")...) 232 | } else { 233 | fieldValues = append(fieldValues, def) 234 | } 235 | } 236 | if len(fieldValues) == 0 { 237 | continue 238 | } 239 | // If the field is a nil pointer to a concrete type, 240 | // create a new addressable value for this type. 241 | if field.Kind() == reflect.Ptr && field.IsNil() { 242 | f := reflect.New(field.Type().Elem()) 243 | field.Set(f) 244 | } 245 | // Dereference pointer. 246 | if field.Kind() == reflect.Ptr { 247 | field = field.Elem() 248 | } 249 | kind := field.Kind() 250 | 251 | // Multiple values can only be filled to types 252 | // Slice and Array. 253 | if len(fieldValues) > 1 && (kind != reflect.Slice && kind != reflect.Array) { 254 | return BindError{field: ft.Name, typ: t, message: "multiple values not supported"} 255 | } 256 | // Ensure that the number of values to fill does 257 | // not exceed the length of a field of type Array. 258 | if kind == reflect.Array { 259 | if field.Len() != len(fieldValues) { 260 | return BindError{field: ft.Name, typ: t, message: fmt.Sprintf( 261 | "parameter expect %d values, got %d", field.Len(), len(fieldValues)), 262 | } 263 | } 264 | } 265 | if kind == reflect.Slice || kind == reflect.Array { 266 | // Create a new slice with an adequate 267 | // length to set all the values. 268 | if kind == reflect.Slice { 269 | field.Set(reflect.MakeSlice(field.Type(), 0, len(fieldValues))) 270 | } 271 | for i, val := range fieldValues { 272 | v := reflect.New(field.Type().Elem()).Elem() 273 | err = bindStringValue(val, v) 274 | if err != nil { 275 | return BindError{field: ft.Name, typ: t, message: err.Error(), tagValue: tagValue, expectedType: field.Kind().String()} 276 | } 277 | if kind == reflect.Slice { 278 | field.Set(reflect.Append(field, v)) 279 | } 280 | if kind == reflect.Array { 281 | field.Index(i).Set(v) 282 | } 283 | } 284 | continue 285 | } 286 | // Handle enum values. 287 | enum := ft.Tag.Get(EnumTag) 288 | if enum != "" { 289 | enumValues := strings.Split(strings.TrimSpace(enum), ",") 290 | if len(enumValues) != 0 { 291 | if !contains(enumValues, fieldValues[0]) { 292 | return BindError{field: ft.Name, typ: t, message: fmt.Sprintf( 293 | "parameter has not an acceptable value, %s=%v", EnumTag, enumValues), 294 | } 295 | } 296 | } 297 | } 298 | // Fill string value into input field. 299 | err = bindStringValue(fieldValues[0], field) 300 | if err != nil { 301 | return BindError{field: ft.Name, typ: t, message: err.Error(), tagValue: tagValue, expectedType: field.Kind().String()} 302 | } 303 | } 304 | return nil 305 | } 306 | 307 | // input checks the input parameters of a tonic handler 308 | // and return the type of the second parameter, if any. 309 | func input(ht reflect.Type, name string) reflect.Type { 310 | n := ht.NumIn() 311 | if n < 1 || n > 2 { 312 | panic(fmt.Sprintf( 313 | "incorrect number of input parameters for handler %s, expected 1 or 2, got %d", 314 | name, n, 315 | )) 316 | } 317 | // First parameter of tonic handler must be 318 | // a pointer to a Gin context. 319 | if !ht.In(0).ConvertibleTo(reflect.TypeOf(&gin.Context{})) { 320 | panic(fmt.Sprintf( 321 | "invalid first parameter for handler %s, expected *gin.Context, got %v", 322 | name, ht.In(0), 323 | )) 324 | } 325 | if n == 2 { 326 | // Check the type of the second parameter 327 | // of the handler. Must be a pointer to a struct. 328 | if ht.In(1).Kind() != reflect.Ptr || ht.In(1).Elem().Kind() != reflect.Struct { 329 | panic(fmt.Sprintf( 330 | "invalid second parameter for handler %s, expected pointer to struct, got %v", 331 | name, ht.In(1), 332 | )) 333 | } else { 334 | return ht.In(1).Elem() 335 | } 336 | } 337 | return nil 338 | } 339 | 340 | // output checks the output parameters of a tonic handler 341 | // and return the type of the return type, if any. 342 | func output(ht reflect.Type, name string) reflect.Type { 343 | n := ht.NumOut() 344 | 345 | if n < 1 || n > 2 { 346 | panic(fmt.Sprintf( 347 | "incorrect number of output parameters for handler %s, expected 1 or 2, got %d", 348 | name, n, 349 | )) 350 | } 351 | // Check the type of the error parameter, which 352 | // should always come last. 353 | if !ht.Out(n - 1).Implements(reflect.TypeOf((*error)(nil)).Elem()) { 354 | panic(fmt.Sprintf( 355 | "unsupported type for handler %s output parameter: expected error interface, got %v", 356 | name, ht.Out(n-1), 357 | )) 358 | } 359 | if n == 2 { 360 | t := ht.Out(0) 361 | if t.Kind() == reflect.Ptr { 362 | t = t.Elem() 363 | } 364 | return t 365 | } 366 | return nil 367 | } 368 | 369 | // handleError handles any error raised during the execution 370 | // of the wrapping gin-handler. 371 | func handleError(c *gin.Context, err error) { 372 | if len(c.Errors) == 0 { 373 | c.Error(err) 374 | } 375 | code, resp := errorHook(c, err) 376 | renderHook(c, code, resp) 377 | } 378 | 379 | // contains returns whether in contain s. 380 | func contains(in []string, s string) bool { 381 | for _, v := range in { 382 | if v == s { 383 | return true 384 | } 385 | } 386 | return false 387 | } 388 | -------------------------------------------------------------------------------- /tonic/listen.go: -------------------------------------------------------------------------------- 1 | package tonic 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "net/http" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | "time" 11 | ) 12 | 13 | var defaultOpts = []ListenOptFunc{ 14 | ListenAddr(":8080"), 15 | CatchSignals(os.Interrupt, syscall.SIGTERM), 16 | ShutdownTimeout(10 * time.Second), 17 | ReadHeaderTimeout(5 * time.Second), 18 | WriteTimeout(30 * time.Second), 19 | KeepAliveTimeout(90 * time.Second), 20 | } 21 | 22 | func ListenAndServe(handler http.Handler, errorHandler func(error), opt ...ListenOptFunc) { 23 | 24 | listener := struct { 25 | net.Listener 26 | }{} 27 | srv := &http.Server{Handler: handler} 28 | 29 | listenOpt := &ListenOpt{Listener: &listener, Server: srv} 30 | 31 | for _, o := range defaultOpts { 32 | err := o(listenOpt) 33 | if err != nil { 34 | if errorHandler != nil { 35 | errorHandler(err) 36 | } 37 | return 38 | } 39 | } 40 | 41 | for _, o := range opt { 42 | err := o(listenOpt) 43 | if err != nil { 44 | if errorHandler != nil { 45 | errorHandler(err) 46 | } 47 | return 48 | } 49 | } 50 | 51 | stop := make(chan struct{}) 52 | 53 | go func() { 54 | var err error 55 | var ln net.Listener 56 | 57 | ln, err = net.Listen("tcp", listenOpt.Server.Addr) 58 | if err == nil { 59 | // delayed listen, store it in the original listener object so any wrapping listener from listenOpt 60 | // will have a correct reference 61 | listener.Listener = ln 62 | if srv.TLSConfig != nil && len(srv.TLSConfig.Certificates) > 0 { 63 | // ServeTLS without cert files lets listenOpts set srv.TLSConfig.Certificates 64 | err = listenOpt.Server.ServeTLS(listenOpt.Listener, "", "") 65 | } else { 66 | err = listenOpt.Server.Serve(listenOpt.Listener) 67 | } 68 | } 69 | if err != nil && err != http.ErrServerClosed && errorHandler != nil { 70 | errorHandler(err) 71 | } 72 | close(stop) 73 | }() 74 | 75 | sig := make(chan os.Signal) 76 | 77 | if len(listenOpt.Signals) > 0 { 78 | signal.Notify(sig, listenOpt.Signals...) 79 | } 80 | 81 | select { 82 | case <-sig: 83 | ctx, cancel := context.WithTimeout(context.Background(), listenOpt.ShutdownTimeout) 84 | defer cancel() 85 | 86 | err := srv.Shutdown(ctx) 87 | if err != nil && errorHandler != nil { 88 | errorHandler(err) 89 | } 90 | 91 | case <-stop: 92 | break 93 | 94 | } 95 | } 96 | 97 | // ListenOpt exposes the Server object so you may change its configuration 98 | // e.g. TLSConfig, and a Listener so that you may wrap it e.g. proxyprotocol 99 | type ListenOpt struct { 100 | Listener net.Listener 101 | Server *http.Server 102 | Signals []os.Signal 103 | ShutdownTimeout time.Duration 104 | } 105 | 106 | type ListenOptFunc func(*ListenOpt) error 107 | 108 | func CatchSignals(sig ...os.Signal) ListenOptFunc { 109 | return func(opt *ListenOpt) error { 110 | opt.Signals = sig 111 | return nil 112 | } 113 | } 114 | 115 | func ListenAddr(addr string) ListenOptFunc { 116 | return func(opt *ListenOpt) error { 117 | opt.Server.Addr = addr 118 | return nil 119 | } 120 | } 121 | 122 | func ReadTimeout(t time.Duration) ListenOptFunc { 123 | return func(opt *ListenOpt) error { 124 | opt.Server.ReadTimeout = t 125 | return nil 126 | } 127 | } 128 | 129 | func ReadHeaderTimeout(t time.Duration) ListenOptFunc { 130 | return func(opt *ListenOpt) error { 131 | opt.Server.ReadHeaderTimeout = t 132 | return nil 133 | } 134 | } 135 | 136 | func WriteTimeout(t time.Duration) ListenOptFunc { 137 | return func(opt *ListenOpt) error { 138 | opt.Server.WriteTimeout = t 139 | return nil 140 | } 141 | } 142 | 143 | func KeepAliveTimeout(t time.Duration) ListenOptFunc { 144 | return func(opt *ListenOpt) error { 145 | opt.Server.IdleTimeout = t 146 | return nil 147 | } 148 | } 149 | 150 | func ShutdownTimeout(t time.Duration) ListenOptFunc { 151 | return func(opt *ListenOpt) error { 152 | opt.ShutdownTimeout = t 153 | return nil 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /tonic/route.go: -------------------------------------------------------------------------------- 1 | package tonic 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "runtime" 7 | "strings" 8 | 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | // A Route contains information about a tonic-enabled route. 13 | type Route struct { 14 | gin.RouteInfo 15 | 16 | defaultStatusCode int 17 | description string 18 | summary string 19 | deprecated bool 20 | tags []string 21 | 22 | // Handler is the route handler. 23 | handler reflect.Value 24 | 25 | // HandlerType is the type of the route handler. 26 | handlerType reflect.Type 27 | 28 | // inputType is the type of the input object. 29 | // This can be nil if the handler use none. 30 | inputType reflect.Type 31 | 32 | // outputType is the type of the output object. 33 | // This can be nil if the handler use none. 34 | outputType reflect.Type 35 | } 36 | 37 | // GetVerb returns the HTTP verb of the route. 38 | func (r *Route) GetVerb() string { return r.Method } 39 | 40 | // GetPath returns the path of the route. 41 | func (r *Route) GetPath() string { return r.Path } 42 | 43 | // GetDescription returns the description of the route. 44 | func (r *Route) GetDescription() string { return r.description } 45 | 46 | // GetSummary returns the summary of the route. 47 | func (r *Route) GetSummary() string { return r.summary } 48 | 49 | // GetDefaultStatusCode returns the default status code of the route. 50 | func (r *Route) GetDefaultStatusCode() int { return r.defaultStatusCode } 51 | 52 | // GetHandler returns the handler of the route. 53 | func (r *Route) GetHandler() reflect.Value { return r.handler } 54 | 55 | // GetDeprecated returns the deprecated flag of the route. 56 | func (r *Route) GetDeprecated() bool { return r.deprecated } 57 | 58 | // InputType returns the input type of the handler. 59 | // If the type is a pointer to a concrete type, it 60 | // is dereferenced. 61 | func (r *Route) InputType() reflect.Type { 62 | if in := r.inputType; in != nil && in.Kind() == reflect.Ptr { 63 | return in.Elem() 64 | } 65 | return r.inputType 66 | } 67 | 68 | // OutputType returns the output type of the handler. 69 | // If the type is a pointer to a concrete type, it 70 | // is dereferenced. 71 | func (r *Route) OutputType() reflect.Type { 72 | if out := r.outputType; out != nil && out.Kind() == reflect.Ptr { 73 | return out.Elem() 74 | } 75 | return r.outputType 76 | } 77 | 78 | // HandlerName returns the name of the route handler. 79 | func (r *Route) HandlerName() string { 80 | parts := strings.Split(r.HandlerNameWithPackage(), ".") 81 | return parts[len(parts)-1] 82 | } 83 | 84 | // HandlerNameWithPackage returns the full name of the rout 85 | // handler with its package path. 86 | func (r *Route) HandlerNameWithPackage() string { 87 | f := runtime.FuncForPC(r.handler.Pointer()).Name() 88 | parts := strings.Split(f, "/") 89 | return parts[len(parts)-1] 90 | } 91 | 92 | // GetTags generates a list of tags for the swagger spec 93 | // from one route definition. 94 | // It uses the first chunk of the path of the route as the tag 95 | // (for example, in /foo/bar it will return the "foo" tag), 96 | // unless specific tags have been defined with tonic.Tags 97 | func (r *Route) GetTags() []string { 98 | if r.tags != nil { 99 | return r.tags 100 | } 101 | tags := make([]string, 0, 1) 102 | paths := strings.SplitN(r.GetPath(), "/", 3) 103 | if len(paths) > 1 { 104 | tags = append(tags, paths[1]) 105 | } 106 | return tags 107 | } 108 | 109 | // GetRouteByHandler returns the route informations of 110 | // the given wrapped handler. 111 | func GetRouteByHandler(h gin.HandlerFunc) (*Route, error) { 112 | ctx := &gin.Context{} 113 | ctx.Set(tonicWantRouteInfos, nil) 114 | 115 | funcsMu.Lock() 116 | defer funcsMu.Unlock() 117 | if _, ok := funcs[runtime.FuncForPC(reflect.ValueOf(h).Pointer()).Name()]; !ok { 118 | return nil, errors.New("handler is not wrapped by tonic") 119 | } 120 | h(ctx) 121 | 122 | i, ok := ctx.Get(tonicRoutesInfos) 123 | if !ok { 124 | return nil, errors.New("failed to retrieve handler infos") 125 | } 126 | route, ok := i.(*Route) 127 | if !ok { 128 | return nil, errors.New("failed to retrieve handler infos") 129 | } 130 | return route, nil 131 | } 132 | -------------------------------------------------------------------------------- /tonic/route_test.go: -------------------------------------------------------------------------------- 1 | package tonic_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/loopfz/gadgeto/tonic" 8 | ) 9 | 10 | func TestRoute_GetTags(t *testing.T) { 11 | r := &tonic.Route{ 12 | RouteInfo: gin.RouteInfo{ 13 | Method: "GET", 14 | Path: "/foo/bar", 15 | }, 16 | } 17 | tags := r.GetTags() 18 | if len(tags) != 1 { 19 | t.Fatalf("expected to have 1 tag, but got %d", len(tags)) 20 | } 21 | if tags[0] != "foo" { 22 | t.Fatalf("expected to have tag='foo', but got tag=%s", tags[0]) 23 | } 24 | tonic.Tags([]string{"otherTag"})(r) 25 | tags = r.GetTags() 26 | if len(tags) != 1 { 27 | t.Fatalf("expected to have 1 tag, but got %d", len(tags)) 28 | } 29 | if tags[0] != "otherTag" { 30 | t.Fatalf("expected to have tag='otherTag', but got tag=%s", tags[0]) 31 | } 32 | tonic.Tags([]string{"otherTag1", "otherTag2"})(r) 33 | tags = r.GetTags() 34 | if len(tags) != 2 { 35 | t.Fatalf("expected to have 2 tags, but got %d", len(tags)) 36 | } 37 | if tags[0] != "otherTag1" { 38 | t.Fatalf("expected to have tag='otherTag1', but got tag=%s", tags[0]) 39 | } 40 | if tags[1] != "otherTag2" { 41 | t.Fatalf("expected to have tag='otherTag2', but got tag=%s", tags[0]) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tonic/tonic.go: -------------------------------------------------------------------------------- 1 | package tonic 2 | 3 | import ( 4 | "bytes" 5 | "encoding" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "reflect" 11 | "strconv" 12 | "strings" 13 | "sync" 14 | "time" 15 | 16 | "github.com/gin-gonic/gin" 17 | "github.com/gin-gonic/gin/binding" 18 | validator "github.com/go-playground/validator/v10" 19 | "sigs.k8s.io/yaml" // sigs.k8s.io/yaml is the alternative to the unmaintained lib github.com/ghodss/yaml. cf https://github.com/ghodss/yaml/issues/80 20 | ) 21 | 22 | // DefaultMaxBodyBytes is the maximum allowed size of a request body in bytes. 23 | const DefaultMaxBodyBytes = 256 * 1024 24 | 25 | // Fields tags used by tonic. 26 | const ( 27 | QueryTag = "query" 28 | PathTag = "path" 29 | HeaderTag = "header" 30 | EnumTag = "enum" 31 | RequiredTag = "required" 32 | DefaultTag = "default" 33 | ValidationTag = "validate" 34 | ExplodeTag = "explode" 35 | ) 36 | 37 | const ( 38 | defaultMediaType = "application/json" 39 | tonicRoutesInfos = "_tonic_route_infos" 40 | tonicWantRouteInfos = "_tonic_want_route_infos" 41 | ) 42 | 43 | var ( 44 | errorHook ErrorHook = DefaultErrorHook 45 | bindHook BindHook = DefaultBindingHook 46 | renderHook RenderHook = DefaultRenderHook 47 | execHook ExecHook = DefaultExecHook 48 | 49 | mediaType = defaultMediaType 50 | 51 | routes = make(map[string]*Route) 52 | routesMu = sync.Mutex{} 53 | funcs = make(map[string]struct{}) 54 | funcsMu = sync.Mutex{} 55 | ) 56 | 57 | // BindHook is the hook called by the wrapping gin-handler when 58 | // binding an incoming request to the tonic-handler's input object. 59 | type BindHook func(*gin.Context, interface{}) error 60 | 61 | // RenderHook is the last hook called by the wrapping gin-handler 62 | // before returning. It takes the Gin context, the HTTP status code 63 | // and the response payload as parameters. 64 | // Its role is to render the payload to the client to the 65 | // proper format. 66 | type RenderHook func(*gin.Context, int, interface{}) 67 | 68 | // ErrorHook lets you interpret errors returned by your handlers. 69 | // After analysis, the hook should return a suitable http status code 70 | // and and error payload. 71 | // This lets you deeply inspect custom error types. 72 | type ErrorHook func(*gin.Context, error) (int, interface{}) 73 | 74 | // An ExecHook is the func called to handle a request. 75 | // The default ExecHook simply calle the wrapping gin-handler 76 | // with the gin context. 77 | type ExecHook func(*gin.Context, gin.HandlerFunc, string) 78 | 79 | // DefaultErrorHook is the default error hook. 80 | // It returns a StatusBadRequest with a payload containing 81 | // the error message. 82 | func DefaultErrorHook(c *gin.Context, e error) (int, interface{}) { 83 | return http.StatusBadRequest, gin.H{ 84 | "error": e.Error(), 85 | } 86 | } 87 | 88 | // DefaultBindingHook is the default binding hook. 89 | // It uses Gin JSON binding to bind the body parameters of the request 90 | // to the input object of the handler. 91 | // Ir teturns an error if Gin binding fails. 92 | var DefaultBindingHook BindHook = DefaultBindingHookMaxBodyBytes(DefaultMaxBodyBytes) 93 | 94 | // DefaultBindingHookMaxBodyBytes returns a BindHook with the default logic, with configurable MaxBodyBytes. 95 | func DefaultBindingHookMaxBodyBytes(maxBodyBytes int64) BindHook { 96 | return func(c *gin.Context, i interface{}) error { 97 | c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxBodyBytes) 98 | if c.Request.ContentLength == 0 || c.Request.Method == http.MethodGet { 99 | return nil 100 | } 101 | switch c.Request.Header.Get("Content-Type") { 102 | case "text/x-yaml", "text/yaml", "text/yml", "application/x-yaml", "application/x-yml", "application/yaml", "application/yml": 103 | if err := c.ShouldBindWith(i, yamlBinding{}); err != nil && err != io.EOF { 104 | return fmt.Errorf("error parsing request body: %s", err.Error()) 105 | } 106 | default: 107 | if err := c.ShouldBindWith(i, binding.JSON); err != nil && err != io.EOF { 108 | return fmt.Errorf("error parsing request body: %s", err.Error()) 109 | } 110 | } 111 | return nil 112 | } 113 | } 114 | 115 | // DefaultRenderHook is the default render hook. 116 | // It marshals the payload to JSON, or returns an empty body if the payload is nil. 117 | // If Gin is running in debug mode, the marshalled JSON is indented. 118 | func DefaultRenderHook(c *gin.Context, statusCode int, payload interface{}) { 119 | var status int 120 | if c.Writer.Written() { 121 | status = c.Writer.Status() 122 | } else { 123 | status = statusCode 124 | } 125 | if payload != nil { 126 | if gin.IsDebugging() { 127 | c.IndentedJSON(status, payload) 128 | } else { 129 | c.JSON(status, payload) 130 | } 131 | } else { 132 | c.String(status, "") 133 | } 134 | } 135 | 136 | // DefaultExecHook is the default exec hook. 137 | // It simply executes the wrapping gin-handler with 138 | // the given context. 139 | func DefaultExecHook(c *gin.Context, h gin.HandlerFunc, fname string) { 140 | h(c) 141 | } 142 | 143 | // GetRoutes returns the routes handled by a tonic-enabled handler. 144 | func GetRoutes() map[string]*Route { 145 | return routes 146 | } 147 | 148 | // MediaType returns the current media type (MIME) 149 | // used by the actual render hook. 150 | func MediaType() string { 151 | return defaultMediaType 152 | } 153 | 154 | // GetErrorHook returns the current error hook. 155 | func GetErrorHook() ErrorHook { 156 | return errorHook 157 | } 158 | 159 | // SetErrorHook sets the given hook as the 160 | // default error handling hook. 161 | func SetErrorHook(eh ErrorHook) { 162 | if eh != nil { 163 | errorHook = eh 164 | } 165 | } 166 | 167 | // GetBindHook returns the current bind hook. 168 | func GetBindHook() BindHook { 169 | return bindHook 170 | } 171 | 172 | // SetBindHook sets the given hook as the 173 | // default binding hook. 174 | func SetBindHook(bh BindHook) { 175 | if bh != nil { 176 | bindHook = bh 177 | } 178 | } 179 | 180 | // GetRenderHook returns the current render hook. 181 | func GetRenderHook() RenderHook { 182 | return renderHook 183 | } 184 | 185 | // SetRenderHook sets the given hook as the default 186 | // rendering hook. The media type is used to generate 187 | // the OpenAPI specification. 188 | func SetRenderHook(rh RenderHook, mt string) { 189 | if rh != nil { 190 | renderHook = rh 191 | } 192 | if mt != "" { 193 | mediaType = mt 194 | } 195 | } 196 | 197 | // SetExecHook sets the given hook as the 198 | // default execution hook. 199 | func SetExecHook(eh ExecHook) { 200 | if eh != nil { 201 | execHook = eh 202 | } 203 | } 204 | 205 | // GetExecHook returns the current execution hook. 206 | func GetExecHook() ExecHook { 207 | return execHook 208 | } 209 | 210 | // Description set the description of a route. 211 | func Description(s string) func(*Route) { 212 | return func(r *Route) { 213 | r.description = s 214 | } 215 | } 216 | 217 | // Summary set the summary of a route. 218 | func Summary(s string) func(*Route) { 219 | return func(r *Route) { 220 | r.summary = s 221 | } 222 | } 223 | 224 | // Deprecated set the deprecated flag of a route. 225 | func Deprecated(b bool) func(*Route) { 226 | return func(r *Route) { 227 | r.deprecated = b 228 | } 229 | } 230 | 231 | // Tags sets the tags of a route. 232 | func Tags(tags []string) func(*Route) { 233 | return func(r *Route) { 234 | r.tags = tags 235 | } 236 | } 237 | 238 | // BindError is an error type returned when tonic fails 239 | // to bind parameters, to differentiate from errors returned 240 | // by the handlers. 241 | type BindError struct { 242 | validationErr error 243 | message string 244 | typ reflect.Type 245 | field string 246 | tagValue string 247 | expectedType string 248 | } 249 | 250 | // Error implements the builtin error interface for BindError. 251 | func (be BindError) Error() string { 252 | if be.tagValue != "" && be.expectedType != "" { 253 | return fmt.Sprintf("expected '%s' to be of type %s", be.tagValue, be.expectedType) 254 | } 255 | 256 | if be.field != "" && be.typ != nil { 257 | return fmt.Sprintf( 258 | "binding error on field '%s' of type '%s': %s", 259 | be.field, 260 | be.typ.Name(), 261 | be.message, 262 | ) 263 | } 264 | return fmt.Sprintf("binding error: %s", be.message) 265 | } 266 | 267 | // ValidationErrors returns the errors from the validate process. 268 | func (be BindError) ValidationErrors() validator.ValidationErrors { 269 | switch t := be.validationErr.(type) { 270 | case validator.ValidationErrors: 271 | return t 272 | } 273 | return nil 274 | } 275 | 276 | // An extractorFunc extracts data from a gin context according to 277 | // parameters specified in a field tag. 278 | type extractor func(*gin.Context, string) (string, []string, error) 279 | 280 | // extractQuery is an extractor tgat operated on the query 281 | // parameters of a request. 282 | func extractQuery(c *gin.Context, tag string) (string, []string, error) { 283 | name, required, defaultVal, err := parseTagKey(tag) 284 | if err != nil { 285 | return "", nil, err 286 | } 287 | var params []string 288 | query := c.Request.URL.Query()[name] 289 | 290 | if c.GetBool(ExplodeTag) { 291 | // Delete empty elements so default and required arguments 292 | // will play nice together. Append to a new collection to 293 | // preserve order without too much copying. 294 | params = make([]string, 0, len(query)) 295 | for i := range query { 296 | if query[i] != "" { 297 | params = append(params, query[i]) 298 | } 299 | } 300 | } else { 301 | splitFn := func(c rune) bool { 302 | return c == ',' 303 | } 304 | if len(query) > 1 { 305 | return name, nil, errors.New("repeating values not supported: use comma-separated list") 306 | } else if len(query) == 1 { 307 | params = strings.FieldsFunc(query[0], splitFn) 308 | } 309 | } 310 | 311 | // XXX: deprecated, use of "default" tag is preferred 312 | if len(params) == 0 && defaultVal != "" { 313 | return name, []string{defaultVal}, nil 314 | } 315 | // XXX: deprecated, use of "validate" tag is preferred 316 | if len(params) == 0 && required { 317 | return "", nil, fmt.Errorf("missing query parameter: %s", name) 318 | } 319 | return name, params, nil 320 | } 321 | 322 | // extractPath is an extractor that operates on the path 323 | // parameters of a request. 324 | func extractPath(c *gin.Context, tag string) (string, []string, error) { 325 | name, required, defaultVal, err := parseTagKey(tag) 326 | if err != nil { 327 | return "", nil, err 328 | } 329 | p := c.Param(name) 330 | 331 | // XXX: deprecated, use of "default" tag is preferred 332 | if p == "" && defaultVal != "" { 333 | return name, []string{defaultVal}, nil 334 | } 335 | // XXX: deprecated, use of "validate" tag is preferred 336 | if p == "" && required { 337 | return "", nil, fmt.Errorf("missing path parameter: %s", name) 338 | } 339 | 340 | return name, []string{p}, nil 341 | } 342 | 343 | // extractHeader is an extractor that operates on the headers 344 | // of a request. 345 | func extractHeader(c *gin.Context, tag string) (string, []string, error) { 346 | name, required, defaultVal, err := parseTagKey(tag) 347 | if err != nil { 348 | return "", nil, err 349 | } 350 | header := c.GetHeader(name) 351 | 352 | // XXX: deprecated, use of "default" tag is preferred 353 | if header == "" && defaultVal != "" { 354 | return name, []string{defaultVal}, nil 355 | } 356 | // XXX: deprecated, use of "validate" tag is preferred 357 | if required && header == "" { 358 | return "", nil, fmt.Errorf("missing header parameter: %s", name) 359 | } 360 | return name, []string{header}, nil 361 | } 362 | 363 | // Public signature does not expose "required" and "default" because 364 | // they are deprecated in favor of the "validate" and "default" tags 365 | func parseTagKey(tag string) (string, bool, string, error) { 366 | parts := strings.Split(tag, ",") 367 | if len(parts) == 0 { 368 | return "", false, "", fmt.Errorf("empty tag") 369 | } 370 | name, options := parts[0], parts[1:] 371 | 372 | var defaultVal string 373 | 374 | // XXX: deprecated, required + default are kept here for backwards compatibility 375 | // use of "default" and "validate" tags is preferred 376 | // Iterate through the tag options to 377 | // find the required key. 378 | var required bool 379 | for _, o := range options { 380 | o = strings.TrimSpace(o) 381 | if o == RequiredTag { 382 | required = true 383 | } else if strings.HasPrefix(o, fmt.Sprintf("%s=", DefaultTag)) { 384 | defaultVal = strings.TrimPrefix(o, fmt.Sprintf("%s=", DefaultTag)) 385 | } else { 386 | return "", false, "", fmt.Errorf("malformed tag for param '%s': unknown option '%s'", name, o) 387 | } 388 | } 389 | return name, required, defaultVal, nil 390 | } 391 | 392 | // ParseTagKey parses the given struct tag key and return the 393 | // name of the field 394 | func ParseTagKey(tag string) (string, error) { 395 | s, _, _, err := parseTagKey(tag) 396 | return s, err 397 | } 398 | 399 | // bindStringValue converts and bind the value s 400 | // to the the reflected value v. 401 | func bindStringValue(s string, v reflect.Value) error { 402 | // Ensure that the reflected value is addressable 403 | // and wasn't obtained by the use of an unexported 404 | // struct field, or calling a setter will panic. 405 | if !v.CanSet() { 406 | return fmt.Errorf("unaddressable value: %v", v) 407 | } 408 | i := reflect.New(v.Type()).Interface() 409 | 410 | // If the value implements the encoding.TextUnmarshaler 411 | // interface, bind the returned string representation. 412 | if unmarshaler, ok := i.(encoding.TextUnmarshaler); ok { 413 | if err := unmarshaler.UnmarshalText([]byte(s)); err != nil { 414 | return err 415 | } 416 | v.Set(reflect.Indirect(reflect.ValueOf(unmarshaler))) 417 | return nil 418 | } 419 | // Handle time.Duration. 420 | if _, ok := i.(time.Duration); ok { 421 | d, err := time.ParseDuration(s) 422 | if err != nil { 423 | return err 424 | } 425 | v.Set(reflect.ValueOf(d)) 426 | } 427 | // Switch over the kind of the reflected value 428 | // and convert the string to the proper type. 429 | switch v.Kind() { 430 | case reflect.String: 431 | v.SetString(s) 432 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 433 | i, err := strconv.ParseInt(s, 10, v.Type().Bits()) 434 | if err != nil { 435 | return err 436 | } 437 | v.SetInt(i) 438 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 439 | i, err := strconv.ParseUint(s, 10, v.Type().Bits()) 440 | if err != nil { 441 | return err 442 | } 443 | v.SetUint(i) 444 | case reflect.Bool: 445 | b, err := strconv.ParseBool(s) 446 | if err != nil { 447 | return err 448 | } 449 | v.SetBool(b) 450 | case reflect.Float32, reflect.Float64: 451 | i, err := strconv.ParseFloat(s, v.Type().Bits()) 452 | if err != nil { 453 | return err 454 | } 455 | v.SetFloat(i) 456 | default: 457 | return fmt.Errorf("unsupported parameter type: %v", v.Kind()) 458 | } 459 | return nil 460 | } 461 | 462 | // yamlBinding is an implementation of gin's binding.Binding 463 | // we don't use official gin's yamlBinding because we prefer to use github.com/ghodss/yaml 464 | type yamlBinding struct{} 465 | 466 | func (yamlBinding) Name() string { 467 | return "yaml" 468 | } 469 | 470 | func (yamlBinding) Bind(req *http.Request, obj interface{}) error { 471 | return decodeYAML(req.Body, obj) 472 | } 473 | 474 | func (yamlBinding) BindBody(body []byte, obj interface{}) error { 475 | return decodeYAML(bytes.NewReader(body), obj) 476 | } 477 | 478 | func decodeYAML(r io.Reader, obj interface{}) error { 479 | btes, err := io.ReadAll(r) 480 | if err != nil { 481 | return err 482 | } 483 | if err := yaml.Unmarshal(btes, &obj); err != nil { 484 | return err 485 | } 486 | if binding.Validator == nil { 487 | return nil 488 | } 489 | return binding.Validator.ValidateStruct(obj) 490 | } 491 | -------------------------------------------------------------------------------- /tonic/tonic_test.go: -------------------------------------------------------------------------------- 1 | package tonic_test 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "strings" 9 | "testing" 10 | "time" 11 | 12 | "github.com/gin-gonic/gin" 13 | "github.com/loopfz/gadgeto/iffy" 14 | "github.com/loopfz/gadgeto/tonic" 15 | ) 16 | 17 | var r http.Handler 18 | 19 | func errorHook(c *gin.Context, e error) (int, interface{}) { 20 | if _, ok := e.(tonic.BindError); ok { 21 | return 400, e.Error() 22 | } 23 | return 500, e.Error() 24 | } 25 | 26 | func TestMain(m *testing.M) { 27 | 28 | tonic.SetErrorHook(errorHook) 29 | 30 | g := gin.Default() 31 | g.GET("/simple", tonic.Handler(simpleHandler, 200)) 32 | g.GET("/scalar", tonic.Handler(scalarHandler, 200)) 33 | g.GET("/error", tonic.Handler(errorHandler, 200)) 34 | g.GET("/path/:param", tonic.Handler(pathHandler, 200)) 35 | g.GET("/query", tonic.Handler(queryHandler, 200)) 36 | g.GET("/query-old", tonic.Handler(queryHandlerOld, 200)) 37 | g.POST("/body", tonic.Handler(bodyHandler, 200)) 38 | 39 | r = g 40 | 41 | m.Run() 42 | } 43 | 44 | func TestSimple(t *testing.T) { 45 | 46 | tester := iffy.NewTester(t, r) 47 | 48 | tester.AddCall("simple", "GET", "/simple", "").Checkers(iffy.ExpectStatus(200), expectEmptyBody) 49 | tester.AddCall("simple", "GET", "/simple/", "").Checkers(iffy.ExpectStatus(301)) 50 | tester.AddCall("simple", "GET", "/simple?", "").Checkers(iffy.ExpectStatus(200)) 51 | tester.AddCall("simple", "GET", "/simple", "{}").Checkers(iffy.ExpectStatus(200)) 52 | tester.AddCall("simple", "GET", "/simple?param=useless", "{}").Checkers(iffy.ExpectStatus(200)) 53 | 54 | tester.AddCall("scalar", "GET", "/scalar", "").Checkers(iffy.ExpectStatus(200)) 55 | 56 | tester.Run() 57 | } 58 | 59 | func TestError(t *testing.T) { 60 | 61 | tester := iffy.NewTester(t, r) 62 | 63 | tester.AddCall("error", "GET", "/error", "").Checkers(iffy.ExpectStatus(500)) 64 | 65 | tester.Run() 66 | } 67 | 68 | func TestPathQuery(t *testing.T) { 69 | 70 | tester := iffy.NewTester(t, r) 71 | 72 | tester.AddCall("path", "GET", "/path/foo", "").Checkers(iffy.ExpectStatus(200), expectString("param", "foo")) 73 | 74 | tester.AddCall("query-normal", "GET", "/query?param=foo", "").Checkers(iffy.ExpectStatus(200), expectString("param", "foo")) 75 | tester.AddCall("query-extra-vals", "GET", "/query?param=foo¶m=bar", "").Checkers(iffy.ExpectStatus(400)) 76 | tester.AddCall("query-missing-required1", "GET", "/query?param=", "").Checkers(iffy.ExpectStatus(400)) 77 | tester.AddCall("query-missing-required2", "GET", "/query", "").Checkers(iffy.ExpectStatus(400)) 78 | tester.AddCall("query-optional", "GET", "/query?param=foo¶m-optional=bar", "").Checkers(iffy.ExpectStatus(200), expectString("param-optional", "bar")) 79 | tester.AddCall("query-int", "GET", "/query?param=foo¶m-int=42", "").Checkers(iffy.ExpectStatus(200), expectInt("param-int", 42)) 80 | tester.AddCall("query-multiple", "GET", "/query?param=foo¶ms=foo¶ms=bar", "").Checkers(iffy.ExpectStatus(200), expectStringArr("params", "foo", "bar")) 81 | tester.AddCall("query-bool", "GET", "/query?param=foo¶m-bool=true", "").Checkers(iffy.ExpectStatus(200), expectBool("param-bool", true)) 82 | tester.AddCall("query-override-default", "GET", "/query?param=foo¶m-default=bla", "").Checkers(iffy.ExpectStatus(200), expectString("param-default", "bla")) 83 | tester.AddCall("query-ptr", "GET", "/query?param=foo¶m-ptr=bar", "").Checkers(iffy.ExpectStatus(200), expectString("param-ptr", "bar")) 84 | tester.AddCall("query-embed", "GET", "/query?param=foo¶m-embed=bar", "").Checkers(iffy.ExpectStatus(200), expectString("param-embed", "bar")) 85 | 86 | now, _ := time.Time{}.Add(87 * time.Hour).MarshalText() 87 | 88 | tester.AddCall("query-complex", "GET", fmt.Sprintf("/query?param=foo¶m-complex=%s", now), "").Checkers(iffy.ExpectStatus(200), expectString("param-complex", string(now))) 89 | 90 | // Explode. 91 | tester.AddCall("query-explode", "GET", "/query?param=foo¶m-explode=a¶m-explode=b¶m-explode=c", "").Checkers(iffy.ExpectStatus(200), expectStringArr("param-explode", "a", "b", "c")) 92 | tester.AddCall("query-explode-disabled-ok", "GET", "/query?param=foo¶m-explode-disabled=x,y,z", "").Checkers(iffy.ExpectStatus(200), expectStringArr("param-explode-disabled", "x", "y", "z")) 93 | tester.AddCall("query-explode-disabled-error", "GET", "/query?param=foo¶m-explode-disabled=a¶m-explode-disabled=b", "").Checkers(iffy.ExpectStatus(400)) 94 | tester.AddCall("query-explode-string", "GET", "/query?param=foo¶m-explode-string=x,y,z", "").Checkers(iffy.ExpectStatus(200), expectString("param-explode-string", "x,y,z")) 95 | tester.AddCall("query-explode-default", "GET", "/query?param=foo", "").Checkers(iffy.ExpectStatus(200), expectStringArr("param-explode-default", "1", "2", "3")) // default with explode 96 | tester.AddCall("query-explode-disabled-default", "GET", "/query?param=foo", "").Checkers(iffy.ExpectStatus(200), expectStringArr("param-explode-disabled-default", "1,2,3")) // default without explode 97 | 98 | tester.Run() 99 | } 100 | 101 | func TestPathQueryBackwardsCompatible(t *testing.T) { 102 | 103 | tester := iffy.NewTester(t, r) 104 | 105 | tester.AddCall("query-old-missing-required1", "GET", "/query-old", "").Checkers(iffy.ExpectStatus(400)) 106 | tester.AddCall("query-old-missing-required2", "GET", "/query-old?param=", "").Checkers(iffy.ExpectStatus(400)) 107 | tester.AddCall("query-old-normal", "GET", "/query-old?param=foo", "").Checkers(iffy.ExpectStatus(200), expectString("param", "foo")) 108 | tester.AddCall("query-old-override-default", "GET", "/query-old?param=foo¶m-default=bla", "").Checkers(iffy.ExpectStatus(200), expectString("param-default", "bla")) 109 | tester.AddCall("query-old-use-default", "GET", "/query-old?param=foo", "").Checkers(iffy.ExpectStatus(200), expectString("param-default", "default")) 110 | 111 | tester.Run() 112 | } 113 | 114 | func TestBody(t *testing.T) { 115 | 116 | tester := iffy.NewTester(t, r) 117 | 118 | tester.AddCall("body", "POST", "/body", `{"param": "foo"}`).Checkers(iffy.ExpectStatus(200), expectString("param", "foo")) 119 | tester.AddCall("body", "POST", "/body", `{}`).Checkers(iffy.ExpectStatus(400)) 120 | tester.AddCall("body", "POST", "/body", `{"param": ""}`).Checkers(iffy.ExpectStatus(400)) 121 | tester.AddCall("body", "POST", "/body", `{"param": "foo", "param-optional": "bar"}`).Checkers(iffy.ExpectStatus(200), expectString("param-optional", "bar")) 122 | tester.AddCall("body1", "POST", "/body", `{"param": "foo"}`).Checkers(iffy.ExpectStatus(200), expectString("param", "foo")) 123 | tester.AddCall("body2", "POST", "/body", `{}`).Checkers(iffy.ExpectStatus(400)) 124 | tester.AddCall("body3", "POST", "/body", `{"param": ""}`).Checkers(iffy.ExpectStatus(400)) 125 | tester.AddCall("body4", "POST", "/body", `{"param": "foo", "param-optional": "bar"}`).Checkers(iffy.ExpectStatus(200), expectString("param-optional", "bar")) 126 | tester.AddCall("body5", "POST", "/body", `{"param": "foo", "param-optional-validated": "ttttt"}`).Checkers(iffy.ExpectStatus(400), expectStringInBody("failed on the 'eq=|eq=foo|gt=10' tag")) 127 | tester.AddCall("body6", "POST", "/body", `{"param": "foo", "param-optional-validated": "foo"}`).Checkers(iffy.ExpectStatus(200), expectString("param-optional-validated", "foo")) 128 | tester.AddCall("body7", "POST", "/body", `{"param": "foo", "param-optional-validated": "foobarfoobuz"}`).Checkers(iffy.ExpectStatus(200), expectString("param-optional-validated", "foobarfoobuz")) 129 | 130 | tester.Run() 131 | } 132 | 133 | func errorHandler(c *gin.Context) error { 134 | return errors.New("error") 135 | } 136 | 137 | func simpleHandler(c *gin.Context) error { 138 | return nil 139 | } 140 | 141 | func scalarHandler(c *gin.Context) (string, error) { 142 | return "", nil 143 | } 144 | 145 | type pathIn struct { 146 | Param string `path:"param" json:"param"` 147 | } 148 | 149 | func pathHandler(c *gin.Context, in *pathIn) (*pathIn, error) { 150 | return in, nil 151 | } 152 | 153 | type queryIn struct { 154 | Param string `query:"param" json:"param" validate:"required"` 155 | ParamOptional string `query:"param-optional" json:"param-optional"` 156 | Params []string `query:"params" json:"params"` 157 | ParamInt int `query:"param-int" json:"param-int"` 158 | ParamBool bool `query:"param-bool" json:"param-bool"` 159 | ParamDefault string `query:"param-default" json:"param-default" default:"default" validate:"required"` 160 | ParamPtr *string `query:"param-ptr" json:"param-ptr"` 161 | ParamComplex time.Time `query:"param-complex" json:"param-complex"` 162 | ParamExplode []string `query:"param-explode" json:"param-explode" explode:"true"` 163 | ParamExplodeDisabled []string `query:"param-explode-disabled" json:"param-explode-disabled" explode:"false"` 164 | ParamExplodeString string `query:"param-explode-string" json:"param-explode-string" explode:"true"` 165 | ParamExplodeDefault []string `query:"param-explode-default" json:"param-explode-default" default:"1,2,3" explode:"true"` 166 | ParamExplodeDefaultDisabled []string `query:"param-explode-disabled-default" json:"param-explode-disabled-default" default:"1,2,3" explode:"false"` 167 | *DoubleEmbedded 168 | } 169 | 170 | // XXX: deprecated, but ensure backwards compatibility 171 | type queryInOld struct { 172 | ParamRequired string `query:"param, required" json:"param"` 173 | ParamDefault string `query:"param-default,required,default=default" json:"param-default"` 174 | } 175 | 176 | type Embedded struct { 177 | ParamEmbed string `query:"param-embed" json:"param-embed"` 178 | } 179 | 180 | type DoubleEmbedded struct { 181 | Embedded 182 | } 183 | 184 | func queryHandler(c *gin.Context, in *queryIn) (*queryIn, error) { 185 | return in, nil 186 | } 187 | 188 | func queryHandlerOld(c *gin.Context, in *queryInOld) (*queryInOld, error) { 189 | return in, nil 190 | } 191 | 192 | type bodyIn struct { 193 | Param string `json:"param" validate:"required"` 194 | ParamOptional string `json:"param-optional"` 195 | ValidatedParamOptional string `json:"param-optional-validated" validate:"eq=|eq=foo|gt=10"` 196 | } 197 | 198 | func bodyHandler(c *gin.Context, in *bodyIn) (*bodyIn, error) { 199 | return in, nil 200 | } 201 | 202 | func expectEmptyBody(r *http.Response, body string, obj interface{}) error { 203 | if len(body) != 0 { 204 | return fmt.Errorf("Body '%s' should be empty", body) 205 | } 206 | return nil 207 | } 208 | 209 | func expectString(paramName, value string) func(*http.Response, string, interface{}) error { 210 | 211 | return func(r *http.Response, body string, obj interface{}) error { 212 | 213 | var i map[string]interface{} 214 | 215 | err := json.Unmarshal([]byte(body), &i) 216 | if err != nil { 217 | return err 218 | } 219 | s, ok := i[paramName] 220 | if !ok { 221 | return fmt.Errorf("%s missing", paramName) 222 | } 223 | if s != value { 224 | return fmt.Errorf("%s: expected %s got %s", paramName, value, s) 225 | } 226 | return nil 227 | } 228 | } 229 | 230 | func expectBool(paramName string, value bool) func(*http.Response, string, interface{}) error { 231 | 232 | return func(r *http.Response, body string, obj interface{}) error { 233 | 234 | i := map[string]interface{}{paramName: 0} 235 | 236 | err := json.Unmarshal([]byte(body), &i) 237 | if err != nil { 238 | return err 239 | } 240 | v, ok := i[paramName] 241 | if !ok { 242 | return fmt.Errorf("%s missing", paramName) 243 | } 244 | vb, ok := v.(bool) 245 | if !ok { 246 | return fmt.Errorf("%s not a number", paramName) 247 | } 248 | if vb != value { 249 | return fmt.Errorf("%s: expected %v got %v", paramName, value, vb) 250 | } 251 | return nil 252 | } 253 | } 254 | 255 | func expectStringArr(paramName string, value ...string) func(*http.Response, string, interface{}) error { 256 | 257 | return func(r *http.Response, body string, obj interface{}) error { 258 | 259 | var i map[string]interface{} 260 | 261 | err := json.Unmarshal([]byte(body), &i) 262 | if err != nil { 263 | return fmt.Errorf("failed to unmarshal json: %s", body) 264 | } 265 | s, ok := i[paramName] 266 | if !ok { 267 | return fmt.Errorf("%s missing", paramName) 268 | } 269 | sArr, ok := s.([]interface{}) 270 | if !ok { 271 | return fmt.Errorf("%s not a string arr", paramName) 272 | } 273 | for n := 0; n < len(value); n++ { 274 | if n >= len(sArr) { 275 | return fmt.Errorf("%s too short", paramName) 276 | } 277 | if sArr[n] != value[n] { 278 | return fmt.Errorf("%s: %s does not match", paramName, sArr[n]) 279 | } 280 | } 281 | return nil 282 | } 283 | } 284 | 285 | func expectStringInBody(value string) func(*http.Response, string, interface{}) error { 286 | 287 | return func(r *http.Response, body string, obj interface{}) error { 288 | if !strings.Contains(body, value) { 289 | return fmt.Errorf("body doesn't contain '%s' (%s)", value, body) 290 | } 291 | return nil 292 | } 293 | } 294 | 295 | func expectInt(paramName string, value int) func(*http.Response, string, interface{}) error { 296 | 297 | return func(r *http.Response, body string, obj interface{}) error { 298 | 299 | i := map[string]interface{}{paramName: 0} 300 | 301 | err := json.Unmarshal([]byte(body), &i) 302 | if err != nil { 303 | return err 304 | } 305 | v, ok := i[paramName] 306 | if !ok { 307 | return fmt.Errorf("%s missing", paramName) 308 | } 309 | vf, ok := v.(float64) 310 | if !ok { 311 | return fmt.Errorf("%s not a number", paramName) 312 | } 313 | vInt := int(vf) 314 | if vInt != value { 315 | return fmt.Errorf("%s: expected %v got %v", paramName, value, vInt) 316 | } 317 | return nil 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /tonic/utils/jujerr/jujerr.go: -------------------------------------------------------------------------------- 1 | package jujerr 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "github.com/juju/errors" 6 | "github.com/loopfz/gadgeto/tonic" 7 | ) 8 | 9 | func ErrHook(c *gin.Context, e error) (int, interface{}) { 10 | 11 | errcode, errpl := 500, e.Error() 12 | if _, ok := e.(tonic.BindError); ok { 13 | errcode, errpl = 400, e.Error() 14 | } else { 15 | switch { 16 | case errors.IsBadRequest(e) || errors.IsNotValid(e) || errors.IsAlreadyExists(e) || errors.IsNotSupported(e) || errors.IsNotAssigned(e) || errors.IsNotProvisioned(e): 17 | errcode, errpl = 400, e.Error() 18 | case errors.IsForbidden(e): 19 | errcode, errpl = 403, e.Error() 20 | case errors.IsMethodNotAllowed(e): 21 | errcode, errpl = 405, e.Error() 22 | case errors.IsNotFound(e) || errors.IsUserNotFound(e): 23 | errcode, errpl = 404, e.Error() 24 | case errors.IsUnauthorized(e): 25 | errcode, errpl = 401, e.Error() 26 | case errors.IsNotImplemented(e): 27 | errcode, errpl = 501, e.Error() 28 | } 29 | } 30 | 31 | return errcode, gin.H{`error`: errpl} 32 | } 33 | -------------------------------------------------------------------------------- /tonic/utils/listenproxyproto/listener.go: -------------------------------------------------------------------------------- 1 | package listenproxyproto 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/loopfz/gadgeto/tonic" 7 | "github.com/pires/go-proxyproto" 8 | ) 9 | 10 | func ListenProxyProtocol(o *tonic.ListenOpt) error { 11 | o.Listener = &proxyproto.Listener{ 12 | Listener: o.Listener, 13 | ReadHeaderTimeout: 2 * time.Second, 14 | } 15 | return nil 16 | } 17 | -------------------------------------------------------------------------------- /zesty/README: -------------------------------------------------------------------------------- 1 | zesty is based on gorp, and abstracts DB transaction specifics. 2 | 3 | It abstracts DB and Tx objects through a unified interface: DBProvider, which in turn lets your function 4 | remain ignorant of the current transaction state they get passed. 5 | 6 | It also manages nested transactions, with mid-Tx savepoints making partial rollbacks very easy. 7 | 8 | You can create a zesty.DB by calling NewDB(). 9 | You can then register this DB by calling RegisterDB(). 10 | 11 | This lets you instantiate DBProviders for this DB with NewDBProvider(), which is the main 12 | object that you manipulate. 13 | 14 | A DBProvider contains a DB instance, and provides Tx functionalities. 15 | 16 | You access the DB by calling provider.DB() 17 | 18 | By calling provider.Tx(), you create a new transaction. 19 | Future calls to provider.DB() will provide the Tx instead of the main DB object, 20 | allowing caller code to be completely ignorant of transaction context. 21 | 22 | Transactions can be nested infinitely, and each nesting level can be rolled back independantly. 23 | Only the final commit will end the transaction and commit the changes to the DB. 24 | -------------------------------------------------------------------------------- /zesty/utils/rekordo/database.go: -------------------------------------------------------------------------------- 1 | package rekordo 2 | 3 | import ( 4 | "database/sql" 5 | "errors" 6 | "time" 7 | 8 | "github.com/go-gorp/gorp" 9 | "github.com/loopfz/gadgeto/zesty" 10 | ) 11 | 12 | // Default database settings. 13 | const ( 14 | maxOpenConns = 5 15 | maxIdleConns = 3 16 | ) 17 | 18 | // DatabaseConfig represents the configuration used to 19 | // register a new database. 20 | type DatabaseConfig struct { 21 | Name string 22 | DSN string 23 | System DBMS 24 | MaxOpenConns int 25 | MaxIdleConns int 26 | ConnMaxLifetime time.Duration 27 | AutoCreateTables bool 28 | } 29 | 30 | // RegisterDatabase creates a gorp map with tables and tc and 31 | // registers it with zesty. 32 | func RegisterDatabase(dbcfg *DatabaseConfig, tc gorp.TypeConverter) (zesty.DB, error) { 33 | dbConn, err := sql.Open(dbcfg.System.DriverName(), dbcfg.DSN) 34 | if err != nil { 35 | return nil, err 36 | } 37 | // Make sure we have proper values for the database 38 | // settings, and replace them with default if necessary 39 | // before applying to the new connection. 40 | if dbcfg.MaxOpenConns == 0 { 41 | dbcfg.MaxOpenConns = maxOpenConns 42 | } 43 | dbConn.SetMaxOpenConns(dbcfg.MaxOpenConns) 44 | if dbcfg.MaxIdleConns == 0 { 45 | dbcfg.MaxIdleConns = maxIdleConns 46 | } 47 | dbConn.SetMaxIdleConns(dbcfg.MaxIdleConns) 48 | dbConn.SetConnMaxLifetime(dbcfg.ConnMaxLifetime) 49 | 50 | // Select the proper dialect used by gorp. 51 | var dialect gorp.Dialect 52 | switch dbcfg.System { 53 | case DatabaseMySQL: 54 | dialect = gorp.MySQLDialect{} 55 | case DatabasePostgreSQL: 56 | dialect = gorp.PostgresDialect{} 57 | case DatabaseSqlite3: 58 | dialect = gorp.SqliteDialect{} 59 | default: 60 | return nil, errors.New("unknown database system") 61 | } 62 | dbmap := &gorp.DbMap{ 63 | Db: dbConn, 64 | Dialect: dialect, 65 | TypeConverter: tc, 66 | } 67 | modelsMu.Lock() 68 | tableModels := models[dbcfg.Name] 69 | for _, t := range tableModels { 70 | dbmap.AddTableWithName(t.Model, t.Name).SetKeys(t.AutoIncrement, t.Keys...) 71 | } 72 | modelsMu.Unlock() 73 | 74 | if dbcfg.AutoCreateTables { 75 | err = dbmap.CreateTablesIfNotExists() 76 | if err != nil { 77 | return nil, err 78 | } 79 | } 80 | db := zesty.NewDB(dbmap) 81 | if err := zesty.RegisterDB(db, dbcfg.Name); err != nil { 82 | return nil, err 83 | } 84 | return db, nil 85 | } 86 | 87 | // DBMS represents a database management system. 88 | type DBMS uint8 89 | 90 | // Database management systems. 91 | const ( 92 | DatabasePostgreSQL DBMS = iota ^ 42 93 | DatabaseMySQL 94 | DatabaseSqlite3 95 | ) 96 | 97 | // DriverName returns the name of the driver for ds. 98 | func (d DBMS) DriverName() string { 99 | switch d { 100 | case DatabasePostgreSQL: 101 | return "postgres" 102 | case DatabaseMySQL: 103 | return "mysql" 104 | case DatabaseSqlite3: 105 | return "sqlite3" 106 | } 107 | return "" 108 | } 109 | -------------------------------------------------------------------------------- /zesty/utils/rekordo/register.go: -------------------------------------------------------------------------------- 1 | package rekordo 2 | 3 | import "sync" 4 | 5 | // modelsMu protect models map. 6 | var modelsMu sync.Mutex 7 | 8 | // models represents the models registered 9 | // for all databases. 10 | var models map[string]map[string]*TableModel 11 | 12 | func init() { 13 | // Initialize tables map. 14 | models = make(map[string]map[string]*TableModel) 15 | } 16 | 17 | // TableModel is a middleman between a database 18 | // table and a model type. 19 | type TableModel struct { 20 | Name string 21 | Model interface{} 22 | Keys []string 23 | AutoIncrement bool 24 | } 25 | 26 | // RegisterTableModel registers a zero-value model to 27 | // the definition of a database table. If a table model 28 | // has already been registered with the same table name, 29 | // this will overwrite it. 30 | func RegisterTableModel(dbName, tableName string, model interface{}) *TableModel { 31 | modelsMu.Lock() 32 | defer modelsMu.Unlock() 33 | 34 | if _, ok := models[dbName]; !ok { 35 | // Detabase entry does not exists, let's 36 | // create it and add a new model for the table. 37 | models[dbName] = make(map[string]*TableModel) 38 | } 39 | m := &TableModel{ 40 | Name: tableName, 41 | Model: model, 42 | Keys: []string{"id"}, 43 | AutoIncrement: true, 44 | } 45 | models[dbName][tableName] = m 46 | 47 | return m 48 | } 49 | 50 | // WithKeys uses keys as table keys for the model. 51 | func (tb *TableModel) WithKeys(keys []string) *TableModel { 52 | tb.Keys = keys 53 | return tb 54 | } 55 | 56 | // WithAutoIncrement uses enable for table model keys auto-increment. 57 | func (tb *TableModel) WithAutoIncrement(enable bool) *TableModel { 58 | tb.AutoIncrement = enable 59 | return tb 60 | } 61 | -------------------------------------------------------------------------------- /zesty/zesty.go: -------------------------------------------------------------------------------- 1 | package zesty 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "errors" 7 | "fmt" 8 | "sync" 9 | 10 | "github.com/go-gorp/gorp" 11 | ) 12 | 13 | // Registered databases 14 | var ( 15 | dbs = make(map[string]DB) 16 | dblock sync.RWMutex 17 | ) 18 | 19 | type SavePoint uint 20 | 21 | /* 22 | * INTERFACES 23 | */ 24 | 25 | type DB interface { 26 | gorp.SqlExecutor 27 | Begin() (Tx, error) 28 | Close() error 29 | Ping() error 30 | PingContext(context.Context) error 31 | Stats() sql.DBStats 32 | } 33 | 34 | type Tx interface { 35 | gorp.SqlExecutor 36 | Commit() error 37 | Rollback() error 38 | Savepoint(string) error 39 | RollbackToSavepoint(string) error 40 | } 41 | 42 | type DBProvider interface { 43 | DB() gorp.SqlExecutor 44 | Tx() error 45 | TxSavepoint() (SavePoint, error) 46 | Commit() error 47 | Rollback() error 48 | RollbackTo(SavePoint) error 49 | Close() error 50 | Ping() error 51 | PingContext(context.Context) error 52 | Stats() sql.DBStats 53 | } 54 | 55 | /* 56 | * FUNCTIONS 57 | */ 58 | 59 | func NewDB(dbmap *gorp.DbMap) DB { 60 | return &zestydb{DbMap: dbmap} 61 | } 62 | 63 | func RegisterDB(db DB, name string) error { 64 | dblock.Lock() 65 | defer dblock.Unlock() 66 | 67 | _, ok := dbs[name] 68 | if ok { 69 | return fmt.Errorf("DB name conflict '%s'", name) 70 | } 71 | 72 | dbs[name] = db 73 | 74 | return nil 75 | } 76 | 77 | func UnregisterDB(name string) error { 78 | dblock.Lock() 79 | defer dblock.Unlock() 80 | 81 | _, ok := dbs[name] 82 | if !ok { 83 | return fmt.Errorf("No such database '%s'", name) 84 | } 85 | 86 | delete(dbs, name) 87 | 88 | return nil 89 | } 90 | 91 | func NewDBProvider(name string) (DBProvider, error) { 92 | dblock.RLock() 93 | defer dblock.RUnlock() 94 | db, ok := dbs[name] 95 | if !ok { 96 | return nil, fmt.Errorf("No such database '%s'", name) 97 | } 98 | return &zestyprovider{ 99 | current: db, 100 | db: db, 101 | }, nil 102 | } 103 | 104 | func NewTempDBProvider(db DB) DBProvider { 105 | return &zestyprovider{ 106 | current: db, 107 | db: db, 108 | } 109 | } 110 | 111 | /* 112 | * PROVIDER IMPLEMENTATION 113 | */ 114 | 115 | type zestyprovider struct { 116 | current gorp.SqlExecutor 117 | db DB 118 | tx Tx 119 | savepoint SavePoint 120 | } 121 | 122 | func (zp *zestyprovider) DB() gorp.SqlExecutor { 123 | return zp.current 124 | } 125 | 126 | func (zp *zestyprovider) Commit() error { 127 | if zp.tx == nil { 128 | return errors.New("No active Tx") 129 | } 130 | 131 | if zp.savepoint > 0 { 132 | zp.savepoint-- 133 | return nil 134 | } 135 | 136 | err := zp.tx.Commit() 137 | if err != nil { 138 | return err 139 | } 140 | 141 | zp.resetTx() 142 | 143 | return nil 144 | } 145 | 146 | func (zp *zestyprovider) Tx() error { 147 | _, err := zp.TxSavepoint() 148 | return err 149 | } 150 | 151 | func (zp *zestyprovider) Rollback() error { 152 | return zp.RollbackTo(zp.savepoint) 153 | } 154 | 155 | const savepointFmt = "tx-savepoint-%d" 156 | 157 | func (zp *zestyprovider) TxSavepoint() (SavePoint, error) { 158 | if zp.tx == nil { 159 | // root transaction 160 | tx, err := zp.db.Begin() 161 | if err != nil { 162 | return 0, err 163 | } 164 | 165 | zp.tx = tx 166 | zp.current = tx 167 | } else { 168 | // nested transaction 169 | s := fmt.Sprintf(savepointFmt, zp.savepoint+1) 170 | err := zp.tx.Savepoint(s) 171 | if err != nil { 172 | return 0, err 173 | } 174 | 175 | zp.savepoint++ 176 | } 177 | 178 | return zp.savepoint, nil 179 | } 180 | 181 | func (zp *zestyprovider) RollbackTo(sp SavePoint) error { 182 | if zp.tx == nil { 183 | return errors.New("No active Tx") 184 | } 185 | if sp > zp.savepoint { 186 | // noop 187 | return nil 188 | } 189 | 190 | if sp == 0 { 191 | // root transaction 192 | err := zp.tx.Rollback() 193 | if err != nil { 194 | return err 195 | } 196 | 197 | zp.resetTx() 198 | } else { 199 | // nested transaction 200 | s := fmt.Sprintf(savepointFmt, sp) 201 | err := zp.tx.RollbackToSavepoint(s) 202 | if err != nil { 203 | return err 204 | } 205 | 206 | zp.savepoint = sp - 1 207 | } 208 | 209 | return nil 210 | } 211 | 212 | func (zp *zestyprovider) resetTx() { 213 | zp.current = zp.db 214 | zp.tx = nil 215 | zp.savepoint = 0 216 | } 217 | 218 | func (zp *zestyprovider) Close() error { 219 | return zp.db.Close() 220 | } 221 | 222 | func (zp *zestyprovider) Ping() error { 223 | return zp.db.Ping() 224 | } 225 | 226 | func (zp *zestyprovider) PingContext(ctx context.Context) error { 227 | return zp.db.PingContext(ctx) 228 | } 229 | 230 | func (zp *zestyprovider) Stats() sql.DBStats { 231 | return zp.db.Stats() 232 | } 233 | 234 | /* 235 | * DATABASE IMPLEMENTATION 236 | */ 237 | 238 | type zestydb struct { 239 | *gorp.DbMap 240 | } 241 | 242 | func (zd *zestydb) Begin() (Tx, error) { 243 | return zd.DbMap.Begin() 244 | } 245 | 246 | func (zd *zestydb) Close() error { 247 | return zd.DbMap.Db.Close() 248 | } 249 | 250 | func (zd *zestydb) Ping() error { 251 | return zd.DbMap.Db.Ping() 252 | } 253 | 254 | func (zd *zestydb) PingContext(ctx context.Context) error { 255 | return zd.DbMap.Db.PingContext(ctx) 256 | } 257 | 258 | func (zd *zestydb) Stats() sql.DBStats { 259 | return zd.DbMap.Db.Stats() 260 | } 261 | -------------------------------------------------------------------------------- /zesty/zesty_test.go: -------------------------------------------------------------------------------- 1 | package zesty 2 | 3 | import ( 4 | "database/sql" 5 | "testing" 6 | 7 | "github.com/go-gorp/gorp" 8 | _ "github.com/mattn/go-sqlite3" 9 | ) 10 | 11 | const ( 12 | dbName = "test" 13 | value1 = 1 14 | value2 = 2 15 | value3 = 3 16 | value4 = 4 17 | ) 18 | 19 | func expectValue(t *testing.T, dbp DBProvider, expected int64) { 20 | i, err := dbp.DB().SelectInt(`SELECT id FROM "t"`) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | if i != expected { 25 | t.Fatalf("unexpected value found in table, expecting %d", expected) 26 | } 27 | } 28 | 29 | func insertValue(t *testing.T, dbp DBProvider, value int64) { 30 | _, err := dbp.DB().Exec(`INSERT INTO "t" VALUES (?)`, value) 31 | if err != nil { 32 | t.Fatal(err) 33 | } 34 | } 35 | 36 | func updateValue(t *testing.T, dbp DBProvider, value int64) { 37 | _, err := dbp.DB().Exec(`UPDATE "t" SET id = ?`, value) 38 | if err != nil { 39 | t.Fatal(err) 40 | } 41 | } 42 | 43 | func rollback(t *testing.T, dbp DBProvider) { 44 | err := dbp.Rollback() 45 | if err != nil { 46 | t.Fatal(err) 47 | } 48 | } 49 | 50 | func rollbackTo(t *testing.T, dbp DBProvider, sp SavePoint) { 51 | err := dbp.RollbackTo(sp) 52 | if err != nil { 53 | t.Fatal(err) 54 | } 55 | } 56 | 57 | func tx(t *testing.T, dbp DBProvider) { 58 | err := dbp.Tx() 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | } 63 | 64 | func txSavepoint(t *testing.T, dbp DBProvider) SavePoint { 65 | sp, err := dbp.TxSavepoint() 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | return sp 70 | } 71 | 72 | func TestTransaction(t *testing.T) { 73 | db, err := sql.Open("sqlite3", ":memory:") 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | RegisterDB( 78 | NewDB(&gorp.DbMap{ 79 | Db: db, 80 | Dialect: gorp.SqliteDialect{}, 81 | }), 82 | dbName, 83 | ) 84 | dbp, err := NewDBProvider(dbName) 85 | if err != nil { 86 | t.Fatal(err) 87 | } 88 | 89 | _, err = dbp.DB().Exec(`CREATE TABLE "t" (id BIGINT);`) 90 | if err != nil { 91 | t.Fatal(err) 92 | } 93 | 94 | // first transaction: insert value 1 95 | tx(t, dbp) 96 | 97 | insertValue(t, dbp, value1) 98 | 99 | expectValue(t, dbp, value1) 100 | 101 | // second transaction: update value to 2 102 | sp1 := txSavepoint(t, dbp) 103 | 104 | updateValue(t, dbp, value2) 105 | expectValue(t, dbp, value2) 106 | 107 | tx(t, dbp) 108 | 109 | updateValue(t, dbp, value3) 110 | expectValue(t, dbp, value3) 111 | 112 | tx(t, dbp) 113 | 114 | updateValue(t, dbp, value4) 115 | expectValue(t, dbp, value4) 116 | 117 | rollback(t, dbp) 118 | 119 | expectValue(t, dbp, value3) 120 | 121 | // rollback on second transaction: value back to 1 122 | rollbackTo(t, dbp, sp1) 123 | 124 | expectValue(t, dbp, value1) 125 | 126 | // noop rollback: savepoint already removed in previous rollback 127 | rollbackTo(t, dbp, sp1) 128 | 129 | expectValue(t, dbp, value1) 130 | 131 | // rollback on first transaction: empty table 132 | rollback(t, dbp) 133 | 134 | j, err := dbp.DB().SelectNullInt(`SELECT id FROM "t"`) 135 | if err != nil { 136 | t.Fatal(err) 137 | } 138 | if j.Valid { 139 | t.Fatal("wrong value, was expecting empty sql.NullInt64 (no rows found)") 140 | } 141 | 142 | // no rollback possible after exiting outermost Tx 143 | err = dbp.Rollback() 144 | if err == nil { 145 | t.Fatal("rollback should fail when there is no transaction") 146 | } 147 | } 148 | --------------------------------------------------------------------------------