├── .codecov.yml ├── .github └── workflows │ └── go.yml ├── .gitignore ├── LICENSE ├── README.md ├── benchmark └── benchmark_test.go ├── doc.go ├── encode.go ├── encode_cache.go ├── encode_cache_test.go ├── encode_field.go ├── encode_test.go ├── err.go ├── example ├── basic │ └── basic.go ├── custom │ └── custom.go ├── embed │ └── embed.go ├── slice │ └── slice.go └── timefmt │ └── timefmt.go ├── go.mod └── go.sum /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | round: down 3 | precision: 2 4 | status: 5 | project: 6 | default: 7 | target: 95% 8 | threshold: 0.1% 9 | patch: 10 | default: 11 | target: 90% -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: build-test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build-test: 11 | name: Build & Test 12 | runs-on: ubuntu-latest 13 | timeout-minutes: 1 14 | steps: 15 | 16 | - name: Set up Go 1.19 17 | uses: actions/setup-go@v5 18 | with: 19 | go-version: ^1.19 20 | id: go 21 | 22 | - name: Check out code into the Go module directory 23 | uses: actions/checkout@v4 24 | 25 | - name: Get dependencies 26 | run: | 27 | go get -v -t -d ./... 28 | if [ -f Gopkg.toml ]; then 29 | curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh 30 | dep ensure 31 | fi 32 | 33 | - name: Build 34 | run: go build -v . 35 | 36 | - name: Test 37 | run: go test -v -coverprofile=coverage.out . 38 | 39 | - name: Codecov 40 | uses: codecov/codecov-action@v4 41 | with: 42 | file: ./coverage.out 43 | token: ${{ secrets.CODECOV_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #Goland build dir 2 | .idea 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | 10 | # Test binary, built with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | #Executable file 17 | main 18 | 19 | # Dependency directories (remove the comment below to include it) 20 | vendor -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Son Huynh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qs # 2 | [![Build](https://github.com/sonh/qs/actions/workflows/go.yml/badge.svg?branch=main)](https://github.com/sonh/qs/actions) 3 | [![Codecov](https://codecov.io/gh/sonh/qs/branch/main/graph/badge.svg)](https://codecov.io/gh/sonh/qs) 4 | [![GoReportCard](https://goreportcard.com/badge/github.com/sonh/qs)](https://goreportcard.com/report/github.com/sonh/qs) 5 | [![Release](https://img.shields.io/github/release/sonh/qs.svg?color=brightgreen)](https://github.com/sonh/qs/releases/) 6 | [![PkgGoDev](https://pkg.go.dev/badge/github.com/sonh/qs)](https://pkg.go.dev/github.com/sonh/qs) 7 | [![MIT License](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/sonh/qs/blob/main/LICENSE) 8 | 9 | Zero-dependencies package to encodes structs into url.Values. 10 | 11 | ## Installation 12 | ```bash 13 | go get github.com/sonh/qs 14 | ``` 15 | 16 | ## Usage 17 | ```go 18 | import ( 19 | "github.com/sonh/qs" 20 | ) 21 | ``` 22 | Package qs exports `NewEncoder()` function to create an encoder. 23 | 24 | Encoder caches struct info to speed up encoding process, use a single instance is highly recommended. 25 | 26 | Use `WithTagAlias()` func to register custom tag alias (default is `qs`) 27 | ```go 28 | encoder = qs.NewEncoder( 29 | qs.WithTagAlias("myTag"), 30 | ) 31 | ``` 32 | 33 | Encoder has `Values()` and `Encode()` functions to encode structs into `url.Values`. 34 | 35 | ### Supported data types: 36 | - all basic types (`bool`, `uint`, `string`, `float64`,...) 37 | - `struct` 38 | - `slice`, `array` 39 | - `pointer` 40 | - `time.Time` 41 | - custom type 42 | 43 | ### Example 44 | ```go 45 | type Query struct { 46 | Tags []string `qs:"tags"` 47 | Limit int `qs:"limit"` 48 | From time.Time `qs:"from"` 49 | Active bool `qs:"active,omitempty"` //omit empty value 50 | Ignore float64 `qs:"-"` //ignore 51 | } 52 | 53 | query := &Query{ 54 | Tags: []string{"docker", "golang", "reactjs"}, 55 | Limit: 24, 56 | From: time.Unix(1580601600, 0).UTC(), 57 | Ignore: 0, 58 | } 59 | 60 | encoder := qs.NewEncoder() 61 | values, err := encoder.Values(query) 62 | if err != nil { 63 | // Handle error 64 | } 65 | fmt.Println(values.Encode()) //(unescaped) output: "from=2020-02-02T00:00:00Z&limit=24&tags=docker&tags=golang&tags=reactjs" 66 | ``` 67 | ### Bool format 68 | Use `int` option to encode bool to integer 69 | ```go 70 | type Query struct { 71 | DefaultFmt bool `qs:"default_fmt"` 72 | IntFmt bool `qs:"int_fmt,int"` 73 | } 74 | 75 | query := &Query{ 76 | DefaultFmt: true, 77 | IntFmt: true, 78 | } 79 | values, _ := encoder.Values(query) 80 | fmt.Println(values.Encode()) // (unescaped) output: "default_fmt=true&int_fmt=1" 81 | ``` 82 | ### Time format 83 | By default, package encodes time.Time values as RFC3339 format. 84 | 85 | Including the `"second"` or `"millis"` option to signal that the field should be encoded as second or millisecond. 86 | ```go 87 | type Query struct { 88 | Default time.Time `qs:"default_fmt"` 89 | Second time.Time `qs:"second_fmt,second"` //use `second` option 90 | Millis time.Time `qs:"millis_fmt,millis"` //use `millis` option 91 | } 92 | 93 | t := time.Unix(1580601600, 0).UTC() 94 | query := &Query{ 95 | Default: t, 96 | Second: t, 97 | Millis: t, 98 | } 99 | 100 | encoder := qs.NewEncoder() 101 | values, _ := encoder.Values(query) 102 | fmt.Println(values.Encode()) // (unescaped) output: "default_fmt=2020-02-02T00:00:00Z&millis_fmt=1580601600000&second_fmt=1580601600" 103 | ``` 104 | 105 | ### Slice/Array Format 106 | Slice and Array default to encoding into multiple URL values of the same value name. 107 | ```go 108 | type Query struct { 109 | Tags []string `qs:"tags"` 110 | } 111 | 112 | values, _ := encoder.Values(&Query{Tags: []string{"foo","bar"}}) 113 | fmt.Println(values.Encode()) //(unescaped) output: "tags=foo&tags=bar" 114 | ``` 115 | 116 | Including the `comma` option to signal that the field should be encoded as a single comma-delimited value. 117 | ```go 118 | type Query struct { 119 | Tags []string `qs:"tags,comma"` 120 | } 121 | 122 | values, _ := encoder.Values(&Query{Tags: []string{"foo","bar"}}) 123 | fmt.Println(values.Encode()) //(unescaped) output: "tags=foo,bar" 124 | ``` 125 | 126 | Including the `bracket` option to signal that the multiple URL values should have "[]" appended to the value name. 127 | ```go 128 | type Query struct { 129 | Tags []string `qs:"tags,bracket"` 130 | } 131 | 132 | values, _ := encoder.Values(&Query{Tags: []string{"foo","bar"}}) 133 | fmt.Println(values.Encode()) //(unescaped) output: "tags[]=foo&tags[]=bar" 134 | ``` 135 | 136 | The `index` option will append an index number with brackets to value name. 137 | ```go 138 | type Query struct { 139 | Tags []string `qs:"tags,index"` 140 | } 141 | 142 | values, _ := encoder.Values(&Query{Tags: []string{"foo","bar"}}) 143 | fmt.Println(values.Encode()) //(unescaped) output: "tags[0]=foo&tags[1]=bar" 144 | ``` 145 | 146 | ### Nested structs 147 | All nested structs are encoded including the parent value name with brackets for scoping. 148 | ```go 149 | type User struct { 150 | Verified bool `qs:"verified"` 151 | From time.Time `qs:"from,millis"` 152 | } 153 | 154 | type Query struct { 155 | User User `qs:"user"` 156 | } 157 | 158 | query := Query{ 159 | User: User{ 160 | Verified: true, 161 | From: time.Now(), 162 | }, 163 | } 164 | values, _ := encoder.Values(query) 165 | fmt.Println(values.Encode()) //(unescaped) output: "user[from]=1601623397728&user[verified]=true" 166 | ``` 167 | 168 | ### Custom Type 169 | Implement funcs: 170 | * `EncodeParam` to encode itself into query param. 171 | * `IsZero` to check whether an object is zero to determine whether it should be omitted when encoding. 172 | ```go 173 | type NullableName struct { 174 | First string 175 | Last string 176 | } 177 | 178 | func (n NullableName) EncodeParam() (string, error) { 179 | return n.First + n.Last, nil 180 | } 181 | 182 | func (n NullableName) IsZero() bool { 183 | return n.First == "" && n.Last == "" 184 | } 185 | 186 | type Struct struct { 187 | User NullableName `qs:"user"` 188 | Admin NullableName `qs:"admin,omitempty"` 189 | } 190 | 191 | s := Struct{ 192 | User: NullableName{ 193 | First: "son", 194 | Last: "huynh", 195 | }, 196 | } 197 | encoder := qs.NewEncoder() 198 | 199 | values, err := encoder.Values(&s) 200 | if err != nil { 201 | // Handle error 202 | fmt.Println("failed") 203 | return 204 | } 205 | fmt.Println(values.Encode()) //(unescaped) output: "user=sonhuynh" 206 | ``` 207 | 208 | ### Limitation 209 | - if elements in `slice/array` are `struct` data type, multi-level nesting are limited 210 | - no decoder yet 211 | 212 | _Will improve in future versions_ 213 | 214 | ## License 215 | Distributed under MIT License, please see license file in code for more details. 216 | -------------------------------------------------------------------------------- /benchmark/benchmark_test.go: -------------------------------------------------------------------------------- 1 | package benchmark 2 | 3 | import ( 4 | "github.com/sonh/qs" 5 | "testing" 6 | ) 7 | 8 | type Primitive struct { 9 | String string 10 | Bool bool 11 | Int int 12 | Int8 int8 13 | Int16 int16 14 | Int32 int32 15 | Int64 int64 16 | Uint uint 17 | Uint8 uint8 18 | Uint16 uint16 19 | Uint32 uint32 20 | Uint64 uint64 21 | Float32 float32 22 | Float64 float64 23 | } 24 | 25 | func BenchmarkEncodePrimitive(b *testing.B) { 26 | encoder := qs.NewEncoder() 27 | s := Primitive{ 28 | String: "abc", 29 | Bool: true, 30 | Int: 12, 31 | Int8: int8(8), 32 | Int16: int16(16), 33 | Int32: int32(32), 34 | Int64: int64(64), 35 | Uint: 24, 36 | Uint8: uint8(8), 37 | Uint16: uint16(16), 38 | Uint32: uint32(32), 39 | Uint64: uint64(64), 40 | } 41 | b.ReportAllocs() 42 | for n := 0; n < b.N; n++ { 43 | if _, err := encoder.Values(&s); err != nil { 44 | b.Error(err) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Son Huynh. All rights reserved. 2 | 3 | /* 4 | Package qs encodes structs into url.Values. 5 | 6 | Package exports `NewEncoder()` function to create an encoder. 7 | Use `WithTagAlias()` func to register custom tag alias (default is `qs`) 8 | 9 | encoder = qs.NewEncoder( 10 | qs.WithTagAlias("myTag"), 11 | ) 12 | 13 | Encoder has `.Values()` and `Encode()` functions to encode structs into url.Values. 14 | 15 | Supported data types: 16 | - all basic types (`bool`, `uint`, `string`, `float64`,...) 17 | - struct 18 | - slice/array 19 | - pointer 20 | - time.Time 21 | - custom type 22 | 23 | Example 24 | 25 | type Query struct { 26 | Tags []string `qs:"tags"` 27 | Limit int `qs:"limit"` 28 | From time.Time `qs:"from"` 29 | Active bool `qs:"active,omitempty"` 30 | Ignore float64 `qs:"-"` //ignore 31 | } 32 | 33 | query := &Query{ 34 | Tags: []string{"docker", "golang", "reactjs"}, 35 | Limit: 24, 36 | From: time.Unix(1580601600, 0).UTC(), 37 | Ignore: 0, 38 | } 39 | 40 | encoder = qs.NewEncoder() 41 | 42 | values, err := encoder.Values(query) 43 | if err != nil { 44 | // Handle error 45 | } 46 | fmt.Println(values.Encode()) //(unescaped) output: "from=2020-02-02T00:00:00Z&limit=24&tags=docker&tags=golang&tags=reactjs" 47 | 48 | Ignoring Fields 49 | 50 | type Struct struct { 51 | Field string `form:"-"` //using `-` to to tell qs to ignore fields 52 | } 53 | 54 | Omitempty 55 | 56 | type Struct struct { 57 | Field1 string `form:",omitempty"` //using `omitempty` to to tell qs to omit empty field 58 | Field2 *int `form:"field2,omitempty"` 59 | } 60 | 61 | By default, package encodes time.Time values as RFC3339 format. 62 | 63 | Including the `"second"` or `"millis"` option to signal that the field should be encoded as second or millisecond. 64 | 65 | type Query struct { 66 | Default time.Time `qs:"default_fmt"` 67 | Second time.Time `qs:"second_fmt,second"` //use `second` option 68 | Millis time.Time `qs:"millis_fmt,millis"` //use `millis` option 69 | } 70 | 71 | t := time.Unix(1580601600, 0).UTC() 72 | query := &Query{ 73 | Default: t, 74 | Second: t, 75 | Millis: t, 76 | Decimal: decimal.NewFromFloat(0.012147483648), 77 | } 78 | 79 | encoder = qs.NewEncoder() 80 | values, _ := encoder.Values(query) 81 | fmt.Println(values.Encode()) // (unescaped) output: "default_fmt=2020-02-02T00:00:00Z&millis_fmt=1580601600000&second_fmt=1580601600" 82 | 83 | Slice and Array default to encoding into multiple URL values of the same value name. 84 | 85 | type Query struct { 86 | Tags []string `qs:"tags"` 87 | } 88 | 89 | values, _ := encoder.Values(&Query{Tags: []string{"foo","bar"}}) 90 | fmt.Println(values.Encode()) //(unescaped) output: "tags=foo&tags=bar" 91 | 92 | Including the `comma` option to signal that the field should be encoded as a single comma-delimited value. 93 | 94 | type Query struct { 95 | Tags []string `qs:"tags,comma"` 96 | } 97 | 98 | values, _ := encoder.Values(&Query{Tags: []string{"foo","bar"}}) 99 | fmt.Println(values.Encode()) //(unescaped) output: "tags=foo,bar" 100 | 101 | Including the `bracket` option to signal that the multiple URL values should have "[]" appended to the value name. 102 | 103 | type Query struct { 104 | Tags []string `qs:"tags,bracket"` 105 | } 106 | 107 | values, _ := encoder.Values(&Query{Tags: []string{"foo","bar"}}) 108 | fmt.Println(values.Encode()) //(unescaped) output: "tags[]=foo&tags[]=bar" 109 | 110 | 111 | The `index` option will append an index number with brackets to value name 112 | 113 | type Query struct { 114 | Tags []string `qs:"tags,index"` 115 | } 116 | 117 | values, _ := encoder.Values(&Query{Tags: []string{"foo","bar"}}) 118 | fmt.Println(values.Encode()) //(unescaped) output: "tags[0]=foo&tags[1]=bar" 119 | 120 | 121 | All nested structs are encoded including the parent value name with brackets for scoping. 122 | 123 | type User struct { 124 | Verified bool `qs:"verified"` 125 | From time.Time `qs:"from,millis"` 126 | } 127 | 128 | type Query struct { 129 | User User `qs:"user"` 130 | } 131 | 132 | querys := Query{ 133 | User: User{ 134 | Verified: true, 135 | From: time.Now(), 136 | }, 137 | } 138 | values, _ := encoder.Values(querys) 139 | fmt.Println(values.Encode()) //(unescaped) output: "user[from]=1601623397728&user[verified]=true" 140 | 141 | 142 | Custom type 143 | Implement `EncodeParam` to encode itself into query param. 144 | Implement `IsZero` to check whether an object is zero to determine whether it should be omitted when encoding. 145 | 146 | type NullableName struct { 147 | First string 148 | Last string 149 | } 150 | 151 | func (n NullableName) EncodeParam() (string, error) { 152 | return n.First + n.Last, nil 153 | } 154 | 155 | func (n NullableName) IsZero() bool { 156 | return n.First == "" && n.Last == "" 157 | } 158 | 159 | type Struct struct { 160 | User NullableName `qs:"user"` 161 | Admin NullableName `qs:"admin,omitempty"` 162 | } 163 | 164 | s := Struct{ 165 | User: NullableName{ 166 | First: "son", 167 | Last: "huynh", 168 | }, 169 | } 170 | encoder := qs.NewEncoder() 171 | 172 | values, err := encoder.Values(&s) 173 | if err != nil { 174 | // Handle error 175 | fmt.Println("failed") 176 | return 177 | } 178 | fmt.Println(values.Encode()) //(unescaped) output: "user=sonhuynh" 179 | 180 | 181 | Limitation 182 | - `interface`, `[]interface`, `map` are not supported yet 183 | - `struct`, `slice`/`array` multi-level nesting are limited 184 | - no decoder yet 185 | */ 186 | package qs 187 | -------------------------------------------------------------------------------- /encode.go: -------------------------------------------------------------------------------- 1 | package qs 2 | 3 | import ( 4 | "net/url" 5 | "reflect" 6 | "strings" 7 | "sync" 8 | "time" 9 | ) 10 | 11 | const ( 12 | tagOmitEmpty = "omitempty" 13 | ) 14 | 15 | var ( 16 | timeType = reflect.TypeOf(time.Time{}) 17 | encoderType = reflect.TypeOf(new(QueryParamEncoder)).Elem() 18 | zeroerType = reflect.TypeOf(new(Zeroer)).Elem() 19 | ) 20 | 21 | // EncoderOption provides option for Encoder 22 | type EncoderOption func(encoder *Encoder) 23 | 24 | // Encoder is the main instance 25 | // Apply options by using WithTagAlias, WithCustomType 26 | type Encoder struct { 27 | tagAlias string 28 | cache *cacheStore 29 | dataPool *sync.Pool 30 | } 31 | 32 | type encoder struct { 33 | e *Encoder 34 | values url.Values 35 | tags [][]byte 36 | scope []byte 37 | } 38 | 39 | // WithTagAlias create a option to set custom tag alias instead of `qs` 40 | func WithTagAlias(tagAlias string) EncoderOption { 41 | return func(encoder *Encoder) { 42 | encoder.tagAlias = tagAlias 43 | } 44 | } 45 | 46 | // NewEncoder init new *Encoder instance 47 | // Use EncoderOption to apply options 48 | func NewEncoder(options ...EncoderOption) *Encoder { 49 | e := &Encoder{ 50 | tagAlias: "qs", 51 | } 52 | 53 | // Apply options 54 | for _, opt := range options { 55 | opt(e) 56 | } 57 | 58 | e.cache = newCacheStore() 59 | 60 | e.dataPool = &sync.Pool{New: func() interface{} { 61 | tagSize := 5 62 | tags := make([][]byte, 0, tagSize) 63 | for i := 0; i < tagSize; i++ { 64 | tags = append(tags, make([]byte, 0, 56)) 65 | } 66 | return &encoder{ 67 | e: e, 68 | tags: tags, 69 | scope: make([]byte, 0, 64), 70 | } 71 | }} 72 | 73 | return e 74 | } 75 | 76 | // Values encodes a struct into url.Values 77 | // v must be struct data type 78 | func (e *Encoder) Values(v interface{}) (url.Values, error) { 79 | val := reflect.ValueOf(v) 80 | for val.Kind() == reflect.Ptr { 81 | if val.IsNil() { 82 | return nil, InvalidInputErr{InputKind: val.Kind()} 83 | } 84 | val = val.Elem() 85 | } 86 | 87 | switch val.Kind() { 88 | case reflect.Invalid: 89 | return nil, InvalidInputErr{InputKind: val.Kind()} 90 | case reflect.Struct: 91 | enc := e.dataPool.Get().(*encoder) 92 | enc.values = make(url.Values) 93 | err := enc.encodeStruct(val, enc.values, nil) 94 | if err != nil { 95 | return nil, err 96 | } 97 | values := enc.values 98 | e.dataPool.Put(enc) 99 | return values, nil 100 | default: 101 | return nil, InvalidInputErr{InputKind: val.Kind()} 102 | } 103 | } 104 | 105 | // Encode encodes a struct into the given url.Values 106 | // v must be struct data type 107 | func (e *Encoder) Encode(v interface{}, values url.Values) error { 108 | val := reflect.ValueOf(v) 109 | for val.Kind() == reflect.Ptr { 110 | if val.IsNil() { 111 | return InvalidInputErr{InputKind: val.Kind()} 112 | } 113 | val = val.Elem() 114 | } 115 | 116 | switch val.Kind() { 117 | case reflect.Invalid: 118 | return InvalidInputErr{InputKind: val.Kind()} 119 | case reflect.Struct: 120 | enc := e.dataPool.Get().(*encoder) 121 | err := enc.encodeStruct(val, values, nil) 122 | if err != nil { 123 | return err 124 | } 125 | return nil 126 | default: 127 | return InvalidInputErr{InputKind: val.Kind()} 128 | } 129 | } 130 | 131 | func (e *encoder) encodeStruct(stVal reflect.Value, values url.Values, scope []byte) error { 132 | stTyp := stVal.Type() 133 | 134 | cachedFlds := e.e.cache.Retrieve(stTyp) 135 | 136 | if cachedFlds == nil { 137 | cachedFlds = make(cachedFields, 0, stTyp.NumField()) 138 | e.structCaching(&cachedFlds, stVal, scope) 139 | e.e.cache.Store(stTyp, cachedFlds) 140 | } 141 | 142 | for i, cachedFld := range cachedFlds { 143 | stFldVal := stVal.Field(i) 144 | 145 | switch cachedFld := cachedFld.(type) { 146 | case nil: 147 | // skip field 148 | continue 149 | case *mapField: 150 | if cachedFld.cachedKeyField == nil || cachedFld.cachedValueField == nil { 151 | //data type is not supported 152 | continue 153 | } 154 | for stFldVal.Kind() == reflect.Ptr { 155 | stFldVal = stFldVal.Elem() 156 | } 157 | if !stFldVal.IsValid() { 158 | continue 159 | } 160 | if stFldVal.Len() == 0 { 161 | continue 162 | } 163 | case *listField: 164 | if cachedFld.cachedField == nil { 165 | //data type is not supported 166 | continue 167 | } 168 | if cachedFld.arrayFormat <= arrayFormatBracket { 169 | // With cachedFld type is slice/array, only accept non-nil value 170 | for stFldVal.Kind() == reflect.Ptr { 171 | stFldVal = stFldVal.Elem() 172 | } 173 | if !stFldVal.IsValid() { 174 | continue 175 | } 176 | if stFldVal.Len() == 0 { 177 | continue 178 | } 179 | if count := countElem(stFldVal); count > 0 { 180 | // preallocate slice 181 | values[cachedFld.name] = make([]string, 0, countElem(stFldVal)) 182 | } else { 183 | continue 184 | } 185 | } 186 | } 187 | 188 | // format value 189 | err := cachedFld.formatFnc(stFldVal, func(name string, val string) { 190 | values[name] = append(values[name], val) 191 | }) 192 | if err != nil { 193 | return err 194 | } 195 | } 196 | return nil 197 | } 198 | 199 | func (e *encoder) structCaching(fields *cachedFields, stVal reflect.Value, scope []byte) { 200 | 201 | structTyp := getType(stVal) 202 | 203 | for i := 0; i < structTyp.NumField(); i++ { 204 | 205 | structField := structTyp.Field(i) 206 | 207 | if structField.PkgPath != "" && !structField.Anonymous { // unexported field 208 | *fields = append(*fields, nil) 209 | continue 210 | } 211 | 212 | e.getTagNameAndOpts(structField) 213 | 214 | if string(e.tags[0]) == "-" { // ignored field 215 | continue 216 | } 217 | 218 | if string(scope) != "" { 219 | scopedName := strings.Builder{} 220 | scopedName.Write(scope) 221 | scopedName.WriteRune('[') 222 | scopedName.Write(e.tags[0]) 223 | scopedName.WriteRune(']') 224 | e.tags[0] = e.tags[0][:0] 225 | e.tags[0] = append(e.tags[0], scopedName.String()...) 226 | } 227 | 228 | fieldVal := stVal.Field(i) 229 | 230 | if fieldVal.Type().Implements(encoderType) { 231 | *fields = append(*fields, newCustomField(fieldVal.Type(), e.tags[0], e.tags[1:])) 232 | continue 233 | } 234 | 235 | fieldTyp := getType(fieldVal) 236 | 237 | if fieldTyp == timeType { 238 | *fields = append(*fields, newTimeField(e.tags[0], e.tags[1:])) 239 | continue 240 | } 241 | 242 | switch fieldTyp.Kind() { 243 | case reflect.Struct: 244 | fieldVal = reflect.Zero(fieldTyp) 245 | // Clear and set new scope 246 | e.scope = e.scope[:0] 247 | e.scope = append(e.scope, e.tags[0]...) 248 | // New embed field 249 | field := newEmbedField(fieldVal.NumField(), e.tags[0], e.tags[1:]) 250 | *fields = append(*fields, field) 251 | // Recursive 252 | e.structCaching(&field.cachedFields, fieldVal, e.scope) 253 | case reflect.Slice, reflect.Array: 254 | //Slice element type 255 | elemType := fieldTyp.Elem() 256 | if elemType.Implements(encoderType) { 257 | *fields = append(*fields, e.newListField(elemType, e.tags[0], e.tags[1:])) 258 | continue 259 | } 260 | for elemType.Kind() == reflect.Ptr { 261 | elemType = elemType.Elem() 262 | } 263 | *fields = append(*fields, e.newListField(elemType, e.tags[0], e.tags[1:])) 264 | case reflect.Map: 265 | keyType := fieldTyp.Key() 266 | /*for keyType.Kind() == reflect.Ptr { 267 | keyType = keyType.Elem() 268 | }*/ 269 | valueType := fieldTyp.Elem() 270 | /*for valueType.Kind() == reflect.Ptr { 271 | valueType = valueType.Elem() 272 | }*/ 273 | *fields = append(*fields, newMapField(keyType, valueType, e.tags[0], e.tags[1:])) 274 | default: 275 | *fields = append(*fields, newCachedFieldByKind(fieldTyp.Kind(), e.tags[0], e.tags[1:])) 276 | } 277 | } 278 | } 279 | 280 | func (e *encoder) getTagNameAndOpts(f reflect.StructField) { 281 | // Get tag by alias 282 | tag := f.Tag.Get(e.e.tagAlias) 283 | 284 | // Clear first tag in slice 285 | e.tags[0] = e.tags[0][:0] 286 | 287 | if len(tag) == 0 { 288 | // no tag, using struct field name 289 | e.tags[0] = append(e.tags[0][:0], f.Name...) 290 | e.tags = e.tags[:1] 291 | } else { 292 | // Use first tag as temp 293 | e.tags[0] = append(e.tags[0][:0], tag...) 294 | 295 | splitTags := strings.Split(tag, ",") 296 | e.tags = e.tags[:len(splitTags)] 297 | 298 | for i := 0; i < len(splitTags); i++ { 299 | if i == 0 { 300 | if len(splitTags[0]) == 0 { 301 | e.tags[0] = append(e.tags[i][:0], f.Name...) 302 | continue 303 | } 304 | } 305 | e.tags[i] = append(e.tags[i][:0], splitTags[i]...) 306 | } 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /encode_cache.go: -------------------------------------------------------------------------------- 1 | package qs 2 | 3 | import ( 4 | "reflect" 5 | "sync" 6 | ) 7 | 8 | type cacheStore struct { 9 | m map[reflect.Type]cachedFields 10 | mutex sync.RWMutex 11 | } 12 | 13 | func newCacheStore() *cacheStore { 14 | return &cacheStore{ 15 | m: make(map[reflect.Type]cachedFields), 16 | } 17 | } 18 | 19 | // Retrieve cachedFields corresponding to reflect.Type 20 | func (cacheStore *cacheStore) Retrieve(typ reflect.Type) cachedFields { 21 | return cacheStore.m[typ] 22 | } 23 | 24 | // Store func stores cachedFields that corresponds to reflect.Type 25 | func (cacheStore *cacheStore) Store(typ reflect.Type, cachedFields cachedFields) { 26 | cacheStore.mutex.Lock() 27 | defer cacheStore.mutex.Unlock() 28 | if _, ok := cacheStore.m[typ]; !ok { 29 | cacheStore.m[typ] = cachedFields 30 | } 31 | } 32 | 33 | type ( 34 | resultFunc func(name string, val string) 35 | 36 | // cachedField 37 | cachedField interface { 38 | formatFnc(value reflect.Value, result resultFunc) error 39 | } 40 | 41 | cachedFields []cachedField 42 | ) 43 | 44 | func newCacheFieldByType(typ reflect.Type, tagName []byte, tagOptions [][]byte) cachedField { 45 | if typ.Implements(encoderType) { 46 | return newCustomField(typ, tagName, tagOptions) 47 | } 48 | switch typ { 49 | case timeType: 50 | return newTimeField(tagName, tagOptions) 51 | default: 52 | return newCachedFieldByKind(typ.Kind(), tagName, tagOptions) 53 | } 54 | } 55 | 56 | func newCachedFieldByKind(kind reflect.Kind, tagName []byte, tagOptions [][]byte) cachedField { 57 | switch kind { 58 | case reflect.String: 59 | return newStringField(tagName, tagOptions) 60 | case reflect.Bool: 61 | return newBoolField(tagName, tagOptions) 62 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 63 | return newIntField(tagName, tagOptions) 64 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 65 | return newUintField(tagName, tagOptions) 66 | case reflect.Float32: 67 | return newFloat32Field(tagName, tagOptions) 68 | case reflect.Float64: 69 | return newFloat64Field(tagName, tagOptions) 70 | case reflect.Complex64: 71 | return newComplex64Field(tagName, tagOptions) 72 | case reflect.Complex128: 73 | return newComplex128Field(tagName, tagOptions) 74 | case reflect.Struct: 75 | return newEmbedField(0, tagName, tagOptions) 76 | case reflect.Interface: 77 | return newInterfaceField(tagName, tagOptions) 78 | default: 79 | return nil 80 | } 81 | } 82 | 83 | func getType(fieldVal reflect.Value) reflect.Type { 84 | stFieldTyp := fieldVal.Type() 85 | for fieldVal.Kind() == reflect.Ptr { 86 | fieldVal = fieldVal.Elem() 87 | stFieldTyp = stFieldTyp.Elem() 88 | } 89 | return stFieldTyp 90 | } 91 | 92 | func countElem(value reflect.Value) int { 93 | count := 0 94 | for i := 0; i < value.Len(); i++ { 95 | elem := value.Index(i) 96 | for elem.Kind() == reflect.Ptr { 97 | elem = elem.Elem() 98 | } 99 | if elem.IsValid() { 100 | count++ 101 | } 102 | } 103 | return count 104 | } 105 | -------------------------------------------------------------------------------- /encode_cache_test.go: -------------------------------------------------------------------------------- 1 | package qs 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | ) 7 | 8 | func TestCacheStore(t *testing.T) { 9 | t.Parallel() 10 | 11 | s := &basicVal{} 12 | 13 | cacheStore := newCacheStore() 14 | if cacheStore == nil { 15 | t.Error("cache store should not be nil") 16 | t.FailNow() 17 | } 18 | 19 | fields := cachedFields{&float64Field{}} 20 | cacheStore.Store(reflect.TypeOf(s), fields) 21 | cachedFlds := cacheStore.Retrieve(reflect.TypeOf(s)) 22 | 23 | if cachedFlds == nil { 24 | t.Error("cache store should not be nil") 25 | t.FailNow() 26 | } 27 | if len(cachedFlds) != len(fields) { 28 | t.Error("cache store should have the same number of fields") 29 | t.FailNow() 30 | } 31 | if &fields[0] != &cachedFlds[0] { 32 | t.Error("cache store should have the same fields") 33 | t.FailNow() 34 | } 35 | } 36 | 37 | func TestNewCacheField(t *testing.T) { 38 | t.Parallel() 39 | 40 | name := []byte(`abc`) 41 | opts := [][]byte{[]byte(`omitempty`)} 42 | 43 | cacheField := newCachedFieldByKind(reflect.ValueOf("").Kind(), name, opts) 44 | 45 | strField, ok := cacheField.(*stringField) 46 | if !ok { 47 | t.Error("strField should be stringField") 48 | t.FailNow() 49 | } 50 | if string(name) != strField.name { 51 | t.Errorf("strField.name should be %s, but %s", string(name), strField.name) 52 | t.FailNow() 53 | } 54 | if !strField.omitEmpty { 55 | t.Error("omitEmpty should be true") 56 | t.FailNow() 57 | } 58 | if !reflect.DeepEqual(reflect.TypeOf(new(stringField)), reflect.TypeOf(cacheField)) { 59 | t.Error("cache field is not of type *stringField") 60 | t.FailNow() 61 | } 62 | } 63 | 64 | func TestNewCacheField2(t *testing.T) { 65 | t.Parallel() 66 | 67 | var strPtr *string 68 | cacheField := newCachedFieldByKind(reflect.ValueOf(strPtr).Kind(), nil, nil) 69 | if cacheField != nil { 70 | t.Error("expect cacheField to be nil") 71 | t.FailNow() 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /encode_field.go: -------------------------------------------------------------------------------- 1 | package qs 2 | 3 | import ( 4 | "reflect" 5 | "strconv" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | type timeFormat uint8 11 | 12 | const ( 13 | _ timeFormat = iota 14 | timeFormatSecond 15 | timeFormatMillis 16 | ) 17 | 18 | type listFormat uint8 19 | 20 | const ( 21 | arrayFormatRepeat listFormat = iota 22 | arrayFormatBracket 23 | arrayFormatComma 24 | arrayFormatIndex 25 | ) 26 | 27 | // other fields implement baseField 28 | type baseField struct { 29 | name string 30 | omitEmpty bool 31 | } 32 | 33 | // embedField represents for nested struct 34 | type embedField struct { 35 | *baseField 36 | cachedFields cachedFields 37 | } 38 | 39 | func newEmbedField(preAlloc int, tagName []byte, tagOptions [][]byte) *embedField { 40 | embedField := &embedField{ 41 | baseField: &baseField{ 42 | name: string(tagName), 43 | }, 44 | cachedFields: make(cachedFields, 0, preAlloc), 45 | } 46 | for _, tagOption := range tagOptions { 47 | if string(tagOption) == tagOmitEmpty { 48 | embedField.omitEmpty = true 49 | } 50 | } 51 | return embedField 52 | } 53 | 54 | func (embedField *embedField) formatFnc(v reflect.Value, result resultFunc) error { 55 | for v.Kind() == reflect.Ptr { 56 | if v.IsNil() { 57 | if !embedField.omitEmpty { 58 | result(embedField.name, "") 59 | } 60 | return nil 61 | } 62 | v = v.Elem() 63 | } 64 | for i, cachedField := range embedField.cachedFields { 65 | if cachedField == nil { 66 | continue 67 | } 68 | err := cachedField.formatFnc(v.Field(i), result) 69 | if err != nil { 70 | return err 71 | } 72 | } 73 | return nil 74 | } 75 | 76 | // Present for field with slice/array data type 77 | type listField struct { 78 | *baseField 79 | cachedField cachedField 80 | arrayFormat listFormat 81 | } 82 | 83 | func (listField *listField) formatFnc(field reflect.Value, result resultFunc) error { 84 | switch listField.arrayFormat { 85 | case arrayFormatComma: 86 | var str strings.Builder 87 | for i := 0; i < field.Len(); i++ { 88 | elemVal := field.Index(i) 89 | if _, ok := listField.cachedField.(*customField); ok { 90 | elem := elemVal 91 | for elem.Kind() == reflect.Ptr { 92 | elem = elem.Elem() 93 | } 94 | if !elem.IsValid() { 95 | continue 96 | } 97 | } else { 98 | for elemVal.Kind() == reflect.Ptr { 99 | elemVal = elemVal.Elem() 100 | } 101 | if !elemVal.IsValid() { 102 | continue 103 | } 104 | } 105 | err := listField.cachedField.formatFnc(elemVal, func(name string, val string) { 106 | if i > 0 { 107 | str.WriteByte(',') 108 | } 109 | str.WriteString(val) 110 | }) 111 | if err != nil { 112 | return err 113 | } 114 | } 115 | returnStr := str.String() 116 | if len(returnStr) > 0 && returnStr[0] == ',' { 117 | returnStr = returnStr[1:] 118 | } 119 | result(listField.name, returnStr) 120 | case arrayFormatRepeat, arrayFormatBracket: 121 | for i := 0; i < field.Len(); i++ { 122 | elemVal := field.Index(i) 123 | if _, ok := listField.cachedField.(*customField); ok { 124 | elem := elemVal 125 | for elem.Kind() == reflect.Ptr { 126 | elem = elem.Elem() 127 | } 128 | if !elem.IsValid() { 129 | continue 130 | } 131 | } else { 132 | for elemVal.Kind() == reflect.Ptr { 133 | elemVal = elemVal.Elem() 134 | } 135 | if !elemVal.IsValid() { 136 | continue 137 | } 138 | } 139 | err := listField.cachedField.formatFnc(elemVal, func(name string, val string) { 140 | result(listField.name, val) 141 | }) 142 | if err != nil { 143 | return err 144 | } 145 | } 146 | case arrayFormatIndex: 147 | count := 0 148 | for i := 0; i < field.Len(); i++ { 149 | elemVal := field.Index(i) 150 | if _, ok := listField.cachedField.(*customField); ok { 151 | elem := elemVal 152 | for elem.Kind() == reflect.Ptr { 153 | elem = elem.Elem() 154 | } 155 | if !elem.IsValid() { 156 | continue 157 | } 158 | } else { 159 | for elemVal.Kind() == reflect.Ptr { 160 | elemVal = elemVal.Elem() 161 | } 162 | if !elemVal.IsValid() { 163 | continue 164 | } 165 | } 166 | if v, ok := listField.cachedField.(*embedField); ok { 167 | err := v.formatFnc(elemVal, func(name string, val string) { 168 | var str strings.Builder 169 | str.WriteString(listField.name) 170 | str.WriteString(strconv.FormatInt(int64(i), 10)) 171 | str.WriteByte(']') 172 | str.WriteByte('[') 173 | str.WriteString(name) 174 | str.WriteByte(']') 175 | result(str.String(), val) 176 | count++ 177 | }) 178 | if err != nil { 179 | return err 180 | } 181 | continue 182 | } 183 | err := listField.cachedField.formatFnc(elemVal, func(name string, val string) { 184 | var key strings.Builder 185 | key.WriteString(listField.name) 186 | key.WriteString(strconv.FormatInt(int64(count), 10)) 187 | key.WriteString("]") 188 | result(key.String(), val) 189 | count++ 190 | }) 191 | if err != nil { 192 | return err 193 | } 194 | } 195 | } 196 | return nil 197 | } 198 | 199 | func (e *encoder) newListField(elemTyp reflect.Type, tagName []byte, tagOptions [][]byte) *listField { 200 | removeIdx := -1 201 | for i, tagOption := range tagOptions { 202 | if string(tagOption) == tagOmitEmpty { 203 | removeIdx = i 204 | } 205 | } 206 | if removeIdx > -1 { 207 | tagOptions = append(tagOptions[:removeIdx], tagOptions[removeIdx+1:]...) 208 | } 209 | 210 | listField := &listField{ 211 | cachedField: newCacheFieldByType(elemTyp, nil, tagOptions), 212 | } 213 | 214 | for _, tagOption := range tagOptions { 215 | switch string(tagOption) { 216 | case "comma": 217 | listField.arrayFormat = arrayFormatComma 218 | case "bracket": 219 | listField.arrayFormat = arrayFormatBracket 220 | case "index": 221 | listField.arrayFormat = arrayFormatIndex 222 | } 223 | } 224 | 225 | switch listField.arrayFormat { 226 | case arrayFormatRepeat, arrayFormatBracket: 227 | if listField.arrayFormat >= arrayFormatBracket { 228 | tagName = append(tagName, '[') 229 | tagName = append(tagName, ']') 230 | } 231 | case arrayFormatIndex: 232 | tagName = append(tagName, '[') 233 | } 234 | 235 | listField.baseField = &baseField{ 236 | name: string(tagName), 237 | } 238 | 239 | if field, ok := listField.cachedField.(*embedField); ok { 240 | e.structCaching(&field.cachedFields, reflect.Zero(elemTyp), nil) 241 | } 242 | 243 | return listField 244 | } 245 | 246 | type mapField struct { 247 | *baseField 248 | cachedKeyField cachedField 249 | cachedValueField cachedField 250 | } 251 | 252 | func (mapField *mapField) formatFnc(field reflect.Value, result resultFunc) error { 253 | mapRange := field.MapRange() 254 | fieldName := make([]byte, 0, 36) 255 | fieldName = append(fieldName, mapField.name...) 256 | 257 | for mapRange.Next() { 258 | fieldName = fieldName[:len(mapField.name)] 259 | err := mapField.cachedKeyField.formatFnc(mapRange.Key(), func(_ string, val string) { 260 | fieldName = append(fieldName, '[') 261 | fieldName = append(fieldName, val...) 262 | fieldName = append(fieldName, ']') 263 | }) 264 | if err != nil { 265 | return err 266 | } 267 | err = mapField.cachedValueField.formatFnc(mapRange.Value(), func(_ string, val string) { 268 | result(string(fieldName), val) 269 | }) 270 | if err != nil { 271 | return err 272 | } 273 | } 274 | return nil 275 | } 276 | 277 | func newMapField(keyType reflect.Type, valueType reflect.Type, tagName []byte, tagOptions [][]byte) *mapField { 278 | removeIdx := -1 279 | for i, tagOption := range tagOptions { 280 | if string(tagOption) == tagOmitEmpty { 281 | removeIdx = i 282 | } 283 | } 284 | if removeIdx > -1 { 285 | tagOptions = append(tagOptions[:removeIdx], tagOptions[removeIdx+1:]...) 286 | } 287 | 288 | if !keyType.Implements(encoderType) { 289 | for keyType.Kind() == reflect.Ptr { 290 | keyType = keyType.Elem() 291 | } 292 | } 293 | 294 | if !valueType.Implements(encoderType) { 295 | for valueType.Kind() == reflect.Ptr { 296 | valueType = valueType.Elem() 297 | } 298 | } 299 | 300 | field := &mapField{ 301 | baseField: &baseField{ 302 | name: string(tagName), 303 | }, 304 | cachedKeyField: newCacheFieldByType(keyType, nil, nil), 305 | cachedValueField: newCacheFieldByType(valueType, nil, nil), 306 | } 307 | return field 308 | } 309 | 310 | // 311 | type boolField struct { 312 | *baseField 313 | useInt bool 314 | } 315 | 316 | func (boolField *boolField) formatFnc(v reflect.Value, result resultFunc) error { 317 | for v.Kind() == reflect.Ptr { 318 | if v.IsNil() { 319 | if !boolField.omitEmpty { 320 | result(boolField.name, "") 321 | } 322 | return nil 323 | } 324 | v = v.Elem() 325 | } 326 | b := v.Bool() 327 | if !b && boolField.omitEmpty { 328 | return nil 329 | } 330 | if boolField.useInt { 331 | if v.Bool() { 332 | result(boolField.name, "1") 333 | } else { 334 | result(boolField.name, "0") 335 | } 336 | } else { 337 | result(boolField.name, strconv.FormatBool(b)) 338 | } 339 | return nil 340 | } 341 | 342 | func newBoolField(tagName []byte, tagOptions [][]byte) *boolField { 343 | field := &boolField{ 344 | baseField: &baseField{ 345 | name: string(tagName), 346 | }, 347 | } 348 | for _, tagOption := range tagOptions { 349 | switch string(tagOption) { 350 | case tagOmitEmpty: 351 | field.omitEmpty = true 352 | case "int": 353 | field.useInt = true 354 | } 355 | } 356 | return field 357 | } 358 | 359 | // Int field 360 | type intField struct { 361 | *baseField 362 | } 363 | 364 | func (intField *intField) formatFnc(value reflect.Value, result resultFunc) error { 365 | for value.Kind() == reflect.Ptr { 366 | if value.IsNil() { 367 | if !intField.omitEmpty { 368 | result(intField.name, "") 369 | } 370 | return nil 371 | } 372 | value = value.Elem() 373 | } 374 | i := value.Int() 375 | if i == 0 && intField.omitEmpty { 376 | return nil 377 | } 378 | result(intField.name, strconv.FormatInt(i, 10)) 379 | return nil 380 | } 381 | 382 | func newIntField(tagName []byte, tagOptions [][]byte) *intField { 383 | field := &intField{ 384 | baseField: &baseField{ 385 | name: string(tagName), 386 | }, 387 | } 388 | for _, tagOption := range tagOptions { 389 | if string(tagOption) == tagOmitEmpty { 390 | field.omitEmpty = true 391 | } 392 | } 393 | return field 394 | } 395 | 396 | // Uint field 397 | type uintField struct { 398 | *baseField 399 | } 400 | 401 | func (uintField *uintField) formatFnc(value reflect.Value, result resultFunc) error { 402 | for value.Kind() == reflect.Ptr { 403 | if value.IsNil() { 404 | if !uintField.omitEmpty { 405 | result(uintField.name, "") 406 | } 407 | return nil 408 | } 409 | value = value.Elem() 410 | } 411 | i := value.Uint() 412 | if i == 0 && uintField.omitEmpty { 413 | return nil 414 | } 415 | result(uintField.name, strconv.FormatUint(i, 10)) 416 | return nil 417 | } 418 | 419 | func newUintField(tagName []byte, tagOptions [][]byte) *uintField { 420 | field := &uintField{ 421 | baseField: &baseField{ 422 | name: string(tagName), 423 | }, 424 | } 425 | for _, tagOption := range tagOptions { 426 | if string(tagOption) == tagOmitEmpty { 427 | field.omitEmpty = true 428 | } 429 | } 430 | return field 431 | } 432 | 433 | // String field 434 | type stringField struct { 435 | *baseField 436 | } 437 | 438 | func (stringField *stringField) formatFnc(value reflect.Value, result resultFunc) error { 439 | for value.Kind() == reflect.Ptr { 440 | if value.IsNil() { 441 | if !stringField.omitEmpty { 442 | result(stringField.name, "") 443 | } 444 | return nil 445 | } 446 | value = value.Elem() 447 | } 448 | str := value.String() 449 | if str == "" && stringField.omitEmpty { 450 | return nil 451 | } 452 | result(stringField.name, str) 453 | return nil 454 | } 455 | 456 | func newStringField(tagName []byte, tagOptions [][]byte) *stringField { 457 | field := &stringField{ 458 | baseField: &baseField{ 459 | name: string(tagName), 460 | }, 461 | } 462 | for _, tagOption := range tagOptions { 463 | if string(tagOption) == tagOmitEmpty { 464 | field.omitEmpty = true 465 | } 466 | } 467 | return field 468 | } 469 | 470 | // Float32 field 471 | type float32Field struct { 472 | *baseField 473 | } 474 | 475 | func (float32Field *float32Field) formatFnc(value reflect.Value, result resultFunc) error { 476 | for value.Kind() == reflect.Ptr { 477 | if value.IsNil() { 478 | if !float32Field.omitEmpty { 479 | result(float32Field.name, "") 480 | } 481 | return nil 482 | } 483 | value = value.Elem() 484 | } 485 | f := value.Float() 486 | if f == 0 && float32Field.omitEmpty { 487 | return nil 488 | } 489 | result(float32Field.name, strconv.FormatFloat(f, 'f', -1, 32)) 490 | return nil 491 | } 492 | 493 | func newFloat32Field(tagName []byte, tagOptions [][]byte) *float32Field { 494 | field := &float32Field{ 495 | baseField: &baseField{ 496 | name: string(tagName), 497 | }, 498 | } 499 | for _, tagOption := range tagOptions { 500 | if string(tagOption) == tagOmitEmpty { 501 | field.omitEmpty = true 502 | } 503 | } 504 | return field 505 | } 506 | 507 | // Float64 field 508 | type float64Field struct { 509 | *baseField 510 | } 511 | 512 | func (float64Field *float64Field) formatFnc(v reflect.Value, result resultFunc) error { 513 | for v.Kind() == reflect.Ptr { 514 | if v.IsNil() { 515 | if !float64Field.omitEmpty { 516 | result(float64Field.name, "") 517 | } 518 | return nil 519 | } 520 | v = v.Elem() 521 | } 522 | f := v.Float() 523 | if f == 0 && float64Field.omitEmpty { 524 | return nil 525 | } 526 | result(float64Field.name, strconv.FormatFloat(f, 'f', -1, 64)) 527 | return nil 528 | } 529 | 530 | func newFloat64Field(tagName []byte, tagOptions [][]byte) *float64Field { 531 | field := &float64Field{ 532 | baseField: &baseField{ 533 | name: string(tagName), 534 | }, 535 | } 536 | for _, tagOption := range tagOptions { 537 | if string(tagOption) == tagOmitEmpty { 538 | field.omitEmpty = true 539 | } 540 | } 541 | return field 542 | } 543 | 544 | // Complex64 field 545 | type complex64Field struct { 546 | *baseField 547 | } 548 | 549 | func (complex64Field *complex64Field) formatFnc(v reflect.Value, result resultFunc) error { 550 | for v.Kind() == reflect.Ptr { 551 | if v.IsNil() { 552 | if !complex64Field.omitEmpty { 553 | result(complex64Field.name, "") 554 | } 555 | return nil 556 | } 557 | v = v.Elem() 558 | } 559 | c := v.Complex() 560 | if c == 0 && complex64Field.omitEmpty { 561 | return nil 562 | } 563 | result(complex64Field.name, strconv.FormatComplex(c, 'f', -1, 64)) 564 | return nil 565 | } 566 | 567 | func newComplex64Field(tagName []byte, tagOptions [][]byte) *complex64Field { 568 | field := &complex64Field{ 569 | baseField: &baseField{ 570 | name: string(tagName), 571 | }, 572 | } 573 | for _, tagOption := range tagOptions { 574 | if string(tagOption) == tagOmitEmpty { 575 | field.omitEmpty = true 576 | } 577 | } 578 | return field 579 | } 580 | 581 | // Complex64 field 582 | type complex128Field struct { 583 | *baseField 584 | } 585 | 586 | func (complex128Field *complex128Field) formatFnc(v reflect.Value, result resultFunc) error { 587 | for v.Kind() == reflect.Ptr { 588 | if v.IsNil() { 589 | if !complex128Field.omitEmpty { 590 | result(complex128Field.name, "") 591 | } 592 | return nil 593 | } 594 | v = v.Elem() 595 | } 596 | c := v.Complex() 597 | if c == 0 && complex128Field.omitEmpty { 598 | return nil 599 | } 600 | result(complex128Field.name, strconv.FormatComplex(c, 'f', -1, 128)) 601 | return nil 602 | } 603 | 604 | func newComplex128Field(tagName []byte, tagOptions [][]byte) *complex128Field { 605 | field := &complex128Field{ 606 | baseField: &baseField{ 607 | name: string(tagName), 608 | }, 609 | } 610 | for _, tagOption := range tagOptions { 611 | if string(tagOption) == tagOmitEmpty { 612 | field.omitEmpty = true 613 | } 614 | } 615 | return field 616 | } 617 | 618 | // Time field 619 | type timeField struct { 620 | *baseField 621 | timeFormat timeFormat 622 | } 623 | 624 | func (timeField *timeField) formatFnc(v reflect.Value, result resultFunc) error { 625 | for v.Kind() == reflect.Ptr { 626 | if v.IsNil() { 627 | if !timeField.omitEmpty { 628 | result(timeField.name, "") 629 | } 630 | return nil 631 | } 632 | v = v.Elem() 633 | } 634 | t := v.Interface().(time.Time) 635 | if t.IsZero() && timeField.omitEmpty { 636 | return nil 637 | } 638 | switch timeField.timeFormat { 639 | case timeFormatSecond: 640 | result(timeField.name, strconv.FormatInt(t.Unix(), 10)) 641 | case timeFormatMillis: 642 | result(timeField.name, strconv.FormatInt(t.UnixNano()/1000000, 10)) 643 | default: 644 | result(timeField.name, t.Format(time.RFC3339)) 645 | } 646 | return nil 647 | } 648 | 649 | func newTimeField(tagName []byte, tagOptions [][]byte) *timeField { 650 | field := &timeField{ 651 | baseField: &baseField{ 652 | name: string(tagName), 653 | }, 654 | } 655 | for _, tagOption := range tagOptions { 656 | switch string(tagOption) { 657 | case tagOmitEmpty: 658 | field.omitEmpty = true 659 | case "second": 660 | field.timeFormat = timeFormatSecond 661 | case "millis": 662 | field.timeFormat = timeFormatMillis 663 | } 664 | } 665 | return field 666 | } 667 | 668 | // Zeroer represents an object has zero value 669 | // IsZeroer is used to check whether an object is zero to 670 | // determine whether it should be omitted when encoding 671 | type Zeroer interface { 672 | IsZero() bool 673 | } 674 | 675 | // QueryParamEncoder is an interface implemented by any type to encode itself into query param 676 | type QueryParamEncoder interface { 677 | EncodeParam() (string, error) 678 | } 679 | 680 | type customField struct { 681 | *baseField 682 | isZeroer bool 683 | } 684 | 685 | func (customField *customField) formatFnc(v reflect.Value, result resultFunc) error { 686 | elem := v 687 | for elem.Kind() == reflect.Ptr { 688 | elem = v.Elem() 689 | } 690 | if !elem.IsValid() { 691 | if !customField.omitEmpty { 692 | result(customField.name, "") 693 | } 694 | return nil 695 | } 696 | valueInterface := v.Interface() 697 | if customField.isZeroer && valueInterface.(Zeroer).IsZero() { 698 | if !customField.omitEmpty { 699 | result(customField.name, "") 700 | } 701 | return nil 702 | } 703 | str, err := valueInterface.(QueryParamEncoder).EncodeParam() 704 | if err != nil { 705 | return err 706 | } 707 | result(customField.name, str) 708 | return nil 709 | } 710 | 711 | func newCustomField(typ reflect.Type, tagName []byte, tagOptions [][]byte) *customField { 712 | field := &customField{ 713 | baseField: &baseField{ 714 | name: string(tagName), 715 | omitEmpty: false, 716 | }, 717 | } 718 | 719 | if typ.Implements(zeroerType) { 720 | field.isZeroer = true 721 | } 722 | 723 | for _, tagOption := range tagOptions { 724 | if string(tagOption) == tagOmitEmpty { 725 | field.omitEmpty = true 726 | } 727 | } 728 | return field 729 | } 730 | 731 | type interfaceField struct { 732 | *baseField 733 | tagName []byte 734 | tagOptions [][]byte 735 | fieldMap map[reflect.Type]cachedField 736 | } 737 | 738 | func (interfaceField *interfaceField) formatFnc(v reflect.Value, result resultFunc) error { 739 | 740 | v = v.Elem() 741 | 742 | if v.IsValid() && v.Type().Implements(encoderType) { 743 | elem := v 744 | for elem.Kind() == reflect.Ptr { 745 | elem = elem.Elem() 746 | } 747 | if !elem.IsValid() { 748 | return nil 749 | } 750 | } else { 751 | for v.Kind() == reflect.Ptr { 752 | v = v.Elem() 753 | } 754 | 755 | if !v.IsValid() { 756 | if !interfaceField.omitEmpty { 757 | result(interfaceField.name, "") 758 | } 759 | return nil 760 | } 761 | } 762 | 763 | if field := interfaceField.fieldMap[v.Type()]; field == nil { 764 | interfaceField.fieldMap[v.Type()] = newCacheFieldByType(v.Type(), interfaceField.tagName, interfaceField.tagOptions) 765 | } 766 | if field := interfaceField.fieldMap[v.Type()]; field != nil { 767 | err := field.formatFnc(v, result) 768 | if err != nil { 769 | return err 770 | } 771 | } 772 | return nil 773 | } 774 | 775 | func newInterfaceField(tagName []byte, tagOptions [][]byte) *interfaceField { 776 | copiedTagName := make([]byte, len(tagName)) 777 | copy(copiedTagName, tagName) 778 | copiedTagOptions := make([][]byte, len(tagOptions)) 779 | copy(copiedTagOptions, tagOptions) 780 | 781 | field := &interfaceField{ 782 | baseField: &baseField{ 783 | name: string(copiedTagName), 784 | }, 785 | tagName: copiedTagName, 786 | tagOptions: copiedTagOptions, 787 | fieldMap: make(map[reflect.Type]cachedField, 5), 788 | } 789 | for _, tagOption := range tagOptions { 790 | if string(tagOption) == tagOmitEmpty { 791 | field.omitEmpty = true 792 | } 793 | } 794 | return field 795 | } 796 | -------------------------------------------------------------------------------- /encode_test.go: -------------------------------------------------------------------------------- 1 | package qs 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | "reflect" 7 | "strconv" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | type basicVal struct { 13 | String string `qs:"string"` 14 | Bool bool `qs:"bool"` 15 | Int int `qs:"int"` 16 | Int8 int8 `qs:"int8"` 17 | Int16 int16 `qs:"int16"` 18 | Int32 int32 `qs:"int32"` 19 | Int64 int64 `qs:"int64"` 20 | Uint uint `qs:"uint"` 21 | Uint8 uint8 `qs:"uint8"` 22 | Uint16 uint16 `qs:"uint16"` 23 | Uint32 uint32 `qs:"uint32"` 24 | Uint64 uint64 `qs:"uint64"` 25 | Uintptr uintptr `qs:"uintptr"` 26 | Float32 float32 `qs:"float32"` 27 | Float64 float64 `qs:"float64"` 28 | Complex64 complex64 `qs:"complex64"` 29 | Complex128 complex128 `qs:"complex128"` 30 | Time time.Time `qs:"time"` 31 | } 32 | 33 | type basicValWithOmit struct { 34 | String string `qs:"string,omitempty"` 35 | Bool bool `qs:"bool,omitempty"` 36 | Int int `qs:"int,omitempty"` 37 | Int8 int8 `qs:"int8,omitempty"` 38 | Int16 int16 `qs:"int16,omitempty"` 39 | Int32 int32 `qs:"int32,omitempty"` 40 | Int64 int64 `qs:"int64,omitempty"` 41 | Uint uint `qs:"uint,omitempty"` 42 | Uint8 uint8 `qs:"uint8,omitempty"` 43 | Uint16 uint16 `qs:"uint16,omitempty"` 44 | Uint32 uint32 `qs:"uint32,omitempty"` 45 | Uint64 uint64 `qs:"uint64,omitempty"` 46 | Float32 float32 `qs:"float32,omitempty"` 47 | Float64 float64 `qs:"float64,omitempty"` 48 | Complex64 complex64 `qs:"complex64,omitempty"` 49 | Complex128 complex128 `qs:"complex128,omitempty"` 50 | Time time.Time `qs:"time,omitempty"` 51 | } 52 | 53 | type basicPtr struct { 54 | String *string `qs:"string"` 55 | Bool *bool `qs:"bool"` 56 | Int *int `qs:"int"` 57 | Int8 *int8 `qs:"int8"` 58 | Int16 *int16 `qs:"int16"` 59 | Int32 *int32 `qs:"int32"` 60 | Int64 *int64 `qs:"int64"` 61 | Uint *uint `qs:"uint"` 62 | Uint8 *uint8 `qs:"uint8"` 63 | Uint16 *uint16 `qs:"uint16"` 64 | Uint32 *uint32 `qs:"uint32"` 65 | Uint64 *uint64 `qs:"uint64"` 66 | UinPtr *uintptr `qs:"uintptr"` 67 | Float32 *float32 `qs:"float32"` 68 | Float64 *float64 `qs:"float64"` 69 | Complex64 *complex64 `qs:"complex64"` 70 | Complex128 *complex128 `qs:"complex128"` 71 | Time *time.Time `qs:"time"` 72 | } 73 | 74 | type basicPtrWithOmit struct { 75 | String *string `qs:"string,omitempty"` 76 | Bool *bool `qs:"bool,omitempty"` 77 | Int *int `qs:"int,omitempty"` 78 | Int8 *int8 `qs:"int8,omitempty"` 79 | Int16 *int16 `qs:"int16,omitempty"` 80 | Int32 *int32 `qs:"int32,omitempty"` 81 | Int64 *int64 `qs:"int64,omitempty"` 82 | Uint *uint `qs:"uint,omitempty"` 83 | Uint8 *uint8 `qs:"uint8,omitempty"` 84 | Uint16 *uint16 `qs:"uint16,omitempty"` 85 | Uint32 *uint32 `qs:"uint32,omitempty"` 86 | Uint64 *uint64 `qs:"uint64,omitempty"` 87 | Float32 *float32 `qs:"float32,omitempty"` 88 | Float64 *float64 `qs:"float64,omitempty"` 89 | Complex64 *complex64 `qs:"complex64,omitempty"` 90 | Complex128 *complex128 `qs:"complex128,omitempty"` 91 | Time *time.Time `qs:"time,omitempty"` 92 | } 93 | 94 | func TestIgnore(t *testing.T) { 95 | t.Parallel() 96 | 97 | encoder := NewEncoder() 98 | 99 | v := struct { 100 | anonymous string 101 | Test string `qs:"-"` 102 | }{} 103 | 104 | values, err := encoder.Values(v) 105 | if err != nil { 106 | t.Errorf("expected no error, got %v", err) 107 | t.FailNow() 108 | } 109 | if !reflect.DeepEqual(url.Values{}, values) { 110 | t.Errorf("values should be empty, but got %v", values) 111 | t.FailNow() 112 | } 113 | } 114 | 115 | func TestWithTagAlias(t *testing.T) { 116 | t.Parallel() 117 | 118 | alias := `go` 119 | opt := WithTagAlias(alias) 120 | if opt == nil { 121 | t.Error("expected non-nil value") 122 | t.FailNow() 123 | } 124 | 125 | encoder := NewEncoder(opt) 126 | if alias != encoder.tagAlias { 127 | t.Errorf("expected tag alias %q, but got %q", alias, encoder.tagAlias) 128 | t.FailNow() 129 | } 130 | } 131 | 132 | func TestGetTag(t *testing.T) { 133 | t.Parallel() 134 | 135 | e := NewEncoder().dataPool.Get().(*encoder) 136 | 137 | s := struct { 138 | A string `qs:"abc"` 139 | }{} 140 | 141 | field := reflect.TypeOf(s).Field(0) 142 | e.getTagNameAndOpts(field) 143 | 144 | if len(e.tags) != 1 { 145 | t.Errorf("expected 1 tag, got %d", len(e.tags)) 146 | t.FailNow() 147 | } 148 | if "abc" != string(e.tags[0]) { 149 | t.Errorf(`expected tag name: "abc", got "%s"`, string(e.tags[0])) 150 | t.FailNow() 151 | } 152 | } 153 | 154 | func TestGetTag2(t *testing.T) { 155 | t.Parallel() 156 | 157 | e := NewEncoder().dataPool.Get().(*encoder) 158 | 159 | s := struct { 160 | ABC string 161 | }{} 162 | 163 | field := reflect.TypeOf(s).Field(0) 164 | e.getTagNameAndOpts(field) 165 | 166 | if len(e.tags) != 1 { 167 | t.Errorf("expected 1 tag, got %d", len(e.tags)) 168 | t.FailNow() 169 | } 170 | if "ABC" != string(e.tags[0]) { 171 | t.Error(`expected tag name: "ABC"`) 172 | t.FailNow() 173 | } 174 | } 175 | 176 | func TestGetTag3(t *testing.T) { 177 | t.Parallel() 178 | 179 | e := NewEncoder().dataPool.Get().(*encoder) 180 | 181 | s := struct { 182 | ABC string `qs:",omitempty"` 183 | }{} 184 | 185 | field := reflect.TypeOf(s).Field(0) 186 | e.getTagNameAndOpts(field) 187 | 188 | if len(e.tags) != 2 { 189 | t.Errorf("expected 2 tags, got %d", len(e.tags)) 190 | t.FailNow() 191 | } 192 | if "ABC" != string(e.tags[0]) { 193 | t.Errorf(`expected tag name: "ABC", got "%s"`, string(e.tags[0])) 194 | t.FailNow() 195 | } 196 | if "omitempty" != string(e.tags[1]) { 197 | t.Errorf(`expected tag name: "omitempty", got "%s"`, string(e.tags[1])) 198 | t.FailNow() 199 | } 200 | } 201 | 202 | func TestEncodeInvalidValue(t *testing.T) { 203 | //t.Parallel() 204 | 205 | var ptr *string 206 | 207 | testCases := []struct { 208 | name string 209 | input interface{} 210 | }{ 211 | { 212 | name: "string", 213 | input: "abc", 214 | }, 215 | { 216 | name: "nil pointer", 217 | input: ptr, 218 | }, 219 | { 220 | name: "nil", 221 | input: nil, 222 | }, 223 | } 224 | 225 | for _, testCase := range testCases { 226 | t.Run(testCase.name, func(t *testing.T) { 227 | encoder := NewEncoder() 228 | _, err := encoder.Values(testCase.input) 229 | if err == nil { 230 | t.Error("expected error but actual is nil") 231 | t.FailNow() 232 | } 233 | // Assert err type and err message 234 | if err, ok := err.(InvalidInputErr); ok { 235 | expectedErrMessage := InvalidInputErr{InputKind: getKindOfValue(testCase.input)}.Error() 236 | if err.Error() != expectedErrMessage { 237 | t.Errorf(`expected err message: "%v", got "%v"`, expectedErrMessage, err.Error()) 238 | t.FailNow() 239 | } 240 | } else { 241 | t.Errorf("expected InvalidInputErr, got %v", err) 242 | t.FailNow() 243 | } 244 | 245 | values := make(url.Values) 246 | err = encoder.Encode(testCase.input, values) 247 | if err == nil { 248 | t.Errorf("expected error but actual is nil") 249 | t.FailNow() 250 | } 251 | // Assert err type and err message 252 | if err, ok := err.(InvalidInputErr); ok { 253 | expectedErrMessage := InvalidInputErr{InputKind: getKindOfValue(testCase.input)}.Error() 254 | if err.Error() != expectedErrMessage { 255 | t.Errorf(`expected err message: "%v", got "%v"`, expectedErrMessage, err.Error()) 256 | t.FailNow() 257 | } 258 | } else { 259 | t.Errorf("expected InvalidInputErr, got %v", err) 260 | t.FailNow() 261 | } 262 | }) 263 | } 264 | } 265 | 266 | func getKindOfValue(v interface{}) reflect.Kind { 267 | val := reflect.ValueOf(v) 268 | for val.Kind() == reflect.Ptr { 269 | if val.IsNil() { 270 | return val.Kind() 271 | } 272 | val = val.Elem() 273 | } 274 | return val.Kind() 275 | } 276 | 277 | func TestEncodeBasicVal(t *testing.T) { 278 | t.Parallel() 279 | 280 | encoder := NewEncoder() 281 | 282 | tm := time.Unix(600, 0).UTC() 283 | 284 | s := basicVal{ 285 | String: "abc", 286 | Bool: true, 287 | Int: 12, 288 | Int8: int8(8), 289 | Int16: int16(16), 290 | Int32: int32(32), 291 | Int64: int64(64), 292 | Uint: 24, 293 | Uint8: uint8(8), 294 | Uint16: uint16(16), 295 | Uint32: uint32(32), 296 | Uint64: uint64(64), 297 | Uintptr: uintptr(72), 298 | Float32: float32(0.1234), 299 | Float64: 1.2345, 300 | Complex64: complex64(64), 301 | Complex128: complex128(128), 302 | Time: tm, 303 | } 304 | values, err := encoder.Values(s) 305 | if err != nil { 306 | t.Errorf("expected no error but got %v", err) 307 | t.FailNow() 308 | } 309 | expected := url.Values{ 310 | "string": []string{"abc"}, 311 | "bool": []string{"true"}, 312 | "int": []string{"12"}, 313 | "int8": []string{"8"}, 314 | "int16": []string{"16"}, 315 | "int32": []string{"32"}, 316 | "int64": []string{"64"}, 317 | "uint": []string{"24"}, 318 | "uint8": []string{"8"}, 319 | "uint16": []string{"16"}, 320 | "uint32": []string{"32"}, 321 | "uint64": []string{"64"}, 322 | "uintptr": []string{"72"}, 323 | "float32": []string{"0.1234"}, 324 | "float64": []string{"1.2345"}, 325 | "complex64": []string{complex128ToStr(complex128(64))}, 326 | "complex128": []string{complex128ToStr(complex128(128))}, 327 | "time": []string{tm.Format(time.RFC3339)}, 328 | } 329 | if !reflect.DeepEqual(expected, values) { 330 | t.Errorf("expected: %v, actual: %v", expected, values) 331 | t.FailNow() 332 | } 333 | } 334 | 335 | func TestEncodeBasicPtr(t *testing.T) { 336 | t.Parallel() 337 | 338 | encoder := NewEncoder() 339 | 340 | tm := time.Unix(600, 0).UTC() 341 | 342 | s := basicPtr{ 343 | String: withStr("abc"), 344 | Bool: withBool(true), 345 | Int: withInt(12), 346 | Int8: withInt8(int8(8)), 347 | Int16: withInt16(int16(16)), 348 | Int32: withInt32(int32(32)), 349 | Int64: withInt64(int64(64)), 350 | Uint: withUint(uint(24)), 351 | Uint8: withUint8(uint8(8)), 352 | Uint16: withUint16(uint16(16)), 353 | Uint32: withUint32(uint32(32)), 354 | Uint64: withUint64(uint64(64)), 355 | UinPtr: withUintPtr(uintptr(72)), 356 | Float32: withFloat32(float32(0.1234)), 357 | Float64: withFloat64(1.2345), 358 | Complex64: withComplex64(complex64(64)), 359 | Complex128: withComplex128(complex128(128)), 360 | Time: withTime(tm), 361 | } 362 | actualValues1, err := encoder.Values(s) 363 | if err != nil { 364 | t.Errorf("expected no error but got %v", err) 365 | t.FailNow() 366 | } 367 | 368 | actualValues2 := make(url.Values) 369 | err = encoder.Encode(&s, actualValues2) 370 | if err != nil { 371 | t.Errorf("expected no error but got %v", err) 372 | t.FailNow() 373 | } 374 | 375 | expected := url.Values{ 376 | "string": []string{"abc"}, 377 | "bool": []string{"true"}, 378 | "int": []string{"12"}, 379 | "int8": []string{"8"}, 380 | "int16": []string{"16"}, 381 | "int32": []string{"32"}, 382 | "int64": []string{"64"}, 383 | "uint": []string{"24"}, 384 | "uint8": []string{"8"}, 385 | "uint16": []string{"16"}, 386 | "uint32": []string{"32"}, 387 | "uint64": []string{"64"}, 388 | "uintptr": []string{"72"}, 389 | "float32": []string{"0.1234"}, 390 | "float64": []string{"1.2345"}, 391 | "complex64": []string{complex128ToStr(complex128(64))}, 392 | "complex128": []string{complex128ToStr(complex128(128))}, 393 | "time": []string{tm.Format(time.RFC3339)}, 394 | } 395 | 396 | if !reflect.DeepEqual(expected, actualValues1) { 397 | t.Errorf("expected: %v, actual: %v", expected, actualValues1) 398 | t.FailNow() 399 | } 400 | if !reflect.DeepEqual(expected, actualValues2) { 401 | t.Errorf("expected: %v, actual: %v", expected, actualValues2) 402 | t.FailNow() 403 | } 404 | } 405 | 406 | func TestZeroVal(t *testing.T) { 407 | t.Parallel() 408 | encoder := NewEncoder() 409 | 410 | values, err := encoder.Values(basicVal{}) 411 | if err != nil { 412 | t.Errorf("expected no error but got %v", err) 413 | t.FailNow() 414 | } 415 | expected := url.Values{ 416 | "string": []string{""}, 417 | "bool": []string{"false"}, 418 | "int": []string{"0"}, 419 | "int8": []string{"0"}, 420 | "int16": []string{"0"}, 421 | "int32": []string{"0"}, 422 | "int64": []string{"0"}, 423 | "uint": []string{"0"}, 424 | "uint8": []string{"0"}, 425 | "uint16": []string{"0"}, 426 | "uint32": []string{"0"}, 427 | "uint64": []string{"0"}, 428 | "uintptr": []string{"0"}, 429 | "float32": []string{"0"}, 430 | "float64": []string{"0"}, 431 | "complex64": []string{complexZeroValStr()}, 432 | "complex128": []string{complexZeroValStr()}, 433 | "time": []string{time.Time{}.Format(time.RFC3339)}, 434 | } 435 | if !reflect.DeepEqual(expected, values) { 436 | t.Errorf("expected %v, got %v", expected, values) 437 | t.FailNow() 438 | } 439 | } 440 | 441 | func TestZeroPtr(t *testing.T) { 442 | t.Parallel() 443 | encoder := NewEncoder() 444 | 445 | values, err := encoder.Values(basicPtr{}) 446 | if err != nil { 447 | t.Errorf("expected no error but got %v", err) 448 | t.FailNow() 449 | } 450 | expected := url.Values{ 451 | "string": []string{""}, 452 | "bool": []string{""}, 453 | "int": []string{""}, 454 | "int8": []string{""}, 455 | "int16": []string{""}, 456 | "int32": []string{""}, 457 | "int64": []string{""}, 458 | "uint": []string{""}, 459 | "uint8": []string{""}, 460 | "uint16": []string{""}, 461 | "uint32": []string{""}, 462 | "uint64": []string{""}, 463 | "uintptr": []string{""}, 464 | "float32": []string{""}, 465 | "float64": []string{""}, 466 | "complex64": []string{""}, 467 | "complex128": []string{""}, 468 | "time": []string{""}, 469 | } 470 | if !reflect.DeepEqual(expected, values) { 471 | t.Errorf("expected: %v, actual: %v", expected, values) 472 | t.FailNow() 473 | } 474 | } 475 | 476 | func TestOmitZeroVal(t *testing.T) { 477 | t.Parallel() 478 | encoder := NewEncoder() 479 | 480 | values, err := encoder.Values(basicValWithOmit{}) 481 | if err != nil { 482 | t.Errorf("expected no error but got %v", err) 483 | t.FailNow() 484 | } 485 | if !reflect.DeepEqual(url.Values{}, values) { 486 | t.Errorf("expected empty, got %v", values) 487 | t.FailNow() 488 | } 489 | } 490 | 491 | func TestOmitZeroPtr(t *testing.T) { 492 | t.Parallel() 493 | encoder := NewEncoder() 494 | 495 | values, err := encoder.Values(basicPtrWithOmit{}) 496 | if err != nil { 497 | t.Errorf("expected no error but got %v", err) 498 | t.FailNow() 499 | } 500 | if !reflect.DeepEqual(url.Values{}, values) { 501 | t.Errorf("expected empty, got %v", values) 502 | t.FailNow() 503 | } 504 | } 505 | 506 | func TestIgnoreEmptySlice(t *testing.T) { 507 | t.Parallel() 508 | encoder := NewEncoder() 509 | 510 | s := struct { 511 | A []string `qs:"a"` 512 | B []string `qs:"b"` 513 | C *[]string `qs:"c"` 514 | }{ 515 | A: nil, 516 | B: []string{}, 517 | C: nil, 518 | } 519 | 520 | values, err := encoder.Values(s) 521 | if err != nil { 522 | t.Errorf("expected no error but got %v", err) 523 | t.FailNow() 524 | } 525 | if !reflect.DeepEqual(url.Values{}, values) { 526 | t.Errorf("expected empty, got %v", values) 527 | t.FailNow() 528 | } 529 | } 530 | 531 | func TestSliceValWithBasicVal(t *testing.T) { 532 | t.Parallel() 533 | encoder := NewEncoder() 534 | 535 | s := struct { 536 | StringList []string `qs:"str_list"` 537 | BoolList []bool `qs:"bool_list"` 538 | IntList []int `qs:"int_list"` 539 | }{ 540 | StringList: []string{"", "a", "b", "c"}, 541 | BoolList: []bool{true, false}, 542 | IntList: []int{0, 1, 2, 3}, 543 | } 544 | values, err := encoder.Values(s) 545 | if err != nil { 546 | t.Errorf("expected no error but got %v", err) 547 | t.FailNow() 548 | } 549 | expected := url.Values{ 550 | "str_list": []string{"", "a", "b", "c"}, 551 | "bool_list": []string{"true", "false"}, 552 | "int_list": []string{"0", "1", "2", "3"}, 553 | } 554 | if !reflect.DeepEqual(expected, values) { 555 | t.Errorf("expected %v, got %v", expected, values) 556 | t.FailNow() 557 | } 558 | } 559 | 560 | func TestSliceValWithBasicPtr(t *testing.T) { 561 | t.Parallel() 562 | encoder := NewEncoder() 563 | 564 | s := struct { 565 | StringList []*string `qs:"str_list"` 566 | BoolList []*bool `qs:"bool_list"` 567 | IntList []*int `qs:"int_list"` 568 | }{ 569 | StringList: []*string{withStr(""), withStr("a"), withStr("b"), withStr("c")}, 570 | BoolList: []*bool{withBool(true), withBool(false)}, 571 | IntList: []*int{withInt(0), withInt(1), withInt(2), withInt(3)}, 572 | } 573 | values, err := encoder.Values(s) 574 | if err != nil { 575 | t.Errorf("expected no error but got %v", err) 576 | t.FailNow() 577 | } 578 | expected := url.Values{ 579 | "str_list": []string{"", "a", "b", "c"}, 580 | "bool_list": []string{"true", "false"}, 581 | "int_list": []string{"0", "1", "2", "3"}, 582 | } 583 | if !reflect.DeepEqual(expected, values) { 584 | t.Errorf("expected %v, got %v", expected, values) 585 | t.FailNow() 586 | } 587 | } 588 | 589 | func TestSlicePtrWithBasicVal(t *testing.T) { 590 | t.Parallel() 591 | encoder := NewEncoder() 592 | 593 | strList := []string{"", "a", "b", "c"} 594 | boolList := []bool{true, false} 595 | intList := []int{0, 1, 2, 3} 596 | 597 | s := struct { 598 | StringList *[]string `qs:"str_list"` 599 | BoolList *[]bool `qs:"bool_list"` 600 | IntList *[]int `qs:"int_list"` 601 | }{ 602 | StringList: &strList, 603 | BoolList: &boolList, 604 | IntList: &intList, 605 | } 606 | values, err := encoder.Values(s) 607 | if err != nil { 608 | t.Errorf("expected no error but got %v", err) 609 | t.FailNow() 610 | } 611 | expected := url.Values{ 612 | "str_list": []string{"", "a", "b", "c"}, 613 | "bool_list": []string{"true", "false"}, 614 | "int_list": []string{"0", "1", "2", "3"}, 615 | } 616 | if !reflect.DeepEqual(expected, values) { 617 | t.Errorf("expected %v, got %v", expected, values) 618 | t.FailNow() 619 | } 620 | } 621 | 622 | func TestSlicePtrWithBasicPtr(t *testing.T) { 623 | t.Parallel() 624 | encoder := NewEncoder() 625 | 626 | strList := []*string{withStr(""), withStr("a"), withStr("b"), withStr("c")} 627 | boolList := []*bool{withBool(true), withBool(false)} 628 | intList := []*int{withInt(0), withInt(1), withInt(2), withInt(3)} 629 | 630 | s := struct { 631 | StringList *[]*string `qs:"str_list"` 632 | BoolList *[]*bool `qs:"bool_list"` 633 | IntList *[]*int `qs:"int_list"` 634 | }{ 635 | StringList: &strList, 636 | BoolList: &boolList, 637 | IntList: &intList, 638 | } 639 | values, err := encoder.Values(s) 640 | if err != nil { 641 | t.Errorf("expected no error but got %v", err) 642 | t.FailNow() 643 | } 644 | expected := url.Values{ 645 | "str_list": []string{"", "a", "b", "c"}, 646 | "bool_list": []string{"true", "false"}, 647 | "int_list": []string{"0", "1", "2", "3"}, 648 | } 649 | if !reflect.DeepEqual(expected, values) { 650 | t.Errorf("expected %v, got %v", expected, values) 651 | t.FailNow() 652 | } 653 | } 654 | 655 | func TestTimeFormat(t *testing.T) { 656 | t.Parallel() 657 | encoder := NewEncoder() 658 | 659 | tm := time.Unix(600, 0).UTC() 660 | 661 | times := struct { 662 | Rfc3339 time.Time `qs:"default_fmt"` 663 | Second time.Time `qs:"default_second,second"` 664 | Millis time.Time `qs:"default_millis,millis"` 665 | Rfc3339Ptr *time.Time `qs:"default_fmt_ptr"` 666 | SecondPtr *time.Time `qs:"default_second_ptr,second"` 667 | MillisPtr *time.Time `qs:"default_millis_ptr,millis"` 668 | }{ 669 | tm, 670 | tm, 671 | tm, 672 | &tm, 673 | &tm, 674 | &tm, 675 | } 676 | values, err := encoder.Values(times) 677 | if err != nil { 678 | t.Errorf("expected no error but got %v", err) 679 | t.FailNow() 680 | } 681 | expected := url.Values{ 682 | "default_fmt": []string{"1970-01-01T00:10:00Z"}, 683 | "default_second": []string{"600"}, 684 | "default_millis": []string{"600000"}, 685 | "default_fmt_ptr": []string{"1970-01-01T00:10:00Z"}, 686 | "default_second_ptr": []string{"600"}, 687 | "default_millis_ptr": []string{"600000"}, 688 | } 689 | if !reflect.DeepEqual(expected, values) { 690 | t.Errorf("expected %v, got %v", expected, values) 691 | t.FailNow() 692 | } 693 | } 694 | 695 | func TestBoolFormat(t *testing.T) { 696 | t.Parallel() 697 | encoder := NewEncoder() 698 | 699 | s := struct { 700 | Bool1 bool `qs:"bool_1,int"` 701 | Bool2 bool `qs:"bool_2,int"` 702 | NilBool *bool `qs:",omitempty"` 703 | }{ 704 | Bool2: true, 705 | } 706 | 707 | values, err := encoder.Values(&s) 708 | if err != nil { 709 | t.Errorf("expected no error but got %v", err) 710 | t.FailNow() 711 | } 712 | 713 | expected := url.Values{ 714 | "bool_1": []string{"0"}, 715 | "bool_2": []string{"1"}, 716 | } 717 | if !reflect.DeepEqual(expected, values) { 718 | t.Errorf("expected %v, got %v", expected, values) 719 | t.FailNow() 720 | } 721 | } 722 | 723 | func TestArrayFormat_Comma(t *testing.T) { 724 | t.Parallel() 725 | encoder := NewEncoder() 726 | 727 | tm := time.Unix(600, 0).UTC() 728 | 729 | s := struct { 730 | EmptyList []string `qs:"empty_list,comma"` 731 | StringList []string `qs:"str_list,comma"` 732 | Times []*time.Time `qs:"times,comma"` 733 | }{ 734 | StringList: []string{"a", "b", "c"}, 735 | Times: []*time.Time{&tm, nil}, 736 | } 737 | values, err := encoder.Values(s) 738 | if err != nil { 739 | t.Errorf("expected no error but got %v", err) 740 | t.FailNow() 741 | } 742 | expected := url.Values{ 743 | "empty_list": []string{""}, 744 | "str_list": []string{"a,b,c"}, 745 | "times": []string{tm.Format(time.RFC3339)}, 746 | } 747 | if !reflect.DeepEqual(expected, values) { 748 | t.Errorf("expected %v, got %v", expected, values) 749 | t.FailNow() 750 | } 751 | } 752 | 753 | func TestArrayFormat_Repeat(t *testing.T) { 754 | t.Parallel() 755 | encoder := NewEncoder() 756 | 757 | tm := time.Unix(600, 0).UTC() 758 | 759 | s := struct { 760 | StringList []string `qs:"str_list"` 761 | Times []*time.Time `qs:"times"` 762 | }{ 763 | StringList: []string{"a", "b", "c"}, 764 | Times: []*time.Time{&tm, nil}, 765 | } 766 | values, err := encoder.Values(s) 767 | if err != nil { 768 | t.Errorf("expected no error but got %v", err) 769 | t.FailNow() 770 | } 771 | expected := url.Values{ 772 | "str_list": []string{"a", "b", "c"}, 773 | "times": []string{tm.Format(time.RFC3339)}, 774 | } 775 | if !reflect.DeepEqual(expected, values) { 776 | t.Errorf("expected %v, got %v", expected, values) 777 | t.FailNow() 778 | } 779 | } 780 | 781 | func TestArrayFormat_Bracket(t *testing.T) { 782 | t.Parallel() 783 | encoder := NewEncoder() 784 | 785 | tm := time.Unix(600, 0).UTC() 786 | 787 | s := struct { 788 | StringList []string `qs:"str_list,bracket"` 789 | Times []*time.Time `qs:"times,bracket"` 790 | }{ 791 | StringList: []string{"a", "b", "c"}, 792 | Times: []*time.Time{&tm, nil}, 793 | } 794 | values, err := encoder.Values(s) 795 | if err != nil { 796 | t.Errorf("expected no error but got %v", err) 797 | t.FailNow() 798 | } 799 | expected := url.Values{ 800 | "str_list[]": []string{"a", "b", "c"}, 801 | "times[]": []string{tm.Format(time.RFC3339)}, 802 | } 803 | if !reflect.DeepEqual(expected, values) { 804 | t.Errorf("expected %v, got %v", expected, values) 805 | t.FailNow() 806 | } 807 | } 808 | 809 | func TestArrayFormat_Index(t *testing.T) { 810 | t.Parallel() 811 | encoder := NewEncoder() 812 | 813 | tm := time.Unix(600, 0).UTC() 814 | 815 | s := struct { 816 | StringList []string `qs:"str_list,index"` 817 | Times []*time.Time `qs:"times,index"` 818 | NilSlice *[]int `qs:",omitempty"` 819 | }{ 820 | StringList: []string{"a", "b", "c"}, 821 | Times: []*time.Time{&tm, nil}, 822 | NilSlice: nil, 823 | } 824 | values, err := encoder.Values(s) 825 | if err != nil { 826 | t.Errorf("expected no error but got %v", err) 827 | t.FailNow() 828 | } 829 | expected := url.Values{ 830 | "str_list[0]": []string{"a"}, 831 | "str_list[1]": []string{"b"}, 832 | "str_list[2]": []string{"c"}, 833 | "times[0]": []string{tm.Format(time.RFC3339)}, 834 | } 835 | if !reflect.DeepEqual(expected, values) { 836 | t.Errorf("expected %v, got %v", expected, values) 837 | t.FailNow() 838 | } 839 | } 840 | 841 | func TestNestedStruct(t *testing.T) { 842 | t.Parallel() 843 | encoder := NewEncoder() 844 | 845 | tm := time.Unix(600, 0) 846 | 847 | type newTime time.Time 848 | 849 | type Nested struct { 850 | Time time.Time `qs:"time,second"` 851 | Name *string `qs:"name,omitempty"` 852 | NewStr newTime `qs:"new_time,omitempty"` 853 | } 854 | 855 | s := struct { 856 | Nested Nested `qs:"nested"` 857 | NestedOmitNilPtr *Nested `qs:"nested_omit_nil_ptr,omitempty"` 858 | NestedNilPtr *Nested `qs:"nested_ptr"` 859 | NestedPtr *Nested `qs:"nested_ptr"` 860 | NestedList []Nested `qs:"nest_list,index"` 861 | }{ 862 | Nested: Nested{ 863 | Time: tm, 864 | }, 865 | NestedPtr: &Nested{ 866 | Time: tm, 867 | }, 868 | NestedList: []Nested{ 869 | { 870 | Time: tm, 871 | Name: withStr("abc"), 872 | }, 873 | { 874 | Time: tm, 875 | Name: withStr("def"), 876 | }, 877 | }, 878 | } 879 | 880 | values, err := encoder.Values(&s) 881 | if err != nil { 882 | t.Errorf("expected no error but got %v", err) 883 | t.FailNow() 884 | } 885 | expected := url.Values{ 886 | "nested[time]": []string{"600"}, 887 | "nested_ptr[time]": []string{"600"}, 888 | "nested_ptr": []string{""}, 889 | "nest_list[0][time]": []string{"600"}, 890 | "nest_list[0][name]": []string{"abc"}, 891 | "nest_list[1][time]": []string{"600"}, 892 | "nest_list[1][name]": []string{"def"}, 893 | } 894 | if !reflect.DeepEqual(expected, values) { 895 | t.Errorf("expected %v, got %v", expected, values) 896 | t.FailNow() 897 | } 898 | } 899 | 900 | func TestEncodeInterface(t *testing.T) { 901 | t.Parallel() 902 | encoder := NewEncoder() 903 | 904 | s := &struct { 905 | String interface{} `qs:"string"` 906 | Bool interface{} `qs:"bool"` 907 | Int interface{} `qs:"int"` 908 | EmptyFloat interface{} `qs:"float,omitempty"` 909 | NilPtr interface{} `qs:"nil_ptr"` 910 | OmitNilPtr interface{} `qs:"omit_nil_ptr,omitempty"` 911 | }{ 912 | String: "abc", 913 | Bool: true, 914 | Int: withInt(5), 915 | NilPtr: nil, 916 | } 917 | 918 | values, err := encoder.Values(&s) 919 | if err != nil { 920 | t.Errorf("expected no error but got %v", err) 921 | t.FailNow() 922 | } 923 | 924 | expected := url.Values{ 925 | "string": []string{"abc"}, 926 | "bool": []string{"true"}, 927 | "int": []string{"5"}, 928 | "nil_ptr": []string{""}, 929 | } 930 | if !reflect.DeepEqual(expected, values) { 931 | t.Errorf("expected %v, got %v", expected, values) 932 | t.FailNow() 933 | } 934 | } 935 | 936 | func TestEncodeMap(t *testing.T) { 937 | t.Parallel() 938 | encoder := NewEncoder() 939 | 940 | s := struct { 941 | Map map[string]bool `qs:"map,int,omitempty"` 942 | PtrMap map[string]*bool `qs:"ptr_map"` 943 | NilMap map[string]int `qs:"nil_map"` 944 | NilPtrMap *map[string]int `qs:"nil_ptr_map"` 945 | EmptyMap map[*string]string `qs:"empty_map"` 946 | }{ 947 | Map: map[string]bool{ 948 | "abc": true, 949 | }, 950 | PtrMap: map[string]*bool{ 951 | "xyz": withBool(false), 952 | }, 953 | EmptyMap: make(map[*string]string), 954 | } 955 | values, err := encoder.Values(s) 956 | if err != nil { 957 | t.Errorf("expected no error but got %v", err) 958 | t.FailNow() 959 | } 960 | 961 | expected := url.Values{ 962 | "map[abc]": []string{"true"}, 963 | "ptr_map[xyz]": []string{"false"}, 964 | } 965 | if !reflect.DeepEqual(expected, values) { 966 | t.Errorf("expected %v, got %v", expected, values) 967 | t.FailNow() 968 | } 969 | } 970 | 971 | type Timestamp struct { 972 | time.Time 973 | } 974 | 975 | func (t Timestamp) EncodeParam() (string, error) { 976 | return t.Format(time.RFC3339), nil 977 | } 978 | 979 | func (t Timestamp) IsZero() bool { 980 | return t.Time.IsZero() 981 | } 982 | 983 | type TimestampPtr struct { 984 | time.Time 985 | } 986 | 987 | func (t *TimestampPtr) EncodeParam() (string, error) { 988 | return t.Format(time.RFC3339), nil 989 | } 990 | 991 | func (t *TimestampPtr) IsZero() bool { 992 | return t.Time.IsZero() 993 | } 994 | 995 | func TestEncodeCustomType(t *testing.T) { 996 | t.Parallel() 997 | encoder := NewEncoder() 998 | 999 | tm := time.Unix(0, 0).UTC() 1000 | 1001 | var zeroPtrTs *TimestampPtr 1002 | s := struct { 1003 | OmitTimestamp Timestamp `qs:"zero_ts,omitempty"` 1004 | ZeroTimestamp Timestamp `qs:"zero_ts"` 1005 | Timestamp Timestamp `qs:"ts"` 1006 | InterfaceTs interface{} `qs:"interface_ts"` 1007 | ZeroInterfaceTs interface{} `qs:"zero_interface_ts,omitempty"` 1008 | OmitPtrTimestamp *TimestampPtr `qs:"omit_ptr_ts,omitempty"` 1009 | NilPtrTimestamp *TimestampPtr `qs:"zero_ptr_ts"` 1010 | TimestampPtr *TimestampPtr `qs:"timestamp_ptr"` 1011 | TsList []Timestamp `qs:"ts_list"` 1012 | TsCommaList []Timestamp `qs:"ts_comma_list,comma"` 1013 | TsBracketList []Timestamp `qs:"ts_bracket_list,bracket"` 1014 | TsIndexList []Timestamp `qs:"ts_index_list,index"` 1015 | OmitTsList []*Timestamp `qs:"omit_ts_list"` 1016 | NilTsList []*Timestamp `qs:"nil_ts_list"` 1017 | TsPtrList []*Timestamp `qs:"ts_ptr_list"` 1018 | TsPtrCommaList []*Timestamp `qs:"ts_ptr_comma_list,comma"` 1019 | TsPtrBracketList []*Timestamp `qs:"ts_ptr_bracket_list,bracket"` 1020 | TsPtrIndexList []*Timestamp `qs:"ts_ptr_index_list,index"` 1021 | }{ 1022 | OmitTimestamp: Timestamp{time.Time{}.UTC()}, 1023 | ZeroTimestamp: Timestamp{time.Time{}.UTC()}, 1024 | Timestamp: Timestamp{tm}, 1025 | ZeroInterfaceTs: zeroPtrTs, 1026 | InterfaceTs: Timestamp{tm}, 1027 | TimestampPtr: &TimestampPtr{tm}, 1028 | TsList: []Timestamp{ 1029 | {tm}, {tm}, 1030 | }, 1031 | TsCommaList: []Timestamp{ 1032 | {tm}, {tm}, 1033 | }, 1034 | TsBracketList: []Timestamp{ 1035 | {tm}, {tm}, 1036 | }, 1037 | TsIndexList: []Timestamp{ 1038 | {tm}, {tm}, 1039 | }, 1040 | NilTsList: []*Timestamp{ 1041 | nil, 1042 | }, 1043 | TsPtrList: []*Timestamp{ 1044 | nil, {tm}, {tm}, 1045 | }, 1046 | TsPtrCommaList: []*Timestamp{ 1047 | nil, {tm}, {tm}, 1048 | }, 1049 | TsPtrBracketList: []*Timestamp{ 1050 | nil, {tm}, {tm}, 1051 | }, 1052 | TsPtrIndexList: []*Timestamp{ 1053 | nil, {tm}, {tm}, 1054 | }, 1055 | } 1056 | 1057 | values, err := encoder.Values(&s) 1058 | if err != nil { 1059 | t.Errorf("expected no error but got %v", err) 1060 | t.FailNow() 1061 | } 1062 | 1063 | expected := url.Values{ 1064 | "zero_ts": []string{""}, 1065 | "ts": []string{"1970-01-01T00:00:00Z"}, 1066 | "zero_ptr_ts": []string{""}, 1067 | "timestamp_ptr": []string{"1970-01-01T00:00:00Z"}, 1068 | "interface_ts": []string{"1970-01-01T00:00:00Z"}, 1069 | "ts_list": []string{"1970-01-01T00:00:00Z", "1970-01-01T00:00:00Z"}, 1070 | "ts_comma_list": []string{"1970-01-01T00:00:00Z,1970-01-01T00:00:00Z"}, 1071 | "ts_bracket_list[]": []string{"1970-01-01T00:00:00Z", "1970-01-01T00:00:00Z"}, 1072 | "ts_index_list[0]": []string{"1970-01-01T00:00:00Z"}, 1073 | "ts_index_list[1]": []string{"1970-01-01T00:00:00Z"}, 1074 | "ts_ptr_list": []string{"1970-01-01T00:00:00Z", "1970-01-01T00:00:00Z"}, 1075 | "ts_ptr_comma_list": []string{"1970-01-01T00:00:00Z,1970-01-01T00:00:00Z"}, 1076 | "ts_ptr_bracket_list[]": []string{"1970-01-01T00:00:00Z", "1970-01-01T00:00:00Z"}, 1077 | "ts_ptr_index_list[0]": []string{"1970-01-01T00:00:00Z"}, 1078 | "ts_ptr_index_list[1]": []string{"1970-01-01T00:00:00Z"}, 1079 | } 1080 | if !reflect.DeepEqual(expected, values) { 1081 | t.Errorf("expected %v, got %v", expected, values) 1082 | t.FailNow() 1083 | } 1084 | } 1085 | 1086 | type ErrTimestamp struct { 1087 | time.Time 1088 | } 1089 | 1090 | func (t *ErrTimestamp) EncodeParam() (string, error) { 1091 | return "", fmt.Errorf("failed to encode param") 1092 | } 1093 | 1094 | func (t *ErrTimestamp) IsZero() bool { 1095 | return t.Time.IsZero() 1096 | } 1097 | 1098 | func TestEncodeErrCustomType(t *testing.T) { 1099 | t.Parallel() 1100 | encoder := NewEncoder() 1101 | 1102 | tm := time.Unix(0, 0).UTC() 1103 | 1104 | s1 := struct { 1105 | ErrTimestamp *ErrTimestamp 1106 | }{ 1107 | ErrTimestamp: &ErrTimestamp{tm}, 1108 | } 1109 | s2 := struct { 1110 | ErrTimestamps []*ErrTimestamp 1111 | }{ 1112 | ErrTimestamps: []*ErrTimestamp{ 1113 | {tm}, 1114 | }, 1115 | } 1116 | s3 := struct { 1117 | ErrBracketList []*ErrTimestamp `qs:",bracket"` 1118 | }{ 1119 | ErrBracketList: []*ErrTimestamp{ 1120 | {tm}, 1121 | }, 1122 | } 1123 | s4 := struct { 1124 | ErrIndexList []*ErrTimestamp `qs:",index"` 1125 | }{ 1126 | ErrIndexList: []*ErrTimestamp{ 1127 | {tm}, 1128 | }, 1129 | } 1130 | s5 := struct { 1131 | ErrIndexList []*ErrTimestamp `qs:",comma"` 1132 | }{ 1133 | ErrIndexList: []*ErrTimestamp{ 1134 | {tm}, 1135 | }, 1136 | } 1137 | s6 := struct { 1138 | ErrTimestampMap map[string]*ErrTimestamp 1139 | }{ 1140 | ErrTimestampMap: map[string]*ErrTimestamp{ 1141 | "test": {tm}, 1142 | }, 1143 | } 1144 | s7 := struct { 1145 | ErrTimestampMap map[*ErrTimestamp]bool 1146 | }{ 1147 | ErrTimestampMap: map[*ErrTimestamp]bool{ 1148 | {tm}: true, 1149 | }, 1150 | } 1151 | s8 := struct { 1152 | Embed struct { 1153 | ErrTimestampMap *ErrTimestamp 1154 | } 1155 | }{ 1156 | Embed: struct { 1157 | ErrTimestampMap *ErrTimestamp 1158 | }{ 1159 | ErrTimestampMap: &ErrTimestamp{tm}, 1160 | }, 1161 | } 1162 | s9 := struct { 1163 | ErrTimestamp interface{} 1164 | }{ 1165 | ErrTimestamp: &ErrTimestamp{tm}, 1166 | } 1167 | 1168 | values, err := encoder.Values(&s1) 1169 | if err == nil { 1170 | t.Errorf("expected error, got: %v", values) 1171 | t.FailNow() 1172 | } 1173 | 1174 | values = url.Values{} 1175 | err = encoder.Encode(&s1, values) 1176 | if err == nil { 1177 | t.Errorf("expected error, got: %v", values) 1178 | t.FailNow() 1179 | } 1180 | 1181 | values, err = encoder.Values(&s2) 1182 | if err == nil { 1183 | t.Errorf("expected error, got: %v", values) 1184 | t.FailNow() 1185 | } 1186 | 1187 | values = url.Values{} 1188 | err = encoder.Encode(&s2, values) 1189 | if err == nil { 1190 | t.Errorf("expected error, got: %v", values) 1191 | t.FailNow() 1192 | } 1193 | 1194 | values, err = encoder.Values(&s3) 1195 | if err == nil { 1196 | t.Errorf("expected error, got: %v", values) 1197 | t.FailNow() 1198 | } 1199 | 1200 | values = url.Values{} 1201 | err = encoder.Encode(&s3, values) 1202 | if err == nil { 1203 | t.Errorf("expected error, got: %v", values) 1204 | t.FailNow() 1205 | } 1206 | 1207 | values, err = encoder.Values(&s4) 1208 | if err == nil { 1209 | t.Errorf("expected error, got: %v", values) 1210 | t.FailNow() 1211 | } 1212 | 1213 | values = url.Values{} 1214 | err = encoder.Encode(&s4, values) 1215 | if err == nil { 1216 | t.Errorf("expected error, got: %v", values) 1217 | t.FailNow() 1218 | } 1219 | 1220 | values, err = encoder.Values(&s5) 1221 | if err == nil { 1222 | t.Errorf("expected error, got: %v", values) 1223 | t.FailNow() 1224 | } 1225 | 1226 | values = url.Values{} 1227 | err = encoder.Encode(&s5, values) 1228 | if err == nil { 1229 | t.Errorf("expected error, got: %v", values) 1230 | t.FailNow() 1231 | } 1232 | 1233 | values, err = encoder.Values(&s6) 1234 | if err == nil { 1235 | t.Errorf("expected error, got: %v", values) 1236 | t.FailNow() 1237 | } 1238 | 1239 | values = url.Values{} 1240 | err = encoder.Encode(&s6, values) 1241 | if err == nil { 1242 | t.Errorf("expected error, got: %v", values) 1243 | t.FailNow() 1244 | } 1245 | 1246 | values, err = encoder.Values(&s7) 1247 | if err == nil { 1248 | t.Errorf("expected error, got: %v", values) 1249 | t.FailNow() 1250 | } 1251 | 1252 | values = url.Values{} 1253 | err = encoder.Encode(&s7, values) 1254 | if err == nil { 1255 | t.Errorf("expected error, got: %v", values) 1256 | t.FailNow() 1257 | } 1258 | 1259 | values, err = encoder.Values(&s8) 1260 | if err == nil { 1261 | t.Errorf("expected error, got: %v", values) 1262 | t.FailNow() 1263 | } 1264 | 1265 | values = url.Values{} 1266 | err = encoder.Encode(&s8, values) 1267 | if err == nil { 1268 | t.Errorf("expected error, got: %v", values) 1269 | t.FailNow() 1270 | } 1271 | 1272 | values, err = encoder.Values(&s9) 1273 | if err == nil { 1274 | t.Errorf("expected error, got: %v", values) 1275 | t.FailNow() 1276 | } 1277 | 1278 | values = url.Values{} 1279 | err = encoder.Encode(&s9, values) 1280 | if err == nil { 1281 | t.Errorf("expected error, got: %v", values) 1282 | t.FailNow() 1283 | } 1284 | } 1285 | 1286 | func TestEncoderIgnoreUnregisterType(t *testing.T) { 1287 | t.Parallel() 1288 | encoder := NewEncoder() 1289 | 1290 | s := struct { 1291 | Fn []func() `qs:"fn,bracket"` 1292 | Ch []chan struct{} `qs:"chan,comma"` 1293 | Ch2 []chan struct{} `qs:"chan2,index"` 1294 | Ch3 []chan struct{} `qs:"chan3"` 1295 | Ch4 map[chan bool]func() `qs:"chan4"` 1296 | }{ 1297 | Fn: []func(){func() {}}, 1298 | Ch: []chan struct{}{make(chan struct{})}, 1299 | Ch2: []chan struct{}{make(chan struct{})}, 1300 | } 1301 | 1302 | values, err := encoder.Values(s) 1303 | if err != nil { 1304 | t.Errorf("expected no error, got: %v", err) 1305 | t.FailNow() 1306 | } 1307 | if !reflect.DeepEqual(url.Values{}, values) { 1308 | t.Errorf("expected empty values, got: %v", values) 1309 | t.FailNow() 1310 | } 1311 | } 1312 | 1313 | //------------------------------------------------ 1314 | 1315 | func withStr(v string) *string { 1316 | return &v 1317 | } 1318 | 1319 | func withBool(v bool) *bool { 1320 | return &v 1321 | } 1322 | 1323 | func withInt(v int) *int { 1324 | return &v 1325 | } 1326 | 1327 | func withInt8(v int8) *int8 { 1328 | return &v 1329 | } 1330 | 1331 | func withInt16(v int16) *int16 { 1332 | return &v 1333 | } 1334 | 1335 | func withInt32(v int32) *int32 { 1336 | return &v 1337 | } 1338 | 1339 | func withInt64(v int64) *int64 { 1340 | return &v 1341 | } 1342 | 1343 | func withUint(v uint) *uint { 1344 | return &v 1345 | } 1346 | 1347 | func withUint8(v uint8) *uint8 { 1348 | return &v 1349 | } 1350 | 1351 | func withUint16(v uint16) *uint16 { 1352 | return &v 1353 | } 1354 | 1355 | func withUint32(v uint32) *uint32 { 1356 | return &v 1357 | } 1358 | 1359 | func withUint64(v uint64) *uint64 { 1360 | return &v 1361 | } 1362 | 1363 | func withUintPtr(v uintptr) *uintptr { 1364 | return &v 1365 | } 1366 | 1367 | func withFloat32(v float32) *float32 { 1368 | return &v 1369 | } 1370 | 1371 | func withFloat64(v float64) *float64 { 1372 | return &v 1373 | } 1374 | 1375 | func withComplex64(v complex64) *complex64 { 1376 | return &v 1377 | } 1378 | 1379 | func withComplex128(v complex128) *complex128 { 1380 | return &v 1381 | } 1382 | 1383 | func complexZeroValStr() string { 1384 | return strconv.FormatComplex(complex128(0), 'f', -1, 128) 1385 | } 1386 | 1387 | func complex128ToStr(v complex128) string { 1388 | return strconv.FormatComplex(v, 'f', -1, 128) 1389 | } 1390 | 1391 | func withTime(v time.Time) *time.Time { 1392 | return &v 1393 | } 1394 | -------------------------------------------------------------------------------- /err.go: -------------------------------------------------------------------------------- 1 | package qs 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | ) 7 | 8 | type InvalidInputErr struct { 9 | InputKind reflect.Kind 10 | } 11 | 12 | func (e InvalidInputErr) Error() string { 13 | return fmt.Sprintf(`input should be struct type, got "%v"`, e.InputKind) 14 | } 15 | -------------------------------------------------------------------------------- /example/basic/basic.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/sonh/qs" 6 | "time" 7 | ) 8 | 9 | type Query struct { 10 | Tags []string `qs:"tags"` 11 | Limit int `qs:"limit"` 12 | From time.Time `qs:"from"` 13 | Open bool `qs:"open,int"` 14 | Active bool `qs:"active,omitempty"` //omit empty value 15 | Ignore float64 `qs:"-"` //ignore 16 | } 17 | 18 | func main() { 19 | query := &Query{ 20 | Tags: []string{"docker", "golang", "reactjs"}, 21 | Limit: 24, 22 | From: time.Unix(1580601600, 0).UTC(), 23 | Ignore: 0, 24 | } 25 | 26 | encoder := qs.NewEncoder() 27 | values, err := encoder.Values(query) 28 | if err != nil { 29 | // Handle error 30 | fmt.Println(err) 31 | return 32 | } 33 | fmt.Println(values.Encode()) 34 | } 35 | -------------------------------------------------------------------------------- /example/custom/custom.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/sonh/qs" 6 | ) 7 | 8 | type NullableName struct { 9 | First string 10 | Last string 11 | } 12 | 13 | func (n NullableName) EncodeParam() (string, error) { 14 | return n.First + n.Last, nil 15 | } 16 | 17 | func (n NullableName) IsZero() bool { 18 | return n.First == "" && n.Last == "" 19 | } 20 | 21 | func main() { 22 | 23 | type Struct struct { 24 | User NullableName `qs:"user"` 25 | Admin NullableName `qs:"admin,omitempty"` 26 | } 27 | 28 | s := Struct{ 29 | User: NullableName{ 30 | First: "son", 31 | Last: "huynh", 32 | }, 33 | } 34 | encoder := qs.NewEncoder() 35 | 36 | values, err := encoder.Values(&s) 37 | if err != nil { 38 | // Handle error 39 | fmt.Println("failed") 40 | return 41 | } 42 | fmt.Println(values.Encode()) 43 | } 44 | -------------------------------------------------------------------------------- /example/embed/embed.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/sonh/qs" 6 | "time" 7 | ) 8 | 9 | type User struct { 10 | Verified bool `qs:"verified"` 11 | From time.Time `qs:"from,millis"` 12 | } 13 | 14 | type Query struct { 15 | User User `qs:"user"` 16 | } 17 | 18 | func main() { 19 | query := Query{ 20 | User: User{ 21 | Verified: true, 22 | From: time.Now(), 23 | }, 24 | } 25 | 26 | encoder := qs.NewEncoder() 27 | values, err := encoder.Values(query) 28 | if err != nil { 29 | // Handle error 30 | fmt.Println("failed") 31 | return 32 | } 33 | fmt.Println(values.Encode()) 34 | } 35 | -------------------------------------------------------------------------------- /example/slice/slice.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/sonh/qs" 6 | ) 7 | 8 | type Query struct { 9 | Default []string `qs:"default_fmt"` 10 | Comma []string `qs:"comma_fmt,comma"` 11 | Bracket []string `qs:"bracket_fmt,bracket"` 12 | Index []string `qs:"index_fmt,index"` 13 | } 14 | 15 | func main() { 16 | tags := []string{"go", "docker"} 17 | query := &Query{ 18 | Default: tags, 19 | Comma: tags, 20 | Bracket: tags, 21 | Index: tags, 22 | } 23 | 24 | encoder := qs.NewEncoder() 25 | values, err := encoder.Values(query) 26 | if err != nil { 27 | // Handle error 28 | fmt.Println("failed") 29 | return 30 | } 31 | fmt.Println(values.Encode()) 32 | } 33 | -------------------------------------------------------------------------------- /example/timefmt/timefmt.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/sonh/qs" 6 | "time" 7 | ) 8 | 9 | type Query struct { 10 | Default time.Time `qs:"default_fmt"` 11 | Second time.Time `qs:"second_fmt,second"` //use `second` option 12 | Millis time.Time `qs:"millis_fmt,millis"` //use `millis` option 13 | } 14 | 15 | func main() { 16 | t := time.Unix(1580601600, 0).UTC() 17 | query := &Query{ 18 | Default: t, 19 | Second: t, 20 | Millis: t, 21 | } 22 | 23 | encoder := qs.NewEncoder() 24 | values, err := encoder.Values(query) 25 | if err != nil { 26 | // Handle error 27 | fmt.Println("failed") 28 | return 29 | } 30 | fmt.Println(values.Encode()) 31 | } 32 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sonh/qs 2 | 3 | go 1.19 4 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sonh/qs/101ebf353862fa368d4f77faef43719585d80ade/go.sum --------------------------------------------------------------------------------