├── .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 |
--------------------------------------------------------------------------------