├── renovate.json ├── Makefile ├── urlstruct.go ├── .travis.yml ├── field.go ├── go.mod ├── struct_info_map.go ├── LICENSE ├── README.md ├── pager.go ├── values.go ├── struct_decoder.go ├── struct_info.go ├── decode_test.go ├── scan.go └── go.sum /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | go test ./... 3 | go test ./... -short -race 4 | go test ./... -run=NONE -bench=. -benchmem 5 | env GOOS=linux GOARCH=386 go test ./... 6 | golangci-lint run 7 | -------------------------------------------------------------------------------- /urlstruct.go: -------------------------------------------------------------------------------- 1 | package urlstruct 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | "reflect" 7 | ) 8 | 9 | // Unmarshal unmarshals the URL query values into the struct. 10 | func Unmarshal(ctx context.Context, values url.Values, strct interface{}) error { 11 | d := newStructDecoder(reflect.ValueOf(strct)) 12 | return d.Decode(ctx, values) 13 | } 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | sudo: false 3 | language: go 4 | 5 | go: 6 | - 1.13.x 7 | - 1.14.x 8 | - tip 9 | 10 | matrix: 11 | allow_failures: 12 | - go: tip 13 | 14 | env: 15 | - GO111MODULE=on 16 | 17 | go_import_path: github.com/go-pg/urlstruct 18 | 19 | before_install: 20 | - curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b $(go env GOPATH)/bin v1.21.0 21 | -------------------------------------------------------------------------------- /field.go: -------------------------------------------------------------------------------- 1 | package urlstruct 2 | 3 | import ( 4 | "reflect" 5 | 6 | "github.com/vmihailenco/tagparser" 7 | ) 8 | 9 | type Field struct { 10 | Type reflect.Type 11 | Name string 12 | Index []int 13 | Tag *tagparser.Tag 14 | 15 | noDecode bool 16 | scanValue scannerFunc 17 | } 18 | 19 | func (f *Field) init() { 20 | _, f.noDecode = f.Tag.Options["nodecode"] 21 | 22 | if f.Type.Kind() == reflect.Slice { 23 | f.scanValue = sliceScanner(f.Type) 24 | } else { 25 | f.scanValue = scanner(f.Type) 26 | } 27 | } 28 | 29 | func (f *Field) Value(strct reflect.Value) reflect.Value { 30 | return strct.FieldByIndex(f.Index) 31 | } 32 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/go-pg/urlstruct 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/codemodus/kace v0.5.1 7 | github.com/google/go-cmp v0.5.1 // indirect 8 | github.com/google/uuid v1.1.4 9 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect 10 | github.com/onsi/ginkgo v1.14.1 11 | github.com/onsi/gomega v1.10.2 12 | github.com/vmihailenco/tagparser v0.1.2 13 | golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect 14 | golang.org/x/sys v0.0.0-20200908134130-d2e65c121b96 // indirect 15 | google.golang.org/protobuf v1.25.0 // indirect 16 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /struct_info_map.go: -------------------------------------------------------------------------------- 1 | package urlstruct 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | "sync" 7 | ) 8 | 9 | var globalMap structInfoMap 10 | 11 | func DescribeStruct(typ reflect.Type) *StructInfo { 12 | return globalMap.DescribeStruct(typ) 13 | } 14 | 15 | type structInfoMap struct { 16 | m sync.Map 17 | } 18 | 19 | func (m *structInfoMap) DescribeStruct(typ reflect.Type) *StructInfo { 20 | if typ.Kind() == reflect.Ptr { 21 | typ = typ.Elem() 22 | } 23 | if typ.Kind() != reflect.Struct { 24 | panic(fmt.Errorf("got %s, wanted %s", typ.Kind(), reflect.Struct)) 25 | } 26 | 27 | if v, ok := m.m.Load(typ); ok { 28 | return v.(*StructInfo) 29 | } 30 | 31 | sinfo := newStructInfo(typ) 32 | if v, loaded := m.m.LoadOrStore(typ, sinfo); loaded { 33 | return v.(*StructInfo) 34 | } 35 | return sinfo 36 | } 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 github.com/go-pg/pg Authors. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 15 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 16 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 17 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 18 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 19 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 20 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 21 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 22 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 23 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # urlstruct decodes url.Values into structs 2 | 3 | [![Build Status](https://travis-ci.org/go-pg/urlstruct.svg?branch=master)](https://travis-ci.org/go-pg/urlstruct) 4 | [![GoDoc](https://godoc.org/github.com/go-pg/urlstruct?status.svg)](https://godoc.org/github.com/go-pg/urlstruct) 5 | 6 | ## Example 7 | 8 | Following example decodes URL query `?page=2&limit=100&author_id=123` into a struct. 9 | 10 | ```go 11 | type Book struct { 12 | tableName struct{} `pg:"alias:b"` 13 | 14 | ID int64 15 | AuthorID int64 16 | CreatedAt time.Time 17 | } 18 | 19 | type BookFilter struct { 20 | tableName struct{} `urlstruct:"b"` 21 | 22 | urlstruct.Pager 23 | AuthorID int64 24 | } 25 | 26 | func ExampleUnmarshal_filter() { 27 | db := pg.Connect(&pg.Options{ 28 | User: "postgres", 29 | Password: "", 30 | Database: "postgres", 31 | }) 32 | defer db.Close() 33 | 34 | values := url.Values{ 35 | "author_id": {"123"}, 36 | "page": {"2"}, 37 | "limit": {"100"}, 38 | } 39 | filter := new(BookFilter) 40 | err := urlstruct.Unmarshal(values, filter) 41 | if err != nil { 42 | panic(err) 43 | } 44 | 45 | filter.Pager.MaxLimit = 100 // default max limit is 1000 46 | filter.Pager.MaxOffset = 100000 // default max offset is 1000000 47 | 48 | // Following query generates: 49 | // 50 | // SELECT "b"."id", "b"."author_id", "b"."created_at" 51 | // FROM "books" AS "b" 52 | // WHERE "b".author_id = 123 53 | // LIMIT 100 OFFSET 100 54 | 55 | var books []*Book 56 | _ = db.Model(&books). 57 | WhereStruct(filter). 58 | Limit(filter.Pager.GetLimit()). 59 | Offset(filter.Pager.GetOffset()). 60 | Select() 61 | 62 | fmt.Println("author", filter.AuthorID) 63 | fmt.Println("limit", filter.GetLimit()) 64 | fmt.Println("offset", filter.GetLimit()) 65 | // Output: author 123 66 | // limit 100 67 | // offset 100 68 | } 69 | ``` 70 | -------------------------------------------------------------------------------- /pager.go: -------------------------------------------------------------------------------- 1 | package urlstruct 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | ) 7 | 8 | type Pager struct { 9 | Limit int 10 | Offset int 11 | 12 | // Default is 100. 13 | DefaultLimit int `urlstruct:"-"` 14 | // Default is 1000. 15 | MaxLimit int `urlstruct:"-"` 16 | 17 | // Default max offset is 1000000. 18 | MaxOffset int `urlstruct:"-"` 19 | 20 | stickyErr error 21 | } 22 | 23 | func NewPager(values url.Values) *Pager { 24 | p := new(Pager) 25 | p.stickyErr = p.UnmarshalValues(context.TODO(), values) 26 | return p 27 | } 28 | 29 | var _ Unmarshaler = (*Pager)(nil) 30 | 31 | func (p *Pager) UnmarshalValues(ctx context.Context, values url.Values) error { 32 | vs := Values(values) 33 | 34 | limit, err := vs.Int("limit") 35 | if err != nil { 36 | return err 37 | } 38 | p.Limit = limit 39 | 40 | page, err := vs.Int("page") 41 | if err != nil { 42 | return err 43 | } 44 | p.SetPage(page) 45 | 46 | return nil 47 | } 48 | 49 | func (p *Pager) maxLimit() int { 50 | if p.MaxLimit > 0 { 51 | return p.MaxLimit 52 | } 53 | return 1000 54 | } 55 | 56 | func (p *Pager) maxOffset() int { 57 | if p.MaxOffset > 0 { 58 | return p.MaxOffset 59 | } 60 | return 1000000 61 | } 62 | 63 | func (p *Pager) GetLimit() int { 64 | const defaultLimit = 100 65 | 66 | if p == nil { 67 | return defaultLimit 68 | } 69 | if p.Limit < 0 { 70 | return p.Limit 71 | } 72 | if p.Limit == 0 { 73 | if p.DefaultLimit == 0 { 74 | return defaultLimit 75 | } 76 | return p.DefaultLimit 77 | } 78 | if p.Limit > p.maxLimit() { 79 | return p.maxLimit() 80 | } 81 | return p.Limit 82 | } 83 | 84 | func (p *Pager) GetOffset() int { 85 | if p == nil { 86 | return 0 87 | } 88 | if p.Offset > p.maxOffset() { 89 | return p.maxOffset() 90 | } 91 | return p.Offset 92 | } 93 | 94 | func (p *Pager) SetPage(page int) { 95 | if page < 1 { 96 | page = 1 97 | } 98 | p.Offset = (page - 1) * p.GetLimit() 99 | } 100 | 101 | func (p *Pager) GetPage() int { 102 | return (p.GetOffset() / p.GetLimit()) + 1 103 | } 104 | -------------------------------------------------------------------------------- /values.go: -------------------------------------------------------------------------------- 1 | package urlstruct 2 | 3 | import ( 4 | "net/url" 5 | "strconv" 6 | "time" 7 | ) 8 | 9 | type Values url.Values 10 | 11 | func (v Values) Has(name string) bool { 12 | _, ok := v[name] 13 | return ok 14 | } 15 | 16 | func (v Values) SetDefault(name string, values ...string) { 17 | if !v.Has(name) { 18 | v[name] = values 19 | } 20 | } 21 | 22 | func (v Values) Strings(name string) []string { 23 | return v[name] 24 | } 25 | 26 | func (v Values) String(name string) string { 27 | values := v.Strings(name) 28 | if len(values) == 0 { 29 | return "" 30 | } 31 | return values[0] 32 | } 33 | 34 | func (v Values) Bool(name string) (bool, error) { 35 | if !v.Has(name) { 36 | return false, nil 37 | } 38 | s := v.String(name) 39 | if s == "" { 40 | return true, nil 41 | } 42 | return strconv.ParseBool(s) 43 | } 44 | 45 | func (v Values) MaybeBool(name string) bool { 46 | flag, _ := v.Bool(name) 47 | return flag 48 | } 49 | 50 | func (v Values) Int(name string) (int, error) { 51 | s := v.String(name) 52 | if s == "" { 53 | return 0, nil 54 | } 55 | return strconv.Atoi(s) 56 | } 57 | 58 | func (v Values) MaybeInt(name string) int { 59 | n, _ := v.Int(name) 60 | return n 61 | } 62 | 63 | func (v Values) Int64(name string) (int64, error) { 64 | s := v.String(name) 65 | if s == "" { 66 | return 0, nil 67 | } 68 | return strconv.ParseInt(s, 10, 64) 69 | } 70 | 71 | func (v Values) MaybeInt64(name string) int64 { 72 | n, _ := v.Int64(name) 73 | return n 74 | } 75 | 76 | func (v Values) Float64(name string) (float64, error) { 77 | s := v.String(name) 78 | if s == "" { 79 | return 0, nil 80 | } 81 | return strconv.ParseFloat(s, 64) 82 | } 83 | 84 | func (v Values) MaybeFloat64(name string) float64 { 85 | n, _ := v.Float64(name) 86 | return n 87 | } 88 | 89 | func (v Values) Time(name string) (time.Time, error) { 90 | s := v.String(name) 91 | if s == "" { 92 | return time.Time{}, nil 93 | } 94 | return parseTime(s) 95 | } 96 | 97 | func (v Values) MaybeTime(name string) time.Time { 98 | tm, _ := v.Time(name) 99 | return tm 100 | } 101 | 102 | func (v Values) Duration(name string) (time.Duration, error) { 103 | s := v.String(name) 104 | if s == "" { 105 | return 0, nil 106 | } 107 | return time.ParseDuration(s) 108 | } 109 | 110 | func (v Values) MaybeDuration(name string) time.Duration { 111 | dur, _ := v.Duration(name) 112 | return dur 113 | } 114 | 115 | func (v Values) Pager() *Pager { 116 | return NewPager(url.Values(v)) 117 | } 118 | -------------------------------------------------------------------------------- /struct_decoder.go: -------------------------------------------------------------------------------- 1 | package urlstruct 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net/url" 7 | "reflect" 8 | "strings" 9 | ) 10 | 11 | type structDecoder struct { 12 | v reflect.Value 13 | sinfo *StructInfo 14 | 15 | decMap map[string]*structDecoder 16 | paramUnmarshaler ParamUnmarshaler 17 | } 18 | 19 | func newStructDecoder(v reflect.Value) *structDecoder { 20 | v = reflect.Indirect(v) 21 | return &structDecoder{ 22 | v: v, 23 | sinfo: DescribeStruct(v.Type()), 24 | } 25 | } 26 | 27 | func (d *structDecoder) Decode(ctx context.Context, values url.Values) error { 28 | var maps map[string][]string 29 | 30 | for name, values := range values { 31 | name = strings.TrimPrefix(name, ":") 32 | name = strings.TrimSuffix(name, "[]") 33 | 34 | if name, key, ok := mapKey(name); ok { 35 | if mdec := d.mapDecoder(name); mdec != nil { 36 | if err := mdec.decodeParam(ctx, key, values); err != nil { 37 | return err 38 | } 39 | continue 40 | } 41 | 42 | if maps == nil { 43 | maps = make(map[string][]string) 44 | } 45 | maps[name] = append(maps[name], key, values[0]) 46 | continue 47 | } 48 | 49 | if err := d.decodeParam(ctx, name, values); err != nil { 50 | return err 51 | } 52 | } 53 | 54 | for name, values := range maps { 55 | if err := d.decodeParam(ctx, name, values); err != nil { 56 | return nil 57 | } 58 | } 59 | 60 | for _, idx := range d.sinfo.unmarshalerIndexes { 61 | fv := d.v.FieldByIndex(idx) 62 | if fv.Kind() == reflect.Struct { 63 | fv = fv.Addr() 64 | } else if fv.IsNil() { 65 | fv.Set(reflect.New(fv.Type().Elem())) 66 | } 67 | 68 | u := fv.Interface().(Unmarshaler) 69 | if err := u.UnmarshalValues(ctx, values); err != nil { 70 | return err 71 | } 72 | } 73 | 74 | if d.sinfo.isUnmarshaler { 75 | return d.v.Addr().Interface().(Unmarshaler).UnmarshalValues(ctx, values) 76 | } 77 | 78 | return nil 79 | } 80 | 81 | func (d *structDecoder) mapDecoder(name string) *structDecoder { 82 | if dec, ok := d.decMap[name]; ok { 83 | return dec 84 | } 85 | if idx, ok := d.sinfo.structs[name]; ok { 86 | dec := newStructDecoder(d.v.FieldByIndex(idx)) 87 | if d.decMap == nil { 88 | d.decMap = make(map[string]*structDecoder) 89 | d.decMap[name] = dec 90 | } 91 | return dec 92 | } 93 | return nil 94 | } 95 | 96 | func (d *structDecoder) decodeParam(ctx context.Context, name string, values []string) error { 97 | if err := d._decodeParam(ctx, name, values); err != nil { 98 | return fmt.Errorf("urlstruct: can't decode %q: %w", name, err) 99 | } 100 | return nil 101 | } 102 | 103 | func (d *structDecoder) _decodeParam(ctx context.Context, name string, values []string) error { 104 | if field := d.sinfo.Field(name); field != nil && !field.noDecode { 105 | return field.scanValue(field.Value(d.v), values) 106 | } 107 | 108 | if d.sinfo.isParamUnmarshaler { 109 | if d.paramUnmarshaler == nil { 110 | d.paramUnmarshaler = d.v.Addr().Interface().(ParamUnmarshaler) 111 | } 112 | return d.paramUnmarshaler.UnmarshalParam(ctx, name, values) 113 | } 114 | 115 | return nil 116 | } 117 | 118 | func mapKey(s string) (name string, key string, ok bool) { 119 | ind := strings.IndexByte(s, '[') 120 | if ind == -1 || s[len(s)-1] != ']' { 121 | return "", "", false 122 | } 123 | key = s[ind+1 : len(s)-1] 124 | if key == "" { 125 | return "", "", false 126 | } 127 | name = s[:ind] 128 | return name, key, true 129 | } 130 | -------------------------------------------------------------------------------- /struct_info.go: -------------------------------------------------------------------------------- 1 | package urlstruct 2 | 3 | import ( 4 | "context" 5 | "net/url" 6 | "reflect" 7 | 8 | "github.com/codemodus/kace" 9 | "github.com/vmihailenco/tagparser" 10 | ) 11 | 12 | type Unmarshaler interface { 13 | UnmarshalValues(ctx context.Context, values url.Values) error 14 | } 15 | 16 | type ParamUnmarshaler interface { 17 | UnmarshalParam(ctx context.Context, name string, values []string) error 18 | } 19 | 20 | //------------------------------------------------------------------------------ 21 | 22 | type StructInfo struct { 23 | fields []*Field 24 | fieldMap map[string]*Field 25 | 26 | structs map[string][]int 27 | 28 | isUnmarshaler bool 29 | isParamUnmarshaler bool 30 | unmarshalerIndexes [][]int 31 | } 32 | 33 | func newStructInfo(typ reflect.Type) *StructInfo { 34 | sinfo := &StructInfo{ 35 | fields: make([]*Field, 0, typ.NumField()), 36 | fieldMap: make(map[string]*Field), 37 | 38 | isUnmarshaler: isUnmarshaler(reflect.PtrTo(typ)), 39 | isParamUnmarshaler: isParamUnmarshaler(reflect.PtrTo(typ)), 40 | } 41 | addFields(sinfo, typ, nil) 42 | return sinfo 43 | } 44 | 45 | func (s *StructInfo) Field(name string) *Field { 46 | return s.fieldMap[name] 47 | } 48 | 49 | func addFields(sinfo *StructInfo, typ reflect.Type, baseIndex []int) { 50 | for i := 0; i < typ.NumField(); i++ { 51 | sf := typ.Field(i) 52 | if sf.PkgPath != "" && !sf.Anonymous { 53 | continue 54 | } 55 | 56 | if sf.Anonymous { 57 | tag := sf.Tag.Get("urlstruct") 58 | if tag == "-" { 59 | continue 60 | } 61 | 62 | sfType := sf.Type 63 | if sfType.Kind() == reflect.Ptr { 64 | sfType = sfType.Elem() 65 | } 66 | if sfType.Kind() != reflect.Struct { 67 | continue 68 | } 69 | 70 | if isUnmarshaler(reflect.PtrTo(sfType)) { 71 | index := joinIndex(baseIndex, sf.Index) 72 | sinfo.unmarshalerIndexes = append(sinfo.unmarshalerIndexes, index) 73 | } 74 | 75 | addFields(sinfo, sfType, joinIndex(baseIndex, sf.Index)) 76 | } else { 77 | addField(sinfo, sf, baseIndex) 78 | } 79 | } 80 | } 81 | 82 | func addField(sinfo *StructInfo, sf reflect.StructField, baseIndex []int) { 83 | tag := tagparser.Parse(sf.Tag.Get("urlstruct")) 84 | if tag.Name == "-" { 85 | return 86 | } 87 | 88 | name := tag.Name 89 | if name == "" { 90 | name = sf.Name 91 | } 92 | index := joinIndex(baseIndex, sf.Index) 93 | 94 | if sf.Type.Kind() == reflect.Struct { 95 | if sinfo.structs == nil { 96 | sinfo.structs = make(map[string][]int) 97 | } 98 | sinfo.structs[kace.Snake(name)] = append(baseIndex, sf.Index...) 99 | } 100 | 101 | if isUnmarshaler(reflect.PtrTo(sf.Type)) { 102 | sinfo.unmarshalerIndexes = append(sinfo.unmarshalerIndexes, index) 103 | } 104 | 105 | f := &Field{ 106 | Type: sf.Type, 107 | Name: kace.Snake(name), 108 | Index: index, 109 | Tag: tag, 110 | } 111 | f.init() 112 | 113 | if f.scanValue != nil { 114 | sinfo.fields = append(sinfo.fields, f) 115 | sinfo.fieldMap[f.Name] = f 116 | } 117 | } 118 | 119 | func joinIndex(base, idx []int) []int { 120 | if len(base) == 0 { 121 | return idx 122 | } 123 | r := make([]int, 0, len(base)+len(idx)) 124 | r = append(r, base...) 125 | r = append(r, idx...) 126 | return r 127 | } 128 | 129 | //------------------------------------------------------------------------------ 130 | 131 | var ( 132 | contextType = reflect.TypeOf((*context.Context)(nil)).Elem() 133 | urlValuesType = reflect.TypeOf((*url.Values)(nil)).Elem() 134 | errorType = reflect.TypeOf((*error)(nil)).Elem() 135 | ) 136 | 137 | func isUnmarshaler(typ reflect.Type) bool { 138 | for i := 0; i < typ.NumMethod(); i++ { 139 | meth := typ.Method(i) 140 | if meth.Name == "UnmarshalValues" && 141 | meth.Type.NumIn() == 3 && 142 | meth.Type.NumOut() == 1 && 143 | meth.Type.In(1) == contextType && 144 | meth.Type.In(2) == urlValuesType && 145 | meth.Type.Out(0) == errorType { 146 | return true 147 | } 148 | } 149 | return false 150 | } 151 | 152 | var ( 153 | stringType = reflect.TypeOf("") 154 | stringSliceType = reflect.TypeOf((*[]string)(nil)).Elem() 155 | ) 156 | 157 | func isParamUnmarshaler(typ reflect.Type) bool { 158 | for i := 0; i < typ.NumMethod(); i++ { 159 | meth := typ.Method(i) 160 | if meth.Name == "UnmarshalParam" && 161 | meth.Type.NumIn() == 4 && 162 | meth.Type.NumOut() == 1 && 163 | meth.Type.In(1) == contextType && 164 | meth.Type.In(2) == stringType && 165 | meth.Type.In(3) == stringSliceType && 166 | meth.Type.Out(0) == errorType { 167 | return true 168 | } 169 | } 170 | return false 171 | } 172 | -------------------------------------------------------------------------------- /decode_test.go: -------------------------------------------------------------------------------- 1 | package urlstruct_test 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "encoding" 7 | "net/url" 8 | "testing" 9 | "time" 10 | 11 | . "github.com/onsi/ginkgo" 12 | . "github.com/onsi/gomega" 13 | 14 | "github.com/go-pg/urlstruct" 15 | "github.com/google/uuid" 16 | ) 17 | 18 | func TestGinkgo(t *testing.T) { 19 | RegisterFailHandler(Fail) 20 | RunSpecs(t, "urlstruct") 21 | } 22 | 23 | //------------------------------------------------------------------------------ 24 | 25 | type CustomField struct { 26 | S string 27 | } 28 | 29 | var _ encoding.TextUnmarshaler = (*CustomField)(nil) 30 | 31 | func (f *CustomField) UnmarshalText(text []byte) error { 32 | f.S = string(text) 33 | return nil 34 | } 35 | 36 | //------------------------------------------------------------------------------ 37 | 38 | type SubFilter struct { 39 | Count int 40 | } 41 | 42 | var _ urlstruct.Unmarshaler = (*SubFilter)(nil) 43 | 44 | func (f *SubFilter) UnmarshalValues(ctx context.Context, values url.Values) error { 45 | f.Count++ 46 | return nil 47 | } 48 | 49 | //------------------------------------------------------------------------------ 50 | 51 | type StructMap struct { 52 | Foo string 53 | Bar string 54 | UnknownMap map[string][]string `urlstruct:",unknown"` 55 | } 56 | 57 | var _ urlstruct.ParamUnmarshaler = (*StructMap)(nil) 58 | 59 | func (s *StructMap) UnmarshalParam(ctx context.Context, name string, values []string) error { 60 | if s.UnknownMap == nil { 61 | s.UnknownMap = make(map[string][]string) 62 | } 63 | s.UnknownMap[name] = values 64 | return nil 65 | } 66 | 67 | //------------------------------------------------------------------------------ 68 | 69 | type Filter struct { 70 | unexported string //nolint:unused,structcheck 71 | 72 | SubFilter 73 | Sub SubFilter 74 | SMap StructMap 75 | Count int 76 | 77 | Field string 78 | FieldNEQ string 79 | FieldLT int8 80 | FieldLTE int16 81 | FieldGT int32 82 | FieldGTE int64 83 | 84 | Multi []string 85 | MultiNEQ []int 86 | 87 | Time time.Time 88 | StartTimeGTE time.Time 89 | 90 | NullBool sql.NullBool 91 | NullInt64 sql.NullInt64 92 | NullFloat64 sql.NullFloat64 93 | NullString sql.NullString 94 | 95 | Map map[string]string 96 | Custom CustomField 97 | 98 | Omit []byte `pg:"-"` 99 | 100 | Uuid []uuid.UUID 101 | } 102 | 103 | var _ urlstruct.Unmarshaler = (*Filter)(nil) 104 | 105 | func (f *Filter) UnmarshalValues(ctx context.Context, values url.Values) error { 106 | f.Count++ 107 | return nil 108 | } 109 | 110 | var _ = Describe("Decode", func() { 111 | ctx := context.TODO() 112 | 113 | It("decodes struct from Values", func() { 114 | f := new(Filter) 115 | err := urlstruct.Unmarshal(ctx, url.Values{ 116 | "unexported": {"test"}, 117 | 118 | "s_map[foo]": {"foo_value"}, 119 | "s_map[bar]": {"bar_value"}, 120 | "s_map[hello]": {"world"}, 121 | 122 | "field": {"one"}, 123 | "field_neq": {"two"}, 124 | "field_lt": {"1"}, 125 | "field_lte": {"2"}, 126 | "field_gt": {"3"}, 127 | "field_gte": {"4"}, 128 | 129 | "multi": {"one", "two"}, 130 | "multi_neq": {"3", "4"}, 131 | 132 | "time": {"1970-01-01T00:00:00Z"}, 133 | "start_time_gte": {"1970-01-01T00:00:00Z"}, 134 | 135 | "null_bool": {"t"}, 136 | "null_int64": {"1234"}, 137 | "null_float64": {"1.234"}, 138 | "null_string": {"string"}, 139 | 140 | "map[foo]": {`bar`}, 141 | "map[hello]": {`world`}, 142 | "map[]": {"invalid"}, 143 | "map][": {"invalid"}, 144 | 145 | "custom": {"custom"}, 146 | 147 | "uuid": {"3fa85f64-5717-4562-b3fc-2c963f66afa6"}, 148 | }, f) 149 | Expect(err).NotTo(HaveOccurred()) 150 | 151 | Expect(f).To(Equal(&Filter{ 152 | SubFilter: SubFilter{Count: 1}, 153 | Sub: SubFilter{Count: 1}, 154 | Count: 1, 155 | 156 | SMap: StructMap{ 157 | Foo: "foo_value", 158 | Bar: "bar_value", 159 | UnknownMap: map[string][]string{"hello": {"world"}}, 160 | }, 161 | 162 | Field: "one", 163 | FieldNEQ: "two", 164 | FieldLT: 1, 165 | FieldLTE: 2, 166 | FieldGT: 3, 167 | FieldGTE: 4, 168 | 169 | Multi: []string{"one", "two"}, 170 | MultiNEQ: []int{3, 4}, 171 | 172 | Time: time.Unix(0, 0).UTC(), 173 | StartTimeGTE: time.Unix(0, 0).UTC(), 174 | 175 | NullBool: sql.NullBool{Bool: true, Valid: true}, 176 | NullInt64: sql.NullInt64{Int64: 1234, Valid: true}, 177 | NullFloat64: sql.NullFloat64{Float64: 1.234, Valid: true}, 178 | NullString: sql.NullString{String: "string", Valid: true}, 179 | 180 | Map: map[string]string{"foo": "bar", "hello": "world"}, 181 | Custom: CustomField{S: "custom"}, 182 | Omit: nil, 183 | Uuid: []uuid.UUID{uuid.MustParse("3fa85f64-5717-4562-b3fc-2c963f66afa6")}, 184 | })) 185 | }) 186 | 187 | It("supports names with suffix `[]`", func() { 188 | f := new(Filter) 189 | err := urlstruct.Unmarshal(ctx, url.Values{ 190 | "field[]": {"one"}, 191 | }, f) 192 | Expect(err).NotTo(HaveOccurred()) 193 | Expect(f.Field).To(Equal("one")) 194 | }) 195 | 196 | It("supports names with prefix `:`", func() { 197 | f := new(Filter) 198 | err := urlstruct.Unmarshal(ctx, url.Values{ 199 | ":field": {"one"}, 200 | }, f) 201 | Expect(err).NotTo(HaveOccurred()) 202 | Expect(f.Field).To(Equal("one")) 203 | }) 204 | 205 | It("decodes sql.Null*", func() { 206 | f := new(Filter) 207 | err := urlstruct.Unmarshal(ctx, url.Values{ 208 | "null_bool": {""}, 209 | "null_int64": {""}, 210 | "null_float64": {""}, 211 | "null_string": {""}, 212 | }, f) 213 | Expect(err).NotTo(HaveOccurred()) 214 | 215 | Expect(f.NullBool.Valid).To(BeTrue()) 216 | Expect(f.NullBool.Bool).To(BeZero()) 217 | 218 | Expect(f.NullInt64.Valid).To(BeTrue()) 219 | Expect(f.NullInt64.Int64).To(BeZero()) 220 | 221 | Expect(f.NullFloat64.Valid).To(BeTrue()) 222 | Expect(f.NullFloat64.Float64).To(BeZero()) 223 | 224 | Expect(f.NullString.Valid).To(BeTrue()) 225 | Expect(f.NullString.String).To(BeZero()) 226 | }) 227 | 228 | It("calls UnmarshalValues", func() { 229 | f := new(Filter) 230 | err := urlstruct.Unmarshal(ctx, url.Values{}, f) 231 | Expect(err).NotTo(HaveOccurred()) 232 | Expect(f.Count).To(Equal(1)) 233 | Expect(f.SubFilter.Count).To(Equal(1)) 234 | Expect(f.Sub.Count).To(Equal(1)) 235 | }) 236 | }) 237 | -------------------------------------------------------------------------------- /scan.go: -------------------------------------------------------------------------------- 1 | package urlstruct 2 | 3 | import ( 4 | "database/sql" 5 | "encoding" 6 | "fmt" 7 | "reflect" 8 | "strconv" 9 | "time" 10 | ) 11 | 12 | var ( 13 | textUnmarshalerType = reflect.TypeOf((*encoding.TextUnmarshaler)(nil)).Elem() 14 | timeType = reflect.TypeOf((*time.Time)(nil)).Elem() 15 | durationType = reflect.TypeOf((*time.Duration)(nil)).Elem() 16 | nullBoolType = reflect.TypeOf((*sql.NullBool)(nil)).Elem() 17 | nullInt64Type = reflect.TypeOf((*sql.NullInt64)(nil)).Elem() 18 | nullFloat64Type = reflect.TypeOf((*sql.NullFloat64)(nil)).Elem() 19 | nullStringType = reflect.TypeOf((*sql.NullString)(nil)).Elem() 20 | mapStringStringType = reflect.TypeOf((*map[string]string)(nil)).Elem() 21 | ) 22 | 23 | type scannerFunc func(v reflect.Value, values []string) error 24 | 25 | func scanner(typ reflect.Type) scannerFunc { 26 | if typ == timeType { 27 | return scanTime 28 | } 29 | 30 | if typ.Implements(textUnmarshalerType) { 31 | return scanTextUnmarshaler 32 | } 33 | if reflect.PtrTo(typ).Implements(textUnmarshalerType) { 34 | return scanTextUnmarshalerAddr 35 | } 36 | 37 | switch typ { 38 | case durationType: 39 | return scanDuration 40 | case nullBoolType: 41 | return scanNullBool 42 | case nullInt64Type: 43 | return scanNullInt64 44 | case nullFloat64Type: 45 | return scanNullFloat64 46 | case nullStringType: 47 | return scanNullString 48 | case mapStringStringType: 49 | return scanMapStringString 50 | } 51 | 52 | switch typ.Kind() { 53 | case reflect.Bool: 54 | return scanBool 55 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 56 | return scanInt64 57 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: 58 | return scanUint64 59 | case reflect.Float32: 60 | return scanFloat32 61 | case reflect.Float64: 62 | return scanFloat64 63 | case reflect.String: 64 | return scanString 65 | } 66 | return nil 67 | } 68 | 69 | func sliceScanner(typ reflect.Type) scannerFunc { 70 | switch typ.Elem().Kind() { 71 | case reflect.Int: 72 | return scanIntSlice 73 | case reflect.Int32: 74 | return scanInt32Slice 75 | case reflect.Int64: 76 | return scanInt64Slice 77 | case reflect.String: 78 | return scanStringSlice 79 | } 80 | 81 | if elementScanner := scanner(typ.Elem()); elementScanner != nil { 82 | return func(v reflect.Value, values []string) error { 83 | nn := reflect.MakeSlice(typ, 0, len(values)) 84 | for _, s := range values { 85 | n := reflect.New(typ.Elem()) 86 | err := elementScanner(n.Elem(), []string{s}) 87 | if err != nil { 88 | return err 89 | } 90 | nn = reflect.Append(nn, n.Elem()) 91 | } 92 | v.Set(nn) 93 | 94 | return nil 95 | } 96 | } 97 | 98 | return nil 99 | } 100 | 101 | func scanTextUnmarshaler(v reflect.Value, values []string) error { 102 | if v.IsNil() { 103 | v.Set(reflect.New(v.Type().Elem())) 104 | } 105 | 106 | u := v.Interface().(encoding.TextUnmarshaler) 107 | return u.UnmarshalText([]byte(values[0])) 108 | } 109 | 110 | func scanTextUnmarshalerAddr(v reflect.Value, values []string) error { 111 | if !v.CanAddr() { 112 | return fmt.Errorf("pg: Scan(nonsettable %s)", v.Type()) 113 | } 114 | u := v.Addr().Interface().(encoding.TextUnmarshaler) 115 | return u.UnmarshalText([]byte(values[0])) 116 | } 117 | 118 | func scanBool(v reflect.Value, values []string) error { 119 | f, err := strconv.ParseBool(values[0]) 120 | if err != nil { 121 | return err 122 | } 123 | v.SetBool(f) 124 | return nil 125 | } 126 | 127 | func scanInt64(v reflect.Value, values []string) error { 128 | n, err := strconv.ParseInt(values[0], 10, 64) 129 | if err != nil { 130 | return err 131 | } 132 | v.SetInt(n) 133 | return nil 134 | } 135 | 136 | func scanUint64(v reflect.Value, values []string) error { 137 | n, err := strconv.ParseUint(values[0], 10, 64) 138 | if err != nil { 139 | return err 140 | } 141 | v.SetUint(n) 142 | return nil 143 | } 144 | 145 | func scanFloat32(v reflect.Value, values []string) error { 146 | return scanFloat(v, values, 32) 147 | } 148 | 149 | func scanFloat64(v reflect.Value, values []string) error { 150 | return scanFloat(v, values, 64) 151 | } 152 | 153 | func scanFloat(v reflect.Value, values []string, bits int) error { 154 | n, err := strconv.ParseFloat(values[0], bits) 155 | if err != nil { 156 | return err 157 | } 158 | v.SetFloat(n) 159 | return nil 160 | } 161 | 162 | func scanString(v reflect.Value, values []string) error { 163 | v.SetString(values[0]) 164 | return nil 165 | } 166 | 167 | func scanTime(v reflect.Value, values []string) error { 168 | tm, err := parseTime(values[0]) 169 | if err != nil { 170 | return err 171 | } 172 | v.Set(reflect.ValueOf(tm)) 173 | return nil 174 | } 175 | 176 | func parseTime(s string) (time.Time, error) { 177 | n, err := strconv.ParseInt(s, 10, 64) 178 | if err == nil { 179 | return time.Unix(n, 0), nil 180 | } 181 | 182 | if len(s) >= 5 && s[4] == '-' { 183 | return time.Parse(time.RFC3339Nano, s) 184 | } 185 | 186 | if len(s) == 15 { 187 | const basicFormat = "20060102T150405" 188 | return time.Parse(basicFormat, s) 189 | } 190 | 191 | const basicFormat = "20060102T150405-07:00" 192 | return time.Parse(basicFormat, s) 193 | } 194 | 195 | func scanDuration(v reflect.Value, values []string) error { 196 | dur, err := time.ParseDuration(values[0]) 197 | if err != nil { 198 | return err 199 | } 200 | v.SetInt(int64(dur)) 201 | return nil 202 | } 203 | 204 | func scanNullBool(v reflect.Value, values []string) error { 205 | value := sql.NullBool{ 206 | Valid: true, 207 | } 208 | 209 | s := values[0] 210 | if s == "" { 211 | v.Set(reflect.ValueOf(value)) 212 | return nil 213 | } 214 | 215 | f, err := strconv.ParseBool(s) 216 | if err != nil { 217 | return err 218 | } 219 | 220 | value.Bool = f 221 | v.Set(reflect.ValueOf(value)) 222 | 223 | return nil 224 | } 225 | 226 | func scanNullInt64(v reflect.Value, values []string) error { 227 | value := sql.NullInt64{ 228 | Valid: true, 229 | } 230 | 231 | s := values[0] 232 | if s == "" { 233 | v.Set(reflect.ValueOf(value)) 234 | return nil 235 | } 236 | 237 | n, err := strconv.ParseInt(s, 10, 64) 238 | if err != nil { 239 | return err 240 | } 241 | 242 | value.Int64 = n 243 | v.Set(reflect.ValueOf(value)) 244 | 245 | return nil 246 | } 247 | 248 | func scanNullFloat64(v reflect.Value, values []string) error { 249 | value := sql.NullFloat64{ 250 | Valid: true, 251 | } 252 | 253 | s := values[0] 254 | if s == "" { 255 | v.Set(reflect.ValueOf(value)) 256 | return nil 257 | } 258 | 259 | n, err := strconv.ParseFloat(s, 64) 260 | if err != nil { 261 | return err 262 | } 263 | 264 | value.Float64 = n 265 | v.Set(reflect.ValueOf(value)) 266 | 267 | return nil 268 | } 269 | 270 | func scanNullString(v reflect.Value, values []string) error { 271 | value := sql.NullString{ 272 | Valid: true, 273 | } 274 | 275 | s := values[0] 276 | if s == "" { 277 | v.Set(reflect.ValueOf(value)) 278 | return nil 279 | } 280 | 281 | value.String = s 282 | v.Set(reflect.ValueOf(value)) 283 | 284 | return nil 285 | } 286 | 287 | func scanMapStringString(v reflect.Value, values []string) error { 288 | if len(values)%2 != 0 { 289 | return nil 290 | } 291 | 292 | m := make(map[string]string) 293 | for i := 0; i < len(values); i += 2 { 294 | m[values[i]] = values[i+1] 295 | } 296 | v.Set(reflect.ValueOf(m)) 297 | return nil 298 | } 299 | 300 | func scanIntSlice(v reflect.Value, values []string) error { 301 | nn := make([]int, 0, len(values)) 302 | for _, s := range values { 303 | n, err := strconv.Atoi(s) 304 | if err != nil { 305 | return err 306 | } 307 | nn = append(nn, n) 308 | } 309 | v.Set(reflect.ValueOf(nn)) 310 | return nil 311 | } 312 | 313 | func scanInt32Slice(v reflect.Value, values []string) error { 314 | nn := make([]int32, 0, len(values)) 315 | for _, s := range values { 316 | n, err := strconv.ParseInt(s, 10, 32) 317 | if err != nil { 318 | return err 319 | } 320 | nn = append(nn, int32(n)) 321 | } 322 | v.Set(reflect.ValueOf(nn)) 323 | return nil 324 | } 325 | 326 | func scanInt64Slice(v reflect.Value, values []string) error { 327 | nn := make([]int64, 0, len(values)) 328 | for _, s := range values { 329 | n, err := strconv.ParseInt(s, 10, 64) 330 | if err != nil { 331 | return err 332 | } 333 | nn = append(nn, n) 334 | } 335 | v.Set(reflect.ValueOf(nn)) 336 | return nil 337 | } 338 | 339 | func scanStringSlice(v reflect.Value, values []string) error { 340 | v.Set(reflect.ValueOf(values)) 341 | return nil 342 | } 343 | -------------------------------------------------------------------------------- /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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= 4 | github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= 5 | github.com/codemodus/kace v0.5.1 h1:4OCsBlE2c/rSJo375ggfnucv9eRzge/U5LrrOZd47HA= 6 | github.com/codemodus/kace v0.5.1/go.mod h1:coddaHoX1ku1YFSe4Ip0mL9kQjJvKkzb9CfIdG1YR04= 7 | github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= 8 | github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= 9 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 10 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 11 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 12 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= 13 | github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= 14 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 15 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 16 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 17 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 18 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 19 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 20 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 21 | github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= 22 | github.com/golang/protobuf v1.4.2 h1:+Z5KGCizgyZCbGh1KZqA0fcLLkwbsjIzS4aV2v7wJX0= 23 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 24 | github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= 25 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 26 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 27 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 28 | github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 29 | github.com/google/go-cmp v0.5.1 h1:JFrFEBb2xKufg6XkJsJr+WbKb4FQlURi5RUcBveYu9k= 30 | github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 31 | github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= 32 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 33 | github.com/google/uuid v1.1.4/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 34 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 35 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 36 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 37 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 38 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 39 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 40 | github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= 41 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 42 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 43 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 44 | github.com/onsi/ginkgo v1.14.1 h1:jMU0WaQrP0a/YAEq8eJmJKjBoMs+pClEr1vDMlM/Do4= 45 | github.com/onsi/ginkgo v1.14.1/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= 46 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 47 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 48 | github.com/onsi/gomega v1.10.2 h1:aY/nuoWlKJud2J6U0E3NWsjlg+0GtwXxgEqthRdzlcs= 49 | github.com/onsi/gomega v1.10.2/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 50 | github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 51 | github.com/vmihailenco/tagparser v0.1.2 h1:gnjoVuB/kljJ5wICEEOpx98oXMWPLj22G67Vbd1qPqc= 52 | github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= 53 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 54 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 55 | golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= 56 | golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= 57 | golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= 58 | golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 59 | golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 60 | golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 61 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 62 | golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 63 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 64 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 65 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 66 | golang.org/x/net v0.0.0-20200904194848-62affa334b73 h1:MXfv8rhZWmFeqX3GNZRsd6vOLoaCHjYEX3qkRo3YBUA= 67 | golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 68 | golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= 69 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 70 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 71 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 72 | golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 73 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 74 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 75 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 76 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 77 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 78 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 79 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 80 | golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 81 | golang.org/x/sys v0.0.0-20200908134130-d2e65c121b96 h1:gJciq3lOg0eS9fSZJcoHfv7q1BfC6cJfnmSSKL1yu3Q= 82 | golang.org/x/sys v0.0.0-20200908134130-d2e65c121b96/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 83 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 84 | golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= 85 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 86 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 87 | golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 88 | golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= 89 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 90 | golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= 91 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 92 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 93 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 94 | google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= 95 | google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= 96 | google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= 97 | google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= 98 | google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= 99 | google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= 100 | google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 101 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 102 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 103 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 104 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 105 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 106 | google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 107 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 108 | google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 109 | google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= 110 | google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= 111 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 112 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 113 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 114 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 115 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 116 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 117 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 118 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 119 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 120 | honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 121 | honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= 122 | --------------------------------------------------------------------------------