pax_global_header00006660000000000000000000000064140222454240014511gustar00rootroot0000000000000052 comment=1f4a1f9d6e29d314b2513651973fca5c4d4498f1 go-querystring-1.1.0/000077500000000000000000000000001402224542400145075ustar00rootroot00000000000000go-querystring-1.1.0/.github/000077500000000000000000000000001402224542400160475ustar00rootroot00000000000000go-querystring-1.1.0/.github/workflows/000077500000000000000000000000001402224542400201045ustar00rootroot00000000000000go-querystring-1.1.0/.github/workflows/linter.yml000066400000000000000000000005101402224542400221200ustar00rootroot00000000000000on: [push, pull_request] name: linter jobs: lint: strategy: matrix: go-version: [1.x] platform: [ubuntu-latest] runs-on: ${{ matrix.platform }} steps: - uses: actions/checkout@v2 - name: golangci-lint uses: golangci/golangci-lint-action@v2 with: version: v1.32 go-querystring-1.1.0/.github/workflows/tests.yml000066400000000000000000000022201402224542400217650ustar00rootroot00000000000000on: push: branches: - master pull_request: branches: - master name: tests jobs: test: strategy: matrix: go-version: [1.x, 1.15.x] platform: [ubuntu-latest] include: # only update test coverage stats with most recent go version on linux - go-version: 1.x platform: ubuntu-latest update-coverage: true runs-on: ${{ matrix.platform }} steps: - name: Cancel previous workflow runs uses: styfle/cancel-workflow-action@0.8.0 with: access_token: ${{ github.token }} - uses: actions/checkout@v2 - uses: actions/setup-go@v2 with: go-version: ${{ matrix.go-version }} - run: go version - name: Cache go modules uses: actions/cache@v2 with: path: ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: ${{ runner.os }}-go- - name: Run go test run: go test -v -race -coverprofile coverage.txt -covermode atomic ./... - name: Upload coverage to Codecov if: ${{ matrix.update-coverage }} uses: codecov/codecov-action@v1 go-querystring-1.1.0/.gitignore000066400000000000000000000000071402224542400164740ustar00rootroot00000000000000*.test go-querystring-1.1.0/.golangci.yml000066400000000000000000000002621402224542400170730ustar00rootroot00000000000000linters: enable: - dogsled - dupl - gofmt - goimports - gosec - misspell - nakedret - stylecheck - unconvert - unparam - whitespace go-querystring-1.1.0/CONTRIBUTING.md000066400000000000000000000061521402224542400167440ustar00rootroot00000000000000# How to contribute # We'd love to accept your patches and contributions to this project. There are a just a few small guidelines you need to follow. ## Contributor License Agreement ## Contributions to any Google project must be accompanied by a Contributor License Agreement. This is not a copyright **assignment**, it simply gives Google permission to use and redistribute your contributions as part of the project. * If you are an individual writing original source code and you're sure you own the intellectual property, then you'll need to sign an [individual CLA][]. * If you work for a company that wants to allow you to contribute your work, then you'll need to sign a [corporate CLA][]. You generally only need to submit a CLA once, so if you've already submitted one (even if it was for a different project), you probably don't need to do it again. [individual CLA]: https://developers.google.com/open-source/cla/individual [corporate CLA]: https://developers.google.com/open-source/cla/corporate ## Submitting a patch ## 1. It's generally best to start by opening a new issue describing the bug or feature you're intending to fix. Even if you think it's relatively minor, it's helpful to know what people are working on. Mention in the initial issue that you are planning to work on that bug or feature so that it can be assigned to you. 1. Follow the normal process of [forking][] the project, and setup a new branch to work in. It's important that each group of changes be done in separate branches in order to ensure that a pull request only includes the commits related to that bug or feature. 1. Go makes it very simple to ensure properly formatted code, so always run `go fmt` on your code before committing it. You should also run [golint][] over your code. As noted in the [golint readme][], it's not strictly necessary that your code be completely "lint-free", but this will help you find common style issues. 1. Any significant changes should almost always be accompanied by tests. The project already has good test coverage, so look at some of the existing tests if you're unsure how to go about it. [gocov][] and [gocov-html][] are invaluable tools for seeing which parts of your code aren't being exercised by your tests. 1. Do your best to have [well-formed commit messages][] for each change. This provides consistency throughout the project, and ensures that commit messages are able to be formatted properly by various git tools. 1. Finally, push the commits to your fork and submit a [pull request][]. [forking]: https://help.github.com/articles/fork-a-repo [golint]: https://github.com/golang/lint [golint readme]: https://github.com/golang/lint/blob/master/README [gocov]: https://github.com/axw/gocov [gocov-html]: https://github.com/matm/gocov-html [well-formed commit messages]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html [squash]: http://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits [pull request]: https://help.github.com/articles/creating-a-pull-request go-querystring-1.1.0/LICENSE000066400000000000000000000026771402224542400155300ustar00rootroot00000000000000Copyright (c) 2013 Google. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name of Google Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. go-querystring-1.1.0/README.md000066400000000000000000000033041402224542400157660ustar00rootroot00000000000000# go-querystring # [![Go Reference](https://pkg.go.dev/badge/github.com/google/go-querystring/query.svg)](https://pkg.go.dev/github.com/google/go-querystring/query) [![Test Status](https://github.com/google/go-querystring/workflows/tests/badge.svg)](https://github.com/google/go-querystring/actions?query=workflow%3Atests) [![Test Coverage](https://codecov.io/gh/google/go-querystring/branch/master/graph/badge.svg)](https://codecov.io/gh/google/go-querystring) go-querystring is a Go library for encoding structs into URL query parameters. ## Usage ## ```go import "github.com/google/go-querystring/query" ``` go-querystring is designed to assist in scenarios where you want to construct a URL using a struct that represents the URL query parameters. You might do this to enforce the type safety of your parameters, for example, as is done in the [go-github][] library. The query package exports a single `Values()` function. A simple example: ```go type Options struct { Query string `url:"q"` ShowAll bool `url:"all"` Page int `url:"page"` } opt := Options{ "foo", true, 2 } v, _ := query.Values(opt) fmt.Print(v.Encode()) // will output: "q=foo&all=true&page=2" ``` See the [package godocs][] for complete documentation on supported types and formatting options. [go-github]: https://github.com/google/go-github/commit/994f6f8405f052a117d2d0b500054341048fbb08 [package godocs]: https://pkg.go.dev/github.com/google/go-querystring/query ## Alternatives ## If you are looking for a library that can both encode and decode query strings, you might consider one of these alternatives: - https://github.com/gorilla/schema - https://github.com/pasztorpisti/qs - https://github.com/hetiansu5/urlquery go-querystring-1.1.0/go.mod000066400000000000000000000001321402224542400156110ustar00rootroot00000000000000module github.com/google/go-querystring go 1.10 require github.com/google/go-cmp v0.5.2 go-querystring-1.1.0/go.sum000066400000000000000000000004261402224542400156440ustar00rootroot00000000000000github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= go-querystring-1.1.0/query/000077500000000000000000000000001402224542400156545ustar00rootroot00000000000000go-querystring-1.1.0/query/encode.go000066400000000000000000000236631402224542400174520ustar00rootroot00000000000000// Copyright 2013 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // Package query implements encoding of structs into URL query parameters. // // As a simple example: // // type Options struct { // Query string `url:"q"` // ShowAll bool `url:"all"` // Page int `url:"page"` // } // // opt := Options{ "foo", true, 2 } // v, _ := query.Values(opt) // fmt.Print(v.Encode()) // will output: "q=foo&all=true&page=2" // // The exact mapping between Go values and url.Values is described in the // documentation for the Values() function. package query import ( "bytes" "fmt" "net/url" "reflect" "strconv" "strings" "time" ) var timeType = reflect.TypeOf(time.Time{}) var encoderType = reflect.TypeOf(new(Encoder)).Elem() // Encoder is an interface implemented by any type that wishes to encode // itself into URL values in a non-standard way. type Encoder interface { EncodeValues(key string, v *url.Values) error } // Values returns the url.Values encoding of v. // // Values expects to be passed a struct, and traverses it recursively using the // following encoding rules. // // Each exported struct field is encoded as a URL parameter unless // // - the field's tag is "-", or // - the field is empty and its tag specifies the "omitempty" option // // The empty values are false, 0, any nil pointer or interface value, any array // slice, map, or string of length zero, and any type (such as time.Time) that // returns true for IsZero(). // // The URL parameter name defaults to the struct field name but can be // specified in the struct field's tag value. The "url" key in the struct // field's tag value is the key name, followed by an optional comma and // options. For example: // // // Field is ignored by this package. // Field int `url:"-"` // // // Field appears as URL parameter "myName". // Field int `url:"myName"` // // // Field appears as URL parameter "myName" and the field is omitted if // // its value is empty // Field int `url:"myName,omitempty"` // // // Field appears as URL parameter "Field" (the default), but the field // // is skipped if empty. Note the leading comma. // Field int `url:",omitempty"` // // For encoding individual field values, the following type-dependent rules // apply: // // Boolean values default to encoding as the strings "true" or "false". // Including the "int" option signals that the field should be encoded as the // strings "1" or "0". // // time.Time values default to encoding as RFC3339 timestamps. Including the // "unix" option signals that the field should be encoded as a Unix time (see // time.Unix()). The "unixmilli" and "unixnano" options will encode the number // of milliseconds and nanoseconds, respectively, since January 1, 1970 (see // time.UnixNano()). Including the "layout" struct tag (separate from the // "url" tag) will use the value of the "layout" tag as a layout passed to // time.Format. For example: // // // Encode a time.Time as YYYY-MM-DD // Field time.Time `layout:"2006-01-02"` // // Slice and Array values default to encoding as multiple URL values of the // same name. Including the "comma" option signals that the field should be // encoded as a single comma-delimited value. Including the "space" option // similarly encodes the value as a single space-delimited string. Including // the "semicolon" option will encode the value as a semicolon-delimited string. // Including the "brackets" option signals that the multiple URL values should // have "[]" appended to the value name. "numbered" will append a number to // the end of each incidence of the value name, example: // name0=value0&name1=value1, etc. Including the "del" struct tag (separate // from the "url" tag) will use the value of the "del" tag as the delimiter. // For example: // // // Encode a slice of bools as ints ("1" for true, "0" for false), // // separated by exclamation points "!". // Field []bool `url:",int" del:"!"` // // Anonymous struct fields are usually encoded as if their inner exported // fields were fields in the outer struct, subject to the standard Go // visibility rules. An anonymous struct field with a name given in its URL // tag is treated as having that name, rather than being anonymous. // // Non-nil pointer values are encoded as the value pointed to. // // Nested structs are encoded including parent fields in value names for // scoping. e.g: // // "user[name]=acme&user[addr][postcode]=1234&user[addr][city]=SFO" // // All other values are encoded using their default string representation. // // Multiple fields that encode to the same URL parameter name will be included // as multiple URL values of the same name. func Values(v interface{}) (url.Values, error) { values := make(url.Values) val := reflect.ValueOf(v) for val.Kind() == reflect.Ptr { if val.IsNil() { return values, nil } val = val.Elem() } if v == nil { return values, nil } if val.Kind() != reflect.Struct { return nil, fmt.Errorf("query: Values() expects struct input. Got %v", val.Kind()) } err := reflectValue(values, val, "") return values, err } // reflectValue populates the values parameter from the struct fields in val. // Embedded structs are followed recursively (using the rules defined in the // Values function documentation) breadth-first. func reflectValue(values url.Values, val reflect.Value, scope string) error { var embedded []reflect.Value typ := val.Type() for i := 0; i < typ.NumField(); i++ { sf := typ.Field(i) if sf.PkgPath != "" && !sf.Anonymous { // unexported continue } sv := val.Field(i) tag := sf.Tag.Get("url") if tag == "-" { continue } name, opts := parseTag(tag) if name == "" { if sf.Anonymous { v := reflect.Indirect(sv) if v.IsValid() && v.Kind() == reflect.Struct { // save embedded struct for later processing embedded = append(embedded, v) continue } } name = sf.Name } if scope != "" { name = scope + "[" + name + "]" } if opts.Contains("omitempty") && isEmptyValue(sv) { continue } if sv.Type().Implements(encoderType) { // if sv is a nil pointer and the custom encoder is defined on a non-pointer // method receiver, set sv to the zero value of the underlying type if !reflect.Indirect(sv).IsValid() && sv.Type().Elem().Implements(encoderType) { sv = reflect.New(sv.Type().Elem()) } m := sv.Interface().(Encoder) if err := m.EncodeValues(name, &values); err != nil { return err } continue } // recursively dereference pointers. break on nil pointers for sv.Kind() == reflect.Ptr { if sv.IsNil() { break } sv = sv.Elem() } if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array { var del string if opts.Contains("comma") { del = "," } else if opts.Contains("space") { del = " " } else if opts.Contains("semicolon") { del = ";" } else if opts.Contains("brackets") { name = name + "[]" } else { del = sf.Tag.Get("del") } if del != "" { s := new(bytes.Buffer) first := true for i := 0; i < sv.Len(); i++ { if first { first = false } else { s.WriteString(del) } s.WriteString(valueString(sv.Index(i), opts, sf)) } values.Add(name, s.String()) } else { for i := 0; i < sv.Len(); i++ { k := name if opts.Contains("numbered") { k = fmt.Sprintf("%s%d", name, i) } values.Add(k, valueString(sv.Index(i), opts, sf)) } } continue } if sv.Type() == timeType { values.Add(name, valueString(sv, opts, sf)) continue } if sv.Kind() == reflect.Struct { if err := reflectValue(values, sv, name); err != nil { return err } continue } values.Add(name, valueString(sv, opts, sf)) } for _, f := range embedded { if err := reflectValue(values, f, scope); err != nil { return err } } return nil } // valueString returns the string representation of a value. func valueString(v reflect.Value, opts tagOptions, sf reflect.StructField) string { for v.Kind() == reflect.Ptr { if v.IsNil() { return "" } v = v.Elem() } if v.Kind() == reflect.Bool && opts.Contains("int") { if v.Bool() { return "1" } return "0" } if v.Type() == timeType { t := v.Interface().(time.Time) if opts.Contains("unix") { return strconv.FormatInt(t.Unix(), 10) } if opts.Contains("unixmilli") { return strconv.FormatInt((t.UnixNano() / 1e6), 10) } if opts.Contains("unixnano") { return strconv.FormatInt(t.UnixNano(), 10) } if layout := sf.Tag.Get("layout"); layout != "" { return t.Format(layout) } return t.Format(time.RFC3339) } return fmt.Sprint(v.Interface()) } // isEmptyValue checks if a value should be considered empty for the purposes // of omitting fields with the "omitempty" option. func isEmptyValue(v reflect.Value) bool { switch v.Kind() { case reflect.Array, reflect.Map, reflect.Slice, reflect.String: return v.Len() == 0 case reflect.Bool: return !v.Bool() case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: return v.Int() == 0 case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: return v.Uint() == 0 case reflect.Float32, reflect.Float64: return v.Float() == 0 case reflect.Interface, reflect.Ptr: return v.IsNil() } type zeroable interface { IsZero() bool } if z, ok := v.Interface().(zeroable); ok { return z.IsZero() } return false } // tagOptions is the string following a comma in a struct field's "url" tag, or // the empty string. It does not include the leading comma. type tagOptions []string // parseTag splits a struct field's url tag into its name and comma-separated // options. func parseTag(tag string) (string, tagOptions) { s := strings.Split(tag, ",") return s[0], s[1:] } // Contains checks whether the tagOptions contains the specified option. func (o tagOptions) Contains(option string) bool { for _, s := range o { if s == option { return true } } return false } go-querystring-1.1.0/query/encode_test.go000066400000000000000000000345341402224542400205100ustar00rootroot00000000000000// Copyright 2013 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package query import ( "errors" "fmt" "net/url" "reflect" "testing" "time" "github.com/google/go-cmp/cmp" ) // test that Values(input) matches want. If not, report an error on t. func testValue(t *testing.T, input interface{}, want url.Values) { v, err := Values(input) if err != nil { t.Errorf("Values(%q) returned error: %v", input, err) } if diff := cmp.Diff(want, v); diff != "" { t.Errorf("Values(%#v) mismatch:\n%s", input, diff) } } func TestValues_BasicTypes(t *testing.T) { tests := []struct { input interface{} want url.Values }{ // zero values {struct{ V string }{}, url.Values{"V": {""}}}, {struct{ V int }{}, url.Values{"V": {"0"}}}, {struct{ V uint }{}, url.Values{"V": {"0"}}}, {struct{ V float32 }{}, url.Values{"V": {"0"}}}, {struct{ V bool }{}, url.Values{"V": {"false"}}}, // simple non-zero values {struct{ V string }{"v"}, url.Values{"V": {"v"}}}, {struct{ V int }{1}, url.Values{"V": {"1"}}}, {struct{ V uint }{1}, url.Values{"V": {"1"}}}, {struct{ V float32 }{0.1}, url.Values{"V": {"0.1"}}}, {struct{ V bool }{true}, url.Values{"V": {"true"}}}, // bool-specific options { struct { V bool `url:",int"` }{false}, url.Values{"V": {"0"}}, }, { struct { V bool `url:",int"` }{true}, url.Values{"V": {"1"}}, }, // time values { struct { V time.Time }{time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC)}, url.Values{"V": {"2000-01-01T12:34:56Z"}}, }, { struct { V time.Time `url:",unix"` }{time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC)}, url.Values{"V": {"946730096"}}, }, { struct { V time.Time `url:",unixmilli"` }{time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC)}, url.Values{"V": {"946730096000"}}, }, { struct { V time.Time `url:",unixnano"` }{time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC)}, url.Values{"V": {"946730096000000000"}}, }, { struct { V time.Time `layout:"2006-01-02"` }{time.Date(2000, 1, 1, 12, 34, 56, 0, time.UTC)}, url.Values{"V": {"2000-01-01"}}, }, } for _, tt := range tests { testValue(t, tt.input, tt.want) } } func TestValues_Pointers(t *testing.T) { str := "s" strPtr := &str tests := []struct { input interface{} want url.Values }{ // nil pointers (zero values) {struct{ V *string }{}, url.Values{"V": {""}}}, {struct{ V *int }{}, url.Values{"V": {""}}}, // non-zero pointer values {struct{ V *string }{&str}, url.Values{"V": {"s"}}}, {struct{ V **string }{&strPtr}, url.Values{"V": {"s"}}}, // slices of pointer values {struct{ V []*string }{}, url.Values{}}, {struct{ V []*string }{[]*string{&str, &str}}, url.Values{"V": {"s", "s"}}}, // pointer to slice {struct{ V *[]string }{}, url.Values{"V": {""}}}, {struct{ V *[]string }{&[]string{"a", "b"}}, url.Values{"V": {"a", "b"}}}, // pointer values for the input struct itself {(*struct{})(nil), url.Values{}}, {&struct{}{}, url.Values{}}, {&struct{ V string }{}, url.Values{"V": {""}}}, {&struct{ V string }{"v"}, url.Values{"V": {"v"}}}, } for _, tt := range tests { testValue(t, tt.input, tt.want) } } func TestValues_Slices(t *testing.T) { tests := []struct { input interface{} want url.Values }{ // slices of strings { struct{ V []string }{}, url.Values{}, }, { struct{ V []string }{[]string{"a", "b"}}, url.Values{"V": {"a", "b"}}, }, { struct { V []string `url:",comma"` }{[]string{"a", "b"}}, url.Values{"V": {"a,b"}}, }, { struct { V []string `url:",space"` }{[]string{"a", "b"}}, url.Values{"V": {"a b"}}, }, { struct { V []string `url:",semicolon"` }{[]string{"a", "b"}}, url.Values{"V": {"a;b"}}, }, { struct { V []string `url:",brackets"` }{[]string{"a", "b"}}, url.Values{"V[]": {"a", "b"}}, }, { struct { V []string `url:",numbered"` }{[]string{"a", "b"}}, url.Values{"V0": {"a"}, "V1": {"b"}}, }, // arrays of strings { struct{ V [2]string }{}, url.Values{"V": {"", ""}}, }, { struct{ V [2]string }{[2]string{"a", "b"}}, url.Values{"V": {"a", "b"}}, }, { struct { V [2]string `url:",comma"` }{[2]string{"a", "b"}}, url.Values{"V": {"a,b"}}, }, { struct { V [2]string `url:",space"` }{[2]string{"a", "b"}}, url.Values{"V": {"a b"}}, }, { struct { V [2]string `url:",semicolon"` }{[2]string{"a", "b"}}, url.Values{"V": {"a;b"}}, }, { struct { V [2]string `url:",brackets"` }{[2]string{"a", "b"}}, url.Values{"V[]": {"a", "b"}}, }, { struct { V [2]string `url:",numbered"` }{[2]string{"a", "b"}}, url.Values{"V0": {"a"}, "V1": {"b"}}, }, // custom delimiters { struct { V []string `del:","` }{[]string{"a", "b"}}, url.Values{"V": {"a,b"}}, }, { struct { V []string `del:"|"` }{[]string{"a", "b"}}, url.Values{"V": {"a|b"}}, }, { struct { V []string `del:"🥑"` }{[]string{"a", "b"}}, url.Values{"V": {"a🥑b"}}, }, // slice of bools with additional options { struct { V []bool `url:",space,int"` }{[]bool{true, false}}, url.Values{"V": {"1 0"}}, }, } for _, tt := range tests { testValue(t, tt.input, tt.want) } } func TestValues_NestedTypes(t *testing.T) { type SubNested struct { Value string `url:"value"` } type Nested struct { A SubNested `url:"a"` B *SubNested `url:"b"` Ptr *SubNested `url:"ptr,omitempty"` } tests := []struct { input interface{} want url.Values }{ { struct { Nest Nested `url:"nest"` }{ Nested{ A: SubNested{ Value: "v", }, }, }, url.Values{ "nest[a][value]": {"v"}, "nest[b]": {""}, }, }, { struct { Nest Nested `url:"nest"` }{ Nested{ Ptr: &SubNested{ Value: "v", }, }, }, url.Values{ "nest[a][value]": {""}, "nest[b]": {""}, "nest[ptr][value]": {"v"}, }, }, { nil, url.Values{}, }, } for _, tt := range tests { testValue(t, tt.input, tt.want) } } func TestValues_OmitEmpty(t *testing.T) { str := "" tests := []struct { input interface{} want url.Values }{ {struct{ v string }{}, url.Values{}}, // non-exported field { struct { V string `url:",omitempty"` }{}, url.Values{}, }, { struct { V string `url:"-"` }{}, url.Values{}, }, { struct { V string `url:"omitempty"` // actually named omitempty }{}, url.Values{"omitempty": {""}}, }, { // include value for a non-nil pointer to an empty value struct { V *string `url:",omitempty"` }{&str}, url.Values{"V": {""}}, }, } for _, tt := range tests { testValue(t, tt.input, tt.want) } } func TestValues_EmbeddedStructs(t *testing.T) { type Inner struct { V string } type Outer struct { Inner } type OuterPtr struct { *Inner } type Mixed struct { Inner V string } type unexported struct { Inner V string } type Exported struct { unexported } tests := []struct { input interface{} want url.Values }{ { Outer{Inner{V: "a"}}, url.Values{"V": {"a"}}, }, { OuterPtr{&Inner{V: "a"}}, url.Values{"V": {"a"}}, }, { Mixed{Inner: Inner{V: "a"}, V: "b"}, url.Values{"V": {"b", "a"}}, }, { // values from unexported embed are still included Exported{ unexported{ Inner: Inner{V: "bar"}, V: "foo", }, }, url.Values{"V": {"foo", "bar"}}, }, } for _, tt := range tests { testValue(t, tt.input, tt.want) } } func TestValues_InvalidInput(t *testing.T) { _, err := Values("") if err == nil { t.Errorf("expected Values() to return an error on invalid input") } } // customEncodedStrings is a slice of strings with a custom URL encoding type customEncodedStrings []string // EncodeValues using key name of the form "{key}.N" where N increments with // each value. A value of "err" will return an error. func (m customEncodedStrings) EncodeValues(key string, v *url.Values) error { for i, arg := range m { if arg == "err" { return errors.New("encoding error") } v.Set(fmt.Sprintf("%s.%d", key, i), arg) } return nil } func TestValues_CustomEncodingSlice(t *testing.T) { tests := []struct { input interface{} want url.Values }{ { struct { V customEncodedStrings `url:"v"` }{}, url.Values{}, }, { struct { V customEncodedStrings `url:"v"` }{[]string{"a", "b"}}, url.Values{"v.0": {"a"}, "v.1": {"b"}}, }, // pointers to custom encoded types { struct { V *customEncodedStrings `url:"v"` }{}, url.Values{}, }, { struct { V *customEncodedStrings `url:"v"` }{(*customEncodedStrings)(&[]string{"a", "b"})}, url.Values{"v.0": {"a"}, "v.1": {"b"}}, }, } for _, tt := range tests { testValue(t, tt.input, tt.want) } } // One of the few ways reflectValues will return an error is if a custom // encoder returns an error. Test all of the various ways that can happen. func TestValues_CustomEncoding_Error(t *testing.T) { type st struct { V customEncodedStrings } tests := []struct { input interface{} }{ { st{[]string{"err"}}, }, { // struct field struct{ S st }{st{[]string{"err"}}}, }, { // embedded struct struct{ st }{st{[]string{"err"}}}, }, } for _, tt := range tests { _, err := Values(tt.input) if err == nil { t.Errorf("Values(%q) did not return expected encoding error", tt.input) } } } // customEncodedInt is an int with a custom URL encoding type customEncodedInt int // EncodeValues encodes values with leading underscores func (m customEncodedInt) EncodeValues(key string, v *url.Values) error { v.Set(key, fmt.Sprintf("_%d", m)) return nil } func TestValues_CustomEncodingInt(t *testing.T) { var zero customEncodedInt = 0 var one customEncodedInt = 1 tests := []struct { input interface{} want url.Values }{ { struct { V customEncodedInt `url:"v"` }{}, url.Values{"v": {"_0"}}, }, { struct { V customEncodedInt `url:"v,omitempty"` }{zero}, url.Values{}, }, { struct { V customEncodedInt `url:"v"` }{one}, url.Values{"v": {"_1"}}, }, // pointers to custom encoded types { struct { V *customEncodedInt `url:"v"` }{}, url.Values{"v": {"_0"}}, }, { struct { V *customEncodedInt `url:"v,omitempty"` }{}, url.Values{}, }, { struct { V *customEncodedInt `url:"v,omitempty"` }{&zero}, url.Values{"v": {"_0"}}, }, { struct { V *customEncodedInt `url:"v"` }{&one}, url.Values{"v": {"_1"}}, }, } for _, tt := range tests { testValue(t, tt.input, tt.want) } } // customEncodedInt is an int with a custom URL encoding defined on its pointer // value. type customEncodedIntPtr int // EncodeValues encodes a 0 as false, 1 as true, and nil as unknown. All other // values cause an error. func (m *customEncodedIntPtr) EncodeValues(key string, v *url.Values) error { if m == nil { v.Set(key, "undefined") } else { v.Set(key, fmt.Sprintf("_%d", *m)) } return nil } // Test behavior when encoding is defined for a pointer of a custom type. // Custom type should be able to encode values for nil pointers. func TestValues_CustomEncodingPointer(t *testing.T) { var zero customEncodedIntPtr = 0 var one customEncodedIntPtr = 1 tests := []struct { input interface{} want url.Values }{ // non-pointer values do not get the custom encoding because // they don't implement the encoder interface. { struct { V customEncodedIntPtr `url:"v"` }{}, url.Values{"v": {"0"}}, }, { struct { V customEncodedIntPtr `url:"v,omitempty"` }{}, url.Values{}, }, { struct { V customEncodedIntPtr `url:"v"` }{one}, url.Values{"v": {"1"}}, }, // pointers to custom encoded types. { struct { V *customEncodedIntPtr `url:"v"` }{}, url.Values{"v": {"undefined"}}, }, { struct { V *customEncodedIntPtr `url:"v,omitempty"` }{}, url.Values{}, }, { struct { V *customEncodedIntPtr `url:"v"` }{&zero}, url.Values{"v": {"_0"}}, }, { struct { V *customEncodedIntPtr `url:"v,omitempty"` }{&zero}, url.Values{"v": {"_0"}}, }, { struct { V *customEncodedIntPtr `url:"v"` }{&one}, url.Values{"v": {"_1"}}, }, } for _, tt := range tests { testValue(t, tt.input, tt.want) } } func TestIsEmptyValue(t *testing.T) { str := "string" tests := []struct { value interface{} empty bool }{ // slices, arrays, and maps {[]int{}, true}, {[]int{0}, false}, {[0]int{}, true}, {[3]int{}, false}, {[3]int{1}, false}, {map[string]string{}, true}, {map[string]string{"a": "b"}, false}, // strings {"", true}, {" ", false}, {"a", false}, // bool {true, false}, {false, true}, // ints of various types {(int)(0), true}, {(int)(1), false}, {(int)(-1), false}, {(int8)(0), true}, {(int8)(1), false}, {(int8)(-1), false}, {(int16)(0), true}, {(int16)(1), false}, {(int16)(-1), false}, {(int32)(0), true}, {(int32)(1), false}, {(int32)(-1), false}, {(int64)(0), true}, {(int64)(1), false}, {(int64)(-1), false}, {(uint)(0), true}, {(uint)(1), false}, {(uint8)(0), true}, {(uint8)(1), false}, {(uint16)(0), true}, {(uint16)(1), false}, {(uint32)(0), true}, {(uint32)(1), false}, {(uint64)(0), true}, {(uint64)(1), false}, // floats {(float32)(0), true}, {(float32)(0.0), true}, {(float32)(0.1), false}, {(float64)(0), true}, {(float64)(0.0), true}, {(float64)(0.1), false}, // pointers {(*int)(nil), true}, {new([]int), false}, {&str, false}, // time {time.Time{}, true}, {time.Now(), false}, // unknown type - always false unless a nil pointer, which are always empty. {(*struct{ int })(nil), true}, {struct{ int }{}, false}, {struct{ int }{0}, false}, {struct{ int }{1}, false}, } for _, tt := range tests { got := isEmptyValue(reflect.ValueOf(tt.value)) want := tt.empty if got != want { t.Errorf("isEmptyValue(%v) returned %t; want %t", tt.value, got, want) } } } func TestParseTag(t *testing.T) { name, opts := parseTag("field,foobar,foo") if name != "field" { t.Fatalf("name = %q, want field", name) } for _, tt := range []struct { opt string want bool }{ {"foobar", true}, {"foo", true}, {"bar", false}, {"field", false}, } { if opts.Contains(tt.opt) != tt.want { t.Errorf("Contains(%q) = %v", tt.opt, !tt.want) } } }