Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(40)

Side by Side Diff: properties.go

Issue 1243323002: Refactor a bit. (Closed) Base URL: https://github.com/luci/gae.git@master
Patch Set: fix golint Created 5 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « prod/taskqueue.go ('k') | properties_test.go » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright 2015 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 package gae
6
7 import (
8 "errors"
9 "fmt"
10 "math"
11 "reflect"
12 "time"
13 )
14
15 var (
16 // ErrDSMetaFieldUnset is returned from DSPropertyLoadSaver.{Get,Set}Met a
17 // implementations when the specified meta key isn't set on the struct a t
18 // all.
19 ErrDSMetaFieldUnset = fmt.Errorf("gae: meta field unset")
20 )
21
22 var (
23 typeOfBSKey = reflect.TypeOf(BSKey(""))
24 typeOfBool = reflect.TypeOf(false)
25 typeOfByteSlice = reflect.TypeOf([]byte(nil))
26 typeOfDSByteString = reflect.TypeOf(DSByteString(nil))
27 typeOfDSGeoPoint = reflect.TypeOf(DSGeoPoint{})
28 typeOfDSKey = reflect.TypeOf((*DSKey)(nil)).Elem()
29 typeOfFloat64 = reflect.TypeOf(float64(0))
30 typeOfInt64 = reflect.TypeOf(int64(0))
31 typeOfString = reflect.TypeOf("")
32 typeOfTime = reflect.TypeOf(time.Time{})
33 )
34
35 var (
36 minTime = time.Unix(int64(math.MinInt64)/1e6, (int64(math.MinInt64)%1e6) *1e3)
37 maxTime = time.Unix(int64(math.MaxInt64)/1e6, (int64(math.MaxInt64)%1e6) *1e3)
38 )
39
40 type IndexSetting bool
41
42 const (
43 // ShouldIndex is the default, which is why it must assume the zero valu e,
44 // even though it's werid :(.
45 ShouldIndex IndexSetting = false
46 NoIndex IndexSetting = true
47 )
48
49 func (i IndexSetting) String() string {
50 if i {
51 return "NoIndex"
52 }
53 return "ShouldIndex"
54 }
55
56 // DSProperty is a value plus an indicator of whether the value should be
57 // indexed. Name and Multiple are stored in the DSPropertyMap object.
58 type DSProperty struct {
59 value interface{}
60 indexSetting IndexSetting
61 propType DSPropertyType
62 }
63
64 // MkDSProperty makes a new indexed* DSProperty and returns it. If val is an
65 // invalid value, this panics (so don't do it). If you want to handle the error
66 // normally, use SetValue(..., ShouldIndex) instead.
67 //
68 // *indexed if val is not an unindexable type like []byte.
69 func MkDSProperty(val interface{}) DSProperty {
70 ret := DSProperty{}
71 if err := ret.SetValue(val, ShouldIndex); err != nil {
72 panic(err)
73 }
74 return ret
75 }
76
77 // MkDSPropertyNI makes a new DSProperty (with noindex set to true), and returns
78 // it. If val is an invalid value, this panics (so don't do it). If you want to
79 // handle the error normally, use SetValue(..., NoIndex) instead.
80 func MkDSPropertyNI(val interface{}) DSProperty {
81 ret := DSProperty{}
82 if err := ret.SetValue(val, NoIndex); err != nil {
83 panic(err)
84 }
85 return ret
86 }
87
88 // DSPropertyConverter may be implemented by the pointer-to a struct field which
89 // is serialized by RawDatastore. Its ToDSProperty will be called on save, and
90 // it's FromDSProperty will be called on load (from datastore). The method may
91 // do arbitrary computation, and if it encounters an error, may return it. This
92 // error will be a fatal error (as defined by DSPropertyLoadSaver) for the
93 // struct conversion.
94 //
95 // Example:
96 // type Complex complex
97 // func (c *Complex) ToDSProperty() (ret DSProperty, err error) {
98 // // something like:
99 // err = ret.SetValue(fmt.Sprint(*c), true)
100 // return
101 // }
102 // func (c *Complex) FromDSProperty(p DSProperty) (err error) {
103 // ... load *c from p ...
104 // }
105 //
106 // type MyStruct struct {
107 // Complexity []Complex // acts like []complex, but can be serialized to DS
108 // }
109 type DSPropertyConverter interface {
110 // TODO(riannucci): Allow a convertable to return multiple values. This is
111 // eminently doable (as long as the single-slice restriction is kept). It
112 // could also cut down on the amount of reflection necessary when resolv ing
113 // a path in a struct (in the struct loading routine in helper).
114
115 ToDSProperty() (DSProperty, error)
116 FromDSProperty(DSProperty) error
117 }
118
119 // DSPropertyType is a single-byte representation of the type of data contained
120 // in a DSProperty. The specific values of this type information are chosen so
121 // that the types sort according to the order of types as sorted by the
122 // datastore.
123 type DSPropertyType byte
124
125 // These constants are in the order described by
126 // https://cloud.google.com/appengine/docs/go/datastore/entities#Go_Value_type _ordering
127 // with a slight divergence for the Int/Time split.
128 // NOTE: this enum can only occupy 7 bits, because we use the high bit to encode
129 // indexed/non-indexed. See typData.WriteBinary.
130 const (
131 DSPTNull DSPropertyType = iota
132 DSPTInt
133
134 // DSPTTime is a slight divergence from the way that datastore natively stores
135 // time. In datastore, times and integers actually sort together
136 // (apparently?). This is probably insane, and I don't want to add the
137 // complexity of field 'meaning' as a sparate concept from the field's ' type'
138 // (which is what datastore seems to do, judging from the protobufs). So if
139 // you're here because you implemented an app which relies on time.Time and
140 // int64 sorting together, then this is why your app acts differently in
141 // production. My advice is to NOT DO THAT. If you really want this (and you
142 // probably don't), you should take care of the time.Time <-> int64 conv ersion
143 // in your app and just use a property type of int64 (consider using
144 // DSPropertyConverter).
145 DSPTTime
146
147 // DSPTBoolFalse and True are also a slight divergence, but not a semant ic
148 // one. IIUC, in datastore 'bool' is actually the type and the value is either
149 // 0 or 1 (taking another byte to store). Since we have plenty of space in
150 // this type byte, I just merge the value into the type for booleans. If this
151 // becomes problematic, consider changing this to just pvBool, and then
152 // encoding a 0 or 1 as a byte in the relevant marshalling routines.
153 DSPTBoolFalse
154 DSPTBoolTrue
155
156 DSPTBytes // []byte or datastore.ByteString
157 DSPTString // string or string noindex
158 DSPTFloat
159 DSPTGeoPoint
160 DSPTKey
161 DSPTBlobKey
162
163 DSPTUnknown
164 )
165
166 func (t DSPropertyType) String() string {
167 switch t {
168 case DSPTNull:
169 return "DSPTNull"
170 case DSPTInt:
171 return "DSPTInt"
172 case DSPTTime:
173 return "DSPTTime"
174 case DSPTBoolFalse:
175 return "DSPTBoolFalse"
176 case DSPTBoolTrue:
177 return "DSPTBoolTrue"
178 case DSPTBytes:
179 return "DSPTBytes"
180 case DSPTString:
181 return "DSPTString"
182 case DSPTFloat:
183 return "DSPTFloat"
184 case DSPTGeoPoint:
185 return "DSPTGeoPoint"
186 case DSPTKey:
187 return "DSPTKey"
188 case DSPTBlobKey:
189 return "DSPTBlobKey"
190 default:
191 return fmt.Sprintf("DSPTUnknown(%02x)", byte(t))
192 }
193 }
194
195 // DSPropertyTypeOf returns the DSPT* type of the given DSProperty-compatible
196 // value v. If checkValid is true, this method will also ensure that time.Time
197 // and DSGeoPoint have valid values.
198 func DSPropertyTypeOf(v interface{}, checkValid bool) (DSPropertyType, error) {
199 switch x := v.(type) {
200 case nil:
201 return DSPTNull, nil
202 case int64:
203 return DSPTInt, nil
204 case float64:
205 return DSPTFloat, nil
206 case bool:
207 if x {
208 return DSPTBoolTrue, nil
209 }
210 return DSPTBoolFalse, nil
211 case []byte, DSByteString:
212 return DSPTBytes, nil
213 case BSKey:
214 return DSPTBlobKey, nil
215 case string:
216 return DSPTString, nil
217 case DSKey:
218 // TODO(riannucci): Check key for validity in its own namespace?
219 return DSPTKey, nil
220 case time.Time:
221 err := error(nil)
222 if checkValid && (x.Before(minTime) || x.After(maxTime)) {
223 err = errors.New("time value out of range")
224 }
225 if checkValid && x.Location() != time.UTC {
226 err = fmt.Errorf("time value has wrong Location: %s", x. Location())
227 }
228 return DSPTTime, err
229 case DSGeoPoint:
230 err := error(nil)
231 if checkValid && !x.Valid() {
232 err = errors.New("invalid GeoPoint value")
233 }
234 return DSPTGeoPoint, err
235 default:
236 return DSPTUnknown, fmt.Errorf("gae: DSProperty has bad type %T" , v)
237 }
238 }
239
240 // DSUpconvertUnderlyingType takes an object o, and attempts to convert it to
241 // its native datastore-compatible type. e.g. int16 will convert to int64, and
242 // `type Foo string` will convert to `string`.
243 func DSUpconvertUnderlyingType(o interface{}, t reflect.Type) (interface{}, refl ect.Type) {
244 v := reflect.ValueOf(o)
245 switch t.Kind() {
246 case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.In t64:
247 o = v.Int()
248 t = typeOfInt64
249 case reflect.Bool:
250 o = v.Bool()
251 t = typeOfBool
252 case reflect.String:
253 if t != typeOfBSKey {
254 o = v.String()
255 t = typeOfString
256 }
257 case reflect.Float32, reflect.Float64:
258 o = v.Float()
259 t = typeOfFloat64
260 case reflect.Slice:
261 if t != typeOfDSByteString && t.Elem().Kind() == reflect.Uint8 {
262 o = v.Bytes()
263 t = typeOfByteSlice
264 }
265 case reflect.Struct:
266 if t == typeOfTime {
267 // time in a DSProperty can only hold microseconds
268 o = v.Interface().(time.Time).Round(time.Microsecond)
269 }
270 }
271 return o, t
272 }
273
274 // Value returns the current value held by this property. It's guaranteed to
275 // be a valid value type (i.e. `p.SetValue(p.Value(), true)` will never return
276 // an error).
277 func (p DSProperty) Value() interface{} { return p.value }
278
279 // IndexSetting says weather or not the datastore should create indicies for
280 // this value.
281 func (p DSProperty) IndexSetting() IndexSetting { return p.indexSetting }
282
283 // Type is the DSPT* type of the data contained in Value().
284 func (p DSProperty) Type() DSPropertyType { return p.propType }
285
286 // SetValue sets the Value field of a DSProperty, and ensures that its value
287 // conforms to the permissible types. That way, you're guaranteed that if you
288 // have a DSProperty, its value is valid.
289 //
290 // value is the property value. The valid types are:
291 // - int64
292 // - bool
293 // - string
294 // - float64
295 // - DSByteString
296 // - DSKey
297 // - time.Time
298 // - BSKey
299 // - DSGeoPoint
300 // - []byte (up to 1 megabyte in length)
301 // This set is smaller than the set of valid struct field types that the
302 // datastore can load and save. A Property Value cannot be a slice (apart
303 // from []byte); use multiple Properties instead. Also, a Value's type
304 // must be explicitly on the list above; it is not sufficient for the
305 // underlying type to be on that list. For example, a Value of "type
306 // myInt64 int64" is invalid. Smaller-width integers and floats are also
307 // invalid. Again, this is more restrictive than the set of valid struct
308 // field types.
309 //
310 // A value may also be the nil interface value; this is equivalent to
311 // Python's None but not directly representable by a Go struct. Loading
312 // a nil-valued property into a struct will set that field to the zero
313 // value.
314 func (p *DSProperty) SetValue(value interface{}, is IndexSetting) (err error) {
315 t := reflect.Type(nil)
316 pt := DSPTNull
317 if value != nil {
318 t = reflect.TypeOf(value)
319 value, t = DSUpconvertUnderlyingType(value, t)
320 if pt, err = DSPropertyTypeOf(value, true); err != nil {
321 return
322 }
323 }
324 p.propType = pt
325 p.value = value
326 p.indexSetting = is
327 if t == typeOfByteSlice {
328 p.indexSetting = NoIndex
329 }
330 return
331 }
332
333 // DSPropertyLoadSaver may be implemented by a user type, and RawDatastore will
334 // use this interface to serialize the type instead of trying to automatically
335 // create a serialization codec for it with helper.GetPLS.
336 type DSPropertyLoadSaver interface {
337 // Load takes the values from the given map and attempts to save them in to
338 // the underlying object (usually a struct or a DSPropertyMap). If a fat al
339 // error occurs, it's returned via error. If non-fatal conversion errors
340 // occur, error will be a MultiError containing one or more ErrDSFieldMi smatch
341 // objects.
342 Load(DSPropertyMap) error
343
344 // Save returns the current property as a DSPropertyMap. if withMeta is true,
345 // then the DSPropertyMap contains all the metadata (e.g. '$meta' fields )
346 // which was held by this DSPropertyLoadSaver.
347 Save(withMeta bool) (DSPropertyMap, error)
348
349 // GetMeta will get information about the field which has the struct tag in
350 // the form of `gae:"$<key>[,<value>]?"`.
351 //
352 // string and int64 fields will return the <value> in the struct tag,
353 // converted to the appropriate type, if the field has the zero value.
354 //
355 // Example:
356 // type MyStruct struct {
357 // CoolField int64 `gae:"$id,1"`
358 // }
359 // val, err := helper.GetPLS(&MyStruct{}).GetMeta("id")
360 // // val == 1
361 // // err == nil
362 //
363 // val, err := helper.GetPLS(&MyStruct{10}).GetMeta("id")
364 // // val == 10
365 // // err == nil
366 GetMeta(key string) (interface{}, error)
367
368 // SetMeta allows you to set the current value of the meta-keyed field.
369 SetMeta(key string, val interface{}) error
370
371 // Problem indicates that this PLS has a fatal problem. Usually this is
372 // set when the underlying struct has recursion, invalid field types, ne sted
373 // slices, etc.
374 Problem() error
375 }
376
377 // DSPropertyMap represents the contents of a datastore entity in a generic way.
378 // It maps from property name to a list of property values which correspond to
379 // that property name. It is the spiritual successor to PropertyList from the
380 // original SDK.
381 //
382 // DSPropertyMap may contain "meta" values, which are keyed with a '$' prefix.
383 // Technically the datastore allows arbitrary property names, but all of the
384 // SDKs go out of their way to try to make all property names valid programming
385 // language tokens. Special values must correspond to a single DSProperty...
386 // corresponding to 0 is equivalent to unset, and corresponding to >1 is an
387 // error. So:
388 //
389 // {
390 // "$id": {MkDSProperty(1)}, // GetProperty("id") -> 1, nil
391 // "$foo": {}, // GetProperty("foo") -> nil, ErrDSMetaFieldUnset
392 // // GetProperty("bar") -> nil, ErrDSMetaFieldUnset
393 // "$meep": {
394 // MkDSProperty("hi"),
395 // MkDSProperty("there")}, // GetProperty("meep") -> nil, error!
396 // }
397 //
398 // Additionally, Save returns a copy of the map with the meta keys omitted (e.g.
399 // these keys are not going to be serialized to the datastore).
400 type DSPropertyMap map[string][]DSProperty
401
402 var _ DSPropertyLoadSaver = DSPropertyMap(nil)
403
404 // Load implements DSPropertyLoadSaver.Load
405 func (pm DSPropertyMap) Load(props DSPropertyMap) error {
406 for k, v := range props {
407 pm[k] = append(pm[k], v...)
408 }
409 return nil
410 }
411
412 // Save implements DSPropertyLoadSaver.Save by returning a copy of the
413 // current map data.
414 func (pm DSPropertyMap) Save(withMeta bool) (DSPropertyMap, error) {
415 if len(pm) == 0 {
416 return DSPropertyMap{}, nil
417 }
418 ret := make(DSPropertyMap, len(pm))
419 for k, v := range pm {
420 if withMeta || len(k) == 0 || k[0] != '$' {
421 ret[k] = append(ret[k], v...)
422 }
423 }
424 return ret, nil
425 }
426
427 func (pm DSPropertyMap) GetMeta(key string) (interface{}, error) {
428 v, ok := pm["$"+key]
429 if !ok || len(v) == 0 {
430 return nil, ErrDSMetaFieldUnset
431 }
432 if len(v) > 1 {
433 return nil, errors.New("gae: too many values for Meta key")
434 }
435 return v[0].Value(), nil
436 }
437
438 func (pm DSPropertyMap) SetMeta(key string, val interface{}) error {
439 prop := DSProperty{}
440 if err := prop.SetValue(val, NoIndex); err != nil {
441 return err
442 }
443 pm["$"+key] = []DSProperty{prop}
444 return nil
445 }
446
447 func (pm DSPropertyMap) Problem() error {
448 return nil
449 }
OLDNEW
« no previous file with comments | « prod/taskqueue.go ('k') | properties_test.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698