| Index: go/src/infra/gae/libs/gae/properties.go
|
| diff --git a/go/src/infra/gae/libs/gae/properties.go b/go/src/infra/gae/libs/gae/properties.go
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..c7aa9e063e9c992a7b0eeee48186ff171199ac3d
|
| --- /dev/null
|
| +++ b/go/src/infra/gae/libs/gae/properties.go
|
| @@ -0,0 +1,356 @@
|
| +// Copyright 2015 The Chromium 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 gae
|
| +
|
| +import (
|
| + "errors"
|
| + "fmt"
|
| + "math"
|
| + "reflect"
|
| + "time"
|
| +)
|
| +
|
| +var (
|
| + // ErrDSSpecialFieldUnset is returned from DSStructPLS.{Get,Set}Special
|
| + // implementations when the specified special key isn't set on the struct at
|
| + // all.
|
| + ErrDSSpecialFieldUnset = fmt.Errorf("gae: special field unset")
|
| +)
|
| +
|
| +var (
|
| + typeOfBSKey = reflect.TypeOf(BSKey(""))
|
| + typeOfBool = reflect.TypeOf(false)
|
| + typeOfByteSlice = reflect.TypeOf([]byte(nil))
|
| + typeOfDSByteString = reflect.TypeOf(DSByteString(nil))
|
| + typeOfDSGeoPoint = reflect.TypeOf(DSGeoPoint{})
|
| + typeOfDSKey = reflect.TypeOf((*DSKey)(nil)).Elem()
|
| + typeOfFloat64 = reflect.TypeOf(float64(0))
|
| + typeOfInt64 = reflect.TypeOf(int64(0))
|
| + typeOfString = reflect.TypeOf("")
|
| + typeOfTime = reflect.TypeOf(time.Time{})
|
| +)
|
| +
|
| +var (
|
| + minTime = time.Unix(int64(math.MinInt64)/1e6, (int64(math.MinInt64)%1e6)*1e3)
|
| + maxTime = time.Unix(int64(math.MaxInt64)/1e6, (int64(math.MaxInt64)%1e6)*1e3)
|
| +)
|
| +
|
| +// DSProperty is a value plus an indicator of whether the value should be
|
| +// indexed. Name and Multiple are stored in the DSPropertyMap object.
|
| +type DSProperty struct {
|
| + value interface{}
|
| + noIndex bool
|
| + propType DSPropertyType
|
| +}
|
| +
|
| +// DSPropertyConverter may be implemented by the pointer-to a struct field which
|
| +// is serialized by RawDatastore. Its ToDSProperty will be called on save, and
|
| +// it's FromDSProperty will be called on load (from datastore). The method may
|
| +// do arbitrary computation, and if it encounters an error, may return it. This
|
| +// error will be a fatal error (as defined by DSPropertyLoadSaver) for the
|
| +// struct conversion.
|
| +//
|
| +// Example:
|
| +// type Complex complex
|
| +// func (c *Complex) ToDSProperty() (ret DSProperty, err error) {
|
| +// // something like:
|
| +// err = ret.SetValue(fmt.Sprint(*c), true)
|
| +// return
|
| +// }
|
| +// func (c *Complex) FromDSProperty(p DSProperty) (err error) {
|
| +// ... load *c from p ...
|
| +// }
|
| +//
|
| +// type MyStruct struct {
|
| +// Complexity []Complex // acts like []complex, but can be serialized to DS
|
| +// }
|
| +type DSPropertyConverter interface {
|
| + // TODO(riannucci): Allow a convertable to return multiple values. This is
|
| + // eminently doable (as long as the single-slice restriction is kept). It
|
| + // could also cut down on the amount of reflection necessary when resolving
|
| + // a path in a struct (in the struct loading routine in helper).
|
| +
|
| + ToDSProperty() (DSProperty, error)
|
| + FromDSProperty(DSProperty) error
|
| +}
|
| +
|
| +// DSPropertyType is a single-byte representation of the type of data contained
|
| +// in a DSProperty. The specific values of this type information are chosen so
|
| +// that the types sort according to the order of types as sorted by the
|
| +// datastore.
|
| +type DSPropertyType byte
|
| +
|
| +// These constants are in the order described by
|
| +// https://cloud.google.com/appengine/docs/go/datastore/entities#Go_Value_type_ordering
|
| +// with a slight divergence for the Int/Time split.
|
| +// NOTE: this enum can only occupy 7 bits, because we use the high bit to encode
|
| +// indexed/non-indexed. See typData.WriteBinary.
|
| +const (
|
| + DSPTNull DSPropertyType = iota
|
| + DSPTInt
|
| +
|
| + // DSPTTime is a slight divergence from the way that datastore natively stores
|
| + // time. In datastore, times and integers actually sort together
|
| + // (apparently?). This is probably insane, and I don't want to add the
|
| + // complexity of field 'meaning' as a sparate concept from the field's 'type'
|
| + // (which is what datastore seems to do, judging from the protobufs). So if
|
| + // you're here because you implemented an app which relies on time.Time and
|
| + // int64 sorting together, then this is why your app acts differently in
|
| + // production. My advice is to NOT DO THAT. If you really want this (and you
|
| + // probably don't), you should take care of the time.Time <-> int64 conversion
|
| + // in your app and just use a property type of int64 (consider using
|
| + // DSPropertyConverter).
|
| + DSPTTime
|
| +
|
| + // DSPTBoolFalse and True are also a slight divergence, but not a semantic
|
| + // one. IIUC, in datastore 'bool' is actually the type and the value is either
|
| + // 0 or 1 (taking another byte to store). Since we have plenty of space in
|
| + // this type byte, I just merge the value into the type for booleans. If this
|
| + // becomes problematic, consider changing this to just pvBool, and then
|
| + // encoding a 0 or 1 as a byte in the relevant marshalling routines.
|
| + DSPTBoolFalse
|
| + DSPTBoolTrue
|
| +
|
| + DSPTBytes // []byte or datastore.ByteString
|
| + DSPTString // string or string noindex
|
| + DSPTFloat
|
| + DSPTGeoPoint
|
| + DSPTKey
|
| + DSPTBlobKey
|
| +
|
| + DSPTUnknown
|
| +)
|
| +
|
| +func (t DSPropertyType) String() string {
|
| + switch t {
|
| + case DSPTNull:
|
| + return "DSPTNull"
|
| + case DSPTInt:
|
| + return "DSPTInt"
|
| + case DSPTTime:
|
| + return "DSPTTime"
|
| + case DSPTBoolFalse:
|
| + return "DSPTBoolFalse"
|
| + case DSPTBoolTrue:
|
| + return "DSPTBoolTrue"
|
| + case DSPTBytes:
|
| + return "DSPTBytes"
|
| + case DSPTString:
|
| + return "DSPTString"
|
| + case DSPTFloat:
|
| + return "DSPTFloat"
|
| + case DSPTGeoPoint:
|
| + return "DSPTGeoPoint"
|
| + case DSPTKey:
|
| + return "DSPTKey"
|
| + case DSPTBlobKey:
|
| + return "DSPTBlobKey"
|
| + default:
|
| + return "DSPTUnknown"
|
| + }
|
| +}
|
| +
|
| +// DSPropertyTypeOf returns the DSPT* type of the given DSProperty-compatible
|
| +// value v. If checkValid is true, this method will also ensure that time.Time
|
| +// and DSGeoPoint have valid values.
|
| +func DSPropertyTypeOf(v interface{}, checkValid bool) (DSPropertyType, error) {
|
| + switch x := v.(type) {
|
| + case nil:
|
| + return DSPTNull, nil
|
| + case int64:
|
| + return DSPTInt, nil
|
| + case float64:
|
| + return DSPTFloat, nil
|
| + case bool:
|
| + if x {
|
| + return DSPTBoolTrue, nil
|
| + }
|
| + return DSPTBoolFalse, nil
|
| + case []byte, DSByteString:
|
| + return DSPTBytes, nil
|
| + case BSKey:
|
| + return DSPTBlobKey, nil
|
| + case string:
|
| + return DSPTString, nil
|
| + case DSKey:
|
| + // TODO(riannucci): Check key for validity in its own namespace?
|
| + return DSPTKey, nil
|
| + case time.Time:
|
| + err := error(nil)
|
| + if checkValid && (x.Before(minTime) || x.After(maxTime)) {
|
| + err = errors.New("time value out of range")
|
| + }
|
| + if checkValid && x.Location() != time.UTC {
|
| + err = fmt.Errorf("time value has wrong Location: %s", x.Location())
|
| + }
|
| + return DSPTTime, err
|
| + case DSGeoPoint:
|
| + err := error(nil)
|
| + if checkValid && !x.Valid() {
|
| + err = errors.New("invalid GeoPoint value")
|
| + }
|
| + return DSPTGeoPoint, err
|
| + default:
|
| + return DSPTUnknown, fmt.Errorf("gae: DSProperty has bad type %T", v)
|
| + }
|
| +}
|
| +
|
| +// DSUpconvertUnderlyingType takes an object o, and attempts to convert it to
|
| +// its native datastore-compatible type. e.g. int16 will convert to int64, and
|
| +// `type Foo string` will convert to `string`.
|
| +func DSUpconvertUnderlyingType(o interface{}, t reflect.Type) (interface{}, reflect.Type) {
|
| + v := reflect.ValueOf(o)
|
| + switch t.Kind() {
|
| + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
| + o = v.Int()
|
| + t = typeOfInt64
|
| + case reflect.Bool:
|
| + o = v.Bool()
|
| + t = typeOfBool
|
| + case reflect.String:
|
| + if t != typeOfBSKey {
|
| + o = v.String()
|
| + t = typeOfString
|
| + }
|
| + case reflect.Float32, reflect.Float64:
|
| + o = v.Float()
|
| + t = typeOfFloat64
|
| + case reflect.Slice:
|
| + if t != typeOfDSByteString && t.Elem().Kind() == reflect.Uint8 {
|
| + o = v.Bytes()
|
| + t = typeOfByteSlice
|
| + }
|
| + case reflect.Struct:
|
| + if t == typeOfTime {
|
| + // time in a DSProperty can only hold microseconds
|
| + o = v.Interface().(time.Time).Round(time.Microsecond)
|
| + }
|
| + }
|
| + return o, t
|
| +}
|
| +
|
| +// Value returns the current value held by this property. It's guaranteed to
|
| +// be a valid value type (i.e. `p.SetValue(p.Value(), true)` will never return
|
| +// an error).
|
| +func (p DSProperty) Value() interface{} { return p.value }
|
| +
|
| +// NoIndex says weather or not the datastore should create indicies for this
|
| +// value.
|
| +func (p DSProperty) NoIndex() bool { return p.noIndex }
|
| +
|
| +// Type is the DSPT* type of the data contained in Value().
|
| +func (p DSProperty) Type() DSPropertyType { return p.propType }
|
| +
|
| +// SetValue sets the Value field of a DSProperty, and ensures that its value
|
| +// conforms to the permissible types. That way, you're guaranteed that if you
|
| +// have a DSProperty, its value is valid.
|
| +//
|
| +// value is the property value. The valid types are:
|
| +// - int64
|
| +// - bool
|
| +// - string
|
| +// - float64
|
| +// - DSByteString
|
| +// - DSKey
|
| +// - time.Time
|
| +// - BSKey
|
| +// - DSGeoPoint
|
| +// - []byte (up to 1 megabyte in length)
|
| +// This set is smaller than the set of valid struct field types that the
|
| +// datastore can load and save. A Property Value cannot be a slice (apart
|
| +// from []byte); use multiple Properties instead. Also, a Value's type
|
| +// must be explicitly on the list above; it is not sufficient for the
|
| +// underlying type to be on that list. For example, a Value of "type
|
| +// myInt64 int64" is invalid. Smaller-width integers and floats are also
|
| +// invalid. Again, this is more restrictive than the set of valid struct
|
| +// field types.
|
| +//
|
| +// A value may also be the nil interface value; this is equivalent to
|
| +// Python's None but not directly representable by a Go struct. Loading
|
| +// a nil-valued property into a struct will set that field to the zero
|
| +// value.
|
| +//
|
| +// noIndex == false will attempt to set NoIndex to false as well. However, if
|
| +// value is an unindexable type, noIndex will be coerced to true automatically.
|
| +func (p *DSProperty) SetValue(value interface{}, noIndex bool) (err error) {
|
| + t := reflect.Type(nil)
|
| + pt := DSPTNull
|
| + if value != nil {
|
| + t = reflect.TypeOf(value)
|
| + value, t = DSUpconvertUnderlyingType(value, t)
|
| + if pt, err = DSPropertyTypeOf(value, true); err != nil {
|
| + return
|
| + }
|
| + }
|
| + p.propType = pt
|
| + p.value = value
|
| + p.noIndex = noIndex || t == typeOfByteSlice
|
| + return
|
| +}
|
| +
|
| +// DSPropertyLoadSaver may be implemented by a user type, and RawDatastore will
|
| +// use this interface to serialize the type instead of trying to automatically
|
| +// create a serialization codec for it with helper.GetStructPLS.
|
| +type DSPropertyLoadSaver interface {
|
| + Load(DSPropertyMap) (convFailures []string, fatal error)
|
| + Save() (DSPropertyMap, error)
|
| +}
|
| +
|
| +// DSStructPLS is a DSPropertyLoadSaver, but with some bonus features which only
|
| +// apply to user structs (instead of raw DSPropertyMap's).
|
| +type DSStructPLS interface {
|
| + DSPropertyLoadSaver
|
| +
|
| + // GetSpecial will get information about the struct field which has the
|
| + // struct tag in the form of `gae:"$<key>"`.
|
| + //
|
| + // Example:
|
| + // type MyStruct struct {
|
| + // CoolField int `gae:"$id,cool"`
|
| + // }
|
| + // val, current, err := helper.GetStructPLS(&MyStruct{10}).GetSpecial("id")
|
| + // // val == "cool"
|
| + // // current == 10
|
| + // // err == nil
|
| + GetSpecial(key string) (val string, current interface{}, err error)
|
| +
|
| + // SetSpecial allows you to set the current value of the special-keyed field.
|
| + SetSpecial(key string, val interface{}) error
|
| +
|
| + // Problem indicates that this StructPLS has a fatal problem. Usually this is
|
| + // set when the underlying struct has recursion, invalid field types, nested
|
| + // slices, etc.
|
| + Problem() error
|
| +}
|
| +
|
| +// DSPropertyMap represents the contents of a datastore entity in a generic way.
|
| +// It maps from property name to a list of property values which correspond to
|
| +// that property name. It is the spiritual successor to PropertyList from the
|
| +// original SDK.
|
| +type DSPropertyMap map[string][]DSProperty
|
| +
|
| +var _ DSPropertyLoadSaver = (*DSPropertyMap)(nil)
|
| +
|
| +// Load implements DSPropertyLoadSaver.Load
|
| +func (pm *DSPropertyMap) Load(props DSPropertyMap) (convErr []string, err error) {
|
| + if pm == nil {
|
| + return nil, errors.New("gae: nil DSPropertyMap")
|
| + }
|
| + if *pm == nil {
|
| + *pm = make(DSPropertyMap, len(props))
|
| + }
|
| + for k, v := range props {
|
| + (*pm)[k] = append((*pm)[k], v...)
|
| + }
|
| + return nil, nil
|
| +}
|
| +
|
| +// Save implements DSPropertyLoadSaver.Save
|
| +func (pm *DSPropertyMap) Save() (DSPropertyMap, error) {
|
| + if pm == nil {
|
| + return nil, errors.New("gae: nil DSPropertyMap")
|
| + }
|
| + return *pm, nil
|
| +}
|
|
|