├── .gitignore ├── go.mod ├── go.sum ├── .github ├── dependabot.yml └── workflows │ ├── linter.yml │ └── tests.yml ├── .golangci.yml ├── LICENSE ├── README.md ├── CONTRIBUTING.md └── query ├── encode.go └── encode_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *.test 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/google/go-querystring 2 | 3 | go 1.13 4 | 5 | require github.com/google/go-cmp v0.6.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 2 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: / 5 | schedule: 6 | interval: daily 7 | - package-ecosystem: github-actions 8 | directory: / 9 | schedule: 10 | interval: daily 11 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | enable: 4 | - dogsled 5 | - dupl 6 | - gosec 7 | - misspell 8 | - nakedret 9 | - staticcheck 10 | - unconvert 11 | - unparam 12 | - whitespace 13 | formatters: 14 | enable: 15 | - gofmt 16 | - goimports 17 | -------------------------------------------------------------------------------- /.github/workflows/linter.yml: -------------------------------------------------------------------------------- 1 | name: linter 2 | on: [push, pull_request] 3 | 4 | concurrency: 5 | group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" 6 | cancel-in-progress: true 7 | 8 | jobs: 9 | lint: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 14 | - name: golangci-lint 15 | uses: golangci/golangci-lint-action@0a35821d5c230e903fcfe077583637dea1b27b47 # v9.0.0 16 | with: 17 | version: v2.1.2 18 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | concurrency: 12 | group: "${{ github.workflow }} @ ${{ github.event.pull_request.head.label || github.head_ref || github.ref }}" 13 | cancel-in-progress: true 14 | 15 | jobs: 16 | test: 17 | strategy: 18 | matrix: 19 | go-version: [stable, oldstable, "1.13"] 20 | platform: [ubuntu-latest] 21 | include: 22 | # only update test coverage stats with most recent go version on linux 23 | - go-version: stable 24 | platform: ubuntu-latest 25 | update-coverage: true 26 | runs-on: ${{ matrix.platform }} 27 | 28 | steps: 29 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 30 | - uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0 31 | with: 32 | go-version: ${{ matrix.go-version }} 33 | 34 | - name: Run go test 35 | run: go test -v -race -coverprofile coverage.txt -covermode atomic ./... 36 | 37 | - name: Upload coverage to Codecov 38 | if: ${{ matrix.update-coverage }} 39 | uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d #v5.4.2 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 Google. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Google Inc. nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-querystring # 2 | 3 | [![Go Reference](https://pkg.go.dev/badge/github.com/google/go-querystring/query.svg)](https://pkg.go.dev/github.com/google/go-querystring/query) 4 | [![Test Status](https://github.com/google/go-querystring/workflows/tests/badge.svg)](https://github.com/google/go-querystring/actions?query=workflow%3Atests) 5 | [![Test Coverage](https://codecov.io/gh/google/go-querystring/branch/master/graph/badge.svg)](https://codecov.io/gh/google/go-querystring) 6 | 7 | go-querystring is a Go library for encoding structs into URL query parameters. 8 | 9 | ## Usage ## 10 | 11 | ```go 12 | import "github.com/google/go-querystring/query" 13 | ``` 14 | 15 | go-querystring is designed to assist in scenarios where you want to construct a 16 | URL using a struct that represents the URL query parameters. You might do this 17 | to enforce the type safety of your parameters, for example, as is done in the 18 | [go-github][] library. 19 | 20 | The query package exports a single `Values()` function. A simple example: 21 | 22 | ```go 23 | type Options struct { 24 | Query string `url:"q"` 25 | ShowAll bool `url:"all"` 26 | Page int `url:"page"` 27 | } 28 | 29 | opt := Options{ "foo", true, 2 } 30 | v, _ := query.Values(opt) 31 | fmt.Print(v.Encode()) // will output: "q=foo&all=true&page=2" 32 | ``` 33 | 34 | See the [package godocs][] for complete documentation on supported types and 35 | formatting options. 36 | 37 | [go-github]: https://github.com/google/go-github/commit/994f6f8405f052a117d2d0b500054341048fbb08 38 | [package godocs]: https://pkg.go.dev/github.com/google/go-querystring/query 39 | 40 | ## Alternatives ## 41 | 42 | If you are looking for a library that can both encode and decode query strings, 43 | you might consider one of these alternatives: 44 | 45 | - https://github.com/gorilla/schema 46 | - https://github.com/pasztorpisti/qs 47 | - https://github.com/hetiansu5/urlquery 48 | - https://github.com/ggicci/httpin (decoder only) 49 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute # 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | a just a few small guidelines you need to follow. 5 | 6 | 7 | ## Contributor License Agreement ## 8 | 9 | Contributions to any Google project must be accompanied by a Contributor 10 | License Agreement. This is not a copyright **assignment**, it simply gives 11 | Google permission to use and redistribute your contributions as part of the 12 | project. 13 | 14 | * If you are an individual writing original source code and you're sure you 15 | own the intellectual property, then you'll need to sign an [individual 16 | CLA][]. 17 | 18 | * If you work for a company that wants to allow you to contribute your work, 19 | then you'll need to sign a [corporate CLA][]. 20 | 21 | You generally only need to submit a CLA once, so if you've already submitted 22 | one (even if it was for a different project), you probably don't need to do it 23 | again. 24 | 25 | [individual CLA]: https://developers.google.com/open-source/cla/individual 26 | [corporate CLA]: https://developers.google.com/open-source/cla/corporate 27 | 28 | 29 | ## Submitting a patch ## 30 | 31 | 1. It's generally best to start by opening a new issue describing the bug or 32 | feature you're intending to fix. Even if you think it's relatively minor, 33 | it's helpful to know what people are working on. Mention in the initial 34 | issue that you are planning to work on that bug or feature so that it can 35 | be assigned to you. 36 | 37 | 1. Follow the normal process of [forking][] the project, and setup a new 38 | branch to work in. It's important that each group of changes be done in 39 | separate branches in order to ensure that a pull request only includes the 40 | commits related to that bug or feature. 41 | 42 | 1. Go makes it very simple to ensure properly formatted code, so always run 43 | `go fmt` on your code before committing it. You should also run 44 | [golint][] over your code. As noted in the [golint readme][], it's not 45 | strictly necessary that your code be completely "lint-free", but this will 46 | help you find common style issues. 47 | 48 | 1. Any significant changes should almost always be accompanied by tests. The 49 | project already has good test coverage, so look at some of the existing 50 | tests if you're unsure how to go about it. [gocov][] and [gocov-html][] 51 | are invaluable tools for seeing which parts of your code aren't being 52 | exercised by your tests. 53 | 54 | 1. Do your best to have [well-formed commit messages][] for each change. 55 | This provides consistency throughout the project, and ensures that commit 56 | messages are able to be formatted properly by various git tools. 57 | 58 | 1. Finally, push the commits to your fork and submit a [pull request][]. 59 | 60 | [forking]: https://help.github.com/articles/fork-a-repo 61 | [golint]: https://github.com/golang/lint 62 | [golint readme]: https://github.com/golang/lint/blob/master/README 63 | [gocov]: https://github.com/axw/gocov 64 | [gocov-html]: https://github.com/matm/gocov-html 65 | [well-formed commit messages]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html 66 | [squash]: http://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits 67 | [pull request]: https://help.github.com/articles/creating-a-pull-request 68 | -------------------------------------------------------------------------------- /query/encode.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package query implements encoding of structs into URL query parameters. 6 | // 7 | // As a simple example: 8 | // 9 | // type Options struct { 10 | // Query string `url:"q"` 11 | // ShowAll bool `url:"all"` 12 | // Page int `url:"page"` 13 | // } 14 | // 15 | // opt := Options{ "foo", true, 2 } 16 | // v, _ := query.Values(opt) 17 | // fmt.Print(v.Encode()) // will output: "q=foo&all=true&page=2" 18 | // 19 | // The exact mapping between Go values and url.Values is described in the 20 | // documentation for the Values() function. 21 | package query 22 | 23 | import ( 24 | "fmt" 25 | "net/url" 26 | "reflect" 27 | "strconv" 28 | "strings" 29 | "time" 30 | ) 31 | 32 | var timeType = reflect.TypeOf(time.Time{}) 33 | 34 | var encoderType = reflect.TypeOf(new(Encoder)).Elem() 35 | 36 | // Encoder is an interface implemented by any type that wishes to encode 37 | // itself into URL values in a non-standard way. 38 | type Encoder interface { 39 | EncodeValues(key string, v *url.Values) error 40 | } 41 | 42 | // Values returns the url.Values encoding of v. 43 | // 44 | // Values expects to be passed a struct, and traverses it recursively using the 45 | // following encoding rules. 46 | // 47 | // Each exported struct field is encoded as a URL parameter unless 48 | // 49 | // - the field's tag is "-", or 50 | // - the field is empty and its tag specifies the "omitempty" option 51 | // 52 | // The empty values are false, 0, any nil pointer or interface value, any array 53 | // slice, map, or string of length zero, and any type (such as time.Time) that 54 | // returns true for IsZero(). 55 | // 56 | // The URL parameter name defaults to the struct field name but can be 57 | // specified in the struct field's tag value. The "url" key in the struct 58 | // field's tag value is the key name, followed by an optional comma and 59 | // options. For example: 60 | // 61 | // // Field is ignored by this package. 62 | // Field int `url:"-"` 63 | // 64 | // // Field appears as URL parameter "myName". 65 | // Field int `url:"myName"` 66 | // 67 | // // Field appears as URL parameter "myName" and the field is omitted if 68 | // // its value is empty 69 | // Field int `url:"myName,omitempty"` 70 | // 71 | // // Field appears as URL parameter "Field" (the default), but the field 72 | // // is skipped if empty. Note the leading comma. 73 | // Field int `url:",omitempty"` 74 | // 75 | // For encoding individual field values, the following type-dependent rules 76 | // apply: 77 | // 78 | // Boolean values default to encoding as the strings "true" or "false". 79 | // Including the "int" option signals that the field should be encoded as the 80 | // strings "1" or "0". 81 | // 82 | // time.Time values default to encoding as RFC3339 timestamps. Including the 83 | // "unix" option signals that the field should be encoded as a Unix time (see 84 | // time.Unix()). The "unixmilli" and "unixnano" options will encode the number 85 | // of milliseconds and nanoseconds, respectively, since January 1, 1970 (see 86 | // time.UnixNano()). Including the "layout" struct tag (separate from the 87 | // "url" tag) will use the value of the "layout" tag as a layout passed to 88 | // time.Format. For example: 89 | // 90 | // // Encode a time.Time as YYYY-MM-DD 91 | // Field time.Time `layout:"2006-01-02"` 92 | // 93 | // Slice and Array values default to encoding as multiple URL values of the 94 | // same name. Including the "comma" option signals that the field should be 95 | // encoded as a single comma-delimited value. Including the "space" option 96 | // similarly encodes the value as a single space-delimited string. Including 97 | // the "semicolon" option will encode the value as a semicolon-delimited string. 98 | // Including the "brackets" option signals that the multiple URL values should 99 | // have "[]" appended to the value name. "numbered" will append a number to 100 | // the end of each incidence of the value name, example: 101 | // name0=value0&name1=value1, etc. Including the "del" struct tag (separate 102 | // from the "url" tag) will use the value of the "del" tag as the delimiter. 103 | // For example: 104 | // 105 | // // Encode a slice of bools as ints ("1" for true, "0" for false), 106 | // // separated by exclamation points "!". 107 | // Field []bool `url:",int" del:"!"` 108 | // 109 | // Anonymous struct fields are usually encoded as if their inner exported 110 | // fields were fields in the outer struct, subject to the standard Go 111 | // visibility rules. An anonymous struct field with a name given in its URL 112 | // tag is treated as having that name, rather than being anonymous. 113 | // 114 | // Non-nil pointer values are encoded as the value pointed to. 115 | // 116 | // Nested structs have their fields processed recursively and are encoded 117 | // including parent fields in value names for scoping. For example, 118 | // 119 | // "user[name]=acme&user[addr][postcode]=1234&user[addr][city]=SFO" 120 | // 121 | // All other values are encoded using their default string representation. 122 | // 123 | // Multiple fields that encode to the same URL parameter name will be included 124 | // as multiple URL values of the same name. 125 | func Values(v interface{}) (url.Values, error) { 126 | values := make(url.Values) 127 | 128 | if v == nil { 129 | return values, nil 130 | } 131 | 132 | val := reflect.ValueOf(v) 133 | for val.Kind() == reflect.Ptr { 134 | if val.IsNil() { 135 | return values, nil 136 | } 137 | val = val.Elem() 138 | } 139 | 140 | if val.Kind() != reflect.Struct { 141 | return nil, fmt.Errorf("query: Values() expects struct input. Got %v", val.Kind()) 142 | } 143 | 144 | err := reflectValue(values, val, "") 145 | return values, err 146 | } 147 | 148 | // reflectValue populates the values parameter from the struct fields in val. 149 | // Embedded structs are followed recursively (using the rules defined in the 150 | // Values function documentation) breadth-first. 151 | func reflectValue(values url.Values, val reflect.Value, scope string) error { 152 | var embedded []reflect.Value 153 | 154 | typ := val.Type() 155 | for i := 0; i < typ.NumField(); i++ { 156 | sf := typ.Field(i) 157 | if sf.PkgPath != "" && !sf.Anonymous { // unexported 158 | continue 159 | } 160 | 161 | sv := val.Field(i) 162 | tag := sf.Tag.Get("url") 163 | if tag == "-" { 164 | continue 165 | } 166 | name, opts := parseTag(tag) 167 | 168 | if name == "" { 169 | if sf.Anonymous { 170 | v := reflect.Indirect(sv) 171 | if v.IsValid() && v.Kind() == reflect.Struct { 172 | // save embedded struct for later processing 173 | embedded = append(embedded, v) 174 | continue 175 | } 176 | } 177 | 178 | name = sf.Name 179 | } 180 | 181 | if scope != "" { 182 | name = scope + "[" + name + "]" 183 | } 184 | 185 | if opts.Contains("omitempty") && isEmptyValue(sv) { 186 | continue 187 | } 188 | 189 | if sv.Type().Implements(encoderType) { 190 | // if sv is a nil pointer and the custom encoder is defined on a non-pointer 191 | // method receiver, set sv to the zero value of the underlying type 192 | if !reflect.Indirect(sv).IsValid() && sv.Type().Elem().Implements(encoderType) { 193 | sv = reflect.New(sv.Type().Elem()) 194 | } 195 | 196 | m := sv.Interface().(Encoder) 197 | if err := m.EncodeValues(name, &values); err != nil { 198 | return err 199 | } 200 | continue 201 | } 202 | 203 | // recursively dereference pointers. break on nil pointers 204 | for sv.Kind() == reflect.Ptr { 205 | if sv.IsNil() { 206 | break 207 | } 208 | sv = sv.Elem() 209 | } 210 | 211 | if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array { 212 | if sv.Len() == 0 { 213 | // skip if slice or array is empty 214 | continue 215 | } 216 | 217 | var del string 218 | if opts.Contains("comma") { 219 | del = "," 220 | } else if opts.Contains("space") { 221 | del = " " 222 | } else if opts.Contains("semicolon") { 223 | del = ";" 224 | } else if opts.Contains("brackets") { 225 | name = name + "[]" 226 | } else { 227 | del = sf.Tag.Get("del") 228 | } 229 | 230 | if del != "" { 231 | s := new(strings.Builder) 232 | first := true 233 | for i := 0; i < sv.Len(); i++ { 234 | if first { 235 | first = false 236 | } else { 237 | s.WriteString(del) 238 | } 239 | s.WriteString(valueString(sv.Index(i), opts, sf)) 240 | } 241 | values.Add(name, s.String()) 242 | } else { 243 | for i := 0; i < sv.Len(); i++ { 244 | k := name 245 | if opts.Contains("numbered") { 246 | k = fmt.Sprintf("%s%d", name, i) 247 | } 248 | values.Add(k, valueString(sv.Index(i), opts, sf)) 249 | } 250 | } 251 | continue 252 | } 253 | 254 | if sv.Type() == timeType { 255 | values.Add(name, valueString(sv, opts, sf)) 256 | continue 257 | } 258 | 259 | if sv.Kind() == reflect.Struct { 260 | if err := reflectValue(values, sv, name); err != nil { 261 | return err 262 | } 263 | continue 264 | } 265 | 266 | values.Add(name, valueString(sv, opts, sf)) 267 | } 268 | 269 | for _, f := range embedded { 270 | if err := reflectValue(values, f, scope); err != nil { 271 | return err 272 | } 273 | } 274 | 275 | return nil 276 | } 277 | 278 | // valueString returns the string representation of a value. 279 | func valueString(v reflect.Value, opts tagOptions, sf reflect.StructField) string { 280 | for v.Kind() == reflect.Ptr { 281 | if v.IsNil() { 282 | return "" 283 | } 284 | v = v.Elem() 285 | } 286 | 287 | if v.Kind() == reflect.Bool && opts.Contains("int") { 288 | if v.Bool() { 289 | return "1" 290 | } 291 | return "0" 292 | } 293 | 294 | if v.Type() == timeType { 295 | t := v.Interface().(time.Time) 296 | if opts.Contains("unix") { 297 | return strconv.FormatInt(t.Unix(), 10) 298 | } 299 | if opts.Contains("unixmilli") { 300 | return strconv.FormatInt((t.UnixNano() / 1e6), 10) 301 | } 302 | if opts.Contains("unixnano") { 303 | return strconv.FormatInt(t.UnixNano(), 10) 304 | } 305 | if layout := sf.Tag.Get("layout"); layout != "" { 306 | return t.Format(layout) 307 | } 308 | return t.Format(time.RFC3339) 309 | } 310 | 311 | return fmt.Sprint(v.Interface()) 312 | } 313 | 314 | // isEmptyValue checks if a value should be considered empty for the purposes 315 | // of omitting fields with the "omitempty" option. 316 | func isEmptyValue(v reflect.Value) bool { 317 | switch v.Kind() { 318 | case reflect.Array, reflect.Map, reflect.Slice, reflect.String: 319 | return v.Len() == 0 320 | case reflect.Bool: 321 | return !v.Bool() 322 | case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: 323 | return v.Int() == 0 324 | case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: 325 | return v.Uint() == 0 326 | case reflect.Float32, reflect.Float64: 327 | return v.Float() == 0 328 | case reflect.Interface, reflect.Ptr: 329 | return v.IsNil() 330 | } 331 | 332 | type zeroable interface { 333 | IsZero() bool 334 | } 335 | 336 | if z, ok := v.Interface().(zeroable); ok { 337 | return z.IsZero() 338 | } 339 | 340 | return false 341 | } 342 | 343 | // tagOptions is the string following a comma in a struct field's "url" tag, or 344 | // the empty string. It does not include the leading comma. 345 | type tagOptions []string 346 | 347 | // parseTag splits a struct field's url tag into its name and comma-separated 348 | // options. 349 | func parseTag(tag string) (string, tagOptions) { 350 | s := strings.Split(tag, ",") 351 | return s[0], s[1:] 352 | } 353 | 354 | // Contains checks whether the tagOptions contains the specified option. 355 | func (o tagOptions) Contains(option string) bool { 356 | for _, s := range o { 357 | if s == option { 358 | return true 359 | } 360 | } 361 | return false 362 | } 363 | -------------------------------------------------------------------------------- /query/encode_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013 The Go Authors. All rights reserved. 2 | // Use of this source code is governed by a BSD-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package query 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "net/url" 11 | "reflect" 12 | "testing" 13 | "time" 14 | 15 | "github.com/google/go-cmp/cmp" 16 | ) 17 | 18 | // test that Values(input) matches want. If not, report an error on t. 19 | func testValue(t *testing.T, input interface{}, want url.Values) { 20 | v, err := Values(input) 21 | if err != nil { 22 | t.Errorf("Values(%q) returned error: %v", input, err) 23 | } 24 | if diff := cmp.Diff(want, v); diff != "" { 25 | t.Errorf("Values(%#v) mismatch:\n%s", input, diff) 26 | } 27 | } 28 | 29 | func TestValues_BasicTypes(t *testing.T) { 30 | tests := []struct { 31 | input interface{} 32 | want url.Values 33 | }{ 34 | // zero values 35 | {struct{ V string }{}, url.Values{"V": {""}}}, 36 | {struct{ V int }{}, url.Values{"V": {"0"}}}, 37 | {struct{ V uint }{}, url.Values{"V": {"0"}}}, 38 | {struct{ V float32 }{}, url.Values{"V": {"0"}}}, 39 | {struct{ V bool }{}, url.Values{"V": {"false"}}}, 40 | 41 | // simple non-zero values 42 | {struct{ V string }{"v"}, url.Values{"V": {"v"}}}, 43 | {struct{ V int }{1}, url.Values{"V": {"1"}}}, 44 | {struct{ V uint }{1}, url.Values{"V": {"1"}}}, 45 | {struct{ V float32 }{0.1}, url.Values{"V": {"0.1"}}}, 46 | {struct{ V bool }{true}, url.Values{"V": {"true"}}}, 47 | 48 | // bool-specific options 49 | { 50 | struct { 51 | V bool `url:",int"` 52 | }{false}, 53 | url.Values{"V": {"0"}}, 54 | }, 55 | { 56 | struct { 57 | V bool `url:",int"` 58 | }{true}, 59 | url.Values{"V": {"1"}}, 60 | }, 61 | 62 | // time values 63 | { 64 | struct { 65 | V time.Time 66 | }{time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC)}, 67 | url.Values{"V": {"2000-01-01T12:34:56Z"}}, 68 | }, 69 | { 70 | struct { 71 | V time.Time `url:",unix"` 72 | }{time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC)}, 73 | url.Values{"V": {"946730096"}}, 74 | }, 75 | { 76 | struct { 77 | V time.Time `url:",unixmilli"` 78 | }{time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC)}, 79 | url.Values{"V": {"946730096000"}}, 80 | }, 81 | { 82 | struct { 83 | V time.Time `url:",unixnano"` 84 | }{time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC)}, 85 | url.Values{"V": {"946730096000000000"}}, 86 | }, 87 | { 88 | struct { 89 | V time.Time `layout:"2006-01-02"` 90 | }{time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC)}, 91 | url.Values{"V": {"2000-01-01"}}, 92 | }, 93 | } 94 | 95 | for _, tt := range tests { 96 | testValue(t, tt.input, tt.want) 97 | } 98 | } 99 | 100 | func TestValues_Pointers(t *testing.T) { 101 | str := "s" 102 | strPtr := &str 103 | 104 | tests := []struct { 105 | input interface{} 106 | want url.Values 107 | }{ 108 | // nil pointers (zero values) 109 | {struct{ V *string }{}, url.Values{"V": {""}}}, 110 | {struct{ V *int }{}, url.Values{"V": {""}}}, 111 | 112 | // non-zero pointer values 113 | {struct{ V *string }{&str}, url.Values{"V": {"s"}}}, 114 | {struct{ V **string }{&strPtr}, url.Values{"V": {"s"}}}, 115 | 116 | // slices of pointer values 117 | {struct{ V []*string }{}, url.Values{}}, 118 | {struct{ V []*string }{[]*string{&str, &str}}, url.Values{"V": {"s", "s"}}}, 119 | 120 | // pointer to slice 121 | {struct{ V *[]string }{}, url.Values{"V": {""}}}, 122 | {struct{ V *[]string }{&[]string{"a", "b"}}, url.Values{"V": {"a", "b"}}}, 123 | 124 | // pointer values for the input struct itself 125 | {(*struct{})(nil), url.Values{}}, 126 | {&struct{}{}, url.Values{}}, 127 | {&struct{ V string }{}, url.Values{"V": {""}}}, 128 | {&struct{ V string }{"v"}, url.Values{"V": {"v"}}}, 129 | } 130 | 131 | for _, tt := range tests { 132 | testValue(t, tt.input, tt.want) 133 | } 134 | } 135 | 136 | func TestValues_Slices(t *testing.T) { 137 | tests := []struct { 138 | input interface{} 139 | want url.Values 140 | }{ 141 | // slices of strings 142 | { 143 | struct{ V []string }{}, 144 | url.Values{}, 145 | }, 146 | { 147 | struct{ V []string }{[]string{}}, 148 | url.Values{}, 149 | }, 150 | { 151 | struct{ V []string }{[]string{""}}, 152 | url.Values{"V": {""}}, 153 | }, 154 | { 155 | struct{ V []string }{[]string{"a", "b"}}, 156 | url.Values{"V": {"a", "b"}}, 157 | }, 158 | { 159 | struct { 160 | V []string `url:",comma"` 161 | }{[]string{}}, 162 | url.Values{}, 163 | }, 164 | { 165 | struct { 166 | V []string `url:",comma"` 167 | }{[]string{""}}, 168 | url.Values{"V": {""}}, 169 | }, 170 | { 171 | struct { 172 | V []string `url:",comma"` 173 | }{[]string{"a", "b"}}, 174 | url.Values{"V": {"a,b"}}, 175 | }, 176 | { 177 | struct { 178 | V []string `url:",space"` 179 | }{[]string{"a", "b"}}, 180 | url.Values{"V": {"a b"}}, 181 | }, 182 | { 183 | struct { 184 | V []string `url:",semicolon"` 185 | }{[]string{"a", "b"}}, 186 | url.Values{"V": {"a;b"}}, 187 | }, 188 | { 189 | struct { 190 | V []string `url:",brackets"` 191 | }{[]string{"a", "b"}}, 192 | url.Values{"V[]": {"a", "b"}}, 193 | }, 194 | { 195 | struct { 196 | V []string `url:",numbered"` 197 | }{[]string{"a", "b"}}, 198 | url.Values{"V0": {"a"}, "V1": {"b"}}, 199 | }, 200 | 201 | // arrays of strings 202 | { 203 | struct{ V [2]string }{}, 204 | url.Values{"V": {"", ""}}, 205 | }, 206 | { 207 | struct{ V [2]string }{[2]string{"a", "b"}}, 208 | url.Values{"V": {"a", "b"}}, 209 | }, 210 | { 211 | struct { 212 | V [2]string `url:",comma"` 213 | }{[2]string{"a", "b"}}, 214 | url.Values{"V": {"a,b"}}, 215 | }, 216 | { 217 | struct { 218 | V [2]string `url:",space"` 219 | }{[2]string{"a", "b"}}, 220 | url.Values{"V": {"a b"}}, 221 | }, 222 | { 223 | struct { 224 | V [2]string `url:",semicolon"` 225 | }{[2]string{"a", "b"}}, 226 | url.Values{"V": {"a;b"}}, 227 | }, 228 | { 229 | struct { 230 | V [2]string `url:",brackets"` 231 | }{[2]string{"a", "b"}}, 232 | url.Values{"V[]": {"a", "b"}}, 233 | }, 234 | { 235 | struct { 236 | V [2]string `url:",numbered"` 237 | }{[2]string{"a", "b"}}, 238 | url.Values{"V0": {"a"}, "V1": {"b"}}, 239 | }, 240 | 241 | // custom delimiters 242 | { 243 | struct { 244 | V []string `del:","` 245 | }{[]string{"a", "b"}}, 246 | url.Values{"V": {"a,b"}}, 247 | }, 248 | { 249 | struct { 250 | V []string `del:"|"` 251 | }{[]string{"a", "b"}}, 252 | url.Values{"V": {"a|b"}}, 253 | }, 254 | { 255 | struct { 256 | V []string `del:"🥑"` 257 | }{[]string{"a", "b"}}, 258 | url.Values{"V": {"a🥑b"}}, 259 | }, 260 | 261 | // slice of bools with additional options 262 | { 263 | struct { 264 | V []bool `url:",space,int"` 265 | }{[]bool{true, false}}, 266 | url.Values{"V": {"1 0"}}, 267 | }, 268 | } 269 | 270 | for _, tt := range tests { 271 | testValue(t, tt.input, tt.want) 272 | } 273 | } 274 | 275 | func TestValues_NestedTypes(t *testing.T) { 276 | type SubNested struct { 277 | Value string `url:"value"` 278 | } 279 | 280 | type Nested struct { 281 | A SubNested `url:"a"` 282 | B *SubNested `url:"b"` 283 | Ptr *SubNested `url:"ptr,omitempty"` 284 | } 285 | 286 | tests := []struct { 287 | input interface{} 288 | want url.Values 289 | }{ 290 | { 291 | struct { 292 | Nest Nested `url:"nest"` 293 | }{ 294 | Nested{ 295 | A: SubNested{ 296 | Value: "v", 297 | }, 298 | }, 299 | }, 300 | url.Values{ 301 | "nest[a][value]": {"v"}, 302 | "nest[b]": {""}, 303 | }, 304 | }, 305 | { 306 | struct { 307 | Nest Nested `url:"nest"` 308 | }{ 309 | Nested{ 310 | Ptr: &SubNested{ 311 | Value: "v", 312 | }, 313 | }, 314 | }, 315 | url.Values{ 316 | "nest[a][value]": {""}, 317 | "nest[b]": {""}, 318 | "nest[ptr][value]": {"v"}, 319 | }, 320 | }, 321 | { 322 | nil, 323 | url.Values{}, 324 | }, 325 | } 326 | 327 | for _, tt := range tests { 328 | testValue(t, tt.input, tt.want) 329 | } 330 | } 331 | 332 | func TestValues_OmitEmpty(t *testing.T) { 333 | str := "" 334 | 335 | tests := []struct { 336 | input interface{} 337 | want url.Values 338 | }{ 339 | {struct{ v string }{}, url.Values{}}, // non-exported field 340 | { 341 | struct { 342 | V string `url:",omitempty"` 343 | }{}, 344 | url.Values{}, 345 | }, 346 | { 347 | struct { 348 | V string `url:"-"` 349 | }{}, 350 | url.Values{}, 351 | }, 352 | { 353 | struct { 354 | V string `url:"omitempty"` // actually named omitempty 355 | }{}, 356 | url.Values{"omitempty": {""}}, 357 | }, 358 | { 359 | // include value for a non-nil pointer to an empty value 360 | struct { 361 | V *string `url:",omitempty"` 362 | }{&str}, 363 | url.Values{"V": {""}}, 364 | }, 365 | } 366 | 367 | for _, tt := range tests { 368 | testValue(t, tt.input, tt.want) 369 | } 370 | } 371 | 372 | func TestValues_EmbeddedStructs(t *testing.T) { 373 | type Inner struct { 374 | V string 375 | } 376 | type Outer struct { 377 | Inner 378 | } 379 | type OuterPtr struct { 380 | *Inner 381 | } 382 | type Mixed struct { 383 | Inner 384 | V string 385 | } 386 | type unexported struct { 387 | Inner 388 | V string 389 | } 390 | type Exported struct { 391 | unexported 392 | } 393 | 394 | tests := []struct { 395 | input interface{} 396 | want url.Values 397 | }{ 398 | { 399 | Outer{Inner{V: "a"}}, 400 | url.Values{"V": {"a"}}, 401 | }, 402 | { 403 | OuterPtr{&Inner{V: "a"}}, 404 | url.Values{"V": {"a"}}, 405 | }, 406 | { 407 | Mixed{Inner: Inner{V: "a"}, V: "b"}, 408 | url.Values{"V": {"b", "a"}}, 409 | }, 410 | { 411 | // values from unexported embed are still included 412 | Exported{ 413 | unexported{ 414 | Inner: Inner{V: "bar"}, 415 | V: "foo", 416 | }, 417 | }, 418 | url.Values{"V": {"foo", "bar"}}, 419 | }, 420 | } 421 | 422 | for _, tt := range tests { 423 | testValue(t, tt.input, tt.want) 424 | } 425 | } 426 | 427 | func TestValues_InvalidInput(t *testing.T) { 428 | _, err := Values("") 429 | if err == nil { 430 | t.Errorf("expected Values() to return an error on invalid input") 431 | } 432 | } 433 | 434 | // customEncodedStrings is a slice of strings with a custom URL encoding 435 | type customEncodedStrings []string 436 | 437 | // EncodeValues using key name of the form "{key}.N" where N increments with 438 | // each value. A value of "err" will return an error. 439 | func (m customEncodedStrings) EncodeValues(key string, v *url.Values) error { 440 | for i, arg := range m { 441 | if arg == "err" { 442 | return errors.New("encoding error") 443 | } 444 | v.Set(fmt.Sprintf("%s.%d", key, i), arg) 445 | } 446 | return nil 447 | } 448 | 449 | func TestValues_CustomEncodingSlice(t *testing.T) { 450 | tests := []struct { 451 | input interface{} 452 | want url.Values 453 | }{ 454 | { 455 | struct { 456 | V customEncodedStrings `url:"v"` 457 | }{}, 458 | url.Values{}, 459 | }, 460 | { 461 | struct { 462 | V customEncodedStrings `url:"v"` 463 | }{[]string{"a", "b"}}, 464 | url.Values{"v.0": {"a"}, "v.1": {"b"}}, 465 | }, 466 | 467 | // pointers to custom encoded types 468 | { 469 | struct { 470 | V *customEncodedStrings `url:"v"` 471 | }{}, 472 | url.Values{}, 473 | }, 474 | { 475 | struct { 476 | V *customEncodedStrings `url:"v"` 477 | }{(*customEncodedStrings)(&[]string{"a", "b"})}, 478 | url.Values{"v.0": {"a"}, "v.1": {"b"}}, 479 | }, 480 | } 481 | 482 | for _, tt := range tests { 483 | testValue(t, tt.input, tt.want) 484 | } 485 | } 486 | 487 | // One of the few ways reflectValues will return an error is if a custom 488 | // encoder returns an error. Test all of the various ways that can happen. 489 | func TestValues_CustomEncoding_Error(t *testing.T) { 490 | type st struct { 491 | V customEncodedStrings 492 | } 493 | tests := []struct { 494 | input interface{} 495 | }{ 496 | { 497 | st{[]string{"err"}}, 498 | }, 499 | { // struct field 500 | struct{ S st }{st{[]string{"err"}}}, 501 | }, 502 | { // embedded struct 503 | struct{ st }{st{[]string{"err"}}}, 504 | }, 505 | } 506 | for _, tt := range tests { 507 | _, err := Values(tt.input) 508 | if err == nil { 509 | t.Errorf("Values(%q) did not return expected encoding error", tt.input) 510 | } 511 | } 512 | } 513 | 514 | // customEncodedInt is an int with a custom URL encoding 515 | type customEncodedInt int 516 | 517 | // EncodeValues encodes values with leading underscores 518 | func (m customEncodedInt) EncodeValues(key string, v *url.Values) error { 519 | v.Set(key, fmt.Sprintf("_%d", m)) 520 | return nil 521 | } 522 | 523 | func TestValues_CustomEncodingInt(t *testing.T) { 524 | var zero customEncodedInt = 0 525 | var one customEncodedInt = 1 526 | tests := []struct { 527 | input interface{} 528 | want url.Values 529 | }{ 530 | { 531 | struct { 532 | V customEncodedInt `url:"v"` 533 | }{}, 534 | url.Values{"v": {"_0"}}, 535 | }, 536 | { 537 | struct { 538 | V customEncodedInt `url:"v,omitempty"` 539 | }{zero}, 540 | url.Values{}, 541 | }, 542 | { 543 | struct { 544 | V customEncodedInt `url:"v"` 545 | }{one}, 546 | url.Values{"v": {"_1"}}, 547 | }, 548 | 549 | // pointers to custom encoded types 550 | { 551 | struct { 552 | V *customEncodedInt `url:"v"` 553 | }{}, 554 | url.Values{"v": {"_0"}}, 555 | }, 556 | { 557 | struct { 558 | V *customEncodedInt `url:"v,omitempty"` 559 | }{}, 560 | url.Values{}, 561 | }, 562 | { 563 | struct { 564 | V *customEncodedInt `url:"v,omitempty"` 565 | }{&zero}, 566 | url.Values{"v": {"_0"}}, 567 | }, 568 | { 569 | struct { 570 | V *customEncodedInt `url:"v"` 571 | }{&one}, 572 | url.Values{"v": {"_1"}}, 573 | }, 574 | } 575 | 576 | for _, tt := range tests { 577 | testValue(t, tt.input, tt.want) 578 | } 579 | } 580 | 581 | // customEncodedInt is an int with a custom URL encoding defined on its pointer 582 | // value. 583 | type customEncodedIntPtr int 584 | 585 | // EncodeValues encodes a 0 as false, 1 as true, and nil as unknown. All other 586 | // values cause an error. 587 | func (m *customEncodedIntPtr) EncodeValues(key string, v *url.Values) error { 588 | if m == nil { 589 | v.Set(key, "undefined") 590 | } else { 591 | v.Set(key, fmt.Sprintf("_%d", *m)) 592 | } 593 | return nil 594 | } 595 | 596 | // Test behavior when encoding is defined for a pointer of a custom type. 597 | // Custom type should be able to encode values for nil pointers. 598 | func TestValues_CustomEncodingPointer(t *testing.T) { 599 | var zero customEncodedIntPtr = 0 600 | var one customEncodedIntPtr = 1 601 | tests := []struct { 602 | input interface{} 603 | want url.Values 604 | }{ 605 | // non-pointer values do not get the custom encoding because 606 | // they don't implement the encoder interface. 607 | { 608 | struct { 609 | V customEncodedIntPtr `url:"v"` 610 | }{}, 611 | url.Values{"v": {"0"}}, 612 | }, 613 | { 614 | struct { 615 | V customEncodedIntPtr `url:"v,omitempty"` 616 | }{}, 617 | url.Values{}, 618 | }, 619 | { 620 | struct { 621 | V customEncodedIntPtr `url:"v"` 622 | }{one}, 623 | url.Values{"v": {"1"}}, 624 | }, 625 | 626 | // pointers to custom encoded types. 627 | { 628 | struct { 629 | V *customEncodedIntPtr `url:"v"` 630 | }{}, 631 | url.Values{"v": {"undefined"}}, 632 | }, 633 | { 634 | struct { 635 | V *customEncodedIntPtr `url:"v,omitempty"` 636 | }{}, 637 | url.Values{}, 638 | }, 639 | { 640 | struct { 641 | V *customEncodedIntPtr `url:"v"` 642 | }{&zero}, 643 | url.Values{"v": {"_0"}}, 644 | }, 645 | { 646 | struct { 647 | V *customEncodedIntPtr `url:"v,omitempty"` 648 | }{&zero}, 649 | url.Values{"v": {"_0"}}, 650 | }, 651 | { 652 | struct { 653 | V *customEncodedIntPtr `url:"v"` 654 | }{&one}, 655 | url.Values{"v": {"_1"}}, 656 | }, 657 | } 658 | 659 | for _, tt := range tests { 660 | testValue(t, tt.input, tt.want) 661 | } 662 | } 663 | 664 | func TestIsEmptyValue(t *testing.T) { 665 | str := "string" 666 | tests := []struct { 667 | value interface{} 668 | empty bool 669 | }{ 670 | // slices, arrays, and maps 671 | {[]int{}, true}, 672 | {[]int{0}, false}, 673 | {[0]int{}, true}, 674 | {[3]int{}, false}, 675 | {[3]int{1}, false}, 676 | {map[string]string{}, true}, 677 | {map[string]string{"a": "b"}, false}, 678 | 679 | // strings 680 | {"", true}, 681 | {" ", false}, 682 | {"a", false}, 683 | 684 | // bool 685 | {true, false}, 686 | {false, true}, 687 | 688 | // ints of various types 689 | {(int)(0), true}, {(int)(1), false}, {(int)(-1), false}, 690 | {(int8)(0), true}, {(int8)(1), false}, {(int8)(-1), false}, 691 | {(int16)(0), true}, {(int16)(1), false}, {(int16)(-1), false}, 692 | {(int32)(0), true}, {(int32)(1), false}, {(int32)(-1), false}, 693 | {(int64)(0), true}, {(int64)(1), false}, {(int64)(-1), false}, 694 | {(uint)(0), true}, {(uint)(1), false}, 695 | {(uint8)(0), true}, {(uint8)(1), false}, 696 | {(uint16)(0), true}, {(uint16)(1), false}, 697 | {(uint32)(0), true}, {(uint32)(1), false}, 698 | {(uint64)(0), true}, {(uint64)(1), false}, 699 | 700 | // floats 701 | {(float32)(0), true}, {(float32)(0.0), true}, {(float32)(0.1), false}, 702 | {(float64)(0), true}, {(float64)(0.0), true}, {(float64)(0.1), false}, 703 | 704 | // pointers 705 | {(*int)(nil), true}, 706 | {new([]int), false}, 707 | {&str, false}, 708 | 709 | // time 710 | {time.Time{}, true}, 711 | {time.Now(), false}, 712 | 713 | // unknown type - always false unless a nil pointer, which are always empty. 714 | {(*struct{ int })(nil), true}, 715 | {struct{ int }{}, false}, 716 | {struct{ int }{0}, false}, 717 | {struct{ int }{1}, false}, 718 | } 719 | 720 | for _, tt := range tests { 721 | got := isEmptyValue(reflect.ValueOf(tt.value)) 722 | want := tt.empty 723 | if got != want { 724 | t.Errorf("isEmptyValue(%v) returned %t; want %t", tt.value, got, want) 725 | } 726 | } 727 | } 728 | 729 | func TestParseTag(t *testing.T) { 730 | name, opts := parseTag("field,foobar,foo") 731 | if name != "field" { 732 | t.Fatalf("name = %q, want field", name) 733 | } 734 | for _, tt := range []struct { 735 | opt string 736 | want bool 737 | }{ 738 | {"foobar", true}, 739 | {"foo", true}, 740 | {"bar", false}, 741 | {"field", false}, 742 | } { 743 | if opts.Contains(tt.opt) != tt.want { 744 | t.Errorf("Contains(%q) = %v", tt.opt, !tt.want) 745 | } 746 | } 747 | } 748 | --------------------------------------------------------------------------------