encode.go raw

   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