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.
 21package query
 22
 23import (
 24	"bytes"
 25	"fmt"
 26	"net/url"
 27	"reflect"
 28	"strconv"
 29	"strings"
 30	"time"
 31)
 32
 33var timeType = reflect.TypeOf(time.Time{})
 34
 35var encoderType = reflect.TypeOf(new(Encoder)).Elem()
 36
 37// Encoder is an interface implemented by any type that wishes to encode
 38// itself into URL values in a non-standard way.
 39type Encoder interface {
 40	EncodeValues(key string, v *url.Values) error
 41}
 42
 43// Values returns the url.Values encoding of v.
 44//
 45// Values expects to be passed a struct, and traverses it recursively using the
 46// following encoding rules.
 47//
 48// Each exported struct field is encoded as a URL parameter unless
 49//
 50//	- the field's tag is "-", or
 51//	- the field is empty and its tag specifies the "omitempty" option
 52//
 53// The empty values are false, 0, any nil pointer or interface value, any array
 54// slice, map, or string of length zero, and any time.Time that returns true
 55// for IsZero().
 56//
 57// The URL parameter name defaults to the struct field name but can be
 58// specified in the struct field's tag value.  The "url" key in the struct
 59// field's tag value is the key name, followed by an optional comma and
 60// options.  For example:
 61//
 62// 	// Field is ignored by this package.
 63// 	Field int `url:"-"`
 64//
 65// 	// Field appears as URL parameter "myName".
 66// 	Field int `url:"myName"`
 67//
 68// 	// Field appears as URL parameter "myName" and the field is omitted if
 69// 	// its value is empty
 70// 	Field int `url:"myName,omitempty"`
 71//
 72// 	// Field appears as URL parameter "Field" (the default), but the field
 73// 	// is skipped if empty.  Note the leading comma.
 74// 	Field int `url:",omitempty"`
 75//
 76// For encoding individual field values, the following type-dependent rules
 77// apply:
 78//
 79// Boolean values default to encoding as the strings "true" or "false".
 80// Including the "int" option signals that the field should be encoded as the
 81// strings "1" or "0".
 82//
 83// time.Time values default to encoding as RFC3339 timestamps.  Including the
 84// "unix" option signals that the field should be encoded as a Unix time (see
 85// time.Unix())
 86//
 87// Slice and Array values default to encoding as multiple URL values of the
 88// same name.  Including the "comma" option signals that the field should be
 89// encoded as a single comma-delimited value.  Including the "space" option
 90// similarly encodes the value as a single space-delimited string. Including
 91// the "semicolon" option will encode the value as a semicolon-delimited string.
 92// Including the "brackets" option signals that the multiple URL values should
 93// have "[]" appended to the value name. "numbered" will append a number to
 94// the end of each incidence of the value name, example:
 95// name0=value0&name1=value1, etc.
 96//
 97// Anonymous struct fields are usually encoded as if their inner exported
 98// fields were fields in the outer struct, subject to the standard Go
 99// visibility rules.  An anonymous struct field with a name given in its URL
100// tag is treated as having that name, rather than being anonymous.
101//
102// Non-nil pointer values are encoded as the value pointed to.
103//
104// Nested structs are encoded including parent fields in value names for
105// scoping. e.g:
106//
107// 	"user[name]=acme&user[addr][postcode]=1234&user[addr][city]=SFO"
108//
109// All other values are encoded using their default string representation.
110//
111// Multiple fields that encode to the same URL parameter name will be included
112// as multiple URL values of the same name.
113func Values(v interface{}) (url.Values, error) {
114	values := make(url.Values)
115	val := reflect.ValueOf(v)
116	for val.Kind() == reflect.Ptr {
117		if val.IsNil() {
118			return values, nil
119		}
120		val = val.Elem()
121	}
122
123	if v == nil {
124		return values, nil
125	}
126
127	if val.Kind() != reflect.Struct {
128		return nil, fmt.Errorf("query: Values() expects struct input. Got %v", val.Kind())
129	}
130
131	err := reflectValue(values, val, "")
132	return values, err
133}
134
135// reflectValue populates the values parameter from the struct fields in val.
136// Embedded structs are followed recursively (using the rules defined in the
137// Values function documentation) breadth-first.
138func reflectValue(values url.Values, val reflect.Value, scope string) error {
139	var embedded []reflect.Value
140
141	typ := val.Type()
142	for i := 0; i < typ.NumField(); i++ {
143		sf := typ.Field(i)
144		if sf.PkgPath != "" && !sf.Anonymous { // unexported
145			continue
146		}
147
148		sv := val.Field(i)
149		tag := sf.Tag.Get("url")
150		if tag == "-" {
151			continue
152		}
153		name, opts := parseTag(tag)
154		if name == "" {
155			if sf.Anonymous && sv.Kind() == reflect.Struct {
156				// save embedded struct for later processing
157				embedded = append(embedded, sv)
158				continue
159			}
160
161			name = sf.Name
162		}
163
164		if scope != "" {
165			name = scope + "[" + name + "]"
166		}
167
168		if opts.Contains("omitempty") && isEmptyValue(sv) {
169			continue
170		}
171
172		if sv.Type().Implements(encoderType) {
173			if !reflect.Indirect(sv).IsValid() {
174				sv = reflect.New(sv.Type().Elem())
175			}
176
177			m := sv.Interface().(Encoder)
178			if err := m.EncodeValues(name, &values); err != nil {
179				return err
180			}
181			continue
182		}
183
184		if sv.Kind() == reflect.Slice || sv.Kind() == reflect.Array {
185			var del byte
186			if opts.Contains("comma") {
187				del = ','
188			} else if opts.Contains("space") {
189				del = ' '
190			} else if opts.Contains("semicolon") {
191				del = ';'
192			} else if opts.Contains("brackets") {
193				name = name + "[]"
194			}
195
196			if del != 0 {
197				s := new(bytes.Buffer)
198				first := true
199				for i := 0; i < sv.Len(); i++ {
200					if first {
201						first = false
202					} else {
203						s.WriteByte(del)
204					}
205					s.WriteString(valueString(sv.Index(i), opts))
206				}
207				values.Add(name, s.String())
208			} else {
209				for i := 0; i < sv.Len(); i++ {
210					k := name
211					if opts.Contains("numbered") {
212						k = fmt.Sprintf("%s%d", name, i)
213					}
214					values.Add(k, valueString(sv.Index(i), opts))
215				}
216			}
217			continue
218		}
219
220		for sv.Kind() == reflect.Ptr {
221			if sv.IsNil() {
222				break
223			}
224			sv = sv.Elem()
225		}
226
227		if sv.Type() == timeType {
228			values.Add(name, valueString(sv, opts))
229			continue
230		}
231
232		if sv.Kind() == reflect.Struct {
233			reflectValue(values, sv, name)
234			continue
235		}
236
237		values.Add(name, valueString(sv, opts))
238	}
239
240	for _, f := range embedded {
241		if err := reflectValue(values, f, scope); err != nil {
242			return err
243		}
244	}
245
246	return nil
247}
248
249// valueString returns the string representation of a value.
250func valueString(v reflect.Value, opts tagOptions) string {
251	for v.Kind() == reflect.Ptr {
252		if v.IsNil() {
253			return ""
254		}
255		v = v.Elem()
256	}
257
258	if v.Kind() == reflect.Bool && opts.Contains("int") {
259		if v.Bool() {
260			return "1"
261		}
262		return "0"
263	}
264
265	if v.Type() == timeType {
266		t := v.Interface().(time.Time)
267		if opts.Contains("unix") {
268			return strconv.FormatInt(t.Unix(), 10)
269		}
270		return t.Format(time.RFC3339)
271	}
272
273	return fmt.Sprint(v.Interface())
274}
275
276// isEmptyValue checks if a value should be considered empty for the purposes
277// of omitting fields with the "omitempty" option.
278func isEmptyValue(v reflect.Value) bool {
279	switch v.Kind() {
280	case reflect.Array, reflect.Map, reflect.Slice, reflect.String:
281		return v.Len() == 0
282	case reflect.Bool:
283		return !v.Bool()
284	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
285		return v.Int() == 0
286	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
287		return v.Uint() == 0
288	case reflect.Float32, reflect.Float64:
289		return v.Float() == 0
290	case reflect.Interface, reflect.Ptr:
291		return v.IsNil()
292	}
293
294	if v.Type() == timeType {
295		return v.Interface().(time.Time).IsZero()
296	}
297
298	return false
299}
300
301// tagOptions is the string following a comma in a struct field's "url" tag, or
302// the empty string. It does not include the leading comma.
303type tagOptions []string
304
305// parseTag splits a struct field's url tag into its name and comma-separated
306// options.
307func parseTag(tag string) (string, tagOptions) {
308	s := strings.Split(tag, ",")
309	return s[0], s[1:]
310}
311
312// Contains checks whether the tagOptions contains the specified option.
313func (o tagOptions) Contains(option string) bool {
314	for _, s := range o {
315		if s == option {
316			return true
317		}
318	}
319	return false
320}