]>
Commit | Line | Data |
---|---|---|
1a77fed1 AG |
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 | "bytes" | |
25 | "fmt" | |
26 | "net/url" | |
27 | "reflect" | |
28 | "strconv" | |
29 | "strings" | |
30 | "time" | |
31 | ) | |
32 | ||
33 | var timeType = reflect.TypeOf(time.Time{}) | |
34 | ||
35 | var 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. | |
39 | type 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. | |
113 | func 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. | |
138 | func 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. | |
250 | func 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. | |
278 | func 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. | |
303 | type tagOptions []string | |
304 | ||
305 | // parseTag splits a struct field's url tag into its name and comma-separated | |
306 | // options. | |
307 | func 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. | |
313 | func (o tagOptions) Contains(option string) bool { | |
314 | for _, s := range o { | |
315 | if s == option { | |
316 | return true | |
317 | } | |
318 | } | |
319 | return false | |
320 | } |